AspNetCoreOData icon indicating copy to clipboard operation
AspNetCoreOData copied to clipboard

Odata 8 dynamic property received error: The given model does not contain the type 'System.Text.Json.JsonElement'

Open MinhMit opened this issue 3 years ago • 18 comments

Hello,

I'm using OData for my project. And my entity must have a dynamic property.

When I create an entity, its success as expected

But when I try to get entities I received error: The given model does not contain the type 'System.Text.Json.JsonElement'

My entity code: entity

My EDM: edmModel

ServiceCollection: services

Configure: configuration

Controller: code

Data created success: dataSuccess

Error when query Get: error

I using: AspNet 5 Microsoft.AspNetCore.OData - v8.0.2 Npgsql.EntityFrameworkCore.PostgreSQL - v5.0.10

I tried add Microsoft.AspNetCore.Mvc.NewtonsoftJson - v5.0.10 and added AddNewtonsoftJson() after AddOData but not working

How can I do to fix this issue? Thanks for your support

MinhMit avatar Sep 17 '21 17:09 MinhMit

Sorry, add JsonExtensionData still the same issue. It working only without OData.

MinhMit avatar Sep 19 '21 09:09 MinhMit

@MinhMit Does https://www.nuget.org/packages/Microsoft.AspNetCore.OData.NewtonsoftJson/ work?

Besides, the type of dynamic property should be resolved from model if you are using OData.

xuzhg avatar Sep 20 '21 17:09 xuzhg

@MinhMit Does https://www.nuget.org/packages/Microsoft.AspNetCore.OData.NewtonsoftJson/ work?

Besides, the type of dynamic property should be resolved from model if you are using OData.

Yes, I resolved this issue by implement ExtraPropertiesJsonConverter : JsonConverter<Dictionary<string, object>> and used HasConversion in my DbContext.

It working without $filter. But when I used $filter I received another error:

2

Without filter: Untitled

How do I fix this error?

Thanks for your support, @xuzhg

MinhMit avatar Sep 21 '21 04:09 MinhMit

It seems EFCore can't translate the expression. It looks related to the client evaluation. @smitpatel any thoughts?

xuzhg avatar Sep 21 '21 06:09 xuzhg

Hi supporters, Any update on my issue?

MinhMit avatar Sep 23 '21 09:09 MinhMit

Besides, the type of dynamic property should be resolved from model if you are using OData.

Hi @xuzhg,

I tried to switch to Microsoft.AspNetCore.OData.NewtonsoftJson instead of my custom HasConversion. But I still received the same error: The given model does not contain the type 'System.Text.Json.JsonElement'

I added AddODataNewtonsoftJson() after AddOData().

And any update about my translate linq issue?

MinhMit avatar Sep 26 '21 15:09 MinhMit

We cannot translate further operation on JSON if it is configured using value converter since we don't know what converter does. It is black box to us in terms of what is inside. In order to have JSON type in model and translate queries with decomposition, https://github.com/dotnet/efcore/issues/4021 is required.

smitpatel avatar Sep 27 '21 12:09 smitpatel

Hi @smitpatel , Thanks for your reply So can I customize the translation operation on JSON to resolve this issue? Have you an example of it?

MinhMit avatar Sep 27 '21 13:09 MinhMit

If you need a dynamic properties on your entity I would recommend using JsonDocument in your entity for dynamic properties. So it would look like this

public class SomeEntity
{
     public Guid Id {get; set;}
     public System.Text.Json.JsonDocument Properties {get; set; }
}

Npgsql can translate jsonb columns into JsonDocument and do queries based on expressions like

_context.MyEntities.Where(e => e.Properties.RootElemenet.GetProperty("myProp").GetInt32() == 123);

To make it work with OData you have to define provide your implementation of ODataResourceSerializer for JsonDocument (to be able to return your value as a response) and FilterBinder to make filtering and ordering to work. If you do it properly you'll be able to query data like http://your-enpoint/odata/entities?$filter=properties/dynamicProp eq 'val'

FlaviusHouk avatar Oct 07 '21 13:10 FlaviusHouk

@FlaviusHouk thanks for your reply. Have you an example to custom ODataResourceSerializer and FilterBinder?

MinhMit avatar Oct 09 '21 09:10 MinhMit

There should be class registered for route (AddRouteComponent method with System.Action<IServiceCollection> parameter to register services for route) like

serviceCollection.AddTransient<FilterBinder>(sp => new CustomFilterBinder(sp));
serviceCollection.AddSingleton<IODataSerializerProvider, CustomODataSerializerProvider>();

Also I had to specify JsonDocument as an open type for EdmModel

