Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 386
Sandor Drieënhuizen Avatar asked Aug 08 '11 09:08

Sandor Drieënhuizen


2 Answers

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?

like image 137
Jamie Ide Avatar answered Sep 22 '22 07:09

Jamie Ide


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.

like image 42
NYCChris Avatar answered Sep 19 '22 07:09

NYCChris