Skip to content

OData routing causing exception with versioned API endpoints #1122

Open
@JackDC16

Description

@JackDC16

I have inherited a .NET 6 Web API project utilising OData and the Asp.Versioning packages. The versioning functionality was not 100% correct, and now that the project is required for more regular usage, I want to implement the versioning correctly.

The issue I am hitting is this runtime exception:

Attribute routes with the same name 'odata/v{version:apiVersion}/Parts' must have the same template: Action: '...v2.Controllers.PartsController.Get' - Template: 'odata/v{version:apiVersion}/Parts/odata/v{version:apiVersion}/Parts' Action: '...v2.Controllers.PartsController.Get' - Template: 'odata/v{version:apiVersion}/Parts' Action: '...v1.Controllers.PartsController.Get' - Template: 'odata/v{version:apiVersion}/Parts' Action: '...v1.Controllers.PartsController.Get' - Template: 'odata/v{version:apiVersion}/Parts'

My controllers look like:

namespace ...Api.Versions.v1.Controllers
{
    [ApiExplorerSettings(GroupName = "v1")]
    [ApiVersion("1")]
    [Route("odata/v{version:apiVersion}/[controller]")]
    public class PartsController : ODataController
    {
        [Produces("application/json")]
        [EnableQuery]
        [HttpGet]
        public async Task<IQueryable<Part>> Get()
        {
            ...
        }
    }
}

and

namespace ...Api.Versions.v2.Controllers
{
    [ApiExplorerSettings(GroupName = "v2")]
    [ApiVersion("2")]
    [Route("odata/v{version:apiVersion}/[controller]")]
    public class PartsController : ODataController
    {
        [Produces("application/json")]
        [EnableQuery]
        [HttpGet]
        public async Task<IQueryable<Part>> Get()
        {
            ...
        }
    }
}

My Startup.cs has the following configuration:

public void ConfigureServices(IServiceCollection services)
{
   ...
    services.AddControllers()
    .AddOData(options =>
    {
        options.EnableQueryFeatures(maxTopValue: 8000);
        options.TimeZone = TimeZoneInfo.Utc;
    });

    services.AddApiVersioning(options =>
    {
        options.AssumeDefaultVersionWhenUnspecified = true;
        options.DefaultApiVersion = new ApiVersion(1);
        options.ReportApiVersions = true;
        options.ApiVersionReader = new UrlSegmentApiVersionReader();
    })
    .AddMvc()
    .AddOData(options =>
    {
        options.AddRouteComponents("odata/v{version:apiVersion}");
    })
    .AddODataApiExplorer(options =>
    {
        options.GroupNameFormat = "'v'V";
        options.SubstituteApiVersionInUrl = true;
    });
    ...
}

public void Configure(IApplicationBuilder app)
{
    ...
    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
    ...
}

and my EDM configuration is as follows:

namespace ...Api.Utilities.ModelConfiguration
{
    using Asp.Versioning;
    using Asp.Versioning.OData;
    using Microsoft.OData.ModelBuilder;

    public class PartModelConfiguration : IModelConfiguration
    {
        public void Apply(ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix)
        {
            switch (apiVersion.MajorVersion)
            {
                case 1:
                    ConfigureV1(builder);
                    break;
                case 2:
                    ConfigureV2(builder);
                    break;
                default:
                    ConfigureCurrent(builder);
                    break;
            }
        }

        private void ConfigureV1(ODataModelBuilder builder) =>
            ConfigureCurrent(builder);

        private void ConfigureV2(ODataModelBuilder builder) =>
            ConfigureCurrent(builder);

        private EntityTypeConfiguration<Part> ConfigureCurrent(ODataModelBuilder builder)
        {
            var part = builder.EntitySet<Part>("Parts").EntityType;
            part.HasKey(p => p.IdPart);
            return part;
        }
    }
}

Right now there is no difference between models for v1 and v2, this is purely to test functionality.

I'm trusting that the above configuration is automatically registered for DI as per the last section on this page: https://github.com/dotnet/aspnet-api-versioning/wiki/OData-Model-Configurations

I feel like there is some small piece of config that I've missed, but struggling to find what that might be.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions