2
votes

Éviter l'erreur d'évaluation du client de requête sur une requête avec une définition de méthode dans la classe d'entité

Dans un projet .NET Core 2.1 , j'utilise EF Core avec un modèle de commande (en utilisant la bibliothèque MediatR ) sur une base de données SQL Server.

Je configure le projet pour éviter l'évaluation de la requête client, en utilisant ces paramètres:

public static void ApplyPhaseConversions<T>(this ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<PhaseArticle>()
        .Property(e => e.Family)
        .HasConversion(new ValueConverter<ArticleFamily, string> {
            v =>
            {
                switch (v)
                {
                    case ArticleFamily.Cell:
                        return "CEL";
                    case ArticleFamily.String:
                        return "STR";
                    case ArticleFamily.OtherRawMaterial:
                        return "RAW";
                    case ArticleFamily.SemiFinishedPanel:
                        return "SFP";
                    case ArticleFamily.FinishedPanel:
                        return "FP";
                    default:
                        return "";
                }
            },
            v =>
            {
                switch (v)
                {
                    case "CEL":
                        return ArticleFamily.Cell;
                    case "STR":
                        return ArticleFamily.String;
                    case "RAW":
                        return ArticleFamily.OtherRawMaterial;
                    case "SFP":
                        return ArticleFamily.SemiFinishedPanel;
                    case "FP":
                        return ArticleFamily.FinishedPanel;
                    default:
                        return ArticleFamily.Other;
                }
            }});
}

Maintenant, j'obtiens une QueryClientEvaluationException avec cette requête:

public class PhaseArticle
{
    public int Id { get; set; }
    public string Code { get; set; }
    public string Description { get; set; }
    public string UnitOfMeasure { get; set; }
    public string Category { get; set; }
    public string Group { get; set; }
    public string Family { get; set; }
    public double UnitCost { get; set; }
    public string AdditionalDescription { get; set; }
    public string ExternalCode { get; set;}
    public string ColorCode { get; set;}
    public string Note { get; set; }

    public ArticleFamily GetArticleFamily()
    {
        switch (Family)
        {
            case "CEL":
                return ArticleFamily.Cell;
            case "STR":
                return ArticleFamily.String;
            case "RAW":
                return ArticleFamily.OtherRawMaterial;
            case "SFP":
                return ArticleFamily.SemiFinishedPanel;
            case "FP":
                return ArticleFamily.FinishedPanel;
            default:
                return ArticleFamily.Other;
        }
    }
}

Le problème est sur l'appel de méthode a.GetArticleFamily () , car cette méthode est maintenant définie comme suit, dans la classe d'entité PhaseArticle :

var articleCodes = await PhaseContext.PhaseArticles
    .Where(a => !request.ArticleFamily.HasValue || a.GetArticleFamily() == request.ArticleFamily.Value)
    .ToListAsync(cancellationToken);

Maintenant, je veux savoir s'il est possible de conserver l'option QueryClientEvaluationWarning en refactorisant (et probablement en s'éloignant de la classe d'entité) ) la méthode GetArticleFamily ().

Mise à jour 2019/02/26

@StriplingWarrior J'ai de nouveau mis à jour le code avec votre suggestion concernant ValueConverter () , mais maintenant il donne cette erreur:

Impossible de convertir l'expression Lambda en un arbre d'expressions.

Mise à jour 2019/02/25

Suite à la suggestion de @StriplingWarrior, j'essaye d'écrire un convertisseur personnalisé mais je ne parviens pas à faire compiler mon code.

