Serialize mongodb document to domain object - c#

I'm trying to read a mondodb document into my domain class (Company) but get an error on one of the properties.
The error message reads:
"Expected a nested document representing the serialized form of a
OrgNumber value, but found a value of type String instead"
The objects looks like this:
public class OrgNumber
{
public string Value { get; private set; }
...
private OrgNumber() { }
public OrgNumber(string value) {
Value = value;
}
}
public class Company
{
public string Name { get; private set; }
public OrgNumber OrgNumber { get; private set; }
...
private Company() { }
public Company(string name, OrgNumber orgNumber)
{
Name = name;
OrgNumber = orgNumber;
}
}
The mongodb document looks like this:
{
"name": "Company name",
"orgNumber": "1234-5678",
}
I'm reading the document and mapping it directly into my domain model:
var collection = _mongoDb.GetCollection<Company>("Companies");
var result = await collection.Find(c => c.CompanyId == companyId).SingleOrDefaultAsync();
How do I correctly get the string representation of OrgNumber to the correct type OrgNumber?

You can create your own serializer inheriting from SerializerBase<T>
public class OrgNumberSerializer : SerializerBase<OrgNumber>
{
public override OrgNumber Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
{
var serializer = BsonSerializer.LookupSerializer(typeof(string));
var data = serializer.Deserialize(context, args);
return new OrgNumber(data.ToString());
}
public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, OrgNumber value)
{
var serializer = BsonSerializer.LookupSerializer(typeof(string));
serializer.Serialize(context, value.Value);
}
}
Then it needs to be registered globally using below line:
BsonSerializer.RegisterSerializer(typeof(OrgNumber), new OrgNumberSerializer());
You can find more about the details here

Related

How to mask sensetive data for particular requests (NLog)

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)

Map JSON payload to DTO with different field names

I have a simple GitHub payload incoming to my ASP.NET Core application and I would like to know how can I map the payload I receive to my DTO.
Example DTO
public class GithubPayload
{
public string Action { get; set; } // action
public string Name { get; set; } // pull_request.title
}
Example payload
{
"action": "deleted",
"pull_request": {
"title": "Fix button"
}
}
You can use JsonProperty attribute on Action and a custom converter on the Name that can interpret nested properties. check Json.Net's JsonConverter
public class GithubPayload {
[JsonProperty("action")]
public string Action { get; set; }
[JsonConverter(typeof(NestedConverter), "pull_request.title")]
public string Name { get; set; }
}
Where NestedConverter is a custom JsonConverter that will read a nested property
public class NestedConverter : JsonConverter {
private readonly string path;
public NestedConverter (string path) {
this.path = path;
}
//...to do
}
Update:
Using a JsonConverter for converting the payload itself actually works as well
public class GithubPayloadConverter : JsonConverter {
public override bool CanConvert(Type objectType) {
return objectType == typeof(GithubPayload);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) {
dynamic data = JObject.Load(reader);
var model = new GithubPayload {
Action = data.action,
Name = data.pull_request.title
};
return model;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) {
throw new NotImplementedException();
}
}
and decorating the class itself
[JsonConverter(typeof(GithubPayloadConverter))]
public class GithubPayload {
public string Action { get; set; }
public string Name { get; set; }
}
Deserialization is simply
string json = #"{ 'action': 'deleted', 'pull_request': { 'title': 'Fix button' } }";
var payload = JsonConvert.DeserializeObject<GithubPayload>(json);
Just an idea:
using Microsoft.AspNetCore.Hosting;
using Newtonsoft.Json;
using System.IO;
namespace WebApplication1
{
public class Program
{
public class GithubPayload
{
public string Action { get; set; } // action
public string Name { get; set; } // pull_request.title
}
public static void Main(string[] args)
{
string json = #"{
""action"": ""deleted"",
""pull_request"": {
""title"": ""Fix button""
}
}";
dynamic obj = JsonConvert.DeserializeObject(json);
GithubPayload entity = new GithubPayload();
entity.Action = obj.action;
entity.Name = obj.pull_request.title;
..................
}
}
}
Tested this solution, it works.

C# WebAPI Routing by POST value type

