3

Does NHibernate 3 or 4 have an equivalent to Entity Framework's "Include" method that takes a string argument as opposed to a llambda? I'd like to do something like this in NHibernate:

Contact contact =
        context.Contacts.Include("SalesOrderHeaders.SalesOrderDetails")
        .FirstOrDefault();

I've come across this code from this post which uses "Fetch" in a loop which is cool, but this only handles objects that are first-level children of the main object whereas the above EF code goes down 2 levels without needing strongly-typed llambdas.

public IQueryable<T> All<T>(params Expression<Func<T, Object>> [] fetchPaths)
{
    var queryable = this.session.Query<T>();

    foreach (var fetchPath in fetchPaths)
    {
        queryable = queryable.Fetch(fetchPath);
    }

    return queryable;
}
Community
  • 1
  • 1
Andy
  • 2,709
  • 5
  • 37
  • 64
  • Is HQL required here? Has anyone written a neat method that generates proper HQL to handle a scenario like .Include("SalesOrderHeaders.SalesOrderDetails")? – Andy May 19 '16 at 14:04

2 Answers2

5

NHibernate has a second method called ThenFetch. You will have to write

this.session.Query<T>()
            .Fetch(x => x.Property)
            .ThenFetch(x => x.SubProperty);
Roland Buergi
  • 1,157
  • 9
  • 23
  • That will work for specific queries, but I'm looking for a sort of base implementation similar to the "All" method in my post that will handle any sort of query. The string argument to "Include" is really nice because then you don't have to worry about the strong-typed nature of the Fetch/ThenFetch llambda syntax. – Andy May 19 '16 at 14:39
  • I don't think you will find an actual alternative. I'd write a parser to build these expression trees automatically. However, I agree with you that it is very cumbersome... – Roland Buergi May 19 '16 at 16:33
0

It seems that there a couple options to solve my problem.

Option 1: Dynamically build HQL in the form of:

from Contacts t0
left join fetch t0.SalesOrderHeaders t1
left join fetch t1.SalesOrderDetails t2

Option 2: Use NHibernate's ICriteria.SetFetchMode. This can handle string-based joins which is very helpful, and is easier than the HQL approach for my purposes. Here's the code I ended up writing (adapted from this post). I hope it's helpful to someone else. The "GetPathsToUse" method is my attempt at having Entity Framework-style include paths. So if I wanted to supply "SalesOrderHeaders.SalesOrderDetails" as the only path, that method would add "SalesOrderHeaders" first before "SalesOrderHeaders.SalesOrderDetails" so that NH would be happy.

// This also uses ICriteria, but adds some logic to auto-handle paths to reduce the number of paths needed to pass in to match how 
        // EntityFramework works e.g. if one path is passed in for OrderInputs.Input, this method will call SetFetchMode twice, once for OrderInputs and again for OrderInputs.Input
        public T GetByIdSetFetchModeEFStyle(Guid id, ISession session, params string[] includePaths)
        {   
            ICriteria criteria = session.CreateCriteria<T>();

            if (includePaths != null && includePaths.Length > 0)
            {
                // NHibernate requires paths to be supplied in order.  So if a path "OrderInputs.Input" is supplied, we must make 
                // 2 separate calls to SetFetchMode, the first with "OrderInputs", and the second with "OrderInputs.Input".
                // EntityFramework handles this for us, but NHibernate requires things to be different.
                List<string> pathsToUse = this.GetPathsToUse(includePaths);

                foreach (string path in pathsToUse)
                {
                    criteria.SetFetchMode(path, FetchMode.Eager);
                }
            }

            return criteria
                // prevent duplicates in the results
                .SetResultTransformer(Transformers.DistinctRootEntity)
                .Add(Restrictions.Eq(typeof(T).Name + "Id", id)).UniqueResult<T>();
        }

        private List<string> GetPathsToUse(string[] includePaths)
        {
            var nhPaths = new List<string>();
            foreach (string path in includePaths)
            {
                if (!path.Contains(".") && !nhPaths.Contains(path))
                {
                    // There is no dot in the path - just add it if it hasn't been added already
                    nhPaths.Add(path);
                }
                else
                {
                    // We have a dot e.g. OrderInputs.Input.  We need to add "OrderInputs" before "OrderInputs.Input"
                    string[] pathParts = path.Split(".".ToCharArray());

                    // Add any missing ancestors of the current path prior to adding the path
                    for (int p = 1; p <= pathParts.Length; p++)
                    {
                        string requiredPath = string.Join(".", pathParts.Take(p).ToArray());
                        if (!nhPaths.Contains(requiredPath))
                        {
                            nhPaths.Add(requiredPath);
                        }
                    }
                }
            }
            return nhPaths;
        }
Community
  • 1
  • 1
Andy
  • 2,709
  • 5
  • 37
  • 64