我应该如何 return 来自 .NET MVC 应用程序中所有函数的通用响应和响应代码?

How should I return a generic response and response code from all functions in a .NET MVC app?

我希望能够 return 来自我的 MVC 应用程序业务层中的函数调用的一般响应。大多数时候我看到一个对象创建函数看起来像这样

 public int Create(ICNUser item)
 {
       return this._repository.Create(item);
 }
 public void Update(ICNUser item)
 {
      this._repository.Create(item);
 }

在这种情况下,_repository 是一个包装 entity framework 的存储库。

这在很多情况下都很有效,但我想要 return 编辑更多信息,并且我想要一个 success/failure 变量和一个响应代码来说明此操作验证失败的原因。我希望能够选择 return 插入的对象或选定的对象。

一个示例是创建用户函数,returns 电子邮件不能为空错误和/或用户已经存在错误,并且根据错误我向用户显示不同的消息。

我 运行 遇到的问题是我想让单元测试涵盖一个函数的所有可能的响应代码,而不必去查看代码并尝试找出可能的响应代码return值即可。我正在做的事情感觉像是一种反模式。有没有更好的方法来完成这一切?

这就是我现在拥有的。

 public IGenericActionResponse<ICNUser> Create(ICNUser item)
 {
        return this._repository.Create(item);
 }

 public IGenericActionResponse Update(ICNUser item)
 {
       return this._repository.Update(item);
 }

接口

 namespace Web.ActionResponses
 {



public enum ActionResponseCode
{
    Success,
    RecordNotFound,
    InvalidCreateHash,
    ExpiredCreateHash,
    ExpiredModifyHash,
    UnableToCreateRecord,
    UnableToUpdateRecord,
    UnableToSoftDeleteRecord,
    UnableToHardDeleteRecord,
    UserAlreadyExists,
    EmailCannotBeBlank,
    PasswordCannotBeBlank,
    PasswordResetHashExpired,
    AccountNotActivated,
    InvalidEmail,
    InvalidPassword,
    InvalidPageAction
}

public interface IGenericActionResponse
{
    bool RequestSuccessful { get; }
    ActionResponseCode ResponseCode { get; }
}

public interface IGenericActionResponse<T>
{
    bool RequestSuccessful { get; }
    bool RecordIsNull{get;}
    ActionResponseCode ResponseCode { get; }
}
}

实现

namespace Web.ActionResponses
{

public class GenericActionResponse<T> : IGenericActionResponse<T>
{
    private bool _requestSuccessful;
    private ActionResponseCode _actionResponseCode;
    public T Item { get; set; }
    public GenericActionResponse(bool success, ActionResponseCode actionResponseCode, T item)
    {
        this._requestSuccessful = success;
        this._actionResponseCode = actionResponseCode;
        this.Item = item;
    }

    public GenericActionResponse(bool success, ActionResponseCode actionResponseCode)
    {
        this._requestSuccessful = success;
        this._actionResponseCode = actionResponseCode;
        this.Item = default(T);
    }

    public bool RecordIsNull
    {
        get
        {
            return this.Item == null;
        }
    }

    public bool RequestSuccessful
    {
        get
        {
            return this._requestSuccessful;
        }
    }

    public ActionResponseCode ResponseCode
    {
        get
        {
            return this._actionResponseCode;
        }
    }

}




public class GenericActionResponse : IGenericActionResponse
{
    private bool _requestSuccessful;
    private ActionResponseCode _actionResponseCode;

    public GenericActionResponse(bool success, ActionResponseCode actionResponseCode)
    {
        this._requestSuccessful = success;
        this._actionResponseCode = actionResponseCode;

    }

    public bool RequestSuccessful
    {
        get
        {
            return this._requestSuccessful;
        }
    }

    public ActionResponseCode ResponseCode
    {
        get
        {
            return this._actionResponseCode;
        }
    }

}}

MVC 应用

