Navigation property in complex type
Applies To: # OData core lib v7 supported OData Core Lib V7
OData V7.0 started to support addition of navigation property under complex type. Basically navigation under complex are same with navigation under entity for usage, the only differences are:
1. Navigation under complex can have multiple bindings with different path.
2. Complex type does not have id, so the navigation link and association link of navigation under complex need contain the entity id which the complex belongs to.
The page will include the usage of navigation under complex in EDM, Uri parser, and serializer and deserializer.
1. EDM
Define navigation property to complex type
EdmModel model = new EdmModel();
EdmEntityType city = new EdmEntityType("Sample", "City");
EdmStructuralProperty cityId = city.AddStructuralProperty("Name", EdmCoreModel.Instance.GetString(false));
city.AddKeys(cityId);
EdmComplexType complex = new EdmComplexType("Sample", "Address");
complex.AddStructuralProperty("Road", EdmCoreModel.Instance.GetString(false));
EdmNavigationProperty navUnderComplex = complex.AddUnidirectionalNavigation(
new EdmNavigationPropertyInfo()
{
Name = "City",
Target = city,
TargetMultiplicity = EdmMultiplicity.One,
});
model.AddElement(city);
model.AddElement(complex);
Please note that only unidirectional navigation is supported, since navigation property must be entity type, so bidirectional navigation property does not make sense to navigation under complex type.
Add navigation property binding for navigation property under complex
When entity type has complex as property, its corresponding entity set can bind the navigation property under complex to a specified entity set. Since multiple properties can be with same complex type, the navigation property under complex can have multiple bindings with different path.
A valid binding path for navigation under complex is: [ qualifiedEntityTypeName "/" ] *( ( complexProperty / complexColProperty ) "/" [ qualifiedComplexTypeName "/" ] ) navigationProperty
For example:
EdmEntityType person = new EdmEntityType("Sample", "Person");
EdmStructuralProperty entityId = person.AddStructuralProperty("UserName", EdmCoreModel.Instance.GetString(false));
person.AddKeys(entityId);
person.AddStructuralProperty("Address", new EdmComplexTypeReference(complex, false));
person.AddStructuralProperty("Addresses", new EdmCollectionTypeReference(new EdmCollectionType(new EdmComplexTypeReference(complex, false))));
model.AddElement(person);
var entityContainer = new EdmEntityContainer("Sample", "Container");
model.AddElement(entityContainer);
EdmEntitySet people = new EdmEntitySet(entityContainer, "People", person);
EdmEntitySet cities1 = new EdmEntitySet(entityContainer, "Cities1", city);
EdmEntitySet cities2 = new EdmEntitySet(entityContainer, "Cities2", city);
people.AddNavigationTarget(navUnderComplex, cities1, new EdmPathExpression("Address/City"));
people.AddNavigationTarget(navUnderComplex, cities2, new EdmPathExpression("Addresses/City"));
entityContainer.AddElement(people);
entityContainer.AddElement(cities1);
entityContainer.AddElement(cities2);
The navigation property navUnderComplex
is binded to cities1 and cities2 with path "Address/City"
and "Addresses/City"
respectively.
Then the csdl of the model would be like:
<Schema xmlns="https://docs.oasis-open.org/odata/ns/edm" Namespace="Sample">
<EntityType Name="City">
<Key>
<PropertyRef Name="Name"/>
</Key>
<Property Name="Name" Nullable="false" Type="Edm.String"/>
</EntityType>
<ComplexType Name="Address">
<Property Name="Road" Type="Edm.String" Nullable="false" />
<NavigationProperty Name="City" Nullable="false" Type="Sample.City"/>
</ComplexType>
<EntityType Name="Person">
<Key>
<PropertyRef Name="UserName"/>
</Key>
<Property Name="UserName" Nullable="false" Type="Edm.String"/>
<Property Name="Address" Nullable="false" Type="Sample.Address"/>
<Property Name="Addresses" Nullable="false" Type="Collection(Sample.Address)"/>
</EntityType>
<EntityContainer Name="Container">
<EntitySet Name="People" EntityType="Sample.Person">
<NavigationPropertyBinding Target="Cities1" Path="Address/City"/>
<NavigationPropertyBinding Target="Cities2" Path="Addresses/City"/>
</EntitySet>
<EntitySet Name="Cities1" EntityType="Sample.City"/>
<EntitySet Name="Cities2" EntityType="Sample.City"/>
</EntityContainer>
</Schema>
The binding path may need include type cast. For example, if there is a navigation property City2
defined in a complex type UsAddress
which is derived from Address
. If add a binding to City2
, it should be like this:
people.AddNavigationTarget(navUnderDerivedComplex, cities1, new EdmPathExpression("Address/Sample.UsAddress/City2"));
Here we do not include type cast sample to keep the scenario simple.
Find navigation target for navigation property under complex
Since a navigation property can be binded to different paths, the exact binding path must be specified for finding the navigation target. For example:
IEdmNavigationSource navigationTarget = people.FindNavigationTarget(navUnderComplex, new EdmPathExpression("Address/City"));
Cities1
will be returned for this case.
2. Uri
Query
Here lists some sample valid query Uris to access navigation property under complex:
Path
https://host/People('abc')/Address/City
Accessing navigation property under collection of complex is not valid, since item in complex collection does not have a canonical Url. That is to say, https://host/People('abc')/Addresses/City
is not valid, City
under Addresses
can only be accessed through $expand
.
Query option
Different with path, navigation under collection of complex can be accessed directly in expressions of $select and $expand, which means Addresses/City
is supported. Refer ABNF for more details.
$select:
https://host/People('abc')/Address?$select=City
https://host/People?$select=Address/City
https://host/People?$select=Addresses/City
$expand:
https://host/People('abc')/Address?$expand=City
https://host/People?$expand=Address/City
https://host/People?$expand=Addresses/City
$filter:
https://host/People?$filter=Address/City/Name eq 'Shanghai'
https://host/People('abc')/Addresses?$filter=City/Name eq 'Shanghai'
https://host/People?$filter=Addresses/any(a:a/City/Name eq 'Shanghai')
$orderby:
https://host/People?$order=Address/City/Name
Uri parser
There is nothing special if using ODataUriParser
. For ODataQueryOptionParser
, if we need resolve the navigation property under complex to its navigation target in the query option, navigation source that the complex belongs to and the binding path are both needed. If the navigation source or part of binding path is in the path, we need it passed to the constructor of ODataQueryOptionParser
. So there are 2 overloaded constructor added to accept ODataPath
as parameter.
public ODataQueryOptionParser(IEdmModel model, ODataPath odataPath, IDictionary<string, string> queryOptions)
public ODataQueryOptionParser(IEdmModel model, ODataPath odataPath, IDictionary<string, string> queryOptions, IServiceProvider container)
Note: Parameter IServiceProvider is related to Dependency Injection.
Actually we do not recommend to use ODataQueryOptionParser
in this case, ODataUriParser
would be more convenient. Here we still give an example just in case:
// https://host/People('abc')/Address?$expand=City
ODataUriParser uriParser = new ODataUriParser(Model, ServiceRoot, new Uri("https://host/People('abc')/Address"));
ODataPath odataPath = uriParser.ParsePath();
ODataQueryOptionParser optionParser = new ODataQueryOptionParser(Model, odataPath, new Dictionary<string, string> { { "$expand", "City" } });
SelectExpandClause clause = optionParser.ParseSelectAndExpand();
// This can achieve same result.
uriParser = new ODataUriParser(Model, ServiceRoot, new Uri("https://host/People('abc')/Address?$expand=City"));
clause = uriParser.ParseSelectAndExpand();
3. Serializer (Writer)
Basically, the writing process is same with writing navigation under entity.
Let's say we are writing an response of query https://host/People('abc')?$expand=Address/City
.
Sample code:
var uriParser = new ODataUriParser(Model, ServiceRoot, new Uri("https://host/People('abc')?$expand=Address/City"));
var odataUri = uriParser.ParseUri();
settings.ODataUri = odataUri;// Specify the odataUri to ODataMessageWriterSettings, which will be reflected in the context url.
ODataResource res = new ODataResource() { Properties = new[] { new ODataProperty { Name = "UserName", Value = "abc" } } };
ODataNestedResourceInfo nestedComplexInfo = new ODataNestedResourceInfo() { Name = "Address" };
ODataResource nestedComplex = new ODataResource() { Properties = new[] { new ODataProperty { Name = "Road", Value = "def" } } };
ODataNestedResourceInfo nestedResInfo = new ODataNestedResourceInfo() { Name = "City", IsCollection = false };
ODataResource nestednav = new ODataResource() { Properties = new[] { new ODataProperty { Name = "Name", Value = "Shanghai" } } };
// Ignore code to CreateODataResourceWriter.
writer.WriteStart(res);
writer.WriteStart(nestedComplexInfo);
writer.WriteStart(nestedComplex);
writer.WriteStart(nestedResInfo);
writer.WriteStart(nestednav);
writer.WriteEnd(); // End of City
writer.WriteEnd(); // End of City nested info
writer.WriteEnd();// End of complex
writer.WriteEnd();// End of complex info
writer.WriteEnd();// End of entity
Payload:
{
"@odata.context": "https://host/$metadata#People/$entity",
"UserName":"abc",
"Address":
{
"Road":"def",
"City":
{
"Name":"Shanghai"
}
}
}
4. Deserializer (Reader)
Reading process is same with reading an navigation property under entity. For navigation property City
under Address
, it will be read as an ODataNestedResourceInfo
which has navigation URL https://host/People('abc')/Complex/City
and an ODataResource
which has Id https://host/Cities1('Shanghai')
.