What we like to do for business logic errors (not neccessarily argument errors etc.) is to have a single enum that defines all potential types of errors:
/// <summary>
/// This enum is used to identify each business rule uniquely.
/// </summary>
public enum BusinessRuleId {
/// <summary>
/// Indicates that a valid body weight value of a patient is missing for dose calculation.
/// </summary>
[Display(Name = @"DoseCalculation_PatientBodyWeightMissing")]
PatientBodyWeightMissingForDoseCalculation = 1,
/// <summary>
/// Indicates that a valid body height value of a patient is missing for dose calculation.
/// </summary>
[Display(Name = @"DoseCalculation_PatientBodyHeightMissing")]
PatientBodyHeightMissingForDoseCalculation = 2,
// ...
}
The [Display(Name = "...")]
attributes define the key in the resource files to be used to translate the error messages.
Also, this file can be used as a starting point to find all occurences where a certain type of error is generated in your code.
Checking of Business Rules can be delegated to specialized Validator classes that yield lists of violated Business Rules.
We then use a custom Exception type to transport the violated rules:
[Serializable]
public class BusinessLogicException : Exception {
/// <summary>
/// The Business Rule that was violated.
/// </summary>
public BusinessRuleId ViolatedBusinessRule { get; set; }
/// <summary>
/// Optional: additional parameters to be used to during generation of the error message.
/// </summary>
public string[] MessageParameters { get; set; }
/// <summary>
/// This exception indicates that a Business Rule has been violated.
/// </summary>
public BusinessLogicException(BusinessRuleId violatedBusinessRule, params string[] messageParameters) {
ViolatedBusinessRule = violatedBusinessRule;
MessageParameters = messageParameters;
}
}
Backend service calls are wrapped in generic error handling code, which translates the violated Busines Rule into an user readable error message:
public object TryExecuteServiceAction(Action a) {
try {
return a();
}
catch (BusinessLogicException bex) {
_logger.Error(GenerateErrorMessage(bex));
}
}
public string GenerateErrorMessage(BusinessLogicException bex) {
var translatedError = bex.ViolatedBusinessRule.ToTranslatedString();
if (bex.MessageParameters != null) {
translatedError = string.Format(translatedError, bex.MessageParameters);
}
return translatedError;
}
Here ToTranslatedString()
is an extension method for enum
that can read resource keys from [Display]
attributes and use the ResourceManager
to translate these keys. The value for the respective resource key can contain placeholders for string.Format
, which match the provided MessageParameters
.
Example of an entry in the resx file:
<data name="DoseCalculation_PatientBodyWeightMissing" xml:space="preserve">
<value>The dose can not be calculated because the body weight observation for patient {0} is missing or not up to date.</value>
<comment>{0} ... Patient name</comment>
</data>
Example usage:
throw new BusinessLogicException(BusinessRuleId.PatientBodyWeightMissingForDoseCalculation, patient.Name);
With this approach, you can decouple the generation of the error message from the generation of the error, without the need to introduce a new exception class for each new type of error. Useful if different frontends should display different messages, if the displayed message should depend on the user language and/or role etc.
Exception.Data
property, "picky" exception catching, calling code catching & adding its own context, along with the captured call stack all contribute information that should allow far less verbose messages. FinallySystem.Reflection.MethodBase
looks promising for providing details to pass to your "exception construction" method.Exception.Data
. The emphasis should be capturing telemetry. Refactoring here is fine, but it misses the problem.