WebApi icon indicating copy to clipboard operation
WebApi copied to clipboard

Serialise enum to ordinal value

Open thomas-rose opened this issue 5 years ago • 12 comments

Is it possible to have enumeration values serialised (to Json) as their as the corresponding ordinal values and not their string representations? If so, how can this be accomplished?

I'm using Asp.Net Core 2.2, Entity Framework Core 2.2.2 and Asp.Net Core OData 7.1.

Background:

I have an enumeration:

public enum MyEnumeration  
{
    Value1 = 10,
    Value2 = 20
}

And a model class:

public class MyModel
{
    public MyEnumeration MyProperty { get; set; }
}

When I query OData MyProperty will get serialised into Json with values "Value1" or "Value2". My goal is to get the values 10 and 20 instead.

I've tried to apply [EnumMember(Value = "10")] attributes to the enumeration values but without luck; I still get "Value1" or "Value2". I have also tried to create a custom JsonConverter and apply the [JsonConverter()] attribute to MyProperty; I've also tried to add the custom converter through services.AddJsonOptions(options => options.SerializerSettings.Converters.Add()) but no luck either; in both cases, the custom converter seems to be ignored by OData (or it simply picks StringEnumConverter as a better match for the conversion).

I stumbled upon this:

https://dotnetcoretutorials.com/2018/11/12/override-json-net-serialization-settings-back-to-default/

It explains how to "revert" the enumeration serialiser to the default, but the solution seems to be specific to Entity Framework without OData. I, at least, could not get it to work with OData on top.

One final note; I would very much like to avoid having two properties that represent the same field; i.e. something like:

public class MyClass
{
    public int MyProperty { get; set; }

    [NotMapped]
    public MyEnumeration MySecondaryProperty
    {
        get {
            return (MyEnueration)MyProperty;
        }
        set {
            MyProperty = (int)value;
        }
}

thomas-rose avatar Mar 20 '19 10:03 thomas-rose

OData protocol says to serialize enum using the enum member string, not the enum member value. Would you please share us more about your use cases? And why do you need to serialize the num member value?

raheph avatar Mar 27 '19 20:03 raheph

I'm extending an existing web service with a number of OData enabled endpoints; this service already exposes a number of endpoints that rely solely on Entity Framework. We have a client that currently requests data from the existing endpoints, but will be expanded to also request data from the OData enabled ones.

One of our issues is that Entity Framework provides ordinal values for enumerations when data is serialized; OData does not.

Our client expects integer values for enumerations; the domain models on the client are simply implemented this way. We would like to be able to re-use these models for the OData endpoints as well, without having to change them.

And why do you need to serialize the num member value?

We're just interested in - somehow - getting the ordinal values for enumerations; if that can be achieved by configuring OData, or by using serialization attributes, or whatever, really, that would be great. So it's not really about the member values per se, but trying to find a solution that works for our scenario.

thomas-rose avatar Mar 28 '19 10:03 thomas-rose

As far I checked, theoretically that case could be solved by customizing ODataEnumSerializer: https://github.com/OData/WebApi/blob/12d998c0e60ed9bcf1a45dbdc650a51a3578b763/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataEnumSerializer.cs#L83

It also can be required for deserializer: https://github.com/OData/WebApi/blob/12d998c0e60ed9bcf1a45dbdc650a51a3578b763/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/EnumDeserializationHelpers.cs

https://github.com/OData/WebApi/blob/12d998c0e60ed9bcf1a45dbdc650a51a3578b763/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataEnumDeserializer.cs

TheAifam5 avatar Oct 02 '19 09:10 TheAifam5

Any progress on this?

dariooo512 avatar Sep 24 '20 15:09 dariooo512

@dariooo512 Does suggestion from @TheAifam5 work for you?

xuzhg avatar Sep 25 '20 05:09 xuzhg

Hi, we have developed this nuget package. You can enable support of "int to enum" by doing

app.ApplicationServices.UseIntAsEnumODataUriResolver();

in your Startup class

bongias avatar Oct 07 '20 12:10 bongias

Hi, we have developed this nuget package. You can enable support of "int to enum" by doing

app.ApplicationServices.UseIntAsEnumODataUriResolver();

in your Startup class

Is this package source on GitHub?

red-man avatar Dec 07 '20 15:12 red-man

any news?

public class MyClass { public DataverseAccountRole? AccountRoleCode { get; set; } }

PATCH: Body > "AccountRoleCode": 1 Error PATCH: Body > "AccountRoleCode": "1" Working

aletfa avatar Aug 25 '22 12:08 aletfa

OData protocol says to serialize enum using the enum member string, not the enum member value. Would you please share us more about your use cases? And why do you need to serialize the num member value?

@raheph i cant find this in the protocol specification. can you tell me where to find this specifically?

tulio84z avatar Jul 03 '23 13:07 tulio84z

As far I checked, theoretically that case could be solved by customizing ODataEnumSerializer:

https://github.com/OData/WebApi/blob/12d998c0e60ed9bcf1a45dbdc650a51a3578b763/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataEnumSerializer.cs#L83

It also can be required for deserializer: https://github.com/OData/WebApi/blob/12d998c0e60ed9bcf1a45dbdc650a51a3578b763/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/EnumDeserializationHelpers.cs

https://github.com/OData/WebApi/blob/12d998c0e60ed9bcf1a45dbdc650a51a3578b763/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataEnumDeserializer.cs

In my service every Enum is Numeric. I tried customizing IODataEdmTypeSerializer:

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.OData.Formatter.Serialization;
using Microsoft.OData;
using Microsoft.OData.Edm;

namespace WebApi.Serializers
{
    public class IntegerEnumSerializer : IODataEdmTypeSerializer
    {
        private ODataEnumSerializer _innerSerializer;

