Extension: IService - quandis/qbo3-Documentation GitHub Wiki

QBO's IService interface is a hook for developers to integrate custom code into QBO. Key steps include:

  • Create a CLR class that implements the IService interface's InvokeAsync(IDictionary<string, object>) method
  • Deploy to server(s) running QBO
  • Configure one or more Service endpoints to call your class

When implementing your class, consider whether to:

  • Implement the IService interface directly,
  • Derive from AbstractService, which provides sugar for accessing QBO settings (like credentials), or
  • Derive from AbstractServiceRequest, which adds logging information via QBO's ServiceRequest class

Stock Quote example

The AlphaVantage plugin obtains stock quotes from AlphaVantage.co.

Starting out, we have:

namespace server.Acme.Financial
{
	public class AlphaVantage : AbstractService
	{
	}
}

Note that in our example, we derived from AbstractService instead of implementing IService directly; this allows use to leverage some sugar for accessing credentials and such.

Both the AbstractService and IService interface are defined in the qbo3.Application NUGET package, available from https://nuget.quandis.net/nuget.

To implement the AbstractService, we need to override the InvokeAsync method:

public override async Task<IServiceResult> InvokeAsync(IDictionary<string, object> parameters)
{
	var url = "https://www.alphavantage.co/query?apikey={myApiKey}&function=GLOBAL_QUOTE&symbol=MSFT";
	var result = new ServiceResult();
	var request = WebRequest.Create(url);
	using (var response = await request.GetResponseAsync())
	{
		using (var stream = response.GetResponseStream())
		{
			using (var reader = new StreamReader(stream))
			{
				var json = await reader.ReadToEndAsync();
				result.Success = true;
			}
		}
	}
	return result;
}

There are a few problems with this approach.

Credentialing

The URL called for this web service should embed an ApiKey. Instead of baking into code, we'll choose to pull it from QBO configuration. The AbstractService base class provides a useful CredentialCache that enables the plugin to fetch credentials from QBO.

private string _ApiKey;
private string ApiKey
{
	get
	{
		if (_ApiKey == null)
		{
			var credential = CredentialCache.GetCredential(new Uri(ApiUrl), "Basic");
			_ApiKey = credential?.Password;
		}
		return _ApiKey;
	}
}

Branching

The AlphaVantage service provides multiple functions: TIME_SERIES_INTRADAY, TIME_SERIES_DAILY, TIME_SERIES_WEEKLY, and TIME_SERIES_WEEKLY. Ideally, power users should be able to call something like this:

/Organization/StockIntraDay?Symbol=MSFT
/Organization/StockDaily?Symbol=MSFT
/Organization/StockWeekly?Symbol=MSFT
/Organization/StockQuote?Symbol=MSFT

The naming here ties to a QBO module (Organization) and follows QBO conventions of Pascal-casing operations and parameters.

To enable the plugin to handle each of these operations, we can leverage the ServiceConfig.Name property:

...
// Let's data-drive the ApiKey, Function and Symbol
private static string ApiUrl = "https://www.alphavantage.co/query?apikey={0}&function={1}&symbol={2}";
...
string timeSeries;
switch (ServiceConfig.Name)
{
	case "StockIntraDay":
		timeSeries = "TIME_SERIES_INTRADAY";
		break;
	case "StockDaily":
		timeSeries = "TIME_SERIES_DAILY";
		break;
	case "StockWeekly":
		timeSeries = "TIME_SERIES_WEEKLY";
		break;
	default:
		timeSeries = "GLOBAL_QUOTE";
		break;
}
// Construct the URL from our parameters and configuration.
var url = string.Format(ApiUrl, ApiKey, timeSeries, parameters.GetEntry<string>("Symbol"));

Final code

using Newtonsoft.Json;
using qbo.Application.Interfaces;
using qbo.Application.Services;
using qbo.Application.Utilities.Extensions;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading.Tasks;

namespace server.Acme.Financial
{
	/// <summary>
	/// Sample financial data interface to <see href="https://www.alphavantage.co/"/>.
	/// </summary>
	public class AlphaVantage : AbstractService
	{
		private static string ApiUrl = "https://www.alphavantage.co/query?apikey={0}&function={1}&symbol={2}";
		private string _ApiKey;

		/// <summary>
		/// The ApiKey will use a QBO credential if present, otherwise an application setting.
		/// This is a free service; we don't really care about different ApiKeys between environments.
		/// </summary>
		private string ApiKey
		{
			get
			{
				if (_ApiKey == null)
				{
					var credential = CredentialCache.GetCredential(new Uri(ApiUrl), "Basic");
					_ApiKey = (credential != null)
						? credential.Password
						: Properties.Settings.Default.AlphaVantageApiKey;
				}
				return _ApiKey;
			}
		}

