简体   繁体   English

ServiceStack请求DTO设计

[英]ServiceStack Request DTO design

I am a .Net developer used to develop web application on Microsoft Technologies. 我是一名.Net开发人员,用于在Microsoft Technologies上开发Web应用程序。 I am trying to educate myself to understand REST approach for web services. 我正在努力教育自己了解Web服务的REST方法。 So far i am loving the ServiceStack framework. 到目前为止,我喜欢ServiceStack框架。

But sometimes i find myself to write services in a fashion that i am used to with WCF. 但有时我发现自己以一种我习惯使用WCF的方式编写服务。 So I have a question which bugs me. 所以我有一个问题让我烦恼。

I have 2 request DTO's so 2 services like these: 我有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; }
}

As seen on these Request DTO's i have similar request DTO's nearly for every services and this seems like not DRY. 正如在这些请求DTO上看到的,我有类似的请求DTO几乎为每个服务,这似乎不干。

I tried to use GetBookingLimitResponse class in a list inside GetBookingLimitsResponse for that reason ResponseStatus inside GetBookingLimitResponse class is dublicated in case i have an error on GetBookingLimits service. 我试图用GetBookingLimitResponse类在列表里面GetBookingLimitsResponse出于这个原因ResponseStatusGetBookingLimitResponse的情况下,我有一个错误类dublicated GetBookingLimits服务。

Also I have service implementations for these requests like : 我也有这些请求的服务实现,如:

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()
        };
    }
}

As you see i also want to use Validation Feature here, so i have to write validation classes for every request DTO i have. 如你所见,我也想在这里使用验证功能,所以我必须为每个DTO请求编写验证类。 So i have a feeling that i should keep my service number low by grouping similar services into one service. 所以我觉得我应该通过将类似的服务分组到一个服务来保持我的服务号码低。

But the question here that pops up in my mind that should i send more information than client need for that request ? 但是在我脑海中突然出现的问题是,我应该发送的信息多于客户对该请求的需求吗?

I think my way of thinking should change because i am not happy with current code which i wrote thinking like a WCF guy. 我认为我的思维方式应该改变,因为我对当前的代码感到不满意,我写的这个代码就像一个WCF人。

Can someone show me the right direction to follow. 有人能告诉我正确的方向。

To give you a flavor of the differences you should think about when designing message-based services in ServiceStack I'll provide some examples comparing WCF/WebApi vs ServiceStack's approach: 为了让您了解在ServiceStack中设计基于消息的服务时应该考虑的差异,我将提供一些比较WCF / WebApi与ServiceStack方法的示例:

WCF vs ServiceStack API Design WCF与ServiceStack API设计

WCF encourages you to think of web services as normal C# method calls, eg: 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);
}

This is what the same Service contract would look like in ServiceStack with the New API : 这是使用New API在ServiceStack中使用相同服务合约的内容:

public class Customers : IReturn<List<Customer>> 
{
   public int[] Ids { get; set; }
   public string[] UserNames { get; set; }
   public string[] Emails { get; set; }
}

The important concept to keep in mind is that the entire query (aka Request) is captured in the Request Message (ie Request DTO) and not in the server method signatures. 要记住的重要概念是整个查询(也称为Request)在请求消息(即请求DTO)中捕获,而不是在服务器方法签名中捕获。 The obvious immediate benefit of adopting a message-based design is that any combination of the above RPC calls can be fulfilled in 1 remote message, by a single service implementation. 采用基于消息的设计的明显直接好处是,上述RPC调用的任何组合都可以通过单个服务实现在1个远程消息中实现。

WebApi vs ServiceStack API Design WebApi与ServiceStack API设计

Likewise WebApi promotes a similar C#-like RPC Api that WCF does: 同样,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 Message-Based API Design ServiceStack基于消息的API设计

Whilst ServiceStack encourages you to retain a Message-based Design: 虽然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;
    }
}

Again capturing the essence of the Request in the Request DTO. 再次在请求DTO中捕获请求的本质。 The message-based design is also able to condense 5 separate RPC WebAPI services into 2 message-based ServiceStack ones. 基于消息的设计还能够将5个单独的RPC WebAPI服务压缩为2个基于消息的ServiceStack服务。

