CrudResult Pattern¶
When a service method can fail for business reasons (not just exceptions), Merchello uses the CrudResult<T> pattern. This gives you a consistent way to return success or failure from mutations, along with user-facing error and warning messages.
The Problem CrudResult Solves¶
Imagine you have a service method that creates a discount. It might fail because: - The discount code already exists - The start date is after the end date - The customer segment does not exist
You could throw exceptions for these, but exceptions are expensive and should be reserved for truly unexpected situations. Instead, CrudResult<T> lets you return a structured result with messages that can be shown to the user.
The CrudResult Class¶
Here is the complete implementation -- it is intentionally simple:
public class CrudResult<T> : IResult
{
public T? ResultObject { get; set; }
public List<ResultMessage> Messages { get; set; } = [];
public bool Success => Messages.All(x => x.ResultMessageType != ResultMessageType.Error);
}
Key things to notice:
Successis computed -- it istruewhen there are zero error messages. You do not set it manually.ResultObjectholds the created/updated entity on success, ornullon failure.Messagescan contain errors, warnings, and success messages simultaneously.
ResultMessage¶
public class ResultMessage
{
public ResultMessageType ResultMessageType { get; set; }
public string? Message { get; set; }
}
public enum ResultMessageType
{
Success,
Warning,
Error
}
When to Use CrudResult¶
Use CrudResult<T> for mutation operations that can fail for business reasons:
| Operation type | Return type |
|---|---|
| Create, Update, Delete | CrudResult<T> |
| Query, Get, List | Return entity directly (T, List<T>, etc.) |
// Good: mutation returns CrudResult
public async Task<CrudResult<Discount>> CreateAsync(CreateDiscountParameters parameters, CancellationToken ct)
// Good: query returns entity directly
public async Task<Discount?> GetAsync(Guid id, CancellationToken ct)
Adding Messages¶
Use the extension methods to add messages to a CrudResult<T>:
public async Task<CrudResult<Discount>> CreateAsync(
CreateDiscountParameters parameters,
CancellationToken ct)
{
var result = new CrudResult<Discount>();
// Validation -- add error messages for failures
if (string.IsNullOrWhiteSpace(parameters.Name))
{
result.AddErrorMessage("Discount name is required.");
return result;
}
// Check for duplicates
var existing = await GetByCodeAsync(parameters.Code, ct);
if (existing != null)
{
result.AddErrorMessage($"A discount with code '{parameters.Code}' already exists.");
return result;
}
// Create the entity
var discount = discountFactory.Create(parameters);
await SaveAsync(discount, ct);
// Add a warning if something is noteworthy but not an error
if (parameters.EndsAt.HasValue && parameters.EndsAt.Value < DateTime.UtcNow.AddDays(1))
{
result.AddWarningMessage("This discount expires within 24 hours.");
}
result.AddSuccessMessage("Discount created successfully.");
result.ResultObject = discount;
return result;
}
Available Extension Methods¶
All extension methods are in CrudResultExtensions:
| Method | What it does |
|---|---|
AddErrorMessage(message) |
Adds an error. This causes Success to become false. |
AddWarningMessage(message) |
Adds a warning. Does not affect Success. |
AddSuccessMessage(message) |
Adds a success message. Does not affect Success. |
Checking Results¶
In a Service¶
var result = await discountService.CreateAsync(parameters, ct);
if (!result.Success)
{
// Log errors
result.LogErrorMessages(logger);
return result; // Propagate the failure
}
var discount = result.ResultObject!;
// Continue with the discount...
In a Controller¶
[HttpPost]
public async Task<IActionResult> Create(CreateDiscountDto dto, CancellationToken ct)
{
var result = await discountService.CreateAsync(parameters, ct);
if (!result.Success)
{
// Return the error messages to the frontend
var errors = result.Messages.ErrorMessages().Select(m => m.Message);
return BadRequest(new { errors });
}
return Ok(MapToDto(result.ResultObject!));
}
Logging Extensions¶
CrudResultExtensions provides logging helpers that work with ILogger<T>:
// Log all errors and warnings
result.LogBadMessages(logger);
// Log only errors
result.LogErrorMessages(logger);
// Log only warnings
result.LogWarningMessages(logger);
Filtering Messages¶
You can filter messages by type using the list extension methods:
// Get only error messages
var errors = result.Messages.ErrorMessages();
// Get only warning messages
var warnings = result.Messages.WarningMessages();
Common Patterns¶
Propagating Results Through Service Layers¶
When one service calls another and both return CrudResult<T>, propagate the messages:
var invoiceResult = await invoiceService.CreateAsync(invoiceParams, ct);
if (!invoiceResult.Success)
{
var orderResult = new CrudResult<Order>();
foreach (var msg in invoiceResult.Messages)
{
orderResult.Messages.Add(msg);
}
return orderResult;
}
Multiple Validations Before Returning¶
You can accumulate multiple error messages before returning, giving the user a complete picture of what needs fixing:
var result = new CrudResult<Product>();
if (string.IsNullOrWhiteSpace(parameters.Name))
result.AddErrorMessage("Product name is required.");
if (parameters.Price < 0)
result.AddErrorMessage("Price cannot be negative.");
if (string.IsNullOrWhiteSpace(parameters.Sku))
result.AddErrorMessage("SKU is required.");
if (!result.Success)
return result; // Return all errors at once
// All validations passed, proceed with creation...
Key Files¶
| File | Description |
|---|---|
Merchello.Core/Shared/Models/CrudResult.cs |
The CrudResult<T> class |
Merchello.Core/Shared/Extensions/CrudResultExtensions.cs |
Extension methods for adding messages and logging |
Merchello.Core/Shared/Models/ResultMessage.cs |
The ResultMessage class |
Merchello.Core/Shared/Models/Enums/ResultMessageType.cs |
The ResultMessageType enum |