FluentValidation use custom IActionFilter - c#

I have a custom IActionFilter which I register with my application like so:
services.AddControllers(options => options.Filters.Add(new HttpResponseExceptionFilter()));
The class looks like this:
public class HttpResponseExceptionFilter : IActionFilter, IOrderedFilter
{
public int Order { get; set; } = int.MaxValue - 10;
public void OnActionExecuting(ActionExecutingContext context)
{
}
public void OnActionExecuted(ActionExecutedContext context)
{
if (context.Exception == null) return;
var attempt = Attempt<string>.Fail(context.Exception);
if (context.Exception is AttemptException exception)
{
context.Result = new ObjectResult(attempt)
{
StatusCode = exception.StatusCode,
};
}
else
{
context.Result = new ObjectResult(attempt)
{
StatusCode = (int)HttpStatusCode.InternalServerError,
};
}
context.ExceptionHandled = true;
}
}
I would expect that when validating it would invoke the OnActionExecuting method. So I added this code:
public void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(context.ModelState);
}
}
And I put a breakpoint at the start of the method, but when I run my application and try to post an invalid model, I get this response:
{
"errors": {
"Url": [
"'Url' is invalid. It should start with 'https://www.youtube.com/embed'",
"'Url' is invalid. It should have the correct parameter '?start='"
]
},
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "|87e96062-42181357ba1ef8c5."
}
How can I force FluentValidation to use my filter?

When [ApiController] attribute is applied ,ASP.NET Core automatically handles model validation errors by returning a 400 Bad Request with ModelState as the response body :
Automatic HTTP 400 responses
To disable the automatic 400 behavior, set the SuppressModelStateInvalidFilter property to true :
services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
options.SuppressModelStateInvalidFilter = true;
});

The best solution I found was:
.ConfigureApiBehaviorOptions(options =>
{
options.InvalidModelStateResponseFactory = context =>
{
var messages = context.ModelState.Values
.Where(x => x.ValidationState == ModelValidationState.Invalid)
.SelectMany(x => x.Errors)
.Select(x => x.ErrorMessage)
.ToList();
return new BadRequestObjectResult(
Attempt<string>.Fail(
new AttemptException(string.Join($"{Environment.NewLine}", messages))));
};
})

Related

How to return a value instead of a status if request is not authorised?

[authorise]
public string Get()
{
return "value1";
}
if I am not authorised it will return a status of 401 not authorised.
can it return a value such as json "{status:false,code:"401"}". ?
According to your description, I suggest you could try to use custommiddleware to achieve your requirement.
You could captured the 401 error in middleware and then rewrite the response body to {status:false,code:"401"}
More details, you could add below codes into Configure method above the app.UseAuthentication();:
app.Use(async (context, next) =>
{
await next();
if (context.Response.StatusCode == 401)
{
await context.Response.WriteAsync("{status:false,code:'401'}");
}
});
Result:
You can create a custom authorize attribute using IAsyncAuthorizationFilter.
public class CustomAuthorizeFilter : IAsyncAuthorizationFilter
{
public AuthorizationPolicy Policy { get; }
public CustomAuthorizeFilter(AuthorizationPolicy policy)
{
Policy = policy ?? throw new ArgumentNullException(nameof(policy));
}
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// Allow Anonymous skips all authorization
if (context.Filters.Any(item =&gt; item is IAllowAnonymousFilter))
{
return;
}
var policyEvaluator = context.HttpContext.RequestServices.GetRequiredService&lt;IPolicyEvaluator&gt;();
var authenticateResult = await policyEvaluator.AuthenticateAsync(Policy, context.HttpContext);
var authorizeResult = await policyEvaluator.AuthorizeAsync(Policy, authenticateResult, context.HttpContext, context);
if (authorizeResult.Challenged)
{
// Return custom 401 result
context.Result = new CustomUnauthorizedResult("Authorization failed.");
}
else if (authorizeResult.Forbidden)
{
// Return default 403 result
context.Result = new ForbidResult(Policy.AuthenticationSchemes.ToArray());
}
}
}
public class CustomUnauthorizedResult : JsonResult
{
public CustomUnauthorizedResult(string message)
: base(new CustomError(message))
{
StatusCode = StatusCodes.Status401Unauthorized;
}
}
public class CustomError
{
public string Error { get; }
public CustomError(string message)
{
Error = message;
}
}
The code in this article does exactly what you want. click here
can it return a value such as json "{status:false,code:"401"}". ?
Sure, you can.
[ApiController]
[Produces("application/json")]
public class TestController : ControllerBase
{
public IActionResult Get()
{
if (User.Identity.IsAuthenticated)
{
return new OkObjectResult(new { status: true, code: 200 });
}
return new OkObjectResult(new { status: false, code: 401 });
}
}
But notice that, the request will return with the real status code 200 (OK)
You can also use UnauthorizedObjectResult like #vivek's comment:
return new UnauthorizedObjectResult(new { status: false, code: 401 });
You can return the below if using Asp.Net Core 3.1, It returns UnauthorizedObjectResult.
return Unauthorized(new { status: false, code: 401 });

