Ho bisogno di controllare la stessa condizione specifica ( where
la clausola) molte volte:
return _ctx.Projects.Where(p => p.CompanyId == companyId &&
(p.Type == Enums.ProjectType.Open ||
p.Invites.Any(i => i.InviteeId == userId))).ToList()
La parte successiva a "&&" causerà che l'utente non è in grado di recuperare progetti limitati.
Volevo astrarre questo controllo per una funzione. In futuro queste condizioni potrebbero cambiare e non voglio sostituire tutte le query LINQ.
Ho fatto questo con il seguente metodo di estensione:
public static IQueryable<Project> IsVisibleForResearcher(this IQueryable<Project> projects, string userId)
{
return projects.Where(p => p.Type == Enums.ProjectType.Open ||
p.Invites.Any(i => i.InviteeId == userId));
}
Ora posso modificare la query LINQ per:
return _ctx.Projects.Where(p => p.CompanyId == companyId)
.IsVisibleForResearcher(userId).ToList()
Questo genera la stessa query SQL. Ora il mio problema inizia quando voglio usare questo metodo di estensione su un altro DbSet che ha progetti.
Immagina che un'azienda abbia dei progetti. E voglio solo recuperare le aziende in cui l'utente può almeno vedere un progetto.
return _ctx.Companies
.Where(c => c.Projects.Where(p =>
p.Type == Enums.ProjectType.Open ||
p.Invites.Any(i => i.InviteeId == userId))
.Any())
Anche qui mi piace usare il metodo di estensione.
return _ctx.Companies
.Where(c => c.Projects.AsQueryable().IsVisibleForCompanyAccount(userId).Any())
Ciò genera seguente eccezione:
Un'eccezione di tipo "System.NotSupportedException" si è verificata in Remotion.Linq.dll ma non è stata gestita nel codice utente
Ulteriori informazioni: Impossibile analizzare l'espressione 'c.Projects.AsQueryable ()': questo overload del metodo 'System.Linq.Queryable.AsQueryable' non è attualmente supportato.
Di quelli che ho creato i seguenti metodi di estensione:
public static IEnumerable<Project> IsVisibleForResearcher(this ICollection<Project> projects, string userId)
{
return projects.Where(p => p.Type == Enums.ProjectType.Open ||
p.Invites.Any(i => i.InviteeId == userId));
}
Ma anche questo non ha funzionato.
Qualcuno ha un'idea? O un passo nella giusta direzione.
Sto usando Entity Framework Core su .NET Core
AGGIORNAMENTO :
Usando Expression<Func<>>
provocato la stessa eccezione:
'System.Linq.Queryable.AsQueryable' non è attualmente supportato.
AGGIORNAMENTO 2
Thx @ ivan-stoev per aver fornito una soluzione. Ho ancora un problema. Voglio anche recuperare il conteggio dei progetti "visibili".
L'ho risolto facendo così:
var companies = _ctx.Companies
.WhereAny(c => c.Projects, Project.IsProjectVisibleForResearcher(userId))
.Select(c => new CompanyListDto
{
Id = c.Id,
Name = c.Name,
LogoId = c.LogoId,
ProjectCount = _ctx.Projects.Where(p => p.CompanyId == c.Id)
.Count(Project.IsProjectVisibleForResearcher(userId))
});
Ma non trovo il modo di usare semplicemente c.Projects
invece di ctx.Projects.Where(p => p.CompanyId == c.Id)
L'SQL che viene generato è corretto, ma vorrei evitare questo controllo non necessario.
Cordialmente, Brecht
L'utilizzo di espressioni / metodi personalizzati all'interno dell'espressione di query IQueryable<T>
è sempre stato problematico e richiede l'elaborazione di un post di un albero di espressioni. Ad esempio, LinqKit fornisce AsExpandable
, Invoke
e Expand
metodi di estensione personalizzati per questo scopo.
Anche se non così generico, ecco una soluzione per i tuoi casi d'uso di esempio senza l'uso di pacchetti di terze parti.
Innanzitutto, estrai la parte dell'espressione del predicato in un metodo. Il posto logico IMO è la classe Project
:
public class Project
{
// ...
public static Expression<Func<Project, bool>> IsVisibleForResearcher(string userId)
{
return p => p.Type == Enums.ProjectType.Open ||
p.Invites.Any(i => i.InviteeId == userId);
}
}
Quindi, crea un metodo di estensione personalizzato come questo:
public static class QueryableExtensions
{
public static IQueryable<T> WhereAny<T, E>(this IQueryable<T> source, Expression<Func<T, IEnumerable<E>>> elements, Expression<Func<E, bool>> predicate)
{
var body = Expression.Call(
typeof(Enumerable), "Any", new Type[] { typeof(E) },
elements.Body, predicate);
return source.Where(Expression.Lambda<Func<T, bool>>(body, elements.Parameters));
}
}
Con questo design, non è necessario il tuo attuale metodo di estensione, perché per la query Projects
puoi utilizzare:
var projects = _ctx.Projects
.Where(p => p.CompanyId == companyId)
.Where(Project.IsVisibleForResearcher(userId));
e per le Companies
:
var companies = _ctx.Companies
.WhereAny(c => c.Projects, Project.IsVisibleForResearcher(userId));
Aggiornamento: questa soluzione è piuttosto limitata, quindi se si hanno diversi casi d'uso (specialmente all'interno dell'espressione Select
come nel secondo aggiornamento), è meglio ricorrere ad alcuni pacchetti di terze parti. Ad esempio, ecco la soluzione LinqKit:
// LInqKit requires expressions to be put into variables
var projects = Linq.Expr((Company c) => c.Projects);
var projectFilter = Project.IsVisibleForResearcher(userId);
var companies = db.Companies.AsExpandable()
.Where(c => projects.Invoke(c).Any(p => projectFilter.Invoke(p)))
.Select(c => new CompanyListDto
{
Id = c.Id,
Name = c.Name,
LogoId = c.LogoId,
ProjectCount = projects.Invoke(c).Count(p => projectFilter.Invoke(p))
});