C# ServiceStack 请求 DTO 设计
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/15927475/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
ServiceStack Request DTO design
提问by mustafasturan
I am a .Net developer used to develop web application on Microsoft Technologies. I am trying to educate myself to understand REST approach for web services. So far i am loving the ServiceStack framework.
我是一名 .Net 开发人员,用于在 Microsoft Technologies 上开发 Web 应用程序。我正在尝试教育自己了解 Web 服务的 REST 方法。到目前为止,我很喜欢 ServiceStack 框架。
But sometimes i find myself to write services in a fashion that i am used to with WCF. So I have a question which bugs me.
但有时我发现自己以一种我习惯使用 WCF 的方式编写服务。所以我有一个问题困扰着我。
I have 2 request DTO's so 2 services like these:
我有 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; }
}
As seen on these Request DTO's i have similar request DTO's nearly for every services and this seems like not DRY.
正如在这些请求 DTO 上看到的,我几乎对每个服务都有类似的请求 DTO,这似乎不是 DRY。
I tried to use GetBookingLimitResponse
class in a list inside GetBookingLimitsResponse
for that reason ResponseStatus
inside GetBookingLimitResponse
class is dublicated in case i have an error on GetBookingLimits
service.
我试图GetBookingLimitResponse
在内部列表中使用类GetBookingLimitsResponse
,因为ResponseStatus
内部GetBookingLimitResponse
类被复制,以防我在GetBookingLimits
服务上出错。
Also I have service implementations for these requests like :
我也有这些请求的服务实现,例如:
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()
};
}
}
As you see i also want to use Validation Feature here, so i have to write validation classes for every request DTO i have. So i have a feeling that i should keep my service number low by grouping similar services into one service.
如您所见,我也想在这里使用验证功能,因此我必须为我拥有的每个请求 DTO 编写验证类。所以我有一种感觉,我应该通过将类似的服务分组为一个服务来保持我的服务编号较低。
But the question here that pops up in my mind that should i send more information than client need for that request ?
但是这里出现在我脑海中的问题是,我应该发送比客户对该请求所需的更多的信息吗?
I think my way of thinking should change because i am not happy with current code which i wrote thinking like a WCF guy.
我认为我的思维方式应该改变,因为我对当前的代码不满意,我写的像 WCF 人一样。
Can someone show me the right direction to follow.
有人可以告诉我正确的方向。
采纳答案by mythz
To give you a flavor of the differences you should think about when designing message-based services in ServiceStackI'll provide some examples comparing WCF/WebApi vs ServiceStack's approach:
为了让您了解在ServiceStack 中设计基于消息的服务时应该考虑的差异,我将提供一些比较 WCF/WebApi 与 ServiceStack 方法的示例:
WCF vs ServiceStack API Design
WCF 与 ServiceStack API 设计
WCF encourages you to think of web services as normal C# method calls, e.g:
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);
}
This is what the same Service contract would look like in ServiceStack with the New API:
这就是具有新 API 的ServiceStack 中相同的 Service 契约的样子:
public class Customers : IReturn<List<Customer>>
{
public int[] Ids { get; set; }
public string[] UserNames { get; set; }
public string[] Emails { get; set; }
}
The important concept to keep in mind is that the entire query (aka Request) is captured in the Request Message (i.e. Request DTO) and not in the server method signatures. The obvious immediate benefit of adopting a message-based design is that any combination of the above RPC calls can be fulfilled in 1 remote message, by a single service implementation.
要记住的重要概念是整个查询(又名请求)在请求消息(即请求 DTO)中捕获,而不是在服务器方法签名中。采用基于消息的设计的明显直接好处是,上述 RPC 调用的任何组合都可以通过单个服务实现在 1 个远程消息中完成。
WebApi vs ServiceStack API Design
WebApi 与 ServiceStack API 设计
Likewise WebApi promotes a similar C#-like RPC Api that WCF does:
同样,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 Message-Based API Design
ServiceStack 基于消息的 API 设计
Whilst ServiceStack encourages you to retain a Message-based Design:
虽然 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;
}
}
Again capturing the essence of the Request in the Request DTO. The message-based design is also able to condense 5 separate RPC WebAPI services into 2 message-based ServiceStack ones.
再次在请求 DTO 中捕获请求的本质。基于消息的设计还能够将 5 个独立的 RPC WebAPI 服务压缩为 2 个基于消息的 ServiceStack 服务。
Group by Call Semantics and Response Types
按呼叫语义和响应类型分组
It's grouped into 2 different services in this example based on Call Semanticsand Response Types:
在此示例中,它根据呼叫语义和响应类型分为 2 个不同的服务:
Every property in each Request DTO has the same semantics that is for FindProducts
each property acts like a Filter (e.g. an AND) whilst in GetProduct
it acts like a combinator (e.g. an OR). The Services also return IEnumerable<Product>
and Product
return types which will require different handling in the call-sites of Typed APIs.
每个请求 DTO 中的每个属性都具有相同的语义,FindProducts
每个属性的作用类似于过滤器(例如,AND),而在GetProduct
其中的作用类似于组合器(例如,OR)。服务还返回IEnumerable<Product>
和Product
返回类型,这将需要在类型化 API 的调用站点中进行不同的处理。
In WCF / WebAPI (and other RPC services frameworks) whenever you have a client-specific requirement you would add a new Server signature on the controller that matches that request. In ServiceStack's message-based approach however you should always be thinking about where this feature belongs and whether you're able to enhance existing services. You should also be thinking about how you can support the client-specific requirement in a generic wayso that the same service could benefit other future potential use-cases.
在 WCF/WebAPI(和其他 RPC 服务框架)中,只要您有特定于客户端的要求,您就会在与该请求匹配的控制器上添加新的服务器签名。然而,在 ServiceStack 的基于消息的方法中,您应该始终考虑此功能的归属以及您是否能够增强现有服务。您还应该考虑如何以通用方式支持特定于客户端的需求,以便相同的服务可以使其他未来的潜在用例受益。
Re-factoring GetBooking Limits services
重构 GetBooking Limits 服务
With the info above we can start re-factoring your services. Since you have 2 different services that return different results e.g. GetBookingLimit
returns 1 item and GetBookingLimits
returns many, they need to be kept in different services.
有了上面的信息,我们就可以开始重构您的服务了。由于您有 2 个不同的服务返回不同的结果,例如GetBookingLimit
返回 1 个项目并GetBookingLimits
返回多个项目,因此需要将它们保存在不同的服务中。
Distinguish Service Operations vs Types
区分服务操作与类型
You should however have a clean split between your Service Operations (e.g. Request DTO) which is unique per service and is used to capture the Services' request, and the DTO types they return. Request DTOs are usually actions so they're verbs, whilst DTO types are entities/data-containers so they're nouns.
但是,您应该在您的服务操作(例如请求 DTO)和它们返回的 DTO 类型之间进行清晰的划分,它是每个服务唯一的并用于捕获服务的请求。请求 DTO 通常是动作,因此它们是动词,而 DTO 类型是实体/数据容器,因此它们是名词。
Return generic responses
返回通用响应
In the New API, ServiceStack responses no longer require a ResponseStatusproperty since if it doesn't exist the generic ErrorResponse
DTO will be thrown and serialized on the client instead. This frees you from having your Responses contain ResponseStatus
properties. With that said I would re-factor the contract of your new services to:
在新 API 中,ServiceStack 响应不再需要 ResponseStatus属性,因为如果它不存在,则通用ErrorResponse
DTO 将在客户端上抛出并序列化。这使您免于让您的 Responses 包含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; }
}
For GET requests I tend to leave them out of the Route definition when they're not ambiguous since it's less code.
对于 GET 请求,当它们没有歧义时,我倾向于将它们排除在 Route 定义之外,因为它的代码较少。
Keep a consistent Nomenclature
保持一致的命名法
You should reserve the word Geton services which query on unique or Primary Keys fields, i.e. when a supplied value matches a field (e.g. Id) it only Gets1 result. For search services that acts like a filter and returns multiple matching results which falls within a desired range I use either the Findor Searchverbs to signal that this is the case.
您应该在查询唯一或主键字段的服务上保留单词Get,即当提供的值与字段(例如 Id)匹配时,它只会获取1 个结果。对于充当过滤器并返回多个落在所需范围内的匹配结果的搜索服务,我使用Find或Search动词来表示这种情况。
Aim for self-describing Service Contracts
旨在实现自我描述的服务合同
Also try to be descriptive with each of your field names, these properties are part of your public APIand should be self-describing as to what it does. E.g. Just by looking at the Service Contract (e.g. Request DTO) we have no idea what Datedoes, I've assumed BookedAfter, but it could also have been BookedBeforeor BookedOnif it only returned bookings made on that Day.
还尝试使用您的每个字段名称进行描述,这些属性是您的公共 API 的一部分,并且应该对它的作用进行自我描述。例如,通过查看服务合同(例如请求 DTO),我们不知道Date做什么,我假设BookedAfter,但如果它只返回当天的预订,它也可能是BookedBefore或BookedOn。
The benefit of this is now the call-sites of your typed .NET clientsbecome easier to read:
这样做的好处是现在您键入的 .NET 客户端的调用站点变得更易于阅读:
Product product = client.Get(new GetProduct { Id = 1 });
List<Product> results = client.Get(
new FindBookingLimits { BookedAfter = DateTime.Today });
Service implementation
服务实现
I've removed the [Authenticate]
attribute from your Request DTOs since you can instead just specify it once on the Service implementation, which now looks like:
我已经[Authenticate]
从您的请求 DTO 中删除了该属性,因为您可以改为在服务实现中指定一次,现在看起来像:
[Authenticate]
public class BookingLimitService : AppServiceBase
{
public BookingLimit Get(GetBookingLimit request) { ... }
public List<BookingLimit> Get(FindBookingLimits request) { ... }
}
Error Handling and Validation
错误处理和验证
For info on how to add validation you either have the option to just throw C# exceptionsand apply your own customizations to them, otherwise you have the option to use the built-in Fluent Validationbut you don't need to inject them into your service as you can wire them all with a single line in your AppHost, e.g:
有关如何添加验证的信息,您可以选择只抛出 C# 异常并将您自己的自定义应用于它们,否则您可以选择使用内置的Fluent 验证,但您不需要将它们注入您的服务因为您可以在 AppHost 中用一行将它们全部连接起来,例如:
container.RegisterValidators(typeof(CreateBookingValidator).Assembly);
Validators are no-touch and invasive free meaning you can add them using a layered approach and maintain them without modifying the service implementation or DTO classes. Since they require an extra class I would only use them on operations with side-effects (e.g. POST/PUT) as GETs' tend to have minimal validation and throwing a C# Exception requires less boiler plate. So an example of a validator you could have is when first creating a booking:
验证器是非接触和无侵入性的,这意味着您可以使用分层方法添加它们并维护它们,而无需修改服务实现或 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);
}
}
Depending on the use-case instead of having separate CreateBooking
and UpdateBooking
DTOs I would re-use the same Request DTO for both in which case I would name StoreBooking
.
根据用例,而不是使用单独的DTOCreateBooking
和UpdateBooking
DTO,我会为两者重新使用相同的 Request DTO,在这种情况下,我将命名为StoreBooking
.
回答by paaschpa
The 'Reponse Dtos' seem unnecessary since ResponseStatus property is no longer needed.. Though, I think you may still need a matching Response class if you use SOAP. If you remove the Response Dtos you no longer need to shove BookLimit into Response objects. Also, ServiceStack's TranslateTo() could help as well.
'Reponse Dtos' 似乎没有必要,因为不再需要ResponseStatus 属性。. 不过,我认为如果您使用 SOAP,您可能仍然需要一个匹配的 Response 类。如果删除 Response Dtos,则不再需要将 BookLimit 推入 Response 对象。此外,ServiceStack 的 TranslateTo() 也可以提供帮助。
Below is how I would try to simplify what you posted...YMMV.
以下是我将如何尝试简化您发布的内容...YMMV。
Make a DTO for BookingLimit - This will be the representation of BookingLimit to all other systems.
为 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; }
}
Requests and Dtos are very important
Requests和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; }
}
No longer returning Reponse objects...just the BookingLimitDto
不再返回 Response 对象...只是 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();
}
}