diff --git a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs index 4c7a32886ff..c216b58ac27 100644 --- a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs +++ b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs @@ -48,6 +48,8 @@ public virtual void Generate(string builderName, IModel model, IndentedStringBui { GenerateFluentApiForAnnotation(ref annotations, RelationalAnnotationNames.DefaultSchema, nameof(RelationalModelBuilderExtensions.HasDefaultSchema), stringBuilder); + IgnoreAnnotationTypes(annotations, RelationalAnnotationNames.DbFunction); + GenerateAnnotations(annotations, stringBuilder); } @@ -609,6 +611,18 @@ protected virtual void IgnoreAnnotations( } } + protected virtual void IgnoreAnnotationTypes( + [NotNull] IList annotations, [NotNull] params string[] annotationPrefixes) + { + Check.NotNull(annotations, nameof(annotations)); + Check.NotNull(annotationPrefixes, nameof(annotationPrefixes)); + + foreach(var ignoreAnnotation in annotations.Where(a => annotationPrefixes.Any(pre => a.Name.StartsWith(pre, StringComparison.OrdinalIgnoreCase))).ToList()) + { + annotations.Remove(ignoreAnnotation); + } + } + protected virtual void GenerateAnnotations( [NotNull] IReadOnlyList annotations, [NotNull] IndentedStringBuilder stringBuilder) { diff --git a/src/EFCore.InMemory/Extensions/InMemoryServiceCollectionExtensions.cs b/src/EFCore.InMemory/Extensions/InMemoryServiceCollectionExtensions.cs index 49a1304df1e..1f05eee2550 100644 --- a/src/EFCore.InMemory/Extensions/InMemoryServiceCollectionExtensions.cs +++ b/src/EFCore.InMemory/Extensions/InMemoryServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using JetBrains.Annotations; +using Remotion.Linq.Parsing.ExpressionVisitors.TreeEvaluation; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure.Internal; using Microsoft.EntityFrameworkCore.Query; @@ -64,6 +65,7 @@ public static IServiceCollection AddEntityFrameworkInMemoryDatabase([NotNull] th .TryAdd() .TryAdd() .TryAdd() + .TryAdd() .TryAddProviderSpecificServices(b => b .TryAddSingleton() .TryAddSingleton() diff --git a/src/EFCore.Relational.Specification.Tests/TestModels/NorthwindDbFunction/NorthwindDbFunctionContext.cs b/src/EFCore.Relational.Specification.Tests/TestModels/NorthwindDbFunction/NorthwindDbFunctionContext.cs new file mode 100644 index 00000000000..72fccd2b4fd --- /dev/null +++ b/src/EFCore.Relational.Specification.Tests/TestModels/NorthwindDbFunction/NorthwindDbFunctionContext.cs @@ -0,0 +1,88 @@ +// 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 Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.TestModels.Northwind; + +namespace Microsoft.EntityFrameworkCore +{ + public class NorthwindDbFunctionContext : NorthwindContext + { + public NorthwindDbFunctionContext(DbContextOptions options, QueryTrackingBehavior queryTrackingBehavior = QueryTrackingBehavior.TrackAll) + : base(options, queryTrackingBehavior) + { + } + + public enum ReportingPeriod + { + Winter = 0, + Spring, + Summer, + Fall + } + + public static int MyCustomLength(string s) + { + throw new Exception(); + } + + [DbFunction(Schema = "dbo", Name = "EmployeeOrderCount")] + public static int EmployeeOrderCount(int employeeId) + { + throw new NotImplementedException(); + } + + [DbFunction(Schema = "dbo", Name = "EmployeeOrderCount")] + public static int EmployeeOrderCountWithClient(int employeeId) + { + switch (employeeId) + { + case 3: return 127; + default: return 1; + } + } + + [DbFunction(Schema = "dbo")] + public static bool IsTopEmployee(int employeeId) + { + throw new NotImplementedException(); + } + + [DbFunction(Schema = "dbo")] + public static int GetEmployeeWithMostOrdersAfterDate(DateTime? startDate) + { + throw new NotImplementedException(); + } + + [DbFunction(Schema = "dbo")] + public static DateTime? GetReportingPeriodStartDate(ReportingPeriod periodId) + { + throw new NotImplementedException(); + } + + [DbFunction(Schema = "dbo")] + public static string StarValue(int starCount, int value) + { + throw new NotImplementedException(); + } + + [DbFunction(Name = "StarValue", Schema = "dbo")] + public static string StarValueAlternateParamOrder([DbFunctionParameter(ParameterIndex = 1)]int value, [DbFunctionParameter(ParameterIndex = 0)]int starCount) + { + throw new NotImplementedException(); + } + + [DbFunction(Schema = "dbo")] + public static int AddValues(int a, int b) + { + throw new NotImplementedException(); + } + + [DbFunction(Schema = "dbo")] + public static DateTime GetBestYearEver() + { + throw new NotImplementedException(); + } + } +} diff --git a/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs b/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs index fa4a71171fc..19ac08ce834 100644 --- a/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs +++ b/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs @@ -21,6 +21,7 @@ using Microsoft.EntityFrameworkCore.Update.Internal; using Microsoft.EntityFrameworkCore.ValueGeneration; using Microsoft.Extensions.DependencyInjection; +using Remotion.Linq.Parsing.ExpressionVisitors.TreeEvaluation; namespace Microsoft.EntityFrameworkCore.Infrastructure { @@ -65,7 +66,7 @@ private static readonly IDictionary _relationalSer { typeof(ISqlTranslatingExpressionVisitorFactory), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(IUpdateSqlGenerator), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(IMemberTranslator), new ServiceCharacteristics(ServiceLifetime.Singleton) }, - { typeof(IMethodCallTranslator), new ServiceCharacteristics(ServiceLifetime.Singleton) }, + { typeof(ICompositeMethodCallTranslator), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(IQuerySqlGeneratorFactory), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(ICommandBatchPreparer), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IModificationCommandBatchFactory), new ServiceCharacteristics(ServiceLifetime.Scoped) }, @@ -77,7 +78,7 @@ private static readonly IDictionary _relationalSer { typeof(IRelationalConnection), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IRelationalDatabaseCreator), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IHistoryRepository), new ServiceCharacteristics(ServiceLifetime.Scoped) }, - { typeof(INamedConnectionStringResolver), new ServiceCharacteristics(ServiceLifetime.Scoped) } + { typeof(INamedConnectionStringResolver), new ServiceCharacteristics(ServiceLifetime.Scoped) }, }; /// @@ -115,6 +116,7 @@ public override EntityFrameworkServicesBuilder TryAddCoreServices() TryAdd(); TryAdd(); TryAdd(); + TryAdd(); TryAdd(); TryAdd(); TryAdd(); @@ -150,6 +152,7 @@ public override EntityFrameworkServicesBuilder TryAddCoreServices() TryAdd(); TryAdd(); TryAdd(); + TryAdd(); ServiceCollectionMap.GetInfrastructure() .AddDependencySingleton() diff --git a/src/EFCore.Relational/Infrastructure/Internal/RelationalModelSource.cs b/src/EFCore.Relational/Infrastructure/Internal/RelationalModelSource.cs index b8e4dc9e017..b491219c2cb 100644 --- a/src/EFCore.Relational/Infrastructure/Internal/RelationalModelSource.cs +++ b/src/EFCore.Relational/Infrastructure/Internal/RelationalModelSource.cs @@ -1,10 +1,7 @@ // 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; using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore.Internal; -using Microsoft.EntityFrameworkCore.Metadata.Internal; namespace Microsoft.EntityFrameworkCore.Infrastructure.Internal { @@ -18,25 +15,5 @@ public RelationalModelSource([NotNull] ModelSourceDependencies dependencies) : base(dependencies) { } - - /// - /// 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 void FindSets(ModelBuilder modelBuilder, DbContext context) - { - base.FindSets(modelBuilder, context); - - var sets = Dependencies.SetFinder.CreateClrTypeDbSetMapping(context); - - foreach (var entityType in modelBuilder.Model.GetEntityTypes().Cast()) - { - if (entityType.BaseType == null - && sets.ContainsKey(entityType.ClrType)) - { - entityType.Builder.Relational(ConfigurationSource.Convention).ToTable(sets[entityType.ClrType].Name); - } - } - } } } diff --git a/src/EFCore.Relational/Infrastructure/RelationalModelCustomizer.cs b/src/EFCore.Relational/Infrastructure/RelationalModelCustomizer.cs new file mode 100644 index 00000000000..f76c3e68d34 --- /dev/null +++ b/src/EFCore.Relational/Infrastructure/RelationalModelCustomizer.cs @@ -0,0 +1,72 @@ +// 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; +using System.Reflection; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Infrastructure +{ + /// + /// 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 RelationalModelCustomizer : ModelCustomizer + { + public RelationalModelCustomizer([NotNull] ModelCustomizerDependencies dependencies) + : base(dependencies) + { + } + + public override void Customize(ModelBuilder modelBuilder, DbContext dbContext) + { + FindDbFunctions(modelBuilder, dbContext); + + base.Customize(modelBuilder, dbContext); + } + + /// + /// 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 virtual void FindDbFunctions([NotNull] ModelBuilder modelBuilder, [NotNull] DbContext context) + { + Check.NotNull(modelBuilder, nameof(modelBuilder)); + Check.NotNull(context, nameof(context)); + + var functions = context.GetType().GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy) + .Where(mi => mi.IsStatic + && mi.IsPublic + && mi.GetCustomAttributes(typeof(DbFunctionAttribute)).Any()); + + foreach (var function in functions) + { + modelBuilder.HasDbFunction(function); + } + } + + /// + /// 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 void FindSets(ModelBuilder modelBuilder, DbContext context) + { + base.FindSets(modelBuilder, context); + + var sets = Dependencies.SetFinder.CreateClrTypeDbSetMapping(context); + + foreach (var entityType in modelBuilder.Model.GetEntityTypes().Cast()) + { + if (entityType.BaseType == null + && sets.ContainsKey(entityType.ClrType)) + { + entityType.Builder.Relational(ConfigurationSource.Convention).ToTable(sets[entityType.ClrType].Name); + } + } + } + } +} diff --git a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs index 18f86e8e31f..fbbfe40e730 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata; @@ -56,6 +57,53 @@ public override void Validate(IModel model) ValidateDataTypes(model); ValidateDefaultValuesOnKeys(model); ValidateBoolsWithDefaults(model); + ValidateDbFunctions(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. + /// + protected virtual void ValidateDbFunctions([NotNull] IModel model) + { + foreach (var dbFunction in model.Relational().DbFunctions) + { + if (string.IsNullOrEmpty(dbFunction.Name)) + throw new InvalidOperationException(CoreStrings.DbFunctionNameEmpty()); + + var paramIndexes = dbFunction.Parameters.Select(fp => fp.Index).ToArray(); + var dbFuncName = $"{dbFunction.MethodInfo.DeclaringType?.Name}.{dbFunction.MethodInfo.Name}"; + + if (paramIndexes.Distinct().Count() != dbFunction.Parameters.Count) + throw new InvalidOperationException(CoreStrings.DbFunctionDuplicateIndex(dbFuncName)); + + if (Enumerable.Range(0, paramIndexes.Length).Except(paramIndexes).Any()) + throw new InvalidOperationException(CoreStrings.DbFunctionNonContinuousIndex(dbFuncName)); + + if (dbFunction.MethodInfo.IsStatic == false + && dbFunction.MethodInfo.DeclaringType.GetTypeInfo().IsSubclassOf(typeof(DbContext))) + { + throw new InvalidOperationException(CoreStrings.DbFunctionDbContextMethodMustBeStatic(dbFuncName)); + } + + if(dbFunction.TranslateCallback == null) + { + if (dbFunction.ReturnType == null || RelationalDependencies.TypeMapper.IsTypeMapped(dbFunction.ReturnType) == false) + throw new InvalidOperationException(CoreStrings.DbFunctionInvalidReturnType(dbFunction.MethodInfo, dbFunction.ReturnType)); + + foreach (var parameter in dbFunction.Parameters) + { + if (parameter.ParameterType == null || RelationalDependencies.TypeMapper.IsTypeMapped(parameter.ParameterType) == false) + { + throw new InvalidOperationException( + CoreStrings.DbFunctionInvalidParameterType( + dbFunction.MethodInfo, + parameter.Name, + dbFunction.ReturnType)); + } + } + } + } } /// diff --git a/src/EFCore.Relational/Metadata/Conventions/Internal/RelationalConventionSetBuilder.cs b/src/EFCore.Relational/Metadata/Conventions/Internal/RelationalConventionSetBuilder.cs index 17617bab601..2c133f6a2fc 100644 --- a/src/EFCore.Relational/Metadata/Conventions/Internal/RelationalConventionSetBuilder.cs +++ b/src/EFCore.Relational/Metadata/Conventions/Internal/RelationalConventionSetBuilder.cs @@ -63,6 +63,8 @@ public virtual ConventionSet AddConventions(ConventionSet conventionSet) conventionSet.ModelBuiltConventions.Add(new RelationalTypeMappingConvention(Dependencies.TypeMapper)); + conventionSet.ModelAnnotationChangedConventions.Add(new RelationalDbFunctionConvention()); + return conventionSet; } diff --git a/src/EFCore.Relational/Metadata/Conventions/Internal/RelationalDbFunctionConvention.cs b/src/EFCore.Relational/Metadata/Conventions/Internal/RelationalDbFunctionConvention.cs new file mode 100644 index 00000000000..4bba94e3204 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Conventions/Internal/RelationalDbFunctionConvention.cs @@ -0,0 +1,55 @@ +// 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.Linq; +using System.Reflection; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal +{ + public class RelationalDbFunctionConvention : IModelAnnotationChangedConvention + { + public virtual Annotation Apply(InternalModelBuilder modelBuilder, string name, Annotation annotation, Annotation oldAnnotation) + { + Check.NotNull(modelBuilder, nameof(modelBuilder)); + Check.NotNull(name, nameof(name)); + + if (name.StartsWith(RelationalAnnotationNames.DbFunction, StringComparison.OrdinalIgnoreCase) + && annotation != null + && oldAnnotation == null) + { + var dbFunctionBuilder = new InternalDbFunctionBuilder((DbFunction)annotation.Value); + + var dbFuncMethodInfo = dbFunctionBuilder.Metadata.MethodInfo; + + var dbFuncAttribute = dbFuncMethodInfo.GetCustomAttributes().SingleOrDefault(); + + dbFunctionBuilder.HasName(dbFuncAttribute?.Name ?? dbFuncMethodInfo.Name, ConfigurationSource.Convention); + dbFunctionBuilder.HasSchema(dbFuncAttribute?.Schema ?? modelBuilder.Metadata.Relational().DefaultSchema, ConfigurationSource.Convention); + + var parameters = dbFuncMethodInfo.GetParameters() + .Where(p => p.ParameterType != typeof(DbFunctions)) + .Select((pi, i) => new + { + ParameterIndex = i, + ParameterInfo = pi, + DbFuncParamAttr = pi.GetCustomAttributes().SingleOrDefault(), + pi.ParameterType + }); + + foreach (var p in parameters) + { + var paramBuilder = dbFunctionBuilder.HasParameter(p.ParameterInfo.Name, ConfigurationSource.Convention); + + paramBuilder.HasIndex(p.DbFuncParamAttr?.ParameterIndex ?? p.ParameterIndex, ConfigurationSource.Convention); + paramBuilder.HasType(p.ParameterType, ConfigurationSource.Convention); + } + } + + return annotation; + } + } +} diff --git a/src/EFCore.Relational/Metadata/DbFunctionAttribute.cs b/src/EFCore.Relational/Metadata/DbFunctionAttribute.cs new file mode 100644 index 00000000000..2acd7dbfa59 --- /dev/null +++ b/src/EFCore.Relational/Metadata/DbFunctionAttribute.cs @@ -0,0 +1,58 @@ +// 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 JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Metadata +{ + /// + /// Defines a user defined database function + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class DbFunctionAttribute : Attribute + { + private string _name; + + /// + /// Defines a user defined database function + /// + public DbFunctionAttribute() + { + } + + /// + /// Defines a user defined database function + /// + /// The name of the function in the underlying datastore. + /// The schema where the function lives in the underlying datastore. + public DbFunctionAttribute([NotNull] string name, [CanBeNull] string schema) + { + Check.NotEmpty(name, nameof(name)); + + Schema = schema; + Name = name; + } + + /// + /// The name of the function in the underlying datastore. + /// + public string Name + { + get { return _name; } + + [param: NotNull] + set + { + Check.NotNull(value, nameof(Name)); + _name = value; + } + } + + /// + /// The schema where the function lives in the underlying datastore. + /// + public string Schema { get; [param: CanBeNull] set; } + } +} diff --git a/src/EFCore.Relational/Metadata/DbFunctionBuilder.cs b/src/EFCore.Relational/Metadata/DbFunctionBuilder.cs new file mode 100644 index 00000000000..dd32d940059 --- /dev/null +++ b/src/EFCore.Relational/Metadata/DbFunctionBuilder.cs @@ -0,0 +1,102 @@ +// 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 System.Linq.Expressions; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Query.Expressions; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Metadata +{ + /// + /// + /// Provides a simple API for configuring a . + /// + /// + /// Instances of this class are returned from methods when using the API + /// and it is not designed to be directly constructed in your application code. + /// + /// + /// + public class DbFunctionBuilder + { + private readonly InternalDbFunctionBuilder _builder; + + /// + /// 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 DbFunctionBuilder([NotNull] DbFunction dbFunction) + { + Check.NotNull(dbFunction, nameof(dbFunction)); + + _builder = new InternalDbFunctionBuilder(dbFunction); + } + + public virtual IMutableDbFunction Metadata => _builder.Metadata; + + /// + /// 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 DbFunctionBuilder HasSchema([CanBeNull]string schema) + { + _builder.HasSchema(schema, ConfigurationSource.Explicit); + + return this; + } + + /// + /// 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 DbFunctionBuilder HasName([NotNull] string name) + { + Check.NotNull(name, nameof(name)); + + _builder.HasName(name, ConfigurationSource.Explicit); + + return this; + } + + /// + /// 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 DbFunctionParameterBuilder HasParameter([NotNull] string name) + { + Check.NotNull(name, nameof(name)); + + return new DbFunctionParameterBuilder(_builder.HasParameter(name, ConfigurationSource.Explicit)); + } + + /// + /// 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 DbFunctionBuilder HasReturnType([NotNull] Type returnType) + { + Check.NotNull(returnType, nameof(returnType)); + + _builder.HasReturnType(returnType, ConfigurationSource.Explicit); + + return this; + } + + /// + /// 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 DbFunctionBuilder TranslateWith([NotNull] Func, IDbFunction, SqlFunctionExpression> translateCallback) + { + Check.NotNull(translateCallback, nameof(translateCallback)); + + _builder.TranslateWith(translateCallback); + + return this; + } + } +} diff --git a/src/EFCore.Relational/Metadata/DbFunctionParameterAttribute.cs b/src/EFCore.Relational/Metadata/DbFunctionParameterAttribute.cs new file mode 100644 index 00000000000..9779b019c24 --- /dev/null +++ b/src/EFCore.Relational/Metadata/DbFunctionParameterAttribute.cs @@ -0,0 +1,20 @@ +// 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 JetBrains.Annotations; + +namespace Microsoft.EntityFrameworkCore.Metadata +{ + /// + /// Define a parameter for a user defined database function + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class DbFunctionParameterAttribute : Attribute + { + /// + /// Sets the index order for this parameter on the parent function + /// + public int ParameterIndex { get; [param: NotNull] set; } + } +} diff --git a/src/EFCore.Relational/Metadata/DbFunctionParameterBuilder.cs b/src/EFCore.Relational/Metadata/DbFunctionParameterBuilder.cs new file mode 100644 index 00000000000..84b15c30b97 --- /dev/null +++ b/src/EFCore.Relational/Metadata/DbFunctionParameterBuilder.cs @@ -0,0 +1,62 @@ +// 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 JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Metadata +{ + /// + /// + /// Provides a simple API for configuring an . + /// + /// + /// Instances of this class are returned from methods when using the API + /// and it is not designed to be directly constructed in your application code. + /// + /// + public class DbFunctionParameterBuilder + { + private readonly InternalDbFunctionParameterBuilder _builder; + + /// + /// 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 DbFunctionParameterBuilder([NotNull] DbFunctionParameter dbFunctionParameter) + { + Check.NotNull(dbFunctionParameter, nameof(dbFunctionParameter)); + + _builder = new InternalDbFunctionParameterBuilder(dbFunctionParameter); + } + + /// + /// 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 DbFunctionParameterBuilder([NotNull] InternalDbFunctionParameterBuilder builder) + { + Check.NotNull(builder, nameof(builder)); + + _builder = builder; + } + + /// + /// 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 IMutableDbFunctionParameter Metadata => _builder.Parameter; + + /// + /// 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 DbFunctionParameterBuilder HasIndex(int index) + { + _builder.HasIndex(index, ConfigurationSource.Explicit); + + return this; + } + } +} diff --git a/src/EFCore.Relational/Metadata/IDbFunction.cs b/src/EFCore.Relational/Metadata/IDbFunction.cs new file mode 100644 index 00000000000..90e568280f0 --- /dev/null +++ b/src/EFCore.Relational/Metadata/IDbFunction.cs @@ -0,0 +1,48 @@ +// 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 System.Reflection; +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Query.Expressions; + +namespace Microsoft.EntityFrameworkCore.Metadata +{ + /// + /// Represents a db function in an . + /// + public interface IDbFunction + { + /// + /// The schema where the function lives in the underlying datastore. + /// + string Schema { get; } + + /// + /// The name of the function in the underlying datastore. + /// + string Name { get; } + + /// + /// The list of parameters which are passed to the underlying datastores function. + /// + IReadOnlyList Parameters { get; } + + /// + /// The .Net method which maps to the function in the underlying datastore + /// + MethodInfo MethodInfo { get; } + + /// + /// The return type of the mapped .Net method + /// + Type ReturnType { get; } + + /// + /// A translate callback for converting a method call into a sql function + /// + Func, IDbFunction, SqlFunctionExpression> TranslateCallback { get; } + } +} diff --git a/src/EFCore.Relational/Metadata/IDbFunctionParameter.cs b/src/EFCore.Relational/Metadata/IDbFunctionParameter.cs new file mode 100644 index 00000000000..73baf9c5542 --- /dev/null +++ b/src/EFCore.Relational/Metadata/IDbFunctionParameter.cs @@ -0,0 +1,28 @@ +// 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; + +namespace Microsoft.EntityFrameworkCore.Metadata +{ + /// + /// Represents a db function parameter in an . + /// + public interface IDbFunctionParameter + { + /// + /// The name of the parameter on the .Net method. + /// + string Name { get; } + + /// + /// The index of the parameter on the mapped datastore method. + /// + int Index { get; } + + /// + /// The .Net parameter type. + /// + Type ParameterType { get; } + } +} diff --git a/src/EFCore.Relational/Metadata/IMutableDbFunction.cs b/src/EFCore.Relational/Metadata/IMutableDbFunction.cs new file mode 100644 index 00000000000..7fba4c8acb7 --- /dev/null +++ b/src/EFCore.Relational/Metadata/IMutableDbFunction.cs @@ -0,0 +1,40 @@ +// 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 System.Linq.Expressions; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Query.Expressions; + +namespace Microsoft.EntityFrameworkCore.Metadata +{ + public interface IMutableDbFunction : IDbFunction + { + /// + /// The schema where the function lives in the underlying datastore. + /// + new string Schema { get; [param: CanBeNull] set; } + + /// + /// The name of the function in the underlying datastore. + /// + new string Name { get; [param: NotNull] set;} + + /// + /// The return type of the mapped .Net method + /// + new Type ReturnType { get; [param: NotNull] set;} + + /// + /// A translate callback for converting a method call into a sql function + /// + new Func, IDbFunction, SqlFunctionExpression> TranslateCallback { get; [param: CanBeNull] set; } + + /// + /// Add a dbFunctionParameter to this DbFunction + /// + DbFunctionParameter AddParameter([NotNull] string name); + } +} diff --git a/src/EFCore.Relational/Metadata/IMutableDbFunctionParameter.cs b/src/EFCore.Relational/Metadata/IMutableDbFunctionParameter.cs new file mode 100644 index 00000000000..e1cfbe25b74 --- /dev/null +++ b/src/EFCore.Relational/Metadata/IMutableDbFunctionParameter.cs @@ -0,0 +1,29 @@ +// 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 JetBrains.Annotations; + +namespace Microsoft.EntityFrameworkCore.Metadata +{ + /// + /// Represents a db function parameter in an . + /// + public interface IMutableDbFunctionParameter : IDbFunctionParameter + { + /// + /// The name of the parameter on the .Net method. + /// + new string Name { get; [param: NotNull] set; } + + /// + /// The index of the parameter on the mapped datastore method. + /// + new int Index { get; set; } + + /// + /// The .Net parameter type. + /// + new Type ParameterType { get; [param: NotNull] set; } + } +} diff --git a/src/EFCore.Relational/Metadata/IRelationalModelAnnotations.cs b/src/EFCore.Relational/Metadata/IRelationalModelAnnotations.cs index 73a6e63ac75..cbc701e688c 100644 --- a/src/EFCore.Relational/Metadata/IRelationalModelAnnotations.cs +++ b/src/EFCore.Relational/Metadata/IRelationalModelAnnotations.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.Collections.Generic; +using System.Reflection; using JetBrains.Annotations; namespace Microsoft.EntityFrameworkCore.Metadata @@ -9,8 +10,10 @@ namespace Microsoft.EntityFrameworkCore.Metadata public interface IRelationalModelAnnotations { ISequence FindSequence([NotNull] string name, [CanBeNull] string schema = null); + IDbFunction FindDbFunction([NotNull] MethodInfo method); IReadOnlyList Sequences { get; } + IReadOnlyList DbFunctions { get; } string DefaultSchema { get; } } diff --git a/src/EFCore.Relational/Metadata/Internal/DbFunction.cs b/src/EFCore.Relational/Metadata/Internal/DbFunction.cs new file mode 100644 index 00000000000..3a7b67c749b --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/DbFunction.cs @@ -0,0 +1,224 @@ +// 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 System.Collections.ObjectModel; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Utilities; +using Microsoft.EntityFrameworkCore.Query.ExpressionTranslators; +using Microsoft.EntityFrameworkCore.Query.Expressions; + +namespace Microsoft.EntityFrameworkCore.Metadata.Internal +{ + /// + /// Represents a db function in an . + /// + public class DbFunction : IMutableDbFunction, IMethodCallTranslator + { + private readonly SortedDictionary _parameters + = new SortedDictionary(StringComparer.OrdinalIgnoreCase); + + private string _schema; + private string _name; + private Type _returnType; + + private ConfigurationSource? _schemaConfigurationSource; + private ConfigurationSource? _nameConfigurationSource; + private ConfigurationSource? _returnTypeConfigurationSource; + + private DbFunction([NotNull] MethodInfo dbFunctionMethodInfo, + [NotNull] IMutableModel model, + [NotNull] string annotationPrefix, + [CanBeNull] string name, + [CanBeNull] string schema, + ConfigurationSource configurationSource) + { + Check.NotNull(dbFunctionMethodInfo, nameof(dbFunctionMethodInfo)); + Check.NotNull(model, nameof(model)); + Check.NotNull(annotationPrefix, nameof(annotationPrefix)); + + if (dbFunctionMethodInfo.IsGenericMethod) + throw new ArgumentException(CoreStrings.DbFunctionGenericMethodNotSupported(dbFunctionMethodInfo)); + + if (name != null) + _nameConfigurationSource = configurationSource; + + if (schema != null) + _schemaConfigurationSource = configurationSource; + + _returnTypeConfigurationSource = configurationSource; + + _name = name; + _schema = schema; + _returnType = dbFunctionMethodInfo.ReturnType; + MethodInfo = dbFunctionMethodInfo; + + model[BuildAnnotationName(annotationPrefix, dbFunctionMethodInfo)] = this; + } + + public static DbFunction GetOrAddDbFunction( + [NotNull] IMutableModel model, + [NotNull] MethodInfo methodInfo, + [NotNull] string annotationPrefix, + ConfigurationSource configurationSource, + [CanBeNull] string name = null, + [CanBeNull] string schema = null) + => FindDbFunction(model, annotationPrefix, methodInfo) + ?? new DbFunction(methodInfo, model, annotationPrefix, name, schema, configurationSource); + + public static DbFunction FindDbFunction([NotNull] IModel model, + [NotNull] string annotationPrefix, + [NotNull] MethodInfo methodInfo) + { + Check.NotNull(model, nameof(model)); + Check.NotNull(annotationPrefix, nameof(annotationPrefix)); + Check.NotNull(methodInfo, nameof(methodInfo)); + + return model[BuildAnnotationName(annotationPrefix, methodInfo)] as DbFunction; + } + + public static IEnumerable GetDbFunctions([NotNull] IModel model, [NotNull] string annotationPrefix) + { + Check.NotNull(model, nameof(model)); + Check.NotEmpty(annotationPrefix, nameof(annotationPrefix)); + + return model.GetAnnotations() + .Where(a => a.Name.StartsWith(annotationPrefix, StringComparison.Ordinal)) + .Select(a => a.Value) + .Cast(); + } + + private static string BuildAnnotationName(string annotationPrefix, MethodInfo methodInfo) + => $@"{annotationPrefix}{methodInfo.Name}({string.Join(",", methodInfo.GetParameters().Select(p => p.ParameterType.Name))})"; + + /// + /// The schema where the function lives in the underlying datastore. + /// + public virtual string Schema + { + get =>_schema; + + [param: CanBeNull] set => SetSchema(value, ConfigurationSource.Explicit); + } + + public virtual void SetSchema([CanBeNull] string schema, ConfigurationSource configurationSource) + { + _schema = schema; + UpdateSchemaConfigurationSource(configurationSource); + } + + private void UpdateSchemaConfigurationSource(ConfigurationSource configurationSource) + => _schemaConfigurationSource = configurationSource.Max(_schemaConfigurationSource); + + public virtual ConfigurationSource? GetSchemaConfigurationSource() => _schemaConfigurationSource; + + /// + /// The name of the function in the underlying datastore. + /// + public virtual string Name + { + get => _name; + + [param: NotNull] set => SetName(value, ConfigurationSource.Explicit); + } + + public virtual void SetName([NotNull] string name, ConfigurationSource configurationSource) + { + Check.NotNull(name, nameof(name)); + + _name = name; + UpdateNameConfigurationSource(configurationSource); + } + + private void UpdateNameConfigurationSource(ConfigurationSource configurationSource) + => _nameConfigurationSource = configurationSource.Max(_nameConfigurationSource); + + public virtual ConfigurationSource? GetNameConfigurationSource() => _nameConfigurationSource; + + /// + /// The return type of the mapped .Net method + /// + public virtual Type ReturnType + { + get => _returnType; + + [param: NotNull] set => SetReturnType(value, ConfigurationSource.Explicit); + } + + public virtual void SetReturnType([NotNull] Type returnType, ConfigurationSource configurationSource) + { + Check.NotNull(returnType, nameof(returnType)); + + _returnType = returnType; + UpdateReturnTypeConfigurationSource(configurationSource); + } + + private void UpdateReturnTypeConfigurationSource(ConfigurationSource configurationSource) + => _returnTypeConfigurationSource = configurationSource.Max(_returnTypeConfigurationSource); + + public virtual ConfigurationSource? GetReturnTypeConfigurationSource() => _nameConfigurationSource; + + /// + /// The list of parameters which are passed to the underlying datastores function. + /// + public virtual IReadOnlyList Parameters => _parameters.Values.ToList(); + + /// + /// The .Net method which maps to the function in the underlying datastore + /// + public virtual MethodInfo MethodInfo { get; } + + /// + /// If set this callback is used to translate the .Net method call to a Linq Expression. + /// + public virtual Func, IDbFunction, SqlFunctionExpression> TranslateCallback { get; [param: CanBeNull] set; } + + public virtual DbFunctionParameter AddParameter(string name) + => AddParameter(name, ConfigurationSource.Explicit); + + public virtual DbFunctionParameter AddParameter([NotNull] string name, ConfigurationSource configurationSource) + { + Check.NotNull(name, nameof(name)); + + var newParam = new DbFunctionParameter(name, configurationSource); + + _parameters.Add(newParam.Name, newParam); + + return newParam; + } + + public virtual DbFunctionParameter FindParameter([NotNull] string name, ConfigurationSource configurationSource) + { + Check.NotNull(name, nameof(name)); + + DbFunctionParameter parameter; + + if(_parameters.TryGetValue(name, out parameter)) + parameter.UpdateConfigurationSource(configurationSource); + + return parameter; + } + + Expression IMethodCallTranslator.Translate(MethodCallExpression methodCallExpression) + { + Check.NotNull(methodCallExpression, nameof(methodCallExpression)); + + var methodArgs = methodCallExpression.Method.GetParameters().Zip(methodCallExpression.Arguments, (p, a) => new { Parameter = p, Argument = a }); + + var arguments = new ReadOnlyCollection( + (from dbParam in Parameters + join methodArg in methodArgs on dbParam.Name equals methodArg.Parameter.Name into passParams + from passParam in passParams + orderby dbParam.Index + select passParam.Argument).ToList()); + + return TranslateCallback?.Invoke(arguments, this) + ?? new SqlFunctionExpression(Name, ReturnType, Schema, arguments); + } + } +} \ No newline at end of file diff --git a/src/EFCore.Relational/Metadata/Internal/DbFunctionParameter.cs b/src/EFCore.Relational/Metadata/Internal/DbFunctionParameter.cs new file mode 100644 index 00000000000..bd58b419031 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/DbFunctionParameter.cs @@ -0,0 +1,92 @@ +// 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 JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Metadata.Internal +{ + public class DbFunctionParameter : IMutableDbFunctionParameter + { + private int _index; + private Type _parameterType; + private string _name; + private ConfigurationSource? _parameterIndexConfigurationSource; + private ConfigurationSource? _parameterTypeConfigurationSource; + private ConfigurationSource? _nameConfigurationSource; + private ConfigurationSource _configurationSource; + + public DbFunctionParameter([NotNull] string name, ConfigurationSource configurationSource) + { + Check.NotEmpty(name, nameof(name)); + + _name = name; + _configurationSource = configurationSource; + _nameConfigurationSource = configurationSource; + } + + public virtual void UpdateConfigurationSource(ConfigurationSource configurationSource) + => _configurationSource = configurationSource.Max(_configurationSource); + + public virtual ConfigurationSource? GetConfigurationSource() => _configurationSource; + + public virtual string Name + { + get => _name; + [param: NotNull] set => SetName(value, ConfigurationSource.Explicit); + } + + public virtual void SetName([NotNull] string name, ConfigurationSource configSource) + { + Check.NotNull(name, nameof(name)); + + UpdateNameConfigurationSource(configSource); + + _name = name; + } + + private void UpdateNameConfigurationSource(ConfigurationSource configurationSource) + => _nameConfigurationSource = configurationSource.Max(_nameConfigurationSource); + + public virtual ConfigurationSource? GetNameConfigurationSource() => _nameConfigurationSource; + + public virtual Type ParameterType + { + get => _parameterType; + [param: NotNull] set => SetParameterType(value, ConfigurationSource.Explicit); + } + + private void UpdateParameterTypeConfigurationSource(ConfigurationSource configurationSource) + => _parameterTypeConfigurationSource = configurationSource.Max(_parameterTypeConfigurationSource); + + public virtual ConfigurationSource? GetParameterTypeConfigurationSource() => _parameterTypeConfigurationSource; + + public virtual void SetParameterType([NotNull] Type parameterType, ConfigurationSource configSource) + { + Check.NotNull(parameterType, nameof(parameterType)); + + UpdateParameterTypeConfigurationSource(configSource); + + _parameterType = parameterType; + } + + public virtual int Index + { + get => _index; + set => SetParameterIndex(value, ConfigurationSource.Explicit); + } + + private void UpdateParameterIndexConfigurationSource(ConfigurationSource configurationSource) + => _parameterIndexConfigurationSource = configurationSource.Max(_parameterIndexConfigurationSource); + + public virtual ConfigurationSource? GetParameterIndexConfigurationSource() => _parameterIndexConfigurationSource; + + public virtual void SetParameterIndex(int index, ConfigurationSource configSource) + { + UpdateParameterIndexConfigurationSource(configSource); + + _index = index; + } + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/InternalDbFunctionBuilder.cs b/src/EFCore.Relational/Metadata/Internal/InternalDbFunctionBuilder.cs new file mode 100644 index 00000000000..6c587c060da --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/InternalDbFunctionBuilder.cs @@ -0,0 +1,97 @@ +// 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 System.Linq.Expressions; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Query.Expressions; + +namespace Microsoft.EntityFrameworkCore.Metadata.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 InternalDbFunctionBuilder + { + private readonly DbFunction _dbFunction; + + public InternalDbFunctionBuilder([NotNull] DbFunction dbFunction) + { + _dbFunction = dbFunction; + } + + public virtual IMutableDbFunction Metadata => _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 virtual InternalDbFunctionBuilder HasSchema([CanBeNull] string schema, ConfigurationSource configurationSource) + { + if (configurationSource.Overrides(_dbFunction.GetSchemaConfigurationSource()) + || _dbFunction.Schema == schema) + { + _dbFunction.SetSchema(schema, configurationSource); + } + + return this; + } + + /// + /// 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 InternalDbFunctionBuilder HasName([NotNull] string name, ConfigurationSource configurationSource) + { + if (configurationSource.Overrides(_dbFunction.GetNameConfigurationSource()) + || _dbFunction.Name == name) + { + _dbFunction.SetName(name, configurationSource); + } + + return this; + } + + /// + /// 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 InternalDbFunctionBuilder HasReturnType([NotNull] Type returnType, ConfigurationSource configurationSource) + { + if (configurationSource.Overrides(_dbFunction.GetReturnTypeConfigurationSource()) + || _dbFunction.ReturnType == returnType) + { + _dbFunction.SetReturnType(returnType, configurationSource); + } + + return this; + } + + /// + /// 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 InternalDbFunctionParameterBuilder HasParameter([NotNull] string name, ConfigurationSource configurationSource) + { + return new InternalDbFunctionParameterBuilder(GetOrCreateParameter(name, configurationSource)); + } + + private DbFunctionParameter GetOrCreateParameter(string name, ConfigurationSource configurationSource) + { + return _dbFunction.FindParameter(name, configurationSource) ?? _dbFunction.AddParameter(name, configurationSource); + } + + /// + /// 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 InternalDbFunctionBuilder TranslateWith([NotNull] Func, IDbFunction, SqlFunctionExpression> translateCallback) + { + _dbFunction.TranslateCallback = translateCallback; + + return this; + } + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/InternalDbFunctionParameterBuilder.cs b/src/EFCore.Relational/Metadata/Internal/InternalDbFunctionParameterBuilder.cs new file mode 100644 index 00000000000..a3d3e1b0578 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/InternalDbFunctionParameterBuilder.cs @@ -0,0 +1,80 @@ +// 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 JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Metadata.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 InternalDbFunctionParameterBuilder + { + private readonly DbFunctionParameter _dbFunctionParameter; + + /// + /// 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 DbFunctionParameter Parameter => _dbFunctionParameter; + + /// + /// 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 InternalDbFunctionParameterBuilder([NotNull] DbFunctionParameter dbFunctionParameter) + { + Check.NotNull(dbFunctionParameter, nameof(dbFunctionParameter)); + + _dbFunctionParameter = dbFunctionParameter; + } + + /// + /// 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 InternalDbFunctionParameterBuilder HasName([NotNull]string name, ConfigurationSource configurationSource) + { + if (configurationSource.Overrides(_dbFunctionParameter.GetNameConfigurationSource()) + || _dbFunctionParameter.Name == name) + { + _dbFunctionParameter.SetName(name, configurationSource); + } + + return this; + } + + /// + /// 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 InternalDbFunctionParameterBuilder HasType([NotNull] Type type, ConfigurationSource configurationSource) + { + if (configurationSource.Overrides(_dbFunctionParameter.GetParameterTypeConfigurationSource()) + || _dbFunctionParameter.ParameterType == type) + { + _dbFunctionParameter.SetParameterType(type, configurationSource); + } + + return this; + } + + /// + /// 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 InternalDbFunctionParameterBuilder HasIndex(int index, ConfigurationSource configurationSource) + { + if (configurationSource.Overrides(_dbFunctionParameter.GetParameterIndexConfigurationSource()) + || _dbFunctionParameter.Index == index) + { + _dbFunctionParameter.SetParameterIndex(index, configurationSource); + } + + return this; + } + } +} diff --git a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs index 082eb543e88..b465bf5185f 100644 --- a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs +++ b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs @@ -84,5 +84,10 @@ public static class RelationalAnnotationNames /// The name for filter annotations. /// public const string TypeMapping = Prefix + "TypeMapping"; + + /// + /// The name for DbFunction annotations. + /// + public const string DbFunction = Prefix + "DbFunction"; } } diff --git a/src/EFCore.Relational/Metadata/RelationalModelAnnotations.cs b/src/EFCore.Relational/Metadata/RelationalModelAnnotations.cs index 35f2cf08a5f..5119eb6cb23 100644 --- a/src/EFCore.Relational/Metadata/RelationalModelAnnotations.cs +++ b/src/EFCore.Relational/Metadata/RelationalModelAnnotations.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using System.Reflection; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Utilities; @@ -43,6 +44,15 @@ public virtual IMutableSequence GetOrAddSequence([NotNull] string name, [CanBeNu private static string BuildAnnotationName(string annotationPrefix, string name, string schema) => annotationPrefix + schema + "." + name; + public virtual IReadOnlyList DbFunctions + => DbFunction.GetDbFunctions(Model, RelationalAnnotationNames.DbFunction).ToList(); + + public virtual IDbFunction FindDbFunction(MethodInfo methodInfo) + => DbFunction.FindDbFunction(Model, RelationalAnnotationNames.DbFunction, methodInfo); + + public virtual DbFunction GetOrAddDbFunction([NotNull] MethodInfo methodInfo, ConfigurationSource configurationSource, [CanBeNull] string name = null, [CanBeNull] string schema = null) + => DbFunction.GetOrAddDbFunction((IMutableModel)Model, methodInfo, RelationalAnnotationNames.DbFunction, configurationSource, name, schema); + public virtual string DefaultSchema { get => (string)Annotations.GetAnnotation(RelationalAnnotationNames.DefaultSchema); diff --git a/src/EFCore.Relational/Query/ExpressionTranslators/ICompositeMethodCallTranslator.cs b/src/EFCore.Relational/Query/ExpressionTranslators/ICompositeMethodCallTranslator.cs new file mode 100644 index 00000000000..b5371919eb6 --- /dev/null +++ b/src/EFCore.Relational/Query/ExpressionTranslators/ICompositeMethodCallTranslator.cs @@ -0,0 +1,25 @@ +// 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; + +namespace Microsoft.EntityFrameworkCore.Query.ExpressionTranslators +{ + /// + /// A LINQ expression translator for CLR expressions. + /// + public interface ICompositeMethodCallTranslator + { + /// + /// Translates the given method call expression. + /// + /// The method call expression. + /// The current model. + /// + /// A SQL expression representing the translated MethodCallExpression. + /// + Expression Translate([NotNull] MethodCallExpression methodCallExpression, [NotNull] IModel model); + } +} diff --git a/src/EFCore.Relational/Query/ExpressionTranslators/RelationalCompositeMethodCallTranslator.cs b/src/EFCore.Relational/Query/ExpressionTranslators/RelationalCompositeMethodCallTranslator.cs index 13a759d6f30..c42166a305d 100644 --- a/src/EFCore.Relational/Query/ExpressionTranslators/RelationalCompositeMethodCallTranslator.cs +++ b/src/EFCore.Relational/Query/ExpressionTranslators/RelationalCompositeMethodCallTranslator.cs @@ -7,6 +7,7 @@ using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Query.ExpressionTranslators.Internal; using Microsoft.EntityFrameworkCore.Utilities; +using Microsoft.EntityFrameworkCore.Metadata; namespace Microsoft.EntityFrameworkCore.Query.ExpressionTranslators { @@ -14,7 +15,7 @@ namespace Microsoft.EntityFrameworkCore.Query.ExpressionTranslators /// A base composite method call translator that dispatches to multiple specialized /// method call translators. /// - public abstract class RelationalCompositeMethodCallTranslator : IMethodCallTranslator + public abstract class RelationalCompositeMethodCallTranslator : ICompositeMethodCallTranslator { private readonly List _methodCallTranslators; @@ -48,13 +49,15 @@ protected RelationalCompositeMethodCallTranslator( /// Translates the given method call expression. /// /// The method call expression. + /// The current model. /// /// A SQL expression representing the translated MethodCallExpression. /// - public virtual Expression Translate(MethodCallExpression methodCallExpression) - => _methodCallTranslators - .Select(translator => translator.Translate(methodCallExpression)) - .FirstOrDefault(translatedMethodCall => translatedMethodCall != null); + public virtual Expression Translate(MethodCallExpression methodCallExpression, IModel model) + => ((IMethodCallTranslator)model.Relational().FindDbFunction(methodCallExpression.Method))?.Translate(methodCallExpression) + ?? _methodCallTranslators + .Select(translator => translator.Translate(methodCallExpression)) + .FirstOrDefault(translatedMethodCall => translatedMethodCall != null); /// /// Adds additional translators to the dispatch list. diff --git a/src/EFCore.Relational/Query/ExpressionVisitors/SqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/ExpressionVisitors/SqlTranslatingExpressionVisitor.cs index 30f60c0dc05..f89934fb564 100644 --- a/src/EFCore.Relational/Query/ExpressionVisitors/SqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/ExpressionVisitors/SqlTranslatingExpressionVisitor.cs @@ -42,7 +42,7 @@ private static readonly Dictionary _inverseOpera }; private readonly IExpressionFragmentTranslator _compositeExpressionFragmentTranslator; - private readonly IMethodCallTranslator _methodCallTranslator; + private readonly ICompositeMethodCallTranslator _methodCallTranslator; private readonly IMemberTranslator _memberTranslator; private readonly RelationalQueryModelVisitor _queryModelVisitor; private readonly IRelationalTypeMapper _relationalTypeMapper; @@ -606,7 +606,7 @@ var boundExpression ? Expression.Call(operand, methodCallExpression.Method, arguments) : Expression.Call(methodCallExpression.Method, arguments); - var translatedExpression = _methodCallTranslator.Translate(boundExpression); + var translatedExpression = _methodCallTranslator.Translate(boundExpression, _queryModelVisitor.QueryCompilationContext.Model); if (translatedExpression != null) { diff --git a/src/EFCore.Relational/Query/ExpressionVisitors/SqlTranslatingExpressionVisitorDependencies.cs b/src/EFCore.Relational/Query/ExpressionVisitors/SqlTranslatingExpressionVisitorDependencies.cs index 23af760da02..cf6e9d5720d 100644 --- a/src/EFCore.Relational/Query/ExpressionVisitors/SqlTranslatingExpressionVisitorDependencies.cs +++ b/src/EFCore.Relational/Query/ExpressionVisitors/SqlTranslatingExpressionVisitorDependencies.cs @@ -50,7 +50,7 @@ public sealed class SqlTranslatingExpressionVisitorDependencies /// The relational type mapper. public SqlTranslatingExpressionVisitorDependencies( [NotNull] IExpressionFragmentTranslator compositeExpressionFragmentTranslator, - [NotNull] IMethodCallTranslator methodCallTranslator, + [NotNull] ICompositeMethodCallTranslator methodCallTranslator, [NotNull] IMemberTranslator memberTranslator, [NotNull] IRelationalTypeMapper relationalTypeMapper) { @@ -73,7 +73,7 @@ public SqlTranslatingExpressionVisitorDependencies( /// /// The method call translator. /// - public IMethodCallTranslator MethodCallTranslator { get; } + public ICompositeMethodCallTranslator MethodCallTranslator { get; } /// /// The member translator. @@ -102,7 +102,7 @@ public SqlTranslatingExpressionVisitorDependencies With([NotNull] IExpressionFra /// /// A replacement for the current dependency of this type. /// A new parameter object with the given service replaced. - public SqlTranslatingExpressionVisitorDependencies With([NotNull] IMethodCallTranslator methodCallTranslator) + public SqlTranslatingExpressionVisitorDependencies With([NotNull] ICompositeMethodCallTranslator methodCallTranslator) => new SqlTranslatingExpressionVisitorDependencies( CompositeExpressionFragmentTranslator, methodCallTranslator, diff --git a/src/EFCore.Relational/Query/Expressions/SqlFunctionExpression.cs b/src/EFCore.Relational/Query/Expressions/SqlFunctionExpression.cs index 62f445eeb73..c7dff9e0005 100644 --- a/src/EFCore.Relational/Query/Expressions/SqlFunctionExpression.cs +++ b/src/EFCore.Relational/Query/Expressions/SqlFunctionExpression.cs @@ -33,20 +33,36 @@ public SqlFunctionExpression( { } + /// + /// Initializes a new instance of the Microsoft.EntityFrameworkCore.Query.Expressions.SqlFunctionExpression class. + /// + /// Name of the function. + /// The return type. + /// The arguments. + public SqlFunctionExpression( + [NotNull] string functionName, + [NotNull] Type returnType, + [NotNull] IEnumerable arguments) + : this(functionName, returnType, /*schema*/ null, arguments) + { + } + /// /// Initializes a new instance of the class. /// /// Name of the function. /// The return type. + /// /// The schema this function exists in if any. /// The arguments. public SqlFunctionExpression( [NotNull] string functionName, [NotNull] Type returnType, + [CanBeNull] string schema, [NotNull] IEnumerable arguments) { FunctionName = functionName; Type = returnType; - + Schema = schema; _arguments = arguments.ToList().AsReadOnly(); } @@ -58,6 +74,14 @@ public SqlFunctionExpression( /// public virtual string FunctionName { get; } + /// + /// Gets the name of the schema. + /// + /// + /// The name of the schema. + /// + public virtual string Schema { get; } + /// /// The arguments. /// diff --git a/src/EFCore.Relational/Query/Internal/RelationalEvaluatableExpressionFilter.cs b/src/EFCore.Relational/Query/Internal/RelationalEvaluatableExpressionFilter.cs new file mode 100644 index 00000000000..6d518c7943f --- /dev/null +++ b/src/EFCore.Relational/Query/Internal/RelationalEvaluatableExpressionFilter.cs @@ -0,0 +1,30 @@ +// 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.Utilities; + +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 RelationalEvaluatableExpressionFilter : EvaluatableExpressionFilter + { + private readonly IModel _model; + + public RelationalEvaluatableExpressionFilter([NotNull] IModel model) + { + Check.NotNull(model, nameof(model)); + + _model = model; + } + + public override bool IsEvaluatableMethodCall(MethodCallExpression methodCallExpression) + => _model.Relational().FindDbFunction(methodCallExpression.Method) == null + && base.IsEvaluatableMethodCall(methodCallExpression); + } +} diff --git a/src/EFCore.Relational/Query/Sql/DefaultQuerySqlGenerator.cs b/src/EFCore.Relational/Query/Sql/DefaultQuerySqlGenerator.cs index c16601beeef..849b79a20a2 100644 --- a/src/EFCore.Relational/Query/Sql/DefaultQuerySqlGenerator.cs +++ b/src/EFCore.Relational/Query/Sql/DefaultQuerySqlGenerator.cs @@ -1275,7 +1275,7 @@ public virtual Expression VisitLike(LikeExpression likeExpression) /// public virtual Expression VisitSqlFunction(SqlFunctionExpression sqlFunctionExpression) { - GenerateFunctionCall(sqlFunctionExpression.FunctionName, sqlFunctionExpression.Arguments); + GenerateFunctionCall(sqlFunctionExpression.FunctionName, sqlFunctionExpression.Arguments, sqlFunctionExpression.Schema); return sqlFunctionExpression; } @@ -1285,18 +1285,31 @@ public virtual Expression VisitSqlFunction(SqlFunctionExpression sqlFunctionExpr /// /// The function name /// The function arguments + /// The function schema protected virtual void GenerateFunctionCall( - [NotNull] string functionName, [NotNull] IReadOnlyList arguments) + [NotNull] string functionName, [NotNull] IReadOnlyList arguments, + [CanBeNull] string schema = null) { Check.NotEmpty(functionName, nameof(functionName)); Check.NotNull(arguments, nameof(arguments)); + if (!string.IsNullOrWhiteSpace(schema)) + { + _relationalCommandBuilder.Append(SqlGenerator.DelimitIdentifier(schema)) + .Append("."); + } + + var parentTypeMapping = _typeMapping; + _typeMapping = null; + _relationalCommandBuilder.Append(functionName); _relationalCommandBuilder.Append("("); ProcessExpressionList(arguments); _relationalCommandBuilder.Append(")"); + + _typeMapping = parentTypeMapping; } /// diff --git a/src/EFCore.Relational/RelationalModelBuilderExtensions.cs b/src/EFCore.Relational/RelationalModelBuilderExtensions.cs index 5f201199af8..10a872d7d07 100644 --- a/src/EFCore.Relational/RelationalModelBuilderExtensions.cs +++ b/src/EFCore.Relational/RelationalModelBuilderExtensions.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.Reflection; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; @@ -201,6 +202,79 @@ public static ModelBuilder HasSequence( return modelBuilder; } + /// + /// Configures a database function when targeting a relational database. + /// + /// The model builder. + /// The methodInfo this dbFunction uses. + /// The name of the dbFunction. + /// The schema of the dbFunction. + /// The same builder instance so that multiple calls can be chained. + public static DbFunctionBuilder HasDbFunction( + [NotNull] this ModelBuilder modelBuilder, + [NotNull] MethodInfo methodInfo, + [CanBeNull] string name = null, + [CanBeNull] string schema = null) + { + Check.NotNull(modelBuilder, nameof(modelBuilder)); + Check.NotNull(methodInfo, nameof(methodInfo)); + Check.NullButNotEmpty(name, nameof(name)); + Check.NullButNotEmpty(schema, nameof(schema)); + + var dbFunction = modelBuilder.Model.Relational().GetOrAddDbFunction(methodInfo, ConfigurationSource.Explicit, name, schema); + + return new DbFunctionBuilder(dbFunction); + } + + /// + /// Configures a database function when targeting a relational database. + /// + /// The model builder. + /// The methodInfo this dbFunction uses. + /// The name of the dbFunction. + /// The schema of the dbFunction. + /// An action that performs configuration of the sequence. + /// The same builder instance so that multiple calls can be chained. + public static ModelBuilder HasDbFunction( + [NotNull] this ModelBuilder modelBuilder, + [NotNull] MethodInfo methodInfo, + [NotNull] string name, + [NotNull] string schema, + [NotNull] Action builderAction) + { + Check.NotNull(modelBuilder, nameof(modelBuilder)); + Check.NotNull(methodInfo, nameof(methodInfo)); + Check.NotEmpty(name, nameof(name)); + Check.NotEmpty(schema, nameof(schema)); + Check.NotNull(builderAction, nameof(builderAction)); + + builderAction(HasDbFunction(modelBuilder, methodInfo, name, schema)); + + return modelBuilder; + } + + /// + /// Configures a database function when targeting a relational database. + /// + /// The model builder. + /// The methodInfo this dbFunction uses. + /// An action that performs configuration of the sequence. + /// The same builder instance so that multiple calls can be chained. + public static ModelBuilder HasDbFunction( + [NotNull] this ModelBuilder modelBuilder, + [NotNull] MethodInfo methodInfo, + [NotNull] Action builderAction) + { + Check.NotNull(modelBuilder, nameof(modelBuilder)); + Check.NotNull(methodInfo, nameof(methodInfo)); + Check.NotNull(builderAction, nameof(builderAction)); + + builderAction(HasDbFunction(modelBuilder, methodInfo)); + + return modelBuilder; + } + + /// /// Configures the default schema that database objects should be created in, if no schema /// is explicitly configured. diff --git a/src/EFCore.Relational/breakingchanges.netcore.json b/src/EFCore.Relational/breakingchanges.netcore.json index edc9d7d0ecd..d932603a6bb 100644 --- a/src/EFCore.Relational/breakingchanges.netcore.json +++ b/src/EFCore.Relational/breakingchanges.netcore.json @@ -348,11 +348,6 @@ "MemberId": "protected .ctor(Microsoft.EntityFrameworkCore.Storage.ISqlGenerationHelper sqlGenerationHelper)", "Kind": "Removal" }, - { - "TypeId": "public abstract class Microsoft.EntityFrameworkCore.Query.ExpressionTranslators.RelationalCompositeMethodCallTranslator : Microsoft.EntityFrameworkCore.Query.ExpressionTranslators.IMethodCallTranslator", - "MemberId": "protected .ctor(Microsoft.Extensions.Logging.ILogger logger)", - "Kind": "Removal" - }, { "TypeId": "public class Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.RelationalProjectionExpressionVisitor : Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.ProjectionExpressionVisitor", "MemberId": "protected override System.Linq.Expressions.Expression VisitMethodCall(System.Linq.Expressions.MethodCallExpression node)", @@ -1668,11 +1663,6 @@ "MemberId": "protected .ctor(Microsoft.EntityFrameworkCore.Storage.ISqlGenerationHelper sqlGenerationHelper)", "Kind": "Removal" }, - { - "TypeId": "public abstract class Microsoft.EntityFrameworkCore.Query.ExpressionTranslators.RelationalCompositeMethodCallTranslator : Microsoft.EntityFrameworkCore.Query.ExpressionTranslators.IMethodCallTranslator", - "MemberId": "protected .ctor(Microsoft.Extensions.Logging.ILogger logger)", - "Kind": "Removal" - }, { "TypeId": "public class Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.RelationalProjectionExpressionVisitor : Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.ProjectionExpressionVisitor", "MemberId": "protected override System.Linq.Expressions.Expression VisitMethodCall(System.Linq.Expressions.MethodCallExpression node)", @@ -3022,74 +3012,74 @@ "MemberId": "public .ctor(System.String storeType, System.Type clrType, System.Nullable dbType)", "Kind": "Removal" }, - { - "TypeId": "public class Microsoft.EntityFrameworkCore.Metadata.RelationalSequenceBuilder", - "Kind": "Removal" - }, - { - "TypeId": "public class Microsoft.EntityFrameworkCore.Metadata.Sequence : Microsoft.EntityFrameworkCore.Metadata.ISequence", - "Kind": "Removal" - }, - { - "TypeId": "public class Microsoft.EntityFrameworkCore.Metadata.RelationalModelAnnotations : Microsoft.EntityFrameworkCore.Metadata.IRelationalModelAnnotations", - "MemberId": "public Microsoft.EntityFrameworkCore.Metadata.ISequence FindSequence(System.String name, System.String schema = null)", - "Kind": "Removal" - }, - { - "TypeId": "public class Microsoft.EntityFrameworkCore.Metadata.RelationalModelAnnotations : Microsoft.EntityFrameworkCore.Metadata.IRelationalModelAnnotations", - "MemberId": "public System.Collections.Generic.IReadOnlyList get_Sequences()", - "Kind": "Removal" - }, - { - "TypeId": "public class Microsoft.EntityFrameworkCore.Metadata.RelationalModelAnnotations : Microsoft.EntityFrameworkCore.Metadata.IRelationalModelAnnotations", - "MemberId": "public virtual Microsoft.EntityFrameworkCore.Metadata.Sequence GetOrAddSequence(System.String name, System.String schema = null)", - "Kind": "Removal" - }, - { - "TypeId": "public static class Microsoft.EntityFrameworkCore.RelationalModelBuilderExtensions", - "MemberId": "public static Microsoft.EntityFrameworkCore.Metadata.RelationalSequenceBuilder HasSequence(this Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder, System.Type clrType, System.String name, System.String schema = null)", - "Kind": "Removal" - }, - { - "TypeId": "public static class Microsoft.EntityFrameworkCore.RelationalModelBuilderExtensions", - "MemberId": "public static Microsoft.EntityFrameworkCore.ModelBuilder HasSequence(this Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder, System.Type clrType, System.String name, System.Action builderAction)", - "Kind": "Removal" - }, - { - "TypeId": "public static class Microsoft.EntityFrameworkCore.RelationalModelBuilderExtensions", - "MemberId": "public static Microsoft.EntityFrameworkCore.ModelBuilder HasSequence(this Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder, System.Type clrType, System.String name, System.String schema, System.Action builderAction)", - "Kind": "Removal" - }, - { - "TypeId": "public static class Microsoft.EntityFrameworkCore.RelationalModelBuilderExtensions", - "MemberId": "public static new Microsoft.EntityFrameworkCore.Metadata.RelationalSequenceBuilder HasSequence(this Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder, System.String name, System.String schema = null)", - "Kind": "Removal" - }, - { - "TypeId": "public static class Microsoft.EntityFrameworkCore.RelationalModelBuilderExtensions", - "MemberId": "public static new Microsoft.EntityFrameworkCore.Metadata.RelationalSequenceBuilder HasSequence(this Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder, System.String name, System.String schema = null)", - "Kind": "Removal" - }, - { - "TypeId": "public static class Microsoft.EntityFrameworkCore.RelationalModelBuilderExtensions", - "MemberId": "public static new Microsoft.EntityFrameworkCore.ModelBuilder HasSequence(this Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder, System.String name, System.Action builderAction)", - "Kind": "Removal" - }, - { - "TypeId": "public static class Microsoft.EntityFrameworkCore.RelationalModelBuilderExtensions", - "MemberId": "public static new Microsoft.EntityFrameworkCore.ModelBuilder HasSequence(this Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder, System.String name, System.String schema, System.Action builderAction)", - "Kind": "Removal" - }, - { - "TypeId": "public static class Microsoft.EntityFrameworkCore.RelationalModelBuilderExtensions", - "MemberId": "public static new Microsoft.EntityFrameworkCore.ModelBuilder HasSequence(this Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder, System.String name, System.Action builderAction)", - "Kind": "Removal" - }, - { - "TypeId": "public static class Microsoft.EntityFrameworkCore.RelationalModelBuilderExtensions", - "MemberId": "public static new Microsoft.EntityFrameworkCore.ModelBuilder HasSequence(this Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder, System.String name, System.String schema, System.Action builderAction)", - "Kind": "Removal" - }, + { + "TypeId": "public class Microsoft.EntityFrameworkCore.Metadata.RelationalSequenceBuilder", + "Kind": "Removal" + }, + { + "TypeId": "public class Microsoft.EntityFrameworkCore.Metadata.Sequence : Microsoft.EntityFrameworkCore.Metadata.ISequence", + "Kind": "Removal" + }, + { + "TypeId": "public class Microsoft.EntityFrameworkCore.Metadata.RelationalModelAnnotations : Microsoft.EntityFrameworkCore.Metadata.IRelationalModelAnnotations", + "MemberId": "public Microsoft.EntityFrameworkCore.Metadata.ISequence FindSequence(System.String name, System.String schema = null)", + "Kind": "Removal" + }, + { + "TypeId": "public class Microsoft.EntityFrameworkCore.Metadata.RelationalModelAnnotations : Microsoft.EntityFrameworkCore.Metadata.IRelationalModelAnnotations", + "MemberId": "public System.Collections.Generic.IReadOnlyList get_Sequences()", + "Kind": "Removal" + }, + { + "TypeId": "public class Microsoft.EntityFrameworkCore.Metadata.RelationalModelAnnotations : Microsoft.EntityFrameworkCore.Metadata.IRelationalModelAnnotations", + "MemberId": "public virtual Microsoft.EntityFrameworkCore.Metadata.Sequence GetOrAddSequence(System.String name, System.String schema = null)", + "Kind": "Removal" + }, + { + "TypeId": "public static class Microsoft.EntityFrameworkCore.RelationalModelBuilderExtensions", + "MemberId": "public static Microsoft.EntityFrameworkCore.Metadata.RelationalSequenceBuilder HasSequence(this Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder, System.Type clrType, System.String name, System.String schema = null)", + "Kind": "Removal" + }, + { + "TypeId": "public static class Microsoft.EntityFrameworkCore.RelationalModelBuilderExtensions", + "MemberId": "public static Microsoft.EntityFrameworkCore.ModelBuilder HasSequence(this Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder, System.Type clrType, System.String name, System.Action builderAction)", + "Kind": "Removal" + }, + { + "TypeId": "public static class Microsoft.EntityFrameworkCore.RelationalModelBuilderExtensions", + "MemberId": "public static Microsoft.EntityFrameworkCore.ModelBuilder HasSequence(this Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder, System.Type clrType, System.String name, System.String schema, System.Action builderAction)", + "Kind": "Removal" + }, + { + "TypeId": "public static class Microsoft.EntityFrameworkCore.RelationalModelBuilderExtensions", + "MemberId": "public static new Microsoft.EntityFrameworkCore.Metadata.RelationalSequenceBuilder HasSequence(this Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder, System.String name, System.String schema = null)", + "Kind": "Removal" + }, + { + "TypeId": "public static class Microsoft.EntityFrameworkCore.RelationalModelBuilderExtensions", + "MemberId": "public static new Microsoft.EntityFrameworkCore.Metadata.RelationalSequenceBuilder HasSequence(this Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder, System.String name, System.String schema = null)", + "Kind": "Removal" + }, + { + "TypeId": "public static class Microsoft.EntityFrameworkCore.RelationalModelBuilderExtensions", + "MemberId": "public static new Microsoft.EntityFrameworkCore.ModelBuilder HasSequence(this Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder, System.String name, System.Action builderAction)", + "Kind": "Removal" + }, + { + "TypeId": "public static class Microsoft.EntityFrameworkCore.RelationalModelBuilderExtensions", + "MemberId": "public static new Microsoft.EntityFrameworkCore.ModelBuilder HasSequence(this Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder, System.String name, System.String schema, System.Action builderAction)", + "Kind": "Removal" + }, + { + "TypeId": "public static class Microsoft.EntityFrameworkCore.RelationalModelBuilderExtensions", + "MemberId": "public static new Microsoft.EntityFrameworkCore.ModelBuilder HasSequence(this Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder, System.String name, System.Action builderAction)", + "Kind": "Removal" + }, + { + "TypeId": "public static class Microsoft.EntityFrameworkCore.RelationalModelBuilderExtensions", + "MemberId": "public static new Microsoft.EntityFrameworkCore.ModelBuilder HasSequence(this Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder, System.String name, System.String schema, System.Action builderAction)", + "Kind": "Removal" + }, { "TypeId": "public class Microsoft.EntityFrameworkCore.Metadata.RelationalKeyAnnotations : Microsoft.EntityFrameworkCore.Metadata.IRelationalKeyAnnotations", "MemberId": "protected const System.String DefaultAlternateKeyNamePrefix = \"AK\"", @@ -3165,9 +3155,23 @@ "MemberId": "public static System.String GetDefaultIndexName(System.String tableName, System.Collections.Generic.IEnumerable propertyNames)", "Kind": "Removal" }, - { - "TypeId": "public abstract class Microsoft.EntityFrameworkCore.Infrastructure.RelationalOptionsExtension : Microsoft.EntityFrameworkCore.Infrastructure.IDbContextOptionsExtension", - "MemberId": "public virtual System.Func get_ExecutionStrategyFactory()", - "Kind": "Removal" - } + { + "TypeId": "public abstract class Microsoft.EntityFrameworkCore.Query.ExpressionTranslators.RelationalCompositeMethodCallTranslator : Microsoft.EntityFrameworkCore.Query.ExpressionTranslators.IMethodCallTranslator", + "Kind": "Removal" + }, + { + "TypeId": "public interface Microsoft.EntityFrameworkCore.Metadata.IRelationalModelAnnotations", + "MemberId": "Microsoft.EntityFrameworkCore.Metadata.IDbFunction FindDbFunction(System.Reflection.MethodInfo method)", + "Kind": "Addition" + }, + { + "TypeId": "public interface Microsoft.EntityFrameworkCore.Metadata.IRelationalModelAnnotations", + "MemberId": "System.Collections.Generic.IReadOnlyList get_DbFunctions()", + "Kind": "Addition" + }, + { + "TypeId": "public abstract class Microsoft.EntityFrameworkCore.Infrastructure.RelationalOptionsExtension : Microsoft.EntityFrameworkCore.Infrastructure.IDbContextOptionsExtension", + "MemberId": "public virtual System.Func get_ExecutionStrategyFactory()", + "Kind": "Removal" + } ] \ No newline at end of file diff --git a/src/EFCore.Specification.Tests/NorthwindQueryFixtureBase.cs b/src/EFCore.Specification.Tests/NorthwindQueryFixtureBase.cs index 105f80b7b82..568bb84cc1a 100644 --- a/src/EFCore.Specification.Tests/NorthwindQueryFixtureBase.cs +++ b/src/EFCore.Specification.Tests/NorthwindQueryFixtureBase.cs @@ -9,8 +9,6 @@ namespace Microsoft.EntityFrameworkCore { public abstract class NorthwindQueryFixtureBase { - private DbContextOptions _options; - public abstract DbContextOptions BuildOptions(IServiceCollection additionalServices = null); public virtual NorthwindContext CreateContext( @@ -20,13 +18,14 @@ public virtual NorthwindContext CreateContext( EnableFilters = enableFilters; return new NorthwindContext( - _options - ?? (_options = new DbContextOptionsBuilder(BuildOptions()) + Options + ?? (Options = new DbContextOptionsBuilder(BuildOptions()) .ConfigureWarnings(w => w.Log(CoreEventId.IncludeIgnoredWarning)).Options), queryTrackingBehavior); } - private bool EnableFilters { get; set; } + protected bool EnableFilters { get; set; } + protected DbContextOptions Options { get; set; } protected virtual void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/src/EFCore.Specification.Tests/TestModelSource.cs b/src/EFCore.Specification.Tests/TestModelSource.cs index ae1f18bc281..555ae733403 100644 --- a/src/EFCore.Specification.Tests/TestModelSource.cs +++ b/src/EFCore.Specification.Tests/TestModelSource.cs @@ -14,11 +14,13 @@ namespace Microsoft.EntityFrameworkCore public class TestModelSource : ModelSource { private readonly Action _onModelCreating; + private readonly Action _customizeModel; - public TestModelSource(Action onModelCreating, ModelSourceDependencies dependencies) + public TestModelSource(Action onModelCreating, ModelSourceDependencies dependencies, Action customizeModel = null) : base(dependencies) { _onModelCreating = onModelCreating; + _customizeModel = customizeModel; } protected override IModel CreateModel(DbContext context, IConventionSetBuilder conventionSetBuilder, IModelValidator validator) @@ -29,10 +31,10 @@ protected override IModel CreateModel(DbContext context, IConventionSetBuilder c var model = (Model)modelBuilder.Model; model.SetProductVersion(ProductInfo.GetVersion()); - FindSets(modelBuilder, context); + _customizeModel?.Invoke(modelBuilder, context); _onModelCreating(modelBuilder); - + model.Validate(); validator.Validate(model); @@ -43,6 +45,13 @@ protected override IModel CreateModel(DbContext context, IConventionSetBuilder c public static Func GetFactory(Action onModelCreating) => p => new TestModelSource( onModelCreating, - p.GetRequiredService()); + p.GetRequiredService(), + (mb, dbc) => + { + foreach (var setInfo in p.GetRequiredService().FindSets(dbc)) + { + mb.Entity(setInfo.ClrType); + } + }); } } diff --git a/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs index 49833f9006e..ba9379c7cda 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs @@ -86,7 +86,7 @@ public static IServiceCollection AddEntityFrameworkSqlServer([NotNull] this ISer .TryAdd() .TryAdd() .TryAdd() - .TryAdd() + .TryAdd() .TryAdd() .TryAdd(p => p.GetService()) .TryAddProviderSpecificServices(b => b diff --git a/src/EFCore.Sqlite.Core/Infrastructure/SqliteServiceCollectionExtensions.cs b/src/EFCore.Sqlite.Core/Infrastructure/SqliteServiceCollectionExtensions.cs index 87e4ea7a03f..b153319df6b 100644 --- a/src/EFCore.Sqlite.Core/Infrastructure/SqliteServiceCollectionExtensions.cs +++ b/src/EFCore.Sqlite.Core/Infrastructure/SqliteServiceCollectionExtensions.cs @@ -78,7 +78,7 @@ public static IServiceCollection AddEntityFrameworkSqlite([NotNull] this IServic .TryAdd() .TryAdd() .TryAdd() - .TryAdd() + .TryAdd() .TryAdd() .TryAddProviderSpecificServices(b => b .TryAddScoped()); diff --git a/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs b/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs index bbc8cd1a5a9..9f36ff82645 100644 --- a/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs +++ b/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Diagnostics; using JetBrains.Annotations; +using Remotion.Linq.Parsing.ExpressionVisitors.TreeEvaluation; using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Internal; @@ -115,7 +116,8 @@ private static readonly IDictionary _coreServices { typeof(IQueryTrackingListener), new ServiceCharacteristics(ServiceLifetime.Scoped, multipleRegistrations: true) }, { typeof(IPropertyListener), new ServiceCharacteristics(ServiceLifetime.Scoped, multipleRegistrations: true) }, { typeof(IResettableService), new ServiceCharacteristics(ServiceLifetime.Scoped, multipleRegistrations: true) }, - { typeof(ISingletonOptions), new ServiceCharacteristics(ServiceLifetime.Singleton, multipleRegistrations: true) } + { typeof(ISingletonOptions), new ServiceCharacteristics(ServiceLifetime.Singleton, multipleRegistrations: true) }, + { typeof(IEvaluatableExpressionFilter), new ServiceCharacteristics(ServiceLifetime.Scoped) } }; /// @@ -239,7 +241,8 @@ public virtual EntityFrameworkServicesBuilder TryAddCoreServices() TryAdd(p => p.GetService()); TryAdd(p => p.GetService()); TryAdd(p => p.GetService()); - + TryAdd(); + ServiceCollectionMap .TryAddSingleton(new DiagnosticListener(DbLoggerCategory.Name)); diff --git a/src/EFCore/Infrastructure/ModelCustomizer.cs b/src/EFCore/Infrastructure/ModelCustomizer.cs index c427ac55348..ae8617da77b 100644 --- a/src/EFCore/Infrastructure/ModelCustomizer.cs +++ b/src/EFCore/Infrastructure/ModelCustomizer.cs @@ -25,8 +25,15 @@ public class ModelCustomizer : IModelCustomizer public ModelCustomizer([NotNull] ModelCustomizerDependencies dependencies) { Check.NotNull(dependencies, nameof(dependencies)); + + Dependencies = dependencies; } + /// + /// Dependencies used to create a + /// + protected virtual ModelCustomizerDependencies Dependencies { get; } + /// /// Performs additional configuration of the model in addition to what is discovered by convention. This default implementation /// builds the model for a given context by calling @@ -38,6 +45,24 @@ public ModelCustomizer([NotNull] ModelCustomizerDependencies dependencies) /// /// The context instance that the model is being created for. /// - public virtual void Customize(ModelBuilder modelBuilder, DbContext dbContext) => dbContext.OnModelCreating(modelBuilder); + public virtual void Customize(ModelBuilder modelBuilder, DbContext dbContext) + { + FindSets(modelBuilder, dbContext); + + dbContext.OnModelCreating(modelBuilder); + } + + /// + /// Adds the entity types found in properties on the context to the model. + /// + /// The being used to build the model. + /// The context to find properties on. + protected virtual void FindSets([NotNull] ModelBuilder modelBuilder, [NotNull] DbContext context) + { + foreach (var setInfo in Dependencies.SetFinder.FindSets(context)) + { + modelBuilder.Entity(setInfo.ClrType); + } + } } } diff --git a/src/EFCore/Infrastructure/ModelCustomizerDependencies.cs b/src/EFCore/Infrastructure/ModelCustomizerDependencies.cs index 5e06eed5c16..11344b3a185 100644 --- a/src/EFCore/Infrastructure/ModelCustomizerDependencies.cs +++ b/src/EFCore/Infrastructure/ModelCustomizerDependencies.cs @@ -1,6 +1,10 @@ // 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 JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Utilities; + namespace Microsoft.EntityFrameworkCore.Infrastructure { /// @@ -36,8 +40,25 @@ public sealed class ModelCustomizerDependencies /// /// // ReSharper disable once EmptyConstructor - public ModelCustomizerDependencies() + public ModelCustomizerDependencies([NotNull] IDbSetFinder setFinder) { + Check.NotNull(setFinder, nameof(setFinder)); + + SetFinder = setFinder; } + + /// + /// Gets the that will locate the properties + /// on the derived context. + /// + public IDbSetFinder SetFinder { get; } + + /// + /// 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 ModelCustomizerDependencies With([NotNull] IDbSetFinder setFinder) + => new ModelCustomizerDependencies(setFinder); } } diff --git a/src/EFCore/Infrastructure/ModelSource.cs b/src/EFCore/Infrastructure/ModelSource.cs index 0e4d6359b0b..6415ad46f7f 100644 --- a/src/EFCore/Infrastructure/ModelSource.cs +++ b/src/EFCore/Infrastructure/ModelSource.cs @@ -77,8 +77,6 @@ protected virtual IModel CreateModel( internalModelBuilder.Metadata.SetProductVersion(ProductInfo.GetVersion()); - FindSets(modelBuilder, context); - Dependencies.ModelCustomizer.Customize(modelBuilder, context); internalModelBuilder.Validate(); @@ -96,18 +94,5 @@ protected virtual IModel CreateModel( /// The convention set to be used. protected virtual ConventionSet CreateConventionSet([NotNull] IConventionSetBuilder conventionSetBuilder) => conventionSetBuilder.AddConventions(Dependencies.CoreConventionSetBuilder.CreateConventionSet()); - - /// - /// Adds the entity types found in properties on the context to the model. - /// - /// The being used to build the model. - /// The context to find properties on. - protected virtual void FindSets([NotNull] ModelBuilder modelBuilder, [NotNull] DbContext context) - { - foreach (var setInfo in Dependencies.SetFinder.FindSets(context)) - { - modelBuilder.Entity(setInfo.ClrType); - } - } } } diff --git a/src/EFCore/Infrastructure/ModelSourceDependencies.cs b/src/EFCore/Infrastructure/ModelSourceDependencies.cs index 4e28c8b1e61..06415e75a60 100644 --- a/src/EFCore/Infrastructure/ModelSourceDependencies.cs +++ b/src/EFCore/Infrastructure/ModelSourceDependencies.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal; using Microsoft.EntityFrameworkCore.Utilities; @@ -45,28 +44,20 @@ public sealed class ModelSourceDependencies /// /// public ModelSourceDependencies( - [NotNull] IDbSetFinder setFinder, [NotNull] ICoreConventionSetBuilder coreConventionSetBuilder, [NotNull] IModelCustomizer modelCustomizer, [NotNull] IModelCacheKeyFactory modelCacheKeyFactory) { - Check.NotNull(setFinder, nameof(setFinder)); + Check.NotNull(coreConventionSetBuilder, nameof(coreConventionSetBuilder)); Check.NotNull(modelCustomizer, nameof(modelCustomizer)); Check.NotNull(modelCacheKeyFactory, nameof(modelCacheKeyFactory)); - SetFinder = setFinder; CoreConventionSetBuilder = coreConventionSetBuilder; ModelCustomizer = modelCustomizer; ModelCacheKeyFactory = modelCacheKeyFactory; } - /// - /// Gets the that will locate the properties - /// on the derived context. - /// - public IDbSetFinder SetFinder { get; } - /// /// Gets the that will build the conventions to be used /// to build the model. @@ -85,21 +76,13 @@ public ModelSourceDependencies( /// public IModelCacheKeyFactory ModelCacheKeyFactory { get; } - /// - /// 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 ModelSourceDependencies With([NotNull] IDbSetFinder setFinder) - => new ModelSourceDependencies(setFinder, CoreConventionSetBuilder, ModelCustomizer, ModelCacheKeyFactory); - /// /// 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 ModelSourceDependencies With([NotNull] ICoreConventionSetBuilder coreConventionSetBuilder) - => new ModelSourceDependencies(SetFinder, coreConventionSetBuilder, ModelCustomizer, ModelCacheKeyFactory); + => new ModelSourceDependencies(coreConventionSetBuilder, ModelCustomizer, ModelCacheKeyFactory); /// /// Clones this dependency parameter object with one service replaced. @@ -107,7 +90,7 @@ public ModelSourceDependencies With([NotNull] ICoreConventionSetBuilder coreConv /// A replacement for the current dependency of this type. /// A new parameter object with the given service replaced. public ModelSourceDependencies With([NotNull] IModelCustomizer modelCustomizer) - => new ModelSourceDependencies(SetFinder, CoreConventionSetBuilder, modelCustomizer, ModelCacheKeyFactory); + => new ModelSourceDependencies(CoreConventionSetBuilder, modelCustomizer, ModelCacheKeyFactory); /// /// Clones this dependency parameter object with one service replaced. @@ -115,6 +98,6 @@ public ModelSourceDependencies With([NotNull] IModelCustomizer modelCustomizer) /// A replacement for the current dependency of this type. /// A new parameter object with the given service replaced. public ModelSourceDependencies With([NotNull] IModelCacheKeyFactory modelCacheKeyFactory) - => new ModelSourceDependencies(SetFinder, CoreConventionSetBuilder, ModelCustomizer, modelCacheKeyFactory); + => new ModelSourceDependencies(CoreConventionSetBuilder, ModelCustomizer, modelCacheKeyFactory); } } diff --git a/src/EFCore/Metadata/Conventions/ConventionSet.cs b/src/EFCore/Metadata/Conventions/ConventionSet.cs index 56b89bc478d..dd4b8a915d9 100644 --- a/src/EFCore/Metadata/Conventions/ConventionSet.cs +++ b/src/EFCore/Metadata/Conventions/ConventionSet.cs @@ -37,6 +37,12 @@ public class ConventionSet public virtual IList EntityTypeAnnotationChangedConventions { get; } = new List(); + /// + /// Conventions to run when an annotation is set or removed on a model. + /// + public virtual IList ModelAnnotationChangedConventions { get; } + = new List(); + /// /// Conventions to run when a foreign key is added. /// diff --git a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.cs b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.cs index 9bbdb038d2f..8ead43bb996 100644 --- a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.cs +++ b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.cs @@ -89,6 +89,21 @@ public virtual Annotation OnEntityTypeAnnotationSet( annotation, oldAnnotation); + /// + /// 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 Annotation OnModelAnnotationSet( + [NotNull] InternalModelBuilder modelBuilder, + [NotNull] string name, + [CanBeNull] Annotation annotation, + [CanBeNull] Annotation oldAnnotation) + => _scope.OnModelAnnotationSet( + Check.NotNull(modelBuilder, nameof(modelBuilder)), + Check.NotNull(name, nameof(name)), + annotation, + oldAnnotation); + /// /// 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/Metadata/Conventions/Internal/ConventionNode.cs b/src/EFCore/Metadata/Conventions/Internal/ConventionNode.cs index 91898d642db..376c8a0b3e1 100644 --- a/src/EFCore/Metadata/Conventions/Internal/ConventionNode.cs +++ b/src/EFCore/Metadata/Conventions/Internal/ConventionNode.cs @@ -111,6 +111,16 @@ public virtual Annotation OnEntityTypeAnnotationSet( return annotation; } + public virtual Annotation OnModelAnnotationSet( + [NotNull] InternalModelBuilder modelBuilder, + [NotNull] string name, + [CanBeNull] Annotation annotation, + [CanBeNull] Annotation oldAnnotation) + { + Add(new OnModelAnnotationSetNode(modelBuilder, name, annotation, oldAnnotation)); + return annotation; + } + public virtual InternalRelationshipBuilder OnForeignKeyAdded([NotNull] InternalRelationshipBuilder relationshipBuilder) { Add(new OnForeignKeyAddedNode(relationshipBuilder)); @@ -327,6 +337,29 @@ public override Annotation OnEntityTypeAnnotationSet( return annotation; } + public override Annotation OnModelAnnotationSet( + InternalModelBuilder modelBuilder, + string name, + Annotation annotation, + Annotation oldAnnotation) + { + if (modelBuilder.Metadata.Builder == null) + { + return null; + } + + foreach (var modelAnnotationSetConvention in _conventionSet.ModelAnnotationChangedConventions) + { + var newAnnotation = modelAnnotationSetConvention.Apply(modelBuilder, name, annotation, oldAnnotation); + if (newAnnotation != annotation) + { + return newAnnotation; + } + } + + return annotation; + } + public override InternalRelationshipBuilder OnForeignKeyAdded(InternalRelationshipBuilder relationshipBuilder) { if (relationshipBuilder.Metadata.Builder == null) @@ -767,6 +800,28 @@ public OnEntityTypeAnnotationSetNode( public override ConventionNode Accept(ConventionVisitor visitor) => visitor.VisitOnEntityTypeAnnotationSet(this); } + private class OnModelAnnotationSetNode : ConventionNode + { + public OnModelAnnotationSetNode( + InternalModelBuilder modelBuilder, + string name, + Annotation annotation, + Annotation oldAnnotation) + { + ModelBuilder = modelBuilder; + Name = name; + Annotation = annotation; + OldAnnotation = oldAnnotation; + } + + public InternalModelBuilder ModelBuilder { get; } + public string Name { get; } + public Annotation Annotation { get; } + public Annotation OldAnnotation { get; } + + public override ConventionNode Accept(ConventionVisitor visitor) => visitor.VisitOnModelAnnotationSet(this); + } + private class OnForeignKeyAddedNode : ConventionNode { public OnForeignKeyAddedNode(InternalRelationshipBuilder relationshipBuilder) diff --git a/src/EFCore/Metadata/Conventions/Internal/ConventionVisitor.cs b/src/EFCore/Metadata/Conventions/Internal/ConventionVisitor.cs index 9b3f9ad4b65..cd89cc35865 100644 --- a/src/EFCore/Metadata/Conventions/Internal/ConventionVisitor.cs +++ b/src/EFCore/Metadata/Conventions/Internal/ConventionVisitor.cs @@ -35,6 +35,7 @@ public virtual ConventionScope VisitConventionScope(ConventionScope node) public virtual OnEntityTypeMemberIgnoredNode VisitOnEntityTypeMemberIgnored(OnEntityTypeMemberIgnoredNode node) => node; public virtual OnBaseEntityTypeSetNode VisitOnBaseEntityTypeSet(OnBaseEntityTypeSetNode node) => node; public virtual OnEntityTypeAnnotationSetNode VisitOnEntityTypeAnnotationSet(OnEntityTypeAnnotationSetNode node) => node; + public virtual OnModelAnnotationSetNode VisitOnModelAnnotationSet(OnModelAnnotationSetNode node) => node; public virtual OnForeignKeyAddedNode VisitOnForeignKeyAdded(OnForeignKeyAddedNode node) => node; public virtual OnForeignKeyRemovedNode VisitOnForeignKeyRemoved(OnForeignKeyRemovedNode node) => node; public virtual OnKeyAddedNode VisitOnKeyAdded(OnKeyAddedNode node) => node; @@ -92,6 +93,12 @@ public override OnEntityTypeAnnotationSetNode VisitOnEntityTypeAnnotationSet(OnE return null; } + public override OnModelAnnotationSetNode VisitOnModelAnnotationSet(OnModelAnnotationSetNode node) + { + Dispatcher._immediateConventionScope.OnModelAnnotationSet(node.ModelBuilder, node.Name, node.Annotation, node.OldAnnotation); + return null; + } + public override OnForeignKeyAddedNode VisitOnForeignKeyAdded(OnForeignKeyAddedNode node) { Dispatcher._immediateConventionScope.OnForeignKeyAdded(node.RelationshipBuilder); diff --git a/src/EFCore/Metadata/Conventions/Internal/IModelAnnotationChangedConvention.cs b/src/EFCore/Metadata/Conventions/Internal/IModelAnnotationChangedConvention.cs new file mode 100644 index 00000000000..3d802922920 --- /dev/null +++ b/src/EFCore/Metadata/Conventions/Internal/IModelAnnotationChangedConvention.cs @@ -0,0 +1,26 @@ +// 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 JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Internal; + +namespace Microsoft.EntityFrameworkCore.Metadata.Conventions.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 IModelAnnotationChangedConvention + { + /// + /// 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. + /// + Annotation Apply( + [NotNull] InternalModelBuilder modelBuilder, + [NotNull] string name, + [CanBeNull] Annotation annotation, + [CanBeNull] Annotation oldAnnotation); + } +} diff --git a/src/EFCore/Metadata/Internal/Model.cs b/src/EFCore/Metadata/Internal/Model.cs index acc99ce90a6..a3a14850fa5 100644 --- a/src/EFCore/Metadata/Internal/Model.cs +++ b/src/EFCore/Metadata/Internal/Model.cs @@ -10,6 +10,7 @@ using Microsoft.EntityFrameworkCore.Metadata.Conventions; using Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal; using Microsoft.EntityFrameworkCore.Utilities; +using Microsoft.EntityFrameworkCore.Infrastructure; namespace Microsoft.EntityFrameworkCore.Metadata.Internal { @@ -462,6 +463,16 @@ public virtual void Unignore([NotNull] string name) _ignoredTypeNames.Remove(name); } + /// + /// Runs the conventions when an annotation was set or removed. + /// + /// The key of the set annotation. + /// The annotation set. + /// The old annotation. + /// The annotation that was set. + protected override Annotation OnAnnotationSet(string name, Annotation annotation, Annotation oldAnnotation) + => ConventionDispatcher.OnModelAnnotationSet(Builder, name, annotation, oldAnnotation); + /// /// 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/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index 716c8bd2a15..a27ae56e0c7 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -1751,7 +1751,67 @@ public static readonly EventDefinition LogPossibleUnintendedRefe _resourceManager.GetString("LogPossibleUnintendedReferenceComparison"))); /// - /// The same entity is being tracked as different delegated identity entity types '{diet1}' and '{diet2}'. If a property value changes it will result in two store changes, which might not be the desired outcome. + /// The Db Function '{dbFunction}' is generic. Generic methods are not supported. + /// + public static string DbFunctionGenericMethodNotSupported([CanBeNull] MethodInfo dbFunction) + => string.Format( + GetString("DbFunctionGenericMethodNotSupported", nameof(dbFunction)), + $"{ dbFunction?.DeclaringType}.{dbFunction?.Name}"); + + /// + /// Db Function has no name set. + /// + public static string DbFunctionNameEmpty() + => GetString("DbFunctionNameEmpty"); + + /// + /// The Db Function '{dbFunction}' has an invalid return type '{returnType}'. + /// + public static string DbFunctionInvalidReturnType([CanBeNull] MethodInfo dbFunction, [CanBeNull] Type returnType) + => string.Format( + GetString("DbFunctionInvalidReturnType", nameof(returnType), nameof(dbFunction)), + returnType?.Name, $"{ dbFunction?.DeclaringType}.{dbFunction?.Name}"); + + /// + /// Db Function '{dbFunctionName}' has parameters with duplicate indexes. + /// + public static string DbFunctionDuplicateIndex([CanBeNull] object dbFunctionName) + => string.Format( + GetString("DbFunctionParametersDuplicateIndex", nameof(dbFunctionName)), + dbFunctionName); + + /// + /// Db Function '{dbFunctionName}' has a non continuous parameter index. + /// + public static string DbFunctionNonContinuousIndex([CanBeNull] object dbFunctionName) + => string.Format( + GetString("DbFunctionNonContinuousIndex", nameof(dbFunctionName)), + dbFunctionName); + + /// + /// The parameter '{dbParameter}' Db Function '{dbFunction}' has an invalid type. + /// + public static string DbFunctionInvalidParameterType([CanBeNull] MethodInfo dbFunction, [CanBeNull] object dbParameter, [CanBeNull] Type dbParamType) + => string.Format( + GetString("DbFunctionInvalidParameterType", nameof(dbParameter), nameof(dbFunction), nameof(dbParamType)), + dbParameter, $"{ dbFunction?.DeclaringType}.{dbFunction?.Name}", dbParamType?.Name); + + /// + /// Db Function '{dbFunctionName}' must be a static method. + /// + public static string DbFunctionDbContextMethodMustBeStatic([CanBeNull] object dbFunctionName) + => string.Format( + GetString("DbFunctionDbContextMethodMustBeStatic", nameof(dbFunctionName)), dbFunctionName); + + /// + /// Db Function '{dbFunctionName}' has no parameter '{dbParameterName}'. Check the method signature for the correct parameter name. + /// + public static string DbFunctionParameterNotFound([CanBeNull] object dbFunctionName, [CanBeNull] object dbParameterName) + => string.Format( + GetString("DbFunctionParameterNotFound", nameof(dbFunctionName), nameof(dbParameterName)), dbFunctionName, dbParameterName); + + /// + /// The same entity is being tracked as different delegated identity entity types '{diet1}' and '{diet2}'. If a property value changes it will result in two store changes, which might not be the desired outcome. /// public static readonly EventDefinition LogDuplicateDietInstance = new EventDefinition( diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index c1edf56e82f..aac47cdebcf 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -777,4 +777,28 @@ The same entity is being tracked as different delegated identity entity types '{diet1}' and '{diet2}'. If a property value changes it will result in two store changes, which might not be the desired outcome. Warning CoreEventId.DuplicateDietInstanceWarning string string + + The Db Function '{dbFunction}' has an invalid return type '{returnType}'. + + + Db Function has no name set. + + + Db Function '{dbFunctionName}' has parameters with duplicate indexes. + + + Db Function '{dbFunctionName}' has a non continuous parameter index. + + + The parameter '{dbParameter}' Db Function '{dbFunction}' has an invalid type. + + + Db Function '{dbFunctionName}' has no parameter '{dbParameterName}'. Check the method signature for the correct parameter name. + + + Db Function '{dbFunctionName}' must be a static method. + + + The Db Function '{dbFunction}' is generic. Generic methods are not supported. + \ No newline at end of file diff --git a/src/EFCore/Query/Internal/QueryCompiler.cs b/src/EFCore/Query/Internal/QueryCompiler.cs index 20da0940640..8afdad7576b 100644 --- a/src/EFCore/Query/Internal/QueryCompiler.cs +++ b/src/EFCore/Query/Internal/QueryCompiler.cs @@ -34,8 +34,7 @@ public class QueryCompiler : IQueryCompiler = typeof(IDatabase).GetTypeInfo() .GetDeclaredMethod(nameof(IDatabase.CompileQuery)); - private static readonly IEvaluatableExpressionFilter _evaluatableExpressionFilter - = new EvaluatableExpressionFilter(); + private readonly IEvaluatableExpressionFilter _evaluatableExpressionFilter; private readonly IQueryContextFactory _queryContextFactory; private readonly ICompiledQueryCache _compiledQueryCache; @@ -59,7 +58,8 @@ public QueryCompiler( [NotNull] IDatabase database, [NotNull] IDiagnosticsLogger logger, [NotNull] INodeTypeProviderFactory nodeTypeProviderFactory, - [NotNull] ICurrentDbContext currentContext) + [NotNull] ICurrentDbContext currentContext, + [NotNull] IEvaluatableExpressionFilter evaluatableExpressionFilter) { Check.NotNull(queryContextFactory, nameof(queryContextFactory)); Check.NotNull(compiledQueryCache, nameof(compiledQueryCache)); @@ -67,7 +67,8 @@ public QueryCompiler( Check.NotNull(database, nameof(database)); Check.NotNull(logger, nameof(logger)); Check.NotNull(currentContext, nameof(currentContext)); - + Check.NotNull(evaluatableExpressionFilter, nameof(evaluatableExpressionFilter)); + _queryContextFactory = queryContextFactory; _compiledQueryCache = compiledQueryCache; _compiledQueryCacheKeyGenerator = compiledQueryCacheKeyGenerator; @@ -75,6 +76,7 @@ public QueryCompiler( _logger = logger; _nodeTypeProviderFactory = nodeTypeProviderFactory; _contextType = currentContext.Context.GetType(); + _evaluatableExpressionFilter = evaluatableExpressionFilter; } /// @@ -117,7 +119,7 @@ public virtual Func CreateCompiledQuery(Expressi return CompileQueryCore(query, NodeTypeProvider, _database, _logger, _contextType); } - private static Func CompileQueryCore( + private Func CompileQueryCore( Expression query, INodeTypeProvider nodeTypeProvider, IDatabase database, @@ -271,7 +273,7 @@ protected virtual Func> CompileAsyncQuer () => CompileAsyncQueryCore(query, NodeTypeProvider, _database)); } - private static Func> CompileAsyncQueryCore( + private Func> CompileAsyncQueryCore( Expression query, INodeTypeProvider nodeTypeProvider, IDatabase database) @@ -305,7 +307,7 @@ var visitor return visitor.ExtractParameters(query); } - private static QueryParser CreateQueryParser(INodeTypeProvider nodeTypeProvider) + private QueryParser CreateQueryParser(INodeTypeProvider nodeTypeProvider) => new QueryParser( new ExpressionTreeParser( nodeTypeProvider, diff --git a/src/EFCore/breakingchanges.netcore.json b/src/EFCore/breakingchanges.netcore.json index ea72746c54c..114dbe9ec45 100644 --- a/src/EFCore/breakingchanges.netcore.json +++ b/src/EFCore/breakingchanges.netcore.json @@ -788,5 +788,10 @@ "TypeId": "public class Microsoft.EntityFrameworkCore.Metadata.Conventions.ConventionSet", "MemberId": "public virtual System.Collections.Generic.IList get_PropertyNullableChangedConventions()", "Kind": "Removal" + }, + { + "TypeId": "public abstract class Microsoft.EntityFrameworkCore.Infrastructure.ModelSource : Microsoft.EntityFrameworkCore.Infrastructure.IModelSource", + "MemberId": "protected virtual System.Void FindSets(Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder, Microsoft.EntityFrameworkCore.DbContext context)", + "Kind": "Removal" } ] diff --git a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs index 4a917ad9c6e..91d2b6862fd 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs @@ -105,7 +105,7 @@ protected override void Down(MigrationBuilder migrationBuilder) typeof(MyContext), "MyMigration", "20150511161616_MyMigration", - new Model { ["Some:EnumValue"] = RegexOptions.Multiline }); + new Model { ["Some:EnumValue"] = RegexOptions.Multiline, ["Relational:DbFunction:MyFunc"] = new object() }); Assert.Equal( @"// using Microsoft.EntityFrameworkCore; @@ -174,7 +174,7 @@ public void Snapshots_compile() new CSharpMigrationOperationGeneratorDependencies(codeHelper)), new CSharpSnapshotGenerator(new CSharpSnapshotGeneratorDependencies(codeHelper)))); - var model = new Model { ["Some:EnumValue"] = RegexOptions.Multiline }; + var model = new Model { ["Some:EnumValue"] = RegexOptions.Multiline, ["Relational:DbFunction:MyFunc"] = new object() }; var entityType = model.AddEntityType("Cheese"); entityType.AddProperty("Pickle", typeof(StringBuilder)); diff --git a/test/EFCore.Relational.Tests/Metadata/DbFunctionMetadataTests.cs b/test/EFCore.Relational.Tests/Metadata/DbFunctionMetadataTests.cs new file mode 100644 index 00000000000..02a1ddc507c --- /dev/null +++ b/test/EFCore.Relational.Tests/Metadata/DbFunctionMetadataTests.cs @@ -0,0 +1,349 @@ +// 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.Reflection; +using System.Linq; +using Xunit; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Metadata.Conventions; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal; +using Microsoft.EntityFrameworkCore.Metadata.Internal; + +namespace Microsoft.EntityFrameworkCore.Metadata +{ + public class DbFunctionMetadataTests + { + public class MyBaseContext : DbContext + { + [DbFunction] + public static void Foo() {} + + public static void Skip2() {} + + private static void Skip() {} + } + + public class MyDerivedContext : MyBaseContext + { + [DbFunction] + public static void Bar() {} + + public static void Skip3() {} + + private static void Skip4() {} + + [DbFunction] + public void NonStatic() { } + } + + 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 MethodCmi = typeof(TestMethods).GetRuntimeMethod(nameof(TestMethods.MethodC), new Type[] { }); + public static MethodInfo MethodHmi = typeof(TestMethods).GetTypeInfo().GetDeclaredMethod(nameof(TestMethods.MethodH)); + + public class TestMethods + { + public static int MethodA(string a, int b) + { + throw new NotImplementedException(); + } + + [DbFunction(Schema = "bar", Name = "MethodFoo")] + public int MethodB([DbFunctionParameter(ParameterIndex = 1)] string c, + [DbFunctionParameter(ParameterIndex = 0)] int d) + { + throw new NotImplementedException(); + } + + public void MethodC() + { + } + + public TestMethods MethodD() + { + throw new NotImplementedException(); + } + + public static int MethodF(MyBaseContext context) + { + throw new NotImplementedException(); + } + + public static int MethodH(T a, string b) + { + throw new Exception(); + } + } + + [Fact] + public void Adding_method_fluent_only_convention_defaults() + { + var modelBuilder = GetModelBuilder(); + + var dbFuncBuilder = modelBuilder.HasDbFunction(MethodAmi); + var dbFunc = dbFuncBuilder.Metadata; + + Assert.Equal("MethodA", dbFunc.Name); + Assert.Equal(null, dbFunc.Schema); + Assert.Equal(typeof(int), dbFunc.ReturnType); + + Assert.Equal(2, dbFunc.Parameters.Count); + + Assert.Equal("a", dbFunc.Parameters[0].Name); + Assert.Equal(0, dbFunc.Parameters[0].Index); + Assert.Equal(typeof(string), dbFunc.Parameters[0].ParameterType); + + Assert.Equal("b", dbFunc.Parameters[1].Name); + Assert.Equal(1, dbFunc.Parameters[1].Index); + Assert.Equal(typeof(int), dbFunc.Parameters[1].ParameterType); + } + + [Fact] + public void Adding_method_fluent_only_with_name_schema() + { + var modelBuilder = GetModelBuilder(); + + var dbFuncBuilder = modelBuilder.HasDbFunction(MethodAmi, "foo", "bar"); + var dbFunc = dbFuncBuilder.Metadata; + + Assert.Equal("foo", dbFunc.Name); + Assert.Equal("bar", dbFunc.Schema); + Assert.Equal(typeof(int), dbFunc.ReturnType); + + Assert.Equal(2, dbFunc.Parameters.Count); + + Assert.Equal("a", dbFunc.Parameters[0].Name); + Assert.Equal(0, dbFunc.Parameters[0].Index); + Assert.Equal(typeof(string), dbFunc.Parameters[0].ParameterType); + + Assert.Equal("b", dbFunc.Parameters[1].Name); + Assert.Equal(1, dbFunc.Parameters[1].Index); + Assert.Equal(typeof(int), dbFunc.Parameters[1].ParameterType); + } + + [Fact] + public void Adding_method_fluent_only_with_builder() + { + var modelBuilder = GetModelBuilder(); + + modelBuilder.HasDbFunction(MethodAmi, funcBuilder => + { + funcBuilder.HasName("foo").HasSchema("bar") ; + }); + + var dbFunc = modelBuilder.HasDbFunction(MethodAmi).Metadata; + + Assert.Equal("foo", dbFunc.Name); + Assert.Equal("bar", dbFunc.Schema); + Assert.Equal(typeof(int), dbFunc.ReturnType); + + Assert.Equal(2, dbFunc.Parameters.Count); + + Assert.Equal("a", dbFunc.Parameters[0].Name); + Assert.Equal(0, dbFunc.Parameters[0].Index); + Assert.Equal(typeof(string), dbFunc.Parameters[0].ParameterType); + + Assert.Equal("b", dbFunc.Parameters[1].Name); + Assert.Equal(1, dbFunc.Parameters[1].Index); + Assert.Equal(typeof(int), dbFunc.Parameters[1].ParameterType); + } + + [Fact] + public void Adding_method_with_attribute_only() + { + var modelBuilder = GetModelBuilder(); + + var dbFuncBuilder = modelBuilder.HasDbFunction(MethodBmi); + var dbFunc = dbFuncBuilder.Metadata; + + Assert.Equal("MethodFoo", dbFunc.Name); + Assert.Equal("bar", dbFunc.Schema); + Assert.Equal(typeof(int), dbFunc.ReturnType); + + Assert.Equal(2, dbFunc.Parameters.Count); + + Assert.Equal("c", dbFunc.Parameters[0].Name); + Assert.Equal(1, dbFunc.Parameters[0].Index); + Assert.Equal(typeof(string), dbFunc.Parameters[0].ParameterType); + + Assert.Equal("d", dbFunc.Parameters[1].Name); + Assert.Equal(0, dbFunc.Parameters[1].Index); + Assert.Equal(typeof(int), dbFunc.Parameters[1].ParameterType); + } + + [Fact] + public void Adding_method_with_attribute_and_fluent_HasDbFunction_configurationSource() + { + var modelBuilder = GetModelBuilder(); + + var dbFuncBuilder = modelBuilder.HasDbFunction(MethodBmi, "foo", "bar"); + var dbFunc = dbFuncBuilder.Metadata; + + Assert.Equal("foo", dbFunc.Name); + Assert.Equal("bar", dbFunc.Schema); + Assert.Equal(typeof(int), dbFunc.ReturnType); + + Assert.Equal(2, dbFunc.Parameters.Count); + + Assert.Equal("c", dbFunc.Parameters[0].Name); + Assert.Equal(1, dbFunc.Parameters[0].Index); + Assert.Equal(typeof(string), dbFunc.Parameters[0].ParameterType); + + Assert.Equal("d", dbFunc.Parameters[1].Name); + Assert.Equal(0, dbFunc.Parameters[1].Index); + Assert.Equal(typeof(int), dbFunc.Parameters[1].ParameterType); + } + + [Fact] + public void Adding_method_with_attribute_and_fluent_configurationSource() + { + var modelBuilder = GetModelBuilder(); + + modelBuilder.HasDbFunction(MethodBmi, funcBuilder => + { + funcBuilder.HasName("foo").HasSchema("bar"); + funcBuilder.HasParameter("c").HasIndex(0); + funcBuilder.HasParameter("d").HasIndex(1); + }); + + var dbFunc = modelBuilder.HasDbFunction(MethodBmi).Metadata; + + Assert.Equal("foo", dbFunc.Name); + Assert.Equal("bar", dbFunc.Schema); + Assert.Equal(typeof(int), dbFunc.ReturnType); + + Assert.Equal(2, dbFunc.Parameters.Count); + + Assert.Equal("c", dbFunc.Parameters[0].Name); + Assert.Equal(0, dbFunc.Parameters[0].Index); + Assert.Equal(typeof(string), dbFunc.Parameters[0].ParameterType); + + Assert.Equal("d", dbFunc.Parameters[1].Name); + Assert.Equal(1, dbFunc.Parameters[1].Index); + Assert.Equal(typeof(int), dbFunc.Parameters[1].ParameterType); + } + + [Fact] + public void Adding_method_with_parameter_fluent_overrides() + { + var modelBuilder = GetModelBuilder(); + + var dbFuncBuilder = modelBuilder.HasDbFunction(MethodAmi); + var parameter = dbFuncBuilder.HasParameter("a").Metadata; + + Assert.Equal(parameter.Index, 0); + Assert.Equal(parameter.Name, "a"); + Assert.Equal(parameter.ParameterType, typeof(string)); + + parameter.Index = 5; + parameter.Name = "abc"; + parameter.ParameterType = typeof(int); + + Assert.Equal(parameter.Index, 5); + Assert.Equal(parameter.Name, "abc"); + Assert.Equal(parameter.ParameterType, typeof(int)); + } + + [Fact] + public void DbFunctionReturnType() + { + var modelBuilder = GetModelBuilder(); + + var dbFuncBuilder = modelBuilder.HasDbFunction(MethodAmi); + + Assert.Equal(dbFuncBuilder.Metadata.ReturnType, typeof(int)); + + dbFuncBuilder.Metadata.ReturnType = typeof(string); + + Assert.Equal(dbFuncBuilder.Metadata.ReturnType, typeof(string)); + + } + + [Fact] + public void Adding_method_with_relational_scema() + { + var modelBuilder = GetModelBuilder(); + + modelBuilder.HasDefaultSchema("dbo"); + + var dbFuncBuilder = modelBuilder.HasDbFunction(MethodAmi); + + Assert.Equal("dbo", dbFuncBuilder.Metadata.Schema); + } + + [Fact] + public void Adding_method_with_relational_scema_fluent_overrides() + { + var modelBuilder = GetModelBuilder(); + + modelBuilder.HasDefaultSchema("dbo"); + + var dbFuncBuilder = modelBuilder.HasDbFunction(MethodAmi, schema:"bar"); + + Assert.Equal("bar", dbFuncBuilder.Metadata.Schema); + } + + [Fact] + public void Adding_method_with_relational_scema_attribute_overrides() + { + var modelBuilder = GetModelBuilder(); + + modelBuilder.HasDefaultSchema("dbo"); + + var dbFuncBuilder = modelBuilder.HasDbFunction(MethodBmi); + + Assert.Equal("bar", dbFuncBuilder.Metadata.Schema); + } + + [Fact] + public void Adding_method_with_void_return_does_not_throw() + { + var modelBuilder = GetModelBuilder(); + + var dbFuncBuilder = modelBuilder.HasDbFunction(MethodCmi); + + Assert.Equal(typeof(void), dbFuncBuilder.Metadata.ReturnType); + } + + [Fact] + public void Add_method_generic_not_supported_throws() + { + var modelBuilder = GetModelBuilder(); + + var expectedMessage = CoreStrings.DbFunctionGenericMethodNotSupported(MethodHmi); + + Assert.Equal(expectedMessage, Assert.Throws(() => modelBuilder.HasDbFunction(MethodHmi)).Message); + } + + + [Fact] + public virtual void Set_empty_function_name_throws() + { + var modelBuilder = GetModelBuilder(); + + var expectedMessage = CoreStrings.ArgumentIsEmpty("name"); + + Assert.Equal(expectedMessage, Assert.Throws(() => modelBuilder.HasDbFunction(MethodAmi, name: "")).Message); + } + + [Fact] + public virtual void Set_empty_function_schema_throws() + { + var modelBuilder = GetModelBuilder(); + + var expectedMessage = CoreStrings.ArgumentIsEmpty("schema"); + + Assert.Equal(expectedMessage, Assert.Throws(() => modelBuilder.HasDbFunction(MethodAmi, schema: "")).Message); + } + + private ModelBuilder GetModelBuilder() + { + var conventionset = new ConventionSet(); + + conventionset.ModelAnnotationChangedConventions.Add(new RelationalDbFunctionConvention()); + + return new ModelBuilder(conventionset); + } + } +} diff --git a/test/EFCore.Relational.Tests/Metadata/RelationalBuilderExtensionsTest.cs b/test/EFCore.Relational.Tests/Metadata/RelationalBuilderExtensionsTest.cs index 529588c9ee1..a3247a1de68 100644 --- a/test/EFCore.Relational.Tests/Metadata/RelationalBuilderExtensionsTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/RelationalBuilderExtensionsTest.cs @@ -1,6 +1,7 @@ // 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.Reflection; using System.Collections.Generic; using System.Linq; using Microsoft.EntityFrameworkCore.Metadata; @@ -969,6 +970,28 @@ public void Can_create_schema_named_sequence_with_specific_facets_using_nested_c ValidateSchemaNamedSpecificSequence(sequence); } + + [Fact] + public void Can_create_dbFunction() + { + var modelBuilder = CreateConventionModelBuilder(); + var testMethod = typeof(TestDbFunctions).GetTypeInfo().GetDeclaredMethod(nameof(TestDbFunctions.MethodA)); + modelBuilder.HasDbFunction(testMethod); + + var dbFunc = modelBuilder.Model.Relational().FindDbFunction(testMethod); + + Assert.NotNull(dbFunc); + Assert.Equal("MethodA", dbFunc.Name); + Assert.Null(dbFunc.Schema); + Assert.Equal(2, dbFunc.Parameters.Count); + Assert.Equal("a", dbFunc.Parameters[0].Name); + Assert.Equal(typeof(string), dbFunc.Parameters[0].ParameterType); + Assert.Equal(0, dbFunc.Parameters[0].Index); + Assert.Equal("b", dbFunc.Parameters[1].Name); + Assert.Equal(typeof(int), dbFunc.Parameters[1].ParameterType); + Assert.Equal(1, dbFunc.Parameters[1].Index); + } + [Fact] public void Relational_entity_methods_dont_break_out_of_the_generics() { diff --git a/test/EFCore.Relational.Tests/Metadata/RelationalMetadataExtensionsTest.cs b/test/EFCore.Relational.Tests/Metadata/RelationalMetadataExtensionsTest.cs index 6257add6524..991ea9bbcdc 100644 --- a/test/EFCore.Relational.Tests/Metadata/RelationalMetadataExtensionsTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/RelationalMetadataExtensionsTest.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.Reflection; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Conventions; @@ -486,6 +487,24 @@ public void Can_get_and_set_schema_name_on_model() Assert.Null(extensions.DefaultSchema); } + [Fact] + public void Can_get_and_set_dbfunction() + { + var testMethod = typeof(TestDbFunctions).GetTypeInfo().GetDeclaredMethod(nameof(TestDbFunctions.MethodA)); + + var modelBuilder = new ModelBuilder(new ConventionSet()); + var model = modelBuilder.Model; + + Assert.Null(model.Relational().FindDbFunction(testMethod)); + + var dbFunc = model.Relational().GetOrAddDbFunction(testMethod, ConfigurationSource.Explicit, "MethodA"); + + Assert.NotNull(dbFunc); + Assert.Equal("MethodA", dbFunc.Name); + Assert.Null(dbFunc.Schema); + Assert.Equal(0, dbFunc.Parameters.Count); + } + [Fact] public void Can_get_and_set_sequence() { @@ -497,7 +516,7 @@ public void Can_get_and_set_sequence() Assert.Null(model.Relational().FindSequence("Foo")); var sequence = extensions.GetOrAddSequence("Foo"); - + Assert.Equal("Foo", extensions.FindSequence("Foo").Name); Assert.Equal("Foo", model.Relational().FindSequence("Foo").Name); diff --git a/test/EFCore.Relational.Tests/Metadata/TestDbFunctions.cs b/test/EFCore.Relational.Tests/Metadata/TestDbFunctions.cs new file mode 100644 index 00000000000..3558bc9ed9b --- /dev/null +++ b/test/EFCore.Relational.Tests/Metadata/TestDbFunctions.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.EntityFrameworkCore.Metadata +{ + public class TestDbFunctions + { + public static int MethodA(string a, int b) + { + throw new NotImplementedException(); + } + } +} diff --git a/test/EFCore.Relational.Tests/RelationalModelValidatorTest.cs b/test/EFCore.Relational.Tests/RelationalModelValidatorTest.cs index 3894cda2ef5..37422e6fbfe 100644 --- a/test/EFCore.Relational.Tests/RelationalModelValidatorTest.cs +++ b/test/EFCore.Relational.Tests/RelationalModelValidatorTest.cs @@ -1,8 +1,11 @@ // 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.ComponentModel.DataAnnotations.Schema; using System.Diagnostics; +using System.Linq; +using System.Reflection; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata; @@ -641,6 +644,93 @@ public virtual void Does_not_detect_missing_discriminator_value_for_abstract_cla Validate(modelBuilder.Model); } + [Fact] + public virtual void Detects_function_duplicate_parameter_index() + { + var modelBuilder = new ModelBuilder(TestRelationalConventionSetBuilder.Build()); + + var methodAmi = typeof(DbFunctionMetadataTests.TestMethods).GetRuntimeMethod(nameof(DbFunctionMetadataTests.TestMethods.MethodA), new[] { typeof(string), typeof(int) }); + + var dbFuncBuilder = modelBuilder.HasDbFunction(methodAmi); + + dbFuncBuilder.HasParameter("a").HasIndex(0); + dbFuncBuilder.HasParameter("b").HasIndex(0); + + VerifyError(CoreStrings.DbFunctionDuplicateIndex($"{methodAmi.DeclaringType.Name}.{methodAmi.Name}"), modelBuilder.Model); + } + + [Fact] + public virtual void Detects_function_missing_parameter_index() + { + var modelBuilder = new ModelBuilder(TestRelationalConventionSetBuilder.Build()); + + var methodAmi = typeof(DbFunctionMetadataTests.TestMethods).GetRuntimeMethod(nameof(DbFunctionMetadataTests.TestMethods.MethodA), new[] { typeof(string), typeof(int) }); + + var dbFuncBuilder = modelBuilder.HasDbFunction(methodAmi); + + dbFuncBuilder.HasParameter("a").HasIndex(5); + + VerifyError(CoreStrings.DbFunctionNonContinuousIndex($"{methodAmi.DeclaringType.Name}.{methodAmi.Name}"), modelBuilder.Model); + } + + [Fact] + public virtual void Detects_function_with_invalid_parameter_but_translate_callback_does_not_throw() + { + var modelBuilder = new ModelBuilder(TestRelationalConventionSetBuilder.Build()); + + var methodFmi = typeof(DbFunctionMetadataTests.TestMethods).GetRuntimeMethod(nameof(DbFunctionMetadataTests.TestMethods.MethodF), new [] { typeof(DbFunctionMetadataTests.MyBaseContext) }); + + var dbFuncBuilder = modelBuilder.HasDbFunction(methodFmi); + + dbFuncBuilder.TranslateWith((parameters, dbFunc) => null); + + Validate(modelBuilder.Model); + } + + [Fact] + public virtual void Detects_function_with_invalid_parameter_but_no_translate_callback_throws() + { + var modelBuilder = new ModelBuilder(TestRelationalConventionSetBuilder.Build()); + + var methodFmi = typeof(DbFunctionMetadataTests.TestMethods).GetRuntimeMethod(nameof(DbFunctionMetadataTests.TestMethods.MethodF), new [] { typeof(DbFunctionMetadataTests.MyBaseContext) }); + + modelBuilder.HasDbFunction(methodFmi); + + VerifyError(CoreStrings.DbFunctionInvalidParameterType(methodFmi, "context", typeof(DbFunctionMetadataTests.MyBaseContext)), modelBuilder.Model); + } + + [Fact] + public void Detects_method_invalid_parameter_type() + { + var modelBuilder = new ModelBuilder(TestRelationalConventionSetBuilder.Build()); + + var methodFmi = typeof(DbFunctionMetadataTests.TestMethods).GetRuntimeMethod(nameof(DbFunctionMetadataTests.TestMethods.MethodF), new [] { typeof(DbFunctionMetadataTests.MyBaseContext) }); + modelBuilder.HasDbFunction(methodFmi); + + VerifyError(CoreStrings.DbFunctionInvalidParameterType(methodFmi, "context", typeof(DbFunctionMetadataTests.MyBaseContext)), modelBuilder.Model); + } + + [Fact] + public virtual void Detects_non_static_function_on_dbcontext() + { + var modelBuilder = new ModelBuilder(TestRelationalConventionSetBuilder.Build()); + + modelBuilder.HasDbFunction(typeof(DbFunctionMetadataTests.MyDerivedContext).GetRuntimeMethod(nameof(DbFunctionMetadataTests.MyDerivedContext.NonStatic), new Type[] { })); + + VerifyError(CoreStrings.DbFunctionDbContextMethodMustBeStatic("MyDerivedContext.NonStatic"), modelBuilder.Model); + } + + [Fact] + public void Detects_invalid_return_type_throws() + { + var modelBuilder = new ModelBuilder(TestRelationalConventionSetBuilder.Build()); + + var methodDmi = typeof(DbFunctionMetadataTests.TestMethods).GetRuntimeMethod(nameof(DbFunctionMetadataTests.TestMethods.MethodD), new Type[] { }); + var dbFuncBuilder = modelBuilder.HasDbFunction(methodDmi); + + VerifyError(CoreStrings.DbFunctionInvalidReturnType(methodDmi, dbFuncBuilder.Metadata.ReturnType), modelBuilder.Model); + } + protected override void SetBaseType(EntityType entityType, EntityType baseEntityType) { base.SetBaseType(entityType, baseEntityType); diff --git a/test/EFCore.Relational.Tests/TestUtilities/FakeProvider/FakeRelationalOptionsExtension.cs b/test/EFCore.Relational.Tests/TestUtilities/FakeProvider/FakeRelationalOptionsExtension.cs index cd5c8f6f81c..1340c5192b2 100644 --- a/test/EFCore.Relational.Tests/TestUtilities/FakeProvider/FakeRelationalOptionsExtension.cs +++ b/test/EFCore.Relational.Tests/TestUtilities/FakeProvider/FakeRelationalOptionsExtension.cs @@ -43,7 +43,7 @@ public static IServiceCollection AddEntityFrameworkRelationalDatabase(IServiceCo .TryAdd() .TryAdd() .TryAdd() - .TryAdd() + .TryAdd() .TryAdd() .TryAdd() .TryAdd(_ => null) diff --git a/test/EFCore.SqlServer.FunctionalTests/Northwind.sql b/test/EFCore.SqlServer.FunctionalTests/Northwind.sql index 12906574052..91a8c2e80a7 100644 Binary files a/test/EFCore.SqlServer.FunctionalTests/Northwind.sql and b/test/EFCore.SqlServer.FunctionalTests/Northwind.sql differ diff --git a/test/EFCore.SqlServer.FunctionalTests/NorthwindDbFunctionSqlServerFixture.cs b/test/EFCore.SqlServer.FunctionalTests/NorthwindDbFunctionSqlServerFixture.cs new file mode 100644 index 00000000000..803a21440ee --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/NorthwindDbFunctionSqlServerFixture.cs @@ -0,0 +1,48 @@ +// 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.Reflection; +using Microsoft.EntityFrameworkCore.Query.Expressions; +using Microsoft.EntityFrameworkCore.TestModels.Northwind; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Internal; + +namespace Microsoft.EntityFrameworkCore +{ + public class NorthwindDbFunctionSqlServerFixture : NorthwindQuerySqlServerFixture + { + public NorthwindDbFunctionSqlServerFixture() + : base(mb => + p => + new TestModelSource( + mb, + p.GetRequiredService(), + (modelBuilder, context) => + { + new RelationalModelCustomizer(p.GetRequiredService()).Customize(modelBuilder, context); + }) + ) + { + } + + public override NorthwindContext CreateContext( + QueryTrackingBehavior queryTrackingBehavior = QueryTrackingBehavior.TrackAll, + bool enableFilters = false) + { + EnableFilters = enableFilters; + + return new NorthwindDbFunctionContext(Options ?? (Options = BuildOptions()), queryTrackingBehavior); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.HasDbFunction(typeof(NorthwindDbFunctionContext).GetRuntimeMethod(nameof(NorthwindDbFunctionContext.MyCustomLength), new[] { typeof(string) })) + .TranslateWith((args, dbFunc) => new SqlFunctionExpression("len", dbFunc.ReturnType, args)); + + modelBuilder.HasDbFunction(typeof(DateTimeExtensions).GetRuntimeMethod(nameof(DateTimeExtensions.IsDate), new[] { typeof(string) })); + } + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/NorthwindQuerySqlServerFixture.cs b/test/EFCore.SqlServer.FunctionalTests/NorthwindQuerySqlServerFixture.cs index 6f6e09e8b6e..729bee1202b 100644 --- a/test/EFCore.SqlServer.FunctionalTests/NorthwindQuerySqlServerFixture.cs +++ b/test/EFCore.SqlServer.FunctionalTests/NorthwindQuerySqlServerFixture.cs @@ -13,6 +13,17 @@ namespace Microsoft.EntityFrameworkCore { public class NorthwindQuerySqlServerFixture : NorthwindQueryRelationalFixture, IDisposable { + private readonly Func, Func> _modelSourceFactory; + + public NorthwindQuerySqlServerFixture() : this(TestModelSource.GetFactory) + { + } + + protected NorthwindQuerySqlServerFixture(Func, Func> modelSourceFactory) + { + _modelSourceFactory = modelSourceFactory; + } + private readonly SqlServerTestStore _testStore = SqlServerTestStore.GetNorthwindStore(); public TestSqlLoggerFactory TestSqlLoggerFactory { get; } = new TestSqlLoggerFactory(); @@ -25,7 +36,7 @@ public override DbContextOptions BuildOptions(IServiceCollection additionalServi .UseInternalServiceProvider( (additionalServices ?? new ServiceCollection()) .AddEntityFrameworkSqlServer() - .AddSingleton(TestModelSource.GetFactory(OnModelCreating)) + .AddSingleton(_modelSourceFactory(OnModelCreating)) .AddSingleton(TestSqlLoggerFactory) .BuildServiceProvider())) .UseSqlServer( diff --git a/test/EFCore.SqlServer.FunctionalTests/UdfDbFunctionSqlServerTests.cs b/test/EFCore.SqlServer.FunctionalTests/UdfDbFunctionSqlServerTests.cs new file mode 100644 index 00000000000..e6fd1ac18b6 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/UdfDbFunctionSqlServerTests.cs @@ -0,0 +1,698 @@ +// 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.Linq; +using Xunit; + +namespace Microsoft.EntityFrameworkCore +{ + public static class DateTimeExtensions + { + public static bool IsDate(this string date) + { + throw new Exception(); + } + } + + public class UdfDbFunctionSqlServerTests : IClassFixture + { + public UdfDbFunctionSqlServerTests(NorthwindDbFunctionSqlServerFixture fixture) + { + Fixture = fixture; + + Fixture.TestSqlLoggerFactory.Clear(); + } + + protected NorthwindDbFunctionSqlServerFixture Fixture { get; } + + protected NorthwindDbFunctionContext CreateContext() => Fixture.CreateContext() as NorthwindDbFunctionContext; + + #region Scalar Tests + + private static int AddFive(int number) + { + return number + 5; + } + + [Fact] + void Scalar_Function_Extension_Method() + { + using (var context = CreateContext()) + { + var len = context.Employees.Where(e => e.FirstName.IsDate() == false).Count(); + + Assert.Equal(9, len); + + AssertSql( + @"SELECT COUNT(*) +FROM [Employees] AS [e] +WHERE CASE + WHEN IsDate([e].[FirstName]) = 1 + THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT) +END = 0"); + } + } + + [Fact] + void Scalar_Function_With_Translator_Translates() + { + using (var context = CreateContext()) + { + var employeeId = 5; + + var len = context.Employees.Where(e => e.EmployeeID == employeeId).Select(e => NorthwindDbFunctionContext.MyCustomLength(e.FirstName)).Single(); + + Assert.Equal(6, len); + + AssertSql( + @"@__employeeId_0='5' + +SELECT TOP(2) len([e].[FirstName]) +FROM [Employees] AS [e] +WHERE [e].[EmployeeID] = @__employeeId_0"); + } + } + + [Fact] + public void Scalar_Function_Parameters_Reordered_By_Index() + { + using (var context = CreateContext()) + { + var employeeId = 5; + var starCount = 3; + + var emp = (from e in context.Employees + where e.EmployeeID == employeeId + select new + { + e.FirstName, + OrderCount = NorthwindDbFunctionContext.StarValueAlternateParamOrder(NorthwindDbFunctionContext.EmployeeOrderCount(employeeId), starCount) + }).Single(); + + Assert.Equal("Steven", emp.FirstName); + Assert.Equal("***42", emp.OrderCount); + + AssertSql( + @"@__starCount_2='3' +@__employeeId_1='5' +@__employeeId_0='5' + +SELECT TOP(2) [e].[FirstName], [dbo].StarValue(@__starCount_2, [dbo].EmployeeOrderCount(@__employeeId_1)) AS [OrderCount] +FROM [Employees] AS [e] +WHERE [e].[EmployeeID] = @__employeeId_0"); + } + } + + [Fact] + public void Scalar_Function_ClientEval_Method_As_Translateable_Method_Parameter() + { + using (var context = CreateContext()) + { + Assert.Throws(() => (from e in context.Employees + where e.EmployeeID == 5 + select new + { + e.FirstName, + OrderCount = NorthwindDbFunctionContext.EmployeeOrderCount(AddFive(e.EmployeeID - 5)) + }).Single()); + } + } + + [Fact] + public void Scalar_Function_Constant_Parameter() + { + using (var context = CreateContext()) + { + var employeeId = 5; + + var emps = context.Employees.Select(e => NorthwindDbFunctionContext.EmployeeOrderCount(employeeId)).ToList(); + + Assert.Equal(9, emps.Count); + + AssertSql( + @"@__employeeId_0='5' + +SELECT [dbo].EmployeeOrderCount(@__employeeId_0) +FROM [Employees] AS [e]"); + } + } + + [Fact] + public void Scalar_Function_Anonymous_Type_Select_Correlated() + { + using (var context = CreateContext()) + { + var emp = (from e in context.Employees + where e.EmployeeID == 5 + select new + { + e.FirstName, + OrderCount = NorthwindDbFunctionContext.EmployeeOrderCount(e.EmployeeID) + }).Single(); + + Assert.Equal("Steven", emp.FirstName); + Assert.Equal(42, emp.OrderCount); + + AssertSql( + @"SELECT TOP(2) [e].[FirstName], [dbo].EmployeeOrderCount([e].[EmployeeID]) AS [OrderCount] +FROM [Employees] AS [e] +WHERE [e].[EmployeeID] = 5"); + } + } + + [Fact] + public void Scalar_Function_Anonymous_Type_Select_Not_Correlated() + { + using (var context = CreateContext()) + { + var emp = (from e in context.Employees + where e.EmployeeID == 5 + select new + { + e.FirstName, + OrderCount = NorthwindDbFunctionContext.EmployeeOrderCount(5) + }).Single(); + + Assert.Equal("Steven", emp.FirstName); + Assert.Equal(42, emp.OrderCount); + + AssertSql( + @"SELECT TOP(2) [e].[FirstName], [dbo].EmployeeOrderCount(5) AS [OrderCount] +FROM [Employees] AS [e] +WHERE [e].[EmployeeID] = 5"); + } + } + + [Fact] + public void Scalar_Function_Anonymous_Type_Select_Parameter() + { + using (var context = CreateContext()) + { + var employeeId = 5; + + var emp = (from e in context.Employees + where e.EmployeeID == employeeId + select new + { + e.FirstName, + OrderCount = NorthwindDbFunctionContext.EmployeeOrderCount(employeeId) + }).Single(); + + Assert.Equal("Steven", emp.FirstName); + Assert.Equal(42, emp.OrderCount); + + AssertSql( + @"@__employeeId_1='5' +@__employeeId_0='5' + +SELECT TOP(2) [e].[FirstName], [dbo].EmployeeOrderCount(@__employeeId_1) AS [OrderCount] +FROM [Employees] AS [e] +WHERE [e].[EmployeeID] = @__employeeId_0"); + } + } + + [Fact] + public void Scalar_Function_Anonymous_Type_Select_Nested() + { + using (var context = CreateContext()) + { + var employeeId = 5; + var starCount = 3; + + var emp = (from e in context.Employees + where e.EmployeeID == employeeId + select new + { + e.FirstName, + OrderCount = NorthwindDbFunctionContext.StarValue(starCount, NorthwindDbFunctionContext.EmployeeOrderCount(employeeId)) + }).Single(); + + Assert.Equal("Steven", emp.FirstName); + Assert.Equal("***42", emp.OrderCount); + + AssertSql( + @"@__starCount_1='3' +@__employeeId_2='5' +@__employeeId_0='5' + +SELECT TOP(2) [e].[FirstName], [dbo].StarValue(@__starCount_1, [dbo].EmployeeOrderCount(@__employeeId_2)) AS [OrderCount] +FROM [Employees] AS [e] +WHERE [e].[EmployeeID] = @__employeeId_0"); + } + } + + [Fact] + public void Scalar_Function_Where_Correlated() + { + using (var context = CreateContext()) + { + var emp = (from e in context.Employees + where NorthwindDbFunctionContext.IsTopEmployee(e.EmployeeID) + select e.EmployeeID.ToString().ToLower()).ToList(); + + Assert.Equal(3, emp.Count); + + AssertSql( + @"SELECT LOWER(CONVERT(VARCHAR(11), [e].[EmployeeID])) +FROM [Employees] AS [e] +WHERE [dbo].IsTopEmployee([e].[EmployeeID]) = 1"); + } + } + + [Fact] + public void Scalar_Function_Where_Not_Correlated() + { + using (var context = CreateContext()) + { + var startDate = DateTime.Parse("1/1/1998"); + + var emp = (from e in context.Employees + where NorthwindDbFunctionContext.GetEmployeeWithMostOrdersAfterDate(startDate) == e.EmployeeID + select e).SingleOrDefault(); + + Assert.NotNull(emp); + Assert.True(emp.EmployeeID == 4); + + AssertSql( + @"@__startDate_0='01/01/1998 00:00:00' + +SELECT TOP(2) [e].[EmployeeID], [e].[City], [e].[Country], [e].[FirstName], [e].[ReportsTo], [e].[Title] +FROM [Employees] AS [e] +WHERE [dbo].GetEmployeeWithMostOrdersAfterDate(@__startDate_0) = [e].[EmployeeID]"); + } + } + + [Fact] + public void Scalar_Function_Where_Parameter() + { + using (var context = CreateContext()) + { + var period = NorthwindDbFunctionContext.ReportingPeriod.Winter; + + var emp = (from e in context.Employees + where e.EmployeeID == NorthwindDbFunctionContext.GetEmployeeWithMostOrdersAfterDate( + NorthwindDbFunctionContext.GetReportingPeriodStartDate(period)) + select e).SingleOrDefault(); + + Assert.NotNull(emp); + Assert.True(emp.EmployeeID == 4); + + AssertSql( + @"@__period_0='Winter' + +SELECT TOP(2) [e].[EmployeeID], [e].[City], [e].[Country], [e].[FirstName], [e].[ReportsTo], [e].[Title] +FROM [Employees] AS [e] +WHERE [e].[EmployeeID] = [dbo].GetEmployeeWithMostOrdersAfterDate([dbo].GetReportingPeriodStartDate(@__period_0))"); + } + } + + [Fact] + public void Scalar_Function_Where_Nested() + { + using (var context = CreateContext()) + { + var emp = (from e in context.Employees + where e.EmployeeID == NorthwindDbFunctionContext.GetEmployeeWithMostOrdersAfterDate( + NorthwindDbFunctionContext.GetReportingPeriodStartDate( + NorthwindDbFunctionContext.ReportingPeriod.Winter)) + select e).SingleOrDefault(); + + Assert.NotNull(emp); + Assert.True(emp.EmployeeID == 4); + + AssertSql( + @"SELECT TOP(2) [e].[EmployeeID], [e].[City], [e].[Country], [e].[FirstName], [e].[ReportsTo], [e].[Title] +FROM [Employees] AS [e] +WHERE [e].[EmployeeID] = [dbo].GetEmployeeWithMostOrdersAfterDate([dbo].GetReportingPeriodStartDate(0))"); + } + } + + [Fact] + public void Scalar_Function_Let_Correlated() + { + using (var context = CreateContext()) + { + var emp = (from e in context.Employees + let orderCount = NorthwindDbFunctionContext.EmployeeOrderCount(e.EmployeeID) + where e.EmployeeID == 5 + select new + { + e.FirstName, + OrderCount = orderCount + }).Single(); + + Assert.Equal("Steven", emp.FirstName); + Assert.Equal(42, emp.OrderCount); + + AssertSql( + @"SELECT TOP(2) [e].[FirstName], [dbo].EmployeeOrderCount([e].[EmployeeID]) AS [OrderCount] +FROM [Employees] AS [e] +WHERE [e].[EmployeeID] = 5"); + } + } + + [Fact] + public void Scalar_Function_Let_Not_Correlated() + { + using (var context = CreateContext()) + { + var emp = (from e in context.Employees + let orderCount = NorthwindDbFunctionContext.EmployeeOrderCount(5) + where e.EmployeeID == 5 + select new + { + e.FirstName, + OrderCount = orderCount + }).Single(); + + Assert.Equal("Steven", emp.FirstName); + Assert.Equal(42, emp.OrderCount); + + AssertSql( + @"SELECT TOP(2) [e].[FirstName], [dbo].EmployeeOrderCount(5) AS [OrderCount] +FROM [Employees] AS [e] +WHERE [e].[EmployeeID] = 5"); + } + } + + [Fact] + public void Scalar_Function_Let_Not_Parameter() + { + var employeeId = 5; + + using (var context = CreateContext()) + { + var emp = (from e in context.Employees + let orderCount = NorthwindDbFunctionContext.EmployeeOrderCount(employeeId) + where e.EmployeeID == employeeId + select new + { + e.FirstName, + OrderCount = orderCount + }).Single(); + + Assert.Equal("Steven", emp.FirstName); + Assert.Equal(42, emp.OrderCount); + + AssertSql( + @"@__employeeId_0='5' +@__employeeId_1='5' + +SELECT TOP(2) [e].[FirstName], [dbo].EmployeeOrderCount(@__employeeId_0) AS [OrderCount] +FROM [Employees] AS [e] +WHERE [e].[EmployeeID] = @__employeeId_1"); + } + } + + #endregion + + [Fact] + public void Scalar_Function_Let_Nested() + { + using (var context = CreateContext()) + { + var employeeId = 5; + var starCount = 3; + + var emp = (from e in context.Employees + let orderCount = NorthwindDbFunctionContext.StarValue(starCount, NorthwindDbFunctionContext.EmployeeOrderCount(employeeId)) + where e.EmployeeID == employeeId + select new + { + e.FirstName, + OrderCount = orderCount + }).Single(); + + Assert.Equal("Steven", emp.FirstName); + Assert.Equal("***42", emp.OrderCount); + + AssertSql( + @"@__starCount_0='3' +@__employeeId_1='5' +@__employeeId_2='5' + +SELECT TOP(2) [e].[FirstName], [dbo].StarValue(@__starCount_0, [dbo].EmployeeOrderCount(@__employeeId_1)) AS [OrderCount] +FROM [Employees] AS [e] +WHERE [e].[EmployeeID] = @__employeeId_2"); + } + } + + public static int AddOne(int num) + { + return num + 1; + } + + [Fact] + public void Scalar_Nested_Function_Unwind_Client_Eval_Where() + { + using (var context = CreateContext()) + { + var results = (from e in context.Employees + where 2 == AddOne(e.EmployeeID) + select e.EmployeeID).Single(); + + Assert.Equal(1, results); + AssertSql( + @"SELECT [e].[EmployeeID] +FROM [Employees] AS [e]"); + } + } + + [Fact] + public void Scalar_Nested__Function_Unwind_Client_Eval_OrderBy() + { + using (var context = CreateContext()) + { + var results = (from e in context.Employees + orderby AddOne(e.EmployeeID) + select e.EmployeeID).ToList(); + + Assert.Equal(9, results.Count); + Assert.True(results.SequenceEqual(Enumerable.Range(1, 9))); + + AssertSql( + @"SELECT [e].[EmployeeID] +FROM [Employees] AS [e]"); + } + } + + [Fact] + public void Scalar_Nested_Function_Unwind_Client_Eval_Select() + { + using (var context = CreateContext()) + { + var results = (from e in context.Employees + orderby e.EmployeeID + select AddOne(e.EmployeeID)).ToList(); + + Assert.Equal(9, results.Count); + Assert.True(results.SequenceEqual(Enumerable.Range(2, 9))); + + AssertSql( + @"SELECT [e].[EmployeeID] +FROM [Employees] AS [e] +ORDER BY [e].[EmployeeID]"); + } + } + + [Fact] + public void Scalar_Nested_Function_Client_BCL_UDF() + { + using (var context = CreateContext()) + { + var results = (from e in context.Employees + where 128 == AddOne(Math.Abs(NorthwindDbFunctionContext.EmployeeOrderCountWithClient(e.EmployeeID))) + select e.EmployeeID).Single(); + + Assert.Equal(3, results); + AssertSql( + @"SELECT [e].[EmployeeID] +FROM [Employees] AS [e]"); + } + } + + [Fact] + public void Scalar_Nested_Function_Client_UDF_BCL() + { + using (var context = CreateContext()) + { + var results = (from e in context.Employees + where 128 == AddOne(NorthwindDbFunctionContext.EmployeeOrderCountWithClient(Math.Abs(e.EmployeeID))) + select e.EmployeeID).Single(); + + Assert.Equal(3, results); + AssertSql( + @"SELECT [e].[EmployeeID] +FROM [Employees] AS [e]"); + } + } + + [Fact] + public void Scalar_Nested_Function_BCL_Client_UDF() + { + using (var context = CreateContext()) + { + var results = (from e in context.Employees + where 128 == Math.Abs(AddOne(NorthwindDbFunctionContext.EmployeeOrderCountWithClient(e.EmployeeID))) + select e.EmployeeID).Single(); + + Assert.Equal(3, results); + AssertSql( + @"SELECT [e].[EmployeeID] +FROM [Employees] AS [e]"); + } + } + + [Fact] + public void Scalar_Nested_Function_BCL_UDF_Client() + { + using (var context = CreateContext()) + { + var results = (from e in context.Employees + where 127 == Math.Abs(NorthwindDbFunctionContext.EmployeeOrderCountWithClient(AddOne(e.EmployeeID))) + select e.EmployeeID).Single(); + + Assert.Equal(2, results); + AssertSql( + @"SELECT [e].[EmployeeID] +FROM [Employees] AS [e]"); + } + } + + [Fact] + public void Scalar_Nested_Function_UDF_BCL_Client() + { + using (var context = CreateContext()) + { + var results = (from e in context.Employees + where 127 == NorthwindDbFunctionContext.EmployeeOrderCountWithClient(Math.Abs(AddOne(e.EmployeeID))) + select e.EmployeeID).Single(); + + Assert.Equal(2, results); + AssertSql( + @"SELECT [e].[EmployeeID] +FROM [Employees] AS [e]"); + } + } + + [Fact] + public void Scalar_Nested_Function_UDF_Client_BCL() + { + using (var context = CreateContext()) + { + var results = (from e in context.Employees + where 127 == NorthwindDbFunctionContext.EmployeeOrderCountWithClient(AddOne(Math.Abs(e.EmployeeID))) + select e.EmployeeID).Single(); + + Assert.Equal(2, results); + AssertSql( + @"SELECT [e].[EmployeeID] +FROM [Employees] AS [e]"); + } + } + + [Fact] + public void Scalar_Nested_Function_Client_BCL() + { + using (var context = CreateContext()) + { + var results = (from e in context.Employees + where 3 == AddOne(Math.Abs(e.EmployeeID)) + select e.EmployeeID).Single(); + + Assert.Equal(2, results); + AssertSql( + @"SELECT [e].[EmployeeID] +FROM [Employees] AS [e]"); + } + } + + [Fact] + public void Scalar_Nested_Function_Client_UDF() + { + using (var context = CreateContext()) + { + var results = (from e in context.Employees + where 128 == AddOne(NorthwindDbFunctionContext.EmployeeOrderCountWithClient(e.EmployeeID)) + select e.EmployeeID).Single(); + + Assert.Equal(3, results); + AssertSql( + @"SELECT [e].[EmployeeID] +FROM [Employees] AS [e]"); + } + } + + [Fact] + public void Scalar_Nested_Function_BCL_Client() + { + using (var context = CreateContext()) + { + var results = (from e in context.Employees + where 3 == Math.Abs(AddOne(e.EmployeeID)) + select e.EmployeeID).Single(); + + Assert.Equal(2, results); + AssertSql( + @"SELECT [e].[EmployeeID] +FROM [Employees] AS [e]"); + } + } + + [Fact] + public void Scalar_Nested_Function_BCL_UDF() + { + using (var context = CreateContext()) + { + var results = (from e in context.Employees + where 127 == Math.Abs(NorthwindDbFunctionContext.EmployeeOrderCountWithClient(e.EmployeeID)) + select e.EmployeeID).Single(); + + Assert.Equal(3, results); + AssertSql( + @"SELECT TOP(2) [e].[EmployeeID] +FROM [Employees] AS [e] +WHERE 127 = ABS([dbo].EmployeeOrderCount([e].[EmployeeID]))"); + } + } + + [Fact] + public void Scalar_Nested_Function_UDF_Client() + { + using (var context = CreateContext()) + { + var results = (from e in context.Employees + where 127 == NorthwindDbFunctionContext.EmployeeOrderCountWithClient(AddOne(e.EmployeeID)) + select e.EmployeeID).Single(); + + Assert.Equal(2, results); + AssertSql( + @"SELECT [e].[EmployeeID] +FROM [Employees] AS [e]"); + } + } + + [Fact] + public void Scalar_Nested_Function_UDF_BCL() + { + using (var context = CreateContext()) + { + var results = (from e in context.Employees + where 127 == NorthwindDbFunctionContext.EmployeeOrderCountWithClient(Math.Abs(e.EmployeeID)) + select e.EmployeeID).Single(); + + Assert.Equal(3, results); + AssertSql( + @"SELECT TOP(2) [e].[EmployeeID] +FROM [Employees] AS [e] +WHERE 127 = [dbo].EmployeeOrderCount(ABS([e].[EmployeeID]))"); + } + } + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + } +} + + diff --git a/test/EFCore.SqlServer.Tests/Migrations/SqlServerModelDifferTest.cs b/test/EFCore.SqlServer.Tests/Migrations/SqlServerModelDifferTest.cs index 2884d457e5b..bd212b31c99 100644 --- a/test/EFCore.SqlServer.Tests/Migrations/SqlServerModelDifferTest.cs +++ b/test/EFCore.SqlServer.Tests/Migrations/SqlServerModelDifferTest.cs @@ -1,7 +1,9 @@ // 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.Linq; +using System.Reflection; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Internal; @@ -809,6 +811,20 @@ public void Alter_sequence_overridden() }); } + [Fact] + public void Add_dbfunction_ignore() + { + var mi = typeof(string).GetRuntimeMethod(nameof(string.ToLower), new Type[] { }); + + Execute( + _ => { }, + modelBuilder => modelBuilder.HasDbFunction(mi, null, "dbo"), + operations => + { + Assert.Equal(0, operations.Count); + }); + } + [Fact] public void Alter_column_rowversion() { diff --git a/test/EFCore.Tests/Metadata/Conventions/Internal/ConventionDispatcherTest.cs b/test/EFCore.Tests/Metadata/Conventions/Internal/ConventionDispatcherTest.cs index a21b030a49e..39797d42917 100644 --- a/test/EFCore.Tests/Metadata/Conventions/Internal/ConventionDispatcherTest.cs +++ b/test/EFCore.Tests/Metadata/Conventions/Internal/ConventionDispatcherTest.cs @@ -398,6 +398,96 @@ public Annotation Apply( } } + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + [Theory] + public void OnModelAnnotationSet_calls_apply_on_conventions_in_order(bool useBuilder, bool useScope) + { + var conventions = new ConventionSet(); + + var convention1 = new ModelAnnotationChangedConvention(false); + var convention2 = new ModelAnnotationChangedConvention(true); + var convention3 = new ModelAnnotationChangedConvention(false); + conventions.ModelAnnotationChangedConventions.Add(convention1); + conventions.ModelAnnotationChangedConventions.Add(convention2); + conventions.ModelAnnotationChangedConventions.Add(convention3); + + var builder = new InternalModelBuilder(new Model(conventions)); + + var scope = useScope ? builder.Metadata.ConventionDispatcher.StartBatch() : null; + + if (useBuilder) + { + Assert.NotNull(builder.HasAnnotation("foo", "bar", ConfigurationSource.Convention)); + } + else + { + builder.Metadata["foo"] = "bar"; + } + + if (useScope) + { + Assert.Empty(convention1.Calls); + Assert.Empty(convention2.Calls); + scope.Dispose(); + } + + Assert.Equal(new[] { "bar" }, convention1.Calls); + Assert.Equal(new[] { "bar" }, convention2.Calls); + Assert.Empty(convention3.Calls); + + if (useBuilder) + { + Assert.NotNull(builder.HasAnnotation("foo", "bar", ConfigurationSource.Convention)); + } + else + { + builder.Metadata["foo"] = "bar"; + } + + Assert.Equal(new[] { "bar" }, convention1.Calls); + Assert.Equal(new[] { "bar" }, convention2.Calls); + Assert.Empty(convention3.Calls); + + if (useBuilder) + { + Assert.NotNull(builder.HasAnnotation("foo", null, ConfigurationSource.Convention)); + } + else + { + builder.Metadata.RemoveAnnotation("foo"); + } + + Assert.Equal(new[] { "bar", null }, convention1.Calls); + Assert.Equal(new[] { "bar", null }, convention2.Calls); + } + + private class ModelAnnotationChangedConvention : IModelAnnotationChangedConvention + { + private readonly bool _terminate; + public readonly List Calls = new List(); + + public ModelAnnotationChangedConvention(bool terminate) + { + _terminate = terminate; + } + + public Annotation Apply( + InternalModelBuilder propertyBuilder, + string name, + Annotation annotation, + Annotation oldAnnotation) + { + Assert.NotNull(propertyBuilder.Metadata.Builder); + + Calls.Add(annotation?.Value); + + return _terminate ? null : annotation; + } + } + [InlineData(false, false)] [InlineData(true, false)] [InlineData(false, true)] diff --git a/test/EFCore.Tests/ModelSourceTest.cs b/test/EFCore.Tests/ModelSourceTest.cs index e63b58057f9..e28986633cb 100644 --- a/test/EFCore.Tests/ModelSourceTest.cs +++ b/test/EFCore.Tests/ModelSourceTest.cs @@ -105,9 +105,8 @@ private class ConcreteModelSource : ModelSource { public ConcreteModelSource(IDbSetFinder setFinder) : base(new ModelSourceDependencies( - setFinder, new CoreConventionSetBuilder(new CoreConventionSetBuilderDependencies(new CoreTypeMapper())), - new ModelCustomizer(new ModelCustomizerDependencies()), + new ModelCustomizer(new ModelCustomizerDependencies(setFinder)), new ModelCacheKeyFactory(new ModelCacheKeyFactoryDependencies()))) { }