EntityFarmeworkCore абстрактный слой доступа к данным

294
29 октября 2017, 16:58

Здравсвуйте, хотелось создать абстрактный слой доступа к данным используя проекты типа NetStandart 2.0

Думаю использовать один проект: работа с данными "DAL", в котором будут зависимости от EntityFarmeworkCore. В нем будет находится Context и Entities. Также обертка в виде обобщенного репозиория.

Второй проект бизнес логики "BL". зависимость от "DAL". Набор конкретных методов которые работают с данными репозиториев. Например: IList GetAllDepartments() Department GetDepartmentByName(string departmentName)

Начал искать похожие реализации и наткнулся на скачанную статью где описывается подобная архитектура (ссылку на статью не помню). Суть подхода в том, что в репозитории на каждое атомарное действие создается короткоживущий контекст, по сути без отслеживания трекинга изменений, методы репозитория возвращают не запрос Iquereble, а сразу IList. Слой бизнес логики использует эти методы, предоставляя понятный интерфейс наружу.

Реализовал тестовый пример и возникли вопросы правильно ли я все делаю. Привожу пример кода где используются всего 2 сущности Department и Employee.

Entities:

public interface IEntitie
{
   int Id { get; set; }
}
public class Department : IEntitie
{
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Employee> Employees { get; set; } = new HashSet<Employee>();
}
public class Employee : IEntitie
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public int DepartmentId { get; set; }
    public virtual Department Department { get; set; }

    public ICollection<Address> Addresses { get; set; } = new HashSet<Address>();
}

DbContext:

public class Context : Microsoft.EntityFrameworkCore.DbContext
{
    #region Reps
    public DbSet<Department> Departments { get; set; }
    public DbSet<Employee> Employes { get; set; }
    #endregion

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        var service = new GetConnectionStringService(); //получение строки подключения
        optionsBuilder.UseSqlServer(service.Connection);
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        //явное описание связи 1 ко многим
        //Employee < --> Departments One2Many----------------------------
        modelBuilder.Entity<Employee>()
            .HasOne(s => s.Department)
            .WithMany(s => s.Employees)
            .HasForeignKey(s => s.DepartmentId)
            .OnDelete(DeleteBehavior.Restrict);
    }
}

Repository:

public interface IGenericDataRepository<T> where T : class
{
    IList<T> GetAll(params Expression<Func<T, object>>[] navigationProperties);
    IList<T> GetList(Func<T, bool> where, params Expression<Func<T, object>>[] navigationProperties);
    T GetSingle(Func<T, bool> where, params Expression<Func<T, object>>[] navigationProperties);
    void Add(params T[] items);
    void Update(params T[] items);
    void Remove(params T[] items);
}

public class GenericDataRepository<T> : IGenericDataRepository<T> where T : class
{
    public virtual IList<T> GetAll(params Expression<Func<T, object>>[] navigationProperties)
    {
        List<T> list;
        using (var context = new Context())
        {
            IQueryable<T> dbQuery = context.Set<T>();
            //Apply eager loading
            foreach (Expression<Func<T, object>> navigationProperty in navigationProperties)
                dbQuery = dbQuery.Include<T, object>(navigationProperty);
            list = dbQuery
                .AsNoTracking()
                .ToList<T>();
        }
        return list;
    }
    public virtual IList<T> GetList(Func<T, bool> where,
        params Expression<Func<T, object>>[] navigationProperties)
    {
        List<T> list;
        using (var context = new Context())
        {
            IQueryable<T> dbQuery = context.Set<T>();
            //Apply eager loading
            foreach (Expression<Func<T, object>> navigationProperty in navigationProperties)
                dbQuery = dbQuery.Include<T, object>(navigationProperty);
            list = dbQuery
                .AsNoTracking()
                .Where(where)
                .ToList<T>();
        }
        return list;
    }
    public virtual T GetSingle(Func<T, bool> where,
        params Expression<Func<T, object>>[] navigationProperties)
    {
        T item = null;
        using (var context = new Context())
        {
            IQueryable<T> dbQuery = context.Set<T>();
            //Apply eager loading
            foreach (Expression<Func<T, object>> navigationProperty in navigationProperties)
                dbQuery = dbQuery.Include<T, object>(navigationProperty);
            item = dbQuery
                .AsNoTracking() //Don't track any changes for the selected item
                .FirstOrDefault(where); //Apply where clause
        }
        return item;
    }
    public virtual void Add(params T[] items)
    {
        using (var context = new Context())
        {
            foreach (T item in items)
            {
                context.Entry(item).State = EntityState.Added;
            }
            context.SaveChanges();
        }
    }
    public virtual void Update(params T[] items)
    {
        using (var context = new Context())
        {
            foreach (T item in items)
            {
                context.Entry(item).State = EntityState.Modified;
            }
            context.SaveChanges();
        }
    }
    public virtual void Remove(params T[] items)
    {
        using (var context = new Context())
        {
            foreach (T item in items)
            {
                context.Entry(item).State = EntityState.Deleted;
            }
            context.SaveChanges();
        }
    }
}

