问题引出
通常在很多的公司里面,对于接口的返回值没做太大规范,所以会比较常看到各个项目各自定义随意的返回值,比如以下情况:
1. 直接返回bool值(True或者False)
2. 返回void,只要不是异常信息,默认成功
3. 直接返回异常详情(这个非常不好,通过一些低级的异常,客户可以看到公司的一个技术水平)
4. 返回多个值,还要使用 out 来添加返回参数
5. 。。。
对于项目数量稍微多点的公司来说,接手多个项目的同事估计要吐血,所以项目间的业务通信规范是很有必要的。
解决方案
结合个人项目经验,定义一个专门用来封装返回值信息的通用类,如下:
/// <summary> /// 返回结果 /// </summary> public interface IResult { /// <summary> /// 结果状态码 /// </summary> ResultCode Code { get; set; } /// <summary> /// 提示信息 /// </summary> /// <example>操作成功</example> string Message { get; set; } /// <summary> /// 是否成功 /// </summary> bool Success { get; } } /// <summary> /// 返回的附带泛型数据 /// </summary> public interface IResult<TType> : IResult { /// <summary> /// 返回的附带数据 /// </summary> TType Data { get; set; } }
这个ResultCode是针对业务操作结果的自定义枚举,用来标志当前返回的一个业务结果
public enum ResultCode { /// <summary> /// 操作成功 ///</summary> [Display(Name = "操作成功")] Ok = 1, /// <summary> /// 操作失败 ///</summary> [Display(Name = "操作失败")] Fail = 11, /// <summary> /// 登陆失败 ///</summary> [Display(Name = "登陆失败")] LoginFail = 12, /// <summary> /// 没有该数据 ///</summary> [Display(Name = "没有数据")] NoRecord = 13, /// <summary> /// 用户不存在 ///</summary> [Display(Name = "用户不存在")] NoSuchUser = 14, /// <summary> /// 未登录 ///</summary> [Display(Name = "未登录")] Unauthorized = 20, /// <summary> /// 未授权 /// </summary> [Display(Name = "未授权")] Forbidden = 21, /// <summary> /// 无效Token /// </summary> [Display(Name = "无效Token")] InvalidToken = 22, /// <summary> /// 参数验证失败 /// </summary> [Display(Name = "参数验证失败")] InvalidData = 23, /// <summary> /// 无效用户 /// </summary> [Display(Name = "无效用户")] InvalidUser = 24 }
有了以上的接口,我们可以看一下具体实现
public class Result : IResult { private string _message; /// <summary> /// 是否成功 /// </summary> public bool Success => Code == ResultCode.Ok; /// <summary> /// 结果码 /// </summary> public ResultCode Code {get; set;} /// <summary> /// 提示信息 /// </summary> public string Message { get { return _message ?? Code.DisplayName(); } set { _message = value; } } /// <summary> /// 返回结果,默认成功 /// </summary> public Result() { Code = ResultCode.Ok; } /// <summary> /// 返回结果 /// </summary> /// <param name="code">状态码</param> /// <param name="message">提示信息</param> public Result(ResultCode code, string message = null) { Code = code; Message = message; } }
这里我们定义了实现类,注意默认的构造函数是返回成功的,这方便我们后面针对业务对这个返回结果再次进行扩展。细心的大家应该注意到了返回的提示信息,我们针对上面的自定义枚举的提示信息会进行显示,后面具体实现再看。先看一下我们的泛型返回结果的实现
/// <summary> /// 返回结果 /// </summary> public class Result<TType> : Result, IResult<TType> { /// <summary> /// cotr /// </summary> public Result() { } /// <summary> /// 返回结果 /// </summary> public Result(TType data) : base(ResultCode.Ok) { Data = data; } /// <summary> /// 返回结果 /// </summary> /// <param name="code">状态码</param> /// <param name="message">提示信息</param> public Result(ResultCode code, string message = null) : base(code, message) { } /// <summary> /// 返回结果 /// </summary> public Result(ResultCode code, string message = null, TType data = default(TType)) : base(code, message) { Data = data; } /// <summary> /// 返回业务数据 /// </summary> public TType Data { get; set; } }
好有了这些,我们在Result类中定义一些静态方法对结果进行封装,这样可以让我们在业务层进行快速的调用
/// <summary> /// 返回指定 Code /// </summary> public static Result FromCode(ResultCode code, string message = null) { return new Result(code, message); } /// <summary> /// 返回错误信息 /// </summary> public static Result FromError(string message, ResultCode code = ResultCode.Fail) { return new Result(code, message); } /// <summary> /// 返回成功 /// </summary> public static Result Ok(string message = null) { return FromCode(ResultCode.Ok, message); } /// <summary> /// 返回指定 Code /// </summary> public static Result<T> FromCode<T>(ResultCode code, string message = null) { return new Result<T>(code, message); } /// <summary> /// 返回指定 Code和提示信息 /// </summary> public static Result<T> FromCode<T>(ResultCode code, T data, string message = null) { return new Result<T>(code, message, data); } /// <summary> /// 返回错误信息 /// </summary> public static Result<T> FromError<T>(string message, ResultCode code = ResultCode.Fail) { return new Result<T>(code, message); } /// <summary> /// 返回数据 /// </summary> public static Result<T> FromData<T>(T data) { return new Result<T>(data); } /// <summary> /// 返回数据和提示信息 /// </summary> public static Result<T> FromData<T>(T data, string message) { return new Result<T>(ResultCode.Ok, message, data); } /// <summary> /// 返回成功 /// </summary> public static Result<T> Ok<T>(T data) { return FromData(data); }
好了有了上面这些,我们该如何调用呢?当我们需要直接返回成功时,我们可以这样
return Result.Ok();
前端接收到的结果如下:
当我们需要返回带有数据的结果时,我们可以这样:
var list = new List<string> { "lex1", "lex2" }; return Result.FromData(list);
前端接收到的结果如下:
当我们需要返回指定Code的时候,如下:
return Result.FromCode(ResultCode.LoginFail);
前端接收到的结果如下:
我们可以看到上面的提示信息是我们在枚举上定义的信息,这是我们在Result类中对Message进行了Code.DisplayName(),思想很简单,就是对枚举进行了扩展,利用DisplayAttribute的公用方法显示信息,那我们怎么知道什么时候调用DisplayAttribute的合适方法呢?
我们先定义一个类DisplayProperty,用来对应DisplayAttribute的各个属性
public enum DisplayProperty { /// <summary> /// 名称 /// </summary> Name, /// <summary> /// 短名称 /// </summary> ShortName, /// <summary> /// 分组名称 /// </summary> GroupName, /// <summary> /// 说明 /// </summary> Description, /// <summary> /// 排序 /// </summary> Order, /// <summary> /// 水印信息 /// </summary> Prompt, }
有了这个之后,我们的枚举扩展方法如下:
/// <summary> /// 获取枚举说明 /// </summary> public static string DisplayName(this Enum val) { return val.Display(DisplayProperty.Name) as string; } /// <summary> /// 获取枚举短名称说明 /// </summary> public static string DisplayShortName(this Enum val) { return val.Display(DisplayProperty.ShortName) as string; } /// <summary> /// 获取枚举水印信息 /// </summary> public static string DisplayPrompt(this Enum val) { return val.Display(DisplayProperty.Prompt) as string; } /// <summary> /// 获取枚举备注 /// </summary> public static string DisplayDescription(this Enum val) { return val.Display(DisplayProperty.Description) as string; } /// <summary> /// 获取枚举指定的显示内容 /// </summary> public static object Display(this Enum val, DisplayProperty property) { var enumType = val.GetType(); var str = val.ToString(); if (enumType.GetAttribute<FlagsAttribute>() != null && str.Contains(",")) { var array = str.Split(",", StringSplitOptions.RemoveEmptyEntries).Select(o => o.Trim()); var result = array.Aggregate("", (s, s1) => { var f = enumType.GetField(s1); if (f != null) {
//MethodInfo的扩展,方法在下面 var text = f.Display(property); return s.IsNullOrEmpty() ? text.ToString() : $"{s},{text}"; } return s; }); return result.IsNullOrEmpty() ? null : result; } var field = enumType.GetField(str); if (field != null) { return field.Display(property); } return null; }
再看针对MemberInfo的一个扩展,这里面就根据我们传入的DisplayProperty属性值调用了DisplayAttribute的对应方法
/// <summary> /// 获取枚举指定的显示内容 /// </summary> public static object Display(this MemberInfo memberInfo, DisplayProperty property) { if (memberInfo == null) return null; var display = memberInfo.GetAttribute<DisplayAttribute>(); if (display != null) { switch (property) { case DisplayProperty.Name: return display.GetName(); case DisplayProperty.ShortName: return display.GetShortName(); case DisplayProperty.GroupName: return display.GetGroupName(); case DisplayProperty.Description: return display.GetDescription(); case DisplayProperty.Order: return display.GetOrder(); case DisplayProperty.Prompt: return display.GetPrompt(); } } return null; }
到此我们的这个业务通讯结果已经可以了,再细想,有几个问题需要我们解决的:
1. ResultCode的意义?
2. 公司这么多项目都这样的话,如果某个系统需要新增一个提示或者英文不规范修改了,那会不会造成不一致呢?
后续文章会针对这些问题和可能存在的问题进行探讨!