NHibernate QueryOver Coalesce une propriété à une autre propriété

Considérez ce domaine idiot:

namespace TryHibernate.Example { public class Employee { public int Id { get; set; } public ssortingng Name { get; set; } } public class WorkItem { public int Id { get; set; } public ssortingng Description { get; set; } public DateTime StartDate { get; set; } public DateTime EndDate { get; set; } } public class Task { public int Id { get; set; } public Employee Assignee { get; set; } public WorkItem WorkItem { get; set; } public ssortingng Details { get; set; } public DateTime? StartDateOverride { get; set; } public DateTime? EndDateOverride { get; set; } } } 

L’idée est que chaque élément de travail peut être affecté à plusieurs employés avec des détails différents, annulant éventuellement les dates de début / fin de l’élément de travail lui-même. Si ces remplacements sont nuls, ils doivent plutôt être extraits de l’élément de travail.

Maintenant, j’aimerais effectuer une requête avec des ressortingctions sur les dates d’effet. J’ai d’abord essayé ceci:

 IList tasks = db.QueryOver(() => taskAlias) .JoinAlias(() => taskAlias.WorkItem, () => wiAlias) .Where(() => taskAlias.StartDateOverride.Coalesce(() => wiAlias.StartDate)  taskAlias.EndDateOverride.Coalesce(() => wiAlias.EndDate) >= start) .List(); 

Malheureusement, il ne comstack pas car Coalesce attend une expression constante et non une propriété.

OK, j’ai essayé ceci:

  .Where(() => (taskAlias.StartDateOverride == null ? wiAlias.StartDate : taskAlias.StartDateOverride)  (taskAlias.EndDateOverride == null ? wiAlias.EndDate : taskAlias.EndDateOverride) >= start) 

Cela lève NullReferenceException. Vous ne savez pas pourquoi, mais probablement parce que NHibernate ne traduit pas correctement cet opérateur ternaire (et tente de l’invoquer à la place) ou parce que == null n’est pas exactement le bon moyen de vérifier les valeurs nulles. Quoi qu’il en soit, je ne m’attendais même pas à ce que cela fonctionne.

Enfin, celui-ci fonctionne:

 IList tasks = db.QueryOver(() => taskAlias) .JoinAlias(() => taskAlias.WorkItem, () => wiAlias) .Where(Ressortingctions.LeProperty( Projections.SqlFunction("COALESCE", NHibernateUtil.DateTime, Projections.Property(() => taskAlias.StartDateOverride), Projections.Property(() => wiAlias.StartDate)), Projections.Constant(end))) .And(Ressortingctions.GeProperty( Projections.SqlFunction("COALESCE", NHibernateUtil.DateTime, Projections.Property(() => taskAlias.EndDateOverride), Projections.Property(() => wiAlias.EndDate)), Projections.Constant(start))) .List(); 

Mais je ne peux pas appeler ce code propre. Je pourrais peut-être extraire certaines expressions dans des méthodes distinctes pour le nettoyer un peu, mais il serait bien préférable d’utiliser la syntaxe d’expression plutôt que ces projections laides. Y a-t-il un moyen de le faire? Existe-t-il une raison pour laquelle NHibernate ne prend pas en charge les expressions de propriété dans l’extension Coalesce ?

Une alternative évidente consiste à tout sélectionner, puis à filtrer les résultats en utilisant Linq ou autre. Mais cela pourrait devenir un problème de performance avec un grand nombre de lignes.

