ServiceStack请求DTO设计

mus*_*ran 39 .net c# servicestack

我是一名.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; }
}
Run Code Online (Sandbox Code Playgroud)

正如在这些请求DTO上看到的,我有类似的请求DTO几乎为每个服务,这似乎不干.

我试图GetBookingLimitResponse在列表里面使用类,GetBookingLimitsResponse因为这个原因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()
        };
    }
}
Run Code Online (Sandbox Code Playgroud)

如你所见,我也想在这里使用验证功能,所以我必须为每个DTO请求编写验证类.所以我觉得我应该通过将类似的服务分组到一个服务来保持我的服务号码低.

但是在我脑海中突然出现的问题是,我应该发送的信息多于客户对该请求的需求吗?

我认为我的思维方式应该改变,因为我对当前的代码感到不满意,我写的这个代码就像一个WCF人.

有人能告诉我正确的方向.

myt*_*thz 88

为了让您了解在ServiceStack中设计基于消息的服务时应该考虑的差异,我将提供一些比较WCF/WebApi与ServiceStack方法的示例:

WCF与ServiceStack API设计

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);
}
Run Code Online (Sandbox Code Playgroud)

这是使用New API在ServiceStack中使用相同服务合约的内容:

public class Customers : IReturn<List<Customer>> 
{
   public int[] Ids { get; set; }
   public string[] UserNames { get; set; }
   public string[] Emails { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

要记住的重要概念是整个查询(也称为Request)在请求消息(即请求DTO)中捕获,而不是在服务器方法签名中捕获.采用基于消息的设计的明显直接好处是,上述RPC调用的任何组合都可以通过单个服务实现在1个远程消息中实现.

WebApi与ServiceStack API设计

同样,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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

再次在请求DTO中捕获请求的本质.基于消息的设计还能够将5个单独的RPC WebAPI服务压缩为2个基于消息的ServiceStack服务.

按呼叫语义和响应类型分组

在此示例中,它基于调用语义响应类型分为2个不同的服务:

每个Request DTO中的每个属性都具有相同的语义,FindProducts每个属性的行为类似于Filter(例如AND),而GetProduct其中的行为类似于组合器(例如OR).服务还返回IEnumerable<Product>Product返回类型,这些类型需要在Typed API的调用站点中进行不同的处理.

在WCF/WebAPI(和其他RPC服务框架)中,只要您有特定于客户端的要求,就会在控制器上添加与该请求匹配的新服务器签名.在ServiceStack的基于消息的方法中,您应始终考虑此功能的所在位置以及是否能够增强现有服务.您还应该考虑如何以通用方式支持特定于客户的需求,以便相同的服务可以使其他未来的潜在用例受益.

重新分解GetBooking限制服务

通过上述信息,我们可以开始重新分配您的服务.由于您有2个不同的服务返回不同的结果,例如GetBookingLimit返回1个项目并GetBookingLimits返回许多,它们需要保存在不同的服务中.

区分服务操作与类型

但是,您应该在服务操作(例如,请求DTO)之间进行清晰的划分,该服务操作是每个服务唯一的,用于捕获服务请求,以及它们返回的DTO类型.请求DTO通常是动作,因此它们是动词,而DTO类型是实体/数据容器,因此它们是名词.

返回通用回复

在New API中,ServiceStack响应不再需要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; }
}
Run Code Online (Sandbox Code Playgroud)

对于GET请求,当它们没有模糊时,我倾向于将它们排除在Route定义之外,因为它的代码较少.

保持一致的命名法

您应该保留单独获取服务或在主要密钥字段上查询的服务,即当提供的值与字段匹配时(例如,Id),它只获得 1个结果.对于充当过滤器并返回多个匹配结果的搜索服务,该结果属于所需范围,我使用查找搜索动词来表示这种情况.

旨在自我描述服务合同

还尝试使用每个字段名称进行描述,这些属性是您的公共API的一部分,并且应该自我描述它的作用.例如,仅仅通过查看服务合同(例如请求DTO),我们不知道Date做了什么,我假设BookedAfter,但如果它只返回当天的预订,它也可能已被BookedBeforeBookedOn.

这样做的好处是,您键入的.NET客户端的调用站点变得更容易阅读:

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

List<Product> results = client.Get(
    new FindBookingLimits { BookedAfter = DateTime.Today });
Run Code Online (Sandbox Code Playgroud)

服务实施

[Authenticate]已从您的请求DTO中删除了该属性,因为您只需在Service实现上指定一次,现在看起来像:

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

    public List<BookingLimit> Get(FindBookingLimits request) { ... }
}
Run Code Online (Sandbox Code Playgroud)

错误处理和验证

有关如何添加验证的信息,您可以选择仅抛出C#异常并将自己的自定义应用于它们,否则您可以选择使用内置的Fluent验证,但不需要将它们注入到服务中因为你可以在AppHost中用一行连接它们,例如:

container.RegisterValidators(typeof(CreateBookingValidator).Assembly);
Run Code Online (Sandbox Code Playgroud)

验证器是无触摸和无侵入的,这意味着您可以使用分层方法添加它们并在不修改服务实现或DTO类的情况下维护它们.由于它们需要一个额外的类,我只会在有副作用的操作(例如POST/PUT)上使用它们,因为GETs往往具有最小的验证并抛出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);
    }
}
Run Code Online (Sandbox Code Playgroud)

根据用例而不是单独的CreateBookingUpdateBookingDTO,我会重新使用相同的请求DTO,在这种情况下我会命名StoreBooking.


paa*_*hpa 10

由于不再需要 ResponseStatus属性,因此"Reponse Dtos"似乎是不必要的..但是,如果您使用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; }
}
Run Code Online (Sandbox Code Playgroud)

请求和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; }
}
Run Code Online (Sandbox Code Playgroud)

不再返回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();
    }
} 
Run Code Online (Sandbox Code Playgroud)