| name | handle-validation-and-errors |
| description | Validates DTOs/inputs and throws/localizes business errors in an ABP app. USE FOR: data annotations, IValidationEnabled, FluentValidation, BusinessException/UserFriendlyException, error codes, localizing error messages, mapping exceptions to HTTP status codes. DO NOT USE FOR: writing the application service itself or CrudAppService plumbing (use application-services); permission/authorization checks and AbpAuthorizationException policy (use permissions-and-authorization / authorize-resources).
|
| license | MIT |
Validation & Error Handling in ABP
ABP builds on ASP.NET Core model validation and adds automatic validation, localization, and a standard exception-handling pipeline. Everything from the ASP.NET Core validation docs still applies.
When to Use
- Validating DTOs/inputs with data annotations or
IValidatableObject.
- Getting automatic method-argument validation via
IValidationEnabled.
- Integrating FluentValidation into ABP's validation pipeline.
- Throwing business errors with
BusinessException / UserFriendlyException.
- Localizing error messages by error code.
- Mapping exception types to HTTP status codes.
When Not to Use
- Writing the application service or CRUD plumbing itself — use the application-services skill; this skill only covers the validation/error concerns inside it.
- Authorization checks and
AbpAuthorizationException policy — use permissions-and-authorization / authorize-resources; here it appears only in the status-code mapping table.
DTO Validation
Use standard data annotation attributes on DTOs. When a DTO is a parameter to an application service or controller action, ABP validates it automatically and throws an AbpValidationException on failure. (The attribute messages are localized only when the DTO's assembly is registered for data-annotations localization — see below — otherwise the raw attribute message is used.)
public class CreateBookDto
{
[Required]
[StringLength(100)]
public string Name { get; set; }
[Range(0, 999.99)]
public decimal Price { get; set; }
}
For custom cross-field logic, implement IValidatableObject:
public class CreateBookDto : IValidatableObject
{
[Required] public string Name { get; set; }
[Required] public string Description { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (Name == Description)
{
yield return new ValidationResult(
"Name and Description can not be the same!",
new[] { "Name", "Description" });
}
}
}
Keep DTOs simple — don't put domain logic in Validate. Data annotation error messages are localized only when the DTO's assembly is registered for it (via AddDataAnnotationsLocalization / AbpMvcDataAnnotationsLocalizationOptions.AssemblyResources); otherwise the raw attribute message is used.
IValidationEnabled
IValidationEnabled is an empty marker interface. Any DI-registered class that implements it gets automatic method-argument validation. All application services implement it already. ABP uses dynamic proxying/interception, so the method must be virtual or the service must be called through an interface.
public class MyService : ITransientDependency, IValidationEnabled
{
public virtual async Task DoItAsync(MyInput input) { }
}
Disable with [DisableValidation] on a method, class, or property.
AbpValidationException
ABP throws AbpValidationException when validation fails. Its ValidationErrors holds the error list, its log level is Warning, and ABP converts it to an HTTP 400 response automatically. You can throw it yourself but rarely need to.
FluentValidation Integration
Add the Volo.Abp.FluentValidation package (abp add-package Volo.Abp.FluentValidation) and depend on AbpFluentValidationModule. Then write standard FluentValidation validators — ABP discovers them and runs them as part of the same validation pipeline.
public class CreateUpdateBookDtoValidator : AbstractValidator<CreateUpdateBookDto>
{
public CreateUpdateBookDtoValidator()
{
RuleFor(x => x.Name).Length(3, 10);
RuleFor(x => x.Price).ExclusiveBetween(0.0f, 999.0f);
}
}
Business Exceptions
Most of your own errors are business exceptions. BusinessException implements IBusinessException, IHasErrorCode, IHasErrorDetails, and IHasLogLevel (default log level Warning). All its constructor arguments are optional; usually you set an error Code or a Message.
throw new BusinessException(QaErrorCodes.CanNotVoteYourOwnAnswer);
The Code follows the format <code-namespace>:<error-code> (e.g. Volo.Qa:010002). The code-namespace is unique to your module/application and is the key used for localization.
Attach message parameters via the Data dictionary; there is a WithData shortcut that is chainable:
throw new BusinessException("App:010046").WithData("UserName", "john");
Localizing Error Messages
Two models:
User-friendly exceptions. UserFriendlyException implements IUserFriendlyException (which derives from IBusinessException). ABP sends its Message and Details to the client unchanged — no localization needed, or localize it yourself with the string localizer at throw time.
throw new UserFriendlyException(_stringLocalizer["UserNameShouldBeUniqueMessage", "john"]);
Error codes (recommended for advanced cases). Instead of localizing at throw time, map a code-namespace to a localization resource once, then localize by the error code key. This avoids needing the localizer everywhere (e.g. in static contexts or entity methods).
services.Configure<AbpExceptionLocalizationOptions>(options =>
{
options.MapCodeNamespace("Volo.Qa", typeof(QaResource));
});
Add the error code as a key in the resource's en.json:
{
"culture": "en",
"texts": {
"Volo.Qa:010002": "You can not vote your own answer!",
"App:010046": "Username should be unique. '{UserName}' is already taken!"
}
}
If no localized string is defined for a code, ABP sends a default error message to the client — it does not fall back to the exception's Message property. Use UserFriendlyException when you want the raw message shown.
HTTP Status Code Mapping
ABP picks a status code automatically for common exception types:
AbpAuthorizationException → 401 if not logged in, 403 if logged in
AbpValidationException → 400
EntityNotFoundException → 404
AbpDbConcurrencyException → 409
IBusinessException / IUserFriendlyException → 403
NotImplementedException → 501
- anything else →
500 (treated as an infrastructure error)
Override per error code with AbpExceptionHttpStatusCodeOptions:
services.Configure<AbpExceptionHttpStatusCodeOptions>(options =>
{
options.Map("Volo.Qa:010002", HttpStatusCode.Conflict);
});
Validation
- Pass an invalid DTO into an app service / auto API controller and confirm an
AbpValidationException → HTTP 400 response, with validationErrors populated in the JSON.
- Throw a
BusinessException with a mapped error code and confirm the localized text from en.json reaches the client (not the raw Message).
- Confirm the response body is a
RemoteServiceErrorResponse with a single error object holding code / message.
- Verify a code mapped via
AbpExceptionHttpStatusCodeOptions.Map(...) returns the overridden status code.
Common Pitfalls
- Data annotation messages are not localized unless the DTO's assembly is registered for data-annotations localization (
AddDataAnnotationsLocalization / AbpMvcDataAnnotationsLocalizationOptions.AssemblyResources); otherwise the raw attribute message shows.
IValidationEnabled needs the method to be virtual or the service to be called through an interface — ABP relies on dynamic proxying/interception, so direct concrete calls to non-virtual methods bypass validation.
- An error code with no localized string falls back to ABP's default message, not to the exception's
Message. Use UserFriendlyException when you want the raw message shown.
- The error JSON is a
RemoteServiceErrorResponse with a single error object (a RemoteServiceErrorInfo) holding code, message, details, data, and validationErrors — i.e. { "error": { "code": ..., "message": ... } } — filled based on which interfaces the exception implements.
- Control what reaches clients with
AbpExceptionHandlingOptions (SendExceptionsDetailsToClients, SendStackTraceToClients).
- Set an exception's log level by implementing
IHasLogLevel; add extra logging with IExceptionWithSelfLogging.
- To react when ABP handles an exception, derive from
ExceptionSubscriber and override HandleAsync.