Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to Ignore Null values while serializing OData response

I've a requirement to omit the null valued fields from the response altogether. I can do this by modifying the JsonFormatter Serialization Setting for a normal webapi response.

config.Formatters.JsonFormatter.SerializationSettings
      .NullValueHandling = NullValueHandling.Ignore;

But that does not seem to work once i switch to OData.

Here are my files: WebApi.config:

public static void Register(HttpConfiguration config)
{
    var builder = new ODataConventionModelBuilder();
    var workerEntitySet = builder.EntitySet<Item>("Values");
    config.Routes.MapODataRoute("Default", "api", builder.GetEdmModel());
}

Item Model:

public class Item
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string OptionalField { get; set; }
}

ValuesController:

public class ValuesController : EntitySetController<Item, int>
{
    public static List<Item> items = new List<Item>() 
    {
        new Item { Id = 1, Name = "name1", OptionalField = "Value Present" }, 
        new Item { Id = 3, Name = "name2" } 
    };
    [Queryable(AllowedQueryOptions = AllowedQueryOptions.All)]
    public override IQueryable<Item> Get()
    {
        return items.AsQueryable();
    }
    [Queryable]
    protected override Item GetEntityByKey(int  id)
    {
        return items.Single(i => i.Id == id);
    }
}

Here is the response I get for GET: api/Values.

{
 "odata.metadata":"http://localhost:28776/api/$metadata#Values",
 "value":[
   {
     "Id":1,
     "Name":"name1",
     "OptionalField":"Value Present"
   },
   {
     "Id":3,
     "Name":"name2",
     "OptionalField":null
   }
  ]
}

But I do not need the elements with null values present in the response - in the response below, I need the "OptionalField" not to be present in the second item (As its value is null). I need to achieve it in my response, I do not want the users to query for non-null values only.

like image 417
Pavan Avatar asked Jul 21 '14 10:07

Pavan


2 Answers

In ODataLib v7 things changed drastically around these sorts of customisations thanks to Depencency Injection (DI)

This advice is for anyone who has upgraded to ODataLib v7, who may have implemented the previously accepted answers.

If you have the Microsoft.OData.Core nuget package v7 or later then this applies to you :). If you are still using older versions then use the code provided by @stas-natalenko but please DO NOT stop inheriting from ODataController...

We can globally override the DefaultODataSerializer so that null values are omitted from all Entity and Complex value serialized outputs using the following steps:

  1. Define your custom Serializer that will omit properties with null values

Inherit from Microsoft.AspNet.OData.Formatter.Serialization.ODataResourceSerializer

    /// <summary>
    /// OData Entity Serilizer that omits null properties from the response
    /// </summary>
    public class IngoreNullEntityPropertiesSerializer : ODataResourceSerializer
    {
        public IngoreNullEntityPropertiesSerializer(ODataSerializerProvider provider)
            : base(provider) { }

        /// <summary>
        /// Only return properties that are not null
        /// </summary>
        /// <param name="structuralProperty">The EDM structural property being written.</param>
        /// <param name="resourceContext">The context for the entity instance being written.</param>
        /// <returns>The property be written by the serilizer, a null response will effectively skip this property.</returns>
        public override Microsoft.OData.ODataProperty CreateStructuralProperty(Microsoft.OData.Edm.IEdmStructuralProperty structuralProperty, ResourceContext resourceContext)
        {
            var property = base.CreateStructuralProperty(structuralProperty, resourceContext);
            return property.Value != null ? property : null;
        }
    }
  1. Define a Provider that will determine when to use our custom Serializer

    Inherit from Microsoft.AspNet.OData.Formatter.Serialization.DefaultODataSerializerProvider

    /// <summary>
    /// Provider that selects the IngoreNullEntityPropertiesSerializer that omits null properties on resources from the response
    /// </summary>
    public class IngoreNullEntityPropertiesSerializerProvider : DefaultODataSerializerProvider
    {
        private readonly IngoreNullEntityPropertiesSerializer _entityTypeSerializer;
    
        public IngoreNullEntityPropertiesSerializerProvider(IServiceProvider rootContainer)
            : base(rootContainer) {
            _entityTypeSerializer = new IngoreNullEntityPropertiesSerializer(this);
        }
    
        public override ODataEdmTypeSerializer GetEdmTypeSerializer(Microsoft.OData.Edm.IEdmTypeReference edmType)
        {
            // Support for Entity types AND Complex types
            if (edmType.Definition.TypeKind == EdmTypeKind.Entity || edmType.Definition.TypeKind == EdmTypeKind.Complex)
                return _entityTypeSerializer;
            else
                return base.GetEdmTypeSerializer(edmType);
        }
    }
    
  2. Now we need to Inject this into your Container Builder.

    the specifics of this will vary depending on your version of .Net, for many older projects this will be where you are mapping the ODataServiceRoute, this will usually be located in your startup.cs or WebApiConfig.cs

    builder => builder
        .AddService(ServiceLifetime.Singleton, sp => model)
        // Injected our custom serializer to override the current ODataSerializerProvider
        // .AddService<{Type of service to Override}>({service lifetime}, sp => {return your custom implementation})
        .AddService<Microsoft.AspNet.OData.Formatter.Serialization.ODataSerializerProvider>(ServiceLifetime.Singleton, sp => new IngoreNullEntityPropertiesSerializerProvider(sp));
    

And there you have it, re-exeecute your query and you should get the following:

