dotnet api 请求/响应 类

dotnet api request / response classes

我目前正在寻找一种优雅的方式来决定请求和响应对象。假设我创建了一个动物,我的 API 生成了该动物的 ID。我不希望有人在请求正文中提供 ID。我也不希望将 ID 显示为请求示例,例如招摇过市 UI.

如果我 return 之后创建动物,那么应该给出 ID,并且 Swagger UI 应该显示响应的 ID 作为示例。

我唯一的想法是为动物创造两个 类:

有更好的方法吗?

My only idea for this is to create two classes for the animal: PetRequest and PetResponse

不,这正是你想要做的(好吧,除了“将 PetRequest 转换为 Pet,不要那样做,改用 AutoMapper 或类似的) .

我认为简单地为所有 use-cases:

重复使用一个 DTO 合约是一个短视的( 或者只是懒惰 )的错误

object/class/type-design 中的一个常见咒语是“make invalid states unrepresentable" (it is not without its detractors though) - 这意味着如果 Pet object 不能有有意义的 PetId 属性(例如,因为还没有 INSERTed 得到 IDENTITY/AUTONUMBER 值)那么它不应该有 PetId 属性 at all - in-turn 意味着您将需要单独的 DTO 类型:NewPetExistingPet(或 EditPet 或类似类型)。

