ServiceStack marker Attribute not found in Collection - c#

I have a custom attribute and I want to check via a GlobalFilter, if the methods I'm calling has this marker attribute.
I can't find a way, to get the information that the called method where my request aims to has the Attribute.
I've found already another post, which recommends to use FilterAttributeCache.GetRequestFilterAttributes(request.GetType()) but this and also other methods are retuning just no elements.
Could you please help me in what I'm missing here?
Following the example code:
CustomAttribute:
using ServiceStack;
using ServiceStack.Web;
namespace MyProject.Web
{
public class CustomAttribute : RequestFilterAttribute
{
public string ServiceName;
public string ServiceClass;
public string ServiceMethod;
public JwtAuthAttribute(string serviceName, string serviceClass, string serviceMethod)
{
ServiceName = serviceName;
ServiceClass = serviceClass;
ServiceMethod = serviceMethod;
}
public override void Execute(IRequest req, IResponse res, object requestDto)
{
//Nothing in here
}
}
}
Filter:
using System;
using System.Linq;
using ServiceStack.Support.WebHost;
using ServiceStack.Web;
namespace MyProject.Web
{
public class CustomFilter
{
private readonly ICustomManager _customManager;
public CustomFilter(ICustomManager customManager)
{
_customManager= customManager;
}
public void Execute(IRequest request)
{
var customHeader = request.Headers.GetValues("CustomHeader");
if (customHeader == null)
{
return;
}
var customAttribute = FilterAttributeCache.GetRequestFilterAttributes(request.GetType())
.OfType<CustomAttribute>().FirstOrDefault();
if (customAttribute == null)
{
return;
}
// Do other things here
}
}
}
Registration of GlobalFilter in Global.asax:
public override void Configure(Container container)
{
//other configurations
GlobalRequestFilters.Add((req, res, dto) => req.ResponseContentType = MimeTypes.Json);
GlobalRequestFilters.Add((req, res, dto) =>
{
var customFilter = new CustomFilter(request.TryResolve<ICustomManager>());
customFilter.Execute(req);
});
}
Method with marker CustomAttribute:
namespace MyProject.Web
{
[Authenticate]
[RequiredRole("CustomUser")]
public class CustomService : Service
{
[Custom("Service", "ServiceClass", "ServiceMethod")]
public object Get(CustomRequest request)
{
//Additional code
}
}
}

I'd recommend adding attributes on the Request DTO which is much easier to access since the Request DTO is available everywhere throughout ServiceStack request pipeline, e.g. dto.GetType() in global filters or IRequest.Dto.GetType() everywhere else.
To access ServiceStack actions, you'd need to access the service Type then its actions, e.g:
GlobalRequestFilters.Add((req, res, dto) =>
{
var serviceType = HostContext.AppHost.Metadata.GetServiceTypeByRequest(dto.GetType());
var action = serviceType.GetActions().First(x => x.RequestType == dto.GetType());
var attrs = action.AllAttributes<CustomAttribute>();
});

After the answer from mythz I was able to fix the issue. I've extented the "Execute" method of "CustomFilter" by requestDto, which was provided by the GobalRequestFilter registration.
Unfortunately I was not able to access the Property "RequestType", maybe because I'm using an older version (ServiceStack 5.9.2, .Net Framework 4.7.2), so I had to go over the MethodParameters.
Here is the final code, which gave me the access to the "CustomAttribute"
var serviceTypeDto = HostContext.AppHost.Metadata.GetServiceTypeByRequest(requestDto.GetType());
var methodInfo = serviceTypeDto.GetDeclaredMethods().FirstOrDefault(x =>
x.GetParameters().FirstOrDefault(y => y.ParameterType == requestDto.GetType()) != null && x.IsPublic);
var customAttribute = methodInfo.AllAttributes<CustomAttribute>().FirstOrDefault();

Related

How to hook into the Asp.Net Core 6 MVC route system to customize urls generation

