我是一个.Net开发人员,用于在Microsoft技术上开发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
类内的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那样思考的当前代码不满意。
有人可以告诉我正确的方向吗?
最佳答案
为了给您带来种种差异,您在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);
}
这就是带有New API的ServiceStack在ServiceStack中的样子:
public class Customers : IReturn<List<Customer>>
{
public int[] Ids { get; set; }
public string[] UserNames { get; set; }
public string[] Emails { get; set; }
}
要记住的重要概念是,整个查询(即“请求”)是在“请求消息”(即“请求DTO”)中捕获的,而不是在服务器方法签名中捕获的。采用基于消息的设计的显而易见的直接好处是,通过单个服务实现,可以在一条远程消息中实现上述RPC调用的任何组合。
WebApi vs ServiceStack API Design
同样,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;
}
}
再次在请求DTO中捕获了请求的本质。基于消息的设计还能够将5个单独的RPC WebAPI服务压缩为2个基于消息的ServiceStack服务。
按 call 语义和响应类型分组
在此示例中,根据 call 语义和响应类型,将其分为2种不同的服务:
每个Request DTO中的每个属性都具有与
FindProducts
相同的语义,每个属性的行为就像一个过滤器(例如AND),而在GetProduct
中,它就像一个组合器(例如OR)。服务还返回IEnumerable<Product>
和Product
返回类型,这将需要在Typed API的调用站点中进行不同的处理。在WCF/WebAPI(和其他RPC服务框架)中,只要您有特定于客户端的要求,就可以在与该请求匹配的 Controller 上添加新的Server签名。但是,在ServiceStack的基于消息的方法中,您应该始终在考虑此功能的位置以及是否能够增强现有服务。您还应该考虑如何以通用方式来支持特定于客户的需求,以便同一服务可以使将来的其他潜在用例受益。
重构GetBooking Limits服务
通过上面的信息,我们可以开始重构您的服务。由于您有2种不同的服务会返回不同的结果,例如
GetBookingLimit
返回1个项目,而GetBookingLimits
返回许多,它们需要保存在不同的服务中。区分服务操作与类型
但是,您应该在服务操作(例如,请求DTO)之间进行清晰的划分,每个服务都是唯一的,用于捕获服务的请求,以及服务返回的DTO类型。请求DTO通常是 Action ,因此它们是动词,而DTO类型是实体/数据容器,因此它们是名词。
返回通用回复
在新API中,ServiceStack会响应no longer require a ResponseStatus属性,因为如果不存在该属性,则将在客户端上抛出通用
ErrorResponse
DTO并对其进行序列化。这使您不必让您的响应包含ResponseStatus
属性。话虽如此,我将把您的新服务契约(Contract)重构为:[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结果。对于充当过滤器并返回落在期望范围内的多个匹配结果的搜索服务,我可以使用查找或搜索动词来表示是这种情况。
旨在自我描述服务契约(Contract)
另外,请尝试使用每个字段名称进行描述,这些属性是公共(public)API 的一部分,应该对其功能进行自我描述。例如。仅查看服务契约(Contract)(例如,请求DTO),我们不知道日期是做什么的,我假设 Booked在之后,但如果它只返回已在那天。
这样做的好处是,您的typed .NET clients的调用站点现在变得更容易阅读:
Product product = client.Get(new GetProduct { Id = 1 });
List<Product> results = client.Get(
new FindBookingLimits { BookedAfter = DateTime.Today });
服务实现
我已经从请求DTO中删除了
[Authenticate]
属性,因为您可以只在Service实现上指定一次,现在看起来像这样:[Authenticate]
public class BookingLimitService : AppServiceBase
{
public BookingLimit Get(GetBookingLimit request) { ... }
public List<BookingLimit> Get(FindBookingLimits request) { ... }
}
错误处理和验证
有关如何添加验证的信息,您可以选择仅将throw C# exceptions并对其应用自定义,否则,您可以选择使用内置的Fluent Validation,但无需将其尽可能地注入(inject)服务中。将它们全部连接到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,我将为这两者重新使用相同的Request DTO,在这种情况下,我将命名为StoreBooking
。关于c# - ServiceStack要求DTO设计,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/15927475/