diff --git a/src/Http/Routing/src/Patterns/RoutePattern.cs b/src/Http/Routing/src/Patterns/RoutePattern.cs index b500f1d2f7..4cd881c8e2 100644 --- a/src/Http/Routing/src/Patterns/RoutePattern.cs +++ b/src/Http/Routing/src/Patterns/RoutePattern.cs @@ -139,9 +139,12 @@ namespace Microsoft.AspNetCore.Routing.Patterns throw new ArgumentNullException(nameof(name)); } - for (var i = 0; i < Parameters.Count; i++) + var parameters = Parameters; + // Read interface .Count once rather than per iteration + var parametersCount = parameters.Count; + for (var i = 0; i < parametersCount; i++) { - var parameter = Parameters[i]; + var parameter = parameters[i]; if (string.Equals(parameter.Name, name, StringComparison.OrdinalIgnoreCase)) { return parameter; diff --git a/src/Http/Routing/src/Template/TemplateBinder.cs b/src/Http/Routing/src/Template/TemplateBinder.cs index e0f72fdfa5..f3924f3c92 100644 --- a/src/Http/Routing/src/Template/TemplateBinder.cs +++ b/src/Http/Routing/src/Template/TemplateBinder.cs @@ -521,16 +521,20 @@ namespace Microsoft.AspNetCore.Routing.Template } } - for (var i = 0; i < _pattern.PathSegments.Count; i++) + var segments = _pattern.PathSegments; + // Read interface .Count once rather than per iteration + var segmentsCount = segments.Count; + for (var i = 0; i < segmentsCount; i++) { Debug.Assert(context.BufferState == SegmentState.Beginning); Debug.Assert(context.UriState == SegmentState.Beginning); - var segment = _pattern.PathSegments[i]; - - for (var j = 0; j < segment.Parts.Count; j++) + var parts = segments[i].Parts; + // Read interface .Count once rather than per iteration + var partsCount = parts.Count; + for (var j = 0; j < partsCount; j++) { - var part = segment.Parts[j]; + var part = parts[j]; if (part is RoutePatternLiteralPart literalPart) { @@ -581,7 +585,7 @@ namespace Microsoft.AspNetCore.Routing.Template // for format, so we remove '.' and generate 5. if (!context.Accept(converted, parameterPart.EncodeSlashes)) { - if (j != 0 && parameterPart.IsOptional && (separatorPart = segment.Parts[j - 1] as RoutePatternSeparatorPart) != null) + if (j != 0 && parameterPart.IsOptional && (separatorPart = parts[j - 1] as RoutePatternSeparatorPart) != null) { context.Remove(separatorPart.Content); } diff --git a/src/Http/Routing/src/Tree/LinkGenerationDecisionTree.cs b/src/Http/Routing/src/Tree/LinkGenerationDecisionTree.cs index 8b8315285f..568c377745 100644 --- a/src/Http/Routing/src/Tree/LinkGenerationDecisionTree.cs +++ b/src/Http/Routing/src/Tree/LinkGenerationDecisionTree.cs @@ -113,14 +113,20 @@ namespace Microsoft.AspNetCore.Routing.Tree { // Any entries in node.Matches have had all their required values satisfied, so add them // to the results. - for (var i = 0; i < node.Matches.Count; i++) + var matches = node.Matches; + // Read interface .Count once rather than per iteration + var matchesCount = matches.Count; + for (var i = 0; i < matchesCount; i++) { - results.Add(new OutboundMatchResult(node.Matches[i], isFallbackPath)); + results.Add(new OutboundMatchResult(matches[i], isFallbackPath)); } - for (var i = 0; i < node.Criteria.Count; i++) + var criteria = node.Criteria; + // Read interface .Count once rather than per iteration + var criteriaCount = criteria.Count; + for (var i = 0; i < criteriaCount; i++) { - var criterion = node.Criteria[i]; + var criterion = criteria[i]; var key = criterion.Key; if (values.TryGetValue(key, out var value)) diff --git a/src/Mvc/Mvc.Core/src/Filters/DefaultFilterProvider.cs b/src/Mvc/Mvc.Core/src/Filters/DefaultFilterProvider.cs index 03c7c62004..f8bae84325 100644 --- a/src/Mvc/Mvc.Core/src/Filters/DefaultFilterProvider.cs +++ b/src/Mvc/Mvc.Core/src/Filters/DefaultFilterProvider.cs @@ -21,10 +21,12 @@ namespace Microsoft.AspNetCore.Mvc.Filters if (context.ActionContext.ActionDescriptor.FilterDescriptors != null) { - // Perf: Avoid allocations - for (var i = 0; i < context.Results.Count; i++) + var results = context.Results; + // Perf: Avoid allocating enumerator and read interface .Count once rather than per iteration + var resultsCount = results.Count; + for (var i = 0; i < resultsCount; i++) { - ProvideFilter(context, context.Results[i]); + ProvideFilter(context, results[i]); } } } diff --git a/src/Mvc/Mvc.Razor/src/RazorViewEngine.cs b/src/Mvc/Mvc.Razor/src/RazorViewEngine.cs index ad87fbb64b..7c3881b37e 100644 --- a/src/Mvc/Mvc.Razor/src/RazorViewEngine.cs +++ b/src/Mvc/Mvc.Razor/src/RazorViewEngine.cs @@ -249,15 +249,18 @@ namespace Microsoft.AspNetCore.Mvc.Razor isMainPage); Dictionary expanderValues = null; - if (_options.ViewLocationExpanders.Count > 0) + var expanders = _options.ViewLocationExpanders; + // Read interface .Count once rather than per iteration + var expandersCount = expanders.Count; + if (expandersCount > 0) { expanderValues = new Dictionary(StringComparer.Ordinal); expanderContext.Values = expanderValues; // Perf: Avoid allocations - for (var i = 0; i < _options.ViewLocationExpanders.Count; i++) + for (var i = 0; i < expandersCount; i++) { - _options.ViewLocationExpanders[i].PopulateValues(expanderContext); + expanders[i].PopulateValues(expanderContext); } } @@ -350,9 +353,12 @@ namespace Microsoft.AspNetCore.Mvc.Razor { var viewLocations = GetViewLocationFormats(expanderContext); - for (var i = 0; i < _options.ViewLocationExpanders.Count; i++) + var expanders = _options.ViewLocationExpanders; + // Read interface .Count once rather than per iteration + var expandersCount = expanders.Count; + for (var i = 0; i < expandersCount; i++) { - viewLocations = _options.ViewLocationExpanders[i].ExpandViewLocations(expanderContext, viewLocations); + viewLocations = expanders[i].ExpandViewLocations(expanderContext, viewLocations); } ViewLocationCacheResult cacheResult = null; @@ -404,9 +410,12 @@ namespace Microsoft.AspNetCore.Mvc.Razor var viewDescriptor = factoryResult.ViewDescriptor; if (viewDescriptor?.ExpirationTokens != null) { - for (var i = 0; i < viewDescriptor.ExpirationTokens.Count; i++) + var viewExpirationTokens = viewDescriptor.ExpirationTokens; + // Read interface .Count once rather than per iteration + var viewExpirationTokensCount = viewExpirationTokens.Count; + for (var i = 0; i < viewExpirationTokensCount; i++) { - expirationTokens.Add(viewDescriptor.ExpirationTokens[i]); + expirationTokens.Add(viewExpirationTokens[i]); } } diff --git a/src/Mvc/Mvc.Razor/src/TagHelpers/UrlResolutionTagHelper.cs b/src/Mvc/Mvc.Razor/src/TagHelpers/UrlResolutionTagHelper.cs index 21ae7d4ce3..b27adba24f 100644 --- a/src/Mvc/Mvc.Razor/src/TagHelpers/UrlResolutionTagHelper.cs +++ b/src/Mvc/Mvc.Razor/src/TagHelpers/UrlResolutionTagHelper.cs @@ -156,9 +156,12 @@ namespace Microsoft.AspNetCore.Mvc.Razor.TagHelpers throw new ArgumentNullException(nameof(output)); } - for (var i = 0; i < output.Attributes.Count; i++) + var attributes = output.Attributes; + // Read interface .Count once rather than per iteration + var attributesCount = attributes.Count; + for (var i = 0; i < attributesCount; i++) { - var attribute = output.Attributes[i]; + var attribute = attributes[i]; if (!string.Equals(attribute.Name, attributeName, StringComparison.OrdinalIgnoreCase)) { continue; @@ -170,7 +173,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.TagHelpers string resolvedUrl; if (TryResolveUrl(stringValue, resolvedUrl: out resolvedUrl)) { - output.Attributes[i] = new TagHelperAttribute( + attributes[i] = new TagHelperAttribute( attribute.Name, resolvedUrl, attribute.ValueStyle); @@ -199,7 +202,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.TagHelpers IHtmlContent resolvedUrl; if (TryResolveUrl(stringValue, resolvedUrl: out resolvedUrl)) { - output.Attributes[i] = new TagHelperAttribute( + attributes[i] = new TagHelperAttribute( attribute.Name, resolvedUrl, attribute.ValueStyle); @@ -207,7 +210,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.TagHelpers else if (htmlString == null) { // Not a ~/ URL. Just avoid re-encoding the attribute value later. - output.Attributes[i] = new TagHelperAttribute( + attributes[i] = new TagHelperAttribute( attribute.Name, new HtmlString(stringValue), attribute.ValueStyle); diff --git a/src/Mvc/Mvc.TagHelpers/src/AttributeMatcher.cs b/src/Mvc/Mvc.TagHelpers/src/AttributeMatcher.cs index 309f2aa574..55584fa94f 100644 --- a/src/Mvc/Mvc.TagHelpers/src/AttributeMatcher.cs +++ b/src/Mvc/Mvc.TagHelpers/src/AttributeMatcher.cs @@ -44,13 +44,20 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers } var foundResult = false; - result = default(TMode); + result = default; // Perf: Avoid allocating enumerator - for (var i = 0; i < modeInfos.Count; i++) + var modeInfosCount = modeInfos.Count; + var allAttributes = context.AllAttributes; + // Read interface .Count once rather than per iteration + var allAttributesCount = allAttributes.Count; + for (var i = 0; i < modeInfosCount; i++) { var modeInfo = modeInfos[i]; - if (!HasMissingAttributes(context, modeInfo.Attributes) && + var requiredAttributes = modeInfo.Attributes; + // If there are fewer attributes present than required, one or more of them must be missing. + if (allAttributesCount >= requiredAttributes.Length && + !HasMissingAttributes(allAttributes, requiredAttributes) && compare(result, modeInfo.Mode) <= 0) { foundResult = true; @@ -61,19 +68,13 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers return foundResult; } - private static bool HasMissingAttributes(TagHelperContext context, string[] requiredAttributes) + private static bool HasMissingAttributes(ReadOnlyTagHelperAttributeList allAttributes, string[] requiredAttributes) { - if (context.AllAttributes.Count < requiredAttributes.Length) - { - // If there are fewer attributes present than required, one or more of them must be missing. - return true; - } - // Check for all attribute values // Perf: Avoid allocating enumerator for (var i = 0; i < requiredAttributes.Length; i++) { - if (!context.AllAttributes.TryGetAttribute(requiredAttributes[i], out var attribute)) + if (!allAttributes.TryGetAttribute(requiredAttributes[i], out var attribute)) { // Missing attribute. return true; diff --git a/src/Mvc/Mvc.TagHelpers/src/LinkTagHelper.cs b/src/Mvc/Mvc.TagHelpers/src/LinkTagHelper.cs index ac090a14cf..4f1e655785 100644 --- a/src/Mvc/Mvc.TagHelpers/src/LinkTagHelper.cs +++ b/src/Mvc/Mvc.TagHelpers/src/LinkTagHelper.cs @@ -381,8 +381,9 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers builder.AppendHtml(", \""); - // Perf: Avoid allocating enumerator - for (var i = 0; i < attributes.Count; i++) + // Perf: Avoid allocating enumerator and read interface .Count once rather than per iteration + var attributesCount = attributes.Count; + for (var i = 0; i < attributesCount; i++) { var attribute = attributes[i]; if (string.Equals(attribute.Name, HrefAttributeName, StringComparison.OrdinalIgnoreCase)) @@ -440,8 +441,10 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers { builder.AppendHtml("["); var firstAdded = false; - // Perf: Avoid allocating enumerator - for (var i = 0; i < fallbackHrefs.Count; i++) + + // Perf: Avoid allocating enumerator and read interface .Count once rather than per iteration + var fallbackHrefsCount = fallbackHrefs.Count; + for (var i = 0; i < fallbackHrefsCount; i++) { if (firstAdded) { @@ -498,8 +501,9 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers var addHref = true; - // Perf: Avoid allocating enumerator - for (var i = 0; i < attributes.Count; i++) + // Perf: Avoid allocating enumerator and read interface .Count once rather than per iteration + var attributesCount = attributes.Count; + for (var i = 0; i < attributesCount; i++) { var attribute = attributes[i]; diff --git a/src/Mvc/Mvc.TagHelpers/src/ScriptTagHelper.cs b/src/Mvc/Mvc.TagHelpers/src/ScriptTagHelper.cs index 27cc753dd2..728e64caa0 100644 --- a/src/Mvc/Mvc.TagHelpers/src/ScriptTagHelper.cs +++ b/src/Mvc/Mvc.TagHelpers/src/ScriptTagHelper.cs @@ -331,8 +331,9 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers var addSrc = true; - // Perf: Avoid allocating enumerator - for (var i = 0; i < attributes.Count; i++) + // Perf: Avoid allocating enumerator and read interface .Count once rather than per iteration + var attributesCount = attributes.Count; + for (var i = 0; i < attributesCount; i++) { var attribute = attributes[i]; if (!attribute.Name.Equals(SrcAttributeName, StringComparison.OrdinalIgnoreCase)) @@ -434,8 +435,9 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers var addSrc = true; - // Perf: Avoid allocating enumerator - for (var i = 0; i < attributes.Count; i++) + // Perf: Avoid allocating enumerator and read interface .Count once rather than per iteration + var attributesCount = attributes.Count; + for (var i = 0; i < attributesCount; i++) { var attribute = attributes[i]; if (!attribute.Name.Equals(SrcAttributeName, StringComparison.OrdinalIgnoreCase)) diff --git a/src/Mvc/Mvc.TagHelpers/src/TagHelperOutputExtensions.cs b/src/Mvc/Mvc.TagHelpers/src/TagHelperOutputExtensions.cs index c79aa9fcfa..eb34746701 100644 --- a/src/Mvc/Mvc.TagHelpers/src/TagHelperOutputExtensions.cs +++ b/src/Mvc/Mvc.TagHelpers/src/TagHelperOutputExtensions.cs @@ -321,13 +321,14 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers TagHelperOutput tagHelperOutput, TagHelperContext context) { - var existingAttribute = context.AllAttributes[allAttributeIndex]; + var allAttributes = context.AllAttributes; + var existingAttribute = allAttributes[allAttributeIndex]; // Move backwards through context.AllAttributes from the provided index until we find a familiar attribute // in tagHelperOutput where we can insert the copied value after the familiar one. for (var i = allAttributeIndex - 1; i >= 0; i--) { - var previousName = context.AllAttributes[i].Name; + var previousName = allAttributes[i].Name; var index = IndexOfFirstMatch(previousName, tagHelperOutput.Attributes); if (index != -1) { @@ -336,11 +337,13 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers } } + // Read interface .Count once rather than per iteration + var allAttributesCount = allAttributes.Count; // Move forward through context.AllAttributes from the provided index until we find a familiar attribute in // tagHelperOutput where we can insert the copied value. - for (var i = allAttributeIndex + 1; i < context.AllAttributes.Count; i++) + for (var i = allAttributeIndex + 1; i < allAttributesCount; i++) { - var nextName = context.AllAttributes[i].Name; + var nextName = allAttributes[i].Name; var index = IndexOfFirstMatch(nextName, tagHelperOutput.Attributes); if (index != -1) { @@ -355,7 +358,9 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers private static int IndexOfFirstMatch(string name, TagHelperAttributeList attributes) { - for (var i = 0; i < attributes.Count; i++) + // Read interface .Count once rather than per iteration + var attributesCount = attributes.Count; + for (var i = 0; i < attributesCount; i++) { if (string.Equals(name, attributes[i].Name, StringComparison.OrdinalIgnoreCase)) { diff --git a/src/Razor/Razor.Runtime/ref/Microsoft.AspNetCore.Razor.Runtime.netcoreapp3.0.cs b/src/Razor/Razor.Runtime/ref/Microsoft.AspNetCore.Razor.Runtime.netcoreapp3.0.cs index 889c6b7449..9e2eb73d09 100644 --- a/src/Razor/Razor.Runtime/ref/Microsoft.AspNetCore.Razor.Runtime.netcoreapp3.0.cs +++ b/src/Razor/Razor.Runtime/ref/Microsoft.AspNetCore.Razor.Runtime.netcoreapp3.0.cs @@ -93,7 +93,6 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers public partial class TagHelperRunner { public TagHelperRunner() { } - [System.Diagnostics.DebuggerStepThroughAttribute] public System.Threading.Tasks.Task RunAsync(Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperExecutionContext executionContext) { throw null; } } public partial class TagHelperScopeManager diff --git a/src/Razor/Razor.Runtime/src/Runtime/TagHelpers/TagHelperRunner.cs b/src/Razor/Razor.Runtime/src/Runtime/TagHelpers/TagHelperRunner.cs index 89a8b0e2a0..76481341da 100644 --- a/src/Razor/Razor.Runtime/src/Runtime/TagHelpers/TagHelperRunner.cs +++ b/src/Razor/Razor.Runtime/src/Runtime/TagHelpers/TagHelperRunner.cs @@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers /// /// Resulting from processing all of the /// 's s. - public async Task RunAsync(TagHelperExecutionContext executionContext) + public Task RunAsync(TagHelperExecutionContext executionContext) { if (executionContext == null) { @@ -28,19 +28,40 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers } var tagHelperContext = executionContext.Context; + var tagHelpers = executionContext.TagHelpers; + OrderTagHelpers(tagHelpers); - OrderTagHelpers(executionContext.TagHelpers); - - for (var i = 0; i < executionContext.TagHelpers.Count; i++) + // Read interface .Count once rather than per iteration + var count = tagHelpers.Count; + for (var i = 0; i < count; i++) { - executionContext.TagHelpers[i].Init(tagHelperContext); + tagHelpers[i].Init(tagHelperContext); } var tagHelperOutput = executionContext.Output; - for (var i = 0; i < executionContext.TagHelpers.Count; i++) + for (var i = 0; i < count; i++) { - await executionContext.TagHelpers[i].ProcessAsync(tagHelperContext, tagHelperOutput); + var task = tagHelpers[i].ProcessAsync(tagHelperContext, tagHelperOutput); + if (!task.IsCompletedSuccessfully) + { + return Awaited(task, executionContext, i + 1, count); + } + } + + return Task.CompletedTask; + + static async Task Awaited(Task task, TagHelperExecutionContext executionContext, int i, int count) + { + await task; + + var tagHelpers = executionContext.TagHelpers; + var tagHelperOutput = executionContext.Output; + var tagHelperContext = executionContext.Context; + for (; i < count; i++) + { + await tagHelpers[i].ProcessAsync(tagHelperContext, tagHelperOutput); + } } } @@ -48,14 +69,16 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers { // Using bubble-sort here due to its simplicity. It'd be an extreme corner case to ever have more than 3 or // 4 tag helpers simultaneously. - ITagHelper temp = null; - for (var i = 0; i < tagHelpers.Count; i++) + + // Read interface .Count once rather than per iteration + var count = tagHelpers.Count; + for (var i = 0; i < count; i++) { - for (var j = i + 1; j < tagHelpers.Count; j++) + for (var j = i + 1; j < count; j++) { if (tagHelpers[j].Order < tagHelpers[i].Order) { - temp = tagHelpers[i]; + var temp = tagHelpers[i]; tagHelpers[i] = tagHelpers[j]; tagHelpers[j] = temp; } diff --git a/src/Razor/Razor/src/TagHelpers/ReadOnlyTagHelperAttributeList.cs b/src/Razor/Razor/src/TagHelpers/ReadOnlyTagHelperAttributeList.cs index f3d1569a59..2b0b55d1f1 100644 --- a/src/Razor/Razor/src/TagHelpers/ReadOnlyTagHelperAttributeList.cs +++ b/src/Razor/Razor/src/TagHelpers/ReadOnlyTagHelperAttributeList.cs @@ -54,11 +54,15 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers } // Perf: Avoid allocating enumerator - for (var i = 0; i < Items.Count; i++) + var items = Items; + // Read interface .Count once rather than per iteration + var itemsCount = items.Count; + for (var i = 0; i < itemsCount; i++) { - if (NameEquals(name, Items[i])) + var attribute = items[i]; + if (NameEquals(name, attribute)) { - return Items[i]; + return attribute; } } @@ -126,16 +130,20 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers // Perf: Avoid allocating enumerator List matchedAttributes = null; - for (var i = 0; i < Items.Count; i++) + var items = Items; + // Read interface .Count once rather than per iteration + var itemsCount = items.Count; + for (var i = 0; i < itemsCount; i++) { - if (NameEquals(name, Items[i])) + var attribute = items[i]; + if (NameEquals(name, attribute)) { if (matchedAttributes == null) { matchedAttributes = new List(); } - matchedAttributes.Add(Items[i]); + matchedAttributes.Add(attribute); } } attributes = matchedAttributes ?? EmptyList; @@ -158,9 +166,12 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers throw new ArgumentNullException(nameof(name)); } - for (var i = 0; i < Items.Count; i++) + var items = Items; + // Read interface .Count once rather than per iteration + var itemsCount = items.Count; + for (var i = 0; i < itemsCount; i++) { - if (NameEquals(name, Items[i])) + if (NameEquals(name, items[i])) { return i; } diff --git a/src/Razor/Razor/src/TagHelpers/TagHelperOutput.cs b/src/Razor/Razor/src/TagHelpers/TagHelperOutput.cs index 51a7f8392b..64e4bf30e3 100644 --- a/src/Razor/Razor/src/TagHelpers/TagHelperOutput.cs +++ b/src/Razor/Razor/src/TagHelpers/TagHelperOutput.cs @@ -305,8 +305,9 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers destination.AppendHtml("<"); destination.AppendHtml(TagName); - // Perf: Avoid allocating enumerator - for (var i = 0; i < Attributes.Count; i++) + // Perf: Avoid allocating enumerator, cache .Count as it goes via interface + var count = Attributes.Count; + for (var i = 0; i < count; i++) { var attribute = Attributes[i]; destination.AppendHtml(" "); @@ -356,8 +357,9 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers destination.AppendHtml("<"); destination.AppendHtml(TagName); - // Perf: Avoid allocating enumerator - for (var i = 0; i < Attributes.Count; i++) + // Perf: Avoid allocating enumerator, cache .Count as it goes via interface + var count = Attributes.Count; + for (var i = 0; i < count; i++) { var attribute = Attributes[i]; destination.AppendHtml(" "); @@ -416,8 +418,9 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers writer.Write("<"); writer.Write(TagName); - // Perf: Avoid allocating enumerator - for (var i = 0; i < Attributes.Count; i++) + // Perf: Avoid allocating enumerator, cache .Count as it goes via interface + var count = Attributes.Count; + for (var i = 0; i < count; i++) { var attribute = Attributes[i]; writer.Write(" ");