Add FindByNameRequired throwing variant (yak: Add FindByName! throwing variant)

This commit is contained in:
2026-09-14 21:23:57 +01:00
parent 325925a592
commit 4b6e6f5a00
4 changed files with 203 additions and 1 deletions
+28 -1
View File
@@ -42,7 +42,7 @@ foreach (var client in DbBase.Context!.Tracked.OfType<Client>())
}
// ----- find by name -------------------------------------------------------
Console.WriteLine("\n=== Find by Name ===");
Console.WriteLine("\n=== Find by Name (null-returning) ===");
// No context argument: FindByName falls back to the DbBase.Context singleton.
// This is the ActiveRecord ergonomic — call sites don't carry the context,
// but the null that comes back on a miss can't tell "no such client" from
@@ -57,3 +57,30 @@ else
{
Console.WriteLine("Not found.");
}
// ----- find by name (throwing variant) ------------------------------------
Console.WriteLine("\n=== Find by Name Required (throws instead of returning null) ===");
try
{
var required = Client.FindByNameRequired("Jane Doe");
Console.WriteLine($"Found: {required.Name} [{required.Id}]");
Console.WriteLine($" Orders: {required.Orders.Count}");
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"Not found: {ex.Message}");
}
// Show the contrast with a non-existent name:
Console.WriteLine("\n=== Miss: null-returning vs throwing ===");
var missNull = Client.FindByName("Nobody Here");
Console.WriteLine($"FindByName(\"Nobody Here\"): {(missNull == null ? "null" : missNull.Name)}");
try
{
Client.FindByNameRequired("Nobody Here");
}
catch (InvalidOperationException ex)
{
var msg = ex.Message.ReplaceLineEndings(" ").Trim();
Console.WriteLine($"FindByNameRequired(\"Nobody Here\"): throws - {msg}");
}
+18
View File
@@ -40,6 +40,24 @@ public class Client : DbBase
/// </summary>
public static Client? FindByName(string name, DbContext? db = null)
=> DbBase.Find<Client>(c => c.Name == name, db);
/// <summary>
/// Throwing variant of <see cref="FindByName(string,DbContext?)"/>. Returns
/// the matching <see cref="Client"/> or throws <see cref="InvalidOperationException"/>
/// with a message that includes the searched <paramref name="name"/> and
/// whether a context was configured.
///
/// Delegates to <see cref="DbBase.FindRequired{T}"/>.
/// </summary>
/// <param name="name">The client name to search for.</param>
/// <param name="db">The context whose tracked entities to search. When
/// <c>null</c>, falls back to the <see cref="DbBase.Context"/> singleton.</param>
/// <returns>The first client whose <see cref="Client.Name"/> equals <paramref name="name"/>.</returns>
/// <exception cref="InvalidOperationException">
/// Thrown when no matching client is found or no context is configured.
/// </exception>
public static Client FindByNameRequired(string name, DbContext? db = null)
=> DbBase.FindRequired<Client>(c => c.Name == name, db, $"name == \"{name}\"");
}
/// <summary>
+39
View File
@@ -82,4 +82,43 @@ public class DbBase
return default;
}
/// <summary>
/// Throwing variant of <see cref="Find{T}(System.Predicate{T},DbContext?)"/>. Finds the first entity
/// whose runtime type matches <typeparamref name="T"/> and satisfies
/// <paramref name="predicate"/>. Throws <see cref="InvalidOperationException"/>
/// when no match is found or when no <see cref="Context"/> singleton is configured.
///
/// The exception message includes <paramref name="predicateToString"/> so
/// callers can debug which lookup failed.
/// </summary>
/// <param name="predicate">Filter applied to candidates of type <typeparamref name="T"/>.</param>
/// <param name="db">The context whose tracked entities to search. When <c>null</c>, falls back
/// to the <see cref="Context"/> singleton.</param>
/// <param name="predicateToString">A human-readable description of the predicate, used in the
/// exception message when the search fails.</param>
/// <returns>The first matching entity.</returns>
/// <exception cref="InvalidOperationException">
/// Thrown when no entity matches <paramref name="predicate"/> or when no context is available.
/// </exception>
public static T FindRequired<T>(Predicate<T> predicate, DbContext? db, string predicateToString)
where T : DbBase
{
db ??= Context;
if (db is null)
throw new InvalidOperationException(
$"Cannot perform FindRequired<{typeof(T).Name}>: " +
$"no DbContext configured (neither passed explicitly nor set as " +
$"DbBase.Context singleton). Set DbBase.Context before calling " +
$"FindRequired<{typeof(T).Name}>.");
foreach (var e in db.Tracked)
if (e is T candidate && predicate(candidate))
return candidate;
throw new InvalidOperationException(
$"FindRequired<{typeof(T).Name}>({predicateToString}) — " +
$"no matching {typeof(T).Name} found in context.");
}
}