.NET Web API 中的统一 API 响应与结果模式

作者:微信公众号:【架构师老卢】
8-29 13:59
11

概述:U.NET 项目中的 nified API 响应简化了应用程序之间的通信,强调了软件开发的一致性和清晰度。本文描述了实现统一 API 响应所涉及的优势和过程步骤。什么是统一 API 响应?统一 API 响应本质上是 API 端点生成的响应的标准化格式。无论请求的结果如何,响应都遵循统一的结构,通常包含在 Result 对象中。这种统一性提高了清晰度,并简化了开发人员和客户的处理。在 .NET 中建立统一的 API 响应:首先,我们定义 Error 类,它封装了消息。您可以使用 Implicit Conversion 轻松地从 String 更改为 Error,反之亦然。public class

U.NET 项目中的 nified API 响应简化了应用程序之间的通信,强调了软件开发的一致性和清晰度。本文描述了实现统一 API 响应所涉及的优势和过程步骤。

什么是统一 API 响应?

统一 API 响应本质上是 API 端点生成的响应的标准化格式。无论请求的结果如何,响应都遵循统一的结构,通常包含在 Result 对象中。这种统一性提高了清晰度,并简化了开发人员和客户的处理。

在 .NET 中建立统一的 API 响应:

首先,我们定义 Error 类,它封装了消息。您可以使用 Implicit Conversion 轻松地从 String 更改为 Error,反之亦然。

public class Error  
{  
    public Error(string message)  
    {  
        Message = message;  
    }  
  
    public string Message { get; }  
  
    public static Error None => new(string.Empty);  
  
    public static implicit operator Error(string message) => new(message);  
  
    public static implicit operator string(Error error) => error.Message;  
}

这个由 Result 类使用的类将用作响应对象。让我们创建具有两个属性的 Result 类:

  1. Error:从上面定义的类返回 Error Object。
  2. IsSuccess:此 Boolean 属性用于指示操作是成功还是失败。
public class Result  
{  
    public Result(bool isSuccess, Error error)  
    {  
        IsSuccess = isSuccess;  
        Error = error;  
    }  
  
    public bool IsSuccess { get; }  
    public Error Error { get; }  
  
    public static Result Success() => new(true, Error.None);  
  
    public static Result Failure(Error error) => new(false, error);  
  
    public static Result<T> Success<T>(T data) => new(true, Error.None, data);  
  
    public static Result<T> Failure<T>(Error error) => new(false, error, default);  
}

看看这两个非泛型方法,Success 和 Failure

  • 该方法 Success 返回一个新的 Result 对象。如果操作成功,则 Error 属性将为 None,并且它将向使用者返回一条指示成功的消息。
  • Failure 方法返回一个 Error 对象,其中包含一条指定的消息,指示失败。

暂时跳过泛型方法

我现在知道你心中的问题了:

如果响应包含数据,我该怎么办?我看到您指示操作成功或失败。

请保持冷静,不要混淆;现在一切都会清楚的。

如您所见,您可以使用 Result 类返回统一响应,但不返回数据

让我们创建泛型 Result 类包含名为 data 的属性,该属性返回带有 data 的 Result

public class Result<T> : Result  
{  
    public T? Data { get; }  
  
    public Result(bool isSuccess, Error error, T? data) : base(isSuccess, error)  
    {  
        Data = data;  
    }  
}

在这个类中,你会看到 'Result<T>' 继承自 Result,并包含返回带有 Data 的 Response 的 Data 属性

现在你可以问我:

您有两个 C-Sharp 文件。使用相同的名称,您是怎么做到的?

听着,我不是魔术师,但在构建统一 API 响应时,您可以执行以下操作:

您在此处看到 Generic Result 类,我将其命名为 'ResultT.cs'

这解释了为什么这些泛型方法存在于非泛型 Result 类中。

 public static Result<T> Success<T>(T data) => new(true, Error.None, data);  
  
 public static Result<T> Failure<T>(Error error) => new(false, error, default);

通过这种方法,我们终止了 Result Object Response,并且 Result 类已准备好使用它。

准备好查看响应结果!

我不知道您在构建 API 时使用的特定架构模式;但是,它可以在您选择的架构模式中实现。

  • 例如,在 3 层架构中,您可以将统一响应类放置在域层中。这将允许业务逻辑层无缝使用它们,从而确保一致性和易用性。
  • 或者,在 onion 架构中,这些类可以驻留在存储库层中。然后,服务层将负责在将它们呈现给表示层之前使用和可能转换它们。
  • 无论选择哪种架构模式,统一响应类都有助于增强代码组织、可维护性以及整体 API 设计。

让我们保持简单

在上面的项目中,我对 3 层架构进行了模拟

项目施工为:

1- 域层

  • Models、Shared 和 User Samples 文件夹

2- 业务逻辑层

  • Services 文件夹

3- 表示层

  • 控制器文件夹

下面是一个使用 User 模型的示例。

public class User  
{  
    public int Id { get; set; }  
    public string Name { get; set; }  
    public string Email { get; set; }  
}

我们将创建一个模拟数据生成器并建立一个虚构的存储库模式,以促进用户模型的 crud 操作。