Voici le code complet au cas où quelqu’un voudrait l’essayer:

 using (ISessionFactory sessionFactory = Fluently.Configure() .Database(SQLiteConfiguration.Standard.UsingFile("temp.sqlite").ShowSql()) .Mappings(m => m.AutoMappings.Add( AutoMap.AssemblyOf(new ExampleConfig()) .Conventions.Add(DefaultLazy.Never()) .Conventions.Add(DefaultCascade.All()))) .ExposeConfiguration(c => new SchemaExport(c).Create(true, true)) .BuildSessionFactory()) { using (ISession db = sessionFactory.OpenSession()) { Employee empl = new Employee() { Name = "Joe" }; WorkItem wi = new WorkItem() { Description = "Important work", StartDate = new DateTime(2016, 01, 01), EndDate = new DateTime(2017, 01, 01) }; Task task1 = new Task() { Assignee = empl, WorkItem = wi, Details = "Do this", }; db.Save(task1); Task task2 = new Task() { Assignee = empl, WorkItem = wi, Details = "Do that", StartDateOverride = new DateTime(2016, 7, 1), EndDateOverride = new DateTime(2017, 1, 1), }; db.Save(task2); Task taskAlias = null; WorkItem wiAlias = null; DateTime start = new DateTime(2016, 1, 1); DateTime end = new DateTime(2016, 6, 30); IList tasks = db.QueryOver(() => taskAlias) .JoinAlias(() => taskAlias.WorkItem, () => wiAlias) // This doesn't comstack: //.Where(() => taskAlias.StartDateOverride.Coalesce(() => wiAlias.StartDate)  taskAlias.EndDateOverride.Coalesce(() => wiAlias.EndDate) >= start) // This throws NullReferenceException: //.Where(() => (taskAlias.StartDateOverride == null ? wiAlias.StartDate : taskAlias.StartDateOverride)  (taskAlias.EndDateOverride == null ? wiAlias.EndDate : taskAlias.EndDateOverride) >= start) // This works: .Where(Ressortingctions.LeProperty( Projections.SqlFunction("COALESCE", NHibernateUtil.DateTime, Projections.Property(() => taskAlias.StartDateOverride), Projections.Property(() => wiAlias.StartDate)), Projections.Constant(end))) .And(Ressortingctions.GeProperty( Projections.SqlFunction("COALESCE", NHibernateUtil.DateTime, Projections.Property(() => taskAlias.EndDateOverride), Projections.Property(() => wiAlias.EndDate)), Projections.Constant(start))) .List(); foreach (Task t in tasks) Console.WriteLine("Found task: {0}", t.Details); } } 

Et la configuration est vraiment simple:

 class ExampleConfig : DefaultAutomappingConfiguration { public override bool ShouldMap(Type type) { return type.Namespace == "TryHibernate.Example"; } } 

Commençons par ceci:

 // This doesn't comstack: //.Where(() => taskAlias.StartDateOverride.Coalesce(() => wiAlias.StartDate) <= end) //.And(() => taskAlias.EndDateOverride.Coalesce(() => wiAlias.EndDate) >= start) 

et le modifier pour:

 .Where(() => taskAlias.StartDateOverride.Coalesce(wiAlias.StartDate) <= end) .And(() => taskAlias.EndDateOverride.Coalesce(wiAlias.EndDate) >= start) 

maintenant il va comstackr. Mais au moment de l’exécution, il génère la même NullReferenceException . Pas bien.

Il s’avère que NHibernate tente en effet d’évaluer l’argument de Coalesce . Cela se voit facilement en regardant l’implémentation de la classe ProjectionExtensions . La méthode suivante gère la traduction Coalesce :

 internal static IProjection ProcessCoalesce(MethodCallExpression methodCallExpression) { IProjection projection = ExpressionProcessor.FindMemberProjection(methodCallExpression.Arguments[0]).AsProjection(); object obj = ExpressionProcessor.FindValue(methodCallExpression.Arguments[1]); return Projections.SqlFunction("coalesce", (IType) NHibernateUtil.Object, projection, Projections.Constant(obj)); } 

Notez le traitement différent du premier argument ( FindMemberExpresion ) vs second argument ( FindValue ). Eh bien, FindValue essaie simplement d’évaluer l’expression.

Maintenant, nous soaps ce qui cause le problème. Je ne sais pas pourquoi cela est mis en œuvre de cette façon, je vais donc me concentrer sur la recherche d’une solution.

Heureusement, la classe ExpressionProcessor est publique et vous permet également d’enregistrer des méthodes personnalisées via les méthodes RegisterCustomMethodCall / RegisterCustomProjection . Ce qui nous amène à la solution:

  • Créez des méthodes d’extensions personnalisées similaires à Coalesce (appelez-les IfNull par exemple)
  • Enregistrer un processeur personnalisé
  • Utilisez-les à la place de Coalesce

Voici l’implémentation:

 public static class CustomProjections { static CustomProjections() { ExpressionProcessor.RegisterCustomProjection(() => IfNull(null, ""), ProcessIfNull); ExpressionProcessor.RegisterCustomProjection(() => IfNull(null, 0), ProcessIfNull); } public static void Register() { } public static T IfNull(this T objectProperty, T replaceValueIfIsNull) { throw new Exception("Not to be used directly - use inside QueryOver expression"); } public static T? IfNull(this T? objectProperty, T replaceValueIfIsNull) where T : struct { throw new Exception("Not to be used directly - use inside QueryOver expression"); } private static IProjection ProcessIfNull(MethodCallExpression mce) { var arg0 = ExpressionProcessor.FindMemberProjection(mce.Arguments[0]).AsProjection(); var arg1 = ExpressionProcessor.FindMemberProjection(mce.Arguments[1]).AsProjection(); return Projections.SqlFunction("coalesce", NHibernateUtil.Object, arg0, arg1); } } 

Étant donné que ces méthodes ne sont jamais appelées, vous devez vous assurer que le processeur personnalisé est enregistré en appelant la méthode Register . C’est une méthode vide juste pour s’assurer que le constructeur statique de la classe est appelé, où l’enregistrement réel a lieu.

Donc, dans votre exemple, incluez au début:

CustomProjections.Register();

puis utilisez dans la requête:

 .Where(() => taskAlias.StartDateOverride.IfNull(wiAlias.StartDate) <= end) .And(() => taskAlias.EndDateOverride.IfNull(wiAlias.EndDate) >= start) 

et cela fonctionnera comme prévu.

PS L’implémentation ci-dessus fonctionne pour les arguments constant et expression, c’est donc un remplacement sûr de Coalesce .