Skip to content

Commit

Permalink
Support nullable navigation property without navigation binding
Browse files Browse the repository at this point in the history
  • Loading branch information
lewischeng-ms committed Nov 11, 2015
1 parent 2649065 commit 74969d9
Show file tree
Hide file tree
Showing 12 changed files with 395 additions and 16 deletions.
5 changes: 5 additions & 0 deletions src/Microsoft.OData.Core/IODataFeedAndEntryTypeContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ internal interface IODataFeedAndEntryTypeContext
/// </summary>
string NavigationSourceEntityTypeName { get; }

/// <summary>
/// The full type name of the navigation source of the feed or entry.
/// </summary>
string NavigationSourceFullTypeName { get; }

/// <summary>
/// The kind of the navigation source of the feed or entry.
/// </summary>
Expand Down
10 changes: 5 additions & 5 deletions src/Microsoft.OData.Core/ODataContextUriBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ internal sealed class ODataContextUriBuilder
{ ODataPayloadKind.IndividualProperty, ValidateResourcePath },
{ ODataPayloadKind.Collection, ValidateCollectionType },
{ ODataPayloadKind.Property, ValidateType },
{ ODataPayloadKind.Entry, ValidateNavigationPath },
{ ODataPayloadKind.Feed, ValidateNavigationPath },
{ ODataPayloadKind.Entry, ValidateNavigationSource },
{ ODataPayloadKind.Feed, ValidateNavigationSource },
{ ODataPayloadKind.Delta, ValidateDelta },
};

Expand Down Expand Up @@ -213,12 +213,12 @@ private static void ValidateCollectionType(ODataContextUrlInfo contextUrlInfo)
}

