为具有 Table Per Hierarchy 模型设计(MVCCore + EF 2.2)的实体实施 REST API 的最佳建议
Best advice to Implement REST API for entities with Table Per Hierarchy model design (MVCCore + EF 2.2)
我有几个派生的 classes Shippable
和 Downloadable
继承自相同的基础 class Product
并且它们每个都有自己的属性:
public abstract class Product : Entity<Guid>
{
public ProductType ProductType{ get; set; }
public string Name { get; set; }
}
public class Downloadable : Product
{
public Guid DownloadId { get; set; }
}
public class Shippable : Product
{
public Guid ShippingInfo { get; set; }
}
我为我的 DataTable 使用了 EF Core TBH 设计,因此此实体的所有属性都存储在 table 中:
builder.Entity<Product>(
b =>
{
b.ToTable(
options.TablePrefix + "Products",
options.Schema
);
b.HasKey(
x => x.Id
);
b.HasDiscriminator<ProductType>(
nameof(Product.ProductType)
)
.HasValue<Downloadable>(
ProductType.Downloadable
)
.HasValue<Shippable>(
ProductType.Shippable
);
}
我的项目是 NLayered DDD,所以我有一个应用程序层,它为 UI/REST API 方法生成业务逻辑并包含应用程序服务和 DTO。
我的问题是应用这个的最佳实践是什么。您对这个选项的看法:
1- 每个派生的业务服务 class 和派生的 DTO
2- 只有一项服务可以为所有服务提供共享 DTO(包含派生的 classes
的可为空属性
3- 您的建议
提前致谢。
我不完全确定你在找什么,但从 API 的角度来看,我会使用一个抽象的产品控制器,然后从中为每个特定的产品类型派生一个控制器:
[Route("api/[controller]")]
[ApiController]
public abstract class ProductController<TProduct, TProductDTO> : ControllerBase
where TProduct : Product, new()
where TProductDTO : class, new()
{
}
public class DownloadableController : ProductController<Downloadable, DownloadableDTO>
{
}
public class ShippableController : ProductController<Shippable, ShippableDTO>
{
}
然后,您使用通用类型 TProduct
和 TProductDTO
在抽象产品控制器上构建所有端点。例如,您可能有如下内容:
[HttpPost("")]
public async Task<ActionResult<TProductResource>> Create(TProductDTO dto)
{
var product = _mapper.Map<TProduct>(dto);
_context.Add(product);
await _context.SaveChangesAsync();
return CreatedAt("Get", new { product.Id }, _mapper.Map<TProductDTO>(product));
}
由于泛型类型,同样的方法适用于所有派生控制器,无需重新定义甚至覆盖,尽管您可以通过添加 virtual
关键字允许它被覆盖。不过,添加占位符方法通常会更好。例如,您可以执行以下操作:
private virtual Task BeforeCreateAsync(TProduct product, TProductDTO dto) =>
Task.CompletedTask;
private virtual Task AfterCreateAsync(TProduct product, TProductDTO dto) =>
Task.CompletedTask;
然后包装您的 SaveChangesAsync
调用:
_context.Add(product);
await BeforeCreateAsync(product, dto);
await _context.SaveChangesAsync();
await AfterCreateAsync(product, dto);
然后,在您派生的控制器中,您可以覆盖这些以在附加功能中存根:
private override async Task BeforeCreateAsync(Downloadable product, DownloadableDTO dto)
{
// do something specific to downloadable products
}
这样,您的大部分逻辑仍然是独立的。您可以添加尽可能多的对您的应用程序有意义的占位符方法。
我有几个派生的 classes Shippable
和 Downloadable
继承自相同的基础 class Product
并且它们每个都有自己的属性:
public abstract class Product : Entity<Guid>
{
public ProductType ProductType{ get; set; }
public string Name { get; set; }
}
public class Downloadable : Product
{
public Guid DownloadId { get; set; }
}
public class Shippable : Product
{
public Guid ShippingInfo { get; set; }
}
我为我的 DataTable 使用了 EF Core TBH 设计,因此此实体的所有属性都存储在 table 中:
builder.Entity<Product>(
b =>
{
b.ToTable(
options.TablePrefix + "Products",
options.Schema
);
b.HasKey(
x => x.Id
);
b.HasDiscriminator<ProductType>(
nameof(Product.ProductType)
)
.HasValue<Downloadable>(
ProductType.Downloadable
)
.HasValue<Shippable>(
ProductType.Shippable
);
}
我的项目是 NLayered DDD,所以我有一个应用程序层,它为 UI/REST API 方法生成业务逻辑并包含应用程序服务和 DTO。
我的问题是应用这个的最佳实践是什么。您对这个选项的看法:
1- 每个派生的业务服务 class 和派生的 DTO
2- 只有一项服务可以为所有服务提供共享 DTO(包含派生的 classes
的可为空属性3- 您的建议
提前致谢。
我不完全确定你在找什么,但从 API 的角度来看,我会使用一个抽象的产品控制器,然后从中为每个特定的产品类型派生一个控制器:
[Route("api/[controller]")]
[ApiController]
public abstract class ProductController<TProduct, TProductDTO> : ControllerBase
where TProduct : Product, new()
where TProductDTO : class, new()
{
}
public class DownloadableController : ProductController<Downloadable, DownloadableDTO>
{
}
public class ShippableController : ProductController<Shippable, ShippableDTO>
{
}
然后,您使用通用类型 TProduct
和 TProductDTO
在抽象产品控制器上构建所有端点。例如,您可能有如下内容:
[HttpPost("")]
public async Task<ActionResult<TProductResource>> Create(TProductDTO dto)
{
var product = _mapper.Map<TProduct>(dto);
_context.Add(product);
await _context.SaveChangesAsync();
return CreatedAt("Get", new { product.Id }, _mapper.Map<TProductDTO>(product));
}
由于泛型类型,同样的方法适用于所有派生控制器,无需重新定义甚至覆盖,尽管您可以通过添加 virtual
关键字允许它被覆盖。不过,添加占位符方法通常会更好。例如,您可以执行以下操作:
private virtual Task BeforeCreateAsync(TProduct product, TProductDTO dto) =>
Task.CompletedTask;
private virtual Task AfterCreateAsync(TProduct product, TProductDTO dto) =>
Task.CompletedTask;
然后包装您的 SaveChangesAsync
调用:
_context.Add(product);
await BeforeCreateAsync(product, dto);
await _context.SaveChangesAsync();
await AfterCreateAsync(product, dto);
然后,在您派生的控制器中,您可以覆盖这些以在附加功能中存根:
private override async Task BeforeCreateAsync(Downloadable product, DownloadableDTO dto)
{
// do something specific to downloadable products
}
这样,您的大部分逻辑仍然是独立的。您可以添加尽可能多的对您的应用程序有意义的占位符方法。