Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

client support for custom uri functions #2631

Merged
merged 12 commits into from
Oct 26, 2023
Merged
11 changes: 11 additions & 0 deletions src/Microsoft.OData.Client/ALinq/Evaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace Microsoft.OData.Client
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;

/// <summary>
Expand Down Expand Up @@ -170,6 +171,16 @@ internal HashSet<Expression> Nominate(Expression expression)
return this.candidates;
}

internal override Expression VisitMethodCall(MethodCallExpression m)
{
UriFunctionAttribute uriFunctionAttribute = (UriFunctionAttribute)m.Method.GetCustomAttributes(typeof(UriFunctionAttribute), false).SingleOrDefault();
if (uriFunctionAttribute != null && !uriFunctionAttribute.AllowClientSideEvaluation)
{
this.cannotBeEvaluated = true;
}
return base.VisitMethodCall(m);
}

/// <summary>
/// Visit method for walking expression tree bottom up.
/// </summary>
Expand Down
13 changes: 8 additions & 5 deletions src/Microsoft.OData.Client/ALinq/ExpressionWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -257,8 +257,10 @@ internal override Expression VisitMethodCall(MethodCallExpression m)
// The implementation in .NET of "matchesPattern" is unique in that the
// invoked method requires an additional hard-coded parameter to match
// the spec (RegexOptions.ECMAScript). Therefore we handle it as a special
// case
if (methodName == "matchesPattern")
// case. To not conflict with a potential Custom Uri Function overload we also
// check the method's declaring type.