        public IntegerEnumSerializer(ODataEnumSerializer innerSerializer)
        {
            _innerSerializer = innerSerializer;
        }

        public ODataPrimitiveValue CreateODataEnumValue(object graph, IEdmEnumTypeReference enumType,
            ODataSerializerContext writeContext)
        {
            if (graph == null)
            {
                return null;
            }

            // Serialize enum value as an integer
            return new ODataPrimitiveValue(Convert.ToInt32(graph));
        }

        public Task WriteObjectAsync(object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext)
        {
            return _innerSerializer.WriteObjectAsync(graph, type, messageWriter, writeContext);
        }

        public ODataPayloadKind ODataPayloadKind => _innerSerializer.ODataPayloadKind;
        public ODataValue CreateODataValue(object graph, IEdmTypeReference expectedType, ODataSerializerContext writeContext)
        {
            if (graph == null)
            {
                return null;
            }
            
            // Serialize enum value as an integer
            return new ODataPrimitiveValue(Convert.ToInt32(graph));
            // return _innerSerializer.CreateODataValue(graph, expectedType, writeContext);
        }

        public Task WriteObjectInlineAsync(object graph, IEdmTypeReference expectedType, ODataWriter writer,
            ODataSerializerContext writeContext)
        {
            return _innerSerializer.WriteObjectInlineAsync(graph, expectedType, writer, writeContext);
        }
    }
}

but i get the following error:

Microsoft.OData.ODataException: A primitive value was specified; however, a value of the non-primitive type 'ModelLayer.IpgTypeEnum' was expected. at Microsoft.OData.ValidationUtils.ValidateIsExpectedPrimitiveType(Object value, IEdmPrimitiveTypeReference valuePrimitiveTypeReference, IEdmTypeReference expectedTypeReference)

ds1371dani avatar Aug 28 '23 12:08 ds1371dani

In case someone is still looking for a solution. You need to do the following steps:

  1. Inherit from DefaultODataSerializerProvider:
public class CustomSerializerProvider : DefaultODataSerializerProvider
{
    public CustomSerializerProvider(IServiceProvider rootContainer)
        : base(rootContainer)
    {
    }

    public override ODataEdmTypeSerializer GetEdmTypeSerializer(Microsoft.OData.Edm.IEdmTypeReference edmType)
    {
        // Override serialization behaviour for enums.
        if (edmType.Definition.TypeKind == EdmTypeKind.Enum)
        {
            return new EnumToIntSerializer();
        }

        var result = base.GetEdmTypeSerializer(edmType);
        return result;
    }
}
  1. Create custom implementation EnumToIntSerializer and create a wrapper for ODataSerializerContext:
internal class EnumToIntSerializer : ODataEdmTypeSerializer
{
    public EnumToIntSerializer() : base(ODataPayloadKind.Property)
    {
    }

    public override void WriteObject(object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext)
    {
        IEdmTypeReference edmType = GetEdmType(graph, type, writeContext);
        messageWriter.WriteProperty(CreateProperty(graph, (IEdmEnumTypeReference)edmType, writeContext.RootElementName, writeContext));
    }

    public override ODataValue CreateODataValue(object graph, IEdmTypeReference expectedType, ODataSerializerContext writeContext)
    {
        return CreateODataEnumValue(graph, (IEdmEnumTypeReference)expectedType, writeContext);
    }

