Custom SQLFunctionExpression pour EFCore — Part 2

Vandenbussche Julien
Just-Tech-IT
Published in
3 min readJan 20, 2023

Dans l’article précèdent, je vous parlais des expressions personnalisées version code-less, cet article sera lui dédié à la version dite plus “complexe” mais plus flexible.

Fonction personnalisée: version plus flexible

A partir des irritants énoncés précédemment, j’ai donc voulu intégrer cette fonctionnalité un peu plus en profondeur dans le mécanisme d’EFCore.

Il faut savoir qu’EFCore utilise son propre context IoC… Il ne sert à rien d’enregistrer nos services dans le context applicatif, EFCore n’en prendra pas compte…. A partir de là, vous n’êtes pas plus avancés :-P.

Notre point d’entrée pour intégrer notre fonctionnalité se fera par la classe SqlServerDbContextOptionsBuilder. Cette classe une fois castée vers IRelationalDbContextOptionsBuilderInfrastructure nous permettra d’avoir accès à la propriété OptionsBuilder pour ensuite y enregistrer notre extension.

public static DbContextOptionsBuilder UseMyCustomFeature(
this IRelationalDbContextOptionsBuilderInfrastructure builderInfrastructure)
{
var optionsBuilder = builderInfrastructure.OptionsBuilder;
var extension = optionsBuilder.Options.FindExtension<DbContextOptionsExtension>()
?? new DbContextOptionsExtension();
((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension);
return optionsBuilder;
}

Ce qui nous permet de l’utiliser comme ceci:

.AddDbContextPool<MyDbContext>(
(provider, builder) =>
builder.UseSqlServer(
configuration.GetConnectionString("MyDbContext"),
optionsBuilder => { optionsBuilder.UseMyCustomFeature(); }))

Nous allons avoir besoin d’une classe qui implémentera IDbContextOptionsExtension afin d’enregistrer notre Filter ainsi que notre Plugin dans le pipeline d’EFCore.

internal sealed class DbContextOptionsExtension : IDbContextOptionsExtension
{
private DbContextOptionsExtensionInfo? info;

public void ApplyServices(IServiceCollection services)
{
services.AddScoped<IEvaluatableExpressionFilter, SqlDbFunctionEvaluatableExpressionFilter>()
.AddScoped<IMethodCallTranslatorPlugin, MethodCallTranslatorPlugin>();
}

public void Validate(IDbContextOptions options)
{
// Method intentionally left empty.
}

public DbContextOptionsExtensionInfo Info => this.info ??= new ExtensionInfo(this);

private sealed class ExtensionInfo : DbContextOptionsExtensionInfo
{
public ExtensionInfo(IDbContextOptionsExtension extension)
: base(extension)
{
}

public override bool IsDatabaseProvider => false;

public override string LogFragment { get; } = "Soundex: Ok";

public override int GetServiceProviderHashCode()
{
var hashCode = new HashCode();
return hashCode.ToHashCode();
}

public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) => false;

public override void PopulateDebugInfo(IDictionary<string, string> debugInfo)
{
}
}
}

La classe SqlDbFunctionEvaluatableExpressionFilter indiquera à EFCode de ne pas évaluer l’expression si celle-ci est présente, ainsi SoudexSqlFunctionExpression sera utilisée pour la génération d’une requête SQL par EntityFramework.

internal sealed class SqlDbFunctionEvaluatableExpressionFilter : EvaluatableExpressionFilter
{
private static readonly Type DeclaringType = SqlDbFunctionsExtensions.DeclaringType;

public SqlDbFunctionEvaluatableExpressionFilter(EvaluatableExpressionFilterDependencies dependencies)
: base(dependencies)
{
}

public override bool IsEvaluatableExpression(Expression expression, IModel model)
{
if (expression is not MethodCallExpression methodCallExpression
|| methodCallExpression.Method.DeclaringType != DeclaringType)
{
return base.IsEvaluatableExpression(expression, model);
}

return false;
}
}

La classe MethodCallTranslatorPlugin servira à intégrer les translators utiles à notre application.

internal sealed class MethodCallTranslatorPlugin : IMethodCallTranslatorPlugin
{
public MethodCallTranslatorPlugin(IRelationalTypeMappingSource typeMappingSource)
{
this.Translators = new[]
{
new MethodCallTranslator<SoundexSqlFunctionExpression>(typeMappingSource,
SqlDbFunctionsExtensions.SoundexMethod,
arguments => new SoundexSqlFunctionExpression(arguments))
};
}

public IEnumerable<IMethodCallTranslator> Translators { get; }
}

Et donc une classe de type IMedhodeCallTranslator pour nous permettre d’instancier SoundexSqlFunctionExpression lorsque la méthode Soundex sera détectée dans l’expression.

internal class MethodCallTranslator<TExpression> : IMethodCallTranslator where TExpression : SqlExpression
{
internal delegate TExpression CreateExpression(params SqlExpression[] arguments);
private readonly IRelationalTypeMappingSource typeMappingSource;
private readonly MethodInfo methodInfo;
private readonly CreateExpression createExpression;

public MethodCallTranslator(IRelationalTypeMappingSource typeMappingSource, MethodInfo methodInfo, CreateExpression createExpression)
{
this.typeMappingSource = typeMappingSource;
this.methodInfo = methodInfo;
this.createExpression = createExpression ?? throw new ArgumentNullException(nameof(createExpression));
}

public SqlExpression? Translate(
SqlExpression? instance,
MethodInfo method,
IReadOnlyList<SqlExpression> arguments,
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
{
if (method != this.methodInfo)
{
return null;
}

var expression = arguments[^1];
if (expression is SqlConstantExpression sqlConstantExpression)
{
var typeMapping = this.typeMappingSource.FindMapping(typeof(string));
expression = sqlConstantExpression.ApplyTypeMapping(typeMapping);
}

return this.createExpression(expression);
}
}

Vous allez me dire que ça en fait du code pour le même résultat… En effet, mais je ne vais pas vous lâcher comme ça si rapidement, dans mon prochain article vous comprendrez pourquoi j’ai privilégié cette approche ;-)

--

--

Vandenbussche Julien
Just-Tech-IT

Developper/TechLead chez AXA France, DotNet est mon terrain de jeu depuis plusieurs années. Craftman dans l’âme, j’adore échanger autour des sujets technologies