Skip to content

Action parameter gets defined twice when using multiple odata routes #555

Closed
@martindafonte

Description

@martindafonte

Hi, I'm building a sample OData project with versioning in AspNet Core. And I've encountered a problem when trying to define an Action on one of my controllers.

When trying to build the swagger.json file the following Exception is thrown:

Activated	Event	Time	Duration	Thread
	Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware:Error: An unhandled exception has occurred while executing the request.

System.ArgumentException: Duplicate type name within an assembly.
   at System.Reflection.Emit.ModuleBuilder.CheckTypeNameConflict(String strTypeName, Type enclosingType)
   at System.Reflection.Emit.AssemblyBuilderData.CheckTypeNameConflict(String strTypeName, TypeBuilder enclosingType)
   at System.Reflection.Emit.TypeBuilder.Init(String fullname, TypeAttributes attr, Type parent, Type[] interfaces, ModuleBuilder module, PackingSize iPackingSize, Int32 iTypeSize, TypeBuilder enclosingType)
   at System.Reflection.Emit.ModuleBuilder.DefineType(String name, TypeAttributes attr)
   at Microsoft.AspNet.OData.DefaultModelTypeBuilder.CreateTypeBuilderFromSignature(ModuleBuilder moduleBuilder, ClassSignature class) in E:\BA\56\s\src\Common.OData.ApiExplorer\AspNet.OData\DefaultModelTypeBuilder.cs:line 236
   at Microsoft.AspNet.OData.DefaultModelTypeBuilder.NewActionParameters(IServiceProvider services, IEdmAction action, ApiVersion apiVersion, String controllerName) in E:\BA\56\s\src\Common.OData.ApiExplorer\AspNet.OData\DefaultModelTypeBuilder.cs:line 60
   at Microsoft.AspNetCore.Mvc.ApiExplorer.PseudoModelBindingVisitor.CreateResult(ApiParameterDescriptionContext bindingContext, BindingSource source, String containerName) in E:\BA\56\s\src\Microsoft.AspNetCore.OData.Versioning.ApiExplorer\AspNetCore.Mvc.ApiExplorer\PseudoModelBindingVisitor.cs:line 85
   at Microsoft.AspNetCore.Mvc.ApiExplorer.PseudoModelBindingVisitor.Visit(ApiParameterDescriptionContext bindingContext, BindingSource ambientSource, String containerName) in E:\BA\56\s\src\Microsoft.AspNetCore.OData.Versioning.ApiExplorer\AspNetCore.Mvc.ApiExplorer\PseudoModelBindingVisitor.cs:line 45
   at Microsoft.AspNetCore.Mvc.ApiExplorer.ODataApiDescriptionProvider.GetParameters(ApiParameterContext context) in E:\BA\56\s\src\Microsoft.AspNetCore.OData.Versioning.ApiExplorer\AspNetCore.Mvc.ApiExplorer\ODataApiDescriptionProvider.cs:line 447
   at Microsoft.AspNetCore.Mvc.ApiExplorer.ODataApiDescriptionProvider.NewODataApiDescriptions(ControllerActionDescriptor action, String groupName, ODataRouteMapping mapping)+MoveNext() in E:\BA\56\s\src\Microsoft.AspNetCore.OData.Versioning.ApiExplorer\AspNetCore.Mvc.ApiExplorer\ODataApiDescriptionProvider.cs:line 388
   at Microsoft.AspNetCore.Mvc.ApiExplorer.ODataApiDescriptionProvider.OnProvidersExecuted(ApiDescriptionProviderContext context) in E:\BA\56\s\src\Microsoft.AspNetCore.OData.Versioning.ApiExplorer\AspNetCore.Mvc.ApiExplorer\ODataApiDescriptionProvider.cs:line 197
   at Microsoft.AspNetCore.Mvc.ApiExplorer.ApiDescriptionGroupCollectionProvider.GetCollection(ActionDescriptorCollection actionDescriptors)
   at Microsoft.AspNetCore.Mvc.ApiExplorer.ApiDescriptionGroupCollectionProvider.get_ApiDescriptionGroups()
   at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GetSwagger(String documentName, String host, String basePath, String[] schemes)
   at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context) 	3.75s		

