RA.Utilities.Core.Exceptions 10.2.0

Prefix Reserved
dotnet add package RA.Utilities.Core.Exceptions --version 10.2.0
                    
NuGet\Install-Package RA.Utilities.Core.Exceptions -Version 10.2.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="RA.Utilities.Core.Exceptions" Version="10.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="RA.Utilities.Core.Exceptions" Version="10.2.0" />
                    
Directory.Packages.props
<PackageReference Include="RA.Utilities.Core.Exceptions" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add RA.Utilities.Core.Exceptions --version 10.2.0
                    
#r "nuget: RA.Utilities.Core.Exceptions, 10.2.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package RA.Utilities.Core.Exceptions@10.2.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=RA.Utilities.Core.Exceptions&version=10.2.0
                    
Install as a Cake Addin
#tool nuget:?package=RA.Utilities.Core.Exceptions&version=10.2.0
                    
Install as a Cake Tool

RA.Utilities.Core.Exceptions

NuGet version Codecov GitHub license NuGet Downloads Documentation

RA.Utilities.Core.Exceptions provides a set of standardized, semantic exceptions like NotFoundException and ConflictException. It solves the problem of using generic exceptions (e.g., Exception or ArgumentException) for predictable business rule failures.

By throwing exceptions that describe what went wrong (e.g., a resource was not found), you can create cleaner, more maintainable code. This allows other parts of your system, like API middleware, to catch specific exception types and produce standardized, meaningful error responses automatically.

  • Clear Intent: Throwing a NotFoundException is more descriptive than a generic exception with a "not found" message.
  • Standardized Error Handling: Middleware (like in RA.Utilities.Api) can catch these specific exception types and automatically map them to the correct HTTP status codes and structured error responses (e.g., 402 Payment Required, 404 Not Found, 409 Conflict).
  • Decoupled Logic: Your domain or application layer can focus on business rules and throw semantic exceptions without needing to know about HTTP details. The web layer handles the translation.
  • Reduced Boilerplate: Eliminates the need for repetitive try-catch blocks in your controllers for common error scenarios.

Table of Contents

  • Getting started
  • How It Works
  • Available Exceptions
    • NotFoundException
    • ConflictException
    • BadRequestException
    • UnprocessableException
    • UnauthorizedException
    • PaymentRequiredException
    • ForbiddenException
    • GatewayTimeoutException
    • TooManyRequestsException
    • ServiceUnavailableException
  • Best Practices
  • Additional documentation
  • Contributing

Getting started

You can install the package via the .NET CLI:

dotnet add package RA.Utilities.Core.Exceptions

Or through the NuGet Package Manager in Visual Studio.


How It Works

This package is designed to integrate seamlessly with an API's error-handling middleware.

  1. Business Logic: Your service or application layer throws a semantic exception (e.g., NotFoundException) when a business rule is violated.
  2. API Middleware: In your API project (e.g., using RA.Utilities.Api), a global error-handling middleware catches these specific exceptions.
  3. Automatic Mapping: The middleware translates the exception into a standardized HTTP response (e.g., NotFoundException becomes 404 Not Found).

This decouples your business logic from API concerns and standardizes error responses across your application.

Constructors and default messages

Every exception follows the same constructor conventions:

Shape Available on Example
Default All exceptions new NotFoundException()
Message Unauthorized, PaymentRequired, Forbidden, Unprocessable, GatewayTimeout, TooManyRequests, ServiceUnavailable new ForbiddenException("Not the owner.")
Error code + message All exceptions (pass a ValidationError first for BadRequestException) new ForbiddenException(4030, "Not the owner.")
Entity aware NotFoundException, ConflictException new NotFoundException("Product", 99)
Validation aware BadRequestException new BadRequestException(error)

Message parameters are string? message = null. Omitting the argument (or passing null) resolves to the matching BaseResponseMessages value at runtime; an explicit "" is preserved as an empty message.

new NotFoundException().Message;            // "The requested resource was not found."
new NotFoundException(404, null).Message;   // "The requested resource was not found."
new NotFoundException(404, "").Message;     // ""
new NotFoundException("Product", 99).Message; // "Product with value '99' was not found."

Available Exceptions

NotFoundException

Inherits from RaBaseException. Use this when a specific resource or entity cannot be found.

Usage:

public Product GetProductById(int id)
{
    var product = _productRepository.Find(id);
    if (product == null)
    {
        throw new NotFoundException(nameof(Product), id);
    }
    return product;
}

