Yak: csharp after situation (Yak 02) — plain domain + DTOs + mappers

After/: plain Client/Order (no DbBase), ClientDto/OrderDto records
carrying the relations both ways, hand-written Mappers with a
reference-identity cache for the back-reference cycle, DbContext with
explicit Save(domain) via shadow entities (flipped dependency), and a
WebApp that consumes only DTOs. AfterTests cover the 7 required
behaviours; full solution builds with 0 warnings, 26/26 tests pass.
This commit is contained in:
2026-09-11 15:02:35 +01:00
parent 463245288a
commit c22a1e2ffa
10 changed files with 644 additions and 0 deletions
+102
View File
@@ -0,0 +1,102 @@
namespace After;
/// <summary>
/// Hand-written mappers between the plain domain and the DTOs (no AutoMapper —
/// the mapping code is part of the illustration). Each public method maps in
/// exactly one direction and never mutates its input.
///
/// The <c>Client.Orders</c> / <c>Order.Client</c> back-reference cycle is
/// resolved with a reference-identity cache: the same instance maps to the
/// same DTO (and back), so the graph terminates and stays consistent.
/// </summary>
public static class Mappers
{
/// <summary>Map a <see cref="Client"/> (with its orders) to a <see cref="ClientDto"/>.</summary>
public static ClientDto ToClientDto(Client client)
{
ArgumentNullException.ThrowIfNull(client);
return MapClient(client, new Dictionary<object, object>());
}
/// <summary>Map an <see cref="Order"/> (with its client back-reference) to an <see cref="OrderDto"/>.</summary>
public static OrderDto ToOrderDto(Order order)
{
ArgumentNullException.ThrowIfNull(order);
return MapOrder(order, new Dictionary<object, object>());
}
/// <summary>Map a <see cref="ClientDto"/> (with its orders) back to a <see cref="Client"/>.</summary>
public static Client FromClientDto(ClientDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return UnmapClient(dto, new Dictionary<object, object>());
}
/// <summary>Map an <see cref="OrderDto"/> (with its client back-reference) back to an <see cref="Order"/>.</summary>
public static Order FromOrderDto(OrderDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return UnmapOrder(dto, new Dictionary<object, object>());
}
// ------------------------------------------------------------------
// Domain -> DTO
// ------------------------------------------------------------------
private static ClientDto MapClient(Client client, Dictionary<object, object> seen)
{
if (seen.TryGetValue(client, out var cached))
return (ClientDto)cached;
var dto = new ClientDto { Id = client.Id, Name = client.Name };
seen[client] = dto; // cache first: breaks the cycle
dto.Orders = client.Orders
.Select(o => MapOrder(o, seen))
.ToList(); // fill second, through the cache
return dto;
}
private static OrderDto MapOrder(Order order, Dictionary<object, object> seen)
{
if (seen.TryGetValue(order, out var cached))
return (OrderDto)cached;
var dto = new OrderDto
{
Id = order.Id,
Description = order.Description,
ClientDto = order.Client is null ? null : MapClient(order.Client, seen),
};
seen[order] = dto;
return dto;
}
// ------------------------------------------------------------------
// DTO -> domain
// ------------------------------------------------------------------
private static Client UnmapClient(ClientDto dto, Dictionary<object, object> seen)
{
if (seen.TryGetValue(dto, out var cached))
return (Client)cached;
var client = new Client { Id = dto.Id, Name = dto.Name };
seen[dto] = client; // cache first: breaks the cycle
foreach (var orderDto in dto.Orders)
client.Orders.Add(UnmapOrder(orderDto, seen)); // fix-up wires order.Client
return client;
}
private static Order UnmapOrder(OrderDto dto, Dictionary<object, object> seen)
{
if (seen.TryGetValue(dto, out var cached))
return (Order)cached;
var order = new Order
{
Id = dto.Id,
Description = dto.Description,
Client = dto.ClientDto is null ? null : UnmapClient(dto.ClientDto, seen),
};
seen[dto] = order;
return order;
}
}