From cb70ccca37ee72833e6c77646deddee6ee0d2818 Mon Sep 17 00:00:00 2001 From: LuukN2 Date: Wed, 31 Oct 2018 10:46:35 +0100 Subject: [PATCH 01/12] Add support for generating types for action parameters that have collection parameters. --- .../AspNet.OData/ClassProperty.cs | 13 ++++++++- .../DefaultModelTypeBuilderTest.cs | 27 +++++++++++++++++++ .../AspNet.OData/Employer.cs | 6 ++--- .../DefaultModelTypeBuilderTest.cs | 27 +++++++++++++++++++ .../AspNet.OData/Employer.cs | 6 ++--- 5 files changed, 72 insertions(+), 7 deletions(-) diff --git a/src/Common.OData.ApiExplorer/AspNet.OData/ClassProperty.cs b/src/Common.OData.ApiExplorer/AspNet.OData/ClassProperty.cs index 76eb0089..29762ff9 100644 --- a/src/Common.OData.ApiExplorer/AspNet.OData/ClassProperty.cs +++ b/src/Common.OData.ApiExplorer/AspNet.OData/ClassProperty.cs @@ -30,7 +30,18 @@ internal ClassProperty( IEnumerable assemblies, IEdmOperationParameter Contract.Requires( parameter != null ); Name = parameter.Name; - type = parameter.Type.Definition.GetClrType( assemblies ); + + if ( parameter.Type.IsCollection() ) + { + var collectionType = parameter.Type.AsCollection(); + var elementType = collectionType.ElementType().Definition.GetClrType( assemblies ); + type = typeof( IEnumerable<> ).MakeGenericType( elementType ); + } + else + { + type = parameter.Type.Definition.GetClrType( assemblies ); + } + Attributes = AttributesFromOperationParameter( parameter ); } diff --git a/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/DefaultModelTypeBuilderTest.cs b/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/DefaultModelTypeBuilderTest.cs index 7a6785c0..38a78c9e 100644 --- a/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/DefaultModelTypeBuilderTest.cs +++ b/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/DefaultModelTypeBuilderTest.cs @@ -242,6 +242,33 @@ public void substitute_should_generate_type_for_action_parameters() substitutionType.Should().HaveProperty( "callbackRequired" ); } + [Fact] + public void substitute_should_generate_type_for_action_parameters_with_collection_parameters() + { + // arrange + var modelBuilder = new ODataConventionModelBuilder(); + var contact = modelBuilder.EntitySet( "Contacts" ).EntityType; + var action = contact.Action( "PlanMeeting" ); + + action.Parameter( "when" ); + action.CollectionParameter( "attendees" ); + action.CollectionParameter( "topics" ); + + var context = NewContext( modelBuilder.GetEdmModel() ); + var model = context.Model; + var qualifiedName = $"{model.EntityContainer.Namespace}.{action.Name}"; + var operation = (IEdmAction) model.FindDeclaredOperations( qualifiedName ).Single(); + + // act + var substitutionType = context.ModelTypeBuilder.NewActionParameters( operation, ApiVersion.Default ); + + // assert + substitutionType.GetRuntimeProperties().Should().HaveCount( 3 ); + substitutionType.Should().HaveProperty( "when" ); + substitutionType.Should().HaveProperty>( "attendees" ); + substitutionType.Should().HaveProperty>( "topics" ); + } + public static IEnumerable SubstitutionNotRequiredData { get diff --git a/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Employer.cs b/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Employer.cs index 17c9b388..e57ab069 100644 --- a/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Employer.cs +++ b/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Employer.cs @@ -1,8 +1,8 @@ -using System; -using System.Collections.Generic; - namespace Microsoft.AspNet.OData { + using System; + using System.Collections.Generic; + public class Employer { public int EmployerId { get; set; } 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 dcacf1ab..7a9c2f1e 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 @@ -242,6 +242,33 @@ public void substitute_should_generate_type_for_action_parameters() substitutionType.Should().HaveProperty( "callbackRequired" ); } + [Fact] + public void substitute_should_generate_type_for_action_parameters_with_collection_parameters() + { + // arrange + var modelBuilder = new ODataConventionModelBuilder(); + var contact = modelBuilder.EntitySet( "Contacts" ).EntityType; + var action = contact.Action( "PlanMeeting" ); + + action.Parameter( "when" ); + action.CollectionParameter( "attendees" ); + action.CollectionParameter( "topics" ); + + var context = NewContext( modelBuilder.GetEdmModel() ); + var model = context.Model; + var qualifiedName = $"{model.EntityContainer.Namespace}.{action.Name}"; + var operation = (IEdmAction) model.FindDeclaredOperations( qualifiedName ).Single(); + + // act + var substitutionType = context.ModelTypeBuilder.NewActionParameters( operation, ApiVersion.Default ); + + // assert + substitutionType.GetRuntimeProperties().Should().HaveCount( 3 ); + substitutionType.Should().HaveProperty( "when" ); + substitutionType.Should().HaveProperty>( "attendees" ); + substitutionType.Should().HaveProperty>( "topics" ); + } + public static IEnumerable SubstitutionNotRequiredData { get diff --git a/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Employer.cs b/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Employer.cs index 17c9b388..e57ab069 100644 --- a/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Employer.cs +++ b/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Employer.cs @@ -1,8 +1,8 @@ -using System; -using System.Collections.Generic; - namespace Microsoft.AspNet.OData { + using System; + using System.Collections.Generic; + public class Employer { public int EmployerId { get; set; } From 5c168763c973397609ea0003966de63c90361cbc Mon Sep 17 00:00:00 2001 From: LuukN2 Date: Thu, 1 Nov 2018 09:30:49 +0100 Subject: [PATCH 02/12] Add support for the substitution of ODataActionParameter parameters. --- .../AspNet.OData/ClassProperty.cs | 11 +++-- .../AspNet.OData/DefaultModelTypeBuilder.cs | 4 +- .../AspNet.OData/IModelTypeBuilder.cs | 5 +- .../Routing/ODataRouteBuilderContext.cs | 2 +- .../PseudoModelBindingVisitor.cs | 2 +- .../DefaultModelTypeBuilderTest.cs | 46 ++++++++++++++++++- .../DefaultModelTypeBuilderTest.cs | 46 ++++++++++++++++++- 7 files changed, 103 insertions(+), 13 deletions(-) diff --git a/src/Common.OData.ApiExplorer/AspNet.OData/ClassProperty.cs b/src/Common.OData.ApiExplorer/AspNet.OData/ClassProperty.cs index 29762ff9..3b9f84f9 100644 --- a/src/Common.OData.ApiExplorer/AspNet.OData/ClassProperty.cs +++ b/src/Common.OData.ApiExplorer/AspNet.OData/ClassProperty.cs @@ -24,22 +24,27 @@ internal ClassProperty( PropertyInfo clrProperty, Type propertyType ) Attributes = AttributesFromProperty( clrProperty ); } - internal ClassProperty( IEnumerable assemblies, IEdmOperationParameter parameter ) + internal ClassProperty( IServiceProvider services, IEnumerable assemblies, IEdmOperationParameter parameter, IModelTypeBuilder typeBuilder ) { Contract.Requires( assemblies != null ); Contract.Requires( parameter != null ); Name = parameter.Name; + var context = new TypeSubstitutionContext( services, assemblies, typeBuilder ); if ( parameter.Type.IsCollection() ) { var collectionType = parameter.Type.AsCollection(); var elementType = collectionType.ElementType().Definition.GetClrType( assemblies ); - type = typeof( IEnumerable<> ).MakeGenericType( elementType ); + var substitutedType = elementType.SubstituteIfNecessary( context ); + + type = typeof( IEnumerable<> ).MakeGenericType( substitutedType ); } else { - type = parameter.Type.Definition.GetClrType( assemblies ); + var parameterType = parameter.Type.Definition.GetClrType( assemblies ); + + type = parameterType.SubstituteIfNecessary( context ); } Attributes = AttributesFromOperationParameter( parameter ); diff --git a/src/Common.OData.ApiExplorer/AspNet.OData/DefaultModelTypeBuilder.cs b/src/Common.OData.ApiExplorer/AspNet.OData/DefaultModelTypeBuilder.cs index 518f2cf6..39bbbdfc 100644 --- a/src/Common.OData.ApiExplorer/AspNet.OData/DefaultModelTypeBuilder.cs +++ b/src/Common.OData.ApiExplorer/AspNet.OData/DefaultModelTypeBuilder.cs @@ -171,14 +171,14 @@ public Type NewStructuredType( IEdmStructuredType structuredType, Type clrType, } /// - public Type NewActionParameters( IEdmAction action, ApiVersion apiVersion ) + public Type NewActionParameters( IServiceProvider services, IEdmAction action, ApiVersion apiVersion ) { Arg.NotNull( action, nameof( action ) ); Arg.NotNull( apiVersion, nameof( apiVersion ) ); Contract.Ensures( Contract.Result() != null ); var name = action.FullName() + "Parameters"; - var properties = action.Parameters.Where( p => p.Name != "bindingParameter" ).Select( p => new ClassProperty( assemblies, p ) ); + var properties = action.Parameters.Where( p => p.Name != "bindingParameter" ).Select( p => new ClassProperty( services, assemblies, p, this ) ); var signature = new ClassSignature( name, properties, apiVersion ); return CreateTypeInfoFromSignature( signature ); diff --git a/src/Common.OData.ApiExplorer/AspNet.OData/IModelTypeBuilder.cs b/src/Common.OData.ApiExplorer/AspNet.OData/IModelTypeBuilder.cs index 35e7fed0..441a175e 100644 --- a/src/Common.OData.ApiExplorer/AspNet.OData/IModelTypeBuilder.cs +++ b/src/Common.OData.ApiExplorer/AspNet.OData/IModelTypeBuilder.cs @@ -29,14 +29,15 @@ public interface IModelTypeBuilder Type NewStructuredType( IEdmStructuredType structuredType, Type clrType, ApiVersion apiVersion ); /// - /// Creates an returns a stronly-typed definition for OData action parameters. + /// Creates an returns a strongly-typed definition for OData action parameters. /// + /// The services needed to potentially substitute types. /// The defining action. /// The API version of the to create the parameter type for. /// A strong type definition for the OData parameters. /// OData action parameters are modeled as a dictionary, /// which is difficult to use effectively by documentation tools such as the API Explorer. The corresponding type is generated only once per /// API version. - Type NewActionParameters( IEdmAction action, ApiVersion apiVersion ); + Type NewActionParameters( IServiceProvider services, IEdmAction action, ApiVersion apiVersion ); } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs b/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs index 0d4a1813..e5b6f3f5 100644 --- a/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs +++ b/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs @@ -79,7 +79,7 @@ void ConvertODataActionParametersToTypedModel( IModelTypeBuilder modelTypeBuilde if ( parameter.ParameterType.IsODataActionParameters() ) { - description.ParameterDescriptor = new ODataModelBoundParameterDescriptor( parameter, modelTypeBuilder.NewActionParameters( action, apiVersion.Value ) ); + description.ParameterDescriptor = new ODataModelBoundParameterDescriptor( parameter, modelTypeBuilder.NewActionParameters( serviceProvider, action, apiVersion.Value ) ); break; } } diff --git a/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc.ApiExplorer/PseudoModelBindingVisitor.cs b/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc.ApiExplorer/PseudoModelBindingVisitor.cs index 6b0ef28f..4fd45bee 100644 --- a/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc.ApiExplorer/PseudoModelBindingVisitor.cs +++ b/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc.ApiExplorer/PseudoModelBindingVisitor.cs @@ -88,7 +88,7 @@ ApiParameterDescription CreateResult( ApiParameterDescriptionContext bindingCont { var action = (IEdmAction) Context.RouteContext.Operation; var apiVersion = Context.RouteContext.ApiVersion; - type = Context.TypeBuilder.NewActionParameters( action, apiVersion ); + type = Context.TypeBuilder.NewActionParameters( Context.Services, action, apiVersion ); } else { diff --git a/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/DefaultModelTypeBuilderTest.cs b/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/DefaultModelTypeBuilderTest.cs index 38a78c9e..2109d3d5 100644 --- a/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/DefaultModelTypeBuilderTest.cs +++ b/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/DefaultModelTypeBuilderTest.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; + using Microsoft.Extensions.DependencyInjection; using Xunit; public class DefaultModelTypeBuilderTest @@ -231,9 +232,11 @@ public void substitute_should_generate_type_for_action_parameters() var model = context.Model; var qualifiedName = $"{model.EntityContainer.Namespace}.{action.Name}"; var operation = (IEdmAction) model.FindDeclaredOperations( qualifiedName ).Single(); + var services = new ServiceCollection(); + services.AddSingleton( model ); // act - var substitutionType = context.ModelTypeBuilder.NewActionParameters( operation, ApiVersion.Default ); + var substitutionType = context.ModelTypeBuilder.NewActionParameters( services.BuildServiceProvider(), operation, ApiVersion.Default ); // assert substitutionType.GetRuntimeProperties().Should().HaveCount( 3 ); @@ -242,6 +245,43 @@ public void substitute_should_generate_type_for_action_parameters() substitutionType.Should().HaveProperty( "callbackRequired" ); } + [Fact] + public void substitute_should_generate_type_for_action_parameters_with_substituted_types() + { + // arrange + var modelBuilder = new ODataConventionModelBuilder(); + var contact = modelBuilder.EntitySet( "Contacts" ).EntityType; + contact.Ignore( c => c.Email ); + var action = contact.Action( "PlanInterview" ); + + action.Parameter( "when" ); + action.Parameter( "interviewer" ); + action.Parameter( "interviewee" ); + + var context = NewContext( modelBuilder.GetEdmModel() ); + var model = context.Model; + var qualifiedName = $"{model.EntityContainer.Namespace}.{action.Name}"; + var operation = (IEdmAction) model.FindDeclaredOperations( qualifiedName ).Single(); + var services = new ServiceCollection(); + services.AddSingleton( model ); + + // act + var substitutionType = context.ModelTypeBuilder.NewActionParameters( services.BuildServiceProvider(), operation, ApiVersion.Default ); + + // assert + substitutionType.GetRuntimeProperties().Should().HaveCount( 3 ); + substitutionType.Should().HaveProperty( "when" ); + var contactType = substitutionType.GetRuntimeProperty( "interviewer" ).PropertyType; + contactType.Should().Be( substitutionType.GetRuntimeProperty( "interviewee" ).PropertyType ); + + contactType.GetRuntimeProperties().Should().HaveCount( 5 ); + contactType.Should().HaveProperty( "ContactId" ); + contactType.Should().HaveProperty( "FirstName" ); + contactType.Should().HaveProperty( "LastName" ); + contactType.Should().HaveProperty( "Phone" ); + contactType.Should().HaveProperty>( "Addresses" ); + } + [Fact] public void substitute_should_generate_type_for_action_parameters_with_collection_parameters() { @@ -258,9 +298,11 @@ public void substitute_should_generate_type_for_action_parameters_with_collectio var model = context.Model; var qualifiedName = $"{model.EntityContainer.Namespace}.{action.Name}"; var operation = (IEdmAction) model.FindDeclaredOperations( qualifiedName ).Single(); + var services = new ServiceCollection(); + services.AddSingleton( model ); // act - var substitutionType = context.ModelTypeBuilder.NewActionParameters( operation, ApiVersion.Default ); + var substitutionType = context.ModelTypeBuilder.NewActionParameters( services.BuildServiceProvider(), operation, ApiVersion.Default ); // assert substitutionType.GetRuntimeProperties().Should().HaveCount( 3 ); 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 7a9c2f1e..f126d215 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 @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; + using Microsoft.Extensions.DependencyInjection; using Xunit; public class DefaultModelTypeBuilderTest @@ -231,9 +232,11 @@ public void substitute_should_generate_type_for_action_parameters() var model = context.Model; var qualifiedName = $"{model.EntityContainer.Namespace}.{action.Name}"; var operation = (IEdmAction) model.FindDeclaredOperations( qualifiedName ).Single(); + var services = new ServiceCollection(); + services.AddSingleton( model ); // act - var substitutionType = context.ModelTypeBuilder.NewActionParameters( operation, ApiVersion.Default ); + var substitutionType = context.ModelTypeBuilder.NewActionParameters( services.BuildServiceProvider(), operation, ApiVersion.Default ); // assert substitutionType.GetRuntimeProperties().Should().HaveCount( 3 ); @@ -242,6 +245,43 @@ public void substitute_should_generate_type_for_action_parameters() substitutionType.Should().HaveProperty( "callbackRequired" ); } + [Fact] + public void substitute_should_generate_type_for_action_parameters_with_substituted_types() + { + // arrange + var modelBuilder = new ODataConventionModelBuilder(); + var contact = modelBuilder.EntitySet( "Contacts" ).EntityType; + contact.Ignore( c => c.Email ); + var action = contact.Action( "PlanInterview" ); + + action.Parameter( "when" ); + action.Parameter( "interviewer" ); + action.Parameter( "interviewee" ); + + var context = NewContext( modelBuilder.GetEdmModel() ); + var model = context.Model; + var qualifiedName = $"{model.EntityContainer.Namespace}.{action.Name}"; + var operation = (IEdmAction) model.FindDeclaredOperations( qualifiedName ).Single(); + var services = new ServiceCollection(); + services.AddSingleton( model ); + + // act + var substitutionType = context.ModelTypeBuilder.NewActionParameters( services.BuildServiceProvider(), operation, ApiVersion.Default ); + + // assert + substitutionType.GetRuntimeProperties().Should().HaveCount( 3 ); + substitutionType.Should().HaveProperty( "when" ); + var contactType = substitutionType.GetRuntimeProperty( "interviewer" ).PropertyType; + contactType.Should().Be( substitutionType.GetRuntimeProperty( "interviewee" ).PropertyType ); + + contactType.GetRuntimeProperties().Should().HaveCount( 5 ); + contactType.Should().HaveProperty( "ContactId" ); + contactType.Should().HaveProperty( "FirstName" ); + contactType.Should().HaveProperty( "LastName" ); + contactType.Should().HaveProperty( "Phone" ); + contactType.Should().HaveProperty>( "Addresses" ); + } + [Fact] public void substitute_should_generate_type_for_action_parameters_with_collection_parameters() { @@ -258,9 +298,11 @@ public void substitute_should_generate_type_for_action_parameters_with_collectio var model = context.Model; var qualifiedName = $"{model.EntityContainer.Namespace}.{action.Name}"; var operation = (IEdmAction) model.FindDeclaredOperations( qualifiedName ).Single(); + var services = new ServiceCollection(); + services.AddSingleton( model ); // act - var substitutionType = context.ModelTypeBuilder.NewActionParameters( operation, ApiVersion.Default ); + var substitutionType = context.ModelTypeBuilder.NewActionParameters( services.BuildServiceProvider(), operation, ApiVersion.Default ); // assert substitutionType.GetRuntimeProperties().Should().HaveCount( 3 ); From 6a2243213cd3f3152999e54f674b67420aa1ad11 Mon Sep 17 00:00:00 2001 From: LuukN2 Date: Thu, 1 Nov 2018 20:10:23 +0100 Subject: [PATCH 03/12] Added null checks and removed GetPropertyType. --- .../AspNet.OData/ClassProperty.cs | 17 +++++++---------- .../AspNet.OData/DefaultModelTypeBuilder.cs | 3 ++- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/Common.OData.ApiExplorer/AspNet.OData/ClassProperty.cs b/src/Common.OData.ApiExplorer/AspNet.OData/ClassProperty.cs index 3b9f84f9..f43dd37a 100644 --- a/src/Common.OData.ApiExplorer/AspNet.OData/ClassProperty.cs +++ b/src/Common.OData.ApiExplorer/AspNet.OData/ClassProperty.cs @@ -11,7 +11,7 @@ struct ClassProperty { - readonly Type type; + internal readonly Type Type; internal readonly string Name; internal ClassProperty( PropertyInfo clrProperty, Type propertyType ) @@ -20,14 +20,16 @@ internal ClassProperty( PropertyInfo clrProperty, Type propertyType ) Contract.Requires( propertyType != null ); Name = clrProperty.Name; - type = propertyType; + Type = propertyType; Attributes = AttributesFromProperty( clrProperty ); } internal ClassProperty( IServiceProvider services, IEnumerable assemblies, IEdmOperationParameter parameter, IModelTypeBuilder typeBuilder ) { + Contract.Requires( services != null ); Contract.Requires( assemblies != null ); Contract.Requires( parameter != null ); + Contract.Requires( typeBuilder != null ); Name = parameter.Name; var context = new TypeSubstitutionContext( services, assemblies, typeBuilder ); @@ -38,13 +40,13 @@ internal ClassProperty( IServiceProvider services, IEnumerable assembl var elementType = collectionType.ElementType().Definition.GetClrType( assemblies ); var substitutedType = elementType.SubstituteIfNecessary( context ); - type = typeof( IEnumerable<> ).MakeGenericType( substitutedType ); + Type = typeof( IEnumerable<> ).MakeGenericType( substitutedType ); } else { var parameterType = parameter.Type.Definition.GetClrType( assemblies ); - type = parameterType.SubstituteIfNecessary( context ); + Type = parameterType.SubstituteIfNecessary( context ); } Attributes = AttributesFromOperationParameter( parameter ); @@ -52,12 +54,7 @@ internal ClassProperty( IServiceProvider services, IEnumerable assembl internal IEnumerable Attributes { get; } - public override int GetHashCode() => ( Name.GetHashCode() * 397 ) ^ type.GetHashCode(); - - public Type GetPropertyType() - { - return type; - } + public override int GetHashCode() => ( Name.GetHashCode() * 397 ) ^ Type.GetHashCode(); static IEnumerable AttributesFromProperty( PropertyInfo clrProperty ) { diff --git a/src/Common.OData.ApiExplorer/AspNet.OData/DefaultModelTypeBuilder.cs b/src/Common.OData.ApiExplorer/AspNet.OData/DefaultModelTypeBuilder.cs index 39bbbdfc..b22db6a1 100644 --- a/src/Common.OData.ApiExplorer/AspNet.OData/DefaultModelTypeBuilder.cs +++ b/src/Common.OData.ApiExplorer/AspNet.OData/DefaultModelTypeBuilder.cs @@ -173,6 +173,7 @@ public Type NewStructuredType( IEdmStructuredType structuredType, Type clrType, /// public Type NewActionParameters( IServiceProvider services, IEdmAction action, ApiVersion apiVersion ) { + Arg.NotNull( services, nameof( services ) ); Arg.NotNull( action, nameof( action ) ); Arg.NotNull( apiVersion, nameof( apiVersion ) ); Contract.Ensures( Contract.Result() != null ); @@ -202,7 +203,7 @@ TypeBuilder CreateTypeBuilderFromSignature( ClassSignature @class ) foreach ( var property in @class.Properties ) { - var type = property.GetPropertyType(); + var type = property.Type; var name = property.Name; var propertyBuilder = AddProperty( typeBuilder, type, name ); From abc2695246f1c8329e39d8d31c73fb09869e190f Mon Sep 17 00:00:00 2001 From: LuukN2 Date: Mon, 5 Nov 2018 15:27:37 +0100 Subject: [PATCH 04/12] Add support for building custom attributes that take params parameters. --- .../AspNet.OData/ClassProperty.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Common.OData.ApiExplorer/AspNet.OData/ClassProperty.cs b/src/Common.OData.ApiExplorer/AspNet.OData/ClassProperty.cs index f43dd37a..0c7a5fd7 100644 --- a/src/Common.OData.ApiExplorer/AspNet.OData/ClassProperty.cs +++ b/src/Common.OData.ApiExplorer/AspNet.OData/ClassProperty.cs @@ -84,6 +84,14 @@ static IEnumerable AttributesFromProperty( PropertyInfo } } + for ( var i = 0; i < ctorArgs.Length; i++ ) + { + if ( ctorArgs[i] is IReadOnlyCollection paramsList ) + { + ctorArgs[i] = paramsList.Select( a => a.Value ).ToArray(); + } + } + yield return new CustomAttributeBuilder( ctor, ctorArgs, From 95defb4f9911f2bba5f92e759ddda8d3f01ce275 Mon Sep 17 00:00:00 2001 From: LuukN2 Date: Mon, 5 Nov 2018 15:29:05 +0100 Subject: [PATCH 05/12] Prevent trying to resolve dependencies for an unfinished type that has been finished already. --- .../AspNet.OData/DefaultModelTypeBuilder.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Common.OData.ApiExplorer/AspNet.OData/DefaultModelTypeBuilder.cs b/src/Common.OData.ApiExplorer/AspNet.OData/DefaultModelTypeBuilder.cs index b22db6a1..6f4380cc 100644 --- a/src/Common.OData.ApiExplorer/AspNet.OData/DefaultModelTypeBuilder.cs +++ b/src/Common.OData.ApiExplorer/AspNet.OData/DefaultModelTypeBuilder.cs @@ -274,7 +274,10 @@ private void ResolveForUnfinishedTypes() var keys = unfinishedTypes.Keys; foreach ( var key in keys ) { - ResolveDependencies(unfinishedTypes[key], key); + if ( unfinishedTypes.TryGetValue( key, out var type ) ) + { + ResolveDependencies(type, key); + } } } From afa5f8b2cf906d4e1ca83793c23ba08c8cae9a30 Mon Sep 17 00:00:00 2001 From: LuukN2 Date: Mon, 5 Nov 2018 15:29:53 +0100 Subject: [PATCH 06/12] Support substitution of single result generic. --- src/Common.OData.ApiExplorer/AspNet.OData/TypeExtensions.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Common.OData.ApiExplorer/AspNet.OData/TypeExtensions.cs b/src/Common.OData.ApiExplorer/AspNet.OData/TypeExtensions.cs index fe776e9d..f43ecbfe 100644 --- a/src/Common.OData.ApiExplorer/AspNet.OData/TypeExtensions.cs +++ b/src/Common.OData.ApiExplorer/AspNet.OData/TypeExtensions.cs @@ -6,6 +6,7 @@ using Microsoft.OData.Edm; #if WEBAPI using Microsoft.Web.Http; + using System.Web.Http; #endif using System; using System.Collections.Generic; @@ -26,6 +27,7 @@ public static partial class TypeExtensions static readonly Type HttpResponseType = typeof( HttpResponseMessage ); static readonly Type IEnumerableOfT = typeof( IEnumerable<> ); static readonly Type ODataValueOfT = typeof( ODataValue<> ); + static readonly Type SingleResultOfT = typeof( SingleResult<> ); /// /// Substitutes the specified type, if required. @@ -106,7 +108,7 @@ static bool IsSubstitutableGeneric( Type type, Stack openTypes, out Type i var typeArg = typeArgs[0]; - if ( typeDef.Equals( IEnumerableOfT ) || typeDef.IsDelta() || typeDef.Equals( ODataValueOfT ) || typeDef.IsActionResult() ) + if ( typeDef.Equals( IEnumerableOfT ) || typeDef.IsDelta() || typeDef.Equals( ODataValueOfT ) || typeDef.IsActionResult() || typeDef.Equals( SingleResultOfT )) { innerType = typeArg; } From 00bf1d7b6efd3651c1b6e729b8ef0ba3d111d582 Mon Sep 17 00:00:00 2001 From: LuukN2 Date: Tue, 6 Nov 2018 10:58:32 +0100 Subject: [PATCH 07/12] Do not set the type if no structured type could be found. Also prevent null pointer when no odata action parameters are found. This is necessary for CreateRef actions. --- src/Common.OData.ApiExplorer/AspNet.OData/TypeExtensions.cs | 4 ++++ .../AspNet.OData/Routing/ODataRouteBuilderContext.cs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Common.OData.ApiExplorer/AspNet.OData/TypeExtensions.cs b/src/Common.OData.ApiExplorer/AspNet.OData/TypeExtensions.cs index f43ecbfe..6f38a480 100644 --- a/src/Common.OData.ApiExplorer/AspNet.OData/TypeExtensions.cs +++ b/src/Common.OData.ApiExplorer/AspNet.OData/TypeExtensions.cs @@ -70,7 +70,11 @@ public static Type SubstituteIfNecessary( this Type type, TypeSubstitutionContex { var (apiVersion, resolver) = holder.Value; var structuredType = resolver.GetStructuredType( type ); + + if ( structuredType != null ) + { type = context.ModelTypeBuilder.NewStructuredType( structuredType, type, apiVersion ); + } } return type; diff --git a/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs b/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs index e5b6f3f5..cbe4c427 100644 --- a/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs +++ b/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs @@ -77,7 +77,7 @@ void ConvertODataActionParametersToTypedModel( IModelTypeBuilder modelTypeBuilde var description = ParameterDescriptions[i]; var parameter = description.ParameterDescriptor; - if ( parameter.ParameterType.IsODataActionParameters() ) + if ( parameter != null && parameter.ParameterType.IsODataActionParameters() ) { description.ParameterDescriptor = new ODataModelBoundParameterDescriptor( parameter, modelTypeBuilder.NewActionParameters( serviceProvider, action, apiVersion.Value ) ); break; From f9b968c0dcb48c6749936b882d18452b433923e9 Mon Sep 17 00:00:00 2001 From: LuukN2 Date: Tue, 6 Nov 2018 14:29:51 +0100 Subject: [PATCH 08/12] Prevent duplicate action parameter type names when actions with the same names are declared in multiple controllers. --- .../AspNet.OData/DefaultModelTypeBuilder.cs | 6 ++++-- .../AspNet.OData/IModelTypeBuilder.cs | 3 ++- .../AspNet.OData/Routing/ODataRouteBuilderContext.cs | 7 ++++--- .../PseudoModelBindingVisitor.cs | 2 +- .../AspNet.OData/DefaultModelTypeBuilderTest.cs | 6 +++--- .../AspNet.OData/DefaultModelTypeBuilderTest.cs | 6 +++--- 6 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/Common.OData.ApiExplorer/AspNet.OData/DefaultModelTypeBuilder.cs b/src/Common.OData.ApiExplorer/AspNet.OData/DefaultModelTypeBuilder.cs index 6f4380cc..05464f3b 100644 --- a/src/Common.OData.ApiExplorer/AspNet.OData/DefaultModelTypeBuilder.cs +++ b/src/Common.OData.ApiExplorer/AspNet.OData/DefaultModelTypeBuilder.cs @@ -171,14 +171,16 @@ public Type NewStructuredType( IEdmStructuredType structuredType, Type clrType, } /// - public Type NewActionParameters( IServiceProvider services, IEdmAction action, ApiVersion apiVersion ) + public Type NewActionParameters( IServiceProvider services, IEdmAction action, ApiVersion apiVersion, string controllerName ) { Arg.NotNull( services, nameof( services ) ); Arg.NotNull( action, nameof( action ) ); Arg.NotNull( apiVersion, nameof( apiVersion ) ); + Arg.NotNull( controllerName, nameof(controllerName) ); Contract.Ensures( Contract.Result() != null ); - var name = action.FullName() + "Parameters"; + var name = controllerName + "." + action.FullName() + "Parameters"; + var properties = action.Parameters.Where( p => p.Name != "bindingParameter" ).Select( p => new ClassProperty( services, assemblies, p, this ) ); var signature = new ClassSignature( name, properties, apiVersion ); diff --git a/src/Common.OData.ApiExplorer/AspNet.OData/IModelTypeBuilder.cs b/src/Common.OData.ApiExplorer/AspNet.OData/IModelTypeBuilder.cs index 441a175e..e1144518 100644 --- a/src/Common.OData.ApiExplorer/AspNet.OData/IModelTypeBuilder.cs +++ b/src/Common.OData.ApiExplorer/AspNet.OData/IModelTypeBuilder.cs @@ -34,10 +34,11 @@ public interface IModelTypeBuilder /// The services needed to potentially substitute types. /// The defining action. /// The API version of the to create the parameter type for. + /// The name of the controller that defines the action. Necessary for generating unique parameter types. /// A strong type definition for the OData parameters. /// OData action parameters are modeled as a dictionary, /// which is difficult to use effectively by documentation tools such as the API Explorer. The corresponding type is generated only once per /// API version. - Type NewActionParameters( IServiceProvider services, IEdmAction action, ApiVersion apiVersion ); + Type NewActionParameters( IServiceProvider services, IEdmAction action, ApiVersion apiVersion, string controllerName ); } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs b/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs index cbe4c427..2273d2ed 100644 --- a/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs +++ b/src/Microsoft.AspNet.OData.Versioning.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilderContext.cs @@ -61,14 +61,15 @@ internal ODataRouteBuilderContext( if ( Operation?.IsAction() == true ) { - ConvertODataActionParametersToTypedModel( modelTypeBuilder, (IEdmAction) Operation ); + ConvertODataActionParametersToTypedModel( modelTypeBuilder, (IEdmAction) Operation, actionDescriptor.ControllerDescriptor.ControllerName ); } } - void ConvertODataActionParametersToTypedModel( IModelTypeBuilder modelTypeBuilder, IEdmAction action ) + void ConvertODataActionParametersToTypedModel( IModelTypeBuilder modelTypeBuilder, IEdmAction action, string controllerName ) { Contract.Requires( modelTypeBuilder != null ); Contract.Requires( action != null ); + Contract.Requires( controllerName != null ); var apiVersion = new Lazy( () => EdmModel.GetAnnotationValue( EdmModel ).ApiVersion ); @@ -79,7 +80,7 @@ void ConvertODataActionParametersToTypedModel( IModelTypeBuilder modelTypeBuilde if ( parameter != null && parameter.ParameterType.IsODataActionParameters() ) { - description.ParameterDescriptor = new ODataModelBoundParameterDescriptor( parameter, modelTypeBuilder.NewActionParameters( serviceProvider, action, apiVersion.Value ) ); + description.ParameterDescriptor = new ODataModelBoundParameterDescriptor( parameter, modelTypeBuilder.NewActionParameters( serviceProvider, action, apiVersion.Value, controllerName ) ); break; } } diff --git a/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc.ApiExplorer/PseudoModelBindingVisitor.cs b/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc.ApiExplorer/PseudoModelBindingVisitor.cs index 4fd45bee..187e51a8 100644 --- a/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc.ApiExplorer/PseudoModelBindingVisitor.cs +++ b/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc.ApiExplorer/PseudoModelBindingVisitor.cs @@ -88,7 +88,7 @@ ApiParameterDescription CreateResult( ApiParameterDescriptionContext bindingCont { var action = (IEdmAction) Context.RouteContext.Operation; var apiVersion = Context.RouteContext.ApiVersion; - type = Context.TypeBuilder.NewActionParameters( Context.Services, action, apiVersion ); + type = Context.TypeBuilder.NewActionParameters( Context.Services, action, apiVersion, Context.RouteContext.EntitySet.Name ); } else { diff --git a/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/DefaultModelTypeBuilderTest.cs b/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/DefaultModelTypeBuilderTest.cs index 2109d3d5..8d7e04cb 100644 --- a/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/DefaultModelTypeBuilderTest.cs +++ b/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/DefaultModelTypeBuilderTest.cs @@ -236,7 +236,7 @@ public void substitute_should_generate_type_for_action_parameters() services.AddSingleton( model ); // act - var substitutionType = context.ModelTypeBuilder.NewActionParameters( services.BuildServiceProvider(), operation, ApiVersion.Default ); + var substitutionType = context.ModelTypeBuilder.NewActionParameters( services.BuildServiceProvider(), operation, ApiVersion.Default, contact.Name ); // assert substitutionType.GetRuntimeProperties().Should().HaveCount( 3 ); @@ -266,7 +266,7 @@ public void substitute_should_generate_type_for_action_parameters_with_substitut services.AddSingleton( model ); // act - var substitutionType = context.ModelTypeBuilder.NewActionParameters( services.BuildServiceProvider(), operation, ApiVersion.Default ); + var substitutionType = context.ModelTypeBuilder.NewActionParameters( services.BuildServiceProvider(), operation, ApiVersion.Default, contact.Name ); // assert substitutionType.GetRuntimeProperties().Should().HaveCount( 3 ); @@ -302,7 +302,7 @@ public void substitute_should_generate_type_for_action_parameters_with_collectio services.AddSingleton( model ); // act - var substitutionType = context.ModelTypeBuilder.NewActionParameters( services.BuildServiceProvider(), operation, ApiVersion.Default ); + var substitutionType = context.ModelTypeBuilder.NewActionParameters( services.BuildServiceProvider(), operation, ApiVersion.Default, contact.Name ); // assert substitutionType.GetRuntimeProperties().Should().HaveCount( 3 ); 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 f126d215..adfcb219 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 @@ -236,7 +236,7 @@ public void substitute_should_generate_type_for_action_parameters() services.AddSingleton( model ); // act - var substitutionType = context.ModelTypeBuilder.NewActionParameters( services.BuildServiceProvider(), operation, ApiVersion.Default ); + var substitutionType = context.ModelTypeBuilder.NewActionParameters( services.BuildServiceProvider(), operation, ApiVersion.Default, contact.Name ); // assert substitutionType.GetRuntimeProperties().Should().HaveCount( 3 ); @@ -266,7 +266,7 @@ public void substitute_should_generate_type_for_action_parameters_with_substitut services.AddSingleton( model ); // act - var substitutionType = context.ModelTypeBuilder.NewActionParameters( services.BuildServiceProvider(), operation, ApiVersion.Default ); + var substitutionType = context.ModelTypeBuilder.NewActionParameters( services.BuildServiceProvider(), operation, ApiVersion.Default, contact.Name ); // assert substitutionType.GetRuntimeProperties().Should().HaveCount( 3 ); @@ -302,7 +302,7 @@ public void substitute_should_generate_type_for_action_parameters_with_collectio services.AddSingleton( model ); // act - var substitutionType = context.ModelTypeBuilder.NewActionParameters( services.BuildServiceProvider(), operation, ApiVersion.Default ); + var substitutionType = context.ModelTypeBuilder.NewActionParameters( services.BuildServiceProvider(), operation, ApiVersion.Default, contact.Name ); // assert substitutionType.GetRuntimeProperties().Should().HaveCount( 3 ); From 23eaa44674c82f00a0ea44b208d97781f419aa05 Mon Sep 17 00:00:00 2001 From: LuukN2 Date: Wed, 7 Nov 2018 09:09:14 +0100 Subject: [PATCH 09/12] Fixed property dependencies on types that have already been generated never being resolved. --- .../AspNet.OData/DefaultModelTypeBuilder.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Common.OData.ApiExplorer/AspNet.OData/DefaultModelTypeBuilder.cs b/src/Common.OData.ApiExplorer/AspNet.OData/DefaultModelTypeBuilder.cs index 05464f3b..e72677f8 100644 --- a/src/Common.OData.ApiExplorer/AspNet.OData/DefaultModelTypeBuilder.cs +++ b/src/Common.OData.ApiExplorer/AspNet.OData/DefaultModelTypeBuilder.cs @@ -226,10 +226,22 @@ private Type ResolveDependencies( TypeBuilder typeBuilder, EdmTypeKey typeKey ) foreach ( var key in keys ) { var propertyDependencies = dependencies[key]; + for ( var x = propertyDependencies.Count - 1; x >= 0; x-- ) { var propertyDependency = propertyDependencies[x]; - if ( propertyDependency.DependentOnTypeKey == typeKey ) + Type dependentOnType = null; + + if ( unfinishedTypes.TryGetValue( propertyDependency.DependentOnTypeKey, out var dependentOnTypeBuilder ) ) + { + dependentOnType = dependentOnTypeBuilder; + } + else if ( generatedEdmTypes.TryGetValue( propertyDependency.DependentOnTypeKey, out var dependentOnTypeInfo ) ) + { + dependentOnType = dependentOnTypeInfo; + } + + if ( dependentOnType != null) { if ( propertyDependency.IsCollection ) { From 13e4e142a46fa82de54ce517749d138c0082e348 Mon Sep 17 00:00:00 2001 From: LuukN2 Date: Wed, 7 Nov 2018 16:08:53 +0100 Subject: [PATCH 10/12] Changed controller name source. --- .../AspNetCore.Mvc.ApiExplorer/PseudoModelBindingVisitor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc.ApiExplorer/PseudoModelBindingVisitor.cs b/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc.ApiExplorer/PseudoModelBindingVisitor.cs index 187e51a8..d46194b1 100644 --- a/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc.ApiExplorer/PseudoModelBindingVisitor.cs +++ b/src/Microsoft.AspNetCore.OData.Versioning.ApiExplorer/AspNetCore.Mvc.ApiExplorer/PseudoModelBindingVisitor.cs @@ -88,7 +88,7 @@ ApiParameterDescription CreateResult( ApiParameterDescriptionContext bindingCont { var action = (IEdmAction) Context.RouteContext.Operation; var apiVersion = Context.RouteContext.ApiVersion; - type = Context.TypeBuilder.NewActionParameters( Context.Services, action, apiVersion, Context.RouteContext.EntitySet.Name ); + type = Context.TypeBuilder.NewActionParameters( Context.Services, action, apiVersion, Context.RouteContext.ActionDescriptor.ControllerName ); } else { From 87dfd1a2833513baacb00886ab096ea9bf997fd7 Mon Sep 17 00:00:00 2001 From: LuukN2 Date: Thu, 8 Nov 2018 12:06:39 +0100 Subject: [PATCH 11/12] Added test cases for building custom attributes that take params parameters. --- .../AspNet.OData/AllowedRolesAttribute.cs | 18 +++++++++++++++ .../DefaultModelTypeBuilderTest.cs | 22 +++++++++++++++++++ .../AspNet.OData/Employee.cs | 1 + .../AspNet.OData/AllowedRolesAttribute.cs | 18 +++++++++++++++ .../DefaultModelTypeBuilderTest.cs | 22 +++++++++++++++++++ .../AspNet.OData/Employee.cs | 1 + 6 files changed, 82 insertions(+) create mode 100644 test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/AllowedRolesAttribute.cs create mode 100644 test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/AllowedRolesAttribute.cs diff --git a/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/AllowedRolesAttribute.cs b/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/AllowedRolesAttribute.cs new file mode 100644 index 00000000..b6ccb24b --- /dev/null +++ b/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/AllowedRolesAttribute.cs @@ -0,0 +1,18 @@ +namespace Microsoft.AspNet.OData +{ + using System; + using System.Collections.Generic; + using System.Linq; + + [AttributeUsage(AttributeTargets.Property)] + public class AllowedRolesAttribute : Attribute + { + + public AllowedRolesAttribute( params string[] parameters) + { + AllowedRoles = parameters.ToList(); + } + + public List AllowedRoles { get; } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/DefaultModelTypeBuilderTest.cs b/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/DefaultModelTypeBuilderTest.cs index 8d7e04cb..be763b15 100644 --- a/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/DefaultModelTypeBuilderTest.cs +++ b/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/DefaultModelTypeBuilderTest.cs @@ -311,6 +311,28 @@ public void substitute_should_generate_type_for_action_parameters_with_collectio substitutionType.Should().HaveProperty>( "topics" ); } + [Fact] + public void should_get_attributes_from_property_that_has_attributes_that_takes_params() + { + // arrange + var modelBuilder = new ODataConventionModelBuilder(); + var employee = modelBuilder.EntitySet( "Employees" ).EntityType; + employee.Ignore( e => e.FirstName ); + var originalType = typeof( Employee ); + + var context = NewContext( modelBuilder.GetEdmModel() ); + + // act + var substitutionType = originalType.SubstituteIfNecessary( context ); + + // assert + var property = substitutionType.GetRuntimeProperty( "Salary" ); + var attributeWithParams = property.GetCustomAttribute(); + + Assert.Equal( "Manager", attributeWithParams.AllowedRoles[0] ); + Assert.Equal( "Employer", attributeWithParams.AllowedRoles[1] ); + } + public static IEnumerable SubstitutionNotRequiredData { get diff --git a/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Employee.cs b/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Employee.cs index 4991efa3..923d5942 100644 --- a/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Employee.cs +++ b/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Employee.cs @@ -6,6 +6,7 @@ public class Employee public Employer Employer { get; set; } + [AllowedRoles("Manager", "Employer")] public decimal Salary { get; set; } public string FirstName { get; set; } diff --git a/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/AllowedRolesAttribute.cs b/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/AllowedRolesAttribute.cs new file mode 100644 index 00000000..b6ccb24b --- /dev/null +++ b/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/AllowedRolesAttribute.cs @@ -0,0 +1,18 @@ +namespace Microsoft.AspNet.OData +{ + using System; + using System.Collections.Generic; + using System.Linq; + + [AttributeUsage(AttributeTargets.Property)] + public class AllowedRolesAttribute : Attribute + { + + public AllowedRolesAttribute( params string[] parameters) + { + AllowedRoles = parameters.ToList(); + } + + public List AllowedRoles { get; } + } +} \ 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 adfcb219..fbce5bb9 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 @@ -311,6 +311,28 @@ public void substitute_should_generate_type_for_action_parameters_with_collectio substitutionType.Should().HaveProperty>( "topics" ); } + [Fact] + public void should_get_attributes_from_property_that_has_attributes_that_takes_params() + { + // arrange + var modelBuilder = new ODataConventionModelBuilder(); + var employee = modelBuilder.EntitySet( "Employees" ).EntityType; + employee.Ignore( e => e.FirstName ); + var originalType = typeof( Employee ); + + var context = NewContext( modelBuilder.GetEdmModel() ); + + // act + var substitutionType = originalType.SubstituteIfNecessary( context ); + + // assert + var property = substitutionType.GetRuntimeProperty( "Salary" ); + var attributeWithParams = property.GetCustomAttribute(); + + Assert.Equal( "Manager", attributeWithParams.AllowedRoles[0] ); + Assert.Equal( "Employer", attributeWithParams.AllowedRoles[1] ); + } + public static IEnumerable SubstitutionNotRequiredData { get diff --git a/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Employee.cs b/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Employee.cs index 4991efa3..923d5942 100644 --- a/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Employee.cs +++ b/test/Microsoft.AspNetCore.OData.Versioning.ApiExplorer.Tests/AspNet.OData/Employee.cs @@ -6,6 +6,7 @@ public class Employee public Employer Employer { get; set; } + [AllowedRoles("Manager", "Employer")] public decimal Salary { get; set; } public string FirstName { get; set; } From 3d3deae6e6e646e121959607a22aff7fc17b3f77 Mon Sep 17 00:00:00 2001 From: LuukN2 Date: Thu, 8 Nov 2018 12:56:55 +0100 Subject: [PATCH 12/12] Added test cases for generating types for action parameters with the same name for different entity sets. (controllers) --- .../DefaultModelTypeBuilderTest.cs | 46 ++++++++++++++++++- .../DefaultModelTypeBuilderTest.cs | 46 ++++++++++++++++++- 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/DefaultModelTypeBuilderTest.cs b/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/DefaultModelTypeBuilderTest.cs index be763b15..624e018a 100644 --- a/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/DefaultModelTypeBuilderTest.cs +++ b/test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/DefaultModelTypeBuilderTest.cs @@ -312,7 +312,51 @@ public void substitute_should_generate_type_for_action_parameters_with_collectio } [Fact] - public void should_get_attributes_from_property_that_has_attributes_that_takes_params() + public void substitute_should_generate_types_for_actions_with_the_same_name_in_different_controllers() + { + // arrange + var modelBuilder = new ODataConventionModelBuilder(); + var contact = modelBuilder.EntitySet( "Contacts" ).EntityType; + var action = contact.Action( "PlanMeeting" ); + + action.Parameter( "when" ); + action.CollectionParameter( "attendees" ); + action.CollectionParameter( "topics" ); + + var employee = modelBuilder.EntitySet( "Employees" ).EntityType; + action = employee.Action( "PlanMeeting" ); + + action.Parameter( "when" ); + action.CollectionParameter( "attendees" ); + action.Parameter( "project" ); + + var context = NewContext( modelBuilder.GetEdmModel() ); + var model = context.Model; + + var qualifiedName = $"{model.EntityContainer.Namespace}.{action.Name}"; + var operations = model.FindDeclaredOperations( qualifiedName ).Select( o => (IEdmAction) o ).ToArray(); + var services = new ServiceCollection(); + services.AddSingleton( model ); + + // act + var contactActionType = context.ModelTypeBuilder.NewActionParameters( services.BuildServiceProvider(), operations[0], ApiVersion.Default, contact.Name ); + var employeesActionType = context.ModelTypeBuilder.NewActionParameters( services.BuildServiceProvider(), operations[1], ApiVersion.Default, employee.Name ); + + // assert + contactActionType.Should().NotBe( employeesActionType ); + contactActionType.GetRuntimeProperties().Should().HaveCount( 3 ); + contactActionType.Should().HaveProperty( "when" ); + contactActionType.Should().HaveProperty>( "attendees" ); + contactActionType.Should().HaveProperty>( "topics" ); + + employeesActionType.GetRuntimeProperties().Should().HaveCount( 3 ); + employeesActionType.Should().HaveProperty( "when" ); + Assert.NotNull(employeesActionType.GetRuntimeProperty( "attendees" )); + employeesActionType.Should().HaveProperty( "project" ); + } + + [Fact] + public void substitute_should_get_attributes_from_property_that_has_attributes_that_takes_params() { // arrange var modelBuilder = new ODataConventionModelBuilder(); 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 fbce5bb9..5ddd1e43 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 @@ -312,7 +312,51 @@ public void substitute_should_generate_type_for_action_parameters_with_collectio } [Fact] - public void should_get_attributes_from_property_that_has_attributes_that_takes_params() + public void substitute_should_generate_types_for_actions_with_the_same_name_in_different_controllers() + { + // arrange + var modelBuilder = new ODataConventionModelBuilder(); + var contact = modelBuilder.EntitySet( "Contacts" ).EntityType; + var action = contact.Action( "PlanMeeting" ); + + action.Parameter( "when" ); + action.CollectionParameter( "attendees" ); + action.CollectionParameter( "topics" ); + + var employee = modelBuilder.EntitySet( "Employees" ).EntityType; + action = employee.Action( "PlanMeeting" ); + + action.Parameter( "when" ); + action.CollectionParameter( "attendees" ); + action.Parameter( "project" ); + + var context = NewContext( modelBuilder.GetEdmModel() ); + var model = context.Model; + + var qualifiedName = $"{model.EntityContainer.Namespace}.{action.Name}"; + var operations = model.FindDeclaredOperations( qualifiedName ).Select( o => (IEdmAction) o ).ToArray(); + var services = new ServiceCollection(); + services.AddSingleton( model ); + + // act + var contactActionType = context.ModelTypeBuilder.NewActionParameters( services.BuildServiceProvider(), operations[0], ApiVersion.Default, contact.Name ); + var employeesActionType = context.ModelTypeBuilder.NewActionParameters( services.BuildServiceProvider(), operations[1], ApiVersion.Default, employee.Name ); + + // assert + contactActionType.Should().NotBe( employeesActionType ); + contactActionType.GetRuntimeProperties().Should().HaveCount( 3 ); + contactActionType.Should().HaveProperty( "when" ); + contactActionType.Should().HaveProperty>( "attendees" ); + contactActionType.Should().HaveProperty>( "topics" ); + + employeesActionType.GetRuntimeProperties().Should().HaveCount( 3 ); + employeesActionType.Should().HaveProperty( "when" ); + Assert.NotNull(employeesActionType.GetRuntimeProperty( "attendees" )); + employeesActionType.Should().HaveProperty( "project" ); + } + + [Fact] + public void substitute_should_get_attributes_from_property_that_has_attributes_that_takes_params() { // arrange var modelBuilder = new ODataConventionModelBuilder();