Group by Call Semantics and Response Types 按呼叫语义和响应类型分组

It's grouped into 2 different services in this example based on Call Semantics and Response Types : 在此示例中,它基于调用语义响应类型分为2个不同的服务:

Every property in each Request DTO has the same semantics that is for FindProducts each property acts like a Filter (eg an AND) whilst in GetProduct it acts like a combinator (eg an OR). 每个Request DTO中的每个属性都具有与FindProducts相同的语义,每个属性的行为类似于Filter(例如AND),而在GetProduct它的作用类似于组合器(例如OR)。 The Services also return IEnumerable<Product> and Product return types which will require different handling in the call-sites of Typed APIs. 服务还返回IEnumerable<Product>Product返回类型,这些类型需要在Typed API的调用站点中进行不同的处理。

In WCF / WebAPI (and other RPC services frameworks) whenever you have a client-specific requirement you would add a new Server signature on the controller that matches that request. 在WCF / WebAPI(和其他RPC服务框架)中,只要您有特定于客户端的要求,就会在控制器上添加与该请求匹配的新服务器签名。 In ServiceStack's message-based approach however you should always be thinking about where this feature belongs and whether you're able to enhance existing services. 在ServiceStack的基于消息的方法中,您应始终考虑此功能的所在位置以及是否能够增强现有服务。 You should also be thinking about how you can support the client-specific requirement in a generic way so that the same service could benefit other future potential use-cases. 您还应该考虑如何以通用方式支持特定于客户的需求,以便相同的服务可以使其他未来的潜在用例受益。

Re-factoring GetBooking Limits services 重新分解GetBooking限制服务

With the info above we can start re-factoring your services. 通过上述信息,我们可以开始重新分配您的服务。 Since you have 2 different services that return different results eg GetBookingLimit returns 1 item and GetBookingLimits returns many, they need to be kept in different services. 由于您有2个不同的服务返回不同的结果,例如GetBookingLimit返回1项而GetBookingLimits返回许多,它们需要保存在不同的服务中。

Distinguish Service Operations vs Types 区分服务操作与类型

You should however have a clean split between your Service Operations (eg Request DTO) which is unique per service and is used to capture the Services' request, and the DTO types they return. 但是,您应该在服务操作(例如,请求DTO)之间进行清晰的划分,该服务操作是每个服务唯一的,用于捕获服务请求,以及它们返回的DTO类型。 Request DTOs are usually actions so they're verbs, whilst DTO types are entities/data-containers so they're nouns. 请求DTO通常是动作,因此它们是动词,而DTO类型是实体/数据容器,因此它们是名词。

Return generic responses 返回通用回复

In the New API, ServiceStack responses no longer require a ResponseStatus property since if it doesn't exist the generic ErrorResponse DTO will be thrown and serialized on the client instead. 在New API中,ServiceStack响应不再需要ResponseStatus属性,因为如果它不存在,则会在客户端上抛出并序列化通用ErrorResponse DTO。 This frees you from having your Responses contain ResponseStatus properties. 这使您无需使用Response包含ResponseStatus属性。 With that said I would re-factor the contract of your new services to: 有了这个说我会重新考虑你的新服务合同:

[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; }
}

For GET requests I tend to leave them out of the Route definition when they're not ambiguous since it's less code. 对于GET请求,当它们没有模糊时,我倾向于将它们排除在Route定义之外,因为它的代码较少。

Keep a consistent Nomenclature 保持一致的命名法

You should reserve the word Get on services which query on unique or Primary Keys fields, ie when a supplied value matches a field (eg Id) it only Gets 1 result. 您应该保留单独获取服务的单词或主键字段上的查询,即当提供的值与字段匹配时(例如Id),它只获得 1个结果。 For search services that acts like a filter and returns multiple matching results which falls within a desired range I use either the Find or Search verbs to signal that this is the case. 对于充当过滤器并返回多个匹配结果的搜索服务,该结果属于所需范围,我使用查找搜索动词来表示这种情况。

Aim for self-describing Service Contracts 旨在自我描述服务合同