I have a request to create a Web API that is able to accept a POST request and take different actions depending on the type of data (DataAvailableNotification vs ExpiredNotification) received in the parameters.
I've created an ApiController and exposed two methods:
[HttpPost]
public void DataAvailable(DataAvailableNotification dataAvailable,
[FromUri] string book, [FromUri] string riskType)
{
}
[HttpPost]
public void DataAvailable(ExpiredNotification dataAvailable,
[FromUri] string book, [FromUri] string riskType)
{
}
public class DataAvailableNotification
{
[JsonProperty(PropertyName = "$type")]
public string RdfType { get { return "App.RRSRC.Feeds.DataAvailable"; } }
public string SnapshotRevisionId { get; set; }
public string[] URLs { get; set; }
public string ConsumerId { get; set; }
public Guid ChannelId { get; set; }
}
public class ExpiredNotification
{
[JsonProperty(PropertyName = "$type")]
public string RdfType { get { return "Service.Feeds.Expired"; } }
public string ConsumerId { get; set; }
public Guid ChannelId { get; set; }
}
However, they don't get called at all.
If I comment out one of them the notification reaches the controller but I cannot handle the notification type correctly (given that both notifications will map to the same method).
Is there any way configuring Web API to look into the type of the POSTed value and call the best matching controller method?
PS: I cannot have 2 different of URLs to handle the different notifications. So please don't suggest this.
Use one action and filter based on the type.
I got around the same issue by using reflection and doing something like the following.
[HttpPost]
public void DataAvailable([FromBody]IDictionary<string, string> dataAvailable,
[FromUri] string book, [FromUri] string riskType) {
if(dataAvailable != null && dataAvailable.ContainsKey("$type") {
var type = dataAvaliable["$type"];
if(type == "App.RRSRC.Feeds.DataAvailable"){
DataAvailableNotification obj = createInstanceOf<DataAvailableNotification>(dataAvailable);
DataAvailable(obj,book,riskType);
} else if (type == "Service.Feeds.Expired") {
ExpiredNotification obj = createInstanceOf<ExpiredNotification>(dataAvailable);
DataAvailable(obj,book,riskType);
}
}
}
private void DataAvailable(DataAvailableNotification dataAvailable, string book, string riskType) {
}
private void DataAvailable(ExpiredNotification dataAvailable, string book, string riskType) {
}
private T createInstanceOf<T>(IDictionary<string, string> data) where T : class, new() {
var result = new T();
var type = typeof(T);
//map properties
foreach (var kvp in data) {
var propertyName = kvp.Key;
var rawValue = kvp.Value;
var property = type.GetProperty(propertyName);
if (property != null && property.CanWrite) {
property.SetValue(result, rawValue );
}
}
return result;
}
The solution I settled with is similar to what #Nikosi and #jpgrassi suggested.
In the controller I've created a single notification point:
[HttpPost]
public void Notify(BaseNotification notification,
[FromUri] string book, [FromUri] string riskType)
{
DataAvailableNotification dataAvailableNotification;
ExpiredNotification expiredNotification;
if ((dataAvailableNotification = notification as DataAvailableNotification) != null)
{
HandleDataAvailableNotification(dataAvailableNotification);
}
else if ((expiredNotification = notification as ExpiredNotification) != null)
{
HandleExpiredNotification(expiredNotification);
}
}
private void HandleDataAvailableNotification(DataAvailableNotification dataAvailableNotification)
{
}
private void HandleExpiredNotification(ExpiredNotification expiredNotification)
{
}
BaseNotification is the base class for all notifications:
public abstract class BaseNotification
{
[JsonProperty(PropertyName = "$type")]
public abstract string RdfType { get; }
public string ConsumerId { get; set; }
public Guid ChannelId { get; set; }
}
Created a JsonConverter:
public class RdfNotificationJsonConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotSupportedException();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var resultJson = JObject.Load(reader);
var rdfType = resultJson["$type"].ToObject<string>();
BaseNotification result;
switch (rdfType)
{
case "App.RRSRC.Feeds.DataAvailable":
{
result = new DataAvailableNotification
{
SnapshotRevisionId = resultJson["SnapshotRevisionId"].ToObject<string>(),
URLs = resultJson["URLs"].ToObject<string[]>()
};
break;
}
case "Service.Feeds.Expired":
{
result = new ExpiredNotification();
break;
}
default:
{
throw new NotSupportedException();
}
}
result.ChannelId = resultJson["ChannelId"].ToObject<Guid>();
result.ConsumerId = resultJson["ConsumerId"].ToObject<string>();
return result;
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(BaseNotification);
}
}
And registered the new converter in the configuration:
public static void Configure(HttpSelfHostConfiguration config)
{
Throw.IfNull(config, "config");
config.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new RdfNotificationJsonConverter());
}
I like this solution better because I have the actual type in the controller and the converter handles the ugly deserialization part (also more testable).
PS: I'll move the literal strings somewhere else so I don't specify them twice in the solution.

