我是一个.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类案件dublicated我有一个错误GetBookingLimits的服务。
GetBookingLimitResponse
GetBookingLimitsResponse
ResponseStatus
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的方法:
API设计](https://github.com/ServiceStack/ServiceStack/wiki/Why- Servicestack#difference-between-an-rpc-chatty-and-message-based-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); }
这就是带有新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调用的任何组合。
API设计](https://github.com/ServiceStack/ServiceStack.UseCases/tree/master/WebApi.ProductsExample)
同样,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鼓励您保留基于消息的设计:
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服务。
在此示例中,根据 呼叫语义 和 响应类型 将其分为2种不同的服务:
每个请求DTO中的每个属性都具有相同的语义,因为FindProducts每个属性的行为就像一个过滤器(例如AND),而其中的每个GetProduct行为都像一个组合器(例如OR)。服务还返回IEnumerable<Product>和Product返回类型,这将需要在Typed API的调用站点中进行不同的处理。
FindProducts
GetProduct
IEnumerable<Product>
Product
在WCF / WebAPI(和其他RPC服务框架)中,只要您有特定于客户端的要求,就可以在与该请求匹配的控制器上添加新的Server签名。但是,在ServiceStack的基于消息的方法中,您应该始终在考虑此功能的位置以及是否能够增强现有服务。您还应该考虑如何以 通用方式 支持特定于客户的需求,以便同一服务可以使将来的其他潜在用例受益。
通过上面的信息,我们可以开始重构您的服务。由于您有2种返回不同结果的不同服务,例如GetBookingLimit返回1个项目并GetBookingLimits返回许多,因此需要将它们保存在不同的服务中。
GetBookingLimit
但是,您应该在服务操作(例如,请求DTO)和服务返回的DTO类型之间进行清晰的划分,每个服务都是唯一的,用于捕获服务的请求。请求DTO通常是动作,因此它们是动词,而DTO类型是实体/数据容器,因此它们是名词。
在新API中,ServiceStack响应不再需要ResponseStatus属性,因为如果不存在,ErrorResponse则将在客户端上引发通用DTO并对其进行序列化。这使您摆脱了包含响应的ResponseStatus属性。话虽如此,我将把您的新服务合同重构为:
ErrorResponse
[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请求,我倾向于将它们放在不明确的路由定义之外,因为它们的代码较少。
您应该保留“ 在服务上 获取 ”一词,该服务在唯一或主键字段上进行查询,即,当提供的值与字段(例如,Id)匹配时,它只会 获取 1个结果。对于充当过滤器并返回多个匹配结果(位于所需范围内)的搜索服务,我使用“ 查找” 或“ 搜索” 动词来表示情况确实如此。
另外,请尝试用您的每个字段名称进行描述,这些属性是 公共API的 一部分,应该对其功能进行自我描述。例如,仅查看服务合同(例如,请求DTO),我们不知道 日期 是什么,我假设是 BookedAfter ,但如果它只返回当天的预订,则也可能是 BookedBefore 或 BookedOn 。
现在,这样做的好处是,您键入的.NET客户端的调用站点变得更易于阅读:
Product product = client.Get(new GetProduct { Id = 1 }); List<Product> results = client.Get( new FindBookingLimits { BookedAfter = DateTime.Today });
我已从“ [Authenticate]请求DTO”中删除了该属性,因为您可以只在Service实现上指定一次即可,现在看起来像这样:
[Authenticate]
[Authenticate] public class BookingLimitService : AppServiceBase { public BookingLimit Get(GetBookingLimit request) { ... } public List<BookingLimit> Get(FindBookingLimits request) { ... } }
有关如何添加验证的信息,您可以选择仅抛出C#异常并对其应用自定义,否则,您可以选择使用内置的Fluent验证,但无需将其注入到服务中因为您可以在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); } }
取决于用例,而不是使用单独的DTO CreateBooking和UpdateBookingDTO,在这种情况下,我将对name重复使用相同的Request DTO StoreBooking。
CreateBooking
UpdateBooking
StoreBooking