Custom SQLFunctionExpression pour EFCore — Part 3

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

Dans les précédents articles, je vous parlais des SQLFunctionExpression en version code-less et dans sa version dite plus “complexe” mais plus flexible. Ici, je vais vous détailler pourquoi j’ai privilégié une version plutôt qu’une autre et ce sera ensuite à vous de vous faire votre propre choix.

L’approche par les Tests

Depuis plusieurs années maintenant, lorsque je pars dans l’implémentation d’une fonctionnalité, j’attaque celle-ci par des tests (que ce soit test technique ou fonctionnel).

Test: Approche Code-Less

Du coup, lorsque je suis parti pour implémenter mon service permettant de requêter mon DbContext et d’utiliser la méthode Soundex, j’ai du bouchonner mon DbContext, le/les DbSet(s), affecter un comportement à la méthode Soundex (laquelle a due être passée en virtual…) et pour finir alimenter mon/mes DbSet(s).

var territories = Builder<Territory>.CreateListOfSize(20)
.All()
.With(t => t.TerritoryDescription = Faker.Address.UsTerritory())
.TheFirst(3)
.With((territory, index) => territory.TerritoryDescription = $"Samta {index}")
.TheNext(2)
.With((territory, index) => territory.TerritoryDescription = $"Santa {index}")
.TheLast(3)
.With((territory, index) => territory.TerritoryDescription = $"Semta {index}")
.Build()
.ToArray();
this.Populate(territories);
this.mockedDbContext.Setup(m => m.Soundex("senta")).Returns("S530");
this.mockedDbContext.Setup(m => m.Soundex(It.Is<string>( v => v.StartsWith("Samta ")))).Returns("S530");
this.mockedDbContext.Setup(m => m.Soundex(It.Is<string>( v => v.StartsWith("Santa ")))).Returns("S530");
this.mockedDbContext.Setup(m => m.Soundex(It.Is<string>( v => v.StartsWith("Semta ")))).Returns("S530");

Ce qui me dérange c’est que je ne bénéficie pas d’un vrai context de base de données… En effet, je shunte le comportement d’EntityFramework et je suis obligé de définir un bouchon pour chaque filtre que je souhaite appliquer… L’utilisation de la méthode CallBack de Moq pourrait être utilisée, cela implique de developper l’algorithme Soundex et possiblement de le dupliquer pour mes futures solutions (et oui on duplique aussi bien du code de production que de test via cette approche…)

var soudexResult = "0000";
this.mockedDbContext.Setup(m => m.Soundex(It.IsAny<string>()))
.Callback<string>((value) =>
{
soudexResult = SoudexComportement(value);
}).Returns(() => soudexResult);

Test: Approche plus complexe

Via cette approche, je vais pouvoir utiliser le package Microsoft.EntityFrameworkCore.InMemory qui va me permettre de garder un context de base de données et ainsi de ne pas devoir chunter EntityFramework et de bouchonner toutes les méthodes, DbSets de mon DbContext. La seule chose que je vais devoir faire c’est d’appliquer la même approche que j’ai eu lors de mon implémentation liée à mon 2eme article.

C’est à dire de développer une extension à InMemoryDbContextOptionsBuilder afin de pouvoir enregistrer ma classe InMemoryQueryableMethodTranslatingExpressionVisitor qui me permettra de détecter dans une Query EntityFramework l’appel à l’expression Soundex et de la remplacer par une méthode que j’aurais soigneusement implémentée.

Une fois ce travail réalisé, je n’ai plus qu’à l’utiliser dans mon test.

new ServiceCollection()
.AddDbContextPool<NorthwindContext>(builder =>
builder.UseInMemoryDatabase("Northwind", optionsBuilder => optionsBuilder
.UseAddedExpressions()))

j’’alimente mon/mes DbSet(s) avec des données fictives et le tour est joué, aucun bouchon à déclarer, j’utilise mon vrai DbContext. Mes extensions pour le provider Sql et InMemory peuvent éventuellement (et je vous le conseille) être publiées via un package NuGet (public ou non) ainsi vous en faites profiter tout le monde ;-).

var territories = Builder<Territory>.CreateListOfSize(20)
.All()
.With(t => t.TerritoryDescription = Faker.Address.UsTerritory())
.TheFirst(3)
.With((territory, index) => territory.TerritoryDescription = $"Samta {index}")
.TheNext(2)
.With((territory, index) => territory.TerritoryDescription = $"Santa {index}")
.TheLast(3)
.With((territory, index) => territory.TerritoryDescription = $"Semta {index}")
.Build()
.ToArray();

this.PopulateDbSet(territories);

Donc, vous l’aurez compris, même si la mise en place est plus complexe, je privilégie quand même la manière décrite dans la partie 2 de cette série d’articles car elle me permet de pouvoir faire des tests de bout-en-bout, ce qui est très appréciable lorsque vous appliquez la pratique BDD dans vos projets. Elle me permet également de ne pas dupliquer de code situé dans les classes partielles des DbContexts.

Je me suis permis de développer une librairie qui me permet d’enregistrer simplement mes nouvelles SqlExpression, SqlFunctionExpression au sein même d’EntityFrameworkCore, je pense la rendre publique et disponible via un package Nuget. Si toutefois cela vous intéresse d’y “jeter” un œil ou même d’y contribuer les sources sont disponibles: ldap-filter-to-lambda-expression et efcore-sqlexpression.

Packages disponible:

--

--

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