Webapi: Example of a custom entity serializer and deserializer

Created on 4 Sep 2020  路  9Comments  路  Source: OData/WebApi

I am using OData in a ASP.NET Core 3.1 WebApi project and I need to write a custom JSON serializer/deserializer for a particular entity type (NetTopologySuite.Geometries.Point). I found this microsoft documentation about a custom serializer, yet, as far as I understood, it only adds a simple annotation to the entity serialization, which still remains the OData default. I want instead to skip default serialization and just serialize only two properties of the entity (X and Y). Moreover I also need an example of a custom deserializer.

Can someone help me and point in the right direction ?

For the serializer the method to be changed is surely this, but I do not know how to create an empty ODataEntry (the two properties I would like to serialize can be just 2 annotations ? Probably not).

  public override ODataEntry CreateEntry(SelectExpandNode selectExpandNode, EntityInstanceContext entityInstanceContext)
    {
        ODataEntry entry = base.CreateEntry(selectExpandNode, entityInstanceContext);
        Document document = entityInstanceContext.EntityInstance as Document;
        if (entry != null && document != null)
        {
            // annotate the document with the score.
            entry.InstanceAnnotations.Add(new ODataInstanceAnnotation("org.northwind.search.score", new ODataPrimitiveValue(document.Score)));
        }
        return entry;
    }
question

Most helpful comment

Hi @fededim Thank you for the question. I'll illustrate how you can achieve this using a quick and dirty example.

Given a model like the one below,
```c#
public class Movie
{
public int Id { get; set; }
public string Name { get; set; }
public int YearOfRelease { get; set; }
}

you could create a custom serializer and serialization provider like the ones below to serialize only `Name` and `YearOfRelease` properties
```c#
public class CustomODataSerializerProvider : DefaultODataSerializerProvider
{
    public CustomODataSerializerProvider(IServiceProvider rootContainer)
        : base(rootContainer)
    {
    }

    public override ODataEdmTypeSerializer GetEdmTypeSerializer(IEdmTypeReference edmType)
    {
        if (edmType.Definition.TypeKind == EdmTypeKind.Entity)
            return new CustomODataResourceSerializer(this);
        else
            return base.GetEdmTypeSerializer(edmType);
    }
}

public class CustomODataResourceSerializer : ODataResourceSerializer
{
    public CustomODataResourceSerializer(ODataSerializerProvider serializerProvider)
        : base(serializerProvider)
    {
    }

    public override ODataProperty CreateStructuralProperty(IEdmStructuralProperty structuralProperty, ResourceContext resourceContext)
    {
        if (resourceContext.StructuredType.FullTypeName().Equals(typeof(Movie).FullName) &&
            !new[] { "Name", "YearOfRelease" }.Contains(structuralProperty.Name))
        {
            return null;
        }

        return base.CreateStructuralProperty(structuralProperty, resourceContext);
    }
}

You could go ahead and register your custom serialization provider with the DI container as follows:
c# public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseMvc(routeBuilder => { // ... routeBuilder.MapODataServiceRoute( routeName: "odata", routePrefix: "odata", configureAction: containerBuilder => containerBuilder .AddService(Microsoft.OData.ServiceLifetime.Singleton, typeof(IEdmModel), serviceProvider => /*YOUR EDM MODEL HERE*/) .AddService(Microsoft.OData.ServiceLifetime.Singleton, typeof(IEnumerable<IODataRoutingConvention>), serviceProvider => ODataRoutingConventions.CreateDefaultWithAttributeRouting("odata", routeBuilder)) .AddService(Microsoft.OData.ServiceLifetime.Singleton, typeof(ODataUriResolver), serviceProvider => new StringAsEnumResolver()) .AddService(Microsoft.OData.ServiceLifetime.Singleton, typeof(ODataSerializerProvider), serviceProvider => new CustomODataSerializerProvider(serviceProvider)) ); // ... }); }
The approach to the deserializer wouldn't differ much from the above. Let me know if this helps.

All 9 comments

Hi @fededim Thank you for the question. I'll illustrate how you can achieve this using a quick and dirty example.

Given a model like the one below,
```c#
public class Movie
{
public int Id { get; set; }
public string Name { get; set; }
public int YearOfRelease { get; set; }
}

you could create a custom serializer and serialization provider like the ones below to serialize only `Name` and `YearOfRelease` properties
```c#
public class CustomODataSerializerProvider : DefaultODataSerializerProvider
{
    public CustomODataSerializerProvider(IServiceProvider rootContainer)
        : base(rootContainer)
    {
    }

    public override ODataEdmTypeSerializer GetEdmTypeSerializer(IEdmTypeReference edmType)
    {
        if (edmType.Definition.TypeKind == EdmTypeKind.Entity)
            return new CustomODataResourceSerializer(this);
        else
            return base.GetEdmTypeSerializer(edmType);
    }
}

