I just wasted a lot of hours with trying to get a custom ComplexTypeModelBinder to work. Whatever I did, it never worked. As it turns out, this only works when the data is POSTed as form data; when you post a JSON object (in my case from a Swagger "try out" form) the ComplexTypeModelBinder never invokes the SetProperty method.
I have a lot of models, some more complex than others, and I have annotated some of the properties with a custom attribute. Whenever that property is bound I want it to 'normalized' (apply some 'formatting' to it) so that by the time the model gets validated the validator gets to see the 'normalized' data instead of the user-entered data.
I really, really, want to keep the current modelbinding behavior because that currently works fine but with the one exception that the annotated properties are processed by some code implemented by me. All other properties and behavior should be kept as-is. That is why I hoped to inherit from ComplexTypeModelBinder, but, as it turns out, this doesn't work if data is POSTed as JSON.
My (example) model looks like:
public class MyComplexModel
{
public int Id { get; set; }
public string Name { get; set; }
[FormatNumber(NumberFormat.E164)]
public string PhoneNumber { get; set; }
}
My controller method looks like this:
[HttpPost]
public MyComplexModel Post(MyComplexModel model)
{
return model;
}
My (not working) custom ComplexTypeModelBinder looks like:
public class MyModelBinder : ComplexTypeModelBinder
{
private readonly INumberFormatter _numberformatter;
private static readonly ConcurrentDictionary<Type, Dictionary<string, FormatNumberAttribute>> _formatproperties = new ConcurrentDictionary<Type, Dictionary<string, FormatNumberAttribute>>();
public MyModelBinder(IDictionary<ModelMetadata, IModelBinder> propertyBinders, INumberFormatter numberFormatter, ILoggerFactory loggerFactory)
: base(propertyBinders, loggerFactory)
{
_numberformatter = numberFormatter;
}
protected override object CreateModel(ModelBindingContext bindingContext)
{
// Index and cache all properties having the FormatNumber Attribute
_formatproperties.GetOrAdd(bindingContext.ModelType, (t) =>
{
return t.GetProperties().Where(prop => Attribute.IsDefined(prop, typeof(FormatNumberAttribute))).ToDictionary(pi => pi.Name, pi => pi.GetCustomAttribute<FormatNumberAttribute>(), StringComparer.OrdinalIgnoreCase);
});
return base.CreateModel(bindingContext);
}
protected override Task BindProperty(ModelBindingContext bindingContext)
{
return base.BindProperty(bindingContext);
}
protected override void SetProperty(ModelBindingContext bindingContext, string modelName, ModelMetadata propertyMetadata, ModelBindingResult result)
{
if (_formatproperties.TryGetValue(bindingContext.ModelType, out var props) && props.TryGetValue(modelName, out var att))
{
// Do our formatting here
var formatted = _numberformatter.FormatNumber(result.Model as string, att.NumberFormat);
base.SetProperty(bindingContext, modelName, propertyMetadata, ModelBindingResult.Success(formatted));
} else
{
// Do nothing
base.SetProperty(bindingContext, modelName, propertyMetadata, result);
}
}
}
(The actual MyModelBinder checks for the FormatNumber attribute and handles the property accordingly, but I left it out for brevity since it doesn't really matter).
And my ModelBinderProvider:
public class MyModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
var modelType = context.Metadata.ModelType;
if (!typeof(MyComplexModel).IsAssignableFrom(modelType))
return null;
if (!context.Metadata.IsComplexType || context.Metadata.IsCollectionType)
return null;
var propertyBinders = context.Metadata.Properties
.ToDictionary(modelProperty => modelProperty, context.CreateBinder);
return new MyModelBinder(
propertyBinders,
(INumberFormatter)context.Services.GetService(typeof(INumberFormatter)),
(ILoggerFactory)context.Services.GetService(typeof(ILoggerFactory))
);
}
}
And ofcourse, I added the provider in the StartUp class:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(config =>
{
config.ModelBinderProviders.Insert(0, new MyModelBinderProvider());
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
Again, this works fine when data is posted as form-data but not when posted as JSON. What would be the correct way to implement this? I have read somewhere that I shouldn't be looking in the ModelBinding direction but in the "JSON converters" direction but I haven't found anything that actually worked (yet).
Edit: I have created a git repository to demonstrate my problem here. To see my problem, set a breakpoint here in the TestController where the model is returned in the Post method. Start the project; a simple webpage will be shown with two buttons. The left one will post the form data as, well, form-data and you will see the model being returned with a reversed phonenumber (as an example). Click the right button and the data will be posted as a JSON model. Notice the model being returned having a 0 id and null values for both properties.
Related
I have multiple Object with different structure, but each time i make a save on these objects, i want to carry out common control actions because these objects are in fact "content objects" of a similar process.
imagine in UI user can do tasks, he must enter different data depend on task but we stay in the same "process".
i want to avoid multiple HttPut Route, so i want something like this :
[HttpPut("{processId}/content")]
public ActionResult SaveContent(IContent content)
{
//Do something with the processID like security controll
//Now i can do the different save
}
i want to have an instance of my specific object, i can determine type with a property "ContentType"
i would like to have a custombinder to do something like this :
if(content.GetContent==ContentA)
{
return ContentA with all properties binded from body
}
else if(content.GetContent==ContentB)
{
return ContentB with all properties binded from body
}
i dont want to map properties manually, i want to work as if I had put "ContentA" or "ContentB" in [FromBody] parameter.
i just want to avoid this:
[HttpPut("{processId}/content-a")]
public ActionResult SaveContentA(ContentA contentA)
{
//Do something with the processID like security control
//Now i can save contentA
}
[HttpPut("{processId}/content-b")]
public ActionResult SaveContentB(ContentB contentb)
{
//Do something with the processID like security control
//Now i can save contentB
}
I can have 20+ different content, that's a lot of different routes (of course I have different services behind that do different actions depending on content A or B but I would like to avoid as many routes).
I looked at the side of the custombinding but I did not manage to do what I wanted.
Thank you.
I'm not so sure if I understand your requirement correctly. From what you shared, the IContent is some kind of a base interface which contains a common set of members (properties, ...) for all the derived types (which should be well-known). So it's actually a scenario of polymorphic model binding and can be implemented by using a customer IModelBinder as demonstrated in a basic example here https://learn.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-model-binding?view=aspnetcore-5.0#polymorphic-model-binding
I've adjusted that example a bit to make it cleaner with more separate responsibility and better naming. The following code is not tested at all. Because the main logic is basically the same as from the example in the given link above:
//the marker interface to indicate the model should be polymorphically bound.
//you can replace this with your IContent
public interface IPolymorphModel { }
//a separate resolver to help resolve actual model type from ModelBindingContext
//(kind of separating responsibility
public interface IPolymorphTypeResolver
{
Type ResolveType(ModelBindingContext modelBindingContext);
}
//a separate types provider to get all derived types from a base type/interface
public interface IPolymorphTypeProvider
{
IEnumerable<Type> GetDerivedTypesFrom<T>();
}
//the custom model binder for polymorphism
public class PolymorphModelBinder : IModelBinder
{
readonly IDictionary<Type, (ModelMetadata ModelMetadata, IModelBinder Binder)> _bindersByType;
readonly IPolymorphTypeResolver _polymorphTypeResolver;
public PolymorphModelBinder(IDictionary<Type, (ModelMetadata,IModelBinder)> bindersByType,
IPolymorphTypeResolver polymorphTypeResolver)
{
_bindersByType = bindersByType;
_polymorphTypeResolver = polymorphTypeResolver;
}
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
var modelType = _polymorphTypeResolver.ResolveType(bindingContext);
if(modelType == null ||
!_bindersByType.TryGetValue(modelType, out var binder))
{
bindingContext.Result = ModelBindingResult.Failed();
return;
}
//create new binding context with the concrete/actual model type
var actualBindingContext = DefaultModelBindingContext.CreateBindingContext(bindingContext.ActionContext,
bindingContext.ValueProvider,
binder.ModelMetadata,
null,
bindingContext.ModelName);
await binder.Binder.BindModelAsync(actualBindingContext);
//set back the result to the original bindingContext
bindingContext.Result = actualBindingContext.Result;
if (actualBindingContext.Result.IsModelSet)
{
bindingContext.ValidationState[bindingContext.Result] = new ValidationStateEntry {
Metadata = binder.ModelMetadata
};
}
}
}
//the custom model binder provider for polymorphism
public class PolymorphModelBinderProvider<T> : IModelBinderProvider
{
readonly IPolymorphTypeResolver _polymorphTypeResolver;
readonly IPolymorphTypeProvider _polymorphTypeProvider;
public PolymorphModelBinderProvider(IPolymorphTypeResolver polymorphTypeResolver,
IPolymorphTypeProvider polymorphTypeProvider)
{
_polymorphTypeResolver = polymorphTypeResolver;
_polymorphTypeProvider = polymorphTypeProvider;
}
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (!typeof(T).IsAssignableFrom(context.Metadata.ModelType)) return null;
//prepare the list of the well-known derived types
var derivedTypes = _polymorphTypeProvider.GetDerivedTypesFrom<T>();
var binders = derivedTypes.ToDictionary(e => e,
e =>
{
var modelMetadata = context.MetadataProvider.GetMetadataForType(e);
return (modelMetadata, context.CreateBinder(modelMetadata));
}
);
return new PolymorphModelBinder(binders, _polymorphTypeResolver);
}
}
Here is the default implementation for a IPolymorphTypeResolver which basically depends on an IValueProvider from the ModelBindingContext (which is just like what used in the Microsoft's example):
public class DefaultPolymorphTypeResolver : IPolymorphTypeResolver
{
public Type ResolveType(ModelBindingContext modelBindingContext)
{
var contentTypeValueKey = ModelNames.CreatePropertyModelName(modelBindingContext.ModelName, "ContentType");
var contentType = modelBindingContext.ValueProvider.GetValue(contentTypeValueKey).FirstValue;
switch (contentType)
{
case "whatever ...":
return ...;
//...
default:
return null;
}
}
}
If you store the contentType in header or some other sources, just implement it your own way, here for the content type stored in the request header:
public class DefaultPolymorphTypeResolver : IPolymorphTypeResolver {
public Type ResolveType(ModelBindingContext modelBindingContext) {
var contentType = modelBindingContext.ActionContext.HttpContext.Request.ContentType;
switch (contentType)
{
//...
default:
return null;
}
}
}
Here is the default implementation of IPolymorphTypeProvider:
public class PolymorphContentTypeProvider : IPolymorphTypeProvider
{
public IEnumerable<Type> GetDerivedTypesFrom<T>()
{
return new List<Type> { /* your logic to populate the list ... */ };
}
}
Now we configure it to register everything needed including the PolymorphModelBinderProvider and your specific implementation of IPolymorphTypeResolver and IPolymorphTypeProvider:
//in ConfigureServices
services.AddSingleton<IPolymorphTypeResolver, DefaultPolymorphTypeResolver>();
services.AddSingleton<IPolymorphTypeProvider, PolymorphContentTypeProvider>();
services.AddOptions<MvcOptions>()
.Configure((MvcOptions o,
IPolymorphTypeResolver typeResolver,
IPolymorphTypeProvider typesProvider) =>
{
o.ModelBinderProviders.Insert(0, new PolymorphModelBinderProvider<IPolymorphModel>(typeResolver, typesProvider));
});
Im trying to learn .net 3.1 by building a small test webapi and currently my objective is to validate dtos with fluentvalidation and in case it fails, present a custom json to the caller. The problems i have found and cant get over are two;
i cant seem to get the messages i write via fluentvalidation (they always are the - i assume .net core default ones)
i cant seem to modify the object type that is json-ified and then output to the caller.
My code is as follows:
1. The Controller
[ApiController]
[Route("[controller]")]
public class AdminController : ControllerBase
{
[HttpPost]
[ProducesResponseType(StatusCodes.Status409Conflict)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status202Accepted)]
public async Task<IActionResult> RegisterAccount(NewAccountInput dto)
{
return Ok();
}
}
2. The Dto and the custom validator
public class NewAccountInput
{
public string Username { get; set; }
public string Email { get; set; }
public string Phone { get; set; }
public AccountType Type { get; set; }
}
public class NewAccountInputValidator : AbstractValidator<NewAccountInput>
{
public NewAccountInputValidator()
{
RuleFor(o => o.Email).NotNull().NotEmpty().WithMessage("Email vazio");
RuleFor(o => o.Username).NotNull().NotEmpty().WithMessage("Username vazio");
}
}
3. The Filter im using for validation
public class ApiValidationFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
//the only output i want are the error descriptions, nothing else
var data = context.ModelState
.Values
.SelectMany(v => v.Errors.Select(b => b.ErrorMessage))
.ToList();
context.Result = new JsonResult(data) { StatusCode = 400 };
}
//base.OnActionExecuting(context);
}
}
finally, my configureservices
public void ConfigureServices(IServiceCollection services)
{
services
//tried both lines and it doesnt seem to work either way
.AddScoped<ApiValidationFilter>()
.AddControllers(//config=>
//config.Filters.Add(new ApiValidationFilter())
)
.AddFluentValidation(fv => {
fv.RunDefaultMvcValidationAfterFluentValidationExecutes = false;//i was hoping this did the trick
fv.RegisterValidatorsFromAssemblyContaining<NewAccountInputValidator>();
});
}
Now, trying this with postman i get the result
which highlights both issues im having atm
This was done with asp.net core 3.15 and visualstudio 16.6.3
The message you are seeing is in fact coming from FluentValidation - see the source.
The reason you aren't seeing the custom message you are providing is that FluentValidation will show the validation message from the first validator that fails in the chain, in this case NotNull.
This question gives some options for specifying a single custom validation message for an entire chain of validators.
In this case the Action Filter you describe is never being hit, as the validation is failing first. To prevent this you can use:
services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressModelStateInvalidFilter = true;
});
which will stop the automatic return of BadRequest for an invalid model. This question provides some alternative solutions, including configuring an InvalidModelStateResponseFactory to do what you require.
I am following this example: https://learn.microsoft.com/en-us/aspnet/core/mvc/models/validation and trying to implement my own custom attribute for validation.
Now, the viewmodel has two fields that I wish to access from inside this method, so that they can be rendered with the "data-val" attributes. My question is, how can I get say a property called "Myprop" from context here? When I debug I can se the information under context.ActionContext.ViewData.Model but I have no way of getting that info other then during the debug when I use Visual Studio "quick watch" feature. The custom attributes are on properties that are on the viewmodel.
public void AddValidation(ClientModelValidationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
MergeAttribute(context.Attributes, "data-val", "true");
MergeAttribute(context.Attributes, "data-val-classicmovie", GetErrorMessage());
var year = _year.ToString(CultureInfo.InvariantCulture);
MergeAttribute(context.Attributes, "data-val-classicmovie-year", year);
}
I ran into a similar problem. The short answer is that you can't from there. You only have access to the metadata from there, not the actual model. The reason for this is that your model metadata and validation based on it are done on first time use, and then cached. So you'll never be able to change what validation rules to return based on the model via an attribute decorator.
If you need to dynamically decide which client side data-val-* attributes to render based off the instance/content of your model, you would need to inherit from DefaultValidationHtmlAttributeProvider, instead of using attributes, and override the AddValidationAttributes method. It's the only way I've found to do this so far. This is because inside of this method, you have access to the ModelExplorer
public class CustomValidationHtmlAttributeProvider : DefaultValidationHtmlAttributeProvider
{
private readonly IModelMetadataProvider metadataProvider;
public CustomValidationHtmlAttributeProvider(IOptions<MvcViewOptions> optionsAccessor, IModelMetadataProvider metadataProvider, ClientValidatorCache clientValidatorCache)
: base(optionsAccessor, metadataProvider, clientValidatorCache)
{
this.metadataProvider = metadataProvider;
}
public override void AddValidationAttributes(ViewContext viewContext, ModelExplorer modelExplorer, IDictionary<string, string> attributes)
{
//base implimentation
base.AddValidationAttributes(viewContext, modelExplorer, attributes);
//re-create the validation context (since it's encapsulated inside of the base implimentation)
var context = new ClientModelValidationContext(viewContext, modelExplorer.Metadata, metadataProvider, attributes);
//Only proceed if it's the model you need to do custom logic for
if (!(modelExplorer.Container.Model is MyViewModelClass model) || !modelExplorer.Metadata.PropertyName == "Myprop") return;
//Do stuff!
var validationAttributeAdapterProvider = viewContext.HttpContext.RequestServices.GetRequiredService<IValidationAttributeAdapterProvider>();
if (model.Myprop)
{
var validationAdapter = (RequiredAttributeAdapter)validationAttributeAdapterProvider.GetAttributeAdapter(new RequiredAttribute(), null);
validationAdapter.Attribute.ErrorMessage = "You not enter right stuff!";
validationAdapter.AddValidation(context);
}
}
}
And then register this class in the ConfigureServices() of your Startup
public void ConfigureServices(IServiceCollection services)
{
//All your other DI stuff here
//register the new ValidationHtmlAttributeProvider
services.AddSingleton<ValidationHtmlAttributeProvider, PseudoAttributeValidationHtmlAttributeProvider>();
}
The downside, is that if you have multiple models you need to do this for, it gets really ugly really fast. If anyone has found a better method, I'd love to hear it :-)
following code in asp.net web API worked fine but doesn't work in Asp.net core.
Endpoint api/devices?query={"deviceName":"example"}
[HttpGet]
public Device ([FromUri] string deviceName)
{
var device = context.Computers.Where(x => x.deviceName == deviceName);
return device;
}
[FromUri] attribute is not present asp.net core web API, and I tried to use following , but no success.
[HttpGet]
public Device Get([FromQuery] string deviceName)
{
return repo.GetDeviceByName(deviceName);
}
Unfortunately there is no way to bind JSON in a GET query like you have there. What you are looking for is to use a custom model binder to tell ASP.net Core how you want to bind.
First, you want to build your model for your JSON object.
public class MyCustomModel
{
public string DeviceName { get; set; }
}
Next you need to build your model binder. A simple example is given below but you would obviously want other checks around if it can be converted, Try/Catch blocks etc. Essentially a model binder tells ASP.net Core how a model should be bound. You might also run into TypeConverters which are given a type, how can I change this to another type during model binding. For now let's just use modelbinders.
public class MyViewModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var jsonString = bindingContext.ActionContext.HttpContext.Request.Query["query"];
MyCustomModel result = JsonConvert.DeserializeObject<MyCustomModel>(jsonString);
bindingContext.Result = ModelBindingResult.Success(result);
return Task.CompletedTask;
}
}
So all we are doing is taking the query string and deserializing it to our model.
Next we build a provider. A provider is what tells ASP.net core which modelbinder to use. In our case it's simple, if the model type is our custom type, then use our custom binder.
public class MyViewModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType == typeof(MyCustomModel))
return new MyViewModelBinder();
return null;
}
}
And the final piece of the puzzle. In our startup.cs, we find where we add MVC services and we insert our model binder to the front of the list. This is important. If we just add our modelbinder to the list, another model binder might think it should be used instead (First in first served), so we might not ever make it to ours. So be sure to insert it at the start.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(config => config.ModelBinderProviders.Insert(0, new MyViewModelBinderProvider()));
}
Now we just create an action where we read the data, no attributes required.
[HttpGet]
public void Get(MyCustomModel model)
{
}
Further reading :
http://dotnetcoretutorials.com/2016/12/28/custom-model-binders-asp-net-core/
https://learn.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-model-binding
I came along this question because I have a similar problem.
In my scenario it was more convinient to implement ModelBinderAttribute:
public class FromUriJsonAttribute : ModelBinderAttribute
{
public FromUriJsonAttribute()
{
BinderType = typeof(JsonBinder);
}
public FromUriJsonAttribute(string paramName)
: this()
{
Name = paramName;
}
}
As in #MindingData answer the binder is needed:
public class JsonBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
try
{
var name = bindingContext.ModelName;
var json = actionContext.Request.GetQueryNameValuePairs().FirstOrDefault(x => x.Key == name).Value;
var targetType = bindingContext.ModelType;
var model = Newtonsoft.Json.JsonConvert.DeserializeObject(json, targetType);
bindingContext.Model = model;
return true;
}
catch
{
}
return false;
}
}
And the usage:
[Route("{id}/items")]
public Item GetItem(string id, [FromUriJson] Filter filter)
{
//logic
}
or:
[Route("{id}/items")]
public Item GetItem(string id, [FromUriJson("queryStringKey")] Filter filter)
{
//logic
}
It can be useful if you want to have Json formatted query string parameter only in some endpoints and/or don't want to mess with default IServiceCollection configuration by inserting a new IModelBinderProvider implementation.
I have the following structure:
public class SampleEntity{
public string Name { get; set; }
public OtherEntity Relation { get; set; }
}
public class OtherEntity {
public string Name { get; set; }
}
When i make a call to update an object in my web.api with the following request body:
"{'Name':'Nome', 'Relation':''}"
The deserializer fills the object with null value, but i think the correct action is throw an exception like 'invalid value for field Relation' and i can return a status code 400.
I tried to make a custom converter to do this, but i'm not happy with the solution and i am quite concerned with the performance of this.
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.StartObject)
{
var #object = JObject.Load(reader);
var target = Activator.CreateInstance(objectType);
var objectProperties = target.GetType().GetProperties().Where(x => x.PropertyType.IsPrimitive == false && x.PropertyType != typeof(string));
foreach (var prop in objectProperties)
{
var value = #object[prop.Name];
if (value != null && value.ToString() == string.Empty)
throw new Exception();
}
serializer.Populate(#object.CreateReader(), target);
return target;
}
return reader.Value;
}
The Web API does not automatically return an error to the client when validation fails. It is up to the controller action to check the model state and respond appropriately. While #loop's answer will work, you may want to consider another option whereby you won't even have to enter your controller's action method.
To do this, you can create an action filter to check the model state before the controller action is even invoked.
For example:
public class ValidateModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (actionContext.ModelState.IsValid == false)
{
actionContext.Response = actionContext.Request.CreateErrorResponse(
HttpStatusCode.BadRequest, actionContext.ModelState);
}
}
}
Note that you will STILL need to decorate your model with attributes that describe the validation rules. This would be similar to what #loop has suggested.
If model validation fails, this filter returns an HTTP response that contains the validation errors. In that case, the controller action is not invoked.
To apply this filter to all Web API controllers, add an instance of the filter to the HttpConfiguration.Filters collection during configuration:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Filters.Add(new ValidateModelAttribute());
// ...
}
}
Another option is to set the filter as an attribute on individual controllers or controller actions:
public class EntitiesController : ApiController
{
[ValidateModel]
public HttpResponseMessage Post(SampleEntity entity)
{
// ...
}
}
For a more detailed explanation, take a look at this article. To learn about the various model annotations that you can use to define validation rules, e.g. [Required], etc., have a look at this MSDN page.