Я не думаю, что есть единственный «лучший» способ сделать это.Все зависит от вашего бизнеса.Поэтому я просто делюсь тем, как бы я это сделал.
Мой подход DDD
В моем практическом проекте по разработке Domain-Drive у меня есть 2 набора моделей, не включая модели представлений для представлений:
- Доменные модели - представляют вашу бизнес-логику, не зависят от кода вашей инфраструктуры
- Постоянные модели - представляют, как вы отображаете модели доменов в выбранной вами базе данных
Модели постоянства
Я могу определить ClassA
и ClassB
как сущности с отношением один-ко-многим.
Допустим, я использую Entity Framework Core
в качестве ORM и SQL Server
в качестве базы данных.
public class ClassAEntity
{
public int Id { get; set; }
public string Name { get; set; }
public int ClassBId { get; set; }
public ClassBEntity ClassB { get; set; }
}
public class ClassBEntity
{
public int Id { get; set; }
public string Name { get; set; }
public List<ClassAEntity> ClassAs { get; set; }
}
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options): base(options) { }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
// Configure relationships
builder.Entity<ClassAEntity>(b =>
{
b.HasKey(x => x.Id);
b.Property(x => x.Name).IsRequired();
b.HasOne(x => x.ClassB)
.WithMany(y => y.ClassAs)
.HasForeignKey(x => x.ClassBId);
b.ToTable("ClassA");
});
builder.Entity<ClassBEntity>(b =>
{
b.HasKey(x => x.Id);
b.Property(x => x.Name).IsRequired();
b.ToTable("ClassB");
});
}
public DbSet<ClassAEntity> ClassAs { get; set; }
public DbSet<ClassBEntity> ClassBs { get; set; }
}
Доменные модели
Вы можете создавать свои доменные модели, полностью отличающиеся от ваших постоянных моделей.На самом деле в моем проекте ClassA
и ClassB
находятся в двух отдельных пространствах имен, и они оба Aggregate Root
- им не разрешено ссылаться друг на друга напрямую.Они могут ссылаться друг на друга только по своей идентичности.Также вы видите эти частные сеттеры?
public class ClassA : AggregateRoot
{
public int Id { get; private set; }
public string Name { get; private set; }
public int ClassBId { get; private set; }
private ClassA() { }
private ClassA(CreateClassACommand command) : base(command.Id)
{
this.Name = command.Name;
this.ClassBId = command.ClassBId;
// You can create events and store them later on as auditing or
// have others subscribe this event.
AddEvent(new ClassACreated
{
// ...
});
}
public static ClassA CreateNew(CreateClassACommand command,
IValidator<CreateClassACommand> validator)
{
// You can have validations here too, with help of FluentValidation
// library
validator.ValidateAndThrow(command);
return new ClassA(command);
}
public void UpdateDetails(UpdateClassADetailsCommand command,
IValidator<UpdateClassADetailsCommand> validator)
{
validator.ValidateAndThrow(command);
this.Name = command.Name;
this.ClassBId = command.SelectedClassBId;
AddEvent(new ClassADetailsUpdated
{
// ...
});
}
}
Надеемся, вы видите преимущества наличия класса для представления вашей бизнес-логики.У вас могут быть частные сеттеры, чтобы предотвратить изменение данных другими классами.Вы можете определить методы, открытые для других, у которых есть валидации.
Экран (ы) редактирования
Поскольку мои ClassA
и ClassB
являются двумя отдельными агрегатами, я не планировалесть экран, чтобы обновить их одновременно.Вместо этого у меня есть 2 отдельных контроллера для их представления.
На экране редактирования ClassA
я могу предоставить список доступных ClassBs
в виде раскрывающегося списка, так как их отношение одно к многим.
Опять нет правильного пути.Все зависит от вашей бизнес-логики.
public ClassAController : AdminControllerBase
{
private readonly AppDbContext _dbContext;
public ClassAController(AppDbContext dbContext)
{
_dbContext = dbContext;
}
public IActionResult Edit(int id)
{
// Find the entity by id in the database
var classAEntity = _dbContext.ClassAs
.AsNoTracking()
.SingleOrDefault(x => x.Id == id);
if (classAEntity == null)
{
return NotFound();
}
// Find a list of available class Bs
var availableClassBs = _dbContext.ClassBs
.AsNoTracking()
.Where(x => ... your filter ...)
.OrderBy(x => x.Name)
.ToDictionary(x => x.Id, x => x.Name);
// Construct the view model for editing
var vm = new EditClassAViewModel
{
ClassAId = classAEntity.Id,
Name = classAEntity.Name,
SelectedClassBId = classAEntity.ClassBId,
AvailableClassBs = availableClassBs
};
return View(vm);
}
}
Вот модель представления для экрана редактирования.В зависимости от того, что вы хотите редактировать, вы можете создавать свойства для этой цели.Также это правильное место для размещения ваших аннотаций данных, если у вас включена проверка на стороне клиента.
public class EditClassAViewModel
{
[Required]
public int ClassAId { get; set; }
[Required]
public string Name { get; set; }
[Display(Name = "Class b")]
public int SelectedClassBId { get; set; }
public IDictionary<int, string> AvailableClassBs { get; set; }
}
Редактирование ClassA View
Я думаю, вы поняли идею здесь, поэтому я несобираюсь опубликовать любой пример представления, чтобы сохранить некоторые пробелы.
При отправке
При обратной отправке контроллер захватывает модель представления, а затем вы можете преобразовать ее в модель вашего домена,Вызовите правильное действие для модели домена, которая сама будет обрабатывать проверки, а затем в конце вы преобразуете модель домена в модель постоянства и сохраните ее обратно в базу данных.
Примечание: в моемВ проекте я использовал библиотеку MediatR - отправлял и обрабатывал запросы / команды, библиотека AutoMapper - конвертировал модели туда-сюда и шаблон хранилища, но здесь я просто собрал все вместе, чтобы упростить процесс.
[HttpPost]
public IActionResult Edit(EditClassAViewModel model)
{
var response = new JsonResponse();
if (!ModelState.IsValid)
{
response.AddModalStateErrors(ModelState);
return Json(response);
}
// Get the ClassA entity from the database and convert the persistence
// model to your domain model. You could have your repository to do
// both in one step.
var classAEntity = _dbContext.ClassAs
.AsNoTracking()
.SingleOrDefault(x => x.Id == model.ClassAId);
if (classAEntity == null)
{
response.AddError(...);
return Json(response);
}
// Convert the persistence model to domain model. You could use
// AutoMapper to do so.
var classA = new ClassA(...);
// Class the ClassA domain model UpdateDetails method
classA.UpdateDetails(...);
// Convert the domain model back to persistence model
// and save it to the database. You could have your repository to do
// both in one step.
var classAPersistenceModel = ...;
// Since this persistence model is not tracked by EFCore,
// you need to fetch the entity again from database by Id and update
// that entity instead.
// Again, you could have your repository to do that in one step too.
classAEntity = _dbContext.ClassAs.Find(classAPersistenceModel.Id);
if (classAEntity != null)
{
_dbContext.Entry(classAEntity).CurrentValues
.SetValues(classAPersistenceModel);
_dbContext.SaveChanges();
}
}
Отказ от ответственности: ДаЯ знаю, что может быть много нового, что я проигнорировал в этом посте: шаблон запроса / команды с использованием IMediatR, проверка с помощью FluentValidation, AutoMaконфигурация pper и шаблон репозитория.Но цель этого поста - просто дать вам понимание моего подхода DDD.XD