我应该如何 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 枚举。
我希望能够 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 枚举。