One to many navigation property not working

894 views Asked by At

I am using breezejs v.1.5.4 with OData Web Api controllers (and AngularJS v.1.4.0 if it makes a difference).

I have the following models (simplified):

public partial class Job
{
    ...

    [Required]
    [StringLength(128)]
    [Index("IX_WorkDoneById")]
    [Display(Name = "Work Done By Id")]
    public string WorkDoneById { get; set; }

    [ForeignKey("WorkDoneById")]
    public virtual User WorkDoneBy { get; set; }
}

[DataContract]
public partial class User : IdentityUser
{
    ...

    [Key]
    [StringLength(128)]
    [Display(Name = "Id")]
    [DataMember]
    public override string Id
    {
        get
        {
            return base.Id;
        }
        set
        {
            base.Id = value;
        }
    }

    [InverseProperty("WorkDoneBy")]
    [DataMember]
    public virtual ICollection<Job> Jobs { get; set; }
}

When trying to get a Job information and expanding the WorkDoneBy, it works and I get the user information (i.e. the user is binded to the job). While when I try to get the Jobs associated with the user, I get an empty array. I inspected the network and the Jobs are transmitted with the server response but not attached to the user instance.

My JS query is like this:

var query = new breeze.EntityQuery()
            .from("Users")
            .expand("Jobs")
            .where(new breeze.Predicate("Id", "eq", "Some long Guid"));

Any suggestions ??

Update 1

Also I am using datajs v.1.1.3 and odata service adapter.

Below is the metadata:

{
"metadataVersion": "1.0.5",
"namingConvention": "noChange",
"localQueryComparisonOptions": "caseInsensitiveSQL",
"dataServices": [
    {
        "serviceName": "odata/",
        "adapterName": "odata",
        "uriBuilderName": "odata",
        "hasServerMetadata": true,
        "jsonResultsAdapter": "OData_default",
        "useJsonp": false
    }
],
"structuralTypes": [
    {
        "shortName": "Job", 
        "namespace": "MyApp.Models",
        "autoGeneratedKeyType": "None", 
        "defaultResourceName": "Jobs", 
        "dataProperties": [
            { 
                "name": "JobId", 
                "dataType": "Guid", 
                "isNullable": false, 
                "defaultValue": "00000000-0000-0000-0000-000000000000", 
                "isPartOfKey": true, 
                "validators": [{ "name": "required" }, { "name": "guid" }] 
            }, 
            { 
                "name": "WorkDoneById", 
                "dataType": "String", 
                "isNullable": false, 
                "defaultValue": "", 
                "validators": [{ "name": "required" }, { "name": "string" }] 
            }
        ],
        "navigationProperties": [
            { 
                "name": "WorkDoneBy", 
                "entityTypeName": "User:#MyApp.Models", 
                "isScalar": true, 
                "associationName": "MyApp_Models_Job_WorkDoneBy_MyApp_Models_User_WorkDoneByPartner" 
            }
        ]
    },
    {
        "shortName": "User", 
        "namespace": "MyApp.Models",
        "autoGeneratedKeyType": "None", 
        "defaultResourceName": "Users", 
        "dataProperties": [
            { 
                "name": "Id", 
                "dataType": "String", 
                "isNullable": false, 
                "defaultValue": "", 
                "isPartOfKey": true, 
                "validators": [{ "name": "required" }, { "name": "string" }] 
            }
        ], 
        "navigationProperties": [
            { 
                "name": "Jobs", 
                "entityTypeName": "Job:#MyApp.Models", 
                "isScalar": false, 
                "associationName": "MyApp_Models_User_Jobs_MyApp_Models_Job_JobsPartner" 
            }
        ]
    }
],
"resourceEntityTypeMap":
{
    "Jobs": "Job:#MyApp.Models",
    "Users": "User:#MyApp.Models"
}
}

and this is breeze configuration:

