OData / OData/AspNetCoreOData

IUrlHelper.Link returns null for OData route name

Open
#605 4 comments 0 reactions 1 assignee View on GitHub

@xuzhg is already working on this.

Since Jul 11, 2022.

investigated
Dominant language
C#
Stars
505
Forks
186
PR merge metrics
No merged PRs in 30d

Description

Assemblies affected
Microsoft.AspNetCore.OData 8.0.10

Describe the bug
Failed to generate absolute URL for the specified OData route name.

Reproduce steps

  1. Create Asp.Net Core Web API project
  2. Include Nuget packages:
  • Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer 5.0.0
  • Microsoft.AspNetCore.OData 8.0.10
  • Microsoft.AspNetCore.OData.NewtonsoftJson 8.0.4
  • Swashbuckle.AspNetCore 5.6.3
  • Swashbuckle.AspNetCore.Newtonsoft 6.3.1
  1. Add ResourceData
  2. Define routes
  3. Add two controllers:
  • ResourceManagementController - non-OData controller for managing resources
  • ResourceDataController - OData controller for resource data requests
  1. Start application

Repo: https://github.com/ivanenkomaksym/ODataCoreUrlHelper

Data Model
ResourceData type:

    public class ResourceData
    {
        public string Id { get; init; }
    }

Define routes:

    internal static class Routes
    {
        public const string VersionedRoutePrefix = "api/v{version:apiVersion}";

        public const string ResourceDataControllerRoute = "api/v{version:apiVersion}/resources/{token}";

        public const string GetResourceDataRoute = nameof(ResourceDataController) + "_" + nameof(ResourceDataController.GetData);
    }

Non-OData controller for managing resources:

    [ApiController]
    [ApiVersion("1.0")]
    [Route(Routes.VersionedRoutePrefix)]
    public class ResourceManagementController : ControllerBase
    {
        [HttpPost("resources")]
        public IActionResult Create(ApiVersion version)
        {
            var location = Url.Link(Routes.GetResourceDataRoute, new
            {
                token = "id1",
                version = version.ToString()
            });

            return Created(location, "id1");
        }
    }

When creating a resource I want to already include in Location response headers URL link to OData resource data request endpoint.

OData controller for resource data requests:

    [ApiController]
    [ApiVersion("1.0")]
    [Produces(MediaTypeNames.Application.Json)]
    [EnableQuery(AllowedQueryOptions = AllowedQueryOptions.Select | AllowedQueryOptions.Count | AllowedQueryOptions.Skip | AllowedQueryOptions.Top | AllowedQueryOptions.Expand)]
    [Route(Routes.ResourceDataControllerRoute)]
    public class ResourceDataController : ODataController
    {
        [HttpGet("data", Name = Routes.GetResourceDataRoute)]
        [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ResourceData))]
        public IActionResult GetData(string token)
        {
            var resource = new ResourceData
            {
                Id = token
            };

            return Ok(resource);
        }
    }

Configure Startup.cs:

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers().AddOData(options =>
            {
                options.Select().Count().SkipToken().Expand().SetMaxTop(10);
                options.AddRouteComponents(Routes.ResourceDataControllerRoute, BuildEdmModel());
            })
            .AddODataNewtonsoftJson();

            services.AddApiVersioning(options => options.AssumeDefaultVersionWhenUnspecified = false);
            services.AddVersionedApiExplorer(options => options.SubstituteApiVersionInUrl = true);

            services.AddSwaggerGen(setupAction =>
            {
                var provider = services.BuildServiceProvider().GetRequiredService<IApiVersionDescriptionProvider>();

                foreach (var description in provider.ApiVersionDescriptions)
                {
                    var info = new OpenApiInfo
                    {
                        Title = "ODataCoreUrlHelper",
                        Version = description.ApiVersion.ToString()
                    };
                    setupAction.SwaggerDoc(description.GroupName, info);
                }
            });

            services.AddSwaggerGenNewtonsoftSupport();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider versionDescription)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseODataRouteDebug();

            app.UseHttpsRedirection();

            app.UseRouting();

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

            app.UseSwagger();
            app.UseSwaggerUI(setupAction =>
            {
                foreach (var description in versionDescription.ApiVersionDescriptions)
                {
                    setupAction.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", $"Test v{description.GroupName}");
                }
            });
        }

        private static IEdmModel BuildEdmModel()
        {
            var builder = new ODataConventionModelBuilder();

            builder.EntitySet<ResourceData>("data");

            return builder.GetEdmModel();
        }

    }

Request/Response

  1. https://localhost:5001/api/v1/resources/1/data - works as expected
    image
  2. https://localhost:5001/api/v1/resources - fails
    image

Expected behavior
It is expected that

            var location = Url.Link(Routes.GetResourceDataRoute, new
            {
                token = "id1",
                version = version.ToString()
            });

correctly generates URL to OData controller endpoint defined as

[HttpGet("data", Name = Routes.GetResourceDataRoute)]

Additional context
If commenting out OData route configuration

            services.AddControllers().AddOData(options =>
            {
                options.Select().Count().SkipToken().Expand().SetMaxTop(10);
                //options.AddRouteComponents(Routes.ResourceDataControllerRoute, BuildEdmModel());
            })
  1. https://localhost:5001/api/v1/resources - works as expected and includes URL location of https://localhost:5001/api/v1/resources/1/data in response headers
    image
  2. https://localhost:5001/api/v1/resources/1/data - doesn't anymore include OData context
    image

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.