OData / WebApi

OData Web API: A server library built upon ODataLib and WebApi
https://docs.microsoft.com/odata
Other
853 stars 476 forks source link

Serialise enum to ordinal value #1785

Open thomas-rose opened 5 years ago

thomas-rose commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 4 years ago

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

dariooo512 commented 3 years ago

Any progress on this?

xuzhg commented 3 years ago

@dariooo512 Does suggestion from @TheAifam5 work for you?

bongias commented 3 years ago

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 commented 3 years ago

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 commented 2 years ago

any news?

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

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

tulio84z commented 1 year ago

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 commented 1 year ago

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)

Dmy1tro commented 9 months ago

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 =)
    }
  2. 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 commented 4 months ago

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