When caught by the RA.Utilities.Api middleware, this will typically be translated into an HTTP 404 Not Found response.

ConflictException

Inherits from RaBaseException. Use this when an action cannot be completed due to a conflict with the current state of a resource, such as trying to create a duplicate item.

Usage:

public void CreateUser(string email)
{
    if (_userRepository.Exists(email))
    {
        throw new ConflictException("User", email);
    }
    // ... creation logic
}

This will typically be translated into an HTTP 409 Conflict response.

BadRequestException

Inherits from RaBaseException. Use this for client-side errors, such as invalid input or validation failures that are discovered in the business layer.

Usage:

public void UpdateOrderStatus(int orderId, string newStatus)
{
    try
    {
        // Assume _validator is a FluentValidation validator
        await _validator.ValidateAndThrowAsync(command);
    }
    catch (ValidationException ex)
    {
        // Convert FluentValidation errors to our custom ValidationError
        var validationErrors = ex.Errors.Select(e => new ValidationError(e.ErrorMessage)
        {
            PropertyName = e.PropertyName,
            AttemptedValue = e.AttemptedValue,
            ErrorCode = e.ErrorCode
        }).ToArray();

        // Throw a failure containing the structured exception
        // highlight-next-line
        throw new BadRequestException(validationErrors);
    }
    // ... update logic
}

This will typically be translated into an HTTP 400 Bad Request response.


UnprocessableException

Inherits from RaBaseException. Use this when an operation cannot be performed due to the current state of a resource. This is for state-based conflicts, not general business rule violations.

Usage:

public async Task<Result> CancelOrderAsync(Guid orderId)
{
    var order = await _orderRepository.GetByIdAsync(orderId);

    if (order == null)
    {
        return new NotFoundException(nameof(Order), orderId);
    }

    // Business Rule: Shipped orders cannot be cancelled.
    if (order.Status == OrderStatus.Shipped)
    {
        return new UnprocessableException(
            errorCode: BaseResponseCode.Unprocessable,
            message: "Cannot cancel an order that has already been shipped."
        );
    }

    order.Status = OrderStatus.Cancelled;
    await _orderRepository.UpdateAsync(order);
    return Result.Success();
}


UnauthorizedException

Inherits from RaBaseException. Use this when a request lacks valid authentication credentials (HTTP 401).

Usage:

public async Task<Result> AuthenticateUserAsync(string token)
{
    if (string.IsNullOrWhiteSpace(token))
    {
        return new UnauthorizedException("No authentication token provided.");
    }

    var isValid = await _authService.ValidateTokenAsync(token);

    if (!isValid)
    {
        return new UnauthorizedException(
            errorCode: 401,
            message: "The provided authentication token is invalid or has expired."
        );
    }

    return Result.Success();
}

PaymentRequiredException

Inherits from RaBaseException. Use this when an authenticated caller must pay before accessing the resource (HTTP 402) — a paywall, an exhausted prepaid balance, or a quota that only a paid plan unlocks.

Usage:

public async Task<Result<Report>> GenerateReportAsync(Guid userId)
{
    var subscription = await _subscriptionService.GetAsync(userId);

    if (subscription.Plan == Plan.Free)
    {
        // The caller is authenticated, but the report requires a paid plan.
        return new PaymentRequiredException("This report is available on paid plans.");
    }

    return await _reportService.GenerateAsync(userId);
}

This will typically be translated into an HTTP 402 Payment Required response.

Do not use it for authentication failures (401), missing permissions (403), or rate limits (429) — those have dedicated exception types.


ForbiddenException

Inherits from RaBaseException. Use this when an authenticated user lacks the necessary permissions to perform an action or access a resource.

Usage:

public async Task<Result> UpdateSensitiveDataAsync(Guid resourceId, User currentUser)
{
    var resource = await _repository.GetByIdAsync(resourceId);

    // Business Rule: Only the 'Owner' or 'SuperAdmin' can edit this specific resource.
    if (resource.OwnerId != currentUser.Id && !currentUser.IsSuperAdmin)
    {
        // The user is logged in (authenticated), but they are FORBIDDEN from this action.
        return new ForbiddenException(
            errorCode: BaseResponseCode.Forbidden, 
            message: "You do not have permission to modify this resource."
        );
    }

    // ... proceed with update
    return Result.Success();
}


GatewayTimeoutException