public ActionResult ValidateResetHash(string passwordResetHash)
    {
        IGenericActionResponse result = (IGenericActionResponse)this._userManager.IsValidPasswordResetHash(passwordResetHash);

        if (result.RequestSuccessful)
        {
            Models.PasswordChangeModel model = new Models.PasswordChangeModel();
            model.PasswordResetHash = passwordResetHash;
            return View("~/Areas/Public/Views/ResetPassword/PasswordChangeForm.cshtml", model);
        }
        else
        {
            switch (result.ResponseCode)
            {
                case ActionResponseCode.RecordNotFound:
                    {
                        FermataFish.Models.GenericActionModel responseModel = new FermataFish.Models.GenericActionModel(true, "/Login", "Login", "You have submitted an invalid password reset link.", false);
                        return View("~/Views/Shared/GenericAction.cshtml", responseModel);
                    }
                case ActionResponseCode.PasswordResetHashExpired:
                    {
                        FermataFish.Models.GenericActionModel responseModel = new FermataFish.Models.GenericActionModel(true, "/ResetPassword", "Reset Password", "You have submitted an expired password reset link. You must reset your password again to change it.", false);
                        return View("~/Views/Shared/GenericAction.cshtml", responseModel);
                    }
                default:
                    {
                        FermataFish.Models.GenericActionModel responseModel = new FermataFish.Models.GenericActionModel(true, "/", "Home", "An unknown error has occured. The system administrator has been notified. Error code:" + Enum.GetName(typeof(ActionResponseCode), result.ResponseCode), false);
                        return View("~/Views/Shared/GenericAction.cshtml", responseModel);
                    }
            }
        }
    }

您的 ValidateResetHash 响应中的 switch 语句有点代码难闻。这向我建议您可以从使用 subclassable enum 中获益。 subclassable 枚举会将动作响应代码或类型映射到带有模型的 return 视图。这是一个如何使用它的编译示例。

首先是一些class补全我曾经得到一个编译示例:

public class GenericActionModel
{
    private bool v1;
    private string v2;
    private string v3;
    private string v4;
    private bool v5;

    protected GenericActionModel() {}
    public GenericActionModel(bool v1, string v2, string v3, string v4, bool v5)
    {
        this.v1 = v1;
        this.v2 = v2;
        this.v3 = v3;
        this.v4 = v4;
        this.v5 = v5;
    }
}

public class ActionResult
{
    private GenericActionModel responseModel;
    private string v;

    public ActionResult(string v, GenericActionModel responseModel)
    {
        this.v = v;
        this.responseModel = responseModel;
    }
}

public class PasswordChangeModel : GenericActionModel
{
    public object PasswordResetHash
    {
        get;
        set;
    }
}

public interface IUserManager
{
    Response IsValidPasswordResetHash(string passwordResetHash);
}

接下来是一些基础设施(框架)classes(我使用来自 AtomicStack 项目的 StringEnum 基础 class 作为 ResponseEnum 基础 class):

public abstract class Response
{
    public abstract string name { get; }
}

public class Response<TResponse> : Response where TResponse : Response<TResponse>
{
    private static string _name = typeof(TResponse).Name;
    public override string name => _name;
}

// Base ResponseEnum class to be used by more specific enum sets
public abstract class ResponseEnum<TResponseEnum> : StringEnum<TResponseEnum>
    where TResponseEnum : ResponseEnum<TResponseEnum>
{
    protected ResponseEnum(string responseName) : base(responseName) {}
    public abstract ActionResult GenerateView(Response response);
}

以下是一些示例回复:

public class HashValidated : Response<HashValidated>
{
    public string passwordResetHash;
}
public class InvalidHash : Response<InvalidHash> {}
public class PasswordResetHashExpired : Response<PasswordResetHashExpired> {}
public class Unexpected : Response<Unexpected> {}

一个示例 subclassable 枚举映射示例响应看起来像这样:

public abstract class ValidateHashResponses : ResponseEnum<ValidateHashResponses>
{

    public static readonly ValidateHashResponses HashOk                     = HashValidatedResponse.instance;
    public static readonly ValidateHashResponses InvalidHash                = InvalidHashResponse.instance;
    public static readonly ValidateHashResponses PasswordResetHashExpired   = PasswordResetHashExpiredResponse.instance;
    public static readonly ValidateHashResponses Default                    = DefaultResponse.instance;

