diff --git a/samples/OracleProvider/test/OracleProvider.FunctionalTests/Query/UdfDbFunctionOracleTest.cs b/samples/OracleProvider/test/OracleProvider.FunctionalTests/Query/UdfDbFunctionOracleTest.cs index 27654b56795..1a69ffa08c8 100644 --- a/samples/OracleProvider/test/OracleProvider.FunctionalTests/Query/UdfDbFunctionOracleTest.cs +++ b/samples/OracleProvider/test/OracleProvider.FunctionalTests/Query/UdfDbFunctionOracleTest.cs @@ -1,14 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; -using System.Collections.Generic; -using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.TestUtilities; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Xunit; -using System.Globalization; using Xunit.Abstractions; namespace Microsoft.EntityFrameworkCore.Query @@ -21,7 +15,157 @@ public UdfDbFunctionOracleTest(Oracle fixture, ITestOutputHelper testOutputHelpe //fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } - public class Oracle : UdfFixtureBase + + #region Table Valued Tests + + [Fact(Skip = "TODO")] + public override void TVF_Stand_Alone() + { + base.TVF_Stand_Alone(); + } + + [Fact(Skip = "TODO")] + public override void TVF_Stand_Alone_With_Translation() + { + base.TVF_Stand_Alone_With_Translation(); + } + + [Fact(Skip = "TODO")] + public override void TVF_Stand_Alone_Parameter() + { + base.TVF_Stand_Alone_Parameter(); + } + + [Fact(Skip = "TODO")] + public override void TVF_Stand_Alone_Nested() + { + base.TVF_Stand_Alone_Nested(); + } + + [Fact(Skip = "TODO")] + public override void TVF_CrossApply_Correlated_Select_Anonymous() + { + base.TVF_CrossApply_Correlated_Select_Anonymous(); + } + + [Fact(Skip = "TODO")] + public override void TVF_Select_Direct_In_Anonymous() + { + base.TVF_Select_Direct_In_Anonymous(); + } + + [Fact(Skip = "TODO")] + public override void TVF_Select_Correlated_Direct_In_Anonymous() + { + base.TVF_Select_Correlated_Direct_In_Anonymous(); + } + + [Fact(Skip = "TODO")] + public override void TVF_Select_Correlated_Direct_With_Function_Query_Parameter_Correlated_In_Anonymous() + { + base.TVF_Select_Correlated_Direct_With_Function_Query_Parameter_Correlated_In_Anonymous(); + } + + [Fact(Skip = "TODO")] + public override void TVF_Select_Correlated_Subquery_In_Anonymous() + { + base.TVF_Select_Correlated_Subquery_In_Anonymous(); + } + + [Fact(Skip = "TODO")] + public override void TVF_Select_Correlated_Subquery_In_Anonymous_Nested() + { + base.TVF_Select_Correlated_Subquery_In_Anonymous_Nested(); + } + + [Fact(Skip = "TODO")] + public override void TVF_Select_NonCorrelated_Subquery_In_Anonymous() + { + base.TVF_Select_NonCorrelated_Subquery_In_Anonymous(); + } + + [Fact(Skip = "TODO")] + public override void TVF_Select_NonCorrelated_Subquery_In_Anonymous_Parameter() + { + base.TVF_Select_NonCorrelated_Subquery_In_Anonymous_Parameter(); + } + + [Fact(Skip = "TODO")] + public override void TVF_Correlated_Select_In_Anonymous() + { + base.TVF_Correlated_Select_In_Anonymous(); + } + + [Fact(Skip = "TODO")] + public override void TVF_CrossApply_Correlated_Select_Result() + { + base.TVF_CrossApply_Correlated_Select_Result(); + } + + [Fact(Skip = "TODO")] + public override void TVF_CrossJoin_Not_Correlated() + { + base.TVF_CrossJoin_Not_Correlated(); + } + + [Fact(Skip = "TODO")] + public override void TVF_CrossJoin_Parameter() + { + base.TVF_CrossJoin_Parameter(); + } + + [Fact(Skip = "TODO")] + public override void TVF_Join() + { + base.TVF_Join(); + } + + [Fact(Skip = "TODO")] + public override void TVF_LeftJoin_Select_Anonymous() + { + base.TVF_LeftJoin_Select_Anonymous(); + } + + [Fact(Skip = "TODO")] + public override void TVF_LeftJoin_Select_Result() + { + base.TVF_LeftJoin_Select_Result(); + } + + [Fact(Skip = "TODO")] + public override void TVF_OuterApply_Correlated_Select_TVF() + { + base.TVF_OuterApply_Correlated_Select_TVF(); + } + + [Fact(Skip = "TODO")] + public override void TVF_OuterApply_Correlated_Select_DbSet() + { + base.TVF_OuterApply_Correlated_Select_DbSet(); + } + + [Fact(Skip = "TODO")] + public override void TVF_OuterApply_Correlated_Select_Anonymous() + { + base.TVF_OuterApply_Correlated_Select_Anonymous(); + } + + [Fact(Skip = "TODO")] + public override void TVF_Nested() + { + base.TVF_Nested(); + } + + [Fact(Skip = "TODO")] + public override void TVF_Correlated_Nested_Func_Call() + { + base.TVF_Correlated_Nested_Func_Call(); + } + + #endregion + + + public class Oracle : BaseUdfFixture { protected override string StoreName { get; } = "UDFDbFunctionOracleTests"; protected override ITestStoreFactory TestStoreFactory => OracleTestStoreFactory.Instance; @@ -108,8 +252,8 @@ RETURN INTEGER IS RETURN NVARCHAR2 IS BEGIN RETURN customerName; -END;"); - +END;"); + context.SaveChanges(); } } diff --git a/src/EFCore.Relational.Specification.Tests/Query/UdfDbFunctionTestBase.cs b/src/EFCore.Relational.Specification.Tests/Query/UdfDbFunctionTestBase.cs index 2fc5a7c935b..de4da459d43 100644 --- a/src/EFCore.Relational.Specification.Tests/Query/UdfDbFunctionTestBase.cs +++ b/src/EFCore.Relational.Specification.Tests/Query/UdfDbFunctionTestBase.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Query.Expressions; using Microsoft.EntityFrameworkCore.TestUtilities; @@ -34,9 +36,16 @@ public class Order { public int Id { get; set; } public string Name { get; set; } - public int ItemCount { get; set; } + public int QuantitySold { get; set; } public DateTime OrderDate { get; set; } public Customer Customer { get; set; } + public Product Product { get; set; } + } + + public class Product + { + public int Id { get; set; } + public string Name { get; set; } } protected class UDFSqlContext : PoolableDbContext @@ -45,11 +54,14 @@ protected class UDFSqlContext : PoolableDbContext public DbSet Customers { get; set; } public DbSet Orders { get; set; } + public DbSet Products { get; set; } #endregion #region Function Stubs + #region Static Functions + public enum ReportingPeriod { Winter = 0, @@ -95,6 +107,8 @@ public static int CustomerOrderCountWithClientStatic(int customerId) return 1; case 4: return 0; + case 5: + return 0; default: throw new Exception(); } @@ -157,6 +171,8 @@ public int CustomerOrderCountWithClientInstance(int customerId) return 1; case 4: return 0; + case 5: + return 0; default: throw new Exception(); } @@ -193,6 +209,65 @@ public static string IdentityString(string s) throw new NotImplementedException(); } + public string SCHEMA_NAME() + { + return Execute(() => SCHEMA_NAME()); + } + + public Task SCHEMA_NAME_Async() + { + return ExecuteAsync(() => SCHEMA_NAME()); + } + + public int AddValues(int a, int b) + { + return Execute(() => AddValues(a, b)); + } + + public int AddValues(Expression> a, int b) + { + return Execute(() => AddValues(a, b)); + } + + #endregion + + #region Table Functions + + public class OrderByYear + { + public int? CustomerId { get; set; } + public int? Count { get; set; } + public int? Year { get; set; } + } + + public IQueryable GetCustomerOrderCountByYear(int customerId) + { + return CreateQuery(() => GetCustomerOrderCountByYear(customerId)); + } + + public IQueryable GetCustomerOrderCountByYear(Expression> customerId) + { + return CreateQuery(() => GetCustomerOrderCountByYear(customerId)); + } + + public class TopSellingProduct + { + public int? ProductId { get; set; } + public int? AmountSold { get; set; } + } + + public IQueryable GetTopTwoSellingProducts() + { + return CreateQuery(() => GetTopTwoSellingProducts()); + } + + public IQueryable GetTopTwoSellingProductsCustomTranslation() + { + return CreateQuery(() => GetTopTwoSellingProductsCustomTranslation()); + } + + #endregion + #endregion public UDFSqlContext(DbContextOptions options) @@ -227,14 +302,25 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.HasDbFunction(typeof(UDFSqlContext).GetMethod(nameof(DollarValueInstance))).HasName("DollarValue"); + modelBuilder.HasDbFunction(typeof(UDFSqlContext).GetMethod(nameof(AddValues), new[] { typeof(int), typeof(int) })); + var methodInfo2 = typeof(UDFSqlContext).GetMethod(nameof(MyCustomLengthInstance)); modelBuilder.HasDbFunction(methodInfo2) .HasTranslation(args => new SqlFunctionExpression("len", methodInfo2.ReturnType, args)); + + //Bootstrap + modelBuilder.HasDbFunction(typeof(UDFSqlContext).GetMethod(nameof(SCHEMA_NAME))).HasName("SCHEMA_NAME").HasSchema(""); + + //Table + modelBuilder.HasDbFunction(typeof(UDFSqlContext).GetMethod(nameof(GetCustomerOrderCountByYear), new[] { typeof(int) })); + modelBuilder.HasDbFunction(typeof(UDFSqlContext).GetMethod(nameof(GetTopTwoSellingProducts))); + modelBuilder.HasDbFunction(typeof(UDFSqlContext).GetMethod(nameof(GetTopTwoSellingProductsCustomTranslation))) + .HasTranslation(args => new SqlFunctionExpression("GetTopTwoSellingProducts", typeof(TopSellingProduct), "dbo", args)); } } - public abstract class UdfFixtureBase : SharedStoreFixtureBase + public abstract class BaseUdfFixture : SharedStoreFixtureBase { protected override Type ContextType { get; } = typeof(UDFSqlContext); @@ -250,41 +336,68 @@ protected override void Seed(DbContext context) { context.Database.EnsureCreatedResiliently(); + var product1 = new Product + { + Name = "Product1" + }; + var product2 = new Product + { + Name = "Product2" + }; + var product3 = new Product + { + Name = "Product3" + }; + var product4 = new Product + { + Name = "Product4" + }; + var product5 = new Product + { + Name = "Product5" + }; + var order11 = new Order { Name = "Order11", - ItemCount = 4, - OrderDate = new DateTime(2000, 1, 20) + QuantitySold = 4, + OrderDate = new DateTime(2000, 1, 20), + Product = product1 }; var order12 = new Order { Name = "Order12", - ItemCount = 8, - OrderDate = new DateTime(2000, 2, 21) + QuantitySold = 8, + OrderDate = new DateTime(2000, 2, 21), + Product = product2 }; var order13 = new Order { Name = "Order13", - ItemCount = 15, - OrderDate = new DateTime(2000, 3, 20) + QuantitySold = 15, + OrderDate = new DateTime(2001, 3, 20), + Product = product3 }; var order21 = new Order { Name = "Order21", - ItemCount = 16, - OrderDate = new DateTime(2000, 4, 21) + QuantitySold = 16, + OrderDate = new DateTime(2000, 4, 21), + Product = product4 }; var order22 = new Order { Name = "Order22", - ItemCount = 23, - OrderDate = new DateTime(2000, 5, 20) + QuantitySold = 23, + OrderDate = new DateTime(2000, 5, 20), + Product = product1 }; var order31 = new Order { Name = "Order31", - ItemCount = 42, - OrderDate = new DateTime(2000, 6, 21) + QuantitySold = 42, + OrderDate = new DateTime(2001, 6, 21), + Product = product2 }; var customer1 = new Customer @@ -317,9 +430,15 @@ protected override void Seed(DbContext context) order31 } }; + var customer4 = new Customer + { + FirstName = "Customer", + LastName = "Four" + }; - ((UDFSqlContext)context).Customers.AddRange(customer1, customer2, customer3); + ((UDFSqlContext)context).Customers.AddRange(customer1, customer2, customer3, customer4); ((UDFSqlContext)context).Orders.AddRange(order11, order12, order13, order21, order22, order31); + ((UDFSqlContext)context).Products.AddRange(product1, product2, product3, product4, product5); } } @@ -329,14 +448,14 @@ protected override void Seed(DbContext context) #region Static - [Fact] + [Fact] public virtual void Scalar_Function_Extension_Method_Static() { using (var context = CreateContext()) { var len = context.Customers.Count(c => UDFSqlContext.IsDateStatic(c.FirstName) == false); - Assert.Equal(3, len); + Assert.Equal(4, len); } } @@ -379,7 +498,7 @@ public virtual void Scalar_Function_Constant_Parameter_Static() var custs = context.Customers.Select(c => UDFSqlContext.CustomerOrderCountStatic(customerId)).ToList(); - Assert.Equal(3, custs.Count); + Assert.Equal(4, custs.Count); } } @@ -622,8 +741,8 @@ public virtual void Scalar_Nested_Function_Unwind_Client_Eval_OrderBy_Static() orderby UDFSqlContext.AddOneStatic(c.Id) select c.Id).ToList(); - Assert.Equal(3, results.Count); - Assert.True(results.SequenceEqual(Enumerable.Range(1, 3))); + Assert.Equal(4, results.Count); + Assert.True(results.SequenceEqual(Enumerable.Range(1, 4))); } } @@ -636,8 +755,8 @@ public virtual void Scalar_Nested_Function_Unwind_Client_Eval_Select_Static() orderby c.Id select UDFSqlContext.AddOneStatic(c.Id)).ToList(); - Assert.Equal(3, results.Count); - Assert.True(results.SequenceEqual(Enumerable.Range(2, 3))); + Assert.Equal(4, results.Count); + Assert.True(results.SequenceEqual(Enumerable.Range(2, 4))); } } @@ -839,7 +958,7 @@ public virtual void Scalar_Function_Extension_Method_Instance() { var len = context.Customers.Count(c => context.IsDateInstance(c.FirstName) == false); - Assert.Equal(3, len); + Assert.Equal(4, len); } } @@ -882,7 +1001,7 @@ public virtual void Scalar_Function_Constant_Parameter_Instance() var custs = context.Customers.Select(c => context.CustomerOrderCountInstance(customerId)).ToList(); - Assert.Equal(3, custs.Count); + Assert.Equal(4, custs.Count); } } @@ -1125,8 +1244,8 @@ public virtual void Scalar_Nested_Function_Unwind_Client_Eval_OrderBy_Instance() orderby context.AddOneInstance(c.Id) select c.Id).ToList(); - Assert.Equal(3, results.Count); - Assert.True(results.SequenceEqual(Enumerable.Range(1, 3))); + Assert.Equal(4, results.Count); + Assert.True(results.SequenceEqual(Enumerable.Range(1, 4))); } } @@ -1139,8 +1258,8 @@ public virtual void Scalar_Nested_Function_Unwind_Client_Eval_Select_Instance() orderby c.Id select context.AddOneInstance(c.Id)).ToList(); - Assert.Equal(3, results.Count); - Assert.True(results.SequenceEqual(Enumerable.Range(2, 3))); + Assert.Equal(4, results.Count); + Assert.True(results.SequenceEqual(Enumerable.Range(2, 4))); } } @@ -1303,5 +1422,674 @@ public virtual void Scalar_Nested_Function_UDF_BCL_Instance() #endregion #endregion + + #region BootStrap + + [Fact] + public virtual void BootstrapScalarNoParams() + { + using (var context = CreateContext()) + { + var schame = context.SCHEMA_NAME(); + + Assert.Equal("dbo", schame); + } + } + + [Fact] + public virtual async Task BootstrapScalarNoParamsAsync() + { + using (var context = CreateContext()) + { + var schema = await context.SCHEMA_NAME_Async(); + + Assert.Equal("dbo", schema); + } + } + + [Fact] + public virtual void BootstrapScalarParams() + { + using (var context = CreateContext()) + { + var value = context.AddValues(1, 2); + + Assert.Equal(3, value); + } + } + + [Fact] + public virtual void BootstrapScalarFuncParams() + { + using (var context = CreateContext()) + { + var value = context.AddValues(() => context.AddValues(1, 2), 2); + + Assert.Equal(5, value); + } + } + + [Fact] + public virtual void BootstrapScalarFuncParamsWithVariable() + { + using (var context = CreateContext()) + { + var x = 5; + var value = context.AddValues(() => context.AddValues(x, 2), 2); + + Assert.Equal(9, value); + } + } + + [Fact] + public virtual void BootstrapScalarFuncParamsConstant() + { + using (var context = CreateContext()) + { + var value = context.AddValues(() => 1, 2); + + Assert.Equal(3, value); + } + } + #endregion + + #region Table Valued Tests + + [Fact] + public virtual void TVF_Stand_Alone() + { + using (var context = CreateContext()) + { + var products = (from t in context.GetTopTwoSellingProducts() + orderby t.ProductId + select t).ToList(); + + Assert.Equal(2, products.Count); + Assert.Equal(1, products[0].ProductId); + Assert.Equal(27, products[0].AmountSold); + Assert.Equal(2, products[1].ProductId); + Assert.Equal(50, products[1].AmountSold); + } + } + + [Fact] + public virtual void TVF_Stand_Alone_With_Translation() + { + using (var context = CreateContext()) + { + var products = (from t in context.GetTopTwoSellingProductsCustomTranslation() + orderby t.ProductId + select t).ToList(); + + Assert.Equal(2, products.Count); + Assert.Equal(1, products[0].ProductId); + Assert.Equal(27, products[0].AmountSold); + Assert.Equal(2, products[1].ProductId); + Assert.Equal(50, products[1].AmountSold); + } + } + + [Fact] + public virtual void TVF_Stand_Alone_Parameter() + { + using (var context = CreateContext()) + { + var orders = (from c in context.GetCustomerOrderCountByYear(1) + orderby c.Count descending + select c).ToList(); + + Assert.Equal(2, orders.Count); + Assert.Equal(2, orders[0].Count); + Assert.Equal(2000, orders[0].Year); + Assert.Equal(1, orders[1].Count); + Assert.Equal(2001, orders[1].Year); + } + } + + [Fact] + public virtual void TVF_Stand_Alone_Nested() + { + using (var context = CreateContext()) + { + var orders = (from r in context.GetCustomerOrderCountByYear(() => context.AddValues(-2, 3)) + orderby r.Count descending + select r).ToList(); + + Assert.Equal(2, orders.Count); + Assert.Equal(2, orders[0].Count); + Assert.Equal(2000, orders[0].Year); + Assert.Equal(1, orders[1].Count); + Assert.Equal(2001, orders[1].Year); + } + } + + [Fact] + public virtual void TVF_CrossApply_Correlated_Select_Anonymous() + { + using (var context = CreateContext()) + { + var orders = (from c in context.Customers + from r in context.GetCustomerOrderCountByYear(c.Id) + orderby c.Id, r.Year + select new + { + c.Id, + c.LastName, + r.Year, + r.Count + }).ToList(); + + Assert.Equal(4, orders.Count); + Assert.Equal(2, orders[0].Count); + Assert.Equal(1, orders[1].Count); + Assert.Equal(2, orders[2].Count); + Assert.Equal(1, orders[3].Count); + Assert.Equal(2000, orders[0].Year); + Assert.Equal(2001, orders[1].Year); + Assert.Equal(2000, orders[2].Year); + Assert.Equal(2001, orders[3].Year); + Assert.Equal(1, orders[0].Id); + Assert.Equal(1, orders[1].Id); + Assert.Equal(2, orders[2].Id); + Assert.Equal(3, orders[3].Id); + } + } + + [Fact] + public virtual void TVF_Select_Direct_In_Anonymous() + { + using (var context = CreateContext()) + { + var results = (from c in context.Customers + select new + { + c.Id, + Prods = context.GetTopTwoSellingProducts().ToList(), + }).ToList(); + + Assert.Equal(4, results.Count); + Assert.Equal(2, results[0].Prods.Count); + Assert.Equal(2, results[1].Prods.Count); + Assert.Equal(2, results[2].Prods.Count); + Assert.Equal(2, results[3].Prods.Count); + } + } + + [Fact] + public virtual void TVF_Select_Correlated_Direct_In_Anonymous() + { + using (var context = CreateContext()) + { + var results = (from c in context.Customers + select new + { + c.Id, + OrderCountYear = context.GetCustomerOrderCountByYear(c.Id).ToList() + }).ToList(); + + Assert.Equal(4, results.Count); + Assert.Equal(1, results[0].Id); + Assert.Equal(2, results[1].Id); + Assert.Equal(3, results[2].Id); + Assert.Equal(4, results[3].Id); + Assert.Equal(2, results[0].OrderCountYear.Count); + Assert.Equal(1, results[1].OrderCountYear.Count); + Assert.Equal(1, results[2].OrderCountYear.Count); + Assert.Equal(0, results[3].OrderCountYear.Count); + } + } + + [Fact] + public virtual void TVF_Select_Correlated_Direct_With_Function_Query_Parameter_Correlated_In_Anonymous() + { + using (var context = CreateContext()) + { + var results = (from c in context.Customers + where c.Id == 1 + select new + { + c.Id, + OrderCountYear = context.GetCustomerOrderCountByYear(context.AddValues(c.Id, 1)).ToList() + }).ToList(); + + Assert.Equal(1, results.Count); + Assert.Equal(1, results[0].Id); + Assert.Equal(1, results[0].OrderCountYear.Count); + } + } + + [Fact] + public virtual void TVF_Select_Correlated_Subquery_In_Anonymous() + { + using (var context = CreateContext()) + { + var results = (from c in context.Customers + select new + { + c.Id, + OrderCountYear = context.GetCustomerOrderCountByYear(c.Id).Where(o => o.Year == 2000).ToList() + }).ToList(); + + Assert.Equal(4, results.Count); + Assert.Equal(1, results[0].Id); + Assert.Equal(2, results[1].Id); + Assert.Equal(3, results[2].Id); + Assert.Equal(4, results[3].Id); + Assert.Equal(1, results[0].OrderCountYear.Count); + Assert.Equal(1, results[1].OrderCountYear.Count); + Assert.Equal(0, results[2].OrderCountYear.Count); + Assert.Equal(0, results[3].OrderCountYear.Count); + } + } + + [Fact] + public virtual void TVF_Select_Correlated_Subquery_In_Anonymous_Nested() + { + using (var context = CreateContext()) + { + var results = (from c in context.Customers + select new + { + c.Id, + OrderCountYear = context.GetCustomerOrderCountByYear(1).Where(o => o.Year == 2000).Select(o => new + { + OrderCountYearNested = context.GetCustomerOrderCountByYear(2000).Where(o2 => o.Year == 2001).ToList(), + Prods = context.GetTopTwoSellingProducts().ToList(), + }).ToList() + }).ToList(); + + Assert.Equal(4, results.Count); + Assert.Equal(1, results[0].OrderCountYear.Count); + Assert.Equal(2, results[0].OrderCountYear[0].Prods.Count); + Assert.Equal(0, results[0].OrderCountYear[0].OrderCountYearNested.Count); + Assert.Equal(1, results[1].OrderCountYear.Count); + Assert.Equal(2, results[1].OrderCountYear[0].Prods.Count); + Assert.Equal(0, results[1].OrderCountYear[0].OrderCountYearNested.Count); + Assert.Equal(1, results[2].OrderCountYear.Count); + Assert.Equal(2, results[2].OrderCountYear[0].Prods.Count); + Assert.Equal(0, results[2].OrderCountYear[0].OrderCountYearNested.Count); + Assert.Equal(1, results[3].OrderCountYear.Count); + Assert.Equal(2, results[3].OrderCountYear[0].Prods.Count); + Assert.Equal(0, results[3].OrderCountYear[0].OrderCountYearNested.Count); + } + } + + [Fact] + public virtual void TVF_Select_NonCorrelated_Subquery_In_Anonymous() + { + using (var context = CreateContext()) + { + var results = (from c in context.Customers + select new + { + c.Id, + Prods = context.GetTopTwoSellingProducts().Where(p => p.AmountSold == 27).Select(p => p.ProductId).ToList(), + }).ToList(); + + Assert.Equal(4, results.Count); + Assert.Equal(1, results[0].Prods.Count); + Assert.Equal(1, results[1].Prods.Count); + Assert.Equal(1, results[2].Prods.Count); + Assert.Equal(1, results[3].Prods.Count); + } + } + + [Fact] + public virtual void TVF_Select_NonCorrelated_Subquery_In_Anonymous_Parameter() + { + using (var context = CreateContext()) + { + var amount = 27; + + var results = (from c in context.Customers + select new + { + c.Id, + Prods = context.GetTopTwoSellingProducts().Where(p => p.AmountSold == amount).Select(p => p.ProductId).ToList(), + }).ToList(); + + Assert.Equal(4, results.Count); + Assert.Equal(1, results[0].Prods.Count); + Assert.Equal(1, results[1].Prods.Count); + Assert.Equal(1, results[2].Prods.Count); + Assert.Equal(1, results[3].Prods.Count); + } + } + + [Fact] + public virtual void TVF_Correlated_Select_In_Anonymous() + { + using (var context = CreateContext()) + { + var cust = (from c in context.Customers + orderby c.Id + select new + { + c.Id, + c.LastName, + Orders = context.GetCustomerOrderCountByYear(c.Id).ToList() + }).ToList(); + + Assert.Equal(4, cust.Count); + Assert.Equal(2, cust[0].Orders[0].Count); + Assert.Equal(2, cust[1].Orders[0].Count); + Assert.Equal(1, cust[2].Orders[0].Count); + Assert.Equal(0, cust[3].Orders.Count); + Assert.Equal("One", cust[0].LastName); + Assert.Equal("Two", cust[1].LastName); + Assert.Equal("Three", cust[2].LastName); + Assert.Equal("Four", cust[3].LastName); + Assert.Equal(1, cust[0].Id); + Assert.Equal(2, cust[1].Id); + Assert.Equal(3, cust[2].Id); + Assert.Equal(4, cust[3].Id); + } + } + + [Fact] + public virtual void TVF_CrossApply_Correlated_Select_Result() + { + using (var context = CreateContext()) + { + var orders = (from c in context.Customers + from r in context.GetCustomerOrderCountByYear(c.Id) + orderby r.Count descending, r.Year descending + select r).ToList(); + + Assert.Equal(4, orders.Count); + + Assert.Equal(4, orders.Count); + Assert.Equal(2, orders[0].Count); + Assert.Equal(2, orders[1].Count); + Assert.Equal(1, orders[2].Count); + Assert.Equal(1, orders[3].Count); + Assert.Equal(2000, orders[0].Year); + Assert.Equal(2000, orders[1].Year); + Assert.Equal(2001, orders[2].Year); + Assert.Equal(2001, orders[3].Year); + } + } + + [Fact] + public virtual void TVF_CrossJoin_Not_Correlated() + { + using (var context = CreateContext()) + { + var orders = (from c in context.Customers + from r in context.GetCustomerOrderCountByYear(2) + where c.Id == 2 + orderby r.Count + select new + { + c.Id, + c.LastName, + r.Year, + r.Count + }).ToList(); + + Assert.Equal(1, orders.Count); + + Assert.Equal(2, orders[0].Count); + Assert.Equal(2000, orders[0].Year); + } + } + + [Fact] + public virtual void TVF_CrossJoin_Parameter() + { + using (var context = CreateContext()) + { + var custId = 2; + + var orders = (from c in context.Customers + from r in context.GetCustomerOrderCountByYear(custId) + where c.Id == custId + orderby r.Count + select new + { + c.Id, + c.LastName, + r.Year, + r.Count + }).ToList(); + + Assert.Equal(1, orders.Count); + + Assert.Equal(2, orders[0].Count); + Assert.Equal(2000, orders[0].Year); + } + } + + [Fact] + public virtual void TVF_Join() + { + using (var context = CreateContext()) + { + var products = (from p in context.Products + join r in context.GetTopTwoSellingProducts() on p.Id equals r.ProductId + select new + { + p.Id, + p.Name, + r.AmountSold + }).OrderBy(p => p.Id).ToList(); + + Assert.Equal(2, products.Count); + Assert.Equal(1, products[0].Id); + Assert.Equal("Product1", products[0].Name); + Assert.Equal(27, products[0].AmountSold); + Assert.Equal(2, products[1].Id); + Assert.Equal("Product2", products[1].Name); + Assert.Equal(50, products[1].AmountSold); + } + } + + [Fact] + public virtual void TVF_LeftJoin_Select_Anonymous() + { + using (var context = CreateContext()) + { + var products = (from p in context.Products + join r in context.GetTopTwoSellingProducts() on p.Id equals r.ProductId into joinTable + from j in joinTable.DefaultIfEmpty() + orderby p.Id descending + select new + { + p.Id, + p.Name, + j.AmountSold + }).ToList(); + + Assert.Equal(5, products.Count); + Assert.Equal(5, products[0].Id); + Assert.Equal(null, products[0].AmountSold); + Assert.Equal("Product5", products[0].Name); + Assert.Equal(4, products[1].Id); + Assert.Equal(null, products[1].AmountSold); + Assert.Equal("Product4", products[1].Name); + Assert.Equal(3, products[2].Id); + Assert.Equal(null, products[2].AmountSold); + Assert.Equal("Product3", products[2].Name); + Assert.Equal(2, products[3].Id); + Assert.Equal(50, products[3].AmountSold); + Assert.Equal("Product2", products[3].Name); + Assert.Equal(1, products[4].Id); + Assert.Equal(27, products[4].AmountSold); + Assert.Equal("Product1", products[4].Name); + } + } + + [Fact] + public virtual void TVF_LeftJoin_Select_Result() + { + using (var context = CreateContext()) + { + var products = (from p in context.Products + join r in context.GetTopTwoSellingProducts() on p.Id equals r.ProductId into joinTable + from j in joinTable.DefaultIfEmpty() + orderby p.Id descending + select j).ToList(); + + Assert.Equal(5, products.Count); + Assert.Equal(null, products[0].ProductId); + Assert.Equal(null, products[0].AmountSold); + Assert.Equal(null, products[1].ProductId); + Assert.Equal(null, products[1].AmountSold); + Assert.Equal(null, products[2].ProductId); + Assert.Equal(null, products[2].AmountSold); + Assert.Equal(2, products[3].ProductId); + Assert.Equal(50, products[3].AmountSold); + Assert.Equal(1, products[4].ProductId); + Assert.Equal(27, products[4].AmountSold); + } + } + + [Fact] + public virtual void TVF_OuterApply_Correlated_Select_TVF() + { + using (var context = CreateContext()) + { + var orders = (from c in context.Customers + from r in context.GetCustomerOrderCountByYear(c.Id).DefaultIfEmpty() + orderby c.Id, r.Year + select r).ToList(); + + Assert.Equal(5, orders.Count); + + Assert.Equal(2, orders[0].Count); + Assert.Equal(1, orders[1].Count); + Assert.Equal(2, orders[2].Count); + Assert.Equal(1, orders[3].Count); + Assert.Null(orders[4].Count); + Assert.Equal(2000, orders[0].Year); + Assert.Equal(2001, orders[1].Year); + Assert.Equal(2000, orders[2].Year); + Assert.Equal(2001, orders[3].Year); + Assert.Null(orders[4].Year); + Assert.Equal(1, orders[0].CustomerId); + Assert.Equal(1, orders[1].CustomerId); + Assert.Equal(2, orders[2].CustomerId); + Assert.Equal(3, orders[3].CustomerId); + Assert.Null(orders[4].CustomerId); + } + } + + [Fact] + public virtual void TVF_OuterApply_Correlated_Select_DbSet() + { + using (var context = CreateContext()) + { + var custs = (from c in context.Customers + from r in context.GetCustomerOrderCountByYear(c.Id).DefaultIfEmpty() + orderby c.Id, r.Year + select c).ToList(); + + Assert.Equal(5, custs.Count); + + Assert.Equal(1, custs[0].Id); + Assert.Equal(1, custs[1].Id); + Assert.Equal(2, custs[2].Id); + Assert.Equal(3, custs[3].Id); + Assert.Equal(4, custs[4].Id); + Assert.Equal("One", custs[0].LastName); + Assert.Equal("One", custs[1].LastName); + Assert.Equal("Two", custs[2].LastName); + Assert.Equal("Three", custs[3].LastName); + Assert.Equal("Four", custs[4].LastName); + } + } + + [Fact] + public virtual void TVF_OuterApply_Correlated_Select_Anonymous() + { + using (var context = CreateContext()) + { + var orders = (from c in context.Customers + from r in context.GetCustomerOrderCountByYear(c.Id).DefaultIfEmpty() + orderby c.Id, r.Year + select new + { + c.Id, + c.LastName, + r.Year, + r.Count + }).ToList(); + + Assert.Equal(5, orders.Count); + + Assert.Equal(1, orders[0].Id); + Assert.Equal(1, orders[1].Id); + Assert.Equal(2, orders[2].Id); + Assert.Equal(3, orders[3].Id); + Assert.Equal(4, orders[4].Id); + Assert.Equal("One", orders[0].LastName); + Assert.Equal("One", orders[1].LastName); + Assert.Equal("Two", orders[2].LastName); + Assert.Equal("Three", orders[3].LastName); + Assert.Equal("Four", orders[4].LastName); + Assert.Equal(2, orders[0].Count); + Assert.Equal(1, orders[1].Count); + Assert.Equal(2, orders[2].Count); + Assert.Equal(1, orders[3].Count); + Assert.Null(orders[4].Count); + Assert.Equal(2000, orders[0].Year); + Assert.Equal(2001, orders[1].Year); + Assert.Equal(2000, orders[2].Year); + Assert.Equal(2001, orders[3].Year); + } + } + + [Fact] + public virtual void TVF_Nested() + { + using (var context = CreateContext()) + { + var custId = 2; + + var orders = (from c in context.Customers + from r in context.GetCustomerOrderCountByYear(context.AddValues(1, 1)) + where c.Id == custId + orderby r.Year + select new + { + c.Id, + c.LastName, + r.Year, + r.Count + }).ToList(); + + Assert.Equal(1, orders.Count); + + Assert.Equal(2, orders[0].Count); + Assert.Equal(2000, orders[0].Year); + } + } + + + [Fact] + public virtual void TVF_Correlated_Nested_Func_Call() + { + var custId = 2; + + using (var context = CreateContext()) + { + var orders = (from c in context.Customers + from r in context.GetCustomerOrderCountByYear(context.AddValues(c.Id, 1)) + where c.Id == custId + select new + { + c.Id, + r.Count, + r.Year + }).ToList(); + + Assert.Equal(1, orders.Count); + + Assert.Equal(1, orders[0].Count); + Assert.Equal(2001, orders[0].Year); + } + } + + #endregion } } diff --git a/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs b/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs index b4697af9d6c..0e6186c37f8 100644 --- a/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs +++ b/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Infrastructure.Internal; +using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations.Internal; @@ -21,6 +22,7 @@ using Microsoft.EntityFrameworkCore.Update.Internal; using Microsoft.EntityFrameworkCore.ValueGeneration; using Microsoft.Extensions.DependencyInjection; +using Remotion.Linq.Parsing.ExpressionVisitors.Transformation; using Remotion.Linq.Parsing.ExpressionVisitors.TreeEvaluation; namespace Microsoft.EntityFrameworkCore.Infrastructure @@ -175,6 +177,8 @@ public override EntityFrameworkServicesBuilder TryAddCoreServices() TryAdd(); TryAdd(); TryAdd(); + TryAdd(); + TryAdd(); TryAdd( p => @@ -203,6 +207,7 @@ public override EntityFrameworkServicesBuilder TryAddCoreServices() .AddDependencySingleton() .AddDependencySingleton() .AddDependencySingleton() + .AddDependencySingleton() .AddDependencyScoped() .AddDependencyScoped() .AddDependencyScoped() diff --git a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs index 744143a9f78..f8b533a5a0b 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs @@ -80,7 +80,8 @@ protected virtual void ValidateDbFunctions([NotNull] IModel model) if (dbFunction.Translation == null) { - if (RelationalDependencies.TypeMappingSource.FindMapping(methodInfo.ReturnType) == null) + if (dbFunction.IsIQueryable && model.FindEntityType(dbFunction.MethodInfo.ReturnType.GetGenericArguments()[0]) == null + || !dbFunction.IsIQueryable && RelationalDependencies.TypeMappingSource.FindMapping(methodInfo.ReturnType) == null) { throw new InvalidOperationException( RelationalStrings.DbFunctionInvalidReturnType( diff --git a/src/EFCore.Relational/Metadata/IDbFunction.cs b/src/EFCore.Relational/Metadata/IDbFunction.cs index 4daecfd6fee..fa8ac22aeda 100644 --- a/src/EFCore.Relational/Metadata/IDbFunction.cs +++ b/src/EFCore.Relational/Metadata/IDbFunction.cs @@ -28,6 +28,11 @@ public interface IDbFunction /// MethodInfo MethodInfo { get; } + /// + /// Whether this method returns IQueryable + /// + bool IsIQueryable { get; } + /// /// A translation callback for performing custom translation of the method call into a SQL expression fragment. /// diff --git a/src/EFCore.Relational/Metadata/Internal/DbFunction.cs b/src/EFCore.Relational/Metadata/Internal/DbFunction.cs index c38577ee2a4..4f97ba041cc 100644 --- a/src/EFCore.Relational/Metadata/Internal/DbFunction.cs +++ b/src/EFCore.Relational/Metadata/Internal/DbFunction.cs @@ -83,6 +83,23 @@ private DbFunction( RelationalStrings.DbFunctionInvalidReturnType(methodInfo.DisplayName(), methodInfo.ReturnType.ShortDisplayName())); } + if (methodInfo.ReturnType.IsGenericType + && methodInfo.ReturnType.GetGenericTypeDefinition() == typeof(IQueryable<>)) + { + if (methodInfo.IsStatic) + { + throw new ArgumentException( + RelationalStrings.DbFunctionQueryableNotStatic(methodInfo.DisplayName())); + } + + IsIQueryable = true; + + if (model.FindEntityType(methodInfo.ReturnType.GetGenericArguments()[0]) == null) + { + model.AddQueryType(methodInfo.ReturnType.GetGenericArguments()[0]); + } + } + MethodInfo = methodInfo; _model = model; @@ -187,6 +204,12 @@ private void UpdateNameConfigurationSource(ConfigurationSource configurationSour /// public virtual Func, Expression> Translation { get; set; } + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual bool IsIQueryable { get; set; } + /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index 937a4913e71..942235a11c0 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -893,6 +893,30 @@ public static string DbFunctionInvalidInstanceType([CanBeNull] object function, GetString("DbFunctionInvalidInstanceType", nameof(function), nameof(type)), function, type); + /// + /// The DbFunction '{function}' must return IQueryable. + /// + public static string DbFunctionQueryableFunctionMustReturnIQueryable([CanBeNull] object function) + => string.Format( + GetString("DbFunctionQueryableFunctionMustReturnIQueryable", nameof(function)), + function); + + /// + /// The DbFunction '{function}' is not registered with the model. + /// + public static string DbFunctionNotFound([CanBeNull] object function) + => string.Format( + GetString("DbFunctionNotFound", nameof(function)), + function); + + /// + /// IQueryable DbFunctions must be instance methods. '{function}' is static. + /// + public static string DbFunctionQueryableNotStatic([CanBeNull] object function) + => string.Format( + GetString("DbFunctionQueryableNotStatic", nameof(function)), + function); + /// /// An ambient transaction has been detected. The ambient transaction needs to be completed before beginning a transaction on this connection. /// diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index 9d003cd4827..5020b23cf50 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -441,6 +441,15 @@ The DbFunction '{function}' defined on type '{type}' must be either a static method or an instance method defined on a DbContext subclass. Instance methods on other types are not supported. + + The DbFunction '{function}' must return IQueryable. + + + The DbFunction '{function}' is not registered with the model. + + + IQueryable DbFunctions must be instance methods. '{function}' is static. + An ambient transaction has been detected. The ambient transaction needs to be completed before beginning a transaction on this connection. diff --git a/src/EFCore.Relational/Query/ExpressionVisitors/RelationalEntityQueryableExpressionVisitor.cs b/src/EFCore.Relational/Query/ExpressionVisitors/RelationalEntityQueryableExpressionVisitor.cs index 71345f0f58c..8bd90e59b7c 100644 --- a/src/EFCore.Relational/Query/ExpressionVisitors/RelationalEntityQueryableExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/ExpressionVisitors/RelationalEntityQueryableExpressionVisitor.cs @@ -33,6 +33,7 @@ public class RelationalEntityQueryableExpressionVisitor : EntityQueryableExpress private readonly IMaterializerFactory _materializerFactory; private readonly IShaperCommandContextFactory _shaperCommandContextFactory; private readonly IQuerySource _querySource; + private readonly ISqlTranslatingExpressionVisitorFactory _sqlTranslatingExpressionVisitorFactory; /// /// Creates a new instance of . @@ -53,6 +54,7 @@ public RelationalEntityQueryableExpressionVisitor( _materializerFactory = dependencies.MaterializerFactory; _shaperCommandContextFactory = dependencies.ShaperCommandContextFactory; _querySource = querySource; + _sqlTranslatingExpressionVisitorFactory = dependencies.SqlTranslatingExpressionVisitorFactory; } private new RelationalQueryModelVisitor QueryModelVisitor => (RelationalQueryModelVisitor)base.QueryModelVisitor; @@ -119,8 +121,15 @@ protected override Expression VisitMethodCall(MethodCallExpression node) { Check.NotNull(node, nameof(node)); - QueryModelVisitor - .BindMethodCallExpression( + var dbFunc = _model.Relational().FindDbFunction(node.Method); + + if (dbFunc != null && dbFunc.IsIQueryable) + { + return VisitDbFunctionSourceExpression(new DbFunctionSourceExpression(node, _model)); + } + else + { + QueryModelVisitor.BindMethodCallExpression( node, (property, querySource, selectExpression) => selectExpression.AddToProjection( @@ -128,7 +137,26 @@ protected override Expression VisitMethodCall(MethodCallExpression node) querySource), bindSubQueries: true); - return base.VisitMethodCall(node); + return base.VisitMethodCall(node); + } + } + + /// + /// Visits Extension nodes. + /// + /// The node being visited. + /// An expression to use in place of the node. + protected override Expression VisitExtension(Expression node) + { + Check.NotNull(node, nameof(node)); + + switch (node) + { + case DbFunctionSourceExpression dbNode: + return VisitDbFunctionSourceExpression(dbNode); + default: + return base.VisitExtension(node); + } } /// @@ -236,6 +264,58 @@ var useQueryComposition Expression.Constant(shaper)); } + /// + /// Visit a node. + /// + /// The node being visited. + /// An Exprssion corresponding to the translated DbFunctionSourceExpression. + protected virtual Expression VisitDbFunctionSourceExpression(DbFunctionSourceExpression dbFunctionSourceExpression) + { + Check.NotNull(dbFunctionSourceExpression, nameof(dbFunctionSourceExpression)); + + var relationalQueryCompilationContext = QueryModelVisitor.QueryCompilationContext; + var selectExpression = _selectExpressionFactory.Create(relationalQueryCompilationContext); + + QueryModelVisitor.AddQuery(_querySource, selectExpression); + + var sqlTranslatingExpressionVisitor = _sqlTranslatingExpressionVisitorFactory.Create(QueryModelVisitor); + + var sqlFuncExpression = (SqlFunctionExpression)sqlTranslatingExpressionVisitor.Visit(dbFunctionSourceExpression); + + Func querySqlGeneratorFunc = selectExpression.CreateDefaultQuerySqlGenerator; + + Shaper shaper; + + if (dbFunctionSourceExpression.IsIQueryable) + { + var tableAlias + = relationalQueryCompilationContext.CreateUniqueTableAlias( + _querySource.HasGeneratedItemName() + ? dbFunctionSourceExpression.Name[0].ToString().ToLowerInvariant() + : _querySource.ItemName); + + selectExpression.AddTable(new QuerableSqlFunctionExpression(sqlFuncExpression, _querySource, tableAlias)); + + var entityType = _model.FindEntityType(dbFunctionSourceExpression.ReturnType); + + shaper = CreateShaper(dbFunctionSourceExpression.ReturnType, entityType, selectExpression); + } + else + { + selectExpression.AddToProjection(sqlFuncExpression); + + shaper = new ValueBufferShaper(_querySource); + } + + return Expression.Call( + QueryModelVisitor.QueryCompilationContext.QueryMethodProvider + .ShapedQueryMethod + .MakeGenericMethod(shaper.Type), + EntityQueryModelVisitor.QueryContextParameter, + Expression.Constant(_shaperCommandContextFactory.Create(querySqlGeneratorFunc)), + Expression.Constant(shaper)); + } + private Shaper CreateShaper(Type elementType, IEntityType entityType, SelectExpression selectExpression) { Shaper shaper; diff --git a/src/EFCore.Relational/Query/ExpressionVisitors/RelationalEntityQueryableExpressionVisitorDependencies.cs b/src/EFCore.Relational/Query/ExpressionVisitors/RelationalEntityQueryableExpressionVisitorDependencies.cs index 937d86d0448..18d0fba69b5 100644 --- a/src/EFCore.Relational/Query/ExpressionVisitors/RelationalEntityQueryableExpressionVisitorDependencies.cs +++ b/src/EFCore.Relational/Query/ExpressionVisitors/RelationalEntityQueryableExpressionVisitorDependencies.cs @@ -47,21 +47,25 @@ public sealed class RelationalEntityQueryableExpressionVisitorDependencies /// The select expression factory. /// The materializer factory. /// The shaper command context factory. + /// The sql translating expression factory. public RelationalEntityQueryableExpressionVisitorDependencies( [NotNull] IModel model, [NotNull] ISelectExpressionFactory selectExpressionFactory, [NotNull] IMaterializerFactory materializerFactory, - [NotNull] IShaperCommandContextFactory shaperCommandContextFactory) + [NotNull] IShaperCommandContextFactory shaperCommandContextFactory, + [NotNull] ISqlTranslatingExpressionVisitorFactory sqlTranslatingExpressionVisitorFactory) { Check.NotNull(model, nameof(model)); Check.NotNull(selectExpressionFactory, nameof(selectExpressionFactory)); Check.NotNull(materializerFactory, nameof(materializerFactory)); Check.NotNull(shaperCommandContextFactory, nameof(shaperCommandContextFactory)); + Check.NotNull(sqlTranslatingExpressionVisitorFactory, nameof(sqlTranslatingExpressionVisitorFactory)); Model = model; SelectExpressionFactory = selectExpressionFactory; MaterializerFactory = materializerFactory; ShaperCommandContextFactory = shaperCommandContextFactory; + SqlTranslatingExpressionVisitorFactory = sqlTranslatingExpressionVisitorFactory; } /// @@ -84,6 +88,11 @@ public RelationalEntityQueryableExpressionVisitorDependencies( /// public IShaperCommandContextFactory ShaperCommandContextFactory { get; } + /// + /// The sql translating expression factory. + /// + public ISqlTranslatingExpressionVisitorFactory SqlTranslatingExpressionVisitorFactory { get; } + /// /// Clones this dependency parameter object with one service replaced. /// @@ -94,7 +103,8 @@ public RelationalEntityQueryableExpressionVisitorDependencies With([NotNull] IMo model, SelectExpressionFactory, MaterializerFactory, - ShaperCommandContextFactory); + ShaperCommandContextFactory, + SqlTranslatingExpressionVisitorFactory); /// /// Clones this dependency parameter object with one service replaced. @@ -106,7 +116,8 @@ public RelationalEntityQueryableExpressionVisitorDependencies With([NotNull] ISe Model, selectExpressionFactory, MaterializerFactory, - ShaperCommandContextFactory); + ShaperCommandContextFactory, + SqlTranslatingExpressionVisitorFactory); /// /// Clones this dependency parameter object with one service replaced. @@ -118,7 +129,8 @@ public RelationalEntityQueryableExpressionVisitorDependencies With([NotNull] IMa Model, SelectExpressionFactory, materializerFactory, - ShaperCommandContextFactory); + ShaperCommandContextFactory, + SqlTranslatingExpressionVisitorFactory); /// /// Clones this dependency parameter object with one service replaced. @@ -130,6 +142,20 @@ public RelationalEntityQueryableExpressionVisitorDependencies With([NotNull] ISh Model, SelectExpressionFactory, MaterializerFactory, - shaperCommandContextFactory); + shaperCommandContextFactory, + SqlTranslatingExpressionVisitorFactory); + + /// + /// Clones this dependency parameter object with one service replaced. + /// + /// A replacement for the current dependency of this type. + /// A new parameter object with the given service replaced. + public RelationalEntityQueryableExpressionVisitorDependencies With([NotNull] ISqlTranslatingExpressionVisitorFactory sqlTranslatingExpressionVisitorFactory) + => new RelationalEntityQueryableExpressionVisitorDependencies( + Model, + SelectExpressionFactory, + MaterializerFactory, + ShaperCommandContextFactory, + sqlTranslatingExpressionVisitorFactory); } } diff --git a/src/EFCore.Relational/Query/ExpressionVisitors/SqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/ExpressionVisitors/SqlTranslatingExpressionVisitor.cs index f3f6cee5a72..be0fbe0729f 100644 --- a/src/EFCore.Relational/Query/ExpressionVisitors/SqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/ExpressionVisitors/SqlTranslatingExpressionVisitor.cs @@ -1144,7 +1144,14 @@ var equalityExpression return newOperand != nullCompensatedExpression.Operand ? new NullCompensatedExpression(newOperand) : nullCompensatedExpression; - + + case DbFunctionSourceExpression dbFunctionExpression: + + var newArguments = Visit(dbFunctionExpression.Arguments); + + return dbFunctionExpression.Translation?.Invoke(newArguments) ?? + new SqlFunctionExpression(dbFunctionExpression.Name, dbFunctionExpression.UnwrappedType, dbFunctionExpression.Schema, newArguments); + case DiscriminatorPredicateExpression discriminatorPredicateExpression: return new DiscriminatorPredicateExpression( base.VisitExtension(expression), discriminatorPredicateExpression.QuerySource); diff --git a/src/EFCore.Relational/Query/Expressions/CrossJoinLateralOuterExpression.cs b/src/EFCore.Relational/Query/Expressions/CrossJoinLateralOuterExpression.cs new file mode 100644 index 00000000000..fb111934a3b --- /dev/null +++ b/src/EFCore.Relational/Query/Expressions/CrossJoinLateralOuterExpression.cs @@ -0,0 +1,86 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq.Expressions; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Query.Sql; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Query.Expressions +{ + /// + /// Represents a SQL CROSS JOIN LATERAL OUTER expression. + /// + public class CrossJoinLateralOuterExpression : JoinExpressionBase + { + /// + /// Creates a new instance of CrossJoinLateralExpression. + /// + /// The target table expression. + public CrossJoinLateralOuterExpression([NotNull] TableExpressionBase tableExpression) + : base(Check.NotNull(tableExpression, nameof(tableExpression))) + { + } + + /// + /// Dispatches to the specific visit method for this node type. + /// + protected override Expression Accept(ExpressionVisitor visitor) + { + Check.NotNull(visitor, nameof(visitor)); + + return visitor is ISqlExpressionVisitor specificVisitor + ? specificVisitor.VisitCrossJoinLateralOuter(this) + : base.Accept(visitor); + } + + /// + /// Tests if this object is considered equal to another. + /// + /// The object to compare with the current object. + /// + /// true if the objects are considered equal, false if they are not. + /// + public override bool Equals(object obj) + { + if (obj is null) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + return obj.GetType() == GetType() && Equals((CrossJoinLateralExpression)obj); + } + + private bool Equals(CrossJoinLateralExpression other) + => string.Equals(Alias, other.Alias) + && Equals(QuerySource, other.QuerySource); + + /// + /// Returns a hash code for this object. + /// + /// + /// A hash code for this object. + /// + public override int GetHashCode() + { + unchecked + { + var hashCode = Alias?.GetHashCode() ?? 0; + hashCode = (hashCode * 397) ^ (QuerySource?.GetHashCode() ?? 0); + + return hashCode; + } + } + + /// + /// Creates a representation of the Expression. + /// + /// A representation of the Expression. + public override string ToString() => "CROSS JOIN LATERAL OUTER " + TableExpression; + } +} diff --git a/src/EFCore.Relational/Query/Expressions/DbFunctionSourceExpression.cs b/src/EFCore.Relational/Query/Expressions/DbFunctionSourceExpression.cs new file mode 100644 index 00000000000..5bc7fd60ac5 --- /dev/null +++ b/src/EFCore.Relational/Query/Expressions/DbFunctionSourceExpression.cs @@ -0,0 +1,176 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Linq.Expressions; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Query.Expressions +{ + /// + /// Represents a Db Function which acts as a query source in the ReLinq parse tree. + /// + public class DbFunctionSourceExpression : Expression + { + private readonly IDbFunction _dbFunction; + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public override ExpressionType NodeType => ExpressionType.Extension; + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public override Type Type { get; } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual Type ReturnType { get; } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual string Schema => _dbFunction.Schema; + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual Type UnwrappedType => Type.IsGenericType ? Type.GetGenericArguments()[0] : Type; + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual string Name => _dbFunction.FunctionName; + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual bool IsIQueryable => _dbFunction.IsIQueryable; + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual ReadOnlyCollection Arguments { get; [param: NotNull] set; } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual Func, Expression> Translation => _dbFunction.Translation ; + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public DbFunctionSourceExpression([NotNull] MethodCallExpression expression, [NotNull] IModel model) + { + Check.NotNull(expression, nameof(expression)); + Check.NotNull(model, nameof(model)); + + _dbFunction = FindDbFunction(expression, model); + Arguments = expression.Arguments; + + if (expression.Method.ReturnType.IsGenericType) + { + if (expression.Method.ReturnType.GetGenericTypeDefinition() != typeof(IQueryable<>)) + { + throw new InvalidOperationException( + RelationalStrings.DbFunctionQueryableFunctionMustReturnIQueryable(_dbFunction.FunctionName)); + } + + Type = expression.Method.ReturnType; + ReturnType = expression.Method.ReturnType.GetGenericArguments()[0]; + } + else + { + Type = typeof(IEnumerable<>).MakeGenericType(expression.Method.ReturnType); + ReturnType = expression.Method.ReturnType; + } + } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public DbFunctionSourceExpression([NotNull] DbFunctionSourceExpression oldFuncExpression, [NotNull] ReadOnlyCollection newArguments) + { + Check.NotNull(oldFuncExpression, nameof(oldFuncExpression)); + Check.NotNull(newArguments, nameof(newArguments)); + + Arguments = new ReadOnlyCollection(newArguments); + _dbFunction = oldFuncExpression._dbFunction; + ReturnType = oldFuncExpression.ReturnType; + Type = oldFuncExpression.Type; + } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + Check.NotNull(visitor, nameof(visitor)); + + var newArguments = visitor.Visit(Arguments); + + if (visitor is ParameterExtractingExpressionVisitor) + { + newArguments = new ReadOnlyCollection(newArguments.Select(a => a is LambdaExpression l ? l.Body : a).ToList()); + } + + return newArguments != Arguments + ? new DbFunctionSourceExpression(this, newArguments) + : this; + } + + private IDbFunction FindDbFunction(MethodCallExpression exp, IModel model) + { + var method = exp.Method.DeclaringType.GetMethod( + exp.Method.Name, + exp.Method.GetParameters() + .Select(p => UnwrapParamterType(p.ParameterType)) + .ToArray()); + + var dbFunction = model.Relational().FindDbFunction(method); + + if (dbFunction == null) + { + throw new InvalidOperationException( + RelationalStrings.DbFunctionNotFound(method.Name)); + } + + return dbFunction; + + Type UnwrapParamterType(Type paramType) + { + if (paramType.IsGenericType + && paramType.GetGenericTypeDefinition() == typeof(Expression<>)) + { + var expressionType = paramType.GetGenericArguments()[0]; + + if (expressionType.IsGenericType + && expressionType.GetGenericTypeDefinition() == typeof(Func<>)) + { + return expressionType.GetGenericArguments().Last(); + } + } + + return paramType; + } + } + } +} diff --git a/src/EFCore.Relational/Query/Expressions/QuerableSqlFunctionExpression.cs b/src/EFCore.Relational/Query/Expressions/QuerableSqlFunctionExpression.cs new file mode 100644 index 00000000000..7cedaa227db --- /dev/null +++ b/src/EFCore.Relational/Query/Expressions/QuerableSqlFunctionExpression.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Linq.Expressions; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Query.Sql; +using Microsoft.EntityFrameworkCore.Utilities; +using Remotion.Linq.Clauses; + +namespace Microsoft.EntityFrameworkCore.Query.Expressions +{ + /// + /// Represents a SQL Table Valued Fuction in the sql generation tree. + /// + public class QuerableSqlFunctionExpression : TableExpressionBase + { + private readonly SqlFunctionExpression _sqlFunctionExpression; + + /// + /// The sql function expression representing the database function + /// + public virtual SqlFunctionExpression SqlFunctionExpression => _sqlFunctionExpression; + + /// + /// Creates a new instance of a QuerableSqlFunctionExpression. + /// + /// The sqlFunctionExprssion representing the database function. + /// The query source. + /// The alias. + public QuerableSqlFunctionExpression([NotNull] SqlFunctionExpression sqlFunction, [NotNull] IQuerySource querySource, [CanBeNull] string alias) + : this(sqlFunction.FunctionName, sqlFunction.Type, sqlFunction.Schema, sqlFunction.Arguments, querySource, alias) + { + } + + /// + /// Creates a new instance of a QuerableSqlFunctionExpression. + /// + /// The db function name. + /// The db function return type. + /// The schema. + /// The arguemnts to the db function. + /// The query source. + /// The alias. + public QuerableSqlFunctionExpression([NotNull] string functionName, + [NotNull] Type returnType, + [CanBeNull] string schema, + [NotNull] IEnumerable arguments, + [NotNull] IQuerySource querySource, + [CanBeNull] string alias) + : base(Check.NotNull(querySource, nameof(querySource)), alias) + { + Check.NotNull(functionName, nameof(functionName)); + Check.NotNull(returnType, nameof(returnType)); + Check.NotNull(arguments, nameof(arguments)); + + _sqlFunctionExpression = new SqlFunctionExpression(functionName, returnType, schema, arguments); + } + + /// + /// Convert this object into a string representation. + /// + /// A string that represents this object. + public override string ToString() + { + return _sqlFunctionExpression.ToString(); + } + + /// + /// Dispatches to the specific visit method for this node type. + /// + protected override Expression Accept(ExpressionVisitor visitor) + { + Check.NotNull(visitor, nameof(visitor)); + + return visitor is ISqlExpressionVisitor specificVisitor + ? specificVisitor.VisitQueryableSqlFunctionExpression(this) + : base.Accept(visitor); + } + + /// + /// Reduces the node and then calls the method passing the + /// reduced expression. + /// Throws an exception if the node isn't reducible. + /// + /// An instance of . + /// The expression being visited, or an expression which should replace it in the tree. + /// + /// Override this method to provide logic to walk the node's children. + /// A typical implementation will call visitor.Visit on each of its + /// children, and if any of them change, should return a new copy of + /// itself with the modified children. + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + Check.NotNull(visitor, nameof(visitor)); + + var newArguments = visitor.Visit(new ReadOnlyCollection(_sqlFunctionExpression.Arguments.ToList())); + + return !Equals(newArguments, _sqlFunctionExpression.Arguments) + ? new QuerableSqlFunctionExpression(new SqlFunctionExpression(_sqlFunctionExpression.FunctionName, Type, _sqlFunctionExpression.Schema, newArguments), QuerySource, Alias) + : this; + } + } +} diff --git a/src/EFCore.Relational/Query/Expressions/SelectExpression.cs b/src/EFCore.Relational/Query/Expressions/SelectExpression.cs index b4aa50496d3..4fdf7d4ab53 100644 --- a/src/EFCore.Relational/Query/Expressions/SelectExpression.cs +++ b/src/EFCore.Relational/Query/Expressions/SelectExpression.cs @@ -1175,6 +1175,32 @@ public virtual JoinExpressionBase AddCrossJoinLateral( return crossJoinLateralExpression; } + /// + /// Adds a SQL CROSS JOIN LATERAL to this SelectExpression. + /// + /// The target table expression. + /// A sequence of expressions that should be added to the projection. + public virtual JoinExpressionBase AddCrossJoinLateralOuter( + [NotNull] TableExpressionBase tableExpression, + [NotNull] IEnumerable projection) + { + Check.NotNull(tableExpression, nameof(tableExpression)); + Check.NotNull(projection, nameof(projection)); + + if (tableExpression is SelectExpression s && s.Tables.First() is QuerableSqlFunctionExpression) + { + tableExpression = s.Tables.First(); + projection = s.Projection; + } + + var crossJoinLateralOuterExpression = new CrossJoinLateralOuterExpression(tableExpression); + + _tables.Add(crossJoinLateralOuterExpression); + _projection.AddRange(projection); + + return crossJoinLateralOuterExpression; + } + /// /// Adds a SQL INNER JOIN to this SelectExpression. /// diff --git a/src/EFCore.Relational/Query/Internal/RelationalDbFunctionTransformer.cs b/src/EFCore.Relational/Query/Internal/RelationalDbFunctionTransformer.cs new file mode 100644 index 00000000000..e46779450d3 --- /dev/null +++ b/src/EFCore.Relational/Query/Internal/RelationalDbFunctionTransformer.cs @@ -0,0 +1,54 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq.Expressions; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Query.Expressions; +using Microsoft.EntityFrameworkCore.Utilities; +using Remotion.Linq.Parsing.ExpressionVisitors.Transformation; + +namespace Microsoft.EntityFrameworkCore.Query.Internal +{ + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public class RelationalDbFunctionTransformer : IExpressionTransformer + { + private readonly IModel _model; + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public RelationalDbFunctionTransformer([NotNull] IModel model) + { + Check.NotNull(model, nameof(model)); + + _model = model; + } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual Expression Transform(MethodCallExpression expression) + { + Check.NotNull(expression, nameof(expression)); + + if (_model.Relational().FindDbFunction(expression.Method)?.IsIQueryable == true) + { + return new DbFunctionSourceExpression(expression, _model); + } + + return expression; + } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual ExpressionType[] SupportedExpressionTypes => new[] { ExpressionType.Call }; + } +} diff --git a/src/EFCore.Relational/Query/Internal/RelationalIExpressionTranformationProvider.cs b/src/EFCore.Relational/Query/Internal/RelationalIExpressionTranformationProvider.cs new file mode 100644 index 00000000000..41d852ca027 --- /dev/null +++ b/src/EFCore.Relational/Query/Internal/RelationalIExpressionTranformationProvider.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq.Expressions; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Utilities; +using Remotion.Linq.Parsing.ExpressionVisitors.Transformation; + +namespace Microsoft.EntityFrameworkCore.Query.Internal +{ + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public class RelationalIExpressionTranformationProvider : IExpressionTranformationProvider + { + private readonly ExpressionTransformerRegistry _transformProvider; + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public RelationalIExpressionTranformationProvider([NotNull] IModel model) + { + Check.NotNull(model, nameof(model)); + + _transformProvider = ExpressionTransformerRegistry.CreateDefault(); + _transformProvider.Register(new RelationalDbFunctionTransformer(model)); + } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual IEnumerable GetTransformations(Expression expression) + { + return _transformProvider.GetTransformations(expression); + } + } +} diff --git a/src/EFCore.Relational/Query/Internal/RelationalResultOperatorHandler.cs b/src/EFCore.Relational/Query/Internal/RelationalResultOperatorHandler.cs index 5d186ef2ba1..f18680979b4 100644 --- a/src/EFCore.Relational/Query/Internal/RelationalResultOperatorHandler.cs +++ b/src/EFCore.Relational/Query/Internal/RelationalResultOperatorHandler.cs @@ -364,28 +364,32 @@ private static Expression HandleDefaultIfEmpty(HandlerContext handlerContext) return handlerContext.EvalOnClient(); } - var selectExpression = handlerContext.SelectExpression; + if (!(handlerContext.QueryModel.MainFromClause.FromExpression is DbFunctionSourceExpression + && handlerContext.QueryModelVisitor.QueryCompilationContext.IsLateralJoinOuterSupported)) + { + var selectExpression = handlerContext.SelectExpression; - selectExpression.PushDownSubquery(); - selectExpression.ExplodeStarProjection(); + selectExpression.PushDownSubquery(); + selectExpression.ExplodeStarProjection(); - var subquery = selectExpression.Tables.Single(); + var subquery = selectExpression.Tables.Single(); - selectExpression.ClearTables(); + selectExpression.ClearTables(); - var emptySelectExpression = handlerContext.SelectExpressionFactory.Create(handlerContext.QueryModelVisitor.QueryCompilationContext, "empty"); - emptySelectExpression.AddToProjection(new AliasExpression("empty", Expression.Constant(null))); + var emptySelectExpression = handlerContext.SelectExpressionFactory.Create(handlerContext.QueryModelVisitor.QueryCompilationContext, "empty"); + emptySelectExpression.AddToProjection(new AliasExpression("empty", Expression.Constant(null))); - selectExpression.AddTable(emptySelectExpression); + selectExpression.AddTable(emptySelectExpression); - var leftOuterJoinExpression = new LeftOuterJoinExpression(subquery); - var constant1 = Expression.Constant(1); + var leftOuterJoinExpression = new LeftOuterJoinExpression(subquery); + var constant1 = Expression.Constant(1); - leftOuterJoinExpression.Predicate = Expression.Equal(constant1, constant1); + leftOuterJoinExpression.Predicate = Expression.Equal(constant1, constant1); - selectExpression.AddTable(leftOuterJoinExpression); + selectExpression.AddTable(leftOuterJoinExpression); - selectExpression.ProjectStarTable = subquery; + selectExpression.ProjectStarTable = subquery; + } handlerContext.QueryModelVisitor.Expression = new DefaultIfEmptyExpressionVisitor( diff --git a/src/EFCore.Relational/Query/RelationalDbFunctionSourceFactory.cs b/src/EFCore.Relational/Query/RelationalDbFunctionSourceFactory.cs new file mode 100644 index 00000000000..d00eb7f87ab --- /dev/null +++ b/src/EFCore.Relational/Query/RelationalDbFunctionSourceFactory.cs @@ -0,0 +1,41 @@ +using System.Linq.Expressions; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Query.Expressions; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Query +{ + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public class RelationalDbFunctionSourceFactory : IDbFunctionSourceFactory + { + /// + /// Initializes a new instance of the class. + /// + /// Parameter object containing dependencies for this service. + public RelationalDbFunctionSourceFactory([NotNull] RelationalDbFunctionSourceFactoryDependencies dependencies) + { + Check.NotNull(dependencies, nameof(dependencies)); + + Dependencies = dependencies; + } + + /// + /// Parameter object containing dependencies for this service. + /// + protected virtual RelationalDbFunctionSourceFactoryDependencies Dependencies { get; } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual Expression GenerateDbFunctionSource(MethodCallExpression methodCall, IModel model) + => new DbFunctionSourceExpression( + Check.NotNull(methodCall, nameof(methodCall)), + Check.NotNull(model, nameof(model))); + } +} diff --git a/src/EFCore.Relational/Query/RelationalDbFunctionSourceFactoryDependencies.cs b/src/EFCore.Relational/Query/RelationalDbFunctionSourceFactoryDependencies.cs new file mode 100644 index 00000000000..5da9118d841 --- /dev/null +++ b/src/EFCore.Relational/Query/RelationalDbFunctionSourceFactoryDependencies.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + + +namespace Microsoft.EntityFrameworkCore.Query +{ + /// + /// + /// Service dependencies parameter class for . + /// + /// + /// This type is typically used by database providers (and other extensions). It is generally + /// not used in application code. + /// + /// + public sealed class RelationalDbFunctionSourceFactoryDependencies + { + } +} diff --git a/src/EFCore.Relational/Query/RelationalQueryCompilationContext.cs b/src/EFCore.Relational/Query/RelationalQueryCompilationContext.cs index eacae581283..434917806fd 100644 --- a/src/EFCore.Relational/Query/RelationalQueryCompilationContext.cs +++ b/src/EFCore.Relational/Query/RelationalQueryCompilationContext.cs @@ -78,6 +78,11 @@ var relationalQueryModelVisitor /// public virtual bool IsLateralJoinSupported => false; + /// + /// True if the current provider supports SQL OUTER LATERAL JOIN. + /// + public virtual bool IsLateralJoinOuterSupported => false; + /// /// Max length of the table alias supported by provider. /// diff --git a/src/EFCore.Relational/Query/RelationalQueryModelVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryModelVisitor.cs index 96a684652fa..01e895e6e47 100644 --- a/src/EFCore.Relational/Query/RelationalQueryModelVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryModelVisitor.cs @@ -40,12 +40,15 @@ public class RelationalQueryModelVisitor : EntityQueryModelVisitor /// /// A map of query source to select expression. /// - protected virtual Dictionary QueriesBySource { get; } = - new Dictionary(); + protected virtual Dictionary QueriesBySource { get; } + = new Dictionary(); private readonly Dictionary _subQueryModelVisitorsBySource = new Dictionary(); + private readonly Dictionary _delayBoundParameters + = new Dictionary(); + private readonly ISqlTranslatingExpressionVisitorFactory _sqlTranslatingExpressionVisitorFactory; private readonly ICompositePredicateExpressionVisitorFactory _compositePredicateExpressionVisitorFactory; private readonly IConditionalRemovingExpressionVisitorFactory _conditionalRemovingExpressionVisitorFactory; @@ -494,6 +497,23 @@ public virtual void VisitSubQueryModel([NotNull] QueryModel queryModel) VisitQueryModel(queryModel); } + /// + /// Visits the node. + /// + /// The node being visited. + /// The query. + public override void VisitMainFromClause( + MainFromClause fromClause, + QueryModel queryModel) + { + Check.NotNull(fromClause, nameof(fromClause)); + Check.NotNull(queryModel, nameof(queryModel)); + + base.VisitMainFromClause(fromClause, queryModel); + + BindDelayBoundParameters(); + } + /// /// Compile main from clause expression. /// @@ -1174,6 +1194,20 @@ public override void VisitSelectClause(SelectClause selectClause, QueryModel que Check.NotNull(selectClause, nameof(selectClause)); Check.NotNull(queryModel, nameof(queryModel)); + if ((selectClause.Selector.TryGetReferencedQuerySource() as MainFromClause)?.FromExpression is DbFunctionSourceExpression d + && !d.IsIQueryable) + { + var readExp = BindReadValueMethod(selectClause.Selector.Type, CurrentParameter, 0); + + Expression = Expression.Call( + LinqOperatorProvider.Select + .MakeGenericMethod(CurrentParameter.Type, readExp.Type), + Expression, + Expression.Lambda(readExp, CurrentParameter)); + + return; + } + base.VisitSelectClause(selectClause, queryModel); if (Expression is MethodCallExpression methodCallExpression @@ -1346,6 +1380,7 @@ protected override void OptimizeQueryModel( queryModel.TransformExpressions(new TypeIsExpressionTranslatingVisitor(QueryCompilationContext.Model).Visit); queryModel.TransformExpressions(new SubqueryProjectingSingleValueOptimizingExpressionVisitor().Visit); + queryModel.SelectClause.TransformExpressions(new DbFunctionSourceSubqueryInjector().Visit); } /// @@ -1362,6 +1397,60 @@ protected virtual void WarnClientEval( QueryCompilationContext.Logger.QueryClientEvaluationWarning(queryModel, queryModelElement); } + private class DbFunctionSourceSubqueryInjector : ExpressionVisitorBase + { + private bool _shouldInject; + + protected override Expression VisitNew(NewExpression expression) + { + _shouldInject = true; + + try + { + return base.VisitNew(expression); + } + finally + { + _shouldInject = false; + } + } + + protected override Expression VisitSubQuery(SubQueryExpression subQueryExpression) + { + var shouldInject = _shouldInject; + _shouldInject = false; + + try + { + return base.VisitSubQuery(subQueryExpression); + } + finally + { + _shouldInject = shouldInject; + } + } + + protected override Expression VisitExtension(Expression extensionExpression) + { + if (_shouldInject && extensionExpression is DbFunctionSourceExpression dbf) + { + return InjectSubquery(dbf); + } + + return base.VisitExtension(extensionExpression); + } + + private static Expression InjectSubquery(DbFunctionSourceExpression expression) + { + var targetType = expression.ReturnType; + var mainFromClause = new MainFromClause(targetType.Name.Substring(0, 1).ToLowerInvariant(), targetType, expression); + var selector = new QuerySourceReferenceExpression(mainFromClause); + + var subqueryModel = new QueryModel(mainFromClause, new SelectClause(selector)); + return new SubQueryExpression(subqueryModel); + } + } + private class TypeIsExpressionTranslatingVisitor : ExpressionVisitorBase { private readonly IModel _model; @@ -1516,9 +1605,17 @@ var innerShapedQuery var joinExpression = correlated - ? outerSelectExpression.AddCrossJoinLateral( - innerSelectExpression.Tables.First(), - innerSelectExpression.Projection) + ? QueryCompilationContext.IsLateralJoinOuterSupported + && innerShapedQuery?.Method.MethodIsClosedFormOf(LinqOperatorProvider.DefaultIfEmpty) == true + && (innerSelectExpression.Tables.First() is SelectExpression s + && s.Tables.First() is QuerableSqlFunctionExpression + || innerSelectExpression.Tables.First() is QuerableSqlFunctionExpression) + ? outerSelectExpression.AddCrossJoinLateralOuter( + innerSelectExpression.Tables.First(), + innerSelectExpression.Projection) + : outerSelectExpression.AddCrossJoinLateral( + innerSelectExpression.Tables.First(), + innerSelectExpression.Projection) : outerSelectExpression.AddCrossJoin( innerSelectExpression.Tables.First(), innerSelectExpression.Projection); @@ -2234,13 +2331,20 @@ var parameterWithSamePrefixCount _injectedParameters[parameterName] = propertyExpression; - Expression - = CreateInjectParametersExpression( - Expression, - new Dictionary - { - [parameterName] = propertyExpression - }); + if(Expression != null) + { + Expression + = CreateInjectParametersExpression( + Expression, + new Dictionary + { + [parameterName] = propertyExpression + }); + } + else + { + _delayBoundParameters.Add(parameterName, propertyExpression); + } return Expression.Parameter( property.ClrType, @@ -2265,6 +2369,14 @@ private bool CanBindToParentUsingOuterParameter(IQuerySource querySource) return result; } + private void BindDelayBoundParameters() + { + if(_delayBoundParameters.Count > 0) + { + Expression = CreateInjectParametersExpression(Expression, _delayBoundParameters); + } + } + private Expression CreateInjectParametersExpression(Expression expression, Dictionary parameters) { var parameterNameExpressions = new List(); diff --git a/src/EFCore.Relational/Query/Sql/DefaultQuerySqlGenerator.cs b/src/EFCore.Relational/Query/Sql/DefaultQuerySqlGenerator.cs index 1120705139b..265c5ebcbb7 100644 --- a/src/EFCore.Relational/Query/Sql/DefaultQuerySqlGenerator.cs +++ b/src/EFCore.Relational/Query/Sql/DefaultQuerySqlGenerator.cs @@ -928,6 +928,24 @@ public virtual Expression VisitCrossJoinLateral(CrossJoinLateralExpression cross return crossJoinLateralExpression; } + /// + /// Visit a CrossJoinLateralOuterExpression expression. + /// + /// The cross join lateral outer expression. + /// + /// An Expression. + /// + public virtual Expression VisitCrossJoinLateralOuter(CrossJoinLateralOuterExpression crossJoinLateralOuterExpression) + { + Check.NotNull(crossJoinLateralOuterExpression, nameof(crossJoinLateralOuterExpression)); + + _relationalCommandBuilder.Append("CROSS JOIN LATERAL OUTER"); + + Visit(crossJoinLateralOuterExpression.TableExpression); + + return crossJoinLateralOuterExpression; + } + /// /// Visit a SqlFragmentExpression. /// @@ -1613,6 +1631,20 @@ protected virtual void GenerateFunctionCall( _typeMapping = parentTypeMapping; } + /// + /// Visits a QuerableSqlFunctionExpression. + /// + /// The QuerableSqlFunctionExpression. + /// + /// An Expression. + /// + public virtual Expression VisitQueryableSqlFunctionExpression(QuerableSqlFunctionExpression querableSqlFunctionExpression) + { + Check.NotNull(querableSqlFunctionExpression, nameof(querableSqlFunctionExpression)); + + return VisitSqlFunction(querableSqlFunctionExpression.SqlFunctionExpression); + } + /// /// Visit a SQL ExplicitCastExpression. /// diff --git a/src/EFCore.Relational/Query/Sql/ISqlExpressionVisitor.cs b/src/EFCore.Relational/Query/Sql/ISqlExpressionVisitor.cs index 01932ab0074..e3be2d27659 100644 --- a/src/EFCore.Relational/Query/Sql/ISqlExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/Sql/ISqlExpressionVisitor.cs @@ -93,6 +93,15 @@ public interface ISqlExpressionVisitor /// Expression VisitCrossJoinLateral([NotNull] CrossJoinLateralExpression crossJoinLateralExpression); + /// + /// Visit a CrossJoinLateralOuterExpression. + /// + /// The cross join lateral outer expression. + /// + /// An Expression. + /// + Expression VisitCrossJoinLateralOuter([NotNull] CrossJoinLateralOuterExpression crossJoinLateralOuterExpression); + /// /// Visit an InnerJoinExpression. /// @@ -182,5 +191,14 @@ public interface ISqlExpressionVisitor /// An Expression. /// Expression VisitColumnReference([NotNull] ColumnReferenceExpression columnReferenceExpression); + + /// + /// Visit a QuerableSqlFunctionExpression. + /// + /// The QuerableSqlFunctionExpression + /// + /// An Expression. + /// + Expression VisitQueryableSqlFunctionExpression([NotNull] QuerableSqlFunctionExpression querableSqlFunctionExpression); } } diff --git a/src/EFCore.Relational/breakingchanges.netcore.json b/src/EFCore.Relational/breakingchanges.netcore.json index 1ed543f0725..d1a92992242 100644 --- a/src/EFCore.Relational/breakingchanges.netcore.json +++ b/src/EFCore.Relational/breakingchanges.netcore.json @@ -1,232 +1,247 @@ - [ - { - "TypeId": "public sealed class Microsoft.EntityFrameworkCore.Migrations.MigrationsSqlGeneratorDependencies", - "MemberId": "public .ctor(Microsoft.EntityFrameworkCore.Storage.IRelationalCommandBuilderFactory commandBuilderFactory, Microsoft.EntityFrameworkCore.Storage.ISqlGenerationHelper sqlGenerationHelper, Microsoft.EntityFrameworkCore.Storage.IRelationalTypeMapper typeMapper)", - "Kind": "Removal" - }, - { - "TypeId": "public sealed class Microsoft.EntityFrameworkCore.Update.UpdateSqlGeneratorDependencies", - "MemberId": "public .ctor(Microsoft.EntityFrameworkCore.Storage.ISqlGenerationHelper sqlGenerationHelper)", - "Kind": "Removal" - }, - { - "TypeId": "public sealed class Microsoft.EntityFrameworkCore.Storage.RelationalConnectionDependencies", - "MemberId": "public .ctor(Microsoft.EntityFrameworkCore.Infrastructure.IDbContextOptions contextOptions, Microsoft.EntityFrameworkCore.Diagnostics.IDiagnosticsLogger transactionLogger, Microsoft.EntityFrameworkCore.Diagnostics.IDiagnosticsLogger connectionLogger, Microsoft.EntityFrameworkCore.Storage.Internal.INamedConnectionStringResolver connectionStringResolver)", - "Kind": "Removal" - }, - { - "TypeId": "public sealed class Microsoft.EntityFrameworkCore.Migrations.HistoryRepositoryDependencies", - "MemberId": "public .ctor(Microsoft.EntityFrameworkCore.Storage.IRelationalDatabaseCreator databaseCreator, Microsoft.EntityFrameworkCore.Storage.IRawSqlCommandBuilder rawSqlCommandBuilder, Microsoft.EntityFrameworkCore.Storage.IRelationalConnection connection, Microsoft.EntityFrameworkCore.Infrastructure.IDbContextOptions options, Microsoft.EntityFrameworkCore.Migrations.IMigrationsModelDiffer modelDiffer, Microsoft.EntityFrameworkCore.Migrations.IMigrationsSqlGenerator migrationsSqlGenerator, Microsoft.EntityFrameworkCore.Storage.ISqlGenerationHelper sqlGenerationHelper)", - "Kind": "Removal" - }, - { - "TypeId": "public sealed class Microsoft.EntityFrameworkCore.Storage.RelationalDatabaseCreatorDependencies", - "MemberId": "public .ctor(Microsoft.EntityFrameworkCore.Metadata.IModel model, Microsoft.EntityFrameworkCore.Storage.IRelationalConnection connection, Microsoft.EntityFrameworkCore.Migrations.IMigrationsModelDiffer modelDiffer, Microsoft.EntityFrameworkCore.Migrations.IMigrationsSqlGenerator migrationsSqlGenerator, Microsoft.EntityFrameworkCore.Migrations.IMigrationCommandExecutor migrationCommandExecutor, Microsoft.EntityFrameworkCore.Storage.IExecutionStrategyFactory executionStrategyFactory)", - "Kind": "Removal" - }, - { - "TypeId": "public sealed class Microsoft.EntityFrameworkCore.Storage.RelationalValueBufferFactoryDependencies", - "MemberId": "public .ctor()", - "Kind": "Removal" - }, - { - "TypeId": "public sealed class Microsoft.EntityFrameworkCore.Query.Expressions.SelectExpressionDependencies", - "MemberId": "public .ctor(Microsoft.EntityFrameworkCore.Query.Sql.IQuerySqlGeneratorFactory querySqlGeneratorFactory)", - "Kind": "Removal" - }, - { - "TypeId": "public interface Microsoft.EntityFrameworkCore.Storage.IRelationalValueBufferFactoryFactory", - "MemberId": "Microsoft.EntityFrameworkCore.Storage.IRelationalValueBufferFactory Create(System.Collections.Generic.IReadOnlyList types)", - "Kind": "Addition" - }, - { - "TypeId": "public interface Microsoft.EntityFrameworkCore.Storage.IRelationalDatabaseCreator : Microsoft.EntityFrameworkCore.Storage.IDatabaseCreator", - "MemberId": "System.String GenerateCreateScript()", - "Kind": "Addition" - }, - { - "TypeId": "public interface Microsoft.EntityFrameworkCore.Design.IAnnotationCodeGenerator", - "MemberId": "Microsoft.EntityFrameworkCore.Design.MethodCallCodeFragment GenerateFluentApi(Microsoft.EntityFrameworkCore.Metadata.IEntityType entityType, Microsoft.EntityFrameworkCore.Infrastructure.IAnnotation annotation)", - "Kind": "Addition" - }, - { - "TypeId": "public interface Microsoft.EntityFrameworkCore.Design.IAnnotationCodeGenerator", - "MemberId": "Microsoft.EntityFrameworkCore.Design.MethodCallCodeFragment GenerateFluentApi(Microsoft.EntityFrameworkCore.Metadata.IForeignKey foreignKey, Microsoft.EntityFrameworkCore.Infrastructure.IAnnotation annotation)", - "Kind": "Addition" - }, - { - "TypeId": "public interface Microsoft.EntityFrameworkCore.Design.IAnnotationCodeGenerator", - "MemberId": "Microsoft.EntityFrameworkCore.Design.MethodCallCodeFragment GenerateFluentApi(Microsoft.EntityFrameworkCore.Metadata.IIndex index, Microsoft.EntityFrameworkCore.Infrastructure.IAnnotation annotation)", - "Kind": "Addition" - }, - { - "TypeId": "public interface Microsoft.EntityFrameworkCore.Design.IAnnotationCodeGenerator", - "MemberId": "Microsoft.EntityFrameworkCore.Design.MethodCallCodeFragment GenerateFluentApi(Microsoft.EntityFrameworkCore.Metadata.IKey key, Microsoft.EntityFrameworkCore.Infrastructure.IAnnotation annotation)", - "Kind": "Addition" - }, - { - "TypeId": "public interface Microsoft.EntityFrameworkCore.Design.IAnnotationCodeGenerator", - "MemberId": "Microsoft.EntityFrameworkCore.Design.MethodCallCodeFragment GenerateFluentApi(Microsoft.EntityFrameworkCore.Metadata.IModel model, Microsoft.EntityFrameworkCore.Infrastructure.IAnnotation annotation)", - "Kind": "Addition" - }, - { - "TypeId": "public interface Microsoft.EntityFrameworkCore.Design.IAnnotationCodeGenerator", - "MemberId": "Microsoft.EntityFrameworkCore.Design.MethodCallCodeFragment GenerateFluentApi(Microsoft.EntityFrameworkCore.Metadata.IProperty property, Microsoft.EntityFrameworkCore.Infrastructure.IAnnotation annotation)", - "Kind": "Addition" - }, - { - "TypeId": "public sealed class Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.SqlTranslatingExpressionVisitorDependencies", - "MemberId": "public .ctor(Microsoft.EntityFrameworkCore.Query.ExpressionTranslators.IExpressionFragmentTranslator compositeExpressionFragmentTranslator, Microsoft.EntityFrameworkCore.Query.ExpressionTranslators.ICompositeMethodCallTranslator methodCallTranslator, Microsoft.EntityFrameworkCore.Query.ExpressionTranslators.IMemberTranslator memberTranslator, Microsoft.EntityFrameworkCore.Storage.IRelationalTypeMapper relationalTypeMapper)", - "Kind": "Removal" - }, - { - "TypeId": "public sealed class Microsoft.EntityFrameworkCore.Query.Sql.QuerySqlGeneratorDependencies", - "MemberId": "public .ctor(Microsoft.EntityFrameworkCore.Storage.IRelationalCommandBuilderFactory commandBuilderFactory, Microsoft.EntityFrameworkCore.Storage.ISqlGenerationHelper sqlGenerationHelper, Microsoft.EntityFrameworkCore.Storage.IParameterNameGeneratorFactory parameterNameGeneratorFactory, Microsoft.EntityFrameworkCore.Storage.IRelationalTypeMapper relationalTypeMapper)", - "Kind": "Removal" - }, - { - "TypeId": "public sealed class Microsoft.EntityFrameworkCore.Infrastructure.RelationalModelValidatorDependencies", - "MemberId": "public .ctor(Microsoft.EntityFrameworkCore.Storage.IRelationalTypeMapper typeMapper)", - "Kind": "Removal" - }, - { - "TypeId": "public class Microsoft.EntityFrameworkCore.Migrations.MigrationBuilder", - "MemberId": "public virtual Microsoft.EntityFrameworkCore.Migrations.Operations.Builders.AlterOperationBuilder AlterColumn(System.String name, System.String table, System.String type = null, System.Nullable unicode = default(System.Nullable), System.Nullable maxLength = default(System.Nullable), System.Boolean rowVersion = False, System.String schema = null, System.Boolean nullable = False, System.Object defaultValue = null, System.String defaultValueSql = null, System.String computedColumnSql = null, System.Type oldClrType = null, System.String oldType = null, System.Nullable oldUnicode = default(System.Nullable), System.Nullable oldMaxLength = default(System.Nullable), System.Boolean oldRowVersion = False, System.Boolean oldNullable = False, System.Object oldDefaultValue = null, System.String oldDefaultValueSql = null, System.String oldComputedColumnSql = null)", - "Kind": "Removal" - }, - { - "TypeId": "public class Microsoft.EntityFrameworkCore.Migrations.MigrationBuilder", - "MemberId": "public virtual Microsoft.EntityFrameworkCore.Migrations.Operations.Builders.OperationBuilder AddColumn(System.String name, System.String table, System.String type = null, System.Nullable unicode = default(System.Nullable), System.Nullable maxLength = default(System.Nullable), System.Boolean rowVersion = False, System.String schema = null, System.Boolean nullable = False, System.Object defaultValue = null, System.String defaultValueSql = null, System.String computedColumnSql = null)", - "Kind": "Removal" - }, - { - "TypeId": "public class Microsoft.EntityFrameworkCore.Migrations.Operations.Builders.ColumnsBuilder", - "MemberId": "public virtual Microsoft.EntityFrameworkCore.Migrations.Operations.Builders.OperationBuilder Column(System.String type = null, System.Nullable unicode = default(System.Nullable), System.Nullable maxLength = default(System.Nullable), System.Boolean rowVersion = False, System.String name = null, System.Boolean nullable = False, System.Object defaultValue = null, System.String defaultValueSql = null, System.String computedColumnSql = null)", - "Kind": "Removal" - }, - { - "TypeId": "public interface Microsoft.EntityFrameworkCore.Metadata.IRelationalPropertyAnnotations", - "MemberId": "System.Boolean get_IsFixedLength()", - "Kind": "Addition" - }, - { - "TypeId": "public interface Microsoft.EntityFrameworkCore.Query.IQueryMethodProvider", - "MemberId": "System.Reflection.MethodInfo get_FastQueryMethod()", - "Kind": "Addition" - }, - { - "TypeId": "public class Microsoft.EntityFrameworkCore.Storage.BoolTypeMapping : Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping", - "MemberId": "public override Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping Clone(System.String storeType, System.Nullable size)", - "Kind": "Removal" - }, - { - "TypeId": "public class Microsoft.EntityFrameworkCore.Storage.ByteArrayTypeMapping : Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping", - "MemberId": "public override Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping Clone(System.String storeType, System.Nullable size)", - "Kind": "Removal" - }, - { - "TypeId": "public class Microsoft.EntityFrameworkCore.Storage.ByteTypeMapping : Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping", - "MemberId": "public override Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping Clone(System.String storeType, System.Nullable size)", - "Kind": "Removal" - }, - { - "TypeId": "public class Microsoft.EntityFrameworkCore.Storage.CharTypeMapping : Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping", - "MemberId": "public override Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping Clone(System.String storeType, System.Nullable size)", - "Kind": "Removal" - }, - { - "TypeId": "public class Microsoft.EntityFrameworkCore.Storage.DateTimeOffsetTypeMapping : Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping", - "MemberId": "public override Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping Clone(System.String storeType, System.Nullable size)", - "Kind": "Removal" - }, - { - "TypeId": "public class Microsoft.EntityFrameworkCore.Storage.DateTimeTypeMapping : Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping", - "MemberId": "public override Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping Clone(System.String storeType, System.Nullable size)", - "Kind": "Removal" - }, - { - "TypeId": "public class Microsoft.EntityFrameworkCore.Storage.DecimalTypeMapping : Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping", - "MemberId": "public override Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping Clone(System.String storeType, System.Nullable size)", - "Kind": "Removal" - }, - { - "TypeId": "public class Microsoft.EntityFrameworkCore.Storage.DoubleTypeMapping : Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping", - "MemberId": "public override Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping Clone(System.String storeType, System.Nullable size)", - "Kind": "Removal" - }, - { - "TypeId": "public class Microsoft.EntityFrameworkCore.Storage.FloatTypeMapping : Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping", - "MemberId": "public override Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping Clone(System.String storeType, System.Nullable size)", - "Kind": "Removal" - }, - { - "TypeId": "public class Microsoft.EntityFrameworkCore.Storage.GuidTypeMapping : Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping", - "MemberId": "public override Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping Clone(System.String storeType, System.Nullable size)", - "Kind": "Removal" - }, - { - "TypeId": "public class Microsoft.EntityFrameworkCore.Storage.IntTypeMapping : Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping", - "MemberId": "public override Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping Clone(System.String storeType, System.Nullable size)", - "Kind": "Removal" - }, - { - "TypeId": "public class Microsoft.EntityFrameworkCore.Storage.LongTypeMapping : Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping", - "MemberId": "public override Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping Clone(System.String storeType, System.Nullable size)", - "Kind": "Removal" - }, - { - "TypeId": "public class Microsoft.EntityFrameworkCore.Storage.SByteTypeMapping : Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping", - "MemberId": "public override Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping Clone(System.String storeType, System.Nullable size)", - "Kind": "Removal" - }, - { - "TypeId": "public class Microsoft.EntityFrameworkCore.Storage.ShortTypeMapping : Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping", - "MemberId": "public override Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping Clone(System.String storeType, System.Nullable size)", - "Kind": "Removal" - }, - { - "TypeId": "public class Microsoft.EntityFrameworkCore.Storage.StringTypeMapping : Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping", - "MemberId": "public override Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping Clone(System.String storeType, System.Nullable size)", - "Kind": "Removal" - }, - { - "TypeId": "public class Microsoft.EntityFrameworkCore.Storage.TimeSpanTypeMapping : Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping", - "MemberId": "public override Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping Clone(System.String storeType, System.Nullable size)", - "Kind": "Removal" - }, - { - "TypeId": "public class Microsoft.EntityFrameworkCore.Storage.UIntTypeMapping : Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping", - "MemberId": "public override Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping Clone(System.String storeType, System.Nullable size)", - "Kind": "Removal" - }, - { - "TypeId": "public class Microsoft.EntityFrameworkCore.Storage.ULongTypeMapping : Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping", - "MemberId": "public override Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping Clone(System.String storeType, System.Nullable size)", - "Kind": "Removal" - }, - { - "TypeId": "public class Microsoft.EntityFrameworkCore.Storage.UShortTypeMapping : Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping", - "MemberId": "public override Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping Clone(System.String storeType, System.Nullable size)", - "Kind": "Removal" - }, - { - "TypeId": "public interface Microsoft.EntityFrameworkCore.Storage.ISqlGenerationHelper", - "MemberId": "System.String GenerateParameterNamePlaceholder(System.String name)", - "Kind": "Addition" - }, - { - "TypeId": "public interface Microsoft.EntityFrameworkCore.Storage.ISqlGenerationHelper", - "MemberId": "System.Void GenerateParameterNamePlaceholder(System.Text.StringBuilder builder, System.String name)", - "Kind": "Addition" - }, - { - "TypeId": "public sealed class Microsoft.EntityFrameworkCore.Query.ExpressionTranslators.RelationalCompositeMemberTranslatorDependencies", - "MemberId": "public .ctor()", - "Kind": "Removal" - }, - { - "TypeId": "public sealed class Microsoft.EntityFrameworkCore.Query.ExpressionTranslators.RelationalCompositeMethodCallTranslatorDependencies", - "MemberId": "public .ctor(Microsoft.EntityFrameworkCore.Diagnostics.IDiagnosticsLogger logger)", - "Kind": "Removal" - } +[ + { + "TypeId": "public sealed class Microsoft.EntityFrameworkCore.Migrations.MigrationsSqlGeneratorDependencies", + "MemberId": "public .ctor(Microsoft.EntityFrameworkCore.Storage.IRelationalCommandBuilderFactory commandBuilderFactory, Microsoft.EntityFrameworkCore.Storage.ISqlGenerationHelper sqlGenerationHelper, Microsoft.EntityFrameworkCore.Storage.IRelationalTypeMapper typeMapper)", + "Kind": "Removal" + }, + { + "TypeId": "public sealed class Microsoft.EntityFrameworkCore.Update.UpdateSqlGeneratorDependencies", + "MemberId": "public .ctor(Microsoft.EntityFrameworkCore.Storage.ISqlGenerationHelper sqlGenerationHelper)", + "Kind": "Removal" + }, + { + "TypeId": "public sealed class Microsoft.EntityFrameworkCore.Storage.RelationalConnectionDependencies", + "MemberId": "public .ctor(Microsoft.EntityFrameworkCore.Infrastructure.IDbContextOptions contextOptions, Microsoft.EntityFrameworkCore.Diagnostics.IDiagnosticsLogger transactionLogger, Microsoft.EntityFrameworkCore.Diagnostics.IDiagnosticsLogger connectionLogger, Microsoft.EntityFrameworkCore.Storage.Internal.INamedConnectionStringResolver connectionStringResolver)", + "Kind": "Removal" + }, + { + "TypeId": "public sealed class Microsoft.EntityFrameworkCore.Migrations.HistoryRepositoryDependencies", + "MemberId": "public .ctor(Microsoft.EntityFrameworkCore.Storage.IRelationalDatabaseCreator databaseCreator, Microsoft.EntityFrameworkCore.Storage.IRawSqlCommandBuilder rawSqlCommandBuilder, Microsoft.EntityFrameworkCore.Storage.IRelationalConnection connection, Microsoft.EntityFrameworkCore.Infrastructure.IDbContextOptions options, Microsoft.EntityFrameworkCore.Migrations.IMigrationsModelDiffer modelDiffer, Microsoft.EntityFrameworkCore.Migrations.IMigrationsSqlGenerator migrationsSqlGenerator, Microsoft.EntityFrameworkCore.Storage.ISqlGenerationHelper sqlGenerationHelper)", + "Kind": "Removal" + }, + { + "TypeId": "public sealed class Microsoft.EntityFrameworkCore.Storage.RelationalDatabaseCreatorDependencies", + "MemberId": "public .ctor(Microsoft.EntityFrameworkCore.Metadata.IModel model, Microsoft.EntityFrameworkCore.Storage.IRelationalConnection connection, Microsoft.EntityFrameworkCore.Migrations.IMigrationsModelDiffer modelDiffer, Microsoft.EntityFrameworkCore.Migrations.IMigrationsSqlGenerator migrationsSqlGenerator, Microsoft.EntityFrameworkCore.Migrations.IMigrationCommandExecutor migrationCommandExecutor, Microsoft.EntityFrameworkCore.Storage.IExecutionStrategyFactory executionStrategyFactory)", + "Kind": "Removal" + }, + { + "TypeId": "public sealed class Microsoft.EntityFrameworkCore.Storage.RelationalValueBufferFactoryDependencies", + "MemberId": "public .ctor()", + "Kind": "Removal" + }, + { + "TypeId": "public sealed class Microsoft.EntityFrameworkCore.Query.Expressions.SelectExpressionDependencies", + "MemberId": "public .ctor(Microsoft.EntityFrameworkCore.Query.Sql.IQuerySqlGeneratorFactory querySqlGeneratorFactory)", + "Kind": "Removal" + }, + { + "TypeId": "public interface Microsoft.EntityFrameworkCore.Storage.IRelationalValueBufferFactoryFactory", + "MemberId": "Microsoft.EntityFrameworkCore.Storage.IRelationalValueBufferFactory Create(System.Collections.Generic.IReadOnlyList types)", + "Kind": "Addition" + }, + { + "TypeId": "public interface Microsoft.EntityFrameworkCore.Storage.IRelationalDatabaseCreator : Microsoft.EntityFrameworkCore.Storage.IDatabaseCreator", + "MemberId": "System.String GenerateCreateScript()", + "Kind": "Addition" + }, + { + "TypeId": "public interface Microsoft.EntityFrameworkCore.Design.IAnnotationCodeGenerator", + "MemberId": "Microsoft.EntityFrameworkCore.Design.MethodCallCodeFragment GenerateFluentApi(Microsoft.EntityFrameworkCore.Metadata.IEntityType entityType, Microsoft.EntityFrameworkCore.Infrastructure.IAnnotation annotation)", + "Kind": "Addition" + }, + { + "TypeId": "public interface Microsoft.EntityFrameworkCore.Design.IAnnotationCodeGenerator", + "MemberId": "Microsoft.EntityFrameworkCore.Design.MethodCallCodeFragment GenerateFluentApi(Microsoft.EntityFrameworkCore.Metadata.IForeignKey foreignKey, Microsoft.EntityFrameworkCore.Infrastructure.IAnnotation annotation)", + "Kind": "Addition" + }, + { + "TypeId": "public interface Microsoft.EntityFrameworkCore.Design.IAnnotationCodeGenerator", + "MemberId": "Microsoft.EntityFrameworkCore.Design.MethodCallCodeFragment GenerateFluentApi(Microsoft.EntityFrameworkCore.Metadata.IIndex index, Microsoft.EntityFrameworkCore.Infrastructure.IAnnotation annotation)", + "Kind": "Addition" + }, + { + "TypeId": "public interface Microsoft.EntityFrameworkCore.Design.IAnnotationCodeGenerator", + "MemberId": "Microsoft.EntityFrameworkCore.Design.MethodCallCodeFragment GenerateFluentApi(Microsoft.EntityFrameworkCore.Metadata.IKey key, Microsoft.EntityFrameworkCore.Infrastructure.IAnnotation annotation)", + "Kind": "Addition" + }, + { + "TypeId": "public interface Microsoft.EntityFrameworkCore.Design.IAnnotationCodeGenerator", + "MemberId": "Microsoft.EntityFrameworkCore.Design.MethodCallCodeFragment GenerateFluentApi(Microsoft.EntityFrameworkCore.Metadata.IModel model, Microsoft.EntityFrameworkCore.Infrastructure.IAnnotation annotation)", + "Kind": "Addition" + }, + { + "TypeId": "public interface Microsoft.EntityFrameworkCore.Design.IAnnotationCodeGenerator", + "MemberId": "Microsoft.EntityFrameworkCore.Design.MethodCallCodeFragment GenerateFluentApi(Microsoft.EntityFrameworkCore.Metadata.IProperty property, Microsoft.EntityFrameworkCore.Infrastructure.IAnnotation annotation)", + "Kind": "Addition" + }, + { + "TypeId": "public sealed class Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.SqlTranslatingExpressionVisitorDependencies", + "MemberId": "public .ctor(Microsoft.EntityFrameworkCore.Query.ExpressionTranslators.IExpressionFragmentTranslator compositeExpressionFragmentTranslator, Microsoft.EntityFrameworkCore.Query.ExpressionTranslators.ICompositeMethodCallTranslator methodCallTranslator, Microsoft.EntityFrameworkCore.Query.ExpressionTranslators.IMemberTranslator memberTranslator, Microsoft.EntityFrameworkCore.Storage.IRelationalTypeMapper relationalTypeMapper)", + "Kind": "Removal" + }, + { + "TypeId": "public sealed class Microsoft.EntityFrameworkCore.Query.Sql.QuerySqlGeneratorDependencies", + "MemberId": "public .ctor(Microsoft.EntityFrameworkCore.Storage.IRelationalCommandBuilderFactory commandBuilderFactory, Microsoft.EntityFrameworkCore.Storage.ISqlGenerationHelper sqlGenerationHelper, Microsoft.EntityFrameworkCore.Storage.IParameterNameGeneratorFactory parameterNameGeneratorFactory, Microsoft.EntityFrameworkCore.Storage.IRelationalTypeMapper relationalTypeMapper)", + "Kind": "Removal" + }, + { + "TypeId": "public sealed class Microsoft.EntityFrameworkCore.Infrastructure.RelationalModelValidatorDependencies", + "MemberId": "public .ctor(Microsoft.EntityFrameworkCore.Storage.IRelationalTypeMapper typeMapper)", + "Kind": "Removal" + }, + { + "TypeId": "public class Microsoft.EntityFrameworkCore.Migrations.MigrationBuilder", + "MemberId": "public virtual Microsoft.EntityFrameworkCore.Migrations.Operations.Builders.AlterOperationBuilder AlterColumn(System.String name, System.String table, System.String type = null, System.Nullable unicode = default(System.Nullable), System.Nullable maxLength = default(System.Nullable), System.Boolean rowVersion = False, System.String schema = null, System.Boolean nullable = False, System.Object defaultValue = null, System.String defaultValueSql = null, System.String computedColumnSql = null, System.Type oldClrType = null, System.String oldType = null, System.Nullable oldUnicode = default(System.Nullable), System.Nullable oldMaxLength = default(System.Nullable), System.Boolean oldRowVersion = False, System.Boolean oldNullable = False, System.Object oldDefaultValue = null, System.String oldDefaultValueSql = null, System.String oldComputedColumnSql = null)", + "Kind": "Removal" + }, + { + "TypeId": "public class Microsoft.EntityFrameworkCore.Migrations.MigrationBuilder", + "MemberId": "public virtual Microsoft.EntityFrameworkCore.Migrations.Operations.Builders.OperationBuilder AddColumn(System.String name, System.String table, System.String type = null, System.Nullable unicode = default(System.Nullable), System.Nullable maxLength = default(System.Nullable), System.Boolean rowVersion = False, System.String schema = null, System.Boolean nullable = False, System.Object defaultValue = null, System.String defaultValueSql = null, System.String computedColumnSql = null)", + "Kind": "Removal" + }, + { + "TypeId": "public class Microsoft.EntityFrameworkCore.Migrations.Operations.Builders.ColumnsBuilder", + "MemberId": "public virtual Microsoft.EntityFrameworkCore.Migrations.Operations.Builders.OperationBuilder Column(System.String type = null, System.Nullable unicode = default(System.Nullable), System.Nullable maxLength = default(System.Nullable), System.Boolean rowVersion = False, System.String name = null, System.Boolean nullable = False, System.Object defaultValue = null, System.String defaultValueSql = null, System.String computedColumnSql = null)", + "Kind": "Removal" + }, + { + "TypeId": "public interface Microsoft.EntityFrameworkCore.Metadata.IRelationalPropertyAnnotations", + "MemberId": "System.Boolean get_IsFixedLength()", + "Kind": "Addition" + }, + { + "TypeId": "public interface Microsoft.EntityFrameworkCore.Query.IQueryMethodProvider", + "MemberId": "System.Reflection.MethodInfo get_FastQueryMethod()", + "Kind": "Addition" + }, + { + "TypeId": "public class Microsoft.EntityFrameworkCore.Storage.BoolTypeMapping : Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping", + "MemberId": "public override Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping Clone(System.String storeType, System.Nullable size)", + "Kind": "Removal" + }, + { + "TypeId": "public class Microsoft.EntityFrameworkCore.Storage.ByteArrayTypeMapping : Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping", + "MemberId": "public override Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping Clone(System.String storeType, System.Nullable size)", + "Kind": "Removal" + }, + { + "TypeId": "public class Microsoft.EntityFrameworkCore.Storage.ByteTypeMapping : Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping", + "MemberId": "public override Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping Clone(System.String storeType, System.Nullable size)", + "Kind": "Removal" + }, + { + "TypeId": "public class Microsoft.EntityFrameworkCore.Storage.CharTypeMapping : Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping", + "MemberId": "public override Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping Clone(System.String storeType, System.Nullable size)", + "Kind": "Removal" + }, + { + "TypeId": "public class Microsoft.EntityFrameworkCore.Storage.DateTimeOffsetTypeMapping : Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping", + "MemberId": "public override Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping Clone(System.String storeType, System.Nullable size)", + "Kind": "Removal" + }, + { + "TypeId": "public class Microsoft.EntityFrameworkCore.Storage.DateTimeTypeMapping : Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping", + "MemberId": "public override Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping Clone(System.String storeType, System.Nullable size)", + "Kind": "Removal" + }, + { + "TypeId": "public class Microsoft.EntityFrameworkCore.Storage.DecimalTypeMapping : Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping", + "MemberId": "public override Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping Clone(System.String storeType, System.Nullable size)", + "Kind": "Removal" + }, + { + "TypeId": "public class Microsoft.EntityFrameworkCore.Storage.DoubleTypeMapping : Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping", + "MemberId": "public override Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping Clone(System.String storeType, System.Nullable size)", + "Kind": "Removal" + }, + { + "TypeId": "public class Microsoft.EntityFrameworkCore.Storage.FloatTypeMapping : Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping", + "MemberId": "public override Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping Clone(System.String storeType, System.Nullable size)", + "Kind": "Removal" + }, + { + "TypeId": "public class Microsoft.EntityFrameworkCore.Storage.GuidTypeMapping : Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping", + "MemberId": "public override Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping Clone(System.String storeType, System.Nullable size)", + "Kind": "Removal" + }, + { + "TypeId": "public class Microsoft.EntityFrameworkCore.Storage.IntTypeMapping : Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping", + "MemberId": "public override Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping Clone(System.String storeType, System.Nullable size)", + "Kind": "Removal" + }, + { + "TypeId": "public class Microsoft.EntityFrameworkCore.Storage.LongTypeMapping : Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping", + "MemberId": "public override Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping Clone(System.String storeType, System.Nullable size)", + "Kind": "Removal" + }, + { + "TypeId": "public class Microsoft.EntityFrameworkCore.Storage.SByteTypeMapping : Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping", + "MemberId": "public override Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping Clone(System.String storeType, System.Nullable size)", + "Kind": "Removal" + }, + { + "TypeId": "public class Microsoft.EntityFrameworkCore.Storage.ShortTypeMapping : Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping", + "MemberId": "public override Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping Clone(System.String storeType, System.Nullable size)", + "Kind": "Removal" + }, + { + "TypeId": "public class Microsoft.EntityFrameworkCore.Storage.StringTypeMapping : Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping", + "MemberId": "public override Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping Clone(System.String storeType, System.Nullable size)", + "Kind": "Removal" + }, + { + "TypeId": "public class Microsoft.EntityFrameworkCore.Storage.TimeSpanTypeMapping : Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping", + "MemberId": "public override Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping Clone(System.String storeType, System.Nullable size)", + "Kind": "Removal" + }, + { + "TypeId": "public class Microsoft.EntityFrameworkCore.Storage.UIntTypeMapping : Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping", + "MemberId": "public override Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping Clone(System.String storeType, System.Nullable size)", + "Kind": "Removal" + }, + { + "TypeId": "public class Microsoft.EntityFrameworkCore.Storage.ULongTypeMapping : Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping", + "MemberId": "public override Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping Clone(System.String storeType, System.Nullable size)", + "Kind": "Removal" + }, + { + "TypeId": "public class Microsoft.EntityFrameworkCore.Storage.UShortTypeMapping : Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping", + "MemberId": "public override Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping Clone(System.String storeType, System.Nullable size)", + "Kind": "Removal" + }, + { + "TypeId": "public interface Microsoft.EntityFrameworkCore.Storage.ISqlGenerationHelper", + "MemberId": "System.String GenerateParameterNamePlaceholder(System.String name)", + "Kind": "Addition" + }, + { + "TypeId": "public interface Microsoft.EntityFrameworkCore.Storage.ISqlGenerationHelper", + "MemberId": "System.Void GenerateParameterNamePlaceholder(System.Text.StringBuilder builder, System.String name)", + "Kind": "Addition" + }, + { + "TypeId": "public sealed class Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.RelationalEntityQueryableExpressionVisitorDependencies", + "MemberId": "public .ctor(Microsoft.EntityFrameworkCore.Metadata.IModel model, Microsoft.EntityFrameworkCore.Query.Expressions.ISelectExpressionFactory selectExpressionFactory, Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal.IMaterializerFactory materializerFactory, Microsoft.EntityFrameworkCore.Query.Internal.IShaperCommandContextFactory shaperCommandContextFactory)", + "Kind": "Removal" + }, + { + "TypeId": "public sealed class Microsoft.EntityFrameworkCore.Query.ExpressionTranslators.RelationalCompositeMethodCallTranslatorDependencies", + "MemberId": "public .ctor(Microsoft.EntityFrameworkCore.Diagnostics.IDiagnosticsLogger logger)", + "Kind": "Removal" + }, + { + "TypeId": "public interface Microsoft.EntityFrameworkCore.Metadata.IDbFunction", + "MemberId": "System.Boolean get_IsIQueryable()", + "Kind": "Addition" + }, + { + "TypeId": "public interface Microsoft.EntityFrameworkCore.Query.Sql.ISqlExpressionVisitor", + "MemberId": "System.Linq.Expressions.Expression VisitCrossJoinLateralOuter(Microsoft.EntityFrameworkCore.Query.Expressions.CrossJoinLateralOuterExpression crossJoinLateralOuterExpression)", + "Kind": "Addition" + }, + { + "TypeId": "public interface Microsoft.EntityFrameworkCore.Query.Sql.ISqlExpressionVisitor", + "MemberId": "System.Linq.Expressions.Expression VisitQueryableSqlFunctionExpression(Microsoft.EntityFrameworkCore.Query.Expressions.QuerableSqlFunctionExpression querableSqlFunctionExpression)", + "Kind": "Addition" + } ] \ No newline at end of file diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryCompilationContext.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryCompilationContext.cs index 1f5d0df4989..110c0ad4751 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryCompilationContext.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryCompilationContext.cs @@ -35,5 +35,11 @@ public SqlServerQueryCompilationContext( /// directly from your code. This API may change or be removed in future releases. /// public override bool IsLateralJoinSupported => true; + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public override bool IsLateralJoinOuterSupported => true; } } diff --git a/src/EFCore.SqlServer/Query/Sql/Internal/SqlServerQuerySqlGenerator.cs b/src/EFCore.SqlServer/Query/Sql/Internal/SqlServerQuerySqlGenerator.cs index e9bd7df462a..a04f696fee6 100644 --- a/src/EFCore.SqlServer/Query/Sql/Internal/SqlServerQuerySqlGenerator.cs +++ b/src/EFCore.SqlServer/Query/Sql/Internal/SqlServerQuerySqlGenerator.cs @@ -73,6 +73,21 @@ public override Expression VisitCrossJoinLateral(CrossJoinLateralExpression cros return crossJoinLateralExpression; } + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public override Expression VisitCrossJoinLateralOuter(CrossJoinLateralOuterExpression crossJoinLateralOuterExpression) + { + Check.NotNull(crossJoinLateralOuterExpression, nameof(crossJoinLateralOuterExpression)); + + Sql.Append("OUTER APPLY "); + + Visit(crossJoinLateralOuterExpression.TableExpression); + + return crossJoinLateralOuterExpression; + } + /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. @@ -127,6 +142,28 @@ public override Expression VisitSqlFunction(SqlFunctionExpression sqlFunctionExp return base.VisitSqlFunction(sqlFunctionExpression); } + /// + /// Visits a QuerableSqlFunctionExpression. + /// + /// The SQL function expression. + /// + /// An Expression. + /// + public override Expression VisitQueryableSqlFunctionExpression(QuerableSqlFunctionExpression querableSqlFunctionExpression) + { + Check.NotNull(querableSqlFunctionExpression, nameof(querableSqlFunctionExpression)); + + base.VisitQueryableSqlFunctionExpression(querableSqlFunctionExpression); + + if (querableSqlFunctionExpression.Alias != null) + { + Sql.Append(" AS ") + .Append(SqlGenerator.DelimitIdentifier(querableSqlFunctionExpression.Alias)); + } + + return querableSqlFunctionExpression; + } + /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. diff --git a/src/EFCore/DbContext.cs b/src/EFCore/DbContext.cs index 3d524987403..b70d1e604e8 100644 --- a/src/EFCore/DbContext.cs +++ b/src/EFCore/DbContext.cs @@ -3,9 +3,11 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; using System.Linq; +using System.Linq.Expressions; using System.Reflection; using System.Threading; using System.Threading.Tasks; @@ -1457,6 +1459,77 @@ public virtual Task FindAsync([CanBeNull] object[] keyValues, /// IServiceProvider IInfrastructure.Instance => InternalServiceProvider; + /// + /// Executes a query expression, which represents a function call, against the query store. + /// + /// The result type of the query expression + /// The query expression to execute. + /// The result of executing the funciton against the datastore. + protected virtual TResult Execute([NotNull] Expression> expression) + { + Check.NotNull(expression, nameof(expression)); + + if (!(expression.Body is MethodCallExpression)) + { + throw new InvalidOperationException( + CoreStrings.ExpressionBodyMustBeMethodCallExpression()); + } + + var dbFuncFac = InternalServiceProvider.GetRequiredService(); + var resultsQuery = DbContextDependencies.QueryProvider.Execute(dbFuncFac.GenerateDbFunctionSource((MethodCallExpression)expression.Body , Model)) as IEnumerable; + + var results = resultsQuery.ToList(); + + return results.Count == 0 ? default : results[0]; + } + + /// + /// Executes a query expression, which represents a function call, against the query store. + /// + /// The result type of the query expression + /// The query expression to execute. + /// The cancellation token. + /// A task containing the result of executing the funciton against the datastore. + protected virtual Task ExecuteAsync([NotNull] Expression> expression, CancellationToken cancellationToken = default) + { + Check.NotNull(expression, nameof(expression)); + + if (!(expression.Body is MethodCallExpression)) + { + throw new InvalidOperationException( + CoreStrings.ExpressionBodyMustBeMethodCallExpression()); + } + + var dbFuncFac = InternalServiceProvider.GetRequiredService(); + + return DbContextDependencies.QueryProvider.ExecuteAsync( + dbFuncFac.GenerateDbFunctionSource((MethodCallExpression)expression.Body, Model), cancellationToken); + } + + + /// + /// Creates a query expression, which represents a function call, against the query store. + /// + /// The result type of the query expression + /// The query expression to create. + /// An IQueryable representing the query. + protected virtual IQueryable CreateQuery([NotNull] Expression>> expression) + { + Check.NotNull(expression, nameof(expression)); + + var dbFuncFac = InternalServiceProvider.GetRequiredService(); + + if (!(expression.Body is MethodCallExpression)) + { + throw new InvalidOperationException( + CoreStrings.ExpressionBodyMustBeMethodCallExpression()); + } + + var resultsQuery = dbFuncFac.GenerateDbFunctionSource((MethodCallExpression)expression.Body, Model); + + return DbContextDependencies.QueryProvider.CreateQuery(resultsQuery); + } + #region Hidden System.Object members /// diff --git a/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs b/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs index 7537e95b83e..4988293a195 100644 --- a/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs +++ b/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs @@ -24,6 +24,7 @@ using Microsoft.EntityFrameworkCore.ValueGeneration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Remotion.Linq.Parsing.ExpressionVisitors.Transformation; using Remotion.Linq.Parsing.ExpressionVisitors.TreeEvaluation; namespace Microsoft.EntityFrameworkCore.Infrastructure @@ -146,6 +147,8 @@ public static readonly IDictionary CoreServices { typeof(IResettableService), new ServiceCharacteristics(ServiceLifetime.Scoped, multipleRegistrations: true) }, { typeof(ISingletonOptions), new ServiceCharacteristics(ServiceLifetime.Singleton, multipleRegistrations: true) }, { typeof(IEvaluatableExpressionFilter), new ServiceCharacteristics(ServiceLifetime.Scoped) }, + { typeof(IExpressionTranformationProvider), new ServiceCharacteristics(ServiceLifetime.Scoped) }, + { typeof(IDbFunctionSourceFactory), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(ITypeMappingSourcePlugin), new ServiceCharacteristics(ServiceLifetime.Singleton, multipleRegistrations: true) } }; @@ -279,6 +282,7 @@ public virtual EntityFrameworkServicesBuilder TryAddCoreServices() TryAdd(p => p.GetService()); TryAdd>(p => p.GetService); TryAdd(); + TryAdd(p => ExpressionTransformerRegistry.CreateDefault()); TryAdd(); TryAdd(); TryAdd(); @@ -288,6 +292,7 @@ public virtual EntityFrameworkServicesBuilder TryAddCoreServices() TryAdd(); TryAdd(); TryAdd(); + TryAdd(); ServiceCollectionMap .TryAddSingleton(new DiagnosticListener(DbLoggerCategory.Name)); diff --git a/src/EFCore/Internal/DbFunctionSourceFactory.cs b/src/EFCore/Internal/DbFunctionSourceFactory.cs new file mode 100644 index 00000000000..21c15bbe75e --- /dev/null +++ b/src/EFCore/Internal/DbFunctionSourceFactory.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Text; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata; + +namespace Microsoft.EntityFrameworkCore.Internal +{ + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public class DbFunctionSourceFactory : IDbFunctionSourceFactory + { + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual Expression GenerateDbFunctionSource(MethodCallExpression methodCall, IModel model) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/EFCore/Internal/IDbFunctionSourceFactory.cs b/src/EFCore/Internal/IDbFunctionSourceFactory.cs new file mode 100644 index 00000000000..8ea523de7c5 --- /dev/null +++ b/src/EFCore/Internal/IDbFunctionSourceFactory.cs @@ -0,0 +1,19 @@ +using System.Linq.Expressions; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata; + +namespace Microsoft.EntityFrameworkCore.Internal +{ + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public interface IDbFunctionSourceFactory + { + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + Expression GenerateDbFunctionSource([NotNull] MethodCallExpression methodCall, [NotNull] IModel model); + } +} diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index 04303e729e4..b0490007dad 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -2673,6 +2673,13 @@ public static string AmbiguousServiceProperty([CanBeNull] object property, [CanB GetString("AmbiguousServiceProperty", nameof(property), nameof(serviceType), nameof(entityType)), property, serviceType, entityType); + /// + /// The expression body must be a MethodCallExpression + /// + public static string ExpressionBodyMustBeMethodCallExpression() + => string.Format( + GetString("ExpressionBodyMustBeMethodCallExpression")); + /// /// Cannot use multiple DbContext instances within a single query execution. Ensure the query uses a single context instance. /// diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index 049d8457a37..6a2a2c5f8eb 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -1081,6 +1081,9 @@ The service property '{property}' of type '{serviceType}' cannot be added to the entity type '{entityType}' because there is another property of the same type. Ignore one of the properties using the NotMappedAttribute or 'EntityTypeBuilder.Ignore' in 'OnModelCreating'. + + The expression body must be a MethodCallExpression. + Cannot use multiple DbContext instances within a single query execution. Ensure the query uses a single context instance. diff --git a/src/EFCore/Query/Internal/QueryModelGenerator.cs b/src/EFCore/Query/Internal/QueryModelGenerator.cs index f1a6836ac84..06a46b1827d 100644 --- a/src/EFCore/Query/Internal/QueryModelGenerator.cs +++ b/src/EFCore/Query/Internal/QueryModelGenerator.cs @@ -23,6 +23,7 @@ public class QueryModelGenerator : IQueryModelGenerator { private readonly INodeTypeProvider _nodeTypeProvider; private readonly IEvaluatableExpressionFilter _evaluatableExpressionFilter; + private readonly IExpressionTranformationProvider _expressionTranformationProvider; private readonly ICurrentDbContext _currentDbContext; /// @@ -32,14 +33,17 @@ public class QueryModelGenerator : IQueryModelGenerator public QueryModelGenerator( [NotNull] INodeTypeProviderFactory nodeTypeProviderFactory, [NotNull] IEvaluatableExpressionFilter evaluatableExpressionFilter, + [NotNull] IExpressionTranformationProvider expressionTranformationProvider, [NotNull] ICurrentDbContext currentDbContext) { Check.NotNull(nodeTypeProviderFactory, nameof(nodeTypeProviderFactory)); Check.NotNull(evaluatableExpressionFilter, nameof(evaluatableExpressionFilter)); + Check.NotNull(expressionTranformationProvider, nameof(expressionTranformationProvider)); Check.NotNull(currentDbContext, nameof(currentDbContext)); _nodeTypeProvider = nodeTypeProviderFactory.Create(); _evaluatableExpressionFilter = evaluatableExpressionFilter; + _expressionTranformationProvider = expressionTranformationProvider; _currentDbContext = currentDbContext; } @@ -81,7 +85,7 @@ private QueryParser CreateQueryParser(INodeTypeProvider nodeTypeProvider) new IExpressionTreeProcessor[] { new PartialEvaluatingExpressionTreeProcessor(_evaluatableExpressionFilter), - new TransformingExpressionTreeProcessor(ExpressionTransformerRegistry.CreateDefault()) + new TransformingExpressionTreeProcessor(_expressionTranformationProvider) }))); } } diff --git a/test/EFCore.Relational.Tests/Metadata/DbFunctionMetadataTests.cs b/test/EFCore.Relational.Tests/Metadata/DbFunctionMetadataTests.cs index b52f06bbd62..cf750a2fdbd 100644 --- a/test/EFCore.Relational.Tests/Metadata/DbFunctionMetadataTests.cs +++ b/test/EFCore.Relational.Tests/Metadata/DbFunctionMetadataTests.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Linq; using System.Linq.Expressions; using System.Reflection; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -161,6 +162,7 @@ public static int DuplicateNameTest() public static MethodInfo MethodAmi = typeof(TestMethods).GetRuntimeMethod(nameof(TestMethods.MethodA), new[] { typeof(string), typeof(int) }); public static MethodInfo MethodBmi = typeof(TestMethods).GetRuntimeMethod(nameof(TestMethods.MethodB), new[] { typeof(string), typeof(int) }); public static MethodInfo MethodHmi = typeof(TestMethods).GetTypeInfo().GetDeclaredMethod(nameof(TestMethods.MethodH)); + public static MethodInfo MethodImi = typeof(TestMethods).GetTypeInfo().GetDeclaredMethod(nameof(TestMethods.MethodI)); public class TestMethods { @@ -195,6 +197,11 @@ public static int MethodH(T a, string b) { throw new Exception(); } + + public static IQueryable MethodI() + { + throw new Exception(); + } } [Fact] @@ -477,6 +484,16 @@ public virtual void Set_empty_function_name_throws() Assert.Equal(expectedMessage, Assert.Throws(() => modelBuilder.HasDbFunction(MethodAmi).HasName("")).Message); } + [Fact] + public virtual void Queryable_method_must_be_static() + { + var modelBuilder = GetModelBuilder(); + + var expectedMessage = RelationalStrings.DbFunctionQueryableNotStatic("TestMethods.MethodI"); + + Assert.Equal(expectedMessage, Assert.Throws(() => modelBuilder.HasDbFunction(MethodImi)).Message); + } + private ModelBuilder GetModelBuilder() { var conventionset = new ConventionSet(); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/UdfDbFunctionSqlServerTests.cs b/test/EFCore.SqlServer.FunctionalTests/Query/UdfDbFunctionSqlServerTests.cs index e7cc2833fab..6c968444d43 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/UdfDbFunctionSqlServerTests.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/UdfDbFunctionSqlServerTests.cs @@ -1,12 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; -using System.Collections.Generic; -using Microsoft.EntityFrameworkCore.Diagnostics; +using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.TestUtilities; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Xunit; using Xunit.Abstractions; @@ -740,9 +736,427 @@ FROM [Customers] AS [c] #endregion - public class SqlServer : UdfFixtureBase + #region BootStrap + + [Fact] + public override void BootstrapScalarNoParams() + { + base.BootstrapScalarNoParams(); + + AssertSql(@"SELECT SCHEMA_NAME()"); + } + + [Fact] + public override async Task BootstrapScalarNoParamsAsync() + { + await base.BootstrapScalarNoParamsAsync(); + + AssertSql(@"SELECT SCHEMA_NAME()"); + } + + [Fact] + public override void BootstrapScalarParams() + { + base.BootstrapScalarParams(); + + AssertSql(@"@__a_0='1' +@__b_1='2' + +SELECT [dbo].[AddValues](@__a_0, @__b_1)"); + } + + [Fact] + public override void BootstrapScalarFuncParams() + { + base.BootstrapScalarFuncParams(); + + AssertSql(@"@__b_1='2' + +SELECT [dbo].[AddValues]([dbo].[AddValues](1, 2), @__b_1)"); + } + + [Fact] + public override void BootstrapScalarFuncParamsWithVariable() + { + base.BootstrapScalarFuncParamsWithVariable(); + + AssertSql(@"@__x_1='5' +@__b_2='2' + +SELECT [dbo].[AddValues]([dbo].[AddValues](@__x_1, 2), @__b_2)"); + } + + [Fact] + public override void BootstrapScalarFuncParamsConstant() + { + base.BootstrapScalarFuncParamsConstant(); + + AssertSql(@"@__b_0='2' + +SELECT [dbo].[AddValues](1, @__b_0)"); + } + #endregion + + #region Table Valued Tests + + [Fact] + public override void TVF_Stand_Alone() + { + base.TVF_Stand_Alone(); + + AssertSql(@"SELECT [t].[AmountSold], [t].[ProductId] +FROM [dbo].[GetTopTwoSellingProducts]() AS [t] +ORDER BY [t].[ProductId]"); + } + + [Fact] + public override void TVF_Stand_Alone_With_Translation() + { + base.TVF_Stand_Alone_With_Translation(); + + AssertSql(@"SELECT [t].[AmountSold], [t].[ProductId] +FROM [dbo].[GetTopTwoSellingProducts]() AS [t] +ORDER BY [t].[ProductId]"); + } + + [Fact] + public override void TVF_Stand_Alone_Parameter() + { + base.TVF_Stand_Alone_Parameter(); + + AssertSql(@"@__customerId_0='1' + +SELECT [c].[Count], [c].[CustomerId], [c].[Year] +FROM [dbo].[GetCustomerOrderCountByYear](@__customerId_0) AS [c] +ORDER BY [c].[Count] DESC"); + } + + [Fact] + public override void TVF_Stand_Alone_Nested() + { + base.TVF_Stand_Alone_Nested(); + + AssertSql(@"SELECT [r].[Count], [r].[CustomerId], [r].[Year] +FROM [dbo].[GetCustomerOrderCountByYear]([dbo].[AddValues](-2, 3)) AS [r] +ORDER BY [r].[Count] DESC"); + } + + [Fact] + public override void TVF_CrossApply_Correlated_Select_Anonymous() + { + base.TVF_CrossApply_Correlated_Select_Anonymous(); + + AssertSql(@"SELECT [c].[Id], [c].[LastName], [r].[Year], [r].[Count] +FROM [Customers] AS [c] +CROSS APPLY [dbo].[GetCustomerOrderCountByYear]([c].[Id]) AS [r] +ORDER BY [c].[Id], [r].[Year]"); + } + + [Fact] + public override void TVF_Select_Direct_In_Anonymous() + { + base.TVF_Select_Direct_In_Anonymous(); + + AssertSql(@"SELECT [c].[Id] +FROM [Customers] AS [c]", + + @"SELECT [t].[AmountSold], [t].[ProductId] +FROM [dbo].[GetTopTwoSellingProducts]() AS [t]"); + } + + [Fact] + public override void TVF_Select_Correlated_Direct_In_Anonymous() + { + base.TVF_Select_Correlated_Direct_In_Anonymous(); + + AssertSql(@"SELECT [c].[Id] +FROM [Customers] AS [c]", + + @"@_outer_Id='1' + +SELECT [o].[Count], [o].[CustomerId], [o].[Year] +FROM [dbo].[GetCustomerOrderCountByYear](@_outer_Id) AS [o]", + + @"@_outer_Id='2' + +SELECT [o].[Count], [o].[CustomerId], [o].[Year] +FROM [dbo].[GetCustomerOrderCountByYear](@_outer_Id) AS [o]", + + @"@_outer_Id='3' + +SELECT [o].[Count], [o].[CustomerId], [o].[Year] +FROM [dbo].[GetCustomerOrderCountByYear](@_outer_Id) AS [o]", + + @"@_outer_Id='4' + +SELECT [o].[Count], [o].[CustomerId], [o].[Year] +FROM [dbo].[GetCustomerOrderCountByYear](@_outer_Id) AS [o]"); + } + + [Fact] + public override void TVF_Select_Correlated_Direct_With_Function_Query_Parameter_Correlated_In_Anonymous() + { + base.TVF_Select_Correlated_Direct_With_Function_Query_Parameter_Correlated_In_Anonymous(); + + AssertSql(@"SELECT [c].[Id] +FROM [Customers] AS [c] +WHERE [c].[Id] = 1", + + @"@_outer_Id='1' + +SELECT [o].[Count], [o].[CustomerId], [o].[Year] +FROM [dbo].[GetCustomerOrderCountByYear]([dbo].[AddValues](@_outer_Id, 1)) AS [o]"); + } + + [Fact] + public override void TVF_Select_Correlated_Subquery_In_Anonymous() + { + base.TVF_Select_Correlated_Subquery_In_Anonymous(); + + AssertSql(@"SELECT [c].[Id] +FROM [Customers] AS [c]", + + @"@_outer_Id='1' + +SELECT [o].[Count], [o].[CustomerId], [o].[Year] +FROM [dbo].[GetCustomerOrderCountByYear](@_outer_Id) AS [o] +WHERE [o].[Year] = 2000", + + @"@_outer_Id='2' + +SELECT [o].[Count], [o].[CustomerId], [o].[Year] +FROM [dbo].[GetCustomerOrderCountByYear](@_outer_Id) AS [o] +WHERE [o].[Year] = 2000", + + @"@_outer_Id='3' + +SELECT [o].[Count], [o].[CustomerId], [o].[Year] +FROM [dbo].[GetCustomerOrderCountByYear](@_outer_Id) AS [o] +WHERE [o].[Year] = 2000", + + @"@_outer_Id='4' + +SELECT [o].[Count], [o].[CustomerId], [o].[Year] +FROM [dbo].[GetCustomerOrderCountByYear](@_outer_Id) AS [o] +WHERE [o].[Year] = 2000"); + } + + [Fact] + public override void TVF_Select_Correlated_Subquery_In_Anonymous_Nested() + { + base.TVF_Select_Correlated_Subquery_In_Anonymous_Nested(); + + AssertSql(@"SELECT [c].[Id] +FROM [Customers] AS [c]", + + @"SELECT [o].[Year] +FROM [dbo].[GetCustomerOrderCountByYear](1) AS [o] +WHERE [o].[Year] = 2000", + + @"@_outer_Year='2000' (Nullable = true) + +SELECT [o2].[Count], [o2].[CustomerId], [o2].[Year] +FROM [dbo].[GetCustomerOrderCountByYear](2000) AS [o2] +WHERE @_outer_Year = 2001"); + + Assert.Equal(13, Fixture.TestSqlLoggerFactory.SqlStatements.Count); + } + + [Fact] + public override void TVF_Select_NonCorrelated_Subquery_In_Anonymous() + { + base.TVF_Select_NonCorrelated_Subquery_In_Anonymous(); + + AssertSql(@"SELECT [c].[Id] +FROM [Customers] AS [c]", + + @"SELECT [p].[ProductId] +FROM [dbo].[GetTopTwoSellingProducts]() AS [p] +WHERE [p].[AmountSold] = 27"); + } + + [Fact] + public override void TVF_Select_NonCorrelated_Subquery_In_Anonymous_Parameter() + { + base.TVF_Select_NonCorrelated_Subquery_In_Anonymous_Parameter(); + + AssertSql(@"SELECT [c].[Id] +FROM [Customers] AS [c]", + + @"@__amount_1='27' + +SELECT [p].[ProductId] +FROM [dbo].[GetTopTwoSellingProducts]() AS [p] +WHERE [p].[AmountSold] = @__amount_1"); + } + + [Fact] + public override void TVF_Correlated_Select_In_Anonymous() + { + base.TVF_Correlated_Select_In_Anonymous(); + + AssertSql(@"SELECT [c].[Id], [c].[LastName] +FROM [Customers] AS [c] +ORDER BY [c].[Id]", + + @"@_outer_Id='1' + +SELECT [o].[Count], [o].[CustomerId], [o].[Year] +FROM [dbo].[GetCustomerOrderCountByYear](@_outer_Id) AS [o]", + + @"@_outer_Id='2' + +SELECT [o].[Count], [o].[CustomerId], [o].[Year] +FROM [dbo].[GetCustomerOrderCountByYear](@_outer_Id) AS [o]", + + @"@_outer_Id='3' + +SELECT [o].[Count], [o].[CustomerId], [o].[Year] +FROM [dbo].[GetCustomerOrderCountByYear](@_outer_Id) AS [o]", + + @"@_outer_Id='4' + +SELECT [o].[Count], [o].[CustomerId], [o].[Year] +FROM [dbo].[GetCustomerOrderCountByYear](@_outer_Id) AS [o]" +); + } + + [Fact] + public override void TVF_CrossApply_Correlated_Select_Result() + { + base.TVF_CrossApply_Correlated_Select_Result(); + + AssertSql(@"SELECT [r].[Count], [r].[CustomerId], [r].[Year] +FROM [Customers] AS [c] +CROSS APPLY [dbo].[GetCustomerOrderCountByYear]([c].[Id]) AS [r] +ORDER BY [r].[Count] DESC, [r].[Year] DESC"); + } + + [Fact] + public override void TVF_CrossJoin_Not_Correlated() + { + base.TVF_CrossJoin_Not_Correlated(); + + AssertSql(@"SELECT [c].[Id], [c].[LastName], [r].[Year], [r].[Count] +FROM [Customers] AS [c] +CROSS JOIN [dbo].[GetCustomerOrderCountByYear](2) AS [r] +WHERE [c].[Id] = 2 +ORDER BY [r].[Count]"); + } + + [Fact] + public override void TVF_CrossJoin_Parameter() + { + base.TVF_CrossJoin_Parameter(); + + AssertSql(@"@__custId_1='2' + +SELECT [c].[Id], [c].[LastName], [r].[Year], [r].[Count] +FROM [Customers] AS [c] +CROSS JOIN [dbo].[GetCustomerOrderCountByYear](@__custId_1) AS [r] +WHERE [c].[Id] = @__custId_1 +ORDER BY [r].[Count]"); + } + + [Fact] + public override void TVF_Join() + { + base.TVF_Join(); + + AssertSql(@"SELECT [p].[Id], [p].[Name], [r].[AmountSold] +FROM [Products] AS [p] +INNER JOIN [dbo].[GetTopTwoSellingProducts]() AS [r] ON [p].[Id] = [r].[ProductId] +ORDER BY [p].[Id]"); + } + + [Fact] + public override void TVF_LeftJoin_Select_Anonymous() + { + base.TVF_LeftJoin_Select_Anonymous(); + + AssertSql(@"SELECT [p].[Id], [p].[Name], [r].[AmountSold] +FROM [Products] AS [p] +LEFT JOIN [dbo].[GetTopTwoSellingProducts]() AS [r] ON [p].[Id] = [r].[ProductId] +ORDER BY [p].[Id] DESC"); + } + + [Fact] + public override void TVF_LeftJoin_Select_Result() + { + base.TVF_LeftJoin_Select_Result(); + + AssertSql(@"SELECT [r].[AmountSold], [r].[ProductId] +FROM [Products] AS [p] +LEFT JOIN [dbo].[GetTopTwoSellingProducts]() AS [r] ON [p].[Id] = [r].[ProductId] +ORDER BY [p].[Id] DESC"); + } + + [Fact] + public override void TVF_OuterApply_Correlated_Select_TVF() + { + base.TVF_OuterApply_Correlated_Select_TVF(); + + AssertSql(@"SELECT [g].[Count], [g].[CustomerId], [g].[Year] +FROM [Customers] AS [c] +OUTER APPLY [dbo].[GetCustomerOrderCountByYear]([c].[Id]) AS [g] +ORDER BY [c].[Id], [g].[Year]"); + } + + [Fact] + public override void TVF_OuterApply_Correlated_Select_DbSet() + { + base.TVF_OuterApply_Correlated_Select_DbSet(); + + AssertSql(@"SELECT [c].[Id], [c].[FirstName], [c].[LastName] +FROM [Customers] AS [c] +OUTER APPLY [dbo].[GetCustomerOrderCountByYear]([c].[Id]) AS [g] +ORDER BY [c].[Id], [g].[Year]"); + } + + [Fact] + public override void TVF_OuterApply_Correlated_Select_Anonymous() + { + base.TVF_OuterApply_Correlated_Select_Anonymous(); + + AssertSql(@"SELECT [c].[Id], [c].[LastName], [g].[Year], [g].[Count] +FROM [Customers] AS [c] +OUTER APPLY [dbo].[GetCustomerOrderCountByYear]([c].[Id]) AS [g] +ORDER BY [c].[Id], [g].[Year]"); + } + + [Fact] + public override void TVF_Nested() + { + base.TVF_Nested(); + + AssertSql(@"@__custId_1='2' + +SELECT [c].[Id], [c].[LastName], [r].[Year], [r].[Count] +FROM [Customers] AS [c] +CROSS JOIN [dbo].[GetCustomerOrderCountByYear]([dbo].[AddValues](1, 1)) AS [r] +WHERE [c].[Id] = @__custId_1 +ORDER BY [r].[Year]"); + } + + [Fact] + public override void TVF_Correlated_Nested_Func_Call() + { + base.TVF_Correlated_Nested_Func_Call(); + + AssertSql(@"@__custId_1='2' + +SELECT [c].[Id], [r].[Count], [r].[Year] +FROM [Customers] AS [c] +CROSS APPLY [dbo].[GetCustomerOrderCountByYear]([dbo].[AddValues]([c].[Id], 1)) AS [r] +WHERE [c].[Id] = @__custId_1"); + } + + #endregion + + public class SqlServer : BaseUdfFixture { protected override string StoreName { get; } = "UDFDbFunctionSqlServerTests"; + protected override ITestStoreFactory TestStoreFactory => SqlServerTestStoreFactory.Instance; protected override void Seed(DbContext context) @@ -812,6 +1226,53 @@ returns nvarchar(max) return @customerName; end"); + context.Database.ExecuteSqlCommand( + @"create function [dbo].GetCustomerOrderCountByYear(@customerId int) + returns @reports table + ( + CustomerId int not null, + Count int not null, + Year int not null + ) + as + begin + + insert into @reports + select @customerId, count(id), year(orderDate) + from orders + where customerId = @customerId + group by customerId, year(orderDate) + order by year(orderDate) + + return + end"); + + context.Database.ExecuteSqlCommand( + @"create function [dbo].GetTopTwoSellingProducts() + returns @products table + ( + ProductId int not null, + AmountSold int + ) + as + begin + + insert into @products + select top 2 ProductID, sum(quantitySold) as totalSold + from orders + group by ProductID + order by totalSold desc + return + end"); + + context.Database.ExecuteSqlCommand( + @"create function [dbo].[AddValues] (@a int, @b int) + returns int + as + begin + return @a + @b; + end"); + context.SaveChanges(); } }