From 95233fc8a399cbae6e816216528d02a581b77b48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Habinshuti?= Date: Tue, 10 Sep 2024 11:42:32 +0300 Subject: [PATCH] Add support for "Any" and Predicates method call expressions (#2863) (#3061) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add support for "Any" method call expression * Add support for predicate expression methods * Add SequenceMethodsTests * Extend SequenceMethodsTests * Separate some tests methods * Assert within tests methods * Refactor: merge TestContext into tests class Co-authored-by: José Carlos --- .../ALinq/DataServiceQueryProvider.cs | 70 +++- .../ALinq/SequenceMethodsTests.cs | 325 ++++++++++++++++++ 2 files changed, 394 insertions(+), 1 deletion(-) create mode 100644 test/FunctionalTests/Microsoft.OData.Client.Tests/ALinq/SequenceMethodsTests.cs diff --git a/src/Microsoft.OData.Client/ALinq/DataServiceQueryProvider.cs b/src/Microsoft.OData.Client/ALinq/DataServiceQueryProvider.cs index e04530ffb7..014d58b274 100644 --- a/src/Microsoft.OData.Client/ALinq/DataServiceQueryProvider.cs +++ b/src/Microsoft.OData.Client/ALinq/DataServiceQueryProvider.cs @@ -9,6 +9,7 @@ namespace Microsoft.OData.Client #region Namespaces using System; + using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -87,13 +88,14 @@ public TResult Execute(Expression expression) #endregion + /// Creates and executes a DataServiceQuery based on the passed in expression which results a single value /// generic type /// The expression for the new query /// single valued results internal TElement ReturnSingleton(Expression expression) { - IQueryable query = new DataServiceQuery.DataServiceOrderedQuery(expression, this); + IQueryable query = CreateQuery(expression); MethodCallExpression mce = expression as MethodCallExpression; Debug.Assert(mce != null, "mce != null"); @@ -105,15 +107,35 @@ internal TElement ReturnSingleton(Expression expression) { case SequenceMethod.Single: return query.AsEnumerable().Single(); + case SequenceMethod.SinglePredicate: + query = CreateQuery(NestPredicateExpression(mce)); + return query.AsEnumerable().Single(); case SequenceMethod.SingleOrDefault: return query.AsEnumerable().SingleOrDefault(); + case SequenceMethod.SingleOrDefaultPredicate: + query = CreateQuery(NestPredicateExpression(mce)); + return query.AsEnumerable().SingleOrDefault(); case SequenceMethod.First: return query.AsEnumerable().First(); + case SequenceMethod.FirstPredicate: + query = CreateQuery(NestPredicateExpression(mce)); + return query.AsEnumerable().First(); case SequenceMethod.FirstOrDefault: return query.AsEnumerable().FirstOrDefault(); + case SequenceMethod.FirstOrDefaultPredicate: + query = CreateQuery(NestPredicateExpression(mce)); + return query.AsEnumerable().FirstOrDefault(); case SequenceMethod.LongCount: case SequenceMethod.Count: return ((DataServiceQuery)query).GetValue(this.Context, ParseQuerySetCount); + case SequenceMethod.LongCountPredicate: + case SequenceMethod.CountPredicate: + query = CreateQuery(NestPredicateExpression(mce)); + return ((DataServiceQuery)query).GetValue(this.Context, ParseQuerySetCount); + case SequenceMethod.Any: + return GetValueForAny(mce); + case SequenceMethod.AnyPredicate: + return GetValueForAny(NestPredicateExpression(mce)); case SequenceMethod.SumIntSelector: case SequenceMethod.SumDoubleSelector: case SequenceMethod.SumDecimalSelector: @@ -196,6 +218,52 @@ internal QueryComponents Translate(Expression e) return queryComponents; } + /// + /// Transforms the 'any' query into a 'count' request since OData does not have a spcific query for 'any'. + /// Then the result is casted to the corresponding return type (boolean). + /// + /// The return type. + /// The original expression with predicate. + /// + private TElement GetValueForAny(MethodCallExpression mce) + { + Expression arg0 = mce.Arguments[0]; + Expression countExpression = Expression.Call( + typeof(Enumerable), + "Count", + new Type[] { arg0.Type.GetGenericArguments()[0] }, + arg0 + ); + var query = CreateQuery(countExpression) as DataServiceQuery; + return query.GetValue(Context, ParseQuerySetCount); + } + + /// + /// Transforms the expression type to one of type 'where'. + /// Then it wraps this 'where' expression into one of the received type but without a predicate. + /// + /// The original expression with predicate. + /// The wrapped expression. + private static MethodCallExpression NestPredicateExpression(MethodCallExpression mce) + { + Type resourceType = mce.Arguments[0].Type.GetGenericArguments()[0]; + + Expression where = Expression.Call( + typeof(Queryable), + "Where", + new Type[] { resourceType }, + mce.Arguments[0], + mce.Arguments[1] + ); + + return Expression.Call( + typeof(Enumerable), + mce.Method.Name, + new Type[] { resourceType }, + where + ); + } + /// /// Parses the result of a query set count request. /// diff --git a/test/FunctionalTests/Microsoft.OData.Client.Tests/ALinq/SequenceMethodsTests.cs b/test/FunctionalTests/Microsoft.OData.Client.Tests/ALinq/SequenceMethodsTests.cs new file mode 100644 index 0000000000..cbb64e2382 --- /dev/null +++ b/test/FunctionalTests/Microsoft.OData.Client.Tests/ALinq/SequenceMethodsTests.cs @@ -0,0 +1,325 @@ +//--------------------------------------------------------------------- +// +// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. +// +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Microsoft.OData.Edm; +using Xunit; + +namespace Microsoft.OData.Client.Tests.ALinq +{ + /// + /// Tests to check sequence methods support. + /// + public class SequenceMethodsTests + { + private const string MockCustomer1 = "{\"CustomerID\":\"ALFKI\",\"CompanyName\":\"Alfreds Futterkiste\",\"ContactName\":\"Maria Anders\",\"Address\":\"Obere Str. 57\",\"City\":\"Berlin\"}"; + + private const string MockCustomer2 = "{\"CustomerID\":\"CHOPS\",\"CompanyName\":\"Chop-suey Chinese\",\"ContactName\":\"Yang Wang\",\"Address\":\"Hauptstr. 29\",\"City\":\"Bern\"}"; + + private readonly DataServiceQuery _customers; + + private readonly DataServiceContext _ctx; + + private readonly string _rootUriStr; + + private Action _onRequestUriBuilt = null; + + public SequenceMethodsTests() + { + _rootUriStr = "https://mock.odata.service"; + Uri uri = new Uri(_rootUriStr); + + _ctx = new DataServiceContext(uri); + _customers = _ctx.CreateQuery("Customers"); + + EdmModel model = BuildEdmModel(); + _ctx.Format.UseJson(model); + _ctx.ResolveName = (type) => $"NS.{type.Name}"; + _ctx.KeyComparisonGeneratesFilterQuery = true; + + _ctx.BuildingRequest += (obj, args) => + { + if (_onRequestUriBuilt == null) return; + string actualUri = args.RequestUri.OriginalString; + _onRequestUriBuilt(actualUri); + }; + } + + [Fact] + public void Any() + { + _onRequestUriBuilt = (string builtUri) => + { + string expectedUri = BuildUriFromPath("/Customers/$count"); + Assert.Equal(expectedUri, builtUri); + }; + InterceptRequestAndMockResponse("91"); + Assert.True(_customers.Any()); + } + + [Fact] + public void Any_ReturnsFalse_WhenNoMatchExists() + { + _onRequestUriBuilt = (string builtUri) => + { + string expectedUri = BuildUriFromPath("/Customers/$count?$filter=contains(ContactName,'thisdoesntexist')"); + Assert.Equal(expectedUri, builtUri); + }; + InterceptRequestAndMockResponse("0"); + Assert.False(_customers.Where(c => c.Name.Contains("thisdoesntexist")).Any()); + } + + [Fact] + public void AnyPredicate() + { + _onRequestUriBuilt = (string builtUri) => + { + string expectedUri = BuildUriFromPath("/Customers/$count?$filter=contains(ContactName,'ab')"); + Assert.Equal(expectedUri, builtUri); + }; + InterceptRequestAndMockResponse("6"); + Assert.True(_customers.Any(c => c.Name.Contains("ab"))); + } + + [Fact] + public void AnyPredicate_ReturnsFalse_WhenNoMatchExists() + { + _onRequestUriBuilt = (string builtUri) => + { + string expectedUri = BuildUriFromPath("/Customers/$count?$filter=contains(ContactName,'thisdoesntexist')"); + Assert.Equal(expectedUri, builtUri); + }; + InterceptRequestAndMockResponse("0"); + Assert.False(_customers.Any(c => c.Name.Contains("thisdoesntexist"))); + } + + [Fact] + public void CountPredicate() + { + _onRequestUriBuilt = (string builtUri) => + { + string expectedUri = BuildUriFromPath("/Customers/$count?$filter=contains(ContactName,'ab')"); + Assert.Equal(expectedUri, builtUri); + }; + InterceptRequestAndMockResponse("6"); + int count = _customers.Count(c => c.Name.Contains("ab")); + Assert.Equal(6, count); + } + + [Fact] + public void LongCountPredicate() + { + _onRequestUriBuilt = (string builtUri) => + { + string expectedUri = BuildUriFromPath("/Customers/$count?$filter=contains(ContactName,'ab')"); + Assert.Equal(expectedUri, builtUri); + }; + InterceptRequestAndMockResponse("6"); + long count = _customers.LongCount(c => c.Name.Contains("ab")); + Assert.Equal(6, count); + } + + [Fact] + public void FirstPredicate() + { + _onRequestUriBuilt = (string builtUri) => + { + string expectedUri = BuildUriFromPath("/Customers?$filter=ContactName ne 'John'&$top=1"); + Assert.Equal(expectedUri, builtUri); + }; + InterceptRequestAndMockResponseValue("Customers", "[" + MockCustomer1 + "]"); + Customer customer = _customers.First(c => c.Name != "John"); + Assert.Equal("ALFKI", customer.Id); + Assert.Equal("Maria Anders", customer.Name); + Assert.Equal("Berlin", customer.City); + } + + [Fact] + public void FirstPredicate_ThrowsException_WhenNoMatchExists() + { + _onRequestUriBuilt = (string builtUri) => + { + string expectedUri = BuildUriFromPath("/Customers?$filter=ContactName eq 'thisdoesntexist'&$top=1"); + Assert.Equal(expectedUri, builtUri); + }; + InterceptRequestAndMockResponseValue("Customers", "[]"); + Assert.Throws(() => _customers.First(c => c.Name == "thisdoesntexist")); + } + + [Fact] + public void FirstOrDefaultPredicate() + { + _onRequestUriBuilt = (string builtUri) => + { + string expectedUri = BuildUriFromPath("/Customers?$filter=ContactName ne 'John'&$top=1"); + Assert.Equal(expectedUri, builtUri); + }; + InterceptRequestAndMockResponseValue("Customers", "[" + MockCustomer1 + "]"); + Customer customer = _customers.FirstOrDefault(c => c.Name != "John"); + Assert.Equal("ALFKI", customer.Id); + Assert.Equal("Maria Anders", customer.Name); + Assert.Equal("Berlin", customer.City); + } + + [Fact] + public void FirstOrDefaultPredicate_ReturnsNull_WhenNoMatchExists() + { + _onRequestUriBuilt = (string builtUri) => + { + string expectedUri = BuildUriFromPath("/Customers?$filter=ContactName eq 'John'&$top=1"); + Assert.Equal(expectedUri, builtUri); + }; + InterceptRequestAndMockResponseValue("Customers", "[]"); + Assert.Null(_customers.FirstOrDefault(c => c.Name == "John")); + } + + [Fact] + public void SinglePredicate() + { + _onRequestUriBuilt = (string builtUri) => + { + string expectedUri = BuildUriFromPath("/Customers?$filter=CustomerID eq 'CHOPS'&$top=2"); + Assert.Equal(expectedUri, builtUri); + }; + InterceptRequestAndMockResponseValue("Customers", "[" + MockCustomer2 + "]"); + Customer customer = _customers.Single(c => c.Id == "CHOPS"); + Assert.Equal("CHOPS", customer.Id); + Assert.Equal("Yang Wang", customer.Name); + Assert.Equal("Bern", customer.City); + } + + [Fact] + public void SinglePredicate_ThrowsException_WhenNoMatchExists() + { + _onRequestUriBuilt = (string builtUri) => + { + string expectedUri = BuildUriFromPath("/Customers?$filter=ContactName eq 'thisdoesntexist'&$top=2"); + Assert.Equal(expectedUri, builtUri); + }; + InterceptRequestAndMockResponseValue("Customers", "[]"); + Assert.Throws(() => _customers.Single(c => c.Name == "thisdoesntexist")); + } + + [Fact] + public void SinglePredicate_ThrowsException_WhenMoreThanOneMatchExists() + { + _onRequestUriBuilt = (string builtUri) => + { + string expectedUri = BuildUriFromPath("/Customers?$filter=ContactName ne 'thisdoesntexist'&$top=2"); + Assert.Equal(expectedUri, builtUri); + }; + InterceptRequestAndMockResponseValue("Customers", "[" + MockCustomer1 + "," + MockCustomer2 + "]"); + Assert.Throws(() => _customers.Single(c => c.Name != "thisdoesntexist")); + } + + [Fact] + public void SingleOrDefaultPredicate() + { + _onRequestUriBuilt = (string builtUri) => + { + string expectedUri = BuildUriFromPath("/Customers?$filter=CustomerID eq 'CHOPS'&$top=2"); + Assert.Equal(expectedUri, builtUri); + }; + InterceptRequestAndMockResponseValue("Customers", "[" + MockCustomer2 + "]"); + Customer customer = _customers.SingleOrDefault(c => c.Id == "CHOPS"); + Assert.Equal("CHOPS", customer.Id); + Assert.Equal("Yang Wang", customer.Name); + Assert.Equal("Bern", customer.City); + } + + [Fact] + public void SingleOrDefaultPredicate_ReturnsNull_WhenNoMatchExists() + { + _onRequestUriBuilt = (string builtUri) => + { + string expectedUri = BuildUriFromPath("/Customers?$filter=CustomerID eq '234111'&$top=2"); + Assert.Equal(expectedUri, builtUri); + }; + InterceptRequestAndMockResponseValue("Customers", "[]"); + Assert.Null(_customers.SingleOrDefault(c => c.Id == "234111")); + } + + [Fact] + public void SingleOrDefaultPredicate_ThrowsException_WhenMoreThanOneMatchExists() + { + _onRequestUriBuilt = (string builtUri) => + { + string expectedUri = BuildUriFromPath("/Customers?$filter=ContactName ne 'thisdoesntexist'&$top=2"); + Assert.Equal(expectedUri, builtUri); + }; + InterceptRequestAndMockResponseValue("Customers", "[" + MockCustomer1 + "," + MockCustomer2 + "]"); + Assert.Throws(() => _customers.SingleOrDefault(c => c.Name != "thisdoesntexist")); + } + + private string BuildUriFromPath(string uriPath) + { + return _rootUriStr + uriPath; + } + + private void InterceptRequestAndMockResponse(string mockResponse) + { + _ctx.Configurations.RequestPipeline.OnMessageCreating = (args) => + { + var contentTypeHeader = "application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8"; + var odataVersionHeader = "4.0"; + + return new TestHttpWebRequestMessage(args, + new Dictionary + { + {"Content-Type", contentTypeHeader}, + {"OData-Version", odataVersionHeader}, + }, + () => new MemoryStream(Encoding.UTF8.GetBytes(mockResponse))); + }; + } + + private void InterceptRequestAndMockResponseValue(string entitySetName, string mockResponseValue) + { + string mockResponse = "{\"@odata.context\":\"" + _rootUriStr + "/$metadata#" + entitySetName + "\",\"value\":" + mockResponseValue + "}"; + + InterceptRequestAndMockResponse(mockResponse); + } + + private static EdmModel BuildEdmModel() + { + var model = new EdmModel(); + + // Create the Customer entity type + var customerType = new EdmEntityType("NS", "Customer"); + var customerId = customerType.AddStructuralProperty("CustomerID", EdmPrimitiveTypeKind.String, false); + customerType.AddKeys(customerId); + customerType.AddStructuralProperty("CompanyName", EdmPrimitiveTypeKind.String, false); + customerType.AddStructuralProperty("ContactName", EdmPrimitiveTypeKind.String, true); + customerType.AddStructuralProperty("City", EdmPrimitiveTypeKind.String, true); + model.AddElement(customerType); + + // Create the EntityContainer + var container = new EdmEntityContainer("NS", "Container"); + model.AddElement(container); + + // Create Entity Sets + container.AddEntitySet("Customers", customerType); + + return model; + } + + [Key("CustomerID")] + public class Customer + { + [OriginalName("CustomerID")] + public string Id { get; set; } + + public string City { get; set; } + + [OriginalName("ContactName")] + public string Name { get; set; } + } + } +}