Generic method taking a PrefetchPath2 as an argument

Posts   
 
    
Posts: 37
Joined: 09-Nov-2016
# Posted on: 14-Dec-2016 10:48:26   

Hi,

I am creating a Web API using ASP.NET Core Web Application (using the .NET Framework) with Database First. I am using LLBLGen Pro version 5.1 and the LLBLGen Runtime.

In order to reduce the amount of overhead and make the code easier to unit-test, I am trying to make a generic version of the most common methods, which is GetAll, Find, Post, Put and Delete (inspired by https://goo.gl/4DnImB). However, I have run into a problem with a generic GetAll method (I am new to generics) where I can specify the prefetch paths as an argument.

What I want is something like this:

public class GenericRepository<T> : IGenericRepository<T> where T : class
{
    public IEnumerable<T> GetAllWithPrefetch(PrefetchPath2 path)
    {
        using (DataAccessAdapter adapter = new DataAccessAdapter(Variables.GetConnectionString(catalog)))
        {
            var data = new LinqMetaData(adapter);
            var queryable = data.GetQueryableForEntity<T>().WithPath(path).ToList();
        }
    }
}

However, I am getting the following error from the compiler:

"There is no implicit reference conversion from 'T' to 'SD.LLBLGen.Pro.ORMSupportClasses.IEntityCore'" as WithPath requires an IQueryable<IEntityCore> and I have IQueryable<T>. I have tried to convert using an old post on the forums as example like so:

var data = new LinqMetaData(adapter);
var queryable = (IQueryable<IEntityCore>) data.GetQueryableForEntity<T>();
return queryable.Select(all => all).WithPath(path).ToList() as IEnumerable<T>;

This satisfies the compiler but throws a "Can't obtain entity factory for type 'SD.LLBLGen.Pro.ORMSupportClasses.IEntityCore'" during runtime.

Is there any way to achieve the above?

Best regards Andreas

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39749
Joined: 17-Aug-2003
# Posted on: 15-Dec-2016 11:58:24   

At runtime you want 'T' to be a real entity type, like CustomerEntity. Unless that's the case, your code won't work.

To avoid the cast you can also add 'IEntityCore' to the 'where' clause of the generics rule.

How is this method called? As that would tell us if 'T' is indeed a real entity type or will always be IEntityCore.

Frans Bouma | Lead developer LLBLGen Pro
Posts: 37
Joined: 09-Nov-2016
# Posted on: 16-Dec-2016 10:07:52   

Dear Otis,

Thank you for your reply.

I currently have a GetAll() method implemented and it works like this (it’s a WebAPI).

The repository method looks like this:

public class GenericRepository<T> : IGenericRepository<T> where T : class
{
    public IEnumerable<T> GetAll() 
    {
        using (DataAccessAdapter adapter = new DataAccessAdapter("connectionString"))
        {
            var data = new LinqMetaData(adapter);

            return data.GetQueryableForEntity<T>().ToList();
        }
    }
}

It is made available like this:

public void ConfigureServices(IServiceCollection services)
{
      services.AddSingleton(typeof(IGenericRepository<>), typeof(GenericRepository<>));
}

Which is injected into the controller like this:

public class ItemController : Controller
{
     public IGenericRepository<ObjektEntity> Items;

     public ThmMuseumItemController(IGenericRepository<ObjektEntity> items)
    {
        Items = items;
    }

    [HttpGet]
    public IEnumerable<ObjektEntity> Get()
    {
        return Items.GetAll();
    }
}

The above is working fine, it's creating an almost identical method that takes a PrefetchPath2 as an argument (or a list of strings and creating a PrefetchPath2 from that) that is causing me problems. It might be something overly simple I have missed, but I can't see it.

Best regards Andreas

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39749
Joined: 17-Aug-2003
# Posted on: 19-Dec-2016 10:26:57   

I think the problem is that the prefetch path isn't something 'generic', it is specific for a type T. How would you use this generic method with the prefetch path input?

Frans Bouma | Lead developer LLBLGen Pro
Posts: 37
Joined: 09-Nov-2016
# Posted on: 19-Dec-2016 17:47:01   

Yes, I think you are right. I must admit I don't see a way to do this using my above approach.

The issue is that I want to implement my API using an Interface for two reasons:

1: Separate business logic from data access logic. Our current API is created using Entity Framework (EF), and it is not separated, which makes it a pain to change to LLBLGen.

2: To enable unit testing without having to code a specific entry point for the test.

For each controller I need the standard methods (GetAll, GetById, Update, Insert, Delete). If I want to do this using an Interface and it is not possible to make a generic version of the above methods, all Interfaces would need to be specific for the controller. If I for example had a CustomerController, I would need to create the following Interface:

IEnumerable<CustomerEntity> GetAll();
CustomerEntity GetById (int customerId);
void Update(CustomerEntity customerEntity);
void Insert(CustomerEntity customerEntity);
void Delete(int customerId);

But since I have a lot of controllers, I would need a lot of interfaces.

I don’t know if I am explaining it well enough, if not this article explains the idea (in EF): https://goo.gl/8S1cQv

I don’t know if my thinking is too much “EF’ish” and there is a better way to use LLBLGen so the above is not an issue, or it is simply not possible in LLBLGen (which is also fine, we like it way more than EF so we are probably going to switch anyway, I just want to make sure before I create 100+ specific interfaces simple_smile )

Best regards Andreas

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39749
Joined: 17-Aug-2003
# Posted on: 22-Dec-2016 09:42:13   

simple_smile

Interfaces are a different form of generic programming than generics. If you have, say, 100 controller classes, you can use a single interface to work with all 100 of them. I think you are after that, I recon? (it's of no use to create an interface I for a class C if I is just re-defining the interface of C, as a class already has an interface).

For the most part, it's doable using straightforward code, which you already have. the painpoint is the prefetch path. Now, in our runtime we do have the same thing.

You can specify prefetch paths in multiple ways:


IEnumerable<T> GetAll<T>(IPrefetchPathCore path) {}
IEnumerable<T> GetAll<T>(IPathEdge[] pathEdges) {}
IEnumerable<T> GetAll<T>(Func<IPathEdgeRootParser<T>, IPathEdgeRootParser<T>> edgeSpecifierFunc) {}

These are the 3 'WithPath' overloads we have in the runtime classes. The last one looks scary, but it's actually a way to specify a lambda and pass it on simple_smile I've created a sample general repo for you below with all 3 methods and the usage of them. You can pick whatever you want, it's up to you simple_smile


public class GeneralRepo<T>
        where T : class, IEntity2
{
    public IEnumerable<T> GetAll(IPrefetchPathCore path)
    {
        using(var adapter = new DataAccessAdapter())
        {
            var metaData = new LinqMetaData(adapter);
            var q = metaData.GetQueryableForEntity<T>();
            return q.WithPath(path).ToList();
        }   
    }

    public IEnumerable<T> GetAll(IPathEdge[] pathEdges)
    {
        using(var adapter = new DataAccessAdapter())
        {
            var metaData = new LinqMetaData(adapter);
            var q = metaData.GetQueryableForEntity<T>();
            return q.WithPath(pathEdges).ToList();
        }

    }

    public IEnumerable<T> GetAll(Func<IPathEdgeRootParser<T>, IPathEdgeRootParser<T>> edgeSpecifierFunc)
    {
        using(var adapter = new DataAccessAdapter())
        {
            var metaData = new LinqMetaData(adapter);
            var q = metaData.GetQueryableForEntity<T>();
            return q.WithPath(edgeSpecifierFunc).ToList();
        }
    }
}

Usage: All 3 load all customers and all orders of the customers. You can augment the prefetch path elements as you want before you call the methods, e.g. to specify filters, just as you would in a normal query.


[Test]
public void GeneralRepoUsagetest()
{
    var repo = new GeneralRepo<CustomerEntity>();

    var path = new PrefetchPath2(EntityType.CustomerEntity);
    path.Add(CustomerEntity.PrefetchPathOrders);
    var m1 = repo.GetAll(path);
    Assert.AreEqual(91, m1.Count());
    var m2 = repo.GetAll(new[] {new PathEdge<OrderEntity>(CustomerEntity.PrefetchPathOrders)});
    Assert.AreEqual(91, m1.Count());
    var m3 = repo.GetAll(p=>p.Prefetch(c=>c.Orders));
    Assert.AreEqual(91, m1.Count());
}

Frans Bouma | Lead developer LLBLGen Pro
Posts: 37
Joined: 09-Nov-2016
# Posted on: 27-Dec-2016 09:38:16   

Thank you for your help! It is working now simple_smile

For any else trying the same thing in Web API 2, here is the complete code (just tested quickly). First of all the Interface:

public interface IGenericRepository<T> where T : class, IEntity2
{
    IEnumerable<T> GetAll();
    IEnumerable<T> GetAllWithPrefetch(IPrefetchPathCore path);
    T GetById(int id);
    T GetByIdWithPrefetch(int id, IPrefetchPath2 path);
    void Insert(T entity);
    void Update(int id, T entity);
    void Delete(int id);
}

and the actual implementation (the implementation is mixed low-level and Linq to LLBLGen, not sure how to do the low-level parts in Linq to LLBLGen and vice versa):

public class GenericRepository<T> : IGenericRepository<T> where T : class, IEntity2, new()
{
    private const string catalog = "GENREG_THM";

    public IEnumerable<T> GetAll() 
    {
        using (DataAccessAdapter adapter = new DataAccessAdapter(Variables.GetConnectionString(catalog)))
        {
            var data = new LinqMetaData(adapter);

            return data.GetQueryableForEntity<T>().ToList();
        }
    }

    public IEnumerable<T> GetAllWithPrefetch(IPrefetchPathCore path)
    {
        using (DataAccessAdapter adapter = new DataAccessAdapter(Variables.GetConnectionString(catalog)))
        {
            var data = new LinqMetaData(adapter);

            var query = data.GetQueryableForEntity<T>();

            return query.WithPath(path).ToList();
        }
    }

    public T GetById(int id)
    {
        using (DataAccessAdapter adapter = new DataAccessAdapter(Variables.GetConnectionString(catalog)))
        {
            T entity = GetEntity(id);
            adapter.FetchEntity(entity);

            return entity;
        }
    }

    public T GetByIdWithPrefetch(int id, IPrefetchPath2 path)
    {
        using (DataAccessAdapter adapter = new DataAccessAdapter(Variables.GetConnectionString(catalog)))
        {
            T entity = GetEntity(id);
            adapter.FetchEntity(entity, path);

            return entity;
        }
    }

    public void Delete(int id)
    {
        using (DataAccessAdapter adapter = new DataAccessAdapter(Variables.GetConnectionString(catalog)))
        {
            adapter.DeleteEntity(GetEntity(id));
        }
    }

    public void Insert(T entity)
    {
        using (DataAccessAdapter adapter = new DataAccessAdapter(Variables.GetConnectionString(catalog)))
        {
            adapter.SaveEntity(entity, true);
        }
    }

    public void Update(int id, T entity)
    {   
        using (DataAccessAdapter adapter = new DataAccessAdapter(Variables.GetConnectionString(catalog)))
        {
            (entity as IEntity2).PrimaryKeyFields[0].CurrentValue = id;
            entity.IsNew = false;

            adapter.SaveEntity(entity);
        }
    }

    private T GetEntity(int id)
    {
        T entity = new T();
        (entity as IEntity2).PrimaryKeyFields[0].CurrentValue = id;
        return entity;
    }
}

It's registrered like this in the Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    ...
    services.AddSingleton(typeof(IGenericRepository<>), typeof(GenericRepository<>));
    ...
}

And finally used like this in the Controller (I have just shown one method):

[Route("api/[controller]")]
public class ItemsController : Controller
{
    public IGenericRepository<ObjectEntity> Items;

    public ItemsController(IGenericRepository<ObjectEntity> items)
    {
        Items = items;
    }

    // GET: api/values
    [HttpGet]
    public IEnumerable<IEntityCore> Get()
    {
        return Items.GetAll();
    }
}

And finally a note. If you are using JSON, you will probably find this blog here: https://weblogs.asp.net/fbouma/how-to-make-asp-net-webapi-serialize-your-llblgen-pro-entities-to-json However in the newer projects, it looks like this:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc()
    .AddJsonOptions(options => options.SerializerSettings.PreserveReferencesHandling = Newtonsoft.Json.PreserveReferencesHandling.Objects)
    .AddJsonOptions(options => options.SerializerSettings.ContractResolver = new DefaultContractResolver() { IgnoreSerializableAttribute = true, IgnoreSerializableInterface = true });
}

Best regards Andreas

daelmo avatar
daelmo
Support Team
Posts: 8245
Joined: 28-Nov-2005
# Posted on: 28-Dec-2016 07:59:34   

Thanks for the feedback. I'm sure it'll be very valuable for other users.

David Elizondo | LLBLGen Support Team