Is there a way to change name of Data property during serialization, so I can reuse this class in my WEB Api.
For an example, if i am returning paged list of users, Data property should be serialized as "users", if i'm returning list of items, should be called "items", etc.
Is something like this possible:
public class PagedData
{
[JsonProperty(PropertyName = "Set from constructor")]??
public IEnumerable<T> Data { get; private set; }
public int Count { get; private set; }
public int CurrentPage { get; private set; }
public int Offset { get; private set; }
public int RowsPerPage { get; private set; }
public int? PreviousPage { get; private set; }
public int? NextPage { get; private set; }
}
EDIT:
I would like to have a control over this functionality, such as passing name to be used if possible. If my class
is called UserDTO
, I still want serialized property to be called Users
, not UserDTOs
.
Example
var usersPagedData = new PagedData("Users", params...);
You can do this with a custom ContractResolver
. The resolver can look for a custom attribute which will signal that you want the name of the JSON property to be based on the class of the items in the enumerable. If the item class has another attribute on it specifying its plural name, that name will then be used for the enumerable property, otherwise the item class name itself will be pluralized and used as the enumerable property name. Below is the code you would need.
First let's define some custom attributes:
public class JsonPropertyNameBasedOnItemClassAttribute : Attribute
{
}
public class JsonPluralNameAttribute : Attribute
{
public string PluralName { get; set; }
public JsonPluralNameAttribute(string pluralName)
{
PluralName = pluralName;
}
}
And then the resolver:
public class CustomResolver : DefaultContractResolver
{
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
JsonProperty prop = base.CreateProperty(member, memberSerialization);
if (prop.PropertyType.IsGenericType && member.GetCustomAttribute<JsonPropertyNameBasedOnItemClassAttribute>() != null)
{
Type itemType = prop.PropertyType.GetGenericArguments().First();
JsonPluralNameAttribute att = itemType.GetCustomAttribute<JsonPluralNameAttribute>();
prop.PropertyName = att != null ? att.PluralName : Pluralize(itemType.Name);
}
return prop;
}
protected string Pluralize(string name)
{
if (name.EndsWith("y") && !name.EndsWith("ay") && !name.EndsWith("ey") && !name.EndsWith("oy") && !name.EndsWith("uy"))
return name.Substring(0, name.Length - 1) + "ies";
if (name.EndsWith("s"))
return name + "es";
return name + "s";
}
}
Now you can decorate the variably-named property in your PagedData<T>
class with the [JsonPropertyNameBasedOnItemClass]
attribute:
public class PagedData<T>
{
[JsonPropertyNameBasedOnItemClass]
public IEnumerable<T> Data { get; private set; }
...
}
And decorate your DTO classes with the [JsonPluralName]
attribute:
[JsonPluralName("Users")]
public class UserDTO
{
...
}
[JsonPluralName("Items")]
public class ItemDTO
{
...
}
Finally, to serialize, create an instance of JsonSerializerSettings
, set the ContractResolver
property, and pass the settings to JsonConvert.SerializeObject
like so:
JsonSerializerSettings settings = new JsonSerializerSettings
{
ContractResolver = new CustomResolver()
};
string json = JsonConvert.SerializeObject(pagedData, settings);
Fiddle: https://dotnetfiddle.net/GqKBnx
If you're using Web API (looks like you are), then you can install the custom resolver into the pipeline via the Register
method of the WebApiConfig
class (in the App_Start
folder).
JsonSerializerSettings settings = config.Formatters.JsonFormatter.SerializerSettings;
settings.ContractResolver = new CustomResolver();
Another Approach
Another possible approach uses a custom JsonConverter
to handle the serialization of the PagedData
class specifically instead using the more general "resolver + attributes" approach presented above. The converter approach requires that there be another property on your PagedData
class which specifies the JSON name to use for the enumerable Data
property. You could either pass this name in the PagedData
constructor or set it separately, as long as you do it before serialization time. The converter will look for that name and use it when writing out JSON for the enumerable property.
Here is the code for the converter:
public class PagedDataConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(PagedData<>);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
Type type = value.GetType();
var bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public;
string dataPropertyName = (string)type.GetProperty("DataPropertyName", bindingFlags).GetValue(value);
if (string.IsNullOrEmpty(dataPropertyName))
{
dataPropertyName = "Data";
}
JObject jo = new JObject();
jo.Add(dataPropertyName, JArray.FromObject(type.GetProperty("Data").GetValue(value)));
foreach (PropertyInfo prop in type.GetProperties().Where(p => !p.Name.StartsWith("Data")))
{
jo.Add(prop.Name, new JValue(prop.GetValue(value)));
}
jo.WriteTo(writer);
}
public override bool CanRead
{
get { return false; }
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
To use this converter, first add a string property called DataPropertyName
to your PagedData
class (it can be private if you like), then add a [JsonConverter]
attribute to the class to tie it to the converter:
[JsonConverter(typeof(PagedDataConverter))]
public class PagedData<T>
{
private string DataPropertyName { get; set; }
public IEnumerable<T> Data { get; private set; }
...
}
And that's it. As long as you've set the DataPropertyName
property, it will be picked up by the converter on serialization.
Fiddle: https://dotnetfiddle.net/8E8fEE
Another option with no need to play with json formatters or use string replacements - only inheritance and overriding (still not very nice solution, imo):
public class MyUser { }
public class MyItem { }
// you cannot use it out of the box, because it's abstract,
// i.e. only for what's intended [=implemented].
public abstract class PaginatedData<T>
{
// abstract, so you don't forget to override it in ancestors
public abstract IEnumerable<T> Data { get; }
public int Count { get; }
public int CurrentPage { get; }
public int Offset { get; }
public int RowsPerPage { get; }
public int? PreviousPage { get; }
public int? NextPage { get; }
}
// you specify class explicitly
// name is clear,.. still not clearer than PaginatedData<MyUser> though
public sealed class PaginatedUsers : PaginatedData<MyUser>
{
// explicit mapping - more agile than implicit name convension
[JsonProperty("Users")]
public override IEnumerable<MyUser> Data { get; }
}
public sealed class PaginatedItems : PaginatedData<MyItem>
{
[JsonProperty("Items")]
public override IEnumerable<MyItem> Data { get; }
}
Here is a solution that doesn't require any change in the way you use the Json serializer. In fact, it should also work with other serializers. It uses the cool DynamicObject class.
The usage is just like you wanted:
var usersPagedData = new PagedData<User>("Users");
....
public class PagedData<T> : DynamicObject
{
private string _name;
public PagedData(string name)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
_name = name;
}
public IEnumerable<T> Data { get; private set; }
public int Count { get; private set; }
public int CurrentPage { get; private set; }
public int Offset { get; private set; }
public int RowsPerPage { get; private set; }
public int? PreviousPage { get; private set; }
public int? NextPage { get; private set; }
public override IEnumerable<string> GetDynamicMemberNames()
{
yield return _name;
foreach (var prop in GetType().GetProperties().Where(p => p.CanRead && p.GetIndexParameters().Length == 0 && p.Name != nameof(Data)))
{
yield return prop.Name;
}
}
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
if (binder.Name == _name)
{
result = Data;
return true;
}
return base.TryGetMember(binder, out result);
}
}
have a look here:
How to rename JSON key
Its not done during serialization but with a string operation.
Not very nice (in my eyes) but at least a possibility.
Cheers Thomas
The following is another solution tested in .NET Standard 2.
public class PagedResult<T> where T : class
{
[JsonPropertyNameBasedOnItemClassAttribute]
public List<T> Results { get; set; }
[JsonProperty("count")]
public long Count { get; set; }
[JsonProperty("total_count")]
public long TotalCount { get; set; }
[JsonProperty("current_page")]
public long CurrentPage { get; set; }
[JsonProperty("per_page")]
public long PerPage { get; set; }
[JsonProperty("pages")]
public long Pages { get; set; }
}
I am using Humanizer for pluralization.
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
JsonProperty property = base.CreateProperty(member, memberSerialization);
if (member.GetCustomAttribute<JsonPropertyNameBasedOnItemClassAttribute>() != null)
{
Type[] arguments = property.DeclaringType.GenericTypeArguments;
if(arguments.Length > 0)
{
string name = arguments[0].Name.ToString();
property.PropertyName = name.ToLower().Pluralize();
}
return property;
}
return base.CreateProperty(member, memberSerialization);
}