【问题标题】:ServiceStack Request DTO designServiceStack 请求 DTO 设计
【发布时间】: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 人一样思考。

谁能告诉我正确的方向。

【问题讨论】:

    标签: c# .net servicestack


    【解决方案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&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结果。对于充当过滤器并返回多个在所需范围内的匹配结果的搜索服务,我使用 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 中添加了指向此内容的链接。
    • 我觉得这里的建议很有帮助。我在这里发布了一个后续问题:stackoverflow.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 - 不用担心。我总是喜欢阅读你的答案。也许我们需要开始在 StackOverflow 上轮班? :)
    • @paaschpa 感谢大家分享您的知识,我可以将两个答案都标记为已接受的答案。
    • 这种方法还有用吗? Web 服务如何通知用户错误请求或未找到?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-01-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多