I'm using Asp.Net Core as a Rest API Service. I need to have API Versioning. Actually, I set in the Startup following settings and It's work correctly but when I set to default Version It's not working on.
services.AddVersionedApiExplorer(
options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
options.AssumeDefaultVersionWhenUnspecified = true;
options.DefaultApiVersion = new ApiVersion(1, 0);
});
services.AddApiVersioning(
options =>
{
options.ReportApiVersions = true;
options.AssumeDefaultVersionWhenUnspecified = true;
options.DefaultApiVersion = new ApiVersion(1, 0);
})
.AddMvc(
options =>
{
options.RespectBrowserAcceptHeader = true;
})
.AddXmlSerializerFormatters();
and set Attribute in Controllers like this:
Version 1:
[ApiController]
[Route("v{version:apiVersion}/[controller]")]
[ApiVersion("1.0")]
public class UsersController : ControllerBase
{
[HttpGet("log")]
public string Get()
{
return $"{DateTime.Now}";
}
}
Version 2:
[ApiController]
[Route("v{version:apiVersion}/[controller]")]
[ApiVersion("2.0")]
public class UsersController : ControllerBase
{
[HttpGet("log")]
public string Get()
{
return $"{DateTime.Now}";
}
}
I can get the result as fllowing urls:
http://localhost:5000/v1/users/log => Status Code: 200
http://localhost:5000/v2/users/log => Status Code: 200
But http://localhost:5000/users/log => Status Code: 404
How I can set the default API in Versioning?
Thanks, everyone for taking the time to try and help explain
Your configuration correctly sets default api version if none is specified. But your routes requires request url to include v{version} part. So one possible solution is to add another route without v{version} like this
[ApiController]
[Route("v{version:apiVersion}/[controller]")]
[Route("/[controller]")]
[ApiVersion("1.0")]
public class UsersController : ControllerBase
{
[HttpGet("log")]
public string Get()
{
return $"{DateTime.Now}";
}
}
Now request to http://localhost:5000/users/log will point to controller with api version 1.0. If you add this route to second controller the version 1.0 will still be picked because default ApiVersion will be selected and it's 1.0.
Related
Just realised that my understanding about ASP.NET Core 6 Web API versioning is wrong.
This is my controller:
[ApiVersion("1.0")]
[ApiController]
[Authorize]
public class FundController
{
[MapToApiVersion("1.0")]
[Route("/Fund/v{version:apiVersion}/delta")]
public async Task<List<PortfolioHolding<Holding>>> Delta([FromQuery] Request dataModel)
{
}
}
What I want is to support route /Fund/v1.0/delta and /Fund/delta, when versioning not provided by the consumer (e.g. calling /Fund/delta), the default version will be hit.
So I configured the versioning like this. However, when I call /Fund/delta, I get a http 404 error.
But /Fund/v1.0/delta will hit the correct controller.
What am I doing wrong?
services.AddApiVersioning(option =>
{
option.DefaultApiVersion = new ApiVersion(1, 0);
option.AssumeDefaultVersionWhenUnspecified = true;
option.ReportApiVersions = true;
});
Usually, it's pretty easy to do this that way. The disadvantage of this approach is that you need to manually change the "default" version of API with this attribute
The problem is that you have not specified the routes in the controller.
You should add the default route as well as the formatted version route. Then you should ensure that your endpoints have the version specified in the MapToApiVersion attribute.
Here is a code sample of what your controller should look like:
[ApiVersion("1.0")]
[ApiVersion("2.0")]
[Route("[controller]")]
[Route("[controller]/v{version:apiVersion}")]
public class FundController : ControllerBase
{
[MapToApiVersion("1.0")]
[Route("delta")]
[HttpGet]
public async Task<List<PortfolioHolding<Holding>>> DeltaV1([FromQuery] Request dataModel)
{
}
[MapToApiVersion("2.0")]
[Route("delta")]
[HttpGet]
public async Task<List<PortfolioHolding<Holding>>> DeltaV2([FromQuery]
Request dataModel)
{
}
}
I am creating an API. I use swagger but due to a huge number of controllers and actions, I want to split API endpoint by domain. To get this I thought about versioning of the API. I thought about using the Status of ApiVersion. The code of my controllers is below.
[ApiVersion("1.0-First")] //This is ApiVersion MajorVersion = 1, Status = "First"
[Route("api/v{version:apiVersion}/[controller]")]
public class FirstController
[ApiVersion("1.0-Second")]
[Route("api/v{version:apiVersion}/other")]
public class SecondController
My swagger looks fine and the definitions of parts of API are good. (I know that path should be without capital letters - this is for test purposes only)
But swagger can't reach any endpoint. Because the valid endpoint is at /api/v1.0-First/First not /api/v1/First.
My startUp class looks like below:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvcCore().AddApiExplorer();
services.AddApiVersioning(c =>
{
c.ApiVersionReader = ApiVersionReader.Combine(
new QueryStringApiVersionReader("V"),
new UrlSegmentApiVersionReader());
c.ReportApiVersions = false;
c.DefaultApiVersion = new ApiVersion(1, 0);
});
services.AddVersionedApiExplorer(options =>
{
options.SubstituteApiVersionInUrl = true;
options.SubstitutionFormat = "V";
options.DefaultApiVersion = new ApiVersion(1, 0);
});
services.RegisterSwaggerConfiguration();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseDeveloperExceptionPage();
app.UseHttpsRedirection();
app.UseMvc();
app.AddSwagger(app.ApplicationServices.GetService<IApiVersionDescriptionProvider>(), Configuration);
}
There is some static class I wrote to add the dependencies based on IApiVersionDescriptionProvider
public static class SwaggerExtension
{
public static void RegisterSwaggerConfiguration(this IServiceCollection services)
{
services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
services.AddSwaggerGen();
}
public static void AddSwagger(this IApplicationBuilder app, IApiVersionDescriptionProvider provider, IConfiguration configuration)
{
var prefix = "swagger";
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.RoutePrefix = string.Empty;
foreach (var description in provider.ApiVersionDescriptions)
{
c.SwaggerEndpoint($"{prefix}/{description.GroupName}/swagger.json", description.GroupName);
}
});
}
}
And another class for SwaggerDoc generation.
public class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
{
private readonly IApiVersionDescriptionProvider provider;
private readonly IConfiguration configuration;
public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider, IConfiguration configuration)
{
this.provider = provider;
this.configuration = configuration;
}
public void Configure(SwaggerGenOptions options)
{
foreach (var description in provider.ApiVersionDescriptions)
{
options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description));
}
}
private OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description)
{
var info = new OpenApiInfo()
{
Title = description.GroupName,
Version = description.ApiVersion.ToString(),
};
if (description.IsDeprecated)
{
info.Description += " This API version has been deprecated.";
}
return info;
}
}
I want to get the routing work as api/v1/First or api/v1.0/First (this should not matter).
Maybe writting some custom middleware to handle this case would be good idea?
By now I am out of ideas and in general I couldn't find any articles about using status of ApiVersion.
EDIT:
Changed Title.
We had a similar problem some time ago. We needed to split an Api by a customer privilege/domain. The research took some time as well :), please note that we are using NSwag.
So as you already mentioned (custom middleware) we've created a custom OperationProcessor and used base type checking. Take a look at an example:
services.AddOpenApiDocument(document =>
{
document.Title = "API A";
document.OperationProcessors.Insert(0, new IncludeAApiControllersInSwagger());
});
services.AddOpenApiDocument(document =>
{
document.Title = "API B";
document.OperationProcessors.Insert(0, new IncludeBApiControllersInSwagger());
});
and then
private class IncludeAApiControllersInSwagger : IOperationProcessor
{
public bool Process(OperationProcessorContext context)
{
return IsControllerInType(context, typeof(AApiController));
}
}
private class IncludeBApiControllersInSwagger : IOperationProcessor
{
public bool Process(OperationProcessorContext context)
{
return IsControllerInType(context, typeof(BApiController));
}
}
The last step would be to build a proper inheritance over your controllers.
An API version is always an API version; the values are explicit - by design. There is no universe where 1.0-First can map to an API, but not include the status.
The status is most useful for pre-releases. For example, you might have /first?api-version=1.0-preview.1. When you have a volatile, preview version of an API, this prevents you from having to bump up to 1.1 and so on. 1.0 is greater than 1.0-preview.1.
From your description, it sounds like you want to group or categorize your APIs by an additional level. The Swagger UI only supports a single level of grouping, but ASP.NET API Versioning 7.0+ now has support to make custom group names with API versions easy to configure using the FormatGroupName option.
If your API has a custom group name like this:
[ApiVersion(1.0)]
[ApiExplorerSettings(GroupName = "First")]
[Route("api/v{version:apiVersion}/[controller]")]
public class FirstController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok();
}
You can now configure the combination of both like this:
services.AddApiVersioning()
.AddApiExplorer(
options =>
{
options.SubstituteApiVersionInUrl = true;
options.FormatGroupName = (group, version) => $"{version}-{group}";
});
This only works if you set a custom group name and define a callback. The rules are:
Default configuration; formatted ApiVersion
Group name set, but not callback; use group name
Group name and callback set; result for callback with group and formatted ApiVersion
Only callback set; ignored and uses default configuration as there's no group name
The ApiVersion is formatted according to GroupNameFormat. By default, this will simply be ApiVersion.ToString(). You can still use it if you want to. For example, if GroupNameFormat = "'v'VVV";, then the formatted name via the callback will result in v1-First.
Despite all of this configuration and grouping, the route to your API will still be: api/v1/first. I believe that will get you both of your goals.
We were using Microsoft.AspNet.WebApi 5.2.3 in our project and we recently have upgraded to 5.2.7 and some of the existing functionality stopped working:
I have this FileController:
[ApiVersion("1")]
[RoutePrefix("api/v{version:apiVersion}/file")]
public class FileController : BaseController
{
[HttpPost]
[MapToApiVersion("1")]
[Route("uploadmethod"), Obsolete("The action is marked as obsolete for missing the support for userId. Please use uploadmethod from version 2.")]
public async Task<HttpResponseMessage> UploadFileStream([FromUri] string filePath = null...)
{
}
}
And then another controller that derives from the one above:
[ApiVersion("2")]
[RoutePrefix("api/v{version:apiVersion}/file")]
public class FileController : Controllers.FileController
{
[HttpPost]
[MapToApiVersion("2")]
[Route("uploadmethod")]
public async Task<HttpResponseMessage> UploadFileWithUserId([FromUri] string userId = null, [FromUri] string filePath = null)
{
//at some point here I call an internal method from the base FileController
}
}
When using 5.2.3, the following code was working fine:
$"api/v1/file/uploadmethod?filePath={path}";
Now, after upgrade to 5.2.7, I receive this error:
"Multiple controller types were found that match the URL. This can happen if attribute routes on multiple controllers match the requested URL."
I need the MapToApiVersion attribute because I need to test below to succeed.
[Fact]
public async Task UploadFileUsingVersion2NUserId_WillThrowError_MethodsIsMissingOnThatVersion()
{
//...
var url = $"api/v2/file/uploadmethod?filePath={path}";
// When
var response = await api.UploadFile(url, content);
// Then
Assert.Equal(HttpStatusCode.MethodNotAllowed, response.StatusCode);
var errorResponse = await response.Content.ReadAsJsonAsync<UnsupportedVersionResponse>();
Assert.Equal("UnsupportedApiVersion", errorResponse.Error.Code);
}
What has changed? How can I enforce v1 only for the base controller?
I like the Automatic HTTP 400 responses functionality new to ASP.NET Core 2.1 and it's working out really well for most cases.
However, in one action I need to do a bit of pre-processing before validation the payload. I have a custom validator that requires two values in the model to perform validation. One of those values is in the path so I would like to set that value on the model from the path then validate.
I don't want to switch the functionality off for all actions with:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressModelStateInvalidFilter = true;
});
}
Is there any way I could switch it off just for an individual action?
Edit:
I tried modifying the InvalidModelStateResponseFactory but it didn't solve my problem because I still need to get into the controller action:
services.Configure<ApiBehaviorOptions>(options =>
{
options.InvalidModelStateResponseFactory = actionContext =>
{
var ignore = actionContext.ActionDescriptor.FilterDescriptors.Any(fd => fd.Filter is SuppressModelStateInvalidFilterAttribute);
if (ignore)
{
// Can only return IActionResult so doesn't enter the controller action.
}
return new BadRequestObjectResult(actionContext.ModelState);
};
});
[AttributeUsage(AttributeTargets.Method)]
public class SuppressModelStateInvalidFilterAttribute : FormatFilterAttribute
{
}
Edit:
Here's a link to an issue I raised on the asp.net core repo in case I get anywhere with that - https://github.com/aspnet/Mvc/issues/8575
Update: you can just use the following code in ConfigureServices in Startup.cs:
services.Configure<ApiBehaviorOptions>(apiBehaviorOptions => {
apiBehaviorOptions.SuppressModelStateInvalidFilter = true;
});
Based on Simon Vane's answer, I had to modify the attribute for ASP.Net Core 2.2 as follows:
/// <summary>
/// Suppresses the default ApiController behaviour of automatically creating error 400 responses
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class SuppressModelStateInvalidFilterAttribute : Attribute, IActionModelConvention {
private static readonly Type ModelStateInvalidFilterFactory = typeof(ModelStateInvalidFilter).Assembly.GetType("Microsoft.AspNetCore.Mvc.Infrastructure.ModelStateInvalidFilterFactory");
public void Apply(ActionModel action) {
for (var i = 0; i < action.Filters.Count; i++) {
if (action.Filters[i] is ModelStateInvalidFilter || action.Filters[i].GetType() == ModelStateInvalidFilterFactory) {
action.Filters.RemoveAt(i);
break;
}
}
}
}
I had a response from Microsoft - https://github.com/aspnet/Mvc/issues/8575
The following worked a charm.
[AttributeUsage(AttributeTargets.Method)]
public class SuppressModelStateInvalidFilterAttribute : Attribute, IActionModelConvention
{
public void Apply(ActionModel action)
{
for (var i = 0; i < action.Filters.Count; i++)
{
if (action.Filters[i] is ModelStateInvalidFilter)
{
action.Filters.RemoveAt(i);
break;
}
}
}
}
In my controller I could then make changes to the model before re-validating it (note the ModelState.Clear(), TryValidateModel add to existing model state):
if (model == null)
{
return BadRequest(ModelState);
}
model.Property = valueFromPath;
ModelState.Clear();
if (TryValidateModel(model) == false)
{
return BadRequest(ModelState);
}
You could play with ApiBehaviorOptions.InvalidModelStateResponseFactory property to handle specific cases based on actionContext details:
services.Configure<ApiBehaviorOptions>(options =>
{
options.InvalidModelStateResponseFactory = actionContext =>
{
// Do what you need here for specific cases with `actionContext`
// I believe you can cehck the action attributes
// if you'd like to make mark / handle specific cases by action attributes.
return new BadRequestObjectResult(context.ModelState);
}
});
This could probably be solved by implementing your own validator for your specific case. It is covered quite well in the documentation.
https://learn.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-2.1#custom-validation
Either that or possibly a custom model binder to create your model with all the preprocessing done before it is validated.
I encountered similar problem and came up with this solution.
public class SuppressModelStateInvalidFilterAttribute : ActionFilterAttribute
{
public SuppressModelStateInvalidFilterAttribute()
{
Order = -2500;
}
public override Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
context.ModelState.Clear();
return next.Invoke();
}
}
I've a self host Web API with 2 controllers:
For controller 1, I need default DataContractSerializer (I'm exposing EF 5 POCO)
For controller 2, I need XmlFormatter with parameter UseXmlSerializer set to true (I'm exposing an XmlDocument)
I've tried to set formatters during controller initialization, but the configuration seems to be global, affecting all controllers:
public class CustomConfigAttribute : Attribute, IControllerConfiguration
{
public void Initialize(HttpControllerSettings settings,
HttpControllerDescriptor descriptor)
{
settings.Formatters.XmlFormatter.UseXmlSerializer = true;
}
}
How can I solve this?
You were very much on the right track. But you need to initallise a new instance of the XmlMediaTypeFormatter in your config attributes otherwise you will affect the global reference.
As you know, you need to create 2 attributes based on the IControllerConfiguration interface.
public class Controller1ConfigAttribute : Attribute, IControllerConfiguration
{
public void Initialize(HttpControllerSettings controllerSettings,
HttpControllerDescriptor controllerDescriptor)
{
var xmlFormater = new XmlMediaTypeFormatter {UseXmlSerializer = true};
controllerSettings.Formatters.Clear();
controllerSettings.Formatters.Add(xmlFormater);
}
}
public class Controller2ConfigAttribute : Attribute, IControllerConfiguration
{
public void Initialize(HttpControllerSettings controllerSettings,
HttpControllerDescriptor controllerDescriptor)
{
var xmlFormater = new XmlMediaTypeFormatter();
controllerSettings.Formatters.Clear();
controllerSettings.Formatters.Add(xmlFormater);
}
}
Then decorate your controllers with the relevant attribute
[Controller1ConfigAttribute]
public class Controller1Controller : ApiController
{
[Controller2ConfigAttribute]
public class Controller2Controller : ApiController
{
Configuration:
config.Formatters.Remove(config.Formatters.JsonFormatter);
config.Formatters.Insert(0, new CustomXmlMediaTypeFormatter());
The Custom formatter:
public class CustomXmlMediaTypeFormatter : XmlMediaTypeFormatter
{
public CustomXmlMediaTypeFormatter()
{
UseXmlSerializer = true;
}
}
This seems to work, ok not so elegant.
Removing default Xml Formatter does not work,
so I concluded that the framework is somehow still using it.
Mark Jones' answer has a big downside: By clearing all formatters it is not possible to request different ContentTypes and make use of the relevant formatter.
A better way to enable the XMLSerializer per Controller is to replace the default formatter.
public class UseXMLSerializerAttribute : Attribute, IControllerConfiguration
{
public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor)
{
// Find default XMLFormatter
var xmlFormatter = controllerSettings.Formatters.FirstOrDefault(c => c.SupportedMediaTypes.Any(x => x.MediaType == "application/xml"));
if (xmlFormatter != null)
{
// Remove default formatter
controllerSettings.Formatters.Remove(xmlFormatter);
}
// Add new XMLFormatter which uses XmlSerializer
controllerSettings.Formatters.Add(new XmlMediaTypeFormatter { UseXmlSerializer = true });
}
}
And use it like this:
[UseXMLSerializer]
public TestController : ApiController
{
//Actions
}
I think you could write a custom ActionFilterAttribute.
In OnActionExecuting, store away the original values in the HttpContext and then in OnActionExecuted, restore the original values.
the controllers actions themselves should not be concerned with how the data is serialized. yo should be able to request the data and any format necessary the operation to retrieve the data would be the same.
by default web api serialized to json objects. however if you set the content type of the request to xml is should return the same result, but formatted as xml instead of json.