ServiceStack Request DTO design
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.
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.
I have 2 request DTO's so 2 services like these:
[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.
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.
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.
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.
Can someone show me the right direction to follow.
Solution 1:
To give you a flavor of the differences you should think about when designing message-based services in ServiceStack I'll provide some examples comparing WCF/WebApi vs ServiceStack's approach:
WCF vs ServiceStack API Design
WCF encourages you to think of web services as normal C# method calls, e.g:
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:
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.
WebApi vs ServiceStack API Design
Likewise WebApi promotes a similar C#-like RPC Api that WCF does:
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
Whilst ServiceStack encourages you to retain a Message-based Design:
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.
Group by Call Semantics and Response Types
It's grouped into 2 different services in this example based on Call Semantics and Response Types:
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.
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 way so that the same service could benefit other future potential use-cases.
Re-factoring GetBooking Limits services
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.
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.
Return generic responses
In the New API, ServiceStack responses no longer require a ResponseStatus property 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:
[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.
Keep a consistent Nomenclature
You should reserve the word Get on services which query on unique or Primary Keys fields, i.e. when a supplied value matches a field (e.g. Id) it only Gets 1 result. For search services that acts like a filter and returns multiple matching results which falls within a desired range I use either the Find or Search verbs to signal that this is the case.
Aim for self-describing Service Contracts
Also try to be descriptive with each of your field names, these properties are part of your public API and 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 Date does, I've assumed BookedAfter, but it could also have been BookedBefore or BookedOn if it only returned bookings made on that Day.
The benefit of this is now the call-sites of your typed .NET clients become easier to read:
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]
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# exceptions and apply your own customizations to them, otherwise you have the option to use the built-in Fluent Validation but 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:
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:
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
.
Solution 2:
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.
Below is how I would try to simplify what you posted...YMMV.
Make a DTO for BookingLimit - This will be the representation of BookingLimit to all other systems.
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
[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
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();
}
}