SimpleResults

SimpleResults

结构化服务返回结果的轻量级库

SimpleResults是一个轻量级的.NET库,用于实现Result模式并结构化服务返回结果。它支持多种结果类型,如Result、ListedResult和PagedResult,能够优雅地处理错误和成功消息。该库提供了与ASP.NET Core的无缝集成,可将Result对象转换为ActionResult或IResult。此外,SimpleResults还支持Fluent Validation集成和结果对象到HTTP状态码的转换,为开发人员提供了灵活且高效的错误处理方案。

SimpleResults结果模式错误处理ASP.NET Core开源库Github开源项目

SimpleResults

SimpleResults downloads

SimpleResults-AspNetCore downloads

SimpleResults-FluentValidation downloads

SimpleResults-logo

这是一个简单的库,用于实现从服务返回的结果模式。它还提供了一种将结果对象转换为ActionResultIResult的机制。

这个库的灵感来自Ardalis.Result

查看API文档以获取有关此项目的更多信息。

目录

操作结果模式

结果设计模式的目的是让操作(方法)能够返回一个复杂的结果(对象),使消费者能够:

  • 访问操作的结果(如果存在)。
  • 访问操作的成功指示器。
  • 访问操作的失败指示器。
  • 访问结果的值(数据)(如果存在)。
  • 访问失败的原因(如果操作不成功)。
  • 访问错误或成功消息。
  • 访问错误消息集合。

为什么我要制作这个库?

  • 我设计这个库是为了在DentallApp项目中使用,以及根据我的需求在其他项目中使用。

  • 我想与社区分享我的知识。我热爱开源。

  • 我不想在所有情况下都抛出异常

为什么我不使用异常?

在开发开源库时,我通常会抛出异常,以立即提醒开发人员发生了意外错误并必须加以纠正。在这种情况下,抛出异常对我来说是有意义的,因为开发人员可以准确地知道错误的来源(通过查看堆栈跟踪)。然而,在开发应用程序时,我很少发现需要使用异常的情况。

例如,当普通用户输入空字段时,我可以抛出异常,但这对我来说没有意义,因为这是最终用户(通过用户界面管理系统)造成的错误。所以在这种情况下,抛出异常是没有用的,因为:

  • 异常对象中包含的堆栈跟踪对任何人都没有用,无论是最终用户还是开发人员。 这不是开发人员应该关心的错误。

  • 没有人关心错误的来源是在方法X还是Y中,这无关紧要。

  • 这不是一个意外错误。异常是为了指示意外错误而抛出的。意外错误是那些不期望发生的,且无法恢复的错误。

    • 例如,如果数据库服务器不在线,它将在应用程序中产生一个意外错误,因此应用程序无法恢复。

还有许多其他由最终用户造成的错误示例:电子邮件重复或密码不符合安全策略等。

我只在出现意外错误时抛出异常;否则,我会创建结果对象,并在方法中使用return语句,以在发生预期错误时立即终止执行。

预期错误和非预期错误的区别

了解预期错误和非预期错误之间的区别对于知道何时抛出异常是必要的。事实上,在实践中,第三方依赖项负责报告非预期错误,因此开发人员只需要关注识别业务应用程序的预期错误。

  • 预期错误是那些预期会发生的错误,我们倾向于恢复它们。它们也被称为可恢复错误。

    • 例如,空字段或重复的电子邮件。这些是预期会发生的错误,并且它们发生是正常的。
    • 要处理这些错误,使用结果模式很有用。
  • 非预期错误是那些不期望发生的错误,它们是不可恢复的。它们也被称为不可恢复错误。

    • 例如,不存在的数据库或错误输入的连接字符串。这些是不期望发生的错误,它们发生是不正常的。它们永远不应该发生,应该立即纠正。这是致命的。
    • 异常是为了表示非预期错误而设计的。

轶事

在工作中,我必须实现一个模块来生成一个报告,该报告对公司的收入和支出进行月度比较,因此需要创建一个负责计算每月余额百分比的函数:

Percentage.Calculate(double amount, double total);

