Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Serialise enum to ordinal value #1785

Open
thomas-rose opened this issue Mar 20, 2019 · 12 comments
Open

Serialise enum to ordinal value #1785

thomas-rose opened this issue Mar 20, 2019 · 12 comments

Comments

@thomas-rose
Copy link

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;
        }
}
@raheph
Copy link
Contributor

raheph commented Mar 27, 2019

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?

@thomas-rose
Copy link
Author

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.

@TheAifam5
Copy link

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

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

@dariooo512
Copy link

Any progress on this?

@xuzhg
Copy link
Member

xuzhg commented Sep 25, 2020

@dariooo512 Does suggestion from @TheAifam5 work for you?

@bongias
Copy link

bongias commented Oct 7, 2020

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

app.ApplicationServices.UseIntAsEnumODataUriResolver();

in your Startup class

@red-man
Copy link

red-man commented Dec 7, 2020

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?

@aletfa
Copy link

aletfa commented Aug 25, 2022

any news?

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

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

@tulio84z
Copy link

tulio84z commented Jul 3, 2023

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?

@ds1371dani
Copy link

ds1371dani commented Aug 28, 2023

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

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)

@Dmy1tro
Copy link

Dmy1tro commented Dec 14, 2023

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.

@DanielVernall
Copy link

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);
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests