From cfd58f17476df9bd296a3e50f519f570e0952877 Mon Sep 17 00:00:00 2001 From: "N. Taylor Mullen" Date: Tue, 3 May 2016 14:08:51 -0700 Subject: [PATCH] React to aspnet/Razor#705 - Updated `ScriptTagHelper` and `LinkTagHelper` to maintain their quotes on generated script tags. - Removed `TagHelperOutputExtensions`. It's no longer needed. - We no longer string replace quotes directly. We rely on encoding and the initial representation of an attribute to ensure quotes don't break generated attributes. - Updated tests. --- .../MvcRazorHost.cs | 2 - .../RazorPage.cs | 27 ++-- .../LinkTagHelper.cs | 12 +- .../ScriptTagHelper.cs | 124 +++++++----------- .../TagHelperContentExtensions.cs | 75 ----------- .../TagHelperOutputExtensions.cs | 10 +- ...ite.HtmlGeneration_Home.Index.Encoded.html | 2 +- ...tionWebSite.HtmlGeneration_Home.Index.html | 2 +- ...Site.HtmlGeneration_Home.Link.Encoded.html | 4 +- ...ationWebSite.HtmlGeneration_Home.Link.html | 4 +- ...te.HtmlGeneration_Home.Script.Encoded.html | 20 +-- ...ionWebSite.HtmlGeneration_Home.Script.html | 20 +-- ...DuplicateAntiforgeryTokenRegistration.html | 3 +- .../Runtime/ModelExpressionTagHelper.cs | 4 +- .../RazorPageTest.cs | 16 +-- .../TagHelpers/UrlResolutionTagHelperTest.cs | 10 +- .../LinkTagHelperTest.cs | 26 ++-- .../ScriptTagHelperTest.cs | 44 +++---- .../TagHelperOutputExtensionsTest.cs | 14 +- 19 files changed, 160 insertions(+), 259 deletions(-) delete mode 100644 src/Microsoft.AspNetCore.Mvc.TagHelpers/TagHelperContentExtensions.cs diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Host/MvcRazorHost.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Host/MvcRazorHost.cs index 4c97ecbea3..bb537aaa51 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Host/MvcRazorHost.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Host/MvcRazorHost.cs @@ -88,8 +88,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor ExecutionContextAddTagHelperAttributeMethodName = nameof(TagHelperExecutionContext.AddTagHelperAttribute), ExecutionContextAddHtmlAttributeMethodName = nameof(TagHelperExecutionContext.AddHtmlAttribute), - ExecutionContextAddMinimizedHtmlAttributeMethodName = - nameof(TagHelperExecutionContext.AddMinimizedHtmlAttribute), ExecutionContextOutputPropertyName = nameof(TagHelperExecutionContext.Output), RunnerTypeName = typeof(TagHelperRunner).FullName, diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/RazorPage.cs b/src/Microsoft.AspNetCore.Mvc.Razor/RazorPage.cs index 0e5581ee75..3fe6054600 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/RazorPage.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/RazorPage.cs @@ -238,7 +238,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor /// /// All writes to the or after calling this method will /// be buffered until is called. - /// The content will be buffered using a shared within this + /// The content will be buffered using a shared within this /// Nesting of and method calls /// is not supported. /// @@ -267,7 +267,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor /// /// The content buffered by the shared of this . /// - /// This method assumes that there will be no nesting of + /// This method assumes that there will be no nesting of /// and method calls. /// public string EndWriteTagHelperAttribute() @@ -572,9 +572,14 @@ namespace Microsoft.AspNetCore.Mvc.Razor public void BeginAddHtmlAttributeValues( TagHelperExecutionContext executionContext, string attributeName, - int attributeValuesCount) + int attributeValuesCount, + HtmlAttributeValueStyle attributeValueStyle) { - _tagHelperAttributeInfo = new TagHelperAttributeInfo(executionContext, attributeName, attributeValuesCount); + _tagHelperAttributeInfo = new TagHelperAttributeInfo( + executionContext, + attributeName, + attributeValuesCount, + attributeValueStyle); } public void AddHtmlAttributeValue( @@ -596,7 +601,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor // attribute was removed from TagHelperOutput.Attributes). _tagHelperAttributeInfo.ExecutionContext.AddTagHelperAttribute( _tagHelperAttributeInfo.Name, - value?.ToString() ?? string.Empty); + value?.ToString() ?? string.Empty, + _tagHelperAttributeInfo.AttributeValueStyle); _tagHelperAttributeInfo.Suppressed = true; return; } @@ -604,7 +610,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor { _tagHelperAttributeInfo.ExecutionContext.AddHtmlAttribute( _tagHelperAttributeInfo.Name, - _tagHelperAttributeInfo.Name); + _tagHelperAttributeInfo.Name, + _tagHelperAttributeInfo.AttributeValueStyle); _tagHelperAttributeInfo.Suppressed = true; return; } @@ -637,7 +644,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor var content = _valueBuffer == null ? HtmlString.Empty : new HtmlString(_valueBuffer.ToString()); _valueBuffer?.GetStringBuilder().Clear(); - executionContext.AddHtmlAttribute(_tagHelperAttributeInfo.Name, content); + executionContext.AddHtmlAttribute(_tagHelperAttributeInfo.Name, content, _tagHelperAttributeInfo.AttributeValueStyle); } } @@ -1081,11 +1088,13 @@ namespace Microsoft.AspNetCore.Mvc.Razor public TagHelperAttributeInfo( TagHelperExecutionContext tagHelperExecutionContext, string name, - int attributeValuesCount) + int attributeValuesCount, + HtmlAttributeValueStyle attributeValueStyle) { ExecutionContext = tagHelperExecutionContext; Name = name; AttributeValuesCount = attributeValuesCount; + AttributeValueStyle = attributeValueStyle; Suppressed = false; } @@ -1096,6 +1105,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor public int AttributeValuesCount { get; } + public HtmlAttributeValueStyle AttributeValueStyle { get; } + public bool Suppressed { get; set; } } diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/LinkTagHelper.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/LinkTagHelper.cs index 82a4bd40b3..2abec8a630 100644 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/LinkTagHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/LinkTagHelper.cs @@ -422,7 +422,8 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers } else { - AppendAttribute(attribute.Name, attribute.Value, builder); + attribute.CopyTo(builder); + builder.AppendHtml(" "); } } @@ -441,15 +442,10 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers hrefValue = _fileVersionProvider.AddFileVersionToPath(hrefValue); } - AppendAttribute(hrefName, hrefValue, builder); - } - - private void AppendAttribute(string key, object value, TagHelperContent builder) - { builder - .AppendHtml(key) + .AppendHtml(hrefName) .AppendHtml("=\"") - .Append(HtmlEncoder, value) + .Append(hrefValue) .AppendHtml("\" "); } diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/ScriptTagHelper.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/ScriptTagHelper.cs index e821afd48a..afec945db1 100644 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/ScriptTagHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/ScriptTagHelper.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics; using System.IO; +using System.Text; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Html; @@ -235,7 +236,8 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers var index = output.Attributes.IndexOfName(SrcAttributeName); output.Attributes[index] = new TagHelperAttribute( SrcAttributeName, - _fileVersionProvider.AddFileVersionToPath(Src)); + _fileVersionProvider.AddFileVersionToPath(Src), + output.Attributes[index].ValueStyle); } } @@ -306,7 +308,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers // Fallback "src" values come from bound attributes and globbing. Must always be non-null. Debug.Assert(src != null); - builder.AppendHtml("<\\/script>"); + StringWriter.Write(">"); } + var stringBuilder = StringWriter.GetStringBuilder(); + var scriptTags = stringBuilder.ToString(); + stringBuilder.Clear(); + var encodedScriptTags = JavaScriptEncoder.Encode(scriptTags); + builder.AppendHtml(encodedScriptTags); + builder.AppendHtml("\"));"); } } - private string GetAttributeValue(object value) - { - string stringValue; - var htmlString = value as HtmlString; - if (htmlString != null) - { - // Value likely came from an HTML context in the .cshtml file but may still contain double quotes - // since attribute could have been enclosed in single quotes. - stringValue = htmlString.Value; - stringValue = stringValue.Replace("\"", """); - } - else - { - var writer = StringWriter; - RazorPage.WriteTo(writer, HtmlEncoder, value); - - // Value is now correctly HTML-encoded but may still contain double quotes since attribute could - // have been enclosed in single quotes and portions that were HtmlStrings are not re-encoded. - var builder = writer.GetStringBuilder(); - builder.Replace("\"", """); - - stringValue = builder.ToString(); - builder.Clear(); - } - - return stringValue; - } - - private void AppendEncodedVersionedSrc( - string srcName, - string srcValue, - TagHelperContent builder, - bool generateForDocumentWrite) + private string GetVersionedSrc(string srcValue) { if (AppendVersion == true) { srcValue = _fileVersionProvider.AddFileVersionToPath(srcValue); } - if (generateForDocumentWrite) - { - // srcValue comes from a C# context and globbing. Must HTML-encode it to ensure the - // written "); } - private void AppendAttribute(TagHelperContent content, string key, object value, bool escapeQuotes) - { - content - .AppendHtml(" ") - .AppendHtml(key); - if (escapeQuotes) - { - // Passed only JavaScript-encoded strings in this case. Do not perform HTML-encoding as well. - content - .AppendHtml("=\\\"") - .AppendHtml((string)value) - .AppendHtml("\\\""); - } - else - { - // HTML-encode the given value if necessary. - content - .AppendHtml("=\"") - .Append(HtmlEncoder, value) - .AppendHtml("\""); - } - } - private enum Mode { /// diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/TagHelperContentExtensions.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/TagHelperContentExtensions.cs deleted file mode 100644 index cd6740bd8e..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/TagHelperContentExtensions.cs +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.IO; -using System.Text.Encodings.Web; -using Microsoft.AspNetCore.Html; -using Microsoft.AspNetCore.Mvc.Razor; -using Microsoft.AspNetCore.Mvc.Rendering; -using Microsoft.AspNetCore.Razor.TagHelpers; - -namespace Microsoft.AspNetCore.Mvc.TagHelpers -{ - /// - /// Extension methods for . - /// - public static class TagHelperContentExtensions - { - /// - /// Writes the specified with HTML encoding to given . - /// - /// The to write to. - /// The to use when encoding . - /// The to write. - /// after the write operation has completed. - /// - /// s of type are written using - /// . - /// For all other types, the encoded result of - /// is written to the . - /// - public static TagHelperContent Append(this TagHelperContent content, HtmlEncoder encoder, object value) - { - if (content == null) - { - throw new ArgumentNullException(nameof(content)); - } - - if (encoder == null) - { - throw new ArgumentNullException(nameof(encoder)); - } - - if (value == null) - { - // No real action but touch content to ensure IsModified is true. - content.Append((string)null); - return content; - } - - string stringValue; - var htmlString = value as HtmlString; - if (htmlString != null) - { - // No need for a StringWriter in this case. - stringValue = htmlString.ToString(); - } - else - { - using (var stringWriter = new StringWriter()) - { - RazorPage.WriteTo(stringWriter, encoder, value); - stringValue = stringWriter.ToString(); - } - } - - // In this case the text likely came directly from the Razor source. Since the original string is - // an attribute value that may have been quoted with single quotes, must handle any double quotes - // in the value. Writing the value out surrounded by double quotes. - content.AppendHtml(stringValue.Replace("\"", """)); - - return content; - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/TagHelperOutputExtensions.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/TagHelperOutputExtensions.cs index dcd917675b..89168a6497 100644 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/TagHelperOutputExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/TagHelperOutputExtensions.cs @@ -158,10 +158,6 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers TagHelperContext context) { var existingAttribute = context.AllAttributes[allAttributeIndex]; - var copiedAttribute = new TagHelperAttribute( - existingAttribute.Name, - existingAttribute.Value, - existingAttribute.Minimized); // 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. @@ -171,7 +167,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers var index = IndexOfFirstMatch(previousName, tagHelperOutput.Attributes); if (index != -1) { - tagHelperOutput.Attributes.Insert(index + 1, copiedAttribute); + tagHelperOutput.Attributes.Insert(index + 1, existingAttribute); return; } } @@ -184,13 +180,13 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers var index = IndexOfFirstMatch(nextName, tagHelperOutput.Attributes); if (index != -1) { - tagHelperOutput.Attributes.Insert(index, copiedAttribute); + tagHelperOutput.Attributes.Insert(index, existingAttribute); return; } } // Couldn't determine the attribute's location, add it to the end. - tagHelperOutput.Attributes.Add(copiedAttribute); + tagHelperOutput.Attributes.Add(existingAttribute); } private static int IndexOfFirstMatch(string name, TagHelperAttributeList attributes) diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Index.Encoded.html b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Index.Encoded.html index 5d696d4c25..39438a1bd7 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Index.Encoded.html +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Index.Encoded.html @@ -4,7 +4,7 @@ Product Index
HtmlGenerationWebSite Index diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Index.html b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Index.html index ea526da9a7..e80ec4969e 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Index.html +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Index.html @@ -4,7 +4,7 @@ Product Index
HtmlGenerationWebSite Index diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Link.Encoded.html b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Link.Encoded.html index c87ec590a7..a5dac00b74 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Link.Encoded.html +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Link.Encoded.html @@ -14,7 +14,7 @@ - + @@ -38,7 +38,7 @@ - + diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Link.html b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Link.html index 0384f385c8..370692a400 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Link.html +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Link.html @@ -14,7 +14,7 @@ - + @@ -38,7 +38,7 @@ - + diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Script.Encoded.html b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Script.Encoded.html index f696105479..680a5729a0 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Script.Encoded.html +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Script.Encoded.html @@ -13,27 +13,27 @@ - +]]")); - - +]]")); - +]]")); - +]]")); - +]]")); - +]]")); - +]]")); - +]]")); - +]]")); - + - - + - + - + - + - + - + - + - + " + - ""; var mixed = new DefaultTagHelperContent(); mixed.Append("HTML encoded"); @@ -636,18 +636,18 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers attributes: new TagHelperAttributeList { { "asp-src-include", "**/*.js" }, - { "encoded", new HtmlString("contains \"quotes\"") }, + { new TagHelperAttribute("encoded", new HtmlString("contains \"quotes\""), HtmlAttributeValueStyle.SingleQuotes) }, { "literal", "all HTML encoded" }, - { "mixed", mixed }, + { new TagHelperAttribute("mixed", mixed, HtmlAttributeValueStyle.SingleQuotes) }, { "src", "/js/site.js" }, }); var output = MakeTagHelperOutput( "script", attributes: new TagHelperAttributeList { - { "encoded", new HtmlString("contains \"quotes\"") }, + { new TagHelperAttribute("encoded", new HtmlString("contains \"quotes\""), HtmlAttributeValueStyle.SingleQuotes) }, { "literal", "all HTML encoded"}, - { "mixed", mixed}, + { new TagHelperAttribute("mixed", mixed, HtmlAttributeValueStyle.SingleQuotes) }, }); var hostingEnvironment = MakeHostingEnvironment(); var viewContext = MakeViewContext(); @@ -786,9 +786,9 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers // Assert Assert.Equal("script", output.TagName); Assert.Equal("/js/site.js?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk", output.Attributes["src"].Value); - Assert.Equal(Environment.NewLine + "", output.PostElement.GetContent()); + Assert.Equal(Environment.NewLine + "]]\"));", output.PostElement.GetContent()); } [Fact] @@ -796,16 +796,14 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers { // Arrange var expectedContent = - "" + Environment.NewLine + - ""; + "]]\"));"; var mixed = new DefaultTagHelperContent(); mixed.Append("HTML encoded"); mixed.AppendHtml(" and contains \"quotes\""); @@ -815,18 +813,18 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers { "asp-append-version", "true" }, { "asp-fallback-src-include", "fallback.js" }, { "asp-fallback-test", "isavailable()" }, - { "encoded", new HtmlString("contains \"quotes\"") }, + { new TagHelperAttribute("encoded", new HtmlString("contains \"quotes\""), HtmlAttributeValueStyle.SingleQuotes) }, { "literal", "all HTML encoded" }, - { "mixed", mixed }, + { new TagHelperAttribute("mixed", mixed, HtmlAttributeValueStyle.SingleQuotes) }, { "src", "/js/site.js" }, }); var output = MakeTagHelperOutput( "script", attributes: new TagHelperAttributeList { - { "encoded", new HtmlString("contains \"quotes\"") }, + { new TagHelperAttribute("encoded", new HtmlString("contains \"quotes\""), HtmlAttributeValueStyle.SingleQuotes) }, { "literal", "all HTML encoded" }, - { "mixed", mixed }, + { new TagHelperAttribute("mixed", mixed, HtmlAttributeValueStyle.SingleQuotes) }, }); var hostingEnvironment = MakeHostingEnvironment(); var viewContext = MakeViewContext(); diff --git a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/TagHelperOutputExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/TagHelperOutputExtensionsTest.cs index 0784d3f29c..eea6e30df0 100644 --- a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/TagHelperOutputExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/TagHelperOutputExtensionsTest.cs @@ -979,13 +979,21 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers // Normal comparer (TagHelperAttribute.Equals()) doesn't care about the Name case, in tests we do. return attributeX != null && string.Equals(attributeX.Name, attributeY.Name, StringComparison.Ordinal) && - attributeX.Minimized == attributeY.Minimized && - (attributeX.Minimized || Equals(attributeX.Value, attributeY.Value)); + attributeX.ValueStyle == attributeY.ValueStyle && + (attributeX.ValueStyle == HtmlAttributeValueStyle.Minimized || Equals(attributeX.Value, attributeY.Value)); } public int GetHashCode(TagHelperAttribute attribute) { - return attribute.GetHashCode(); + // Manually combine hash codes here. We can't reference HashCodeCombiner because we have internals visible + // from Mvc.Core and Mvc.TagHelpers; both of which reference HashCodeCombiner. + var baseHashCode = 0x1505L; + var attributeHashCode = attribute.GetHashCode(); + var combinedHash = ((baseHashCode << 5) + baseHashCode) ^ attributeHashCode; + var nameHashCode = StringComparer.Ordinal.GetHashCode(attribute.Name); + combinedHash = ((combinedHash << 5) + combinedHash) ^ nameHashCode; + + return combinedHash.GetHashCode(); } } }