如果total参数为零,将导致除以零(未定义操作),然而,这个值不是由最终用户提供的,而是由收入和支出报告模块提供的,但由于我没有正确实现这个模块,我创建了一个错误,所以算法由于某种奇怪的原因传递了一个零值(我称之为逻辑错误,由开发人员造成)。

由于我没有在Percentage.Calculate函数中抛出异常,我花了几分钟才找出错误的来源(我不知道问题是除以零)。

将浮点值除以零不会抛出异常;结果是非数字(NaN)。 这让我感到惊讶!我不知道!我期望会有一个异常,但事实并非如此。

如果我抛出了一个异常,我会很快找到错误,只需查看堆栈跟踪。在这种情况下,异常对象对我和其他开发人员来说非常有用,是的,除以零是一个非预期错误,应该抛出异常。

如果在所有情况下都使用异常会发生什么?

需要考虑一些细节:

  • 你的应用程序的新维护者将学会在所有情况下都抛出异常是可以的。这对他们的学习不利,因为他们并不真正理解C#中异常的设计目的。

  • 你的代码变得混乱,因为你没有遵循C#中异常的官方定义。

    • 是的,异常代表非预期错误。这是定义,改变它只会造成混乱。
  • 你需要创建继承自Exception类型的自定义类,否则你最终会在许多地方使用Exception类型。这种类型不向消费者(调用公共API的人)表达任何信息。

  • 您需要记录那些会抛出异常的方法,否则使用者将不知道该处理哪些异常,最终只能查看方法的源代码(这不太好)。

  • 性能。是的,抛出异常的开销很大。虽然在许多应用程序中可能不会有任何影响,但这并不是浪费资源的理由。欲了解更多信息,请参阅以下链接:

  • 如果你的项目是一个Web应用程序,你将需要找到一种机制将异常对象转换为HTTP状态码,因此你必须创建像InvalidDataException这样的基类以便从全局异常处理程序中捕获它。

    • 例如:WrongEmailException继承自InvalidDataException,而InvalidDataException又继承自Exception。 有必要考虑使用继承的类型层次结构(这增加了另一层复杂性)。

关于异常的有趣资源

安装

你可以在终端中运行以下任一命令:

dotnet add package SimpleResults dotted add package SimpleResults.AspNetCore dotnet add package SimpleResults.FluentValidation

SimpleResults包是主要库(核心)。其他两个包是对主库的补充(它们像是插件)。

概述

你必须在类文件的开头导入命名空间类型:

using SimpleResults;

这个库提供了四种主要类型:

  • Result
  • Result<TValue>
  • ListedResult<TValue>
  • PagedResult<TValue>PagedInfo

使用这些类型中的任何一种,你都可以处理错误,同时使用return语句生成错误。

这种方法提供了一种新的方式来使用return语句生成错误,而无需抛出异常。

有关这些类型的更多信息,请参阅API文档

使用Result类型

当你不想返回任何值时,可以使用Result类。

示例:

public class UserService { private readonly List<User> _users; public UserService(List<User> users) => _users = users; public Result Update(string id, string name) { if (string.IsNullOrWhiteSpace(id)) return Result.Invalid("ID是必需的"); if (string.IsNullOrWhiteSpace(name)) return Result.Invalid("名称是必需的"); var user = _users.Find(u => u.Id == id); if (user is null) return Result.NotFound(); user.Name = name; return Result.UpdatedResource(); } }

当你想返回一个值(例如一个User对象)时,可以使用Result<TValue>类。

示例:

public class UserService { private readonly List<User> _users; public UserService(List<User> users) => _users = users; public Result<User> GetById(string id) { if(string.IsNullOrWhiteSpace(id)) return Result.Invalid("ID是必需的"); var user = _users.Find(u => u.Id == id); if(user is null) return Result.NotFound(); return Result.Success(user, "找到用户"); } }

使用ListedResult类型

当你想返回一组值(例如User类型的对象集合)时,可以使用ListedResult<TValue>类。

示例:

public class UserService { private readonly List<User> _users; public UserService(List<User> users) => _users = users; public ListedResult<User> GetAll() { if(_users.Count == 0) return Result.Failure("未找到用户"); return Result.ObtainedResources(_users); } }

