Я пытаюсь построить структуру, используя шаблон репозитория в ядре структуры сущности.
У меня есть общий интерфейс и служба для общих операций. Они просто выполняют транзакции CRUD.
Мой интерфейс:
public interface IGeneralService<TEntity> where TEntity : class
{
void Delete(TEntity entityToDelete);
void Delete(object id);
IEnumerable<TEntity> Get(
Expression<Func<TEntity, bool>> filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
string includeProperties = "");
TEntity GetById(object id);
void Insert(TEntity entity);
void Update(TEntity entityToUpdate);
}
Мой сервисный слой:
public class GeneralService<TEntity> : IGeneralService<TEntity> where TEntity : class
{
internal DrivingSchoolContext context;
internal DbSet<TEntity> dbSet;
public GeneralService(DrivingSchoolContext context)
{
this.context = context;
this.dbSet = context.Set<TEntity>();
}
public virtual IEnumerable<TEntity> Get(
Expression<Func<TEntity, bool>> filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
string includeProperties = "")
{
IQueryable<TEntity> query = dbSet;
if (filter != null)
{
query = query.Where(filter);
}
if (includeProperties != null)
{
foreach (var includeProperty in includeProperties.Split
(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
query = query.Include(includeProperty);
}
}
if (orderBy != null)
{
return orderBy(query).ToList();
}
else
{
return query.ToList();
}
}
public virtual TEntity GetById(object id)
{
return dbSet.Find(id);
}
public virtual void Insert(TEntity entity)
{
dbSet.Add(entity);
}
public virtual void Delete(object id)
{
TEntity entityToDelete = dbSet.Find(id);
Delete(entityToDelete);
}
public virtual void Delete(TEntity entityToDelete)
{
if (context.Entry(entityToDelete).State == EntityState.Detached)
{
dbSet.Attach(entityToDelete);
}
dbSet.Remove(entityToDelete);
}
public virtual void Update(TEntity entityToUpdate)
{
dbSet.Attach(entityToUpdate);
context.Entry(entityToUpdate).State = EntityState.Modified;
}
}
И мой Startup.cs
services.AddScoped(typeof(IGeneralService<>), typeof(GeneralService<>));
Если я хочу выполнить операцию получения в этой простой структуре, я делаю это (в моем контроллере)
[HttpGet]
public Student GetStudent(int id)
{
var student = _generalService.Get(filter: x => x.id == id).FirstOrDefault();
return student;
}
Я хочу разместить еще один сервисный слой перед контроллером
когда я хочу сделать вставку, она должна сначала соответствовать моим бизнес-правилам (таким как номер телефона, нулевая проверка и т. д.), но я не мог найти, как вызвать несколько служб (логика) в «шаблоне репозитория» (без создания новый экземпляр)
если я создам новый экземпляр следующим образом:
public static DrivingSchoolContext DrivingSchoolContext;
private readonly IGeneralService<Student> _generalService;
private readonly StudentService _studentService;
public StudentController(DrivingSchoolContext drivingSchoolContext, IGeneralService<Student> generalService)
{
DrivingSchoolContext = drivingSchoolContext;
_generalService = generalService;
this._studentService = new StudentService(drivingSchoolContext);
}
[HttpGet]
public Student GetStudent(int id)
{
var studentFromNewInstance = _studentService.GetStudent(id);
var student = _generalService.Get(filter: x => x.id == id).FirstOrDefault();
return student;
}
этот код работает, но у меня слишком много сервисов, и я не хочу их создавать.
Могу ли я ссылаться на свои бизнес-правила, не создавая новый «экземпляр» для каждой службы?
Я хочу использовать обе мои услуги следующим образом. Например, у нас есть сценарий создания студента. Прежде всего, я хочу зайти в сервис StudentService
и применить свои бизнес-правила. если он соответствует бизнес-правилам, я хочу, чтобы он запускал метод Insert в Generalservice
. В основном так:
[HttpGet]
public IActionResult CreateStudent(Student student)
{
student.FullName = StudentService.GetFullName(student.firstName + student.lastName);
_generalService.Insert(student);
return Ok("Successfully Created Student");
}
Привет @Hopeless, спасибо за комментарий. Да, это два разных сервиса. My StudentService состоит из определенных операций и бизнес-правил. (например, GetStudentPhoneNumber или CheckStudentExist и т. д.) IGeneralService (и GeneralService), выполняющие только общие операции. (Например, CRUD, GetAll и т. д.) Если я хочу выполнять определенные операции для классов, я хочу использовать класс EntityService (в данном случае это UserService). Это злоупотребление?
на самом деле я не понимаю, что вы имеете в виду, вы только что представили 2 новых имени службы (EntityService
и UserService
), которых вообще нет в вашем опубликованном коде. Так что это своего рода потеря трека.
Позвольте мне завершить тему. EntityService на самом деле мой StudentService. моя сущность (студент). Я думал, что это похоже на Entity (Student) Service, но, думаю, я не мог объяснить это очень хорошо. Мне очень жаль. Как бы то ни было, у меня просто есть GeneralService
и StudentService
GeneralService, выполняющие мои общие операции (например, CRUD), но у StudentService есть более конкретные функции (например, CheckStudentExist). Надеюсь, я смог хорошо объяснить свою проблему. Если есть что-то, что я не могу объяснить, пожалуйста, не стесняйтесь спрашивать. Спасибо за вашу помощь
Я думаю, что дело в том, правильно это спроектировать или нет, а не в том, правильно или нет использовать. Ваш код использует его правильно (поскольку вашему контроллеру может потребоваться использовать API, отличный от ваших IGeneralService
и StudentService
). Но дело здесь в том, что вы могли бы каким-то образом объединить эти службы в одну (например, на основе какой-то общей абстрактной службы,...).
Я понимаю. Я хочу использовать обе мои услуги следующим образом. Например, у нас есть сценарий создания студента. Прежде всего, я хочу зайти в сервис StudentService и применить свои бизнес-правила. если он проходит бизнес-правила, я хочу, чтобы он запускал метод Insert в Generalservice. В основном так: (я обновляю вопрос выше, потому что он не появляется прямо здесь)
Одна из рекомендаций состоит в том, чтобы избегать использования шаблона Generic для репозиториев/служб и не уделять слишком много внимания абстракциям. Я знаю, что это заманчиво и выглядит так, как будто это было бы эффективно, но это загоняет вас в угол, когда дело доходит до написания высокопроизводительного доступа к данным. Когда дело доходит до абстракций, я советую следовать K.I.S.S. и консолидировать, когда и где это целесообразно после реализации простейшего решения. Когда дело доходит до Generics и Inheritance, попытка спроектировать это «наперед» является формой преждевременной оптимизации. И Generics, и Inheritance должны применяться для идентичного поведения. Это абсолютно ключевой момент. Слишком часто я вижу, как разработчики зацикливаются на этом на ранних стадиях проекта из-за того, что поведение просто похоже. Они полезны для поведения, которое совпадает на 100%, а не на 95%. Это либо отбрасывает вас в сторону менее оптимального решения, обычно ограничивая производительность или вашу способность адаптироваться к меняющимся требованиям, либо приводит к компромиссам или отклонениям (т. -коэффициент для новых требований, которые не соответствуют проектным решениям, основанным на прошлых предположениях/знаниях.
Подход, который я рекомендую, заключается в том, чтобы обращаться с репозиториями так же, как с контроллером. Если у вас есть ManageStudentController, то у меня будет ManageStudentRepository. Бизнес-логика низкого уровня может быть размещена в репозитории в зависимости от требований к хранилищу данных. Если вы действительно хотите углубиться в разделение проблем, вы можете создать класс StudentFactory, но я считаю, что репозиторий идеально подходит для того, чтобы взять на себя эту ответственность. В конечном счете, логика представления должна содержать бизнес-правила для обеспечения достоверности данных, а репозиторий служит последним привратником в хранилище данных. Он будет содержать метод CreateStudent, который принимает параметры для всех необходимых деталей, чтобы создать минимально полную и действительную сущность Student, связывает ее с DbContext и возвращает ее. Поэтому, если требуются такие сведения, как имя и номер телефона, метод CreateStudent применяет эти правила данных низкого уровня. Он также будет принимать либо значения FK, либо связанные сущности для ненулевых отношений. Вот почему репозиторий является хорошим местом для этого метода, поскольку он может проверять значения (например, проверку на уникальность) и загружать связанные сущности при наличии ключа. Контроллер и т. д. могут затем заполнить любые необязательные данные в возвращаемом студенте (в идеале, используя методы мутации DDD, а не сеттеры) контролируемым образом до того, как единица работы зафиксирует изменения.
Проблема с общими шаблонами, подобными тому, что вы описали, который является очень распространенной реализацией репозитория, заключается в том, что он очень неэффективен.
IEnumerable<TEntity> Get(
Expression<Func<TEntity, bool>> filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
string includeProperties = "");
Это всегда будет возвращать объекты. Хотя вы добавили выражения для фильтрации (Where
) и сортировки (OrderBy
/OrderByDescending
), includeProperties
полагается на магические строки, и нет поддержки таких вещей, как нумерация страниц. Вы также ограничиваете доступ к данным к синхронным вызовам, если только вы не пишете больше логики для вариантов, поддерживающих ожидание.
Более простое решение — использовать IQueryable<TEntity>
.
IQueryable<TEntity> Get();
Если вы используете систему мягкого удаления (используя что-то вроде IsActive, а не жесткое удаление строк), то:
IQueryable<TEntity> Get(bool includeInactive = false);
Репозиторий может автоматически управлять низкоуровневыми правилами, такими как IsActive, Authentication/Authorization, применимыми к запросу. Затем абоненты могут полностью свободно фильтровать, упорядочивать, разбивать на страницы и проецировать результаты так, как им удобно, а также использовать рычаги async
/await
. Это дает вам полный контроль над потреблением данных для максимальной производительности и очень простую точку абстракции для модульного тестирования. Типичный вопрос звучит так: «Если это такая тонкая оболочка над DbSets, зачем вообще возиться с репозиторием?». Ответ на этот вопрос состоит в двух вещах: а) Гораздо проще имитировать репозиторий и IQueryable<TEntity>
, чем имитировать DbContext
/DbSet
; и B) Репозиторий по-прежнему служит отличным местом для обеспечения соблюдения правил данных низкого уровня, чтобы гарантировать, что сущности «достаточно полны», а также проверки авторизации и т. д.
Распространенный аргумент против использования IQueryable<TEntity>
заключается в том, что люди чувствуют, что это «утечка» EF-измов/знаний. Однако решение, использующее выражения и тому подобное, чтобы попытаться использовать фильтрацию, сортировку и т. д., на 100% так же негерметично. Выражения фильтрации и сортировки по-прежнему должны учитывать то, что будет понимать EF, например, не использовать локальные функции или несопоставленные свойства и т. д. Чтобы быть такими же гибкими, как IQueryable<TEntity>
, вы в конечном итоге переписываете большую часть IQueryable<TEntity>
и все равно остаетесь короткими. (например, проекция /w Select
или Automapper ProjectTo
)
При организации репозиториев, таких как контроллеры, одним из аргументов может быть нарушение DRY (не повторяйтесь), поскольку процесс управления студентом может включать методы репозитория для извлечения других связанных данных, поэтому что-то вроде GetClasses()
для получения списка доступных классов для привязки к student также может существовать в других областях приложения, то есть в других репозиториях. Это, безусловно, верно, но контраргумент заключается в том, что DRY должен соблюдать принцип единой ответственности. Код должен иметь одну причину и только одну причину для изменения. Напишите ли вы Repository<Class>
или ClassRepository
, чтобы придерживаться DRY, вы нарушаете SRP. Может показаться, что ClassRepository существует только для того, чтобы обслуживать классы, но на самом деле каждый контроллер/сервис, который ссылается на ClassRepository, накладывает «причину изменения» на репозиторий. Каждый контроллер неизбежно будет хотеть что-то немного другое, когда дело доходит до извлечения классов, и эти требования со временем будут меняться. Это вводит сложность, условный код или множество похожих, но тонких вариантов методов, или, чтобы «форсировать» SRP, он требует, чтобы весь код принимал очень стандартное, минимально жизнеспособное представление данных, утрачивая производительность. С другой стороны, ManageStudentRepository имеет только одну причину для изменения. Для обслуживания ManageStudentController. Это также сводит к минимуму количество зависимостей в данном контроллере, поэтому вместо ссылок, возможно, на дюжину репозиториев, контроллер будет иметь ссылку на один или два репозитория. (Второе — это что-то вроде стандартного LookupRepository, который может легко работать с минимально жизнеспособными данными). В сочетании с использованием IQueryable<TEntity>
любое дублирование методов между репозиториями представляет собой очень маленькие, простые и понятные выражения Linq.
Наконец, если есть требование иметь бизнес-логику, вызываемую несколькими точками входа, такими как контроллер MVC и контроллер WebAPI, то это может привести к тому, что эти контроллеры будут рассматриваться как более анемичная служба, передавая параметры в общий класс службы, а затем отвечает за сопоставление ответа с представлением. Опять же, это то, что я хотел бы решить только после того, как это требование будет закреплено в камне, а не преждевременно. Перемещение кода, который взаимодействует с репозиториями и проецирует ViewModels, из чего-то вроде контроллера MVC в отдельную службу, проецирование на общий DTO и обновление контроллера MVC для вызова этого и проецирования DTO на ViewModels — довольно простой процесс, и есть вероятность, что это так. не нужно выполнять для каждого действия, а только для подмножества общих функций.
Я знаю, что это, вероятно, не «ответ» на ваш вопрос, но, надеюсь, поднимает некоторые моменты, которые следует учитывать в вашем решении.
мы не должны серьезно думать о спектакле поначалу. Написание службы абстрактных сущностей может немного снизить производительность, но вернуть большое удобство и уменьшить большое количество логически дублированного кода. Я использовал службу абстрактных объектов в некоторых проектах, и она отлично работает. Он основан на EF, даже если у вас есть некоторая пользовательская логика (которая не может быть реализована службой сущностей), вы всегда можете вернуться к непосредственному использованию EF. Наконец, в худшем случае нам нужно максимально повысить производительность, мы можем обойти EF и написать чистый SQL. Это концепция слоев с несколькими абстрактными уровнями.
Дело не столько в том, чтобы сосредоточиться на производительности, сколько в том, что абстракция ограничивает возможности, предоставляемые EF. Простое написание абстракций, которые возвращают «TEntity» и IEnumerable<TEntitiy>
, являются ужасными стандартами для построения системы. Использование проекций — это одна часть создания эффективных запросов, а другая — не раскрытие информации о вашей системе больше, чем требуется для работы представления. Помимо сложных функций отчетности, я еще не сталкивался с проблемой производительности, которую нельзя было бы улучшить более чем в 10 раз, если бы они использовали проекцию. Это абстракция по неправильной причине.
как я уже сказал, EntityService общего назначения — это всего лишь инструмент, который мы можем применять во многих случаях (поэтому вместо того, чтобы писать около 30-50 строк кода при каждом обновлении, вам достаточно 1-3 строк кода), особенно при сохранении данных (Я написал свой собственный метод обновления, используя отражение для выполнения многих ручных работ). В некоторых сценариях вы всегда можете использовать EF напрямую так, как он предназначен. Во многих небольших проектах (обслуживающих менее 200 тыс. пользователей) нам не нужно особо заботиться о производительности, поэтому мы можем использовать общий код (каждый запрос может занимать всего 50-300 мс).
как выглядит ваш
StudentService
? это имеет какое-то отношение кIGeneralService
? Если они служат совершенно разным задачам и не связаны между собой, это действительно две разные службы. Поэтому, если ваш контроллер использует API-интерфейсы обоих из них, они должны иметь отдельные экземпляры (в качестве зависимостей). Ваш вопрос все еще расплывчат, чтобы иметь ответ.