How to add the project() (mapper) option to this framework to extend the repositories queries

May 16, 2014 at 5:58 PM
Edited May 16, 2014 at 5:59 PM
Hi. I kind of new in this kind of development (structure, patterns...)

I would like to be able to compose a the queries with a mapper option:
basically i want to be able to convert the return of GetAllFilesInMain
(File) to AllFilesInMainVm (FileVm)

public static class FileRepository
{
    public static IEnumerable<File> GetAllFilesInMain(this IRepository<File> repository)
    {
        var res = repository.Query()
            .Include(d=>d.FileDetail)
            .Include(p=>p.Country)
            .Include(u=> u.Usuario).Select();            
        return res;
    }
public static IEnumerable<File> GetAllFilesInMain(this IRepository<File> repository)
    {
        var res = repository.Queryable().Project().To<FileVm>();  // (plus the includes)         
        return res;
    }
}
when i check the query, the different is enourmous, because FileVm only return a few fields.

And I want to be able to compose or add this Procject().To<FileVm> on the Service Layer, so the repository doesnt have to make the convert.

I Hope this can be done (if you understand the question, my english isnt very good)
Thanks in advance for your help.
Coordinator
May 22, 2014 at 3:03 PM
Edited May 22, 2014 at 3:04 PM
Teams are using AutoMapper for this.

https://github.com/AutoMapper/AutoMapper
Marked as answer by lelong37 on 5/22/2014 at 8:03 AM
May 27, 2014 at 2:18 PM
Hi, Thanks for your time, ill give it a try. Many Many Thanks.
May 27, 2014 at 4:34 PM
I Initially started using AutoMapper to "autogen" my DTO's or ViewModels, however, something you should be aware of in using AutoMapper for DAL ccode - http://www.devtrends.co.uk/blog/stop-using-automapper-in-your-data-access-code.

I have since switched to the tried and tested way of just doing it yourself with projection. Essentially, create DTO's/ViewModel classes for each of your exposed entities, using projection:
            var results = odataQueryOptions.ApplyTo(_repository
                .ODataQueryable()
                .Where(u => u.UserId == userId)
                .OrderBy(o => o.WebsiteName)).Cast<Website>()
                .Select(s => new ProjectEditorDTO()
                {
                    // projection
                    WebsiteGUID = s.WebsiteGuid,
                    WebsiteName = s.WebsiteName,
                    WebsiteNotes = s.WebsiteNotes,
                    DefaultContentType = new DefaulContentType
                        {
                            ContentTypeId = s.ContentType.ContentTypeId,
                            Description = s.ContentType.Description
                        }
                });
This code resides in an OData ProjectEditor class, and configured in WebApiConfig as follows:
             modelBuilder.EntitySet<ProjectEditorDTO>("ProjectEditor"); 
Hope that is of some assistance.
Marked as answer by yzjuandedios on 6/1/2014 at 9:55 AM
Jun 1, 2014 at 5:24 PM
Hi lelong37 and elhaix, thanks a lot for your responses.