您可以(ab)使用从基础 PetValues DTO class 继承来避免重复自己或手动维护 object 属性的 copy+pasta 集,但我觉得使用继承代替 mixin(C# 仍然不支持,唉)是个坏主意,因为你会 end-up 无意中包含不合适的成员,而且因为如果你想使用 immutable类型作为 DTO 那么你将 运行 陷入 immutable 类型的构造函数的痛苦之中,因为 C#(仍然)没有自动构造函数参数继承(即你不能将新成员添加到基本 immutable DTO 类型,而无需更新所有子classes 的构造函数来为新成员添加这些新参数。

不幸的是,由于工具的限制或其他原因,Swagger 不 play-nice 具有 DTO 类型继承,但实际上我发现这不是问题,因为工具可以生成 C#/.NET DTO 类型 from Swagger JSON 将允许您自定义生成的输出。无论如何,多态性不应该是 DTOs/data-contracts 的一个特征:它们应该是 record-style 结构化数据的表示,而不是 mutable object 图。

无论如何,我会这样做(注意:这是我的个人风格,同时假设启用了 C# 8.0+ non-nullable reference-types,并且所有 DTO 成员都是 non-nullable):

// Use this interface type to facilitate mapping between DTOs and internal business/domain types for as long as they're generally compatible.
// So you won't be able to use this type indefinitely into the future once your business/domain types start to evolve over-time as your DTOs will be frozen or versioned as not to break remote clients.
internal interface IReadOnlyPetValues
{
    String Name { get; }
    String Kind { get; }
}

// Using `record class` saves on a LOT of repetitiveness.
// Also, be sure to set explicit [JsonProperty] names (in camelCase) so renamed C#/.NET properties won't change the rendered JSON property names (and also protects you from environments using PascalCase serialization by default, philistines!)

public record class NewPetDto(
    [JsonProperty("name")] String Name,
    [JsonProperty("kind")] String Kind
) : IReadOnlyPetValues;

public record class EditPetDto(
    [JsonProperty("petId")] Int32 PetId,
    [JsonProperty("name")] String Name,
    [JsonProperty("kind")] String Kind
) : IReadOnlyPetValues;

public static class PetDtoMapping
{
    public static void CopyDtoToPetEntity( this IReadOnlyPetValues dto, Pet entity )
    {
        entity.Name = dto.Name;
        entity.Kind = dto.Kind;
    }

    public static EditPetDto ToResponseDto( this Pet entity )
    {
        if( entity.PetId < 1 ) throw new ArgumentException( message: "This Pet has not been saved yet and so cannot be returned in a response.", paramName: nameof(entity) );

        return new EditPetDto(
            PetId: entity.PetId,
            Name : entity.Name,
            Kind : entity.Kind,
        );
    }

    // You don't need a factory for `NewPetDto` as you'll only ever receive those from remote clients, your program won't need to return a `NewPetDto` as a response.
}

所以你的 ASP.NET 核心 PetApiController class 可能看起来像这样:

[ApiController]
public class PetApiController : Controller
{
    // (ctor PetDbContext DI omited for brevity)

    [HttpPost("/pets/new")]
    [Produces( typeof(EditPetDto), 201 )]
    public async Task<IActionResult> PostNewPet( [FromBody] NewPetDto dto )
    {
        // (this.ModelState validation omited for brevity)

        Pet newPetEntity = new Pet();
        dto.CopyDtoToPetEntity( newPetEntity );
        this.db.Pets.Add( newPetEntity );
        await this.db.SaveChangesAsync();
        
        EditPetDto responseDto = newPetEntity.ToResponseDto();
        return this.Created( "/pets/{$newPetEntity.PetId}", responseDto ); // HTTP 201 Created with Location: header.
    }

    [HttpGet("/pets/{petId:int}")]
    [Produces( typeof(EditPetDto), 200 )]
    public async Task<IActionResult> PostNewPet( [FromRoute] Int32 petId )
    {
        Pet pet = await this.db.Pets.Where( p => p.PetId == petId ).SingleAsync();
        EditPetDto responseDto = newPetEntity.ToResponseDto();
        return this.OK( responseDto );
    }

    [HttpPut("/pets/{petId:int}")]
    [Produces( typeof(EditPetDto), 200 )]
    public async Task<IActionResult> PutEditedPet( [FromRoute] Int32 petId, [FromBody] EditPetDto dto ) )
    {
        Pet pet = await this.db.Pets.Where( p => p.PetId == petId ).SingleAsync();
        dto.CopyDtoToPetEntity( pet );
        await this.db.SaveChangesAsync();

        EditPetDto responseDto = pet.ToResponseDto();
        return this.OK( responseDto );
    }
}

我想说的另一个重点是你不应该使用你的 domain/business 实体类型作为 DTO 类型,即使是继承的(并且 尤其是 不是那些使用Entity Framework)。这是出于多种原因:

  • DTO(和一般的 data-contracts)是领域模型(我的意思是一般方式)的某些组合的 特定表示 ,这将不可避免地发展与您的 内部表示 分开,这会引入版本控制问题。您需要能够改进您的内部模型(和数据库设计),而不用担心使用旧版本的 DTO 库破坏您的外部客户端。
  • 即使您的 DTO 总是将 1:1 映射到您的 entity/domain classes(提示:它们不会),您 也不会 想要公开诸如 Users table 的 PasswordHash 列之类的东西 - 因为继承不能用于删除成员,所以你需要手动维护属性列表以“始终公开"/"never expose" - 这会增加 human-error 导致意外数据泄露的风险。
  • DTO 通常也往往需要加载其他相关实体才能正确填充它们,例如您的 OrderDto 可能还需要包括 OrderItemDto object 的列表,除非您真的希望您的远程客户端分别为每个实体发出数百个请求:这会给您带来可怕的后果整体系统性能,也使得无法对多个实体 object 执行有意义的交易操作(例如,使用单个 HTTP 请求同时编辑 Order 的 header 详细信息,同时还添加新 OrderItem 行)。
  • 从不 使用 Entity Framework 实体 classes 作为 DTO 的主要原因是因为 EF 使用代理子 classes 和 lazily-loaded 默认情况下的导航属性,因此只需将 EF 实体 object 传递给 JSON 序列化程序,然后通过以下方式使其 无限遍历 整个数据库那些 lazily-loaded 导航属性。

此外,考虑使用 JSON PATCH 而不是 PUT,因为这通常更 forwards-compatible(好像 remote-client 想要更新 object 通过覆盖一些 property-values 他们只发送一个较小的“将这些属性设置为这些值”消息,而不是“将所有属性更新为所有这些值 exactly” , 所以如果远程客户端使用的是属性较少的旧版本 DTO 但服务器使用的是属性更多的更现代版本, 那么默认情况下新属性将在 C# 中显示为 null (而不是 undefined 与 JSON/TypeScript) 一样,因此服务器随后将用 null 覆盖这些新属性的任何值,从而导致无意的信息丢失。

另一种选择:使用 a GraphQL or OData 层,它可以为您处理很多单调乏味的事情。