Přeskočit na hlavní obsah

Domain exceptions

Overview

Application failure should raise an exception to be handled by problem details and mapped to appropriate error status code. The best way how to make your code readable is to use specific domain exceptions. The implementation of your domain exception can be optionally simplified when inherited from suitable base exception. Domain exceptions are placed into Domain.Exceptions namespace.

Usage

//check if input parameters are valid
var isValid = Validator.TryValidateObject(input.Data, context, result, true);
if (!isValid)
{
throw new InvalidEmployeeDetailException(result.SelectMany(x => x.MemberNames));
}
//check if input data fits to app requirement
if (!instance.Numbers.Contains(input.Number))
{
throw new SingleValidationException($"The '{input.Number}' number isn't available.", new[] { nameof(input.Number) });
}
//check candidate status before executing command
if (candidate.CandidateStatus != CandidateStatus.PositionDefined)
{
throw new InvalidCandidateStatusException(candidate.Id, candidate.CandidateStatus, CandidateStatus.PositionDefined);
}

Implementation

using System;
using System.Runtime.Serialization;
using ASOL.Core.Processing;

namespace My.App.Domain.Exceptions
{
/// <summary>
/// Represents the exception that application identifier is invalid.
/// </summary>
[Serializable]
public class InvalidApplicationCodeException : BadRequestExceptionBase
{
private static string GetMessage(string applicationCode)
{
return $"Application '{applicationCode}' doesn't exist.";
}

/// <summary>
/// Initializes a new instance of the <see cref="InvalidApplicationCodeException"/> class.
/// </summary>
public InvalidApplicationCodeException(string applicationCode)
: base(GetMessage(applicationCode), Array.Empty<ValidationError>())
{
}

/// <summary>
/// Initializes a new instance of the <see cref="InvalidApplicationCodeException"/> class
/// from the specified instances of the <see cref="SerializationInfo"/> and <see cref="StreamingContext"/> classes.
/// </summary>
protected InvalidApplicationCodeException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
}
using System;
using System.Runtime.Serialization;
using ASOL.Core.Processing;

namespace My.App.Domain.Exceptions
{
/// <summary>
/// Represents the exception that single validation failed.
/// </summary>
[Serializable]
public class SingleValidationException : BadRequestExceptionBase
{
/// <summary>
/// Initializes a new instance of the <see cref="SingleValidationException"/> class.
/// </summary>
public SingleValidationException(string message, IList<string> memberNames)
: base(DefaultMessage, new[] { new ValidationError(message, memberNames) })
{
}

/// <summary>
/// Initializes a new instance of the <see cref="SingleValidationException"/> class
/// from the specified instances of the <see cref="SerializationInfo"/> and <see cref="StreamingContext"/> classes.
/// </summary>
protected SingleValidationException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
}
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;

namespace My.App.Domain.Exceptions
{
/// <summary>
/// Represents the exception that current <see cref="CandidateStatus"/> of <see cref="Candidate"/> is not valid for given command.
/// </summary>
[Serializable]
public class InvalidCandidateStatusException : PreconditionFailedExceptionBase
{
private static string GetMessage(string candidateId, CandidateStatus currentStatus, params CandidateStatus[] validStatuses)
{
var msg = $"Candidate '{candidateId}' has invalid status '{currentStatus}' for this command. The valid statuses are: [{string.Join(", ", validStatuses)}].";
return msg;
}

/// <summary>
/// Initializes a new instance of the <see cref="InvalidCandidateStatusException"/> class.
/// </summary>
public InvalidCandidateStatusException(string candidateId, CandidateStatus currentStatus, params CandidateStatus[] validStatuses)
: base(GetMessage(candidateId, currentStatus, validStatuses))
{
}

/// <summary>
/// Initializes a new instance of the <see cref="InvalidCandidateStatusException"/> class
/// from the specified instances of the <see cref="SerializationInfo"/> and <see cref="StreamingContext"/> classes.
/// </summary>
protected InvalidCandidateStatusException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
}

Base exceptions

Problem details

{
// ...
options.Map<PreconditionFailedExceptionBase>(ex => PreconditionFailedExceptionMapping(TrackException(ex)));
options.Map<BadRequestExceptionBase>((ctx, ex) => BadRequestExceptionMapping(ctx, TrackException(ex)));
// ...

options.Map<ProcessingCoreException>((ctx, ex) =>
{
Telemetry.TrackException(ex);

var processingEx = ProcessingCoreException.FindProcessingException(ex);
return processingEx.InnerException switch
{
// ...
PreconditionFailedExceptionBase inner => PreconditionFailedExceptionMapping(inner),
BadRequestExceptionBase inner => BadRequestExceptionMapping(ctx, inner),
// ...
};
});
}

private static ProblemDetails PreconditionFailedExceptionMapping(PreconditionFailedExceptionBase exception)
{
return new StatusCodeProblemDetails(StatusCodes.Status412PreconditionFailed) { Detail = exception.Message };
}

private static ProblemDetails BadRequestExceptionMapping(HttpContext ctx, BadRequestExceptionBase exception)
{
var factory = ctx.RequestServices.GetRequiredService<ProblemDetailsFactory>();
return factory.CreateValidationProblemDetails(ctx, exception.ValidationErrors, StatusCodes.Status400BadRequest);
}

BadRequestExceptionBase

The BadRequestExceptionBase is the base exception mapped to (400)-BadRequest http status code. Use this exception when validating user inputs required for business logic execution from REST API.

using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using ASOL.Core.Processing;

namespace My.App.Domain.Exceptions
{
/// <summary>
/// Represents the bad request base exception class (mapped to BadRequest(400)).
/// </summary>
public abstract class BadRequestExceptionBase : Exception
{
protected const string DefaultMessage = "Validation failed";

public IDictionary<string, string[]> ValidationErrors { get; set; }

/// <summary>
/// Initializes a new instance of the exception class.
/// </summary>
protected BadRequestExceptionBase(string message, params ValidationError[] validationErrors)
: this(message, ValidationErrorExtensions.ToDictionary(validationErrors))
{
}

/// <summary>
/// Initializes a new instance of the exception class.
/// </summary>
protected BadRequestExceptionBase(string message, IDictionary<string, string[]> validationErrors)
: base(message)
{
ValidationErrors = validationErrors;
}

/// <summary>
/// Initializes a new instance of the exception class
/// from the specified instances of the <see cref="SerializationInfo"/> and <see cref="StreamingContext"/> classes.
/// </summary>
protected BadRequestExceptionBase(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
}

PreconditionFailedExceptionBase

The PreconditionFailedExceptionBase is the base exception mapped to (412)-PreconditionFailed http status code. Use this exception when checking current settings or state required for business logic execution from REST API.

using System;
using System.Runtime.Serialization;

namespace My.App.Domain.Exceptions
{
/// <summary>
/// Represents the precondition failed base exception class (mapped to PreconditionFailed(412)).
/// </summary>
public abstract class PreconditionFailedExceptionBase : Exception
{
protected const string DefaultMessage = "Precondition failed";

/// <summary>
/// Initializes a new instance of the exception class.
/// </summary>
protected PreconditionFailedExceptionBase(string message)
: base(message ?? DefaultMessage)
{
}

/// <summary>
/// Initializes a new instance of the exception class.
/// </summary>
protected PreconditionFailedExceptionBase(string message, Exception innerException)
: base(message ?? DefaultMessage, innerException)
{
}

/// <summary>
/// Initializes a new instance of the exception class
/// from the specified instances of the <see cref="SerializationInfo"/> and <see cref="StreamingContext"/> classes.
/// </summary>
protected PreconditionFailedExceptionBase(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
}