if (methodName == "matchesPattern" && m.Method.DeclaringType == typeof(Regex))
{
Debug.Assert(m.Method.Name == "IsMatch", "m.Method.Name == 'IsMatch'");
Debug.Assert(m.Object == null, "m.Object == null");
Expand All @@ -272,8 +274,9 @@ internal override Expression VisitMethodCall(MethodCallExpression m)
}
// There is a single function, 'contains', which reorders its argument with
// respect to the CLR method. Thus handling it as a special case rather than
// using a more general argument reordering mechanism.
else if (methodName == "contains")
// using a more general argument reordering mechanism. To not conflict with
// a potential Custom Uri Function overload we also check the methods declaring type.
else if (methodName == "contains" && m.Method.DeclaringType == typeof(string))
{
Debug.Assert(m.Method.Name == "Contains", "m.Method.Name == 'Contains'");
Debug.Assert(m.Object != null, "m.Object != null");
Expand Down Expand Up @@ -413,7 +416,7 @@ internal override Expression VisitMethodCall(MethodCallExpression m)

if (m.Method.Name != "GetValue" && m.Method.Name != "GetValueAsync")
{
if (this.parent != null)
if (m.Object != null && (this.InSubScope || this.parent?.NodeType == ExpressionType.Call || this.parent?.NodeType == ExpressionType.MemberAccess))
{
this.builder.Append(UriHelper.FORWARDSLASH);
}
Expand Down
21 changes: 21 additions & 0 deletions src/Microsoft.OData.Client/ALinq/TypeSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ namespace Microsoft.OData.Client
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using Microsoft.OData.Client.Metadata;
using Microsoft.OData.Edm;
using Microsoft.OData.UriParser.Aggregation;
using Microsoft.Spatial;
Expand Down Expand Up @@ -242,10 +244,29 @@ static TypeSystem()
internal static bool TryGetQueryOptionMethod(MethodInfo mi, out string methodName)
{
return (expressionMethodMap.TryGetValue(mi, out methodName) ||
TryResolveUriFunction(mi, out methodName) ||
(IsVisualBasicAssembly(mi.DeclaringType.GetAssembly()) &&
expressionVBMethodMap.TryGetValue(mi.DeclaringType.FullName + "." + mi.Name, out methodName)));
}

/// <summary>
/// Sees if method is declared as a UriFunction and resolves the uri method name
/// </summary>
/// <param name="mi">The method info</param>
/// <param name="methodName">uri method name</param>
/// <returns>true/ false</returns>
private static bool TryResolveUriFunction(MethodInfo mi, out string methodName)
{
UriFunctionAttribute uriFunctionAttribute = (UriFunctionAttribute)mi.GetCustomAttributes(typeof(UriFunctionAttribute), false).FirstOrDefault();
if (uriFunctionAttribute != null)
{
methodName = ClientTypeUtil.GetServerDefinedName(mi);
return true;
}
methodName = null;
return false;
}

/// <summary>
/// Sees if property can be represented as method for translation to URI
/// </summary>
Expand Down
32 changes: 32 additions & 0 deletions src/Microsoft.OData.Client/Attribute/UriFunctionAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//---------------------------------------------------------------------
// <copyright file="UriFunctionAttribute.cs" company="Microsoft">
// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
// </copyright>
//---------------------------------------------------------------------

namespace Microsoft.OData.Client
{
using System;

/// <summary>Indicates a method that should be mapped to a Uri Function.</summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public sealed class UriFunctionAttribute : Attribute
{
/// <summary>Allow client side evaluation.</summary>
private readonly bool allowClientSideEvaluation;

/// <summary>Initializes a new instance of the <see cref="Microsoft.OData.Client.UriFunctionAttribute" /> class. </summary>
/// <param name="allowClientSideEvaluation">Use client side evaluation when possible. Default is false.</param>
public UriFunctionAttribute(bool allowClientSideEvaluation = false)
{
this.allowClientSideEvaluation = allowClientSideEvaluation;
}

/// <summary>Can client side evaluation be used.</summary>
/// <returns>Boolean value indicating if client side evaluation can be used. </returns>
public bool AllowClientSideEvaluation
{
get { return this.allowClientSideEvaluation; }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Microsoft.OData.Client.UriFunctionAttribute
Microsoft.OData.Client.UriFunctionAttribute.AllowClientSideEvaluation.get -> bool
Microsoft.OData.Client.UriFunctionAttribute.UriFunctionAttribute(bool allowClientSideEvaluation = false) -> void
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Microsoft.OData.Client.UriFunctionAttribute
Microsoft.OData.Client.UriFunctionAttribute.AllowClientSideEvaluation.get -> bool
Microsoft.OData.Client.UriFunctionAttribute.UriFunctionAttribute(bool allowClientSideEvaluation = false) -> void
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ public void EnumerableContainsOnCollectionValuedPropertiesWithMemberAccessIsNotS

#endregion

#region CustomUriFunction tests
#region CustomFunction tests

[Fact]
public void TranslatesStaticFunction()
Expand Down Expand Up @@ -206,6 +206,157 @@ public void TranslatesInstanceFunction()
Assert.Equal(@"http://root/Products?$filter=$it/ServiceNamespace.InstanceFunction(parameter=$it/Name)", queryComponents.Uri.ToString());
}

[Fact]
public void TranslatesStaticFunctionWhenLast()
{
// Arrange
var localDsc = new DataServiceContext(new Uri("http://root"), ODataProtocolVersion.V4);
localDsc.ResolveName = (t) => "ServiceNamespace.Product";
var sut = new DataServiceQueryProvider(localDsc);
var products = localDsc.CreateQuery<Product>("Products")
.Where(product => true && Product.StaticFunction(product.Name));

// Act
var queryComponents = sut.Translate(products.Expression);

// Assert
Assert.Equal(@"http://root/Products?$filter=true and ServiceNamespace.StaticFunction(parameter=$it/Name)", queryComponents.Uri.ToString());
}

[Fact]
public void TranslatesInstanceFunctionWhenLast()
{
// Arrange
var localDsc = new DataServiceContext(new Uri("http://root"), ODataProtocolVersion.V4);
localDsc.ResolveName = (t) => "ServiceNamespace.Product";
var sut = new DataServiceQueryProvider(localDsc);
var products = localDsc.CreateQuery<Product>("Products")
.Where(product => true && product.InstanceFunction(product.Name));

// Act
var queryComponents = sut.Translate(products.Expression);

// Assert
Assert.Equal(@"http://root/Products?$filter=true and $it/ServiceNamespace.InstanceFunction(parameter=$it/Name)", queryComponents.Uri.ToString());
}

#endregion

#region CustomUriFunction tests

[Fact]
public void TranslatesInstanceUriFunction()
{
// Arrange - products selling more than 1000 in year 2022
var localDsc = new DataServiceContext(new Uri("http://root"), ODataProtocolVersion.V4);
var sut = new DataServiceQueryProvider(localDsc);
var products = localDsc.CreateQuery<Product>("Products")
.Where(product => product.YearSale(2022) > 1000);

// Act
var queryComponents = sut.Translate(products.Expression);

// Assert
Assert.Equal(@"http://root/Products?$filter=sale($it,2022) gt 1000", queryComponents.Uri.ToString());
}

[Fact]
public void TranslatesInstanceUriFunctionOfProperty()
{
// Arrange - products selling more than 1000 the year it was launched
var localDsc = new DataServiceContext(new Uri("http://root"), ODataProtocolVersion.V4);
var sut = new DataServiceQueryProvider(localDsc);
var products = localDsc.CreateQuery<Product>("Products")
.Where(product => product.YearSale(product.LaunchDate.Year) > 1000);

// Act
var queryComponents = sut.Translate(products.Expression);

// Assert
Assert.Equal(@"http://root/Products?$filter=sale($it,year(LaunchDate)) gt 1000", queryComponents.Uri.ToString());
}

[Fact]
public void TranslatesStaticUriFunction()
{
// Arrange - products launched 7 days ago, evaluated on server
var localDsc = new DataServiceContext(new Uri("http://root"), ODataProtocolVersion.V4);
var sut = new DataServiceQueryProvider(localDsc);
var products = localDsc.CreateQuery<Product>("Products")
.Where(product => product.LaunchDate == UriFunctions.ServerDate(UriFunctions.ServerNow() - TimeSpan.FromDays(7)));

// Act
var queryComponents = sut.Translate(products.Expression);

// Assert
Assert.Equal(@"http://root/Products?$filter=LaunchDate eq date(now() sub duration'P7D')", queryComponents.Uri.ToString());
}

[Fact]
public void TranslatesStaticUriFunctionCanResolve()
{
// Arrange - client evaluated Even()
var localDsc = new DataServiceContext(new Uri("http://root"), ODataProtocolVersion.V4);
var sut = new DataServiceQueryProvider(localDsc);
var products = localDsc.CreateQuery<Product>("Products")
.Where(product => UriFunctions.Even(2));
uffelauesen marked this conversation as resolved.
Show resolved Hide resolved

// Act
var queryComponents = sut.Translate(products.Expression);

// Assert
Assert.Equal(@"http://root/Products?$filter=true", queryComponents.Uri.ToString());
}

[Fact]
public void TranslatesStaticUriFunctionOfProperty()
{
// Arrange - products with Even Id
var localDsc = new DataServiceContext(new Uri("http://root"), ODataProtocolVersion.V4);
var sut = new DataServiceQueryProvider(localDsc);
var products = localDsc.CreateQuery<Product>("Products")
.Where(product => UriFunctions.Even(product.Id));

// Act
var queryComponents = sut.Translate(products.Expression);

// Assert
Assert.Equal(@"http://root/Products?$filter=Even(Id)", queryComponents.Uri.ToString());
}

[Fact]
public void TranslatesInstanceUriFunctionOfProperty2()
{
// Arrange - products with Even Id
var localDsc = new DataServiceContext(new Uri("http://root"), ODataProtocolVersion.V4);
var sut = new DataServiceQueryProvider(localDsc);
var products = localDsc.CreateQuery<Product>("Products")
.Where(product => product.Test(product.Name));

// Act
var queryComponents = sut.Translate(products.Expression);

// Assert
Assert.Equal(@"http://root/Products?$filter=Test($it,Name)", queryComponents.Uri.ToString());
}

[Fact]
public void TranslatesInstanceUriFunctionOfProperty3()
{
Product clientProduct = new Product();
// Arrange - products with Even Id
var localDsc = new DataServiceContext(new Uri("http://root"), ODataProtocolVersion.V4);
var sut = new DataServiceQueryProvider(localDsc);
var products = localDsc.CreateQuery<Product>("Products")
.Where(product => clientProduct.Test(""));

// Act
var queryComponents = sut.Translate(products.Expression);

// Assert
Assert.Equal(@"http://root/Products?$filter=true", queryComponents.Uri.ToString());
}

#endregion

[EntityType]
Expand All @@ -218,11 +369,37 @@ private class Product

public decimal Price { get; set; }

public Edm.Date LaunchDate { get; set; }

public IEnumerable<string> Comments { get; set; }

public static bool StaticFunction(string parameter) { return true; }

public bool InstanceFunction(string parameter) { return true; }

[UriFunction, OriginalName("sale")]
public int YearSale(int year) => throw new NotSupportedException();

[UriFunction(true)]
public bool Test(string data)
{
return string.IsNullOrEmpty(data);
}
}

private static class UriFunctions
{
[UriFunction, OriginalName("now")]
public static DateTimeOffset ServerNow() => throw new NotSupportedException();

[UriFunction, OriginalName("date")]
public static Edm.Date ServerDate(DateTimeOffset value) => throw new NotSupportedException();

[UriFunction(true)]
uffelauesen marked this conversation as resolved.
Show resolved Hide resolved
public static bool Even(int value)
{
return value % 2 == 0;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8918,6 +8918,15 @@ public sealed class Microsoft.OData.Client.UriEntityOperationParameter : Microso
public UriEntityOperationParameter (string name, object value, bool useEntityReference)
}

[
AttributeUsageAttribute(),
]
public sealed class Microsoft.OData.Client.UriFunctionAttribute : System.Attribute {
public UriFunctionAttribute (params bool allowClientSideEvaluation)

bool AllowClientSideEvaluation { public get; }
}

public sealed class Microsoft.OData.Client.WritingEntityReferenceLinkArgs {
public WritingEntityReferenceLinkArgs (Microsoft.OData.ODataEntityReferenceLink entityReferenceLink, object source, object target)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8918,6 +8918,15 @@ public sealed class Microsoft.OData.Client.UriEntityOperationParameter : Microso
public UriEntityOperationParameter (string name, object value, bool useEntityReference)
}

[
AttributeUsageAttribute(),
]
public sealed class Microsoft.OData.Client.UriFunctionAttribute : System.Attribute {
public UriFunctionAttribute (params bool allowClientSideEvaluation)

bool AllowClientSideEvaluation { public get; }
}

public sealed class Microsoft.OData.Client.WritingEntityReferenceLinkArgs {
public WritingEntityReferenceLinkArgs (Microsoft.OData.ODataEntityReferenceLink entityReferenceLink, object source, object target)

Expand Down