Home > Mobile >  How to retrofit Non Generic based EF Core code to use Generic IQueryable
How to retrofit Non Generic based EF Core code to use Generic IQueryable

Time:12-15

I am updating an old EF solution to EF Core and I would like to remove much of the code that is not using generics (T) but I have to do it in stages. The original code is suffering from organic growth of learning entity framework over 10 years and is a mix of "philosophies" and patterns from the code like in the tutorials from Microsoft to Repository pattern.

The ideal solution would be something that would allow me to build up the query for a particular entity then open the connection and run the "ToList()". I can then move parts of the code in stages to something cleaner.

//logic to figure out what is needed with no connection or dbconnection yet
public void GetOrder(){
     var query = new List<Order>().AsQueryable();
     query = query.Where(x => x.Id > 100);
     var orders = repo.GetItems<Order>(query);
}

public IEnumerable<T> GetItems<T>(IQueryable<T> query, int page = 0, int maxcount = 0)
        {
            using (MyEntities context = new MyEntities(getConnectionString()))
            {
                context.Attach(query); //This does not seem to work
                if (page > 1)
                    query = query.Skip(page * maxcount);
                if (maxcount > 0)
                    query = query.Take(maxcount);

                return query.ToList();
            }
        }


In the original older code the EF repository was initializing the entities in the code/request constructor then it was calling the above method GetItems. It looked like two different connections were being made to the database and two different Entity initializations were happening to make 1 request to the database.

I am using EF Core 6 and .NET 6 (Core 6).

CodePudding user response:

You use context.Set<T>() to get the DbSet<T> (which is already an IQueryable<T>).

context.Attach attaches entities to the change tracker of the context, which is not what you want.

So you want something like:

public void GetOrders(){
    List<Order> orders;
    using(MyEntities context = new MyEntities(getConnectionString()))
    {
       var query = new List<Order>().AsQueryable();
       orders = context.GetItems<Order>().Where(x => x.Id > 100).ToList();    
    }
    // Do whatever with "orders", which is already materialized and doesn't need the dbcontext instance anymore
}


public static class DbContextExtensions
{
    // On some extension static class
    public static IQueryable<T> GetItems<T>(this MyEntities context, int page = 0, int maxcount = 0)
    where T: class
    {
        var query = context.Set<T>().AsQueryable();
        if (page > 1)
            query = query.Skip(page * maxcount);
        if (maxcount > 0)
            query = query.Take(maxcount);

        return query;
    }
}

Note that this is way out of how you should use dbcontext's these days (there's dependency injection, async, etc.), but I'm basing on the code you posted on the question

CodePudding user response:

You can build Func<IQueryable<T>, IQueryable<T>> and use context.Set<T>() method. Something along this lines:

public void GetOrder(){
     Func<IQueryable<Order>, IQueryable<Order>> queryProvider = 
     query => query.Where(x => x.Id > 100);
     var orders = repo.GetItems<Order>(query);
}

public IEnumerable<T> GetItems<T>(Func<IQueryable<T>, IQueryable<T>> queryProvider, int page = 0, int maxcount = 0)
{
    using (MyEntities context = new MyEntities(getConnectionString()))
    {
         var query = queryProvider(context.Set<T>()); 
         if (page > 1)
            query = query.Skip(page * maxcount);
         if (maxcount > 0)
            query = query.Take(maxcount);

         return query.ToList();
        }
    }
}

Note that manually creating DbContext's is not an idiomatic approach anymore, usually it is handled by DI.

Also I would recommend just to create an Paginate extension method. Something like this:

public static class QueryableExt
{
    public static IQueryable<T> Paginate<T>(this IQueryable<T> query, int page = 0, int maxcount = 0)
    {
        if (page > 1)
            query = query.Skip(page * maxcount);
        if (maxcount > 0)
            query = query.Take(maxcount);
        return query;
    }
}

CodePudding user response:

The code's problem isn't generics - by definition, EF Core is generic. DbSet<T> and IQueryable<T> are generic. All operators are generic.

The problem is mixing up two things. Paging over an IQueryable and creating a specific IQueryable implementation. If you split these two into separate parts, the problem becomes very easy.

The paging method can be :

public static IQueryable<T> GetPage<T>(this IQueryable<T> query,int page,int maxcount)
{
    if (page > 1)
        query = query.Skip(page * maxcount);
    if (maxcount > 0)
        query = query.Take(maxcount);
    return query;
 }

This can be used with any IQueryable<T> implementation, whether it's a container implementing IEnumerable<T> like a List<T> or an EF Core query:

var myList=new List<Order>();
...
var query=myList.AsQueryable()
                .Where(...)
                .GetPage(2,10);
var pageOrders=query.ToList();

Or

var query=context.Orders
                 .Where(...)
                 .GetPage(2,10);
var pageOrders=query.ToList();

There's no reason to create a GetItems method, as all it does is create a specific DbContext and use a specific DbSet. There's no need to hide context.Orders or the code that creates context. In fact, in environments with dependency injection like ASP.NET Core, that DbContext will be injected by the DI container

  • Related