For a scenario like this one would need a custom model binder. The framework allows for such flexibility.
Using the walkthrough provided here
Custom model binder sample
and adapting it to this question.
The following sample uses the ModelBinder
attribute on the SalesRecord
model:
[ModelBinder(BinderType = typeof(SalesRecordBinder))]
[JsonConverter(typeof(JsonPathConverter))]
public class SalesRecord {
[JsonProperty("user.name.first")]
public string FirstName {get; set;}
[JsonProperty("user.name.last")]
public string LastName {get; set;}
[JsonProperty("payment.type")]
public string PaymentType {get; set;}
}
In the preceding code, the ModelBinder
attribute specifies the type of IModelBinder
that should be used to bind SalesRecord
action parameters.
The SalesRecordBinder
is used to bind an SalesRecord
parameter by trying to parse the posted content using a custom JSON converter to simplify the deseiralization.
class JsonPathConverter : JsonConverter {
public override object ReadJson(JsonReader reader, Type objectType,
object existingValue, JsonSerializer serializer) {
JObject jo = JObject.Load(reader);
object targetObj = Activator.CreateInstance(objectType);
foreach (PropertyInfo prop in objectType.GetProperties()
.Where(p => p.CanRead && p.CanWrite)) {
JsonPropertyAttribute att = prop.GetCustomAttributes(true)
.OfType<JsonPropertyAttribute>()
.FirstOrDefault();
string jsonPath = (att != null ? att.PropertyName : prop.Name);
JToken token = jo.SelectToken(jsonPath);
if (token != null && token.Type != JTokenType.Null) {
object value = token.ToObject(prop.PropertyType, serializer);
prop.SetValue(targetObj, value, null);
}
}
return targetObj;
}
public override bool CanConvert(Type objectType) {
// CanConvert is not called when [JsonConverter] attribute is used
return false;
}
public override bool CanWrite {
get { return false; }
}
public override void WriteJson(JsonWriter writer, object value,
JsonSerializer serializer) {
throw new NotImplementedException();
}
}
Source: Can I specify a path in an attribute to map a property in my class to a child property in my JSON?
public class SalesRecordBinder : IModelBinder {
public Task BindModelAsync(ModelBindingContext bindingContext) {
if (bindingContext == null){
throw new ArgumentNullException(nameof(bindingContext));
}
// Try to fetch the value of the argument by name
var valueProviderResult = bindingContext.ValueProvider
.GetValue(bindingContext.ModelName);
if (valueProviderResult == ValueProviderResult.None){
return Task.CompletedTask;
}
var json = valueProviderResult.FirstValue;
// Check if the argument value is null or empty
if (string.IsNullOrEmpty(json)) {
return Task.CompletedTask;
}
//Try to parse the provided value into the desired model
var model = JsonConvert.DeserializeObject<SalesRecord>(json);
//Model will be null if unable to desrialize.
if (model == null) {
bindingContext.ModelState
.TryAddModelError(
bindingContext.ModelName,
"Invalid data"
);
return Task.CompletedTask;
}
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, model);
//could consider checking model state if so desired.
//set result state of binding the model
bindingContext.Result = ModelBindingResult.Success(model);
return Task.CompletedTask;
}
}
From there it should be now a simple matter of using the model in the action
[HttpPost("create", Name = "CreateSalesRecord")]
public IActionResult Create([FromBody] SalesRecord record) {
if(ModelState.IsValid) {
//...
return Ok();
}
return BadRequest(ModelState);
}
Disclaimer: This has not been tested as yet. There may be issues still to be fixed as it is based on the linked sources provided above.