ServiceStack Request DTO design

24,992

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();
    }
} 
Share:
24,992
mustafasturan
Author by

mustafasturan

Updated on April 24, 2020

Comments

  • mustafasturan
    mustafasturan about 4 years

    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.

  • mythz
    mythz about 11 years
    Apologies Pat, I only noticed you answered after I posted, I usually try not to re-answer Q's you've already covered unless I've noticed an anomaly :)
  • paaschpa
    paaschpa about 11 years
    @mythz - No worries. I always enjoy reading your answers. Maybe we need to start taking shifts on StackOverflow? :)
  • mustafasturan
    mustafasturan about 11 years
    Thank you for the insights. Even though i read a lot, it really helps to see examples over your own code. If you have time to read this wanna ask quick question, you suggest keeping ServiceModel dll dependecy free but i have a situation that breaks this rule, I want to share custom user share between ServiceInterface and WebApplication so i ended up with putting UserSession : AuthUserSession inside ServiceModel dll so that i need to add ServiceStack reference to ServiceModel dll because i wanted to share that class.
  • mustafasturan
    mustafasturan about 11 years
    @paaschpa Thank you guys for sharing your knowledge both i could mark both answer as accepted answer.
  • mythz
    mythz about 11 years
    Can you open a new question please, comments are not the best place for new questions.
  • kampsj
    kampsj about 11 years
    A lot of solid "best practices" here. Not sure if it is yet but this show definitely go into the github wiki.
  • mythz
    mythz about 11 years
    @kampsj Yep added a link to this in 5) How to design a Message-Based API in the Getting Started section of the wiki.
  • Ellesedil
    Ellesedil over 8 years
    Is this approach still a thing? How does the web service inform the user of a bad request or not found?
  • scotru
    scotru over 7 years
    I find the advice here very helpful. I've posted a follow up question here: stackoverflow.com/questions/39947319/…