ServiceStack 请求 DTO 设计
Posted
技术标签:
【中文标题】ServiceStack 请求 DTO 设计【英文标题】:ServiceStack Request DTO design 【发布时间】:2013-04-02 09:22:10 【问题描述】:我是一名 .Net 开发人员,曾在 Microsoft Technologies 上开发 Web 应用程序。我正在尝试教育自己了解 Web 服务的 REST 方法。到目前为止,我很喜欢 ServiceStack 框架。
但有时我发现自己以我习惯使用 WCF 的方式编写服务。 所以我有一个问题困扰着我。
我有 2 个请求 DTO,所以有 2 个这样的服务:
[Route("/bookinglimit", "GET")]
[Authenticate]
public class GetBookingLimit : IReturn<GetBookingLimitResponse>
public int Id get; set;
public class GetBookingLimitResponse
public int Id get; set;
public int ShiftId get; set;
public DateTime StartDate get; set;
public DateTime EndDate get; set;
public int Limit get; set;
public ResponseStatus ResponseStatus get; set;
[Route("/bookinglimits", "GET")]
[Authenticate]
public class GetBookingLimits : IReturn<GetBookingLimitsResponse>
public DateTime Date get; set;
public class GetBookingLimitsResponse
public List<GetBookingLimitResponse> BookingLimits get; set;
public ResponseStatus ResponseStatus get; set;
从这些请求 DTO 中可以看出,我几乎对每项服务都有类似的请求 DTO,这似乎不是 DRY。
我尝试在GetBookingLimitsResponse
内部的列表中使用GetBookingLimitResponse
类,因为ResponseStatus
内部GetBookingLimitResponse
类被复制,以防我在GetBookingLimits
服务上出现错误。
我也有这些请求的服务实现,例如:
public class BookingLimitService : AppServiceBase
public IValidator<AddBookingLimit> AddBookingLimitValidator get; set;
public GetBookingLimitResponse Get(GetBookingLimit request)
BookingLimit bookingLimit = new BookingLimitRepository().Get(request.Id);
return new GetBookingLimitResponse
Id = bookingLimit.Id,
ShiftId = bookingLimit.ShiftId,
Limit = bookingLimit.Limit,
StartDate = bookingLimit.StartDate,
EndDate = bookingLimit.EndDate,
;
public GetBookingLimitsResponse Get(GetBookingLimits request)
List<BookingLimit> bookingLimits = new BookingLimitRepository().GetByRestaurantId(base.UserSession.RestaurantId);
List<GetBookingLimitResponse> listResponse = new List<GetBookingLimitResponse>();
foreach (BookingLimit bookingLimit in bookingLimits)
listResponse.Add(new GetBookingLimitResponse
Id = bookingLimit.Id,
ShiftId = bookingLimit.ShiftId,
Limit = bookingLimit.Limit,
StartDate = bookingLimit.StartDate,
EndDate = bookingLimit.EndDate
);
return new GetBookingLimitsResponse
BookingLimits = listResponse.Where(l => l.EndDate.ToShortDateString() == request.Date.ToShortDateString() && l.StartDate.ToShortDateString() == request.Date.ToShortDateString()).ToList()
;
如您所见,我也想在这里使用验证功能,因此我必须为我拥有的每个请求 DTO 编写验证类。所以我有一种感觉,我应该通过将类似的服务分组到一个服务中来降低我的服务数量。
但是我脑海中突然出现的问题是,我是否应该发送比客户对该请求所需的信息更多的信息?
我认为我的思维方式应该改变,因为我对当前编写的代码不满意,因为我像 WCF 人一样思考。
谁能告诉我正确的方向。
【问题讨论】:
【参考方案1】:为了让您了解在 ServiceStack 中设计基于消息的服务时应该考虑的差异,我将提供一些比较 WCF/WebApi 与 ServiceStack 方法的示例:
WCF vs ServiceStack API Design
WCF 鼓励您将 Web 服务视为普通的 C# 方法调用,例如:
public interface IWcfCustomerService
Customer GetCustomerById(int id);
List<Customer> GetCustomerByIds(int[] id);
Customer GetCustomerByUserName(string userName);
List<Customer> GetCustomerByUserNames(string[] userNames);
Customer GetCustomerByEmail(string email);
List<Customer> GetCustomerByEmails(string[] emails);
这就是 ServiceStack 中带有 New API 的相同服务合约的样子:
public class Customers : IReturn<List<Customer>>
public int[] Ids get; set;
public string[] UserNames get; set;
public string[] Emails get; set;
要记住的重要概念是,整个查询(也称为请求)是在请求消息(即请求 DTO)中捕获的,而不是在服务器方法签名中。采用基于消息的设计的明显直接好处是,上述 RPC 调用的任何组合都可以通过单个服务实现在 1 个远程消息中完成。
WebApi vs ServiceStack API Design
Likewise WebApi 促进了 WCF 所做的类似 C# 的 RPC Api:
public class ProductsController : ApiController
public IEnumerable<Product> GetAllProducts()
return products;
public Product GetProductById(int id)
var product = products.FirstOrDefault((p) => p.Id == id);
if (product == null)
throw new HttpResponseException(HttpStatusCode.NotFound);
return product;
public Product GetProductByName(string categoryName)
var product = products.FirstOrDefault((p) => p.Name == categoryName);
if (product == null)
throw new HttpResponseException(HttpStatusCode.NotFound);
return product;
public IEnumerable<Product> GetProductsByCategory(string category)
return products.Where(p => string.Equals(p.Category, category,
StringComparison.OrdinalIgnoreCase));
public IEnumerable<Product> GetProductsByPriceGreaterThan(decimal price)
return products.Where((p) => p.Price > price);
ServiceStack 基于消息的 API 设计
虽然 ServiceStack 鼓励您保留基于消息的设计:
public class FindProducts : IReturn<List<Product>>
public string Category get; set;
public decimal? PriceGreaterThan get; set;
public class GetProduct : IReturn<Product>
public int? Id get; set;
public string Name get; set;
public class ProductsService : Service
public object Get(FindProducts request)
var ret = products.AsQueryable();
if (request.Category != null)
ret = ret.Where(x => x.Category == request.Category);
if (request.PriceGreaterThan.HasValue)
ret = ret.Where(x => x.Price > request.PriceGreaterThan.Value);
return ret;
public Product Get(GetProduct request)
var product = request.Id.HasValue
? products.FirstOrDefault(x => x.Id == request.Id.Value)
: products.FirstOrDefault(x => x.Name == request.Name);
if (product == null)
throw new HttpError(HttpStatusCode.NotFound, "Product does not exist");
return product;
再次在 Request DTO 中捕捉到 Request 的本质。基于消息的设计还能够将 5 个独立的 RPC WebAPI 服务压缩为 2 个基于消息的 ServiceStack 服务。
按调用语义和响应类型分组
在此示例中,它根据 Call Semantics 和 Response Types 分为 2 个不同的服务:
每个请求 DTO 中的每个属性都具有与 FindProducts
相同的语义,每个属性的作用类似于过滤器(例如 AND),而在 GetProduct
中它的作用类似于组合器(例如 OR)。服务还返回 IEnumerable<Product>
和 Product
返回类型,这将需要在 Typed API 的调用站点中进行不同的处理。
在 WCF / WebAPI(和其他 RPC 服务框架)中,只要您有特定于客户端的要求,您就会在与该请求匹配的控制器上添加新的服务器签名。然而,在 ServiceStack 的基于消息的方法中,您应该始终考虑此功能属于何处以及您是否能够增强现有服务。您还应该考虑如何以通用方式支持特定于客户的需求,以便相同的服务可以使其他未来的潜在用例受益。
重构 GetBooking 限制服务
有了以上信息,我们就可以开始重构您的服务了。由于您有 2 个不同的服务会返回不同的结果,例如GetBookingLimit
返回 1 项,GetBookingLimits
返回许多项,它们需要保存在不同的服务中。
区分服务操作与类型
但是,您应该在每个服务唯一且用于捕获服务请求的服务操作(例如请求 DTO)与它们返回的 DTO 类型之间进行清晰的划分。请求 DTO 通常是动作,所以它们是动词,而 DTO 类型是实体/数据容器,所以它们是名词。
返回通用响应
在新 API 中,ServiceStack 响应 no longer require a ResponseStatus 属性,因为如果它不存在,则通用 ErrorResponse
DTO 将被抛出并在客户端上序列化。这使您免于让您的响应包含ResponseStatus
属性。话虽如此,我会将您的新服务合同重新考虑为:
[Route("/bookinglimits/Id")]
public class GetBookingLimit : IReturn<BookingLimit>
public int Id get; set;
public class BookingLimit
public int Id get; set;
public int ShiftId get; set;
public DateTime StartDate get; set;
public DateTime EndDate get; set;
public int Limit get; set;
[Route("/bookinglimits/search")]
public class FindBookingLimits : IReturn<List<BookingLimit>>
public DateTime BookedAfter get; set;
对于 GET 请求,当它们没有歧义时,我倾向于将它们排除在 Route 定义之外,因为它的代码更少。
保持一致的命名法
您应该在查询唯一或主键字段的服务上保留单词Get,即当提供的值与字段(例如Id)匹配时,它仅Gets 1结果。对于充当过滤器并返回多个在所需范围内的匹配结果的搜索服务,我使用 Find 或 Search 动词来表示情况就是这样。
旨在实现自我描述的服务合同
同时尝试对您的每个字段名称进行描述,这些属性是您的公共 API 的一部分,并且应该对它的作用进行自我描述。例如。仅通过查看服务合同(例如请求 DTO)我们不知道 Date 做了什么,我假设 BookedAfter,但它也可能是 BookedBefore 或 BookedOn,如果它只返回当天的预订。
这样做的好处是现在您的typed .NET clients 的呼叫站点变得更易于阅读:
Product product = client.Get(new GetProduct Id = 1 );
List<Product> results = client.Get(
new FindBookingLimits BookedAfter = DateTime.Today );
服务实现
我已经从您的请求 DTO 中删除了 [Authenticate]
属性,因为您可以改为在服务实现中指定一次,现在看起来像:
[Authenticate]
public class BookingLimitService : AppServiceBase
public BookingLimit Get(GetBookingLimit request) ...
public List<BookingLimit> Get(FindBookingLimits request) ...
错误处理和验证
有关如何添加验证的信息,您可以选择仅throw C# exceptions 并将您自己的自定义设置应用于它们,否则您可以选择使用内置的Fluent Validation,但您不需要注入将它们连接到您的服务中,因为您可以通过 AppHost 中的一行将它们全部连接起来,例如:
container.RegisterValidators(typeof(CreateBookingValidator).Assembly);
验证器是非接触式和无侵入性的,这意味着您可以使用分层方法添加它们并维护它们,而无需修改服务实现或 DTO 类。由于它们需要一个额外的类,我只会将它们用于具有副作用的操作(例如 POST/PUT),因为 GET 往往具有最少的验证,并且抛出 C# 异常需要更少的样板。因此,您可能拥有的验证器示例是在首次创建预订时:
public class CreateBookingValidator : AbstractValidator<CreateBooking>
public CreateBookingValidator()
RuleFor(r => r.StartDate).NotEmpty();
RuleFor(r => r.ShiftId).GreaterThan(0);
RuleFor(r => r.Limit).GreaterThan(0);
根据用例而不是使用单独的 CreateBooking
和 UpdateBooking
DTO,我将为两者重复使用相同的请求 DTO,在这种情况下,我将命名为 StoreBooking
。
【讨论】:
感谢您的见解。尽管我阅读了很多,但通过您自己的代码查看示例确实很有帮助。如果您有时间阅读此想要快速提问的问题,您建议保持 ServiceModel dll 依赖免费,但我有一种情况违反了此规则,我想在 ServiceInterface 和 WebApplication 之间共享自定义用户共享,所以我最终将 UserSession : AuthUserSession 放在里面ServiceModel dll,因此我需要将 ServiceStack 引用添加到 ServiceModel dll,因为我想共享该类。 请您开一个新问题,cmets 不是新问题的最佳场所。 这里有很多可靠的“最佳实践”。不确定它是否还没有,但这个节目肯定会进入 github wiki。 @kampsj 是的,在 wiki 的 Getting Started 部分的5) How to design a Message-Based API
中添加了指向此内容的链接。
我觉得这里的建议很有帮助。我在这里发布了一个后续问题:***.com/questions/39947319/…【参考方案2】:
“响应 Dtos”似乎没有必要,因为 ResponseStatus 属性是 no longer needed.。不过,我认为如果您使用 SOAP,您可能仍需要匹配的 Response 类。如果您删除 Response Dtos,您不再需要将 BookLimit 推入 Response 对象。此外,ServiceStack 的 TranslateTo() 也可以提供帮助。
以下是我将如何尝试简化您发布的内容...YMMV。
为 BookingLimit 创建一个 DTO - 这将是 BookingLimit 对所有其他系统的表示。
public class BookingLimitDto
public int Id get; set;
public int ShiftId get; set;
public DateTime StartDate get; set;
public DateTime EndDate get; set;
public int Limit get; set;
请求和 Dto 是 very important
[Route("/bookinglimit", "GET")]
[Authenticate]
public class GetBookingLimit : IReturn<BookingLimitDto>
public int Id get; set;
[Route("/bookinglimits", "GET")]
[Authenticate]
public class GetBookingLimits : IReturn<List<BookingLimitDto>>
public DateTime Date get; set;
不再返回 Reponse 对象...只是 BookingLimitDto
public class BookingLimitService : AppServiceBase
public IValidator AddBookingLimitValidator get; set;
public BookingLimitDto Get(GetBookingLimit request)
BookingLimitDto bookingLimit = new BookingLimitRepository().Get(request.Id);
//May need to bookingLimit.TranslateTo<BookingLimitDto>() if BookingLimitRepository can't return BookingLimitDto
return bookingLimit;
public List<BookingLimitDto> Get(GetBookingLimits request)
List<BookingLimitDto> bookingLimits = new BookingLimitRepository().GetByRestaurantId(base.UserSession.RestaurantId);
return
bookingLimits.Where(
l =>
l.EndDate.ToShortDateString() == request.Date.ToShortDateString() &&
l.StartDate.ToShortDateString() == request.Date.ToShortDateString()).ToList();
【讨论】:
道歉帕特,我只注意到你在我发帖后回答了,我通常尽量不重新回答你已经讨论过的问题,除非我注意到异常:) @mythz - 不用担心。我总是喜欢阅读你的答案。也许我们需要开始在 *** 上轮班? :) @paaschpa 感谢大家分享您的知识,我可以将两个答案都标记为已接受的答案。 这种方法还有用吗? Web 服务如何通知用户错误请求或未找到?以上是关于ServiceStack 请求 DTO 设计的主要内容,如果未能解决你的问题,请参考以下文章
使用 Servicestack.Text 进行 XML 反序列化
ServiceStack Json Serializer 忽略属性