Custom Model Binder Provider always null .net core - asp.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.

Related

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

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

DataAnnotationsModelValidatorProvider.RegisterAdapter in ASP.Net Core MVC

In ASP.Net MVC 5, custom data annotation validator can be implemented by inheriting DataAnnotationsModelValidator and registering using DataAnnotationsModelValidatorProvider.RegisterAdapter(...). In ASP.Net Core MVC, how can I achieve this?
I found similar question at ASP.net core MVC 6 Data Annotations separation of concerns, but can anyone show me simple example code?
It seems to me ASP.NET Core MVC does not have support for DataAnnotationsModelValidatorProvider.RegisterAdapter anymore. The solution I discovered is as follows:
Suppose I want to change the Validator for RequiredAttribute to my own validator adaptor (MyRequiredAttributeAdaptor), Change the default error message of EmailAddressAttribute, and change the Localized Error Message Source for 'CompareAttribute' to my own message.
1- Create a custom ValidationAttributeAdapterProvider
using Microsoft.AspNetCore.Mvc.DataAnnotations;
using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal;
using Microsoft.Extensions.Localization;
using System.ComponentModel.DataAnnotations;
public class CustomValidationAttributeAdapterProvider
: ValidationAttributeAdapterProvider, IValidationAttributeAdapterProvider
{
public CustomValidationAttributeAdapterProvider() { }
IAttributeAdapter IValidationAttributeAdapterProvider.GetAttributeAdapter(
ValidationAttribute attribute,
IStringLocalizer stringLocalizer)
{
IAttributeAdapter adapter;
if (attribute is RequiredAttribute)
{
adapter = new MyRequiredAttributeAdaptor((RequiredAttribute) attribute, stringLocalizer);
}
else if (attribute is EmailAddressAttribute)
{
attribute.ErrorMessage = "Invalid Email Address.";
adapter = base.GetAttributeAdapter(attribute, stringLocalizer);
}
else if (attribute is CompareAttribute)
{
attribute.ErrorMessageResourceName = "InvalidCompare";
attribute.ErrorMessageResourceType = typeof(Resources.ValidationMessages);
var theNewattribute = attribute as CompareAttribute;
adapter = new CompareAttributeAdapter(theNewattribute, stringLocalizer);
}
else
{
adapter = base.GetAttributeAdapter(attribute, stringLocalizer);
}
return adapter;
}
}
2- Add the CustomValidationAttributeAdapterProvider to start up:
Add the following line to public void ConfigureServices(IServiceCollection services) in Startup.cs:
services.AddSingleton <IValidationAttributeAdapterProvider, CustomValidationAttributeAdapterProvider> ();
Here is MyRequiredAttributeAdaptor adaptor:
using System;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Localization;
using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal;
public class MyRequiredAttributeAdaptor : AttributeAdapterBase<RequiredAttribute>
{
public MyRequiredAttributeAdaptor(RequiredAttribute attribute, IStringLocalizer stringLocalizer)
: base(attribute, stringLocalizer)
{
}
public override void AddValidation(ClientModelValidationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
MergeAttribute(context.Attributes, "data-val", "true");
MergeAttribute(context.Attributes, "data-val-required", GetErrorMessage(context));
}
/// <inheritdoc />
public override string GetErrorMessage(ModelValidationContextBase validationContext)
{
if (validationContext == null)
{
throw new ArgumentNullException(nameof(validationContext));
}
return GetErrorMessage(validationContext.ModelMetadata, validationContext.ModelMetadata.GetDisplayName());
}
}
References:
1- See the example of Microsoft: Entropy project: This is a great sample for diffrent features of .NET Core. In this question: see the MinLengthSixAttribute implementation in the Mvc.LocalizationSample.Web sample:
https://github.com/aspnet/Entropy/tree/dev/samples/Mvc.LocalizationSample.Web
2- In order to see how the attribute adapters works see asp.Microsoft.AspNetCore.Mvc.DataAnnotations on github:
https://github.com/aspnet/Mvc/tree/master/src/Microsoft.AspNetCore.Mvc.DataAnnotations
To define a custom validator by a annotation you can define your own class that derives from ValidationAttribute and override the IsValid method. There is no need to register this class explicitly.
In this example a custom validation attribute is used to accept only odd numbers as valid values.
public class MyModel
{
[OddNumber]
public int Number { get; set; }
}
public class OddNumberAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
try
{
var number = (int) value;
if (number % 2 == 1)
return ValidationResult.Success;
else
return new ValidationResult("Only odd numbers are valid.");
}
catch (Exception)
{
return new ValidationResult("Not a number.");
}
}
}
A second approach is that the Model class implements IValidatableObject. This is especially useful, if validation requires access to multiple members of the model class. Here is the second version of the odd number validator:
public class MyModel : IValidatableObject
{
public int Number { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (Number % 2 == 0)
yield return new ValidationResult(
"Only odd numbers are valid.",
new [] {"Number"});
}
}
You can find more information about custom validation in https://docs.asp.net/en/latest/mvc/models/validation.html#custom-validation.

Add values to Web API model using ActionFilter or Custom Model Binder?

