WebApi 2 - Check for web.config values on startup - c#

I am interested in building a startup routine for my API to check that certain configuration values in the web.config are present. If the routine does not contain values I would like to redirect to a route, log the missing configuration item and display a custom application offline page.
Any assistance in pointing me in the right direction would be appreciated.
Guard Class
public static class Guard
{
public static bool ConfigurationValueExists(string key, [CallerMemberName] string caller = null)
{
if (!string.IsNullOrEmpty(Configuration.GetAppConfig(key, string.Empty))) return true;
ApiLogger.Log($"The configuration value {key} is not present and needs to be defined. Calling method is {caller}.");
return false;
}
}
Configuration Class
public static class Configuration
{
public static T GetAppConfig<T>(string key, T defaultVal = default(T))
{
if (null == ConfigurationManager.AppSettings[key])
{
return defaultVal;
}
return string.IsNullOrEmpty(key)
? defaultVal
: Generic.Turn(ConfigurationManager.AppSettings[key], defaultVal);
}
public static bool ConfigurationsAreInPlace()
{
return AssertMainApplicationConfiguration();
}
private static bool AssertMainApplicationConfiguration()
{
return Guard.ConfigurationValueExists("MyKey1");
}
}
I would like to be able to call ConfigurationsAreInPlace on the startup routine and redirect to my custom offline page.

I decided to create an Index Controller and use the Route Attribute of root to override what happens on the page. I then do the check if configurations are in place and issue a new response as needed.
Code if interested:
public class IndexController : ApiController
{
[AllowAnonymous]
[Route]
public HttpResponseMessage GetIndex()
{
string startUrl = "/help/";
if (!Helpers.Configuration.ConfigurationsAreInPlace())
{
startUrl += "offline";
}
HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.Moved);
string fullyQualifiedUrl = Request.RequestUri.GetLeftPart(UriPartial.Authority);
response.Headers.Location = new Uri(fullyQualifiedUrl + startUrl);
return response;
}
}

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

How to store data in cache?

