From 69d8e52bf924ce85350c012db73c61ed92859bf0 Mon Sep 17 00:00:00 2001 From: Doug Bunting Date: Sat, 27 Jun 2015 16:35:52 -0700 Subject: [PATCH] Add support for get-only indexer properties - #399 - move invalid `HtmlAttributeNameAttribute.Name` checking to `TagHelperDescriptorFactory` - add a few new error cases - but does not cover all the new error cases e.g. `[HtmlAttributeName(...)]` on a get-only `int` property nit: - `resx` target removed some older resources from `RazorResources.Designer.cs` --- .../Properties/Resources.Designer.cs | 74 ++++++++- .../Resources.resx | 14 +- .../TagHelpers/HtmlAttributeNameAttribute.cs | 35 +++- .../TagHelpers/TagHelperDescriptorFactory.cs | 150 +++++++++++++----- .../Properties/RazorResources.Designer.cs | 16 -- .../TagHelperDescriptorFactoryTest.cs | 62 +++++++- 6 files changed, 277 insertions(+), 74 deletions(-) diff --git a/src/Microsoft.AspNet.Razor.Runtime/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Razor.Runtime/Properties/Resources.Designer.cs index 6416da158b..5e61f5058a 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/Properties/Resources.Designer.cs @@ -234,6 +234,22 @@ namespace Microsoft.AspNet.Razor.Runtime return GetString("TagHelperDescriptorFactory_Tag"); } + /// + /// Invalid tag helper bound property '{0}.{1}'. An '{2}' must not be associated with a property with no public setter unless its type implements '{3}'. + /// + internal static string TagHelperDescriptorFactory_InvalidAttributeNameAttribute + { + get { return GetString("TagHelperDescriptorFactory_InvalidAttributeNameAttribute"); } + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. An '{2}' must not be associated with a property with no public setter unless its type implements '{3}'. + /// + internal static string FormatTagHelperDescriptorFactory_InvalidAttributeNameAttribute(object p0, object p1, object p2, object p3) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidAttributeNameAttribute"), p0, p1, p2, p3); + } + /// /// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with {2} '{3}' because {2} contains a '{4}' character. /// @@ -283,19 +299,67 @@ namespace Microsoft.AspNet.Razor.Runtime } /// - /// Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must be null unless property type implements '{4}'. + /// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with a null or empty name. /// - internal static string TagHelperDescriptorFactory_InvalidAttributePrefix + internal static string TagHelperDescriptorFactory_InvalidAttributeNameNullOrEmpty { - get { return GetString("TagHelperDescriptorFactory_InvalidAttributePrefix"); } + get { return GetString("TagHelperDescriptorFactory_InvalidAttributeNameNullOrEmpty"); } + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with a null or empty name. + /// + internal static string FormatTagHelperDescriptorFactory_InvalidAttributeNameNullOrEmpty(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidAttributeNameNullOrEmpty"), p0, p1); + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must be null or empty if property has no public setter. + /// + internal static string TagHelperDescriptorFactory_InvalidAttributeNameNotNullOrEmpty + { + get { return GetString("TagHelperDescriptorFactory_InvalidAttributeNameNotNullOrEmpty"); } + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must be null or empty if property has no public setter. + /// + internal static string FormatTagHelperDescriptorFactory_InvalidAttributeNameNotNullOrEmpty(object p0, object p1, object p2, object p3) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidAttributeNameNotNullOrEmpty"), p0, p1, p2, p3); + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must not be null if property has no public setter and its type implements '{4}'. + /// + internal static string TagHelperDescriptorFactory_InvalidAttributePrefixNull + { + get { return GetString("TagHelperDescriptorFactory_InvalidAttributePrefixNull"); } + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must not be null if property has no public setter and its type implements '{4}'. + /// + internal static string FormatTagHelperDescriptorFactory_InvalidAttributePrefixNull(object p0, object p1, object p2, object p3, object p4) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidAttributePrefixNull"), p0, p1, p2, p3, p4); } /// /// Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must be null unless property type implements '{4}'. /// - internal static string FormatTagHelperDescriptorFactory_InvalidAttributePrefix(object p0, object p1, object p2, object p3, object p4) + internal static string TagHelperDescriptorFactory_InvalidAttributePrefixNotNull { - return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidAttributePrefix"), p0, p1, p2, p3, p4); + get { return GetString("TagHelperDescriptorFactory_InvalidAttributePrefixNotNull"); } + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must be null unless property type implements '{4}'. + /// + internal static string FormatTagHelperDescriptorFactory_InvalidAttributePrefixNotNull(object p0, object p1, object p2, object p3, object p4) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidAttributePrefixNotNull"), p0, p1, p2, p3, p4); } /// diff --git a/src/Microsoft.AspNet.Razor.Runtime/Resources.resx b/src/Microsoft.AspNet.Razor.Runtime/Resources.resx index 3d845ce34e..c59fd6e955 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/Resources.resx +++ b/src/Microsoft.AspNet.Razor.Runtime/Resources.resx @@ -159,6 +159,9 @@ Tag + + Invalid tag helper bound property '{0}.{1}'. An '{2}' must not be associated with a property with no public setter unless its type implements '{3}'. + Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with {2} '{3}' because {2} contains a '{4}' character. @@ -168,7 +171,16 @@ Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with a whitespace {2}. - + + Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with a null or empty name. + + + Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must be null or empty if property has no public setter. + + + Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must not be null if property has no public setter and its type implements '{4}'. + + Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must be null unless property type implements '{4}'. diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/HtmlAttributeNameAttribute.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/HtmlAttributeNameAttribute.cs index f22fb4b40f..07e0649a6b 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/HtmlAttributeNameAttribute.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/HtmlAttributeNameAttribute.cs @@ -13,23 +13,41 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers { private string _dictionaryAttributePrefix; + /// + /// Instantiates a new instance of the class with + /// equal to null. + /// + /// + /// Associated property must not have a public setter and must be compatible with + /// where TKey is + /// . + /// + public HtmlAttributeNameAttribute() + { + } + /// /// Instantiates a new instance of the class. /// - /// HTML attribute name for the associated property. + /// + /// HTML attribute name for the associated property. Must be null or empty if associated property does + /// not have a public setter and is compatible with + /// where TKey is + /// . Otherwise must not be null or empty. + /// public HtmlAttributeNameAttribute(string name) { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(name)); - } - Name = name; } /// /// HTML attribute name of the associated property. /// + /// + /// null or empty if and only if associated property does not have a public setter and is compatible + /// with where TKey is + /// . + /// public string Name { get; } /// @@ -42,9 +60,14 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers /// . /// /// + /// /// If associated property is compatible with /// , default value is Name + "-". + /// must not be null or empty in this case. + /// + /// /// Otherwise default value is null. + /// /// public string DictionaryAttributePrefix { diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorFactory.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorFactory.cs index e6ce710d9e..df6e13d3a9 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorFactory.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorFactory.cs @@ -256,45 +256,72 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers bool designTime, ErrorSink errorSink) { - var accessibleProperties = type.GetRuntimeProperties().Where(IsAccessibleProperty); var attributeDescriptors = new List(); // Keep indexer descriptors separate to avoid sorting the combined list later. var indexerDescriptors = new List(); + var accessibleProperties = type.GetRuntimeProperties().Where(IsAccessibleProperty); foreach (var property in accessibleProperties) { var attributeNameAttribute = property.GetCustomAttribute(inherit: false); - var descriptor = ToAttributeDescriptor(property, attributeNameAttribute, designTime); - if (ValidateTagHelperAttributeDescriptor(descriptor, type, errorSink)) + var hasExplicitName = + attributeNameAttribute != null && !string.IsNullOrEmpty(attributeNameAttribute.Name); + var attributeName = hasExplicitName ? attributeNameAttribute.Name : ToHtmlCase(property.Name); + + TagHelperAttributeDescriptor mainDescriptor = null; + if (property.SetMethod?.IsPublic == true) { - bool isInvalid; - var indexerDescriptor = ToIndexerAttributeDescriptor( - property, - attributeNameAttribute, - parentType: type, - errorSink: errorSink, - defaultPrefix: descriptor.Name + "-", - designTime: designTime, - isInvalid: out isInvalid); - - if (indexerDescriptor != null && - !ValidateTagHelperAttributeDescriptor(indexerDescriptor, type, errorSink)) + mainDescriptor = ToAttributeDescriptor(property, attributeName, designTime); + if (!ValidateTagHelperAttributeDescriptor(mainDescriptor, type, errorSink)) { - isInvalid = true; - } - - if (isInvalid) - { - // HtmlAttributeNameAttribute was not valid. Ignore this property completely. + // HtmlAttributeNameAttribute.Name is invalid. Ignore this property completely. continue; } + } + else if (hasExplicitName) + { + // Specified HtmlAttributeNameAttribute.Name though property has no public setter. + errorSink.OnError( + SourceLocation.Zero, + Resources.FormatTagHelperDescriptorFactory_InvalidAttributeNameNotNullOrEmpty( + type.FullName, + property.Name, + typeof(HtmlAttributeNameAttribute).FullName, + nameof(HtmlAttributeNameAttribute.Name))); + continue; + } - attributeDescriptors.Add(descriptor); - if (indexerDescriptor != null) - { - indexerDescriptors.Add(indexerDescriptor); - } + bool isInvalid; + var indexerDescriptor = ToIndexerAttributeDescriptor( + property, + attributeNameAttribute, + parentType: type, + errorSink: errorSink, + defaultPrefix: attributeName + "-", + designTime: designTime, + isInvalid: out isInvalid); + if (indexerDescriptor != null && + !ValidateTagHelperAttributeDescriptor(indexerDescriptor, type, errorSink)) + { + isInvalid = true; + } + + if (isInvalid) + { + // The property type or HtmlAttributeNameAttribute.DictionaryAttributePrefix (or perhaps the + // HTML-casing of the property name) is invalid. Ignore this property completely. + continue; + } + + if (mainDescriptor != null) + { + attributeDescriptors.Add(mainDescriptor); + } + + if (indexerDescriptor != null) + { + indexerDescriptors.Add(indexerDescriptor); } } @@ -309,9 +336,26 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers Type parentType, ErrorSink errorSink) { - var nameOrPrefix = attributeDescriptor.IsIndexer ? - Resources.TagHelperDescriptorFactory_Prefix : - Resources.TagHelperDescriptorFactory_Name; + string nameOrPrefix; + if (attributeDescriptor.IsIndexer) + { + nameOrPrefix = Resources.TagHelperDescriptorFactory_Prefix; + } + else if (string.IsNullOrEmpty(attributeDescriptor.Name)) + { + errorSink.OnError( + SourceLocation.Zero, + Resources.FormatTagHelperDescriptorFactory_InvalidAttributeNameNullOrEmpty( + parentType.FullName, + attributeDescriptor.PropertyName)); + + return false; + } + else + { + nameOrPrefix = Resources.TagHelperDescriptorFactory_Name; + } + return ValidateTagHelperAttributeNameOrPrefix( attributeDescriptor.Name, parentType, @@ -329,8 +373,9 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers { if (string.IsNullOrEmpty(attributeNameOrPrefix)) { - // HtmlAttributeNameAttribute validates Name is non-null and non-empty. Both are valid for - // DictionaryAttributePrefix. (Empty DictionaryAttributePrefix is a corner case which would bind every + // ValidateTagHelperAttributeDescriptor validates Name is non-null and non-empty. The empty string is + // valid for DictionaryAttributePrefix and null is impossible at this point because it means "don't + // create a descriptor". (Empty DictionaryAttributePrefix is a corner case which would bind every // attribute of a target element. Likely not particularly useful but unclear what minimum length // should be required and what scenarios a minimum length would break.) return true; @@ -388,13 +433,9 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers private static TagHelperAttributeDescriptor ToAttributeDescriptor( PropertyInfo property, - HtmlAttributeNameAttribute attributeNameAttribute, + string attributeName, bool designTime) { - var attributeName = attributeNameAttribute != null ? - attributeNameAttribute.Name : - ToHtmlCase(property.Name); - return ToAttributeDescriptor( property, attributeName, @@ -413,6 +454,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers out bool isInvalid) { isInvalid = false; + var hasPublicSetter = property.SetMethod?.IsPublic == true; var dictionaryTypeArguments = ClosedGenericMatcher.ExtractGenericInterface( property.PropertyType, typeof(IDictionary<,>)) @@ -426,13 +468,44 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers isInvalid = true; errorSink.OnError( SourceLocation.Zero, - Resources.FormatTagHelperDescriptorFactory_InvalidAttributePrefix( + Resources.FormatTagHelperDescriptorFactory_InvalidAttributePrefixNotNull( parentType.FullName, property.Name, nameof(HtmlAttributeNameAttribute), nameof(HtmlAttributeNameAttribute.DictionaryAttributePrefix), "IDictionary")); } + else if (attributeNameAttribute != null && !hasPublicSetter) + { + // Associated an HtmlAttributeNameAttribute with a non-dictionary property that lacks a public + // setter. + isInvalid = true; + errorSink.OnError( + SourceLocation.Zero, + Resources.FormatTagHelperDescriptorFactory_InvalidAttributeNameAttribute( + parentType.FullName, + property.Name, + nameof(HtmlAttributeNameAttribute), + "IDictionary")); + } + + return null; + } + else if (!hasPublicSetter && + attributeNameAttribute != null && + !attributeNameAttribute.DictionaryAttributePrefixSet) + { + // Must set DictionaryAttributePrefix when using HtmlAttributeNameAttribute with a dictionary property + // that lacks a public setter. + isInvalid = true; + errorSink.OnError( + SourceLocation.Zero, + Resources.FormatTagHelperDescriptorFactory_InvalidAttributePrefixNull( + parentType.FullName, + property.Name, + nameof(HtmlAttributeNameAttribute), + nameof(HtmlAttributeNameAttribute.DictionaryAttributePrefix), + "IDictionary")); return null; } @@ -482,9 +555,8 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers private static bool IsAccessibleProperty(PropertyInfo property) { - // Accessible properties are those with public getters and setters and without [HtmlAttributeNotBound]. + // Accessible properties are those with public getters and without [HtmlAttributeNotBound]. return property.GetMethod?.IsPublic == true && - property.SetMethod?.IsPublic == true && property.GetCustomAttribute(inherit: false) == null; } diff --git a/src/Microsoft.AspNet.Razor/Properties/RazorResources.Designer.cs b/src/Microsoft.AspNet.Razor/Properties/RazorResources.Designer.cs index 1345e2b627..b717e43638 100644 --- a/src/Microsoft.AspNet.Razor/Properties/RazorResources.Designer.cs +++ b/src/Microsoft.AspNet.Razor/Properties/RazorResources.Designer.cs @@ -1286,22 +1286,6 @@ namespace Microsoft.AspNet.Razor return string.Format(CultureInfo.CurrentCulture, GetString("TagHelpers_CannotHaveCSharpInTagDeclaration"), p0); } - /// - /// A TagHelperChunkGenerator must only be used with TagHelperBlocks. - /// - internal static string TagHelpers_TagHelperCodeGeneartorMustBeAssociatedWithATagHelperBlock - { - get { return GetString("TagHelpers_TagHelperCodeGeneartorMustBeAssociatedWithATagHelperBlock"); } - } - - /// - /// A TagHelperChunkGenerator must only be used with TagHelperBlocks. - /// - internal static string FormatTagHelpers_TagHelperCodeGeneartorMustBeAssociatedWithATagHelperBlock() - { - return GetString("TagHelpers_TagHelperCodeGeneartorMustBeAssociatedWithATagHelperBlock"); - } - /// /// Directive '{0}' must have a value. /// diff --git a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs index 9d66a23d0d..b4c2170f18 100644 --- a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs +++ b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs @@ -958,7 +958,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers isStringProperty: false, designTimeDescriptor: null), new TagHelperAttributeDescriptor( - name: "valid-prefix", + name: "valid-name-", propertyName: nameof(SingleValidHtmlAttributePrefix.DictionaryProperty), typeName: typeof(string).FullName, isIndexer: true, @@ -1048,6 +1048,20 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers isIndexer: true, isStringProperty: false, designTimeDescriptor: null), + new TagHelperAttributeDescriptor( + name: "get-only-dictionary-property-", + propertyName: nameof(MultipleValidHtmlAttributePrefix.GetOnlyDictionaryProperty), + typeName: typeof(int).FullName, + isIndexer: true, + isStringProperty: false, + designTimeDescriptor: null), + new TagHelperAttributeDescriptor( + name: "valid-prefix6", + propertyName: nameof(MultipleValidHtmlAttributePrefix.GetOnlyDictionaryPropertyWithAttributePrefix), + typeName: typeof(string).FullName, + isIndexer: true, + isStringProperty: true, + designTimeDescriptor: null), }, new string[0] }, @@ -1087,6 +1101,14 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers onError( typeof(MultipleInvalidHtmlAttributePrefix).FullName, nameof(MultipleInvalidHtmlAttributePrefix.DictionaryOfIntSubclassProperty)), + onError( + typeof(MultipleInvalidHtmlAttributePrefix).FullName, + nameof(MultipleInvalidHtmlAttributePrefix.GetOnlyDictionaryAttributePrefix)), + $"Invalid tag helper bound property '{ typeof(MultipleInvalidHtmlAttributePrefix).FullName }." + + $"{ nameof(MultipleInvalidHtmlAttributePrefix.GetOnlyDictionaryPropertyWithAttributeName) }'. " + + $"'{ typeof(HtmlAttributeNameAttribute).FullName }." + + $"{ nameof(HtmlAttributeNameAttribute.Name) }' must be null or empty if property has " + + "no public setter.", } }, }; @@ -1128,14 +1150,12 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers TagHelperAttributeDescriptorComparer.Default); } - public static TheoryData ValidAttributeNameOrPrefixData + public static TheoryData ValidAttributeNameData { get { return new TheoryData { - null, - string.Empty, "data", "dataa-", "ValidName", @@ -1147,7 +1167,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers } [Theory] - [MemberData(nameof(ValidAttributeNameOrPrefixData))] + [MemberData(nameof(ValidAttributeNameData))] public void ValidateTagHelperAttributeDescriptor_WithValidName_ReturnsTrue(string name) { // Arrange @@ -1170,8 +1190,25 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers Assert.Empty(errorSink.Errors); } + public static TheoryData ValidAttributePrefixData + { + get + { + return new TheoryData + { + string.Empty, + "data", + "dataa-", + "ValidName", + "valid-name", + "--valid--name--", + ",,--__..oddly.valid::;;", + }; + } + } + [Theory] - [MemberData(nameof(ValidAttributeNameOrPrefixData))] + [MemberData(nameof(ValidAttributePrefixData))] public void ValidateTagHelperAttributeDescriptor_WithValidPrefix_ReturnsTrue(string prefix) { // Arrange @@ -1732,7 +1769,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers private class SingleValidHtmlAttributePrefix : TagHelper { - [HtmlAttributeName("valid-name", DictionaryAttributePrefix = "valid-prefix")] + [HtmlAttributeName("valid-name")] public IDictionary DictionaryProperty { get; set; } } @@ -1755,6 +1792,11 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers [HtmlAttributeName("valid-name6")] public string StringProperty { get; set; } + + public IDictionary GetOnlyDictionaryProperty { get; } + + [HtmlAttributeName(DictionaryAttributePrefix = "valid-prefix6")] + public IDictionary GetOnlyDictionaryPropertyWithAttributePrefix { get; } } private class SingleInvalidHtmlAttributePrefix : TagHelper @@ -1779,6 +1821,12 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers [HtmlAttributeName("valid-name5", DictionaryAttributePrefix = "valid-prefix5-")] public DictionaryOfIntSubclass DictionaryOfIntSubclassProperty { get; set; } + + [HtmlAttributeName(DictionaryAttributePrefix = "valid-prefix6")] + public IDictionary GetOnlyDictionaryAttributePrefix { get; } + + [HtmlAttributeName("invalid-name7")] + public IDictionary GetOnlyDictionaryPropertyWithAttributeName { get; } } private class DictionarySubclass : Dictionary