I'm using WebAPI 2 and I'm finding that most of my API POST endpoints are requiring the same two values be added to the model: UserId from HttpContext.Current.User.Identity.GetUserId() and SignalR_ConnectionId from a custom request header. I'd like to add these two values to the model without repeating the same code within each request handler.
I like the idea of using an ActionFilter because then I can easily decorate the endpoints that need this functionality with the annotation. So far my ActionFilter looks like the following:
public class AddUserIdActionFilter : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
var headerValues = actionContext.Request.Headers.GetValues("SignalRId");
if (HttpContext.Current.User.Identity != null)
{
Guid userId = Guid.Parse(HttpContext.Current.User.Identity.GetUserId());
}
else
{
throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.Unauthorized));
}
//Modify the model??
}
}
How do I modify the model within the ActionFilter? (I assume that each model that is decorated with the attribute would descend from a base class with a property for userId and SignalRId).
If I can't add the values to the model, I'll have to consider a Custom Model Binder. I don't want to change the default model binding, just add my two properties to what's already happening. How do I approach this in Web API 2?
The following worked fine.
public class AddUserIdActionFilter : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
IEnumerable<string> headers;
string connectionId;
Guid userId;
if(actionContext.Request.Headers.TryGetValues("SignalR_Id", out headers))
{
connectionId = headers.First();
}
else
{
throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.BadRequest));
}
if (HttpContext.Current.User.Identity != null)
{
userId = Guid.Parse(HttpContext.Current.User.Identity.GetUserId());
}
else
{
throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.Unauthorized));
}
try
{
var model = (BaseCommand)actionContext.ActionArguments.Values.First();
model.UserId = userId;
model.ConnectionId = connectionId;
}
catch
{
throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.InternalServerError));
}
}
}

How can i define default binder for type in MVC6?

I have following working code:
public ObjectResult GetDocumentById([ModelBinder(BinderType = typeof(CustomModelBinder))] CustomId id)
How can I specify CustomModelBinder to be the default binder for CustomId type?
Have you tried decorating your CustomId class with ModelBinderAttribute?
[ModelBinder(BinderType = typeof(CustomModelBinder))]
public class CustomId { }
You can also register your model binder directly in ConfigureServices in your Startup class (note that IModelBinderProvider no longer exists in ASP.NET 5):
public void ConfigureServices(IServiceCollection services) {
services.ConfigureMvc(options => {
options.ModelBinders.Insert(0, new CustomModelBinder());
});
}
Since your model binder will be the first one to be invoked, return null when it cannot handle the model type to make sure the built-in binders will still be invoked for other types:
public class CancellationTokenModelBinder : IModelBinder
{
/// <inheritdoc />
public Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext.ModelType == typeof(CancellationToken))
{
var model = bindingContext.OperationBindingContext.HttpContext.RequestAborted;
var validationNode =
new ModelValidationNode(bindingContext.ModelName, bindingContext.ModelMetadata, model);
return Task.FromResult(new ModelBindingResult(
model,
bindingContext.ModelName,
isModelSet: true,
validationNode: validationNode));
}
return Task.FromResult<ModelBindingResult>(null);
}
}
Here is an example to have a custom binder that, instead of null, binds empty string to the properties of type string
public Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{
var key = bindingContext.ModelName;
var value = bindingContext.ValueProvider.GetValue(key);
if ((bindingContext.ModelType == typeof(string)) && (string.IsNullOrWhiteSpace(value.FirstValue)))
{
var result = ModelBindingResult.Success(key, string.Empty);
return Task.FromResult<ModelBindingResult>(result);
}
return Task.FromResult<ModelBindingResult>(default(ModelBindingResult));
}
Note that compared to the answer posted above, in DNX 1.0.0-rc1-update1, the line
return Task.FromResult<ModelBindingResult>(null);
causes a compile error because ModelBindingResult is a struct (a non-nullable value type).
Try changing it to the following in order to fix the compile error
return Task.FromResult<ModelBindingResult>(default(ModelBindingResult));

How do I get access to my requests content in a custom model binder in Asp Net MVC 4 Web Api?

I have been thinking about how I can solve the problem I had in my previous question
Can I get access to the data that the .net web api model binding was not able to handle?
I'm thing that I can use my own custom model binder, that way I can handle the perfect case , and write to a log when I get data that I wasn't expecting.
I have the following class and Model Binders
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
}
public class CustomPersonModelBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
var myPerson = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
var myPersonName = bindingContext.ValueProvider.GetValue("Name");
var myId = bindingContext.ValueProvider.GetValue("Id");
bindingContext.Model = new Person {Id = 2, Name = "dave"};
return true;
}
}
public class CustomPersonModelBinderProvider : ModelBinderProvider
{
private CustomPersonModelBinder _customPersonModelBinder = new CustomPersonModelBinder();
public override IModelBinder GetBinder(HttpConfiguration configuration, Type modelType)
{
if (modelType == typeof (Person))
{
return _customPersonModelBinder;
}
return null;
}
}
and here is my controller method
public HttpResponseMessage Post([ModelBinder(typeof(CustomPersonModelBinderProvider))]Person person)
{
return new HttpResponseMessage(HttpStatusCode.OK);
}
And I have been invoking it using fiddler with
Post http://localhost:18475/00.00.001/trial/343
{
"Id": 31,
"Name": "Camera Broken"
}
This works great, Without using the custom model binder I get a Person object populated from my json data in my post method, and with the custom model binder I always get a person(Id= 2, Name = "dave").
The problem is I can't seem to get access to the JSon data in my custom Model binder.
The myPerson and myPersonName variables in the bindModel method are both null. however the myId variable is populated with 343.
Any Ideas how I can get access to the data in the json within my BindModel method?
Try this:
actionContext.Request.Content.ReadAsStreamAsync()