Microservices: Type Discovery and Instancing
Posted on June 19, 2016
Let’s suppose that a web client has the URL of your microservice endpoint. When the client makes a web request to that endpoint, how does the correct code get called on the web server?
Our microservice consists of code running on a web server organized into classes in an object-oriented language, and each microservice endpoint corresponds to a unique method in one of these classes.
In my discussion of endpoint naming I used this microservice endpoint URL:
/services/HomeDepot.Platform.Issues.Microservice/1.0.1/Issue/resolve/42
In our microservice framework, the name HomeDepot.Platform.Issues.Microservice corresponds to the class library of that name. Issue corresponds to the class name, and resolve maps to a method named Resolve in that class. This naming convention is the basis of our type discovery mechanism.
Type Discovery
Our endpoint URL names help us understand which class and method will handle a given web request. The different segments of the URL map nicely to a hierarchy of classes and methods. If the request URL indicates a microservice domain/category of “Issue”, we’ll look for a class named Issue. Similarly, if the operation in the request is “resolve”, we will look for a method of Issue named Resolve(). We can simplify a little bit by ignoring case when matching the name so that a request for resolve matches Resolve() or resolve().
It is also helpful to make our code explicitly opt in to responding to web requests, so that methods that perform sensitive operations aren’t automatically exposed to the world of web clients. To do this, we require classes that support APIs to derive from a specific base class, and require methods be annotated with a special attribute to indicate that they are callable through web requests.
On receipt of a microservice web request, and if you’re doing more than receipts and want to do online signatures you can use the sodapdf software for this, we look up the class that implements the business domain named in the request URL, and find the class method that matches the name of the requested operation.
A Microservice Class
Let’s look the code that responds to the URL for issue/resolve above. Our microservice framework is built on ASP.NET Core, so from here on in, code examples will be in C#.
public class Issue : DomainEntity { [APIContract(APIContractVerb.POST)] public async Task Resolve() { return await UpdateIssueAsync(IssueStatus.Resolved); } }
A lot of supporting code in the above snippet has been removed for clarity. What is clear though is that the Issue class derives from the DomainEntity base class that supports microservice capabilities, and the Resolve method is annotated with the [APIContract] attribute, identifying it as a method that will respond to web requests.
Instancing
Let’s assume for now that the web server hosting our microservice has this domain entity Issue class available in memory. We know which class method we should call. Yet there are still more decisions for us. If the method is static, we can call it directly. But if the API is an instance method, we must decide if any instance will do, or if a specific instance must be used.
Our Forge microservice framework is based on domain-driven design principles. This means that our API operations reflect operations in a specific business domain. We model these domains as domain entity classes, and instances of those classes correspond to specific real-world entity in our business model. An Invoice class models the business domain concept of an invoice, and an instance of the Invoice class models a specific, actual invoice. These domain entities are the “nouns” in the language of our business context.
Each concrete entity in our business model (such as an invoice, or a support issue) has a unique identity. An invoice might be associated with a customer, but there might be many invoices for a customer over time. Thus we assign each invoice a unique number that we can use to distinguish it from all of the other invoices. In our microservice API, we’ll use this unique number or Id to specify which instance of a domain entity class should be used when invoking the API method. This instance Id thus must also be included in the API URL when calling an instance method:
/services/HomeDepot.Platform.Issues.Microservice/1.0.1/Issue/resolve/42
Here we represent the Issue instance Id as the last segment of the URL: 42.
If we were specifying an operation that affects all of our business entities (or no specific entity) we could provide a static method of the domain entity class to implement the API.
Repositories
Where does a domain entity instance come from?
Certainly we could just call the entity class constructor and conjure up an instance. But we don’t want any old instance. We want the instance that contains the state associated with the identifier specified in the API request. To do this, we request the instance from an entity repository class. A repository is simply a class that abstracts operations on the whole collection of a type of entities. This includes operations such as fetching an entity by Id, or searching for entities that match some criteria, or adding a new entity. Typically a repository wraps calls to read and write to a database. Our implementation of entity repository provides a method to fetch a specific entity instance:
public abstract class EntityRepository : where T : DomainEntity { [APIContract(APIContractVerb.GET, "fetch/{id}")] public virtual async Task<T> FetchAsync(int id) { T result = await Cache.FetchAsync(id); if (result == null) result = await Persistence.FetchAsync(id); return result; } }
This simplified version of FetchAsync() illustrates how we first try to look up the entity in a global cache, and only if that fails, go to our slower persistence layer (i.e. the database) to retrieve an instance.
Since EntityRepository is an abstract class, we would derive a specific implementation for our Issue domain entity:
public class IssueRepository : EntityRepository { ... }
After calling FetchAsync() on our repository class we have an instance of Issue, so we’re finally ready to call the API method Resolve(), right? For this simple case, yes. We have everything we need: the class, the method, and the specific instance of the class to call the method on. Unfortunately, this scenario is not typical. Usually, an API method will require arguments to be provided by the caller. The client will need to format these arguments properly in the web request, and our web server code will need to retrieve these values from the incoming web request and map them to the data types specified by the method signature.
In my next post, we’ll look at how API arguments are sent by the caller and how these arguments are decoded by the server and passed to the microservice API method.
Got something to say?