Adding Custom Routes to the REST API
Overview
It is possible to add your own custom routes to the Jiwa 7 REST API. This is achieved via a plugin.
A sample plugin is installed but disabled - "REST API Custom Routes Example". This plugin demonstrates how you can add your own custom routes without altering the standard Jiwa REST API plugin.
What follows in this article is an analysis of the plugin "REST API Custom Routes Example" and how it adds a route for consumers of the REST API to retrieve contacts for a nominated debtor.
Plugin Structure
The plugin consists of four distinct components - Configure, Requests, Responses and Services.
Configure
The plugin should have a class which implements the JiwaFinancials.Jiwa.JiwaApplication.IJiwaRESTAPIPlugin
interface. It includes a Configure public method. The method is invoked when your implemented service starts (e.g. self hosted Jiwa 7 API or IIS Site).
You can have multiple plugins which have implementations of IJiwaRESTAPIPlugin - each plugin will be invoked in turn by order of the Execution Order of the plugin.
The Configure method is also where the routes are defined.
Upon examining the plugin "REST API Custom Routes Example", you will see the following class implementing the IJiwaRESTAPIPlugin interface:
namespace MyCustomNamespace { public class RESTAPIPlugin : System.MarshalByRefObject, JiwaFinancials.Jiwa.JiwaApplication.IJiwaRESTAPIPlugin { public void Configure(JiwaFinancials.Jiwa.JiwaApplication.Plugin.Plugin Plugin, ServiceStack.ServiceStackHost AppHost, Funq.Container Container, JiwaFinancials.Jiwa.JiwaApplication.Manager JiwaApplicationManager) { AppHost.RegisterService<CustomServices>(); AppHost.Routes.Add(typeof(MyDebtorContactGetRequest), "/Debtors/{DebtorID}/MyContacts", "GET", "Retrieves a list of contacts for a debtor.", ""); } } }
Plugins extending the API should always have a namespace around their classes.
Failure to place DTO classes (request and response classes) in a namespace will result in those classes being excluded from code generation for strongly typed clients.
Let's analyse what is going on here.
We have provided a Configure method as required by the interface.
The AppHost.RegisterService<CustomServices>()
is telling ServiceStack to register the class CustomServices (defined further down in the plugin). This is so ServiceStack can autowire things up on the next line where we add a route:
AppHost.Routes.Add(typeof(MyDebtorContactGetRequest), "/Debtors/{DebtorID}/MyContacts", "GET"...
In the above, we are adding a single route: /Debtors/{DebtorID}/MyContacts. We're also telling ServiceStack that the route will use a request DTO (Data Transfer Object - another term for a simple class or POCO) type of MyDebtorContactGetRequest - which is defined later in the plugin. Now that ServiceStack knows what DTO type the route wants as a request, it will look at all the public methods of all registered services for one that accepts this DTO type as a parameter - it will find the Get method of the CustomServices class in this case. That will be the method invoked when the route is called.
The {DebtorID} part of the route refers to a route parameter - in this case it is a variable placeholder parameter.
For example, calling /Debtors/0000000061000000001V/MyContacts will create a new instance of the MyDebtorContactGetRequest class, populate the public property DebtorID with 0000000061000000001V and invoke the Get method of a the CustomServices class, passing this new instance of the request DTO MyDebtorContactGetRequest as a parameter.
Requests
The request DTO is simply a class with some public properties. We specify what response DTO is returned by the service with the IReturn<MyDebtorContactGetResponse>
implementation. This is an empty implementation, it's simply used to signify to ServiceStack what response DTO is expected from this request purely for consumers of the API which choose to use ServiceStack clients.
We only have one property in this DTO in this example: DebtorID. Because it's named the same as the placeholder variable {DebtorID} when we defined the route - it gets automatically set for us. So, ServiceStack will create an instance of our request DTO below, set the DebtorID property and then invoke the method on the CustomServices class which has a request type of MyDebtorContactGetRequest, passing the newly created object as the request parameter.
[ApiResponse(200, "Contacts read OK")] [ApiResponse(401, "Not authenticated")] [ApiResponse(403, "Not authorised")] [ApiResponse(404, "No debtor with the DebtorID provided was found")] public class MyDebtorContactGetRequest : IReturn<MyDebtorContactGetResponse> { public string DebtorID { get; set; } }
The [ApiResponse]
attributes decorating the request DTO indicate what possible HTTP status codes might be expected. This is used for generating the Open API Specification document.
DTO request classes cannot be shared across routes/services
You must have a unique request DTO class for each route. The name of the class needs to be unique, otherwise there is ambiguity as to which service method should handle the request.
This is necessary because some clients (such as javascript) do not have a namespace concept.
Responses
The response DTO is also simply a class with some public properties.
public class MyDebtorContactGetResponse { public List<CN_Contact> Contacts {get; set;} }
This class will get serialised to JSON, XML or CSV depending on the route requested, the Accept headers and the DefaultContentType configured:
- If a client calls the route /Debtors/0000000061000000001V/MyContacts, then the result is returned as defined as the DefaultContentType configured (JSON is configured as the DefaultContentType in the Jiwa 7 REST API plugin)
- If a client calls the route /Debtors/0000000061000000001V/MyContacts, and the client has set a HTTP Accept header of application/json then the result is returned as JSON
- If a client calls the route /Debtors/0000000061000000001V/MyContacts.json, then the result is returned as JSON
- If a client calls the route /Debtors/0000000061000000001V/MyContacts?format=json, then the result is returned as JSON
- If a client calls the route /Debtors/0000000061000000001V/MyContacts, and the client has set a HTTP Accept header of application/xml then the result is returned as XML
- If a client calls the route /Debtors/0000000061000000001V/MyContacts.xml, then the result is returned as XML
- If a client calls the route /Debtors/0000000061000000001V/MyContacts?format=xml, then the result is returned as XML
- If a client calls the route /Debtors/0000000061000000001V/MyContacts, and the client has set a HTTP Accept header of application/csv then the result is returned as a CSV File
- If a client calls the route /Debtors/0000000061000000001V/MyContacts.csv, then the result is returned as a CSV File.
- If a client calls the route /Debtors/0000000061000000001V/MyContacts?format=csv, then the result is returned as a CSV File.
Services
Now we get to the actual code that does our work. The CustomServices class must inherit from ServiceStack.Service, and it defines the methods that handle the requests defined when we added the routes.
In this case we have a single method named Get which accepts a DebtorContactGetRequest and returns a DebtorContactGetResponse. As mentioned earlier, ServiceStack will create the request, set properties automatically based on naming convention and invoke our method:
public class CustomServices : Service { [Authenticate] public MyDebtorContactGetResponse Get(MyDebtorContactGetRequest request) { JiwaApplication.Manager manager = this.SessionAs<JiwaAuthUserSession>().Manager; var query = Db.From<CN_Contact>() .Join<CN_Contact, CN_Main>((contact, prospect) => prospect.DebtorID == contact.ProspectID) .Join<CN_Main, DB_Main>((prospect, debtor) => prospect.DebtorID == debtor.DebtorID && debtor.DebtorID == request.DebtorID); return new MyDebtorContactGetResponse() { Contacts = Db.Select(query) } ; } }
The method has a [Authenticate]
attribute decorating it - this tells ServiceStack the user must be authenticated to invoke this method, a 401 (Not Authenticated) will be returned otherwise. Authentication is implemented in the Jiwa 7 REST API Plugin - as a developer extending the API you don't need to concern yourself with that aspect, as it is handled automatically.
The first line of the Get method should probably always be invoked:
JiwaApplication.Manager manager = this.SessionAs<JiwaAuthUserSession>().Manager;
What this is attempting to do is retrieve from the Session the Manager instance associated with the users session. This is important as any use of Jiwa business logic will need the right Manager instance associated with the user session at time of logon. The right instance is important as it contains the correct identity and permissions for the user. This should be viewed as simple boiler-plate code you can simply copy and paste into each method of your service class.
The next part of the code is constructing a SQL query using ORMLite:
var query = Db.From<CN_Contact>() .Join<CN_Contact, CN_Main>((contact, prospect) => prospect.DebtorID == contact.ProspectID) .Join<CN_Main, DB_Main>((prospect, debtor) => prospect.DebtorID == debtor.DebtorID && debtor.DebtorID == request.DebtorID);
The classes CN_Main, CN_Contact and DB_Main above are generated from our database schema automatically with every release and placed in the JiwaServiceModel.dll assembly. You don't need to use ORMLite to run SQL queries - see the Jiwa 7 REST API plugin for examples using the perhaps more familiar SqlClient.SqlCommand. Later in this article an example is shown of also using the JiwaDebtors.Debtor business logic to retrieve the debtor and serialise the contacts into a response.
The final line of the method is the returning of the response:
return new MyDebtorContactGetResponse() { Contacts = Db.Select(query) } ;
It is simply creating a new MyDebtorContactGetResponse response DTO and setting the Contacts property. ServiceStack will serialise the response to the appropriate format automatically.
For the sake of comparison, the below method shows an alternative service method which does not use ORMLite, but instead uses the Jiwa debtor business logic to read a debtor, serialise it and return the contact names:
public class CustomServices : Service { [Authenticate] public DebtorContactBLGetResponse Get(DebtorContactBLGetRequest request) { JiwaApplication.Manager manager = this.SessionAs<JiwaAuthUserSession>().Manager; JiwaDebtors.Debtor debtor = manager.BusinessLogicFactory.CreateBusinessLogic<JiwaDebtors.Debtor>(null); debtor.Read(request.DebtorID); JiwaDebtors.ServiceModel.Debtor debtorServiceModel = debtor.DTO_Serialise(); return new DebtorContactBLGetResponse() { Contacts = debtorServiceModel.ContactNames } ; } }
The request and response classes for the above:
[ApiResponse(200, "Contacts read OK")] [ApiResponse(401, "Not authenticated")] [ApiResponse(403, "Not authorised")] [ApiResponse(404, "No debtor with the DebtorID provided was found")] public class DebtorContactBLGetRequest : IReturn<DebtorContactBLGetResponse> { public string DebtorID { get; set; } } public class DebtorContactBLGetResponse { public List<JiwaDebtors.ServiceModel.ContactName> Contacts {get; set;} }
And the alternate configure to wire the alternate service and request:
public class RESTAPIPlugin : System.MarshalByRefObject, JiwaFinancials.Jiwa.JiwaApplication.IJiwaRESTAPIPlugin { public void Configure(JiwaFinancials.Jiwa.JiwaApplication.Plugin.Plugin Plugin, ServiceStack.ServiceStackHost AppHost, Funq.Container Container, JiwaFinancials.Jiwa.JiwaApplication.Manager JiwaApplicationManager) { AppHost.RegisterService<CustomServices>(); AppHost.Routes.Add(typeof(DebtorContactBLGetRequest), "/Debtors/{DebtorID}/contactsBL", "GET", "Retrieves a list of contacts for a debtor.", ""); } }
Requires Business Logic JiwaDebtors
As the above alternate example uses the JiwaDebtors.dll assembly, a reference to that assembly should be added to the references tab of the plugin.
Restarting After Plugin Changes
Whenever a plugin is enabled, disabled or modified, the REST API Service should be restarted for those changes to take effect.
If running using IIS a route is provided to restart using the REST API. This way a developer does not need access to the IIS Server to issue a restart to test changes made during development.
If running using the Self hosted service, a restart is not possible - but a stop is. The REST API has a stop route for self hosted configurations:
If the service recovery properties are set to "Restart the Service" then the service will restart automatically when stopped - essentially emulating a restart. Again, this can be useful for developers as they do not need access to the Windows service running the API to test changes during development.