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:

Class implementing IJiwaRESTAPIPlugin
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.

Request class
[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.  

Response class
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:

Service class
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_MainCN_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:

Alternate Method - Using Jiwa business logic
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:

Alternate Request and Response
[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:

Configure
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.

Swagger UI for servicerestart

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:

Swagger UI for servicestop

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.



Consuming the new routes



Invoke the new route
 ServiceStack Client C#
var client = new ServiceStack.JsonServiceClient("https://api.jiwa.com.au");
var authResponse = client.Get(new ServiceStack.Authenticate() { UserName = "admin", Password = "password" });

var MyDebtorContactGetRequest = new MyDebtorContactGetRequest () { DebtorID = "0000000061000000001V" };
var MyDebtorContactGetResponse = client.Get(MyDebtorContactGetRequest);
 C#
using (var webClient = new System.Net.WebClient())
{
    // Authenticate               
    webClient.QueryString.Add("username", "Admin");
    webClient.QueryString.Add("password", "password");
     
    string responsebody = webClient.DownloadString("https://api.jiwa.com.au/auth");               
    // Above returns something like this: {"SessionId":"0hKBFAnutUk8Mw6YY6DN","UserName":"api","DisplayName":"","ResponseStatus":{}}
 
    // Deserialise response into a dynamic - below requires the Newtonsoft.Json nuget package
    var authResponse = Newtonsoft.Json.JsonConvert.DeserializeObject<dynamic>(responsebody);
    var sessionId = authResponse.SessionId;
 
    webClient.Headers.Add(System.Net.HttpRequestHeader.Cookie, string.Format("ss-id={0}", sessionId));               
    responsebody = webClient.DownloadString("https://api.jiwa.com.au/Debtors/0000000061000000001V/MyContacts");
}
 Curl
curl -H 'Accept: application/json' -H 'Content-Type: application/json' -X GET https://api.jiwa.com.au/auth -d '{"username":"Admin","password":"password"}'

Returns the following authentication response, containing the SessionId which subsequent requests will need to include in the cookie "ss-id"

{"SessionId":"6w1nLX8r0sIrJHClX9Vj","UserName":"Admin","DisplayName":"","ResponseStatus":{}}

Then, with the SessionId now known, the route can be called:

 curl -H 'Accept: application/json' -H 'Content-Type: application/json' --cookie 'ss-id=6w1nLX8r0sIrJHClX9Vj' -X GET https://api.jiwa.com.au/Debtors/0000000061000000001V/MyContacts
 Web Browser

Navigate to the auth URL and provide the username and password as parameters:

https://api.jiwa.com.au/auth?username=admin&password=password

This authenticates the user and creates a cookie, so a subsequent request can be made:

https://api.jiwa.com.au/Debtors/0000000061000000001V/MyContacts?format=json

Note the ?format=json in the above URL this overrides the content type returned. For browsers the default content type is HTML - if a content type override is omitted, then a HTML razor view of the data will be returned instead of json. xml and csv are also valid overrides for the content type to be returned.