Я пишу серверное приложение API с использованием .NET Core и Visual Studio.
Вот структура решения:
[ProjectName]
— Решение
[ProjectName].API
— Проект API, содержащий контроллеры, DTO, AutoMapper и т. Д.[ProjectName].Core
— Проект библиотеки классов, содержащий сервисы и бизнес-логику[ProjectName].Data
— Проект библиотеки классов, содержащий DbContext и Migrations[ProjectName].Domain
— Проект библиотеки классов, содержащий объекты базы данных[ProjectName].Interfaces
— Проект библиотеки классов, содержащий интерфейсы[ProjectName].Repositories
— Проект библиотеки классов, содержащий репозитории[ProjectName].Tests
— Консольное приложение, содержащее модульные тесты
Вот структура кода:
В .Repositories
проект есть BaseRepository
:
public class BaseRepository<T, TPrimaryKey> : IBaseRepository<T, TPrimaryKey> where T : class where TPrimaryKey : struct
{
private readonly DatabaseContext _dbContext;
public BaseRepository(DatabaseContext dbContext)
{
_dbContext = dbContext;
}
public async Task<IEnumerable<T>> GetAll()
{
return await _dbContext.Set<T>().ToListAsync();
}
public IQueryable<T> GetQueryable()
{
return _dbContext.Set<T>();
}
public async Task<T> Find(TPrimaryKey id)
{
return await _dbContext.Set<T>().FindAsync(id);
}
public async Task<T> Add(T entity, bool saveChanges = true)
{
await _dbContext.Set<T>().AddAsync(entity);
if (saveChanges)
await _dbContext.SaveChangesAsync();
return await Task.FromResult(entity);
}
public async Task Edit(T entity, bool saveChanges = true)
{
_dbContext.Entry(entity).State = EntityState.Modified;
if (saveChanges)
await _dbContext.SaveChangesAsync();
}
public async Task Delete(T entity, bool saveChanges = true)
{
if (entity == null)
throw new NullReferenceException();
_dbContext.Set<T>().Remove(entity);
if (saveChanges)
await _dbContext.SaveChangesAsync();
}
public async Task<IEnumerable<T>> BulkInsert(IEnumerable<T> entities, bool saveChanges = true)
{
foreach (T entity in entities)
{
await _dbContext.Set<T>().AddAsync(entity);
}
if (saveChanges)
await _dbContext.SaveChangesAsync();
return await Task.FromResult(entities);
}
public async Task BulkUpdate(IEnumerable<T> entities, bool saveChanges = true)
{
foreach (T entity in entities)
{
_dbContext.Entry(entity).State = EntityState.Modified;
}
if (saveChanges)
await _dbContext.SaveChangesAsync();
}
public async Task Save()
{
await _dbContext.SaveChangesAsync();
}
}
В .Interfaces
проект есть:
IBaseRepository
:
public interface IBaseRepository<T, E> where T : class where E : struct
{
Task<IEnumerable<T>> GetAll();
IQueryable<T> GetQueryable();
Task<T> Find(E id);
Task<T> Add(T entity, bool saveChanges = true);
Task Edit(T entity, bool saveChanges = true);
Task Delete(T entity, bool saveChanges = true);
Task<IEnumerable<T>> BulkInsert(IEnumerable<T> entities, bool saveC
Task BulkUpdate(IEnumerable<T> entities, bool saveChanges = true);
Task Save();
}
IServiceBase
:
public interface IServiceBase<TEntity, TPrimaryKey>
{
Task<TEntity> GetById(TPrimaryKey id);
Task<TEntity> GetSingle(Expression<Func<TEntity, bool>> whereCondition);
Task<IEnumerable<TEntity>> GetAll();
IEnumerable<TEntity> GetAll(Expression<Func<TEntity, bool>> whereCondition);
IQueryable<TEntity> GetAllQueryable();
IQueryable<TEntity> Query(Expression<Func<TEntity, bool>> whereCondition);
Task<TEntity> Create(TEntity entity);
Task Delete(TEntity entity);
Task Update(TEntity entity);
Task<long> Count(Expression<Func<TEntity, bool>> whereCondition);
Task<long> Count();
Task<IEnumerable<TEntity>> BulkInsert(IEnumerable<TEntity> entities);
Task BulkUpdate(IEnumerable<TEntity> entities);
}
и все интерфейсы сервисов конкретных сущностей:
Например IAddressServices
:
public interface IAddressService : IServiceBase<Address, Guid>
{
Task<Address> VerifyAddress(Address address);
}
В .Core
проект есть:
ServiceBase
:
public abstract class ServiceBase<TEntity, TRepository, TPrimaryKey> : IServiceBase<TEntity, TPrimaryKey>
where TEntity : class
where TPrimaryKey : struct
where TRepository : IBaseRepository<TEntity, TPrimaryKey>
{
public TRepository Repository;
public ServiceBase(IBaseRepository<TEntity, TPrimaryKey> rep)
{
Repository = (TRepository)rep;
}
public virtual async Task<TEntity> GetById(TPrimaryKey id)
{
return await Repository.Find(id);
}
public async Task<TEntity> GetSingle(Expression<Func<TEntity, bool>> whereCondition)
{
return await Repository.GetQueryable().Where(whereCondition).FirstOrDefaultAsync();
}
public async Task<IEnumerable<TEntity>> GetAll()
{
return await Repository.GetAll();
}
public IEnumerable<TEntity> GetAll(Expression<Func<TEntity, bool>> whereCondition)
{
return Repository.GetQueryable().Where(whereCondition);
}
public IQueryable<TEntity> GetAllQueryable()
{
return Repository.GetQueryable();
}
public IQueryable<TEntity> Query(Expression<Func<TEntity, bool>> whereCondition)
{
return Repository.GetQueryable().Where(whereCondition);
}
public virtual async Task<TEntity> Create(TEntity entity)
{
return await Repository.Add(entity);
}
public virtual async Task Delete(TEntity entity)
{
await Repository.Delete(entity);
}
public virtual async Task Update(TEntity entity)
{
await Repository.Edit(entity);
}
public async Task<long> Count(Expression<Func<TEntity, bool>> whereCondition)
{
return await Repository.GetQueryable().Where(whereCondition).CountAsync();
}
public async Task<long> Count()
{
return await Repository.GetQueryable().CountAsync();
}
public async Task<IEnumerable<TEntity>> BulkInsert(IEnumerable<TEntity> entities)
{
return await Repository.BulkInsert(entities);
}
public async Task BulkUpdate(IEnumerable<TEntity> entities)
{
await Repository.BulkUpdate(entities);
}
}
и конкретные реализации услуг:
Например AddressService
:
public class AddressService : ServiceBase<Address, IBaseRepository<Address, Guid>, Guid>, IAddressService
{
public AddressService(IBaseRepository<Address, Guid> rep) : base(rep)
{
}
public async Task<Address> VerifyAddress(Address address)
{
//logic
}
}
В .API
В проекте есть контроллеры:
Например ProductController
:
public class ProductController : ControllerBase
{
private readonly IProductService _productService;
private readonly IAddressService _addressService;
private readonly ILogger _logger;
private readonly IMapper _mapper;
public ProductController (IProductService productService,
IAddressService addressService,
ILogger<ProductController> logger,
IMapper mapper)
{
_packageService = packageService;
_addressService = addressService;
_logger = logger;
_mapper = mapper;
}
[HttpGet]
public async Task<IActionResult> GetAllProductsWithAddresses()
{
try
{
var products = await _productService.GetAllQueryable().Include(x => x.Address).ToListAsync();
return Ok(_mapper.Map<List<ProductResponse>>(products));
}
catch (Exception e)
{
_logger.LogError($"An unexpected error occured: ${e}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
}
Я много читал о том, что шаблон репозитория иногда бывает плохим, и его следует использовать только в случае необходимости модульных тестов. В моем случае необходимы модульные тесты, поэтому я хочу убедиться, что использую его правильно, и в будущем не будет узких мест в производительности.
- Использует
GetQueryable
вProductController
исправить дизайн шаблона репозитория? Если нет, то это только нарушение шаблона репозитория или есть другие проблемы с производительностью? - Если мне не нужны модульные тесты, лучше ли просто удалить уровень репозитория и использовать EF непосредственно в службах или контроллерах?
- Поскольку мне нужно было обновить несколько записей в базе данных, используя
BulkUpdate
изBaseRepository
не идеален с точки зрения производительности, поскольку для обновления 1000 строк в базе данных будет выполнено 1000 отдельных запросов. В идеале было бы выполнить 1 необработанный SQL-запрос, используяdbContext.Database.ExecuteSqlCommand
. Как и где я могу интегрировать выполнение необработанных SQL-запросов в такую настройку проекта, чтобы убедиться, что чистая архитектура удовлетворяет требованиям? - Если ответ на третий вопрос заключается в том, что вы не можете интегрировать необработанные SQL-запросы в эту архитектуру, чтобы сделать ее чистой, стоит ли мне посмотреть на сторонние библиотеки, которые включают эти функции, такие как Entity Framework plus (https://entityframework-plus.net/?z=ef-extended). Я не так хорошо знаком с этой библиотекой, но, как я выяснил, они выполняют 1 необработанный SQL-запрос при обновлении нескольких записей. Пример интеграции будет
await GetAllQueryable().Where(p => p.Name.Contains("test")).UpdateAsync(p => new Product() { Quantity = 10 });
. Лучше использовать сторонние библиотеки, такие как EF Plus, вместо сырых SQL-запросов?
Любая помощь приветствуется.