I created a ViewComponent to display a List<Product>, the list is valorized taken data from a REST API service, this is my class implementation:
public class ProductsViewComponent : ViewComponent
{
private readonly HttpClient _client;
public ProductsViewComponent(HttpClient client)
{
_client = client ?? throw new ArgumentNullException(nameof(client));
}
public async Task<IViewComponentResult> InvokeAsync(string date)
{
using (var response = await _client.GetAsync($"/"product/get_products/{date}"))
{
response.EnsureSuccessStatusCode();
var products = await response.Content.ReadAsAsync<List<Product>>();
return View(products);
}
}
}
I load the List inside an html table which is available inside the Components folder: Views\Shared\Components\Products\Default.cshtml.
In each View that needs to display the Products I did:
#await Component.InvokeAsync("Products", new { date = myDate })
The REST API is called using the HttpClient configured in the Startup.cs as following:
services.AddHttpClient<ProductsViewComponent>(c =>
{
c.BaseAddress = new Uri('https://api.myservice.com');
});
This works well, but the main problem is each time the user reload the page or maybe go inside another View which require to display the list of products, then the app will make another API call.
Is possible store the list in something like a cache and prevent to call the API again if the date is equal than the previous date selected?
I'm learning ASP.NET Core so I'm not really expert on this argument.
Thanks in advance for any help.
As per microsoft documentation https://learn.microsoft.com/en-us/aspnet/core/performance/caching/memory?view=aspnetcore-2.1
you can use IMemoryCache to cache data
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMemoryCache();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
public void Configure(IApplicationBuilder app)
{
app.UseMvcWithDefaultRoute();
}
}
and create instance of IMemoryCache. This is an example from Microsoft documentation. You can Create another class to handle this all together and In below example this is just saving DateTime But, you can save any object in cache and when you try to read that value from cache just need to cast that object into a Type.
I will strongly recommend you go through the above documentation.
public class HomeController : Controller
{
private IMemoryCache _cache;
public HomeController(IMemoryCache memoryCache)
{
_cache = memoryCache;
}
public IActionResult CacheTryGetValueSet()
{
DateTime cacheEntry;
// Look for cache key.
if (!_cache.TryGetValue(CacheKeys.Entry, out cacheEntry))
{
// Key not in cache, so get data.
cacheEntry = DateTime.Now;
// Set cache options.
var cacheEntryOptions = new MemoryCacheEntryOptions()
// Keep in cache for this time, reset time if accessed.
.SetSlidingExpiration(TimeSpan.FromSeconds(3));
// Save data in cache.
_cache.Set(CacheKeys.Entry, cacheEntry, cacheEntryOptions);
}
return View("Cache", cacheEntry);
}
}
Update: CacheKeys.Entry is a static class where all keys are defined. (Just coding standards). Please check the above documentation link.
public static class CacheKeys
{
public static string Entry { get { return "_Entry"; } }
public static string CallbackEntry { get { return "_Callback"; } }
public static string CallbackMessage { get { return "_CallbackMessage"; } }
public static string Parent { get { return "_Parent"; } }
public static string Child { get { return "_Child"; } }
public static string DependentMessage { get { return "_DependentMessage";} }
public static string DependentCTS { get { return "_DependentCTS"; } }
public static string Ticks { get { return "_Ticks"; } }
public static string CancelMsg { get { return "_CancelMsg"; } }
public static string CancelTokenSource { get { return "_CancelTokenSource";} }
}
You can use a distributed cache and so use Redis for example with a ConnectionMultiplexer.
And so foreach call you can call your redis for the cache which is implement thanks to an interface call here 'IDistributedCache'
You can find a lot of documentation to implement cache and use it.
: .Net framework
DotNet Core
Your controller X :
[HttpGet]
[Route("{itemId}")]
public async Task<IHttpActionResult> GetItemById(int eventId, [FromUri]EventTabs tabId)
{
ServiceResponse<ItemDto> result = await _itemDispatcher.GetItemById(itemId);
return WrapResponse(result);
}
Your dispatcher to get the item by id which use redis cache (already implement)
public class ItemDispatcher : ItemDispatcher
{
private readonly IUnitOfWork _unitOfWork;
private readonly IDistributedCache _distributedCache; // use interface of your implementation of redis cache
private readonly int _cacheDuration;
private readonly bool _isCacheEnabled;
public EventDispatcher(IUnitOfWork unitOfWork, IDistributedCache distCache)
{
_unitOfWork = unitOfWork;
_distributedCache = distCache; // init cache in constructor
_cacheDuration = _configuration.Get<int>("cache.duration"); // duration of your cache
_isCacheEnabled = _configuration.Get<bool>("cache.isEnable"); // if the cache is enable or not
}
public async Task<ServiceResponse<ItemDto>> GetItemById(int id)
{
// Add this for each Task call
var cacheKey = string.Empty;
if (_isCacheEnabled)
{
cacheKey = CacheUtils.GetCacheKey(CacheKeys.Item, id);
itemDto cacheResult = await _distributedCache.Get<ItemDto>(cacheKey);
if (cacheResult != null)
return new ServiceResponse<Item>(cacheResult);
}
}
Try This
Cache["KeyName"] = VariableOrTable; Cache.Insert("Key", VariableOrTable, null,
Cache.NoAbsoluteExpiration, ts);

Can I use Content Negotiation to return a View to browers and JSON to API calls in ASP.NET Core?

I've got a pretty basic controller method that returns a list of Customers. I want it to return the List View when a user browses to it, and return JSON to requests that have application/json in the Accept header.
Is that possible in ASP.NET Core MVC 1.0?
I've tried this:
[HttpGet("")]
public async Task<IActionResult> List(int page = 1, int count = 20)
{
var customers = await _customerService.GetCustomers(page, count);
return Ok(customers.Select(c => new { c.Id, c.Name }));
}
But that returns JSON by default, even if it's not in the Accept list. If I hit "/customers" in my browser, I get the JSON output, not my view.
I thought I might need to write an OutputFormatter that handled text/html, but I can't figure out how I can call the View() method from an OutputFormatter, since those methods are on Controller, and I'd need to know the name of the View I wanted to render.
Is there a method or property I can call to check if MVC will be able to find an OutputFormatter to render? Something like the following:
[HttpGet("")]
public async Task<IActionResult> List(int page = 1, int count = 20)
{
var customers = await _customerService.GetCustomers(page, count);
if(Response.WillUseContentNegotiation)
{
return Ok(customers.Select(c => new { c.Id, c.Name }));
}
else
{
return View(customers.Select(c => new { c.Id, c.Name }));
}
}
I think this is a reasonable use case as it would simplify creating APIs that return both HTML and JSON/XML/etc from a single controller. This would allow for progressive enhancement, as well as several other benefits, though it might not work well in cases where the API and Mvc behavior needs to be drastically different.
I have done this with a custom filter, with some caveats below:
public class ViewIfAcceptHtmlAttribute : Attribute, IActionFilter
{
public void OnActionExecuted(ActionExecutedContext context)
{
if (context.HttpContext.Request.Headers["Accept"].ToString().Contains("text/html"))
{
var originalResult = context.Result as ObjectResult;
var controller = context.Controller as Controller;
if(originalResult != null && controller != null)
{
var model = originalResult.Value;
var newResult = controller.View(model);
newResult.StatusCode = originalResult.StatusCode;
context.Result = newResult;
}
}
}
public void OnActionExecuting(ActionExecutingContext context)
{
}
}
which can be added to a controller or action:
[ViewIfAcceptHtml]
[Route("/foo/")]
public IActionResult Get(){
return Ok(new Foo());
}
or registered globally in Startup.cs
services.AddMvc(x=>
{
x.Filters.Add(new ViewIfAcceptHtmlAttribute());
});
This works for my use case and accomplishes the goal of supporting text/html and application/json from the same controller. I suspect isn't the "best" approach as it side-steps the custom formatters. Ideally (in my mind), this code would just be another Formatter like Xml and Json, but that outputs Html using the View rendering engine. That interface is a little more involved, though, and this was the simplest thing that works for now.
I haven't tried this, but could you just test for that content type in the request and return accordingly:
var result = customers.Select(c => new { c.Id, c.Name });
if (Request.Headers["Accept"].Contains("application/json"))
return Json(result);
else
return View(result);
I liked Daniel's idea and felt inspired, so here's a convention based approach as well. Because often the ViewModel needs to include a little bit more 'stuff' than just the raw data returned from the API, and it also might need to check different stuff before it does its work, this will allow for that and help in following a ViewModel for every View principal. Using this convention, you can write two controller methods <Action> and <Action>View both of which will map to the same route. The constraint applied will choose <Action>View if "text/html" is in the Accept header.
public class ContentNegotiationConvention : IActionModelConvention
{
public void Apply(ActionModel action)
{
if (action.ActionName.ToLower().EndsWith("view"))
{
//Make it match to the action of the same name without 'view', exa: IndexView => Index
action.ActionName = action.ActionName.Substring(0, action.ActionName.Length - 4);
foreach (var selector in action.Selectors)
//Add a constraint which will choose this action over the API action when the content type is apprpriate
selector.ActionConstraints.Add(new TextHtmlContentTypeActionConstraint());
}
}
}
public class TextHtmlContentTypeActionConstraint : ContentTypeActionConstraint
{
public TextHtmlContentTypeActionConstraint() : base("text/html") { }
}
public class ContentTypeActionConstraint : IActionConstraint, IActionConstraintMetadata
{
string _contentType;
public ContentTypeActionConstraint(string contentType)
{
_contentType = contentType;
}
public int Order => -10;
public bool Accept(ActionConstraintContext context) =>
context.RouteContext.HttpContext.Request.Headers["Accept"].ToString().Contains(_contentType);
}
which is added in startup here:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(o => { o.Conventions.Add(new ContentNegotiationConvention()); });
}
In you controller, you can write method pairs like:
public class HomeController : Controller
{
public ObjectResult Index()
{
//General checks
return Ok(new IndexDataModel() { Property = "Data" });
}
public ViewResult IndexView()
{
//View specific checks
return View(new IndexViewModel(Index()));
}
}
Where I've created ViewModel classes meant to take the output of API actions, another pattern which connects the API to the View output and reinforces the intent that these two represent the same action:
public class IndexViewModel : ViewModelBase
{
public string ViewOnlyProperty { get; set; }
public string ExposedDataModelProperty { get; set; }
public IndexViewModel(IndexDataModel model) : base(model)
{
ExposedDataModelProperty = model?.Property;
ViewOnlyProperty = ExposedDataModelProperty + " for a View";
}
public IndexViewModel(ObjectResult apiResult) : this(apiResult.Value as IndexDataModel) { }
}
public class ViewModelBase
{
protected ApiModelBase _model;
public ViewModelBase(ApiModelBase model)
{
_model = model;
}
}
public class ApiModelBase { }
public class IndexDataModel : ApiModelBase
{
public string Property { get; internal set; }
}

Why WebApi marks empty string as error in model state?

I have strange behaviour of Web API, .Net 4.5.2. If optional string parameter is null, ModelState has no error. If it is not null and not empty, no errors again. But if it is just an empty string I have model state error.
Why do I get it and how to disable it?
Assuming app served on localhost:82 I have those results:
Url: http://localhost:82/
Response: "null"
Url: http://localhost:82/?q=1
Response: "1"
Url: http://localhost:82/?q=
Response: {
"Message": "The request is invalid.",
"ModelState": {
"q.String": [
"A value is required but was not present in the request."
]
}
}
Test controller and config is below. This is reduced to bare minimum default "Asp.net web application" with "WebApi" in VS2013.
namespace Web.Api.Test.Controllers
{
using System.Web.Http;
[Route]
public class HomeController : ApiController
{
[Route]
[HttpGet]
public IHttpActionResult Search(string q = default(string))
{
return this.ModelState.IsValid
? this.Ok(q ?? "null")
: (IHttpActionResult)this.BadRequest(this.ModelState);
}
}
}
Startup.cs is:
using Microsoft.Owin;
using WebApplication1;
[assembly: OwinStartup(typeof(Startup))]
namespace WebApplication1
{
using System.Web.Http;
using Newtonsoft.Json;
using Owin;
public class Startup
{
public void Configuration(IAppBuilder app)
{
GlobalConfiguration.Configure(config =>
{
config.MapHttpAttributeRoutes();
config.Formatters.JsonFormatter.SerializerSettings.Formatting = Formatting.Indented;
config.Formatters.Remove(config.Formatters.XmlFormatter);
});
}
}
}
PS: This question has a workaround, but it does not answer the main question: why does this situation happen and what reasons are behind this design decision.
I have had the same issue, came up with the following eventually:
public class SimpleTypeParameterBindingFactory
{
private readonly TypeConverterModelBinder converterModelBinder = new TypeConverterModelBinder();
private readonly IEnumerable<ValueProviderFactory> factories;
public SimpleTypeParameterBindingFactory(HttpConfiguration configuration)
{
factories = configuration.Services.GetValueProviderFactories();
}
public HttpParameterBinding BindOrNull(HttpParameterDescriptor descriptor)
{
return IsSimpleType(descriptor.ParameterType)
? new ModelBinderParameterBinding(descriptor, converterModelBinder, factories)
: null;
}
private static bool IsSimpleType(Type type)
{
return TypeDescriptor.GetConverter(type).CanConvertFrom(typeof (string));
}
}
public class Startup
{
public void Configure(IAppBuilder appBuilder)
{
var configuration = new HttpConfiguration();
configuration.ParameterBindingRules.Insert(0, new SimpleTypeParameterBindingFactory(configuration).BindOrNull);
configuration.EnsureInitialized();
}
}
The problem is rooted in some magic code in ModelValidationNode, which creates model errors for null models even if corresponding parameter has default value. The code above just replaces CompositeModelBinder (which calls ModelValidationNode) with TypeConverterModelBinder for simple type parameters.
Why do I get it and how to disable it?
Don't know why you get it. This maybe how you disable it, but after reading I don't think you want to really as there are simpler solutions, e.g:
Use of a model class solves this in a cleaner way.
public class SearchModel
{
public string Q { get; set; }
}
public IHttpActionResult Search([FromUri] SearchModel model)
{
return ModelState.IsValid
? Ok(model.Q ?? "null")
: (IHttpActionResult) BadRequest(ModelState);
}
That's why:
This is a MVC feature which binds empty strings to nulls.
We have found the same behavior in our application, and deep dive debugging with source code
git clone https://github.com/ASP-NET-MVC/aspnetwebstack
makes sense to search in the right direction. Here the method which set whitespace strings to null, and here the error is added to model state:
if (parentNode == null && ModelMetadata.Model == null)
{
string trueModelStateKey = ModelBindingHelper.CreatePropertyModelName(ModelStateKey, ModelMetadata.GetDisplayName());
modelState.AddModelError(trueModelStateKey, SRResources.Validation_ValueNotFound);
return;
}
IMHO it is a bug. But who cares. We used this workaround
Have you tried [DisplayFormat(ConvertEmptyStringToNull = false)]?
We found another solution
public class EmptyStringToNullModelBinder : Attribute, IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
ValueProviderResult valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
bindingContext.Model = string.IsNullOrWhiteSpace(valueResult?.RawValue?.ToString()) ? null : valueResult.RawValue;
return true;
}
}
and for your case it would be like this:
[Route]
[HttpGet]
public IHttpActionResult Search([FromUri(BinderType = typeof(EmptyStringToNullModelBinder))]string q = null)
{
return this.ModelState.IsValid
? this.Ok(q ?? "null")
: (IHttpActionResult)this.BadRequest(this.ModelState);
}
code below is adapted version of this answer
public class WebApiDefaultValueBinder<T> : IModelBinder
{
public bool BindModel(System.Web.Http.Controllers.HttpActionContext actionContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType != typeof(T))
{
return false;
}
var val = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (val == null)
{
return false;
}
var rawValue = val.RawValue as string;
// Not supplied : /test/5
if (rawValue == null)
{
bindingContext.Model = default(T);
return true;
}
// Provided but with no value : /test/5?something=
if (rawValue == string.Empty)
{
bindingContext.Model = default(T);
return true;
}
// Provided with a value : /test/5?something=1
try
{
bindingContext.Model = (T)Convert.ChangeType(val.RawValue, typeof(T));
return true;
}
catch
{
//
}
bindingContext.ModelState.AddModelError(bindingContext.ModelName, $"Cannot convert value to {typeof(T).Name}");
return false;
}
}

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