OData / OData/AspNetCoreOData

The EDM instance of type '[type Nullable=True]' is missing the property 'id'.

Open
#887 11 comments 1 reaction 1 assignee View on GitHub

@xuzhg is already working on this.

Since Apr 18, 2023.

bug
Dominant language
C#
Stars
505
Forks
186
PR merge metrics
No merged PRs in 30d

Description

Assemblies affected
Microsoft.AspNetCore.OData Version="8.1.1"

Describe the bug
I'm getting:

An unhandled exception has occurred while executing the request.
      System.InvalidOperationException: The EDM instance of type '[ODataError.Domain.Taxonomies.Country Nullable=True]' is missing the property 'id'.
         at Microsoft.AspNetCore.OData.Formatter.ResourceContext.GetPropertyValue(String propertyName)
         at Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.CreateStructuralProperty(IEdmStructuralProperty structuralProperty, ResourceContext resourceContext)
         at Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.CreateStructuralPropertyBag(SelectExpandNode selectExpandNode, ResourceContext resourceContext)
         at Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.CreateResource(SelectExpandNode selectExpandNode, ResourceContext resourceContext)
         at Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.WriteResourceAsync(Object graph, ODataWriter writer, ODataSerializerContext writeContext, IEdmTypeReference expectedType)
         at Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.WriteObjectInlineAsync(Object graph, IEdmTypeReference expectedType, ODataWriter writer, ODataSerializerContext writeContext)
         at Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSetSerializer.WriteResourceSetAsync(IEnumerable enumerable, IEdmTypeReference resourceSetType, ODataWriter writer, ODataSerializerContext writeContext)
         at Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSetSerializer.WriteObjectInlineAsync(Object graph, IEdmTypeReference expectedType, ODataWriter writer, ODataSerializerContext writeContext)
         at Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSetSerializer.WriteObjectAsync(Object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext)
         at Microsoft.AspNetCore.OData.Formatter.ODataOutputFormatterHelper.WriteToStreamAsync(Type type, Object value, IEdmModel model, ODataVersion version, Uri baseAddress, MediaTypeHeaderValue contentType, HttpRequest request, IHeaderDictionary requestHeaders, IODataSerializerProvider serializerProvider)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResultFilterAsync>g__Awaited|30_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 ---
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
         at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
         at Microsoft.AspNetCore.OData.Query.ODataQueryRequestMiddleware.Invoke(HttpContext context)
         at Microsoft.AspNetCore.OData.Routing.ODataRouteDebugMiddleware.Invoke(HttpContext context)
         at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

Reproduce steps

I made a small project where I include the minimum necessary of my code to reproduce it:

  • download ODataError.zip
  • Make a request to [GET] https://localhost:7066/odata/v1/countries

Data Model

public abstract class Entity<TId>
{
    [Key]
    public TId Id { get; set; }

    public override bool Equals(object obj) {...}

    public override int GetHashCode() {...}

    public static bool operator ==(Entity<TId> a, Entity<TId> b) {...}

    public static bool operator !=(Entity<TId> a, Entity<TId> b) {...}
}

public abstract class AuditableEntity<T> : Entity<T>
{
    public AuditableEntity() {}
    public AuditableEntity(T id) {...}
    
    public DateTimeOffset CreatedOn { get; set; }
    public Guid CreatedById { get; set; }
    public string CreatedByDisplayName { get; set; }

    public DateTimeOffset? ModifiedOn { get; set; }
    public Guid? ModifiedById { get; set; }
    public string ModifiedByDisplayName { get; set; }

    public DateTime Timestamp { get; set; }


    public virtual void UpdateCreationMetadata(Guid? userId = null, string displayName = null, DateTimeOffset? createdOn = null) {...}
    public virtual void UpdateModificationMetadata(Guid? userId = null, string displayName = null, DateTimeOffset? updatedOn = null) {...}
}

public class Country : AuditableEntity<int>
{
    [JsonConstructor]
    public Country() {...}
    public Country(int id, string name, string iso2, string iso3, string phoneCode) : base(id) {...}

    public string Name { get; set; }
    public string Iso2 { get; set; }
    public string Iso3 { get; set; }
    public string PhoneCode { get; set; }
}

public class Language : AuditableEntity<int>
{
    [JsonConstructor]
    public Language() {...}
    public Language(int id, string name, string code) : base(id) {...}

    public string Name { get; set; }
    public string Code { get; set; }
}

public class Methodology : AuditableEntity<Guid>
{
    [JsonConstructor]
    public Methodology() {...}
    public Methodology(Guid id, string name) : base(id) {...}

    public string Name { get; set; }
}

EDM (CSDL) Model

