0

First question here, I hope I'm doing it right.

I'm using Entity Framework Core 5.0 (Code First) with an onion architecture (data/repo/service/mvc) and so I have a service for each table (almost). It's work well but now I need to manage (get, insert, update, delete) about 150 tables which all have the same structure (Id, name, order).

I have added each of them as Entity class and their DbSet too in my DbContext, but I don't want to make 150 services, I would like to have a generic one .

How can I bind it to my generic repository ?

public class Repository<T> : IRepository<T> where T : BaseEntity
{
    private readonly ApplicationContext context;
    private DbSet<T> entities;
    private readonly RepositorySequence repoSequence;
            
    private string typeName { get; set; }
            
    public Repository(ApplicationContext context)
    {
        this.context = context;
        entities = context.Set<T>();
        this.repoSequence = new RepositorySequence(context);
            
        this.typeName = typeof(T).Name;
    }
            
    public T Get(long plng_Id)
    {
        return entities.SingleOrDefault(s => s.Id == plng_Id);
    }
   [...]
}

In an ideal world, would like to have something like this :

public async Task Insert(dynamic pdyn_Entity)
{
    Type DynamicType = Type.GetType(pdyn_Entity);
    Repository<DynamicType> vobj_Repo = new Repository<DynamicType>(mobj_AppContext);
    long Id = await vobj_Repo.InsertAsync(pdyn_Entity);
}

But I can try to get type from DbSet string Name too, I just managed to retrieve some data :

public IEnumerable<object> GetAll(string pstr_DbSetName)
{
     return ((IEnumerable<BaseEntity>)typeof(ApplicationContext).GetProperty(pstr_DbSetName).GetValue(mobj_AppContext, null));
 }

I've tried the following method (2.0 compatible apparently) to get the good DbSet, not working neither (no Query) : https://stackoverflow.com/a/48042166/10359024

What am I missing?

Thanks a lot for your help

Connell.O'Donnell
  • 3,603
  • 11
  • 27
  • 61
Rémy B.
  • 3
  • 1
  • Ask for yourself, why do you use generic repository, And if you have no answer - remove that antipattern. – Svyatoslav Danyliv Jan 22 '21 at 18:22
  • I already use it for other typed services. Indeed I had added new functions to entit, for instance GetWithoutTracking, manage Sequence on all insert... – Rémy B. Jan 22 '21 at 19:44

3 Answers3

0

Not sure why you need to get type?

You can use something like this.

Repository.cs

public class Repository<T> : IRepository<T> where T : BaseEntity
{
    private readonly ApplicationContext context;
    private DbSet<T> entities;

    public Repository(ApplicationContext context)
    {
        this.context = context;
        entities = context.Set<T>();
    }
    
    public List<T> Get()
       => entities.ToList();

    public T Get(long plng_Id)
       => entities.Find(plng_Id);
    
    public long Save(T obj)
    { 
        if (obj.ID > 0)
           entities.Update(obj);
        else
           entities.Add(obj);

        return obj.ID;
    }

    public void Delete(T obj)
       => entities.Remove(obj);
}

Then you can use either one of these 2 options you want

  1. Multiple repositories following your tables

UserRepository.cs

public class UserRepository : Repository<User> : IUserRepository
{
    private readonly ApplicationContext context;
    public UserRepository(ApplicationContext context)
    {
        this.context = context;
    }
}

BaseService.cs

public class BaseService : IBaseService
{
    private readonly ApplicationContext context;
    private IUserRepository user;
    private IRoleRepository role;
    public IUserRepository User { get => user ??= new UserRepository(context); }
    public IRoleRepository Role { get => user ??= new RoleRepository(context); }

    public BaseService(ApplicationContext context)
    {
        this.context = context;
    }
}
  1. If you are lazy to create multiple repositories, can use this way also. Your service just simple call Repository with entity name.

BaseService.cs

public class BaseService : IBaseService
{
    private readonly ApplicationContext context;
    private IRepository<User> user;
    private IRepository<Role> role;
    public IRepository<User> User { get => user ??= new Repository<User>(context); }
    public IRepository<Role> Role { get => role ??= new Repository<Role>(context); }

    public BaseService(ApplicationContext context)
    {
        this.context = context;
    }
}