使用PagedResult类型

当你想在结果中包含分页信息和数据集合时,可以使用PagedResult<TValue>类。

示例:

public class UserService { private readonly List<User> _users; public UserService(List<User> users) => _users = users; public PagedResult<User> GetPagedList(int pageNumber, int pageSize) { if(pageNumber <= 0) return Result.Invalid("页码必须大于零"); int itemsToSkip = (pageNumber - 1) * pageSize; var data = _users .Skip(itemsToSkip) .Take(pageSize); if (data.Any()) { var pagedInfo = new PagedInfo(pageNumber, pageSize, _users.Count); return Result.Success(data, pagedInfo); } return Result.Failure("未找到结果"); } }

使用Result<T>类型创建资源

你可以通过使用Result.CreatedResource方法来告诉方法返回一个成功创建的资源作为结果。 此外,你可以使用CreatedGuid类来指定分配给创建的资源的ID。

示例:

public class UserService { private readonly List<User> _users; public UserService(List<User> users) => _users = users; public Result<CreatedGuid> Create(string name) { if(string.IsNullOrWhiteSpace(name)) return Result.Invalid("名称是必需的"); var guid = Guid.NewGuid(); _users.Add(new User { Id = guid.ToString(), Name = name }); return Result.CreatedResource(guid); } }

当使用整数作为标识符时,你也可以使用CreatedId类。

使用Entity Framework Core的示例:

public class UserModel { public int Id { get; set; } public string Name { get; set; } } public class UserService { private readonly DbContext _db; public UserService(DbContext db) => _db = db; public Result<CreatedId> Create(string name) { if(string.IsNullOrWhiteSpace(name)) return Result.Invalid("名称是必需的"); var user = new UserModel { Name = name }; _db.Add(user); _db.SaveChanges(); return Result.CreatedResource(user.Id); } }

设计错误和成功消息

你可以创建一个表示错误或成功消息的对象。优点是错误或成功的所有相关信息都封装在一个对象中。

示例:

public readonly ref struct StartDateIsAfterEndDateError { public string Message { get; } public StartDateIsAfterEndDateError(DateTime startDate, DateTime endDate) { Message = string.Format( "开始日期 {0} 晚于结束日期 {1}", startDate.ToString("yyyy-MM-dd"), endDate.ToString("yyyy-MM-dd")); } }

这种方法允许你更改消息的格式,而无需在其他地方进行更改。

然后你可以在你的服务中使用它:

public class UserService { public Result<List<User>> GetUsersByDateRange(DateTime startDate, DateTime endDate) { if(startDate > endDate) return Result.Invalid(new StartDateIsAfterEndDateError(startDate, endDate).Message); // 执行其他操作.. } }

与ASP.NET Core集成

你可以使用ToActionResult扩展方法将Result对象转换为Microsoft.AspNetCore.Mvc.ActionResult

你需要安装SimpleResults.AspNetCore包才能访问扩展方法。查看ResultExtensions类以找到所有扩展方法。

示例:

public class UserRequest { public string Name { get; init; } } [ApiController] [Route("[controller]")] public class UserController { private readonly UserService _userService; public UserController(UserService userService) => _userService = userService; [HttpPost] public ActionResult<Result<CreatedGuid>> Create([FromBody]UserRequest request) => _userService.Create(request.Name).ToActionResult(); [HttpPut("{id}")] public ActionResult<Result> Update(string id, [FromBody]UserRequest request) => _userService.Update(id, request.Name).ToActionResult(); [HttpGet("{id}")] public ActionResult<Result<User>> Get(string id) => _userService.GetById(id).ToActionResult(); [HttpGet("paged")] public ActionResult<PagedResult<User>> GetPagedList([FromQuery]PagedRequest request) => _userService .GetPagedList(request.PageNumber, request.PageSize) .ToActionResult();
[HttpGet] public ActionResult<ListedResult<User>> Get() => _userService.GetAll().ToActionResult(); }

请查看API文档以获取可用的扩展方法列表。

将TranslateResultToActionResult用作操作筛选器

您还可以使用TranslateResultToActionResult筛选器将Result对象转换为ActionResult

TranslateResultToActionResultAttribute类将在内部调用ToActionResult方法并执行转换。

示例:

[TranslateResultToActionResult] [ApiController] [Route("[controller]")] public class UserController { private readonly UserService _userService; public UserController(UserService userService) => _userService = userService; [HttpGet("{id}")] public Result<User> Get(string id) => _userService.GetById(id); }

Get操作的返回值是Result<User>在执行操作后,筛选器(即TranslateResultToActionResult)将运行并将Result<User>转换为ActionResult

查看源代码,它非常简单。

将操作筛选器添加为全局筛选器

如果您不想在每个控制器上使用筛选器,可以将其全局添加到所有控制器中(参见示例)。

builder.Services.AddControllers(options => { // 为所有控制器添加筛选器。 options.Filters.Add<TranslateResultToActionResultAttribute>(); });

这样您就不再需要在每个控制器或单个操作上添加TranslateResultToActionResult属性。

对Minimal APIs的支持

从版本2.3.0开始,添加了一个功能,可以将Result对象转换为Microsoft.AspNetCore.Http.IResult的实现。

您只需使用名为ToHttpResult的扩展方法。查看ResultExtensions类以找到所有扩展方法。

示例:

public static class UserEndpoint { public static void AddRoutes(this WebApplication app) { var userGroup = app .MapGroup("/User") .WithTags("User"); userGroup .MapGet("/", (UserService service) => service.GetAll().ToHttpResult()) .Produces<ListedResult<User>>(); userGroup .MapGet("/{id}", (string id, UserService service) => service.GetById(id).ToHttpResult()) .Produces<Result<User>>(); userGroup.MapPost("/", ([FromBody]UserRequest request, UserService service) => { return service.Create(request.Name).ToHttpResult(); }) .Produces<Result<CreatedGuid>>(); } }

您还可以使用TranslateResultToHttpResult筛选器将Result对象转换为IResult的实现。

TranslateResultToHttpResultFilter类将在内部调用ToHttpResult方法并执行转换。

示例:

public static class UserEndpoint { public static void AddRoutes(this WebApplication app) { var userGroup = app .MapGroup("/User") .WithTags("User") .AddEndpointFilter<TranslateResultToHttpResultFilter>(); userGroup .MapGet("/{id}", (string id, UserService service) => service.GetById(id)) .Produces<Result<User>>(); } }

端点处理程序返回一个Result<User>。在执行处理程序后,筛选器(即TranslateResultToHttpResult)将运行并将Result<User>转换为IResult的实现。

查看源代码,它非常简单。

使用ModelState属性进行验证

SimpleResults.AspNetCore包还为ModelStateDictionary类型添加了扩展方法。

查看ModelStateDictionaryExtensions类以找到所有扩展方法。

ModelStateDictionary类型包含要显示给客户端的验证错误。这些错误必须以某种方式包含在Result类型的实例中。

手动验证

手动验证直接在控制器操作中执行。

示例:

[TranslateResultToActionResult] [Route("[controller]")] public class OrderController : ControllerBase { private readonly OrderService _orderService; public OrderController(OrderService orderService) => _orderService = orderService; [HttpPost] public Result<CreatedGuid> Create([FromBody]CreateOrderRequest request) { if (ModelState.IsFailed()) return ModelState.Invalid(); return _orderService.Create(request); } }

在这个示例中,使用ModelState.IsFailed()(一个扩展方法)进行手动验证,如果模型状态失败,则返回无效的结果类型。ModelState.Invalid()的作用是将ModelStateDictionary实例转换为Result类型的实例,因此验证错误将添加到结果对象中。

在执行控制器操作后,TranslateResultToActionResult筛选器将把Result对象转换为ActionResult类型的实例。

您也可以直接在控制器操作中返回ActionResult,而不使用操作筛选器。

示例:

[Route("[controller]")] public class OrderController : ControllerBase { private readonly OrderService _orderService; public OrderController(OrderService orderService) => _orderService = orderService; [HttpPost] public ActionResult<Result<CreatedGuid>> Create([FromBody]CreateOrderRequest request) { if (ModelState.IsFailed()) return ModelState.BadRequest(); return _orderService .Create(request) .ToActionResult(); } }

ModelState.BadRequest()的行为类似于ModelState.Invalid(),区别在于前者返回BadRequestObjectResult类型的实例,其中包含Result类型的实例。

自动验证

您需要在Program.cs中进行设置,以在模型验证失败时将ModelStateDictionary类型的实例转换为Result类型的实例。

示例:

builder.Services.AddControllers() .ConfigureApiBehaviorOptions(options => { options.InvalidModelStateResponseFactory = (ActionContext context) => context.ModelState.BadRequest(); });

此委托仅在带有ApiControllerAttribute注解的操作上调用,并在发生模型验证失败时执行context.ModelState.BadRequest()调用。如果模型中发生验证失败,则控制器操作将永远不会被执行。

您的控制器不再需要执行手动验证,例如:

[ApiController] [TranslateResultToActionResult] [Route("[controller]")] public class OrderController : ControllerBase { private readonly OrderService _orderService; public OrderController(OrderService orderService) => _orderService = orderService; [HttpPost] public Result<CreatedGuid> Create([FromBody]CreateOrderRequest request) => _orderService.Create(request); }

ApiController是必要的,因为它允许激活ModelStateInvalid筛选器,以在执行控制器操作之前执行模型验证。

将Result对象转换为HTTP状态码

SimpleResults.AspNetCore包负责将Result对象的状态转换为HTTP状态码。 下表用作参考,以了解哪种结果类型对应哪个 HTTP 状态码:

结果类型HTTP 状态码
Result.Success200 - Ok
Result.CreatedResource201 - Created
Result.UpdatedResource200 - Ok
Result.DeletedResource200 - Ok
Result.ObtainedResource200 - Ok
Result.ObtainedResources200 - Ok
Result.File200 - Ok
Result.Invalid400 - Bad Request
Result.NotFound404 - Not Found
Result.Unauthorized401 - Unauthorized
Result.Conflict409 - Conflict
Result.Failure422 - Unprocessable Entity
Result.CriticalError500 - Internal Server Error
Result.Forbidden403 - Forbidden

与 Fluent Validation 集成

你需要安装 SimpleResults.FluentValidation 包以访问扩展方法。

示例:

public class UserService { public Result Create(CreateUserRequest request) { ValidationResult result = new CreateUserValidator().Validate(request); if(result.IsFailed()) return result.Invalid(); // 一些代码.. } }

查看 API 文档 以获取可用扩展方法的列表。

示例

你可以在以下项目中找到完整且功能性的示例:

语言设置

SimpleResults 包含响应消息的资源。查看源代码

目前只有两种资源:

  • ResponseMessages.resx。包含英文消息。
  • ResponseMessages.es.resx。包含西班牙语消息。

这些资源的加载取决于你的区域设置。 例如,如果你的计算机语言设置为西班牙语,将加载 ResponseMessages.es.resx 资源。 同样,如果设置为英语,将加载默认资源:ResponseMessages.resx

如果设置为法语,由于没有名为 ResponseMessages.fr.resx 的资源,将加载默认资源(即 ResponseMessages.resx)。

你可以显式指定文化,以确保无论计算机的语言设置如何,都会加载特定资源:

Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo("es");

在 ASP.NET Core 应用程序中,使用 UseRequestLocalization 扩展方法:

app.UseRequestLocalization("es");

贡献

欢迎任何形式的贡献!请记住,你不仅可以在代码方面做出贡献,还可以改进文档或测试。

请遵循以下步骤:

  • Fork 项目
  • 创建你的特性分支 (git checkout -b my-new-feature)
  • 提交你的更改 (git commit -am '添加了某个特性')
  • 将分支推送到远程仓库 (git push origin my-new-feature)
  • 创建新的 Pull Request

编辑推荐精选

Keevx

Keevx

AI数字人视频创作平台

Keevx 一款开箱即用的AI数字人视频创作平台,广泛适用于电商广告、企业培训与社媒宣传,让全球企业与个人创作者无需拍摄剪辑,就能快速生成多语言、高质量的专业视频。

即梦AI

即梦AI

一站式AI创作平台

提供 AI 驱动的图片、视频生成及数字人等功能,助力创意创作

扣子-AI办公

扣子-AI办公

AI办公助手,复杂任务高效处理

AI办公助手,复杂任务高效处理。办公效率低?扣子空间AI助手支持播客生成、PPT制作、网页开发及报告写作,覆盖科研、商业、舆情等领域的专家Agent 7x24小时响应,生活工作无缝切换,提升50%效率!

TRAE编程

TRAE编程

AI辅助编程,代码自动修复

Trae是一种自适应的集成开发环境(IDE),通过自动化和多元协作改变开发流程。利用Trae,团队能够更快速、精确地编写和部署代码,从而提高编程效率和项目交付速度。Trae具备上下文感知和代码自动完成功能,是提升开发效率的理想工具。

AI工具TraeAI IDE协作生产力转型热门
蛙蛙写作

蛙蛙写作

AI小说写作助手,一站式润色、改写、扩写

蛙蛙写作—国内先进的AI写作平台,涵盖小说、学术、社交媒体等多场景。提供续写、改写、润色等功能,助力创作者高效优化写作流程。界面简洁,功能全面,适合各类写作者提升内容品质和工作效率。

AI辅助写作AI工具蛙蛙写作AI写作工具学术助手办公助手营销助手AI助手
问小白

问小白

全能AI智能助手,随时解答生活与工作的多样问题

问小白,由元石科技研发的AI智能助手,快速准确地解答各种生活和工作问题,包括但不限于搜索、规划和社交互动,帮助用户在日常生活中提高效率,轻松管理个人事务。

热门AI助手AI对话AI工具聊天机器人
Transly

Transly

实时语音翻译/同声传译工具

Transly是一个多场景的AI大语言模型驱动的同声传译、专业翻译助手,它拥有超精准的音频识别翻译能力,几乎零延迟的使用体验和支持多国语言可以让你带它走遍全球,无论你是留学生、商务人士、韩剧美剧爱好者,还是出国游玩、多国会议、跨国追星等等,都可以满足你所有需要同传的场景需求,线上线下通用,扫除语言障碍,让全世界的语言交流不再有国界。

讯飞智文

讯飞智文

一键生成PPT和Word,让学习生活更轻松

讯飞智文是一个利用 AI 技术的项目,能够帮助用户生成 PPT 以及各类文档。无论是商业领域的市场分析报告、年度目标制定,还是学生群体的职业生涯规划、实习避坑指南,亦或是活动策划、旅游攻略等内容,它都能提供支持,帮助用户精准表达,轻松呈现各种信息。

AI办公办公工具AI工具讯飞智文AI在线生成PPTAI撰写助手多语种文档生成AI自动配图热门
讯飞星火

讯飞星火

深度推理能力全新升级,全面对标OpenAI o1

科大讯飞的星火大模型,支持语言理解、知识问答和文本创作等多功能,适用于多种文件和业务场景,提升办公和日常生活的效率。讯飞星火是一个提供丰富智能服务的平台,涵盖科技资讯、图像创作、写作辅助、编程解答、科研文献解读等功能,能为不同需求的用户提供便捷高效的帮助,助力用户轻松获取信息、解决问题,满足多样化使用场景。

热门AI开发模型训练AI工具讯飞星火大模型智能问答内容创作多语种支持智慧生活
Spark-TTS

Spark-TTS

一种基于大语言模型的高效单流解耦语音令牌文本到语音合成模型

Spark-TTS 是一个基于 PyTorch 的开源文本到语音合成项目,由多个知名机构联合参与。该项目提供了高效的 LLM(大语言模型)驱动的语音合成方案,支持语音克隆和语音创建功能,可通过命令行界面(CLI)和 Web UI 两种方式使用。用户可以根据需求调整语音的性别、音高、速度等参数,生成高质量的语音。该项目适用于多种场景,如有声读物制作、智能语音助手开发等。

下拉加载更多