<?xml version="1.0" encoding="utf-8"?>
<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
    <edmx:DataServices>
        <Schema Namespace="ODataError.Domain.Taxonomies" xmlns="http://docs.oasis-open.org/odata/ns/edm">
            <EntityType Name="Country">
                <Key>
                    <PropertyRef Name="id" />
                </Key>
                <Property Name="id" Type="Edm.Int32" Nullable="false" />
                <Property Name="name" Type="Edm.String" />
                <Property Name="iso2" Type="Edm.String" />
                <Property Name="iso3" Type="Edm.String" />
                <Property Name="phoneCode" Type="Edm.String" />
                <Property Name="createdOn" Type="Edm.DateTimeOffset" Nullable="false" />
                <Property Name="createdById" Type="Edm.Guid" Nullable="false" />
                <Property Name="createdByDisplayName" Type="Edm.String" />
                <Property Name="modifiedOn" Type="Edm.DateTimeOffset" />
                <Property Name="modifiedById" Type="Edm.Guid" />
                <Property Name="modifiedByDisplayName" Type="Edm.String" />
                <Property Name="timestamp" Type="Edm.DateTimeOffset" Nullable="false" />
            </EntityType>
            <EntityType Name="Language">
                <Key>
                    <PropertyRef Name="id" />
                </Key>
                <Property Name="id" Type="Edm.Int32" Nullable="false" />
                <Property Name="name" Type="Edm.String" />
                <Property Name="code" Type="Edm.String" />
                <Property Name="createdOn" Type="Edm.DateTimeOffset" Nullable="false" />
                <Property Name="createdById" Type="Edm.Guid" Nullable="false" />
                <Property Name="createdByDisplayName" Type="Edm.String" />
                <Property Name="modifiedOn" Type="Edm.DateTimeOffset" />
                <Property Name="modifiedById" Type="Edm.Guid" />
                <Property Name="modifiedByDisplayName" Type="Edm.String" />
                <Property Name="timestamp" Type="Edm.DateTimeOffset" Nullable="false" />
            </EntityType>
            <EntityType Name="Methodology">
                <Key>
                    <PropertyRef Name="id" />
                </Key>
                <Property Name="id" Type="Edm.Guid" Nullable="false" />
                <Property Name="name" Type="Edm.String" />
                <Property Name="createdOn" Type="Edm.DateTimeOffset" Nullable="false" />
                <Property Name="createdById" Type="Edm.Guid" Nullable="false" />
                <Property Name="createdByDisplayName" Type="Edm.String" />
                <Property Name="modifiedOn" Type="Edm.DateTimeOffset" />
                <Property Name="modifiedById" Type="Edm.Guid" />
                <Property Name="modifiedByDisplayName" Type="Edm.String" />
                <Property Name="timestamp" Type="Edm.DateTimeOffset" Nullable="false" />
            </EntityType>
        </Schema>
        <Schema Namespace="Default" xmlns="http://docs.oasis-open.org/odata/ns/edm">
            <EntityContainer Name="Container">
                <EntitySet Name="Countries" EntityType="ODataError.Domain.Taxonomies.Country" />
                <EntitySet Name="Languages" EntityType="ODataError.Domain.Taxonomies.Language" />
                <EntitySet Name="Methodologies" EntityType="ODataError.Domain.Taxonomies.Methodology" />
            </EntityContainer>
        </Schema>
    </edmx:DataServices>
</edmx:Edmx>

model built using:

public static IEdmModel GetODataModel()
{
    // Create the service EDM model.
    var modelBuilder = new ODataConventionModelBuilder()
                            .EnableLowerCamelCase();

    modelBuilder.EntitySet<Country>("Countries");
    modelBuilder.EntityType<Country>().HasKey(x => x.Id);

    modelBuilder.EntitySet<Language>("Languages");
    modelBuilder.EntityType<Language>().HasKey(x => x.Id);

    modelBuilder.EntitySet<Methodology>("Methodologies");
    modelBuilder.EntityType<Methodology>().HasKey(x => x.Id);

    return modelBuilder.GetEdmModel();
}

controller example:

[ApiController]
[Route("api/v1/{controller}")]
[AllowAnonymous]
public class CountriesController : ODataController
{
    List<Country> _countries = new List<Country> { new Country(1, "spain", "sp", "spa", "34"), new Country(2, "usa", "us", "usa", "1") };


    public CountriesController()
    {
    }

    [HttpGet]
    [EnableQuery]
    public async Task<ActionResult<IQueryable<Country>>> Get()
    {
        return Ok(_countries);
    }

    [HttpGet("{key}")]
    public async Task<ActionResult<Country>> Get([FromODataUri] int key)
    {
        var result = _countries.FirstOrDefault(x => x.Id == key);
        if (result is object) return result;
        return NotFound();
    }
}

