业务层验证失败的最佳做法

时间:2018-02-22 13:56:00

标签: c# .net validation domain-driven-design

我正在寻找有关业务领域层验证失败信息的反馈解决方案,特别是在逻辑可以深度嵌套的情况下。

目前,我们正在从每个业务方法返回一个结果对象,描述哪些(如果有的话)无效或出错。这导致一个真正的混乱,一个方法可能会调用另一个,然后我们需要从一种结果类型映射到另一种。写作也非常繁琐!

这方面的一个很好的例子是我们需要创建帐户和用户。可以随时创建用户以添加到现有帐户。但是,在内部创建帐户会调用创建用户以确保帐户从用户开始。因此,我们有用户验证错误,帐户验证错误是帐户特定的错误加上用户错误集。

public enum AccountErrors
{
    AccountNameMissing,
    AddressMissing,
    FirstNameMissing,
    LastNameMissing
}

public enum UserErrors
{
    FirstNameMissing,
    LastNameMissing,
}

public class ServiceLayer
{
    public IList<AccountErrors> CreateAccount(string accountName, string, address, string userFirstName, string userLastName)
    {
        return accountFactory.CreateAccount(accountName, address, userFirsrtName, userLastName);
    }

    public IList<UserErrors> CreateUser(string firstName, string lastName, int accountId)
    {
        return userFactory.CreateUser(firstName, lastName, accountId);
    }
}


public class AccountDomainFactory
{
    public IList<AccountErrors> CreateAccount(string accountName, string, address, string userFirstName, string userLastName)
    {
        List<AccountErrors> result = new List<AccountErrors>();
        if (accountName== null) result.Add(AccountErrors.AccountNameMissing);
        if (address== null) result.Add(AccountErrors.AddressMissing);

        // omitted here is the actual creation of the Account object.

        List<UserErrors> userResult = userFactory.CreateUser(userFirstName, userLastName, createdAccount.id);

        // omitted here is the tedious mapping to convert userResult into the other results.        

        return result;
    }
}

public class UserDomainFactory
{
    public IList<UserErrors> CreateUser(string firstName, string lastName, int accountId)
    {
        List<UserErrors> result = new List<UserErrors>();
        if (firstName == null) result.Add(UserErrors.FirstNameMissing);
        if (lastName== null) result.Add(UserErrors.LastNameMissing);

        // omitted here is the actual creation of the User object.

        return result;
    }
}

这段代码显然错过了很多但希望它能让你对我们所遵循的模式有一个很好的了解。可以想象,在任何相当复杂的过程中,我们都会遇到错误枚举和映射代码。我希望有更好的方法。也许我们需要彻底改变策略,而不使用这些错误结果对象?也许改用异常?

我应该指出,我们还将此机制用于更复杂的故障条件,这些条件不仅仅是简单的参数验证检查。例如,我们系统上的许多操作都可能失败,因为用户的帐户上没有足够的Credits,因此我们不仅仅讨论验证,而是讨论是否可以执行操作的实际业务逻辑。

另一个约束(虽然我愿意重新考虑)是我们希望我们的服务层方法返回强类型错误状态。这允许UI层轻松查看需要处理的验证/错误条件。这就是我们过去避免返回字符串消息列表的原因,虽然易于映射在一起,但是如果不深入了解所有较低代码,就无法知道会发生什么。

3 个答案:

答案 0 :(得分:0)

您的问题没有正确的答案。由您来设计您的架构。我更喜欢抛出自定义异常,因为每次在枚举中添加新值时,都需要重建/检查调用那些违反OCP(Open-Close-Principle)方法的地方。但是如果你选择使用返回操作状态,那么返回一个对象而不是枚举会好得多。如果应用程序可以扩展,您可以提供更多信息或简单地添加更多逻辑。此外,为每个组件添加抽象以支持DI

答案 1 :(得分:0)

根据我的理解,您的业务层有2个需求:

  • 了解操作是否成功
  • 了解出现错误时出现的问题