{
 "odata.metadata":"http://localhost:28776/api/$metadata#Values",
 "value":[
   {
     "Id":1,
     "Name":"name1",
     "OptionalField":"Value Present"
   },
   {
     "Id":3,
     "Name":"name2"
   }
  ]
}

This is a very handy solution that can significantly reduce the data consumption on many data entry applications based on OData Services

NOTE: At this point in time, this technique must be used to override any of these default services: (as defined here OData.Net - Dependency Injection Support

Service                     Default Implementation      Lifetime    Prototype?
--------------------------  --------------------------  ----------  ---------
IJsonReaderFactory          DefaultJsonReaderFactory    Singleton   N
IJsonWriterFactory          DefaultJsonWriterFactory    Singleton   N
ODataMediaTypeResolver      ODataMediaTypeResolver      Singleton   N
ODataMessageReaderSettings  ODataMessageReaderSettings  Scoped      Y
ODataMessageWriterSettings  ODataMessageWriterSettings  Scoped      Y
ODataPayloadValueConverter  ODataPayloadValueConverter  Singleton   N
IEdmModel                   EdmCoreModel.Instance       Singleton   N
ODataUriResolver            ODataUriResolver            Singleton   N
UriPathParser               UriPathParser               Scoped      N
ODataSimplifiedOptions      ODataSimplifiedOptions      Scoped      Y

UPDATE: How to handle lists or complex types

Another common scenario is to exclude complex types from the output if all of their properties are null, especially now that we do not include the null properties. We can override the WriteObjectInline method in the IngoreNullEntityPropertiesSerializer for this:

    public override void WriteObjectInline(object graph, IEdmTypeReference expectedType, Microsoft.OData.ODataWriter writer, ODataSerializerContext writeContext)
    {
        if (graph != null)
        {
            // special case, nullable Complex Types, just skip them if there is no value to write
            if (expectedType.IsComplex() && graph.GetType().GetProperty("Instance")?.GetValue(graph) == null
                && (bool?)graph.GetType().GetProperty("UseInstanceForProperties")?.GetValue(graph) == true)
            {
                // skip properties that are null, especially if they are wrapped in generic types or explicitly requested by an expander
            }
            else
            {
                base.WriteObjectInline(graph, expectedType, writer, writeContext);
            }
        }
    }

Q: And if we need to omit null list properties as well?

If you wanted to use the same logic to exclude all lists, if they are null, then you could remove the expectedType.IsComplex() clause:

            // special case, nullable Complex Types, just skip them if there is no value to write
            if (graph.GetType().GetProperty("Instance")?.GetValue(graph) == null
                && (bool?)graph.GetType().GetProperty("UseInstanceForProperties")?.GetValue(graph) == true)
            {
                // skip properties that are null, especially if they are wrapped in generic types or explicitly requested by an expander
            }

I don't recommend this for lists that are navigation properties, navigation properties will only be included in the output if they are explicitly requested in an $expand clause, or by other convention-based logic you may have that does the same thing. An empty or null array in the output might be significant for some client side logic as a confirmation that the requested property data was loaded but that there is no data to return.

like image 148
Chris Schaller Avatar answered Nov 07 '22 20:11

Chris Schaller


I know it does not look anyway logical, but simply adding DefaultODataSerializerProvider and DefaultODataDeserializerProvider to the list of Formatters did the trick for me:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        //... 

        var odataFormatters = System.Web.OData.Formatter.ODataMediaTypeFormatters.Create(
            System.Web.OData.Formatter.Serialization.DefaultODataSerializerProvider.Instance, 
            System.Web.OData.Formatter.Deserialization.DefaultODataDeserializerProvider.Instance);
        config.Formatters.AddRange(odataFormatters);

UPDATE

Since the global formatters modification didn't work correctly for me, I chose a different way. First I stepped away from the ODataController and inherited my controller from ApiController using a custom ODataFormatting attribute:

[ODataRouting]
[CustomODataFormatting]
public class MyController : ApiController
{
    ...
}

public class CustomODataFormattingAttribute : ODataFormattingAttribute
{
    public override IList<System.Web.OData.Formatter.ODataMediaTypeFormatter> CreateODataFormatters()
    {
        return System.Web.OData.Formatter.ODataMediaTypeFormatters.Create(
            new CustomODataSerializerProvider(),
            new System.Web.OData.Formatter.Deserialization.DefaultODataDeserializerProvider());
    }
}

The formatting attribute replaces the DefaultODataSerializerProvider with a modified one:

public class CustomODataSerializerProvider : DefaultODataSerializerProvider
{
    public override ODataEdmTypeSerializer GetEdmTypeSerializer(IEdmTypeReference edmType)
    {
        if(edmType.Definition.TypeKind == EdmTypeKind.Entity)
            return new CustomODataEntityTypeSerializer(this);
        else 
            return base.GetEdmTypeSerializer(edmType);
    }
}

And the last, the custom serializer filters structural properties with null values:

public class CustomODataEntityTypeSerializer : System.Web.OData.Formatter.Serialization.ODataEntityTypeSerializer
{
    public CustomODataEntityTypeSerializer(ODataSerializerProvider provider)
        : base(provider) { }

    public override ODataProperty CreateStructuralProperty(IEdmStructuralProperty structuralProperty, EntityInstanceContext entityInstanceContext)
    {
        var property = base.CreateStructuralProperty(structuralProperty, entityInstanceContext);
        return property.Value != null ? property : null;
    }
}

This doesn't seem to me like the best possible solution, but this is the one I found.

like image 2
Stas Natalenko Avatar answered Nov 07 '22 22:11

Stas Natalenko