I have the following poco (other properties omitted for simplicity) :
public class Address
{
. . .
public string CountryCode { get; set; }
. . .
}
What do I have to do in the BsonClassMap to enforce Upper Case only for this property.
For Example "us" will be stored in the db as "US"
BsonClassMap.RegisterClassMap<Address>(cm =>
{
// what am I missing here ?
});
Or am I approaching this the wrong way ?
here's a custom serializer attribute you can decorate the country code property with:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class UpperCaseAttribute : BsonSerializerAttribute
{
public UpperCaseAttribute() : base(typeof(UpperCaseSerializer)) { }
private class UpperCaseSerializer : SerializerBase<string>
{
public override void Serialize(BsonSerializationContext ctx, BsonSerializationArgs args, string value)
{
if (value is null)
ctx.Writer.WriteNull();
else
ctx.Writer.WriteString(value.ToUpper());
}
public override string Deserialize(BsonDeserializationContext ctx, BsonDeserializationArgs args)
{
switch (ctx.Reader.CurrentBsonType)
{
case BsonType.String:
return ctx.Reader.ReadString();
case BsonType.Null:
ctx.Reader.ReadNull();
return null;
default:
throw new BsonSerializationException($"'{ctx.Reader.CurrentBsonType}' values are not valid on properties decorated with an [UpperCase] attribute!");
}
}
}
usage:
public class Address
{
[UpperCase]
public string CountryCode { get; set; }
}
Check this:
SetElementName way:
BsonClassMap.RegisterClassMap<Address>(cm =>
{
cm.MapField(e => e.CountryCode).SetElementName("COUNTRY_CODE");
});
var address = new Address();
var bson = address.ToBsonDocument();
// bson: { "COUNTRY_CODE" : null }
BsonElement attribute way:
public class Address
{
[BsonElement("COUNTRY_CODE")]
public string CountryCode { get; set; }
}
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 a route in a controller with some model transferred. This model (DTO) contains a custom type property (eg. Password, zip-code, ...). I would like to add a ModelBinder like one described here (https://learn.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-model-binding?view=aspnetcore-6.0) to the Property Type (Password). !Not to the whole DTO!
The resulting model (DTO) definition should look like:
namespace UserService.Models.DTO
{
public class UserRegisterDto
{
public string Firstname { get; set; }
public string Lastname { get; set; }
public string Email { get; set; }
public Password Password { get; set; }
}
}
When a JSON (eg. see below) is now transferred the Password property shall be automatically validated.
{
"firstname": "John",
"lastname": "Doe",
"email": "johndoe#mail.com",
"password": "Test12345!"
}
The route implementation should look like that:
namespace UserService.Controllers
{
[ApiController]
[Route("/api/user/")]
public class AuthController : ControllerBase
{
[HttpPost("register")]
public async Task<ActionResult> Register(UserRegisterDto request)
{
User? user = await this._userService.Register(request);
if (user == null)
{
return BadRequest();
}
return Ok(user);
}
}
}
At first I thought I could implement something like the following but the validation does not work at all...
namespace UserService.Models
{
[ModelBinder(typeof(PasswordEntityBinder))]
public class Password
{
private const string PasswordRegex = "(?=^.{8,}$)(?=.*\\d)(?=.*[!##$%&?*\\\"§$\\/()=~]+)(?![.\\n])(?=.*[A-Z])(?=.*[a-z]).*$";
#region Properties
public string Value { get; set; }
public override string ToString() => Value;
public static implicit operator string(Password e) => e.Value;
public static bool TryParse(ReadOnlySpan<char> s, out Password? result)
{
result = null;
if (string.IsNullOrWhiteSpace(s.ToString()))
return false;
if (!Regex.IsMatch(s.ToString(), PasswordRegex))
return false;
result = new Password()
{
Value = s.ToString(),
};
return true;
}
}
}
Does anyone know if there is any possibility to achieve a modelbinding to a type which is used as a property in a model (DTO)?
I was having a further look for my question since I was not satisfied with using some 3rd party lib. I found a proper solution which allows me to use a custom complex type as for example int with validation and conversion at the same time. This solution works for asp.net net6.0. I use:
A System.Text.Json JSON custom converter
IValidatableObject
The json password is at first converted with the JSON converter and then validated.
The following shows the example, which can be applied to any type:
// Password.cs
namespace UserService.Models
{
[JsonConverter(typeof(PasswordJsonConverter))]
public class Password : IValidatableObject
{
private const string PasswordRegex = "(?=^.{8,}$)(?=.*\\d)(?=.*[!##$%&?*\\\"§$\\/()=~]+)(?![.\\n])(?=.*[A-Z])(?=.*[a-z]).*$";
public string Value { get; set; } = "";
public override string ToString() => Value;
public static implicit operator string(Password e) => e.Value;
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (string.IsNullOrWhiteSpace(this.Value))
{
yield return new ValidationResult("Password may not be empty");
yield break;
}
if (!Regex.IsMatch(this.Value, PasswordRegex))
yield return new ValidationResult("Password invalid formatted");
}
}
}
// PasswordJsonConverter.cs
namespace UserService.Converter
{
public class PasswordJsonConverter : JsonConverter<Password>
{
public override bool CanConvert(Type typeToConvert)
{
return (typeToConvert == typeof(Password));
}
public override Password? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
Password pw = new Password();
string? pw_string;
try
{
pw_string = reader.GetString();
}catch(InvalidOperationException)
{
return null;
}
if (pw_string == null)
return null;
pw.Value = pw_string;
return pw;
}
public override void Write(Utf8JsonWriter writer, Password value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString());
}
}
}
This allows me to use the Password object in a DTO like any basic type. For example:
// RegisterDto.cs
namespace UserService.Models.DTO
{
public class UserRegisterDto
{
public string Firstname { get; set; }
public string Lastname { get; set; }
public string Email { get; set; }
public Password Password { get; set; }
}
}
The input json would look like:
{
"firstname": "John",
"lastname": "Doe",
"email": "johndoe#mail.com",
"password": "Test12345!"
}
I need to deserialize jsons to a type that contains a property of interface type - IExceptionModel. I prescribe maps for interfaces to classes like this:
public static class JsonSerialization
{
public static T FromJson<T>(this string obj) => JsonConvert.DeserializeObject<T>(obj, Settings);
public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings
{
ContractResolver = new ContractResolver()
};
private class ContractResolver : DefaultContractResolver
{
protected override JsonContract CreateContract(Type objectType)
{
var result = base.CreateContract(objectType);
if (objectType == typeof(IExceptionModel))
{
result.CreatedType = typeof(ExceptionModel);
result.DefaultCreator = () => result.CreatedType.GetConstructor(new Type[0]).Invoke(null);
}
return result;
}
}
}
Here are my interface and class types:
public interface IExceptionModel : IModelBase
{
public string Message { get; set; }
public int Index { get; set; }
}
public class ExceptionModel : IExceptionModel
{
public string Message { get ; set ; }
public byte Index { get; set; }
}
Here is the class to deserialize to:
public class Status
{
public IExceptionModel Error { get; set; }
}
When I take a proper input string like:
{
"Error" : {
"Message": "Error",
"Index": 404
}
}
and feed it to FromJson<Status> method, described above, I get Error property set to null, although I believe I have resolved the contract for the interface.
What else do I need to do in order to make it work?
Update.
When preparing this example, I messed some details. The IExceptionModel Error property doesn't have setter on the interface. It does on implementation. So now, when I have added setter to the interface, the property ends up with the needed value. If I wipe it, it has null after deserialization.
So the question turns into, how do I tell Newtonsoft Serializer to use the constructor of the implementation, use ITS getters and setters to fill what properties it can and only then treat it as the interface instance?
I found a workaround to assign an internal setter to the interface property and then instruct:
jsonContract.DefaultCreatorNonPublic = true;
But it makes the interface look crooked, to say the least.
I made some corrections and this worked for me:
result.CreatedType = typeof(Status); --> result.CreatedType = typeof(ExceptionModel);
public byte Index { get; set; } --> public int Index { get; set; }
I uploaded this online example: https://dotnetfiddle.net/ETSJee
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
public interface IModelBase {}
public interface IExceptionModel : IModelBase
{
public string Message { get; set; }
public int Index { get; set; }
}
public class ExceptionModel : IExceptionModel
{
public string Message { get ; set ; }
public int Index { get; set; }
}
public class Status
{
public IExceptionModel Error { get; set; }
}
public static class JsonSerialization
{
public static T FromJson<T>(this string obj)
{
return JsonConvert.DeserializeObject<T>(obj, Settings);
}
public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings
{
ContractResolver = new ContractResolver()
};
private class ContractResolver : DefaultContractResolver
{
protected override JsonContract CreateContract(Type objectType)
{
var result = base.CreateContract(objectType);
if (objectType == typeof(IExceptionModel))
{
result.CreatedType = typeof(ExceptionModel);
result.DefaultCreator = () => result.CreatedType.GetConstructor(new Type[0]).Invoke(null);
}
return result;
}
}
}
class Program
{
static void Main(string[] args)
{
var txt = #"
{
'Error' : {
'Message': 'Error',
'Index': 404
}
}
";
var obj = JsonSerialization.FromJson<Status>(txt);
Console.WriteLine(obj.Error.Index);
Console.WriteLine(obj.Error.Message);
}
}
this works for me without any contract resolvers
var status = JsonConvert.DeserializeObject<Status>(txt);
public class Status
{
public IExceptionModel Error { get; set; }
[JsonConstructor]
public Status (ExceptionModel error) {
Error=error;
}
public Status() {}
}
if you need to use it in many classes you can use this code instead
public class Status
{
[JsonProperty("Error")]
private ExceptionModel _error
{
set { Error = value; }
get { return (ExceptionModel)Error; }
}
[JsonIgnore]
public IExceptionModel Error { get; set; }
}
test
var status = JsonConvert.DeserializeObject<MyClass>(txt);
Console.WriteLine(status.Error.Index); //404
Console.WriteLine(status.Error.Message); //Error
public class MyClass:Status
{
public string Name {get; set;}
}
Sample class:
public class ClassA
{
public int Id { get; set; }
public string SomeString { get; set; }
public int? SomeInt { get; set; }
}
Default deserializer:
var myObject = JsonConvert.DeserializeObject<ClassA>(str);
Create the same object for two different inputs
{"Id":5}
or
{"Id":5,"SomeString":null,"SomeInt":null}
How can I track properties that were missing during deserialization process and preserve the same behavior? Is there a way to override some of JSON.net serializer methods (e.g. DefaultContractResolver class methods) to achive this. For example:
List<string> missingProps;
var myObject = JsonConvert.DeserializeObject<ClassA>(str, settings, missingProps);
For the first input list should contains the names of the missing properties ("SomeString", "SomeInt") and for second input it should be empty. Deserialized object remains the same.
1. JSON has a property which is missing in your class
Using property JsonSerializerSettings.MissingMemberHandling you can say whether missing properties are handled as errors.
Than you can install the Error delegate which will register errors.
This will detect if there is some "garbage" property in JSON string.
public class ClassA
{
public int Id { get; set; }
public string SomeString { get; set; }
}
internal class Program
{
private static void Main(string[] args)
{
const string str = "{'Id':5, 'FooBar': 42 }";
var myObject = JsonConvert.DeserializeObject<ClassA>(str
, new JsonSerializerSettings
{
Error = OnError,
MissingMemberHandling = MissingMemberHandling.Error
});
Console.ReadKey();
}
private static void OnError(object sender, ErrorEventArgs args)
{
Console.WriteLine(args.ErrorContext.Error.Message);
args.ErrorContext.Handled = true;
}
}
2. Your class has a property which is missing in JSON
Option 1:
Make it a required property:
public class ClassB
{
public int Id { get; set; }
[JsonProperty(Required = Required.Always)]
public string SomeString { get; set; }
}
Option 2:
Use some "special" value as a default value and check afterwards.
public class ClassB
{
public int Id { get; set; }
[DefaultValue("NOTSET")]
public string SomeString { get; set; }
public int? SomeInt { get; set; }
}
internal class Program
{
private static void Main(string[] args)
{
const string str = "{ 'Id':5 }";
var myObject = JsonConvert.DeserializeObject<ClassB>(str
, new JsonSerializerSettings
{
DefaultValueHandling = DefaultValueHandling.Populate
});
if (myObject.SomeString == "NOTSET")
{
Console.WriteLine("no value provided for property SomeString");
}
Console.ReadKey();
}
}
Option 3:
Another good idea would be to encapsulate this check iside the class istself. Create a Verify() method as shown below and call it after deserialization.
public class ClassC
{
public int Id { get; set; }
[DefaultValue("NOTSET")]
public string SomeString { get; set; }
public int? SomeInt { get; set; }
public void Verify()
{
if (SomeInt == null ) throw new JsonSerializationException("SomeInt not set!");
if (SomeString == "NOTSET") throw new JsonSerializationException("SomeString not set!");
}
}
Another way to find null/undefined tokens during De-serialization is to write a custom JsonConverter , Here is an example of custom converter which can report both omitted tokens (e.g. "{ 'Id':5 }") and null tokens (e.g {"Id":5,"SomeString":null,"SomeInt":null})
public class NullReportConverter : JsonConverter
{
private readonly List<PropertyInfo> _nullproperties=new List<PropertyInfo>();
public bool ReportDefinedNullTokens { get; set; }
public IEnumerable<PropertyInfo> NullProperties
{
get { return _nullproperties; }
}
public void Clear()
{
_nullproperties.Clear();
}
public override bool CanConvert(Type objectType)
{
return true;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
existingValue = existingValue ?? Activator.CreateInstance(objectType, true);
var jObject = JObject.Load(reader);
var properties =
objectType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
foreach (var property in properties)
{
var jToken = jObject[property.Name];
if (jToken == null)
{
_nullproperties.Add(property);
continue;
}
var value = jToken.ToObject(property.PropertyType);
if(ReportDefinedNullTokens && value ==null)
_nullproperties.Add(property);
property.SetValue(existingValue, value, null);
}
return existingValue;
}
//NOTE: we can omit writer part if we only want to use the converter for deserializing
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var objectType = value.GetType();
var properties =
objectType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
writer.WriteStartObject();
foreach (var property in properties)
{
var propertyValue = property.GetValue(value, null);
writer.WritePropertyName(property.Name);
serializer.Serialize(writer, propertyValue);
}
writer.WriteEndObject();
}
}
Note: we can omit the Writer part if we don't need to use it for serializing objects.
Usage Example:
class Foo
{
public int Id { get; set; }
public string SomeString { get; set; }
public int? SomeInt { get; set; }
}
class Program
{
static void Main(string[] args)
{
var nullConverter=new NullReportConverter();
Console.WriteLine("Pass 1");
var obj0 = JsonConvert.DeserializeObject<Foo>("{\"Id\":5, \"Id\":5}", nullConverter);
foreach(var p in nullConverter.NullProperties)
Console.WriteLine(p);
nullConverter.Clear();
Console.WriteLine("Pass2");
var obj1 = JsonConvert.DeserializeObject<Foo>("{\"Id\":5,\"SomeString\":null,\"SomeInt\":null}" , nullConverter);
foreach (var p in nullConverter.NullProperties)
Console.WriteLine(p);
nullConverter.Clear();
nullConverter.ReportDefinedNullTokens = true;
Console.WriteLine("Pass3");
var obj2 = JsonConvert.DeserializeObject<Foo>("{\"Id\":5,\"SomeString\":null,\"SomeInt\":null}", nullConverter);
foreach (var p in nullConverter.NullProperties)
Console.WriteLine(p);
}
}
I got this problem, but defaultValue was not solution due to POCO object. I think this is simpler approach than NullReportConverter.
There are three unit tests. Root is class that encapsulate whole json. Key is type of the Property. Hope this helps someone.
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
namespace SomeNamespace {
[TestClass]
public class NullParseJsonTest {
[TestMethod]
public void TestMethod1()
{
string slice = "{Key:{guid:\"asdf\"}}";
var result = JsonConvert.DeserializeObject<Root>(slice);
Assert.IsTrue(result.OptionalKey.IsSet);
Assert.IsNotNull(result.OptionalKey.Value);
Assert.AreEqual("asdf", result.OptionalKey.Value.Guid);
}
[TestMethod]
public void TestMethod2()
{
string slice = "{Key:null}";
var result = JsonConvert.DeserializeObject<Root>(slice);
Assert.IsTrue(result.OptionalKey.IsSet);
Assert.IsNull(result.OptionalKey.Value);
}
[TestMethod]
public void TestMethod3()
{
string slice = "{}";
var result = JsonConvert.DeserializeObject<Root>(slice);
Assert.IsFalse(result.OptionalKey.IsSet);
Assert.IsNull(result.OptionalKey.Value);
}
}
class Root {
public Key Key {
get {
return OptionalKey.Value;
}
set {
OptionalKey.Value = value;
OptionalKey.IsSet = true; // This does the trick, it is never called by JSON.NET if attribute missing
}
}
[JsonIgnore]
public Optional<Key> OptionalKey = new Optional<Key> { IsSet = false };
};
class Key {
public string Guid { get; set; }
}
class Optional<T> {
public T Value { get; set; }
public bool IsSet { get; set; }
}
}
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!
}