public class UserSamplesList  
{  
    private static List<User> _users = new()  
    {  
    new User { Id = 1, Name = "John Doe", Email = "joh@gmail.com"},  
    new User { Id = 2, Name = "Jane Smith", Email = "jane@gmail.com"},  
    new User { Id = 3, Name = "Alice Johnson", Email = "alice@gmail.com"},  
    new User { Id = 4, Name = "Bob Brown", Email = "bob@gmail.com"},  
    new User { Id = 5, Name = "Emily Davis", Email = "emily@gmail.com"},  
    new User { Id = 6, Name = "Michael Wilson", Email = "michael@gmail.com"},  
    new User { Id = 7, Name = "Sophia Taylor", Email = "sophia@gmail.com"},  
    new User { Id = 8, Name = "James Martinez", Email = "james@gmail.com"},  
    new User { Id = 9, Name = "Olivia Garcia", Email = "olivia@gmail.com"},  
    new User { Id = 10, Name = "William Rodriguez", Email = "william@gmail.com"}  
    };  
  
    public static IEnumerable<User> GetUsers()  
    {  
        return _users;  
    }  
  
    public static User GetUser(int id)  
    {  
        return _users.FirstOrDefault(u => u.Id == id);  
    }  
  
    public static void AddUser(User user)  
    {  
        _users.Add(user);  
    }  
  
    public static void UpdateUser(User user)  
    {  
        var existingUser = _users.FirstOrDefault(u => u.Id == user.Id);  
        if (existingUser != null)  
        {  
            existingUser.Name = user.Name;  
            existingUser.Email = user.Email;  
        }  
    }  
}

Domain Layer 已成功完成。

在 Service 层中使用 Result 类

  • 'IUserService' 接口中,每个方法都将返回一个 'Result' 对象,确保跨服务层的标准化响应处理。
public interface IUserService  
{  
    Task<Result<IEnumerable<User>>> GetUsersAsync();  
  
    Task<Result<User>> GetUserAsync(int id);  
  
    Task<Result> CreateUserAsync(User user);  
  
    Task<Result> UpdateUserAsync(User user);  
}

实现 'IUserService' 接口。

public class UserService : IUserService  
{  
    public Task<Result> CreateUserAsync(User user)  
    {  
        if (user is null)  
            return Task.FromResult(Result.Failure("User is null"));  
  
        UserSamplesList.AddUser(user);  
        return Task.FromResult(Result.Success());  
    }  
  
    public Task<Result<User>> GetUserAsync(int id)  
    {  
        var user = UserSamplesList.GetUser(id);  
        if (user == null)  
        {  
            return Task.FromResult(Result.Failure<User>($"User with id {id} not found"));  
        }  
  
        return Task.FromResult(Result.Success(user));  
    }  
  
    public Task<Result<IEnumerable<User>>> GetUsersAsync()  
    {  
        var users = UserSamplesList.GetUsers();  
        return Task.FromResult(Result.Success(users));  
    }  
  
    public Task<Result> UpdateUserAsync(User user)  
    {  
        if (user == null)  
        {  
            return Task.FromResult(Result.Failure("User is null"));  
        }  
  
        UserSamplesList.UpdateUser(user);  
        return Task.FromResult(Result.Success());  
    }  
}
  • 如前所述,**“IUserService”**接口中的所有方法都返回“Result”对象,无论它们是否返回数据。

定义 'UserController'

  • 实现两个终端节点(一个用于 GET,一个用于 POST),以观察有数据的结果和没有数据的结果。
using Microsoft.AspNetCore.Mvc;
using UnifiedApiResponse.Models;
using UnifiedApiResponse.Services;

namespace UnifiedApiResponse.Controllers;

[Route("api/[controller]")]
[ApiController]
public class UserController : ControllerBase
{
    private readonly IUserService _userService;

    public UserController(IUserService userService)
    {
        _userService = userService;
    }

    [HttpGet()]
    public async Task<IActionResult> GetUsers()
    {
        var result = await _userService.GetUsersAsync();
        return result.IsSuccess
            ? Ok(result)
            : NotFound(result.Error);
    }

    [HttpPost]
    public async Task<IActionResult> CreateUser(User user)
    {
        var result = await _userService.CreateUserAsync(user);
        return result.IsSuccess
            ? Ok(result)
            : BadRequest(result.Error);
    }
}

如果调用 get 方法,则响应的结构将如下所示。

{
  "data": [
    {
      "id": 1,
      "name": "John Doe",
      "email": "joh@gmail.com"
    },
    {
      "id": 2,
      "name": "Jane Smith",
      "email": "jane@gmail.com"
    }
  ],
  "isSuccess": true,
  "error": {
    "message": ""
  }
}

如果 post 方法遇到失败,响应将按如下方式构建:

{  
"isSuccess": false,  
"data": null,  
"error": "Failed to create user: Email is already in use."  
}

使用统一响应的好处:

  • 一致性: 通过统一的响应结构简化处理。
  • 清晰:清楚地传达运营结果和错误。
  • 轻松:通过标准化响应促进客户端集成。
  • 错误处理:有效地传达错误以简化流程。
  • 故障 排除。
  • 可维护性:确保设计一致性,便于维护。
阅读排行