Search code examples
asp.netasp.net-coreasp.net-web-apiodata

How to pass ODataQueryOptions to Mediator while allowing input for OData queries only in Swagger?


I'm using OData with ASP.NET Core and Mediator to handle filtering, sorting, etc. of my Person entities. In my controller, I have the following code:

[HttpGet]
public async Task<IActionResult> Get(ODataQueryOptions<Person> queryOptions, CancellationToken cancellationToken)
{
    var query = new GetFilteredPersonQuery(queryOptions);
    var result = await _mediator.Send(query, cancellationToken);
    return Ok(result);
}

The ODataQueryOptions are passed from the client via query string parameters. In swagger it looks like thisSwagger screenshot I would like to make it so that in Swagger, users can enter only the relevant OData query parameters (e.g., $filter, $orderby, etc.) without having to fill out the entire ODataQueryOptions object.

Is there a way to achieve this behavior while passing ODataQueryOptions to the Mediator?


Solution

  • You could add [SwaggerIgnore] attribute before ODataQueryOptions and add required query parameter like [FromQuery(Name ="$orderby")] string? orderby in your controller action:

    A minimal example:

    Project file:

    <Project Sdk="Microsoft.NET.Sdk.Web">
    
      <PropertyGroup>
        <TargetFramework>net8.0</TargetFramework>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
      </PropertyGroup>
    
      <ItemGroup>
        <PackageReference Include="Microsoft.AspNetCore.OData" Version="8.2.7" />
        <PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
      </ItemGroup>
    
    </Project>
    

    Program.cs:

    var builder = WebApplication.CreateBuilder(args);
    
    // Add services to the container.
    
    builder.Services.AddControllers();
    
    var modelBuilder = new ODataConventionModelBuilder();
    modelBuilder.EntitySet<Customer>("Customers");
    
    builder.Services.AddControllers().AddOData(
        options => options.Select().Filter().OrderBy().Expand().SetMaxTop(null).AddRouteComponents(
            routePrefix: "odata",
            model: modelBuilder.GetEdmModel()));
    // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
    builder.Services.AddEndpointsApiExplorer();
    
    builder.Services.AddSwaggerGen();
    
    
    var app = builder.Build();
    
    // Configure the HTTP request pipeline.
    if (app.Environment.IsDevelopment())
    {
        app.UseSwagger();
        app.UseSwaggerUI();
    }
    
    app.UseHttpsRedirection();
    
    app.UseAuthorization();
    
    app.MapControllers();
    
    app.Run();
    

    Models:

    public class Customer
    {
        [Key]
        public int Id { get; set; }
        public String Name { get; set; }
        public int Age { get; set; }
        public List<Order> Orders { get; set; }
    }
    
    public class Order
    {
        [Key]
        public int Id { get; set; }
        public int Price { get; set; }
        public int Quantity { get; set; }
    }
    

    Controller:

    public class CustomersController : ODataController
    {
        private static List<Order> Orders = new List<Order>
    {
        new Order { Id = 1001, Price = 10, Quantity = 10 },
        new Order { Id = 1002, Price = 35, Quantity = 2 },
        new Order { Id = 1003, Price = 70, Quantity = 5 },
        new Order { Id = 1004, Price = 20, Quantity = 20 },
        new Order { Id = 1005, Price = 40, Quantity = 15 },
        new Order { Id = 1006, Price = 15, Quantity = 50 },
    };
    
        private static List<Customer> Customers = new List<Customer>
    {
        new Customer { Id = 1, Name = "Customer 1", Age = 33, Orders = new List<Order>(){Orders[0], Orders[1]} },
        new Customer { Id = 2, Name = "Customer 2", Age = 32, Orders = new List<Order>(){Orders[2], Orders[3]} },
        new Customer { Id = 3, Name = "Customer 3", Age = 31, Orders = new List<Order>(){Orders[4], Orders[5]} }
    };
    
        
        public IQueryable<Customer> Get([SwaggerIgnore] ODataQueryOptions<Customer> options,
            [FromQuery(Name ="$orderby")] string? orderby,
            [FromQuery(Name = "$skip")] string? skip,
            [FromQuery(Name = "$filter")] string? filter)
        {
            IQueryable results =options.ApplyTo(Customers.AsQueryable(), new ODataQuerySettings());
    
            return results as IQueryable<Customer>;
        }
    }
    

    Result:

    enter image description here

    enter image description here

    If there's many controllers in your project ,you may create an OperationFilter instead of adding parameters one by one:

    public class ODataQueryFilter : IOperationFilter
    {
        public void Apply(OpenApiOperation operation, OperationFilterContext context)
        {
            if (operation.Parameters == null) operation.Parameters = new List<OpenApiParameter>();
    
            var descriptor = context.ApiDescription.ActionDescriptor as ControllerActionDescriptor;
            // replace the logic here
            if (descriptor != null && descriptor.ControllerName.Contains("Customer"))
            {
                operation.Parameters.Add(new OpenApiParameter()
                {
                    Name = "$orderby",
                    In = ParameterLocation.Query,
                    Description = "OdataQuery",
                    Required = false
                });
    
                operation.Parameters.Add(new OpenApiParameter()
                {
                    Name = "$skip",
                    In = ParameterLocation.Query,
                    Description = "OdataQuery",
                    Required = false
                });
                operation.Parameters.Add(new OpenApiParameter()
                {
                    Name = "$skip",
                    In = ParameterLocation.Query,
                    Description = "OdataQuery",
                    Required = false
                });
            }
        }
    }
    

    Register it :

    builder.Services.AddSwaggerGen(c=>c.OperationFilter<ODataQueryFilter>() );