Minimal API in .NET 6 using multiple files - c#

In .NET 6 it is possible to create minimal APIs:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/products/{id}", (int id) => { return Results.Ok(); })
app.MapGet("/users/{id}", (int id) => { return Results.Ok(); })
app.Run();
What would be an approach to group endpoints in multiple files instead of having all in Program file?
ProductEndpoints.cs:
app.MapGet("/products/{id}", (int id) => { return Results.Ok(); })
UserEndpoints.cs
app.MapGet("/users/{id}", (int id) => { return Results.Ok(); })

Only one file with top-level statement is allowed per project. But nobody forbids moving endpoints to some static method of another class:
public static class ProductEndpointsExt
{
public static void MapProductEndpoints(this WebApplication app)
{
app.MapGet("/products/{id}", (int id) => { return Results.Ok(); });
}
}
And in the Program file:
app.MapProductEndpoints();

We can use partial Program.cs files too
Example: "Program.Users.cs"
partial class Program
{
/// <summary>
/// Map all users routes
/// </summary>
/// <param name="app"></param>
private static void AddUsers(WebApplication app)
{
app.MapGet("/users", () => "All users");
app.MapGet("/user/{id?}", (int? id) => $"A users {id}");
///post, patch, delete...
}
}
And in "Program.cs"
...
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
//add...
AddUsers(app);
...

What I did is creating a interface IEndPoint that each class that need to define endpoints must implement, and an extension method to find all implementations to call the interface mapping method.
You just have to call that extension method in your Program.cs or Startup to register all the endpoints.
// IEndpoint.cs
public interface IEndPoint
{
void MapEndpoint(WebApplication app);
}
// FeatureA.cs
public class FeatureA: IEndPoint
{
public void MapEndpoint(WebApplication app)
{
app.MapGet("api/FeatureA/{id}", async (int id) => $"Fetching {id} data");
}
}
// WebApplicationExtension.cs
public static class WebApplicationExtensions
{
public static void MapEndpoint(this WebApplication app)
{
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
var classes = assemblies.Distinct().SelectMany(x => x.GetTypes())
.Where(x => typeof(IEndPoint).IsAssignableFrom(x) && !x.IsInterface && !x.IsAbstract);
foreach (var classe in classes)
{
var instance = Activator.CreateInstance(classe) as IEndPoint;
instance?.MapEndpoint(app);
}
}
}
// Program.cs
...
app.MapEndpoint();
...