由于您的业务层由各种客户共享,并假设它是通过webapi提供的,因此您可以执行以下操作:

  • 创建一个ErrorHandler类(包含保存错误的列表)
  • 在您需要的ecah业务层类中注入它
  • 向该类添加特定错误类型,而不是在业务层函数中返回可枚举
  • 在您的ErrorHandler类中创建一个方法,将所有这些错误转换为您的应用程序可理解的格式(可能是仅针对消息的字符串列表,可能包含更多信息,例如哪个字段出错...)

这假设您有一个IoC控制器,可以为您的webapi的每个请求处理ErrorHandler的生命周期。

如果您不使用webapi将业务层的输出提供给应用程序,事情会变得有点复杂,因为您需要针对每种类型的应用程序以不同的方式处理ErrorHandler的生命周期,但仍然可行。

这种方法的优点是,您现在每个请求都有一个实例,其中包含治疗期间的所有错误。如果你想知道一切进展顺利,你仍然可以从函数中返回一个bool,但是你不必单独处理函数的每个返回,它将集中在一个类中。

答案 2 :(得分:0)

以下是我的想法(我相信这是非常复杂的问题,并且很难保持代码组合,同时不易出错且易于维护。)

1。验证应该在何处进行?在'API'图层

  • 为什么要在域中深入处理验证?您的服务和 模型应该只接受经过验证的参数,以便您可以 在没有的情况下直接创建模型或执行服务命令 需要通过验证来增加域中的复杂性(这是应用程序中最复杂的部分)。 因此,'accout name','first name','last name'等原语的验证可以由类处理,该类将处于有效状态。这应该是方法和value obejcts的其余部分。以下是帐户名称值对象的示例。
[DebuggerDisplay("{Value}")]
public class AccountName
{
    public static IEnumerable<Error> TryCreate(String value, out AccountName name)
    {
        // some validation logic
        var isValid = true;
        if (isValid)
        {
            name = new AccountName(value);

            return Error.Empty;
        }
        else
        {
            // return validation errors
        }
    }

    public String Value { get; }

    private AccountName(String value)
    {
        this.Value = value;
    }
} 

对象是可归位的,如果不是有效状态则无法创建。现在,您的服务方法和模型构造函数应该接受值对象而不是基元。

所以验证应该发生在某个地方吗?通常我创建API层(不要误用REST API控制器......这只是暴露特定域/域模型的核心功能的静态类)。 API层将是您的组合根。例如:

public static class AccountAPI
    {
        public static IEnumerable<Error> CreateAccount(String accName, String addrs)
        {
            var errors = new List<Error>();
            var accountNameErrors = AccountName.TryCreate(accName, out AccountName name);
            var addressErrors = Address.TryCreate(addrs, out Address address);

            // other validation

            errors.AddRange(accountNameErrors.Concat(addressErrors));
            if (errors.Any())
            {
                return errors;
            }
            else
            {
                // execute domain logic
                AccountDomainFactory.CreateAccount(name, address);

                return Error.Empty;
            }
        }
    } 

2。不要对错误这么明确。

  • 大多数情况下,您不需要知道发生了什么验证错误,因此返回标记界面,如IError或基本抽象类Error,仅此而已。在需要对用户进行日志记录或响应之前,不需要实际的错误类型,因此不要一直明确地保持类型(在域逻辑的深处)。
  • 也可以避免一般化错误。为不同的验证方案/域模型创建显式类型。不要试图将所有可能的错误都放入单个对象中这是帐户验证定义的示例。
    public abstract class Error // marker class from which all errors will inherit
    {
        public static readonly IEnumerable<Error> Empty = new Error[0];
    } 

    public enum AccountValidationErrorType
    {
        None = 0, NameTooShort = 1, NameIsEmpty = 2, PasswordInvalid = 3
    }

    public class AccountValidationError : Error
    {
        public IEnumerable<AccountValidationErrorType> Errors { get; }
    } 

当需要响应用户的时间(如果从REST api中使用)时,您可以创建BadRequest响应,并将验证错误提供给json,或者在MVC应用程序的上下文中,您可以对{{{{{{ 1}}接口一点点转换得到实际的错误类型,并为用户创建带有通知消息的适当视图。