elhaix, I tried what you suggest and it feels so much better than using automapper, at first got version 1, but the author add a longCammelCase support and someone else suggest something to enhance it a little more, I made a small change myself and this is what I have: (hope someone is able to use it and extend it or improve it.
Many many thanks to both of you.

as you can see, I added the QueryableExtensions to the Repository.Ef and the MappingPathAttribute to Core (as lelong37 suggested to not have Repository and Repository.Ef tightly couple with the POCOs)

namespace Core
{
public class MappingPathAttribute : Attribute
{
    public string Path = string.Empty; 
}
}

namespace Repository.Ef
{
/// <summary>
/// Provides projection mapping from an IQueryable source to a target type.
/// 
/// This allows from strongly-typed mapping and querying only necessary fields from the database.
/// It takes the place of Domain -> ViewModel mapping as it allows the ViewModel to stay as
/// IQueryable.  AutoMapper works on in-memory objects and will pull all full records to perform
/// a collection mapping.  Use AutoMapper for Input -> Domain scenarios, but not DAL.
/// 
/// Reference: http://devtrends.co.uk/blog/stop-using-automapper-in-your-data-access-code
/// </summary>
public static class QueryableExtensions
{
    public static ProjectionExpression<TSource> Project<TSource>(this IQueryable<TSource> source)
    {
        return new ProjectionExpression<TSource>(source);
    }
    public static string GetSql<TSource>(this IQueryable<TSource> source)
    {
        return source.ToString();
    }
}

public class ProjectionExpression<TSource>
{
    private static readonly Dictionary<string, Expression> ExpressionCache = new Dictionary<string, Expression>();
    private readonly IQueryable<TSource> _source;

    public ProjectionExpression(IQueryable<TSource> source)
    {
        _source = source;
    }

    /// <summary>
    /// Specifies the target type
    /// </summary>
    /// <typeparam name="TDest"></typeparam>
    /// <returns></returns>
    public IQueryable<TDest> To<TDest>()
    {
        var queryExpression = GetCachedExpression<TDest>() ?? BuildExpression<TDest>();
        return _source.Select(queryExpression);
    }        

    private static Expression<Func<TSource, TDest>> GetCachedExpression<TDest>()
    {
        var key = GetCacheKey<TDest>();
        return ExpressionCache.ContainsKey(key) ? ExpressionCache[key] as Expression<Func<TSource, TDest>> : null;
    }

    private static Expression<Func<TSource, TDest>> BuildExpression<TDest>()
    {
        var sourceProperties = typeof(TSource).GetProperties();
        var destinationProperties = typeof(TDest).GetProperties().Where(dest => dest.CanWrite);
        var parameterExpression = Expression.Parameter(typeof(TSource), "src");

        var bindings = destinationProperties
                            .Select(destinationProperty => BuildBinding(parameterExpression, destinationProperty, sourceProperties))
                            .Where(binding => binding != null);

        var expression = Expression.Lambda<Func<TSource, TDest>>(Expression.MemberInit(Expression.New(typeof(TDest)), bindings), parameterExpression);

        var key = GetCacheKey<TDest>();

        ExpressionCache.Add(key, expression);

        return expression;
    }
    private static MemberAssignment BuildBinding(Expression parameterExpression, MemberInfo destinationProperty, 
        IEnumerable<PropertyInfo> sourceProperties)
    {
        var obj = destinationProperty.GetCustomAttributes(true).FirstOrDefault(p => p.GetType().Name == "MappingPathAttribute");

        string[] sections;
        if (obj != null)
        {
            var path = ((MappingPathAttribute)obj).Path.Split('.');
            sections = path.Length == 1 ? new[] { path[0], destinationProperty.Name } : path;
        }
        else
        {
            sections = SplitCamelCase(destinationProperty.Name);
        }
        return ResolveProperty(
            parameterExpression,
            destinationProperty,
            sections[0],
            1,
            sourceProperties,
            sections.ToArray());
    }
    /// <summary>
    /// <para>
    /// Attempt to find the first property that matches with a depth-first recursive search.
    /// This will yield a path along the shortest property names.
    /// </para>
    /// <para>
    /// If sections = {"Bar", "Two"} it will match Foo.Bar, and then the child 
    /// Bar.Two.  Failing that, it will  continue to Foo.BarTwo
    /// </para>
    /// </summary>
    /// <param name="parameterExpression"></param>
    /// <param name="destinationProperty"></param>
    /// <param name="currentName">The current property name being looked for</param>
    /// <param name="currentIndex">The current index in the sections collection</param>
    /// <param name="properties">Collection of properties on the source object</param>
    /// <param name="sections">collection of name sections, split by camel case</param>
    /// <returns></returns>
    private static MemberAssignment ResolveProperty(
        Expression parameterExpression,
        MemberInfo destinationProperty,
        string currentName,
        int currentIndex,
        IEnumerable<PropertyInfo> properties,
        string[] sections)
    {
        // Check if any of the current properties match in name
        var property = properties.FirstOrDefault(src => src.Name == currentName);

        // If we're at the end of the sections, attempt to bind, or nothing found
        if (currentIndex == sections.Length)
        {
            return (property != null) ?
                Expression.Bind(destinationProperty, Expression.Property(parameterExpression, property)) :
                null;
        }

        // The property exists and there are still sections left - look into the child
        if (property != null)
        {
            // We found a property with currentName, so move to the next section
            // Examine the remaining sections on child properties
            var result = ResolveProperty(
                Expression.Property(parameterExpression, property),
                destinationProperty,
                sections[currentIndex],
                currentIndex + 1,             // proceed to the next section index
                property.PropertyType.GetProperties(),
                sections);

            // If we found a property on the child, return it.  Otherwise continue searching
            if (result != null)
                return result;
        }

        // currentName doesn't exist, so add the next section and keep searching
        return ResolveProperty(
                parameterExpression,
                destinationProperty,
                currentName + sections[currentIndex],
                currentIndex + 1,             // proceed to the next section index
                properties,
                sections);
    }
    private static string GetCacheKey<TDest>()
    {
        return string.Concat(typeof(TSource).FullName, typeof(TDest).FullName);
    }

    private static string[] SplitCamelCase(string input)
    {
        return Regex.Replace(input, "([A-Z])", " $1", RegexOptions.Compiled).Trim().Split(' ');
    }
}    
}

and I use it like this:

public class PedidoVmLoad
{
// This property belongs to the main table (Pedidos) so gets mapped automatically (the three tables have this same property)
    public int Id { get; set; } 
// This property follows the conventions (Navigation property + field Name Property : Pedidos.Almacenes.Id) Gets automatically Mapped
    public int AlmacenesId { get; set; } 
// In this property to follow the conventions I should had write this:
// PedidoDatosUnicaIdUsuarioUnica (Navigation property + field Property : Pedidos.PedidoDatosUnica.IdUsuarioUnica)
// Instead I used the attribute class to be able to work with the field Name Property in a short meaningful name.
    [MappingPath(Path = "PedidoDatosUnica")]
    public int IdUsuarioUnica { get; set; }
// This one needs to have a more meaningful name (No Navigation nor Property Naming Conventions),
//in this case I explicit write the Navigation Property and the field Name Property:
// PedidoDatosUnicaPass.SaldoActual (Pedidos.PedidoDatosUnicaPass.SaldoActual)
    [MappingPath(Path = "PedidoDatosUnicaPass.SaldoActual")]
    public decimal SaldoAntesPedido { get; set; }
}

//Linqpad
var pedidos = pediRepo.Queryable()
    .Where(w => w.Estado != EnPedidoEstado.Completo && w.Estado != EnPedidoEstado.Eliminado)
    .Include(i => i.PedidoDatosUnica).Project().To<PedidoVmLoad>().Dump();
By the way (this one doesn't compile in VS (I'm using 2013 update 2 and the latest version of the genericRepository)

I had to change it to:
//VS 2013
var pedidos = repository.Query(w => w.Estado != EnPedidoEstado.Completo && w.Estado != EnPedidoEstado.Eliminado)
            .Include(i => i.PedidoDatosUnica).Select().AsQueryable().Project().To<PedidoVmLoad>();
if someone knows why Queryable() isn't working for me in VS, I'll appreciate the help.

Ahh, and the generated Sql Query has exactly what you ask for.
once again, I cannot thank enough for this project and the help of the others.
Thanks a lot, from Honduras.
Jun 1, 2014 at 5:50 PM
Edited Jun 1, 2014 at 6:01 PM
Hi again, the problem I wrote about i forgot to write that only happens in the public static class PedidoRepository
so Please forget about it: I dindnt have Entity Framework Libraries reference to the Repository Project, so sorry to bother with this.
Sorry.

//Linqpad
var pedidos = pediRepo.Queryable()
    .Where(w => w.Estado != EnPedidoEstado.Completo && w.Estado != EnPedidoEstado.Eliminado)
    .Include(i => i.PedidoDatosUnica).Project().To<PedidoVmLoad>().Dump();
By the way (this one doesn't compile in VS (I'm using 2013 update 2 and the latest version of the genericRepository)

I had to change it to:
//VS 2013
var pedidos = repository.Query(w => w.Estado != EnPedidoEstado.Completo && w.Estado != EnPedidoEstado.Eliminado)
            .Include(i => i.PedidoDatosUnica).Select().AsQueryable().Project().To<PedidoVmLoad>();
if someone knows why Queryable() isn't working for me in VS, I'll appreciate the help.