I have an ASP.Net Core 2.1 application.
Below is my DTO
public class Movie
{
public int Id { get; set;}
public bool IsSpecial {get; set;}
public IEnumerable<Ticket> Tickets { get; set; }
public Movie()
{
if(IsSpecial)
{
this.Tickets = new List<TicketSpecial>();
}
else
{
this.Tickets = new List<Ticket>();
}
}}}
Tickets (Base Class)
public class Ticket
{
public int Id { get; set;}
public string Name { get; set;}
public decimal price { get; set;}
}
TicketsSpecial (Child/Derived Class)
public class TicketsSpecial : Ticket
{
public string SpecialProp1 { get; set;}
public string SpecialProp2 { get; set;}
}
WebAPI Controller
public class MovieController : ControllerBase
{
public IActionResult Post([FromBody]Movie movie)
{
}
}
Postman (HTTPPost Req payload Content-Type = application/json)
{
"IsSpecial": true,
"SpecialProp1": "Mumbai Test",
}
When I call the above API via Postman & debug at Movie ctor, it always catches the value of IsSpecial = false & all fields default value (ex. for string type null)
Thanks!
There are two issues in your current implementation. First, your request json is invalid for nested properties and json deserializer would not deserialize for you with TicketsSpecial.
Follow steps below for a workaround:
Movie
public class Movie
{
public int Id { get; set; }
public bool IsSpecial { get; set; }
public IEnumerable<Ticket> Tickets { get; set; }
}
MyJsonInputFormatter
public class MyJsonInputFormatter : JsonInputFormatter
{
public MyJsonInputFormatter(ILogger logger, JsonSerializerSettings serializerSettings, ArrayPool<char> charPool, ObjectPoolProvider objectPoolProvider, MvcOptions options, MvcJsonOptions jsonOptions) : base(logger, serializerSettings, charPool, objectPoolProvider, options, jsonOptions)
{
}
public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
var result = await base.ReadRequestBodyAsync(context);
if (result.Model is Movie movie && movie.IsSpecial)
{
context.HttpContext.Request.Body.Position = 0;
string request = await new StreamReader(context.HttpContext.Request.Body).ReadToEndAsync();
var tickets = JObject.Parse(request)["Tickets"].ToObject<List<TicketSpecial>>();
movie.Tickets = tickets;
}
return result;
}
}
Register MyJsonInputFormatter
services.AddMvc(o =>
{
var serviceProvider = services.BuildServiceProvider();
var customJsonInputFormatter = new MyJsonInputFormatter(
serviceProvider.GetRequiredService<ILoggerFactory>().CreateLogger<MyJsonInputFormatter>(),
serviceProvider.GetRequiredService<IOptions<MvcJsonOptions>>().Value.SerializerSettings,
serviceProvider.GetRequiredService<ArrayPool<char>>(),
serviceProvider.GetRequiredService<ObjectPoolProvider>(),
o,
serviceProvider.GetRequiredService<IOptions<MvcJsonOptions>>().Value
);
o.InputFormatters.Insert(0, customJsonInputFormatter);
})
Request
{
"IsSpecial": true,
"Tickets": [
{"SpecialProp1": "Mumbai Test"}
]
}
Change "Isspecial" to "isSpecial", same with the other property.
Another problem is that you're checking "IsSpecial" in the constructor and at this time it should be false anyway.
Related
Some of my actions accept models like:
public class PaymentRequest
{
public decimal Amount { get; set; }
public bool? SaveCard { get; set; }
public int? SmsCode { get; set; }
public BankCardDetails Card { get; set; }
}
public class BankCardDetails
{
public string Number { get; set; }
public string HolderName { get; set; }
public string ExpiryDate { get; set; }
public string ValidationCode { get; set; }
}
And the action method looks like:
[HttpPost]
[Route("api/v1/payment/pay")]
public Task<BankCardActionResponse> Pay([FromBody] PaymentRequest request)
{
if (request == null)
throw new HttpResponseException(HttpStatusCode.BadRequest);
return _paymentService.PayAsync(DataUserHelper.PhoneNumber, request);
}
I use Nlog. I think it's clear this is a bad idea to log all this bank data. My log config file contained the following line:
<attribute name="user-requestBody" layout="${aspnet-request-posted-body}"/>
I logged the request. I decided to refactor that and planned the following strategy. Actions that contain sensitive data into their requests I will mark with an attribute like
[RequestMethodFormatter(typeof(PaymentRequest))]
then take a look at my custom renderer:
[LayoutRenderer("http-request")]
public class NLogHttpRequestLayoutRenderer : AspNetRequestPostedBody
{
protected override void DoAppend(StringBuilder builder, LogEventInfo logEvent)
{
base.DoAppend(builder, logEvent);
var body = builder.ToString();
// Get attribute of the called action.
var type = ... // How can I get "PaymentRequest" from the [RequestMethodFormatter(typeof(PaymentRequest))]
var res = MaskHelper.GetMaskedJsonString(body, type);
// ... and so on
}
}
I think you understand the idea. I need the type from the method's RequestMethodFormatter attribute. Is it even possible to get it into the renderer? I need it because I'm going to deserialize request JSON into particular models (it's gonna be into the MaskHelper.GetMaskedJsonString), work with the models masking the data, serialize it back into JSON.
So, did I choose a wrong approach? Or it's possible to get the type from the attribute into the renderer?
After some research, I ended up with the following solution:
namespace ConsoleApp7
{
internal class Program
{
private static void Main()
{
var sourceJson = GetSourceJson();
var userInfo = JsonConvert.DeserializeObject(sourceJson, typeof(User));
Console.WriteLine("----- Serialize without Resolver-----");
Console.WriteLine(JsonConvert.SerializeObject(userInfo));
Console.WriteLine("----- Serialize with Resolver-----");
Console.WriteLine(JsonConvert.SerializeObject(userInfo, new JsonSerializerSettings
{
ContractResolver = new MaskPropertyResolver()
}));
}
private static string GetSourceJson()
{
var guid = Guid.Parse("3e92f0c4-55dc-474b-ae21-8b3dac1a0942");
return JsonConvert.SerializeObject(new User
{
UserId = guid,
Age = 19,
Name = "John",
BirthDate = new DateTime(1990, 5, 12),
Hobbies = new[]
{
new Hobby
{
Name = "Football",
Rating = 5,
DurationYears = 3,
},
new Hobby
{
Name = "Basketball",
Rating = 7,
DurationYears = 4,
}
}
});
}
}
public class User
{
[MaskGuidValue]
public Guid UserId { get; set; }
[MaskStringValue("***")] public string Name { get; set; }
public int Age { get; set; }
[MaskDateTimeValue]
public DateTime BirthDate { get; set; }
public Hobby[] Hobbies { get; set; }
}
public class Hobby
{
[MaskStringValue("----")]
public string Name { get; set; }
[MaskIntValue(replacement: 11111)]
public int Rating { get; set; }
public int DurationYears { get; set; }
}
public class MaskPropertyResolver : DefaultContractResolver
{
protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
{
var props = base.CreateProperties(type, memberSerialization);
var allowedPropertyTypes = new Type[]
{
typeof(Guid),
typeof(DateTime),
typeof(string),
typeof(int),
};
foreach (var prop in props.Where(p => allowedPropertyTypes.Contains(p.PropertyType)))
{
if (prop.UnderlyingName == null)
continue;
var propertyInfo = type.GetProperty(prop.UnderlyingName);
var attribute =
propertyInfo?.GetCustomAttributes().FirstOrDefault(x => x is IMaskAttribute) as IMaskAttribute;
if (attribute == null)
{
continue;
}
if (attribute.Type != propertyInfo.PropertyType)
{
// Log this case, cause somebody used wrong attribute
continue;
}
prop.ValueProvider = new MaskValueProvider(propertyInfo, attribute.Replacement, attribute.Type);
}
return props;
}
private class MaskValueProvider : IValueProvider
{
private readonly PropertyInfo _targetProperty;
private readonly object _replacement;
private readonly Type _type;
public MaskValueProvider(PropertyInfo targetProperty, object replacement, Type type)
{
_targetProperty = targetProperty;
_replacement = replacement;
_type = type;
}
public object GetValue(object target)
{
return _replacement;
}
public void SetValue(object target, object value)
{
_targetProperty.SetValue(target, value);
}
}
}
[AttributeUsage(AttributeTargets.Property)]
public class MaskStringValueAttribute : Attribute, IMaskAttribute
{
public Type Type => typeof(string);
public object Replacement { get; }
public MaskStringValueAttribute(string replacement)
{
Replacement = replacement;
}
}
[AttributeUsage(AttributeTargets.Property)]
public class MaskIntValueAttribute : Attribute, IMaskAttribute
{
public object Replacement { get; }
public Type Type => typeof(int);
public MaskIntValueAttribute(int replacement)
{
Replacement = replacement;
}
}
[AttributeUsage(AttributeTargets.Property)]
public class MaskGuidValueAttribute : Attribute, IMaskAttribute
{
public Type Type => typeof(Guid);
public object Replacement => Guid.Empty;
}
[AttributeUsage(AttributeTargets.Property)]
public class MaskDateTimeValueAttribute : Attribute, IMaskAttribute
{
public Type Type => typeof(DateTime);
public object Replacement => new DateTime(1970, 1, 1);
}
public interface IMaskAttribute
{
Type Type { get; }
object Replacement { get; }
}
}
I hope somebody will find it helpful.
You can try nuget package https://www.nuget.org/packages/Slin.Masking and https://www.nuget.org/packages/Slin.Masking.NLog.
It can easily be integrated with DotNet projects with slight changes, and you can define your rules for it. But the document needs some improvement.
As a suggestion, you can use two files:
masking.json (can be a generic one, that shared across all projects)
masking.custom.json (can be used with particular rules for specific projects)
I have an application with a general search request which has an identification number that could be anything like product number or customer number along with additional search criteria. I want to display all the results. I am writing a middleware to call the search api end points.
public class GeneralRequest
{
public string IdentificationNumber { get; set; }
public string Location { get; set; }
public bool IsActive { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime Start { get; set; }
public DateTime Stop { get; set; }
}
public class AdditionalSearch
{
public RangeSearch Range { get; set; }
public string Location { get; set; }
public bool IsActive { get; set; }
}
public class RangeSearch
{
public DateTime Start { get; set; }
public DateTime Stop { get; set; }
}
public class GetProductRequest : AdditionalSearch, ISearchRequest
{
public string ProductId { get; set; }
}
public class GetCustomerRequst : AdditionalSearch, ISearchRequest
{
public string CustomerNumber { get; set; }
}
public class GetManufacturerRequest : AdditionalSearch, ISearchRequest
{
public string ManufacturerNumber { get; set; }
}
// this is a dummy interface to make all the requests general
public interface ISearchRequest
{
}
This is the searchprocessor where I am creating the correct request based on the identification number pattern. But I am failing to assign the AdditonalSearch to the request that I get after invoking the func. Of course it is an interface that has nothing in it. How can I achieve this by not repeating(I mean I don't want to repeat the initialization logic in the dictionary)
Please suggest me what is the best practice here.
public class SearchProcessor
{
private readonly Dictionary<Regex, Func<GeneralRequest, ISearchRequest>> _pattern;
private readonly IAppClient _appClient;
public SearchProcessor(IAppClient appClient)
{
_appClient = appClient;
_pattern = new Dictionary<Regex, Func<GeneralRequest, ISearchRequest>>
{
{new Regex("/\b([0-9]|10)\b /"), p=> new GetProductRequest(){ProductId = p.IdentificationNumber} },
{new Regex("^\\d{}1,9}$"), p=> new GetCustomerRequst(){CustomerNumber = p.IdentificationNumber} },
{new Regex("^\\d{}1,11}$"), p=> new GetManufacturerRequest(){ManufacturerNumber = p.IdentificationNumber} }
};
}
public List<SearchResult> GetAllSearchResults(GeneralRequest request)
{
var requests = _pattern.Where(r => r.Key.IsMatch(request.IdentificationNumber)).Select(v => v.Value);
var responses = new List<SearchResult>();
foreach (var req in requests)
{
var appRequest = req.Invoke(request);
appRequest.AdditionalSearch = new AdditionalSearch // this is where I am not able to assign the additional seach from the general request
{
Range = new RangeSearch { Start = request.Start, Stop = request.Stop}
IsActive = request.IsActive,
Location = request.Location
};
//This calls another api to get the response.
responses.Add(_appclient.FindResult(appRequest));
}
return responses;
}
}
---UPDATE--
Here is the appclient that calls the external api..
sample request for the getproduct route is
public class AppClient : IAppClient
{
private readonly string _baseurl;
private readonly string _getProductRoute;
private readonly string _getCustomerRoute;
private readonly string _getManufacturerRoute;
public AppClient()
{
_getProductRoute = $"{_baseurl}/api/get-product";
_getCustomerRoute = $"{_baseurl}/api/get-customer";
_getManufacturerRoute = $"{_baseurl}/api/get-Manufacturer";
}
public SearchResult FindResult(ISearchRequest searchRequest)
{
var routes = new Dictionary<Type, string>
{
{typeof(GetProductRequest), _getProductRoute },
{typeof(GetCustomerRequst), _getCustomerRoute },
{typeof(GetManufacturerRequest), _getManufacturerRoute }
};
// Here it is going to be http implementation to call above routes.
return new SearchResult();
}
}
The request for get product route is
{
"ProductId":"",
"RangeSearch":{
"Start":"",
"Stop":""
},
"Location":"",
"IsActive":true
}
For get-customer request is
{
"CustomerNumber":"",
"RangeSearch":{
"Start":"",
"Stop":""
},
"Location":"",
"IsActive":true
}
You can simplify your work by doing this :
public class SearchRequest
{
public string IdentificationNumber { get; set; }
public string Location { get; set; }
public bool IsActive { get; set; }
public SearchRequestRange Range { get; set; }
public SearchRequest(string identificationNumber)
{
IdentificationNumber = identificationNumber;
}
}
public class SearchRequestRange
{
public DateTime Start { get; set; }
public DateTime Stop { get; set; }
}
now the process you are doing is not needed, you only need to adjust your ApiClient to something like this :
public class AppClient : IAppClient
{
private static readonly IReadOnlyDictionary<string, string> _endPointsSearchPatterns = new Dictionary<string, string>
{
{"get-product", "/\b([0-9]|10)\b /"},
{"get-customer", "^\\d{}1,9}$"},
{"get-Manufacturer", "^\\d{}1,11}$"}
};
private readonly string _baseUrl;
public AppClient(string baseUrl)
{
if(string.IsNotNullOrWhiteSpace(baseUrl))
throw new ArgumentNullException(nameof(baseUrl));
_baseurl = baseUrl;
}
public IEnumerable<SearchResult> FindResult(SearchRequest searchRequest)
{
var endPoints = _endPointsSearchPatterns.Where(x=> Regex.IsMatch(request.IdentificationNumber , x.Value))?.ToList();
if(endPoints?.Count == 0)
{
yield break;
}
var responses = new List<SearchResult>();
foreach(var endpoint in endPoints)
{
ISearchBy search = null;
switch(endPoint.Key)
{
case "get-product":
action = new ProductApiClient(this);
break;
case "get-customer":
action = new ProductApiClient(this);
break;
case "get-Manufacturer":
action = new ProductApiClient(this);
break;
}
yield return action.SearchBy(searchRequest);
}
return searchResult;
}
}
Regarding GetProductRequest, GetCustomerRequst, and GetManufacturerRequest these should be refactored, and instead you can create a class for each entity like this :
public interface ISearchBy
{
SearchResult ISearchBy(SearchRequest request);
}
public class ProductApiClient : ISearchBy
{
private readonly IAppClient _appClient;
public ProductApiClient(IAppClient appClient)
{
_appClient = appClient;
}
public SearchResult SearchBy(SearchRequest request)
{
// do stuff
}
// other related endpoints
}
public class CustomerApiClient : ISearchBy
{
private readonly IAppClient _appClient;
public CustomerApiClient(IAppClient appClient)
{
_appClient = appClient;
}
public SearchResult SearchBy(SearchRequest request)
{
// do stuff
}
// other related endpoints
}
public class ManufacturerApiClient : ISearchBy
{
private readonly IAppClient _appClient;
public ManufacturerApiClient(IAppClient appClient)
{
_appClient = appClient;
}
public SearchResult SearchBy(SearchRequest request)
{
// do stuff
}
// other related endpoints
}
*Update:
Was able to follow this tutorial, and now the error I'm getting states:
"message": "No HTTP resource was found that matches the request URI 'http://localhost:0000/api/v1/Pets('dog')/Color'."
"message": "No routing convention was found to select an action for the OData path with template '~/entityset/key/unresolved'."
Any ideas?*
I am getting an error when trying to retrieve the Color in my Pet query using OData V4.
I'm having quite a bit of trouble, ideally I would use an expand on color (e.g. localhost:0000/api/v1/Pets('dog')?$expand=Colors)
The JSON I need returned is something like:
[
{
"_Key": "1",
"animalname": "dog",
"furcolorname": "black,white",
"color": {
"_Key": "1",
"colorname": "black"
},
{
"_Key": "2",
"colorname": "white"
}
}
]
Maybe I'm on the completely wrong path, either way any input is appreciated!
If I query localhost:0000/api/v1/Pets('dog') :
…
"Message\": \"Invalid column name 'Pet__Key'.\"
…
If I query localhost:0000/api/v1/Pets('dog')?$exand=Colors :
…
"The query specified in the URI is not valid. Could not find a property named 'Colors' on type 'PetShop.Odata.Models.Pet'."
…
Pet.cs model:
namespace PetShop.Odata.Models
{
[Table("pet")]
public class Pet
{
[Key]
public string _Key { get; set; }
public string AnimalName { get; set; }
[ForeignKey("Color")]
public string FurColorName { get; set; }
public virtual Color Color { get; set; }
public virtual ICollection<Color> Colors { get; set; }
}
}
Color.cs model:
namespace PetShop.Odata.Models
{
[Table("color")]
public class Color
{
public Color()
{
new HashSet<Pet>();
}
[Key]
public string _Key { get; set; }
public string ColorName { get; set; }
}
}
PetsController.cs:
namespace PetShop.Odata.Controllers
{
[ApiVersion("1.0")]
[ODataRoutePrefix("Pets")]
[ApiControllerMetricReport]
public class PetsController : ODataController
{
private readonly MyContext context = new MyContext ();
[HttpGet]
[ODataRoute]
[EnableQuery]
[ResponseType(typeof(List<Pet>))]
public IHttpActionResult Get()
{
return Ok(context.Pets.AsQueryable());
}
[EnableQuery]
public IQueryable<Color> Get ([FromODataUri] string key)
{
return context.Pets.Where(m => m._Key == key).SelectMany(a => a.Colors);
}
protected override void Dispose(bool disposing)
{
context.Dispose();
base.Dispose(disposing);
}
}
}
ColorsController.cs:
namespace PetShop.Odata.Controllers
{
[ApiVersion("1.0")]
[ODataRoutePrefix("Colors")]
[ApiControllerMetricReport]
public class ColorsController : ODataController
{
private readonly MyContext context = new MyContext ();
[HttpGet]
[ODataRoute]
[EnableQuery]
[ResponseType(typeof(List<Color>))]
public IHttpActionResult Get()
{
return Ok(context.Colors.AsQueryable());
}
protected override void Dispose(bool disposing)
{
context.Dispose();
base.Dispose(disposing);
}
}
}
PetConfig.cs:
namespace PetShop.Odata.Configuration
{ public class PetModelConfiguration : IModelConfiguration
{
public void Apply(ODataModelBuilder builder, ApiVersion apiVersion)
{
builder.EntitySet<Pet>("Pets");
}
}
}
ColorConfig.cs:
namespace PetShop.Odata.Configuration
{ public class ColorModelConfiguration : IModelConfiguration
{
public void Apply(ODataModelBuilder builder, ApiVersion apiVersion)
{
builder.EntitySet<Color>("Colors");
}
}
}
MyContext.cs:
…
public DbSet<Pet> Pets { get; set; }
public DbSet<Color> Colors { get; set; }
…
Setup.cs:
…
public static HttpServer CreateHttpServer()
{
var httpConfig = new HttpConfiguration();
var webApiServer = new HttpServer(httpConfig);
httpConfig.AddApiVersioning(options => options.ReportApiVersions = true);
httpConfig.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
var modelBuilder = new VersionedODataModelBuilder(httpConfig)
{
ModelBuilderFactory = () => new ODataConventionModelBuilder().EnableLowerCamelCase(),
ModelConfigurations =
{
new PetConfig(),
new ColorConfig()
},
};
var models = modelBuilder.GetEdmModels();
httpConfig.Count().Filter().OrderBy().Expand().Select().MaxTop(null);
…
Assuming you are using default routing convention, your Get colors method in PetsController.cs does not match the expected OData route format.
Instead of the following:
[EnableQuery]
public IQueryable<Color> Get ([FromODataUri] string key)
{
return context.Pets.Where(m => m._Key == key).SelectMany(a => a.Colors);
}
You should try:
[EnableQuery]
public IQueryable<Color> GetColors ([FromODataUri] string key)
{
return context.Pets.Where(m => m._Key == key).SelectMany(a => a.Colors);
}
This definition would make the OData route: http://localhost:0000/api/v1/Pets('dog')/Colors
For more information on routing conventions see https://learn.microsoft.com/en-us/odata/webapi/built-in-routing-conventions.
A couple alternative approaches exist as well:
Register a different name to a custom nav property
Define a custom OData Function
I have used custom InputFormatters for creating a subset of request from the generic request that request body receives in API request.
var reqModel = new XmlSerializer(CurrentType).Deserialize(xmlDoc.CreateReader());
SubSetRequest model = ConvertToSubSetRequestObject(reqModel as BigRequest);
return InputFormatterResult.Success(model);
Now in controller ModelState.IsValid is not pointing to SubSetRequest but to BigRequest, which I have received request body
public ActionResult<object> Calculate(SubSetRequest request)
{
if (!ModelState.IsValid){ }
// other codes..
}
Any idea how can we validate ModelState against SubSetRequest type.
Important Classes :
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddMvcCore(options =>
{
options.OutputFormatters.Add(new XmlSerializerOutputFormatter());
options.InputFormatters.Insert(0, new XMLDocumentInputFormatter());
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddXmlSerializerFormatters()
.AddXmlDataContractSerializerFormatters();
}
BigRequest.cs
[System.SerializableAttribute()]
public class BigRequest
{
public string Name { get; set; }
public string Email { get; set; }
public string Address { get; set; }
public string Company { get; set; }
public string Designation { get; set; }
public string CompanyAddress { get; set; }
}
SubSetRequest.cs
[System.SerializableAttribute()]
public class SubSetRequest
{
public string Name { get; set; }
[Required] //This should tiger **Validation** error
public string Email { get; set; }
public string Address { get; set; }
}
XMLDocumentInputFormatter.cs
internal class XMLDocumentInputFormatter : InputFormatter
{
private Type CurrentType { get; set; }
public XMLDocumentInputFormatter()
{
SupportedMediaTypes.Add("application/xml");
}
public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
using (var streamReader = new StreamReader(context.HttpContext.Request.Body))
{
CurrentType = typeof(BigRequest);
var xmlDoc = await XDocument.LoadAsync(streamReader, LoadOptions.None, CancellationToken.None);
var reqModel = new XmlSerializer(CurrentType).Deserialize(xmlDoc.CreateReader());
var model = ConvertToSubSetRequestObject(reqModel as BigRequest);
return InputFormatterResult.Success(model);
}
}
public SubSetRequest ConvertToSubSetRequestObject(BigRequest request)
{
var retObject = new SubSetRequest
{
Name = request.Name,
Address = request.Address
};
return retObject;
}
}
ValueController.cs
[HttpPost]
[Route("/api/Value/Calculate")]
public virtual ActionResult<object> Calculate(SubSetRequest request)
{
TryValidateModel(request);
if (ModelState.IsValid) // is giving as TRUE, even if EMAIL is NULL
{
var context = new ValidationContext(request, serviceProvider: null, items: null);
var results = new List<ValidationResult>();
// this is working properly
var isValid = Validator.TryValidateObject(request, context, results);
}
return new ActionResult<object>(request.ToString());
}
I'm posting json with variables names with underscores (like_this) and attempting to bind to a model that is camelcased (LikeThis), but the values are unable to be bound.
I know I could write a custom model binder, but since the underscored convention is so common I'd expect that a solution already existed.
The action/model I'm trying to post to is:
/* in controller */
[HttpPost]
public ActionResult UpdateArgLevel(UserArgLevelModel model) {
// do something with the data
}
/* model */
public class UserArgLevelModel {
public int Id { get; set; }
public string FirstName { get; set; }
public string Surname { get; set; }
public int ArgLevelId { get; set; }
}
and the json data is like:
{
id: 420007,
first_name: "Marc",
surname: "Priddes",
arg_level_id: 4
}
(Unfortunately I can't change either the naming of either the json or the model)
You can start writing a custom Json.NET ContractResolver:
public class DeliminatorSeparatedPropertyNamesContractResolver :
DefaultContractResolver
{
private readonly string _separator;
protected DeliminatorSeparatedPropertyNamesContractResolver(char separator)
: base(true)
{
_separator = separator.ToString();
}
protected override string ResolvePropertyName(string propertyName)
{
var parts = new List<string>();
var currentWord = new StringBuilder();
foreach (var c in propertyName)
{
if (char.IsUpper(c) && currentWord.Length > 0)
{
parts.Add(currentWord.ToString());
currentWord.Clear();
}
currentWord.Append(char.ToLower(c));
}
if (currentWord.Length > 0)
{
parts.Add(currentWord.ToString());
}
return string.Join(_separator, parts.ToArray());
}
}
This is for your particular case, becase you need a snake case ContractResolver:
public class SnakeCasePropertyNamesContractResolver :
DeliminatorSeparatedPropertyNamesContractResolver
{
public SnakeCasePropertyNamesContractResolver() : base('_') { }
}
Then you can write a custom attribute to decorate your controller actions:
public class JsonFilterAttribute : ActionFilterAttribute
{
public string Parameter { get; set; }
public Type JsonDataType { get; set; }
public JsonSerializerSettings Settings { get; set; }
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (filterContext.HttpContext.Request.ContentType.Contains("application/json"))
{
string inputContent;
using (var reader = new StreamReader(filterContext.HttpContext.Request.InputStream))
{
inputContent = reader.ReadToEnd();
}
var result = JsonConvert.DeserializeObject(inputContent, JsonDataType, Settings ?? new JsonSerializerSettings());
filterContext.ActionParameters[Parameter] = result;
}
}
}
And finally:
[JsonFilter(Parameter = "model", JsonDataType = typeof(UserArgLevelModel), Settings = new JsonSerializerSettings { ContractResolver = new SnakeCasePropertyNamesContractResolver() })]
public ActionResult UpdateArgLevel(UserArgLevelModel model) {
{
// model is deserialized correctly!
}