I declare two routes one for access by the users and another one for the services running by other components of the application.
My startup looks like this:

            modelBuilder.DefaultModelConfiguration = (b, v) => UniversityModelBuilder.BuildEdmModel(b, v);
            var models = modelBuilder.GetEdmModels();
            app.UseMvc(builder =>
                        {
                            builder.Select().Expand().Filter().OrderBy().MaxTop(100).Count();
                            builder.SetUrlKeyDelimiter(ODataUrlKeyDelimiter.Slash);
                            builder.MapVersionedODataRoutes("tenant", "tenant/{tenant}", models);
                            builder.MapVersionedODataRoutes("host", "host", models);
                            builder.MapRoute(name: "default",
                                template: "{controller=Documentation}/{action=Index}");
                        });

If I comment the line builder.MapVersionedODataRoutes("host", "host", models); everything works.

The model where I declare my Action is:

if (v == Versiones.V2)
            {
                var sequence = modelBuilder.EntitySet<Secuencia>("Sequence").EntityType;
                sequence.HasKey(x => x.Id);
                sequence
                    .Action("AdvanceSequence")
                    .Returns<SequenceResult>()
                    .Parameter<int>("Ammount").HasDefaultValue("1");
            }

And the controller is:

    [ODataRoutePrefix(nameof(Secuencia)), V2]
    public class SequenceController : ODataController
    {
        private readonly UniveristyContext _db;
        private readonly DbSet<Secuencia> _dbSet;
        private readonly ISequenceService _seqService;

        public SequenceController(UniveristyContext db, ISequenceService seqService)
        {
            _db = db;
            _dbSet = _db.Set<Secuencia>();
            _seqService = seqService;
        }

        [HttpPost]
        public async Task<IActionResult> AdvanceSequence([FromODataUri] string key, ODataActionParameters param)
        {
            int ammount = 0;
            if (param != null)
            {
                param.TryGetValue("Ammount", out object ammountObj);
                ammount = (int)ammountObj;
            }
            return Created(_seqService.GetNumbers(key, (int)ammount));
        }

I tried to debug the problem using the source code and the problem seems to be that the method NewActionParameters is called once for each route declared, and doesn't check if the parameter was defined previously in the moduleBuilder, or use a different key for every mapping

        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<Type>() != null );

            var name = controllerName + "." + action.FullName() + "Parameters";
            var properties = action.Parameters.Where( p => p.Name != "bindingParameter" ).Select( p => new ClassProperty( services, p, this ) );
            var signature = new ClassSignature( name, properties, apiVersion );
            var moduleBuilder = modules.GetOrAdd( apiVersion, CreateModuleForApiVersion );

            return CreateTypeInfoFromSignature( moduleBuilder, signature );
        }
        static TypeInfo CreateTypeInfoFromSignature( ModuleBuilder moduleBuilder, ClassSignature @class ) => CreateTypeBuilderFromSignature( moduleBuilder, @class ) .GetTypeInfo();

static TypeBuilder CreateTypeBuilderFromSignature( ModuleBuilder moduleBuilder, ClassSignature @class )
        {
            Contract.Requires( moduleBuilder != null );
            Contract.Requires( @class != null );
            Contract.Ensures( Contract.Result<TypeBuilder>() != null );

            var typeBuilder = moduleBuilder.DefineType( @class.Name, TypeAttributes.Class );

            foreach ( var attribute in @class.Attributes )
            {
                typeBuilder.SetCustomAttribute( attribute );
            }
            foreach ( var property in @class.Properties )
            {
                var type = property.Type;
                var name = property.Name;
                var propertyBuilder = AddProperty( typeBuilder, type, name );

                foreach ( var attribute in property.Attributes )
                {
                    propertyBuilder.SetCustomAttribute( attribute );
                }
            }
            return typeBuilder;
        }

Maybe changing this part can fix the issue

static TypeInfo CreateTypeInfoFromSignature( ModuleBuilder moduleBuilder, ClassSignature @class ) =>
            ( moduleBuilder.GetType( @class.Name, false, false ) ?? CreateTypeBuilderFromSignature( moduleBuilder, @class ) ).GetTypeInfo();

Metadata

Metadata

Labels

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions