diff --git a/src/DocumentFormat.OpenXml.Framework/Framework/StringBuilderPool.cs b/src/DocumentFormat.OpenXml.Framework/Framework/StringBuilderPool.cs
new file mode 100644
index 000000000..deac3307d
--- /dev/null
+++ b/src/DocumentFormat.OpenXml.Framework/Framework/StringBuilderPool.cs
@@ -0,0 +1,103 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Text;
+using System.Threading;
+
+#if !NET35
+using System;
+using System.Collections.Concurrent;
+#endif
+
+namespace DocumentFormat.OpenXml.Framework
+{
+ ///
+ /// Provides a shared, reusable pool of instances to reduce allocations.
+ ///
+ ///
+ /// Design mirrors DefaultObjectPool from Microsoft.Extensions.ObjectPool:
+ /// a single lock-free fast-path slot plus a bounded overflow queue capped at
+ /// Environment.ProcessorCount * 2.
+ ///
+ internal static class StringBuilderPool
+ {
+ // Maximum capacity (in characters) a StringBuilder may have before it is not returned to the pool.
+ private const int MaxBuilderCapacity = 360;
+
+#if !NET35
+ // Secondary queue for concurrent overflow; capacity is ProcessorCount * 2 minus 1 for _fastItem.
+ private static readonly ConcurrentQueue _items = new ConcurrentQueue();
+ private static readonly int _maxCapacity = (Environment.ProcessorCount * 2) - 1;
+#endif
+
+ // Single fast-path slot; accessed via Interlocked.CompareExchange for lock-free hand-off.
+ private static StringBuilder? _fastItem;
+
+#if !NET35
+ private static int _numItems;
+#endif
+
+ ///
+ /// Acquires a from the pool, or creates a new one if the pool is empty.
+ ///
+ public static StringBuilder Acquire()
+ {
+ var item = _fastItem;
+ if (item != null && Interlocked.CompareExchange(ref _fastItem, null, item) == item)
+ {
+ return item;
+ }
+
+#if !NET35
+ if (_items.TryDequeue(out item))
+ {
+ Interlocked.Decrement(ref _numItems);
+ return item;
+ }
+#endif
+
+ return new StringBuilder();
+ }
+
+ ///
+ /// Returns the string value of the and releases it back to the pool.
+ ///
+ public static string GetValueAndRelease(StringBuilder sb)
+ {
+ var result = sb.ToString();
+ Release(sb);
+ return result;
+ }
+
+ ///
+ /// Releases a back to the pool after clearing it.
+ ///
+ public static void Release(StringBuilder sb)
+ {
+ // Don't pool excessively large instances to avoid retaining large memory allocations.
+ if (sb.Capacity > MaxBuilderCapacity)
+ {
+ return;
+ }
+
+ // Use Length = 0 instead of Clear() for .NET 3.5 compatibility.
+ sb.Length = 0;
+
+ if (Interlocked.CompareExchange(ref _fastItem, sb, null) == null)
+ {
+ return;
+ }
+
+#if !NET35
+ if (Interlocked.Increment(ref _numItems) <= _maxCapacity)
+ {
+ _items.Enqueue(sb);
+ return;
+ }
+
+ // Pool is full; drop the instance so it can be GC'd.
+ Interlocked.Decrement(ref _numItems);
+#endif
+ }
+ }
+}
diff --git a/src/DocumentFormat.OpenXml.Framework/OpenXmlCompositeElement.cs b/src/DocumentFormat.OpenXml.Framework/OpenXmlCompositeElement.cs
index 9a7312433..8b76fe681 100644
--- a/src/DocumentFormat.OpenXml.Framework/OpenXmlCompositeElement.cs
+++ b/src/DocumentFormat.OpenXml.Framework/OpenXmlCompositeElement.cs
@@ -8,7 +8,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
-using System.Text;
using System.Xml;
namespace DocumentFormat.OpenXml
@@ -122,14 +121,14 @@ public override string InnerText
{
get
{
- var innerText = new StringBuilder();
+ var innerText = StringBuilderPool.Acquire();
foreach (var child in ChildElements)
{
innerText.Append(child.InnerText);
}
- return innerText.ToString();
+ return StringBuilderPool.GetValueAndRelease(innerText);
}
}
diff --git a/src/DocumentFormat.OpenXml.Framework/Properties/Properties.cs b/src/DocumentFormat.OpenXml.Framework/Properties/Properties.cs
index 3fd12daf7..3571df3a7 100644
--- a/src/DocumentFormat.OpenXml.Framework/Properties/Properties.cs
+++ b/src/DocumentFormat.OpenXml.Framework/Properties/Properties.cs
@@ -6,6 +6,7 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("DocumentFormat.OpenXml, Publickey=002400000480000094000000060200000024000052534131000400000100010061d8931836c82bf25ca6b773dfd6e7b3ab4e43fba60cf4a86347170373415a165ccc40da3da4a52163822db9fa91f15828236d32d6a9fe754859f10d1f8262646c1f3fb6b4348123f14d733db0ff11c3198b7cf56caaebbf14563990446a6c32aff36d5a7097194294c127fe3cdf9f2609daae5f4daf26f8b6227f203d2a8bbf")]
+[assembly: InternalsVisibleTo("DocumentFormat.OpenXml.Features, Publickey=002400000480000094000000060200000024000052534131000400000100010061d8931836c82bf25ca6b773dfd6e7b3ab4e43fba60cf4a86347170373415a165ccc40da3da4a52163822db9fa91f15828236d32d6a9fe754859f10d1f8262646c1f3fb6b4348123f14d733db0ff11c3198b7cf56caaebbf14563990446a6c32aff36d5a7097194294c127fe3cdf9f2609daae5f4daf26f8b6227f203d2a8bbf")]
[assembly: InternalsVisibleTo("DocumentFormat.OpenXml.Linq, Publickey=002400000480000094000000060200000024000052534131000400000100010061d8931836c82bf25ca6b773dfd6e7b3ab4e43fba60cf4a86347170373415a165ccc40da3da4a52163822db9fa91f15828236d32d6a9fe754859f10d1f8262646c1f3fb6b4348123f14d733db0ff11c3198b7cf56caaebbf14563990446a6c32aff36d5a7097194294c127fe3cdf9f2609daae5f4daf26f8b6227f203d2a8bbf")]
// For testing
diff --git a/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs b/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs
index 196a432ee..d321ee52d 100644
--- a/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs
+++ b/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs
@@ -2,10 +2,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
-
-#if !NET6_0_OR_GREATER
using System.Text;
-#endif
namespace DocumentFormat.OpenXml
{
@@ -73,6 +70,8 @@ static char ToCharUpper(int value)
///
/// A byte array to use to create a new hex string.
/// A hex string that corresponds to the value parameter.
- public static string Create(params byte[] bytes) => Create(bytes.AsSpan());
+ // Use constructor instead of bytes.AsSpan() to avoid SpanExtensions ambiguity
+ // when this file is linked into DocumentFormat.OpenXml.Features.
+ public static string Create(params byte[] bytes) => Create(new ReadOnlySpan(bytes));
}
}
diff --git a/src/DocumentFormat.OpenXml.Framework/SimpleTypes/ListValue.cs b/src/DocumentFormat.OpenXml.Framework/SimpleTypes/ListValue.cs
index c4a1b2cad..78bcb1784 100644
--- a/src/DocumentFormat.OpenXml.Framework/SimpleTypes/ListValue.cs
+++ b/src/DocumentFormat.OpenXml.Framework/SimpleTypes/ListValue.cs
@@ -1,13 +1,13 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+using DocumentFormat.OpenXml.Framework;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Diagnostics;
-using System.Text;
namespace DocumentFormat.OpenXml
{
@@ -194,7 +194,7 @@ public override string? InnerText
{
if (TextValue is null && _list is not null)
{
- var textString = new StringBuilder();
+ var textString = StringBuilderPool.Acquire();
string separator = string.Empty;
foreach (var value in _list)
@@ -207,7 +207,7 @@ public override string? InnerText
}
}
- TextValue = textString.ToString();
+ TextValue = StringBuilderPool.GetValueAndRelease(textString);
}
return TextValue;
diff --git a/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeAbsentConditionToNonValue.cs b/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeAbsentConditionToNonValue.cs
index 74bf90d85..0ae39800f 100644
--- a/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeAbsentConditionToNonValue.cs
+++ b/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeAbsentConditionToNonValue.cs
@@ -2,7 +2,6 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using DocumentFormat.OpenXml.Framework;
-using System.Text;
namespace DocumentFormat.OpenXml.Validation.Semantic
{
@@ -60,19 +59,25 @@ public AttributeAbsentConditionToNonValue(OpenXmlQualifiedName absentAttribute,
}
}
- var sb = new StringBuilder();
- sb.Append('\'').Append(_values[0]).Append('\'');
+ var sb = StringBuilderPool.Acquire();
+ sb.Append('\'');
+ sb.Append(_values[0]);
+ sb.Append('\'');
if (_values.Length > 1)
{
for (int i = 1; i < _values.Length - 1; i++)
{
- sb.Append(", '").Append(_values[i]).Append('\'');
+ sb.Append(", '");
+ sb.Append(_values[i]);
+ sb.Append('\'');
}
- sb.Append(" and '").Append(_values[_values.Length - 1]).Append('\'');
+ sb.Append(" and '");
+ sb.Append(_values[_values.Length - 1]);
+ sb.Append('\'');
}
- string valueString = sb.ToString();
+ string valueString = StringBuilderPool.GetValueAndRelease(sb);
return new ValidationErrorInfo()
{
diff --git a/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeAbsentConditionToValue.cs b/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeAbsentConditionToValue.cs
index 0bd5f3c0d..e7074b6eb 100644
--- a/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeAbsentConditionToValue.cs
+++ b/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeAbsentConditionToValue.cs
@@ -2,7 +2,6 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using DocumentFormat.OpenXml.Framework;
-using System.Text;
namespace DocumentFormat.OpenXml.Validation.Semantic
{
@@ -56,19 +55,25 @@ public AttributeAbsentConditionToValue(OpenXmlQualifiedName absentAttribute, Ope
{
if (AttributeValueEquals(conditionAttribute.Value, value, false))
{
- var sb = new StringBuilder();
- sb.Append('\'').Append(_values[0]).Append('\'');
+ var sb = StringBuilderPool.Acquire();
+ sb.Append('\'');
+ sb.Append(_values[0]);
+ sb.Append('\'');
if (_values.Length > 1)
{
for (int i = 1; i < _values.Length - 1; i++)
{
- sb.Append(", '").Append(_values[i]).Append('\'');
+ sb.Append(", '");
+ sb.Append(_values[i]);
+ sb.Append('\'');
}
- sb.Append(" or '").Append(_values[_values.Length - 1]).Append('\'');
+ sb.Append(" or '");
+ sb.Append(_values[_values.Length - 1]);
+ sb.Append('\'');
}
- string valueString = sb.ToString();
+ string valueString = StringBuilderPool.GetValueAndRelease(sb);
return new ValidationErrorInfo()
{
diff --git a/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeMutualExclusive.cs b/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeMutualExclusive.cs
index 8ad401a7c..d64d8e9fd 100644
--- a/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeMutualExclusive.cs
+++ b/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeMutualExclusive.cs
@@ -2,7 +2,6 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using DocumentFormat.OpenXml.Framework;
-using System.Text;
namespace DocumentFormat.OpenXml.Validation.Semantic
{
@@ -58,8 +57,8 @@ private AttributeMutualExclusive(OpenXmlQualifiedName[] attributes)
return null;
}
- var attributesSb = new StringBuilder();
- var existAttributeSb = new StringBuilder();
+ var attributesSb = StringBuilderPool.Acquire();
+ var existAttributeSb = StringBuilderPool.Acquire();
string? existAttribute2 = null;
foreach (var attribute in _attributes)
@@ -94,6 +93,8 @@ private AttributeMutualExclusive(OpenXmlQualifiedName[] attributes)
if (existAttributeSb.Length == 0)
{
+ StringBuilderPool.Release(attributesSb);
+ StringBuilderPool.Release(existAttributeSb);
return null;
}
@@ -104,9 +105,9 @@ private AttributeMutualExclusive(OpenXmlQualifiedName[] attributes)
Node = element,
Description = SR.Format(
ValidationResources.Sem_AttributeMutualExclusive,
- existAttributeSb.ToString(),
+ StringBuilderPool.GetValueAndRelease(existAttributeSb),
existAttribute2,
- attributesSb.ToString()),
+ StringBuilderPool.GetValueAndRelease(attributesSb)),
};
}
}
diff --git a/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeValueConditionToAnother.cs b/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeValueConditionToAnother.cs
index 37744f78f..7c2ff8071 100644
--- a/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeValueConditionToAnother.cs
+++ b/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeValueConditionToAnother.cs
@@ -2,7 +2,6 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using DocumentFormat.OpenXml.Framework;
-using System.Text;
namespace DocumentFormat.OpenXml.Validation.Semantic
{
@@ -66,33 +65,45 @@ public AttributeValueConditionToAnother(OpenXmlQualifiedName attribute, OpenXmlQ
{
if (AttributeValueEquals(conditionAttribute.Value, value, false))
{
- var sb = new StringBuilder();
- sb.Append('\'').Append(_values[0]).Append('\'');
+ var sb = StringBuilderPool.Acquire();
+ sb.Append('\'');
+ sb.Append(_values[0]);
+ sb.Append('\'');
if (_values.Length > 1)
{
for (int i = 1; i < _values.Length - 1; i++)
{
- sb.Append(", '").Append(_values[i]).Append('\'');
+ sb.Append(", '");
+ sb.Append(_values[i]);
+ sb.Append('\'');
}
- sb.Append(" or '").Append(_values[_values.Length - 1]).Append('\'');
+ sb.Append(" or '");
+ sb.Append(_values[_values.Length - 1]);
+ sb.Append('\'');
}
- string attributeValueString = sb.ToString();
+ string attributeValueString = StringBuilderPool.GetValueAndRelease(sb);
- var otherSb = new StringBuilder();
- otherSb.Append('\'').Append(_otherValues[0]).Append('\'');
+ var otherSb = StringBuilderPool.Acquire();
+ otherSb.Append('\'');
+ otherSb.Append(_otherValues[0]);
+ otherSb.Append('\'');
if (_otherValues.Length > 1)
{
for (int i = 1; i < _otherValues.Length - 1; i++)
{
- otherSb.Append(", '").Append(_otherValues[i]).Append('\'');
+ otherSb.Append(", '");
+ otherSb.Append(_otherValues[i]);
+ otherSb.Append('\'');
}
- otherSb.Append(" or '").Append(_otherValues[_otherValues.Length - 1]).Append('\'');
+ otherSb.Append(" or '");
+ otherSb.Append(_otherValues[_otherValues.Length - 1]);
+ otherSb.Append('\'');
}
- string otherAttributeValueString = otherSb.ToString();
+ string otherAttributeValueString = StringBuilderPool.GetValueAndRelease(otherSb);
return new ValidationErrorInfo()
{
diff --git a/src/DocumentFormat.OpenXml.Framework/XmlPath.cs b/src/DocumentFormat.OpenXml.Framework/XmlPath.cs
index 510c9dfa7..8478b9423 100644
--- a/src/DocumentFormat.OpenXml.Framework/XmlPath.cs
+++ b/src/DocumentFormat.OpenXml.Framework/XmlPath.cs
@@ -9,7 +9,6 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
-using System.Text;
using System.Xml;
namespace DocumentFormat.OpenXml
@@ -113,7 +112,7 @@ private static string TryBuildXPath(Stack elements, out XmlNames
return string.Empty;
}
- var xpath = new StringBuilder();
+ var xpath = StringBuilderPool.Acquire();
namespaces = new XmlNamespaceManager(new NameTable());
foreach (var element in elements)
@@ -152,7 +151,7 @@ private static string TryBuildXPath(Stack elements, out XmlNames
}
}
- return xpath.ToString();
+ return StringBuilderPool.GetValueAndRelease(xpath);
}
private static Stack GetElements(OpenXmlElement? element)