How to configure JSON.net deserializer to track missing properties?

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; }
}
}

Change Names of Properties in Object which was Deserialize from a JSON String in C#

I have a problem with the Names of my deserialized object, which I want to return.
At the moment I am writing a WebService, which requests data from another WebService and then returns this Data to an Application.
Example:
public class UserController
{
public HttpResponseMessage GetUser()
{
// Get Data from WebService
string jsonString = #"{'key': 'A45', 'field1': 'John', 'field2': 'Doe', address{'field3': 'HelloWorld Ave.', 'field4': 'Somewhere'}}";
// Make Object from JSON-String
User user = JsonConvert.DeserializeObject<User>(jsonString);
// Return Object to Application
return Request.CreateResponse(HttpStatusCode.OK, user);
}
}
public class User
{
[JsonProperty("key")]
public string Key { get; set; }
[JsonProperty("field1")]
public string FirstName { get; set; }
[JsonProperty("field2")]
public string LastName { get; set; }
[JsonProperty("address")]
public Address Address { get; set; }
}
public class Address
{
[JsonProperty("field3")]
public string Street { get; set; }
[JsonProperty("field4")]
public string City { get; set; }
}
So far so good. My WebService creates the Object "User" and returns it to the Application.
And now my problem:
The returning JSON string changes the field names back to its original name.
Instead of:
"{'key': 'A45', 'field1': 'John', 'field2': 'Doe', Address {'Street': 'HelloWorld Ave.', 'City': 'Somewhere'}}"
I get:
"{'key': 'A45', 'field1': 'John', 'field2': 'Doe', Address {'Street': 'HelloWorld Ave.', 'City': 'Somewhere'}}"
I know that the PropertyName in JsonProperty is not Case-Sensitive, so I could write [JsonProperty("Key")] with an uppercase "K" and the returning Json-String will contain the Uppercase "K".
But what about my fields? Is there any way I can change "field1" to "FirstName", and "field2" to "LastName"?
Edit 1 - 2015-01-28: Added more code to the example.
I would suggest to perform 2 steps
You (de)serialize your DTO (Data Transfer Object) as it is
You implement an assembly layer where you control the creation of your domain objects.
In your case it seems that it's a 1:1 relation between DTO and DomainModel with just different property names - so you only need to project it that way. I can highly recommend Automapper Projections or in case it's nested Automapper Nested Projections for this.
The big advantage here is that you decouple your whole domain layer from your external services, so even if your webservices gets broken / changes, it doesn't affect your business logic.
Both of #stefankmitph and #nhaberl answers were good answers, but I used a slightly different technic in the end.
Both of my classes now have private fields, for setting the value of the JSON-String to public-fields.
The private fields get the PropertyName of the incoming JSON-String and the public-fields get the PropertyName for the outgoing JSON-String:
public class User
{
#region private
[JsonProperty("key")]
private string _Key { set { Key = value; } }
[JsonProperty("field1")]
private string _FirstName { set { FirstName = value; } }
[JsonProperty("field2")]
private string _LastName { set { LastName = value; } }
[JsonProperty("address")]
private Address _Address { set { Address = value; } }
#endregion
#region public
[JsonProperty("Key")]
public string Key { get; private set; }
[JsonProperty("FirstName")]
public string FirstName { get; private set; }
[JsonProperty("LastName")]
public string LastName { get; private set; }
[JsonProperty("Address")]
public Address Address { get; private set; }
#endregion
}
public class Address
{
#region private
[JsonProperty("field3")]
private string _Street { set { Key = value; } }
[JsonProperty("field4")]
private string _City { set { FirstName = value; } }
#endregion
#region public
[JsonProperty("Street")]
public string Street { get; private set; }
[JsonProperty("City")]
public string City { get; private set; }
#endregion
}
Example output:
"{'Key': 'A45', 'FirstName': 'John', 'LastName': 'Doe', Address {'Street': 'HelloWorld Ave.', 'City': 'Somewhere'}}"
With this method I could also get all Data from the unerlying classes to the main class:
public class User
{
#region private
// Other fields, see above. \\
[JsonProperty("address")]
private Address _Address
{
set
{
City = value.City;
Street = value.Street;
}
}
#endregion
#region public
// Other fields, see above. \\
[JsonProperty("City")]
public string City { get; private set; }
[JsonProperty("Street")]
public string Street { get; private set; }
#endregion
}
public class Address
{
// Address fields. See above.
}
Example output:
"{'Key': 'A45', 'FirstName': 'John', 'LastName': 'Doe', 'Street': 'HelloWorld Ave.', 'City': 'Somewhere'}"
By writing your own serializer and adding a custom attribute to the properties you can force the serializer to take custom names when serializing back to Json.
public class JsonSerializeProperty : Attribute
{
public string PropertyName { get; set; }
}
// derive from JsonConverter and override WriteJson (serialize)
// and ReadJson (deserialize)
// note: i have not implemented CanConvert
public class CustomUserSerializer : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var user = value as User;
if(user == null)
throw new NullReferenceException("user");
var properties = user.GetType().GetProperties();
writer.WriteStartObject();
foreach (var property in properties)
{
// get the attribute assigned to the property [JsonSerializeProperty]
object customAttributes = property.GetCustomAttributes(typeof(JsonSerializeProperty), false).SingleOrDefault();
JsonSerializeProperty attribute = customAttributes as JsonSerializeProperty;
if(attribute != null)
{
// JsonSerializeProperty
string propertyName = attribute.PropertyName;
// just write new property name and value
writer.WritePropertyName(propertyName);
writer.WriteValue(property.GetValue(value, new object[] {}));
}
}
writer.WriteEndObject();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// just map every JProperty from the Json string to the
// JsonProperty of the user class. I know this is kind of ugly... but it may serve your purpose
JObject jsonObject = JObject.Load(reader);
List<JProperty> properties = jsonObject.Properties().ToList();
// create an instance of User to assign the values of the
// Json string
object instance = Activator.CreateInstance(objectType);
// loop through the user properties and get the
// JsonProperty customattribute. then set the value of the JProperty
PropertyInfo[] objectProperties = objectType.GetProperties();
foreach (var objectProperty in objectProperties)
{
var customAttribute = objectProperty.GetCustomAttributes(typeof(JsonPropertyAttribute), false).SingleOrDefault();
JsonPropertyAttribute attribute = customAttribute as JsonPropertyAttribute;
if (attribute != null)
{
JProperty jsonProperty = properties.SingleOrDefault(prop => prop.Name == attribute.PropertyName);
if (jsonProperty != null)
{
objectProperty.SetValue(instance, jsonProperty.Value.ToString(), new object[] {});
}
}
}
return instance;
// {
// _Key = "A45",
// _FirstName = "John",
// _LastName = "Doe"
// }
}
public override bool CanConvert(Type objectType)
{
throw new NotImplementedException();
}
}
[JsonConverter(typeof(CustomUserSerializer))]
public class User
{
[JsonProperty(PropertyName = "key")]
[JsonSerializeProperty(PropertyName = "_Key")]
public string Key { get; set; }
[JsonProperty("field1")]
[JsonSerializeProperty(PropertyName = "_FirstName")]
public string FirstName { get; set; }
[JsonProperty("field2")]
[JsonSerializeProperty(PropertyName = "_LastName")]
public string LastName { get; set; }
}

Categories

Resources