    private ODataProperty CreateProperty(object graph, IEdmEnumTypeReference type, string elementName, ODataSerializerContext context)
    {
        return new ODataProperty
        {
            Name = elementName,
            Value = CreateODataEnumValue(graph, type, context)
        };
    }

    private ODataValue CreateODataEnumValue(object graph, IEdmEnumTypeReference enumType, ODataSerializerContext writeContext)
    {
        if (graph == null)
        {
            return new ODataNullValue();
        }

        long? value = null;

        ClrEnumMemberAnnotation clrEnumMemberAnnotation = GetClrEnumMemberAnnotation(writeContext.Model, enumType.EnumDefinition());
        if (clrEnumMemberAnnotation != null)
        {
            IEdmEnumMember edmEnumMember = clrEnumMemberAnnotation.GetEdmEnumMember((Enum)graph);
            if (edmEnumMember != null)
            {
                value = edmEnumMember.Value.Value;
            }
        }

        if (value == null)
        {
            return new ODataNullValue();
        }

        var result = new ODataPrimitiveValue(value.Value);

        // Remove unnecessary data annotation in response.
        result.TypeAnnotation = new ODataTypeAnnotation();

        return result;
    }

    private ClrEnumMemberAnnotation GetClrEnumMemberAnnotation(IEdmModel edmModel, IEdmEnumType enumType)
    {
        if (edmModel == null)
        {
            throw new ArgumentNullException(nameof(edmModel));
        }

        ClrEnumMemberAnnotation annotationValue = edmModel.GetAnnotationValue<ClrEnumMemberAnnotation>(enumType);
        if (annotationValue != null)
        {
            return annotationValue;
        }

        return null;
    }

    private IEdmTypeReference GetEdmType(object instance, Type type, ODataSerializerContext context)
    {
        var wrapper = new CustomSerializerContextWrapper(context);
        var edmType = wrapper.GetEdmType(instance, type);
        return edmType;
    }
}

CustomSerializerContextWrapper:

public class CustomSerializerContextWrapper
{
    // 'GetEdmType' is internal method, have to call it using reflection.
    private static readonly MethodInfo _getEdmTypeMethod = typeof(ODataSerializerContext).GetMethod("GetEdmType", BindingFlags.Instance | BindingFlags.NonPublic);
    private readonly ODataSerializerContext _context;

    public CustomSerializerContextWrapper(ODataSerializerContext context)
    {
        _context = context;
    }

    public IEdmTypeReference GetEdmType(object instance, Type type)
    {
        var edmType = (IEdmTypeReference)_getEdmTypeMethod.Invoke(_context, new[] { instance, type });

        return edmType;
    }
}
  1. You almost done! After doing these steps you will get error like A primitive value was specified; however, a value of the non-primitive type 'ModelLayer.IpgTypeEnum' was expected.. To solve this error you need to provide mock implementation for ODataPayloadValueConverter:
public class CustomPayloadValueConverter : ODataPayloadValueConverter
{
    // Just mock class =)
}
  1. Final step -> register all your custom implementations in DI container:
app.UseMvc(routes =>
{
    routes.MapVersionedODataRoutes("odata", "api/odata/v{version:apiVersion}", modelBuilder.GetEdmModels(), configure =>
    {
        configure.AddService(Microsoft.OData.ServiceLifetime.Scoped, typeof(ODataSerializerProvider), serviceProvider => new CustomSerializerProvider(serviceProvider));

        // Register mock-converter class to avoid validation errors.
        configure.AddService(Microsoft.OData.ServiceLifetime.Scoped, typeof(ODataPayloadValueConverter), sp => new CustomPayloadValueConverter());
    });
});

Hope this solution works for you.

Dmy1tro avatar Dec 14 '23 13:12 Dmy1tro

I use a simpler method to achieve this by creating a custom ODataResourceSerializer:

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

    public override ODataProperty CreateStructuralProperty(IEdmStructuralProperty structuralProperty, ResourceContext resourceContext)
    {
        if (structuralProperty.Type.IsEnum)
        {
            var propertyValue = resourceContext.GetPropertyValue(structuralProperty.Name);

            int value = (int) propertyValue;
            var result = new ODataPrimitiveValue(value)
            {
                TypeAnnotation = new ODataTypeAnnotation()
            };

            return new ODataProperty()
            {
                Name = structuralProperty.Name,
                Value = result
            };
        }

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

DanielVernall avatar May 16 '24 12:05 DanielVernall