		/// <summary>
		/// The AlphaVantage API offers different endpoints; let's support them all.
		/// </summary>
		/// <param name="parameters">Parameters passed by method signature.</param>
		public override async Task<IServiceResult> InvokeAsync(IDictionary<string, object> parameters)
		{
			// Let's figure out which stock function to call based on the QBO URL
			// Eg. Organization/StockIntraDay?Symbol=MFST
			string timeSeries;
			switch (ServiceConfig.Name)
			{
				case "StockIntraDay":
					timeSeries = "TIME_SERIES_INTRADAY";
					break;
				case "StockDaily":
					timeSeries = "TIME_SERIES_DAILY";
					break;
				case "StockWeekly":
					timeSeries = "TIME_SERIES_WEEKLY";
					break;
				default:
					timeSeries = "GLOBAL_QUOTE";
					break;
			}
			var url = string.Format(ApiUrl, ApiKey, timeSeries, parameters.GetEntry<string>("Symbol"));
			var result = new ServiceResult();
			var request = WebRequest.Create(url);
			try
			{
				using (var response = await request.GetResponseAsync())
				{
					using (var stream = response.GetResponseStream())
					{
						using (var reader = new StreamReader(stream))
						{
							var json = await reader.ReadToEndAsync();
							// Convert to XmlReader
							var xml = JsonConvert.DeserializeXNode(json);
							result.Response = xml.CreateReader();
							result.Success = true;
						}
					}
				}
			}
			catch (System.Exception ex)
			{
				result.Success = false;
				result.Exception = ex;
			}

			return result;
		}

		/// <summary>
		/// In case we hit a sync code path, cover the sync call. But async is better.
		/// </summary>
		public override IServiceResult Invoke(IDictionary<string, object> parameters)
		{
			return InvokeAsync(parameters).Result;
		}


	}
}

IService Inteface Properties

Property Description
ServiceConfig A qbo.Application.Configuration.Service configuration section containing power user-configured properties.
Parent A QBO AbstractObject from which the IService plugin is being invoked. This property is set on the IService after instantiation.

AbstractService Properties

In addition to the IService properties, AbstractService provides the following:

Property Description
CredentialCache A .NET System.Credential.CredentialCache from which the plugin may request credentials managed by QBO.
RequestTransform A XslCompiledTransform based on ServiceConfiguration.RequestTransform.
ResponseTransform A XslCompiledTransform based on ServiceConfiguration.ResponseTransform.

ServiceConfig Properties

The ServiceConfig property is how IService plugins are bound to QBO-based routes. These configuration items can be set via the UI, API calls or as part of setup packages. The configuration properties include:

Property Description
Name Name of the operation to bind the plugin to. In the example above, Organization/StockDaily binds to a Service configuration entry named StockDaily.
Type Common assembly name of the class implementing the IService; this is used to load the assembly via reflection.
ReturnType The type of object returned in IServiceResult.Result by the IService.InvokeMethodAsync. Options include DataSet, DataReader, XmlReader, JsonReader, Object, Collection, Scalar and Void.
RequestMethod The method signature that the plugin may optionally use to gather data from QBO.
RequestTransform Path to an XSLT that may optionally be used to transform request data.
ResponseMethod The method signature that the plugin may optionally use to push resulting data into QBO.
ResponseTransform Path to an XSLT that may optionally be used to transform response data.
EndPoint Optionally used by the plugin to identify the Web API endpoint being called. This is particularly useful when endpoints change between UAT and PROD environments.
Async If true, QBO queues a callback to be executing when End{Operation} is invoked. This is useful for very long runnig operations, like requests that invoke human interaction. QBO workflow steps that invoke an async IService will not be completed until the End{Operation} is invoked.
RequireStream If true, the IDictionary(string, object) parameters passed to the plugin will include the Request and Response streams if the IService is invoked via the web (as opposed to a queued message).
CompleteStep If true, a child Service will mark a parent AbstractServiceRequest step completed, triggering any async callbacks.
LogData Optionally used by the IService plugin to trigger the logging of data.
Repeatable Used by AbstractServiceRequest to control whether multiple requests may be made on the same parent object.
AllowInheritance Optionally used by plugins to determine whether QBO objects created should be created using Parent.InheritedUser or Parent.User. If true, InheritedUser bypasses permissions checks, since QBO enforces a permissions check prior to calling the IService in the first place.
Steps Child IService operations that can be made in relation to the IService current operation. See below for more details.
⚠️ **GitHub.com Fallback** ⚠️