L'erreur avec le code ci-dessous concerne la valeur de retour du premier bloc switch (il s'agit d'une chaîne mais il devrait s'agir d'un enum ) et à propos de la valeur d'entrée attendue du deuxième bloc de commutation (c'est une chaîne mais on s'attend à ce qu'elle soit une énumération code>).

Voici le code:

var phaseOptions = new DbContextOptionsBuilder<PhaseDbContext>().UseSqlServer(configuration.GetConnectionString("PhaseDbContext"),
        sqlServerOptions => sqlServerOptions
            .EnableRetryOnFailure(
                maxRetryCount: 5,
                maxRetryDelay: TimeSpan.FromSeconds(30),
                errorNumbersToAdd: null))
    .ConfigureWarnings(warnings => warnings
        .Throw(RelationalEventId.QueryClientEvaluationWarning)) // Disable Client query evaluation
    .Options;


0 commentaires

3 Réponses :


1
votes

Vous pouvez créer une nouvelle variable et transférer le résultat request.ArticleFamily.Value afin qu'il puisse renvoyer ArticleFamily.Cell ou ArticleFamily.String, puis exécuter la requête

par exemple.

if (!ModelState.IsValid)
{
   return BadRequest(ModelState);
}

La validation des paramètres de la méthode doit être effectuée avant d'exécuter la requête. Une autre chose est ce qui se passe si la requête est null?

Il est également nécessaire de valider le objet de requête . Il peut y avoir un cas où vous faites une faute de frappe ou une erreur dans la structure (vous oubliez d'ajouter une virgule après avoir défini une valeur de champ) de l'objet JSON que vous envoyez à l'API. Dans un tel cas, l'objet de requête aurait une valeur null , il est donc nécessaire de valider un tel comportement. Par exemple. vous pouvez ajouter

if(request != null && !request.ArticleFamily.HasValue)
// or throw an exception here
 return ...;

ArticleFamily newVariable = (ArticleFamily)Enum.Parse(typeof(ArticleFamily), request.ArticleFamily);
var articleCodes = await PhaseContext.PhaseArticles
    .Where(a => a.Family == newVariable)
    .ToListAsync(cancellationToken);

dans l'action de votre contrôleur pour valider l'ensemble du corps de la requête. Le client recevra le message d'erreur approprié.


0 commentaires

2
votes

Il semble que vous utilisiez GetArticleFamily () pour effectuer une conversion entre les valeurs de la base de données et vos énumérations C #. EF Core possède une fonctionnalité intégrée appelée Conversions de valeur qui vise à résoudre ce problème: https://docs.microsoft.com/en-us/ef/core/modeling/value-conversions

Vous devriez être en mesure de définir un ValueConverter à traduire vers et depuis ArticleFamily , puis modifiez le type de la propriété Family en ArticleFamily et utilisez cette propriété dans votre requête:

var articleQuery = PhaseContext.PhaseArticles.AsQueryable();
if(request.ArticleFamily.HasValue)
{
    articleQuery = articleQuery.Where(a => a.Family == request.ArticleFamily.Value);
}
var articleCodes = await articleQuery.ToListAsync(cancellationToken);


9 commentaires

quel est l'intérêt d'exécuter cette requête si request.ArticleFamily est nul? Il en va de même si la demande est nulle.


@GoldenAge: Il semble que request.ArticleFamily est un filtre optionnel. S'il est nul, vous renvoyez tous les PhaseArticles .


(Je suppose que request ne devrait jamais être null. Il est probablement en train d'être initialisé à partir d'un objet JSON envoyé d'un client à une action d'API Web.)


En pratique, je suis d'accord. C'était plutôt une digression théorique car je ne suis pas en mesure de voir l'implémentation complète du problème décrit :)


donc j'ai fait une vérification et il y a une telle option lorsque l'objet de requête est null . J'ai édité ma réponse.


@GoldenAge L'utilisation de ValueConverter est la bonne solution EF Core pour ce problème (de mappage client vers db). Il effectuera automatiquement la conversion similaire et la même requête que ce que vous faites manuellement. La partie conditionnelle. Where n'est cependant pas nécessaire, car EF Core effectue une élimination constante des prédicats, de sorte que les requêtes générées seront les mêmes.


Merci pour cette clarification sur l'élimination constante des prédicats, Ivan. J'ai appris quelque chose de nouveau. :-)


@StriplingWarrior Je connais ValueConverter d'EF Core et je l'utilise déjà pour d'autres conversions simples d'énumérations en chaîne, mais dans ces cas, j'écris dans la base de données la même valeur de chaîne que l'énumération. Dans ce cas, j'ai besoin d'un commutateur, ou de toute façon d'une logique de conversion, de l'énumération à la chaîne et vice-versa. J'ai mis à jour le code car il me donne des erreurs.


@CheshireCat: Il semble que vous ayez la bonne idée en utilisant les instructions switch dans le convertisseur de valeur, mais en regardant les signatures de méthode pour Méthode HasConversion Je suis presque sûr que vous devez passer un nouveau ValueConverter < ArticleFamily, string> () plutôt que de transmettre ces délégués de conversion directement en tant que paramètres.



0
votes

Enfin, la solution était presque là, comme l'a également dit @StriplingWarrior.

En raison des limitations du compilateur C #, telles qu'il ne peut pas créer d'arbres d'expression pour ce code, la solution consiste à fabriquer le code de conversion en méthodes, puis appelez-les dans HasConversion.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<PhaseArticle>()
        .Property(e => e.Family)
        .HasConversion(new ValueConverter<ArticleFamily, string>(
            v => StringFromArticleFamily(v),
            v => ArticleFamilyFromString(v));
}

private static ArticleFamily ArticleFamilyFromString(string family)
{
    switch (family)
    {
        case "CEL":
            return ArticleFamily.Cell;
        case "STR":
            return ArticleFamily.String;
        case "RAW":
            return ArticleFamily.OtherRawMaterial;
        case "SFP":
            return ArticleFamily.SemiFinishedPanel;
        case "FP":
            return ArticleFamily.FinishedPanel;
        default:
            return ArticleFamily.Other;
    }
}

private static string StringFromArticleFamily(ArticleFamily articleFamily)
{
    switch (articleFamily)
    {
        case ArticleFamily.Cell:
            return "CEL";
        case ArticleFamily.String:
            return "STR";
        case ArticleFamily.OtherRawMaterial:
            return "RAW";
        case ArticleFamily.SemiFinishedPanel:
            return "SFP";
        case ArticleFamily.FinishedPanel:
            return "FP";
        default:
            return "";
    }
}


0 commentaires