1
votes

Comment puis-je ajouter une expression Where générée dynamiquement pour un objet navigable dans ma requête Linq-To-SQL?

Mon client aimerait avoir une méthode d'envoi sur un tableau de valeurs de champ (chaîne), valeur (chaîne) et comparaison (enum) afin de récupérer leurs données .

.Lambda #Lambda3<System.Func`2[ClassroomEntity,System.Boolean]>(ClassroomEntity $var1)
{
    $var1.LeadTeacherName == "Sharon Candelariatest"
}

Mon entreprise et moi n'avons jamais tenté de faire quelque chose de tel auparavant, c'est donc à mon équipe de trouver une solution viable. C'est le résultat de travailler sur une solution avec une semaine ou deux de recherche.

Ce qui fonctionne: Partie 1

J'ai créé un service qui est capable de récupérer les données de notre tableau Salle de classe . La récupération des données est effectuée dans Entity Framework Core au moyen de LINQ-to-SQL. La façon dont j'ai écrit ci-dessous fonctionne si l'un des champs fournis dans le filtre n'existe pas pour Classroom mais existe pour son organisation associée (le client voulait être capable de rechercher parmi les adresses des organisations) et a une propriété navigable.

.Call Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.AsNoTracking(.Call System.Linq.Queryable.Where(
        .Call System.Linq.Queryable.Where(
            .Constant<Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[ClassroomEntity]>(Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[ClassroomEntity]),
            '(.Lambda #Lambda1<System.Func`2[ClassroomEntity,System.Boolean]>)),
        '(.Lambda #Lambda2<System.Func`2[ClassroomEntity,System.Boolean]>)))

.Lambda #Lambda1<System.Func`2[ClassroomEntity,System.Boolean]>(ClassroomEntity $x)
{
    ($x.ClassroomId).HasValue
}

.Lambda #Lambda2<System.Func`2[ClassroomEntity,System.Boolean]>(ClassroomEntity $x)
{
    ($x.Organization).City == "Bronx"
}

Ce qui fonctionne: partie 2

La BuildExpression qui existe dans le code est quelque chose que j'ai créé en tant que tel (avec de la place pour l'expansion).

IQueryable<ClassroomEntity>classroomQuery = ClassroomEntity.Where(x => x.ClassroomId.HasValue).Where(x => x.Organization.City == "Atlanta").AsNoTracking();

Le problème / Se déplacer vers ma question

Alors que la jointure interne de la salle de classe et de l ' organisation fonctionne, je préfère pas besoin d'extraire un deuxième ensemble d'entités pour vérifier les valeurs qui sont navigables. Si je saisis une Ville comme nom de filtre, je ferais normalement ceci:

classroomQuery = classroomQuery.Where(x => x.Organization.BuildExpression(filter.Name, filter.Value, filter.Compare));

Cela ne fonctionne pas vraiment ici. P >

J'ai essayé plusieurs méthodes différentes pour obtenir ce que je recherche:

  • Une fonction compilée qui renverrait Func , mais lorsqu'elle est passée via LINQ-to-SQL, la requête ne l'inclut pas.
  • Je l'ai changé en une expression >, mais mon retour n'a pas renvoyé de valeur booléenne comme j'ai tenté de l'implémenter, donc cela n'a pas fonctionné.
  • J'ai changé la façon dont j'implémentais la propriété de navigation, mais aucune de mes fonctions ne lirait la valeur correctement.

En gros, existe-t-il un moyen d'implémenter ce qui suit de manière à ce que LINQ-to-SQL à partir d'Entity Framework Core fonctionne? D'autres options sont également les bienvenues.

classroomQuery = classroomQuery.Where(x => x.Organization.City == "Atlanta");

Edit 01:

Lorsque vous utilisez l'expression sans le générateur dynamique comme ceci:

public static IQueryable<T> BuildExpression<T>(this IQueryable<T> source, string columnName, string value, QueryableFilterCompareEnum? compare = QueryableFilterCompareEnum.Equal) {
    var param = Expression.Parameter(typeof(T));

    // Get the field/column from the Entity that matches the supplied columnName value
    // If the field/column does not exists on the Entity, throw an exception; There is nothing more that can be done
    MemberExpression dataField;
    try {
        dataField = Expression.Property(param, propertyName);
    } catch (ArgumentException ex) {
        if (ex.ParamName == "propertyName") {
            throw new ArgumentException($"Queryable selection does not have a \"{propertyName}\" field.", ex.ParamName);
        } else {
            throw new ArgumentException(ex.Message);
        }
    }

    ConstantExpression constant = !string.IsNullOrWhiteSpace(value)
        ? Expression.Constant(value.Trim(), typeof(string))
        : Expression.Constant(value, typeof(string));

    BinaryExpression binary = GetBinaryExpression(dataField, constant, compare);
    Expression<Func<T, bool>> lambda = (Expression<Func<T, bool>>)Expression.Lambda(binary, param)
    return source.Where(lambda);
}

private static Expression GetBinaryExpression(MemberExpression member, ConstantExpression constant, QueryableFilterCompareEnum? comparisonOperation) {
    switch (comparisonOperation) {
        case QueryableFilterCompareEnum.NotEqual:
            return Expression.Equal(member, constant);
        case QueryableFilterCompareEnum.GreaterThan:
            return Expression.GreaterThan(member, constant);
        case QueryableFilterCompareEnum.GreaterThanOrEqual:
            return Expression.GreaterThanOrEqual(member, constant);
        case QueryableFilterCompareEnum.LessThan:
            return Expression.LessThan(member, constant);
        case QueryableFilterCompareEnum.LessThanOrEqual:
            return Expression.LessThanOrEqual(member, constant);
        case QueryableFilterCompareEnum.Equal:
        default:
            return Expression.Equal(member, constant);
        }
    }
}

Le débogage se lit comme suit:

public async Task<IEnumerable<IExportClassroom>> GetClassroomsAsync(
    IEnumerable<QueryableFilter> queryableFilters = null) {
    var filters = queryableFilters?.ToList();

    IQueryable<ClassroomEntity> classroomQuery = ClassroomEntity.All().AsNoTracking();

    // The organization table may have filters searched against it
    // If any are, the organization table should be inner joined to all filters are used
    IQueryable<OrganizationEntity> organizationQuery = OrganizationEntity.All().AsNoTracking();
    var joinOrganizationQuery = false;

    // Loop through the supplied queryable filters (if any) to construct a dynamic LINQ-to-SQL queryable
    if (filters?.Count > 0) {
        foreach (var filter in filters) {
            try {
                classroomQuery = classroomQuery.BuildExpression(filter.Name, filter.Value, filter.Compare);
            } catch (ArgumentException ex) {
                if (ex.ParamName == "propertyName") {
                    organizationQuery = organizationQuery.BuildExpression(filter.Name, filter.Value, filter.Compare);
                    joinOrganizationQuery = true;
                } else {
                    throw new ArgumentException(ex.Message);
                }
            }
        }
    }

    // Inner join the classroom and organization queriables (if necessary)
    var query = joinOrganizationQuery
        ? classroomQuery.Join(organizationQuery, classroom => classroom.OrgId, org => org.OrgId, (classroom, org) => classroom)
        : classroomQuery;

    query = query.OrderBy(x => x.ClassroomId);

    IEnumerable<IExportClassroom> results = await query.Select(ClassroomMapper).ToListAsync();
    return results;
}

J'ai essayé avec le constructeur dynamique d'obtenir l'enseignant de Classroom, ce qui m'a donné un débogage de:

public class QueryableFilter {
    public string Name { get; set; }
    public string Value { get; set; }
    public QueryableFilterCompareEnum? Compare { get; set; }
}

Je n'arrive toujours pas à comprendre comment obtenir ($ var1.Organization) comme entité à partir de laquelle je lis. p >


5 commentaires

Conseil: écrivez votre requête en C # et utilisez le débogueur pour explorer l'expression. Vous verrez la structure et comprendrez comment générer l'expression attendue.


Vous devrez générer une expression Expression.Property : docs.microsoft.com/en-us/dotnet/api/...


Plug Shamelss: j'ai écrit une bibliothèque et visualiseur de débogage qui représente plus clairement la structure d'un arbre d'expression. (ping @Vernou) En particulier, vous pouvez voir les appels de méthode de fabrique nécessaires pour construire une expression similaire.


Est-ce que plusieurs filtres sur une entité doivent être combinés avec && ? Il semble évident que .Where (x => x.Organization.City == "Atlanta" && x.Organization.City == "Boston") ne renverra aucun résultat; .Where (x => x.Organization.City == "Atlanta"). Where (x => x.Organization.City == "Boston") non plus. Mais pour différents champs, cela aurait du sens: .Where (x => x.Organization.City == "Atlanta" && x.Organization.Size> 500) .


Vous mentionnez l'utilisation d'EF via LINQ to SQL. LINQ to SQL est une technologie différente de celle d'EF. Voulez-vous LINQ to EF?


3 Réponses :


0
votes

Si j'obtiens votre énoncé de problème, vous voulez pouvoir remonter la chaîne de propriétés de navigation.

Si c'est effectivement le cas, le vrai défi est d'obtenir les relations de navigation d'EF. Et c'est là que EF Core 3.1 m'a généré le SQL suivant:

SELECT [e].[Id], [e].[EmployeeName], [e].[TeamId]
FROM [Employees] AS [e]
LEFT JOIN [Teams] AS [t] ON [e].[TeamId] = [t].[Id]
LEFT JOIN [Companies] AS [c] ON [t].[CompanyId] = [c].[Id]
WHERE [c].[CompanyName] = N'Microsoft'
ORDER BY [e].[Id]

Cette approche semble produire résultat souhaité mais présente un problème: les noms de colonne doivent être uniques dans toutes vos entités. Cela peut probablement être résolu, mais comme je ne connais pas beaucoup de détails sur votre modèle de données, je vous en remets.


2 commentaires

Ouais, je ne savais pas combien donner car il s'agissait de la composition de l'entité de base de données d'un client et je ne savais pas combien je pouvais donner. Mais, puisque l'organisation a un ExternalRefNbr et que la salle de classe a un ExternalRefNbr (utilisé par une agence secondaire), la recherche à ce sujet ne fonctionnait pas non plus.


Vous avez donc probablement pensé aux règles que vous utiliseriez pour gérer ces situations?



0
votes

(Clause de non-responsabilité: j'ai écrit un code similaire à celui-ci, mais je n'ai pas réellement testé le code de cette réponse.)

Votre BuildExpression prend une requête (sous la forme d'un IQueryable ) et renvoie une autre requête. Cela contraint tous vos filtres à être appliqués à la propriété du paramètre - x.ClassroomId - lorsque vous voulez réellement en appliquer certains à une propriété d'une propriété du paramètre - x.Organization.City .

Je suggérerais une méthode GetFilterExpression , qui produit l'expression de filtre à partir d'une expression de base arbitraire:

private static Dictionary<QueryableFilterCompareEnum, ExpressionType> comparisonMapping = new  Dictionary<QueryableFilterCompareEnum, ExpressionType> {
    [QueryableFilterCompareEnum.NotEqual] = ExpressionType.NotEqual,
    [QueryableFilterCompareEnum.GreaterThan] = ExpressionType.GreaterThan,
    [QueryableFilterCompareEnum.GreaterThanOrEqual] = ExpressionType.GreaterThanOrEqual,
    [QueryableFilterCompareEnum.LessThan] = ExpressionType.LessThan,
    [QueryableFilterCompareEnum.LessThanOrEqual] = ExpressionType.LessThanOrEqual,
    [QueryableFilterCompareEnum.Equal] = ExpressionType.Equal
}

private static Expression GetBinaryExpression(MemberExpression member, ConstantExpression constant, QueryableFilterCompareEnum? comparisonOperation) {
    comparisonOperation = comparisonOperation ?? QueryableFilterCompareEnum.Equal;
    var expressionType = comparisonMapping[comparisonOperation];
    return Expression.MakeBinary(
        expressionType,
        member,
        constant
    );
}

Dans GetClassroomsAsync , vous pouvez soit créer l'expression de filtre par rapport au paramètre d'origine ClassroomEntity , soit par rapport à la valeur renvoyée de Organisation sur le paramètre, en passant une expression différente:

private static Expression LogicalCombined(IEnumerable<Expression> exprs, ExpressionType expressionType = ExpressionType.AndAlso) {
    // ensure the expression type is a boolean operator
    switch (expressionType) {
        case ExpressionType.And:
        case ExpressionType.AndAlso:
        case ExpressionType.Or:
        case ExpressionType.OrElse:
        case ExpressionType.ExclusiveOr:
            break;
        default:
            throw new ArgumentException("Invalid expression type for logically combining expressions.");
    }
    Expression? final = null;
    foreach (var expr in exprs) {
        if (final is null) {
            final = expr;
            continue;
        }
        final = Expression.MakeBinary(expressionType, final, expr);
    }
    return final;
}

LogicalCombined prend plusieurs booléens - renvoyant des expressions et les combine en une seule expression:

public async Task<IEnumerable<IExportClassroom>> GetClassroomsAsync(IEnumerable<QueryableFilter> queryableFilters = null) {
    var filters = queryableFilters?.ToList();
    var param = Expression.Parameter(typeof(ClassroomEntity));
    var orgExpr = Expression.Property(param, "Organization"); // equivalent of x.Organization

    IQueryable<ClassroomEntity> query = ClassroomEntity.All().AsNoTracking();

    if (filters is {}) {
        // Map the filters to expressions, applied to the `x` or to the `x.Organization` as appropriate
        var filterExpressions = filters.Select(filter => {
            try {
                return GetFilterExpression(param, filter.Name, filter.Value, filter.Compare);
            } catch (ArgumentException ex) {
                if (ex.ParamName == "propertyName") {
                    return GetFilterExpression(orgExpr, filter.Name, filter.Value, filter.Compare);
                } else {
                    throw new ArgumentException(ex.Message);
                }
            }
        });

        // LogicalCombined is shown later in the answer
        query = query.Where(
            Expression.Lambda<Func<ClassroomEntity, bool>>(LogicalCombined(filters))
        );
    }

    query = query.OrderBy(x => x.ClassroomId);
    IEnumerable<IExportClassroom> results = await query.Select(ClassroomMapper).ToListAsync();
    return results;
}

Quelques suggestions:

Comme je l'ai écrit, GetFilterExpression est une méthode statique . Étant donné que tous les arguments (à l'exception de l'expression de base) proviennent de QueryableFilter , vous pouvez envisager d'en faire une méthode d'instance hors de QueryableFilter.


Je suggérerais également de changer GetBinaryExpression pour utiliser un dictionnaire pour mapper de QueryableFilterCompareEnum au ExpressionType intégré. Ensuite, l'implémentation de GetBinaryExpression n'est qu'un wrapper pour la méthode intégrée Expression.MakeBinary :

private static Expression GetFilterExpression(Expression baseExpr, string columnName, string value, QueryableFilterCompareEnum? compare = QueryableFilterCompareEnum.Equal) {
    MemberExpression dataField;
    try {
        dataField = Expression.Property(baseExpr, columnName);
    } catch (ArgumentException ex) {
        if (ex.ParamName == "propertyName") {
            throw new ArgumentException($"Base expression type does not have a \"{propertyName}\" field.", ex.ParamName);
        } else {
            throw new ArgumentException(ex.Message);
        }
    }

    if (!string.IsNullOrWhiteSpace(value)) {
        value = value.Trim();
    }
    ConstantExpression constant = Expression.Constant(value, typeof(string));

    BinaryExpression binary = GetBinaryExpression(dataField, constant, compare);
    return binary;
}

Les deux GetFilterExpression et GetClassroomsAsync gèrent la possibilité que la propriété spécifiée n'existe pas sur ClassroomEntity ou OrganizationEntity , en essayant de construire l'expression d'accès aux membres et en gérant l'exception levée.

Il peut être plus clair d'utiliser la réflexion pour tester si la propriété existe sur l'un ou l'autre type.

Plus , vous pouvez envisager de stocker un HashSet statique avec tous les noms de champ valides, et vérifier par rapport à cela.


0 commentaires

2
votes

Si vous pouvez demander au client de fournir l'expression de notation par points complète pour la propriété. par exemple "Organization.City";

    dataField = (MemberExpression)propertyName.split(".")
        .Aggregate(
            (Expression)param,
            (result,name) => Expression.Property(result, name));


2 commentaires

Ceci est préférable, car cela évite toute ambiguïté si les deux entités ont une propriété portant le même nom.


Cela fonctionne très bien car l'organisation a un ExternalRefNbr et la classe a un ExternalRefNbr (utilisé par une agence secondaire).