Add FindByNameRequired throwing variant (yak: Add FindByName! throwing variant)
This commit is contained in:
@@ -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}");
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
@@ -339,4 +339,122 @@ public class BeforeTests
|
||||
|
||||
DbBase.Context = null; // cleanup
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// (17) DbBase.FindRequired<T> finds the matching entity — same result as Find<T>.
|
||||
// ---------------------------------------------------------------------
|
||||
[Fact]
|
||||
public void DbBase_FindRequired_T_finds_matching_entity()
|
||||
{
|
||||
var db = new DbContext();
|
||||
var acme = new Client { Name = "Acme" };
|
||||
var globex = new Client { Name = "Globex" };
|
||||
db.Attach(acme);
|
||||
db.Attach(globex);
|
||||
|
||||
var result = DbBase.FindRequired<Client>(c => c.Name == "Globex", db, "name == Globex");
|
||||
|
||||
Assert.Same(globex, result);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// (18) DbBase.FindRequired<T> throws when no match is found.
|
||||
// ---------------------------------------------------------------------
|
||||
[Fact]
|
||||
public void DbBase_FindRequired_T_throws_when_no_match()
|
||||
{
|
||||
var db = new DbContext();
|
||||
db.Attach(new Client { Name = "Acme" });
|
||||
|
||||
var ex = Assert.Throws<InvalidOperationException>(() =>
|
||||
DbBase.FindRequired<Client>(c => c.Name == "Nobody", db, "name == Nobody"));
|
||||
|
||||
Assert.Contains("Nobody", ex.Message);
|
||||
Assert.Contains("no matching Client found", ex.Message);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// (19) DbBase.FindRequired<T> throws with a useful message when no context is
|
||||
// configured (both explicit null and singleton null).
|
||||
// ---------------------------------------------------------------------
|
||||
[Fact]
|
||||
public void DbBase_FindRequired_T_throws_when_no_context_configured()
|
||||
{
|
||||
DbBase.Context = null;
|
||||
|
||||
var ex = Assert.Throws<InvalidOperationException>(() =>
|
||||
DbBase.FindRequired<Client>(_ => true, null, "true"));
|
||||
|
||||
Assert.Contains("no DbContext configured", ex.Message);
|
||||
Assert.Contains("DbBase.Context", ex.Message);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// (20) Client.FindByNameRequired finds the matching client by name.
|
||||
// ---------------------------------------------------------------------
|
||||
[Fact]
|
||||
public void Client_FindByNameRequired_finds_by_name()
|
||||
{
|
||||
var db = new DbContext();
|
||||
var acme = new Client { Name = "Acme Corp" };
|
||||
db.Attach(acme);
|
||||
|
||||
var result = Client.FindByNameRequired("Acme Corp", db);
|
||||
|
||||
Assert.Same(acme, result);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// (21) Client.FindByNameRequired throws when no match — message includes name.
|
||||
// ---------------------------------------------------------------------
|
||||
[Fact]
|
||||
public void Client_FindByNameRequired_throws_with_name_when_not_found()
|
||||
{
|
||||
var db = new DbContext();
|
||||
db.Attach(new Client { Name = "Acme Corp" });
|
||||
|
||||
var ex = Assert.Throws<InvalidOperationException>(() =>
|
||||
Client.FindByNameRequired("Nobody", db));
|
||||
|
||||
Assert.Contains("Nobody", ex.Message);
|
||||
Assert.Contains("no matching Client found", ex.Message);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// (22) Client.FindByNameRequired uses Context singleton when no context arg.
|
||||
// ---------------------------------------------------------------------
|
||||
[Fact]
|
||||
public void Client_FindByNameRequired_uses_Context_singleton()
|
||||
{
|
||||
var db = new DbContext();
|
||||
var acme = new Client { Name = "SingletonCo" };
|
||||
db.Attach(acme);
|
||||
|
||||
DbBase.Context = db;
|
||||
try
|
||||
{
|
||||
var result = Client.FindByNameRequired("SingletonCo");
|
||||
Assert.Same(acme, result);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DbBase.Context = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// (23) Client.FindByNameRequired throws with a useful message when the
|
||||
// singleton is not configured (no arg, no singleton).
|
||||
// ---------------------------------------------------------------------
|
||||
[Fact]
|
||||
public void Client_FindByNameRequired_throws_with_context_hint_when_singleton_null()
|
||||
{
|
||||
DbBase.Context = null;
|
||||
|
||||
var ex = Assert.Throws<InvalidOperationException>(() =>
|
||||
Client.FindByNameRequired("SomeBody"));
|
||||
|
||||
Assert.Contains("no DbContext configured", ex.Message);
|
||||
Assert.Contains("DbBase.Context", ex.Message);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user