In the old .Net Framework MVC implementations, I was creating routes by myself so that I could also influence urls generation. Part of the code:
public class RouteBase : Route
{
public RouteBase(string url, IRouteHandler routeHandler) : base(url, routeHandler) { }
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
if (Url.Contains("{segment}") && !values.ContainsKey("segment"))
values["segment"] = requestContext.HttpContext.Items["segmentValue"];
return base.GetVirtualPath(requestContext, values);
}
}
Thanks to GetVirtualPath, I was able to detect a particular segment in the route template and inject a proper value in the route values dictionary so that the client app did not have to specify it when calling for instance Url.RouteUrl(routeName).
In asp.net core 6, I'm now using attributes based routing and I don't know how to hook into this so that I can inject some value into the route values dictionary when I generate urls. If I have a route template like so:
[Route("{segment}/test", Name = "name"]
When I call this, I want an injection mechanism from somewhere else in the code so that the known segment value is injected into the route values used to build the url:
var url = Url.RouteUrl("name"); // Not passing new { segment = value } as second param
For information, I simply use this in Startup:
app.MapControllers();
You can create and register a custom UrlHelper. It will give you the ability to manipulate the behavior as per your use case:
public class CustomUrlHelper : UrlHelper
{
public CustomUrlHelper(ActionContext actionContext)
: base(actionContext) { }
public override string? RouteUrl(UrlRouteContext routeContext)
{
// if(routeContext.RouteName == "name" && routeContext.Values....)
// routeContext.Values = ....
return base.RouteUrl(routeContext);
}
}
public class CustomUrlHelperFactory : IUrlHelperFactory
{
public IUrlHelper GetUrlHelper(ActionContext context)
{
return new CustomUrlHelper(context);
}
}
and in your Program.cs:
builder.Services.AddSingleton<IUrlHelperFactory, CustomUrlHelperFactory>();
Then by calling the Url.RouteUrl("name"), your CustomUrlHelper will be called.
Amir's answer put me on track to find a solution (bounty award for him). Creating a custom UrlHelper was the way to go, but not with a UrlHelper derived class. For enpoint routing, the framework is using the sealed EndpointRoutingUrlHelper class. So I just needed to derive from UrlHelperBase, paste the code from EndpointRoutingUrlHelper and add my customizations. I was lucky that there were no internal pieces of code in there...
Here is the solution. Note that:
the term "segment" mentioned in the original question is replaced by what I actually have in my code i.e. "lang".
HttpContext.Items["lang"] is set by a middleware.
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc;
// The custom UrlHelper is registered with serviceCollection.AddSingleton<IUrlHelperFactory, LanguageAwareUrlHelperFactory>();
public class LanguageAwareUrlHelperFactory : IUrlHelperFactory
{
private readonly LinkGenerator _linkGenerator;
public LanguageAwareUrlHelperFactory(LinkGenerator linkGenerator)
{
_linkGenerator = linkGenerator;
}
public IUrlHelper GetUrlHelper(ActionContext context)
{
return new LanguageAwareUrlHelper(context, _linkGenerator);
}
}
// Source code is taken from https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/Routing/EndpointRoutingUrlHelper.cs
// and modified to inject the desired route value
public class LanguageAwareUrlHelper : UrlHelperBase
{
private readonly LinkGenerator _linkGenerator;
public LanguageAwareUrlHelper(ActionContext actionContext, LinkGenerator linkGenerator) : base(actionContext)
{
if (linkGenerator == null)
throw new ArgumentNullException(nameof(linkGenerator));
_linkGenerator = linkGenerator;
}
public override string? Action(UrlActionContext urlActionContext)
{
if (urlActionContext == null)
throw new ArgumentNullException(nameof(urlActionContext));
var values = GetValuesDictionary(urlActionContext.Values);
if (urlActionContext.Action == null)
{
if (!values.ContainsKey("action") && AmbientValues.TryGetValue("action", out var action))
values["action"] = action;
}
else
values["action"] = urlActionContext.Action;
if (urlActionContext.Controller == null)
{
if (!values.ContainsKey("controller") && AmbientValues.TryGetValue("controller", out var controller))
values["controller"] = controller;
}
else
values["controller"] = urlActionContext.Controller;
if (!values.ContainsKey("lang") && ActionContext.HttpContext.Items.ContainsKey("lang"))
values["lang"] = ActionContext.HttpContext.Items["lang"];
var path = _linkGenerator.GetPathByRouteValues(
ActionContext.HttpContext,
routeName: null,
values,
fragment: urlActionContext.Fragment == null ? FragmentString.Empty : new FragmentString("#" + urlActionContext.Fragment));
return GenerateUrl(urlActionContext.Protocol, urlActionContext.Host, path);
}
public override string? RouteUrl(UrlRouteContext routeContext)
{
if (routeContext == null)
throw new ArgumentNullException(nameof(routeContext));
var langRouteValues = GetValuesDictionary(routeContext.Values);
if (!langRouteValues.ContainsKey("lang") && ActionContext.HttpContext.Items.ContainsKey("lang"))
langRouteValues.Add("lang", ActionContext.HttpContext.Items["lang"]);
var path = _linkGenerator.GetPathByRouteValues(
ActionContext.HttpContext,
routeContext.RouteName,
langRouteValues,
fragment: routeContext.Fragment == null ? FragmentString.Empty : new FragmentString("#" + routeContext.Fragment));
return GenerateUrl(routeContext.Protocol, routeContext.Host, path);
}
}
In Asp.Net Core, I use the below two methods and it is able to successfully generate the URL.
[Route("{segment}/test", Name = "name"]
var url1 = Url.RouteUrl("name", new { segment = "aa" });
var url2 = Url.Action("Action", "Controller", new { segment = "aa" });

Custom Authorize attribute - ASP .NET Core 2.2

I want to create a custom Authorize attribute to be able to send a personalized response when it fails. There are many examples, but I could not find what I'm looking for.
When registering a policy, I add a "claim". Is it possible to access that registered claim within the custom attribute without having to pass the claim by parameter? or is it possible to know if the check of the claim happened and if not, return a personalized response? Thx!
public static void AddCustomAuthorization(this IServiceCollection serviceCollection)
{
serviceCollection.AddAuthorization(x =>
{
x.AddPolicy(UserPolicy.Read,
currentPolicy => currentPolicy.RequireClaim(UserClaims.Read));
});
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class CustomAuthorizeAttribute : AuthorizeAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext authorizationFilterContext)
{
if (authorizationFilterContext.HttpContext.User.Identity.IsAuthenticated)
{
if (!authorizationFilterContext.HttpContext.User.HasClaim(x => x.Value == "CLAIM_NAME")) // ACCESS TO REGISTER CLAIM => currentPolicy => currentPolicy.RequireClaim(UserClaims.Read)
{
authorizationFilterContext.Result = new ObjectResult(new ApiResponse(HttpStatusCode.Unauthorized));
}
}
}
}
[HttpGet]
[CustomAuthorizeAttribute(Policy = UserPolicy.Read)]
public async Task<IEnumerable<UserDTO>> Get()
{
return ...
}
You can use IAuthorizationPolicyProvider to get the policy and then use ClaimsAuthorizationRequirement.ClaimType to get a claim name. And since it has async API, it is better to use IAsyncAuthorizationFilter instead of IAuthorizationFilter. Try this:
public class CustomAuthorizeAttribute : AuthorizeAttribute, IAsyncAuthorizationFilter
{
public async Task OnAuthorizationAsync(AuthorizationFilterContext authorizationFilterContext)
{
var policyProvider = authorizationFilterContext.HttpContext
.RequestServices.GetService<IAuthorizationPolicyProvider>();
var policy = await policyProvider.GetPolicyAsync(UserPolicy.Read);
var requirement = (ClaimsAuthorizationRequirement)policy.Requirements
.First(r => r.GetType() == typeof(ClaimsAuthorizationRequirement));
if (authorizationFilterContext.HttpContext.User.Identity.IsAuthenticated)
{
if (!authorizationFilterContext.HttpContext
.User.HasClaim(x => x.Value == requirement.ClaimType))
{
authorizationFilterContext.Result =
new ObjectResult(new ApiResponse(HttpStatusCode.Unauthorized));
}
}
}
}
This attribute takes an array of strings, which was needed in my case. I needed to pass different users roles to this attribute and return result based on some custom logic.
public class CustomAuthFilter : AuthorizeAttribute, IAuthorizationFilter
{
public CustomAuthFilter(params string[] args)
{
Args = args;
}
public string[] Args { get; }
public void OnAuthorization(AuthorizationFilterContext context)
{
//Custom code ...
//Resolving a custom Services from the container
var service = context.HttpContext.RequestServices.GetRequiredService<ISample>();
string name = service.GetName(); // returns "anish"
//Return based on logic
context.Result = new UnauthorizedResult();
}
}
You can decorate your controller with this attribute as shown below
[CustomAuthFilter("Anish","jiya","sample")]
public async Task<IActionResult> Index()
Sample is a class that returns a hard coded string
public class Sample : ISample
{
public string GetName() => "anish";
}
services.AddScoped(); //Register ISample, Sample as scoped.
FOR ASYNCHRONOUS SUPPORT use IAsyncAuthorizationFilter
public class CustomAuthFilter : AuthorizeAttribute, IAsyncAuthorizationFilter
{
public CustomAuthFilter(params string[] args)
{
Args = args;
}
public string[] Args { get; }
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
//DO Whatever...
//Resolve Services from the container
var service = context.HttpContext.RequestServices.GetRequiredService<ISample>();
var httpClientFactory = context.HttpContext.RequestServices.GetRequiredService<IHttpClientFactory>();
string name = service.GetName();
using var httpClient = httpClientFactory.CreateClient();
var resp = await httpClient.GetAsync("https://jsonplaceholder.typicode.com/todos/1");
var data = await resp.Content.ReadAsStringAsync();
//Return based on logic
context.Result = new UnauthorizedResult();
}
}
Hope that helps..

