Files
explicit-mapping/src/After/Mappers.cs
T
2026-09-16 18:39:48 +01:00

141 lines
5.5 KiB
C#

namespace After;
/// <summary>
/// Hand-written mappers out of the plain domain (no AutoMapper — the mapping
/// code is part of the illustration). Each public method maps in exactly one
/// direction and never mutates its input. Two families, two destinations:
/// <list type="bullet">
/// <item><see cref="ClientDto"/>/<see cref="OrderDto"/> — transport to the
/// WebApp;</item>
/// <item><see cref="ClientData"/>/<see cref="OrderData"/> — the data-only
/// records the persistence framework stores.</item>
/// </list>
///
/// 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. The
/// persistence mapping needs no cache: records reference each other by id,
/// not by object, so the graph is flattened and cannot cycle.
/// </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,
TotalInCents = order.TotalInCents,
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,
TotalInCents = dto.TotalInCents,
Client = dto.ClientDto is null ? null : UnmapClient(dto.ClientDto, seen),
};
seen[dto] = order;
return order;
}
// ------------------------------------------------------------------
// Domain -> persistence (data-only records; graph flattened to ids)
// ------------------------------------------------------------------
/// <summary>Map a <see cref="Client"/> to its persistence record <see cref="ClientData"/>.</summary>
public static ClientData ToClientData(Client client)
{
ArgumentNullException.ThrowIfNull(client);
return new ClientData { Id = client.Id, Name = client.Name };
}
/// <summary>
/// Map an <see cref="Order"/> to its persistence record <see cref="OrderData"/>:
/// the object graph is flattened — <see cref="Order.Client"/> becomes the
/// <see cref="OrderData.ClientId"/> foreign key (empty when the order has
/// no client, or when that client has not been saved yet).
/// </summary>
public static OrderData ToOrderData(Order order)
{
ArgumentNullException.ThrowIfNull(order);
return new OrderData
{
Id = order.Id,
Description = order.Description,
TotalInCents = order.TotalInCents,
ClientId = order.Client?.Id ?? Guid.Empty,
};
}
}