конкретные репозитории, если нужны специфические свойства или переопределение virtual методов
public interface IDepartmentRepository : IGenericDataRepository<Department>
{
}
public interface IEmployeeRepository : IGenericDataRepository<Employee>
{
}
public interface IAdressRepository : IGenericDataRepository<Address>
{
}

public class DepartmentRepository : GenericDataRepository<Department>, IDepartmentRepository
{
}
public class EmployeeRepository : GenericDataRepository<Employee>, IEmployeeRepository
{
}
public class AddressRepository : GenericDataRepository<Address>, IAdressRepository
{
}

интерфейс бизнес логики:

public interface IBusinessLayer
{
    IList<Department> GetAllDepartments();
    Department GetDepartmentByName(string departmentName);
    void AddDepartment(params Department[] departments);
    void UpdateDepartment(params Department[] departments);
    void RemoveDepartment(params Department[] departments);
    IList<Employee> GetEmployeesByDepartmentName(string departmentName);
    void AddEmployee(Employee employee);
    void UpdateEmploee(Employee employee);
    void RemoveEmployee(Employee employee);
    Employee GetEmployeeById(int employeeId);
}

реализация бизнес логики:

public class BuinessLayer : IBusinessLayer
{
    private readonly IDepartmentRepository _deptRepository;
    private readonly IEmployeeRepository _employeeRepository;
    private readonly IAdressRepository _adressRepository;

    #region ctor
    public BuinessLayer()
    {
        _deptRepository = new DepartmentRepository();
        _employeeRepository = new EmployeeRepository();
        _adressRepository= new AddressRepository();
    }

    public BuinessLayer(IDepartmentRepository deptRepository,
        IEmployeeRepository employeeRepository)
    {
        _deptRepository = deptRepository;
        _employeeRepository = employeeRepository;
    }
    #endregion


    public IList<Department> GetAllDepartments()
    {
        return _deptRepository.GetAll();
    }
    public Department GetDepartmentByName(string departmentName)
    {
        return _deptRepository.GetSingle(
            d => d.Name.Equals(departmentName),
            d => d.Employees); //include related employees
    }
    public void AddDepartment(params Department[] departments)
    {
        /* Validation and error handling omitted */
        _deptRepository.Add(departments);
    }
    public void UpdateDepartment(params Department[] departments)
    {
        /* Validation and error handling omitted */
        _deptRepository.Update(departments);
    }
    public void RemoveDepartment(params Department[] departments)
    {
        /* Validation and error handling omitted */
        _deptRepository.Remove(departments);
    }
    public IList<Employee> GetEmployeesByDepartmentName(string departmentName)
    {
        return _employeeRepository.GetList(e => e.Department.Name.Equals(departmentName));
    }
    public void AddEmployee(Employee employee)
    {
        /* Validation and error handling omitted */
        _employeeRepository.Add(employee);
    }
    public void UpdateEmploee(Employee employee)
    {
        /* Validation and error handling omitted */
        _employeeRepository.Update(employee);
    }
    public void RemoveEmployee(Employee employee)
    {
        /* Validation and error handling omitted */
        _employeeRepository.Remove(employee);
    }