Also try to be descriptive with each of your field names, these properties are part of your public API and should be self-describing as to what it does. 还尝试使用每个字段名称进行描述,这些属性是您的公共API的一部分,并且应该自我描述它的作用。 Eg Just by looking at the Service Contract (eg Request DTO) we have no idea what Date does, I've assumed BookedAfter , but it could also have been BookedBefore or BookedOn if it only returned bookings made on that Day. 例如,仅仅通过查看服务合同(例如请求DTO),我们不知道Date做了什么,我假设BookedAfter ,但如果它只返回当天的预订,它也可能已被BookedBeforeBookedOn

The benefit of this is now the call-sites of your typed .NET clients become easier to read: 这样做的好处是,您键入的.NET客户端的调用站点变得更容易阅读:

Product product = client.Get(new GetProduct { Id = 1 });

List<Product> results = client.Get(
    new FindBookingLimits { BookedAfter = DateTime.Today });

Service implementation 服务实施

I've removed the [Authenticate] attribute from your Request DTOs since you can instead just specify it once on the Service implementation, which now looks like: 我已经从您的请求DTO中删除了[Authenticate]属性,因为您只需在Service实现上指定一次,现在看起来像:

[Authenticate]
public class BookingLimitService : AppServiceBase 
{ 
    public BookingLimit Get(GetBookingLimit request) { ... }

    public List<BookingLimit> Get(FindBookingLimits request) { ... }
}

Error Handling and Validation 错误处理和验证

For info on how to add validation you either have the option to just throw C# exceptions and apply your own customizations to them, otherwise you have the option to use the built-in Fluent Validation but you don't need to inject them into your service as you can wire them all with a single line in your AppHost, eg: 有关如何添加验证的信息,您可以选择仅抛出C#异常并将自己的自定义应用于它们,否则您可以选择使用内置的Fluent验证,但不需要将它们注入到服务中因为你可以在AppHost中用一行连接它们,例如:

container.RegisterValidators(typeof(CreateBookingValidator).Assembly);

Validators are no-touch and invasive free meaning you can add them using a layered approach and maintain them without modifying the service implementation or DTO classes. 验证器是无触摸和无侵入的,这意味着您可以使用分层方法添加它们并在不修改服务实现或DTO类的情况下维护它们。 Since they require an extra class I would only use them on operations with side-effects (eg POST/PUT) as GETs' tend to have minimal validation and throwing a C# Exception requires less boiler plate. 由于它们需要一个额外的类,我只会在带有副作用的操作(例如POST / PUT)上使用它们,因为GETs往往具有最小的验证并抛出C#异常需要更少的锅炉板。 So an example of a validator you could have is when first creating a booking: 因此,您可以拥有的验证器示例是首次创建预订时:

public class CreateBookingValidator : AbstractValidator<CreateBooking>
{
    public CreateBookingValidator()
    {
        RuleFor(r => r.StartDate).NotEmpty();
        RuleFor(r => r.ShiftId).GreaterThan(0);
        RuleFor(r => r.Limit).GreaterThan(0);
    }
}

Depending on the use-case instead of having separate CreateBooking and UpdateBooking DTOs I would re-use the same Request DTO for both in which case I would name StoreBooking . 根据用例而不是单独的CreateBookingUpdateBooking DTO,我会重复使用相同的Request DTO,在这种情况下,我将命名StoreBooking

The 'Reponse Dtos' seem unnecessary since ResponseStatus property is no longer needed. 由于不再需要 ResponseStatus属性,因此“Reponse Dtos”似乎是不必要的 . Though, I think you may still need a matching Response class if you use SOAP. 但是,如果您使用SOAP,我认为您可能仍需要匹配的Response类。 If you remove the Response Dtos you no longer need to shove BookLimit into Response objects. 如果删除Response Dtos,则不再需要将BookLimit推送到Response对象中。 Also, ServiceStack's TranslateTo() could help as well. 此外,ServiceStack的TranslateTo()也可以提供帮助。

Below is how I would try to simplify what you posted...YMMV. 以下是我将如何简化您发布的内容... YMMV。

Make a DTO for BookingLimit - This will be the representation of BookingLimit to all other systems. 为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; }
}

Requests and Dtos are very important 请求和Dtos 非常重要

[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; }
}

No longer returning Reponse objects...just the BookingLimitDto 不再返回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();
    }
} 

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM