Skip to content

Take 2 - Add support for user defined scalar functions #8507

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 9, 2017
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs
Original file line number Diff line number Diff line change
@@ -48,6 +48,8 @@ public virtual void Generate(string builderName, IModel model, IndentedStringBui
{
GenerateFluentApiForAnnotation(ref annotations, RelationalAnnotationNames.DefaultSchema, nameof(RelationalModelBuilderExtensions.HasDefaultSchema), stringBuilder);

IgnoreAnnotationTypes(annotations, RelationalAnnotationNames.DbFunction);

GenerateAnnotations(annotations, stringBuilder);
}

@@ -609,6 +611,18 @@ protected virtual void IgnoreAnnotations(
}
}

protected virtual void IgnoreAnnotationTypes(
[NotNull] IList<IAnnotation> annotations, [NotNull] params string[] annotationPrefixes)
{
Check.NotNull(annotations, nameof(annotations));
Check.NotNull(annotationPrefixes, nameof(annotationPrefixes));

foreach(var ignoreAnnotation in annotations.Where(a => annotationPrefixes.Any(pre => a.Name.StartsWith(pre, StringComparison.OrdinalIgnoreCase))).ToList())
{
annotations.Remove(ignoreAnnotation);
}
}

protected virtual void GenerateAnnotations(
[NotNull] IReadOnlyList<IAnnotation> annotations, [NotNull] IndentedStringBuilder stringBuilder)
{
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using JetBrains.Annotations;
using Remotion.Linq.Parsing.ExpressionVisitors.TreeEvaluation;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Infrastructure.Internal;
using Microsoft.EntityFrameworkCore.Query;
@@ -64,6 +65,7 @@ public static IServiceCollection AddEntityFrameworkInMemoryDatabase([NotNull] th
.TryAdd<IQueryContextFactory, InMemoryQueryContextFactory>()
.TryAdd<IEntityQueryModelVisitorFactory, InMemoryQueryModelVisitorFactory>()
.TryAdd<IEntityQueryableExpressionVisitorFactory, InMemoryEntityQueryableExpressionVisitorFactory>()
.TryAdd<IEvaluatableExpressionFilter, EvaluatableExpressionFilter>()
.TryAddProviderSpecificServices(b => b
.TryAddSingleton<IInMemoryStoreCache, InMemoryStoreCache>()
.TryAddSingleton<IInMemoryTableFactory, InMemoryTableFactory>()
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.TestModels.Northwind;

namespace Microsoft.EntityFrameworkCore
{
public class NorthwindDbFunctionContext : NorthwindContext
{
public NorthwindDbFunctionContext(DbContextOptions options, QueryTrackingBehavior queryTrackingBehavior = QueryTrackingBehavior.TrackAll)
: base(options, queryTrackingBehavior)
{
}

public enum ReportingPeriod
{
Winter = 0,
Spring,
Summer,
Fall
}

public static int MyCustomLength(string s)
{
throw new Exception();
}

[DbFunction(Schema = "dbo", Name = "EmployeeOrderCount")]
public static int EmployeeOrderCount(int employeeId)
{
throw new NotImplementedException();
}

[DbFunction(Schema = "dbo", Name = "EmployeeOrderCount")]
public static int EmployeeOrderCountWithClient(int employeeId)
{
switch (employeeId)
{
case 3: return 127;
default: return 1;
}
}

[DbFunction(Schema = "dbo")]
public static bool IsTopEmployee(int employeeId)
{
throw new NotImplementedException();
}

[DbFunction(Schema = "dbo")]
public static int GetEmployeeWithMostOrdersAfterDate(DateTime? startDate)
{
throw new NotImplementedException();
}

[DbFunction(Schema = "dbo")]
public static DateTime? GetReportingPeriodStartDate(ReportingPeriod periodId)
{
throw new NotImplementedException();
}

[DbFunction(Schema = "dbo")]
public static string StarValue(int starCount, int value)
{
throw new NotImplementedException();
}

[DbFunction(Name = "StarValue", Schema = "dbo")]
public static string StarValueAlternateParamOrder([DbFunctionParameter(ParameterIndex = 1)]int value, [DbFunctionParameter(ParameterIndex = 0)]int starCount)
{
throw new NotImplementedException();
}

[DbFunction(Schema = "dbo")]
public static int AddValues(int a, int b)
{
throw new NotImplementedException();
}

[DbFunction(Schema = "dbo")]
public static DateTime GetBestYearEver()
{
throw new NotImplementedException();
}
}
}
Original file line number Diff line number Diff line change
@@ -21,6 +21,7 @@
using Microsoft.EntityFrameworkCore.Update.Internal;
using Microsoft.EntityFrameworkCore.ValueGeneration;
using Microsoft.Extensions.DependencyInjection;
using Remotion.Linq.Parsing.ExpressionVisitors.TreeEvaluation;

namespace Microsoft.EntityFrameworkCore.Infrastructure
{
@@ -65,7 +66,7 @@ private static readonly IDictionary<Type, ServiceCharacteristics> _relationalSer
{ typeof(ISqlTranslatingExpressionVisitorFactory), new ServiceCharacteristics(ServiceLifetime.Singleton) },
{ typeof(IUpdateSqlGenerator), new ServiceCharacteristics(ServiceLifetime.Singleton) },
{ typeof(IMemberTranslator), new ServiceCharacteristics(ServiceLifetime.Singleton) },
{ typeof(IMethodCallTranslator), new ServiceCharacteristics(ServiceLifetime.Singleton) },
{ typeof(ICompositeMethodCallTranslator), new ServiceCharacteristics(ServiceLifetime.Singleton) },
{ typeof(IQuerySqlGeneratorFactory), new ServiceCharacteristics(ServiceLifetime.Singleton) },
{ typeof(ICommandBatchPreparer), new ServiceCharacteristics(ServiceLifetime.Scoped) },
{ typeof(IModificationCommandBatchFactory), new ServiceCharacteristics(ServiceLifetime.Scoped) },
@@ -77,7 +78,7 @@ private static readonly IDictionary<Type, ServiceCharacteristics> _relationalSer
{ typeof(IRelationalConnection), new ServiceCharacteristics(ServiceLifetime.Scoped) },
{ typeof(IRelationalDatabaseCreator), new ServiceCharacteristics(ServiceLifetime.Scoped) },
{ typeof(IHistoryRepository), new ServiceCharacteristics(ServiceLifetime.Scoped) },
{ typeof(INamedConnectionStringResolver), new ServiceCharacteristics(ServiceLifetime.Scoped) }
{ typeof(INamedConnectionStringResolver), new ServiceCharacteristics(ServiceLifetime.Scoped) },
};

/// <summary>
@@ -115,6 +116,7 @@ public override EntityFrameworkServicesBuilder TryAddCoreServices()
TryAdd<IMigrationsIdGenerator, MigrationsIdGenerator>();
TryAdd<IKeyValueIndexFactorySource, KeyValueIndexFactorySource>();
TryAdd<IModelSource, RelationalModelSource>();
TryAdd<IModelCustomizer, RelationalModelCustomizer>();
TryAdd<IMigrationsAnnotationProvider, MigrationsAnnotationProvider>();
TryAdd<IModelValidator, RelationalModelValidator>();
TryAdd<IMigrator, Migrator>();
@@ -150,6 +152,7 @@ public override EntityFrameworkServicesBuilder TryAddCoreServices()
TryAdd<IExpressionFragmentTranslator, RelationalCompositeExpressionFragmentTranslator>();
TryAdd<ISqlTranslatingExpressionVisitorFactory, SqlTranslatingExpressionVisitorFactory>();
TryAdd<INamedConnectionStringResolver, NamedConnectionStringResolver>();
TryAdd<IEvaluatableExpressionFilter, RelationalEvaluatableExpressionFilter>();

ServiceCollectionMap.GetInfrastructure()
.AddDependencySingleton<RelationalCompositeMemberTranslatorDependencies>()
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Linq;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Metadata.Internal;

namespace Microsoft.EntityFrameworkCore.Infrastructure.Internal
{
@@ -18,25 +15,5 @@ public RelationalModelSource([NotNull] ModelSourceDependencies dependencies)
: base(dependencies)
{
}

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
protected override void FindSets(ModelBuilder modelBuilder, DbContext context)
{
base.FindSets(modelBuilder, context);

var sets = Dependencies.SetFinder.CreateClrTypeDbSetMapping(context);

foreach (var entityType in modelBuilder.Model.GetEntityTypes().Cast<EntityType>())
{
if (entityType.BaseType == null
&& sets.ContainsKey(entityType.ClrType))
{
entityType.Builder.Relational(ConfigurationSource.Convention).ToTable(sets[entityType.ClrType].Name);
}
}
}
}
}
72 changes: 72 additions & 0 deletions src/EFCore.Relational/Infrastructure/RelationalModelCustomizer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Linq;
using System.Reflection;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Utilities;

namespace Microsoft.EntityFrameworkCore.Infrastructure
{
/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public class RelationalModelCustomizer : ModelCustomizer
{
public RelationalModelCustomizer([NotNull] ModelCustomizerDependencies dependencies)
: base(dependencies)
{
}

public override void Customize(ModelBuilder modelBuilder, DbContext dbContext)
{
FindDbFunctions(modelBuilder, dbContext);

base.Customize(modelBuilder, dbContext);
}

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
protected virtual void FindDbFunctions([NotNull] ModelBuilder modelBuilder, [NotNull] DbContext context)
{
Check.NotNull(modelBuilder, nameof(modelBuilder));
Check.NotNull(context, nameof(context));

var functions = context.GetType().GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy)
.Where(mi => mi.IsStatic
&& mi.IsPublic
&& mi.GetCustomAttributes(typeof(DbFunctionAttribute)).Any());

foreach (var function in functions)
{
modelBuilder.HasDbFunction(function);
}
}

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
protected override void FindSets(ModelBuilder modelBuilder, DbContext context)
{
base.FindSets(modelBuilder, context);

var sets = Dependencies.SetFinder.CreateClrTypeDbSetMapping(context);

foreach (var entityType in modelBuilder.Model.GetEntityTypes().Cast<EntityType>())
{
if (entityType.BaseType == null
&& sets.ContainsKey(entityType.ClrType))
{
entityType.Builder.Relational(ConfigurationSource.Convention).ToTable(sets[entityType.ClrType].Name);
}
}
}
}
}
48 changes: 48 additions & 0 deletions src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Metadata;
@@ -56,6 +57,53 @@ public override void Validate(IModel model)
ValidateDataTypes(model);
ValidateDefaultValuesOnKeys(model);
ValidateBoolsWithDefaults(model);
ValidateDbFunctions(model);
}

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
protected virtual void ValidateDbFunctions([NotNull] IModel model)
{
foreach (var dbFunction in model.Relational().DbFunctions)
{
if (string.IsNullOrEmpty(dbFunction.Name))
throw new InvalidOperationException(CoreStrings.DbFunctionNameEmpty());

var paramIndexes = dbFunction.Parameters.Select(fp => fp.Index).ToArray();
var dbFuncName = $"{dbFunction.MethodInfo.DeclaringType?.Name}.{dbFunction.MethodInfo.Name}";

if (paramIndexes.Distinct().Count() != dbFunction.Parameters.Count)
throw new InvalidOperationException(CoreStrings.DbFunctionDuplicateIndex(dbFuncName));

if (Enumerable.Range(0, paramIndexes.Length).Except(paramIndexes).Any())
throw new InvalidOperationException(CoreStrings.DbFunctionNonContinuousIndex(dbFuncName));

if (dbFunction.MethodInfo.IsStatic == false
&& dbFunction.MethodInfo.DeclaringType.GetTypeInfo().IsSubclassOf(typeof(DbContext)))
{
throw new InvalidOperationException(CoreStrings.DbFunctionDbContextMethodMustBeStatic(dbFuncName));
}

if(dbFunction.TranslateCallback == null)
{
if (dbFunction.ReturnType == null || RelationalDependencies.TypeMapper.IsTypeMapped(dbFunction.ReturnType) == false)
throw new InvalidOperationException(CoreStrings.DbFunctionInvalidReturnType(dbFunction.MethodInfo, dbFunction.ReturnType));

foreach (var parameter in dbFunction.Parameters)
{
if (parameter.ParameterType == null || RelationalDependencies.TypeMapper.IsTypeMapped(parameter.ParameterType) == false)
{
throw new InvalidOperationException(
CoreStrings.DbFunctionInvalidParameterType(
dbFunction.MethodInfo,
parameter.Name,
dbFunction.ReturnType));
}
}
}
}
}

