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

Microsoft.AspNetCore.OData: Serialization fails when the entity has 'dynamic' or 'JObject' or 'object' as a complex object in the EDM mapping. #2112

Open VamseeInala opened 4 years ago

VamseeInala commented 4 years ago

I am trying to build a ASPNet Core OData API on entities which has complex properties of type anonymous JSON (i.e. 'dynamic' or 'JObject' or object) and a simple GET API fails while serialization of response from the provider.

Assemblies affected

Microsoft.AspNetCore.OData - 7.3.0

Reproduce steps

  1. Create a entity object with dynamic or JObject property like below: public class SampleEntity { public string id { get; set; } public object payload { get; set; } public IDictionary<string, object> DynamicProperties { get; set; } }
  2. Add the complex property to EDM while creating the EDM var odataBuilder = new ODataConventionModelBuilder(); odataBuilder.EntitySet("SampleEntity"); odataBuilder.EntitySet("SampleEntity").EntityType.ComplexProperty(x => x.payload); return odataBuilder.GetEdmModel();
  3. Use any underlying provider to populate the list of sample entities with 'payload' property filed with a JSON object.
  4. Try a GET route.

Expected result

The response would be returned with the dynamic JSON object as one of the properties. The response is returned as expected with using the ODataController.

Actual result

Exception is being logged in the output and the response is half baked. Below are exception details in case of JObject.

System.Runtime.Serialization.SerializationException: ODataResourceSerializer cannot write an object of type 'Collection(Newtonsoft.Json.Linq.JToken)'. at Microsoft.AspNet.OData.Formatter.Serialization.ODataResourceSerializer.GetResourceType(Object graph, ODataSerializerContext writeContext) at Microsoft.AspNet.OData.Formatter.Serialization.ODataResourceSerializer.WriteResource(Object graph, ODataWriter writer, ODataSerializerContext writeContext, IEdmTypeReference expectedType) at Microsoft.AspNet.OData.Formatter.Serialization.ODataResourceSerializer.WriteObjectInline(Object graph, IEdmTypeReference expectedType, ODataWriter writer, ODataSerializerContext writeContext) at Microsoft.AspNet.OData.Formatter.Serialization.ODataResourceSerializer.WriteComplexAndExpandedNavigationProperty(IEdmProperty edmProperty, SelectItem selectItem, ResourceContext resourceContext, ODataWriter writer) at Microsoft.AspNet.OData.Formatter.Serialization.ODataResourceSerializer.WriteComplexProperties(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer) at Microsoft.AspNet.OData.Formatter.Serialization.ODataResourceSerializer.WriteResource(Object graph, ODataWriter writer, ODataSerializerContext writeContext, IEdmTypeReference expectedType) at Microsoft.AspNet.OData.Formatter.Serialization.ODataResourceSerializer.WriteObjectInline(Object graph, IEdmTypeReference expectedType, ODataWriter writer, ODataSerializerContext writeContext) at Microsoft.AspNet.OData.Formatter.Serialization.ODataResourceSetSerializer.WriteResourceSet(IEnumerable enumerable, IEdmTypeReference resourceSetType, ODataWriter writer, ODataSerializerContext writeContext) at Microsoft.AspNet.OData.Formatter.Serialization.ODataResourceSetSerializer.WriteObjectInline(Object graph, IEdmTypeReference expectedType, ODataWriter writer, ODataSerializerContext writeContext) at Microsoft.AspNet.OData.Formatter.Serialization.ODataResourceSetSerializer.WriteObject(Object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext) at Microsoft.AspNet.OData.Formatter.ODataOutputFormatterHelper.WriteToStream(Type type, Object value, IEdmModel model, ODataVersion version, Uri baseAddress, MediaTypeHeaderValue contentType, IWebApiUrlHelper internaUrlHelper, IWebApiRequestMessage internalRequest, IWebApiHeaders internalRequestHeaders, Func2 getODataMessageWrapper, Func2 getEdmTypeSerializer, Func2 getODataPayloadSerializer, Func1 getODataSerializerContext) at Microsoft.AspNet.OData.Formatter.ODataOutputFormatter.WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) --- End of stack trace from previous location where exception was thrown --- at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.gLogged|21_0(ResourceInvoker invoker, IActionResult result) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.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() --- End of stack trace from previous location where exception was thrown --- at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.gAwaited|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() --- End of stack trace from previous location where exception was thrown --- at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Logged|17_1(ResourceInvoker invoker) at Microsoft.AspNetCore.Builder.RouterMiddleware.Invoke(HttpContext httpContext) at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context) 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()

CaminGui commented 4 years ago

Hi, Not sure it is related but more generally I also have trouble using JObject / JsonDocument in Models.

public class Job
    {
        [Key]
        public int Id { get; set; }
...
        [Required]
        [JsonConverter(typeof(JsonDocumentConverter))]
        public JsonDocument Data { get; set; }

       // Trick to not serialize RootElement at return
       // Without this, having {"prop1": "Val1"} will result in { "rootElement": {"prop1": "Val1"} }
        internal sealed class JsonDocumentConverter
        : JsonConverter<System.Text.Json.JsonDocument>
        {
            public override JsonDocument Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
            {
                return JsonDocument.ParseValue(ref reader);
            }

            public override void Write(Utf8JsonWriter writer, JsonDocument value, JsonSerializerOptions options)
            {
                value.WriteTo(writer);
            }
        }

And I'm not able to query the JsonDocument using path like: https://localhost:4003/odata/dashboards?$filter=Data/Prop1 eq 'Val1'

The result is the same using JObject, it looks for accessible properties, but these types mostly works with lookup methods.

I found a way to display specific Json properties doing this:

public class Job
    {
// ... ADD PART
        // Here is the accessor trick to get specific property value in json
       // But still not able to filter using this
        [NotMapped]
        public string TM
        {
            get { return Data.RootElement.GetProperty("TM").GetString();  }
        }

// Startup.cs EDM model builder to use the new property
builder.StructuralTypes.First(t => t.ClrType == typeof(Job)).AddProperty(typeof(Job).GetProperty("TM"));

So the result looks like: ODataJson

Just to clarify, this class is used as Npgsql entity and here is how I use it following Npgsql Doc:

private IQueryable<Job> FilterJobs(IQueryable<Job> jobsQuery, JobFilter jobFilter)
        {
            return jobsQuery.Where(j => 
                                        // Search in some Data tags
                                        j.Data.RootElement.GetProperty("SN").GetString().Contains(jobFilter.Search) ||
                                        j.Data.RootElement.GetProperty("RO").GetString().Contains(jobFilter.Search) ||
                                        ...

                                    );
        }

Is it something straightforward to do in order to manage such properties or quiet complicated?

jotatoledo commented 3 years ago

any updates regarding this issue? any workarounds?