AspNetCoreOData
AspNetCoreOData copied to clipboard
Odata 8 dynamic property received error: The given model does not contain the type 'System.Text.Json.JsonElement'
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:
My EDM:
ServiceCollection:
Configure:
Controller:
Data created success:
Error when query Get:
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
Sorry, add JsonExtensionData still the same issue. It working only without OData.
@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.
@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:
Without filter:
How do I fix this error?
Thanks for your support, @xuzhg
It seems EFCore can't translate the expression. It looks related to the client evaluation. @smitpatel any thoughts?
Hi supporters, Any update on my issue?
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?
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.
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?
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 thanks for your reply. Have you an example to custom ODataResourceSerializer and FilterBinder?
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.
Thank you, I'll try that.
@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.
@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.
@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 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 I can't get that to work with a model like this:
public class Foo
{
public JsonObject Bar { get; set; }
}
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)
())