Skip to content

Commit 7afc44a

Browse files
author
Caitlin Bales (MSFT)
authored
Merge pull request #121 from microsoftgraph/implicit-containment
Implicit containment
2 parents 2ebeb1e + 474f4c0 commit 7afc44a

6 files changed

Lines changed: 200 additions & 9 deletions

File tree

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
using System.Collections.Generic;
2+
using System.IO;
3+
using System.Linq;
4+
using Microsoft.Graph.ODataTemplateWriter.Extensions;
5+
using Microsoft.VisualStudio.TestTools.UnitTesting;
6+
using Vipr.Core;
7+
using Vipr.Core.CodeModel;
8+
using Vipr.Reader.OData.v4;
9+
10+
namespace GraphODataTemplateWriter.Test
11+
{
12+
/// <summary>
13+
/// Test GetServiceCollectionNavigationPropertyForPropertyType method
14+
///
15+
/// We have revised the LINQ statement to support the following OData rules
16+
/// regarding containment:
17+
///
18+
/// 1. If a navigation property specifies "ContainsTarget='true'", it is self-contained.
19+
/// Generate a direct path to the item (ie "parent/child").
20+
/// 2. If a navigation property does not specify ContainsTarget but there is a defined EntitySet
21+
/// of the given type, it is a reference relationship. Generate a reference path to the item (ie "item/$ref").
22+
/// 3. If a navigation property does not have a defined EntitySet but there is a Singleton which has
23+
/// a self-contained reference to the given type, we can make a relationship to the implied EntitySet of
24+
/// the singleton. Generate a reference path to the item (ie "singleton/item/$ref").
25+
/// 4. If none of the above pertain to the navigation property, it should be treated as a metadata error.
26+
/// </summary>
27+
[TestClass]
28+
public class ContainmentTests
29+
{
30+
/// <summary>
31+
/// The object model of the test containment metadata
32+
/// </summary>
33+
public OdcmModel model;
34+
35+
/// <summary>
36+
/// These tests load a test metadata file from the file system using VIPR's OData V4 reader
37+
/// </summary>
38+
[TestInitialize]
39+
public void Initialize()
40+
{
41+
string dir = Directory.GetCurrentDirectory();
42+
dir = dir.Replace("\\bin\\Debug", "");
43+
44+
string edmx = File.ReadAllText(dir + "\\Edmx\\Containment.xml");
45+
OdcmReader reader = new OdcmReader();
46+
47+
model = reader.GenerateOdcmModel(new List<TextFile> { new TextFile("$metadata", edmx) });
48+
}
49+
50+
/// <summary>
51+
/// Test an implicit entity set is found for a given navigation property without
52+
/// containment or an explicit entity set
53+
/// </summary>
54+
[TestMethod]
55+
public void TestImplicitEntitySet()
56+
{
57+
var type = model.GetEntityTypes().Where(t => t.Name == "testEntity").First();
58+
var prop = type.Properties.Where(p => p.Name == "testNav").First();
59+
60+
OdcmProperty result = OdcmModelExtensions.GetServiceCollectionNavigationPropertyForPropertyType(prop);
61+
var singleton = model.GetEntityTypes().Where(t => t.Name == "testSingleton").First();
62+
Assert.AreEqual(singleton.Name, result.Name);
63+
}
64+
65+
/// <summary>
66+
/// Test that null is returned when there is no valid explicit or implicit entity set for a
67+
/// given navigation property
68+
/// </summary>
69+
[TestMethod]
70+
public void TestNoValidEntitySet()
71+
{
72+
var type = model.GetEntityTypes().Where(t => t.Name == "testEntity").First();
73+
var prop = type.Properties.Where(p => p.Name == "testInvalidNav").First(); ;
74+
75+
OdcmProperty result = OdcmModelExtensions.GetServiceCollectionNavigationPropertyForPropertyType(prop);
76+
Assert.IsNull(result);
77+
}
78+
79+
/// <summary>
80+
/// Test that an explicit entity set is returned instead of an implicit entity set when both
81+
/// are present in the metadata
82+
/// </summary>
83+
[TestMethod]
84+
public void TestExplicitEntitySet()
85+
{
86+
var type = model.GetEntityTypes().Where(t => t.Name == "testEntity").First();
87+
var prop = type.Properties.Where(p => p.Name == "testExplicitNav").First();
88+
89+
OdcmProperty result = OdcmModelExtensions.GetServiceCollectionNavigationPropertyForPropertyType(prop);
90+
91+
var entitySet = model.EntityContainer.Properties.Where(t => t.Name == "testTypes").First();
92+
Assert.AreEqual(entitySet.Name, result.Name);
93+
}
94+
}
95+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
2+
<edmx:Edmx xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx" Version="4.0">
3+
<edmx:DataServices>
4+
<Schema Namespace="graph" xmlns="http://docs.oasis-open.org/odata/ns/edm">
5+
<EntityType Name="entity">
6+
<Key>
7+
<PropertyRef Name="id"/>
8+
</Key>
9+
<Property Name="id" Unicode="false" Nullable="false" Type="Edm.String"/>
10+
</EntityType>
11+
<EntityType Name="testType"></EntityType>
12+
<EntityType Name="testType2"></EntityType>
13+
<EntityType Name="testType3"></EntityType>
14+
<EntityType Name="testEntity">
15+
<NavigationProperty Name="testNav" Type="graph.testType"/>
16+
<NavigationProperty Name="testInvalidNav" Type="graph.testType2"/>
17+
<NavigationProperty Name="testExplicitNav" Type="graph.testType3"/>
18+
</EntityType>
19+
<EntityType Name="testSingleton">
20+
<NavigationProperty Name="testSingleNav" Type="graph.testType" ContainsTarget="true" />
21+
</EntityType>
22+
<EntityType Name="testSingleton2">
23+
<NavigationProperty Name="testSingleNav2" Type="graph.testType3" ContainsTarget="true" />
24+
</EntityType>
25+
<EntityContainer Name="GraphService">
26+
<Singleton Name="testSingleton" Type="graph.testSingleton"/>
27+
<Singleton Name="testSingleton2" Type="graph.testSingleton2"/>
28+
<EntitySet Name="testTypes" EntityType="graph.testType3"/>
29+
</EntityContainer>
30+
</Schema>
31+
</edmx:DataServices>
32+
</edmx:Edmx>

GraphODataTemplateWriter.Test/GraphODataTemplateWriter.Test.csproj

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@
3939
<Reference Include="System" />
4040
<Reference Include="System.Core" />
4141
<Reference Include="Vipr.Core">
42-
<HintPath>..\..\vipr\src\Core\Vipr.Core\bin\Debug\Vipr.Core.dll</HintPath>
42+
<HintPath>..\submodules\vipr\src\Core\Vipr.Core\bin\Debug\Vipr.Core.dll</HintPath>
43+
</Reference>
44+
<Reference Include="Vipr.Reader.OData.v4">
45+
<HintPath>..\submodules\vipr\src\Readers\Vipr.Reader.OData.v4\bin\Debug\Vipr.Reader.OData.v4.dll</HintPath>
4346
</Reference>
4447
</ItemGroup>
4548
<ItemGroup>
@@ -55,6 +58,9 @@
5558
<Name>GraphODataTemplateWriter</Name>
5659
</ProjectReference>
5760
</ItemGroup>
61+
<ItemGroup>
62+
<Content Include="Edmx\Containment.xml" />
63+
</ItemGroup>
5864
<Import Project="$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets" Condition="Exists('$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets')" />
5965
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
6066
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">

Templates/Android/generated/BaseEntityCollectionReferenceRequest.java.tt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,24 @@
2424
<#
2525
var navigationProperty = c.AsOdcmProperty().GetServiceCollectionNavigationPropertyForPropertyType();
2626
if (navigationProperty != null) {
27+
28+
//Append the singleton's navigation type to the @odata.id if relevant
29+
var implicitNavigationProperty = string.Empty;
30+
if (navigationProperty.GetType() == typeof(OdcmSingleton))
31+
implicitNavigationProperty = c.AsOdcmProperty().Name + "/";
32+
2733
String prop = c.AsOdcmProperty().GetServiceCollectionNavigationPropertyForPropertyType().Name;
2834
#>
2935
final String requestUrl = getBaseRequest().getRequestUrl().toString();
30-
final ReferenceRequestBody body = new ReferenceRequestBody(getBaseRequest().getClient().getServiceRoot() + "/<#=prop#>/" + new<#=TypeName(c)#>.id);
36+
final ReferenceRequestBody body = new ReferenceRequestBody(getBaseRequest().getClient().getServiceRoot() + "/<#=prop#>/<#=implicitNavigationProperty#>" + new<#=TypeName(c)#>.id);
3137
new <#=TypeWithReferencesRequestBuilder(c)#>(requestUrl, getBaseRequest().getClient(), /* Options */ null)
3238
.buildRequest(getBaseRequest().getOptions())
3339
.post(new<#=TypeName(c)#>, body, callback);
3440
}
3541

3642
public <#=TypeName(c)#> post(final <#=TypeName(c)#> new<#=TypeName(c)#>) throws ClientException {
3743
final String requestUrl = getBaseRequest().getRequestUrl().toString();
38-
final ReferenceRequestBody body = new ReferenceRequestBody(getBaseRequest().getClient().getServiceRoot() + "/<#=prop#>/" + new<#=TypeName(c)#>.id);
44+
final ReferenceRequestBody body = new ReferenceRequestBody(getBaseRequest().getClient().getServiceRoot() + "/<#=prop#>/<#=implicitNavigationProperty#>" + new<#=TypeName(c)#>.id);
3945
return new <#=TypeWithReferencesRequestBuilder(c)#>(requestUrl,getBaseRequest().getClient(), /* Options */ null)
4046
.buildRequest(getBaseRequest().getOptions())
4147
.post(new<#=TypeName(c)#>, body);

Templates/CSharp/Base/CollectionRequest.Base.template.tt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,11 @@ public string GetPostAsyncMethodForReferencesRequest(OdcmProperty odcmProperty)
137137
var serviceNavigationProperty = odcmProperty.GetServiceCollectionNavigationPropertyForPropertyType();
138138
if (serviceNavigationProperty == null)
139139
return string.Empty;
140+
141+
//Append the singleton's navigation type to the @odata.id if relevant
142+
var implicitNavigationProperty = string.Empty;
143+
if (serviceNavigationProperty.GetType() == typeof(OdcmSingleton))
144+
implicitNavigationProperty = "/" + odcmProperty.Name;
140145

141146
var stringBuilder = new StringBuilder();
142147

@@ -178,7 +183,7 @@ public string GetPostAsyncMethodForReferencesRequest(OdcmProperty odcmProperty)
178183
stringBuilder.Append(" }");
179184
stringBuilder.Append(Environment.NewLine);
180185
stringBuilder.Append(Environment.NewLine);
181-
stringBuilder.AppendFormat(" var requestBody = new ReferenceRequestBody {{ ODataId = string.Format(\"{{0}}/{0}/{{1}}\", this.Client.BaseUrl, {1}.Id) }};", serviceNavigationProperty.Name, sanitizedPropertyName);
186+
stringBuilder.AppendFormat(" var requestBody = new ReferenceRequestBody {{ ODataId = string.Format(\"{{0}}/{0}{1}/{{1}}\", this.Client.BaseUrl, {2}.Id) }};", serviceNavigationProperty.Name, implicitNavigationProperty, sanitizedPropertyName);
182187
stringBuilder.Append(Environment.NewLine);
183188
stringBuilder.AppendFormat(" return this.SendAsync(requestBody, cancellationToken);");
184189
stringBuilder.Append(Environment.NewLine);

src/GraphODataTemplateWriter/Extensions/OdcmModelExtensions.cs

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -137,25 +137,72 @@ public static IEnumerable<OdcmMethod> GetMethods(this OdcmModel model)
137137
return model.GetEntityTypes().SelectMany(entityType => entityType.Methods);
138138
}
139139

140+
/// <summary>
141+
/// Get the service collection navigation property for the given property type
142+
///
143+
/// We have revised the LINQ statement to support the following OData rules
144+
/// regarding containment:
145+
///
146+
/// 1. If a navigation property specifies "ContainsTarget='true'", it is self-contained.
147+
/// Generate a direct path to the item (ie "parent/child").
148+
/// 2. If a navigation property does not specify ContainsTarget but there is a defined EntitySet
149+
/// of the given type, it is a reference relationship. Generate a reference path to the item (ie "item/$ref").
150+
/// 3. If a navigation property does not have a defined EntitySet but there is a Singleton which has
151+
/// a self-contained reference to the given type, we can make a relationship to the implied EntitySet of
152+
/// the singleton. Generate a reference path to the item (ie "singleton/item/$ref").
153+
/// 4. If none of the above pertain to the navigation property, it should be treated as a metadata error.
154+
/// </summary>
140155
public static OdcmProperty GetServiceCollectionNavigationPropertyForPropertyType(this OdcmProperty odcmProperty)
141156
{
142157
// Try to find the first collection navigation property for the specified type directly on the service
143-
// class object. Use First() instead of FirstOrDefault() so template generation would fail if not found
144-
// instead of silently continuing. If an entity is used in a reference property a navigation collection
158+
// class object. If an entity is used in a reference property a navigation collection
145159
// on the client for that type is required.
146160
try
147161
{
148-
return odcmProperty
162+
var explicitProperty = odcmProperty
149163
.Class
150164
.Namespace
151165
.Classes
152166
.Where(odcmClass => odcmClass.Kind == OdcmClassKind.Service)
153167
.SelectMany(service => (service as OdcmServiceClass).NavigationProperties())
154168
.Where(property => property.IsCollection && property.Projection.Type.FullName.Equals(odcmProperty.Projection.Type.FullName))
155-
.First();
169+
.FirstOrDefault();
170+
171+
if (explicitProperty != null)
172+
return explicitProperty;
173+
174+
// Check the singletons for a matching implicit EntitySet
175+
else
176+
{
177+
var implicitProperty = odcmProperty
178+
.Class
179+
.Namespace
180+
.Classes
181+
.Where(odcmClass => odcmClass.Kind == OdcmClassKind.Service)
182+
.First()
183+
.Properties
184+
.Where(property => property.GetType() == typeof(OdcmSingleton)) //Get the list of singletons defined by the service
185+
.Where(singleton => singleton
186+
.Type
187+
.AsOdcmClass()
188+
.Properties
189+
//Find navigation properties on the singleton that are self-contained (implicit EntitySets) that match the type
190+
//we are searching for
191+
.Where(prop => prop.ContainsTarget == true && prop.Type.Name == odcmProperty.Type.Name)
192+
.FirstOrDefault() != null
193+
)
194+
.FirstOrDefault();
195+
196+
if (implicitProperty != null)
197+
return implicitProperty;
198+
}
199+
//If we are unable to find a valid EntitySet for the property, treat this
200+
//as an exception so the service has an opportunity to correct this in the metadata
201+
throw new Exception("Found no valid EntitySet for the given property.");
202+
156203
} catch (Exception e)
157204
{
158-
logger.Error("The navigation property \"{0}\" on class \"{1}\" does not specify it is self-contained nor is it defined in an EntitySet", odcmProperty.Name.ToString(), odcmProperty.Class.FullName.ToString());
205+
logger.Error("The navigation property \"{0}\" on class \"{1}\" does not specify it is self-contained nor is it defined in an explicit or implicit EntitySet", odcmProperty.Name.ToString(), odcmProperty.Class.FullName.ToString());
159206
logger.Error(e);
160207
return null;
161208
}

0 commit comments

Comments
 (0)