asp.net core middleware can't catch newtonsoft exception

i use middleware catch request exception and write response like this
public async Task Invoke(HttpContext context /* other dependencies */)
{
try
{
await next(context);
}
catch (Exception ex)
{
logger.LogError(ex.Message);
await HandleExceptionAsync(context, ex); //write response
}
}
private static Task HandleExceptionAsync(HttpContext context, Exception exception)
{
FanjiaApiResultMessage resultMessage = new FanjiaApiResultMessage()
{
ResultCode = -1,
Data = null,
Msg = exception.Message
};
string result = JsonConvert.SerializeObject(resultMessage);
context.Response.ContentType = "application/json;charset=utf-8";
if (exception is QunarException)
{
context.Response.StatusCode = (int)(exception as QunarException).httpStatusCode;
}
else
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
return context.Response.WriteAsync(result);
}
a request model param like this
public class FlightModel {
[JsonProperty("depCity", Required = Required.Always)]
public string DepCity { get; set; }
}
public IActionResult Test(FlightModel model){
return Content("test");
}
when i post the FlightModel without DepCity , i will get the exception
{
"errors": {
"": [
"Required property 'depCity' not found in JSON. Path '', line 6, position 1."
]
},
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "8000000a-0003-ff00-b63f-84710c7967bb"
}
Obviously the exception are not catched by middleware.
why middleware is not catch?
An Aspnet Core Model Validation failure does not throw an exception. It provides it's own response with a status code of 400 (Bad request) in a default format.
There are a few ways to override this including a custom attribute: https://www.jerriepelser.com/blog/validation-response-aspnet-core-webapi/
It looks like this:
public class ValidateModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new ValidationFailedResult(context.ModelState);
}
}
}
And then is added like so:
[Route("api/values")]
[ValidateModel]
public class ValuesController : Controller
{
...
}
Or you can control the response generation by overriding the InvalidModelStateResponseFactory, like in this SO question: How do I customize ASP.Net Core model binding errors?
Here is an example:
services.Configure<ApiBehaviorOptions>(o =>
{
o.InvalidModelStateResponseFactory = actionContext =>
new MyCustomBadRequestObjectResult(actionContext.ModelState);
});

Why is my GlobalExceptionFilter not working on NET.Core?

I'm trying to implement GlobalExceptionFilter in NET Core WEB API.
This my filter code:
public class GlobalExceptionFilter : IExceptionFilter, IDisposable
{
private readonly ILogger _logger;
public GlobalExceptionFilter(ILoggerFactory logger)
{
if (logger == null)
{
throw new ArgumentNullException(nameof(logger));
}
this._logger = logger.CreateLogger("Global Exception Filter");
}
public void Dispose()
{
}
public void OnException(ExceptionContext context)
{
Dictionary<string, string> data = new Dictionary<string, string>();
HttpStatusCode statusCode = HttpStatusCode.InternalServerError;
String message = String.Empty;
var ex = context.Exception;
TypeSwitch.Do(ex,
TypeSwitch.Case<ArgumentException>(() => { statusCode = HttpStatusCode.BadRequest; }),
TypeSwitch.Case<ArgumentNullException>(() => { statusCode = HttpStatusCode.BadRequest; }),
TypeSwitch.Case<ArgumentOutOfRangeException>(() => { statusCode = HttpStatusCode.BadRequest; }),
TypeSwitch.Case<KeyNotFoundException>(() => { statusCode = HttpStatusCode.NotFound; }),
TypeSwitch.Case<DivideByZeroException>(() => { statusCode = HttpStatusCode.MethodNotAllowed; }),
TypeSwitch.Case<QueryFormatException>(() => { statusCode = HttpStatusCode.MethodNotAllowed; })
);
HttpResponse response = context.HttpContext.Response;
response.StatusCode = (int)statusCode;
response.ContentType = "application/json";
var err = new ErrorPayload()
{
Data = data,
StackTrace = ex.StackTrace,
Message = ex.Message,
StatusCode = (int)statusCode
};
response.WriteAsync(JsonConvert.SerializeObject(err));
}
}
This is my initializing code in
public void ConfigureServices(IServiceCollection services)
{
services.AddApplicationInsightsTelemetry(Configuration);
services.AddMvc( config =>
{
config.Filters.Add(typeof(GlobalExceptionFilter));
}
);
}
And i'm testing the error handling in this controller method
[HttpGet("{idCliente}")]
public IActionResult GetCliente(int idCliente)
{
throw new QueryFormatException("My Custom Exception");
}
Any ideas? thanks!
UPDATE
Well, I have to admit that I asume that wasn't working because Postman shows me connection error instead of MethodNotAllowed of NotFound (404). As suggested i examine the debug and the status response and was actually expected value.
As the docs say (last section)
Prefer middleware for exception handling. Use exception filters only
where you need to do error handling differently based on which MVC
action was chosen. For example, your app might have action methods for
both API endpoints and for views/HTML. The API endpoints could return
error information as JSON, while the view-based actions could return
an error page as HTML.
In your case, if the application only serving API:s then use the exception middleware implementation instead. Here's a good example of one