/// <summary>
/// Validate NavigationPath for given ODataContextUrlInfo for entry or feed
/// Validate NavigationSource for given ODataContextUrlInfo for entry or feed
/// </summary>
/// <param name="contextUrlInfo">The ODataContextUrlInfo to evaluate on.</param>
private static void ValidateNavigationPath(ODataContextUrlInfo contextUrlInfo)
private static void ValidateNavigationSource(ODataContextUrlInfo contextUrlInfo)
{
if (string.IsNullOrEmpty(contextUrlInfo.NavigationPath))
if (string.IsNullOrEmpty(contextUrlInfo.NavigationSource))
{
throw new ODataException(Strings.ODataContextUriBuilder_NavigationSourceMissingForEntryAndFeed);
}
Expand Down
22 changes: 21 additions & 1 deletion src/Microsoft.OData.Core/ODataContextUrlInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ internal sealed class ODataContextUrlInfo
/// <summary>Whether target is contained</summary>
private bool isContained;

/// <summary>Whether target is unknown entity set</summary>
private bool isUnknownEntitySet;

/// <summary>The navigation soruce for current target</summary>
private string navigationSource;

Expand All @@ -48,6 +51,14 @@ internal string NavigationPath
{
get
{
if (this.isUnknownEntitySet)
{
// If the navigation target is not specified, i.e., UnknownEntitySet,
// the navigation path should be null so that type name will be used
// to build the context Url.
return null;
}

string navigationPath = null;
if (this.isContained && this.odataUri != null && this.odataUri.Path != null)
{
Expand All @@ -64,6 +75,12 @@ internal string NavigationPath
}
}

/// <summary>Name of the navigation source used for building context Url</summary>
internal string NavigationSource
{
get { return this.navigationSource; }
}

/// <summary>ResourcePath used for building context Url</summary>
internal string ResourcePath
{
Expand Down Expand Up @@ -151,6 +168,7 @@ internal static ODataContextUrlInfo Create(IEdmNavigationSource navigationSource
return new ODataContextUrlInfo()
{
isContained = kind == EdmNavigationSourceKind.ContainedEntitySet,
isUnknownEntitySet = kind == EdmNavigationSourceKind.UnknownEntitySet,
navigationSource = navigationSource.Name,
TypeCast = navigationSourceEntityType == expectedEntityTypeName ? null : expectedEntityTypeName,
TypeName = navigationSourceEntityType,
Expand All @@ -173,9 +191,10 @@ internal static ODataContextUrlInfo Create(ODataFeedAndEntryTypeContext typeCont
return new ODataContextUrlInfo()
{
isContained = typeContext.NavigationSourceKind == EdmNavigationSourceKind.ContainedEntitySet,
isUnknownEntitySet = typeContext.NavigationSourceKind == EdmNavigationSourceKind.UnknownEntitySet,
navigationSource = typeContext.NavigationSourceName,
TypeCast = typeContext.NavigationSourceEntityTypeName == typeContext.ExpectedEntityTypeName ? null : typeContext.ExpectedEntityTypeName,
TypeName = typeContext.NavigationSourceEntityTypeName,
TypeName = typeContext.NavigationSourceFullTypeName,
IncludeFragmentItemSelector = isSingle && typeContext.NavigationSourceKind != EdmNavigationSourceKind.Singleton,
odataUri = odataUri
};
Expand All @@ -195,6 +214,7 @@ internal static ODataContextUrlInfo Create(ODataFeedAndEntryTypeContext typeCont
ODataContextUrlInfo contextUriInfo = new ODataContextUrlInfo()
{
isContained = typeContext.NavigationSourceKind == EdmNavigationSourceKind.ContainedEntitySet,
isUnknownEntitySet = typeContext.NavigationSourceKind == EdmNavigationSourceKind.UnknownEntitySet,
navigationSource = typeContext.NavigationSourceName,
TypeCast = typeContext.NavigationSourceEntityTypeName == typeContext.ExpectedEntityTypeName ? null : typeContext.ExpectedEntityTypeName,
TypeName = typeContext.NavigationSourceEntityTypeName,
Expand Down
44 changes: 44 additions & 0 deletions src/Microsoft.OData.Core/ODataFeedAndEntryTypeContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ public virtual string NavigationSourceEntityTypeName
get { return this.ValidateAndReturn(default(string)); }
}

/// <summary>
/// The full type name of the navigation source of the feed or entry.
/// </summary>
public virtual string NavigationSourceFullTypeName
{
get { return this.ValidateAndReturn(default(string)); }
}

/// <summary>
/// The kind of the navigation source of the feed or entry.
/// </summary>
Expand Down Expand Up @@ -174,6 +182,25 @@ public override string NavigationSourceEntityTypeName
get { return this.serializationInfo.NavigationSourceEntityTypeName; }
}

/// <summary>
/// The full type name of the navigation source of the feed or entry.
/// </summary>
public override string NavigationSourceFullTypeName
{
get
{
if (this.IsFromCollection)
{
return EdmLibraryExtensions.GetCollectionTypeName(
this.serializationInfo.NavigationSourceEntityTypeName);
}
else
{
return this.serializationInfo.NavigationSourceEntityTypeName;
}
}
}

/// <summary>
/// The kind of the navigation source of the feed or entry.
/// </summary>
Expand Down Expand Up @@ -299,6 +326,15 @@ internal ODataFeedAndEntryTypeContextWithModel(IEdmNavigationSource navigationSo
}
}

IEdmUnknownEntitySet unknownEntitySet = navigationSource as IEdmUnknownEntitySet;
if (unknownEntitySet != null)
{
if (unknownEntitySet.Type.TypeKind == EdmTypeKind.Collection)
{
this.isFromCollection = true;
}
}

this.navigationSourceName = this.navigationSource.Name;
this.isMediaLinkEntry = this.expectedEntityType.HasStream;
this.lazyUrlConvention = new SimpleLazy<UrlConvention>(() => UrlConvention.ForModel(this.model));
Expand All @@ -320,6 +356,14 @@ public override string NavigationSourceEntityTypeName
get { return this.navigationSourceEntityType.FullName(); }
}

/// <summary>
/// The full type name of the navigation source of the feed or entry.
/// </summary>
public override string NavigationSourceFullTypeName
{
get { return this.navigationSource.Type.FullTypeName(); }
}

/// <summary>
/// The kind of the navigation source of the feed or entry.
/// </summary>
Expand Down
12 changes: 10 additions & 2 deletions src/Microsoft.OData.Core/UriParser/Parsers/ODataPathParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1190,8 +1190,16 @@ private void CreatePropertySegment(ODataPathSegment previous, IEdmProperty prope
var navigationProperty = (IEdmNavigationProperty)property;
IEdmNavigationSource navigationSource = previous.TargetEdmNavigationSource.FindNavigationTarget(navigationProperty);

// If we can't compute the target navigation source, then throw
if (navigationSource is IEdmUnknownEntitySet)
// Relationship between TargetMultiplicity and navigation property:
// 1) EdmMultiplicity.Many <=> collection navigation property
// 2) EdmMultiplicity.ZeroOrOne <=> nullable singleton navigation property
// 3) EdmMultiplicity.One <=> non-nullable singleton navigation property
//
// According to OData Spec CSDL 7.1.3:
// 1) non-nullable singleton navigation property => navigation source required
// 2) the other cases => navigation source optional
if (navigationProperty.TargetMultiplicity() == EdmMultiplicity.One
&& navigationSource is IEdmUnknownEntitySet)
{
// Specifically not throwing ODataUriParserException since it's more an an internal server error
throw new ODataException(ODataErrorStrings.RequestUriProcessor_TargetEntitySetNotFound(property.Name));
Expand Down
8 changes: 8 additions & 0 deletions src/Microsoft.OData.Edm/ExtensionMethods/ExtensionMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2253,6 +2253,14 @@ public static IEdmEntityType EntityType(this IEdmNavigationSource navigationSour
return collectionType.ElementType.Definition as IEdmEntityType;
}

var unknownEntitySet = entitySetBase as IEdmUnknownEntitySet;
if (unknownEntitySet != null)
{
// Handle missing navigation target for nullable
// singleton navigation property.
return unknownEntitySet.Type as IEdmEntityType;
}

return null;
}

Expand Down
8 changes: 8 additions & 0 deletions src/Microsoft.OData.Edm/Library/EdmUnknownEntitySet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ public override IEdmPathExpression Path
get { return this.path ?? (this.path = ComputePath()); }
}

/// <summary>
/// Gets the type of this navigation source.
/// </summary>
public override IEdmType Type
{
get { return this.navigationProperty.Type.Definition; }
}

/// <summary>
/// Finds the entity set that a navigation property targets.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -584,18 +584,31 @@ public void NavigationPropertyOnEntity()
path.NavigationSource().Should().Be(HardCodedTestModel.GetDogsSet());
}

// TODO: everywhere we call FindNavigationTarget() use a helper and throw the right exception
[TestMethod]
public void NavigationPropertyWithMissingEntitySetShouldThrow()
{
var model = ModelBuildingHelpers.GetModelWithNavPropWithNoTargetSet();
var model = ModelBuildingHelpers.GetTestModelForNavigationPropertyBinding();
var path = new ODataUriParser(model, new Uri("http://gobbldygook/"), new Uri("http://gobbldygook/Vegetables(1)/GenesModified")).ParsePath();
Assert.AreEqual(path.LastSegment.Identifier, "GenesModified");
}

// We want a descriptive error message, and do NOT want a ODataUriParserException so the service implementor does not blindly surface this to users
Action parse = () => new ODataUriParser(model, new Uri("http://gobbldygook/"), new Uri("http://gobbldygook/Vegetables(1)/GenesModified")).ParsePath();
parse.ShouldThrow<ODataException>().WithMessage("The target Entity Set of Navigation Property 'GenesModified' could not be found. This is most likely an error in the IEdmModel.").
[TestMethod]
public void SingletonNonNullableNavigationPropertyWithMissingEntitySetShouldThrow()
{
var model = ModelBuildingHelpers.GetTestModelForNavigationPropertyBinding();
Action parse = () => new ODataUriParser(model, new Uri("http://gobbldygook/"), new Uri("http://gobbldygook/Vegetables(1)/KeyGene")).ParsePath();
parse.ShouldThrow<ODataException>().WithMessage("The target Entity Set of Navigation Property 'KeyGene' could not be found. This is most likely an error in the IEdmModel.").
And.GetType().Should().Be<ODataException>();
}

[TestMethod]
public void SingletonNullableNavigationPropertyWithMissingEntitySetShouldNotThrow()
{
var model = ModelBuildingHelpers.GetTestModelForNavigationPropertyBinding();
var path = new ODataUriParser(model, new Uri("http://gobbldygook/"), new Uri("http://gobbldygook/Vegetables(1)/DefectiveGene")).ParsePath();
Assert.AreEqual(path.LastSegment.Identifier, "DefectiveGene");
}

[TestMethod]
public void ActionImportWithNoReturnEntitySet()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,19 @@ public static IEdmType BuildValidComplexType()
return new EdmComplexType("Name.Space", "ComplexType");
}

public static EdmModel GetModelWithNavPropWithNoTargetSet()
public static EdmModel GetTestModelForNavigationPropertyBinding()
{
// Create a model with a Navigation Property with a missing target entity set
// Create a model with three navigation properties:
// 2. "Many" with no target entity set
// 4. "ZeroOrOne" with no target entity set
// 6. "One" with no target entity set
EdmEntityType geneType = new EdmEntityType("Test", "Gene");
EdmEntityType vegetableType = new EdmEntityType("Test", "Vegetable");
IEdmStructuralProperty id = vegetableType.AddStructuralProperty("ID", EdmCoreModel.Instance.GetInt32(false));
vegetableType.AddKeys(id);
vegetableType.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo() {Name = "GenesModified", Target = geneType, TargetMultiplicity = EdmMultiplicity.Many,});
vegetableType.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo { Name = "GenesModified", Target = geneType, TargetMultiplicity = EdmMultiplicity.Many });
vegetableType.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo { Name = "DefectiveGene", Target = geneType, TargetMultiplicity = EdmMultiplicity.ZeroOrOne });
vegetableType.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo { Name = "KeyGene", Target = geneType, TargetMultiplicity = EdmMultiplicity.One });

// Note how we do NOT call AddNavigationTarget(...) to create associations
EdmEntityContainer container = new EdmEntityContainer("Test", "Container");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1197,6 +1197,8 @@ internal class TestFeedAndEntryTypeContext : IODataFeedAndEntryTypeContext

public string NavigationSourceEntityTypeName { get; set; }

public string NavigationSourceFullTypeName { get; set; }

public string ExpectedEntityTypeName { get; set; }

public bool IsMediaLinkEntry { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@
<Compile Include="BinaryValueEncodingTests.cs" />
<Compile Include="ContextUrlWriterReaderTests.cs" />
<Compile Include="Writer\FullPayloadValidateTests.cs" />
<Compile Include="Writer\JsonLight\ODataJsonLightFeedWriterTests.cs" />
<Compile Include="Writer\JsonLight\ODataJsonLightDeltaWriterTests.cs" />
<Compile Include="Writer\JsonLight\ODataJsonLightInheritComplexCollectionWriterTests.cs" />
<Compile Include="Writer\JsonLight\ODataJsonLightODataTypeSerializerTests.cs" />
Expand Down
Loading

0 comments on commit 74969d9

Please sign in to comment.