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 SemanticsResponse Types 分为 2 个不同的服务:

每个请求 DTO 中的每个属性都具有与 FindProducts 相同的语义,每个属性的作用类似于过滤器(例如 AND),而在 GetProduct 中它的作用类似于组合器(例如 OR)。服务还返回 IEnumerable&lt;Product&gt;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结果。对于充当过滤器并返回多个在所需范围内的匹配结果的搜索服务,我使用 FindSearch 动词来表示情况就是这样。

旨在实现自我描述的服务合同

同时尝试对您的每个字段名称进行描述,这些属性是您的公共 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);
    

根据用例而不是使用单独的 CreateBookingUpdateBooking 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:VS 2012 添加服务参考

ServiceStack 并返回一个流

使用 Servicestack.Text 进行 XML 反序列化

ServiceStack Json Serializer 忽略属性

ServiceStack - 检查单元测试中的 WSDL 更改

将 ServiceStack 客户端与区分大小写的 REST 服务一起使用