.Net Core Override Controller Route for Generic Controller

I'm writing a RestFramework and I'm trying to figure out how I can allow the users to create a custom name for a generic controller. I'm registering my generic controllers like so:
public class GenericControllerFeatureProvider : IApplicationFeatureProvider<ControllerFeature>
{
public void PopulateFeature(IEnumerable<ApplicationPart> parts, ControllerFeature feature)
{
foreach (var entityConfig in _entityConfigurations)
{
var entityType = entityConfig.Type;
var typeName = entityType.Name + "Controller";
if (!feature.Controllers.Any(t => t.Name == typeName))
{
var controllerType = typeof(GenericController<>)
.MakeGenericType(entityType.AsType())
.GetTypeInfo();
//Normally I would expect there to be an overload to configure the controller name
//feature.Controllers.Add(controllerType, entityConfig.ControllerName);
}
}
}
}
How ever I need to figure out a way that I can override the route for the controllers. The only information about this in the documentation shows how to create a controller convention like so:
public class GenericControllerNameConvention : Attribute, IControllerModelConvention
{
public void Apply(ControllerModel controller)
{
if (controller.ControllerType.GetGenericTypeDefinition() !=
typeof(GenericController<>))
{
return;
}
var entityType = controller.ControllerType.GenericTypeArguments[0];
controller.ControllerName = entityType.Name;
}
}
This will not work since it is done at compile time. I need user to be able to override the controller name on Startup, How can I Achieve this?
Based on your comment and code you were pretty much on par with how you would achieve this. Note I have cut down the example quite a bit so I could setup a test.
Say I have a basic generic controller as:
public class GenericController<T> : Controller
where T: class
{
public IActionResult Get()
{
return Content(typeof(T).FullName);
}
}
I now have a typed controller with Get action. Now most of your code was right on the money. So my Feature Provider as (note i have a static array of types):
public class GenericControllerFeatureProvider : IApplicationFeatureProvider<ControllerFeature>
{
public void PopulateFeature(IEnumerable<ApplicationPart> parts, ControllerFeature feature)
{
foreach (var entityConfig in ControllerEntity.EntityTypes)
{
var entityType = entityConfig;
var typeName = entityType.Name + "Controller";
if (!feature.Controllers.Any(t => t.Name == typeName))
{
var controllerType = typeof(GenericController<>)
.MakeGenericType(entityType)
.GetTypeInfo();
feature.Controllers.Add(controllerType);
}
}
}
}
Next the IControllerModelConvention implementation.
public class GenericControllerModelConvention : IControllerModelConvention
{
public void Apply(ControllerModel controller)
{
if (!controller.ControllerType.IsGenericType || controller.ControllerType.GetGenericTypeDefinition() != typeof(GenericController<>))
{
return;
}
var entityType = controller.ControllerType.GenericTypeArguments[0];
controller.ControllerName = entityType.Name + "Controller";
controller.RouteValues["Controller"] = entityType.Name;
}
}
And finally the startup is where all the magic happens. Basically we register the IControllerModelConvention into the MVC convention options, and then register the FeatureProvider.
public void ConfigureServices(IServiceCollection services)
{
var mvcBuilder = services.AddMvc();
mvcBuilder.AddMvcOptions(o => o.Conventions.Add(new GenericControllerModelConvention()));
mvcBuilder.ConfigureApplicationPartManager(c =>
{
c.FeatureProviders.Add(new GenericControllerFeatureProvider());
});
}
From my review two things struck me.
I am not sure why you have your GenericControllerNameConvention as an attribute?
You should implicitly set the Controller Route Value to your entity type (not the type + name).
Given two entities (EntityA and EntityB) the result of the controllers is
/Entitya/get/ prints WebApplication11.Infrastructure.EntityA
/Entityb/get/ prints WebApplication11.Infrastructure.EntityB