EdmModel model = new();

EdmEntityType yourType= 
    new("Your.Namespace",
            nameof(YourType));

/*All other properties might be added here as well for YourType.*/

EdmComplexType metadataType =
                new("System.Text.Json", nameof(JsonDocument), null, false, /*isOpen*/ true);

EdmComplexTypeReference typeRef =
                new(metadataType,
                        isNullable: true);

yourType.AddStructuralProperty(nameof(YourType.Properties), typeRef);

model.AddElement(yourType);
model.AddElement(metadataType);

The class itself should look like

internal class CustomFilterBinder : FilterBinder
{
    public CustomFilterBinder (IServiceProvider requestContainer) :
            base(requestContainer)
    {}

   public override Expression Bind(QueryNode node)
   {
        if(node.Kind == QueryNodeKind.SingleValueOpenPropertyAccess &&
           node is SingleValueOpenPropertyAccessNode dynNode &&
           dynNode.Source.TypeReference.ShortQualifiedName().Split('.').Last() == nameof(JsonDocument))
        {
             Expression sourceExpr = Bind(propNode.Source);
             Expression rootElementExpr = Expression.Property(sourceExpr, nameof(JsonDocument.RootElement));
             
             Expression jsonProp =
                Expression.Call(rootElementExpr ,
                                typeof(JsonElement).GetMethod(nameof(JsonElement.GetProperty), new[] { typeof(string) }),
                                Expression.Constant(propNode.Name));

             return Expression.Call(jsonProp,
                                    typeof(JsonElement).GetMethod(JsonElement.GetString));
        }

    return base.Bind(node);
   }
}

To get custom Serializer I had to inject Serializer Factory as well (CustomODataSerializerProvider). Implement interface and hold a reference to original ODataSerializerProvider in private field. Only in case your type (GetEdmTypeSerializer method) create your own implementation, for all others call original provider.

As for implementation itself... It is quite big. I suggest you inherit ODataResourceSerializer and override WriteObjectInlineAsync. For all types except JsonDocument call base class implementation. For JsonDocument you'll have to check all properties available in the document and write it into ODataWriter.

FlaviusHouk avatar Oct 11 '21 09:10 FlaviusHouk

Thank you, I'll try that.

MinhMit avatar Oct 11 '21 16:10 MinhMit

@MinhMit - were you able to implement this? If so, could you please share your code/findings? I'm running into this issue at the moment.

NGUYET25483-pki avatar Nov 03 '21 14:11 NGUYET25483-pki

@MinhMit - were you able to implement this? If so, could you please share your code/findings? I'm running into this issue at the moment.

Not yet, I used an clear object for this issue, or client don't filter in dynamic fields. If you are interested, please contact me via Skype at skypeofminh to discuss and finish this issue together.

MinhMit avatar Nov 04 '21 03:11 MinhMit

@NGUYET25483-pki @MinhMit I've implemented it like this, with support for objects as the base only so far.

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

	public override async Task WriteObjectInlineAsync(
		object graph, IEdmTypeReference expectedType, ODataWriter writer, ODataSerializerContext writeContext)
	{
		if (graph is null)
			return;

		if (graph is JsonObject obj)
		{
			await writer.WriteStartAsync(new ODataResource
			{
				Properties = obj.Select((KeyValuePair<string, JsonNode?> node) => new ODataProperty
				{
					Name = node.Key,
					Value = new ODataUntypedValue { RawValue = node.Value?.ToJsonString() },
				}),
			});
			await writer.WriteEndAsync();
		}
		else if (graph is JsonElement element && element.ValueKind is JsonValueKind.Object)
		{
			await writer.WriteStartAsync(new ODataResource
			{
				Properties = element.EnumerateObject().Select((JsonProperty prop) => new ODataProperty
				{
					Name = prop.Name,
					Value = new ODataUntypedValue { RawValue = prop.Value.GetRawText() },
				}),
			});
			await writer.WriteEndAsync();
		}
		else
		{
			await base.WriteObjectInlineAsync(graph, expectedType, writer, writeContext);
		}
	}
}

I'm having issues with implementing the deserialiser though for reading dynamic JSON as well. Anyone have an idea how to do that?

danbluhmhansen avatar Dec 27 '21 13:12 danbluhmhansen

@danbluhmhansen something like this

public class JsonODataResourceDeserializer : ODataResourceDeserializer
{
        public JsonODataResourceDeserializer(IODataDeserializerProvider deserializerProvider) : base(deserializerProvider)
        {
        }

