132 lines
5.8 KiB
C#
132 lines
5.8 KiB
C#
namespace After;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// The persistence side of the "after" situation. The (fake) framework stores
|
|
// its own data-only records — <see cref="ClientData"/> / <see cref="OrderData"/>,
|
|
// both <see cref="DbBase"/> subclasses — never the domain objects themselves.
|
|
// Domain state reaches storage only through explicit mapping; the context keeps
|
|
// a reference-identity map so a second <see cref="Save"/> of the same domain
|
|
// object refreshes its row instead of duplicating it.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// <summary>
|
|
/// Persisted shape of a <see cref="Client"/>: data only — the orders are
|
|
/// separate <see cref="OrderData"/> rows, so nothing more is needed here.
|
|
/// </summary>
|
|
public sealed class ClientData : DbBase
|
|
{
|
|
/// <summary>Display name of the client.</summary>
|
|
public string Name { get; set; } = string.Empty;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Persisted shape of an <see cref="Order"/>: data only. The object graph is
|
|
/// flattened for storage — the back-reference to the client becomes a foreign
|
|
/// key, because storage has ids, not object references.
|
|
/// </summary>
|
|
public sealed class OrderData : DbBase
|
|
{
|
|
/// <summary>Free-text description of what this order is for.</summary>
|
|
public string Description { get; set; } = string.Empty;
|
|
|
|
/// <summary>Order total in cents of the stored order.</summary>
|
|
public int TotalInCents { get; set; }
|
|
|
|
/// <summary>
|
|
/// Foreign key to the owning client: <see cref="Guid.Empty"/> when the
|
|
/// order has no client, or when its client had no id yet at save time —
|
|
/// save clients before their orders.
|
|
/// </summary>
|
|
public Guid ClientId { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The fake <c>DbContext</c>: per-type tables of data records, mirroring EF
|
|
/// Core's <c>Set<T></c>. <see cref="Save(Client)"/> / <see cref="Save(Order)"/>
|
|
/// map the domain object's *current* state into its record — inserting on the
|
|
/// first save, refreshing in place on later ones.
|
|
/// </summary>
|
|
public class DbContext
|
|
{
|
|
// ── per-type data tables ─────────────────────────────────────────
|
|
private readonly List<ClientData> _clients = new();
|
|
private readonly List<OrderData> _orders = new();
|
|
|
|
// ── fast lookup: domain object reference → its persisted record ──
|
|
private readonly Dictionary<object, DbBase> _byRef = new();
|
|
|
|
// ── exposed per-type tables (read-only view) ─────────────────────
|
|
public IReadOnlyList<ClientData> Clients => _clients.AsReadOnly();
|
|
public IReadOnlyList<OrderData> Orders => _orders.AsReadOnly();
|
|
|
|
/// <summary>Generic accessor: the strongly-typed "table" for <typeparamref name="T"/>.</summary>
|
|
public IReadOnlyList<T> Set<T>() where T : DbBase
|
|
=> typeof(T) switch
|
|
{
|
|
var t when t == typeof(ClientData) => (IReadOnlyList<T>)_clients.AsReadOnly(),
|
|
var t when t == typeof(OrderData) => (IReadOnlyList<T>)_orders.AsReadOnly(),
|
|
_ => throw new NotSupportedException($"No per-type table for {typeof(T)}"),
|
|
};
|
|
|
|
// ── explicit persistence: map + insert, or refresh ───────────────
|
|
|
|
/// <summary>
|
|
/// Persist a <see cref="Client"/>: the first save maps it to a fresh
|
|
/// <see cref="ClientData"/> row and writes the generated id back into the
|
|
/// domain object; a later save refreshes that row's data in place.
|
|
/// </summary>
|
|
public void Save(Client client)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(client);
|
|
var fresh = Mappers.ToClientData(client);
|
|
if (_byRef.TryGetValue(client, out var existing))
|
|
{
|
|
((ClientData)existing).Name = fresh.Name; // refresh, keep row identity
|
|
return;
|
|
}
|
|
var id = Guid.NewGuid();
|
|
fresh.Id = client.Id = id; // write-back identity
|
|
fresh.DbContext = this;
|
|
_clients.Add(fresh);
|
|
_byRef[client] = fresh;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Persist an <see cref="Order"/> (same insert-or-refresh rule as
|
|
/// <see cref="Save(Client)"/>; save its client first so the foreign key
|
|
/// can be filled).
|
|
/// </summary>
|
|
public void Save(Order order)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(order);
|
|
var fresh = Mappers.ToOrderData(order);
|
|
if (_byRef.TryGetValue(order, out var existing))
|
|
{
|
|
var row = (OrderData)existing; // refresh, keep row identity
|
|
row.Description = fresh.Description;
|
|
row.TotalInCents = fresh.TotalInCents;
|
|
row.ClientId = fresh.ClientId;
|
|
return;
|
|
}
|
|
var id = Guid.NewGuid();
|
|
fresh.Id = order.Id = id; // write-back identity
|
|
fresh.DbContext = this;
|
|
_orders.Add(fresh);
|
|
_byRef[order] = fresh;
|
|
}
|
|
|
|
// ── query helpers ───────────────────────────────────────────────
|
|
|
|
/// <summary>Search the per-type table for a record whose predicate matches.</summary>
|
|
public T? Find<T>(Func<T, bool> predicate) where T : DbBase
|
|
=> Set<T>().FirstOrDefault(predicate);
|
|
|
|
// ── tracking / status ───────────────────────────────────────────
|
|
|
|
/// <summary>True if <paramref name="entity"/> has been saved through this context.</summary>
|
|
public bool IsTracked(object entity) => _byRef.ContainsKey(entity);
|
|
|
|
/// <summary>All data records this context currently manages.</summary>
|
|
public IReadOnlyList<DbBase> Tracked => _byRef.Values.ToList();
|
|
}
|