How to incorporate property value conversion into NHibernate QueryOver .SelectList?
Asked Answered
U

2

8

I'm looking to incorporate property value translations into my QueryOver queries.

I like writing queries following the Query Object Pattern, directly producing MVC view models. In my view models, I try to use property types that are as simple as possible, keeping conversion complexity out of the views and controllers. This means that sometimes, I'll need to convert one type into another, such as dates into strings.

One could argue that such conversions should be performed in views but since most of my view models are directly translated to JSON objects, that would cause the conversion to become much more cumbersome. Performing date to string conversion in JavaScript is problematic at best and my JSON convertor is not flexible enough.

Here's an example of what I'm doing:

// Entity.
public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTimeOffset DateCreated { get; set; }
}

// View model.
public class CustomerViewModel
{
    public string Name { get; set; }
    public string DateCreated { get; set; } // Note the string type here.
}

// Query.
CustomerViewModel model = null;

List<CustomerViewModel> result = Session.QueryOver<Customer>()
    .SelectList(list => list
        .Select(n => n.Name).WithAlias(() => model.Name)
        .Select(n => n.DateCreated).WithAlias(() => model.DateCreated))
    .TransformUsing(Transformers.AliasToBean<CustomerViewModel>());
    .Future<CustomerViewModel>()
    .ToList();

When running the query code, the following exception is thrown:

Object of type 'System.DateTimeOffset' cannot be converted to type 'System.String'.

Obviously, this is because of the following line:

.Select(n => n.DateCreated).WithAlias(() => model.DateCreated))

So the question is: how to incorporate the date to string conversion into the query?

I don't want to perform the conversion after the query has executed because I would need an additional intermediate class to store the results before converting them.

Unchancy answered 8/8, 2011 at 9:55 Comment(0)
K
12
List<CustomerViewModel> result = Session.QueryOver<Customer>(() => customerAlias)
    .SelectList(list => list
        .Select(n => customerAlias.Name).WithAlias(() => model.Name)
        // I'm not sure if customerAlias works here or why you have declared it at all
        .Select(Projections.Cast(NHibernateUtil.String, Projections.Property<Customer>(c => c.DateCreated))).WithAlias(() => model.DateCreated))
    .TransformUsing(Transformers.AliasToBean<CustomerViewModel>());
    .Future<CustomerViewModel>()
    .ToList();

Should work but unfortunately does not give you any control over the format of the string. I've handled a similar problem by defining a private property on the model that holds the data as the correct type and a string property to return the formatted value, i.e.:

public class CustomerViewModel
{
    public string Name { get; set; }
    private DateTime DateCreatedImpl { get; set; }
    public string DateCreated { get { return DateCreatedImpl.ToString(); }}
}

This has several advantages but might not work well with your JSON converter. Does your converter have a setting or attribute that would allow it to ignore private properties?

Kronos answered 8/8, 2011 at 18:57 Comment(5)
Thanks. As for the alias, it is superfluous indeed so I removed it from the question. The string conversion in your first example indeed leaves no control over the string format, while I really need that. I'm going to investigate your second suggestion, it might help me out though I still prefer doing the conversion in the query object instead of the view model class.Sterling
Good luck, I tried a bunch of things but this is the best I came up with. Other possibilities are creating a custom transformer or using Automapper.Kronos
Terribly ugly, but AFAIK still the only solution that works. Strange that QueryOver has so much trouble with ToString(), considering that it translates to exactly the same thing...Wivinia
I know it's the way the OP posted, but just to be sure: adding ToList() in the end doesn't make Future<CustomerViewModel>() useless?Kettering
@kopranb You're probably right, ToList negates the benefit of using Future.Kronos
S
4

I came across the same problem today, and saw this posting. I went ahead and created my own transformer that can be given Converter functions to handle type conversions per property.

Here is the Transformer class.

public class AliasToDTOTransformer<D> : IResultTransformer where D: class, new()
{
    //Keep a dictionary of converts from Source -> Dest types...
    private readonly IDictionary<Tuple<Type, Type>, Func<object, object>> _converters;

    public AliasToDTOTransformer()
    {
        _converters = _converters = new Dictionary<Tuple<Type, Type>, Func<object, object>>();
    }

    public void AddConverter<S,R>(Func<S,R> converter)
    {
         _converters[new Tuple<Type, Type>(typeof (S), typeof (R))] = s => (object) converter((S) s);
    }
    public object TransformTuple(object[] tuple, string[] aliases)
    {
        var dto = new D();
        for (var i = 0; i < aliases.Length; i++)
        {
            var propinfo = dto.GetType().GetProperty(aliases[i]);
            if (propinfo == null) continue;
            var valueToSet = ConvertValue(propinfo.PropertyType, tuple[i]);
            propinfo.SetValue(dto, valueToSet, null);
        }
        return dto;
    }
    private object ConvertValue(Type destinationType, object sourceValue)
    {
        //Approximate default(T) here
        if (sourceValue == null)
            return destinationType.IsValueType ? Activator.CreateInstance(destinationType) : null;

        var sourceType = sourceValue.GetType();
        var tuple = new Tuple<Type, Type>(sourceType, destinationType);
        if (_converters.ContainsKey(tuple))
        {
            var func = _converters[tuple];
            return Convert.ChangeType(func.Invoke(sourceValue), destinationType);
        }

        if (destinationType.IsAssignableFrom(sourceType))
            return sourceValue;

        return Convert.ToString(sourceValue); // I dunno... maybe throw an exception here instead?
    }

    public IList TransformList(IList collection)
    {
        return collection;
    }

And here is how I use it, first my DTO:

public class EventDetailDTO : DescriptionDTO
{
    public string Code { get; set; }
    public string Start { get; set; }
    public string End { get; set; }
    public int Status { get; set; }

    public string Comment { get; set; }
    public int Client { get; set; }
    public int BreakMinutes { get; set; }
    public int CanBeViewedBy { get; set; } 
}

Later on when I call my query, it returns Start and End as DateTime values. So this is how I actually use the converter.

var transformer = new AliasToDTOTransformer<EventDetailDTO>();
transformer.AddConverter((DateTime d) => d.ToString("g"));

Hope this helps.

Scraggly answered 2/12, 2011 at 18:18 Comment(2)
That looks like a very elegant solution! I'll probably give it a try this sunday.Sterling
Glad I could help. I probably should have tested this better before posting. The original version was far to aggressive in locating a converter func. Now I key the converters in the dictionary as Tuple<SourceType,ReturnType>Scraggly

© 2022 - 2024 — McMap. All rights reserved.