diff --git a/samples/aspnetcore/SwaggerODataSample/Models/Address.cs b/samples/aspnetcore/SwaggerODataSample/Models/Address.cs index 70e684e9..e6e21a96 100644 --- a/samples/aspnetcore/SwaggerODataSample/Models/Address.cs +++ b/samples/aspnetcore/SwaggerODataSample/Models/Address.cs @@ -1,40 +1,45 @@ namespace Microsoft.Examples.Models { - using Microsoft.AspNet.OData.Query; - using System; + using System.Runtime.Serialization; /// /// Represents an address. /// + [DataContract] public class Address { /// /// Gets or sets the address identifier. /// + [IgnoreDataMember] public int Id { get; set; } /// /// Gets or sets the street address. /// /// The street address. + [DataMember] public string Street { get; set; } /// /// Gets or sets the address city. /// /// The address city. + [DataMember] public string City { get; set; } /// /// Gets or sets the address state. /// /// The address state. + [DataMember] public string State { get; set; } /// /// Gets or sets the address zip code. /// /// The address zip code. + [DataMember(Name = "zip")] public string ZipCode { get; set; } } } \ No newline at end of file diff --git a/src/Common.OData.ApiExplorer/AspNet.OData/DefaultModelTypeBuilder.cs b/src/Common.OData.ApiExplorer/AspNet.OData/DefaultModelTypeBuilder.cs index 9bef7319..0a66863b 100644 --- a/src/Common.OData.ApiExplorer/AspNet.OData/DefaultModelTypeBuilder.cs +++ b/src/Common.OData.ApiExplorer/AspNet.OData/DefaultModelTypeBuilder.cs @@ -96,14 +96,26 @@ static Type GenerateTypeIfNeeded( IEdmStructuredType structuredType, BuilderCont const BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.Instance; var properties = new List(); - var structuralProperties = structuredType.Properties().ToDictionary( p => p.Name, StringComparer.OrdinalIgnoreCase ); + var structuralProperties = new Dictionary( StringComparer.OrdinalIgnoreCase ); + var mappedClrProperties = new Dictionary(); var clrTypeMatchesEdmType = true; var hasUnfinishedTypes = false; var dependentProperties = new List(); + foreach ( var property in structuredType.Properties() ) + { + structuralProperties.Add( property.Name, property ); + var clrProperty = edmModel.GetAnnotationValue( property )?.ClrPropertyInfo; + if ( clrProperty != null ) + { + mappedClrProperties.Add( clrProperty, property ); + } + } + foreach ( var property in clrType.GetProperties( bindingFlags ) ) { - if ( !structuralProperties.TryGetValue( property.Name, out var structuralProperty ) ) + if ( !structuralProperties.TryGetValue( property.Name, out var structuralProperty ) && + !mappedClrProperties.TryGetValue( property, out structuralProperty ) ) { clrTypeMatchesEdmType = false; continue; @@ -129,7 +141,7 @@ static Type GenerateTypeIfNeeded( IEdmStructuredType structuredType, BuilderCont { clrTypeMatchesEdmType = false; hasUnfinishedTypes = true; - dependentProperties.Add( new PropertyDependency( elementKey, true, property.Name ) ); + dependentProperties.Add( new PropertyDependency( elementKey, true, property.Name, property.DeclaredAttributes() ) ); continue; } @@ -162,7 +174,7 @@ static Type GenerateTypeIfNeeded( IEdmStructuredType structuredType, BuilderCont { clrTypeMatchesEdmType = false; hasUnfinishedTypes = true; - dependentProperties.Add( new PropertyDependency( propertyTypeKey, false, property.Name ) ); + dependentProperties.Add( new PropertyDependency( propertyTypeKey, false, property.Name, property.DeclaredAttributes() ) ); continue; } } @@ -232,12 +244,7 @@ static TypeBuilder CreateTypeBuilderFromSignature( ModuleBuilder moduleBuilder, { var type = property.Type; var name = property.Name; - var propertyBuilder = AddProperty( typeBuilder, type, name ); - - foreach ( var attribute in property.Attributes ) - { - propertyBuilder.SetCustomAttribute( attribute ); - } + AddProperty( typeBuilder, type, name, property.Attributes ); } return typeBuilder; @@ -258,7 +265,7 @@ static IDictionary ResolveDependencies( BuilderContext con dependentOnType = IEnumerableOfT.MakeGenericType( dependentOnType ).GetTypeInfo(); } - AddProperty( propertyDependency.DependentType, dependentOnType, propertyDependency.PropertyName ); + AddProperty( propertyDependency.DependentType, dependentOnType, propertyDependency.PropertyName, propertyDependency.CustomAttributes ); } var keys = edmTypes.Keys.ToArray(); @@ -276,7 +283,7 @@ static IDictionary ResolveDependencies( BuilderContext con return edmTypes; } - static PropertyBuilder AddProperty( TypeBuilder addTo, Type shouldBeAdded, string name ) + static PropertyBuilder AddProperty( TypeBuilder addTo, Type shouldBeAdded, string name, IEnumerable customAttributes ) { const MethodAttributes propertyMethodAttributes = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig; var field = addTo.DefineField( "_" + name, shouldBeAdded, FieldAttributes.Private ); @@ -297,6 +304,11 @@ static PropertyBuilder AddProperty( TypeBuilder addTo, Type shouldBeAdded, strin propertyBuilder.SetGetMethod( getter ); propertyBuilder.SetSetMethod( setter ); + foreach ( var attribute in customAttributes ) + { + propertyBuilder.SetCustomAttribute( attribute ); + } + return propertyBuilder; } diff --git a/src/Common.OData.ApiExplorer/AspNet.OData/PropertyDependency.cs b/src/Common.OData.ApiExplorer/AspNet.OData/PropertyDependency.cs index 956c04b2..2fa7d893 100644 --- a/src/Common.OData.ApiExplorer/AspNet.OData/PropertyDependency.cs +++ b/src/Common.OData.ApiExplorer/AspNet.OData/PropertyDependency.cs @@ -1,5 +1,6 @@ namespace Microsoft.AspNet.OData { + using System.Collections.Generic; using System.Reflection.Emit; /// @@ -13,13 +14,15 @@ internal class PropertyDependency /// The key of the type the property has a dependency on. /// The name of the property. /// Whether the property is a collection or not. - internal PropertyDependency( EdmTypeKey dependentOnTypeKey, bool isCollection, string propertyName ) + /// A collection of custom attribute builders. + internal PropertyDependency( EdmTypeKey dependentOnTypeKey, bool isCollection, string propertyName, IEnumerable customAttributes ) { Arg.NotNull( dependentOnTypeKey, nameof( dependentOnTypeKey ) ); Arg.NotNull( propertyName, nameof( propertyName ) ); DependentOnTypeKey = dependentOnTypeKey; PropertyName = propertyName; + CustomAttributes = customAttributes; IsCollection = isCollection; } @@ -42,5 +45,10 @@ internal PropertyDependency( EdmTypeKey dependentOnTypeKey, bool isCollection, /// Gets a value indicating whether the property is a collection. /// internal bool IsCollection { get; } + + /// + /// Gets custom attribute builders of the property. + /// + internal IEnumerable CustomAttributes { get; } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Contact.cs b/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Contact.cs index 6f4a1ad5..f62058dc 100644 --- a/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Contact.cs +++ b/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Contact.cs @@ -1,19 +1,27 @@ namespace Microsoft.AspNet.OData { using System.Collections.Generic; + using System.Runtime.Serialization; + [DataContract] public class Contact { + [DataMember] public int ContactId { get; set; } + [DataMember( Name = "first_name" )] public string FirstName { get; set; } + [DataMember] public string LastName { get; set; } - public string Email { get; set; } + [DataMember] + public Email Email { get; set; } + [DataMember( Name = "telephone" )] public string Phone { get; set; } + [DataMember] public List
Addresses { get; set; } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/DefaultModelTypeBuilderTest.cs b/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/DefaultModelTypeBuilderTest.cs index 315af1a1..269325e5 100644 --- a/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/DefaultModelTypeBuilderTest.cs +++ b/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/DefaultModelTypeBuilderTest.cs @@ -1,6 +1,8 @@ namespace Microsoft.AspNet.OData { using FluentAssertions; + using FluentAssertions.Common; + using System.Runtime.Serialization; using Microsoft.AspNet.OData.Builder; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; @@ -207,7 +209,7 @@ public void type_should_match_edm_with_child_entity_substitution( Type originalT nextType.Should().HaveProperty( nameof( Contact.ContactId ) ); nextType.Should().HaveProperty( nameof( Contact.FirstName ) ); nextType.Should().HaveProperty( nameof( Contact.LastName ) ); - nextType.Should().HaveProperty( nameof( Contact.Email ) ); + nextType.Should().HaveProperty( nameof( Contact.Email ) ); nextType.Should().HaveProperty( nameof( Contact.Phone ) ); nextType = nextType.GetRuntimeProperty( nameof( Contact.Addresses ) ).PropertyType.GetGenericArguments()[0]; nextType.GetRuntimeProperties().Should().HaveCount( 5 ); @@ -408,6 +410,61 @@ public void substitute_should_resolve_types_that_reference_a_model_that_match_th substitutionType.Should().NotBeOfType(); } + [Fact] + public void substituted_type_should_have_renamed_with_attribute_properties_from_original_type() + { + // arrange + var modelBuilder = new ODataConventionModelBuilder(); + modelBuilder.EntitySet( "Contacts" ); + + var context = NewContext( modelBuilder.GetEdmModel() ); + var originalType = typeof( Contact ); + + // act + var substitutedType = originalType.SubstituteIfNecessary( context ); + + // assert + substitutedType.Should().HaveProperty( nameof( Contact.FirstName ) ); + substitutedType.GetRuntimeProperty( nameof( Contact.FirstName ) ).Should().NotBeNull(); + substitutedType.GetRuntimeProperty( nameof( Contact.FirstName ) ).HasAttribute(); + } + + [Fact] + public void substituted_type_should_keep_custom_attributes_on_dependency_property() + { + // arrange + var modelBuilder = new ODataConventionModelBuilder(); + modelBuilder.EntitySet( "Contacts" ); + + var context = NewContext( modelBuilder.GetEdmModel() ); + var originalType = typeof( Contact ); + + // act + var substitutedType = originalType.SubstituteIfNecessary( context ); + + // assert + substitutedType.GetRuntimeProperty( nameof( Contact.Email ) ).Should().NotBeNull(); + substitutedType.GetRuntimeProperty( nameof( Contact.Email ) ).HasAttribute(); + } + + [Fact] + public void substituted_type_should_keep_custom_attributes_on_collection_dependency_property() + { + // arrange + var modelBuilder = new ODataConventionModelBuilder(); + modelBuilder.EntitySet( "Contacts" ); + + var context = NewContext( modelBuilder.GetEdmModel() ); + var originalType = typeof( Contact ); + + // act + var substitutedType = originalType.SubstituteIfNecessary( context ); + + // assert + substitutedType.GetRuntimeProperty( nameof( Contact.Addresses ) ).Should().NotBeNull(); + substitutedType.GetRuntimeProperty( nameof( Contact.Addresses ) ).HasAttribute(); + } + public static IEnumerable SubstitutionNotRequiredData { get diff --git a/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Email.cs b/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Email.cs new file mode 100644 index 00000000..64a9541b --- /dev/null +++ b/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Email.cs @@ -0,0 +1,14 @@ +namespace Microsoft.AspNet.OData +{ + using System.Runtime.Serialization; + + [DataContract] + public class Email + { + [DataMember] + public string Server { get; set; } + + [DataMember] + public string Username { get; set; } + } +} \ No newline at end of file