ServiceStack请求DTOdevise

我是一个.Net开发者,用于在Microsoft Technologies上开发Web应用程序。 我正试图教育自己了解REST Web服务的方法。 到目前为止,我很喜欢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的几乎每个服务,这似乎不是干。

我试图在GetBookingLimitResponse里面的GetBookingLimitResponse类中使用GetBookingLimitResponse类,因为GetBookingLimitResponse类里面的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() }; } } 

正如你所看到的,我也想在这里使用validationfunction,所以我必须为每个请求DTO编写validation类。 所以我有一种感觉,我应该通过将类似的服务分组到一个服务来保持我的服务数量低。

但是这个问题出现在我脑海里,我应该发送更多的信息,而不是客户需要的信息吗?

我认为我的思维方式应该改变,因为我不喜欢现在的代码,我写的像一个WCF家伙一样思考。

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

为了给你一个在ServiceStack中devise基于消息的服务时应该考虑的差异,我将提供一些比较WCF / WebApi和ServiceStack方法的例子:

WCF vs ServiceStack APIdevise

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

在新的API中,这是ServiceStack中同样的服务契约:

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

要记住的重要概念是整个查询(又名请求)在请求消息(即请求DTO)中被捕获,而不是在服务器方法签名中。 采用基于消息的devise的直接好处是,上述RPC调用的任何组合都可以在一个远程消息中通过单个服务实现来实现。

WebApi vs ServiceStack APIdevise

同样的,WebApi推广了一个类似C#的RPC API,WCF可以:

 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基于消息的APIdevise

虽然ServiceStack鼓励您保留一个基于消息的devise:

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

在请求DTO中再次捕获请求的本质。 基于消息的devise也能够将5个独立的RPC WebAPI服务压缩成2个基于消息的ServiceStack服务。

按调用语义和响应types分组

在这个例子中,它基于呼叫语义响应types被分成两个不同的服务:

每个Request DTO中的每个属性都具有与FindProducts相同的语义,每个属性的行为就像一个Filter(例如一个AND),而在GetProduct它就像一个组合器(比如一个OR)。 服务还会返回IEnumerable<Product>Product返回types,这些types需要在Typed API的调用站点中进行不同的处理。

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

重新分解GetBooking限制服务

有了上面的信息,我们可以开始重新考虑您的服务。 由于你有两个不同的服务返回不同的结果,例如GetBookingLimit返回1个项目, GetBookingLimits返回很多,他们需要保存在不同的服务。

区分服务操作与types

然而,您应该在您的服务操作(例如请求DTO)之间进行清晰的分割,这个服务是唯一的,并且用于捕获服务请求以及它们返回的DTOtypes。 请求DTO通常是动作,因此它们是动词,而DTOtypes是实体/数据容器,所以它们是名词。

返回通用响应

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

对于GET请求,我倾向于将它们放在Route定义之外,因为它们不是模糊的,因为它的代码less了。

保持一致的术语

你应该保留单词获取的服务,查询唯一或主键字段,即当提供的值匹配一个字段(如Id)它只获得 1结果。 对于像filter一样的search服务,返回多个匹配的结果,这些结果落在期望的范围内。我使用FindSearch动词来表示这种情况。

旨在自我描述的服务合同

还要试着描述每个字段的名称,这些属性是公共API的一部分,应该自我描述它的function。 例如,仅仅通过查看服务合同(例如请求DTO),我们不知道date是什么,我已经预定了预订,但是如果它只返回了当天预订,那么它也可以被预订。

这样做的好处是现在你的types化的.NET客户端的调用网站变得更容易阅读:

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

服务实施

我已经从Request DTO中删除了[Authenticate]属性,因为您可以在Service实现中指定一次,现在看起来像这样:

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

error handling和validation

有关如何添加validation的信息,您可以select只抛出C#exception并将自定义设置应用于它们,否则您可以select使用内置的Fluentvalidation,但不需要将它们注入到服务中因为你可以用AppHost中的一行来连接它们,例如:

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

validation程序是非触摸式和无侵入式的,意味着您可以使用分层方法添加它们,并在不修改服务实现或DTO类的情况下进行维护。 因为他们需要一个额外的类,所以我只会在带有副作用的操作(例如POST / PUT)上使用它们,因为GETs往往具有最小限度的validation,抛出一个C#Exception需要较less的锅炉板。 因此,您可以使用的validation器的示例是首次创build预订时:

 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 DTOs,我会重复使用相同的Request DTO,在这种情况下,我会命名StoreBooking

由于不再需要 ResponseStatus属性,所以'Reponse Dtos'看起来没有必要。 。 尽pipe如此,我认为如果你使用SOAP,你可能仍然需要一个匹配的Response类。 如果您删除响应数据,则不再需要将BookLimit插入到响应对象中。 此外,ServiceStack的TranslateTo()也可以帮助。

以下是我将如何尝试简化您发布的内容… YMMV。

为BookingLimit制作一个DTO – 这将是所有其他系统的BookingLimit表示forms。

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

请求和数据是非常重要的

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