var dataService = new breeze.DataService({
    adapterName: "odata",
    hasServerMetadata: false,  // don't ask the server for metadata 
    serviceName: "odata",
    uriBuilderName: "odata",
});

// create the metadataStore 
var metadataStore = new breeze.MetadataStore();

// initialize the store from the application's metadata variable
metadataStore.importMetadata(Models.metaData);

// Apply additional functions and properties to the models
metadataStore.registerEntityTypeCtor("Job", Models.Job);
metadataStore.registerEntityTypeCtor("User", Models.User);

// Initializes entity manager.
this.entityManager = new breeze.EntityManager(
    { dataService: dataService, metadataStore: metadataStore }
);

Update 2

Metadata generated from the server odata/$metadata:

<edmx:Edmx Version="1.0">
  <edmx:DataServices m:DataServiceVersion="3.0" m:MaxDataServiceVersion="3.0">
    <Schema Namespace="MyApp.Models">
      <EntityType Name="Job">
        <Key>
          <PropertyRef Name="JobId"/>
        </Key>
        <Property Name="JobId" Type="Edm.Guid" Nullable="false"/>
        <Property Name="WorkDoneById" Type="Edm.String" Nullable="false"/>
        <NavigationProperty Name="WorkDoneBy" Relationship="MyApp.Models.MyApp_Models_Job_WorkDoneBy_MyApp_Models_User_WorkDoneByPartner" ToRole="WorkDoneBy" FromRole="WorkDoneByPartner"/>
      </EntityType>
      <EntityType Name="User">
        <Key>
          <PropertyRef Name="Id"/>
        </Key>
        <Property Name="Id" Type="Edm.String" Nullable="false"/>
        <NavigationProperty Name="Jobs" Relationship="MyApp.Models.MyApp_Models_User_Jobs_MyApp_Models_Job_JobsPartner" ToRole="Jobs" FromRole="JobsPartner"/>
      </EntityType>
      <Association Name="MyApp_Models_Job_WorkDoneBy_MyApp_Models_User_WorkDoneByPartner">
        <End Type="MyApp.Models.User" Role="WorkDoneBy" Multiplicity="0..1"/>
        <End Type="MyApp.Models.Job" Role="WorkDoneByPartner" Multiplicity="0..1"/>
      </Association>
      <Association Name="MyApp_Models_User_Jobs_MyApp_Models_Job_JobsPartner">
        <End Type="MyApp.Models.Job" Role="Jobs" Multiplicity="*"/>
        <End Type="MyApp.Models.User" Role="JobsPartner" Multiplicity="0..1"/>
      </Association>
    </Schema>
    <Schema Namespace="Default">
      <EntityContainer Name="Container" m:IsDefaultEntityContainer="true">
        <EntitySet Name="Jobs" EntityType="MyApp.Models.Job"/>
        <EntitySet Name="Users" EntityType="MyApp.Models.User"/>
        <AssociationSet Name="MyApp_Models_Job_WorkDoneBy_MyApp_Models_User_WorkDoneByPartnerSet" Association="MyApp.Models.MyApp_Models_Job_WorkDoneBy_MyApp_Models_User_WorkDoneByPartner">
          <End Role="WorkDoneByPartner" EntitySet="Jobs"/>
          <End Role="WorkDoneBy" EntitySet="Users"/>
        </AssociationSet>
        <AssociationSet Name="MyApp_Models_User_Jobs_MyApp_Models_Job_JobsPartnerSet" Association="MyApp.Models.MyApp_Models_User_Jobs_MyApp_Models_Job_JobsPartner">
          <End Role="JobsPartner" EntitySet="Users"/>
          <End Role="Jobs" EntitySet="Jobs"/>
        </AssociationSet>
      </EntityContainer>
    </Schema>
  </edmx:DataServices>
</edmx:Edmx>
2

There are 2 answers

6
Ward On

According to your configuration code, you appear to be using locally defined metadata

although it's a bit confusing because the metadata object shown earlier in your question is attached to a dataservice that says "hasServerMetadata": true. I don't know that this inner dataservice actually matters.