/// <summary>
Original file line number Diff line number Diff line change
@@ -63,6 +63,8 @@ public virtual ConventionSet AddConventions(ConventionSet conventionSet)

conventionSet.ModelBuiltConventions.Add(new RelationalTypeMappingConvention(Dependencies.TypeMapper));

conventionSet.ModelAnnotationChangedConventions.Add(new RelationalDbFunctionConvention());

return conventionSet;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Linq;
using System.Reflection;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Utilities;

namespace Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal
{
public class RelationalDbFunctionConvention : IModelAnnotationChangedConvention
{
public virtual Annotation Apply(InternalModelBuilder modelBuilder, string name, Annotation annotation, Annotation oldAnnotation)
{
Check.NotNull(modelBuilder, nameof(modelBuilder));
Check.NotNull(name, nameof(name));

if (name.StartsWith(RelationalAnnotationNames.DbFunction, StringComparison.OrdinalIgnoreCase)
&& annotation != null
&& oldAnnotation == null)
{
var dbFunctionBuilder = new InternalDbFunctionBuilder((DbFunction)annotation.Value);

var dbFuncMethodInfo = dbFunctionBuilder.Metadata.MethodInfo;

var dbFuncAttribute = dbFuncMethodInfo.GetCustomAttributes<DbFunctionAttribute>().SingleOrDefault();

dbFunctionBuilder.HasName(dbFuncAttribute?.Name ?? dbFuncMethodInfo.Name, ConfigurationSource.Convention);
dbFunctionBuilder.HasSchema(dbFuncAttribute?.Schema ?? modelBuilder.Metadata.Relational().DefaultSchema, ConfigurationSource.Convention);

var parameters = dbFuncMethodInfo.GetParameters()
.Where(p => p.ParameterType != typeof(DbFunctions))
.Select((pi, i) => new
{
ParameterIndex = i,
ParameterInfo = pi,
DbFuncParamAttr = pi.GetCustomAttributes<DbFunctionParameterAttribute>().SingleOrDefault(),
pi.ParameterType
});

foreach (var p in parameters)
{
var paramBuilder = dbFunctionBuilder.HasParameter(p.ParameterInfo.Name, ConfigurationSource.Convention);

paramBuilder.HasIndex(p.DbFuncParamAttr?.ParameterIndex ?? p.ParameterIndex, ConfigurationSource.Convention);
paramBuilder.HasType(p.ParameterType, ConfigurationSource.Convention);
}
}

return annotation;
}
}
}
58 changes: 58 additions & 0 deletions src/EFCore.Relational/Metadata/DbFunctionAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Utilities;

namespace Microsoft.EntityFrameworkCore.Metadata
{
/// <summary>
/// Defines a user defined database function
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public sealed class DbFunctionAttribute : Attribute
{
private string _name;

/// <summary>
/// Defines a user defined database function
/// </summary>
public DbFunctionAttribute()
{
}

/// <summary>
/// Defines a user defined database function
/// </summary>
/// <param name="name">The name of the function in the underlying datastore.</param>
/// <param name="schema">The schema where the function lives in the underlying datastore.</param>
public DbFunctionAttribute([NotNull] string name, [CanBeNull] string schema)
{
Check.NotEmpty(name, nameof(name));

Schema = schema;
Name = name;
}

/// <summary>
/// The name of the function in the underlying datastore.
/// </summary>
public string Name
{
get { return _name; }

[param: NotNull]
set
{
Check.NotNull(value, nameof(Name));
_name = value;
}
}

/// <summary>
/// The schema where the function lives in the underlying datastore.
/// </summary>
public string Schema { get; [param: CanBeNull] set; }
}
}
102 changes: 102 additions & 0 deletions src/EFCore.Relational/Metadata/DbFunctionBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Query.Expressions;
using Microsoft.EntityFrameworkCore.Utilities;

namespace Microsoft.EntityFrameworkCore.Metadata
{
/// <summary>
/// <para>
/// Provides a simple API for configuring a <see cref="Metadata" />.
/// </para>
/// <para>
/// Instances of this class are returned from methods when using the <see cref="ModelBuilder" /> API
/// and it is not designed to be directly constructed in your application code.
/// </para>
/// </summary>
///
public class DbFunctionBuilder
{
private readonly InternalDbFunctionBuilder _builder;

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public DbFunctionBuilder([NotNull] DbFunction dbFunction)
{
Check.NotNull(dbFunction, nameof(dbFunction));

_builder = new InternalDbFunctionBuilder(dbFunction);
}

public virtual IMutableDbFunction Metadata => _builder.Metadata;

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual DbFunctionBuilder HasSchema([CanBeNull]string schema)
{
_builder.HasSchema(schema, ConfigurationSource.Explicit);

return this;
}

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual DbFunctionBuilder HasName([NotNull] string name)
{
Check.NotNull(name, nameof(name));

_builder.HasName(name, ConfigurationSource.Explicit);

return this;
}

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual DbFunctionParameterBuilder HasParameter([NotNull] string name)
{
Check.NotNull(name, nameof(name));

return new DbFunctionParameterBuilder(_builder.HasParameter(name, ConfigurationSource.Explicit));
}

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual DbFunctionBuilder HasReturnType([NotNull] Type returnType)
{
Check.NotNull(returnType, nameof(returnType));

_builder.HasReturnType(returnType, ConfigurationSource.Explicit);

return this;
}

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual DbFunctionBuilder TranslateWith([NotNull] Func<IReadOnlyCollection<Expression>, IDbFunction, SqlFunctionExpression> translateCallback)
{
Check.NotNull(translateCallback, nameof(translateCallback));

_builder.TranslateWith(translateCallback);

return this;
}
}
}
20 changes: 20 additions & 0 deletions src/EFCore.Relational/Metadata/DbFunctionParameterAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using JetBrains.Annotations;

namespace Microsoft.EntityFrameworkCore.Metadata
{
/// <summary>
/// Define a parameter for a user defined database function
/// </summary>
[AttributeUsage(AttributeTargets.Parameter)]
public sealed class DbFunctionParameterAttribute : Attribute
{
/// <summary>
/// Sets the index order for this parameter on the parent function
/// </summary>
public int ParameterIndex { get; [param: NotNull] set; }
}
}
62 changes: 62 additions & 0 deletions src/EFCore.Relational/Metadata/DbFunctionParameterBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Utilities;

namespace Microsoft.EntityFrameworkCore.Metadata
{
/// <summary>
/// <para>
/// Provides a simple API for configuring an <see cref="DbFunctionParameter" />.
/// </para>
/// <para>
/// Instances of this class are returned from methods when using the <see cref="ModelBuilder" /> API
/// and it is not designed to be directly constructed in your application code.
/// </para>
/// </summary>
public class DbFunctionParameterBuilder
{
private readonly InternalDbFunctionParameterBuilder _builder;

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public DbFunctionParameterBuilder([NotNull] DbFunctionParameter dbFunctionParameter)
{
Check.NotNull(dbFunctionParameter, nameof(dbFunctionParameter));

_builder = new InternalDbFunctionParameterBuilder(dbFunctionParameter);
}

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public DbFunctionParameterBuilder([NotNull] InternalDbFunctionParameterBuilder builder)
{
Check.NotNull(builder, nameof(builder));

_builder = builder;
}

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual IMutableDbFunctionParameter Metadata => _builder.Parameter;

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual DbFunctionParameterBuilder HasIndex(int index)
{
_builder.HasIndex(index, ConfigurationSource.Explicit);

return this;
}
}
}
48 changes: 48 additions & 0 deletions src/EFCore.Relational/Metadata/IDbFunction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Query.Expressions;

namespace Microsoft.EntityFrameworkCore.Metadata
{
/// <summary>
/// Represents a db function in an <see cref="IModel" />.
/// </summary>
public interface IDbFunction
{
/// <summary>
/// The schema where the function lives in the underlying datastore.
/// </summary>
string Schema { get; }

/// <summary>
/// The name of the function in the underlying datastore.
/// </summary>
string Name { get; }

/// <summary>
/// The list of parameters which are passed to the underlying datastores function.
/// </summary>
IReadOnlyList<DbFunctionParameter> Parameters { get; }

/// <summary>
/// The .Net method which maps to the function in the underlying datastore
/// </summary>
MethodInfo MethodInfo { get; }

/// <summary>
/// The return type of the mapped .Net method
/// </summary>
Type ReturnType { get; }

/// <summary>
/// A translate callback for converting a method call into a sql function
/// </summary>
Func<IReadOnlyCollection<Expression>, IDbFunction, SqlFunctionExpression> TranslateCallback { get; }
}
}
28 changes: 28 additions & 0 deletions src/EFCore.Relational/Metadata/IDbFunctionParameter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;

namespace Microsoft.EntityFrameworkCore.Metadata
{
/// <summary>
/// Represents a db function parameter in an <see cref="IDbFunction" />.
/// </summary>
public interface IDbFunctionParameter
{
/// <summary>
/// The name of the parameter on the .Net method.
/// </summary>
string Name { get; }

/// <summary>
/// The index of the parameter on the mapped datastore method.
/// </summary>
int Index { get; }

/// <summary>
/// The .Net parameter type.
/// </summary>
Type ParameterType { get; }
}
}
40 changes: 40 additions & 0 deletions src/EFCore.Relational/Metadata/IMutableDbFunction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Query.Expressions;

namespace Microsoft.EntityFrameworkCore.Metadata
{
public interface IMutableDbFunction : IDbFunction
{
/// <summary>
/// The schema where the function lives in the underlying datastore.
/// </summary>
new string Schema { get; [param: CanBeNull] set; }

/// <summary>
/// The name of the function in the underlying datastore.
/// </summary>
new string Name { get; [param: NotNull] set;}

/// <summary>
/// The return type of the mapped .Net method
/// </summary>
new Type ReturnType { get; [param: NotNull] set;}

/// <summary>
/// A translate callback for converting a method call into a sql function
/// </summary>
new Func<IReadOnlyCollection<Expression>, IDbFunction, SqlFunctionExpression> TranslateCallback { get; [param: CanBeNull] set; }

/// <summary>
/// Add a dbFunctionParameter to this DbFunction
/// </summary>
DbFunctionParameter AddParameter([NotNull] string name);
}
}
29 changes: 29 additions & 0 deletions src/EFCore.Relational/Metadata/IMutableDbFunctionParameter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using JetBrains.Annotations;

