Reputation: 23
Looking for some general guidance around the Clean Architecture approach and EF Core.
I have a Web App (UI) project, Domain/Business project, and my Data/Infrastructure project for all crud operations.
When pulling in and creating a DbContext should the context itself be stored in the Data project and the Entities (table classes) be stored in the Domain/Business project since they have no direct crud options.
Or should the DbContext and corresponding entities (tables) be in the Data project together.
Appreciate any help.
Upvotes: 1
Views: 4813
Reputation: 3911
You should design your domain model classes according to the business requirements and in such a way that business domain invariants can be implemented the most efficient way.
This means not to burden any infrastructure concerns on your domain model entities. This principle can be adhered more easily if you put these classes into the domain project and stuff like DbContext and EFCore configuration classes into the data layer.
You can take a look at a reference Microsoft reference project where this approach is being followed.
The order entity (here also an aggregate root) placed in the core (or business domain} project does not have any infrastructure concerns other than the parameterless private constructor required by EFCore - an acceptable trade-off.
using Ardalis.GuardClauses;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using System;
using System.Collections.Generic;
namespace Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate
{
public class Order : BaseEntity, IAggregateRoot
{
private Order()
{
// required by EF
}
public Order(string buyerId, Address shipToAddress, List<OrderItem> items)
{
Guard.Against.NullOrEmpty(buyerId, nameof(buyerId));
Guard.Against.Null(shipToAddress, nameof(shipToAddress));
Guard.Against.Null(items, nameof(items));
BuyerId = buyerId;
ShipToAddress = shipToAddress;
_orderItems = items;
}
public string BuyerId { get; private set; }
public DateTimeOffset OrderDate { get; private set; } = DateTimeOffset.Now;
public Address ShipToAddress { get; private set; }
// DDD Patterns comment
// Using a private collection field, better for DDD Aggregate's encapsulation
// so OrderItems cannot be added from "outside the AggregateRoot" directly to the collection,
// but only through the method Order.AddOrderItem() which includes behavior.
private readonly List<OrderItem> _orderItems = new List<OrderItem>();
// Using List<>.AsReadOnly()
// This will create a read only wrapper around the private list so is protected against "external updates".
// It's much cheaper than .ToList() because it will not have to copy all items in a new collection. (Just one heap alloc for the wrapper instance)
//https://msdn.microsoft.com/en-us/library/e78dcd75(v=vs.110).aspx
public IReadOnlyCollection<OrderItem> OrderItems => _orderItems.AsReadOnly();
public decimal Total()
{
var total = 0m;
foreach (var item in _orderItems)
{
total += item.UnitPrice * item.Units;
}
return total;
}
}
}
The order entity database configuration is placed in the infrastructure/data project to instrument the mapping between the domain and database layer keeping the domain project independent from such infrastructure concerns.
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate;
namespace Microsoft.eShopWeb.Infrastructure.Data.Config
{
public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> builder)
{
var navigation = builder.Metadata.FindNavigation(nameof(Order.OrderItems));
navigation.SetPropertyAccessMode(PropertyAccessMode.Field);
builder.OwnsOne(o => o.ShipToAddress, a =>
{
a.WithOwner();
a.Property(a => a.ZipCode)
.HasMaxLength(18)
.IsRequired();
a.Property(a => a.Street)
.HasMaxLength(180)
.IsRequired();
a.Property(a => a.State)
.HasMaxLength(60);
a.Property(a => a.Country)
.HasMaxLength(90)
.IsRequired();
a.Property(a => a.City)
.HasMaxLength(100)
.IsRequired();
});
}
}
}
With this you have a clean separation of concerns and can focus on business logic in your business layer, getting better testability of the logic and you are still able to leverage the advantages of EFCore.
Upvotes: 4