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:
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 >
3 Réponses :
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.
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?
(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.
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));
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).
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?