    private ValidateHashResponses(string responseName) : base(responseName) {}

    protected abstract class ValidateHashResponse<TValidateHashResponse, TResponse> : ValidateHashResponses
        where TValidateHashResponse : ValidateHashResponse<TValidateHashResponse, TResponse>, new()
        where TResponse : Response<TResponse>
    {
        public static TValidateHashResponse instance = new TValidateHashResponse();
        private static string name = Response<TResponse>.Name;
        protected ValidateHashResponse() : base(name) {}
    }

    protected class HashValidatedResponse : ValidateHashResponse<HashValidatedResponse, HashValidated>
    {
        public override ActionResult GenerateView(Response response)
        {
            PasswordChangeModel model = new PasswordChangeModel();
            model.PasswordResetHash = ((HashValidated) response).passwordResetHash;
            return new ActionResult("~/Areas/Public/Views/ResetPassword/PasswordChangeForm.cshtml", model);
        }
    }

    protected class InvalidHashResponse : ValidateHashResponse<InvalidHashResponse, InvalidHash>
    {
        public override ActionResult GenerateView(Response response)
        {
            GenericActionModel responseModel = new GenericActionModel(true, "/Login", "Login", "You have submitted an invalid password reset link.", false);
            return new ActionResult("~/Views/Shared/GenericAction.cshtml", responseModel);
        }
    }

    protected class PasswordResetHashExpiredResponse : ValidateHashResponse<PasswordResetHashExpiredResponse, PasswordResetHashExpired>
    {
        public override ActionResult GenerateView(Response response)
        {
            GenericActionModel responseModel = new GenericActionModel(true, "/ResetPassword", "Reset Password", "You have submitted an expired password reset link. You must reset your password again to change it.", false);
            return new ActionResult("~/Views/Shared/GenericAction.cshtml", responseModel);
        }
    }

    protected class DefaultResponse : ValidateHashResponses
    {
        public static DefaultResponse instance = new DefaultResponse();
        private DefaultResponse() : base("Default") {}
        public override ActionResult GenerateView(Response response)
        {
            GenericActionModel responseModel = new GenericActionModel(true, "/", "Home", "An unknown error has occured. The system administrator has been notified. Error code:" + response.name, false);
            return new ActionResult("~/Views/Shared/GenericAction.cshtml", responseModel);
        }
    }

}

实施 SampleController:

public class SampleController
{
    private IUserManager _userManager;
    public ActionResult ValidateResetHash(string passwordResetHash)
    {
        Response    result      = this._userManager.IsValidPasswordResetHash(passwordResetHash);
        var         resultType  = ValidateHashResponses.TrySelect(result.name,ValidateHashResponses.Default);
        return resultType.GenerateView(result);
    }

}

调整上面的代码以适应您的情况。

如果您想允许其他人扩展 ValidateHashResponses 枚举,您可以将构造函数设置为受保护的而不是私有的。然后他们可以扩展 ValidateHashResponses 并添加他们自己的额外枚举 values.

使用 subclassable 枚举的关键在于利用 TrySelect 方法解析对特定枚举值的响应。然后我们在枚举值上调用 GenerateView 方法来生成视图。

枚举的另一个好处是,如果您需要根据枚举值做出其他决定,您只需向枚举添加另一个抽象方法,所有 value 定义将被迫实现新的抽象方法,这与传统的 enum/switch 语句组合不同,传统的 enum/switch 语句组合不需要添加新的枚举值,并且人们可能会忘记重新访问所有使用枚举的 switch 语句。

免责声明: 我是 AtomicStack 项目的作者。如果您觉得它适合您的需要,请随意从项目中获取 Subclassable 枚举 class 代码。

更新:

如果你想注入响应枚举,你应该创建一个IResponseHandler适配器接口和一个GenerateViewForResponse类型的方法,并提供一个具体的实现使用 ValidateHashResponses 枚举。