Search code examples
c#entity-frameworkn-tier-architecturedata-transfer-objects

Passing a GetWhere query (Func<entityDTO,bool>) to a data layer method which needs a (Func<entity,bool>) parameter to work


I have the following method in a data access class which uses entity framework:

public static IEnumerable<entityType> GetWhere(Func<entityType, bool> wherePredicate)
{
    using (DataEntities db = new DataEntities())
    {
        var query = (wherePredicate != null)
            ? db.Set<entityType>().Where(wherePredicate).ToList()
            : db.Set<entityType>().ToList();                    
        return query;
    }
}

This works fine when I use the entities across all layers... however I am trying to move to using a DTO class and I would like to do something like the following:

public static IEnumerable<EntityTypeDTO> GetWhere(Func<EntityTypeDTO, bool> wherePredicate)
{
    //call a method here which will convert Func<EntityTypeDTO,bool> to 
    // Func<EntityType,bool>

    using (DataEntities db = new DataEntities())
    {
        var query = new List<EntityType>();
        if (wherePredicate == null)
        {
            query = db.Set<EntityType>().ToList();
        }
        else
        {   
            query = (wherePredicate != null)
                ? db.Set<EntityType>().Where(wherePredicate).AsQueryable<EntityType>().ToList()
                : db.Set<EntityType>().ToList();
        }
        List<EntityTypeDTO> result = new List<EntityTypeDTO>();
        foreach(EntityType item in query)
        {
            result.Add(item.ToDTO());
        }

        return result;
    }
}

Essentially I want a method which will convert Func to Func.

I think I have to break down the Func into an expression tree and then rebuild it somehow in the entityType?

I want to do this to allow the Presentation Layer to just pass the Expression queries?

Am I missing something basic or is there an easier design pattern that can pass a query from a DTO to a data access class without knowing the details of the query?

I have tried making the DTO inherit from the entity which doesn't seem to work either?

If there is a better design pattern that I am missing I would love a pointer and I can investigate from there...


Solution

  • Firstly I would suggest that you put a querying layer of your own in front of Entity Framework rather than allowing any arbitrary Func to be passed in because it will be very easy in the future to pass a Func that Entity Framework can not translate into a SQL statement (it can only translate some expressions - the basics are fine but if your expression calls a C# method, for example, then Entity Framework will probably fail).

    So your search layer could have classes that you build up as criteria (eg. a "ContainsName" search class or a "ProductHasId" class) that are then translated into expressions in your search layer. This separates your app entirely from the ORM, which means that ORM details (like the entities or like the limitations of what Funcs can and can't be translated) don't leak out. There's lots out there that's been written about this some of arrangement.

    One final note, though, if you are working close to the ORM layer, Entity Framework is very clever and you could probably get a long way without trying to translate your Func<dto, bool> to a Func<entity, bool>. For example, in the below code, accessing "context.Products" returns a "DbSet" and calling Select on it returns an IQueryable and calling Where on that also returns an IQueryable. Entity Framework will translate all of that into a single SQL statement so it won't pull all other Products into memory and then filter the ID on that memory set, it will actually perform the filtering in SQL even though the filter is operating on a projected type (which is equivalent to the DTO in your case) and not the Entity Framework entity -

    var results = context.Products
        .Select(p => new { ID = p.ProductID, Name = p.ProductName })
        .Where(p => p.ID < 10)
        .ToList();
    

    The SQL executed is:

    SELECT 
        [Extent1].[ProductID] AS [ProductID], 
        [Extent1].[ProductName] AS [ProductName]
    FROM [dbo].[Products] AS [Extent1]
    WHERE [Extent1].[ProductID] < 10
    

    So, if you changed your code to get something like..

    return context.Products
        .Map<Product, ProductDTO()>()
        .Where(productDtoWherePredicate)
        .ToList();
    

    .. then you might be just fine with the Funcs that you already have. I presume that you already have some sort of mapping functions to get from EF entities to DTOs (but if not then you might want to look into AutoMapper to help you out - which has support for "projections", which are basically IQueryable maps).