Web Api Required Parameter

Using ASP.NET Web API. Is there a way to automatically return a status code 400 if a parameter is null? I found this question but that is a global solution that is applied to all methods, I want to do this on a per method per parameter basis.
So, for example, this is what I am currently doing:
public HttpResponseMessage SomeMethod(SomeNullableParameter parameter)
{
if (parameter == null)
throw new HttpResponseException(HttpStatusCode.BadRequest);
// Otherwise do more stuff.
}
I would really just like to do something like this (notice the required attribute):
public HttpResponseMessage SomeMethod([Required] SomeNullableParameter parameter)
{
// Do stuff.
}
The approach I ended up using was to create a custom filter that I registered globally. The filter checks all request parameters for the RequiredAttribute. If the attribute is found then it checks if the parameter was passed with the request (not null) and returns status code 400 if it was null. I also added a cache to the filter to store the required parameters for each request to avoid the reflection hit on future calls. I was pleasantly surprised to find that this works for value types as well since the action context stores the parameters as objects.
EDIT - Updated solution based on tecfield's comment
public class RequiredParametersFilter : ActionFilterAttribute
{
// Cache used to store the required parameters for each request based on the
// request's http method and local path.
private readonly ConcurrentDictionary<Tuple<HttpMethod, string>, List<string>> _Cache =
new ConcurrentDictionary<Tuple<HttpMethod, string>, List<string>>();
public override void OnActionExecuting(HttpActionContext actionContext)
{
// Get the request's required parameters.
List<string> requiredParameters = this.GetRequiredParameters(actionContext);
// If the required parameters are valid then continue with the request.
// Otherwise, return status code 400.
if(this.ValidateParameters(actionContext, requiredParameters))
{
base.OnActionExecuting(actionContext);
}
else
{
throw new HttpResponseException(HttpStatusCode.BadRequest);
}
}
private bool ValidateParameters(HttpActionContext actionContext, List<string> requiredParameters)
{
// If the list of required parameters is null or containst no parameters
// then there is nothing to validate.
// Return true.
if (requiredParameters == null || requiredParameters.Count == 0)
{
return true;
}
// Attempt to find at least one required parameter that is null.
bool hasNullParameter =
actionContext
.ActionArguments
.Any(a => requiredParameters.Contains(a.Key) && a.Value == null);
// If a null required paramter was found then return false.
// Otherwise, return true.
return !hasNullParameter;
}
private List<string> GetRequiredParameters(HttpActionContext actionContext)
{
// Instantiate a list of strings to store the required parameters.
List<string> result = null;
// Instantiate a tuple using the request's http method and the local path.
// This will be used to add/lookup the required parameters in the cache.
Tuple<HttpMethod, string> request =
new Tuple<HttpMethod, string>(
actionContext.Request.Method,
actionContext.Request.RequestUri.LocalPath);
// Attempt to find the required parameters in the cache.
if (!this._Cache.TryGetValue(request, out result))
{
// If the required parameters were not found in the cache then get all
// parameters decorated with the 'RequiredAttribute' from the action context.
result =
actionContext
.ActionDescriptor
.GetParameters()
.Where(p => p.GetCustomAttributes<RequiredAttribute>().Any())
.Select(p => p.ParameterName)
.ToList();
// Add the required parameters to the cache.
this._Cache.TryAdd(request, result);
}
// Return the required parameters.
return result;
}
}
Set [Required] on a property in your model and then check the ModelState to see if it IsValid.
This will allow all the required properties to be tested at the same time.
See the "Under-Posting" section # Model validation in WebAPI
we can use the BindRequired, which is from Microsoft.AspNetCore.Mvc.ModelBinding namespace.
public async Task<ActionResult<IEnumerable<Numbers>>> GetAll([BindRequired, FromQuery]string[] numbers)
{
var result = await _service.GetAllDetails(numbers);
return Ok(result);
}
after that your swagger will look like below.
A solution for asp.net core...
[AttributeUsage(AttributeTargets.Method)]
public sealed class CheckRequiredModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
var requiredParameters = context.ActionDescriptor.Parameters.Where(
p => ((ControllerParameterDescriptor)p).ParameterInfo.GetCustomAttribute<RequiredModelAttribute>() != null).Select(p => p.Name);
foreach (var argument in context.ActionArguments.Where(a => requiredParameters.Contains(a.Key, StringComparer.Ordinal)))
{
if (argument.Value == null)
{
context.ModelState.AddModelError(argument.Key, $"The argument '{argument.Key}' cannot be null.");
}
}
if (!context.ModelState.IsValid)
{
var errors = context.ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage);
context.Result = new BadRequestObjectResult(errors);
return;
}
base.OnActionExecuting(context);
}
}
[AttributeUsage(AttributeTargets.Parameter)]
public sealed class RequiredModelAttribute : Attribute
{
}
services.AddMvc(options =>
{
options.Filters.Add(typeof(CheckRequiredModelAttribute));
});
public async Task<IActionResult> CreateAsync([FromBody][RequiredModel]RequestModel request, CancellationToken cancellationToken)
{
//...
}
The accepted solution takes it upon itself to report back any errors. A more appropriate approach for MVC5 is to let the controller handle (via model validation) the reporting of any errors, aka something like this:
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
using System.Web.Http.ModelBinding;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public sealed class ValidateParametersAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext context)
{
var descriptor = context.ActionDescriptor;
if (descriptor != null)
{
var modelState = context.ModelState;
foreach (var parameterDescriptor in descriptor.GetParameters())
{
EvaluateValidationAttributes(
suppliedValue: context.ActionArguments[parameterDescriptor.ParameterName],
modelState: modelState,
parameterDescriptor: parameterDescriptor
);
}
}
base.OnActionExecuting(context);
}
static private void EvaluateValidationAttributes(HttpParameterDescriptor parameterDescriptor, object suppliedValue, ModelStateDictionary modelState)
{
var parameterName = parameterDescriptor.ParameterName;
parameterDescriptor
.GetCustomAttributes<object>()
.OfType<ValidationAttribute>()
.Where(x => !x.IsValid(suppliedValue))
.ForEach(x => modelState.AddModelError(parameterName, x.FormatErrorMessage(parameterName)));
}
}
You may then plug it in universally via WebApiConfig.cs:
config.Filters.Add(new ValidateParametersAttribute());

