Reputation: 41
I have an Angular client and create a POST request with this body:
{"Name":"example","Currency":"EUR"}
I Use Odata protocol and my Controller is:
[HttpPost, ODataRoute("Templates")]
public IActionResult Insert([FromBody] Template value)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
value.Id = Guid.NewGuid();
_context.Templates.Add(value);
_context.SaveChanges();
return Created(value);
}
with Template:
public class Template
{
public Guid Id { get; set; }
public string Name { get; set; }
public Currency Currency { get; set; }
}
and Currency:
[Serializable]
public class Currency : StringEnumeration<Currency>
{
public static Currency EUR = new Currency("EUR", "EUR");
public static Currency USD = new Currency("USD", "USD");
Currency() { }
Currency(string code, string description) : base(code, description) { }
}
Currency is a particular class because it has private constructors and for this reason i can't create a new instance of Currency. I want use ones of the existing instances (EUR or USD).
(StringEnumeration support a Parse and TryParse Method and return the correct instance)
Standard Configuration:
public void ConfigureServices(IServiceCollection services)
{
services.ConfigureCors();
services.AddOData();
services.ConfigureIISIntegration();
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddDbContext<GpContext>(option => option
.UseSqlServer(Configuration.GetConnectionString(GpConnection)));
}
My problem is when the client call POST on http://localhost:4200/template with the body: {"Name":"example","Currency":"EUR"}
The Model Bindel cannot Convert "EUR" in Currency.EUR instance, so i want provide something to help model binder to create Template with Currency property with the instance Currency.EUR
This is the error generated: A 'PrimitiveValue' node with non-null value was found when trying to read the value of the property 'Currency'; however, a 'StartArray' node, a 'StartObject' node, or a 'PrimitiveValue' node with null value was expected.
In my project i have many classes with Currency property inside.
I tryed to use IModelBinder on Template class and it works, but i dont want write a modelBinder for any Currency Property.
I tried with JsonConverter, but it doesn't work for me (maybe something wrong)
My Expected result is a Template instance with this values:
Id = defaluf(Guid)
Name = "example"
Currency = Currency.EUR
Upvotes: 3
Views: 2339
Reputation: 41
I try this implementation and i have same error.
I set breakpoints in CurrencyModelBinder and in the CurrencyModelBinderProvider
Breakpoint on Model Binder Provider
The problem is in the compare: context.Metadata.ModelType = "Template" and the CurrencyModelBinder is called only for Currency.
I solved with this workarond:
Deserialize with JsonConverter
[HttpPost, ODataRoute("Templates")]
public IActionResult Insert([FromBody] object value)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
var template = JsonConvert.DeserializeObject<Template>(value.ToString());
template.Id = Guid.NewGuid();
_context.Templates.Add(template);
_context.SaveChanges();
return Created(value);
}
The Currency class now is
[Serializable]
[JsonConverter(typeof(CurrencyJsonConverter))]
public class Currency : StringEnumeration<Currency>
{
public static Currency CHF = new Currency("CHF", "CHF");
public static Currency EUR = new Currency("EUR", "EUR");
public static Currency USD = new Currency("USD", "USD");
Currency() { }
Currency(string code, string description) : base(code, description) { }
}
and JsonConverter
public class CurrencyJsonConverter : JsonConverter
{
public override bool CanWrite => true;
public override bool CanConvert(Type objectType)
{
return objectType == typeof(Currency);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null) return null;
var value = reader.Value as string;
return Currency.Parse(value);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value is Currency currency)
serializer.Serialize(writer, currency.Code);
}
}
I don't understand why Defaul Model Binder don't use Json deserialization.
I remain waiting for your kind reply.
Upvotes: 1
Reputation: 388303
If you already have a working model binder implemented for your Currency
type, then you can just implement an IModelBinderProvider
that provides the model binder automatically whenever MVC needs to bind to the Currency
type:
public class CurrencyModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType == typeof(Currency))
return new BinderTypeModelBinder(typeof(CurrencyModelBinder));
return null;
}
}
You then need to register this in your Startup’s ConfigureServices
:
services.AddMvc(options =>
{
options.ModelBinderProviders.Insert(0, new CurrencyModelBinderProvider());
});
And then, all Currency
elements will be automatically bound using your CurrencyModelBinder
without you having to use the [ModelBinder]
attribute everywhere.
This is also described in “custom model binder sample” section of the documentation.
Just for completeness, a possible implementation of CurrencyModelBinder
:
public class CurrencyModelBinder : IModelBinder
{
private static readonly Currency[] _currencies = new Currency[]
{
Currency.EUR,
Currency.USD,
};
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var modelName = bindingContext.ModelName;
var providerResult = bindingContext.ValueProvider.GetValue(modelName);
if (providerResult == ValueProviderResult.None)
{
return Task.CompletedTask;
}
var value = providerResult.FirstValue;
if (string.IsNullOrEmpty(value))
{
return Task.CompletedTask;
}
var currency = _currencies
.FirstOrDefault(c => c.Code.Equals(value, StringComparison.OrdinalIgnoreCase));
if (currency != null)
bindingContext.Result = ModelBindingResult.Success(currency);
else
bindingContext.ModelState.TryAddModelError(modelName, "Unknown currency");
return Task.CompletedTask;
}
}
Upvotes: 1