OData / WebApi

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

with EnableLowerCamelCase and HasKey only last entityset works #1625

Open BDomzalski opened 6 years ago

BDomzalski commented 6 years ago

Multiple conditions must be met for this issue to occur. OData (model) configuration requires:

With such configuration only the last registered entity set works correctly. Other gives error which is connected to casing of the key property name.

Assemblies affected

Microsoft.AspNet.OData 7.0.1 (and earlier probably)

Reproduce steps

Call an endpoint for other than last registered entity (for the attached solution: http://localhost:52128/AClass).

Expected result

Data returned correctly.

Actual result

An error returned with an exception (most inner message: "The EDM instance of type '[WebApiTest.Models.A Nullable=True]' is missing the property 'code'.") Json file with error result in attachment result error.json.txt.

Additional detail

Attached simple project with setup for this issue to occur. Call the api to get all AClass items (http://localhost:52128/AClass) to get the exception. WebApiTest.zip

seangwright commented 5 years ago

I think I've also run into this issue.

My scenario involves a base class that has the [Key] annotation. My other classes inherit from this class and each have their own OData configuration.

OData configuration

 var builder = new ODataConventionModelBuilder
   {
      ContainerName = "DefaultContainer",
      Namespace = "default"
   }
   .EnableLowerCamelCase(NameResolverOptions.ProcessDataMemberAttributePropertyNames)
   .EnableLowerCamelCase(NameResolverOptions.ProcessReflectedPropertyNames)
   .EnableLowerCamelCase(NameResolverOptions.ProcessExplicitPropertyNames);

Base class

public class BaseEntity
{
   [Key]
   [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
   public Guid Id {get;set;}
}

Role class

public class Role : BaseEntity
{
   public string Name {get;set;}
}

Role OData Configuration

builder
   .EntitySet<Role>("role")
   .EntityType
   .HasKey(u => u.Id);

The error I get when making a request to the endpoint is The EDM instance of type '[default.Role Nullable=True]' is missing the property 'id'.

When I change Role to not inherit from BaseEntity and instead annotate its own Id property with [Key] everything works.

Kizmar commented 5 years ago

In case anyone else ends up here for the reason I did...

I thought I was running into a related issue. After a bunch of flailing around, I realized the OData EDMX doesn't recognize entity properties that have a private set. It took me hitting the document endpoint to figure this out (/odata/$metadata).

davidyee commented 4 years ago

We're noticing this issue too on our DotNet Core 3.1 projects using the latest Microsoft.AspNetCore.OData 7.3.0.

We are specifying HasKey by model configuration which appears to write the correct $metadata with the expected key.

A workaround for us was to add [Key] attribute to the primary key. This approach appears to resolve the issue but we would prefer to be able to configure that by code via HasKey so that we can re-use the classes across other projects.

I haven't checked if this issue occurs with classes following the OData Entity key convention described here but at least with the models we're getting errors on they do not follow the convention so that's why we expect to configure them by code or by the key attribute. Perhaps this issue is specific to OData models that do not conform to the convention?

Trace details / error output:

Our model with issue has a $metadata output that looks like this:

<Schema Namespace="MyProject.Data" xmlns="http://docs.oasis-open.org/odata/ns/edm">
    <EntityType Name="Position">
        <Key>
            <PropertyRef Name="rowUid" />
        </Key>
        <Property Name="rowUid" Type="Edm.Guid" Nullable="false" />
        <Property Name="title" Type="Edm.String" />
        <Property Name="created" Type="Edm.DateTimeOffset" />
        <Property Name="changeToken" Type="Edm.String" />
    </EntityType>
</Schema>

Querying GET on the OData endpoint results in error An unhandled exception was thrown by the application. System.InvalidOperationException: The EDM instance of type '[MyProject.Data.Position Nullable=True]' is missing the property 'rowUid' (where rowUid is the key attribute):

2020-01-16 16:34:00.6604|13|ERROR|Microsoft.AspNetCore.Server.Kestrel|Connection id "0HLSQREUT4RI8", Request id "0HLSQREUT4RI8:00000001": An unhandled exception was thrown by the application. System.InvalidOperationException: The EDM instance of type '[MyProject.Data.Position Nullable=True]' is missing the property 'rowUid'.
   at Microsoft.AspNet.OData.ResourceContext.GetPropertyValue(String propertyName)
   at Microsoft.AspNet.OData.Formatter.Serialization.ODataResourceSerializer.CreateStructuralProperty(IEdmStructuralProperty structuralProperty, ResourceContext resourceContext)
   at Microsoft.AspNet.OData.Formatter.Serialization.ODataResourceSerializer.CreateStructuralPropertyBag(SelectExpandNode selectExpandNode, ResourceContext resourceContext)
   at Microsoft.AspNet.OData.Formatter.Serialization.ODataResourceSerializer.CreateResource(SelectExpandNode selectExpandNode, ResourceContext resourceContext)
   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, Func`2 getODataMessageWrapper, Func`2 getEdmTypeSerializer, Func`2 getODataPayloadSerializer, Func`1 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.<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()
--- End of stack trace from previous location where exception was thrown ---
   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 Microsoft.AspNetCore.Builder.Extensions.MapMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
   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.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)

And adding a $select on one of the attributes returns System.ArgumentNullException: Value cannot be null. (Parameter 'property') error:

2020-01-16 16:29:47.2824|1|ERROR|Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware|An unhandled exception has occurred while executing the request. System.ArgumentNullException: Value cannot be null. (Parameter 'property')
   at System.Linq.Expressions.Expression.Property(Expression expression, PropertyInfo property)
   at Microsoft.AspNet.OData.Query.Expressions.SelectExpandBinder.CreatePropertyValueExpression(IEdmStructuredType elementType, IEdmProperty property, Expression source, FilterClause filterClause)
   at Microsoft.AspNet.OData.Query.Expressions.SelectExpandBinder.BuildPropertyContainer(Expression source, IEdmStructuredType structuredType, IDictionary`2 propertiesToExpand, IDictionary`2 propertiesToInclude, ISet`1 autoSelectedProperties, Boolean isSelectingOpenTypeSegments)
   at Microsoft.AspNet.OData.Query.Expressions.SelectExpandBinder.ProjectElement(Expression source, SelectExpandClause selectExpandClause, IEdmStructuredType structuredType, IEdmNavigationSource navigationSource)
   at Microsoft.AspNet.OData.Query.Expressions.SelectExpandBinder.GetProjectionLambda(SelectExpandQueryOption selectExpandQuery)
   at Microsoft.AspNet.OData.Query.Expressions.SelectExpandBinder.Bind(IQueryable queryable, SelectExpandQueryOption selectExpandQuery)
   at Microsoft.AspNet.OData.Query.Expressions.SelectExpandBinder.Bind(IQueryable queryable, ODataQuerySettings settings, SelectExpandQueryOption selectExpandQuery)
   at Microsoft.AspNet.OData.Query.SelectExpandQueryOption.ApplyTo(IQueryable queryable, ODataQuerySettings settings)
   at Microsoft.AspNet.OData.Query.ODataQueryOptions.ApplySelectExpand[T](T entity, ODataQuerySettings querySettings)
   at Microsoft.AspNet.OData.Query.ODataQueryOptions.ApplyTo(IQueryable query, ODataQuerySettings querySettings)
   at Microsoft.AspNet.OData.EnableQueryAttribute.ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)
   at Microsoft.AspNet.OData.EnableQueryAttribute.ExecuteQuery(Object responseValue, IQueryable singleResultCollection, IWebApiActionDescriptor actionDescriptor, Func`2 modelFunction, IWebApiRequestMessage request, Func`2 createQueryOptionFunction)
   at Microsoft.AspNet.OData.EnableQueryAttribute.OnActionExecuted(Object responseValue, IQueryable singleResultCollection, IWebApiActionDescriptor actionDescriptor, IWebApiRequestMessage request, Func`2 modelFunction, Func`2 createQueryOptionFunction, Action`1 createResponseAction, Action`3 createErrorAction)
   at Microsoft.AspNet.OData.EnableQueryAttribute.OnActionExecuted(ActionExecutedContext actionExecutedContext)
   at Microsoft.AspNetCore.Mvc.Filters.ActionFilterAttribute.OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeInnerFilterAsync>g__Awaited|13_0(ControllerActionInvoker 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 Microsoft.AspNetCore.Builder.Extensions.MapMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.<Invoke>g__Awaited|6_0(ExceptionHandlerMiddleware middleware, HttpContext context, Task task)
LouisMarx127 commented 1 year ago

Still happening in version 8.0.12

jjavierdguezas commented 1 year ago

still happening in version 8.1.1 😕

xuzhg commented 1 year ago

@LouisMarx127 It's related to the 'PropertyInfo' as key in a dictionary. See details at: https://stackoverflow.com/questions/75981014/whats-the-differerence-between-propertyinfos-retrieved-from-different-scenari/75981153?noredirect=1#comment134020079_75981153

As a workaround, you can "explicitly" add the ClrPropertyInfoAnnotation manually for failing types. See here

IEdmEntityType countryType = model.SchemaElements.OfType<IEdmEntityType>().FirstOrDefault(e => e.Name == "Country");
IEdmProperty idProperty = countryType.FindProperty("id");

PropertyInfo idPropertyInfo = typeof(Country).GetProperty("Id");
model.SetAnnotationValue(idProperty, new ClrPropertyInfoAnnotation(idPropertyInfo));

You should do it for all except the last one.

@BDomzalski @davidyee