ProvisionData.Common
4.1.0
See the version list below for details.
dotnet add package ProvisionData.Common --version 4.1.0
NuGet\Install-Package ProvisionData.Common -Version 4.1.0
<PackageReference Include="ProvisionData.Common" Version="4.1.0" />
<PackageVersion Include="ProvisionData.Common" Version="4.1.0" />
<PackageReference Include="ProvisionData.Common" />
paket add ProvisionData.Common --version 4.1.0
#r "nuget: ProvisionData.Common, 4.1.0"
#:package ProvisionData.Common@4.1.0
#addin nuget:?package=ProvisionData.Common&version=4.1.0
#tool nuget:?package=ProvisionData.Common&version=4.1.0
ProvisionData.Common
A selection of useful classes and utilities commonly used in software by Provision Data Systems Inc.
Installation
You can install the ProvisionData.Common package via NuGet Package Manager Console:
Install-Package ProvisionData.Common
Or via .NET CLI:
dotnet add package ProvisionData.Common
Features
- Result Pattern for handling operation outcomes
- Safe IAsyncDisposable and IDisposable pattern
Result Pattern
The Result class provides a way to represent the outcome of operations,
encapsulating success and failure states along with relevant data or error messages.
Basic Usage
public class UserService
{
private readonly IUserRepository _repository;
public Result<User> GetById(Int32 id)
{
var user = _repository.Find(id);
if (user is null)
return Error.NotFound($"User with ID {id} was not found");
return user; // Implicit conversion to Result<User>.Success
}
public Result<User> Create(CreateUserRequest request)
{
// Validation happens in ASP.NET pipeline using FluentValidation
// Domain only handles business rules
if (_repository.ExistsByEmail(request.Email))
return Error.Conflict("A user with this email already exists");
var user = new User(request.Name, request.Email);
_repository.Add(user);
return user;
}
}
Error Codes
Error codes use singleton instances with reference equality. Each error type has a unique code that can be used programmatically:
// Programmatic use - implicit String conversion
String errorName = error.Code; // "NotFoundError"
// Debugging - ToString() for logging
Console.WriteLine(error.Code.ToString()); // "NotFoundError"
// Type checking
if (result.Error.IsErrorType<NotFoundError>())
{
// Handle not found scenario
}
Note: The .ToString() method is primarily for debugging and logging. For programmatic use, rely on the implicit String operator or IsErrorType<T>() method.
Chaining Operations
public Result<OrderConfirmation> ProcessOrder(CreateOrderRequest request)
{
return ValidateOrder(request)
.Bind(order => CheckInventory(order))
.Bind(order => ProcessPayment(order))
.Bind(order => CreateShipment(order))
.Map(shipment => new OrderConfirmation(shipment.TrackingNumber));
}
private Result<Order> ValidateOrder(CreateOrderRequest request)
{
if (request.Items.Count == 0)
return Error.Validation("Order must contain at least one item");
return new Order(request.CustomerId, request.Items);
}
private Result<Order> CheckInventory(Order order)
{
foreach (var item in order.Items)
{
if (!_inventory.IsAvailable(item.ProductId, item.Quantity))
return Error.Conflict($"Product {item.ProductId} is out of stock");
}
return order;
}
Async Operations
The Result pattern includes comprehensive async support for modern C# codebases:
// Example 1: Async data access
public async Task<Result<User>> GetUserAsync(Int32 userId)
{
return await _repository.FindAsync(userId)
.MapAsync(user => user ?? Error.NotFound("User not found"));
}
// Example 2: Async pipeline with external services
public async Task<Result<OrderConfirmation>> ProcessOrderAsync(CreateOrderRequest request)
{
return await ValidateOrderAsync(request)
.BindAsync(async order => await CheckInventoryAsync(order))
.BindAsync(async order => await ProcessPaymentAsync(order))
.TapAsync(async order => await SendConfirmationEmailAsync(order))
.MapAsync(async order => await CreateConfirmationAsync(order));
}
private async Task<Result<Order>> CheckInventoryAsync(Order order)
{
foreach (var item in order.Items)
{
var isAvailable = await _inventoryService.CheckAvailabilityAsync(item.ProductId, item.Quantity);
if (!isAvailable)
return Error.Conflict($"Product {item.ProductId} is out of stock");
}
return order;
}
// Example 3: Async Match for handling results
var message = await userService.GetUserAsync(userId)
.MatchAsync(
onSuccess: async user =>
{
await _analytics.TrackUserAccessAsync(user.Id);
return $"Welcome, {user.Name}!";
},
onFailure: async error =>
{
await _logger.LogErrorAsync(error.Description);
return $"Error: {error.Description}";
});
When to use async methods:
MapAsync- When transforming the result value requires async operations (e.g., calling external APIs, database queries)BindAsync- When the next step in the pipeline is async and can fail (returnsTask<Result<T>>)MatchAsync- When handling success/failure cases requires async operations (e.g., logging, analytics)TapAsync- When side effects are async but shouldn't affect the result (e.g., sending notifications, caching)
Using Match
var message = userService.GetById(userId).Match(
onSuccess: user => $"Welcome, {user.Name}!",
onFailure: error => $"Error: {error.Description}"
);
Getting Values Safely
// With explicit default
var user = result.GetValueOrDefault(User.Guest);
// With type default (null for reference types)
var userId = result.GetValueOrDefault();
// Using Match for custom logic
var user = result.Match(
onSuccess: u => u,
onFailure: error =>
{
_logger.LogError(error.Description);
return User.Guest;
});
Domain Errors
public static class DomainErrors
{
public static class User
{
public static Error NotFound(Int32 id) =>
Error.NotFound($"User with ID {id} was not found");
public static Error EmailAlreadyExists(String email) =>
Error.Conflict($"Email {email} is already registered");
public static Error InvalidEmail =>
Error.Validation("The email format is invalid");
public static Error PasswordTooWeak =>
Error.Validation(
"Password must be at least 8 characters with uppercase, lowercase, and digits");
}
public static class Order
{
public static Error NotFound(Guid id) =>
Error.NotFound($"Order {id} was not found");
public static Error EmptyCart =>
Error.Validation("Cannot create order with empty cart");
public static Error InsufficientStock(String productId) =>
Error.Conflict($"Insufficient stock for product {productId}");
}
}
Exception Handling
The Error.Exception() method is available to convert exceptions to errors, but its use should be rare and discouraged.
Why it exists: In Blazor applications, unhandled exceptions can crash the entire app, forcing a page reload. Converting exceptions to errors provides a recovery path.
Important limitations:
try
{
await externalService.CallAsync();
}
catch (Exception ex)
{
// ⚠️ This deliberately loses stack trace and inner exceptions
return Error.Exception(ex);
}
The conversion is deliberately minimal:
- ✅ Captures exception type name and message only
- ❌ Loses stack trace (security/privacy concern)
- ❌ Loses inner exceptions
- ❌ Loses custom exception properties
Why these limitations:
- Performance - Error values must be lightweight for high-throughput scenarios
- Security - Error descriptions may be visible to end users; stack traces can leak internal details
- Serialization - Full exceptions are not serializable across API boundaries
Best practice: Always log the full exception separately before converting:
try
{
await riskyOperation();
}
catch (Exception ex)
{
_logger.LogError(ex, "Operation failed for user {UserId}", userId);
return Error.Exception(ex); // Only for user-facing message
}
Input Validation vs Business Rules
The Result pattern is designed for business rule violations, not input validation.
Use FluentValidation for input validation:
public class CreateUserCommandValidator : AbstractValidator<CreateUserCommand>
{
public CreateUserCommandValidator()
{
RuleFor(x => x.Email).NotEmpty().EmailAddress();
RuleFor(x => x.Password).MinimumLength(8);
}
}
ASP.NET pipeline validates input and returns 400 BadRequest with detailed validation errors before the domain logic executes.
Use Result<T> for business logic:
public async Task<Result<User>> Handle(CreateUserCommand command)
{
// Input is already validated by pipeline
// Domain only checks business rules
if (await _userRepository.EmailExistsAsync(command.Email))
return Error.Conflict("Email already registered");
return await _userRepository.CreateAsync(command);
}
This separation ensures:
- Multiple validation errors caught at API boundary
- Type-safe domain logic with single error per operation
- Clean architecture with clear responsibility boundaries
Web API Integration
This is packaged separately in ProvisionData.WebApi so you need to install that package as well.
Install-Package ProvisionData.WebApi
Or via .NET CLI:
dotnet add package ProvisionData.WebApi
Once installed, you can use the extension methods to convert Result instances to appropriate HTTP responses.
var app = builder.Build();
app.MapGet("/api/users/{id}", (Int32 id, UserService userService) =>
{
return userService.GetById(id).ToApiResult();
});
app.MapPost("/api/users", (CreateUserRequest request, UserService userService) =>
{
return userService.Create(request)
.ToCreatedResult($"/api/users/{request.Email}");
});
app.MapPost("/api/orders", (CreateOrderRequest request, OrderService orderService) =>
{
return orderService.ProcessOrder(request).Match(
onSuccess: confirmation => Results.Ok(confirmation),
onFailure: error => error switch
{
ValidationError => Results.BadRequest(new { error.Code, error.Description }),
ConflictError => Results.Conflict(new { error.Code, error.Description }),
_ => Results.Problem(error.Description)
}
);
});
Safe IAsyncDisposable and IDisposable pattern
public class MyTestFixture : DisposableBase
{
private HttpClient? _httpClient; // IDisposable only
private DbConnection? _dbConnection; // IAsyncDisposable
protected override void Dispose(Boolean disposing)
{
if (disposing)
{
_httpClient?.Dispose();
_httpClient = null;
}
base.Dispose(disposing);
}
protected override async ValueTask DisposeAsyncCore()
{
if (_dbConnection is not null)
{
await _dbConnection.DisposeAsync().ConfigureAwait(false);
_dbConnection = null;
}
// HttpClient also implements IAsyncDisposable in modern .NET
if (_httpClient is IAsyncDisposable asyncDisposable)
{
await asyncDisposable.DisposeAsync().ConfigureAwait(false);
}
else
{
_httpClient?.Dispose();
}
_httpClient = null;
await base.DisposeAsyncCore().ConfigureAwait(false);
}
}
How does a derived class know which dispose to override?
| Scenario | Override |
|---|---|
Async resources (e.g., IAsyncDisposable fields, async streams) |
DisposeAsyncCore() |
Sync-only resources (e.g., IDisposable fields, unmanaged handles) |
Dispose(Boolean) |
| Mixed resources | Both methods |
The key insight: when DisposeAsync() is called, it calls DisposeAsyncCore() first (cleaning
managed resources async), then Dispose(false) (cleaning only unmanaged resources). This
prevents double-disposal of managed resources.
| Product | Versions 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. |
-
net10.0
- No dependencies.
NuGet packages (4)
Showing the top 4 NuGet packages that depend on ProvisionData.Common:
| Package | Downloads |
|---|---|
|
ProvisionData.Testing.Integration
Basic classes and utilities used across all PDSI projects. |
|
|
ProvisionData.WebApi
Enables using the Result pattern in ASP.NET Core applications. |
|
|
ProvisionData.AspNetCore
Basic classes and utilities used across all PDSI projects. |
|
|
ProvisionData.Testing.Integration.Examples
Package Description |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 4.1.4 | 0 | 2/5/2026 |
| 4.1.0 | 0 | 2/5/2026 |
| 4.0.21 | 21 | 2/4/2026 |
| 4.0.19 | 24 | 2/3/2026 |
| 4.0.18 | 42 | 2/2/2026 |
| 4.0.17 | 36 | 1/31/2026 |
| 4.0.15 | 34 | 1/30/2026 |
| 4.0.4 | 79 | 1/29/2026 |
| 4.0.3 | 84 | 1/29/2026 |
| 4.0.2 | 94 | 1/28/2026 |
| 4.0.1 | 89 | 1/28/2026 |
| 3.1.0 | 572 | 10/22/2021 |
| 3.0.0 | 672 | 9/25/2021 |
| 2.3.2 | 290 | 5/9/2020 |
| 2.3.1 | 1,064 | 2/28/2020 |
| 2.3.0 | 1,059 | 2/25/2020 |
| 2.2.2 | 1,072 | 1/16/2020 |
| 2.2.1 | 1,079 | 11/23/2019 |
| 2.2.0 | 1,107 | 11/13/2019 |
| 2.1.1 | 1,085 | 11/13/2019 |
| 2.1.0 | 1,116 | 11/12/2019 |
| 1.4.0 | 921 | 10/17/2019 |
| 1.3.0 | 898 | 10/15/2019 |
| 1.2.16 | 632 | 10/8/2019 |
| 1.2.15 | 685 | 9/21/2019 |
| 1.1.11 | 721 | 9/21/2019 |
| 1.1.10 | 736 | 8/29/2019 |
| 1.1.7 | 693 | 8/27/2019 |
| 1.0.2 | 760 | 5/3/2019 |
| 1.0.1 | 773 | 5/3/2019 |