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'sInvokeAsync(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'sServiceRequest
class
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.
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;
}
}
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"));
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;
}
}
}
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. |
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 . |
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. |