Throw ObjectNotFoundException from FindRequired; add run-console wrappers
- DbBase.FindRequired / Client.FindByNameRequired now throw the domain-specific ObjectNotFoundException (NHibernate lineage; Rails equivalent: ActiveRecord::RecordNotFound) on a lookup miss; the no-context case stays InvalidOperationException (configuration error) - Console demo catches the new type; tests (18)/(21) assert it, (19)/(23) keep InvalidOperationException with rationale comments - scripts/run-console.sh delegates to dotnet.sh; run-console.ps1 is the Windows/PowerShell counterpart (mise-first, repo-root cd) Yak: objectnotfoundexception-run-console-wrappers-3p8b
This commit is contained in:
@@ -26,6 +26,16 @@ agent/sandbox shells, and SDK 10 additionally fails without
|
|||||||
scripts/dotnet.sh build db-subclass-to-dto.sln
|
scripts/dotnet.sh build db-subclass-to-dto.sln
|
||||||
scripts/dotnet.sh test db-subclass-to-dto.sln
|
scripts/dotnet.sh test db-subclass-to-dto.sln
|
||||||
scripts/run-tests.sh # full test suite, from repo root
|
scripts/run-tests.sh # full test suite, from repo root
|
||||||
|
scripts/run-console.sh # run the Before.Console demo (bash; delegates
|
||||||
|
# to dotnet.sh); app args go after `--`
|
||||||
|
```
|
||||||
|
|
||||||
|
Windows/PowerShell counterpart:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
scripts\run-console.ps1 # run the Before.Console demo; app args go
|
||||||
|
# directly (the script inserts `--` for
|
||||||
|
# dotnet run); needs no DOTNET_CLI_HOME setup
|
||||||
```
|
```
|
||||||
|
|
||||||
The wrappers cd to the repo root (the test runner must run from the parent
|
The wrappers cd to the repo root (the test runner must run from the parent
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# Run the Before.Console demo app (src/Before.Console/Program.cs) on Windows.
|
||||||
|
#
|
||||||
|
# Windows counterpart of scripts/run-console.sh:
|
||||||
|
# - runs from the repo root, like the other scripts
|
||||||
|
# - prefers mise (see mise.toml) and falls back to a dotnet on PATH
|
||||||
|
#
|
||||||
|
# Troubleshooting: if dotnet fails with "The user's home directory could not
|
||||||
|
# be determined" (some sandboxed shells), set DOTNET_CLI_HOME first:
|
||||||
|
# PS> $env:DOTNET_CLI_HOME = "$PWD\.dotnet-cli"
|
||||||
|
#
|
||||||
|
# Usage (from anywhere):
|
||||||
|
# PS> .\scripts\run-console.ps1 # no args
|
||||||
|
# PS> .\scripts\run-console.ps1 --verbose # args go to the program
|
||||||
|
# The script inserts the `--` separator for dotnet run itself.
|
||||||
|
#
|
||||||
|
# If script execution is blocked by policy:
|
||||||
|
# PS> powershell -ExecutionPolicy Bypass -File .\scripts\run-console.ps1
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||||
|
Push-Location $repoRoot
|
||||||
|
try {
|
||||||
|
$dotnetArgs = @('run', '--project', 'src/Before.Console')
|
||||||
|
if ($args.Count -gt 0) {
|
||||||
|
$dotnetArgs += '--'
|
||||||
|
$dotnetArgs += $args
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Get-Command mise -ErrorAction SilentlyContinue) {
|
||||||
|
mise exec -- dotnet @dotnetArgs
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
dotnet @dotnetArgs
|
||||||
|
}
|
||||||
|
exit $LASTEXITCODE
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
Executable
+10
@@ -0,0 +1,10 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Run the Before.Console demo app (src/Before.Console/Program.cs).
|
||||||
|
# Delegates to scripts/dotnet.sh — see there for why the wrapper exists
|
||||||
|
# (mise-installed dotnet, DOTNET_CLI_HOME, run from repo root).
|
||||||
|
#
|
||||||
|
# Usage: scripts/run-console.sh [app args]
|
||||||
|
# scripts/run-console.sh # no args
|
||||||
|
# scripts/run-console.sh -- --verbose # args are passed to the program
|
||||||
|
set -euo pipefail
|
||||||
|
exec "$(cd "$(dirname "$0")" && pwd)/dotnet.sh" run --project src/Before.Console -- "$@"
|
||||||
@@ -44,9 +44,6 @@ foreach (var client in DbBase.Context!.Tracked.OfType<Client>())
|
|||||||
// ----- find by name -------------------------------------------------------
|
// ----- find by name -------------------------------------------------------
|
||||||
Console.WriteLine("\n=== Find by Name (null-returning) ===");
|
Console.WriteLine("\n=== Find by Name (null-returning) ===");
|
||||||
// No context argument: FindByName falls back to the DbBase.Context singleton.
|
// 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
|
|
||||||
// "nobody configured the context".
|
|
||||||
var found = Client.FindByName("Jane Doe");
|
var found = Client.FindByName("Jane Doe");
|
||||||
if (found is not null)
|
if (found is not null)
|
||||||
{
|
{
|
||||||
@@ -66,7 +63,7 @@ try
|
|||||||
Console.WriteLine($"Found: {required.Name} [{required.Id}]");
|
Console.WriteLine($"Found: {required.Name} [{required.Id}]");
|
||||||
Console.WriteLine($" Orders: {required.Orders.Count}");
|
Console.WriteLine($" Orders: {required.Orders.Count}");
|
||||||
}
|
}
|
||||||
catch (InvalidOperationException ex)
|
catch (ObjectNotFoundException ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Not found: {ex.Message}");
|
Console.WriteLine($"Not found: {ex.Message}");
|
||||||
}
|
}
|
||||||
@@ -79,7 +76,7 @@ try
|
|||||||
{
|
{
|
||||||
Client.FindByNameRequired("Nobody Here");
|
Client.FindByNameRequired("Nobody Here");
|
||||||
}
|
}
|
||||||
catch (InvalidOperationException ex)
|
catch (ObjectNotFoundException ex)
|
||||||
{
|
{
|
||||||
var msg = ex.Message.ReplaceLineEndings(" ").Trim();
|
var msg = ex.Message.ReplaceLineEndings(" ").Trim();
|
||||||
Console.WriteLine($"FindByNameRequired(\"Nobody Here\"): throws - {msg}");
|
Console.WriteLine($"FindByNameRequired(\"Nobody Here\"): throws - {msg}");
|
||||||
|
|||||||
@@ -43,9 +43,10 @@ public class Client : DbBase
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Throwing variant of <see cref="FindByName(string,DbContext?)"/>. Returns
|
/// Throwing variant of <see cref="FindByName(string,DbContext?)"/>. Returns
|
||||||
/// the matching <see cref="Client"/> or throws <see cref="InvalidOperationException"/>
|
/// the matching <see cref="Client"/> or throws <see cref="ObjectNotFoundException"/>
|
||||||
/// with a message that includes the searched <paramref name="name"/> and
|
/// with a message that includes the searched <paramref name="name"/>.
|
||||||
/// whether a context was configured.
|
/// Throws <see cref="InvalidOperationException"/> instead when no context at all
|
||||||
|
/// is configured (a configuration error, not a lookup miss).
|
||||||
///
|
///
|
||||||
/// Delegates to <see cref="DbBase.FindRequired{T}"/>.
|
/// Delegates to <see cref="DbBase.FindRequired{T}"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -53,8 +54,11 @@ public class Client : DbBase
|
|||||||
/// <param name="db">The context whose tracked entities to search. When
|
/// <param name="db">The context whose tracked entities to search. When
|
||||||
/// <c>null</c>, falls back to the <see cref="DbBase.Context"/> singleton.</param>
|
/// <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>
|
/// <returns>The first client whose <see cref="Client.Name"/> equals <paramref name="name"/>.</returns>
|
||||||
|
/// <exception cref="ObjectNotFoundException">
|
||||||
|
/// Thrown when no matching client is found.
|
||||||
|
/// </exception>
|
||||||
/// <exception cref="InvalidOperationException">
|
/// <exception cref="InvalidOperationException">
|
||||||
/// Thrown when no matching client is found or no context is configured.
|
/// Thrown when no context is configured (neither passed explicitly nor set as the singleton).
|
||||||
/// </exception>
|
/// </exception>
|
||||||
public static Client FindByNameRequired(string name, DbContext? db = null)
|
public static Client FindByNameRequired(string name, DbContext? db = null)
|
||||||
=> DbBase.FindRequired<Client>(c => c.Name == name, db, $"name == \"{name}\"");
|
=> DbBase.FindRequired<Client>(c => c.Name == name, db, $"name == \"{name}\"");
|
||||||
|
|||||||
@@ -86,8 +86,9 @@ public class DbBase
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Throwing variant of <see cref="Find{T}(System.Predicate{T},DbContext?)"/>. Finds the first entity
|
/// Throwing variant of <see cref="Find{T}(System.Predicate{T},DbContext?)"/>. Finds the first entity
|
||||||
/// whose runtime type matches <typeparamref name="T"/> and satisfies
|
/// whose runtime type matches <typeparamref name="T"/> and satisfies
|
||||||
/// <paramref name="predicate"/>. Throws <see cref="InvalidOperationException"/>
|
/// <paramref name="predicate"/>. Throws <see cref="ObjectNotFoundException"/>
|
||||||
/// when no match is found or when no <see cref="Context"/> singleton is configured.
|
/// when no match is found, and <see cref="InvalidOperationException"/> when no
|
||||||
|
/// <see cref="Context"/> singleton is configured (a configuration error, not a lookup miss).
|
||||||
///
|
///
|
||||||
/// The exception message includes <paramref name="predicateToString"/> so
|
/// The exception message includes <paramref name="predicateToString"/> so
|
||||||
/// callers can debug which lookup failed.
|
/// callers can debug which lookup failed.
|
||||||
@@ -98,8 +99,11 @@ public class DbBase
|
|||||||
/// <param name="predicateToString">A human-readable description of the predicate, used in the
|
/// <param name="predicateToString">A human-readable description of the predicate, used in the
|
||||||
/// exception message when the search fails.</param>
|
/// exception message when the search fails.</param>
|
||||||
/// <returns>The first matching entity.</returns>
|
/// <returns>The first matching entity.</returns>
|
||||||
|
/// <exception cref="ObjectNotFoundException">
|
||||||
|
/// Thrown when no entity matches <paramref name="predicate"/>.
|
||||||
|
/// </exception>
|
||||||
/// <exception cref="InvalidOperationException">
|
/// <exception cref="InvalidOperationException">
|
||||||
/// Thrown when no entity matches <paramref name="predicate"/> or when no context is available.
|
/// Thrown when no context is available (neither passed explicitly nor set as the singleton).
|
||||||
/// </exception>
|
/// </exception>
|
||||||
public static T FindRequired<T>(Predicate<T> predicate, DbContext? db, string predicateToString)
|
public static T FindRequired<T>(Predicate<T> predicate, DbContext? db, string predicateToString)
|
||||||
where T : DbBase
|
where T : DbBase
|
||||||
@@ -117,7 +121,7 @@ public class DbBase
|
|||||||
if (e is T candidate && predicate(candidate))
|
if (e is T candidate && predicate(candidate))
|
||||||
return candidate;
|
return candidate;
|
||||||
|
|
||||||
throw new InvalidOperationException(
|
throw new ObjectNotFoundException(
|
||||||
$"FindRequired<{typeof(T).Name}>({predicateToString}) — " +
|
$"FindRequired<{typeof(T).Name}>({predicateToString}) — " +
|
||||||
$"no matching {typeof(T).Name} found in context.");
|
$"no matching {typeof(T).Name} found in context.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace Before;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Thrown when a "required" ActiveRecord-style lookup —
|
||||||
|
/// <see cref="DbBase.FindRequired{T}"/> / <see cref="Client.FindByNameRequired"/> —
|
||||||
|
/// finds no matching entity in the context.
|
||||||
|
///
|
||||||
|
/// Named after NHibernate's <c>ObjectNotFoundException</c> (Rails' ActiveRecord
|
||||||
|
/// raises <c>ActiveRecord::RecordNotFound</c> for the same situation), so a
|
||||||
|
/// lookup miss is distinguishable from unrelated
|
||||||
|
/// <see cref="InvalidOperationException"/>s such as EF's <c>Single()</c>
|
||||||
|
/// "Sequence contains no elements".
|
||||||
|
/// </summary>
|
||||||
|
public class ObjectNotFoundException : Exception
|
||||||
|
{
|
||||||
|
public ObjectNotFoundException(string message) : base(message) { }
|
||||||
|
}
|
||||||
@@ -358,7 +358,8 @@ public class BeforeTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
// (18) DbBase.FindRequired<T> throws when no match is found.
|
// (18) DbBase.FindRequired<T> throws ObjectNotFoundException when no match
|
||||||
|
// is found (the domain-specific lookup miss, not a BCL exception).
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DbBase_FindRequired_T_throws_when_no_match()
|
public void DbBase_FindRequired_T_throws_when_no_match()
|
||||||
@@ -366,7 +367,7 @@ public class BeforeTests
|
|||||||
var db = new DbContext();
|
var db = new DbContext();
|
||||||
db.Attach(new Client { Name = "Acme" });
|
db.Attach(new Client { Name = "Acme" });
|
||||||
|
|
||||||
var ex = Assert.Throws<InvalidOperationException>(() =>
|
var ex = Assert.Throws<ObjectNotFoundException>(() =>
|
||||||
DbBase.FindRequired<Client>(c => c.Name == "Nobody", db, "name == Nobody"));
|
DbBase.FindRequired<Client>(c => c.Name == "Nobody", db, "name == Nobody"));
|
||||||
|
|
||||||
Assert.Contains("Nobody", ex.Message);
|
Assert.Contains("Nobody", ex.Message);
|
||||||
@@ -376,6 +377,9 @@ public class BeforeTests
|
|||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
// (19) DbBase.FindRequired<T> throws with a useful message when no context is
|
// (19) DbBase.FindRequired<T> throws with a useful message when no context is
|
||||||
// configured (both explicit null and singleton null).
|
// configured (both explicit null and singleton null).
|
||||||
|
//
|
||||||
|
// Deliberately InvalidOperationException, not ObjectNotFoundException:
|
||||||
|
// a missing context is a configuration error, not a lookup miss.
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DbBase_FindRequired_T_throws_when_no_context_configured()
|
public void DbBase_FindRequired_T_throws_when_no_context_configured()
|
||||||
@@ -405,7 +409,8 @@ public class BeforeTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
// (21) Client.FindByNameRequired throws when no match — message includes name.
|
// (21) Client.FindByNameRequired throws ObjectNotFoundException when no
|
||||||
|
// match — message includes name.
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Client_FindByNameRequired_throws_with_name_when_not_found()
|
public void Client_FindByNameRequired_throws_with_name_when_not_found()
|
||||||
@@ -413,7 +418,7 @@ public class BeforeTests
|
|||||||
var db = new DbContext();
|
var db = new DbContext();
|
||||||
db.Attach(new Client { Name = "Acme Corp" });
|
db.Attach(new Client { Name = "Acme Corp" });
|
||||||
|
|
||||||
var ex = Assert.Throws<InvalidOperationException>(() =>
|
var ex = Assert.Throws<ObjectNotFoundException>(() =>
|
||||||
Client.FindByNameRequired("Nobody", db));
|
Client.FindByNameRequired("Nobody", db));
|
||||||
|
|
||||||
Assert.Contains("Nobody", ex.Message);
|
Assert.Contains("Nobody", ex.Message);
|
||||||
@@ -445,6 +450,7 @@ public class BeforeTests
|
|||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
// (23) Client.FindByNameRequired throws with a useful message when the
|
// (23) Client.FindByNameRequired throws with a useful message when the
|
||||||
// singleton is not configured (no arg, no singleton).
|
// singleton is not configured (no arg, no singleton).
|
||||||
|
// Stays InvalidOperationException: configuration error, not a miss.
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Client_FindByNameRequired_throws_with_context_hint_when_singleton_null()
|
public void Client_FindByNameRequired_throws_with_context_hint_when_singleton_null()
|
||||||
|
|||||||
Reference in New Issue
Block a user