From 9d5a379e109e2e1ef0b875a6d7e6aa0d868834b1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 28 Apr 2026 01:16:22 +0000
Subject: [PATCH 1/7] Initial plan
From e9617793d16169cc1621692a30ce3b387229ab9f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 28 Apr 2026 01:27:24 +0000
Subject: [PATCH 2/7] Use a pooled StringBuilder for all instances
Agent-Logs-Url: https://github.com/dotnet/Open-XML-SDK/sessions/64d34d63-d66c-4446-b6e3-3697d0681811
Co-authored-by: twsouthwick <583206+twsouthwick@users.noreply.github.com>
---
.../OpenXmlCompositeElement.cs | 5 +-
.../SimpleTypes/HexStringFactory.cs | 8 +--
.../SimpleTypes/ListValue.cs | 5 +-
.../System/StringBuilderPool.cs | 71 +++++++++++++++++++
.../AttributeAbsentConditionToNonValue.cs | 5 +-
.../AttributeAbsentConditionToValue.cs | 5 +-
.../Semantic/AttributeMutualExclusive.cs | 11 +--
.../AttributeValueConditionToAnother.cs | 9 ++-
.../XmlPath.cs | 5 +-
9 files changed, 93 insertions(+), 31 deletions(-)
create mode 100644 src/DocumentFormat.OpenXml.Framework/System/StringBuilderPool.cs
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/SimpleTypes/HexStringFactory.cs b/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs
index 196a432ee..796eaef83 100644
--- a/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs
+++ b/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs
@@ -3,10 +3,6 @@
using System;
-#if !NET6_0_OR_GREATER
-using System.Text;
-#endif
-
namespace DocumentFormat.OpenXml
{
///
@@ -43,7 +39,7 @@ static string Create(ReadOnlySpan bytes)
return new string(chars);
#else
- var sb = new StringBuilder(bytes.Length * 2);
+ var sb = StringBuilderPool.Acquire();
foreach (var b in bytes)
{
@@ -51,7 +47,7 @@ static string Create(ReadOnlySpan bytes)
sb.Append(ToCharUpper(b));
}
- return sb.ToString();
+ return StringBuilderPool.GetValueAndRelease(sb);
#endif
static char ToCharUpper(int value)
diff --git a/src/DocumentFormat.OpenXml.Framework/SimpleTypes/ListValue.cs b/src/DocumentFormat.OpenXml.Framework/SimpleTypes/ListValue.cs
index c4a1b2cad..782014aa4 100644
--- a/src/DocumentFormat.OpenXml.Framework/SimpleTypes/ListValue.cs
+++ b/src/DocumentFormat.OpenXml.Framework/SimpleTypes/ListValue.cs
@@ -7,7 +7,6 @@
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Diagnostics;
-using System.Text;
namespace DocumentFormat.OpenXml
{
@@ -194,7 +193,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 +206,7 @@ public override string? InnerText
}
}
- TextValue = textString.ToString();
+ TextValue = StringBuilderPool.GetValueAndRelease(textString);
}
return TextValue;
diff --git a/src/DocumentFormat.OpenXml.Framework/System/StringBuilderPool.cs b/src/DocumentFormat.OpenXml.Framework/System/StringBuilderPool.cs
new file mode 100644
index 000000000..4b11e241c
--- /dev/null
+++ b/src/DocumentFormat.OpenXml.Framework/System/StringBuilderPool.cs
@@ -0,0 +1,71 @@
+// 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;
+
+namespace DocumentFormat.OpenXml;
+
+///
+/// Provides a reusable pool of instances to reduce allocations.
+///
+internal static class StringBuilderPool
+{
+ // Two slots per thread to handle cases where two StringBuilders are active simultaneously.
+ [System.ThreadStatic]
+ private static StringBuilder? _primary;
+
+ [System.ThreadStatic]
+ private static StringBuilder? _secondary;
+
+ ///
+ /// Acquires a from the pool, or creates a new one if the pool is empty.
+ ///
+ public static StringBuilder Acquire()
+ {
+ var sb = _primary;
+ if (sb is not null)
+ {
+ _primary = null;
+ return sb;
+ }
+
+ sb = _secondary;
+ if (sb is not null)
+ {
+ _secondary = null;
+ return sb;
+ }
+
+ 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)
+ {
+ // Use Length = 0 instead of Clear() for .NET 3.5 compatibility
+ sb.Length = 0;
+
+ if (_primary is null)
+ {
+ _primary = sb;
+ }
+ else if (_secondary is null)
+ {
+ _secondary = sb;
+ }
+
+ // If both slots are full, the instance is abandoned and will be garbage collected.
+ }
+}
diff --git a/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeAbsentConditionToNonValue.cs b/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeAbsentConditionToNonValue.cs
index 74bf90d85..4a6a5b036 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,7 +59,7 @@ public AttributeAbsentConditionToNonValue(OpenXmlQualifiedName absentAttribute,
}
}
- var sb = new StringBuilder();
+ var sb = StringBuilderPool.Acquire();
sb.Append('\'').Append(_values[0]).Append('\'');
if (_values.Length > 1)
{
@@ -72,7 +71,7 @@ public AttributeAbsentConditionToNonValue(OpenXmlQualifiedName absentAttribute,
sb.Append(" and '").Append(_values[_values.Length - 1]).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..6de68d1a7 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,7 +55,7 @@ public AttributeAbsentConditionToValue(OpenXmlQualifiedName absentAttribute, Ope
{
if (AttributeValueEquals(conditionAttribute.Value, value, false))
{
- var sb = new StringBuilder();
+ var sb = StringBuilderPool.Acquire();
sb.Append('\'').Append(_values[0]).Append('\'');
if (_values.Length > 1)
{
@@ -68,7 +67,7 @@ public AttributeAbsentConditionToValue(OpenXmlQualifiedName absentAttribute, Ope
sb.Append(" or '").Append(_values[_values.Length - 1]).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..a699dcde9 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,7 +65,7 @@ public AttributeValueConditionToAnother(OpenXmlQualifiedName attribute, OpenXmlQ
{
if (AttributeValueEquals(conditionAttribute.Value, value, false))
{
- var sb = new StringBuilder();
+ var sb = StringBuilderPool.Acquire();
sb.Append('\'').Append(_values[0]).Append('\'');
if (_values.Length > 1)
{
@@ -78,9 +77,9 @@ public AttributeValueConditionToAnother(OpenXmlQualifiedName attribute, OpenXmlQ
sb.Append(" or '").Append(_values[_values.Length - 1]).Append('\'');
}
- string attributeValueString = sb.ToString();
+ string attributeValueString = StringBuilderPool.GetValueAndRelease(sb);
- var otherSb = new StringBuilder();
+ var otherSb = StringBuilderPool.Acquire();
otherSb.Append('\'').Append(_otherValues[0]).Append('\'');
if (_otherValues.Length > 1)
{
@@ -92,7 +91,7 @@ public AttributeValueConditionToAnother(OpenXmlQualifiedName attribute, OpenXmlQ
otherSb.Append(" or '").Append(_otherValues[_otherValues.Length - 1]).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)
From 31bceaa11bc294e7c07f7ec5384902eba926da22 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 28 Apr 2026 01:32:24 +0000
Subject: [PATCH 3/7] Address code review: add capacity cap and EnsureCapacity
hint
Agent-Logs-Url: https://github.com/dotnet/Open-XML-SDK/sessions/64d34d63-d66c-4446-b6e3-3697d0681811
Co-authored-by: twsouthwick <583206+twsouthwick@users.noreply.github.com>
---
.../SimpleTypes/HexStringFactory.cs | 1 +
.../System/StringBuilderPool.cs | 9 +++++++++
2 files changed, 10 insertions(+)
diff --git a/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs b/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs
index 796eaef83..9b67da17f 100644
--- a/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs
+++ b/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs
@@ -40,6 +40,7 @@ static string Create(ReadOnlySpan bytes)
return new string(chars);
#else
var sb = StringBuilderPool.Acquire();
+ sb.EnsureCapacity(bytes.Length * 2);
foreach (var b in bytes)
{
diff --git a/src/DocumentFormat.OpenXml.Framework/System/StringBuilderPool.cs b/src/DocumentFormat.OpenXml.Framework/System/StringBuilderPool.cs
index 4b11e241c..7f62dfe2e 100644
--- a/src/DocumentFormat.OpenXml.Framework/System/StringBuilderPool.cs
+++ b/src/DocumentFormat.OpenXml.Framework/System/StringBuilderPool.cs
@@ -10,6 +10,9 @@ namespace DocumentFormat.OpenXml;
///
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;
+
// Two slots per thread to handle cases where two StringBuilders are active simultaneously.
[System.ThreadStatic]
private static StringBuilder? _primary;
@@ -54,6 +57,12 @@ public static string GetValueAndRelease(StringBuilder sb)
///
public static void Release(StringBuilder sb)
{
+ // Don't pool excessively large instances to avoid holding onto large memory allocations.
+ if (sb.Capacity > MaxBuilderCapacity)
+ {
+ return;
+ }
+
// Use Length = 0 instead of Clear() for .NET 3.5 compatibility
sb.Length = 0;
From 725c004f7e53803e20943d058ea1877719e45660 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 28 Apr 2026 02:14:13 +0000
Subject: [PATCH 4/7] Replace ThreadStatic pool with shared
DefaultObjectPool-style pool; move to Framework/; use InternalsVisibleTo for
Features
Agent-Logs-Url: https://github.com/dotnet/Open-XML-SDK/sessions/b9e3379d-a520-4210-aa32-03e0ec470e6e
Co-authored-by: twsouthwick <583206+twsouthwick@users.noreply.github.com>
---
.../Framework/StringBuilderPool.cs | 103 ++++++++++++++++++
.../Properties/Properties.cs | 1 +
.../SimpleTypes/HexStringFactory.cs | 3 +-
.../SimpleTypes/ListValue.cs | 1 +
.../System/StringBuilderPool.cs | 80 --------------
5 files changed, 107 insertions(+), 81 deletions(-)
create mode 100644 src/DocumentFormat.OpenXml.Framework/Framework/StringBuilderPool.cs
delete mode 100644 src/DocumentFormat.OpenXml.Framework/System/StringBuilderPool.cs
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/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 9b67da17f..4bb35a7a1 100644
--- a/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs
+++ b/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs
@@ -1,6 +1,7 @@
// 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;
namespace DocumentFormat.OpenXml
@@ -70,6 +71,6 @@ 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());
+ 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 782014aa4..78bcb1784 100644
--- a/src/DocumentFormat.OpenXml.Framework/SimpleTypes/ListValue.cs
+++ b/src/DocumentFormat.OpenXml.Framework/SimpleTypes/ListValue.cs
@@ -1,6 +1,7 @@
// 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;
diff --git a/src/DocumentFormat.OpenXml.Framework/System/StringBuilderPool.cs b/src/DocumentFormat.OpenXml.Framework/System/StringBuilderPool.cs
deleted file mode 100644
index 7f62dfe2e..000000000
--- a/src/DocumentFormat.OpenXml.Framework/System/StringBuilderPool.cs
+++ /dev/null
@@ -1,80 +0,0 @@
-// 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;
-
-namespace DocumentFormat.OpenXml;
-
-///
-/// Provides a reusable pool of instances to reduce allocations.
-///
-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;
-
- // Two slots per thread to handle cases where two StringBuilders are active simultaneously.
- [System.ThreadStatic]
- private static StringBuilder? _primary;
-
- [System.ThreadStatic]
- private static StringBuilder? _secondary;
-
- ///
- /// Acquires a from the pool, or creates a new one if the pool is empty.
- ///
- public static StringBuilder Acquire()
- {
- var sb = _primary;
- if (sb is not null)
- {
- _primary = null;
- return sb;
- }
-
- sb = _secondary;
- if (sb is not null)
- {
- _secondary = null;
- return sb;
- }
-
- 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 holding onto large memory allocations.
- if (sb.Capacity > MaxBuilderCapacity)
- {
- return;
- }
-
- // Use Length = 0 instead of Clear() for .NET 3.5 compatibility
- sb.Length = 0;
-
- if (_primary is null)
- {
- _primary = sb;
- }
- else if (_secondary is null)
- {
- _secondary = sb;
- }
-
- // If both slots are full, the instance is abandoned and will be garbage collected.
- }
-}
From 490955e04a852d755c2fff07e7d6504de53bbef3 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 28 Apr 2026 02:14:56 +0000
Subject: [PATCH 5/7] Add comment explaining ReadOnlySpan constructor use in
HexStringFactory
Agent-Logs-Url: https://github.com/dotnet/Open-XML-SDK/sessions/b9e3379d-a520-4210-aa32-03e0ec470e6e
Co-authored-by: twsouthwick <583206+twsouthwick@users.noreply.github.com>
---
.../SimpleTypes/HexStringFactory.cs | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs b/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs
index 4bb35a7a1..75a458006 100644
--- a/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs
+++ b/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs
@@ -71,6 +71,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.
+ // 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));
}
}
From c6244f4c323a6efe29ebef4432d6b53d00c163ca Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 28 Apr 2026 02:25:58 +0000
Subject: [PATCH 6/7] Un-chain .Append calls to one per line for readability
Agent-Logs-Url: https://github.com/dotnet/Open-XML-SDK/sessions/07f56062-8462-44e4-9b69-bba899c6df89
Co-authored-by: twsouthwick <583206+twsouthwick@users.noreply.github.com>
---
.../AttributeAbsentConditionToNonValue.cs | 12 +++++++---
.../AttributeAbsentConditionToValue.cs | 12 +++++++---
.../AttributeValueConditionToAnother.cs | 24 ++++++++++++++-----
3 files changed, 36 insertions(+), 12 deletions(-)
diff --git a/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeAbsentConditionToNonValue.cs b/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeAbsentConditionToNonValue.cs
index 4a6a5b036..0ae39800f 100644
--- a/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeAbsentConditionToNonValue.cs
+++ b/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeAbsentConditionToNonValue.cs
@@ -60,15 +60,21 @@ public AttributeAbsentConditionToNonValue(OpenXmlQualifiedName absentAttribute,
}
var sb = StringBuilderPool.Acquire();
- sb.Append('\'').Append(_values[0]).Append('\'');
+ 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 = StringBuilderPool.GetValueAndRelease(sb);
diff --git a/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeAbsentConditionToValue.cs b/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeAbsentConditionToValue.cs
index 6de68d1a7..e7074b6eb 100644
--- a/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeAbsentConditionToValue.cs
+++ b/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeAbsentConditionToValue.cs
@@ -56,15 +56,21 @@ public AttributeAbsentConditionToValue(OpenXmlQualifiedName absentAttribute, Ope
if (AttributeValueEquals(conditionAttribute.Value, value, false))
{
var sb = StringBuilderPool.Acquire();
- sb.Append('\'').Append(_values[0]).Append('\'');
+ 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 = StringBuilderPool.GetValueAndRelease(sb);
diff --git a/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeValueConditionToAnother.cs b/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeValueConditionToAnother.cs
index a699dcde9..7c2ff8071 100644
--- a/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeValueConditionToAnother.cs
+++ b/src/DocumentFormat.OpenXml.Framework/Validation/Semantic/AttributeValueConditionToAnother.cs
@@ -66,29 +66,41 @@ public AttributeValueConditionToAnother(OpenXmlQualifiedName attribute, OpenXmlQ
if (AttributeValueEquals(conditionAttribute.Value, value, false))
{
var sb = StringBuilderPool.Acquire();
- sb.Append('\'').Append(_values[0]).Append('\'');
+ 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 = StringBuilderPool.GetValueAndRelease(sb);
var otherSb = StringBuilderPool.Acquire();
- otherSb.Append('\'').Append(_otherValues[0]).Append('\'');
+ 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 = StringBuilderPool.GetValueAndRelease(otherSb);
From 84dad9cef821092bd942fb0de75f6ca171011268 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 28 Apr 2026 03:41:25 +0000
Subject: [PATCH 7/7] Fix CS0122: use new StringBuilder() in HexStringFactory
instead of internal StringBuilderPool
Agent-Logs-Url: https://github.com/dotnet/Open-XML-SDK/sessions/dba14ab9-b782-474a-8a60-32881273ee7a
Co-authored-by: twsouthwick <583206+twsouthwick@users.noreply.github.com>
---
.../SimpleTypes/HexStringFactory.cs | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs b/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs
index 75a458006..d321ee52d 100644
--- a/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs
+++ b/src/DocumentFormat.OpenXml.Framework/SimpleTypes/HexStringFactory.cs
@@ -1,8 +1,8 @@
// 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.Text;
namespace DocumentFormat.OpenXml
{
@@ -40,8 +40,7 @@ static string Create(ReadOnlySpan bytes)
return new string(chars);
#else
- var sb = StringBuilderPool.Acquire();
- sb.EnsureCapacity(bytes.Length * 2);
+ var sb = new StringBuilder(bytes.Length * 2);
foreach (var b in bytes)
{
@@ -49,7 +48,7 @@ static string Create(ReadOnlySpan bytes)
sb.Append(ToCharUpper(b));
}
- return StringBuilderPool.GetValueAndRelease(sb);
+ return sb.ToString();
#endif
static char ToCharUpper(int value)