        public override void ApplyStructuralProperty(object resource, ODataProperty structuralProperty, IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext)
        {

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

            else if(structuredType.Definition.FullTypeName() == "System.Text.Json.JsonElement" || 
                            (structuredType.Definition.FullTypeName() == "System.Text.Json.JsonNode" && resource is JsonObject) ||
                            structuredType.Definition.FullTypeName() == "System.Text.Json.JsonObject")
            {
                if (resource == null)
                {
                    throw new ArgumentNullException(nameof(resource));
                }

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

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

                var value = JsonNode.Parse(JsonSerializer.Serialize(structuralProperty.Value));

                JsonObject? obj = resource as JsonObject;
                obj?.Add(structuralProperty.Name, value);
            }
            else
            {
                base.ApplyStructuralProperty(resource, structuralProperty, structuredType, readContext);
            }
        }

        public override object CreateResourceInstance(IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext)
        {
            if (structuredType == null)
            {
                throw new ArgumentNullException(nameof(structuredType));
            }

            if (structuredType.Definition.FullTypeName() != "System.Text.Json.JsonElement")
            {
                return base.CreateResourceInstance(structuredType, readContext);
            }

            return new JsonObject();
        }

        public override object ReadResource(ODataResourceWrapper resourceWrapper, IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext)
        {
            var resource = base.ReadResource(resourceWrapper, structuredType, readContext);

            if (structuredType.Definition.FullTypeName() == "System.Text.Json.JsonElement")
            {
                return JsonSerializer.Deserialize<JsonElement>((JsonObject)resource); 
            }

            return resource;
        }
    }

solbirn avatar Mar 16 '22 18:03 solbirn

@solbirn I can't get that to work with a model like this:

public class Foo
{
    public JsonObject Bar { get; set; }
}

danbluhmhansen avatar Mar 17 '22 17:03 danbluhmhansen

I did a bit research about this problem and leave my partial solution here, this supports JsonDocument as Edm Property type with some limitations:

  • serializer: only work if root element is object
  • deserializer: cannot support primitive arrays
using System.ComponentModel;
using Microsoft.AspNetCore.OData.Formatter.Serialization;
using System.Text.Json;
using Microsoft.AspNetCore.OData.Formatter.Deserialization;
using Microsoft.AspNetCore.OData.Formatter.Wrapper;
using Microsoft.OData;
using Microsoft.OData.Edm;

public class CustomODataSerializerProvider : ODataSerializerProvider
{
    private readonly JsonObjectODataResourceSerializer _jsonObjectODataResourceSerializer;

    public CustomODataSerializerProvider(IServiceProvider serviceProvider) : base(serviceProvider)
    {
        _jsonObjectODataResourceSerializer = new JsonObjectODataResourceSerializer(this);
    }

    public override IODataEdmTypeSerializer GetEdmTypeSerializer(IEdmTypeReference edmType)
    {
        if (edmType.FullName() == typeof(JsonDocument).FullName)
        {
            return _jsonObjectODataResourceSerializer;
        }

        return base.GetEdmTypeSerializer(edmType);
    }
}

public class CustomODataDeserializerProvider : ODataDeserializerProvider
{
    private readonly JsonODataResourceDeserializer _jsonODataResourceDeserializer;

    public CustomODataDeserializerProvider(IServiceProvider serviceProvider) : base(serviceProvider)
    {
        _jsonODataResourceDeserializer = new JsonODataResourceDeserializer();
    }

    public override IODataEdmTypeDeserializer GetEdmTypeDeserializer(IEdmTypeReference edmType, bool isDelta = false)
    {
        if (edmType.FullName() == typeof(JsonDocument).FullName)
        {
            return _jsonODataResourceDeserializer;
        }

        return base.GetEdmTypeDeserializer(edmType, isDelta);
    }
}

// only work if root element is object
public class JsonObjectODataResourceSerializer : ODataResourceSerializer
{
    public JsonObjectODataResourceSerializer(IODataSerializerProvider serializerProvider) : base(serializerProvider)
    {
    }

