Skip to content

Commit

Permalink
updated odata.context computation to remove trailing cast segments th… (
Browse files Browse the repository at this point in the history
#2681)

* updated odata.context computation to remove trailing cast segments that are following by key segments as well as the case where the standard key syntax is used within a cast segment

* Removed use of Predicate delegate

* Update ReadOnlyCollectionExtensions.cs

* fixed public api tests

* actually fix public api tests?

* add unit tests for the new trim extension

* added comment

* adding back the odataurislim variant for computing context url

* moving findlastindex from spatial to edm

* adding test for findlastindex

* fix publicapi tests

* public api tests

* asdf
  • Loading branch information
corranrogue9 authored Nov 17, 2023
1 parent f5bc755 commit f9a9405
Show file tree
Hide file tree
Showing 19 changed files with 442 additions and 7 deletions.
6 changes: 2 additions & 4 deletions src/Microsoft.OData.Core/ODataContextUrlInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ private static string ComputeNavigationPath(EdmNavigationSourceKind kind, ODataU
string navigationPath = null;
if (kind == EdmNavigationSourceKind.ContainedEntitySet && odataUri != null && odataUri.Path != null)
{
ODataPath odataPath = odataUri.Path.TrimEndingTypeSegment().TrimEndingKeySegment();
ODataPath odataPath = odataUri.Path.TrimEndingTypeAndKeySegments();
if (!(odataPath.LastSegment is NavigationPropertySegment) && !(odataPath.LastSegment is OperationSegment))
{
throw new ODataException(Strings.ODataContextUriBuilder_ODataPathInvalidForContainedElement(odataPath.ToContextUrlPathString()));
Expand All @@ -322,15 +322,13 @@ private static string ComputeNavigationPath(EdmNavigationSourceKind kind, in ODa
string navigationPath = null;
if (kind == EdmNavigationSourceKind.ContainedEntitySet && odataUri.Path != null)
{
ODataPath odataPath = odataUri.Path.TrimEndingTypeSegment().TrimEndingKeySegment();
ODataPath odataPath = odataUri.Path.TrimEndingTypeAndKeySegments();
if (!(odataPath.LastSegment is NavigationPropertySegment) && !(odataPath.LastSegment is OperationSegment))
{
throw new ODataException(Strings.ODataContextUriBuilder_ODataPathInvalidForContainedElement(odataPath.ToContextUrlPathString()));
}

navigationPath = odataPath.ToContextUrlPathString();
}

return navigationPath ?? navigationSource;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
static Microsoft.OData.UriParser.ODataPathExtensions.TrimEndingTypeAndKeySegments(this Microsoft.OData.UriParser.ODataPath path) -> Microsoft.OData.UriParser.ODataPath
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
static Microsoft.OData.UriParser.ODataPathExtensions.TrimEndingTypeAndKeySegments(this Microsoft.OData.UriParser.ODataPath path) -> Microsoft.OData.UriParser.ODataPath
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
static Microsoft.OData.UriParser.ODataPathExtensions.TrimEndingTypeAndKeySegments(this Microsoft.OData.UriParser.ODataPath path) -> Microsoft.OData.UriParser.ODataPath
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
static Microsoft.OData.UriParser.ODataPathExtensions.TrimEndingTypeAndKeySegments(this Microsoft.OData.UriParser.ODataPath path) -> Microsoft.OData.UriParser.ODataPath
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,22 @@ public static ODataPath TrimEndingTypeSegment(this ODataPath path)
return handler.FirstPart;
}

/// <summary>
/// Creates a <see cref="ODataPath"/> that is <paramref name="path"/> with the type segments and key segments removed from the end
/// </summary>
/// <param name="path">The <see cref="ODataPath"/> to trim the ending of</param>
/// <returns>A <see cref="ODataPath"/> without type-cast and key segments at the end</returns>
/// <exception cref="System.ArgumentNullException">Thrown if <paramref name="path"/> is <see langword="null"/></exception>
public static ODataPath TrimEndingTypeAndKeySegments(this ODataPath path)
{
if (path == null)
{
throw Error.ArgumentNull(nameof(path));
}

return new ODataPath(path.Segments.Take(path.Segments.FindLastIndex(segment => !(segment is KeySegment || segment is TypeSegment)) + 1));
}

/// <summary>
/// Creates a new ODataPath with the specified segment added.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@

static System.Collections.Generic.ReadOnlyListExtensions.FindLastIndex<T>(this System.Collections.Generic.IReadOnlyList<T> list, System.Func<T, bool> predicate) -> int
System.Collections.Generic.ReadOnlyListExtensions
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@

static System.Collections.Generic.ReadOnlyListExtensions.FindLastIndex<T>(this System.Collections.Generic.IReadOnlyList<T> list, System.Func<T, bool> predicate) -> int
System.Collections.Generic.ReadOnlyListExtensions
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@

static System.Collections.Generic.ReadOnlyListExtensions.FindLastIndex<T>(this System.Collections.Generic.IReadOnlyList<T> list, System.Func<T, bool> predicate) -> int
System.Collections.Generic.ReadOnlyListExtensions
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
namespace System.Collections.Generic
{
/// <summary>
/// Extensions methods <see cref="IReadOnlyList{T}"/>
/// </summary>
public static class ReadOnlyListExtensions
{
/// <summary>
/// Searches for an element that matches the conditions defined by the specified predicate, and returns the zero-based index of the last occurrence within the
/// entire <see cref="IReadOnlyList{T}"/>
/// </summary>
/// <typeparam name="T">The type of the elements in <paramref name="list"/></typeparam>
/// <param name="list">The <see cref="IReadOnlyList{T}"/> to find the index of the last element of</param>
/// <param name="predicate">The <see cref="Func{T, TResult}"/> delegate that defines the conditions of the element to search for.</param>
/// <returns>
/// The zero-based index of the last occurrence of an element that matches the conditions defined by <paramref name="predicate"/>, if found; otherwise, -1
/// </returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="list"/> or <paramref name="predicate"/> is <see langword="null"/></exception>
/// <remarks>
/// Copied from <see href="https://github.com/dotnet/runtime/blob/87c25589bda5a79baf8d056501663b8525f366a8/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/List.cs#L560"/>
/// </remarks>
public static int FindLastIndex<T>(this IReadOnlyList<T> list, Func<T, bool> predicate)
{
if (list == null)
{
throw new ArgumentNullException(nameof(list));
}

if (predicate == null)
{
throw new ArgumentNullException(nameof(predicate));
}

for (int i = list.Count - 1; i > -1; --i)
{
if (predicate(list[i]))
{
return i;
}
}

return -1;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8" ?>
<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx" xmlns:ags="http://aggregator.microsoft.com/internal" xmlns:odata="http://schemas.microsoft.com/oDataCapabilities">
<edmx:DataServices>
<Schema Namespace="ns" Alias="self" xmlns="http://docs.oasis-open.org/odata/ns/edm" xmlns:ags="http://aggregator.microsoft.com/internal" xmlns:odata="http://schemas.microsoft.com/oDataCapabilities">
<EntityContainer Name="Container">
<EntitySet Name="orders" EntityType="self.order" />
<EntitySet Name="categories" EntityType="self.category" />
</EntityContainer>
<EntityType Name="order">
<Key>
<PropertyRef Name="id" />
</Key>
<Property Name="id" Type="Edm.String" Nullable="false" />
<NavigationProperty Name="products" Type="Collection(self.product)" ContainsTarget="true" Nullable="false" />
</EntityType>
<EntityType Name="product">
<Key>
<PropertyRef Name="id" />
</Key>
<Property Name="id" Type="Edm.String" Nullable="false" />
<Property Name="name" Type="Edm.String" Nullable="false" />
</EntityType>
<EntityType Name="category">
<Key>
<PropertyRef Name="id" />
</Key>
<Property Name="id" Type="Edm.String" Nullable="false" />
<Property Name="foo" Type="Edm.String" Nullable="false" />
</EntityType>
<EntityType Name="derivedProduct" BaseType="self.product">
<NavigationProperty Name="category" Type="self.category" ContainsTarget="true" />
</EntityType>
</Schema>
</edmx:DataServices>
</edmx:Edmx>
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,18 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using Microsoft.OData.Edm;
using Microsoft.OData.Edm.Csdl;
#if NETCOREAPP3_1_OR_GREATER
using Microsoft.OData.Json;
#endif
using Microsoft.OData.JsonLight;
using Microsoft.OData.UriParser;
using Microsoft.OData.Tests;
using Microsoft.Test.OData.DependencyInjection;
using Xunit;
Expand Down Expand Up @@ -739,6 +746,181 @@ public async Task WriteEntityReferenceLinkAsync()
result);
}

/// <summary>
/// Gets the name of the caller method of this method
/// </summary>
/// <param name="caller">The string that the method name of the caller will be written into</param>
/// <returns>The name of the caller method of this method</returns>
public static string GetCurrentMethodName([System.Runtime.CompilerServices.CallerMemberName] string caller = null)
{
return caller;
}

/// <summary>
/// A <see cref="IEdmNavigationSource"/> that pretends to be the "products" contained navigation collection for the purposes of computing a context URL
/// </summary>
private sealed class MockNavigationSource : IEdmNavigationSource, IEdmContainedEntitySet, IEdmUnknownEntitySet
{
public IEnumerable<IEdmNavigationPropertyBinding> NavigationPropertyBindings => throw new NotImplementedException();

public IEdmPathExpression Path => throw new NotImplementedException();

public IEdmType Type => new EdmEntityType("ns", "products");

public string Name => "products";

public IEdmNavigationSource ParentNavigationSource => throw new NotImplementedException();

public IEdmNavigationProperty NavigationProperty => throw new NotImplementedException();

public IEnumerable<IEdmNavigationPropertyBinding> FindNavigationPropertyBindings(IEdmNavigationProperty navigationProperty)
{
throw new NotImplementedException();
}

public IEdmNavigationSource FindNavigationTarget(IEdmNavigationProperty navigationProperty)
{
throw new NotImplementedException();
}

public IEdmNavigationSource FindNavigationTarget(IEdmNavigationProperty navigationProperty, IEdmPathExpression bindingPath)
{
throw new NotImplementedException();
}
}

#if !NETCOREAPP1_1
/// <summary>
/// Generates a context URL from a <see cref="ODataUriSlim"/> that ends with cast and key segments
/// </summary>
/// <returns><see cref="void"/></returns>
[Fact]
public static void GenerateContextUrlFromSlimUriWithDerivedTypeCastAndKeySegment()
{
var domain = new Uri("http://tempuri.org");
var requestUrl = new Uri(domain, "/orders('1')/products/ns.derivedProduct('2')");

// load the CSDL from the embedded resources
var assembly = Assembly.GetExecutingAssembly();
var currentMethod = GetCurrentMethodName();
var csdlResourceName = assembly.GetManifestResourceNames().Where(name => name.EndsWith($"{currentMethod}.xml")).Single();

// parse the CSDL
IEdmModel model;
using (var csdlResourceStream = assembly.GetManifestResourceStream(csdlResourceName))
{
using (var xmlReader = XmlReader.Create(csdlResourceStream))
{
if (!CsdlReader.TryParse(xmlReader, out model, out var errors))
{
Assert.True(false, string.Join(Environment.NewLine, errors));
}
}
}

var uriParser = new ODataUriParser(model, domain, requestUrl);
var slimUri = new ODataUriSlim(uriParser.ParseUri());
var contextUrlInfo = ODataContextUrlInfo.Create(new MockNavigationSource(), "ns.product", true, slimUri, ODataVersion.V4);
Assert.Equal(@"orders('1')/products", contextUrlInfo.NavigationPath);
}

/// <summary>
/// Writes a resource as the response to a request where the URL ends with a combined cast and key segment
/// </summary>
/// <returns><see cref="void"/></returns>
[Fact]
public static async Task WriteContextWithDerivedTypeCastAndKeySegmentAsync()
{
var domain = new Uri("http://tempuri.org");
var requestUrl = new Uri(domain, "/orders('1')/products/ns.derivedProduct('2')");
var serviceSideResponseResource = new ODataResource
{
TypeName = "ns.product",
Properties = new List<ODataProperty>
{
new ODataProperty
{
Name = "id",
Value = "1",
SerializationInfo = new ODataPropertySerializationInfo
{
PropertyKind = ODataPropertyKind.Key
},
},
new ODataProperty
{
Name = "name",
Value = "somename",
},
},
};
var expectedResponsePayload =
"{" +
"\"@odata.context\":\"http://tempuri.org/$metadata#orders('1')/products/$entity\"," +
"\"id\":\"1\"," +
"\"name\":\"somename\"" +
"}";

// load the CSDL from the embedded resources
var assembly = Assembly.GetExecutingAssembly();
var currentMethod = GetCurrentMethodName();
var csdlResourceName = assembly.GetManifestResourceNames().Where(name => name.EndsWith($"{currentMethod}.xml")).Single();

// parse the CSDL
IEdmModel model;
using (var csdlResourceStream = assembly.GetManifestResourceStream(csdlResourceName))
{
using (var xmlReader = XmlReader.Create(csdlResourceStream))
{
if (!CsdlReader.TryParse(xmlReader, out model, out var errors))
{
Assert.True(false, string.Join(Environment.NewLine, errors));
}
}
}

using (var memoryStream = new MemoryStream())
{
// initialize the json response writer
var uriParser = new ODataUriParser(model, domain, requestUrl);
var odataMessageWriterSettings = new ODataMessageWriterSettings
{
EnableMessageStreamDisposal = false,
Version = ODataVersion.V4,
ShouldIncludeAnnotation = ODataUtils.CreateAnnotationFilter("*"),
ODataUri = uriParser.ParseUri(),
};
var messageInfo = new ODataMessageInfo
{
MessageStream = memoryStream,
MediaType = new ODataMediaType("application", "json"),
Encoding = Encoding.Default,
IsResponse = true,
IsAsync = true,
Model = model,
};
var jsonLightOutputContext = new ODataJsonLightOutputContext(messageInfo, odataMessageWriterSettings);
var jsonLightWriter = new ODataJsonLightWriter(
jsonLightOutputContext,
null,
null,
false);

// write the response
await jsonLightWriter.WriteStartAsync(serviceSideResponseResource);
await jsonLightWriter.WriteEndAsync();

// confirm that the written response was the expected response
memoryStream.Position = 0;
using (var streamReader = new StreamReader(memoryStream))
{
var actualResponsePayload = await streamReader.ReadToEndAsync();
Assert.Equal(expectedResponsePayload, actualResponsePayload);
}
}
}
#endif

[Fact]
public async Task WriteEntityReferenceLinkForCollectionNavigationPropertyAsync()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8" ?>
<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx" xmlns:ags="http://aggregator.microsoft.com/internal" xmlns:odata="http://schemas.microsoft.com/oDataCapabilities">
<edmx:DataServices>
<Schema Namespace="ns" Alias="self" xmlns="http://docs.oasis-open.org/odata/ns/edm" xmlns:ags="http://aggregator.microsoft.com/internal" xmlns:odata="http://schemas.microsoft.com/oDataCapabilities">
<EntityContainer Name="Container">
<EntitySet Name="orders" EntityType="self.order" />
</EntityContainer>
<EntityType Name="order">
<Key>
<PropertyRef Name="id" />
</Key>
<Property Name="id" Type="Edm.String" Nullable="false" />
<NavigationProperty Name="products" Type="Collection(self.product)" ContainsTarget="true" Nullable="false" />
</EntityType>
<EntityType Name="product">
<Key>
<PropertyRef Name="id" />
</Key>
<Property Name="id" Type="Edm.String" Nullable="false" />
<Property Name="name" Type="Edm.String" Nullable="false" />
</EntityType>
<EntityType Name="derivedProduct" BaseType="self.product">
</EntityType>
</Schema>
</edmx:DataServices>
</edmx:Edmx>
Loading

0 comments on commit f9a9405

Please sign in to comment.