WebApi icon indicating copy to clipboard operation
WebApi copied to clipboard

Serialization of NodaTime Instant fails in ODataController

Open awaitJosh opened this issue 6 years ago • 5 comments

I am using NodaTime as DateTime-API in my AspNetCore OData - webapp. Everything works as expected until serialization. This is my entityBase.

public class DbEntity
{
    public Instant CreateDate { get; set; }
}

Here is my Startup-ConfigureServices Method:

services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
    .AddJsonOptions(options => options.SerializerSettings.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb));
services.AddDbContext<DefaultContext>(
    options => options
        .UseLazyLoadingProxies()
        .UseNpgsql(Configuration.GetConnectionString("someString"), o => o.UseNodaTime()));
services.AddTransient<ICrudEditable, CrudEditable>();
services.AddOData();

I am using Microsoft.AspNetCore.OData 7.0.1 NodaTime 2.4.0 Version together with NodaTime.Serialization.JsonNet 2.0.0 and Newtonsoft.Json 11.0.2.

Actions:

// GET api/values
[HttpGet]
public ActionResult Get ( )
{
    return Ok(new DbEntity { CreateDate = SystemClock.Instance.GetCurrentInstant() });
}

Everything fine so far. It´s an action from the default Values controller (derived from ControllerBase). But if I call below action, where Controller derives from ODataController, CreateDate is empty:

[EnableQuery]
[ODataRoute("entity")]
public IActionResult Get()
{
    return Ok(new DbEntity { CreateDate = SystemClock.Instance.GetCurrentInstant() });
}

No error, no nothing, just empty..

Any ideas..? This is the related Stackoverflow question: https://stackoverflow.com/questions/52403578/serialization-of-nodatime-instant-does-not-work-with-asp-net-core-web-api-and-od

awaitJosh avatar Sep 27 '18 12:09 awaitJosh

Same issue here on integration between OData and NodaTime.

NodaTime types are considered as complex types by OData. So even if you manage to serialize correctly NodaTime types and display them correctly on frontend side, you won't be able to use OData operations like sort, filter , ordering etc. because OData can't appy these operations on a complex type ..

So your problem is just the tip of the iceberg unfortunately ..

Riana

GasyTek avatar Sep 23 '20 08:09 GasyTek

The same for: Microsoft.AspNetCore.OData 7.5.6 Microsoft.OData.Core 7.8.3 NodaTime 3.0.5 NodaTime.Serialization.JsonNet 3.0.0 Newtonsoft.Json 12.0.3

Microsoft.OData.ODataException: The type 'NodaTime.LocalDate' of a resource in an expanded link is not compatible with the element type 'System.Nullable_1OfLocalDate' of the expanded link. Entries in an expanded link must have entity types that are assignable to the element type of the expanded link.
   at Microsoft.OData.WriterValidationUtils.ValidateNestedResource(IEdmStructuredType resourceType, IEdmStructuredType parentNavigationPropertyType)
   at Microsoft.OData.WriterValidator.ValidateResourceInNestedResourceInfo(IEdmStructuredType resourceType, IEdmStructuredType parentNavigationPropertyType)
   at Microsoft.OData.ODataWriterCore.ValidateResourceForResourceSet(ODataResourceBase resource, ResourceBaseScope resourceScope)
   at Microsoft.OData.ODataWriterCore.<>c__DisplayClass121_0.<WriteStartResourceImplementation>b__0()
   at Microsoft.OData.ODataWriterCore.InterceptException(Action action)
   at Microsoft.OData.ODataWriterCore.WriteStartResourceImplementation(ODataResource resource)
   at Microsoft.OData.ODataWriterCore.<>c__DisplayClass49_0.<WriteStartAsync>b__0()
   at Microsoft.OData.TaskUtils.GetTaskForSynchronousOperation(Action synchronousOperation)
