OData / odata.net

ODataLib: Open Data Protocol - .NET Libraries and Frameworks
https://docs.microsoft.com/odata
Other
686 stars 349 forks source link

How to do nested $expand? #2211

Open danleydmello opened 2 years ago

danleydmello commented 2 years ago

I am trying to query an entity using nested expand to get the related entities. e.g queryContainer.Persons.Expand("Address($expand=Location)").ExecuteAsync(); These are cardinality one relationships.

I get the results as expected from the Odata service but the Odata client gives error:

Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100] Unhandled exception rendering component: The Id cannot be computed, since the navigation source 'Location' cannot be resolved to a known entity set from model. Microsoft.OData.ODataException: The Id cannot be computed, since the navigation source 'Asset' cannot be resolved to a known entity set from model. at Microsoft.OData.Evaluation.ODataConventionalIdMetadataBuilder.ComputeAndCacheId() at Microsoft.OData.Evaluation.ODataConventionalIdMetadataBuilder.get_ComputedId() at Microsoft.OData.Evaluation.ODataConventionalIdMetadataBuilder.GetId() at Microsoft.OData.ODataResourceBase.get_Id() at Microsoft.OData.Client.Materialization.MaterializerEntry.UpdateEntityDescriptor() at Microsoft.OData.Client.Materialization.FeedAndEntryMaterializerAdapter.ReadEntryCore()

I know the Location is in the model because if I try this query, odata client works: e.g queryContainer.Address.Expand("Location").ExecuteAsync();

danleydmello commented 2 years ago

It works when this.MergeOption = MergeOption.NoTracking; This is okay for me as my application is Readonly and I don't need tracking on.

KenitoInc commented 2 years ago

I have looked at this issue

Model

public class Book
{
    public int Id { get; set; }
    public string Isbn { get; set; }
    public string Title { get; set; }
    public ICollection<Author> Authors { get; set; }
}

public class Author
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ICollection<Award> Awards { get; set; }
}

public class Award
{
    public int Id { get; set; }
    public string Name { get; set; }
}

OData Client Code

string uri = "";
Uri serviceUri = new Uri(uri);
Container dsc = new Container(serviceUri);
var books = await dsc.Books.Expand("Authors($expand=Awards)").ExecuteAsync();
foreach (Book book in books)
{
    Console.WriteLine($"Book: {book.Title}");
    foreach (Author author in book.Authors)
    {
        Console.WriteLine($"\tAuthor: {author.Name}");
        foreach (Award award in author.Awards)
        {
            Console.WriteLine($"\t\t Award: {award.Name}");
        }
    }
}

I do not need to set the MergeOption.NoTracking

Below is my output. FYI am using an In-Memory data store. nestedexpand t

KenitoInc commented 2 years ago

@danleydmello Are you using DataServiceCollection?

danleydmello commented 2 years ago

@KenitoInc , In my case, the references are cardinality one , not sure if it matters

public class ActionPlan
    {
        public Guid UniversalID { get; set; }

        public MapFailureMode FailureMode { get; set; }
    }

public class MapFailureMode
    {
        public Guid UniversalID { get; set; }            
        public Asset Asset { get; set; }
    }

 public class Asset
    {
        public Guid UniversalID { get; set; }
        public string AssetNumber { get; set; }
        public string Title { get; set; }
    }

QueryContainer Definition, I followed microsoft documentation [https://docs.microsoft.com/en-us/odata/client/using-blazor-wasm-with-odata-client]:

public class QueryContainer : DataServiceContext
    {
        private readonly string _authHeader;

        public QueryContainer(Uri baseUri, string authHeader, IEdmModel edmModel) :base(baseUri)
        {
            _authHeader = authHeader;
            Format.LoadServiceModel = () => edmModel;
            HttpRequestTransportMode = HttpRequestTransportMode.HttpClient;
            Format.UseJson();
            MergeOption = MergeOption.NoTracking;
            ActionPlans = base.CreateQuery<ActionPlan>("MNT/MaintenanceActionPlans");
            SendingRequest2 += QueryContainer_SendingRequest2;
        }

        private void QueryContainer_SendingRequest2(object? sender, SendingRequest2EventArgs e)
        {
            e.RequestMessage.SetHeader("Authorization", _authHeader);
        }

        public DataServiceQuery<ActionPlan> ActionPlans { get; }
    }
habex-ch commented 1 year ago

Any news on this issue? I really don't like adding MergeOption.NoTracking;

TCSmith05 commented 1 month ago

@habex-ch I've posted a solution to this in StackOverflow previously: https://stackoverflow.com/questions/60188656/the-id-cannot-be-computed-since-the-navigation-source-values-cannot-be-resolv/66592953#66592953

The problem (in the 3 cases I have had) was that the OData service didn't have a key defined for the entity in the error. If you can update the OData service, you can specify the key in the implementation of the IModelConfiguration

So for an EntityType named "Foo" with an Id field "id":

` namespace MyProject.Api.Configuration.Model_Configurations {

public class ValueModelConfiguration : IModelConfiguration
{

    public void Apply(ODataModelBuilder builder, ApiVersion apiVersion)
    {

        builder.EntitySet<Foo>(nameof(Foo)).EntityType.HasKey(v => v.id);

        // other configurations for your entity may be here

    }

}

} `

So for OP's original exception where "Location" is the EntityType, you would replace "Foo" with "Location".

Build the OData service, update your Connected Service in your project to use this new version, and the problem should be resolved for that particular EntityType. Hope that helps.