public class CustomODataResourceSerializer : ODataResourceSerializer
{
    public CustomODataResourceSerializer(ODataSerializerProvider serializerProvider)
        : base(serializerProvider)
    {
    }

    public override ODataProperty CreateStructuralProperty(IEdmStructuralProperty structuralProperty, ResourceContext resourceContext)
    {
        if (resourceContext.StructuredType.FullTypeName().Equals(typeof(Movie).FullName) &&
            !new[] { "Name", "YearOfRelease" }.Contains(structuralProperty.Name))
        {
            return null;
        }

        return base.CreateStructuralProperty(structuralProperty, resourceContext);
    }
}

You could go ahead and register your custom serialization provider with the DI container as follows:
c# public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseMvc(routeBuilder => { // ... routeBuilder.MapODataServiceRoute( routeName: "odata", routePrefix: "odata", configureAction: containerBuilder => containerBuilder .AddService(Microsoft.OData.ServiceLifetime.Singleton, typeof(IEdmModel), serviceProvider => /*YOUR EDM MODEL HERE*/) .AddService(Microsoft.OData.ServiceLifetime.Singleton, typeof(IEnumerable<IODataRoutingConvention>), serviceProvider => ODataRoutingConventions.CreateDefaultWithAttributeRouting("odata", routeBuilder)) .AddService(Microsoft.OData.ServiceLifetime.Singleton, typeof(ODataUriResolver), serviceProvider => new StringAsEnumResolver()) .AddService(Microsoft.OData.ServiceLifetime.Singleton, typeof(ODataSerializerProvider), serviceProvider => new CustomODataSerializerProvider(serviceProvider)) ); // ... }); }
The approach to the deserializer wouldn't differ much from the above. Let me know if this helps.

Ok so in CreateStructuralProperty you basically return null for unwanted properties and the base function for wanted ones. Just to understand this is a "global" serializer, is it possible to define a local serializer which applies only to Movie types (e.g. decorate class with an attribute like JsonConverter) ? I am asking this because if I need to add another serializer for another type (e.g. Actor) I have to perform ifs in CreateStructuralProperty according to the types.

@fededim From the resourceContext, you can extract the CLR types and against the type query for properties decorated with attribute(s) that is/are of interest to you.
E.g., suppose you created a custom attribute SkipAttribute and applied it to the properties you'd wish not to serialize/deserialize:
c# public class MyType { public string Property1 { get; set; } [Skip] public string Property2 { get; set; } public string Property3 { get; set; } [Skip] public string Property4 { get; set; } }
When overriding the CreateStructuralProperty method, you could look out for that Skip attribute to decide what to serialize or not serialize.
If your model is not generated using CLR types, you can use annotations on the properties to make decisions about the same.

@gathogojr I'm back on this issue...On .net 5 I have a problem in registering the OData route because I'm using the new method MapODataRoute , e.g.

 app.UseEndpoints(endpoints =>
                {
                    endpoints.MapControllers();
                    endpoints.EnableDependencyInjection();
                    endpoints.MaxTop(5000).SkipToken().Select().Filter().OrderBy().Expand().Count();
                    endpoints.MapODataRoute("odata", "odata", GetEdmModel(context));
                });