I think the best way is to use Controller based web service. Although, you can this approach like this:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.MapWeatherForecastRoutes();
app.Run();
internal static class WeatherForecastController
{
internal static void MapWeatherForecastRoutes(this WebApplication app)
{
app.MapGet("/weatherforecast", () =>
{
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();
}
}
internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
The only thing we need to consider is how to make the best use of extension methods.
It is enough to implement each group of web services in a static class and add them to the program using Extension methods.

Another option is to use Carter project
Add carter project to Nuget dotnet add package carter
Modify Program.cs to use carter
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCarter();
var app = builder.Build();
app.MapCarter();
app.Run();
Notice that .AddControllers() can be removed
Add Carter Module, it will be later auto-discovered
using Carter;
using MapEndpoints;
public class WeatherModule : ICarterModule
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
public void AddRoutes(IEndpointRouteBuilder app)
{
app.MapGet("/GetWeatherForecast", (ILoggerFactory loggerFactory) => Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray());
}
}
public class WeatherForecast
{
public DateTime Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
public string? Summary { get; set; }
}

Related

Problems encountered using Autofac

Program.cs
var builder = WebApplication.CreateBuilder(args);
// Use Autofac
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Replace the default controller activator
//builder.Services.Replace(ServiceDescriptor.Transient<IControllerActivator, CustomControllerActivator>());
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseAuthorization();
app.MapControllers();
app.Run();
CustomControllerActivator.cs
public class CustomControllerActivator : IControllerActivator
{
public object Create(ControllerContext context)
{
// `serviceProvider` the object is `Autofac.Extensions.DependencyInjection.AutofacServiceProvider` type
var serviceProvider = context.HttpContext.RequestServices;
...
}
public void Release(ControllerContext context, object controller)
{
...
}
}
WeatherForecastController.cs
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
private readonly IServiceScope serviceProvider;
public WeatherForecastController(ILogger<WeatherForecastController> logger,IServiceProvider serviceProvider)
{
_logger = logger;
//`serviceProvider` the object is `Autofac.Extensions.DependencyInjection.AutofacServiceProvider` type
serviceProvider = serviceProvider;
}
[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
}
I checked the source code and didn't see you replace 'IControllerActivator' or 'IControllerActivatorProvider', so I don't quite understand how you operate the activation of the controller
I don't understand how you replace the object of 'HttpContext.RequestServices' with the type of 'Autofac.Extensions.DependencyInjection.AutofacServiceProvider'
Thank you very much for asking for relief

IContainerBuilder.AddService is not working in IRouteBuilder OData .Net Core in Adding EdmModel

I was using this part of code in OData .Net. This code is working in .Net Core. Here is my configuration class:
public static class Configuration
{
public static Action<IApplicationBuilder> ConvertToAppBuilder(Action<object> myActionT)
{
if (myActionT == null) return null;
else return new Action<object>(o => myActionT(o));
}
public static Action<IApplicationBuilder> GetBuilder()
{
return ConvertToAppBuilder(Configure);
}
public static void Configure(object appBuilder)
{
var app = appBuilder as IApplicationBuilder;
var config = new RouteBuilder(app);
var builder = new ODataConventionModelBuilder(config.ApplicationBuilder.ApplicationServices) { Namespace = "Model.Entities", ContainerName = "DefaultContainer" };
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseMvc(routeBuilder =>
{
routeBuilder.Select().Filter();
routeBuilder.MapODataServiceRoute("odata", "odata", configureAction: c => c
.AddService(Microsoft.OData.ServiceLifetime.Transient, typeof(IEdmModel), sp => GetEdmModel()));
});
}
static IEdmModel GetEdmModel()
{
var odataBuilder = new ODataConventionModelBuilder();
odataBuilder.EntitySet<Student>("Student");
return odataBuilder.GetEdmModel();
}
}
It is working when I use the first constructor
app.UseMvc(routeBuilder =>
{
routeBuilder.Select().Filter();
routeBuilder.MapODataServiceRoute("odata", "odata", GetEdmModel());
});
But I need to use the second constructor because I will add there custom ODataUriResolver. So how can I fix this issue?
I had the same problem. Seams source code has issue on adding custom convension. Following code worked on me:
routeBuilder.MapODataRoute("OData", "odata", b =>
{
b.AddService(Microsoft.OData.ServiceLifetime.Singleton, sp => edmModel);
var customRoutingConvention = new ODataCustomRoutingConvention();
var conventions = ODataRoutingConventions.CreateDefault();
//Workaround for https://github.com/OData/WebApi/issues/1622
conventions.Insert(0, new AttributeRoutingConvention("OData", app.ApplicationServices, new DefaultODataPathHandler()));
//Custom Convention
conventions.Insert(0, customRoutingConvention);
b.AddService<IEnumerable<IODataRoutingConvention>>(Microsoft.OData.ServiceLifetime.Singleton, a => conventions);
});

How to add Swagger in OData-enabled Web API running on ASP.NET Core 3.1

I want to use both OData and Swagger in my Web API. I'm running ASP.NET Core 3.1.
I have found these articles, one to enable OData and another to enable SwaggerUI
Enable OData: https://devblogs.microsoft.com/odata/enabling-endpoint-routing-in-odata/
Enable Swagger: https://www.coderjony.com/blogs/adding-swagger-to-aspnet-core-31-web-api/
However, I can't seem to enable both at the same time. It seems that I'm mixing them wrong.
This is the code that I have currently:
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();
services.AddOData();
AddSwagger(services);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Foo API V1");
});
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.Select().Filter().OrderBy().Count().MaxTop(10);
endpoints.MapODataRoute("odata", "odata", GetEdmModel());
});
}
private IEdmModel GetEdmModel()
{
var odataBuilder = new ODataConventionModelBuilder();
odataBuilder.EntitySet<WeatherForecast>("WeatherForecast");
return odataBuilder.GetEdmModel();
}
private void AddSwagger(IServiceCollection services)
{
services.AddSwaggerGen(options =>
{
var groupName = "v1";
options.SwaggerDoc(groupName, new OpenApiInfo
{
Title = $"Foo {groupName}",
Version = groupName,
Description = "Foo API",
Contact = new OpenApiContact
{
Name = "Foo Company",
Email = string.Empty,
Url = new Uri("https://example.com/"),
}
});
});
}
}
It works when I go to https://localhost:44363/odata/weatherforecast
But when I try to load the Swagger interface, this is showing:
It doesn't show anything!
This is my controller:
Controller
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
[EnableQuery]
public IEnumerable<WeatherForecast> Get()
{
var rng = new Random();
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Id = Guid.NewGuid(),
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
})
.ToArray();
}
}
My understanding is that the combination of:
ASP.NET Core 3.1
Endpoint routing
OData (even 7.4+)
Swagger
does not really work at this time because there is no good ApiExplorer implementation for OData controllers/routing.
However, I had the same issue and I was able to make actions appear in Swagger/UI using this :
[ApiExplorerSettings(IgnoreApi = false)]
[Route("Data")]
[HttpGet]
public async Task<IEnumerable<Data>> GetData()
{
// ...
}
and by applying this in Startup code (adapted from This) :
services.AddControllers(options =>
{
IEnumerable<ODataOutputFormatter> outputFormatters =
options.OutputFormatters.OfType<ODataOutputFormatter>()
.Where(formatter => !formatter.SupportedMediaTypes.Any());
foreach (var outputFormatter in outputFormatters)
{
outputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/odata"));
}
IEnumerable<ODataInputFormatter> inputFormatters =
options.InputFormatters.OfType<ODataInputFormatter>()
.Where(formatter => !formatter.SupportedMediaTypes.Any());
foreach (var inputFormatter in inputFormatters)
{
inputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/odata"));
}
});
However, this works for some actions but I do not think this is a good fix since it forces you to reproduce OData conventions routing with non-OData API routing metadata ([Route] + HTTP verbs attributes) everywhere. This is non-sense !
It would be wonderful to be able to automatically generate an OpenAPI document from the entire API, using EDM and OData conventions...
Resources:
https://github.com/OData/WebApi/issues/2024
https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/581#issuecomment-482747072
I used below nuget package and this issue got resolved.
Install-Package OData.Swagger
Ref: https://github.com/KishorNaik/Sol_OData_Swagger_Support
Make this change.
c.SwaggerEndpoint("../swagger/v1/swagger.json", "Foo API V1");
Basically it not able to read your swagger.json file.
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Swagger Demo Project");
});
For more detail: https://findandsolve.com/articles/how-to-implemenation-swagger-in-asp-net-core