Inherits from RaBaseException. Use this when the server, acting as a gateway or proxy, did not receive a timely response from an upstream server (HTTP 504).

Usage:

public async Task<Result<Order>> ProcessPaymentAsync(Guid orderId)
{
    using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));

    try
    {
        var paymentResult = await _paymentGateway.ProcessAsync(orderId, cts.Token);
        return paymentResult;
    }
    catch (TaskCanceledException)
    {
        return new GatewayTimeoutException(
            errorCode: 504,
            message: "The payment gateway did not respond in time. Please try again later."
        );
    }
}

TooManyRequestsException

Inherits from RaBaseException. Use this when a client has exceeded a rate limit or throttling threshold (HTTP 429).

Usage:

public async Task<Result> EnforceRateLimitAsync(string clientId)
{
    var requestCount = await _rateLimitStore.GetCountAsync(clientId);

    if (requestCount >= _maxRequestsPerMinute)
    {
        return new TooManyRequestsException(
            errorCode: BaseResponseCode.TooManyRequests,
            message: $"Rate limit exceeded. Maximum {_maxRequestsPerMinute} requests per minute allowed."
        );
    }

    await _rateLimitStore.IncrementAsync(clientId);
    return Result.Success();
}

ServiceUnavailableException

Inherits from RaBaseException. Use this when the service is temporarily unable to handle requests (HTTP 503).

Usage:

public async Task<Result> ProcessRequestAsync()
{
    if (_maintenanceModeService.IsActive())
    {
        return new ServiceUnavailableException(
            "The service is currently undergoing scheduled maintenance. Please try again in a few minutes."
        );
    }

    // ... process the request
    return Result.Success();
}

Best Practices

To get the most out of this exception model, follow these guidelines:

  1. Throw from Business Logic, Catch in the API Layer: Your services, domain, and application layers should be responsible for throwing these exceptions when a business rule is violated. The outermost layer of your application (e.g., the API project) should be responsible for catching them and translating them into the appropriate user-facing response. This keeps your core logic free of presentation concerns.

  2. Be Specific: Always use the most specific exception that fits the scenario. Throw a NotFoundException instead of a generic Exception with a "not found" message. This allows your error-handling middleware to act on the exception type, not the message text.

  3. Use for Predictable Failures: These exceptions are designed for expected and predictable business rule failures (e.g., a requested item doesn't exist, a user tries to register with a duplicate email). They are not intended for unexpected system errors like a database connection failure or a NullReferenceException.

  4. Provide Clear, Log-Friendly Messages: The exception message should clearly state what went wrong for logging and debugging purposes. However, avoid putting sensitive information in the message, as it might be logged or inadvertently exposed in an error response.

  5. Combine with the Result<T> Pattern: For a functional approach, return Result<T> from your business logic and carry the semantic exception as the failure value (the implicit conversion handles this). At the API boundary, pass ErrorResultResponse.Result to Result<T>.Match so the failure is mapped to the standardized response without an extra try-catch.


Additional documentation

For more information on how this package fits into the larger RA.Utilities ecosystem, please see the main repository documentation.


Contributing

Contributions are welcome! If you have a suggestion for a new exception type or find a bug, please open an issue to discuss it. Please follow the contribution guidelines outlined in the other projects in this repository.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (3)

Showing the top 3 NuGet packages that depend on RA.Utilities.Core.Exceptions:

Package Downloads
RA.Utilities.Api

Provides a collection of essential utilities for building robust and consistent ASP.NET Core APIs. This package includes endpoint registration helpers, standardized response models, and exception handling middleware to streamline development and promote best practices.

RA.Utilities.Core.ValueObjects

Strongly-typed, validated value objects for .NET, including Email, Currency, SSN, Money, and IBAN, with built-in System.Text.Json converters and structured validation errors. Part of the RA.Utilities suite.

RA.Utilities.Application.Validation

RA.Utilities.Application.Validation provides reusable validators and helpers built on FluentValidation for validating incoming requests. It aggregates multiple validators asynchronously and converts FluentValidation failures into structured BadRequestException/ValidationError results.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
10.2.0 41 10/28/2026
10.1.0 188 10/18/2026
10.0.100-rc.2 255 10/27/2025
10.0.4 185 9/23/2026
10.0.3 158 4/25/2026
10.0.2 140 4/25/2026
10.0.1 164 1/15/2026
10.0.0 268 11/23/2025
10.0.0-rc.2 301 11/16/2025