FluentValidation with IActionFilter in Asp.net Core 2.1 [duplicate] - asp.net-core

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();
}

Related

Asp.net Core + Swagger : How to show APIs of type GET

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

specify which API is documented by Swagger

I'm new to swagger and have it installed and running but it's picking up far more API files than desired. I have been hunting a way to specify which API is documented.
You can put an ApiExplorerSettings attribute on a controller to remove it from Swagger:
[ApiExplorerSettings(IgnoreApi = true)]
public class TestApiController : ControllerBase
{
}
If you want to apply this on a lot of controllers based on some logic,
it can be done e.g. with an action model convention: https://github.com/juunas11/AspNetCoreHideRoutesFromSwagger/blob/983bad788755b4a81d2cce30f82bc28887b61924/HideRoutesFromSwagger/Controllers/SecondController.cs#L18-L28
public class ActionHidingConvention : IActionModelConvention
{
public void Apply(ActionModel action)
{
// Replace with any logic you want
if (action.Controller.ControllerName == "Second")
{
action.ApiExplorer.IsVisible = false;
}
}
}
The convention is added in ConfigureServices like:
services.AddControllers(o =>
{
o.Conventions.Add(new ActionHidingConvention());
});

How to use polymorphism one method on controller actions

I tried to convert ASP.NET WEB API to ASP.NET CORE WEB API and have errors
My code in ASP.NET WebAPI
public class TestController : ApiController
{
// GET /test
public object Get()
{
return "get";
}
// GET /test?id={id}
public object Get(string id)
{
return id;
}
// GET /test?id={id}&anyParam={anyParam}
public object Get(string id, string anyParam)
{
return id + anyParam;
}
}
config.Routes.MapHttpRoute("Controller", "{controller}");
Try to convert it to ASP.NET Core 2.1 / 3.0
[ApiController]
[Route("{controller}")]
public class TestController : ControllerBase
{
// GET /test
public object Get()
{
return "get";
}
// GET /test?id={id}
public object Get(string id)
{
return id;
}
// GET /test?id={id}&anyParam={anyParam}
public object Get(string id, string anyParam)
{
return id + anyParam;
}
}
services.AddControllers();
app.UseRouting();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
And i have in ASP.NET Core
AmbiguousMatchException: The request matched multiple endpoints
The sensible solution is just have one method that takes three parameters.
But, sensible solutions don't make for the most interesting stackoverflow answers, so here is how you can do this with two custom attributes, one which states the parameters that are required, and another which states which parameters are excluded:
public class RequireRequestParameterAttribute : ActionMethodSelectorAttribute
{
private readonly string[] _requiredNames;
public RequireRequestParameterAttribute(params string[] names)
{
this._requiredNames = names;
}
public override bool IsValidForRequest(
RouteContext routeContext,
ActionDescriptor action
) =>
this._requiredNames
.All(
routeContext
.HttpContext
.Request
.Query
.ContainsKey
);
}
public class DisallowRequestParameterAttribute : ActionMethodSelectorAttribute
{
private readonly string[] _forbiddenNames;
public DisallowRequestParameterAttribute(params string[] names)
{
this._forbiddenNames = names;
}
public override bool IsValidForRequest(
RouteContext routeContext,
ActionDescriptor action
) =>
!(this._forbiddenNames
.Any(
routeContext
.HttpContext
.Request
.Query
.ContainsKey
)
);
}
Now you can apply the attributes as follows:
[ApiController]
[Route("[controller]")]
public class TestController : ControllerBase
{
// GET test
public object Get()
{
return "Get";
}
// GET test?id={id}
[RequireRequestParameter("id")]
[DisallowRequestParameter("anyParam")]
public object Get(string id)
{
return id;
}
// GET test?id={id}&anyParam={anyParam}
[RequireRequestParameter("id", "anyParam")]
public object Get(string id, string anyParam)
{
return $"{id}: {anyParam}";
}
}
This means if you add another method with a third parameter, you have the maintenance burden of adding or modifying the DisallowRequestParameter attribute on the other methods.
I look your generated urls on actions and they are both /test which cause AmbiguousMatchException because your parameters are GET and are optional.
I think you can have same names on actions but you need define different ROUTE attribute (diff urls) on actions. Eg. you can not use default route with polymorphism on controller actions.
[Route("Home/About")]
MVC controllers Mapping of controllers now takes place inside
UseEndpoints.
Add MapControllers if the app uses attribute routing.
Source
https://learn.microsoft.com/cs-cz/aspnet/core/mvc/controllers/routing?view=aspnetcore-3.0#attribute-routing
Thanks to daremachine with his answer I was able to find information on Google
First step in ASP.NET Core we need class which inherit ActionMethodSelectorAttribute
public class RequireRequestValueAttribute : ActionMethodSelectorAttribute
{
public RequireRequestValueAttribute(string name, string value = null)
{
Name = name;
Value = value;
}
public string Name { get; }
public string Value { get; }
public StringComparison ComparisonType { get; } = StringComparison.OrdinalIgnoreCase;
private bool ValueIsValid(object value)
{
return ValueIsValid(value?.ToString());
}
private bool ValueIsValid(string value)
{
if (Value == null)
{
return true;
}
return string.Equals(value, Value, ComparisonType);
}
public override bool IsValidForRequest(RouteContext routeContext, ActionDescriptor action)
{
var value = default(object);
if (routeContext.RouteData.Values.TryGetValue(Name, out value) && ValueIsValid(value))
return true;
if (routeContext.RouteData.DataTokens.TryGetValue(Name, out value) && ValueIsValid(value))
return true;
if (routeContext.HttpContext.Request.Query.ContainsKey(Name))
{
var values = routeContext.HttpContext.Request.Query[Name];
if (values.Count <= 0)
{
if (ValueIsValid(null))
return true;
}
else if (values.Any(v => ValueIsValid(v)))
return true;
}
return false;
}
}
Then we can add to question methods [RequireRequestValue("")], the controller will look like this
[ApiController]
[Route("{controller}")]
public class TestController : ControllerBase
{
// GET /test
public object Get()
{
return "get";
}
// GET /test?id={id}
[RequireRequestValue("id")]
public object Get(string id)
{
return id;
}
}
But it can't polymorphism two similar fields, type id in my question
For asp net core 2. If you try to implement the same logic as was in web api controllers then use Microsoft.AspNetCore.Mvc.WebApiCompatShim. This nuget package provides compatibility in ASP.NET Core MVC with ASP.NET Web API 2 to simplify migration of existing Web API implementations. Please check this answer. Starting with ASP.NET Core 3.0, the Microsoft.AspNetCore.Mvc.WebApiCompatShim package is no longer available.

Custom Model Binder Provider always null .net core

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.

Wrapping results of ASP.NET Core WebAPI methods using IResultFilter

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);
}