Exception handling
When you send a command or query through IRequestDispatcher (MediatR), you normally get back a Result or Result<T> from CodeBlock DevKit. You should treat Result.IsSuccess and Result.Errors as the stable contract for callers (Blazor, API controllers, application services).
If something goes wrong while the pipeline runs:
DomainException,ApplicationException, andValidationException(all managed exceptions) are caught by the DevKit’s managed exception handler. It turns the exception into one or more human-readable strings (using resource keys when you used the localized constructors) and pushes them into anINotificationService. The pipeline is told the exception was handled, so it does not crash the process.- Any other exception (unmanaged—for example
InvalidOperationException,NullReferenceException, or bugs) is caught by the unmanaged exception handler. It logs the real exception for you and still adds a safe, localized generic message to the same notification list so clients never see raw stack traces in theResult.
After MediatR finishes, MediatRDispatcher reads INotificationService.GetListAndReset() and, whenever there are messages or the handler returned a failure state, returns Result.Failure(...) with those strings in Errors. So whether the failure was managed or unmanaged, the caller can use the same pattern: if (!result.IsSuccess) { … result.Errors … }.
API hosts also include global HTTP middleware that returns a consistent JSON error for anything that escapes the normal pipeline entirely (true last resort).
Request flow: from caller to Result
The diagram below is the typical vertical slice when a user (or any HTTP client) calls your Web API, an application service builds a command or query model, and IRequestDispatcher runs the MediatR pipeline. Exceptions never leak out of the dispatcher as raw exceptions to the application service: they are handled inside the pipeline, turned into messages (and logged on unmanaged paths), and surfaced as Result.Failure with strings in Errors—same as Result.Success for the happy path.
flowchart TD
U[User or HTTP client] --> API[Web API host]
API --> ASVC[Application service]
ASVC --> MODEL[Create use case request model command or query]
MODEL --> DISP["IRequestDispatcher Send*"]
DISP --> VAL{Request passes annotation and FluentValidation?}
VAL -->|No| VEX[ValidationException — handler not run]
VAL -->|Yes| HAND[Use case handler runs]
HAND --> DOM[Domain aggregate entity or domain service]
DOM -->|Expected failure| DEX[DomainException]
DOM -->|Unexpected| OEX[Other exception]
HAND -->|Expected failure| AEX[ApplicationException]
HAND -->|Unexpected| OEX
VEX --> EH[DevKit pipeline exception handlers — managed vs unmanaged]
DEX --> EH
AEX --> EH
OEX --> EH
EH --> FAIL["Result.Failure — MediatRDispatcher fills Errors from INotificationService; unmanaged exceptions are logged in full"]
HAND -->|Completes without throwing| OK["Result.Success"]
FAIL --> OUT[Application service receives Result]
OK --> OUT
OUT --> RESP[API maps Result to HTTP response]
RESP --> U
Reading the chart: ValidationException is raised when validation fails before the handler runs. DomainException and ApplicationException are managed failures you throw from domain or application code; anything else is treated as unmanaged (logged, with a safe message for the client). In all failure cases the caller still receives a Result and should branch on IsSuccess and Errors—the same pattern from API down to Blazor.
Base managed exceptions (ManagedException, ValidationException)
ManagedException is the abstract base for localized and plain-string managed errors. ValidationException is thrown by the validation pipeline when data annotations or FluentValidation fail, before your handler runs.
ManagedException.cs
- Namespace:
CodeBlock.DevKit.Core.Exceptions(includesMessagePlaceholderandMessagePlaceholderType)
// Copyright (c) CodeBlock.Dev. All rights reserved.
// For more information visit https://codeblock.dev
using CodeBlock.DevKit.Core.Extensions;
namespace CodeBlock.DevKit.Core.Exceptions;
/// <summary>
/// Base exception class for managed application errors with support for localized messages.
/// Provides structured error handling with resource keys and placeholder values for internationalization.
/// </summary>
public abstract class ManagedException : Exception
{
public string MessageResourceKey { get; }
public Type MessageResourceType { get; }
public IEnumerable<MessagePlaceholder> MessagePlaceholders { get; }
public ManagedException(string messageResourceKey, Type messageResourceType, IEnumerable<MessagePlaceholder> messagePlaceholders = null)
: base()
{
MessageResourceKey = messageResourceKey;
MessageResourceType = messageResourceType;
MessagePlaceholders = messagePlaceholders ?? new List<MessagePlaceholder>();
}
public ManagedException()
{
MessagePlaceholders = new List<MessagePlaceholder>();
}
public ManagedException(string message)
: base(message)
{
MessagePlaceholders = new List<MessagePlaceholder>();
}
public bool HasResourceMessage()
{
return !MessageResourceKey.IsNullOrEmptyOrWhiteSpace() && MessageResourceType != null;
}
}
/// <summary>
/// Represents a placeholder value for exception messages, supporting both plain text and localized resources.
/// Used to dynamically insert values into localized error messages.
/// </summary>
public class MessagePlaceholder
{
private MessagePlaceholder(string plainText)
{
Type = MessagePlaceholderType.PlainText;
PlainText = plainText;
}
private MessagePlaceholder(string resourceKey, Type resourceType)
{
Type = MessagePlaceholderType.Resource;
ResourceKey = resourceKey;
ResourceType = resourceType;
}
public static MessagePlaceholder CreatePlainText(string plainText)
{
return new MessagePlaceholder(plainText);
}
public static MessagePlaceholder CreateResource(string resourceKey, Type resourceType)
{
return new MessagePlaceholder(resourceKey, resourceType);
}
public MessagePlaceholderType Type { get; }
public string ResourceKey { get; }
public Type ResourceType { get; }
public string PlainText { get; }
}
/// <summary>
/// Defines the type of placeholder value in exception messages.
/// </summary>
public enum MessagePlaceholderType
{
Resource,
PlainText,
}
ValidationException.cs
- Namespace:
CodeBlock.DevKit.Application.Exceptions
// Copyright (c) CodeBlock.Dev. All rights reserved.
// For more information visit https://codeblock.dev
using CodeBlock.DevKit.Core.Exceptions;
namespace CodeBlock.DevKit.Application.Exceptions;
/// <summary>
/// Exception thrown when validation errors occur during command or query processing.
/// Extends ManagedException to provide structured error handling for validation failures.
/// </summary>
public class ValidationException : ManagedException
{
#region Ctors
/// <summary>
/// Initializes a new instance of ValidationException.
/// </summary>
public ValidationException()
: base() { }
#endregion
}
DomainException and ApplicationException
Throw these for expected failures: broken invariants (domain) or application rules such as “not found” (application). Both extend ManagedException and offer the same two constructor shapes: localized (resource key + resource type + optional placeholders) or plain string.
DomainException (CodeBlock DevKit):
// Copyright (c) CodeBlock.Dev. All rights reserved.
// For more information visit https://codeblock.dev
using CodeBlock.DevKit.Core.Exceptions;
namespace CodeBlock.DevKit.Domain.Exceptions;
/// <summary>
/// Exception thrown when domain business rules or invariants are violated.
/// Extends ManagedException to provide localized error messages and structured error handling.
/// </summary>
public class DomainException : ManagedException
{
/// <summary>
/// Initializes a new instance of DomainException with localized message support.
/// </summary>
/// <param name="messageResourceKey">Resource key for the localized error message.</param>
/// <param name="messageResourceType">Type containing the resource definitions.</param>
/// <param name="messagePlaceholders">Optional placeholders for dynamic message content.</param>
public DomainException(string messageResourceKey, Type messageResourceType, IEnumerable<MessagePlaceholder> messagePlaceholders = null)
: base(messageResourceKey, messageResourceType, messagePlaceholders) { }
/// <summary>
/// Initializes a new instance of DomainException with a plain text message.
/// </summary>
/// <param name="message">Plain text error message.</param>
public DomainException(string message)
: base(message) { }
}
ApplicationException (DevKit application namespace—not System.ApplicationException):
// Copyright (c) CodeBlock.Dev. All rights reserved.
// For more information visit https://codeblock.dev
using CodeBlock.DevKit.Core.Exceptions;
namespace CodeBlock.DevKit.Application.Exceptions;
/// <summary>
/// Exception thrown when application-level errors occur during command or query processing.
/// Extends ManagedException to provide localized error messages and structured error handling.
/// </summary>
public class ApplicationException : ManagedException
{
/// <summary>
/// Initializes a new instance of ApplicationException with localized message support.
/// </summary>
/// <param name="messageResourceKey">Resource key for the localized error message.</param>
/// <param name="messageResourceType">Type containing the resource definitions.</param>
/// <param name="messagePlaceholders">Optional placeholders for dynamic message content.</param>
public ApplicationException(string messageResourceKey, Type messageResourceType, IEnumerable<MessagePlaceholder> messagePlaceholders = null)
: base(messageResourceKey, messageResourceType, messagePlaceholders) { }
/// <summary>
/// Initializes a new instance of ApplicationException with a plain text message.
/// </summary>
/// <param name="message">Plain text error message.</param>
public ApplicationException(string message)
: base(message) { }
}
For production code, prefer the resource-based overloads and small static factory types (like the template’s DemoThingDomainExceptions) so messages stay localized and consistent.
Domain rules with DomainException
Keep invariants inside entities or domain services. When a rule fails, throw a DomainException. A minimal pattern:
1. Factories for domain exceptions
using CodeBlock.DevKit.Domain.Exceptions;
internal static class ProductDomainExceptions
{
public static DomainException NameTooLong(int maxLength) =>
new($"Product name cannot exceed {maxLength} characters.");
}
2. Entity enforcing the rule
public sealed class Product
{
public const int MaxNameLength = 100;
public string Name { get; private set; }
public Product(string name) => Rename(name);
public void Rename(string name)
{
if (name.Length > MaxNameLength)
throw ProductDomainExceptions.NameTooLong(MaxNameLength);
Name = name;
}
}
3. Caller reading errors from the application service
The use case that calls new Product(...) does not catch the exception yourself—the DevKit pipeline does. The Result returned by SendCommand (via your application service) carries the message in Errors:
var result = await _demoThingService.CreateDemoThing(input);
if (!result.IsSuccess)
{
foreach (var error in result.Errors)
{
// Bind to UI, return API problem details, log, etc.
}
}
The real template uses the same flow for DemoThings: factories in DemoThingDomainExceptions.cs, rules in DemoThing.cs, and IDemoThingService / DemoThingService returning Result<CommandResult>.
Validation and ValidationException
Before a command handler runs, the DevKit runs a validation behavior on BaseCommand instances. It combines:
- Data annotation validation on the command (and the same attributes work on DTOs you validate in a similar way).
- FluentValidation rules for that command type, resolved from DI.
Every failed rule is appended to INotificationService; then a ValidationException is thrown so the handler does not run. Those messages end up in Result.Errors just like domain and application failures.
Data annotations on DTOs — For example CreateDemoThingDto.cs uses [Required] and localized error resources on Name, Description, and Type. When the application service builds a CreateDemoThingRequest and sends it, the command type CreateDemoThingRequest is also decorated with [Required] attributes so the pipeline validates the actual object MediatR receives.
FluentValidation — Add classes that inherit AbstractValidator<TCommand> (for the same TCommand you send through the dispatcher). Example aligned with the DemoThings create command:
using FluentValidation;
namespace CanBeYours.Application.UseCases.DemoThings.CreateDemoThing;
internal sealed class CreateDemoThingValidation : AbstractValidator<CreateDemoThingRequest>
{
public CreateDemoThingValidation()
{
RuleFor(x => x.Name)
.NotEqual(x => x.Description)
.WithMessage("Name and description must not be identical.");
}
}
See also
- Logging — HTTP and MediatR logging,
[DoNotLog], and sensitive payload markers. - Extensions and helpers —
Resulthelpers and core extensions. - General services —
IRequestDispatcher,INotificationService, and related injectables.