namespace Microsoft.EntityFrameworkCore.Metadata
{
/// <summary>
/// Represents a db function parameter in an <see cref="IDbFunction" />.
/// </summary>
public interface IMutableDbFunctionParameter : IDbFunctionParameter
{
/// <summary>
/// The name of the parameter on the .Net method.
/// </summary>
new string Name { get; [param: NotNull] set; }

/// <summary>
/// The index of the parameter on the mapped datastore method.
/// </summary>
new int Index { get; set; }

/// <summary>
/// The .Net parameter type.
/// </summary>
new Type ParameterType { get; [param: NotNull] set; }
}
}
3 changes: 3 additions & 0 deletions src/EFCore.Relational/Metadata/IRelationalModelAnnotations.cs
Original file line number Diff line number Diff line change
@@ -2,15 +2,18 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Collections.Generic;
using System.Reflection;
using JetBrains.Annotations;

namespace Microsoft.EntityFrameworkCore.Metadata
{
public interface IRelationalModelAnnotations
{
ISequence FindSequence([NotNull] string name, [CanBeNull] string schema = null);
IDbFunction FindDbFunction([NotNull] MethodInfo method);

IReadOnlyList<ISequence> Sequences { get; }
IReadOnlyList<IDbFunction> DbFunctions { get; }

string DefaultSchema { get; }
}
224 changes: 224 additions & 0 deletions src/EFCore.Relational/Metadata/Internal/DbFunction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Utilities;
using Microsoft.EntityFrameworkCore.Query.ExpressionTranslators;
using Microsoft.EntityFrameworkCore.Query.Expressions;

namespace Microsoft.EntityFrameworkCore.Metadata.Internal
{
/// <summary>
/// Represents a db function in an <see cref="IModel" />.
/// </summary>
public class DbFunction : IMutableDbFunction, IMethodCallTranslator
{
private readonly SortedDictionary<string, DbFunctionParameter> _parameters
= new SortedDictionary<string, DbFunctionParameter>(StringComparer.OrdinalIgnoreCase);

private string _schema;
private string _name;
private Type _returnType;

private ConfigurationSource? _schemaConfigurationSource;
private ConfigurationSource? _nameConfigurationSource;
private ConfigurationSource? _returnTypeConfigurationSource;

private DbFunction([NotNull] MethodInfo dbFunctionMethodInfo,
[NotNull] IMutableModel model,
[NotNull] string annotationPrefix,
[CanBeNull] string name,
[CanBeNull] string schema,
ConfigurationSource configurationSource)
{
Check.NotNull(dbFunctionMethodInfo, nameof(dbFunctionMethodInfo));
Check.NotNull(model, nameof(model));
Check.NotNull(annotationPrefix, nameof(annotationPrefix));

if (dbFunctionMethodInfo.IsGenericMethod)
throw new ArgumentException(CoreStrings.DbFunctionGenericMethodNotSupported(dbFunctionMethodInfo));

if (name != null)
_nameConfigurationSource = configurationSource;

if (schema != null)
_schemaConfigurationSource = configurationSource;

_returnTypeConfigurationSource = configurationSource;

_name = name;
_schema = schema;
_returnType = dbFunctionMethodInfo.ReturnType;
MethodInfo = dbFunctionMethodInfo;

model[BuildAnnotationName(annotationPrefix, dbFunctionMethodInfo)] = this;
}

public static DbFunction GetOrAddDbFunction(
[NotNull] IMutableModel model,
[NotNull] MethodInfo methodInfo,
[NotNull] string annotationPrefix,
ConfigurationSource configurationSource,
[CanBeNull] string name = null,
[CanBeNull] string schema = null)
=> FindDbFunction(model, annotationPrefix, methodInfo)
?? new DbFunction(methodInfo, model, annotationPrefix, name, schema, configurationSource);

public static DbFunction FindDbFunction([NotNull] IModel model,
[NotNull] string annotationPrefix,
[NotNull] MethodInfo methodInfo)
{
Check.NotNull(model, nameof(model));
Check.NotNull(annotationPrefix, nameof(annotationPrefix));
Check.NotNull(methodInfo, nameof(methodInfo));

return model[BuildAnnotationName(annotationPrefix, methodInfo)] as DbFunction;
}

public static IEnumerable<IDbFunction> GetDbFunctions([NotNull] IModel model, [NotNull] string annotationPrefix)
{
Check.NotNull(model, nameof(model));
Check.NotEmpty(annotationPrefix, nameof(annotationPrefix));

return model.GetAnnotations()
.Where(a => a.Name.StartsWith(annotationPrefix, StringComparison.Ordinal))
.Select(a => a.Value)
.Cast<IDbFunction>();
}

private static string BuildAnnotationName(string annotationPrefix, MethodInfo methodInfo)
=> $@"{annotationPrefix}{methodInfo.Name}({string.Join(",", methodInfo.GetParameters().Select(p => p.ParameterType.Name))})";

/// <summary>
/// The schema where the function lives in the underlying datastore.
/// </summary>
public virtual string Schema
{
get =>_schema;

[param: CanBeNull] set => SetSchema(value, ConfigurationSource.Explicit);
}

public virtual void SetSchema([CanBeNull] string schema, ConfigurationSource configurationSource)
{
_schema = schema;
UpdateSchemaConfigurationSource(configurationSource);
}

private void UpdateSchemaConfigurationSource(ConfigurationSource configurationSource)
=> _schemaConfigurationSource = configurationSource.Max(_schemaConfigurationSource);

public virtual ConfigurationSource? GetSchemaConfigurationSource() => _schemaConfigurationSource;

/// <summary>
/// The name of the function in the underlying datastore.
/// </summary>
public virtual string Name
{
get => _name;

[param: NotNull] set => SetName(value, ConfigurationSource.Explicit);
}

public virtual void SetName([NotNull] string name, ConfigurationSource configurationSource)
{
Check.NotNull(name, nameof(name));

_name = name;
UpdateNameConfigurationSource(configurationSource);
}

private void UpdateNameConfigurationSource(ConfigurationSource configurationSource)
=> _nameConfigurationSource = configurationSource.Max(_nameConfigurationSource);

public virtual ConfigurationSource? GetNameConfigurationSource() => _nameConfigurationSource;

/// <summary>
/// The return type of the mapped .Net method
/// </summary>
public virtual Type ReturnType
{
get => _returnType;

[param: NotNull] set => SetReturnType(value, ConfigurationSource.Explicit);
}

public virtual void SetReturnType([NotNull] Type returnType, ConfigurationSource configurationSource)
{
Check.NotNull(returnType, nameof(returnType));

_returnType = returnType;
UpdateReturnTypeConfigurationSource(configurationSource);
}

private void UpdateReturnTypeConfigurationSource(ConfigurationSource configurationSource)
=> _returnTypeConfigurationSource = configurationSource.Max(_returnTypeConfigurationSource);

public virtual ConfigurationSource? GetReturnTypeConfigurationSource() => _nameConfigurationSource;

/// <summary>
/// The list of parameters which are passed to the underlying datastores function.
/// </summary>
public virtual IReadOnlyList<DbFunctionParameter> Parameters => _parameters.Values.ToList();

/// <summary>
/// The .Net method which maps to the function in the underlying datastore
/// </summary>
public virtual MethodInfo MethodInfo { get; }

/// <summary>
/// If set this callback is used to translate the .Net method call to a Linq Expression.
/// </summary>
public virtual Func<IReadOnlyCollection<Expression>, IDbFunction, SqlFunctionExpression> TranslateCallback { get; [param: CanBeNull] set; }

public virtual DbFunctionParameter AddParameter(string name)
=> AddParameter(name, ConfigurationSource.Explicit);

public virtual DbFunctionParameter AddParameter([NotNull] string name, ConfigurationSource configurationSource)
{
Check.NotNull(name, nameof(name));

var newParam = new DbFunctionParameter(name, configurationSource);

_parameters.Add(newParam.Name, newParam);

return newParam;
}

public virtual DbFunctionParameter FindParameter([NotNull] string name, ConfigurationSource configurationSource)
{
Check.NotNull(name, nameof(name));

DbFunctionParameter parameter;

if(_parameters.TryGetValue(name, out parameter))
parameter.UpdateConfigurationSource(configurationSource);

return parameter;
}

Expression IMethodCallTranslator.Translate(MethodCallExpression methodCallExpression)
{
Check.NotNull(methodCallExpression, nameof(methodCallExpression));

var methodArgs = methodCallExpression.Method.GetParameters().Zip(methodCallExpression.Arguments, (p, a) => new { Parameter = p, Argument = a });

var arguments = new ReadOnlyCollection<Expression>(
(from dbParam in Parameters
join methodArg in methodArgs on dbParam.Name equals methodArg.Parameter.Name into passParams
from passParam in passParams
orderby dbParam.Index
select passParam.Argument).ToList());

return TranslateCallback?.Invoke(arguments, this)
?? new SqlFunctionExpression(Name, ReturnType, Schema, arguments);
}
}
}
92 changes: 92 additions & 0 deletions src/EFCore.Relational/Metadata/Internal/DbFunctionParameter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Utilities;

namespace Microsoft.EntityFrameworkCore.Metadata.Internal
{
public class DbFunctionParameter : IMutableDbFunctionParameter
{
private int _index;
private Type _parameterType;
private string _name;
private ConfigurationSource? _parameterIndexConfigurationSource;
private ConfigurationSource? _parameterTypeConfigurationSource;
private ConfigurationSource? _nameConfigurationSource;
private ConfigurationSource _configurationSource;

public DbFunctionParameter([NotNull] string name, ConfigurationSource configurationSource)
{
Check.NotEmpty(name, nameof(name));

_name = name;
_configurationSource = configurationSource;
_nameConfigurationSource = configurationSource;
}

public virtual void UpdateConfigurationSource(ConfigurationSource configurationSource)
=> _configurationSource = configurationSource.Max(_configurationSource);

public virtual ConfigurationSource? GetConfigurationSource() => _configurationSource;

public virtual string Name
{
get => _name;
[param: NotNull] set => SetName(value, ConfigurationSource.Explicit);
}

public virtual void SetName([NotNull] string name, ConfigurationSource configSource)
{
Check.NotNull(name, nameof(name));

UpdateNameConfigurationSource(configSource);

_name = name;
}

private void UpdateNameConfigurationSource(ConfigurationSource configurationSource)
=> _nameConfigurationSource = configurationSource.Max(_nameConfigurationSource);

public virtual ConfigurationSource? GetNameConfigurationSource() => _nameConfigurationSource;

public virtual Type ParameterType
{
get => _parameterType;
[param: NotNull] set => SetParameterType(value, ConfigurationSource.Explicit);
}

private void UpdateParameterTypeConfigurationSource(ConfigurationSource configurationSource)
=> _parameterTypeConfigurationSource = configurationSource.Max(_parameterTypeConfigurationSource);

public virtual ConfigurationSource? GetParameterTypeConfigurationSource() => _parameterTypeConfigurationSource;

public virtual void SetParameterType([NotNull] Type parameterType, ConfigurationSource configSource)
{
Check.NotNull(parameterType, nameof(parameterType));

UpdateParameterTypeConfigurationSource(configSource);

_parameterType = parameterType;
}

public virtual int Index
{
get => _index;
set => SetParameterIndex(value, ConfigurationSource.Explicit);
}

private void UpdateParameterIndexConfigurationSource(ConfigurationSource configurationSource)
=> _parameterIndexConfigurationSource = configurationSource.Max(_parameterIndexConfigurationSource);

public virtual ConfigurationSource? GetParameterIndexConfigurationSource() => _parameterIndexConfigurationSource;

public virtual void SetParameterIndex(int index, ConfigurationSource configSource)
{
UpdateParameterIndexConfigurationSource(configSource);

_index = index;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Query.Expressions;

namespace Microsoft.EntityFrameworkCore.Metadata.Internal
{
/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public class InternalDbFunctionBuilder
{
private readonly DbFunction _dbFunction;

public InternalDbFunctionBuilder([NotNull] DbFunction dbFunction)
{
_dbFunction = dbFunction;
}

public virtual IMutableDbFunction Metadata => _dbFunction;

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual InternalDbFunctionBuilder HasSchema([CanBeNull] string schema, ConfigurationSource configurationSource)
{
if (configurationSource.Overrides(_dbFunction.GetSchemaConfigurationSource())
|| _dbFunction.Schema == schema)
{
_dbFunction.SetSchema(schema, configurationSource);
}

return this;
}

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual InternalDbFunctionBuilder HasName([NotNull] string name, ConfigurationSource configurationSource)
{
if (configurationSource.Overrides(_dbFunction.GetNameConfigurationSource())
|| _dbFunction.Name == name)
{
_dbFunction.SetName(name, configurationSource);
}

return this;
}

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual InternalDbFunctionBuilder HasReturnType([NotNull] Type returnType, ConfigurationSource configurationSource)
{
if (configurationSource.Overrides(_dbFunction.GetReturnTypeConfigurationSource())
|| _dbFunction.ReturnType == returnType)
{
_dbFunction.SetReturnType(returnType, configurationSource);
}

return this;
}

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual InternalDbFunctionParameterBuilder HasParameter([NotNull] string name, ConfigurationSource configurationSource)
{
return new InternalDbFunctionParameterBuilder(GetOrCreateParameter(name, configurationSource));
}

private DbFunctionParameter GetOrCreateParameter(string name, ConfigurationSource configurationSource)
{
return _dbFunction.FindParameter(name, configurationSource) ?? _dbFunction.AddParameter(name, configurationSource);
}

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual InternalDbFunctionBuilder TranslateWith([NotNull] Func<IReadOnlyCollection<Expression>, IDbFunction, SqlFunctionExpression> translateCallback)
{
_dbFunction.TranslateCallback = translateCallback;

return this;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Utilities;

namespace Microsoft.EntityFrameworkCore.Metadata.Internal
{
/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public class InternalDbFunctionParameterBuilder
{
private readonly DbFunctionParameter _dbFunctionParameter;

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual DbFunctionParameter Parameter => _dbFunctionParameter;

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public InternalDbFunctionParameterBuilder([NotNull] DbFunctionParameter dbFunctionParameter)
{
Check.NotNull(dbFunctionParameter, nameof(dbFunctionParameter));

_dbFunctionParameter = dbFunctionParameter;
}

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual InternalDbFunctionParameterBuilder HasName([NotNull]string name, ConfigurationSource configurationSource)
{
if (configurationSource.Overrides(_dbFunctionParameter.GetNameConfigurationSource())
|| _dbFunctionParameter.Name == name)
{
_dbFunctionParameter.SetName(name, configurationSource);
}

return this;
}

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual InternalDbFunctionParameterBuilder HasType([NotNull] Type type, ConfigurationSource configurationSource)
{
if (configurationSource.Overrides(_dbFunctionParameter.GetParameterTypeConfigurationSource())
|| _dbFunctionParameter.ParameterType == type)
{
_dbFunctionParameter.SetParameterType(type, configurationSource);
}

return this;
}

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual InternalDbFunctionParameterBuilder HasIndex(int index, ConfigurationSource configurationSource)
{
if (configurationSource.Overrides(_dbFunctionParameter.GetParameterIndexConfigurationSource())
|| _dbFunctionParameter.Index == index)
{
_dbFunctionParameter.SetParameterIndex(index, configurationSource);
}

return this;
}
}
}
5 changes: 5 additions & 0 deletions src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs
Original file line number Diff line number Diff line change
@@ -84,5 +84,10 @@ public static class RelationalAnnotationNames
/// The name for filter annotations.
/// </summary>
public const string TypeMapping = Prefix + "TypeMapping";

/// <summary>
/// The name for DbFunction annotations.
/// </summary>
public const string DbFunction = Prefix + "DbFunction";
}
}
10 changes: 10 additions & 0 deletions src/EFCore.Relational/Metadata/RelationalModelAnnotations.cs
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@

using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Utilities;
@@ -43,6 +44,15 @@ public virtual IMutableSequence GetOrAddSequence([NotNull] string name, [CanBeNu
private static string BuildAnnotationName(string annotationPrefix, string name, string schema)
=> annotationPrefix + schema + "." + name;

public virtual IReadOnlyList<IDbFunction> DbFunctions
=> DbFunction.GetDbFunctions(Model, RelationalAnnotationNames.DbFunction).ToList();

public virtual IDbFunction FindDbFunction(MethodInfo methodInfo)
=> DbFunction.FindDbFunction(Model, RelationalAnnotationNames.DbFunction, methodInfo);

public virtual DbFunction GetOrAddDbFunction([NotNull] MethodInfo methodInfo, ConfigurationSource configurationSource, [CanBeNull] string name = null, [CanBeNull] string schema = null)
=> DbFunction.GetOrAddDbFunction((IMutableModel)Model, methodInfo, RelationalAnnotationNames.DbFunction, configurationSource, name, schema);

public virtual string DefaultSchema
{
get => (string)Annotations.GetAnnotation(RelationalAnnotationNames.DefaultSchema);
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Linq.Expressions;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Metadata;

namespace Microsoft.EntityFrameworkCore.Query.ExpressionTranslators
{
/// <summary>
/// A LINQ expression translator for CLR <see cref="MethodCallExpression" /> expressions.
/// </summary>
public interface ICompositeMethodCallTranslator
{
/// <summary>
/// Translates the given method call expression.
/// </summary>
/// <param name="methodCallExpression"> The method call expression. </param>
/// <param name="model"> The current model. </param>
/// <returns>
/// A SQL expression representing the translated MethodCallExpression.
/// </returns>
Expression Translate([NotNull] MethodCallExpression methodCallExpression, [NotNull] IModel model);
}
}
Original file line number Diff line number Diff line change
@@ -7,14 +7,15 @@
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Query.ExpressionTranslators.Internal;
using Microsoft.EntityFrameworkCore.Utilities;
using Microsoft.EntityFrameworkCore.Metadata;

namespace Microsoft.EntityFrameworkCore.Query.ExpressionTranslators
{
/// <summary>
/// A base composite method call translator that dispatches to multiple specialized
/// method call translators.
/// </summary>
public abstract class RelationalCompositeMethodCallTranslator : IMethodCallTranslator
public abstract class RelationalCompositeMethodCallTranslator : ICompositeMethodCallTranslator
{
private readonly List<IMethodCallTranslator> _methodCallTranslators;

@@ -48,13 +49,15 @@ protected RelationalCompositeMethodCallTranslator(
/// Translates the given method call expression.
/// </summary>
/// <param name="methodCallExpression"> The method call expression. </param>
/// <param name="model"> The current model. </param>
/// <returns>
/// A SQL expression representing the translated MethodCallExpression.
/// </returns>
public virtual Expression Translate(MethodCallExpression methodCallExpression)
=> _methodCallTranslators
.Select(translator => translator.Translate(methodCallExpression))
.FirstOrDefault(translatedMethodCall => translatedMethodCall != null);
public virtual Expression Translate(MethodCallExpression methodCallExpression, IModel model)
=> ((IMethodCallTranslator)model.Relational().FindDbFunction(methodCallExpression.Method))?.Translate(methodCallExpression)
?? _methodCallTranslators
.Select(translator => translator.Translate(methodCallExpression))
.FirstOrDefault(translatedMethodCall => translatedMethodCall != null);

/// <summary>
/// Adds additional translators to the dispatch list.
Original file line number Diff line number Diff line change
@@ -42,7 +42,7 @@ private static readonly Dictionary<ExpressionType, ExpressionType> _inverseOpera
};

private readonly IExpressionFragmentTranslator _compositeExpressionFragmentTranslator;
private readonly IMethodCallTranslator _methodCallTranslator;
private readonly ICompositeMethodCallTranslator _methodCallTranslator;
private readonly IMemberTranslator _memberTranslator;
private readonly RelationalQueryModelVisitor _queryModelVisitor;
private readonly IRelationalTypeMapper _relationalTypeMapper;
@@ -606,7 +606,7 @@ var boundExpression
? Expression.Call(operand, methodCallExpression.Method, arguments)
: Expression.Call(methodCallExpression.Method, arguments);

var translatedExpression = _methodCallTranslator.Translate(boundExpression);
var translatedExpression = _methodCallTranslator.Translate(boundExpression, _queryModelVisitor.QueryCompilationContext.Model);

if (translatedExpression != null)
{
Original file line number Diff line number Diff line change
@@ -50,7 +50,7 @@ public sealed class SqlTranslatingExpressionVisitorDependencies
/// <param name="relationalTypeMapper"> The relational type mapper. </param>
public SqlTranslatingExpressionVisitorDependencies(
[NotNull] IExpressionFragmentTranslator compositeExpressionFragmentTranslator,
[NotNull] IMethodCallTranslator methodCallTranslator,
[NotNull] ICompositeMethodCallTranslator methodCallTranslator,
[NotNull] IMemberTranslator memberTranslator,
[NotNull] IRelationalTypeMapper relationalTypeMapper)
{
@@ -73,7 +73,7 @@ public SqlTranslatingExpressionVisitorDependencies(
/// <summary>
/// The method call translator.
/// </summary>
public IMethodCallTranslator MethodCallTranslator { get; }
public ICompositeMethodCallTranslator MethodCallTranslator { get; }

/// <summary>
/// The member translator.
@@ -102,7 +102,7 @@ public SqlTranslatingExpressionVisitorDependencies With([NotNull] IExpressionFra
/// </summary>
/// <param name="methodCallTranslator"> A replacement for the current dependency of this type. </param>
/// <returns> A new parameter object with the given service replaced. </returns>
public SqlTranslatingExpressionVisitorDependencies With([NotNull] IMethodCallTranslator methodCallTranslator)
public SqlTranslatingExpressionVisitorDependencies With([NotNull] ICompositeMethodCallTranslator methodCallTranslator)
=> new SqlTranslatingExpressionVisitorDependencies(
CompositeExpressionFragmentTranslator,
methodCallTranslator,
26 changes: 25 additions & 1 deletion src/EFCore.Relational/Query/Expressions/SqlFunctionExpression.cs
Original file line number Diff line number Diff line change
@@ -33,20 +33,36 @@ public SqlFunctionExpression(
{
}

/// <summary>
/// Initializes a new instance of the Microsoft.EntityFrameworkCore.Query.Expressions.SqlFunctionExpression class.
/// </summary>
/// <param name="functionName"> Name of the function. </param>
/// <param name="returnType"> The return type. </param>
/// <param name="arguments"> The arguments. </param>
public SqlFunctionExpression(
[NotNull] string functionName,
[NotNull] Type returnType,
[NotNull] IEnumerable<Expression> arguments)
: this(functionName, returnType, /*schema*/ null, arguments)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="SqlFunctionExpression" /> class.
/// </summary>
/// <param name="functionName"> Name of the function. </param>
/// <param name="returnType"> The return type. </param>
/// /// <param name="schema"> The schema this function exists in if any. </param>
/// <param name="arguments"> The arguments. </param>
public SqlFunctionExpression(
[NotNull] string functionName,
[NotNull] Type returnType,
[CanBeNull] string schema,
[NotNull] IEnumerable<Expression> arguments)
{
FunctionName = functionName;
Type = returnType;

Schema = schema;
_arguments = arguments.ToList().AsReadOnly();
}

@@ -58,6 +74,14 @@ public SqlFunctionExpression(
/// </value>
public virtual string FunctionName { get; }

/// <summary>
/// Gets the name of the schema.
/// </summary>
/// <value>
/// The name of the schema.
/// </value>
public virtual string Schema { get; }

/// <summary>
/// The arguments.
/// </summary>
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Linq.Expressions;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Utilities;

namespace Microsoft.EntityFrameworkCore.Query.Internal
{
/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public class RelationalEvaluatableExpressionFilter : EvaluatableExpressionFilter
{
private readonly IModel _model;

public RelationalEvaluatableExpressionFilter([NotNull] IModel model)
{
Check.NotNull(model, nameof(model));

_model = model;
}

public override bool IsEvaluatableMethodCall(MethodCallExpression methodCallExpression)
=> _model.Relational().FindDbFunction(methodCallExpression.Method) == null
&& base.IsEvaluatableMethodCall(methodCallExpression);
}
}
17 changes: 15 additions & 2 deletions src/EFCore.Relational/Query/Sql/DefaultQuerySqlGenerator.cs
Original file line number Diff line number Diff line change
@@ -1275,7 +1275,7 @@ public virtual Expression VisitLike(LikeExpression likeExpression)
/// </returns>
public virtual Expression VisitSqlFunction(SqlFunctionExpression sqlFunctionExpression)
{
GenerateFunctionCall(sqlFunctionExpression.FunctionName, sqlFunctionExpression.Arguments);
GenerateFunctionCall(sqlFunctionExpression.FunctionName, sqlFunctionExpression.Arguments, sqlFunctionExpression.Schema);

return sqlFunctionExpression;
}
@@ -1285,18 +1285,31 @@ public virtual Expression VisitSqlFunction(SqlFunctionExpression sqlFunctionExpr
/// </summary>
/// <param name="functionName">The function name</param>
/// <param name="arguments">The function arguments</param>
/// <param name="schema">The function schema</param>
protected virtual void GenerateFunctionCall(
[NotNull] string functionName, [NotNull] IReadOnlyList<Expression> arguments)
[NotNull] string functionName, [NotNull] IReadOnlyList<Expression> arguments,
[CanBeNull] string schema = null)
{
Check.NotEmpty(functionName, nameof(functionName));
Check.NotNull(arguments, nameof(arguments));

if (!string.IsNullOrWhiteSpace(schema))
{
_relationalCommandBuilder.Append(SqlGenerator.DelimitIdentifier(schema))
.Append(".");
}

var parentTypeMapping = _typeMapping;
_typeMapping = null;

_relationalCommandBuilder.Append(functionName);
_relationalCommandBuilder.Append("(");

ProcessExpressionList(arguments);

_relationalCommandBuilder.Append(")");

_typeMapping = parentTypeMapping;
}

/// <summary>
74 changes: 74 additions & 0 deletions src/EFCore.Relational/RelationalModelBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Reflection;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
@@ -201,6 +202,79 @@ public static ModelBuilder HasSequence<T>(
return modelBuilder;
}

/// <summary>
/// Configures a database function when targeting a relational database.
/// </summary>
/// <param name="modelBuilder"> The model builder. </param>
/// <param name="methodInfo"> The methodInfo this dbFunction uses. </param>
/// <param name="name"> The name of the dbFunction. </param>
/// <param name="schema">The schema of the dbFunction. </param>
/// <returns> The same builder instance so that multiple calls can be chained. </returns>
public static DbFunctionBuilder HasDbFunction(
[NotNull] this ModelBuilder modelBuilder,
[NotNull] MethodInfo methodInfo,
[CanBeNull] string name = null,
[CanBeNull] string schema = null)
{
Check.NotNull(modelBuilder, nameof(modelBuilder));
Check.NotNull(methodInfo, nameof(methodInfo));
Check.NullButNotEmpty(name, nameof(name));
Check.NullButNotEmpty(schema, nameof(schema));

var dbFunction = modelBuilder.Model.Relational().GetOrAddDbFunction(methodInfo, ConfigurationSource.Explicit, name, schema);

return new DbFunctionBuilder(dbFunction);
}

/// <summary>
/// Configures a database function when targeting a relational database.
/// </summary>
/// <param name="modelBuilder"> The model builder. </param>
/// <param name="methodInfo"> The methodInfo this dbFunction uses. </param>
/// <param name="name"> The name of the dbFunction. </param>
/// <param name="schema">The schema of the dbFunction. </param>
/// <param name="builderAction"> An action that performs configuration of the sequence. </param>
/// <returns> The same builder instance so that multiple calls can be chained. </returns>
public static ModelBuilder HasDbFunction(
[NotNull] this ModelBuilder modelBuilder,
[NotNull] MethodInfo methodInfo,
[NotNull] string name,
[NotNull] string schema,
[NotNull] Action<DbFunctionBuilder> builderAction)
{
Check.NotNull(modelBuilder, nameof(modelBuilder));
Check.NotNull(methodInfo, nameof(methodInfo));
Check.NotEmpty(name, nameof(name));
Check.NotEmpty(schema, nameof(schema));
Check.NotNull(builderAction, nameof(builderAction));

builderAction(HasDbFunction(modelBuilder, methodInfo, name, schema));

return modelBuilder;
}

/// <summary>
/// Configures a database function when targeting a relational database.
/// </summary>
/// <param name="modelBuilder"> The model builder. </param>
/// <param name="methodInfo"> The methodInfo this dbFunction uses. </param>
/// <param name="builderAction"> An action that performs configuration of the sequence. </param>
/// <returns> The same builder instance so that multiple calls can be chained. </returns>
public static ModelBuilder HasDbFunction(
[NotNull] this ModelBuilder modelBuilder,
[NotNull] MethodInfo methodInfo,
[NotNull] Action<DbFunctionBuilder> builderAction)
{
Check.NotNull(modelBuilder, nameof(modelBuilder));
Check.NotNull(methodInfo, nameof(methodInfo));
Check.NotNull(builderAction, nameof(builderAction));

builderAction(HasDbFunction(modelBuilder, methodInfo));

return modelBuilder;
}


/// <summary>
/// Configures the default schema that database objects should be created in, if no schema
/// is explicitly configured.
170 changes: 87 additions & 83 deletions src/EFCore.Relational/breakingchanges.netcore.json
Original file line number Diff line number Diff line change
@@ -348,11 +348,6 @@
"MemberId": "protected .ctor(Microsoft.EntityFrameworkCore.Storage.ISqlGenerationHelper sqlGenerationHelper)",
"Kind": "Removal"
},
{
"TypeId": "public abstract class Microsoft.EntityFrameworkCore.Query.ExpressionTranslators.RelationalCompositeMethodCallTranslator : Microsoft.EntityFrameworkCore.Query.ExpressionTranslators.IMethodCallTranslator",
"MemberId": "protected .ctor(Microsoft.Extensions.Logging.ILogger logger)",
"Kind": "Removal"
},
{
"TypeId": "public class Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.RelationalProjectionExpressionVisitor : Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.ProjectionExpressionVisitor",
"MemberId": "protected override System.Linq.Expressions.Expression VisitMethodCall(System.Linq.Expressions.MethodCallExpression node)",
@@ -1668,11 +1663,6 @@
"MemberId": "protected .ctor(Microsoft.EntityFrameworkCore.Storage.ISqlGenerationHelper sqlGenerationHelper)",
"Kind": "Removal"
},
{
"TypeId": "public abstract class Microsoft.EntityFrameworkCore.Query.ExpressionTranslators.RelationalCompositeMethodCallTranslator : Microsoft.EntityFrameworkCore.Query.ExpressionTranslators.IMethodCallTranslator",
"MemberId": "protected .ctor(Microsoft.Extensions.Logging.ILogger logger)",
"Kind": "Removal"
},
{
"TypeId": "public class Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.RelationalProjectionExpressionVisitor : Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.ProjectionExpressionVisitor",
"MemberId": "protected override System.Linq.Expressions.Expression VisitMethodCall(System.Linq.Expressions.MethodCallExpression node)",
@@ -3022,74 +3012,74 @@
"MemberId": "public .ctor(System.String storeType, System.Type clrType, System.Nullable<System.Data.DbType> dbType)",
"Kind": "Removal"
},
{
"TypeId": "public class Microsoft.EntityFrameworkCore.Metadata.RelationalSequenceBuilder",
"Kind": "Removal"
},
{
"TypeId": "public class Microsoft.EntityFrameworkCore.Metadata.Sequence : Microsoft.EntityFrameworkCore.Metadata.ISequence",
"Kind": "Removal"
},
{
"TypeId": "public class Microsoft.EntityFrameworkCore.Metadata.RelationalModelAnnotations : Microsoft.EntityFrameworkCore.Metadata.IRelationalModelAnnotations",
"MemberId": "public Microsoft.EntityFrameworkCore.Metadata.ISequence FindSequence(System.String name, System.String schema = null)",
"Kind": "Removal"
},
{
"TypeId": "public class Microsoft.EntityFrameworkCore.Metadata.RelationalModelAnnotations : Microsoft.EntityFrameworkCore.Metadata.IRelationalModelAnnotations",
"MemberId": "public System.Collections.Generic.IReadOnlyList<Microsoft.EntityFrameworkCore.Metadata.ISequence> get_Sequences()",
"Kind": "Removal"
},
{
"TypeId": "public class Microsoft.EntityFrameworkCore.Metadata.RelationalModelAnnotations : Microsoft.EntityFrameworkCore.Metadata.IRelationalModelAnnotations",
"MemberId": "public virtual Microsoft.EntityFrameworkCore.Metadata.Sequence GetOrAddSequence(System.String name, System.String schema = null)",
"Kind": "Removal"
},
{
"TypeId": "public static class Microsoft.EntityFrameworkCore.RelationalModelBuilderExtensions",
"MemberId": "public static Microsoft.EntityFrameworkCore.Metadata.RelationalSequenceBuilder HasSequence(this Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder, System.Type clrType, System.String name, System.String schema = null)",
"Kind": "Removal"
},
{
"TypeId": "public static class Microsoft.EntityFrameworkCore.RelationalModelBuilderExtensions",
"MemberId": "public static Microsoft.EntityFrameworkCore.ModelBuilder HasSequence(this Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder, System.Type clrType, System.String name, System.Action<Microsoft.EntityFrameworkCore.Metadata.RelationalSequenceBuilder> builderAction)",
"Kind": "Removal"
},
{
"TypeId": "public static class Microsoft.EntityFrameworkCore.RelationalModelBuilderExtensions",
"MemberId": "public static Microsoft.EntityFrameworkCore.ModelBuilder HasSequence(this Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder, System.Type clrType, System.String name, System.String schema, System.Action<Microsoft.EntityFrameworkCore.Metadata.RelationalSequenceBuilder> builderAction)",
"Kind": "Removal"
},
{
"TypeId": "public static class Microsoft.EntityFrameworkCore.RelationalModelBuilderExtensions",
"MemberId": "public static new Microsoft.EntityFrameworkCore.Metadata.RelationalSequenceBuilder HasSequence(this Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder, System.String name, System.String schema = null)",
"Kind": "Removal"
},
{
"TypeId": "public static class Microsoft.EntityFrameworkCore.RelationalModelBuilderExtensions",
"MemberId": "public static new Microsoft.EntityFrameworkCore.Metadata.RelationalSequenceBuilder HasSequence<T0>(this Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder, System.String name, System.String schema = null)",
"Kind": "Removal"
},
{
"TypeId": "public static class Microsoft.EntityFrameworkCore.RelationalModelBuilderExtensions",
"MemberId": "public static new Microsoft.EntityFrameworkCore.ModelBuilder HasSequence(this Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder, System.String name, System.Action<Microsoft.EntityFrameworkCore.Metadata.RelationalSequenceBuilder> builderAction)",
"Kind": "Removal"
},
{
"TypeId": "public static class Microsoft.EntityFrameworkCore.RelationalModelBuilderExtensions",
"MemberId": "public static new Microsoft.EntityFrameworkCore.ModelBuilder HasSequence(this Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder, System.String name, System.String schema, System.Action<Microsoft.EntityFrameworkCore.Metadata.RelationalSequenceBuilder> builderAction)",
"Kind": "Removal"
},
{
"TypeId": "public static class Microsoft.EntityFrameworkCore.RelationalModelBuilderExtensions",
"MemberId": "public static new Microsoft.EntityFrameworkCore.ModelBuilder HasSequence<T0>(this Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder, System.String name, System.Action<Microsoft.EntityFrameworkCore.Metadata.RelationalSequenceBuilder> builderAction)",
"Kind": "Removal"
},
{
"TypeId": "public static class Microsoft.EntityFrameworkCore.RelationalModelBuilderExtensions",
"MemberId": "public static new Microsoft.EntityFrameworkCore.ModelBuilder HasSequence<T0>(this Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder, System.String name, System.String schema, System.Action<Microsoft.EntityFrameworkCore.Metadata.RelationalSequenceBuilder> builderAction)",
"Kind": "Removal"
},
{
"TypeId": "public class Microsoft.EntityFrameworkCore.Metadata.RelationalSequenceBuilder",
"Kind": "Removal"
},
{
"TypeId": "public class Microsoft.EntityFrameworkCore.Metadata.Sequence : Microsoft.EntityFrameworkCore.Metadata.ISequence",
"Kind": "Removal"
},
{
"TypeId": "public class Microsoft.EntityFrameworkCore.Metadata.RelationalModelAnnotations : Microsoft.EntityFrameworkCore.Metadata.IRelationalModelAnnotations",
"MemberId": "public Microsoft.EntityFrameworkCore.Metadata.ISequence FindSequence(System.String name, System.String schema = null)",
"Kind": "Removal"
},
{
"TypeId": "public class Microsoft.EntityFrameworkCore.Metadata.RelationalModelAnnotations : Microsoft.EntityFrameworkCore.Metadata.IRelationalModelAnnotations",
"MemberId": "public System.Collections.Generic.IReadOnlyList<Microsoft.EntityFrameworkCore.Metadata.ISequence> get_Sequences()",
"Kind": "Removal"
},
{
"TypeId": "public class Microsoft.EntityFrameworkCore.Metadata.RelationalModelAnnotations : Microsoft.EntityFrameworkCore.Metadata.IRelationalModelAnnotations",
"MemberId": "public virtual Microsoft.EntityFrameworkCore.Metadata.Sequence GetOrAddSequence(System.String name, System.String schema = null)",
"Kind": "Removal"
},
{
"TypeId": "public static class Microsoft.EntityFrameworkCore.RelationalModelBuilderExtensions",
"MemberId": "public static Microsoft.EntityFrameworkCore.Metadata.RelationalSequenceBuilder HasSequence(this Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder, System.Type clrType, System.String name, System.String schema = null)",
"Kind": "Removal"
},
{
"TypeId": "public static class Microsoft.EntityFrameworkCore.RelationalModelBuilderExtensions",
"MemberId": "public static Microsoft.EntityFrameworkCore.ModelBuilder HasSequence(this Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder, System.Type clrType, System.String name, System.Action<Microsoft.EntityFrameworkCore.Metadata.RelationalSequenceBuilder> builderAction)",
"Kind": "Removal"
},
{
"TypeId": "public static class Microsoft.EntityFrameworkCore.RelationalModelBuilderExtensions",
"MemberId": "public static Microsoft.EntityFrameworkCore.ModelBuilder HasSequence(this Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder, System.Type clrType, System.String name, System.String schema, System.Action<Microsoft.EntityFrameworkCore.Metadata.RelationalSequenceBuilder> builderAction)",
"Kind": "Removal"
},
{
"TypeId": "public static class Microsoft.EntityFrameworkCore.RelationalModelBuilderExtensions",
"MemberId": "public static new Microsoft.EntityFrameworkCore.Metadata.RelationalSequenceBuilder HasSequence(this Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder, System.String name, System.String schema = null)",
"Kind": "Removal"
},
{
"TypeId": "public static class Microsoft.EntityFrameworkCore.RelationalModelBuilderExtensions",
"MemberId": "public static new Microsoft.EntityFrameworkCore.Metadata.RelationalSequenceBuilder HasSequence<T0>(this Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder, System.String name, System.String schema = null)",
"Kind": "Removal"
},
{
"TypeId": "public static class Microsoft.EntityFrameworkCore.RelationalModelBuilderExtensions",
"MemberId": "public static new Microsoft.EntityFrameworkCore.ModelBuilder HasSequence(this Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder, System.String name, System.Action<Microsoft.EntityFrameworkCore.Metadata.RelationalSequenceBuilder> builderAction)",
"Kind": "Removal"
},
{
"TypeId": "public static class Microsoft.EntityFrameworkCore.RelationalModelBuilderExtensions",
"MemberId": "public static new Microsoft.EntityFrameworkCore.ModelBuilder HasSequence(this Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder, System.String name, System.String schema, System.Action<Microsoft.EntityFrameworkCore.Metadata.RelationalSequenceBuilder> builderAction)",
"Kind": "Removal"
},
{
"TypeId": "public static class Microsoft.EntityFrameworkCore.RelationalModelBuilderExtensions",
"MemberId": "public static new Microsoft.EntityFrameworkCore.ModelBuilder HasSequence<T0>(this Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder, System.String name, System.Action<Microsoft.EntityFrameworkCore.Metadata.RelationalSequenceBuilder> builderAction)",
"Kind": "Removal"
},
{
"TypeId": "public static class Microsoft.EntityFrameworkCore.RelationalModelBuilderExtensions",
"MemberId": "public static new Microsoft.EntityFrameworkCore.ModelBuilder HasSequence<T0>(this Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder, System.String name, System.String schema, System.Action<Microsoft.EntityFrameworkCore.Metadata.RelationalSequenceBuilder> builderAction)",
"Kind": "Removal"
},
{
"TypeId": "public class Microsoft.EntityFrameworkCore.Metadata.RelationalKeyAnnotations : Microsoft.EntityFrameworkCore.Metadata.IRelationalKeyAnnotations",
"MemberId": "protected const System.String DefaultAlternateKeyNamePrefix = \"AK\"",
@@ -3165,9 +3155,23 @@
"MemberId": "public static System.String GetDefaultIndexName(System.String tableName, System.Collections.Generic.IEnumerable<System.String> propertyNames)",
"Kind": "Removal"
},
{
"TypeId": "public abstract class Microsoft.EntityFrameworkCore.Infrastructure.RelationalOptionsExtension : Microsoft.EntityFrameworkCore.Infrastructure.IDbContextOptionsExtension",
"MemberId": "public virtual System.Func<Microsoft.EntityFrameworkCore.Storage.ExecutionStrategyContext, Microsoft.EntityFrameworkCore.Storage.IExecutionStrategy> get_ExecutionStrategyFactory()",
"Kind": "Removal"
}
{
"TypeId": "public abstract class Microsoft.EntityFrameworkCore.Query.ExpressionTranslators.RelationalCompositeMethodCallTranslator : Microsoft.EntityFrameworkCore.Query.ExpressionTranslators.IMethodCallTranslator",
"Kind": "Removal"
},
{
"TypeId": "public interface Microsoft.EntityFrameworkCore.Metadata.IRelationalModelAnnotations",
"MemberId": "Microsoft.EntityFrameworkCore.Metadata.IDbFunction FindDbFunction(System.Reflection.MethodInfo method)",
"Kind": "Addition"
},
{
"TypeId": "public interface Microsoft.EntityFrameworkCore.Metadata.IRelationalModelAnnotations",
"MemberId": "System.Collections.Generic.IReadOnlyList<Microsoft.EntityFrameworkCore.Metadata.IDbFunction> get_DbFunctions()",
"Kind": "Addition"
},
{
"TypeId": "public abstract class Microsoft.EntityFrameworkCore.Infrastructure.RelationalOptionsExtension : Microsoft.EntityFrameworkCore.Infrastructure.IDbContextOptionsExtension",
"MemberId": "public virtual System.Func<Microsoft.EntityFrameworkCore.Storage.ExecutionStrategyContext, Microsoft.EntityFrameworkCore.Storage.IExecutionStrategy> get_ExecutionStrategyFactory()",
"Kind": "Removal"
}
]
9 changes: 4 additions & 5 deletions src/EFCore.Specification.Tests/NorthwindQueryFixtureBase.cs
Original file line number Diff line number Diff line change
@@ -9,8 +9,6 @@ namespace Microsoft.EntityFrameworkCore
{
public abstract class NorthwindQueryFixtureBase
{
private DbContextOptions _options;

public abstract DbContextOptions BuildOptions(IServiceCollection additionalServices = null);

public virtual NorthwindContext CreateContext(
@@ -20,13 +18,14 @@ public virtual NorthwindContext CreateContext(
EnableFilters = enableFilters;

return new NorthwindContext(
_options
?? (_options = new DbContextOptionsBuilder(BuildOptions())
Options
?? (Options = new DbContextOptionsBuilder(BuildOptions())
.ConfigureWarnings(w => w.Log(CoreEventId.IncludeIgnoredWarning)).Options),
queryTrackingBehavior);
}

private bool EnableFilters { get; set; }
protected bool EnableFilters { get; set; }
protected DbContextOptions Options { get; set; }

protected virtual void OnModelCreating(ModelBuilder modelBuilder)
{
17 changes: 13 additions & 4 deletions src/EFCore.Specification.Tests/TestModelSource.cs
Original file line number Diff line number Diff line change
@@ -14,11 +14,13 @@ namespace Microsoft.EntityFrameworkCore
public class TestModelSource : ModelSource
{
private readonly Action<ModelBuilder> _onModelCreating;
private readonly Action<ModelBuilder, DbContext> _customizeModel;

public TestModelSource(Action<ModelBuilder> onModelCreating, ModelSourceDependencies dependencies)
public TestModelSource(Action<ModelBuilder> onModelCreating, ModelSourceDependencies dependencies, Action<ModelBuilder, DbContext> customizeModel = null)
: base(dependencies)
{
_onModelCreating = onModelCreating;
_customizeModel = customizeModel;
}

protected override IModel CreateModel(DbContext context, IConventionSetBuilder conventionSetBuilder, IModelValidator validator)
@@ -29,10 +31,10 @@ protected override IModel CreateModel(DbContext context, IConventionSetBuilder c
var model = (Model)modelBuilder.Model;
model.SetProductVersion(ProductInfo.GetVersion());

FindSets(modelBuilder, context);
_customizeModel?.Invoke(modelBuilder, context);

_onModelCreating(modelBuilder);

model.Validate();

validator.Validate(model);
@@ -43,6 +45,13 @@ protected override IModel CreateModel(DbContext context, IConventionSetBuilder c
public static Func<IServiceProvider, IModelSource> GetFactory(Action<ModelBuilder> onModelCreating)
=> p => new TestModelSource(
onModelCreating,
p.GetRequiredService<ModelSourceDependencies>());
p.GetRequiredService<ModelSourceDependencies>(),
(mb, dbc) =>
{
foreach (var setInfo in p.GetRequiredService<IDbSetFinder>().FindSets(dbc))
{
mb.Entity(setInfo.ClrType);
}
});
}
}
Original file line number Diff line number Diff line change
@@ -86,7 +86,7 @@ public static IServiceCollection AddEntityFrameworkSqlServer([NotNull] this ISer
.TryAdd<IExecutionStrategyFactory, SqlServerExecutionStrategyFactory>()
.TryAdd<IQueryCompilationContextFactory, SqlServerQueryCompilationContextFactory>()
.TryAdd<IMemberTranslator, SqlServerCompositeMemberTranslator>()
.TryAdd<IMethodCallTranslator, SqlServerCompositeMethodCallTranslator>()
.TryAdd<ICompositeMethodCallTranslator, SqlServerCompositeMethodCallTranslator>()
.TryAdd<IQuerySqlGeneratorFactory, SqlServerQuerySqlGeneratorFactory>()
.TryAdd<ISingletonOptions, ISqlServerOptions>(p => p.GetService<ISqlServerOptions>())
.TryAddProviderSpecificServices(b => b
Original file line number Diff line number Diff line change
@@ -78,7 +78,7 @@ public static IServiceCollection AddEntityFrameworkSqlite([NotNull] this IServic
.TryAdd<IRelationalDatabaseCreator, SqliteDatabaseCreator>()
.TryAdd<IHistoryRepository, SqliteHistoryRepository>()
.TryAdd<IMemberTranslator, SqliteCompositeMemberTranslator>()
.TryAdd<IMethodCallTranslator, SqliteCompositeMethodCallTranslator>()
.TryAdd<ICompositeMethodCallTranslator, SqliteCompositeMethodCallTranslator>()
.TryAdd<IQuerySqlGeneratorFactory, SqliteQuerySqlGeneratorFactory>()
.TryAddProviderSpecificServices(b => b
.TryAddScoped<ISqliteRelationalConnection, SqliteRelationalConnection>());
7 changes: 5 additions & 2 deletions src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using JetBrains.Annotations;
using Remotion.Linq.Parsing.ExpressionVisitors.TreeEvaluation;
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Internal;
@@ -115,7 +116,8 @@ private static readonly IDictionary<Type, ServiceCharacteristics> _coreServices
{ typeof(IQueryTrackingListener), new ServiceCharacteristics(ServiceLifetime.Scoped, multipleRegistrations: true) },
{ typeof(IPropertyListener), new ServiceCharacteristics(ServiceLifetime.Scoped, multipleRegistrations: true) },
{ typeof(IResettableService), new ServiceCharacteristics(ServiceLifetime.Scoped, multipleRegistrations: true) },
{ typeof(ISingletonOptions), new ServiceCharacteristics(ServiceLifetime.Singleton, multipleRegistrations: true) }
{ typeof(ISingletonOptions), new ServiceCharacteristics(ServiceLifetime.Singleton, multipleRegistrations: true) },
{ typeof(IEvaluatableExpressionFilter), new ServiceCharacteristics(ServiceLifetime.Scoped) }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be singleton?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No.

The IEvaluatableExpressionFilter needs access to the current model in order to know which methods are marked as translatable. The ParameterExtractingExpressionVisitor will try to evaluate methods at query compilation not marked as translatable which also have constant parameters.

We could look at injecting the model directly into the ParameterExtractingExpressionVisitor, but that will involve making it a scoped service and make the code there more confusing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@anpete - I'm confused on your thumbs up. Are you for leaving the code as is, or looking into if it can be moved into ParameterExtractingExpressionVisitor?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leave as is

};

/// <summary>
@@ -239,7 +241,8 @@ public virtual EntityFrameworkServicesBuilder TryAddCoreServices()
TryAdd<IEntityStateListener, ILocalViewListener>(p => p.GetService<ILocalViewListener>());
TryAdd<IResettableService, IStateManager>(p => p.GetService<IStateManager>());
TryAdd<IResettableService, IDbContextTransactionManager>(p => p.GetService<IDbContextTransactionManager>());

TryAdd<IEvaluatableExpressionFilter, EvaluatableExpressionFilter>();

ServiceCollectionMap
.TryAddSingleton<DiagnosticSource>(new DiagnosticListener(DbLoggerCategory.Name));

27 changes: 26 additions & 1 deletion src/EFCore/Infrastructure/ModelCustomizer.cs
Original file line number Diff line number Diff line change
@@ -25,8 +25,15 @@ public class ModelCustomizer : IModelCustomizer
public ModelCustomizer([NotNull] ModelCustomizerDependencies dependencies)
{
Check.NotNull(dependencies, nameof(dependencies));

Dependencies = dependencies;
}

/// <summary>
/// Dependencies used to create a <see cref="ModelCustomizer" />
/// </summary>
protected virtual ModelCustomizerDependencies Dependencies { get; }

/// <summary>
/// Performs additional configuration of the model in addition to what is discovered by convention. This default implementation
/// builds the model for a given context by calling <see cref="DbContext.OnConfiguring(DbContextOptionsBuilder)" />
@@ -38,6 +45,24 @@ public ModelCustomizer([NotNull] ModelCustomizerDependencies dependencies)
/// <param name="dbContext">
/// The context instance that the model is being created for.
/// </param>
public virtual void Customize(ModelBuilder modelBuilder, DbContext dbContext) => dbContext.OnModelCreating(modelBuilder);
public virtual void Customize(ModelBuilder modelBuilder, DbContext dbContext)
{
FindSets(modelBuilder, dbContext);

dbContext.OnModelCreating(modelBuilder);
}

/// <summary>
/// Adds the entity types found in <see cref="DbSet{TEntity}" /> properties on the context to the model.
/// </summary>
/// <param name="modelBuilder"> The <see cref="ModelBuilder" /> being used to build the model. </param>
/// <param name="context"> The context to find <see cref="DbSet{TEntity}" /> properties on. </param>
protected virtual void FindSets([NotNull] ModelBuilder modelBuilder, [NotNull] DbContext context)
{
foreach (var setInfo in Dependencies.SetFinder.FindSets(context))
{
modelBuilder.Entity(setInfo.ClrType);
}
}
}
}
23 changes: 22 additions & 1 deletion src/EFCore/Infrastructure/ModelCustomizerDependencies.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Utilities;

namespace Microsoft.EntityFrameworkCore.Infrastructure
{
/// <summary>
@@ -36,8 +40,25 @@ public sealed class ModelCustomizerDependencies
/// </para>
/// </summary>
// ReSharper disable once EmptyConstructor
public ModelCustomizerDependencies()
public ModelCustomizerDependencies([NotNull] IDbSetFinder setFinder)
{
Check.NotNull(setFinder, nameof(setFinder));

SetFinder = setFinder;
}

/// <summary>
/// Gets the <see cref="IDbSetFinder" /> that will locate the <see cref="DbSet{TEntity}" /> properties
/// on the derived context.
/// </summary>
public IDbSetFinder SetFinder { get; }

/// <summary>
/// Clones this dependency parameter object with one service replaced.
/// </summary>
/// <param name="setFinder"> A replacement for the current dependency of this type. </param>
/// <returns> A new parameter object with the given service replaced. </returns>
public ModelCustomizerDependencies With([NotNull] IDbSetFinder setFinder)
=> new ModelCustomizerDependencies(setFinder);
}
}
15 changes: 0 additions & 15 deletions src/EFCore/Infrastructure/ModelSource.cs
Original file line number Diff line number Diff line change
@@ -77,8 +77,6 @@ protected virtual IModel CreateModel(

internalModelBuilder.Metadata.SetProductVersion(ProductInfo.GetVersion());

FindSets(modelBuilder, context);

Dependencies.ModelCustomizer.Customize(modelBuilder, context);

internalModelBuilder.Validate();
@@ -96,18 +94,5 @@ protected virtual IModel CreateModel(
/// <returns> The convention set to be used. </returns>
protected virtual ConventionSet CreateConventionSet([NotNull] IConventionSetBuilder conventionSetBuilder)
=> conventionSetBuilder.AddConventions(Dependencies.CoreConventionSetBuilder.CreateConventionSet());

/// <summary>
/// Adds the entity types found in <see cref="DbSet{TEntity}" /> properties on the context to the model.
/// </summary>
/// <param name="modelBuilder"> The <see cref="ModelBuilder" /> being used to build the model. </param>
/// <param name="context"> The context to find <see cref="DbSet{TEntity}" /> properties on. </param>
protected virtual void FindSets([NotNull] ModelBuilder modelBuilder, [NotNull] DbContext context)
{
foreach (var setInfo in Dependencies.SetFinder.FindSets(context))
{
modelBuilder.Entity(setInfo.ClrType);
}
}
}
}
25 changes: 4 additions & 21 deletions src/EFCore/Infrastructure/ModelSourceDependencies.cs
Original file line number Diff line number Diff line change
@@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal;
using Microsoft.EntityFrameworkCore.Utilities;

@@ -45,28 +44,20 @@ public sealed class ModelSourceDependencies
/// </para>
/// </summary>
public ModelSourceDependencies(
[NotNull] IDbSetFinder setFinder,
[NotNull] ICoreConventionSetBuilder coreConventionSetBuilder,
[NotNull] IModelCustomizer modelCustomizer,
[NotNull] IModelCacheKeyFactory modelCacheKeyFactory)
{
Check.NotNull(setFinder, nameof(setFinder));

Check.NotNull(coreConventionSetBuilder, nameof(coreConventionSetBuilder));
Check.NotNull(modelCustomizer, nameof(modelCustomizer));
Check.NotNull(modelCacheKeyFactory, nameof(modelCacheKeyFactory));

SetFinder = setFinder;
CoreConventionSetBuilder = coreConventionSetBuilder;
ModelCustomizer = modelCustomizer;
ModelCacheKeyFactory = modelCacheKeyFactory;
}

/// <summary>
/// Gets the <see cref="IDbSetFinder" /> that will locate the <see cref="DbSet{TEntity}" /> properties
/// on the derived context.
/// </summary>
public IDbSetFinder SetFinder { get; }

/// <summary>
/// Gets the <see cref="ICoreConventionSetBuilder" /> that will build the conventions to be used
/// to build the model.
@@ -85,36 +76,28 @@ public ModelSourceDependencies(
/// </summary>
public IModelCacheKeyFactory ModelCacheKeyFactory { get; }

/// <summary>
/// Clones this dependency parameter object with one service replaced.
/// </summary>
/// <param name="setFinder"> A replacement for the current dependency of this type. </param>
/// <returns> A new parameter object with the given service replaced. </returns>
public ModelSourceDependencies With([NotNull] IDbSetFinder setFinder)
=> new ModelSourceDependencies(setFinder, CoreConventionSetBuilder, ModelCustomizer, ModelCacheKeyFactory);

/// <summary>
/// Clones this dependency parameter object with one service replaced.
/// </summary>
/// <param name="coreConventionSetBuilder"> A replacement for the current dependency of this type. </param>
/// <returns> A new parameter object with the given service replaced. </returns>
public ModelSourceDependencies With([NotNull] ICoreConventionSetBuilder coreConventionSetBuilder)
=> new ModelSourceDependencies(SetFinder, coreConventionSetBuilder, ModelCustomizer, ModelCacheKeyFactory);
=> new ModelSourceDependencies(coreConventionSetBuilder, ModelCustomizer, ModelCacheKeyFactory);

/// <summary>
/// Clones this dependency parameter object with one service replaced.
/// </summary>
/// <param name="modelCustomizer"> A replacement for the current dependency of this type. </param>
/// <returns> A new parameter object with the given service replaced. </returns>
public ModelSourceDependencies With([NotNull] IModelCustomizer modelCustomizer)
=> new ModelSourceDependencies(SetFinder, CoreConventionSetBuilder, modelCustomizer, ModelCacheKeyFactory);
=> new ModelSourceDependencies(CoreConventionSetBuilder, modelCustomizer, ModelCacheKeyFactory);

/// <summary>
/// Clones this dependency parameter object with one service replaced.
/// </summary>
/// <param name="modelCacheKeyFactory"> A replacement for the current dependency of this type. </param>
/// <returns> A new parameter object with the given service replaced. </returns>
public ModelSourceDependencies With([NotNull] IModelCacheKeyFactory modelCacheKeyFactory)
=> new ModelSourceDependencies(SetFinder, CoreConventionSetBuilder, ModelCustomizer, modelCacheKeyFactory);
=> new ModelSourceDependencies(CoreConventionSetBuilder, ModelCustomizer, modelCacheKeyFactory);
}
}
6 changes: 6 additions & 0 deletions src/EFCore/Metadata/Conventions/ConventionSet.cs
Original file line number Diff line number Diff line change
@@ -37,6 +37,12 @@ public class ConventionSet
public virtual IList<IEntityTypeAnnotationChangedConvention> EntityTypeAnnotationChangedConventions { get; }
= new List<IEntityTypeAnnotationChangedConvention>();

/// <summary>
/// Conventions to run when an annotation is set or removed on a model.
/// </summary>
public virtual IList<IModelAnnotationChangedConvention> ModelAnnotationChangedConventions { get; }
= new List<IModelAnnotationChangedConvention>();

/// <summary>
/// Conventions to run when a foreign key is added.
/// </summary>
15 changes: 15 additions & 0 deletions src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.cs
Original file line number Diff line number Diff line change
@@ -89,6 +89,21 @@ public virtual Annotation OnEntityTypeAnnotationSet(
annotation,
oldAnnotation);

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public virtual Annotation OnModelAnnotationSet(
[NotNull] InternalModelBuilder modelBuilder,
[NotNull] string name,
[CanBeNull] Annotation annotation,
[CanBeNull] Annotation oldAnnotation)
=> _scope.OnModelAnnotationSet(
Check.NotNull(modelBuilder, nameof(modelBuilder)),
Check.NotNull(name, nameof(name)),
annotation,
oldAnnotation);

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
55 changes: 55 additions & 0 deletions src/EFCore/Metadata/Conventions/Internal/ConventionNode.cs
Original file line number Diff line number Diff line change
@@ -111,6 +111,16 @@ public virtual Annotation OnEntityTypeAnnotationSet(
return annotation;
}

public virtual Annotation OnModelAnnotationSet(
[NotNull] InternalModelBuilder modelBuilder,
[NotNull] string name,
[CanBeNull] Annotation annotation,
[CanBeNull] Annotation oldAnnotation)
{
Add(new OnModelAnnotationSetNode(modelBuilder, name, annotation, oldAnnotation));
return annotation;
}

public virtual InternalRelationshipBuilder OnForeignKeyAdded([NotNull] InternalRelationshipBuilder relationshipBuilder)
{
Add(new OnForeignKeyAddedNode(relationshipBuilder));
@@ -327,6 +337,29 @@ public override Annotation OnEntityTypeAnnotationSet(
return annotation;
}

public override Annotation OnModelAnnotationSet(
InternalModelBuilder modelBuilder,
string name,
Annotation annotation,
Annotation oldAnnotation)
{
if (modelBuilder.Metadata.Builder == null)
{
return null;
}

foreach (var modelAnnotationSetConvention in _conventionSet.ModelAnnotationChangedConventions)
{
var newAnnotation = modelAnnotationSetConvention.Apply(modelBuilder, name, annotation, oldAnnotation);
if (newAnnotation != annotation)
{
return newAnnotation;
}
}

return annotation;
}

public override InternalRelationshipBuilder OnForeignKeyAdded(InternalRelationshipBuilder relationshipBuilder)
{
if (relationshipBuilder.Metadata.Builder == null)
@@ -767,6 +800,28 @@ public OnEntityTypeAnnotationSetNode(
public override ConventionNode Accept(ConventionVisitor visitor) => visitor.VisitOnEntityTypeAnnotationSet(this);
}

private class OnModelAnnotationSetNode : ConventionNode
{
public OnModelAnnotationSetNode(
InternalModelBuilder modelBuilder,
string name,
Annotation annotation,
Annotation oldAnnotation)
{
ModelBuilder = modelBuilder;
Name = name;
Annotation = annotation;
OldAnnotation = oldAnnotation;
}

public InternalModelBuilder ModelBuilder { get; }
public string Name { get; }
public Annotation Annotation { get; }
public Annotation OldAnnotation { get; }

public override ConventionNode Accept(ConventionVisitor visitor) => visitor.VisitOnModelAnnotationSet(this);
}

private class OnForeignKeyAddedNode : ConventionNode
{
public OnForeignKeyAddedNode(InternalRelationshipBuilder relationshipBuilder)
7 changes: 7 additions & 0 deletions src/EFCore/Metadata/Conventions/Internal/ConventionVisitor.cs
Original file line number Diff line number Diff line change
@@ -35,6 +35,7 @@ public virtual ConventionScope VisitConventionScope(ConventionScope node)
public virtual OnEntityTypeMemberIgnoredNode VisitOnEntityTypeMemberIgnored(OnEntityTypeMemberIgnoredNode node) => node;
public virtual OnBaseEntityTypeSetNode VisitOnBaseEntityTypeSet(OnBaseEntityTypeSetNode node) => node;
public virtual OnEntityTypeAnnotationSetNode VisitOnEntityTypeAnnotationSet(OnEntityTypeAnnotationSetNode node) => node;
public virtual OnModelAnnotationSetNode VisitOnModelAnnotationSet(OnModelAnnotationSetNode node) => node;
public virtual OnForeignKeyAddedNode VisitOnForeignKeyAdded(OnForeignKeyAddedNode node) => node;
public virtual OnForeignKeyRemovedNode VisitOnForeignKeyRemoved(OnForeignKeyRemovedNode node) => node;
public virtual OnKeyAddedNode VisitOnKeyAdded(OnKeyAddedNode node) => node;
@@ -92,6 +93,12 @@ public override OnEntityTypeAnnotationSetNode VisitOnEntityTypeAnnotationSet(OnE
return null;
}

public override OnModelAnnotationSetNode VisitOnModelAnnotationSet(OnModelAnnotationSetNode node)
{
Dispatcher._immediateConventionScope.OnModelAnnotationSet(node.ModelBuilder, node.Name, node.Annotation, node.OldAnnotation);
return null;
}

public override OnForeignKeyAddedNode VisitOnForeignKeyAdded(OnForeignKeyAddedNode node)
{
Dispatcher._immediateConventionScope.OnForeignKeyAdded(node.RelationshipBuilder);
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata.Internal;

namespace Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal
{
/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public interface IModelAnnotationChangedConvention
{
/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
Annotation Apply(
[NotNull] InternalModelBuilder modelBuilder,
[NotNull] string name,
[CanBeNull] Annotation annotation,
[CanBeNull] Annotation oldAnnotation);
}
}
11 changes: 11 additions & 0 deletions src/EFCore/Metadata/Internal/Model.cs
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@
using Microsoft.EntityFrameworkCore.Metadata.Conventions;
using Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal;
using Microsoft.EntityFrameworkCore.Utilities;
using Microsoft.EntityFrameworkCore.Infrastructure;

namespace Microsoft.EntityFrameworkCore.Metadata.Internal
{
@@ -462,6 +463,16 @@ public virtual void Unignore([NotNull] string name)
_ignoredTypeNames.Remove(name);
}

/// <summary>
/// Runs the conventions when an annotation was set or removed.
/// </summary>
/// <param name="name"> The key of the set annotation. </param>
/// <param name="annotation"> The annotation set. </param>
/// <param name="oldAnnotation"> The old annotation. </param>
/// <returns> The annotation that was set. </returns>
protected override Annotation OnAnnotationSet(string name, Annotation annotation, Annotation oldAnnotation)
=> ConventionDispatcher.OnModelAnnotationSet(Builder, name, annotation, oldAnnotation);

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
62 changes: 61 additions & 1 deletion src/EFCore/Properties/CoreStrings.Designer.cs
24 changes: 24 additions & 0 deletions src/EFCore/Properties/CoreStrings.resx
Original file line number Diff line number Diff line change
@@ -777,4 +777,28 @@
<value>The same entity is being tracked as different delegated identity entity types '{diet1}' and '{diet2}'. If a property value changes it will result in two store changes, which might not be the desired outcome.</value>
<comment>Warning CoreEventId.DuplicateDietInstanceWarning string string</comment>
</data>
<data name="DbFunctionInvalidReturnType" xml:space="preserve">
<value>The Db Function '{dbFunction}' has an invalid return type '{returnType}'.</value>
</data>
<data name="DbFunctionNameEmpty" xml:space="preserve">
<value>Db Function has no name set.</value>
</data>
<data name="DbFunctionParametersDuplicateIndex" xml:space="preserve">
<value>Db Function '{dbFunctionName}' has parameters with duplicate indexes.</value>
</data>
<data name="DbFunctionNonContinuousIndex" xml:space="preserve">
<value>Db Function '{dbFunctionName}' has a non continuous parameter index.</value>
</data>
<data name="DbFunctionInvalidParameterType" xml:space="preserve">
<value>The parameter '{dbParameter}' Db Function '{dbFunction}' has an invalid type.</value>
</data>
<data name="DbFunctionParameterNotFound" xml:space="preserve">
<value>Db Function '{dbFunctionName}' has no parameter '{dbParameterName}'. Check the method signature for the correct parameter name.</value>
</data>
<data name="DbFunctionDbContextMethodMustBeStatic" xml:space="preserve">
<value>Db Function '{dbFunctionName}' must be a static method.</value>
</data>
<data name="DbFunctionGenericMethodNotSupported" xml:space="preserve">
<value>The Db Function '{dbFunction}' is generic. Generic methods are not supported.</value>
</data>
</root>
Loading