40

我是一名 .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。

我试图GetBookingLimitResponse在里面的列表中使用类GetBookingLimitsResponse,因为在我有服务错误的情况下,ResponseStatus里面的类被复制了。GetBookingLimitResponseGetBookingLimits

我也有这些请求的服务实现,例如:

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 人一样思考。

有人可以告诉我正确的方向吗?

4

2 回答 2

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

这就是在 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 与 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);
    }
}

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 服务。

按调用语义和响应类型分组

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

每个请求 DTO 中的每个属性都具有相同的语义,FindProducts每个属性的作用类似于过滤器(例如 AND),而GetProduct其中的作用类似于组合器(例如 OR)。服务还返回IEnumerable<Product>Product返回类型,这将需要在类型化 API 的调用站点中进行不同的处理。

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

重构 GetBooking 限制服务

有了上面的信息,我们就可以开始重构你的服务了。由于您有 2 种不同的服务返回不同的结果,例如GetBookingLimit返回 1 项并GetBookingLimits返回许多项,因此它们需要保存在不同的服务中。

区分服务操作与类型

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

返回通用响应

在新 API 中,ServiceStack 响应不再需要 ResponseStatus属性,因为如果它不存在,则通用ErrorResponseDTO 将被抛出并在客户端上序列化。这使您免于让您的响应包含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)匹配时,它只获得1 个结果。对于充当过滤器并返回多个匹配结果且在所需范围内的搜索服务,我使用FindSearch动词来表示情况就是这样。

以自我描述的服务合同为目标

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

这样做的好处是现在您的类型化 .NET 客户端的调用站点变得更易于阅读:

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

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

服务实施

我已经[Authenticate]从您的请求 DTO 中删除了该属性,因为您可以在服务实现中指定一次,现在看起来像:

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

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

错误处理和验证

有关如何添加验证的信息,您可以选择仅抛出 C# 异常并将您自己的自定义应用到它们,否则您可以选择使用内置的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);
    }
}

根据用例而不是拥有单独的CreateBookingDTO,UpdateBooking我会为两者重复使用相同的 Request DTO,在这种情况下,我将命名为StoreBooking.

于 2013-04-11T05:15:14.350 回答
10

'Reponse Dtos' 似乎没有必要,因为不再需要 ResponseStatus 属性。. 不过,我认为如果您使用 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; }
}

请求和 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; }
}

不再返回 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();
    }
} 
于 2013-04-11T04:49:13.280 回答