
Representing available string values for parameters as list in Swagger ASP.NET Core
Valid values as list in Swagger UI in ASP.NET Core
Swagger is a great way do describe your RESTful API. In ASP.NET it is pretty easy to wire up your OpenAPI documentation with your service facade with Swashbuckle NuGet package.
In .NET 5, WebApi project template comes with already built in support for OpenAPI via Swashbuckle package and you can get it already setup in your pipeline and dependency injection with an easy tick in new WebApi project in Visual Studio.
This way you get instantly Swagger UI working in your WebApi project which makes testing your endpoint much easier as you do not need to use any external tool like Postman or Nightingale REST client for checking the functionality and code flow of your service you are working on.
One feature built into Swagger UI is Enum representation in UI. Instead of having to remember values and names of an Enum, you can simply tell Swagger to represent it as a list of Enum names. It works great, but Enum items are constant and they do not change overtime. They are basically hard-coded in your code.
Quite often your application will request from the client string value which needs to be validated against some list of supported values. This can be hard for the client party which is using Swagger UI to test the endpoints and shape the client code to your API documentation. Unfortunately this is not supported in Swagger out of the box, but it is not hard to implement it either.
For this article I will use project I used in article Accessing multiple databases from the same DbContext in EF Core to save some time because the DbContext is already setup in the Startup class.
Setting up Enum list support in Swagger UI and FluentValidation of the model
Let's first see what we want to achieve as a final result. As I mentioned I wanted to represent list of valid values, in this case values from the database table in the same fashion as Enum values.
In this simple sample API, since we already have planets, I created an endpoint GET /rockets/{rocket} which will simply return string message that rocket landed to specified planet using specified fuel type. It takes three mandatory parameters:
- rocket (String) - name of the rocket to be launched
- fuelType (Enum) - type of the fuel rocket uses
- planet (String) - name of the planet in Solar system to launch rocket to
First things first, we need Enum for fuelType parameter
public enum FuelTypeEnum { Solid, Liquid }
And we have the query model in GET which will be generated from the query string of the request
public class RocketQueryModel { [Required] public FuelTypeEnum FuelType { get; set; } [Required] public String Planet { get; set; } }
We do not want have any value passed as a planet name so we want to validate that against the database values. For this purpose I used FluentValidation package to validate planet value against values in the database table previously populated.
I first added all the packages we are going to need to reference for the validation
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>net5.0</TargetFramework> </PropertyGroup> <ItemGroup> <PackageReference Include="FluentValidation" Version="9.3.0" /> <PackageReference Include="FluentValidation.AspNetCore" Version="9.3.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" /> <PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="5.6.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.0" NoWarn="NU1605" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.0" NoWarn="NU1605" /> </ItemGroup> </Project>
Before we create controller and method for GET endpoint, we need to implement the validation for the planet name value. This is quite easy with FluentValidation AbstractValidator class implementation for our RocketQueryModel DTO.
public class RocketQueryModelValidator : AbstractValidator<RocketQueryModel> { public RocketQueryModelValidator(Database1Context database1Context) { RuleFor(q => q.Planet).NotEmpty().NotNull().WithMessage("Planet name is required"); When(q => !String.IsNullOrEmpty(q.Planet), () => { RuleFor(p => p).Custom((value, context) => { if (!database1Context.Planets.AsNoTracking().Any(p => p.Name == value.Planet)) { context.AddFailure($"Planet {value.Planet} does not exist in solar system"); } }); }); } }
One last step before we actually add the controller is to wire up Enum support and fluent validation in ConfigureServices method in Startup.cs.
public void ConfigureServices(IServiceCollection services) { ...... services.AddControllers() .AddNewtonsoftJson(options => { options.SerializerSettings.Converters.Add(new StringEnumConverter()); }) .AddFluentValidation(); services.AddTransient<IValidator<RocketQueryModel>, RocketQueryModelValidator>(); services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "MulitpleDb.Sample", Version = "v1" }); }).AddSwaggerGenNewtonsoftSupport(); }
Finally we can add our controller and test it out in the Swagger UI
[Route("api/[controller]")] [ApiController] public class RocketsController : ControllerBase { [HttpGet("{rocket}")] public async Task<String> Get( [FromRoute]String rocket, [FromQuery]RocketQueryModel query) { return $"Rocket {rocket} launched to {query.Planet} using {Enum.GetName(typeof(FuelTypeEnum), query.FuelType)} fuel type"; } }
For the testing purposes I already have some data that I used in Running multiple queries at the same time in EF Core article.
Our planet name value in the query model will be validated against these values, so for initial test let's got with a happy path where all values are met.
We used value "Mars" for the planet name in the query and we got the proper 200 OK response. Now let's try to use invalid value for the planet and see if the validation kicks in and returns the proper response.
Response from the API endpoint is as expected, 400 Bad request which is result of invalid planet name validated against the database table values in FluetnValidator registered for the RocketQueryModel DTO.
You see the obvious problem here. If you compare the input in Swagger UI for the Fuel Type vs. Planet you see that user is limited with options for the Fuel Type as it is dictated by Enum values which are declared in the code.
Planets are coming from the database, so therefore they cannot be rendered as a list, well not out of the box at least. Next thing we are going to so is to make the planets input same as for the FuelTypeEnum parameter and that is drop down list.
Rendering the list for available string values in Swagger UI
The easiest way to control parameters description and therefore rendering of them in Swagger UI is to implement our own IParameterFilter implementation where we are going to add items to Enum OpenApi schema by altering the OpenApiParameter parameter of Apply method.
There is one problem though with injection DbContext in the constructor. We are registering parameter filter in ConfigureServices method by invoking ParameterFilter of the instance of SwaggerGenOptions class. The only way we get instance of the DbContext is to build ServiceProvider by invoking BuildServiceProvider method on the IServiceCollection instance and then call GetRequiredService<T> to get the DbContext instance which we do not want to do.
Instead, similar to what I did in Running multiple queries at the same time in EF Core article, I will inject instance of IServiceScopeFactory in the constructor of IParemeterFilter instance and use it to create new scope every time that is required which is in this case when the parameter name is "Planet".
public class PlanetsParameterFilter : IParameterFilter { readonly IServiceScopeFactory _serviceScopeFactory; public PlanetsParameterFilter(IServiceScopeFactory serviceScopeFactory) { _serviceScopeFactory = serviceScopeFactory; } public void Apply(OpenApiParameter parameter, ParameterFilterContext context) { if (parameter.Name.Equals("planet", StringComparison.InvariantCultureIgnoreCase)) { using (var scope = _serviceScopeFactory.CreateScope()) { var planetsContext = scope.ServiceProvider.GetRequiredService<Database1Context>(); IEnumerable<Planet> planets = planetsContext.Planets.ToArray(); parameter.Schema.Enum = planets.Select(p => new OpenApiString(p.Name)).ToList<IOpenApiAny>(); } } } }
As a last step we just need to add our parameter filter to Swagger UI configuration in ConfigureServices method in Startup.cs
public void ConfigureServices(IServiceCollection services) { ..... services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "MulitpleDb.Sample", Version = "v1" }); c.ParameterFilter<PlanetsParameterFilter>(); }).AddSwaggerGenNewtonsoftSupport(); }
Now when we run our service, planet parameter will be rendered as a list of string values retrieved from the database.
References
Disclaimer
Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.
Comments for this article