From f29293682ffb716d049dd24e9f2ea979a702f70a Mon Sep 17 00:00:00 2001 From: AndriySvyryd Date: Mon, 23 Mar 2020 12:08:15 -0700 Subject: [PATCH] Add a convention to create the entity type returned by a queryable function. Store mapped EntityType in the DbFunction. Extract InternalDbFunctionBuilder and InternalDbFunctionParameterBuilder from DbFunctionBuilder and DbFunctionParameterBuilder. Add more conditions to metadata API consistency test. Part of #20051 Fixes #20160 --- .../Query/Internal/SelectExpression.cs | 1 - .../RelationalEntityTypeBuilderExtensions.cs | 31 -- .../RelationalEntityTypeExtensions.cs | 5 - .../RelationalModelBuilderExtensions.cs | 80 ++---- .../Extensions/RelationalModelExtensions.cs | 44 +-- .../RelationalModelValidator.cs | 4 +- .../Metadata/Builders/DbFunctionBuilder.cs | 153 ++-------- .../Builders/DbFunctionParameterBuilder.cs | 64 ++--- .../Builders/IConventionDbFunctionBuilder.cs | 12 +- .../IConventionDbFunctionParameterBuilder.cs | 6 +- .../Builders/IConventionSequenceBuilder.cs | 4 +- .../Metadata/Builders/SequenceBuilder.cs | 137 ++------- .../DbFunctionTypeMappingConvention.cs | 19 +- .../RelationalConventionSetBuilder.cs | 5 + .../QueryableDbFunctionConvention.cs | 92 ++++++ ...RelationalDbFunctionAttributeConvention.cs | 4 +- .../Metadata/IConventionDbFunction.cs | 4 +- .../IConventionDbFunctionParameter.cs | 7 + src/EFCore.Relational/Metadata/IDbFunction.cs | 12 + .../Metadata/Internal/CheckConstraint.cs | 8 + .../Internal/CheckConstraintExtensions.cs | 51 ++++ .../Metadata/Internal/DbFunction.cs | 270 +++++++++--------- .../Metadata/Internal/DbFunctionExtensions.cs | 69 +++++ .../Metadata/Internal/DbFunctionParameter.cs | 125 ++++---- .../Internal/DbFunctionParameterExtensions.cs | 50 ++++ .../Internal/InternalDbFunctionBuilder.cs | 258 +++++++++++++++++ .../InternalDbFunctionParameterBuilder.cs | 119 ++++++++ .../Internal/InternalSequenceBuilder.cs | 264 +++++++++++++++++ .../Metadata/Internal/Sequence.cs | 40 ++- .../Metadata/Internal/SequenceExtensions.cs | 84 ++++++ .../Metadata/Internal/TableExtensions.cs | 14 +- .../Metadata/RelationalAnnotationNames.cs | 5 - src/EFCore.Relational/Migrations/Migration.cs | 2 +- .../Properties/RelationalStrings.Designer.cs | 16 ++ .../Properties/RelationalStrings.resx | 6 + ...nToQueryRootConvertingExpressionVisitor.cs | 6 +- .../Query/RelationalEntityShaperExpression.cs | 2 +- .../Query/SqlExpressionFactory.cs | 2 +- .../Infrastructure/ConventionAnnotatable.cs | 1 - .../Metadata/Builders/EntityTypeBuilder.cs | 1 - .../Conventions/KeyDiscoveryConvention.cs | 2 +- .../Internal/InternalEntityTypeBuilder.cs | 5 +- .../Query/UdfDbFunctionTestBase.cs | 171 +---------- .../RelationalModelValidatorTest.cs | 2 +- .../QueryableDbFunctionConventionTest.cs | 121 ++++++++ .../Metadata/DbFunctionMetadataTests.cs | 80 ++++-- .../Internal/MigrationsModelDifferTest.cs | 42 +-- .../TestUtilities/TestHelpers.cs | 6 +- .../Query/UdfDbFunctionSqlServerTests.cs | 98 +------ test/EFCore.Tests/ApiConsistencyTestBase.cs | 37 ++- 50 files changed, 1680 insertions(+), 961 deletions(-) create mode 100644 src/EFCore.Relational/Metadata/Conventions/QueryableDbFunctionConvention.cs create mode 100644 src/EFCore.Relational/Metadata/Internal/CheckConstraintExtensions.cs create mode 100644 src/EFCore.Relational/Metadata/Internal/DbFunctionExtensions.cs create mode 100644 src/EFCore.Relational/Metadata/Internal/DbFunctionParameterExtensions.cs create mode 100644 src/EFCore.Relational/Metadata/Internal/InternalDbFunctionBuilder.cs create mode 100644 src/EFCore.Relational/Metadata/Internal/InternalDbFunctionParameterBuilder.cs create mode 100644 src/EFCore.Relational/Metadata/Internal/InternalSequenceBuilder.cs create mode 100644 src/EFCore.Relational/Metadata/Internal/SequenceExtensions.cs create mode 100644 test/EFCore.Relational.Tests/Metadata/Conventions/Internal/QueryableDbFunctionConventionTest.cs diff --git a/src/EFCore.Cosmos/Query/Internal/SelectExpression.cs b/src/EFCore.Cosmos/Query/Internal/SelectExpression.cs index 8becafef953..83e36f8282c 100644 --- a/src/EFCore.Cosmos/Query/Internal/SelectExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/SelectExpression.cs @@ -7,7 +7,6 @@ using System.Linq.Expressions; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Cosmos.Internal; -using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Utilities; diff --git a/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs b/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs index fccddb57601..cdd3def5a02 100644 --- a/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs @@ -264,37 +264,6 @@ public static bool CanSetSchema( return entityTypeBuilder.CanSetAnnotation(RelationalAnnotationNames.Schema, schema, fromDataAnnotation); } - /// - /// Configures the entity as a result of a queryable function. Prevents a table from being created for this entity. - /// - /// The builder for the entity type being configured. - /// The same builder instance so that multiple calls can be chained. - public static EntityTypeBuilder ToQueryableFunctionResultType( - [NotNull] this EntityTypeBuilder entityTypeBuilder) - { - Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder)); - - entityTypeBuilder.Metadata.SetAnnotation(RelationalAnnotationNames.QueryableFunctionResultType, null); - - return entityTypeBuilder; - } - - /// - /// Configures the entity as a result of a queryable function. Prevents a table from being created for this entity. - /// - /// The builder for the entity type being configured. - /// The same builder instance so that multiple calls can be chained. - public static EntityTypeBuilder ToQueryableFunctionResultType( - [NotNull] this EntityTypeBuilder entityTypeBuilder) - where TEntity : class - { - Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder)); - - entityTypeBuilder.Metadata.SetAnnotation(RelationalAnnotationNames.QueryableFunctionResultType, null); - - return entityTypeBuilder; - } - /// /// Configures the view that the entity type maps to when targeting a relational database. /// diff --git a/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs b/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs index 381f1712afc..0154dd3b9ce 100644 --- a/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs @@ -531,11 +531,6 @@ public static bool IsIgnoredByMigrations([NotNull] this IEntityType entityType) return false; } - if (entityType.FindAnnotation(RelationalAnnotationNames.QueryableFunctionResultType) != null) - { - return true; - } - var viewDefinition = entityType.FindAnnotation(RelationalAnnotationNames.ViewDefinition); if (viewDefinition?.Value != null) { diff --git a/src/EFCore.Relational/Extensions/RelationalModelBuilderExtensions.cs b/src/EFCore.Relational/Extensions/RelationalModelBuilderExtensions.cs index 1e294b4eee2..82a9fc9b11b 100644 --- a/src/EFCore.Relational/Extensions/RelationalModelBuilderExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalModelBuilderExtensions.cs @@ -30,13 +30,11 @@ public static SequenceBuilder HasSequence( [NotNull] this ModelBuilder modelBuilder, [NotNull] string name, [CanBeNull] string schema = null) - { - Check.NotNull(modelBuilder, nameof(modelBuilder)); - Check.NotEmpty(name, nameof(name)); - Check.NullButNotEmpty(schema, nameof(schema)); - - return new SequenceBuilder(GetOrAddSequence(modelBuilder, name, schema)); - } + => new SequenceBuilder(HasSequence( + Check.NotNull(modelBuilder, nameof(modelBuilder)).Model, + name, + schema, + ConfigurationSource.Explicit)); /// /// Configures a database sequence when targeting a relational database. @@ -65,9 +63,6 @@ public static ModelBuilder HasSequence( [CanBeNull] string schema, [NotNull] Action builderAction) { - Check.NotNull(modelBuilder, nameof(modelBuilder)); - Check.NotEmpty(name, nameof(name)); - Check.NullButNotEmpty(schema, nameof(schema)); Check.NotNull(builderAction, nameof(builderAction)); builderAction(HasSequence(modelBuilder, name, schema)); @@ -89,12 +84,10 @@ public static SequenceBuilder HasSequence( [NotNull] string name, [CanBeNull] string schema = null) { - Check.NotNull(clrType, nameof(clrType)); Check.NotNull(modelBuilder, nameof(modelBuilder)); - Check.NotEmpty(name, nameof(name)); - Check.NullButNotEmpty(schema, nameof(schema)); + Check.NotNull(clrType, nameof(clrType)); - var sequence = GetOrAddSequence(modelBuilder, name, schema); + var sequence = HasSequence(modelBuilder.Model, name, schema, ConfigurationSource.Explicit); sequence.ClrType = clrType; return new SequenceBuilder(sequence); @@ -131,10 +124,6 @@ public static ModelBuilder HasSequence( [CanBeNull] string schema, [NotNull] Action builderAction) { - Check.NotNull(clrType, nameof(clrType)); - Check.NotNull(modelBuilder, nameof(modelBuilder)); - Check.NotEmpty(name, nameof(name)); - Check.NullButNotEmpty(schema, nameof(schema)); Check.NotNull(builderAction, nameof(builderAction)); builderAction(HasSequence(modelBuilder, clrType, name, schema)); @@ -156,10 +145,8 @@ public static SequenceBuilder HasSequence( [CanBeNull] string schema = null) { Check.NotNull(modelBuilder, nameof(modelBuilder)); - Check.NotEmpty(name, nameof(name)); - Check.NullButNotEmpty(schema, nameof(schema)); - var sequence = GetOrAddSequence(modelBuilder, name, schema); + var sequence = HasSequence(modelBuilder.Model, name, schema, ConfigurationSource.Explicit); sequence.ClrType = typeof(T); return new SequenceBuilder(sequence); @@ -194,9 +181,6 @@ public static ModelBuilder HasSequence( [CanBeNull] string schema, [NotNull] Action builderAction) { - Check.NotNull(modelBuilder, nameof(modelBuilder)); - Check.NotEmpty(name, nameof(name)); - Check.NullButNotEmpty(schema, nameof(schema)); Check.NotNull(builderAction, nameof(builderAction)); builderAction(HasSequence(modelBuilder, name, schema)); @@ -204,32 +188,6 @@ public static ModelBuilder HasSequence( return modelBuilder; } - private static IMutableSequence GetOrAddSequence(ModelBuilder modelBuilder, string name, string schema) - { - var sequence = modelBuilder.Model.FindSequence(name, schema); - if (sequence != null) - { - ((Sequence)sequence).UpdateConfigurationSource(ConfigurationSource.Explicit); - return sequence; - } - - return modelBuilder.Model.AddSequence(name, schema); - } - - private static IConventionSequence GetOrAddSequence( - IConventionModelBuilder modelBuilder, string name, string schema, bool fromDataAnnotation) - { - var sequence = modelBuilder.Metadata.FindSequence(name, schema); - if (sequence != null) - { - ((Sequence)sequence).UpdateConfigurationSource( - fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - return sequence; - } - - return modelBuilder.Metadata.AddSequence(name, schema); - } - /// /// Configures a database sequence when targeting a relational database. /// @@ -243,12 +201,26 @@ public static IConventionSequenceBuilder HasSequence( [NotNull] string name, [CanBeNull] string schema = null, bool fromDataAnnotation = false) + => HasSequence( + (IMutableModel)Check.NotNull(modelBuilder, nameof(modelBuilder)).Metadata, + name, + schema, + fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention).Builder; + + private static Sequence HasSequence( + IMutableModel model, string name, string schema, ConfigurationSource configurationSource) { - Check.NotNull(modelBuilder, nameof(modelBuilder)); Check.NotEmpty(name, nameof(name)); Check.NullButNotEmpty(schema, nameof(schema)); - return new SequenceBuilder((IMutableSequence)GetOrAddSequence(modelBuilder, name, schema, fromDataAnnotation)); + var sequence = Sequence.FindSequence(model, name, schema); + if (sequence != null) + { + sequence.UpdateConfigurationSource(configurationSource); + return sequence; + } + + return Sequence.AddSequence(model, name, schema, configurationSource); } /// @@ -312,8 +284,6 @@ public static ModelBuilder HasDbFunction( [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)); @@ -347,7 +317,7 @@ public static IConventionDbFunctionBuilder HasDbFunction( fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); } - return new DbFunctionBuilder((IMutableDbFunction)dbFunction); + return dbFunction.Builder; } /// diff --git a/src/EFCore.Relational/Extensions/RelationalModelExtensions.cs b/src/EFCore.Relational/Extensions/RelationalModelExtensions.cs index 7bfbf1ca688..85f15411aaa 100644 --- a/src/EFCore.Relational/Extensions/RelationalModelExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalModelExtensions.cs @@ -263,7 +263,7 @@ public static IConventionDbFunction FindDbFunction([NotNull] this IConventionMod => (IConventionDbFunction)((IModel)model).FindDbFunction(method); /// - /// Finds a that is mapped to the method represented by the given name. + /// Finds an that is mapped to the method represented by the given name. /// /// The model to find the function in. /// The model name of the function. @@ -274,7 +274,7 @@ public static IDbFunction FindDbFunction([NotNull] this IModel model, [NotNull] Check.NotNull(name, nameof(name))); /// - /// Finds a that is mapped to the method represented by the given name. + /// Finds an that is mapped to the method represented by the given name. /// /// The model to find the function in. /// The model name of the function. @@ -283,7 +283,7 @@ public static IMutableDbFunction FindDbFunction([NotNull] this IMutableModel mod => (IMutableDbFunction)((IModel)model).FindDbFunction(name); /// - /// Finds a that is mapped to the method represented by the given name. + /// Finds an that is mapped to the method represented by the given name. /// /// The model to find the function in. /// The model name of the function. @@ -292,24 +292,22 @@ public static IConventionDbFunction FindDbFunction([NotNull] this IConventionMod => (IConventionDbFunction)((IModel)model).FindDbFunction(name); /// - /// Either returns the existing mapped to the given method - /// or creates a new function mapped to the method. + /// Creates an mapped to the given method. /// /// The model to add the function to. /// The for the method that is mapped to the function. - /// The . + /// The new . public static IMutableDbFunction AddDbFunction([NotNull] this IMutableModel model, [NotNull] MethodInfo methodInfo) => DbFunction.AddDbFunction( model, Check.NotNull(methodInfo, nameof(methodInfo)), ConfigurationSource.Explicit); /// - /// Either returns the existing mapped to the given method - /// or creates a new function mapped to the method. + /// Creates an mapped to the given method. /// /// The model to add the function to. /// The for the method that is mapped to the function. /// Indicates whether the configuration was specified using a data annotation. - /// The . + /// The new . public static IConventionDbFunction AddDbFunction( [NotNull] this IConventionModel model, [NotNull] MethodInfo methodInfo, bool fromDataAnnotation = false) => DbFunction.AddDbFunction( @@ -317,28 +315,36 @@ public static IConventionDbFunction AddDbFunction( fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); /// - /// Either returns the existing mapped to the given method - /// or creates a new function mapped to the method. + /// Creates an . /// /// The model to add the function to. /// The model name of the function. - /// The . - public static IMutableDbFunction AddDbFunction([NotNull] this IMutableModel model, [NotNull] string name) + /// The function return type. + /// The new . + public static IMutableDbFunction AddDbFunction( + [NotNull] this IMutableModel model, + [NotNull] string name, + [NotNull] Type returnType) => DbFunction.AddDbFunction( - model, Check.NotNull(name, nameof(name)), ConfigurationSource.Explicit); + model, Check.NotNull(name, nameof(name)), returnType, ConfigurationSource.Explicit); /// - /// Either returns the existing mapped to the given method - /// or creates a new function mapped to the method. + /// Creates an . /// /// The model to add the function to. /// The model name of the function. + /// The function return type. /// Indicates whether the configuration was specified using a data annotation. - /// The . + /// The new . public static IConventionDbFunction AddDbFunction( - [NotNull] this IConventionModel model, [NotNull] string name, bool fromDataAnnotation = false) + [NotNull] this IConventionModel model, + [NotNull] string name, + [NotNull] Type returnType, + bool fromDataAnnotation = false) => DbFunction.AddDbFunction( - (IMutableModel)model, Check.NotNull(name, nameof(name)), + (IMutableModel)model, + Check.NotNull(name, nameof(name)), + returnType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); /// diff --git a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs index 8fc57d13606..09e1620f9c3 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs @@ -80,8 +80,8 @@ protected virtual void ValidateDbFunctions( RelationalStrings.DbFunctionNameEmpty(methodInfo.DisplayName())); } - if (dbFunction.TypeMapping == null && - !(dbFunction.IsQueryable && model.FindEntityType(dbFunction.MethodInfo.ReturnType.GetGenericArguments()[0]) != null)) + if (dbFunction.TypeMapping == null + && dbFunction.QueryableEntityType == null) { throw new InvalidOperationException( RelationalStrings.DbFunctionInvalidReturnType( diff --git a/src/EFCore.Relational/Metadata/Builders/DbFunctionBuilder.cs b/src/EFCore.Relational/Metadata/Builders/DbFunctionBuilder.cs index 69f24900cdb..312d82e25a9 100644 --- a/src/EFCore.Relational/Metadata/Builders/DbFunctionBuilder.cs +++ b/src/EFCore.Relational/Metadata/Builders/DbFunctionBuilder.cs @@ -4,14 +4,11 @@ using System; using System.Collections.Generic; using System.ComponentModel; -using System.Linq; +using System.Diagnostics; using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; -using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Utilities; namespace Microsoft.EntityFrameworkCore.Metadata.Builders @@ -19,10 +16,8 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Builders /// /// Provides a simple API for configuring a . /// - public class DbFunctionBuilder : IConventionDbFunctionBuilder + public class DbFunctionBuilder : IInfrastructure { - private readonly DbFunction _function; - /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -34,13 +29,23 @@ public DbFunctionBuilder([NotNull] IMutableDbFunction function) { Check.NotNull(function, nameof(function)); - _function = (DbFunction)function; + Builder = ((DbFunction)function).Builder; + } + + private InternalDbFunctionBuilder Builder { [DebuggerStepThrough] get; } + + /// + IConventionDbFunctionBuilder IInfrastructure.Instance + { + [DebuggerStepThrough] get => Builder; } /// /// The function being configured. /// - public virtual IMutableDbFunction Metadata => _function; +#pragma warning disable EF1001 // Internal EF Core API usage. + public virtual IMutableDbFunction Metadata => Builder.Metadata; +#pragma warning restore EF1001 // Internal EF Core API usage. /// /// Sets the name of the database function. @@ -49,30 +54,11 @@ public DbFunctionBuilder([NotNull] IMutableDbFunction function) /// The same builder instance so that multiple configuration calls can be chained. public virtual DbFunctionBuilder HasName([NotNull] string name) { - Check.NotEmpty(name, nameof(name)); - - _function.Name = name; + Builder.HasName(name, ConfigurationSource.Explicit); return this; } - /// - IConventionDbFunctionBuilder IConventionDbFunctionBuilder.HasName(string name, bool fromDataAnnotation) - { - if (((IConventionDbFunctionBuilder)this).CanSetName(name, fromDataAnnotation)) - { - ((IConventionDbFunction)_function).SetName(name, fromDataAnnotation); - return this; - } - - return null; - } - - /// - bool IConventionDbFunctionBuilder.CanSetName(string name, bool fromDataAnnotation) - => Overrides(fromDataAnnotation, _function.GetNameConfigurationSource()) - || _function.Name == name; - /// /// Sets the schema of the database function. /// @@ -80,28 +66,11 @@ bool IConventionDbFunctionBuilder.CanSetName(string name, bool fromDataAnnotatio /// The same builder instance so that multiple configuration calls can be chained. public virtual DbFunctionBuilder HasSchema([CanBeNull] string schema) { - _function.Schema = schema; + Builder.HasSchema(schema, ConfigurationSource.Explicit); return this; } - /// - IConventionDbFunctionBuilder IConventionDbFunctionBuilder.HasSchema(string schema, bool fromDataAnnotation) - { - if (((IConventionDbFunctionBuilder)this).CanSetSchema(schema, fromDataAnnotation)) - { - ((IConventionDbFunction)_function).SetSchema(schema, fromDataAnnotation); - return this; - } - - return null; - } - - /// - bool IConventionDbFunctionBuilder.CanSetSchema(string schema, bool fromDataAnnotation) - => Overrides(fromDataAnnotation, _function.GetSchemaConfigurationSource()) - || _function.Schema == schema; - /// /// Sets the store type of the database function. /// @@ -109,46 +78,11 @@ bool IConventionDbFunctionBuilder.CanSetSchema(string schema, bool fromDataAnnot /// The same builder instance so that multiple configuration calls can be chained. public virtual DbFunctionBuilder HasStoreType([CanBeNull] string storeType) { - _function.StoreType = storeType; + Builder.HasStoreType(storeType, ConfigurationSource.Explicit); return this; } - /// - IConventionDbFunctionBuilder IConventionDbFunctionBuilder.HasStoreType(string storeType, bool fromDataAnnotation) - { - if (((IConventionDbFunctionBuilder)this).CanSetStoreType(storeType, fromDataAnnotation)) - { - ((IConventionDbFunction)_function).SetStoreType(storeType, fromDataAnnotation); - return this; - } - - return null; - } - - /// - bool IConventionDbFunctionBuilder.CanSetStoreType(string storeType, bool fromDataAnnotation) - => Overrides(fromDataAnnotation, _function.GetStoreTypeConfigurationSource()) - || _function.StoreType == storeType; - - /// - IConventionDbFunctionBuilder IConventionDbFunctionBuilder.HasTypeMapping( - RelationalTypeMapping returnTypeMapping, bool fromDataAnnotation) - { - if (((IConventionDbFunctionBuilder)this).CanSetTypeMapping(returnTypeMapping, fromDataAnnotation)) - { - ((IConventionDbFunction)_function).SetTypeMapping(returnTypeMapping, fromDataAnnotation); - return this; - } - - return null; - } - - /// - bool IConventionDbFunctionBuilder.CanSetTypeMapping(RelationalTypeMapping returnTypeMapping, bool fromDataAnnotation) - => Overrides(fromDataAnnotation, _function.GetTypeMappingConfigurationSource()) - || _function.TypeMapping == returnTypeMapping; - /// /// /// Sets a callback that will be invoked to perform custom translation of this @@ -164,62 +98,21 @@ bool IConventionDbFunctionBuilder.CanSetTypeMapping(RelationalTypeMapping return /// The same builder instance so that multiple configuration calls can be chained. public virtual DbFunctionBuilder HasTranslation([NotNull] Func, SqlExpression> translation) { - Check.NotNull(translation, nameof(translation)); - - _function.Translation = translation; + Builder.HasTranslation(translation, ConfigurationSource.Explicit); return this; } - /// - IConventionDbFunction IConventionDbFunctionBuilder.Metadata => _function; - - /// - IConventionDbFunctionBuilder IConventionDbFunctionBuilder.HasTranslation( - Func, SqlExpression> translation, bool fromDataAnnotation) - { - if (((IConventionDbFunctionBuilder)this).CanSetTranslation(translation, fromDataAnnotation)) - { - ((IConventionDbFunction)_function).SetTranslation(translation, fromDataAnnotation); - return this; - } - - return null; - } - /// - /// Creates a for a parameter with the given name. + /// Returns an object that can be used to configure a parameter with the given name. + /// If no parameter with the given name exists, then a new parameter will be added. /// /// The parameter name. /// The builder to use for further parameter configuration. public virtual DbFunctionParameterBuilder HasParameter([NotNull] string name) - { - return new DbFunctionParameterBuilder((DbFunctionParameter)FindParameter(name)); - } - - private IDbFunctionParameter FindParameter(string name) - { - var parameter = Metadata.Parameters.SingleOrDefault( - funcParam => string.Compare(funcParam.Name, name, StringComparison.OrdinalIgnoreCase) == 0); - - if (parameter == null) - { - throw new ArgumentException( - RelationalStrings.DbFunctionInvalidParameterName(name, Metadata.MethodInfo.DisplayName())); - } - - return parameter; - } - - /// - bool IConventionDbFunctionBuilder.CanSetTranslation( - Func, SqlExpression> translation, bool fromDataAnnotation) - => Overrides(fromDataAnnotation, _function.GetTranslationConfigurationSource()) - || _function.Translation == translation; - - private bool Overrides(bool fromDataAnnotation, ConfigurationSource? configurationSource) - => (fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention) - .Overrides(configurationSource); +#pragma warning disable EF1001 // Internal EF Core API usage. + => new DbFunctionParameterBuilder(Builder.HasParameter(name, ConfigurationSource.Explicit).Metadata); +#pragma warning restore EF1001 // Internal EF Core API usage. #region Hidden System.Object members diff --git a/src/EFCore.Relational/Metadata/Builders/DbFunctionParameterBuilder.cs b/src/EFCore.Relational/Metadata/Builders/DbFunctionParameterBuilder.cs index 8e3f10434ef..f4975f3c187 100644 --- a/src/EFCore.Relational/Metadata/Builders/DbFunctionParameterBuilder.cs +++ b/src/EFCore.Relational/Metadata/Builders/DbFunctionParameterBuilder.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.ComponentModel; +using System.Diagnostics; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata.Internal; @@ -19,10 +20,8 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Builders /// and it is not designed to be directly constructed in your application code. /// /// - public class DbFunctionParameterBuilder : IConventionDbFunctionParameterBuilder + public class DbFunctionParameterBuilder : IInfrastructure { - private readonly DbFunctionParameter _parameter; - /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -34,16 +33,24 @@ public DbFunctionParameterBuilder([NotNull] IMutableDbFunctionParameter paramete { Check.NotNull(parameter, nameof(parameter)); - _parameter = (DbFunctionParameter)parameter; + Builder = ((DbFunctionParameter)parameter).Builder; + } + + private InternalDbFunctionParameterBuilder Builder { [DebuggerStepThrough] get; } + + /// + IConventionDbFunctionParameterBuilder IInfrastructure.Instance + { + [DebuggerStepThrough] + get => Builder; } /// /// The function parameter metadata that is being built. /// - public virtual IMutableDbFunctionParameter Metadata => _parameter; - - /// - IConventionDbFunctionParameter IConventionDbFunctionParameterBuilder.Metadata => _parameter; +#pragma warning disable EF1001 // Internal EF Core API usage. + public virtual IMutableDbFunctionParameter Metadata => Builder.Metadata; +#pragma warning restore EF1001 // Internal EF Core API usage. /// /// Sets the store type of the function parameter in the database. @@ -52,50 +59,11 @@ public DbFunctionParameterBuilder([NotNull] IMutableDbFunctionParameter paramete /// The same builder instance so that further configuration calls can be chained. public virtual DbFunctionParameterBuilder HasStoreType([CanBeNull] string storeType) { - _parameter.StoreType = storeType; + Builder.HasStoreType(storeType, ConfigurationSource.Explicit); return this; } - /// - IConventionDbFunctionParameterBuilder IConventionDbFunctionParameterBuilder.HasStoreType(string storeType, bool fromDataAnnotation) - { - if (((IConventionDbFunctionParameterBuilder)this).CanSetStoreType(storeType, fromDataAnnotation)) - { - ((IConventionDbFunctionParameter)_parameter).SetStoreType(storeType, fromDataAnnotation); - return this; - } - - return null; - } - - /// - bool IConventionDbFunctionParameterBuilder.CanSetStoreType(string storeType, bool fromDataAnnotation) - => Overrides(fromDataAnnotation, _parameter.GetStoreTypeConfigurationSource()) - || _parameter.StoreType == storeType; - - /// - IConventionDbFunctionParameterBuilder IConventionDbFunctionParameterBuilder.HasTypeMapping( - RelationalTypeMapping typeMapping, bool fromDataAnnotation) - { - if (((IConventionDbFunctionParameterBuilder)this).CanSetTypeMapping(typeMapping, fromDataAnnotation)) - { - ((IConventionDbFunctionParameter)_parameter).SetTypeMapping(typeMapping, fromDataAnnotation); - return this; - } - - return null; - } - - /// - bool IConventionDbFunctionParameterBuilder.CanSetTypeMapping(RelationalTypeMapping typeMapping, bool fromDataAnnotation) - => Overrides(fromDataAnnotation, _parameter.GetTypeMappingConfigurationSource()) - || _parameter.TypeMapping == typeMapping; - - private bool Overrides(bool fromDataAnnotation, ConfigurationSource? configurationSource) - => (fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention) - .Overrides(configurationSource); - #region Hidden System.Object members /// diff --git a/src/EFCore.Relational/Metadata/Builders/IConventionDbFunctionBuilder.cs b/src/EFCore.Relational/Metadata/Builders/IConventionDbFunctionBuilder.cs index 59288e58cdd..07f43564f4d 100644 --- a/src/EFCore.Relational/Metadata/Builders/IConventionDbFunctionBuilder.cs +++ b/src/EFCore.Relational/Metadata/Builders/IConventionDbFunctionBuilder.cs @@ -12,12 +12,12 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Builders /// /// Provides a simple API for configuring a . /// - public interface IConventionDbFunctionBuilder + public interface IConventionDbFunctionBuilder : IConventionAnnotatableBuilder { /// /// The function being configured. /// - IConventionDbFunction Metadata { get; } + new IConventionDbFunction Metadata { get; } /// /// Sets the name of the database function. @@ -123,5 +123,13 @@ IConventionDbFunctionBuilder HasTranslation( /// true if the given translation can be set for the database function. bool CanSetTranslation( [CanBeNull] Func, SqlExpression> translation, bool fromDataAnnotation = false); + + /// + /// Returns an object that can be used to configure a parameter with the given name. + /// + /// The parameter name. + /// Indicates whether the configuration was specified using a data annotation. + /// The builder to use for further parameter configuration. + IConventionDbFunctionParameterBuilder HasParameter([NotNull] string name, bool fromDataAnnotation = false); } } diff --git a/src/EFCore.Relational/Metadata/Builders/IConventionDbFunctionParameterBuilder.cs b/src/EFCore.Relational/Metadata/Builders/IConventionDbFunctionParameterBuilder.cs index 8c3a04c8783..99599463fa6 100644 --- a/src/EFCore.Relational/Metadata/Builders/IConventionDbFunctionParameterBuilder.cs +++ b/src/EFCore.Relational/Metadata/Builders/IConventionDbFunctionParameterBuilder.cs @@ -4,17 +4,17 @@ using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Storage; -namespace Microsoft.EntityFrameworkCore.Metadata +namespace Microsoft.EntityFrameworkCore.Metadata.Builders { /// /// Provides a simple API for configuring a . /// - public interface IConventionDbFunctionParameterBuilder + public interface IConventionDbFunctionParameterBuilder : IConventionAnnotatableBuilder { /// /// The function parameter metadata that is being built. /// - IConventionDbFunctionParameter Metadata { get; } + new IConventionDbFunctionParameter Metadata { get; } /// /// Sets the store type of the function parameter in the database. diff --git a/src/EFCore.Relational/Metadata/Builders/IConventionSequenceBuilder.cs b/src/EFCore.Relational/Metadata/Builders/IConventionSequenceBuilder.cs index a31ebfd0a05..16404c6b4b4 100644 --- a/src/EFCore.Relational/Metadata/Builders/IConventionSequenceBuilder.cs +++ b/src/EFCore.Relational/Metadata/Builders/IConventionSequenceBuilder.cs @@ -9,12 +9,12 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Builders /// /// Provides a simple API for configuring a . /// - public interface IConventionSequenceBuilder + public interface IConventionSequenceBuilder : IConventionAnnotatableBuilder { /// /// The sequence being configured. /// - IConventionSequence Metadata { get; } + new IConventionSequence Metadata { get; } /// /// Sets the type of values returned by the sequence. diff --git a/src/EFCore.Relational/Metadata/Builders/SequenceBuilder.cs b/src/EFCore.Relational/Metadata/Builders/SequenceBuilder.cs index 053306000c7..64b9a4a5dbc 100644 --- a/src/EFCore.Relational/Metadata/Builders/SequenceBuilder.cs +++ b/src/EFCore.Relational/Metadata/Builders/SequenceBuilder.cs @@ -3,8 +3,10 @@ using System; using System.ComponentModel; +using System.Diagnostics; using System.Linq; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Utilities; @@ -13,10 +15,8 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Builders /// /// Provides a simple API for configuring a . /// - public class SequenceBuilder : IConventionSequenceBuilder + public class SequenceBuilder : IInfrastructure { - private readonly Sequence _sequence; - /// /// Creates a new builder for the given . /// @@ -25,32 +25,24 @@ public SequenceBuilder([NotNull] IMutableSequence sequence) { Check.NotNull(sequence, nameof(sequence)); - _sequence = (Sequence)sequence; + Builder = ((Sequence)sequence).Builder; } - /// - /// The sequence. - /// - public virtual IMutableSequence Metadata => _sequence; + private InternalSequenceBuilder Builder { [DebuggerStepThrough] get; } /// - IConventionSequenceBuilder IConventionSequenceBuilder.HasType(Type type, bool fromDataAnnotation) + IConventionSequenceBuilder IInfrastructure.Instance { - if (Overrides(fromDataAnnotation, _sequence.GetClrTypeConfigurationSource()) - || _sequence.ClrType == type) - { - ((IConventionSequence)_sequence).SetClrType(type, fromDataAnnotation); - return this; - } - - return null; + [DebuggerStepThrough] + get => Builder; } - /// - bool IConventionSequenceBuilder.CanSetType(Type type, bool fromDataAnnotation) - => (type == null || Sequence.SupportedTypes.Contains(type)) - && (Overrides(fromDataAnnotation, _sequence.GetClrTypeConfigurationSource()) - || _sequence.ClrType == type); + /// + /// The sequence. + /// +#pragma warning disable EF1001 // Internal EF Core API usage. + public virtual IMutableSequence Metadata => Builder.Metadata; +#pragma warning restore EF1001 // Internal EF Core API usage. /// /// Sets the to increment by the given amount when generating each next value. @@ -59,28 +51,11 @@ bool IConventionSequenceBuilder.CanSetType(Type type, bool fromDataAnnotation) /// The same builder so that multiple calls can be chained. public virtual SequenceBuilder IncrementsBy(int increment) { - _sequence.IncrementBy = increment; + Builder.IncrementsBy(increment, ConfigurationSource.Explicit); return this; } - /// - IConventionSequenceBuilder IConventionSequenceBuilder.IncrementsBy(int? increment, bool fromDataAnnotation) - { - if (((IConventionSequenceBuilder)this).CanSetIncrementsBy(increment, fromDataAnnotation)) - { - ((IConventionSequence)_sequence).SetIncrementBy(increment, fromDataAnnotation); - return this; - } - - return null; - } - - /// - bool IConventionSequenceBuilder.CanSetIncrementsBy(int? increment, bool fromDataAnnotation) - => Overrides(fromDataAnnotation, _sequence.GetIncrementByConfigurationSource()) - || _sequence.IncrementBy == increment; - /// /// Sets the to start at the given value. /// @@ -88,28 +63,11 @@ bool IConventionSequenceBuilder.CanSetIncrementsBy(int? increment, bool fromData /// The same builder so that multiple calls can be chained. public virtual SequenceBuilder StartsAt(long startValue) { - _sequence.StartValue = startValue; + Builder.StartsAt(startValue, ConfigurationSource.Explicit); return this; } - /// - IConventionSequenceBuilder IConventionSequenceBuilder.StartsAt(long? startValue, bool fromDataAnnotation) - { - if (((IConventionSequenceBuilder)this).CanSetStartsAt(startValue, fromDataAnnotation)) - { - ((IConventionSequence)_sequence).SetStartValue(startValue, fromDataAnnotation); - return this; - } - - return null; - } - - /// - bool IConventionSequenceBuilder.CanSetStartsAt(long? startValue, bool fromDataAnnotation) - => Overrides(fromDataAnnotation, _sequence.GetStartValueConfigurationSource()) - || _sequence.StartValue == startValue; - /// /// Sets the maximum value for the . /// @@ -117,28 +75,11 @@ bool IConventionSequenceBuilder.CanSetStartsAt(long? startValue, bool fromDataAn /// The same builder so that multiple calls can be chained. public virtual SequenceBuilder HasMax(long maximum) { - _sequence.MaxValue = maximum; + Builder.HasMax(maximum, ConfigurationSource.Explicit); return this; } - /// - IConventionSequenceBuilder IConventionSequenceBuilder.HasMax(long? maximum, bool fromDataAnnotation) - { - if (((IConventionSequenceBuilder)this).CanSetMax(maximum, fromDataAnnotation)) - { - ((IConventionSequence)_sequence).SetMaxValue(maximum, fromDataAnnotation); - return this; - } - - return null; - } - - /// - bool IConventionSequenceBuilder.CanSetMax(long? maximum, bool fromDataAnnotation) - => Overrides(fromDataAnnotation, _sequence.GetMaxValueConfigurationSource()) - || _sequence.MaxValue == maximum; - /// /// Sets the minimum value for the . /// @@ -146,28 +87,11 @@ bool IConventionSequenceBuilder.CanSetMax(long? maximum, bool fromDataAnnotation /// The same builder so that multiple calls can be chained. public virtual SequenceBuilder HasMin(long minimum) { - _sequence.MinValue = minimum; + Builder.HasMin(minimum, ConfigurationSource.Explicit); return this; } - /// - IConventionSequenceBuilder IConventionSequenceBuilder.HasMin(long? minimum, bool fromDataAnnotation) - { - if (((IConventionSequenceBuilder)this).CanSetMin(minimum, fromDataAnnotation)) - { - ((IConventionSequence)_sequence).SetMinValue(minimum, fromDataAnnotation); - return this; - } - - return null; - } - - /// - bool IConventionSequenceBuilder.CanSetMin(long? minimum, bool fromDataAnnotation) - => Overrides(fromDataAnnotation, _sequence.GetMinValueConfigurationSource()) - || _sequence.MinValue == minimum; - /// /// Sets whether or not the sequence will start again from the beginning once /// the maximum value is reached. @@ -176,34 +100,11 @@ bool IConventionSequenceBuilder.CanSetMin(long? minimum, bool fromDataAnnotation /// The same builder so that multiple calls can be chained. public virtual SequenceBuilder IsCyclic(bool cyclic = true) { - _sequence.IsCyclic = cyclic; + Builder.IsCyclic(cyclic, ConfigurationSource.Explicit); return this; } - /// - IConventionSequenceBuilder IConventionSequenceBuilder.IsCyclic(bool? cyclic, bool fromDataAnnotation) - { - if (((IConventionSequenceBuilder)this).CanSetIsCyclic(cyclic, fromDataAnnotation)) - { - ((IConventionSequence)_sequence).SetIsCyclic(cyclic, fromDataAnnotation); - return this; - } - - return null; - } - - /// - bool IConventionSequenceBuilder.CanSetIsCyclic(bool? cyclic, bool fromDataAnnotation) - => Overrides(fromDataAnnotation, _sequence.GetIsCyclicConfigurationSource()) - || _sequence.IsCyclic == cyclic; - - private bool Overrides(bool fromDataAnnotation, ConfigurationSource? configurationSource) - => (fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention) - .Overrides(configurationSource); - - IConventionSequence IConventionSequenceBuilder.Metadata => (IConventionSequence)Metadata; - #region Hidden System.Object members /// diff --git a/src/EFCore.Relational/Metadata/Conventions/DbFunctionTypeMappingConvention.cs b/src/EFCore.Relational/Metadata/Conventions/DbFunctionTypeMappingConvention.cs index 96fb908c7f4..1016ec643b6 100644 --- a/src/EFCore.Relational/Metadata/Conventions/DbFunctionTypeMappingConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/DbFunctionTypeMappingConvention.cs @@ -41,20 +41,21 @@ public virtual void ProcessModelFinalizing( foreach (var dbFunction in modelBuilder.Metadata.GetDbFunctions()) { - var typeMapping = !string.IsNullOrEmpty(dbFunction.StoreType) - ? _relationalTypeMappingSource.FindMapping(dbFunction.StoreType) - : _relationalTypeMappingSource.FindMapping(dbFunction.MethodInfo.ReturnType); - - dbFunction.Builder.HasTypeMapping(typeMapping); - foreach (var parameter in dbFunction.Parameters) { - typeMapping = !string.IsNullOrEmpty(parameter.StoreType) + parameter.Builder.HasTypeMapping(!string.IsNullOrEmpty(parameter.StoreType) ? _relationalTypeMappingSource.FindMapping(parameter.StoreType) - : _relationalTypeMappingSource.FindMapping(parameter.ClrType); + : _relationalTypeMappingSource.FindMapping(parameter.ClrType)); + } - parameter.Builder.HasTypeMapping(typeMapping); + if (dbFunction.IsQueryable) + { + continue; } + + dbFunction.Builder.HasTypeMapping(!string.IsNullOrEmpty(dbFunction.StoreType) + ? _relationalTypeMappingSource.FindMapping(dbFunction.StoreType) + : _relationalTypeMappingSource.FindMapping(dbFunction.ReturnType)); } } } diff --git a/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs b/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs index 1bca898bf86..4dcccb1579f 100644 --- a/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs +++ b/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs @@ -88,6 +88,11 @@ public override ConventionSet CreateConventionSet() var dbFunctionAttributeConvention = new RelationalDbFunctionAttributeConvention(Dependencies, RelationalDependencies); conventionSet.ModelInitializedConventions.Add(dbFunctionAttributeConvention); + // ModelCleanupConvention would remove the entity types added by QueryableDbFunctionConvention #15898 + ConventionSet.AddAfter( + conventionSet.ModelFinalizingConventions, + new QueryableDbFunctionConvention(Dependencies, RelationalDependencies), + typeof(ModelCleanupConvention)); conventionSet.ModelFinalizingConventions.Add(dbFunctionAttributeConvention); conventionSet.ModelFinalizingConventions.Add(tableNameFromDbSetConvention); conventionSet.ModelFinalizingConventions.Add(storeGenerationConvention); diff --git a/src/EFCore.Relational/Metadata/Conventions/QueryableDbFunctionConvention.cs b/src/EFCore.Relational/Metadata/Conventions/QueryableDbFunctionConvention.cs new file mode 100644 index 00000000000..72f5ed325bf --- /dev/null +++ b/src/EFCore.Relational/Metadata/Conventions/QueryableDbFunctionConvention.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 System.Linq; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; + +namespace Microsoft.EntityFrameworkCore.Metadata.Conventions +{ + /// + /// A convention that configures the entity type to which a queryable function is mapped. + /// + public class QueryableDbFunctionConvention : IModelFinalizingConvention + { + /// + /// Creates a new instance of . + /// + /// Parameter object containing dependencies for this convention. + /// Parameter object containing relational dependencies for this convention. + public QueryableDbFunctionConvention( + [NotNull] ProviderConventionSetBuilderDependencies dependencies, + [NotNull] RelationalConventionSetBuilderDependencies relationalDependencies) + { + Dependencies = dependencies; + } + + /// + /// Parameter object containing service dependencies. + /// + protected virtual ProviderConventionSetBuilderDependencies Dependencies { get; } + + /// + public virtual void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext context) + { + foreach (var function in modelBuilder.Metadata.GetDbFunctions()) + { + ProcessDbFunctionAdded(function.Builder, context); + } + } + + /// + /// Called when an is added to the model. + /// + /// The builder for the . + /// Additional information associated with convention execution. + protected virtual void ProcessDbFunctionAdded( + [NotNull] IConventionDbFunctionBuilder dbFunctionBuilder, [NotNull] IConventionContext context) + { + var function = dbFunctionBuilder.Metadata; + if (!function.IsQueryable) + { + return; + } + + var elementType = function.ReturnType.TryGetElementType(typeof(IQueryable<>)); + if (!elementType.IsValidEntityType()) + { + throw new InvalidOperationException(RelationalStrings.DbFunctionInvalidIQueryableReturnType( + function.Name, function.ReturnType.ShortDisplayName())); + } + + var model = function.Model; + IConventionEntityTypeBuilder entityTypeBuilder; + var entityType = model.FindEntityType(elementType); + if (entityType?.IsOwned() == true || model.IsOwned(elementType)) + { + throw new InvalidOperationException(RelationalStrings.DbFunctionInvalidIQueryableOwnedReturnType( + function.Name, function.ReturnType.ShortDisplayName())); + } + + if (entityType != null) + { + entityTypeBuilder = entityType.Builder; + } + else + { + entityTypeBuilder = dbFunctionBuilder.ModelBuilder.Entity(elementType); + if (entityTypeBuilder == null) + { + return; + } + } + + entityTypeBuilder.ToTable(null); + entityTypeBuilder.HasNoKey(); + } + } +} diff --git a/src/EFCore.Relational/Metadata/Conventions/RelationalDbFunctionAttributeConvention.cs b/src/EFCore.Relational/Metadata/Conventions/RelationalDbFunctionAttributeConvention.cs index b6ab6f2cb5d..7865e588e91 100644 --- a/src/EFCore.Relational/Metadata/Conventions/RelationalDbFunctionAttributeConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/RelationalDbFunctionAttributeConvention.cs @@ -71,9 +71,9 @@ public virtual void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, } /// - /// Called when an is added to the model. + /// Called when an is added to the model. /// - /// The builder for the . + /// The builder for the . /// Additional information associated with convention execution. protected virtual void ProcessDbFunctionAdded( [NotNull] IConventionDbFunctionBuilder dbFunctionBuilder, [NotNull] IConventionContext context) diff --git a/src/EFCore.Relational/Metadata/IConventionDbFunction.cs b/src/EFCore.Relational/Metadata/IConventionDbFunction.cs index dbb03463f44..9d22b34fba4 100644 --- a/src/EFCore.Relational/Metadata/IConventionDbFunction.cs +++ b/src/EFCore.Relational/Metadata/IConventionDbFunction.cs @@ -27,9 +27,9 @@ public interface IConventionDbFunction : IConventionAnnotatable, IDbFunction new IConventionDbFunctionBuilder Builder { get; } /// - /// Gets the configuration source for this . + /// Gets the configuration source for this function. /// - /// The configuration source for . + /// The configuration source for this function. ConfigurationSource GetConfigurationSource(); /// diff --git a/src/EFCore.Relational/Metadata/IConventionDbFunctionParameter.cs b/src/EFCore.Relational/Metadata/IConventionDbFunctionParameter.cs index 0621434c77c..0be28d1b609 100644 --- a/src/EFCore.Relational/Metadata/IConventionDbFunctionParameter.cs +++ b/src/EFCore.Relational/Metadata/IConventionDbFunctionParameter.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 Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Storage; namespace Microsoft.EntityFrameworkCore.Metadata @@ -21,6 +22,12 @@ public interface IConventionDbFunctionParameter : IConventionAnnotatable, IDbFun /// new IConventionDbFunctionParameterBuilder Builder { get; } + /// + /// Returns the configuration source for the parameter. + /// + /// The configuration source for the parameter. + ConfigurationSource GetConfigurationSource(); + /// /// Sets the store type of the parameter in the database. /// diff --git a/src/EFCore.Relational/Metadata/IDbFunction.cs b/src/EFCore.Relational/Metadata/IDbFunction.cs index 86696ac7acb..0ed24146fa8 100644 --- a/src/EFCore.Relational/Metadata/IDbFunction.cs +++ b/src/EFCore.Relational/Metadata/IDbFunction.cs @@ -45,11 +45,23 @@ public interface IDbFunction : IAnnotatable /// bool IsQueryable { get; } + /// + /// Gets the entity type returned by this queryable function + /// + IEntityType QueryableEntityType => IsQueryable + ? Model.FindEntityType(ReturnType.GetGenericArguments()[0]) + : null; + /// /// Gets the configured store type string /// string StoreType { get; } + /// + /// Gets the returned CLR type. + /// + Type ReturnType { get; } + /// /// Gets the type mapping for the function's return type /// diff --git a/src/EFCore.Relational/Metadata/Internal/CheckConstraint.cs b/src/EFCore.Relational/Metadata/Internal/CheckConstraint.cs index cddc3d5ae8d..a865e0a1e2a 100644 --- a/src/EFCore.Relational/Metadata/Internal/CheckConstraint.cs +++ b/src/EFCore.Relational/Metadata/Internal/CheckConstraint.cs @@ -153,6 +153,14 @@ public virtual void UpdateConfigurationSource(ConfigurationSource configurationS private static Dictionary GetConstraintsDictionary(IEntityType entityType) => (Dictionary)entityType[RelationalAnnotationNames.CheckConstraints]; + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public override string ToString() => this.ToDebugString(MetadataDebugStringOptions.SingleLineDefault); + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore.Relational/Metadata/Internal/CheckConstraintExtensions.cs b/src/EFCore.Relational/Metadata/Internal/CheckConstraintExtensions.cs new file mode 100644 index 00000000000..aa66959589e --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/CheckConstraintExtensions.cs @@ -0,0 +1,51 @@ +// 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.Text; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Infrastructure; + +namespace Microsoft.EntityFrameworkCore.Metadata.Internal +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static class CheckConstraintExtensions + { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static string ToDebugString( + [NotNull] this ICheckConstraint constraint, + MetadataDebugStringOptions options, + [NotNull] string indent = "") + { + var builder = new StringBuilder(); + + builder + .Append(indent) + .Append("Check: "); + + builder.Append(constraint.Name) + .Append(" \"") + .Append(constraint.Sql) + .Append("\""); + + if ((options & MetadataDebugStringOptions.SingleLine) == 0) + { + if ((options & MetadataDebugStringOptions.IncludeAnnotations) != 0) + { + builder.Append(constraint.AnnotationsToDebugString(indent: indent + " ")); + } + } + + return builder.ToString(); + } + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/DbFunction.cs b/src/EFCore.Relational/Metadata/Internal/DbFunction.cs index 1fdeda8c639..081c59d4f87 100644 --- a/src/EFCore.Relational/Metadata/Internal/DbFunction.cs +++ b/src/EFCore.Relational/Metadata/Internal/DbFunction.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Reflection; using JetBrains.Annotations; @@ -12,6 +13,7 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Utilities; namespace Microsoft.EntityFrameworkCore.Metadata.Internal { @@ -23,7 +25,6 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Internal /// public class DbFunction : ConventionAnnotatable, IMutableDbFunction, IConventionDbFunction { - private readonly IMutableModel _model; private readonly List _parameters; private string _schema; private string _name; @@ -37,6 +38,7 @@ public class DbFunction : ConventionAnnotatable, IMutableDbFunction, IConvention private ConfigurationSource? _storeTypeConfigurationSource; private ConfigurationSource? _typeMappingConfigurationSource; private ConfigurationSource? _translationConfigurationSource; + private IEntityType _queryableEntityType; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -48,6 +50,11 @@ public DbFunction( [NotNull] MethodInfo methodInfo, [NotNull] IMutableModel model, ConfigurationSource configurationSource) + : this(methodInfo.DisplayName(), + methodInfo.ReturnType, + methodInfo.GetParameters().Select(pi => (pi.Name, pi.ParameterType)), + model, + configurationSource) { if (methodInfo.IsGenericMethod) { @@ -63,40 +70,9 @@ public DbFunction( methodInfo.DisplayName(), methodInfo.DeclaringType.ShortDisplayName())); } - if (methodInfo.ReturnType == null - || methodInfo.ReturnType == typeof(void)) - { - throw new ArgumentException( - RelationalStrings.DbFunctionInvalidReturnType( - methodInfo.DisplayName(), methodInfo.ReturnType.ShortDisplayName())); - } - - if (methodInfo.ReturnType.IsGenericType - && methodInfo.ReturnType.GetGenericTypeDefinition() == typeof(IQueryable<>)) - { - IsQueryable = true; - - //todo - if the generic argument is not usuable as an entitytype should we throw here? IE IQueryable - //the built in entitytype will throw is the type is not a class - if (model.FindEntityType(methodInfo.ReturnType.GetGenericArguments()[0]) == null) - { - model.AddEntityType(methodInfo.ReturnType.GetGenericArguments()[0]).SetAnnotation(RelationalAnnotationNames.QueryableFunctionResultType, null); - } - } - MethodInfo = methodInfo; - var parameters = methodInfo.GetParameters(); - - _parameters = parameters - .Select((pi, i) => new DbFunctionParameter(this, pi.Name, pi.ParameterType)) - .ToList(); - - ModelName = GetFunctionName(methodInfo, parameters); - - _model = model; - _configurationSource = configurationSource; - Builder = new DbFunctionBuilder(this); + ModelName = GetFunctionName(methodInfo, methodInfo.GetParameters()); } /// @@ -107,24 +83,53 @@ public DbFunction( /// public DbFunction( [NotNull] string name, + [NotNull] Type returnType, + [CanBeNull] IEnumerable<(string Name, Type Type)> parameters, [NotNull] IMutableModel model, ConfigurationSource configurationSource) { + if (returnType == null + || returnType == typeof(void)) + { + throw new ArgumentException( + RelationalStrings.DbFunctionInvalidReturnType( + name, returnType.ShortDisplayName())); + } + + if (returnType.IsGenericType + && returnType.GetGenericTypeDefinition() == typeof(IQueryable<>)) + { + IsQueryable = true; + } + ModelName = name; - _parameters = new List(); - _model = model; + ReturnType = returnType; + Model = model; _configurationSource = configurationSource; - Builder = new DbFunctionBuilder(this); +#pragma warning disable EF1001 // Internal EF Core API usage. + Builder = new InternalDbFunctionBuilder(this, ((Model)model).Builder.ModelBuilder); +#pragma warning restore EF1001 // Internal EF Core API usage. + _parameters = parameters == null + ? new List() + : parameters + .Select(p => new DbFunctionParameter(this, p.Name, p.Type)) + .ToList(); } private static string GetFunctionName(MethodInfo methodInfo, ParameterInfo[] parameters) => methodInfo.DeclaringType.FullName + "." + methodInfo.Name + "(" + string.Join(",", parameters.Select(p => p.ParameterType.FullName)) + ")"; + /// + public virtual IMutableModel Model { get; } + /// - /// The builder that can be used to configure this function. + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual DbFunctionBuilder Builder { get; private set; } + public virtual InternalDbFunctionBuilder Builder { get; private set; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -184,9 +189,10 @@ public static DbFunction AddDbFunction( public static DbFunction AddDbFunction( [NotNull] IMutableModel model, [NotNull] string name, + [NotNull] Type returnType, ConfigurationSource configurationSource) { - var function = new DbFunction(name, model, configurationSource); + var function = new DbFunction(name, returnType, null, model, configurationSource); GetOrCreateFunctions(model).Add(name, function); return function; @@ -241,28 +247,47 @@ public static DbFunction RemoveDbFunction( return null; } - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// + /// public virtual string ModelName { get; private set; } - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// + /// public virtual MethodInfo MethodInfo { get; } + /// + public virtual Type ReturnType { get; } + + /// + public virtual bool IsQueryable { get; } + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// + IEntityType QueryableEntityType + { + get + { + if (!IsQueryable) + { + return null; + } + + if (_queryableEntityType == null) + { + _queryableEntityType = Model.FindEntityType(ReturnType.GetGenericArguments()[0]); + } + + return _queryableEntityType; + } + + [param: CanBeNull] + set => _queryableEntityType = value; + } + + /// + [DebuggerStepThrough] public virtual ConfigurationSource GetConfigurationSource() => _configurationSource; @@ -272,6 +297,7 @@ public virtual ConfigurationSource GetConfigurationSource() /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// + [DebuggerStepThrough] public virtual void UpdateConfigurationSource(ConfigurationSource configurationSource) => _configurationSource = configurationSource.Max(_configurationSource); @@ -283,7 +309,7 @@ public virtual void UpdateConfigurationSource(ConfigurationSource configurationS /// public virtual string Schema { - get => _schema ?? _model.GetDefaultSchema(); + get => _schema ?? Model.GetDefaultSchema(); set => SetSchema(value, ConfigurationSource.Explicit); } @@ -332,6 +358,8 @@ public virtual string Name /// public virtual string SetName([CanBeNull] string name, ConfigurationSource configurationSource) { + Check.NullButNotEmpty(name, nameof(name)); + _name = name; _nameConfigurationSource = name == null @@ -449,12 +477,7 @@ public virtual Func, SqlExpression> SetTransl if (translation != null && IsQueryable) { - if (configurationSource == ConfigurationSource.Explicit) - { - throw new InvalidOperationException(RelationalStrings.DbFunctionQueryableCustomTranslation(MethodInfo.DisplayName())); - } - - return null; + throw new InvalidOperationException(RelationalStrings.DbFunctionQueryableCustomTranslation(MethodInfo.DisplayName())); } _translation = translation; @@ -480,31 +503,31 @@ public virtual Func, SqlExpression> SetTransl /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual bool IsQueryable { get; } + public override string ToString() => this.ToDebugString(MetadataDebugStringOptions.SingleLineDefault); - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - IConventionDbFunctionBuilder IConventionDbFunction.Builder => Builder; + /// + IConventionDbFunctionBuilder IConventionDbFunction.Builder + { + [DebuggerStepThrough] + get => Builder; + } - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - IModel IDbFunction.Model => _model; + /// + IModel IDbFunction.Model + { + [DebuggerStepThrough] + get => Model; + } - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - IMutableModel IMutableDbFunction.Model => _model; + /// + IConventionModel IConventionDbFunction.Model + { + [DebuggerStepThrough] + get => (IConventionModel)Model; + } + + /// + IEntityType IDbFunction.QueryableEntityType => QueryableEntityType; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -512,7 +535,32 @@ public virtual Func, SqlExpression> SetTransl /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - IConventionModel IConventionDbFunction.Model => (IConventionModel)_model; + public virtual IReadOnlyList Parameters + { + [DebuggerStepThrough] + get => _parameters; + } + + /// + IReadOnlyList IDbFunction.Parameters + { + [DebuggerStepThrough] + get => _parameters; + } + + /// + IReadOnlyList IConventionDbFunction.Parameters + { + [DebuggerStepThrough] + get => _parameters; + } + + /// + IReadOnlyList IMutableDbFunction.Parameters + { + [DebuggerStepThrough] + get => _parameters; + } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -520,68 +568,32 @@ public virtual Func, SqlExpression> SetTransl /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// + public virtual DbFunctionParameter FindParameter([NotNull] string name) => Parameters.SingleOrDefault(p => p.Name == name); + + /// + [DebuggerStepThrough] string IConventionDbFunction.SetName(string name, bool fromDataAnnotation) => SetName(name, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// + /// + [DebuggerStepThrough] string IConventionDbFunction.SetSchema(string schema, bool fromDataAnnotation) => SetSchema(schema, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// + /// + [DebuggerStepThrough] string IConventionDbFunction.SetStoreType(string storeType, bool fromDataAnnotation) => SetStoreType(storeType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// + /// + [DebuggerStepThrough] RelationalTypeMapping IConventionDbFunction.SetTypeMapping(RelationalTypeMapping returnTypeMapping, bool fromDataAnnotation) => SetTypeMapping(returnTypeMapping, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// + /// + [DebuggerStepThrough] Func, SqlExpression> IConventionDbFunction.SetTranslation( Func, SqlExpression> translation, bool fromDataAnnotation) => SetTranslation(translation, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public virtual IReadOnlyList Parameters => _parameters; - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - IReadOnlyList IConventionDbFunction.Parameters => _parameters; - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - IReadOnlyList IMutableDbFunction.Parameters => _parameters; } } diff --git a/src/EFCore.Relational/Metadata/Internal/DbFunctionExtensions.cs b/src/EFCore.Relational/Metadata/Internal/DbFunctionExtensions.cs new file mode 100644 index 00000000000..353b2daa818 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/DbFunctionExtensions.cs @@ -0,0 +1,69 @@ +// 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.Text; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Infrastructure; + +namespace Microsoft.EntityFrameworkCore.Metadata.Internal +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static class DbFunctionExtensions + { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static string ToDebugString( + [NotNull] this IDbFunction function, + MetadataDebugStringOptions options, + [NotNull] string indent = "") + { + var builder = new StringBuilder(); + + builder + .Append(indent) + .Append("DbFunction: "); + + builder.Append(function.ReturnType.ShortDisplayName()) + .Append(" "); + + if (function.Schema != null) + { + builder + .Append(function.Schema) + .Append("."); + } + + builder.Append(function.Name); + + if ((options & MetadataDebugStringOptions.SingleLine) == 0) + { + var parameters = function.Parameters.ToList(); + if (parameters.Count != 0) + { + builder.AppendLine().Append(indent).Append(" Parameters: "); + foreach (var parameter in parameters) + { + builder.AppendLine().Append(parameter.ToDebugString(options, indent + " ")); + } + } + + if ((options & MetadataDebugStringOptions.IncludeAnnotations) != 0) + { + builder.Append(function.AnnotationsToDebugString(indent: indent + " ")); + } + } + + return builder.ToString(); + } + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/DbFunctionParameter.cs b/src/EFCore.Relational/Metadata/Internal/DbFunctionParameter.cs index 107474bac17..bbd4a1d52af 100644 --- a/src/EFCore.Relational/Metadata/Internal/DbFunctionParameter.cs +++ b/src/EFCore.Relational/Metadata/Internal/DbFunctionParameter.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.Diagnostics; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata.Builders; @@ -18,9 +19,6 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Internal /// public class DbFunctionParameter : ConventionAnnotatable, IMutableDbFunctionParameter, IConventionDbFunctionParameter { - private readonly IMutableDbFunction _function; - private readonly string _name; - private readonly Type _clrType; private string _storeType; private RelationalTypeMapping _typeMapping; @@ -33,16 +31,21 @@ public class DbFunctionParameter : ConventionAnnotatable, IMutableDbFunctionPara /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public DbFunctionParameter([NotNull] IMutableDbFunction function, [NotNull] string name, [NotNull] Type clrType) + public DbFunctionParameter( + [NotNull] DbFunction function, + [NotNull] string name, + [NotNull] Type clrType) { + Check.NotNull(function, nameof(function)); Check.NotEmpty(name, nameof(name)); Check.NotNull(clrType, nameof(clrType)); - Check.NotNull(function, nameof(function)); - _name = name; - _function = function; - _clrType = clrType; - Builder = new DbFunctionParameterBuilder(this); + Name = name; + Function = function; + ClrType = clrType; +#pragma warning disable EF1001 // Internal EF Core API usage. + Builder = new InternalDbFunctionParameterBuilder(this, function.Builder.ModelBuilder); +#pragma warning restore EF1001 // Internal EF Core API usage. } /// @@ -51,7 +54,49 @@ public DbFunctionParameter([NotNull] IMutableDbFunction function, [NotNull] stri /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual IConventionDbFunctionParameterBuilder Builder { get; private set; } + public virtual InternalDbFunctionParameterBuilder Builder { get; private set; } + + /// + IConventionDbFunctionParameterBuilder IConventionDbFunctionParameter.Builder => Builder; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual DbFunction Function { get; } + + /// + IConventionDbFunction IConventionDbFunctionParameter.Function + { + [DebuggerStepThrough] + get => Function; + } + + /// + IDbFunction IDbFunctionParameter.Function + { + [DebuggerStepThrough] + get => Function; + } + + /// + IMutableDbFunction IMutableDbFunctionParameter.Function + { + [DebuggerStepThrough] + get => Function; + } + + /// + public virtual string Name { get; } + + /// + public virtual Type ClrType { get; } + + /// + [DebuggerStepThrough] + public virtual ConfigurationSource GetConfigurationSource() => Function.GetConfigurationSource(); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -97,6 +142,11 @@ private void UpdateStoreTypeConfigurationSource(ConfigurationSource configuratio /// public virtual ConfigurationSource? GetStoreTypeConfigurationSource() => _storeTypeConfigurationSource; + /// + [DebuggerStepThrough] + string IConventionDbFunctionParameter.SetStoreType(string storeType, bool fromDataAnnotation) + => SetStoreType(storeType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -136,54 +186,10 @@ private void UpdateTypeMappingConfigurationSource(ConfigurationSource configurat /// public virtual ConfigurationSource? GetTypeMappingConfigurationSource() => _typeMappingConfigurationSource; - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - IConventionDbFunction IConventionDbFunctionParameter.Function => (IConventionDbFunction)_function; - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - IDbFunction IDbFunctionParameter.Function => _function; - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - IMutableDbFunction IMutableDbFunctionParameter.Function => _function; - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - string IDbFunctionParameter.Name => _name; - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - Type IDbFunctionParameter.ClrType => _clrType; - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - string IConventionDbFunctionParameter.SetStoreType(string storeType, bool fromDataAnnotation) - => SetStoreType(storeType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + /// + [DebuggerStepThrough] + RelationalTypeMapping IConventionDbFunctionParameter.SetTypeMapping(RelationalTypeMapping typeMapping, bool fromDataAnnotation) + => SetTypeMapping(typeMapping, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -191,7 +197,6 @@ string IConventionDbFunctionParameter.SetStoreType(string storeType, bool fromDa /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - RelationalTypeMapping IConventionDbFunctionParameter.SetTypeMapping(RelationalTypeMapping typeMapping, bool fromDataAnnotation) - => SetTypeMapping(typeMapping, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + public override string ToString() => this.ToDebugString(MetadataDebugStringOptions.SingleLineDefault); } } diff --git a/src/EFCore.Relational/Metadata/Internal/DbFunctionParameterExtensions.cs b/src/EFCore.Relational/Metadata/Internal/DbFunctionParameterExtensions.cs new file mode 100644 index 00000000000..d120bc11cbd --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/DbFunctionParameterExtensions.cs @@ -0,0 +1,50 @@ +// 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.Text; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Infrastructure; + +namespace Microsoft.EntityFrameworkCore.Metadata.Internal +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static class DbFunctionParameterExtensions + { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static string ToDebugString( + [NotNull] this IDbFunctionParameter parameter, + MetadataDebugStringOptions options, + [NotNull] string indent = "") + { + var builder = new StringBuilder(); + + builder + .Append(indent) + .Append("DbFunctionParameter: "); + + builder.Append(parameter.Name) + .Append(" ") + .Append(parameter.StoreType); + + if ((options & MetadataDebugStringOptions.SingleLine) == 0) + { + if ((options & MetadataDebugStringOptions.IncludeAnnotations) != 0) + { + builder.Append(parameter.AnnotationsToDebugString(indent: indent + " ")); + } + } + + return builder.ToString(); + } + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/InternalDbFunctionBuilder.cs b/src/EFCore.Relational/Metadata/Internal/InternalDbFunctionBuilder.cs new file mode 100644 index 00000000000..a25cb5bf1e9 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/InternalDbFunctionBuilder.cs @@ -0,0 +1,258 @@ +// 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.Diagnostics; +using System.Linq; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Microsoft.EntityFrameworkCore.Metadata.Builders +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// +#pragma warning disable EF1001 // Internal EF Core API usage. + public class InternalDbFunctionBuilder : InternalModelItemBuilder, IConventionDbFunctionBuilder +#pragma warning restore EF1001 // Internal EF Core API usage. + { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public InternalDbFunctionBuilder([NotNull] DbFunction function, [NotNull] InternalModelBuilder modelBuilder) + : base(function, modelBuilder) + { + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IConventionDbFunctionBuilder HasName([CanBeNull] string name, ConfigurationSource configurationSource) + { + if (CanSetName(name, configurationSource)) + { + Metadata.SetName(name, configurationSource); + return this; + } + + return null; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool CanSetName([CanBeNull] string name, ConfigurationSource configurationSource) + => (name != "" || configurationSource == ConfigurationSource.Explicit) + && (configurationSource.Overrides(Metadata.GetNameConfigurationSource()) + || Metadata.Name == name); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IConventionDbFunctionBuilder HasSchema([CanBeNull] string schema, ConfigurationSource configurationSource) + { + if (CanSetSchema(schema, configurationSource)) + { + Metadata.SetSchema(schema, configurationSource); + return this; + } + + return null; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool CanSetSchema([CanBeNull] string schema, ConfigurationSource configurationSource) + => configurationSource.Overrides(Metadata.GetSchemaConfigurationSource()) + || Metadata.Schema == schema; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IConventionDbFunctionBuilder HasStoreType([CanBeNull] string storeType, ConfigurationSource configurationSource) + { + if (CanSetStoreType(storeType, configurationSource)) + { + Metadata.SetStoreType(storeType, configurationSource); + return this; + } + + return null; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool CanSetStoreType([CanBeNull] string storeType, ConfigurationSource configurationSource) + => configurationSource.Overrides(Metadata.GetStoreTypeConfigurationSource()) + || Metadata.StoreType == storeType; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IConventionDbFunctionBuilder HasTypeMapping( + [CanBeNull] RelationalTypeMapping returnTypeMapping, ConfigurationSource configurationSource) + { + if (CanSetTypeMapping(returnTypeMapping, configurationSource)) + { + Metadata.SetTypeMapping(returnTypeMapping, configurationSource); + return this; + } + + return null; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool CanSetTypeMapping([CanBeNull] RelationalTypeMapping returnTypeMapping, ConfigurationSource configurationSource) + => configurationSource.Overrides(Metadata.GetTypeMappingConfigurationSource()) + || Metadata.TypeMapping == returnTypeMapping; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IConventionDbFunctionBuilder HasTranslation( + [CanBeNull] Func, SqlExpression> translation, ConfigurationSource configurationSource) + { + if (CanSetTranslation(translation, configurationSource)) + { + Metadata.SetTranslation(translation, configurationSource); + return this; + } + + return null; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool CanSetTranslation( + [CanBeNull] Func, SqlExpression> translation, ConfigurationSource configurationSource) + => (!Metadata.IsQueryable || configurationSource == ConfigurationSource.Explicit) + && (configurationSource.Overrides(Metadata.GetTranslationConfigurationSource()) + || Metadata.Translation == translation); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalDbFunctionParameterBuilder HasParameter([NotNull] string name, ConfigurationSource configurationSource) + { + var parameter = Metadata.FindParameter(name); + if (parameter == null) + { + throw new ArgumentException( + RelationalStrings.DbFunctionInvalidParameterName(name, Metadata.MethodInfo.DisplayName())); + } + + return parameter.Builder; + } + + IConventionDbFunction IConventionDbFunctionBuilder.Metadata + { + [DebuggerStepThrough] get => Metadata; + } + + /// + [DebuggerStepThrough] + IConventionDbFunctionBuilder IConventionDbFunctionBuilder.HasName(string name, bool fromDataAnnotation) + => HasName(name, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + bool IConventionDbFunctionBuilder.CanSetName(string name, bool fromDataAnnotation) + => CanSetName(name, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + IConventionDbFunctionBuilder IConventionDbFunctionBuilder.HasSchema(string schema, bool fromDataAnnotation) + => HasSchema(schema, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + bool IConventionDbFunctionBuilder.CanSetSchema(string schema, bool fromDataAnnotation) + => CanSetSchema(schema, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + IConventionDbFunctionBuilder IConventionDbFunctionBuilder.HasStoreType(string storeType, bool fromDataAnnotation) + => HasStoreType(storeType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + bool IConventionDbFunctionBuilder.CanSetStoreType(string storeType, bool fromDataAnnotation) + => CanSetStoreType(storeType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + IConventionDbFunctionBuilder IConventionDbFunctionBuilder.HasTypeMapping(RelationalTypeMapping typeMapping, bool fromDataAnnotation) + => HasTypeMapping(typeMapping, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + bool IConventionDbFunctionBuilder.CanSetTypeMapping(RelationalTypeMapping typeMapping, bool fromDataAnnotation) + => CanSetTypeMapping(typeMapping, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + IConventionDbFunctionBuilder IConventionDbFunctionBuilder.HasTranslation( + Func, SqlExpression> translation, bool fromDataAnnotation) + => HasTranslation(translation, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + bool IConventionDbFunctionBuilder.CanSetTranslation( + Func, SqlExpression> translation, bool fromDataAnnotation) + => CanSetTranslation(translation, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + IConventionDbFunctionParameterBuilder IConventionDbFunctionBuilder.HasParameter(string name, bool fromDataAnnotation) + => HasParameter(name, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/InternalDbFunctionParameterBuilder.cs b/src/EFCore.Relational/Metadata/Internal/InternalDbFunctionParameterBuilder.cs new file mode 100644 index 00000000000..c40d6f820e6 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/InternalDbFunctionParameterBuilder.cs @@ -0,0 +1,119 @@ +// 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.Diagnostics; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Microsoft.EntityFrameworkCore.Metadata.Builders +{ + /// + /// + /// 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. + /// + /// +#pragma warning disable EF1001 // Internal EF Core API usage. + public class InternalDbFunctionParameterBuilder : InternalModelItemBuilder, IConventionDbFunctionParameterBuilder +#pragma warning restore EF1001 // Internal EF Core API usage. + { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [EntityFrameworkInternal] + public InternalDbFunctionParameterBuilder([NotNull] DbFunctionParameter parameter, [NotNull] InternalModelBuilder modelBuilder) + : base(parameter, modelBuilder) + { + } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IConventionDbFunctionParameterBuilder HasStoreType( + [CanBeNull] string storeType, ConfigurationSource configurationSource) + { + if (CanSetStoreType(storeType, configurationSource)) + { + Metadata.SetStoreType(storeType, configurationSource); + return this; + } + + return null; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool CanSetStoreType([CanBeNull] string storeType, ConfigurationSource configurationSource) + => configurationSource.Overrides(Metadata.GetStoreTypeConfigurationSource()) + || Metadata.StoreType == storeType; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IConventionDbFunctionParameterBuilder HasTypeMapping( + [CanBeNull] RelationalTypeMapping typeMapping, ConfigurationSource configurationSource) + { + if (CanSetTypeMapping(typeMapping, configurationSource)) + { + Metadata.SetTypeMapping(typeMapping, configurationSource); + return this; + } + + return null; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool CanSetTypeMapping([CanBeNull] RelationalTypeMapping typeMapping, ConfigurationSource configurationSource) + => configurationSource.Overrides(Metadata.GetTypeMappingConfigurationSource()) + || Metadata.TypeMapping == typeMapping; + + /// + IConventionDbFunctionParameter IConventionDbFunctionParameterBuilder.Metadata + { + [DebuggerStepThrough] get => Metadata; + } + + /// + [DebuggerStepThrough] + IConventionDbFunctionParameterBuilder IConventionDbFunctionParameterBuilder.HasStoreType(string storeType, bool fromDataAnnotation) + => HasStoreType(storeType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + bool IConventionDbFunctionParameterBuilder.CanSetStoreType(string storeType, bool fromDataAnnotation) + => CanSetStoreType(storeType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + IConventionDbFunctionParameterBuilder IConventionDbFunctionParameterBuilder.HasTypeMapping( + RelationalTypeMapping typeMapping, bool fromDataAnnotation) + => HasTypeMapping(typeMapping, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + bool IConventionDbFunctionParameterBuilder.CanSetTypeMapping(RelationalTypeMapping typeMapping, bool fromDataAnnotation) + => CanSetTypeMapping(typeMapping, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/InternalSequenceBuilder.cs b/src/EFCore.Relational/Metadata/Internal/InternalSequenceBuilder.cs new file mode 100644 index 00000000000..b02ec5096f2 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/InternalSequenceBuilder.cs @@ -0,0 +1,264 @@ +// 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.Diagnostics; +using System.Linq; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata.Internal; + +namespace Microsoft.EntityFrameworkCore.Metadata.Builders +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// +#pragma warning disable EF1001 // Internal EF Core API usage. + public class InternalSequenceBuilder : InternalModelItemBuilder, IConventionSequenceBuilder +#pragma warning restore EF1001 // Internal EF Core API usage. + { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public InternalSequenceBuilder([NotNull] Sequence sequence, [NotNull] InternalModelBuilder modelBuilder) + : base(sequence, modelBuilder) + { + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IConventionSequenceBuilder HasType([CanBeNull] Type type, ConfigurationSource configurationSource) + { + if (configurationSource.Overrides(Metadata.GetClrTypeConfigurationSource()) + || Metadata.ClrType == type) + { + Metadata.SetClrType(type, configurationSource); + return this; + } + + return null; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool CanSetType([CanBeNull] Type type, ConfigurationSource configurationSource) + => (type == null || Sequence.SupportedTypes.Contains(type)) + && (configurationSource.Overrides(Metadata.GetClrTypeConfigurationSource()) + || Metadata.ClrType == type); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IConventionSequenceBuilder IncrementsBy( + int? increment, ConfigurationSource configurationSource) + { + if (CanSetIncrementsBy(increment, configurationSource)) + { + Metadata.SetIncrementBy(increment, configurationSource); + return this; + } + + return null; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool CanSetIncrementsBy(int? increment, ConfigurationSource configurationSource) + => configurationSource.Overrides(Metadata.GetIncrementByConfigurationSource()) + || Metadata.IncrementBy == increment; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IConventionSequenceBuilder StartsAt(long? startValue, ConfigurationSource configurationSource) + { + if (CanSetStartsAt(startValue, configurationSource)) + { + Metadata.SetStartValue(startValue, configurationSource); + return this; + } + + return null; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool CanSetStartsAt(long? startValue, ConfigurationSource configurationSource) + => configurationSource.Overrides(Metadata.GetStartValueConfigurationSource()) + || Metadata.StartValue == startValue; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IConventionSequenceBuilder HasMax(long? maximum, ConfigurationSource configurationSource) + { + if (CanSetMax(maximum, configurationSource)) + { + Metadata.SetMaxValue(maximum, configurationSource); + return this; + } + + return null; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool CanSetMax(long? maximum, ConfigurationSource configurationSource) + => configurationSource.Overrides(Metadata.GetMaxValueConfigurationSource()) + || Metadata.MaxValue == maximum; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IConventionSequenceBuilder HasMin(long? minimum, ConfigurationSource configurationSource) + { + if (CanSetMin(minimum, configurationSource)) + { + Metadata.SetMinValue(minimum, configurationSource); + return this; + } + + return null; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool CanSetMin(long? minimum, ConfigurationSource configurationSource) + => configurationSource.Overrides(Metadata.GetMinValueConfigurationSource()) + || Metadata.MinValue == minimum; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IConventionSequenceBuilder IsCyclic(bool? cyclic, ConfigurationSource configurationSource) + { + if (CanSetIsCyclic(cyclic, configurationSource)) + { + Metadata.SetIsCyclic(cyclic, configurationSource); + return this; + } + + return null; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool CanSetIsCyclic(bool? cyclic, ConfigurationSource configurationSource) + => configurationSource.Overrides(Metadata.GetIsCyclicConfigurationSource()) + || Metadata.IsCyclic == cyclic; + + /// + IConventionSequence IConventionSequenceBuilder.Metadata + { + [DebuggerStepThrough] get => Metadata; + } + + /// + [DebuggerStepThrough] + IConventionSequenceBuilder IConventionSequenceBuilder.HasType(Type type, bool fromDataAnnotation) + => HasType(type, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + bool IConventionSequenceBuilder.CanSetType(Type type, bool fromDataAnnotation) + => CanSetType(type, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + IConventionSequenceBuilder IConventionSequenceBuilder.IncrementsBy(int? increment, bool fromDataAnnotation) + => IncrementsBy(increment, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + bool IConventionSequenceBuilder.CanSetIncrementsBy(int? increment, bool fromDataAnnotation) + => CanSetIncrementsBy(increment, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + IConventionSequenceBuilder IConventionSequenceBuilder.StartsAt(long? startValue, bool fromDataAnnotation) + => StartsAt(startValue, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + bool IConventionSequenceBuilder.CanSetStartsAt(long? startValue, bool fromDataAnnotation) + => CanSetStartsAt(startValue, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + IConventionSequenceBuilder IConventionSequenceBuilder.HasMax(long? maximum, bool fromDataAnnotation) + => HasMax(maximum, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + bool IConventionSequenceBuilder.CanSetMax(long? maximum, bool fromDataAnnotation) + => CanSetMax(maximum, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + IConventionSequenceBuilder IConventionSequenceBuilder.HasMin(long? minimum, bool fromDataAnnotation) + => HasMin(minimum, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + bool IConventionSequenceBuilder.CanSetMin(long? minimum, bool fromDataAnnotation) + => CanSetMin(minimum, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + IConventionSequenceBuilder IConventionSequenceBuilder.IsCyclic(bool? cyclic, bool fromDataAnnotation) + => IsCyclic(cyclic, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + bool IConventionSequenceBuilder.CanSetIsCyclic(bool? cyclic, bool fromDataAnnotation) + => CanSetIsCyclic(cyclic, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/Sequence.cs b/src/EFCore.Relational/Metadata/Internal/Sequence.cs index bf2b3f089a0..a48257f3067 100644 --- a/src/EFCore.Relational/Metadata/Internal/Sequence.cs +++ b/src/EFCore.Relational/Metadata/Internal/Sequence.cs @@ -108,7 +108,9 @@ public Sequence( _name = name; _schema = schema; _configurationSource = configurationSource; - Builder = new SequenceBuilder(this); +#pragma warning disable EF1001 // Internal EF Core API usage. + Builder = new InternalSequenceBuilder(this, ((Model)model).Builder.ModelBuilder); +#pragma warning restore EF1001 // Internal EF Core API usage. } /// @@ -135,7 +137,9 @@ public Sequence([NotNull] IModel model, [NotNull] string annotationName) _maxValue = data.MaxValue; _clrType = data.ClrType; _isCyclic = data.IsCyclic; - Builder = new SequenceBuilder(this); +#pragma warning disable EF1001 // Internal EF Core API usage. + Builder = new InternalSequenceBuilder(this, ((Model)model).Builder.ModelBuilder); +#pragma warning restore EF1001 // Internal EF Core API usage. } /// @@ -214,7 +218,7 @@ public static Sequence RemoveSequence([NotNull] IMutableModel model, [NotNull] s /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual SequenceBuilder Builder { get; private set; } + public virtual InternalSequenceBuilder Builder { get; private set; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -240,6 +244,24 @@ public static Sequence RemoveSequence([NotNull] IMutableModel model, [NotNull] s /// public virtual string Schema => _schema ?? Model.GetDefaultSchema(); + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual ConfigurationSource GetConfigurationSource() + => _configurationSource; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual void UpdateConfigurationSource(ConfigurationSource configurationSource) + => _configurationSource = _configurationSource.Max(configurationSource); + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -489,17 +511,7 @@ public virtual bool IsCyclic /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual ConfigurationSource GetConfigurationSource() - => _configurationSource; - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public virtual void UpdateConfigurationSource(ConfigurationSource configurationSource) - => _configurationSource = _configurationSource.Max(configurationSource); + public override string ToString() => this.ToDebugString(MetadataDebugStringOptions.SingleLineDefault); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.Relational/Metadata/Internal/SequenceExtensions.cs b/src/EFCore.Relational/Metadata/Internal/SequenceExtensions.cs new file mode 100644 index 00000000000..0f14c040db1 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/SequenceExtensions.cs @@ -0,0 +1,84 @@ +// 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.Text; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Infrastructure; + +namespace Microsoft.EntityFrameworkCore.Metadata.Internal +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static class SequenceExtensions + { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static string ToDebugString( + [NotNull] this ISequence sequence, + MetadataDebugStringOptions options, + [NotNull] string indent = "") + { + var builder = new StringBuilder(); + + builder + .Append(indent) + .Append("Sequence: "); + + if (sequence.Schema != null) + { + builder + .Append(sequence.Schema) + .Append("."); + } + + builder.Append(sequence.Name); + + if (!sequence.IsCyclic) + { + builder.Append(" Cyclic"); + } + + if (sequence.StartValue != 1) + { + builder.Append(" Start: ") + .Append(sequence.StartValue); + } + + if (sequence.IncrementBy != 1) + { + builder.Append(" IncrementBy: ") + .Append(sequence.IncrementBy); + } + + if (sequence.MinValue != null) + { + builder.Append(" Min: ") + .Append(sequence.MinValue); + } + + if (sequence.MaxValue != null) + { + builder.Append(" Max: ") + .Append(sequence.MaxValue); + } + + if ((options & MetadataDebugStringOptions.SingleLine) == 0) + { + if ((options & MetadataDebugStringOptions.IncludeAnnotations) != 0) + { + builder.Append(sequence.AnnotationsToDebugString(indent: indent + " ")); + } + } + + return builder.ToString(); + } + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/TableExtensions.cs b/src/EFCore.Relational/Metadata/Internal/TableExtensions.cs index 467e964294e..46234958e4d 100644 --- a/src/EFCore.Relational/Metadata/Internal/TableExtensions.cs +++ b/src/EFCore.Relational/Metadata/Internal/TableExtensions.cs @@ -62,16 +62,26 @@ public static string ToDebugString( var columns = table.Columns.ToList(); if (columns.Count != 0) { - builder.AppendLine().Append(indent).Append(" Properties: "); + builder.AppendLine().Append(indent).Append(" Columns: "); foreach (var column in columns) { builder.AppendLine().Append(column.ToDebugString(options, indent + " ")); } } + var checkConstraints = table.CheckConstraints.ToList(); + if (checkConstraints.Count != 0) + { + builder.AppendLine().Append(indent).Append(" Check constraints: "); + foreach (var checkConstraint in checkConstraints) + { + builder.AppendLine().Append(checkConstraint.ToDebugString(options, indent + " ")); + } + } + if ((options & MetadataDebugStringOptions.IncludeAnnotations) != 0) { - builder.Append(table.AnnotationsToDebugString(indent: indent + " ")); + builder.Append(table.AnnotationsToDebugString(indent + " ")); } } diff --git a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs index dcf22cf94ae..6ad79e568df 100644 --- a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs +++ b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs @@ -153,11 +153,6 @@ public static class RelationalAnnotationNames /// public const string ViewColumnMappings = Prefix + "ViewColumnMappings"; - /// - /// The definition of a Queryable Function Result Type. - /// - public const string QueryableFunctionResultType = Prefix + "QueryableFunctionResultType"; - /// /// The name for foreign key mappings annotations. /// diff --git a/src/EFCore.Relational/Migrations/Migration.cs b/src/EFCore.Relational/Migrations/Migration.cs index 959b67b8009..5b79b8da2de 100644 --- a/src/EFCore.Relational/Migrations/Migration.cs +++ b/src/EFCore.Relational/Migrations/Migration.cs @@ -85,7 +85,7 @@ public virtual IReadOnlyList DownOperations public virtual string ActiveProvider { get; [param: NotNull] set; } /// - /// Implemented to builds the . + /// Implemented to build the . /// /// The to use to build the model. protected virtual void BuildTargetModel([NotNull] ModelBuilder modelBuilder) diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index 8f49c7f97da..c40fc0904f0 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -632,6 +632,22 @@ public static string DbFunctionQueryableCustomTranslation([CanBeNull] object fun GetString("DbFunctionQueryableCustomTranslation", nameof(function)), function); + /// + /// The DbFunction '{function}' has an invalid return type '{type}'. Only functions that return IQueryable of entity type are supported. + /// + public static string DbFunctionInvalidIQueryableReturnType([CanBeNull] object function, [CanBeNull] object type) + => string.Format( + GetString("DbFunctionInvalidIQueryableReturnType", nameof(function), nameof(type)), + function, type); + + /// + /// The DbFunction '{function}' has an invalid return type '{type}'. Owned entity types cannot be used as the return type of a DbFunction. + /// + public static string DbFunctionInvalidIQueryableOwnedReturnType([CanBeNull] object function, [CanBeNull] object type) + => string.Format( + GetString("DbFunctionInvalidIQueryableOwnedReturnType", nameof(function), nameof(type)), + function, type); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index 7063f58e440..a18f3e230b6 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -518,4 +518,10 @@ Cannot set custom translation on the DbFunction '{function}' since it returns IQueryable type. + + The DbFunction '{function}' has an invalid return type '{type}'. Only functions that return IQueryable of entity type are supported. + + + The DbFunction '{function}' has an invalid return type '{type}'. Owned entity types cannot be used as the return type of a DbFunction. + \ No newline at end of file diff --git a/src/EFCore.Relational/Query/Internal/QueryableFunctionToQueryRootConvertingExpressionVisitor.cs b/src/EFCore.Relational/Query/Internal/QueryableFunctionToQueryRootConvertingExpressionVisitor.cs index 753b20e19da..e1ba427b890 100644 --- a/src/EFCore.Relational/Query/Internal/QueryableFunctionToQueryRootConvertingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/Internal/QueryableFunctionToQueryRootConvertingExpressionVisitor.cs @@ -31,10 +31,6 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp private Expression CreateQueryableFunctionQueryRootExpression( IDbFunction function, IReadOnlyCollection arguments) - { - var entityType = _model.FindEntityType(function.MethodInfo.ReturnType.GetGenericArguments()[0]); - - return new QueryableFunctionQueryRootExpression(entityType, function, arguments); - } + => new QueryableFunctionQueryRootExpression(function.QueryableEntityType, function, arguments); } } diff --git a/src/EFCore.Relational/Query/RelationalEntityShaperExpression.cs b/src/EFCore.Relational/Query/RelationalEntityShaperExpression.cs index dd0d961b06f..ad9e23f664c 100644 --- a/src/EFCore.Relational/Query/RelationalEntityShaperExpression.cs +++ b/src/EFCore.Relational/Query/RelationalEntityShaperExpression.cs @@ -34,7 +34,7 @@ protected override LambdaExpression GenerateMaterializationCondition(IEntityType if (entityType.FindPrimaryKey() != null) { - var linkingFks = entityType.GetViewOrTableMappings().Single().Table.GetInternalForeignKeys(entityType); + var linkingFks = entityType.GetViewOrTableMappings().SingleOrDefault()?.Table.GetInternalForeignKeys(entityType); if (linkingFks != null && linkingFks.Any()) { diff --git a/src/EFCore.Relational/Query/SqlExpressionFactory.cs b/src/EFCore.Relational/Query/SqlExpressionFactory.cs index d8485f4dfbd..f247784b154 100644 --- a/src/EFCore.Relational/Query/SqlExpressionFactory.cs +++ b/src/EFCore.Relational/Query/SqlExpressionFactory.cs @@ -762,7 +762,7 @@ private void AddConditions( else { var tableMappings = entityType.GetViewOrTableMappings(); - if (tableMappings == null) + if (!tableMappings.Any()) { return; } diff --git a/src/EFCore/Infrastructure/ConventionAnnotatable.cs b/src/EFCore/Infrastructure/ConventionAnnotatable.cs index 91d023f9751..135ea7d817e 100644 --- a/src/EFCore/Infrastructure/ConventionAnnotatable.cs +++ b/src/EFCore/Infrastructure/ConventionAnnotatable.cs @@ -24,7 +24,6 @@ namespace Microsoft.EntityFrameworkCore.Infrastructure public abstract class ConventionAnnotatable : Annotatable, IConventionAnnotatable { - /// /// Gets all convention annotations on the current object. /// diff --git a/src/EFCore/Metadata/Builders/EntityTypeBuilder.cs b/src/EFCore/Metadata/Builders/EntityTypeBuilder.cs index 65b2b7bcefe..6bde5d52d30 100644 --- a/src/EFCore/Metadata/Builders/EntityTypeBuilder.cs +++ b/src/EFCore/Metadata/Builders/EntityTypeBuilder.cs @@ -11,7 +11,6 @@ using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Utilities; diff --git a/src/EFCore/Metadata/Conventions/KeyDiscoveryConvention.cs b/src/EFCore/Metadata/Conventions/KeyDiscoveryConvention.cs index 6a28f9c69ff..be1a03079d3 100644 --- a/src/EFCore/Metadata/Conventions/KeyDiscoveryConvention.cs +++ b/src/EFCore/Metadata/Conventions/KeyDiscoveryConvention.cs @@ -61,7 +61,7 @@ private void TryConfigurePrimaryKey(IConventionEntityTypeBuilder entityTypeBuild { var entityType = entityTypeBuilder.Metadata; if (entityType.BaseType != null - || entityType.IsKeyless + || (entityType.IsKeyless && entityType.GetIsKeylessConfigurationSource() != ConfigurationSource.Convention) || !entityTypeBuilder.CanSetPrimaryKey(null)) { return; diff --git a/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs b/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs index 1a9d2a02e05..a82e63b3b1f 100644 --- a/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs @@ -152,7 +152,10 @@ public virtual bool CanSetPrimaryKey([CanBeNull] IReadOnlyList diff --git a/test/EFCore.Relational.Specification.Tests/Query/UdfDbFunctionTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/UdfDbFunctionTestBase.cs index 5ffc07b40bc..729b8e93e40 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/UdfDbFunctionTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/UdfDbFunctionTestBase.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.EntityFrameworkCore.TestUtilities; @@ -24,43 +23,14 @@ public abstract class UdfDbFunctionTestBase : IClassFixture #region Model - public enum PhoneType - { - Home = 0, - Work = 1, - Cell = 2 - } - - public enum CreditCardType - { - ShutupAndTakeMyMoney = 0, - BuyNLarge = 1, - BankOfDad = 2 - } - - public class CreditCard - { - public CreditCardType CreditCardType { get; set; } - public string Number { get; set; } - } - - public class PhoneInformation - { - public PhoneType PhoneType { get; set; } - public string Number { get; set; } - } - public class Customer { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } - public CreditCard CreditCard { get; set; } - public List Orders { get; set; } public List
Addresses { get; set; } - public List PhoneNumbers { get; set; } } public class Order @@ -226,16 +196,6 @@ public IQueryable GetOrdersWithMultipleProducts(int customerI return CreateQuery(() => GetOrdersWithMultipleProducts(customerId)); } - public IQueryable GetCreditCards(int customerId) - { - return CreateQuery(() => GetCreditCards(customerId)); - } - - public IQueryable GetPhoneInformation(int customerId, string areaCode) - { - return CreateQuery(() => GetPhoneInformation(customerId, areaCode)); - } - #endregion #endregion @@ -247,9 +207,6 @@ public UDFSqlContext(DbContextOptions options) protected override void OnModelCreating(ModelBuilder modelBuilder) { - modelBuilder.Entity().OwnsOne(typeof(CreditCard), "CreditCard"); - modelBuilder.Entity().OwnsMany(typeof(PhoneInformation), "PhoneNumbers"); - //Static modelBuilder.HasDbFunction(typeof(UDFSqlContext).GetMethod(nameof(CustomerOrderCountStatic))).HasName("CustomerOrderCount"); modelBuilder.HasDbFunction(typeof(UDFSqlContext).GetMethod(nameof(CustomerOrderCountWithClientStatic))) @@ -319,17 +276,12 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) methodInfo2.ReturnType, null)); - modelBuilder.Entity().ToQueryableFunctionResultType().HasKey(mpo => mpo.OrderId); - - modelBuilder.Entity().ToQueryableFunctionResultType().HasNoKey(); - modelBuilder.Entity().ToQueryableFunctionResultType().HasNoKey(); + modelBuilder.Entity().ToTable("MultProductOrders").HasKey(mpo => mpo.OrderId); //Table modelBuilder.HasDbFunction(typeof(UDFSqlContext).GetMethod(nameof(GetCustomerOrderCountByYear), new[] { typeof(int) })); modelBuilder.HasDbFunction(typeof(UDFSqlContext).GetMethod(nameof(GetTopTwoSellingProducts))); modelBuilder.HasDbFunction(typeof(UDFSqlContext).GetMethod(nameof(GetTopSellingProductsForCustomer))); - modelBuilder.HasDbFunction(typeof(UDFSqlContext).GetMethod(nameof(GetCreditCards))); - modelBuilder.HasDbFunction(typeof(UDFSqlContext).GetMethod(nameof(GetPhoneInformation))); modelBuilder.HasDbFunction(typeof(UDFSqlContext).GetMethod(nameof(GetOrdersWithMultipleProducts))); } @@ -418,13 +370,7 @@ protected override void Seed(DbContext context) FirstName = "Customer", LastName = "One", Orders = new List { order11, order12, order13 }, - Addresses = new List
{ address11, address12 }, - CreditCard = new CreditCard() { CreditCardType = CreditCardType.BankOfDad, Number = "123"}, - PhoneNumbers = new List - { - new PhoneInformation { Number = "123-978-2342", PhoneType = PhoneType.Cell}, - new PhoneInformation { Number = "654-323-2342", PhoneType = PhoneType.Home} - } + Addresses = new List
{ address11, address12 } }; var customer2 = new Customer @@ -432,13 +378,7 @@ protected override void Seed(DbContext context) FirstName = "Customer", LastName = "Two", Orders = new List { order21, order22 }, - Addresses = new List
{ address21 }, - PhoneNumbers = new List - { - new PhoneInformation { Number = "234-873-4921", PhoneType = PhoneType.Cell}, - new PhoneInformation { Number = "345-345-9234", PhoneType = PhoneType.Home}, - new PhoneInformation { Number = "923-913-1232", PhoneType = PhoneType.Work} - } + Addresses = new List
{ address21 } }; var customer3 = new Customer @@ -446,27 +386,14 @@ protected override void Seed(DbContext context) FirstName = "Customer", LastName = "Three", Orders = new List { order31 }, - Addresses = new List
{ address31, address32 }, - CreditCard = new CreditCard() { CreditCardType = CreditCardType.BuyNLarge, Number = "554355" }, - PhoneNumbers = new List - { - new PhoneInformation { Number = "789-834-0934", PhoneType = PhoneType.Cell}, - new PhoneInformation { Number = "902-092-2342", PhoneType = PhoneType.Home}, - new PhoneInformation { Number = "234-789-2345", PhoneType = PhoneType.Work} - - } + Addresses = new List
{ address31, address32 } }; var customer4 = new Customer { FirstName = "Customer", LastName = "Four", - Addresses = new List
{ address41, address42, address43 }, - CreditCard = new CreditCard() { CreditCardType = CreditCardType.ShutupAndTakeMyMoney, Number = "99-99" }, - PhoneNumbers = new List - { - new PhoneInformation { Number = "269-980-9238", PhoneType = PhoneType.Work} - } + Addresses = new List
{ address41, address42, address43 } }; ((UDFSqlContext)context).Products.AddRange(product1, product2, product3, product4, product5); @@ -1312,94 +1239,6 @@ public virtual void Scalar_Nested_Function_UDF_BCL_Instance() #region QueryableFunction - [ConditionalFact] - public virtual void QF_Owned_Many_Tracked_Select_Owned() - { - using (var context = CreateContext()) - { - var query = (from c in context.Customers - from pi in context.GetPhoneInformation(c.Id, "234") - orderby c.Id - select new - { - Customer = c - }).ToList(); - - Assert.Equal(2, query.Count); - Assert.Equal(2, query[0].Customer.Id); - Assert.Equal(3, query[1].Customer.Id); - } - } - - [ConditionalFact] - public virtual void QF_Owned_Many_NoTracking_Select_Owned() - { - using (var context = CreateContext()) - { - var query = (from c in context.Customers - from pi in context.GetPhoneInformation(c.Id, "234") - orderby pi.Number - select pi).AsNoTracking().ToList(); - - Assert.Equal(2, query.Count); - Assert.Equal("234-789-2345", query[0].Number); - Assert.Equal("234-873-4921", query[1].Number); - } - } - - [ConditionalFact] - public virtual void QF_Owned_One_NoTracking_Select_Owned() - { - using (var context = CreateContext()) - { - var query = (from c in context.Customers - from cc in context.GetCreditCards(c.Id) - orderby cc.Number - select cc).AsNoTracking().ToList(); - - Assert.Equal(3, query.Count); - - Assert.Equal("123", query[0].Number); - Assert.Equal(CreditCardType.BankOfDad, query[0].CreditCardType); - - Assert.Equal("554355", query[1].Number); - Assert.Equal(CreditCardType.BuyNLarge, query[1].CreditCardType); - - Assert.Equal("99-99", query[2].Number); - Assert.Equal(CreditCardType.ShutupAndTakeMyMoney, query[2].CreditCardType); - } - } - - [ConditionalFact] - public virtual void QF_Owned_One_Tracked() - { - using (var context = CreateContext()) - { - var query = (from c in context.Customers - from cc in context.GetCreditCards(c.Id) - orderby cc.Number - select new - { - Customer = c, - CreditCard = cc - }).ToList(); - - Assert.Equal(3, query.Count); - - Assert.Equal(1, query[0].Customer.Id); - Assert.Equal("123", query[0].CreditCard.Number); - Assert.Equal(CreditCardType.BankOfDad, query[0].CreditCard.CreditCardType); - - Assert.Equal(3, query[1].Customer.Id); - Assert.Equal("554355", query[1].CreditCard.Number); - Assert.Equal(CreditCardType.BuyNLarge, query[1].CreditCard.CreditCardType); - - Assert.Equal(4, query[2].Customer.Id); - Assert.Equal("99-99", query[2].CreditCard.Number); - Assert.Equal(CreditCardType.ShutupAndTakeMyMoney, query[2].CreditCard.CreditCardType); - } - } - [ConditionalFact(Skip = "Issue#15873")] public virtual void QF_Anonymous_Collection_No_PK_Throws() { diff --git a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs index 77d48833d9f..844b654e663 100644 --- a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs +++ b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs @@ -1065,7 +1065,7 @@ var methodInfo = typeof(DbFunctionMetadataTests.TestMethods) .GetRuntimeMethod(nameof(DbFunctionMetadataTests.TestMethods.MethodD), Array.Empty()); - ((IConventionDbFunctionBuilder)modelBuilder.HasDbFunction(methodInfo)).HasName(""); + modelBuilder.HasDbFunction(methodInfo).GetInfrastructure().HasName(""); VerifyError( RelationalStrings.DbFunctionNameEmpty(methodInfo.DisplayName()), diff --git a/test/EFCore.Relational.Tests/Metadata/Conventions/Internal/QueryableDbFunctionConventionTest.cs b/test/EFCore.Relational.Tests/Metadata/Conventions/Internal/QueryableDbFunctionConventionTest.cs new file mode 100644 index 00000000000..94be19d87e2 --- /dev/null +++ b/test/EFCore.Relational.Tests/Metadata/Conventions/Internal/QueryableDbFunctionConventionTest.cs @@ -0,0 +1,121 @@ +// 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.Linq; +using System.Reflection; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Xunit; + +namespace Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal +{ + public class QueryableDbFunctionConventionTest + { + [ConditionalFact] + public void Configures_return_entity_as_not_mapped_keyless() + { + var modelBuilder = CreateModelBuilder(); + modelBuilder.HasDbFunction(typeof(QueryableDbFunctionConventionTest).GetMethod( + nameof(GetKeylessEntities), + BindingFlags.NonPublic | BindingFlags.Static)); + + var model = modelBuilder.FinalizeModel(); + + var entityType = model.FindEntityType(typeof(KeylessEntity)); + + Assert.Null(entityType.FindPrimaryKey()); + Assert.Empty(entityType.GetViewOrTableMappings()); + } + + [ConditionalFact] + public void Finds_existing_entity_type() + { + var modelBuilder = CreateModelBuilder(); + modelBuilder.Entity().ToTable("TestTable").HasKey(e => e.Name); + modelBuilder.HasDbFunction(typeof(QueryableDbFunctionConventionTest).GetMethod( + nameof(GetEntities), + BindingFlags.NonPublic | BindingFlags.Static)); + + var model = modelBuilder.FinalizeModel(); + + var entityType = model.FindEntityType(typeof(TestEntity)); + + Assert.Equal(nameof(TestEntity.Name), entityType.FindPrimaryKey().Properties.Single().Name); + Assert.Equal("TestTable", entityType.GetViewOrTableMappings().Single().Table.Name); + } + + [ConditionalFact] + public void Throws_when_adding_a_function_returning_an_owned_type() + { + var modelBuilder = CreateModelBuilder(); + modelBuilder.Owned(); + modelBuilder.HasDbFunction(typeof(QueryableDbFunctionConventionTest).GetMethod( + nameof(GetKeylessEntities), + BindingFlags.NonPublic | BindingFlags.Static)); + + Assert.Equal( + RelationalStrings.DbFunctionInvalidIQueryableOwnedReturnType( + nameof(GetKeylessEntities), typeof(IQueryable).ShortDisplayName()), + Assert.Throws(() => modelBuilder.FinalizeModel()).Message); + } + + [ConditionalFact] + public void Throws_when_adding_a_function_returning_an_existing_owned_type() + { + var modelBuilder = CreateModelBuilder(); + modelBuilder.Entity().OwnsOne(e => e.KeylessEntity); + modelBuilder.HasDbFunction(typeof(QueryableDbFunctionConventionTest).GetMethod( + nameof(GetKeylessEntities), + BindingFlags.NonPublic | BindingFlags.Static)); + + Assert.Equal( + RelationalStrings.DbFunctionInvalidIQueryableOwnedReturnType( + nameof(GetKeylessEntities), typeof(IQueryable).ShortDisplayName()), + Assert.Throws(() => modelBuilder.FinalizeModel()).Message); + } + + [ConditionalFact] + public void Throws_when_adding_a_function_returning_a_scalar() + { + var modelBuilder = CreateModelBuilder(); + modelBuilder.HasDbFunction(typeof(QueryableDbFunctionConventionTest).GetMethod( + nameof(GetScalars), + BindingFlags.NonPublic | BindingFlags.Static)); + + Assert.Equal( + RelationalStrings.DbFunctionInvalidIQueryableReturnType( + nameof(GetScalars), typeof(IQueryable).ShortDisplayName()), + Assert.Throws(() => modelBuilder.FinalizeModel()).Message); + } + + private static ModelBuilder CreateModelBuilder() => RelationalTestHelpers.Instance.CreateConventionBuilder(); + + private static IQueryable GetEntities(int id) + => throw new NotImplementedException(); + + private static IQueryable GetKeylessEntities(int id) + => throw new NotImplementedException(); + + private static IQueryable GetScalars(int id) + => throw new NotImplementedException(); + + private class TestEntity + { + public int Id { get; set; } + + public string Name { get; set; } + + [NotMapped] + public KeylessEntity KeylessEntity { get; set; } + } + + private class KeylessEntity + { + public string Name { get; set; } + } + } +} diff --git a/test/EFCore.Relational.Tests/Metadata/DbFunctionMetadataTests.cs b/test/EFCore.Relational.Tests/Metadata/DbFunctionMetadataTests.cs index 9ea209c507d..89c670029c6 100644 --- a/test/EFCore.Relational.Tests/Metadata/DbFunctionMetadataTests.cs +++ b/test/EFCore.Relational.Tests/Metadata/DbFunctionMetadataTests.cs @@ -327,6 +327,7 @@ var methodInfo Assert.Equal("InstancePublicBase", dbFunc.Name); Assert.Equal(typeof(int), dbFunc.MethodInfo.ReturnType); + Assert.Equal(typeof(int), dbFunc.ReturnType); } [ConditionalFact] @@ -574,6 +575,26 @@ public void Add_method_generic_not_supported_throws() Assert.Throws(() => modelBuilder.HasDbFunction(MethodHmi)).Message); } + [ConditionalFact] + public void DbFunction_HasName() + { + var modelBuilder = GetModelBuilder(); + + var methodA = typeof(OuterA.Inner).GetMethod(nameof(OuterA.Inner.Min)); + var methodB = typeof(OuterB.Inner).GetMethod(nameof(OuterB.Inner.Min)); + + var funcA = modelBuilder.HasDbFunction(methodA); + var funcB = modelBuilder.HasDbFunction(methodB); + + funcA.HasName("MinA"); + + modelBuilder.FinalizeModel(); + + Assert.Equal("MinA", funcA.Metadata.Name); + Assert.Equal("Min", funcB.Metadata.Name); + Assert.NotEqual(funcA.Metadata.Name, funcB.Metadata.Name); + } + [ConditionalFact] public virtual void Set_empty_function_name_throws() { @@ -598,6 +619,26 @@ public void DbParameters_load_no_parameters() Assert.Equal(0, dbFunc.Parameters.Count); } + [ConditionalFact] + public void DbFunction_IsQueryable() + { + var modelBuilder = GetModelBuilder(); + + var queryableNoParams + = typeof(MyDerivedContext) + .GetRuntimeMethod(nameof(MyDerivedContext.QueryableNoParams), Array.Empty()); + + IDbFunction function = modelBuilder.HasDbFunction(queryableNoParams).Metadata; + + var model = modelBuilder.FinalizeModel(); + + function = model.FindDbFunction(function.ModelName); + var entityType = model.FindEntityType(typeof(Foo)); + + Assert.True(function.IsQueryable); + Assert.Same(entityType, function.QueryableEntityType); + } + [ConditionalFact] public void DbParameters_invalid_parameter_name_throws() { @@ -693,26 +734,6 @@ public void DbParameters_StoreType() Assert.Equal(typeof(int), dbFunc.Parameters[1].ClrType); } - [ConditionalFact] - public void DbFunction_Annotation_FullName() - { - var modelBuilder = GetModelBuilder(); - - var methodA = typeof(OuterA.Inner).GetMethod(nameof(OuterA.Inner.Min)); - var methodB = typeof(OuterB.Inner).GetMethod(nameof(OuterB.Inner.Min)); - - var funcA = modelBuilder.HasDbFunction(methodA); - var funcB = modelBuilder.HasDbFunction(methodB); - - funcA.HasName("MinA"); - - modelBuilder.FinalizeModel(); - - Assert.Equal("MinA", funcA.Metadata.Name); - Assert.Equal("Min", funcB.Metadata.Name); - Assert.NotEqual(funcA.Metadata.Name, funcB.Metadata.Name); - } - [ConditionalFact] public void DbFunction_Queryable_custom_translation() { @@ -720,10 +741,14 @@ public void DbFunction_Queryable_custom_translation() var methodInfo = typeof(TestMethods).GetMethod(nameof(TestMethods.MethodJ)); var dbFunctionBuilder = modelBuilder.HasDbFunction(methodInfo); - ((IConventionDbFunctionBuilder)dbFunctionBuilder).HasTranslation(args => new SqlFragmentExpression("Empty")); + Assert.False(dbFunctionBuilder.GetInfrastructure() + .CanSetTranslation(args => new SqlFragmentExpression("Empty"), fromDataAnnotation: true)); Assert.Null(dbFunctionBuilder.Metadata.Translation); - ((IConventionDbFunctionBuilder)dbFunctionBuilder) + dbFunctionBuilder.GetInfrastructure().HasTranslation(args => new SqlFragmentExpression("Empty")); + Assert.Null(dbFunctionBuilder.Metadata.Translation); + + dbFunctionBuilder.GetInfrastructure() .HasTranslation(args => new SqlFragmentExpression("Empty"), fromDataAnnotation: true); Assert.Null(dbFunctionBuilder.Metadata.Translation); @@ -733,9 +758,14 @@ public void DbFunction_Queryable_custom_translation() var dbFunction = dbFunctionBuilder.Metadata; - Assert.Null(((IConventionDbFunction)dbFunction).SetTranslation(args => new SqlFragmentExpression("Empty"))); - Assert.Null(((IConventionDbFunction)dbFunction) - .SetTranslation(args => new SqlFragmentExpression("Empty"), fromDataAnnotation: true)); + Assert.Equal(RelationalStrings.DbFunctionQueryableCustomTranslation(methodInfo.DisplayName()), + Assert.Throws( + () => ((IConventionDbFunction)dbFunction).SetTranslation(args => new SqlFragmentExpression("Empty"))).Message); + + Assert.Equal(RelationalStrings.DbFunctionQueryableCustomTranslation(methodInfo.DisplayName()), + Assert.Throws( + () => ((IConventionDbFunction)dbFunction) + .SetTranslation(args => new SqlFragmentExpression("Empty"), fromDataAnnotation: true)).Message); Assert.Equal(RelationalStrings.DbFunctionQueryableCustomTranslation(methodInfo.DisplayName()), Assert.Throws( diff --git a/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs b/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs index 2fe54a628fb..25d28b7688c 100644 --- a/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs +++ b/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations.Operations; @@ -20,26 +21,12 @@ namespace Microsoft.EntityFrameworkCore.Migrations.Internal { public class MigrationsModelDifferTest : MigrationsModelDifferTestBase { - private class TestQueryType - { - public string Something { get; set; } - } - - [ConditionalFact(Skip = "Issue#20051")] - public void Model_differ_does_not_detect_queryable_function_result_type() - { - Execute( - _ => { }, - modelBuilder => modelBuilder.Entity().ToQueryableFunctionResultType(), - result => Assert.Equal(0, result.Count)); - } - [ConditionalFact] public void Model_differ_does_not_detect_views() { Execute( _ => { }, - modelBuilder => modelBuilder.Entity().HasNoKey().ToView("Vista", "dbo"), + modelBuilder => modelBuilder.Entity().HasNoKey().ToView("Vista", "dbo"), result => Assert.Equal(0, result.Count)); } @@ -78,8 +65,8 @@ public void Model_differ_does_not_detect_queries() DbContext context = null; Execute( _ => { }, - modelBuilder => modelBuilder.Entity().HasNoKey().ToQuery( - () => context.Set().FromSqlRaw("SELECT * FROM Vista")), + modelBuilder => modelBuilder.Entity().HasNoKey().ToQuery( + () => context.Set().FromSqlRaw("SELECT * FROM Vista")), result => Assert.Empty(result)); } @@ -8497,6 +8484,27 @@ public void Construction_of_shadow_values_buffer_account_for_shadow_navigations_ ops => { }); } + private class TestKeylessType + { + public string Something { get; set; } + } + + private static IQueryable GetCountByYear(int id) + => throw new NotImplementedException(); + + [ConditionalFact] + public void Model_differ_does_not_detect_queryable_function_result_type() + { + Execute( + _ => { }, + modelBuilder => + modelBuilder.HasDbFunction(typeof(MigrationsModelDifferTest).GetMethod( + nameof(GetCountByYear), + BindingFlags.NonPublic | BindingFlags.Static)), + result => Assert.Equal(0, result.Count), + skipSourceConventions: true); + } + protected override TestHelpers TestHelpers => RelationalTestHelpers.Instance; } } diff --git a/test/EFCore.Specification.Tests/TestUtilities/TestHelpers.cs b/test/EFCore.Specification.Tests/TestUtilities/TestHelpers.cs index d0f090f2f24..a217f19bb03 100644 --- a/test/EFCore.Specification.Tests/TestUtilities/TestHelpers.cs +++ b/test/EFCore.Specification.Tests/TestUtilities/TestHelpers.cs @@ -202,8 +202,7 @@ public IMutableModel BuildModelFor() public ModelBuilder CreateConventionBuilder(bool skipValidation = false) { - var conventionSet = CreateContextServices().GetRequiredService() - .CreateConventionSet(); + var conventionSet = CreateConventionSetBuilder().CreateConventionSet(); if (skipValidation) { @@ -213,6 +212,9 @@ public ModelBuilder CreateConventionBuilder(bool skipValidation = false) return new ModelBuilder(conventionSet); } + public virtual IConventionSetBuilder CreateConventionSetBuilder() + => CreateContextServices().GetRequiredService(); + public ModelBuilder CreateConventionBuilder( DiagnosticsLogger modelLogger, DiagnosticsLogger validationLogger) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/UdfDbFunctionSqlServerTests.cs b/test/EFCore.SqlServer.FunctionalTests/Query/UdfDbFunctionSqlServerTests.cs index 5ad4f8a43a1..acf87173385 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/UdfDbFunctionSqlServerTests.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/UdfDbFunctionSqlServerTests.cs @@ -696,12 +696,11 @@ public override void QF_OuterApply_Correlated_Select_Entity() base.QF_OuterApply_Correlated_Select_Entity(); AssertSql( - @"SELECT [c].[Id], [c].[FirstName], [c].[LastName], [c].[CreditCard_CreditCardType], [c].[CreditCard_Number], [p].[CustomerId], [p].[Id], [p].[Number], [p].[PhoneType] + @"SELECT [c].[Id], [c].[FirstName], [c].[LastName] FROM [Customers] AS [c] OUTER APPLY [dbo].[GetCustomerOrderCountByYear]([c].[Id]) AS [o] -LEFT JOIN [PhoneInformation] AS [p] ON [c].[Id] = [p].[CustomerId] WHERE [o].[Year] = 2000 -ORDER BY [c].[Id], [o].[Year], [p].[CustomerId], [p].[Id]"); +ORDER BY [c].[Id], [o].[Year]"); } public override void QF_OuterApply_Correlated_Select_Anonymous() @@ -753,59 +752,6 @@ FROM [dbo].[GetOrdersWithMultipleProducts]([c].[Id]) AS [m] ORDER BY [c].[Id], [t].[OrderId], [t].[Id]"); } - public override void QF_Owned_Many_NoTracking_Select_Owned() - { - base.QF_Owned_Many_NoTracking_Select_Owned(); - - AssertSql(@"SELECT [p].[CustomerId], [p].[Id], [p].[Number], [p].[PhoneType] -FROM [Customers] AS [c] -CROSS APPLY [dbo].[GetPhoneInformation]([c].[Id], N'234') AS [p] -ORDER BY [p].[Number]"); - } - - public override void QF_Owned_Many_Tracked_Select_Owned() - { - base.QF_Owned_Many_Tracked_Select_Owned(); - - AssertSql( - @"SELECT [c].[Id], [c].[FirstName], [c].[LastName], [c].[CreditCard_CreditCardType], [c].[CreditCard_Number], [p].[CustomerId], [p].[Id], [p0].[CustomerId], [p0].[Id], [p0].[Number], [p0].[PhoneType] -FROM [Customers] AS [c] -CROSS APPLY [dbo].[GetPhoneInformation]([c].[Id], N'234') AS [p] -LEFT JOIN [PhoneInformation] AS [p0] ON [c].[Id] = [p0].[CustomerId] -ORDER BY [c].[Id], [p].[CustomerId], [p].[Id], [p0].[CustomerId], [p0].[Id]"); - } - - public override void QF_Owned_One_NoTracking_Select_Owned() - { - base.QF_Owned_One_NoTracking_Select_Owned(); - - AssertSql( - @"SELECT [t].[Id], [t].[CreditCard_CreditCardType], [t].[CreditCard_Number] -FROM [Customers] AS [c] -CROSS APPLY ( - SELECT [c0].[Id], [c0].[CreditCard_CreditCardType], [c0].[CreditCard_Number] - FROM [dbo].[GetCreditCards]([c].[Id]) AS [c0] - WHERE [c0].[CreditCard_CreditCardType] IS NOT NULL -) AS [t] -ORDER BY [t].[CreditCard_Number]"); - } - - public override void QF_Owned_One_Tracked() - { - base.QF_Owned_One_Tracked(); - - AssertSql( - @"SELECT [c].[Id], [c].[FirstName], [c].[LastName], [c].[CreditCard_CreditCardType], [c].[CreditCard_Number], [t].[Id], [t].[CreditCard_CreditCardType], [t].[CreditCard_Number], [p].[CustomerId], [p].[Id], [p].[Number], [p].[PhoneType] -FROM [Customers] AS [c] -CROSS APPLY ( - SELECT [c0].[Id], [c0].[CreditCard_CreditCardType], [c0].[CreditCard_Number] - FROM [dbo].[GetCreditCards]([c].[Id]) AS [c0] - WHERE [c0].[CreditCard_CreditCardType] IS NOT NULL -) AS [t] -LEFT JOIN [PhoneInformation] AS [p] ON [c].[Id] = [p].[CustomerId] -ORDER BY [t].[CreditCard_Number], [c].[Id], [t].[Id], [p].[CustomerId], [p].[Id]"); - } - #endregion public class SqlServer : UdfFixtureBase @@ -959,46 +905,6 @@ having count(productId) > 1 end"); - context.Database.ExecuteSqlRaw( - @"create function [dbo].[GetCreditCards] (@customerId int) - returns @cc table - ( - id int not null, - CreditCard_CreditCardType int null, - CreditCard_Number nvarchar(max) - ) - as - begin - insert into @cc - select id, CreditCard_CreditCardType, CreditCard_Number - from customers c - where c.CreditCard_CreditCardType is not null and c.CreditCard_Number is not null - and c.id = @customerId - - return - end"); - - context.Database.ExecuteSqlRaw( - @"create function [dbo].[GetPhoneInformation] (@customerId int, @areaCode nvarchar(3)) - returns @pn table - ( - PhoneType int, - Number nvarchar(max), - CustomerId int not null, - Id int not null - ) - as - begin - insert into @pn - select PhoneType, Number, CustomerId, Id - from PhoneInformation pi - where pi.customerId = @customerId - and charindex(@areaCode, pi.number) = 1 - - return - end"); - - context.Database.ExecuteSqlRaw( @"create function [dbo].[AddValues] (@a int, @b int) returns int diff --git a/test/EFCore.Tests/ApiConsistencyTestBase.cs b/test/EFCore.Tests/ApiConsistencyTestBase.cs index 6291a5a7234..19900486b14 100644 --- a/test/EFCore.Tests/ApiConsistencyTestBase.cs +++ b/test/EFCore.Tests/ApiConsistencyTestBase.cs @@ -68,6 +68,9 @@ public void Metadata_types_have_expected_structure() "\r\n-- Errors: --\r\n" + string.Join(Environment.NewLine, errors)); } + private static readonly string MetadataNamespace = typeof(IModel).Namespace; + private static readonly string MetadataBuilderNamespace = typeof(IConventionModelBuilder).Namespace; + private string ValidateMetadata(KeyValuePair types) { var readonlyType = types.Key; @@ -83,11 +86,12 @@ private string ValidateMetadata(KeyValuePair types) return $"{mutableType.Name} should derive from {readonlyType.Name}"; } - if (typeof(IAnnotation) != readonlyType) + if (typeof(IAnnotation) != readonlyType + && typeof(IAnnotatable) != readonlyType) { if (!typeof(IAnnotatable).IsAssignableFrom(readonlyType)) { - return $"{mutableType.Name} should derive from IAnnotatable"; + return $"{readonlyType.Name} should derive from IAnnotatable"; } if (!typeof(IMutableAnnotatable).IsAssignableFrom(mutableType)) @@ -97,7 +101,34 @@ private string ValidateMetadata(KeyValuePair types) if (!typeof(IConventionAnnotatable).IsAssignableFrom(conventionType)) { - return $"{mutableType.Name} should derive from IConventionAnnotatable"; + return $"{conventionType.Name} should derive from IConventionAnnotatable"; + } + + if (conventionBuilderType != null + && !typeof(IConventionAnnotatableBuilder).IsAssignableFrom(conventionBuilderType)) + { + return $"{conventionBuilderType.Name} should derive from IConventionAnnotatableBuilder"; + } + + if (readonlyType.Namespace != MetadataNamespace) + { + return $"{readonlyType.Name} is expected to be in the {MetadataNamespace} namespace"; + } + + if (mutableType.Namespace != MetadataNamespace) + { + return $"{mutableType.Name} is expected to be in the {MetadataNamespace} namespace"; + } + + if (conventionType.Namespace != MetadataNamespace) + { + return $"{conventionType.Name} is expected to be in the {MetadataNamespace} namespace"; + } + + if (conventionBuilderType != null + && conventionBuilderType.Namespace != MetadataBuilderNamespace) + { + return $"{conventionBuilderType.Name} is expected to be in the {MetadataBuilderNamespace} namespace"; } }