Service Gateway

The Service Gateway is implemented on top of ServiceStack's existing message-based architecture to open up exciting new possibilities for development of loosely-coupled Modularized Service Architectures.

The new IServiceGateway interfaces represent the minimal surface area required to support ServiceStack's different calling conventions in a formalized API that supports both Sync and Async Service Integrations:

public interface IServiceGateway
{
    // Normal Request/Reply Services
    TResponse Send<TResponse>(object requestDto);

    // Auto Batched Request/Reply Requests
    List<TResponse> SendAll<TResponse>(IEnumerable<object> requestDtos);

    // OneWay Service
    void Publish(object requestDto);

    // Auto Batched OneWay Requests
    void PublishAll(IEnumerable<object> requestDtos);
}

// Equivalent Async API's
public interface IServiceGatewayAsync
{
    Task<TResponse> SendAsync<TResponse>(object requestDto, 
        CancellationToken token = default(CancellationToken));

    Task<List<TResponse>> SendAllAsync<TResponse>(IEnumerable<object> requestDtos, 
        CancellationToken token = default(CancellationToken));

    Task PublishAsync(object requestDto, 
        CancellationToken token = default(CancellationToken));

    Task PublishAllAsync(IEnumerable<object> requestDtos, 
        CancellationToken token = default(CancellationToken));
}

The minimum set of API's above requires the least burden for IServiceGateway implementers whilst the ServiceGatewayExtensions overlays convenience API's common to all implementations providing the nicest API's possible for Request DTO's implementing the recommended IReturn<T> and IReturnVoid interface markers. The extension methods also provide fallback pseudo-async support for IServiceGateway implementations that also don't implement the optional IServiceGatewayAsync, but will use native async implementations for those that do.

Naked Request DTO's without annotations are sent as a POST but alternative Verbs are also supported by annotating Request DTO's with HTTP Verb Interface Markers where Request DTO's containing IGet, IPut, etc. are sent using the typed Verb API, e.g:

[Route("/customers/{Id}")]
public class GetCustomer : IGet, IReturn<Customer>
{
    public int Id { get; set ;}
}

var customer = client.Send(new GetCustomer { Id = 1 }); //GET /customers/1
//Equivalent to:
var customer = client.Get(new GetCustomer { Id = 1 });  

Service Integration API's

To execute existing ServiceStack Services internally you can call ExecuteRequest(requestDto) which passes the Request DTO along with the current IRequest into the ServiceController.Execute() to execute. The alternative is to call ResolveService<T> to resolve an autowired instance of the Service that's injected with the current IRequest context letting you call methods on the Service instance directly. Below is an example of using both API's:

public object Any(GetCustomerOrders request)
{
    using var orderService = base.ResolveService<OrderService>();
    return new GetCustomerOrders {
        Customer = (Customer)base.ExecuteRequest(new GetCustomer { Id = request.Id }),
        Orders = orderService.Any(new QueryOrders { CustomerId = request.Id })
    };
}

The recommended approach now is to instead use the IServiceGateway accessible from base.Gateway available in all Service, Razor Views, MVC ServiceStackController classes, etc. It works similar to the ExecuteRequest() API (which it now replaces) where you can invoke a Service with just a populated Request DTO, but instead yields an ideal typed API for Request DTO's implementing the recommended IReturn<T> or IReturnVoid markers:

public object Any(GetCustomerOrders request)
{
    return new GetCustomerOrders {
        Customer = Gateway.Send(new GetCustomer { Id = request.Id }),
        Orders = Gateway.Send(new QueryOrders { CustomerId = request.Id })
    };
}

Or you can use the Async API if you prefer the non-blocking version:

public async Task<GetCustomerOrdersResponse> Any(GetCustomerOrders request)
{
    return new GetCustomerOrdersResponse {
        Customer = await Gateway.SendAsync(new GetCustomer { Id = request.Id }),
        Orders = await Gateway.SendAsync(new QueryOrders { CustomerId = request.Id })
    };
}

The capability that sets the ServiceGateway apart (other than offering a nicer API to work with) is that this System could later have its Customer and Order Subsystems split out into different hosts and this exact Service implementation would continue to function as before, albeit a little slower due to the overhead of any introduced out-of-process communications.

The default implementation of IServiceGateway uses the InProcessServiceGateway which delegates the Request DTO to the appropriate ServiceController.Execute() or ServiceController.ExecuteAsync() methods to execute the Service. One noticeable difference is that any Exceptions thrown by downstream Services are automatically converted into the same WebServiceException that clients would throw when calling the Service externally, this is so Exceptions are indistinguishable whether it's calling an internal Service or an external one, which begins touching on the benefits of the Gateway...