I tried to change it accordingly to what you wrote but I do not know how to map the IODataRoutingConvention (gives me the error CS1503 Argument 2: cannot convert from 'Microsoft.AspNetCore.Routing.IEndpointRouteBuilder' to 'Microsoft.AspNetCore.Routing.IRouteBuilder' )

                app.UseEndpoints(endpoints =>
                {
                    endpoints.MapControllers();
                    endpoints.EnableDependencyInjection();
                    endpoints.MaxTop(5000).SkipToken().Select().Filter().OrderBy().Expand().Count();
                    endpoints.MapODataRoute("odata", "odata",
                        builder =>
                        {
                            builder.AddService(Microsoft.OData.ServiceLifetime.Singleton, typeof(IEdmModel), serviceProvider => GetEdmModel(context));
                            builder.AddService(Microsoft.OData.ServiceLifetime.Singleton, typeof(IEnumerable<IODataRoutingConvention>), serviceProvider => ODataRoutingConventions.CreateDefaultWithAttributeRouting("odata", endpoints));  // error on endpoints parameter
                            builder.AddService(Microsoft.OData.ServiceLifetime.Singleton, typeof(ODataUriResolver), serviceProvider => new StringAsEnumResolver());
                            builder.AddService(Microsoft.OData.ServiceLifetime.Singleton, typeof(ODataSerializerProvider), serviceProvider => new SincroADRODataSerializerProvider(serviceProvider));
                        });

                 }


Can you post also a version based on MapODataRoute ?

If I change the endpoints parameter with serviceProvider, e.g.

builder.AddService(Microsoft.OData.ServiceLifetime.Singleton, typeof(IEnumerable<IODataRoutingConvention>), serviceProvider => ODataRoutingConventions.CreateDefaultWithAttributeRouting("odata", serviceProvider));

the compilation is fine, however OData no longer works, if I try to access the odata/$metadata route this error is returned:

System.InvalidOperationException: No service for type 'Microsoft.AspNet.OData.IPerRouteContainer' has been registered.
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
   at Microsoft.AspNet.OData.Routing.Conventions.AttributeRoutingConvention..ctor(String routeName, IServiceProvider serviceProvider, IODataPathTemplateHandler pathTemplateHandler)
   at Microsoft.AspNet.OData.Routing.Conventions.ODataRoutingConventions.CreateDefaultWithAttributeRouting(String routeName, IServiceProvider serviceProvider)
   at SincroADR_API.Startup.<>c.<Configure>b__10_4(IServiceProvider serviceProvider)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitFactory(FactoryCallSite factoryCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitRootCache(ServiceCallSite singletonCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.DynamicServiceProviderEngine.<>c__DisplayClass1_0.<RealizeService>b__0(ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetServices[T](IServiceProvider provider)
   at Microsoft.AspNet.OData.Extensions.HttpRequestExtensions.GetRoutingConventions(HttpRequest request)
   at Microsoft.AspNet.OData.Routing.ODataActionSelector.SelectCandidates(RouteContext context)
   at Microsoft.AspNet.OData.Extensions.ODataEndpointRouteValueTransformer.TransformAsync(HttpContext httpContext, RouteValueDictionary values)
   at Microsoft.AspNetCore.Mvc.Routing.DynamicControllerEndpointMatcherPolicy.ApplyAsync(HttpContext httpContext, CandidateSet candidates)
   at Microsoft.AspNetCore.Routing.Matching.DfaMatcher.SelectEndpointWithPoliciesAsync(HttpContext httpContext, IEndpointSelectorPolicy[] policies, CandidateSet candidateSet)
   at Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware.<Invoke>g__AwaitMatch|8_1(EndpointRoutingMiddleware middleware, HttpContext httpContext, Task matchTask)
   at Microsoft.AspNetCore.Builder.Extensions.MapWhenMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Builder.Extensions.MapMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

With the standard

endpoints.MapODataRoute("odata", "odata", GetEdmModel(context));

everything works perfectly. Isn't there a simpler method to add a custom serializer ?

@gathogojr Managed to get it working by using endpoints.ServiceProvider as parameter. The custom serializer gets called, I have now to fiddle with it, thanks.

builder.AddService(Microsoft.OData.ServiceLifetime.Singleton, typeof(IEnumerable<IODataRoutingConvention>), serviceProvider => ODataRoutingConventions.CreateDefaultWithAttributeRouting("odata",endpoints.ServiceProvider));

Fiddled a little bit with the serializer, but the solution you proposed was not working...the problem was that the entity of my custom serializer for type NetTopologySuite.Geometries.Point had also complexProperties with inner cycles which made the serialization troublesome...for those interested it is far better to override the method CreateSelectExpandNode removing unwanted properties from there, e.g.

public class PointResourceSerializer : ODataResourceSerializer
{
    String[] serializableProps = new String[] { "X", "Y" };

    public SincroADRPointResourceSerializer(ODataSerializerProvider provider) : base(provider)
    {

    }


    public override SelectExpandNode CreateSelectExpandNode(ResourceContext resourceContext)
    {
        var selectExpandNode = base.CreateSelectExpandNode(resourceContext);

        selectExpandNode.SelectedComplexProperties.Clear();
        selectExpandNode.SelectedComplexTypeProperties.Clear();

        List<IEdmStructuralProperty> propsToRemove = new List<IEdmStructuralProperty>();
        foreach (var n in selectExpandNode.SelectedStructuralProperties)
            if (!serializableProps.Contains(n.Name))
                propsToRemove.Add(n);

        foreach (var p in propsToRemove)
            selectExpandNode.SelectedStructuralProperties.Remove(p);

        return selectExpandNode;
    }

}

Found a better solution which does not even need a custom serializer....remove explicitly unwanted properties from the model builder, e.g.

            var builder = new ODataConventionModelBuilder();

            builder.ComplexType<NetTopologySuite.Geometries.Point>().Ignore(p => p.Coordinates);
            builder.ComplexType<NetTopologySuite.Geometries.Point>().Ignore(p => p.Z);
            builder.ComplexType<NetTopologySuite.Geometries.Point>().Ignore(p => p.M);
            builder.ComplexType<NetTopologySuite.Geometries.Point>().Ignore(p => p.SRID);

@fededim your solution allow to serialize not deserialize as I got exception "No parameterless constructor defined for type 'NetTopologySuite.Geometries.Point' " when sending { "X": 30.12555, "Y": 31.85985 } to the api. any thoughts?

Was this page helpful?
0 / 5 - 0 ratings