--- End of stack trace from previous location where exception was thrown ---
   at Microsoft.AspNet.OData.Formatter.Serialization.ODataResourceSerializer.WriteResourceAsync(Object graph, ODataWriter writer, ODataSerializerContext writeContext, IEdmTypeReference expectedType)
   at Microsoft.AspNet.OData.Formatter.Serialization.ODataResourceSerializer.WriteComplexAndExpandedNavigationPropertyAsync(IEdmProperty edmProperty, SelectItem selectItem, ResourceContext resourceContext, ODataWriter writer)
   at Microsoft.AspNet.OData.Formatter.Serialization.ODataResourceSerializer.WriteComplexPropertiesAsync(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer)
   at Microsoft.AspNet.OData.Formatter.Serialization.ODataResourceSerializer.WriteResourceAsync(Object graph, ODataWriter writer, ODataSerializerContext writeContext, IEdmTypeReference expectedType)
   at Microsoft.AspNet.OData.Formatter.Serialization.ODataResourceSetSerializer.WriteResourceSetAsync(IEnumerable enumerable, IEdmTypeReference resourceSetType, ODataWriter writer, ODataSerializerContext writeContext)
   at Microsoft.AspNet.OData.Formatter.Serialization.ODataResourceSetSerializer.WriteObjectAsync(Object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext)
   at Microsoft.AspNet.OData.Formatter.ODataOutputFormatterHelper.WriteToStreamAsync(Type type, Object value, IEdmModel model, ODataVersion version, Uri baseAddress, MediaTypeHeaderValue contentType, IWebApiUrlHelper internaUrlHelper, IWebApiRequestMessage internalRequest, IWebApiHeaders internalRequestHeaders, Func`2 getODataMessageWrapper, Func`2 getEdmTypeSerializer, Func`2 getODataPayloadSerializer, Func`1 getODataSerializerContext)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor.ExecuteAsyncEnumerable(ActionContext context, ObjectResult result, Object asyncEnumerable, Func`2 reader)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeResultAsync>g__Logged|21_0(ResourceInvoker invoker, IActionResult result)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResultFilterAsync>g__Awaited|29_0[TFilter,TFilterAsync](ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResultExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeResultFilters>g__Awaited|27_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|24_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Builder.RouterMiddleware.Invoke(HttpContext httpContext)
   at XXX
   at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.<Invoke>g__Awaited|6_0(ExceptionHandlerMiddleware middleware, HttpContext context, Task task)
   at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.HandleException(HttpContext context, ExceptionDispatchInfo edi)
   at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.<Invoke>g__Awaited|6_0(ExceptionHandlerMiddleware middleware, HttpContext context, Task task)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Server.IIS.Core.IISHttpContextOfT`1.ProcessRequestAsync()

and the reponse is cut :(

OldShaterhan avatar Mar 22 '21 09:03 OldShaterhan

Please help me

I am facing the same issue the response is cut. I have created a custom serializer, but I am facing a problem with the serializer. This is what I am able to serialize it

{
        "Name": null,
	"UiColor": null,
	"UiTextColor": null,
	"Id": "b4ccca70-7ad6-4913-92c1-16312ac85dec",
	"IsActive": false,
	"IsBin": false,
	"Deleted": false,
        //NodaTime.Instant property LockoutEnd
	"LockoutEnd": {
		Value: "2020-2-20T03:30:20Z"
	}
}

But I want to serialize it something like this

{
        "Name": null,
	"UiColor": null,
	"UiTextColor": null,
	"Id": "b4ccca70-7ad6-4913-92c1-16312ac85dec",
	"IsActive": false,
	"IsBin": false,
	"Deleted": false,
          //NodaTime.Instant property LockoutEnd
	"LockoutEnd": "2020-2-20T03:30:20Z",
}

This is the serializer Class

public class NodaTimeODataTypeSerializer : ODataResourceSerializer
{
        //public NodaTimeODataTypeSerializer() : base(ODataPayloadKind.Property)
        //{

        //}
        public NodaTimeODataTypeSerializer(IODataSerializerProvider serializerProvider) : base( serializerProvider)
        {

        }
        
        public override async Task WriteObjectInlineAsync(object graph, IEdmTypeReference expectedType, ODataWriter writer, ODataSerializerContext writeContext)
        {
            //var refe = writeContext.Model.GetTypeMapper().GetEdmTypeReference(writeContext.Model, graph.GetType());
            var rc = new Microsoft.AspNetCore.OData.Formatter.ResourceContext(writeContext, expectedType.AsStructured(), graph);
            //rc.SerializerContext.Items
            ODataProperty property = new ODataProperty
            {
                Name = "Value",
                Value = graph != null ? ((Instant)graph).ToString(InstantPattern.General.PatternText, null) : null
            };
            ODataResource resource = new ODataResource
            {
                TypeName = graph.GetType().FullName,
                Properties = new[] { property },
            };
            

            //ODataPrimitiveValue primitiveValue = new ODataPrimitiveValue(graph.ToString());

            //ODataDeletedResource deletedResource = new ODataDeletedResource()
            //{
            //    Properties = new[] { property }
            //};
            //await writer.WriteStartAsync(deletedResource);
            //await writer.WriteEndAsync();

            await writer.WriteStartAsync(resource);
            await writer.WriteEndAsync();
            //await Task.Run(() => writer.Write(resource));
            //var tw = await writer.CreateTextWriterAsync();
            //var clock = new ClockService();
            ////writer.Write()
            ////var instant = (NodaTime.Instant)graph;

            ////await tw.WriteAsync(instant.ToString());
            ////IEdmTypeReference edmType = writeContext.GetEdmType(graph, graph.GetType());
            //await base.WriteObjectInlineAsync(graph, expectedType, writer, writeContext);
        }
        public override ODataValue CreateODataValue(object graph, IEdmTypeReference expectedType, ODataSerializerContext writeContext)
        {
            if (!expectedType.IsPrimitive())
            {
                throw new Exception();
            }

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

            return value;
        }
        public virtual ODataProperty CreateStructuralProperty(IEdmStructuralProperty structuralProperty, ResourceContext resourceContext)
        {
            if (structuralProperty == null)
            {
            }
            if (resourceContext == null)
            {
            }

            ODataSerializerContext writeContext = resourceContext.SerializerContext;

            IODataEdmTypeSerializer serializer = SerializerProvider.GetEdmTypeSerializer(structuralProperty.Type);
            if (serializer == null)
            {

            }

            object propertyValue = resourceContext.GetPropertyValue(structuralProperty.Name);

            IEdmTypeReference propertyType = structuralProperty.Type;
            if (propertyValue != null)
            {
                if (!propertyType.IsPrimitive() && !propertyType.IsEnum())
                {
                    IEdmTypeReference actualType = null;
                    if (propertyType != null && propertyType != actualType)
                    {
                        propertyType = actualType;
                    }
                }
            }
            //return null;
            return NodaTimeODataTypeSerializer.CreateProperty(this, propertyValue, propertyType, structuralProperty.Name, writeContext);
        }
        public override async Task WriteObjectAsync(object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext)
        {
            if (messageWriter == null)
            {
                throw new ArgumentNullException(nameof(messageWriter));
            }

            if (writeContext == null)
            {
                throw new ArgumentNullException(nameof(writeContext));
            }

            if (writeContext.RootElementName == null)
            {
                throw new Exception("writeContext" + typeof(ODataSerializerContext).Name);
            }

            IEdmTypeReference edmType = null; //writeContext.GetEdmType(graph, type);
            await messageWriter.WritePropertyAsync(CreateProperty(this, graph, edmType, writeContext.RootElementName, writeContext)).ConfigureAwait(false);
        }

        /// <summary>
        /// Creates an <see cref="ODataProperty"/> with name <paramref name="elementName"/> and value
        /// based on the object represented by <paramref name="graph"/>.
        /// </summary>
        /// <param name="serializer">The <see cref="IODataEdmTypeSerializer"/> writing the property value.</param>
        /// <param name="graph">The object to base the value of the property on.</param>
        /// <param name="expectedType">The expected EDM type of the object represented by <paramref name="graph"/>.</param>
        /// <param name="elementName">The name of the property.</param>
        /// <param name="writeContext">The <see cref="ODataSerializerContext"/>.</param>
        /// <returns>The <see cref="ODataProperty"/> created.</returns>
        public static ODataProperty CreateProperty(IODataEdmTypeSerializer serializer, object graph, IEdmTypeReference expectedType, string elementName,
            ODataSerializerContext writeContext)
        {
            if (serializer is ODataCollectionSerializer collectionSerializer)
            {
                return CreateCollectionProperty(collectionSerializer, graph, expectedType, elementName, writeContext);
            }

            return new ODataProperty
            {
                Name = elementName,
                Value = serializer.CreateODataValue(graph, expectedType, writeContext)
            };
        }

        private static ODataProperty CreateCollectionProperty(ODataCollectionSerializer serializer, object graph, IEdmTypeReference expectedType, string elementName,
            ODataSerializerContext writeContext)
        {
            var property = serializer.CreateODataValue(graph, expectedType, writeContext);
            if (property != null)
            {
                return new ODataProperty
                {
                    Name = elementName,
                    Value = property
                };
            }
            else
            {
                return null;
            }
        }

        //
 }

and this is the provider class

public class NodaTimeODataSerializerProvider : ODataSerializerProvider
 {
        IServiceProvider provider;
        public NodaTimeODataSerializerProvider(IServiceProvider serviceProvider):base(serviceProvider)
        {
            provider = serviceProvider;
        }
                        
        public override IODataEdmTypeSerializer GetEdmTypeSerializer(IEdmTypeReference edmType)
        {
            if(edmType.TypeKind() == EdmTypeKind.Complex &&
                ((EdmComplexType)edmType.Definition).Name.Contains(nameof(NodaTime.Instant)))
            {

                return provider.GetService<NodaTimeODataTypeSerializer>();
            }
            return base.GetEdmTypeSerializer(edmType);
        }

        public override IODataSerializer GetODataPayloadSerializer(Type type, HttpRequest request)
        {
            return base.GetODataPayloadSerializer(type, request);
        }
}

DI

.AddOData(opt => opt.AddRouteComponents("api", GetEdmModel(), ser =>
                {
                    ser.AddSingleton(typeof(IODataSerializerProvider), pro => new NodaTimeODataSerializerProvider(pro));
                    ser.AddSingleton<NodaTimeODataTypeSerializer>();
                })

This is the startup settings and dependency injection

services.AddControllers()
                .AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining<Startup>())
                .AddJsonOptions(options =>
                {
                    options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
                    options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
                })
                //.AddNewtonsoftJson(options =>
                //{
                //    options.SerializerSettings.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
                //    options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
                //})
                //.AddOData(opt => opt.AddRouteComponents("api", GetEdmModel())
                .AddOData(opt => opt.AddRouteComponents("api", GetEdmModel(), ser =>
                {
                    ser.AddSingleton(typeof(IODataSerializerProvider), pro => new NodaTimeODataSerializerProvider(pro));
                    ser.AddSingleton<NodaTimeODataTypeSerializer>();
                    //ser.AddSingleton<ODataPayloadValueConverter, NodaTimeODataPayloadConverter>();
                })
                    .Filter()
                    .Select()
                    .Expand()
                    .Count()
                    .OrderBy()
                    .SkipToken()
                    .SetMaxTop(100));

This is the GetEdmModel() function which I have written in the starup.cs. Please ignore other entities The above json sample is User entity. I haven't listed all the properties

private static IEdmModel GetEdmModel()
{
            ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
            builder.AddEnumType(typeof(IsoDayOfWeek));
            //var ct = builder.AddComplexType(typeof(Interval));
            //ct.IsAbstract= true;
            //ct = builder.AddComplexType(typeof(Interval?));
            //ct.IsAbstract= true;
            builder.AddComplexType(typeof(Interval?));
            builder.AddComplexType(typeof(Interval));
            builder.AddComplexType(typeof(Duration));
            builder.AddComplexType(typeof(DateTimeZone));
            builder.AddComplexType(typeof(AnnualDate));
            builder.AddComplexType(typeof(ZonedDateTime));
            builder.AddComplexType(typeof(OffsetDate));
            builder.AddComplexType(typeof(OffsetTime));
            builder.AddComplexType(typeof(OffsetDateTime));
            builder.AddComplexType(typeof(LocalDate));
            builder.AddComplexType(typeof(LocalTime));
            builder.AddComplexType(typeof(LocalDateTime));
            var model = new EdmModel();
            //sample Odata Query https://localhost:5001/api/User?$select=LoginEmail
            builder.EntitySet<User>(nameof(User));
            builder.EntitySet<Entity>(nameof(Entity));
            builder.EntitySet<UserRole>(nameof(UserRole));
            builder.EntitySet<Role>(nameof(Role));
            builder.EntitySet<AppRole>(nameof(AppRole));
            builder.EntitySet<AppRoleRole>(nameof(AppRoleRole));
            builder.EntitySet<RoleEntity>(nameof(RoleEntity));
            
            return builder.GetEdmModel();
}

sniperwolfpk5 avatar May 10 '22 06:05 sniperwolfpk5

I'm getting same issue here unfortunately. Since this issue is over 4 years old, I will assume it is not going to get addressed. Did anybody find a work around?

enantiomer2000 avatar Jan 11 '23 02:01 enantiomer2000

Would be really nice if we could figure this out. @xuzhg?

robertmclaws avatar Nov 01 '23 20:11 robertmclaws