Asp.Net Core produces 404 when API is separated into multiple projects

I am trying to break out reusable code for the APIs that I have to create, but there is one issue that has had me stumped for a few days now. When all of the code resides in a single project, WebAPI and the common code exists within a folder called Core, everything works.
Now, if I take the code within Core and put it in its own project WebAPI.Core, the API starts, Swagger loads, but every request produces 404. I have narrowed down the problem, and it only occurs when Startup and Program are in different projects. Is that the intended behavior, or did I do something wrong?
Here is the Startup which is located in the WebAPI.Core project:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddApiVersioning(options =>
{
options.UseApiBehavior = false;
options.ReportApiVersions = true;
options.ApiVersionReader = new HeaderApiVersionReader("api-version");
options.AssumeDefaultVersionWhenUnspecified = true;
});
services.AddMvc(options => { options.EnableEndpointRouting = false; });
services.AddVersionedApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});
services
.AddControllers(options => options.Filters.Add(new ErrorFilter()))
.AddJsonOptions(options => options.JsonSerializerOptions.IgnoreNullValues = true);
}
public void Configure(IApplicationBuilder app)
{
const string swagger = "Swagger";
app.UseMvc(routes => routes.MapRoute("default", "/{controller}/{action}/{*id}", new {id = string.Empty}));
app.UseRouting();
app.UseEndpoints(builder => builder.MapControllers());
var directory = Combine(Api.CurrentDirectory, swagger);
if (Exists(directory))
{
Delete(directory, true);
}
CreateDirectory(directory);
app.UseDefaultFiles().UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(directory),
RequestPath = $"/{swagger}"
});
Api.Assembly.CreateDocuments(AppSettings["Title"], AppSettings["Description"], AppSettings["Name"], AppSettings["Email"], directory);
app.UseSwaggerUI(setupAction =>
{
setupAction.RoutePrefix = string.Empty;
foreach (var file in GetFiles(directory))
{
var fileName = GetFileNameWithoutExtension(file);
setupAction.SwaggerEndpoint($"/{swagger}/{fileName}.json", fileName.Replace("-", " "));
}
});
}
}
Here is Api, the link between Startup and Program, also located in WebAPI.Core:
public static class Api
{
public static void Run<TStartup>(this Assembly assembly, string[] args) where TStartup : Startup
{
Assembly = assembly;
CurrentDirectory = GetDirectoryName(UnescapeDataString(new UriBuilder(assembly.CodeBase).Path));
CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<TStartup>(); }).Build().Run();
}
public static Assembly Assembly { get; private set; }
public static string CurrentDirectory { get; private set; }
}
And finally, here is Program, located in WebAPI:
public static class Program
{
public static void Main(string[] args)
{
GetExecutingAssembly().Run<Startup>(args);
}
}
As was pointed out in one of the comments, I was missing a link to the API assembly.
from:
services.AddMvc(options => { options.EnableEndpointRouting = false; });
to:
services.AddMvc(options => { options.EnableEndpointRouting = false; }).AddApplicationPart(Api.Assembly);
and here is some extra information on AddApplicationPart.

