dotnet api 请求/响应 类
dotnet api request / response classes
我目前正在寻找一种优雅的方式来决定请求和响应对象。假设我创建了一个动物,我的 API 生成了该动物的 ID。我不希望有人在请求正文中提供 ID。我也不希望将 ID 显示为请求示例,例如招摇过市 UI.
如果我 return 之后创建动物,那么应该给出 ID,并且 Swagger UI 应该显示响应的 ID 作为示例。
我唯一的想法是为动物创造两个 类:
- PetRequest & Pet(响应)并将 PetRequest 投射到 Pet。
有更好的方法吗?
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 类型:NewPet
和 ExistingPet
(或 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
覆盖这些新属性的任何值,从而导致无意的信息丢失。
我目前正在寻找一种优雅的方式来决定请求和响应对象。假设我创建了一个动物,我的 API 生成了该动物的 ID。我不希望有人在请求正文中提供 ID。我也不希望将 ID 显示为请求示例,例如招摇过市 UI.
如果我 return 之后创建动物,那么应该给出 ID,并且 Swagger UI 应该显示响应的 ID 作为示例。
我唯一的想法是为动物创造两个 类:
- PetRequest & Pet(响应)并将 PetRequest 投射到 Pet。
有更好的方法吗?
My only idea for this is to create two classes for the animal:
PetRequest
andPetResponse
不,这正是你想要做的(好吧,除了“将 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 类型:NewPet
和 ExistingPet
(或 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
覆盖这些新属性的任何值,从而导致无意的信息丢失。