I'm not positive but I suspect that the problem is your association names; they are different for the two navigation properties:

    // Job
    ...
    "navigationProperties": [
        { 
            "name": "WorkDoneBy", 
            ...
            "associationName": "MyApp_Models_Job_WorkDoneBy_MyApp_Models_User_WorkDoneByPartner" 
        }
    ]

     // User
     ...
    "navigationProperties": [
        { 
            "name": "Jobs", 
             ...
            "associationName": "MyApp_Models_User_Jobs_MyApp_Models_Job_JobsPartner" 
        }
    ]

If you want Breeze to pair up these properties, the associationName must be the same string value. The value itself doesn't matter; just the fact that both property ends of the relationship have the same associationName. That's how Breeze learns that these properties are mated.

Try something nice and short that spells out the underlying relationship ... something like "User_Jobs" or "Job.WorkDoneBy_User.Jobs"

Update #1

It is a mystery how you got those different associationName values.

In a comment I asked that you look at the raw metadata coming from your OData source.

Here is an example from the "ODataBreezejsSample" that gets metadata from an OData v.3 source.

The OData nuget package is "Microsoft.AspNet.WebApi.OData" version="5.2.2". The sample uses the EdmBuilder class as explained in the documentation

There are two types - TodoList and TodoItem - that have a one-to-many relationship. The pertinent raw metadata XML is:

// TodoList
<NavigationProperty Name="TodoItems" Relationship="ODataBreezejsSample.Models.TodoList_TodoItems" ... />
// TodoItem
<NavigationProperty Name="TodoList" Relationship="ODataBreezejsSample.Models.TodoList_TodoItems" ... />

Note they have the same Relationship name: "ODataBreezejsSample.Models.TodoList_TodoItems"

Then I inspect the corresponding navigation properties in the client-side metadata that Breeze produces from this XML. Both properties share the associationName of "TodoList_TodoItems" ... equal to the Relationship name stripped of the namespace.

What kind of OData source (and what OData version) are you querying? Are you using the EdmBuilder class to generate the metadata?

Update #2

So you're using the "Microsoft ASP.NET Web API 2.2 for OData v4.0" v.5.6 nuget package! That means you're using OData v.4.

What a PITA!

This is a huge breaking change from v.5.5.x, the last of the OData v.3 nuget packages.

How could they make that big a leap and re-use the version numbers, especialy the major version digit? It boggles the mind.

To really confuse things, there are now two nuget package names that are just slightly different:

Did you happen to notice the "WebApi" in the middle of the v.3 package name? I didn't at first.

The bad news is that their implementation of v.4 broke everything ... again ... including metadata. And ... again ... they failed to follow the OData spec, especially w/r/t navigation properties in metadata.

Consequently, Breeze does not yet work with Web API OData v.4 metadata ... and there are other problems as well.

We are in the process of working through the issues with the Microsoft OData team. Until then, you're options are to wait for us or go back to OData v.3.

Also critical: the datajs 3rd party client-side JavaScript library that we have all used for OData v.1-3 does not work with OData v.4. Like everyone else, you'll have to switch to the olingo library. It may be that the olingo library plays a constructive role in rationalizing navigation property metadata. I don't know and our expert on the subject is not available at the moment.

Yes ... this is a mess.

1
ashishraaj On

Rather then querying you can use fetchEntityByKey on manager. Which will fetch entity by key.

function getAJobDetail(){
         return manager.fetchEntityByKey(
                          "Jobs", "Some long Guid", true)
                       .then(fetchSucceeded)
                       .fail(queryFailed);
                function fetchSucceeded(data) {
                    var s = data.entity;}
       function queryFailed(error) {

                var msg = '[datacontext.js] Error retrieving data. ' + error.message;
                //logError(msg, error);
                throw error;
                }
}

Note : this method will work only if that your key which is some Guid must be a primary key, else you have to use predicates and compare the fields then query the breeze.