Search code examples
c#nhibernateouter-joinqueryovernhibernate-projections

Join collection in QueryOver with projections


I want to retrieve the list of short User versions with their Characters collection (short version too). I use QueryOver with projections.

public class User
{
        public virtual int Id { get; set; }
        public virtual string Nickname { get; set; }
        public virtual ISet<Character> Characters { get; set; }
        // and 30 other properties
}

public class Character
{
        public virtual int Id { get; set; }
        public virtual int XP { get; set; }
        public virtual int UserId { get; set; }
        public virtual int ClassId { get; set; }
        // and 30 other properties
}

public class UserDto // short version
{
        public virtual string Nickname { get; set; }
        public virtual ISet<CharacterDto> Characters { get; set; }
}


public class CharacterDto // short version
{
        public virtual int XP { get; set; }
        public virtual int ClassId { get; set; }
}

    User userAlias = null;
    UserDto userDto = null;
    Character characterAlias = null;
    CharacterDto characterDto = null;

    var result = session.QueryOver<User>(() => userAlias)
            .Where(Restrictions.Like(
                Projections.SqlFunction("lower", NHibernateUtil.String, Projections.Property<User>(x => x.Nickname)),
                nickname.ToLowerInvariant(), MatchMode.Start))
            .JoinAlias(x => x.Characters, () => characterAlias, JoinType.LeftOuterJoin)
             .Select(
                 Projections.Property(() => userAlias.Nickname).WithAlias(() => userDto.Nickname),
                 Projections.Property(() => userAlias.Characters).WithAlias(() => userDto.Characters),
                 Projections.Property(() => characterAlias.XP).WithAlias(() => characterDto.XP),
                 Projections.Property(() => characterAlias.ClassId).WithAlias(() => characterDto.ClassId)
                )
            .TransformUsing(Transformers.AliasToBean<UserDto>())
             .Take(50)
             .List<UserDto>();

When I run this code it throws an exception: Could not find a setter for property ClassId in class UserDto. If I remove the transformer then the result contains entries for one character per each user (User1-Character1, User1-Character2, ...). Each entry has correct nickname and character class id but wrong xp (user id instead) and characters collection (== null).

How can I get the correct data without using hql or doing seperate queries for characters?

update

I removed the transformer and commented out Projections.Property(() => userAlias.Characters)..., now it returns the correct results but is there a way to transform them into projected dtos like AliasToBeanTransformer does and collapse the repeated user data for each character entries?


Solution

  • You are trying to project to two different object types (UserDto and CharacterDto). I don't think it's supported. If you look at your code, you tell NHibernate that he should use UseDto (Transformers.AliasToBean<UserDto>()) but you don't tell it about CharacterDto.

    See for example http://blog.andrewawhitaker.com/blog/2014/06/19/queryover-series-part-4-transforming/ :

    You cannot populate collections (e.g., if you had a class with ProductID and a collection of ProductReviews you could not do that in one step using AliasToBean)

    What you can do:

    Add a class with everything (UserCharacterDto)

    public class UserCharacterDto // short version
    {
        public virtual int Id { get; set; }
        public virtual string Nickname { get; set; }
        public virtual int? XP { get; set; }
        public virtual int? ClassId { get; set; }
    }
    
    public class UserDto // short version
    {
        public virtual string Nickname { get; set; }
        // Changed from ISet: you don't have a key!
        public virtual IList<CharacterDto> Characters { get; set; }
    }
    
    public class CharacterDto // short version
    {
        public virtual int XP { get; set; }
        public virtual int ClassId { get; set; }
    }
    

    and first you project to UserCharacterDto,

    UserCharacterDto uc = null;
    
    IList<UserCharacterDto> result = ...
    
        .Select(
            Projections.Property(() => userAlias.Nickname).WithAlias(() => uc.Nickname),
            Projections.Property(() => characterAlias.XP).WithAlias(() => uc.XP),
            Projections.Property(() => characterAlias.ClassId).WithAlias(() => uc.ClassId)
        )
        .TransformUsing(Transformers.AliasToBean<UserDto>())
        .Take(50)
        .List<UserCharacterDto>()
    

    then you GroupBy and in the end you create your elements

    IList<UserDto> result2 = (from x in result
                              // Here you group by User
                              group x by x.Id into y
                              let first = y.First()
                              // Here you build the "definitive" UserDto
                              select new UserDto {
                                  Nickname = first.Nickname,
                                  // and here you build the "definitive" CharacterDto
                                  // note the handling of empty objects!
                                  Characters = new List<CharacterDto>(y.Select(z => z.XP != null && z.ClassId != null ? new CharacterDto { XP = z.XP.Value, ClassId = z.ClassId.Value } : null))
                              }
                             ).ToList();
    
    // remove empty CharacterDto from left join
    foreach (UserDto user in result2) {
        if (user.Characters.Count == 1 && user.Characters[0] == null) {
            user.Characters.Clear();
        }
    }
    

    (note that I'm not using the functional syntax of LINQ because I want to use the let keyword)