How to set up Swashbuckle vs Microsoft.AspNetCore.Mvc.Versioning

We have asp.net core webapi. We added Microsoft.AspNetCore.Mvc.Versioning and Swashbuckle to have swagger UI.
We specified controllers as this:
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class ContactController : Controller
{
When we run swagger ui we get version as parameter in routes:
How to set-up default "v1" for route ?
If version 2 come to the stage how support swagger ui for both versions ?
At the moment Swashbuckle and Microsoft.AspNetCore.Mvc.Versioning are friends. It works good. I just created test project in VS2017 and checked how it works.
First include these two nuget packages:
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="1.2.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="1.0.0" />
Configure everything in Startup.cs (read my comments):
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
// Configure versions
services.AddApiVersioning(o =>
{
o.AssumeDefaultVersionWhenUnspecified = true;
o.DefaultApiVersion = new ApiVersion(1, 0);
});
// Configure swagger
services.AddSwaggerGen(options =>
{
// Specify two versions
options.SwaggerDoc("v1",
new Info()
{
Version = "v1",
Title = "v1 API",
Description = "v1 API Description",
TermsOfService = "Terms of usage v1"
});
options.SwaggerDoc("v2",
new Info()
{
Version = "v2",
Title = "v2 API",
Description = "v2 API Description",
TermsOfService = "Terms of usage v2"
});
// This call remove version from parameter, without it we will have version as parameter
// for all endpoints in swagger UI
options.OperationFilter<RemoveVersionFromParameter>();
// This make replacement of v{version:apiVersion} to real version of corresponding swagger doc.
options.DocumentFilter<ReplaceVersionWithExactValueInPath>();
// This on used to exclude endpoint mapped to not specified in swagger version.
// In this particular example we exclude 'GET /api/v2/Values/otherget/three' endpoint,
// because it was mapped to v3 with attribute: MapToApiVersion("3")
options.DocInclusionPredicate((version, desc) =>
{
var versions = desc.ControllerAttributes()
.OfType<ApiVersionAttribute>()
.SelectMany(attr => attr.Versions);
var maps = desc.ActionAttributes()
.OfType<MapToApiVersionAttribute>()
.SelectMany(attr => attr.Versions)
.ToArray();
return versions.Any(v => $"v{v.ToString()}" == version) && (maps.Length == 0 || maps.Any(v => $"v{v.ToString()}" == version));
});
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint($"/swagger/v2/swagger.json", $"v2");
c.SwaggerEndpoint($"/swagger/v1/swagger.json", $"v1");
});
app.UseMvc();
}
There two classes that make the trick:
public class RemoveVersionFromParameter : IOperationFilter
{
public void Apply(Operation operation, OperationFilterContext context)
{
var versionParameter = operation.Parameters.Single(p => p.Name == "version");
operation.Parameters.Remove(versionParameter);
}
}
public class ReplaceVersionWithExactValueInPath : IDocumentFilter
{
public void Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context)
{
swaggerDoc.Paths = swaggerDoc.Paths
.ToDictionary(
path => path.Key.Replace("v{version}", swaggerDoc.Info.Version),
path => path.Value
);
}
}
The RemoveVersionFromParameter removes from swagger UI this textbox:
The ReplaceVersionWithExactValueInPath change this:
to this:
Controller class looks now as follows:
[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("1")]
[ApiVersion("2")]
public class ValuesController : Controller
{
// GET api/values
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
// GET api/values/5
[HttpGet("{id}")]
public string Get(int id)
{
return "value";
}
// POST api/values
[HttpPost]
public void Post([FromBody]string value)
{
}
// PUT api/values/5
[HttpPut("{id}")]
public void Put(int id, [FromBody]string value)
{
}
// DELETE api/values/5
[HttpDelete("{id}")]
public void Delete(int id)
{
}
[HttpGet("otherget/one")]
[MapToApiVersion("2")]
public IEnumerable<string> Get2()
{
return new string[] { "value1", "value2" };
}
/// <summary>
/// THIS ONE WILL BE EXCLUDED FROM SWAGGER Ui, BECAUSE v3 IS NOT SPECIFIED. 'DocInclusionPredicate' MAKES THE
/// TRICK
/// </summary>
/// <returns></returns>
[HttpGet("otherget/three")]
[MapToApiVersion("3")]
public IEnumerable<string> Get3()
{
return new string[] { "value1", "value2" };
}
}
Code: https://gist.github.com/Alezis/bab8b559d0d8800c994d065db03ab53e
If working with .Net Core 3, Basically I have taken #Alezis's solution and updated it to work with .Net core 3:
public void ConfigureServices(IServiceCollection services)
{
....
services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo() { Title = "My API", Version = "v1" });
options.OperationFilter<RemoveVersionFromParameter>();
options.DocumentFilter<ReplaceVersionWithExactValueInPath>();
});
...
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});
...
}
public class RemoveVersionFromParameter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var versionParameter = operation.Parameters.Single(p => p.Name == "version");
operation.Parameters.Remove(versionParameter);
}
}
public class ReplaceVersionWithExactValueInPath : IDocumentFilter
{
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
var paths = new OpenApiPaths();
foreach (var path in swaggerDoc.Paths)
{
paths.Add(path.Key.Replace("v{version}", swaggerDoc.Info.Version), path.Value);
}
swaggerDoc.Paths = paths;
}
}
Instead of tweaking the OpenAPI document, you can use the library provided by Microsoft that adds versions to the API Explorer. That way the versions are provided before Swashbuckle (or another toolchain) needs it and allows you to avoid custom code.
Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer
I was able to get versions configured correctly after adding the package and this block of code.
services.AddVersionedApiExplorer(
options =>
{
// add the versioned api explorer, which also adds IApiVersionDescriptionProvider service
// note: the specified format code will format the version as "'v'major[.minor][-status]"
options.GroupNameFormat = "'v'VVV";
// note: this option is only necessary when versioning by url segment. the SubstitutionFormat
// can also be used to control the format of the API version in route templates
options.SubstituteApiVersionInUrl = true;
}
);
#Alezis Nice approach, but if you are using the latest version of Microsoft.AspNetCore.Mvc.Versioning (2.3.0) library, ControllerAttributes() and ActionAttributes() are deprecated, you can update DocInclusionPredicate as follows:
options.DocInclusionPredicate((version, desc) =>
{
if (!desc.TryGetMethodInfo(out MethodInfo methodInfo)) return false;
var versions = methodInfo.DeclaringType
.GetCustomAttributes(true)
.OfType<ApiVersionAttribute>()
.SelectMany(attr => attr.Versions);
return versions.Any(v => $"v{v.ToString()}" == version);
});
Swashbuckle.AspNetCore github project helps me a lot.
When updating to .net core 3 I got the following error:
'Unable to cast object of type 'System.Collections.Generic.Dictionary`2[System.String,Microsoft.OpenApi.Models.OpenApiPathItem]' to type 'Microsoft.OpenApi.Models.OpenApiPaths'.'
Fixed this by changes to code to:
public class ReplaceVersionWithExactValueInPath : IDocumentFilter
{
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
if (swaggerDoc == null)
throw new ArgumentNullException(nameof(swaggerDoc));
var replacements = new OpenApiPaths();
foreach (var (key, value) in swaggerDoc.Paths)
{
replacements.Add(key.Replace("{version}", swaggerDoc.Info.Version, StringComparison.InvariantCulture), value);
}
swaggerDoc.Paths = replacements;
}
}
In Asp.core 2.+ Add this class:
public class ApiVersionOperationFilter : IOperationFilter
{
public void Apply(Operation operation, OperationFilterContext context)
{
var actionApiVersionModel = context.ApiDescription.ActionDescriptor?.GetApiVersion();
if (actionApiVersionModel == null)
{
return;
}
if (actionApiVersionModel.DeclaredApiVersions.Any())
{
operation.Produces = operation.Produces
.SelectMany(p => actionApiVersionModel.DeclaredApiVersions
.Select(version => $"{p};v={version.ToString()}")).ToList();
}
else
{
operation.Produces = operation.Produces
.SelectMany(p => actionApiVersionModel.ImplementedApiVersions.OrderByDescending(v => v)
.Select(version => $"{p};v={version.ToString()}")).ToList();
}
}
}
next add below codes in configureServices method in startup:
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info { Title = "Versioned Api v1", Version = "v1" });
c.OperationFilter<ApiVersionOperationFilter>();
});
then add below codes in configure method in startup:
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Versioned Api v1");
c.RoutePrefix = string.Empty;
in Asp.core 3.+ add these classes:
public class RemoveVersionFromParameter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
if (!operation.Parameters.Any())
return;
var versionParameter = operation.Parameters
.FirstOrDefault(p => p.Name.ToLower() == "version");
if (versionParameter != null)
operation.Parameters.Remove(versionParameter);
}
}
public class ReplaceVersionWithExactValueInPath : IDocumentFilter
{
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
if (swaggerDoc == null)
throw new ArgumentNullException(nameof(swaggerDoc));
var replacements = new OpenApiPaths();
foreach (var (key, value) in swaggerDoc.Paths)
{
replacements.Add(key.Replace("v{version}", swaggerDoc.Info.Version,
StringComparison.InvariantCulture), value);
}
swaggerDoc.Paths = replacements;
}
}
next add below codes in ConfigureServices method in startup:
protected virtual IEnumerable<int> Versions => new[] {1};
services.AddSwaggerGen(options =>
{
Versions.ToList()
.ForEach(v =>
options.SwaggerDoc($"v{v}",
new OpenApiInfo
{
Title = $"Versioned Api:v{v}", Version = $"v{v}"
}));
options.OperationFilter<RemoveVersionFromParameter>();
options.DocumentFilter<ReplaceVersionWithExactValueInPath>();
options.RoutePrefix = string.Empty;
});
then add below codes in configure method in startup:
app.UseSwagger();
app.UseSwaggerUI(options =>
{
Versions.ToList()
.ForEach(v => options.SwaggerEndpoint($"/swagger/v{v}/swagger.json", $"Versioned Api:v{v}"));
options.RoutePrefix = string.Empty;
});
#ArlanG it helped me, thanks. It works in Asp.Net Core 3.1. There is one small clarification from my point of view. If you want to get more similar behavior like main answer #Alezis method implementation of DocInclusionPredicate() can be:
options.DocInclusionPredicate((version, desc) =>
{
if (!desc.TryGetMethodInfo(out MethodInfo methodInfo)) return false;
var versions = methodInfo.DeclaringType
.GetCustomAttributes(true)
.OfType<ApiVersionAttribute>()
.SelectMany(attr => attr.Versions);
var maps = methodInfo
.GetCustomAttributes(true)
.OfType<MapToApiVersionAttribute>()
.SelectMany(attr => attr.Versions)
.ToArray();
return versions.Any(v => $"v{v.ToString()}" == version)
&& (!maps.Any() || maps.Any(v => $"v{v.ToString()}" == version));
});
In this case when you choose a version on SwaggerUi page, it will show only controller methods that are mapped to this version.
I found that using the method ArlanG highlighted took {00:00:00.0001905} to complete whereas running
var versions = methodInfo.DeclaringType.GetConstructors().SelectMany(x =>
x.DeclaringType.CustomAttributes.Where(y =>
y.AttributeType == typeof(ApiVersionAttribute))
.SelectMany(z => z.ConstructorArguments.Select(i=>i.Value)));
took {00:00:00.0000626}
I know we're talking about minor differences but still.

Categories