This website uses cookies

This website uses cookies to give you the best and most relevant experience. By continuing to browse this site, you are agreeing to our use of cookies. Learn More.

On Demand Property Serialization in Asp.Net Core MVC

This article describes a simple approach to selectively control which properties of a class get serialized to the output and which ones get ignored in an asp.net core mvc (2.2 at the time of writing). This method does not require any alteration in the serialized class. The caller must specify the list of fields to return in the querystring as a comma separated value - if none are specified, all the fields of that class would be returned. The field selection is specified in an ActionFilter by changing the behaviour of the json serializer (Newtonsoft.Json) to ignore fields that are not part of the request querystring and happens relatively late in the overall pipeline.

Ignoring Properties in Newtonsoft.Json

Asp.Net Core MVC uses Newtonsoft.Json as it's json serializer (this might change in the future). Applying the JsonIgnore attribute to a property of a class prevents it from being serialized.

Applying the JsonIgnore attribute to a property
public class MyClass
{
    //ignore this property
    [JsonIgnore]
    public int MyProperty { get; set; }
}

The above approach works well if properties to be ignored are known ahead of time. A more flexible method - one where the properties are selected at runtime - would involve changing the behaviour of the method that creates the json version of the properties. In Newtonsoft.Json, this can be achieved by creating a subclass of DefaultContractResolver and overriding its CreateProperty method.

A custom contract resolver to ignore fields at runtime
public class DynamicFieldResolver : DefaultContractResolver
{
    private readonly Dictionary<Type, HashSet<string>> serializedProperties;
    public DynamicFieldResolver() => serializedProperties = new Dictionary<Type, HashSet<string>>();

    public void IncludePropertyForSerialization(Type type, params string[] jsonProperties)
    {
        if (!serializedProperties.ContainsKey(type)) serializedProperties[type] = new HashSet<string>();

        foreach (var prop in jsonProperties)
            if(!string.IsNullOrWhiteSpace(prop))
                serializedProperties[type].Add(prop);
    }

    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        var property = base.CreateProperty(member, memberSerialization);
        var type = property.DeclaringType;

        if (!IsPropertySerializable(type, property))
        {
            property.ShouldSerialize = s => false;
            property.Ignored = true;
        }

        return property;
    }

    private bool IsPropertySerializable(Type type, JsonProperty property)
    {
        //serialize all properties if nothing is specified
        if (!serializedProperties.ContainsKey(type))
            return true;
        else if (serializedProperties[type].Count == 0)
            return true;

        //otherwise ignore the ones that are not specified in the list
        return serializedProperties[type].Any(p => p.ToLowerInvariant().Equals(property.PropertyName.ToLowerInvariant()));
    }
}

The IncludePropertyForSerialization method creates a collection of properties (for a given type) that should be considered for serialization based on what the caller provided in the request. When it's time to consider a property for serialization - which happens in the CreateProperty method - it's ShouldSerialize and Ignored values are set accordingly.

A property will only get serialized if one of the following conditions is met:

  • The serializedProperties collection does not contain any value for the specified type
  • The serializedProperties collection contains an empty list of properties to be serialized
  • The serializedProperties collection contains the targeted property for the specified type

Providing Caller's Input to DynamicFieldResolver

The final piece in the jigsaw is to create an ActionFilterAttribute to decorate a controller or any of its action methods. This filter will inspect the request's querystring and provide the list of fields to serialize to the field resolver and setup the JsonSerializerSettings.

A custom ActionFilterAttribute to set up the JsonSerializerSettings
public class DynamicFieldsFilter : ActionFilterAttribute
{
    private readonly Type targetType;

    public DynamicFieldsFilter(Type type) => this.targetType = type;

    public override void OnActionExecuted(ActionExecutedContext context)
    {
        var fields = context.HttpContext.Request.Query["fields"];
        var jsonResolver = new DynamicFieldResolver();

        foreach (var field in fields)
        {
            string[] requestedFields = field.ToLower().Split(',');
            jsonResolver.IncludePropertyForSerialization(targetType, requestedFields);
        }

        var serializerSettings = new JsonSerializerSettings();
        serializerSettings.ContractResolver = jsonResolver;
        ((JsonResult)context.Result).SerializerSettings = serializerSettings;
    }
}

Applying the Filter Attribute

The snippet below shows an example of how to use this filter attribute.

DynamicFieldFilter in action
[DynamicFieldsFilter(typeof(Product))]
[Route("products")]
public class ProductsController : Controller
{        
    private readonly List<Product> products;

    public ProductsController()
    {
        products = Repository.GetProducts();
    }
    
    [HttpGet]        
    public IActionResult Index() => Json(products);

    [HttpGet("{id}")]
    public IActionResult Details(int id) => Json(products.FirstOrDefault(p => p.Id == id));
}

Request Examples

The following sections highlights a few examples of the output.

Class to be serialized
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description{ get; set; }
    public decimal Price { get; set; }        
}
Everything gets serialized - http://localhost:5002/products
[
    {"Id":1,"Name":"iPhone 8","Description":"Mobile phone made by Apple and running on iOS","Price":1000.0},
    {"Id":2,"Name":"Galaxy 10","Description":"Manufactured by Samsung and running Android OS","Price":999.0},
    {"Id":3,"Name":"Pixel","Description":"Google's phone, running Android","Price":888.0}, 
    {"Id":4,"Name":"Librem","Description":"Built on PureOS Linux distro, Designed by Purism","Price":777.0}
]
Only Id and Name serialized - http://localhost:5002/products?fields=Id,Name
[
  { "Id": 1, "Name": "iPhone 8" },
  { "Id": 2, "Name": "Galaxy 10" },
  { "Id": 3, "Name": "Pixel" },
  { "Id": 4, "Name": "Librem" }
]

The source code is available here.