How to hook FluentValidator to a Web API?

I'm trying to hook Fluent Validation to my MVC WEB Api project, and it doesn't wanna work.
When I use MyController : Controller -> works fine (ModelState.IsValid returns False)
but when I use MyController :ApiController ... nothing.
Does anyone have experience on how to hook those up ?
latest version of Fluent Validation (5.0.0.1) supports web api
Just install it from Nuget and register it in Global.asax like so:
using FluentValidation.Mvc.WebApi;
public class WebApiApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
...
FluentValidationModelValidatorProvider.Configure();
}
}
The answer is in this pull request.
Basically You need to implement custom ModelValidation Provider.
And a couple more things to note:
Web API don't work with modelValidator from System.Web.Mvc namespace, only with the ones from System.Web.Http as noted here:
Server side validation with custom DataAnnotationsModelValidatorProvider
You don't add it like this:
ModelValidatorProviders.Providers.Add(new WebApiFluentValidationModelValidatorProvider());`
BUT like this:
GlobalConfiguration.Configuration.Services.Add(typeof(System.Web.Http.Validation.ModelValidatorProvider), new WebApiFluentValidationModelValidatorProvider());`
I have found another simple solution for using FluentValidation in Web API, but it lacks integration with ModelState and Metadata. However, when building an API that doesn't need to return the entire ModelState to the client (as is needed in MVC to rebuild the page), I have found the trade-off for simplicity to be worthwhile. Whenever an API input is invalid, I return a 400 Bad Request status code with a list of property IDs and error messages. To do this, I use a simple ActionFilterAttribute:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class ValidateInputsAttribute : ActionFilterAttribute
{
private static readonly IValidatorFactory ValidatorFactory = new AttributedValidatorFactory();
public override void OnActionExecuting(HttpActionContext actionContext)
{
base.OnActionExecuting(actionContext);
var errors = new Dictionary<string, string>();
foreach (KeyValuePair<string, object> arg in actionContext.ActionArguments.Where(a => a.Value != null))
{
var argType = arg.Value.GetType();
IValidator validator = ValidatorFactory.GetValidator(argType);
if (validator != null)
{
var validationResult = validator.Validate(arg.Value);
foreach (ValidationFailure error in validationResult.Errors)
{
errors[error.PropertyName] = error.ErrorMessage;
}
}
}
if (errors.Any())
{
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.BadRequest, errors);
}
}
}
This attribute can be added as a global filter, to individual controllers/actions, or to a base class.
This code can certainly be improved, but it has served me well so far so I wanted to make it available to others. Here are some of its shortcomings:
Null inputs are not validated. I thought that this would be more of a problem, but in practice it simply doesn't happen much (if at all) in our app. My controllers throw ArgumentNullExceptions for null inputs which would return a 500 to the client informing the client that the input cannot be null.
I can't use ModelState in my controllers. But, after validating the required inputs are non-null, I already know that the ModelState is valid so this may actually serve to simplify code. But it's important for devs to know not to use it.
Right now this implementation is hard coded for the AttributedValidatorFactory. This should be abstracted, but it's been pretty low on my priority list so far.
As I was looking to solve this I wanted to make it so that the same validator instance could be used for MVC and Web API. I was able to accomplish this by making two factories and using them together.
MVC Factory:
public class MVCValidationFactory : ValidatorFactoryBase
{
private readonly IKernel _kernel;
public MVCValidationFactory(IKernel kernel)
{
_kernel = kernel;
}
public override IValidator CreateInstance(Type validatorType)
{
var returnType = _kernel.TryGet(validatorType);
return returnType as IValidator;
}
}
API Factory:
public class WebAPIValidationFactory : ModelValidatorProvider
{
private readonly MVCValidationFactory _mvcValidationFactory;
private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
public WebAPIValidationFactory(MVCValidationFactory mvcValidationFactory)
{
_mvcValidationFactory = mvcValidationFactory;
}
public override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, IEnumerable<ModelValidatorProvider> validatorProviders)
{
try
{
var type = GetType(metadata);
if (type != null)
{
var fluentValidator =
_mvcValidationFactory.CreateInstance(typeof(FluentValidation.IValidator<>).MakeGenericType(type));
if (fluentValidator != null)
{
yield return new FluentValidationModelValidator(validatorProviders, fluentValidator);
}
}
}
catch (Exception ex)
{
Log.Error(ex);
}
return new List<ModelValidator>();
}
private static Type GetType(ModelMetadata metadata)
{
return metadata.ContainerType != null ? metadata.ContainerType.UnderlyingSystemType : null;
}
The trick then was figuring out how to run the validation for both MVC and Web API. I ended up creating a wrapper for the IValidator<> that worked with the ModelValidator signature.
public class FluentValidationModelValidator : ModelValidator
{
public IValidator innerValidator { get; private set; }
public FluentValidationModelValidator(
IEnumerable<ModelValidatorProvider> validatorProviders, IValidator validator)
: base(validatorProviders)
{
innerValidator = validator;
}
public override IEnumerable<ModelValidationResult> Validate(ModelMetadata metadata, object container)
{
if (InnerValidator != null && container != null)
{
var result = innerValidator.Validate(container);
return GetResults(result);
}
return new List<ModelValidationResult>();
}
private static IEnumerable<ModelValidationResult> GetResults(FluentValidation.Results.ValidationResult result)
{
return result.Errors.Select(error =>
new ModelValidationResult
{
MemberName = error.PropertyName,
Message = error.ErrorMessage
}));
}
}
The last part was to wire up the validators in the Global.asax:
MVCValidationFactory mvcValidationFactory = new MVCValidationFactory(KernelProvider.Instance.GetKernel());
GlobalConfiguration.Configuration.Services.Add(
typeof(ModelValidatorProvider),
new WebAPIValidationFactory(mvcValidationFactory));
ModelValidatorProviders.Providers.Add(new FluentValidationModelValidatorProvider(mvcValidationFactory));
DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = false;
Sorry this was a bit long, but hopefully it helps someone out.
In the WebApiConfig add two lines
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// snip...
//Fluent Validation
config.Filters.Add(new ValidateModelStateFilter());
FluentValidationModelValidatorProvider.Configure(config);
}
}
Create a model and a validator as follows -
[Validator(typeof(PersonCreateRequestModelValidator))]
public class PersonCreateRequestModel
{
public Guid PersonId { get; set; }
public string Firstname { get; set; }
public string Lastname { get; set; }
}
public class PersonCreateRequestModelValidator : AbstractValidator
{
//Simple validator that checks for values in Firstname and Lastname
public PersonCreateRequestModelValidator()
{
RuleFor(r => r.Firstname).NotEmpty();
RuleFor(r => r.Lastname).NotEmpty();
}
}
That's about all you need. Just write the controller as you would normally.
public IHttpActionResult Post([FromBody]PersonCreateRequestModel requestModel)
{
//snip..
//return Ok(some new id);
}
If you want a full source code example you can get it here - http://NoDogmaBlog.bryanhogan.net/2016/12/fluent-validation-with-web-api-2/
Latest version of Fluent Validation doesn't support Mvc 4 or Web Api.
Read this.

Categories