Customize automatic response on validation error

With asp.net core 2.1 an ApiController will automatically respond with a 400 BadRequest when validation errors occur.
How can I change/modify the response (json-body) that is sent back to the client? Is there some kind of middleware?
I´m using FluentValidation to validate the parameters sent to my controller, but I am not happy with the response that I am get. It looks like
{
"Url": [
"'Url' must not be empty.",
"'Url' should not be empty."
]
}
I want to change the response, cause we have some default values that we attach to responses. So it should look like
{
"code": 400,
"request_id": "dfdfddf",
"messages": [
"'Url' must not be empty.",
"'Url' should not be empty."
]
}
The ApiBehaviorOptions class allows for the generation of ModelState responses to be customised via its InvalidModelStateResponseFactory property, which is of type Func<ActionContext, IActionResult>.
Here's an example implementation:
apiBehaviorOptions.InvalidModelStateResponseFactory = actionContext => {
return new BadRequestObjectResult(new {
Code = 400,
Request_Id = "dfdfddf",
Messages = actionContext.ModelState.Values.SelectMany(x => x.Errors)
.Select(x => x.ErrorMessage)
});
};
The incoming ActionContext instance provides both ModelState and HttpContext properties for the active request, which contains everything I expect you could need. I'm not sure where your request_id value is coming from, so I've left that as your static example.
To use this implementation, configure the ApiBehaviorOptions instance in ConfigureServices:
serviceCollection.Configure<ApiBehaviorOptions>(apiBehaviorOptions =>
apiBehaviorOptions.InvalidModelStateResponseFactory = ...
);
Consider creating of custom action filer, e.g.:
public class CustomValidationResponseActionFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
var errors = new List<string>();
foreach (var modelState in context.ModelState.Values)
{
foreach (var error in modelState.Errors)
{
errors.Add(error.ErrorMessage);
}
}
var responseObj = new
{
code = 400,
request_id = "dfdfddf",
messages = errors
};
context.Result = new JsonResult(responseObj)
{
StatusCode = 400
};
}
}
public void OnActionExecuted(ActionExecutedContext context)
{ }
}
You can register it in ConfigureServices:
services.AddMvc(options =>
{
options.Filters.Add(new CustomValidationResponseActionFilter());
});

Model state error filter

On my DTO objects I have several attributes to check it's validity
And I catch such a body response when validation is failed
{
"TransactionId": [
"Max length is 20"
],
"AdditionalInfo": [
"Additional Info has to be no longer than 30 chars"
]
}
But I need to unify all the errors to be with "Error" key.
Something like that
{
"Error": [
"Max length is 20",
"Additional Info has to be no longer than 30 chars"
]
}
I wrote special filter and registered it in Startup.cs
public class ModelStateErrorHandlingFilter : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
if (!context.ModelState.IsValid)
{
context.ModelState.SetModelValue("Errors", new ValueProviderResult(new StringValues(context.ModelState.ToString())));
context.Result = new BadRequestObjectResult(context.ModelState);
}
else
{
await next().ConfigureAwait(false);
}
}
}
But nothing changes. I also have tried to change the key, but it has privat setter
You would need to provide you own custom IActionResult or build the desired object model and pass it to an appropriate ObjectResult.
public class ModelStateErrorHandlingFilter : IAsyncActionFilter {
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) {
if (!context.ModelState.IsValid) {
var model = new {
Error = context.ModelState
.SelectMany(keyValuePair => keyValuePair.Value.Errors)
.Select(modelError => modelError.ErrorMessage)
.ToArray()
};
context.Result = new BadRequestObjectResult (model);
} else {
await next().ConfigureAwait(false);
}
}
}
setting context.Result will short-circuit the request and pass it your custom response with desired content.

Categories