    public Employee GetEmployeeById(int employeeId)
    {
        return _employeeRepository.GetSingle(
            d => d.Id.Equals(employeeId),
            d => d.Addresses); //include related employees
    }
}

ВОПРОСЫ:

  1. я так понимаю слой BL включает в себя нужные репозитории, из которых можно получить(загрузить)связанные данные. на набор всего из 2-ух сушностей такое большое кол-во методов в IBusinessLayer, это немного напрягает.

  2. Пример приведенн всего с одной связью 1 ко многим, а если будет глубина зависимостей больше, например у Employee появится набор адрессов (1 ко многим с таблицей адресов) тогда каким образом можно указать такую вложенность в методе репозитория с указанием navigationProperties? Необходимо явно указать .ThenInclude() и передать зависимое свойство, как это сделать например в GetAll?

    public virtual IList<T> GetAll(params Expression<Func<T, object>>[] navigationProperties)
    {
        List<T> list;
        using (var context = new Context())
        {
            IQueryable<T> dbQuery = context.Set<T>();
    
            foreach (Expression<Func<T, object>> navigationProperty in navigationProperties)
                dbQuery = dbQuery.Include<T, object>(navigationProperty); //???
            list = dbQuery
                .AsNoTracking()
                .ToList<T>();
        }
        return list;
    }

И как быть с реализацией многие ко многим которая разбивается на 2 реализации 1 ко многим. может я плохо понимаю как задавать Expression?

  1. Может такая архитектура и расчитанна на атомарные (отдельные) действия, например вытащили из базы Отдел и всех сотрудников, далее если нужны адресса вытаскиваем для каждого сотрудника список его адресов. Но это ведь жутко не оптимально, стольбко запросов отделных?

Может кто-то работал с подобным подходом прошу высказать свои мысли. Я бы сделал классическую реализацию репозитория возвращающую IQuereble и уже в методах бизнес логики составлял запросы (с необходимой глубиной вложенности), но методов в слое бизнес логики было бы опять много...

Answer 1

Подход, описанный выше вполне оправдан, не рекомендовал бы возвращать IQueryable, поскольку каждый слой должен инкапсулировать свою логику от внешнего мира, перенос части логики DAL нарушает само понятие разделения на слои.
К примеру, в какой то момент вы решили некоторые запросы перенести на Dapper(или другой микро-ORM) для увеличения производительности, однако вам придется менять:

  1. метод бизнес логики
  2. метод логики данных
  3. возвращаемый тип метода(хотя может и возможно IEnumerable привести к IQueryable, но по мне это извращенство)

Да, кол-во методов будет расти с ростом вашего приложения, этого никак не избежать (понимаю стремление к идеалу архитектуры, но такого, увы, не найти). Выше в комментариях привели CQRS, но в этом случае будет расти кол-во классов(причем со скоростью, гораздо большей, чем кол-во репозиториев).

READ ALSO
Не работает JQuery в проекте ASP.NET

Не работает JQuery в проекте ASP.NET

Есть Html-хелпер, который генерирует элемент input

258
Узнать о построении DOM | Selenium | C#

Узнать о построении DOM | Selenium | C#

Как узнать о построении DOM в Selenium ?

188
Проверка на вхождение элемента в массив

Проверка на вхождение элемента в массив

Добрый вечер! Есть двумерный массив arr и некорректная проверка на вхождение цифры 1

215
Упаковка ValueType при использовании IEnumerable

Упаковка ValueType при использовании IEnumerable

Допустим, имеется некий массив, например:

250