    public override async Task WriteObjectInlineAsync(
        object? graph, IEdmTypeReference expectedType, ODataWriter writer, ODataSerializerContext writeContext)
    {
        switch (graph)
        {
            case null:
                return;
            case JsonDocument { RootElement.ValueKind: JsonValueKind.Object } doc:
                await writer.WriteStartAsync(new ODataResource
                {
                    Properties = doc.RootElement.EnumerateObject().Select(property => new ODataProperty
                    {
                        Name = property.Name,
                        Value = new ODataUntypedValue { RawValue = property.Value.GetRawText() },
                    })
                });
                await writer.WriteEndAsync();
                break;
            case JsonDocument { RootElement.ValueKind: JsonValueKind.Array } doc:
                await writer.WriteStartAsync(new ODataResourceSet());

                foreach (var jsonElement in doc.RootElement.EnumerateArray())
                {
                    switch (jsonElement.ValueKind)
                    {
                        case JsonValueKind.Object:
                            await writer.WriteStartAsync(new ODataResource
                            {
                                Properties = jsonElement.EnumerateObject().Select(property => new ODataProperty
                                {
                                    Name = property.Name,
                                    Value = new ODataUntypedValue { RawValue = property.Value.GetRawText() },
                                })
                            });
                            await writer.WriteEndAsync();
                            break;
                        case JsonValueKind.Undefined:
                        case JsonValueKind.Array:
                        case JsonValueKind.String:
                        case JsonValueKind.Number:
                        case JsonValueKind.True:
                        case JsonValueKind.False:
                        case JsonValueKind.Null:
                        default:
                            break;
                    }
                }

                await writer.WriteEndAsync();
                break;
            default:
                await base.WriteObjectInlineAsync(graph, expectedType, writer, writeContext);
                break;
        }
    }
}

// support all except primitive arrays
public class JsonODataResourceDeserializer : IODataEdmTypeDeserializer
{
    public ODataPayloadKind ODataPayloadKind => ODataPayloadKind.Resource;

    public object ReadInline(object item, IEdmTypeReference edmType, ODataDeserializerContext readContext)
    {
        switch (item)
        {
            case ODataItemWrapper wrapper:
                ReadODataItemWrapper(wrapper); // this should return a JsonNode/JsonObject
                return JsonSerializer.Deserialize<JsonDocument>("{\"a\": 1}")!;
            default:
                throw new InvalidEnumArgumentException(nameof(item));
        }
    }

    private static void ReadODataNestedResourceInfoWrapper(ODataNestedResourceInfoWrapper nestedResourceInfoWrapper)
    {
        Console.WriteLine($"Name: {nestedResourceInfoWrapper.NestedResourceInfo.Name}");
        foreach (var nestedItem in nestedResourceInfoWrapper.NestedItems)
        {
            ReadODataItemWrapper(nestedItem);
        }
    }

    private static void ReadODataResourceSetWrapper(ODataResourceSetWrapper resourceSetWrapper)
    {
        foreach (var resource in resourceSetWrapper.Resources)
        {
            ReadODataResourceWrapper(resource);
        }
    }

    private static void ReadODataResourceBase(ODataResourceBase oDataResource)
    {
        foreach (var oDataProperty in oDataResource.Properties)
        {
            Console.WriteLine($"{oDataProperty.Name} -> {oDataProperty.Value}");
        }
    }

    private static void ReadODataResourceWrapper(ODataResourceWrapper resourceWrapper)
    {
        switch (resourceWrapper.Resource)
        {
            case { } resource:
                ReadODataResourceBase(resource);
                break;
            default:
                Console.WriteLine($"unknown resource {resourceWrapper.Resource.GetType()}");
                break;
        }

        resourceWrapper.NestedResourceInfos.ToList().ForEach(ReadODataNestedResourceInfoWrapper);
    }

    private static void ReadODataItemWrapper(ODataItemWrapper itemWrapper)
    {
        switch (itemWrapper)
        {
            case ODataResourceWrapper resourceWrapper:
                ReadODataResourceWrapper(resourceWrapper);
                break;
            case ODataResourceSetWrapper resourceSetWrapper:
                ReadODataResourceSetWrapper(resourceSetWrapper);
                break;
            case ODataNestedResourceInfoWrapper nestedResourceInfoWrapper:
                ReadODataNestedResourceInfoWrapper(nestedResourceInfoWrapper);
                break;
            default:
                Console.WriteLine($"unknown type {itemWrapper.GetType()}");
                break;
        }
    }

    public Task<object> ReadAsync(ODataMessageReader messageReader, Type type, ODataDeserializerContext readContext)
    {
        throw new NotImplementedException();
    }
}

model:

public class Event : IDisposable
{
    public Guid Id { get; set; }

    public JsonDocument Data { get; set; } = null!;

    public void Dispose()
    {
        Data.Dispose();
    }
}

usage:

    let config (s: IServiceCollection) =
        s.AddSingleton<IODataSerializerProvider, CustomODataSerializerProvider>()
        s.AddSingleton<IODataDeserializerProvider, CustomODataDeserializerProvider>()
        ()

    builder.AddOData
        (fun option ->
            option
                .AddRouteComponents("api/odata", getEdmModel (), config)
                .EnableQueryFeatures(100)
            ())

sep2 avatar Mar 28 '22 03:03 sep2