I have a generic Result<T> response type in my controllers, e.g.
public Result<T> GetSomething()
{
...
}
I also have a custom asp.net core filter that returns a Json representation of T
To have swashbuckle generate correct documentation, I have to decorate every method with:
[Produces(typeof(T))]
As this is cumbersome, easily forgotten and error prone, I was looking for a way to automate this.
Now in Swashbuckle you have a MapType, but I can't get a hold of the T in those methods:
services.AddSwaggerGen(c =>
{
...
c.MapType(typeof(Result<>), () => /*can't get T here*/);
};
I was looking at the IOperationFilter but I can't find a way to override the result type in there.
Then there are ISchemaFilter
public class ResultSchemaFilter : ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
if (!context.Type.IsGenericType || !context.Type.GetGenericTypeDefinition().IsAssignableFrom(typeof(Result<>)))
{
return;
}
var returnType = context.Type.GetGenericArguments()[0];
//How do I override the schema here ?
var newSchema = context.SchemaGenerator.GenerateSchema(returnType, context.SchemaRepository);
}
}
IOperationFilter is the correct choice. Here is an example that changes the response type for OData endpoints.
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
//EnableQueryAttribute refers to an OData endpoint.
if (context.ApiDescription.ActionDescriptor.EndpointMetadata.Any(em => em is EnableQueryAttribute))
{
//Fixing the swagger response for Controller style endpoints
if (context.ApiDescription.ActionDescriptor is ControllerActionDescriptor cad)
{
//If the return type is IQueryable<T>, use ODataResponseValue<T> as the Swagger response type.
var returnType = cad.MethodInfo.ReturnType;
if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(IQueryable<>))
{
var actualType = returnType.GetGenericArguments()[0];
var responseType = typeof(ODataResponseValue<>).MakeGenericType(actualType);
var schema = context.SchemaGenerator.GenerateSchema(responseType, context.SchemaRepository);
foreach (var item in operation.Responses["200"].Content)
item.Value.Schema = schema;
}
}
}
}
As you can see here, I'm looping through all of the items in operation.Responses["200"].Content, replacing their schema one by one using the GenerateSchema method that you found.
Related
How can I show only APIs of type GET in Swagger page and hide others?
I found that the attribute [ApiExplorerSettings(IgnoreApi = true)]
can hide the API from Swagger page, but I have lot of APIs to hide and I need an approach to hide the APIs depending on its HTTP type.
I've tried this approach :
public class SwaggerFilter : IDocumentFilter
{
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
var nonGetPaths = swaggerDoc.Paths.Where(x => x.Value.Operations.First().Key != OperationType.Get);
var count=nonGetPaths.Count();
foreach (var item in nonGetPaths)
{
swaggerDoc.Paths.Remove(item.Key);
}
}
}
but it didn't work
Write a custom filter like this:
public class SwaggerFilter : IDocumentFilter
{
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
foreach (var path in swaggerDoc.Paths)
{
foreach (var key in path.Value.Operations.Keys )
{
if (key != OperationType.Get)
{
swaggerDoc.Paths.Remove(path.Key);
}
}
}
}
}
Then configure in program.cs(.Net 6)
//.......
builder.Services.AddSwaggerGen(x=>x.DocumentFilter<SwaggerFilter>());
//......
I don't add [ApiExplorerSettings(IgnoreApi = true)] in my apicontroller and it works all fine.
But, Make sure Get endpoint and other type of endpoint have different route in the same controller, You can add attribute route like [HttpGet("/get")] on Get endpoint. If you just write like this in the same controller:
[HttpPost]
public IActionResult Post()
{
return Ok();
}
[HttpGet]
public IActionResult Get()
{
return NotFound();
}
Get and Post endpoint will have the same path. swaggerDoc.Paths.Remove(xxx); will remove all of them.
Reuslt:
Before
After
I'm trying to make a WebAPI controller on .NET Core 3.1 witch supports both JSON and XML as request/response content-type.
Controller works perfectly when it receive JSON with "application/json", but when it receive XML with "application/xml", method argument are created with default values, not values that was posted in request body.
Example project - https://github.com/rincew1nd/ASPNetCore_XMLMethods
Additional XML serializer in startup:
services.AddControllers().AddXmlSerializerFormatters();
Controller with method and test model:
[ApiController]
[Route("[controller]")]
public class TestController : ControllerBase
{
[HttpPost, Route("v1")]
[Consumes("application/json", "application/xml")]
[Produces("application/json", "application/xml")]
public TestRequest Test([FromBody] TestRequest data)
{
return data;
}
}
[DataContract]
public class TestRequest
{
[DataMember]
public Guid TestGuid { get; set; }
[DataMember]
public string TestString { get; set; }
}
P.S. Project contains Swagger for API testing purposes.
Your xml post request body uses camel cases which results in the model binding as null.
Add using Swashbuckle.AspNetCore.SwaggerGen; in starup.cs and try to configure like below code:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers().AddXmlSerializerFormatters();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Neocase <-> 1C Integration", Version = "v1" });
c.SchemaFilter<XmlSchemaFilter>();
});
}
public class XmlSchemaFilter : Swashbuckle.AspNetCore.SwaggerGen.ISchemaFilter
{
public void Apply(OpenApiSchema model, SchemaFilterContext context)
{
if (model.Properties == null) return;
foreach (var entry in model.Properties)
{
var name = entry.Key;
entry.Value.Xml = new OpenApiXml
{
Name = name.Substring(0, 1).ToUpper() + name.Substring(1)
};
}
}
}
Don't use FromBody attribute for application/xml.
When a parameter has [FromBody], Web API uses the Content-Type header to select a formatter. In this example, the content type is "application/json" and the request body is a raw JSON string (not a JSON object).
Using [FromBody]
After some more research i found that swagger generates wrong xml examples without even noticing custom naming of classes or properties.
I wrote custom schema for naming xml attributes as they are named by XML attributes.
Only problem i faced is that SchemaFilterContext doesn't provide description of properties of Enum type. So to name Enums i use custom attribute for swagger name and XMLElementAttribute on property with same names (yeah, it's junky but works).
public class XmlSchemaFilter : ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
//Try to find XmlRootAttribute on class
var xmlroot = context.Type.GetAttributeValue((XmlRootAttribute xra) => xra);
if (xmlroot != null)
{
schema.Xml = new OpenApiXml
{
Name = xmlroot.ElementName
};
}
//Try to find XmlElementAttribute on property
if (context.MemberInfo != null)
{
var xmlelement = context.MemberInfo.GetAttributeValue((XmlElementAttribute xea) => xea);
if (xmlelement != null)
{
schema.Xml = new OpenApiXml
{
Name = xmlelement.ElementName
};
}
}
//Try to find XmlEnumNameAttribute on enums
if (context.Type.IsEnum)
{
var enumname = context.Type.GetAttributeValue((XmlEnumNameAttribute xea) => xea);
if (enumname != null)
{
schema.Xml = new OpenApiXml
{
Name = enumname.ElementName
};
}
}
}
}
public static class AttributeHelper
{
public static TValue GetAttributeValue<TAttribute, TValue>(
this Type type,
Func<TAttribute, TValue> valueSelector)
where TAttribute : Attribute
{
var att = type.GetCustomAttributes(
typeof(TAttribute), true
).FirstOrDefault() as TAttribute;
if (att != null)
{
return valueSelector(att);
}
return default(TValue);
}
public static TValue GetAttributeValue<TAttribute, TValue>(
this MemberInfo mi,
Func<TAttribute, TValue> valueSelector)
where TAttribute : Attribute
{
var att = mi.GetCustomAttributes(
typeof(TAttribute), true
).FirstOrDefault() as TAttribute;
if (att != null)
{
return valueSelector(att);
}
return default(TValue);
}
}
I have a logic to apply in case the request received is a BadRequest, to do this I have created a filter:
public class ValidateModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
// Apply logic
}
}
}
In Startup:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options => { options.Filters.Add<ValidateModelAttribute>(); });
}
Controller:
[Route("api/[controller]")]
[ApiController]
public class VerifyController : ControllerBase
{
[Route("test")]
[HttpPost]
[ValidateModel]
public ActionResult<Guid> validationTest(PersonalInfo personalInfo)
{
return null;
}
}
Model:
public class PersonalInfo
{
public string FirstName { get; set; }
[RegularExpression("\\d{4}-?\\d{2}-?\\d{2}", ErrorMessage = "Date must be properly formatted according to ISO 8601")]
public string BirthDate { get; set; }
}
The thing is when I put a break point on the line:
if (!context.ModelState.IsValid)
execution reaches this line only if the request I send is valid. Why it is not passing the filter if I send a bad request?
The [ApiController] attribute that you've applied to your controller adds Automatic HTTP 400 Responses to the MVC pipeline, which means that your custom filter and action aren't executed if ModelState is invalid.
I see a few options for affecting how this works:
Remove the [ApiController] attribute
Although you can just remove the [ApiController] attribute, this would also cause the loss of some of the other features it provides, such as Binding source parameter inference.
Disable only the Automatic HTTP 400 Responses
Here's an example from the docs that shows how to disable just this feature:
services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
// ...
options.SuppressModelStateInvalidFilter = true;
// ...
}
This code goes inside of your Startup's ConfigureServices method.
Customise the automatic response that gets generated
If you just want to provide a custom response to the caller, you can customise what gets returned. I've already described how this works in another answer, here.
An example of intersection for logging is describe in Log automatic 400 responses
Add configuration in Startup.ConfigureServices.
services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
// To preserve the default behavior, capture the original delegate to call later.
var builtInFactory = options.InvalidModelStateResponseFactory;
options.InvalidModelStateResponseFactory = context =>
{
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<Startup>>();
// Perform logging here.
//E.g. logger.LogError($”{context.ModelState}”);
logger.LogWarning(context.ModelState.ModelStateErrorsToString());
// Invoke the default behavior, which produces a ValidationProblemDetails response.
// To produce a custom response, return a different implementation of IActionResult instead.
return builtInFactory(context);
};
});
public static String ModelStateErrorsToString(this ModelStateDictionary modelState)
{
IEnumerable<ModelError> allErrors = modelState.Values.SelectMany(v => v.Errors);
StringBuilder sb = new StringBuilder();
foreach (ModelError error in allErrors)
{
sb.AppendLine($"error {error.ErrorMessage} {error.Exception}");
}
return sb.ToString();
}
As the attribute filter in the life cycle of the .Net Core you can’t handle it. The filter layer with ModelState will run after the model binding.
You can handle it with .Net Core middleware as the following https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-2.1&tabs=aspnetcore2x
If you want to SuppressModelStateInvalidFilter on individual action, consider to use custom attribute suggested on https://learn.microsoft.com/en-us/answers/questions/297568/how-to-suppress-suppressmodelstateinvalidfilter-at.html. (And similar answer https://github.com/aspnet/Mvc/issues/8575)
public class SuppressModelStateInvalidFilterAttribute : Attribute, IActionModelConvention
{
private const string FilterTypeName = "ModelStateInvalidFilterFactory";
public void Apply(ActionModel action)
{
for (var i = 0; i < action.Filters.Count; i++)
{
//if (action.Filters[i] is ModelStateInvalidFilter)
if (action.Filters[i].GetType().Name == FilterTypeName)
{
action.Filters.RemoveAt(i);
break;
}
}
}
}
Example of use
[ApiController]
public class PersonController
{
[SuppressModelStateInvalidFilter]
public ActionResult<Person> Get() => new Person();
}
I'm having a problem trying to get custom model binders to work as a query parameter like I have gotten to work previously in .net framework 4.7.
To ensure this wasn't a scenario where my object was too complex, I reduced the model to a simple string but even then I cannot get this to work.
I have a simple model I would like to be binded from query parameters.
public class SearchModel {
public string SearchTerms { get; set; }
}
And I have configured the ModelBinder and ModelBinderProvider as shown here like so.
public class TestModelBinder : IModelBinder {
public Task BindModelAsync(ModelBindingContext bindingContext) {
if (bindingContext.ModelType != typeof(SearchModel)) {
throw new ArgumentException($"Invalid binding context supplied {bindingContext.ModelType}");
}
var model = (SearchModel)bindingContext.Model ?? new SearchModel();
var properties = model.GetType().GetProperties();
foreach(var p in properties) {
var value = this.GetValue(bindingContext, p.Name);
p.SetValue(model, Convert.ChangeType(value, p.PropertyType), null);
}
return Task.CompletedTask;
}
protected string GetValue(ModelBindingContext context, string key) {
var result = context.ValueProvider.GetValue(key);
return result.FirstValue;
}
}
public class TestModelBinderProvider : IModelBinderProvider {
public IModelBinder GetBinder(ModelBinderProviderContext context) {
if (context == null) {
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.ModelType == typeof(SearchModel)) {
var returnType = new BinderTypeModelBinder(typeof(TestModelBinder));
return returnType;
}
return null;
}
}
As stated in the last step in Microsoft documentation I updated my ConfigureServices method in Startup.cs to include the BinderProvider.
services.AddMvc(options => {
options.ModelBinderProviders.Insert(0, new TestModelBinderProvider());
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
But when I call my Search endpoint with a url such as "https://localhost:44387/api/testbinding?searchTerms=newSearch" I am always seeing a return of "request == null True" even though I see it properly hit the custom binding and bind correctly if I step through debugging, can anyone please point me in the right direction as to what I am doing wrong?
[Route("api/[controller]")]
[ApiController]
public class TestBindingController : ControllerBase {
[HttpGet()]
public IActionResult GetResult([FromQuery] SearchModel request) {
return Ok($"request == null {request == null}");
}
}
I think what you're missing if the statement that sets the result of the model binding operation, as you can see in the AuthorEntityBinder code sample in this section of the docs:
bindingContext.Result = ModelBindingResult.Success(model);
Your implementation of the model binder does create an instance of SearchModel, but doesn't feed it back to the model binding context.
As a separate note, I don't think you need to add a custom model binder is the query string segments match the properties names of the model you're trying to bind.
I have implemented a result filter like this:
public class ResultWrapperFilter : IResultFilter
{
public void OnResultExecuting(ResultExecutingContext context)
{
if (!(context.ActionDescriptor is ControllerActionDescriptor))
{
return;
}
var objectResult = context.Result as ObjectResult;
if (objectResult == null)
{
return;
}
if (!(objectResult.Value is WrappedResponseBase))
{
objectResult.Value = new WrappedResponse(objectResult.Value);
}
}
public void OnResultExecuted(ResultExecutedContext context)
{
}
}
The filter is used by configuring MvcOptions through ConfigureServices(IServiceCollection services) like this:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<MvcOptions>(
options => { options.Filters.AddService<ResultWrapperFilter>(); });
services.AddMvc();
// ... the rest is omitted for readability
}
The problem I'm experiencing is this filter is causing InvalidCastException: Unable to cast object of type 'WrappedResponse' to type 'System.String' (the method in question has string as the return value type).
Am I even allowed to do this using IResultFilter?
NOTE: I am aware of the possibility of using middleware to accomplish the response wrapping. I don't want to use the middleware to accomplish this because the middleware doesn't have access to context.Result as ObjectResult. Deserializing from the response stream, wrapping and serializing again seems so unnecessary.
An answer just came to me.
When setting objectResult.Value, objectResult.DeclaredType also needs to be set.
So in this case:
if (!(objectResult.Value is WrappedResponseBase))
{
objectResult.Value = new WrappedResponse(objectResult.Value);
objectResult.DeclaredType = typeof(WrappedResponse);
}