Request/Response
[GET] https://localhost:7066/odata/v1/countries

      System.InvalidOperationException: The EDM instance of type '[ODataError.Domain.Taxonomies.Country Nullable=True]' is missing the property 'id'.
         at Microsoft.AspNetCore.OData.Formatter.ResourceContext.GetPropertyValue(String propertyName)
         at Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.CreateStructuralProperty(IEdmStructuralProperty structuralProperty, ResourceContext resourceContext)
         at Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.CreateStructuralPropertyBag(SelectExpandNode selectExpandNode, ResourceContext resourceContext)
         at Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.CreateResource(SelectExpandNode selectExpandNode, ResourceContext resourceContext)
         at Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.WriteResourceAsync(Object graph, ODataWriter writer, ODataSerializerContext writeContext, IEdmTypeReference expectedType)
         at Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.WriteObjectInlineAsync(Object graph, IEdmTypeReference expectedType, ODataWriter writer, ODataSerializerContext writeContext)
         at Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSetSerializer.WriteResourceSetAsync(IEnumerable enumerable, IEdmTypeReference resourceSetType, ODataWriter writer, ODataSerializerContext writeContext)
         at Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSetSerializer.WriteObjectInlineAsync(Object graph, IEdmTypeReference expectedType, ODataWriter writer, ODataSerializerContext writeContext)
         at Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSetSerializer.WriteObjectAsync(Object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext)
         at Microsoft.AspNetCore.OData.Formatter.ODataOutputFormatterHelper.WriteToStreamAsync(Type type, Object value, IEdmModel model, ODataVersion version, Uri baseAddress, MediaTypeHeaderValue contentType, HttpRequest request, IHeaderDictionary requestHeaders, IODataSerializerProvider serializerProvider)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResultFilterAsync>g__Awaited|30_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 ---
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
         at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
         at Microsoft.AspNetCore.OData.Query.ODataQueryRequestMiddleware.Invoke(HttpContext context)
         at Microsoft.AspNetCore.OData.Routing.ODataRouteDebugMiddleware.Invoke(HttpContext context)
         at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
         at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
         at Microsoft.AspNetCore.Watch.BrowserRefresh.BrowserRefreshMiddleware.InvokeAsync(HttpContext context)
         at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)

Expected behavior
the odata response (like [GET] https://localhost:7066/odata/v1/languages)

Additional context
First I added Country and it worked, then I added Language and Methodology and it stopped working. Looking at other issues on the internet (like https://github.com/OData/WebApi/issues/1625) I think it is related to the use of .EnableLowerCamelCase() and .HasKey(x => x.Id).

this doesnt work:

public static IEdmModel GetODataModel()
{
    // Create the service EDM model.
    var modelBuilder = new ODataConventionModelBuilder()
                            .EnableLowerCamelCase();

    modelBuilder.EntitySet<Country>("Countries");
    modelBuilder.EntityType<Country>().HasKey(x => x.Id);

    modelBuilder.EntitySet<Language>("Languages");
    modelBuilder.EntityType<Language>().HasKey(x => x.Id);

    modelBuilder.EntitySet<Methodology>("Methodologies");
    modelBuilder.EntityType<Methodology>().HasKey(x => x.Id);

    return modelBuilder.GetEdmModel();
}

this work (but there are entities missing as expected):

public static IEdmModel GetODataModel()
{
    // Create the service EDM model.
    var modelBuilder = new ODataConventionModelBuilder()
                            .EnableLowerCamelCase();

    modelBuilder.EntitySet<Country>("Countries");
    modelBuilder.EntityType<Country>().HasKey(x => x.Id);

    /*modelBuilder.EntitySet<Language>("Languages");
    modelBuilder.EntityType<Language>().HasKey(x => x.Id);

    modelBuilder.EntitySet<Methodology>("Methodologies");
    modelBuilder.EntityType<Methodology>().HasKey(x => x.Id);*/

    return modelBuilder.GetEdmModel();
}

this work (but I like to define here my entities and do not rely on conventions on the model class):

public static IEdmModel GetODataModel()
{
    // Create the service EDM model.
    var modelBuilder = new ODataConventionModelBuilder()
                            .EnableLowerCamelCase();

    modelBuilder.EntitySet<Country>("Countries");
    //modelBuilder.EntityType<Country>().HasKey(x => x.Id);

    modelBuilder.EntitySet<Language>("Languages");
    //modelBuilder.EntityType<Language>().HasKey(x => x.Id);

    modelBuilder.EntitySet<Methodology>("Methodologies");
    //modelBuilder.EntityType<Methodology>().HasKey(x => x.Id);

    return modelBuilder.GetEdmModel();
}

this work (but I need to use lower case):

public static IEdmModel GetODataModel()
{
    // Create the service EDM model.
    var modelBuilder = new ODataConventionModelBuilder();
                           // .EnableLowerCamelCase();

    modelBuilder.EntitySet<Country>("Countries");
    modelBuilder.EntityType<Country>().HasKey(x => x.Id);

    modelBuilder.EntitySet<Language>("Languages");
    modelBuilder.EntityType<Language>().HasKey(x => x.Id);

    modelBuilder.EntitySet<Methodology>("Methodologies");
    modelBuilder.EntityType<Methodology>().HasKey(x => x.Id);

    return modelBuilder.GetEdmModel();
}

[GET] https://localhost:7066/odata/v1/languages and [GET] https://localhost:7066/odata/v1/methodologies work but [GET] https://localhost:7066/odata/v1/countries doesnt

Screenshots

from https://localhost:7066/$odata

image

image

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.