Getting Started - Services
API Design
The primary difference between developing RPC vs ServiceStack's Message-based Services is that the Services entire contract is defined by its typed messages, specifically the Request DTO which defines both the System inputs and identifies the System output. Typically both are POCO DTOs however the response can be any serializable object.
As only the Any()
wildcard method is defined, it will get executed whenever the GetContacts
Service is invoked via any HTTP Verb, gRPC, MQ or SOAP Request.
The Request DTO is also all that’s required to invoke it via any Typed Generic Service Client in any supported language, e.g: client.Get(new GetContacts());
public class Contact
{
public int Id { get; set; }
public string Name { get; set; }
}
[Route("/contacts")]
public class GetContacts : IReturn<List<Contact>> { }
public class ContactsService : Service
{
public object Any(GetContacts request) =>
Db.Select<Contact>();
}
Client.cs
var client = new JsonServiceClient(baseUrl);
List<Contact> response = client.Get(new GetContacts());
Routing
Without any configuration required, ServiceStack already includes pre-defined routes for all services in the format:
/api/[RequestDto]For example, the pre-defined URL to call a JSON
Hello
Service is /api/Hello
Friendly custom routes are defined using the RouteAttribute
applied to the Request DTO. Variable place holders bind by property name and can be used in the path. For example, the path /hello/world
will bind "world" to the Name
property of the Request DTO.
The QueryString, FormData and HTTP Request Body isn’t apart of the Route (i.e. only the /path/info is) but they can all be used in addition to every web service call to further populate the Request DTO. Routing can be limited to specific verbs, have flexible matching rules as well as matching specific service methods to be Content-Type
specific.
[Route("/hello")]
[Route("/hello/{Name}")]
public class Hello : IGet, IReturn<HelloResponse>
{
public string Name { get; set; }
}
public class HelloResponse
{
public string Result { get; set; }
}
public class MyServices : Service
{
public object Any(Hello request) =>
new HelloResponse {
Result = $"Hello, {request.Name}!"
};
}
HTTP Verbs
ServiceStack Services lets you handle any HTTP Verb in the same way by using a matching method name in your Service class. Routing by default will match All HTTP verbs, but an additional parameter can be specified to limit routes to individual verbs. This lets you route the same path to different services.
Multiple verbs on a Route
can be used with a single Any
service method to handle situations where you want multiple, but specific, verbs to map to a single service method like handling both the creation and update of a resource.
public class ContactServices
{
public object Get(GetContacts request)
{
var contacts = request.Id == null ?
Db.Select<Contact>() :
Db.Select<Contact>(x => x.Id == request.Id.ToInt());
return contacts;
}
public object Any(UpdateContact request)
{
var contact = Db.SingleById<Contact>(request.Id);
contact.PopulateWith(request);
Db.Update(contact);
return contact;
}
public void Delete(DeleteContact request) =>
Db.Delete<Contact>(x => x.Id == request.Id);
}
Contacts.cs
[Route("/contacts")]
[Route("/contacts/{Id}")]
public class GetContacts : IGet, IReturn<List<Contact>>
{
public int? Id { get; set; }
}
[Route("/contacts/{Id}", "PATCH PUT")]
public class UpdateContact : IPut, IReturn<Contact>
{
public int Id { get; set; }
}
[Route("/contacts/{Id}")]
public class DeleteContact : IDelete, IReturnVoid
{
public int Id { get; set; }
}
Filters
ServiceStack has several ways to have custom filter methods on requests and responses. Global filters will fire every request/response allowing you to apply your own generic custom behavior across your application.
Typed request/response filters give you a strongly typed API for specific message types. These can be registered for MQ and HTTP independently. All filters can access the underlying Request, Response and related typed DTO. If you need access to resources from your IoC, you can also register Autowired Typed Filters which can have IoC dependencies injected like services.
Filter attributes are another powerful way to have generic filter that can be then applied to service methods as .NET attributes as needed. An example of this is the ServiceStack [Authenticate]
and [RequiredPermission]
attributes are filter attributes.
public class AppHost : AppHostBase
{
public AppHost() : base("Web",typeof(MyServices).Assembly){}
public override void Configure(Container container)
{
GlobalRequestFilters.Add((req, res, requestDto) => {
var sessionId = req.GetCookieValue("user-session");
if (sessionId == null)
res.ReturnAuthRequired();
});
RegisterTypedRequestFilter<Resource>((req, res, dto) =>
{
var route = req.GetRoute();
if (route?.Path == "/tenant/{Name}/resource")
dto.SubResourceName = "CustomResource";
});
}
}
public class MyServices : Service
{
[NoNumbersInPath]
public object Any(Hello request)
{
return new HelloResponse {
Result = $"Hello, {request.Name}!"
};
}
}
public class NoNumbersInPathAttribute : RequestFilterAttribute
{
public override void Execute(IRequest req, IResponse res,
object requestDto)
{
if (Regex.IsMatch(req.PathInfo, "[0-9]"))
throw HttpError.BadRequest("No numbers");
}
}
Validation
As validation and error handling is an essential part of developing services, ServiceStack provides a rich array of error handling options that work intuitively out-of-the-box.
The ValidationFeature
plugin automatically scans and auto-wires all validators in the AppHost.ServiceAssemblies
that’s injected in the AppHost
constructor. Like services registered in the IoC container, validators are also auto-wired, so if there’s a public property which can be resolved by the IoC container, the IoC container will inject it.
The validation rules for this request dto are made with FluentValidation. ServiceStack makes heavy use of rule sets to provide different validation rules for each HTTP method (GET, POST, PUT…). This enables developers to write comprehensive validation rules to suit complex business requirements.
public class AppHost() : AppHostBase("MyApp"), IHostingStartup
{
public void Configure(IWebHostBuilder builder) => builder
.ConfigureServices(services => {
services.AddSingleton<IAddressValidator>(c => new AddressValidator());
});
}
// ServiceInterface
public class MyServices : Service
{
public object Post(CreateUser request)
{
var id = Db.Insert(request.ConvertTo<User>());
var user = Db.SingleById<User>(id);
return user;
}
}
public interface IAddressValidator
{
bool ValidAddress(string address);
}
public class AddressValidator : IAddressValidator
{
public bool ValidAddress(string address)
{
return address != null
&& address.Length >= 20
&& address.Length <= 250;
}
}
public class UserValidator(IAddressValidator addressValidator)
: AbstractValidator<CreateUser>
{
public UserValidator()
{
//Validation rules for all requests
RuleFor(r => r.Name).NotEmpty();
RuleFor(r => r.Age).GreaterThan(0);
RuleFor(x => x.Address).Must(x =>
addressValidator.ValidAddress(x));
}
}
// ServiceModel
[Route("/users", "POST")]
public class CreateUser : IReturn<User>
{
public string Name { get; set; }
public string Company { get; set; }
public int Age { get; set; }
public int Count { get; set; }
public string Address { get; set; }
}
public class User
{
[AutoIncrement]
public int Id { get; set; }
public string Name { get; set; }
public string Company { get; set; }
public int Age { get; set; }
public int Count { get; set; }
public string Address { get; set; }
}
Formats
ServiceStack bundles support for 5 default formats. Those are HTML, JSON, XML, CSV and JSV. Additionally ServiceStack has support for SOAP, Message Pack, Protocol Buffers and Wire. Since ServiceStack is message based, the serialization of requests and responses are completely interchangeable.
ServiceStack Services supports a number of Content Negotiation options where you can define which
format should be returned by adding a .{format}
extension to your /route.{format}
. For example, /hello/world.json
and /hello/world?format=json
will both respond to in the JSON format.
Adding these additional formatters like MessagePack can be done by including the relevant dependency from NuGet and adding support using Plugins.Add
. For Example, adding the NuGet package ServiceStack.MsgPack
and then in the AppHost, Plugins.Add(new MsgPackFormat());
.
public class AppHost() : AppHostBase("MyApp"), IHostingStartup
{
public void Configure(IWebHostBuilder builder) => builder
.ConfigureServices(services => {
services.AddSingleton<IAddressValidator>(c => new AddressValidator());
// Add additional format support by using Plugins
services.AddPlugin(new MsgPackFormat());
});
}
public class MyServices : Service
{
// HTML, JSON, CSV, XML and JSV enabled by default
public object Any(Hello request) =>
new HelloResponse {
Result = $"Hello, {request.Name}"
};
}