Finally, you can call service like this. You can use multiple services instead of BaseService if you want.

HomeController.cs

public class HomeController : Controller
{
   private readonly IBaseService service;
   public HomeController(IBaseService service)
   {
       this.service = service;
   }

   public IActionResult Index()
   {
       var user = service.User.Get();
       return View(user);
   }

   public IActionResult Add(User user)
   {
       var id = service.User.Save(user);
       return View();
   }
}

I suggest to use first option (multiple repositories) because you may need to customise functions in own repository in future. And create service class following your controller name. For example, you have HomeController, UserController, etc. Create HomeService, UserService and link them with BaseService so that you can create customised functions in their own service class.

Asherguru
  • 1,687
  • 1
  • 5
  • 10
0

I assume you have a base entity like this:

public class BaseEntity 
    {
        [Key]
        public int Id { get; set; }

        public string Name { get; set; }

        public string Order { get; set; }
    }

Then you can do CRUD operations in your generic repository like this:

public int Create(T item)
    {
        if (item == null) return 0;
        entities.Add(item);////SaveChanges    
        return item.Id;
    }

public void Update(T updatedItem)
    {
      context.SetModified(updatedItem);//SaveChanges    
    }

public IQueryable<T> All()
    {
        return entities();
    }

And in each of the methods you have access to your 3 common fields in BaseEntity

Hamed Rashno
  • 314
  • 1
  • 2
  • 9
0

Thank you all for your responses.

I need to have the type because I am using a blazor component which automatically binds to these tables. This component has the name of the desired entity class (in string) as a parameter. Thanks to @Asherguru's response I was able to find a way to do this:

1 - I made a 'SedgmentEntity' Class :

public abstract class SegmentEntity : ISegmentEntity
{
    public abstract long Id { get; set; }

    public abstract string Name { get; set; }

    public abstract short? Order { get; set; }
}

2 - A SegmentRepository which is typed via Reflection:

public class SegmentRepository : ISegmentRepository
{
    private readonly ApplicationContext context;
    private readonly RepositorySequence repoSequence;
    
    public SegmentRepository(ApplicationContext context)
    {
        this.context = context;
        this.repoSequence = new RepositorySequence(context);
    }
    
    public async Task<long> Insert(string pstr_EntityType, SegmentEntity pobj_Entity)
    {
            Type? vobj_EntityType = Assembly.GetAssembly(typeof(SegmentEntity)).GetType("namespace.Data." + pstr_EntityType);
        
            if (vobj_EntityType != null)
            {
                // create an instance of that type
                object vobj_Instance = Activator.CreateInstance(vobj_EntityType);
        
                long? nextId = await repoSequence.GetNextId(GetTableName(vobj_EntityType));
        
                if (nextId == null)
                {
                    throw new TaskCanceledException("Sequence introuvable pour " + vobj_EntityType.FullName);
                }
        
                PropertyInfo vobj_PropId = vobj_EntityType.GetProperty("Id");
                vobj_PropId.SetValue(vobj_Instance, nextId.Value, null);
        
                PropertyInfo vobj_PropName = vobj_EntityType.GetProperty("Name");
                vobj_PropName.SetValue(vobj_Instance, pobj_Entity.Name, null);
        
                PropertyInfo vobj_PropOrder = vobj_EntityType.GetProperty("Order");
                vobj_PropOrder.SetValue(vobj_Instance, pobj_Entity.Order, null);
        
                return ((SegmentEntity)context.Add(vobj_Instance).Entity).Id;
            }
        }
    
    public IEnumerable<object> GetAll(string pstr_EntityType)
    {
        Type? vobj_EntityType = Assembly.GetAssembly(typeof(SegmentEntity)).GetType("namespace.Data." + pstr_EntityType);
    
        if (vobj_EntityType != null)
        {
            PropertyInfo vobj_DbSetProperty = typeof(ApplicationContext).GetProperties().FirstOrDefault(prop =>         
            prop.PropertyType.FullName.Contains(vobj_EntityType.FullName));
            return (IEnumerable<object>)vobj_DbSetProperty.GetValue(context, null);
        }
        return null;
    }
}

I still have to handle the Get and the Delete functions but it should be fine. Then I will be able to create a single service which will be called by my component.

Thanks again !

Rémy B.
  • 3
  • 1