[英]ServiceStack Request DTO design
我是一名.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幾乎為每個服務,這似乎不干。
我試圖用GetBookingLimitResponse
類在列表里面GetBookingLimitsResponse
出於這個原因ResponseStatus
內GetBookingLimitResponse
的情況下,我有一個錯誤類dublicated 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鼓勵您將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中使用相同服務合約的內容:
public class Customers : IReturn<List<Customer>>
{
public int[] Ids { get; set; }
public string[] UserNames { get; set; }
public string[] Emails { get; set; }
}
要記住的重要概念是整個查詢(也稱為Request)在請求消息(即請求DTO)中捕獲,而不是在服務器方法簽名中捕獲。 采用基於消息的設計的明顯直接好處是,上述RPC調用的任何組合都可以通過單個服務實現在1個遠程消息中實現。
同樣,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個不同的服務:
每個Request DTO中的每個屬性都具有與FindProducts
相同的語義,每個屬性的行為類似於Filter(例如AND),而在GetProduct
它的作用類似於組合器(例如OR)。 服務還返回IEnumerable<Product>
和Product
返回類型,這些類型需要在Typed API的調用站點中進行不同的處理。
在WCF / WebAPI(和其他RPC服務框架)中,只要您有特定於客戶端的要求,就會在控制器上添加與該請求匹配的新服務器簽名。 在ServiceStack的基於消息的方法中,您應始終考慮此功能的所在位置以及是否能夠增強現有服務。 您還應該考慮如何以通用方式支持特定於客戶的需求,以便相同的服務可以使其他未來的潛在用例受益。
通過上述信息,我們可以開始重新分配您的服務。 由於您有2個不同的服務返回不同的結果,例如GetBookingLimit
返回1項而GetBookingLimits
返回許多,它們需要保存在不同的服務中。
但是,您應該在服務操作(例如,請求DTO)之間進行清晰的划分,該服務操作是每個服務唯一的,用於捕獲服務請求,以及它們返回的DTO類型。 請求DTO通常是動作,因此它們是動詞,而DTO類型是實體/數據容器,因此它們是名詞。
在New API中,ServiceStack響應不再需要ResponseStatus屬性,因為如果它不存在,則會在客戶端上拋出並序列化通用ErrorResponse
DTO。 這使您無需使用Response包含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定義之外,因為它的代碼較少。
您應該保留單獨獲取服務的單詞或主鍵字段上的查詢,即當提供的值與字段匹配時(例如Id),它只獲得 1個結果。 對於充當過濾器並返回多個匹配結果的搜索服務,該結果屬於所需范圍,我使用查找或搜索動詞來表示這種情況。
還嘗試使用每個字段名稱進行描述,這些屬性是您的公共API的一部分,並且應該自我描述它的作用。 例如,僅僅通過查看服務合同(例如請求DTO),我們不知道Date做了什么,我假設BookedAfter ,但如果它只返回當天的預訂,它也可能已被BookedBefore或BookedOn 。
這樣做的好處是,您鍵入的.NET客戶端的調用站點變得更容易閱讀:
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) { ... }
}
有關如何添加驗證的信息,您可以選擇僅拋出C#異常並將自己的自定義應用於它們,否則您可以選擇使用內置的Fluent驗證,但不需要將它們注入到服務中因為你可以在AppHost中用一行連接它們,例如:
container.RegisterValidators(typeof(CreateBookingValidator).Assembly);
驗證器是無觸摸和無侵入的,這意味着您可以使用分層方法添加它們並在不修改服務實現或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);
}
}
根據用例而不是單獨的CreateBooking
和UpdateBooking
DTO,我會重復使用相同的Request DTO,在這種情況下,我將命名StoreBooking
。
由於不再需要 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; }
}
請求和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();
}
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.