The ServiceGateway is the same interface whether you're calling an Internal Service on the Server or a remote Service from a client. It exposes an ideal message-based API that's optimal for remote Service Integrations that also supports Auto Batched Requests for combining multiple Service Calls into a single Request, minimizing latency when possible.

Substitutable Service Gateways

These characteristics makes it easy to substitute and customize the behavior of the Gateway as visible in the examples below. The easiest scenario to support is to redirect all Service Gateway calls to a remote ServiceStack instance which can be done by registering any .NET Service Client against the IServiceGateway interface, e.g:

public override void Configure(Container container)
{
    container.Register<IServiceGateway>(c => new JsonApiClient(baseUrl));
}

A more likely scenario you'd want to support is a mix where internal requests are executed in-process and external requests call their respective Service. If your system is split in two this becomes a simple check to return the local InProcess Gateway for Requests which are defined in this ServiceStack instance otherwise return a Service Client configured to the alternative host when not, e.g:

public class CustomServiceGatewayFactory : ServiceGatewayFactoryBase
{
    public override IServiceGateway GetGateway(Type requestType)
    {
        var isLocal = HostContext.Metadata.RequestTypes.Contains(requestType);
        var gateway = isLocal
            ? (IServiceGateway)base.localGateway
            : new JsonApiClient(alternativeBaseUrl);
        return gateway;
    }
}

For this we needed to implement the IServiceGatewayFactory so we can first capture the current IRequest that's needed in order to call the In Process Service Gateway with. The convenience ServiceGatewayFactoryBase abstracts the rest of the API away so you're only tasked with returning the appropriate Service Gateway for the specified Request DTO.

Capturing the current IRequest makes the Gateway factory instance non-suitable to use as a singleton, so we'll need to register it with AddTransient or ReuseScope.None scope so a new instance is resolved each time:

public override void Configure(Container container)
{
    container.AddTransient<IServiceGatewayFactory>(() => new CustomServiceGatewayFactory());
    
// Equivalent to:
//    container.Register<IServiceGatewayFactory>(x => new CustomServiceGatewayFactory())
//        .ReusedWithin(ReuseScope.None);
}

Call Internal and External Services from URLs

The Metadata.CreateRequestFromUrl() API lets you create Request DTOs from absolute or relative URLs. This is useful if you need a generic routine to be able to execute a number of different Services from a collection or URL's, e.g:

var processUrls = new []{ "https://example.org/invoices/generate?userId=1", "/assets/1/generate" };
foreach (var url in processUrls) 
{
    var request = HostContext.Metadata.CreateRequestFromUrl(url);
    var responseType = HostContext.Metadata.GetResponseTypeByRequest(request.GetType());
    var response = HostContext.AppHost.GetServiceGateway().Send(responseType, request);

    db.Insert(new Task { Url = url, Response = response.ToJson(), Completed = DateTime.UtcNow });
}

The Service Gateway provides an optimal way for executing Services where it will transparently execute local requests in process or external requests remotely using either the configured Service Gateway or Service Discovery Solution.

Service Discovery

This demonstrates the underpinnings by which we can plug into and intercept all intra-Service calls and apply our own high-level custom logic which sets the foundation for other value-added functionality like Service Discovery which can transparently route service calls to the most appropriate available remote endpoint at run-time, automatically without additional configuration or code-maintenance overhead.

Designing for Microservices

Whether or not Systems benefit overall from a fine-grained microservices architecture, enough to justify the additional latency, management and infrastructure overhead it requires, we still see value in the development process of designing for Microservices where decoupling naturally isolated components into loosely-coupled subsystems has software-architecture benefits with overall complexity of an entire system being reduced into smaller, more manageable logical scopes which encapsulates their capabilities behind re-usable, coarse-grained messages to small, well-defined facades.

The ServiceGateway and its Services Discovery ecosystem together with ServiceStack's recommended use of impl-free reusable POCO DTO's and its ability to modularize Service implementations across multiple projects naturally promote a microservices-ready architecture where Service interactions are loosely-coupled behind well-defined, reusable, coarse-grained messages. Designing systems in this way later allows the isolated Service Implementation .dll to be extracted from the main System and wrapped into its own AppHost. Together with an agreed Service Discovery solution, allows you to spawn multiple instances of the new Service - letting you scale, deploy and maintain it independently from the rest of the system.