Nel nostro database abbiamo un certo numero di tabelle che hanno corrispondenti tabelle di traduzione, con gli ID di lingua e regione (mappati ad altre tabelle) con la lingua 1 in inglese e la regione di default 1 in UK. Tutte le tabelle che hanno una tabella di conversione hanno le seguenti colonne predefinite (sebbene non sia stata definita alcuna interfaccia nelle classi del framework di entità):
<EntityTableName>
EntityTableNameID INT PK
Reference NVARCHAR NULL
[Any other columns]
<EntityTableNameTranslation>
EntityTableNameID INT NOT NULL
LanguageID INT NOT NULL
RegionID INT NULL
Title NVARCHAR NOT NULL
Description NVARCHAR NULL
La denominazione è coerente in tutto il database, quindi potremmo aggiungere interfacce se necessario, ma per ora ho cercato di farlo senza salvare lo sforzo.
La logica per determinare quale titolo e descrizione della traduzione restituire è: 1) Se esiste una corrispondenza esatta sia per la lingua che per la regione, restituirla 2) Se esiste una corrispondenza per la lingua, ma non la regione, restituire il valore predefinito "per quella lingua (che è dove l'RegionID è nullo, e ce ne sarà sempre uno per ogni lingua) 3) Se non c'è corrispondenza per la lingua, basta restituire il sistema predefinito (LanguageID = 1, RegionID IS NULL).
So che tutto questo potrebbe sembrare strano e tutti hanno modi migliori per farlo, ma questo è il brief con cui devo lavorare. Quindi questa è la funzione lambda group join che ho creato, che sta usando un'entità nel database chiamata "OrgGroup":
public static IEnumerable<TransViewModel> GetUserAreaOrgGroups(TransTestEntities context, int companyID, int languageID, int? regionID)
{
var transFull = context.OrgGroupTranslations.Where(tr => tr.LanguageID == languageID && tr.RegionID == regionID);
var transLang = context.OrgGroupTranslations.Where(tr => tr.LanguageID == languageID && !tr.RegionID.HasValue);
var transDefault = context.OrgGroupTranslations.Where(tr => tr.LanguageID == 1 && !tr.RegionID.HasValue);
var results = context.OrgGroups.Where(en => en.CompanyID == companyID)
.GroupJoin(transFull, en => en.OrgGroupID, tr => tr.OrgGroupID,
(en, tr) => new TransJoin<OrgGroup, OrgGroupTranslation> { Entity = en, TransFull = tr.DefaultIfEmpty().FirstOrDefault(), TransLang = null, TransDefault = null})
.GroupJoin(transLang, en => en.Entity.OrgGroupID, tr => tr.OrgGroupID,
(en, tr) => new TransJoin<OrgGroup, OrgGroupTranslation> { Entity = en.Entity, TransFull = en.TransFull, TransLang = tr.DefaultIfEmpty().FirstOrDefault(), TransDefault = null })
.GroupJoin(transDefault, en => en.Entity.OrgGroupID, tr => tr.OrgGroupID,
(en, tr) => new TransJoin<OrgGroup, OrgGroupTranslation> { Entity = en.Entity, TransFull = en.TransFull, TransLang = en.TransLang, TransDefault = tr.DefaultIfEmpty().FirstOrDefault() })
.Select(vm => new TransViewModel
{
EntityID = vm.Entity.OrgGroupID,
Title = (vm.TransFull ?? vm.TransLang ?? vm.TransDefault).Title,
Description = (vm.TransFull ?? vm.TransLang ?? vm.TransDefault).Description
});
return results;
}
Il che sembra funzionare come previsto, e ora sto provando a convertirlo in una funzione che accetterà i due tipi di tabella e userà alberi di espressioni per creare, eseguire e restituire la query equivalente. Sono arrivato fino a:
public static IEnumerable<TransViewModel> GetUserAreaTranslations<TEntity, TTrans>(TransTestEntities context, int companyID, int languageID, int? regionID)
{
// Get types
Type entityType = typeof(TEntity);
Type transType = typeof(TTrans);
string entityName = entityType.Name;
string transName = transType.Name;
// Parameters
var entityParam = Expression.Parameter(entityType, "en");
var transParam = Expression.Parameter(transType, "tr");
var combinedParam = new ParameterExpression[] { entityParam, transParam };
// Properties
var CompanyIDProp = Expression.Property(entityParam, "CompanyID");
var entityIDProp = Expression.Property(entityParam, entityName + "ID");
var transIDProp = Expression.Property(transParam, entityName + "ID");
var transLanProp = Expression.Property(transParam, "LanguageID");
var transRegProp = Expression.Property(transParam, "RegionID");
var transTitleProp = Expression.Property(transParam, "Title");
var transDescProp = Expression.Property(transParam, "Description");
// Tables
//TODO: Better way of finding pluralised table names
var entityTable = Expression.PropertyOrField(Expression.Constant(context), entityName + "s");
var transTable = Expression.PropertyOrField(Expression.Constant(context), transName + "s");
// Build translation subqueries
//e.g. context.OrgGroupTranslations.Where(tr => tr.LanguageID == languageID && tr.RegionID == regionID);
MethodCallExpression fullTranWhereLambda = Expression.Call(typeof(Queryable),
"Where",
new Type[] { transType },
new Expression[]
{
transTable,
Expression.Quote
(
Expression.Lambda
(
Expression.AndAlso
(
Expression.Equal(transLanProp, Expression.Constant(languageID)),
Expression.Equal(transRegProp, Expression.Convert(Expression.Constant(languageID), transRegProp.Type))
), transParam
)
)
});
MethodCallExpression lanTranWhereLambda = Expression.Call(typeof(Queryable),
"Where",
new Type[] { transType },
new Expression[]
{
transTable,
Expression.Quote
(
Expression.Lambda
(
Expression.AndAlso
(
Expression.Equal(transLanProp, Expression.Constant(languageID)),
Expression.IsFalse(MemberExpression.Property(transRegProp, "HasValue"))
), transParam
)
)
});
MethodCallExpression defaultTranWhereLambda = Expression.Call(typeof(Queryable),
"Where",
new Type[] { transType },
new Expression[]
{
transTable,
Expression.Quote
(
Expression.Lambda
(
Expression.AndAlso
(
Expression.Equal(transLanProp, Expression.Constant(1)),
Expression.IsFalse(MemberExpression.Property(transRegProp, "HasValue"))
), transParam
)
)
});
MethodCallExpression entityWhereLambda = Expression.Call(typeof(Queryable),
"Where",
new Type[] { entityType },
new Expression[]
{
entityTable,
Expression.Quote(
Expression.Lambda
(
Expression.Equal(CompanyIDProp, Expression.Convert(Expression.Constant(companyID), CompanyIDProp.Type))
, entityParam
)
)
});
// Create the "left join" call:
// tr.DefaultIfEmpty().FirstOrDefault()
var joinType = typeof(TransJoin<TEntity, TTrans>);
var joinParam = Expression.Parameter(joinType, "tr");
var leftJoinMethods =
Expression.Call(
typeof(Enumerable),
"FirstOrDefault",
new Type[] { transType },
Expression.Call(
typeof(Enumerable),
"DefaultIfEmpty",
new Type[] { transType },
Expression.Parameter(typeof(IEnumerable<TTrans>), "tr"))
);
// Create the return bindings
var emptyTrans = Expression.Constant(null, typeof(TTrans));
//var emptyTrans = Expression.Constant(null);
var fullBindings = new List<MemberBinding>();
fullBindings.Add(Expression.Bind(joinType.GetProperty("Entity"), entityParam));
fullBindings.Add(Expression.Bind(joinType.GetProperty("TransFull"), leftJoinMethods));
fullBindings.Add(Expression.Bind(joinType.GetProperty("TransLang"), emptyTrans));
fullBindings.Add(Expression.Bind(joinType.GetProperty("TransDefault"), emptyTrans));
// Create an object initialiser which also sets the properties
Expression fullInitialiser = Expression.MemberInit(Expression.New(joinType), fullBindings);
// Create the lambda expression, which represents the complete delegate
Expression<Func<TEntity, TTrans, TransJoin<TEntity, TTrans>>> fullResultSelector =
Expression.Lambda <Func<TEntity, TTrans, TransJoin<TEntity, TTrans>>>(fullInitialiser, combinedParam);
// Create first group join
var fullJoin = Expression.Call(
typeof(Queryable),
"GroupJoin",
new Type[]
{
typeof (TEntity), // TOuter,
typeof (TTrans), // TInner,
typeof (int), // TKey,
typeof (TransJoin<TEntity, TTrans>) // TResult
},
new Expression[]
{
entityWhereLambda,
fullTranWhereLambda,
Expression.Lambda<Func<TEntity, int>>(entityIDProp, entityParam),
Expression.Lambda<Func<TTrans, int>>(transIDProp, transParam),
fullResultSelector
}
);
Il problema è che groupjoin si aspetta di restituire un IEnumerable di TTrans, che non riesco a legare, e non posso cambiarlo in un join standard perché non potrò usare la coalizione nel proiezione come nessun risultato sarà restituito.
Sono sicuro che sto facendo qualcosa di molto stupido, quindi qualcuno può aiutarmi a far lavorare il mio gruppo per favore?
Risposta n. 2 ... questa volta con una risposta più reale: P
Il problema sembra essere che i tipi di lambda e così via che stai passando al metodo GroupJoin
sono sbagliati.
In particolare:
// Create the lambda expression, which represents the complete delegate
Expression<Func<TEntity, TTrans, TransJoin<TEntity, TTrans>>> fullResultSelector =
Expression.Lambda<Func<TEntity, TTrans, TransJoin<TEntity, TTrans>>>(fullInitialiser, combinedParam);
... anche se alcuni degli altri sembrano anche un po 'incerti, ma potrebbe essere solo io.
L'espressione del selettore prevista da GroupJoin
è di tipo Expression<Func<TEntity, IEnumerable<TTrans>, TransJoin<TEntity, TTrans>>>
. Passerà in un singolo TEntity
e in un gruppo di TTrans
(come IEnumerable<TTrans>
) anche se c'è una sola istanza in quel gruppo. L'albero delle espressioni deve gestire correttamente IEnumerable<TTrans>
, che attualmente non ha.
Sei sicuro di volere un GroupJoin
e non un Join
qui?
Ho scritto del codice in LINQPad per testare il concetto. È finito su > PasteBin < se vuoi esaminarlo.
Per inciso, l'uso dell'estensione di Dump
di LINQPad su un'espressione ti darà una completa spiegazione di come viene costruita l'espressione. Assegnare un lambda a una variabile di un tipo di Expression<Func<....>>
appropriato Expression<Func<....>>
e quindi chiamare Dump
per vedere come è costruito. Aiuta a scoprire gli usi e mostra ciò che devi fare per costruirlo.