Skip to content

My First Microservice

This tutorial describes what a configurable provider service (microservice) is and how to create a service from the Alkami template. We will do the following:

  1. Build and configure the template service.

  2. Test and debug the example service implementation.

  3. Add new settings and a new contract method that will fetch and use those settings.

  4. Create a new feature for this example and equip it with new request and response objects.

  5. Create object models to store the request and response data with data validations that confirm our request data is valid.

  6. Implement the new feature logic.

  7. Use the Micros Service Tester to test.

Provider-based Services

Developers use provider-based services to define configurable settings for use in the service implementation.

Use Alkami’s Admin Portal to manage these settings.

To do this, go to Setup > Integration Settings and click Providers.

setup screen

The Alkami Provider Service Template

The SDK template creates a structured Visual Studio solution that contains all the projects and implementation details to run the service. The service can be built and run out-of-the-box and contains an easy-to-follow implementation of a single public method. The example code is commented and lays down a request and response pattern to follow when adding additional contract methods.

Install the Template

Run the following command:

choco install Alkami.SDK.Templates -y

Create the Service

Alkami’s SDK template creates a structured Visual Studio solution that contains all the projects and implementation required to run a service with a single public contract method. The example code adheres to Alkami’s preferred request and response pattern so you can take advantage of the Validation framework.

  1. Open Visual Studio and search alkami to filter for Alkami templates. Note: Your version of Visual Studio might look different than the following screenshots. setup screen

  2. Select the Alkami Provider Service template and click Next.

  3. Name the project according to the Microservice Coding Guidelines page. In this example, use USBFI.MS.FirstService.

  4. Select a path close to your C:\ drive.

  5. Uncheck ‘Place solution and project in same directory’ option if it is selected.

  6. Do not select any options and click Create. setup screen

Build the Solution

  1. By default the Service.Host will be the Startup Project. setup screen a.If it is not, right-click the project and select Set as StartUp Project. Build the solution to restore the NuGet packages and create a Debug build of the service.

Configure the Service

  1. To insert the service configuration into the DeveloperDynamic database, locate the insert_provider_setting.sql in the Service.Host project and open it using your SQL engine of choice.

    setup screen

  2. Run the script.

  3. In Server Name, type . then click Connect. setup screen

Run and Test the Service

  1. Open the Service project. setup screen

  2. Click to set a break point on the first line of of the ServiceImp.GetSettingsAsync() method within the ServiceImp.cs class. setup screen

  3. Run (F5) the solution within Visual Studio (Administrator). This opens a cmd.exe window that encapsulates the Host project to make debugging easier. See Topshelf’s documentation page for more information about this strategy.

    A console window opens and runs the service so you can see what is happening in the subscription service. The GetSettingsAsync() public method has a solid red break point. setup screen

  4. Open the Windows Services to see a list of Alkami services including the Alkami.Services.SubscriptionHost. setup screen

Alkami Micro Service Tester

To exercise and test the service, use Alkami’s Micro Service Tester.

  1. Run the following command:

    choco install Alkami.MicroServiceTester -y

  2. After installation, from the Windows Start Menu, search for and open MicroServiceTester. The left margin lists the available services.

  3. Double-click a service to view the available contract methods.

  4. Select a contract method.

  5. For our new service, we will select GetSettings and then click Send Message. The tester sends a request and the code execution should pause on the break point we set in the GetSettingsAsync() implementation method. setup screen

Unit Tests

The template creates a test project and a single test method CanGetSettings() that will create an instance of the distributed service. It then uses the DistributedServices.cs class in the ServiceHost project to call the GetSettingsAsync() method and assert that the two default settings are fetched properly.

  1. Place a break point in the CanGetSettings() test method to debug the service using a unit test.
  2. Press F10 to step through the code execution. setup screen

First Service Loan Decision Feature

The remainder of this tutorial will use an invented feature to guide the developer through the process of extending the template service. The example feature is a Loan Decision engine and will generate a loan approval and a loan amount based on some information provided by the member. The client request will contain a data model that describes the Application Detail submitted by a member. The response will contain a similar model that describes the Loan Detail returned by our decision logic.

We will define new settings, create a request and a response object, create data validations where necessary and add a new GetLoanDecisionAsync() method so that other services and widgets can use the new First Service feature.

Define the new settings

All settings are defined within the service itself. The admin portal has the privilege of overriding the default settings by persisting the values to Alkami’s database. These database values will be used as overrides of the DefaultSettings we’ll be creating in this tutorial.

We’ll duplicate a lot of the existing pattern in order to add two new template settings. The areas that should be added to your classes are highlighted in yellow.

Add the new setting name(s)

One of the features in the FirstService service will determine if a member is eligible for a loan. The loan amount should not exceed a maximum amount and must be greater than a minimum amount. These two values are great candidates to be converted to configurable settings. setup screen

Add the default setting values

In this case, we’ll set the default values for the max and min to 95000 and 5000 respectively. setup screen

Add new descriptors

Descriptors are required and further define the settings when viewing them in the Admin Portal. It’s important to make these meaningful and as descriptive as possible. setup screen

Create new setting validations

The last step is to add validations for these two new settings. These validations are executed when an admin user attempts to update these values using Alkami’s Admin Portal UI.

For this example, we’ll make sure to fail any values that cannot be parsed into an integer. setup screen

Read the new setting

The existing template settings already show the proper pattern for reading settings, we’ll do the same but instead of assigning the string value we’ll parse it into a usable integer type.

We’ll create two local variables so that we can use the settings outside of the disposable DataScope() and then parse the settings into the proper primitive type and assign them to the local variables. setup screen

Adding New Contract Methods

In the previous section we added new settings and updated the existing GetSettingsAsync() method to read in the new values. This section will focus on adding a new public contract method to the service that contains some business logic that will use the new settings we added before.

Much of this implementation is new code, we’ve provided the classes in full so that users can easily copy and paste where necessary. Also, the source code has been provided at the top of this page.

Defining New Request and Response Types

Requests and responses should be more than a list of properties. The service architecture gives the developer full freedom to define complex object types and for that reason we’ll be creating new data objects that will be members of the request and response objects.

Create new data objects

The request and response types will have data object members to customize the type of information we want to receive and return. We’ll be taking a member’s application details as part of the request and responding with the loan details once we’ve ran through our logic.

Add two new classes to the Data project, named LoanDecisionDetail.cs that will be used to store the data from our decisioning logic and MemberApplicationDetail.cs that will contain the information we will use within the decision process.

LoanDecisionDetail.cs

using System;
using System.Runtime.Serialization;
namespace USBFI.MS.FirstService.Data
{
/// <summary>
/// The loan decision detail contains the results of a decision and returned as part of the LoanDecisionResponse
/// </summary>
[DataContract(IsReference = false)]
public class LoanDecisionDetail
{
/// <summary>
/// The decisions unique identifier for reference
/// </summary>
[DataMember(EmitDefaultValue = false)]
public Guid DecisionId { get; set; }
/// <summary>
/// The approval decision on this particular loan detail
/// </summary>
[DataMember(EmitDefaultValue = false)]
public bool Approved { get; set; }
/// <summary>
/// The originating application details that this decision was based on
/// </summary>
[DataMember(EmitDefaultValue = false)]
public MemberApplicationDetail ApplicationDetail { get; set; }
/// <summary>
/// The custom internal score for this member
/// </summary>
[DataMember(EmitDefaultValue = false)]
public int CustomCompositeScore { get; set; }
/// <summary>
/// The loan amount approved for this decision
/// </summary>
[DataMember(EmitDefaultValue = false)]
public int LoanAmount { get; set; }
}
}

MemberApplicationDetail.cs

using System.Runtime.Serialization;
namespace USBFI.MS.FirstService.Data
{
[DataContract(IsReference = false)]
public class MemberApplicationDetail
{
/// <summary>
/// An arbitrary application number
/// </summary>
[DataMember(EmitDefaultValue = false)]
public string ApplicationNumber { get; set; }
/// <summary>
/// The unique identifier of this member
/// </summary>
[DataMember(EmitDefaultValue = false)]
public string MemberIdentifier { get; set; }
/// <summary>
/// The members transunion credit score
/// </summary>
[DataMember(EmitDefaultValue = false)]
public int TransUnionScore { get; set; }
/// <summary>
/// The members equifax credit score
/// </summary>
[DataMember(EmitDefaultValue = false)]
public int EquifaxScore { get; set; }
/// <summary>
/// The members annual income
/// </summary>
[DataMember(EmitDefaultValue = false)]
public int AnnualIncome { get; set; }
}
}

Create the new request and responses classes

In the Contracts project, add these classes below to the Requests and Responses folders respectively

GetLoanDecisionRequest.cs

using Alkami.Contracts;
using System.Runtime.Serialization;
using USBFI.MS.FirstService.Data;
namespace USBFI.MS.FirstService.Contracts.Requests
{
/// <summary>
/// The request object used when calling the GetLoanDecisionAsync() implementation method
/// </summary>
[DataContract(IsReference = true)]
public class GetLoanDecisionRequest : BaseRequest
{
/// <summary>
/// All the information need to make a loan decision for this member
/// </summary>
[DataMember(EmitDefaultValue = true)]
public MemberApplicationDetail ApplicationDetail { get; set; }
}
}

LoanDecisionResponse.cs

using Alkami.Contracts;
using System.Runtime.Serialization;
using USBFI.MS.FirstService.Data;
namespace USBFI.MS.FirstService.Contracts.Responses
{/// <summary>
/// A response object that contains an ItemList of LoanDecisionDetail
/// </summary>
[DataContract(IsReference = true)]
public class LoanDecisionResponse : BaseResponse<LoanDecisionDetail>
{
}
}

Creating an Entity Validator

The new request and its members should have validations to make sure the service performs as efficiently as possible. We will create two new entity validators, one for the GetLoanDecisionRequest.cs and another for the MemberApplicationDetail.cs which is an object member of the GetLoanDecisionRequest.cs. We want to make sure both of these types are formatted correctly before we send the request to the implementation method.

Add the following classes to the Validations project

using Alkami.Data.Validations;
using System.Collections.Generic;
namespace USBFI.MS.FirstService.Data.Validations
{/// <summary>
/// This validator interrogates a MemberLoanDetail object for the proper information
/// </summary>
public class MemberApplicationDetailValidator : EntityValidatorImpl<MemberApplicationDetail>
{
/// <summary>
/// Make sure the application details are properly filled out
/// </summary>
/// <param name="src"></param>
/// <returns></returns>
protected override List<ValidationResult> ValidateInternal(MemberApplicationDetail src)
{
var results = new List<ValidationResult>();
if (src == null)
{
results.Add(new ValidationResult()
{
ErrorCode = ErrorCode.ValidationError,
Field = "MemberApplicationDetail",
Message = "The MemberApplicationDetail cannot be null.",
Severity = Severity.Error,
SubCode = SubCode.None
});
return results;
}
if (src.AnnualIncome <= 0)
{
results.Add(new ValidationResult()
{
ErrorCode = ErrorCode.ValidationError,
Field = "AnnualIncome",
Message = "The AnnualIncome cannot be less than or equal to 0.",
Severity = Severity.Error,
SubCode = SubCode.None
});
}
if (src.MemberIdentifier == null)
{
results.Add(new ValidationResult()
{
ErrorCode = ErrorCode.ValidationError,
Field = "MemberIdentifier",
Message = "The MemberIdentifier cannot be null",
Severity = Severity.Error,
SubCode = SubCode.None
});
}
if (src.EquifaxScore <= 0)
{
results.Add(new ValidationResult()
{
ErrorCode = ErrorCode.ValidationError,
Field = "EquifaxScore",
Message = "The EquifaxScore is missing. This may cause a rejection when reviewing the application.",
Severity = Severity.Warning,
SubCode = SubCode.None
});
}
if (src.TransUnionScore <= 0)
{
results.Add(new ValidationResult()
{
ErrorCode = ErrorCode.ValidationError,
Field = "TransUnionScore",
Message = "The TransUnionScore is missing. This may cause a rejection when reviewing the application.",
Severity = Severity.Warning,
SubCode = SubCode.None
});
}
return results;
}
}
}

GetLoanDecisionRequestValidator.cs

using Alkami.Data.Validations;
using System.Collections.Generic;
using USBFI.MS.FirstService.Contracts.Requests;
namespace USBFI.MS.FirstService.Data.Validations
{
/// <summary>
/// Request Validators are useful to keep malformed requests from crossing the wire.
/// We'll only send requests that pass these validations.
/// </summary>
public class GetLoanDecisionRequestValidator : EntityValidatorImpl<GetLoanDecisionRequest>
{
/// <summary>
/// Validate that all the loan details on this request are properly filled out
/// </summary>
/// <param name="src"></param>
/// <returns></returns>
protected override List<ValidationResult> ValidateInternal(GetLoanDecisionRequest src)
{
List<ValidationResult> results = new List<ValidationResult>();
// Make sure the response has application details to validate
if (src.ApplicationDetail == null)
{
// The application detail is null, we cannot do anything more
results.Add(new ValidationResult()
{
ErrorCode = ErrorCode.ValidationError,
Field = "GetLoanDecisionRequest",
Message = "Member Application Details cannot be null.",
Severity = Severity.Error,
SubCode = SubCode.MalformedRequest
});
return results;
}
// We created a validator for the application details in the MemberApplicationDetailValidator class, we can call that validator directly
var isLoanDetailValid = src.ApplicationDetail.Validate(out results);
if (!isLoanDetailValid)
{
// The application detail is not passing our validation and therefor we will not send the request
results.Add(new ValidationResult()
{
ErrorCode = ErrorCode.ValidationError,
Field = "GetLoanDecisionRequest",
Message = "The request contained incomplete Member Application Details. Please see other validation errors for detailed information.",
Severity = Severity.Error,
SubCode = SubCode.MalformedRequest
});
}
return results;
}
}
}

Updating the Service Contract

The IFirstServiceContract is the interface that defines which implementation methods are available to be publicly called. We want our new method to be publicly accessible so we’ll need to add that method stub.

This is the full IFirstServiceContract.cs with the new GetLoanDecisionAsync() method stubbed out.

IFirstServiceContract.cs

using System.ServiceModel;
using System.Threading.Tasks;
using USBFI.MS.FirstService.Contracts.Requests;
using USBFI.MS.FirstService.Contracts.Responses;
namespace USBFI.MS.FirstService.Contracts
{
/// <summary>
/// The IService Contract for USBFI.MS.FirstService.Contracts
/// </summary>
[ServiceContract]
public interface IFirstServiceServiceContract
{
/// <summary>
/// This is a template method that demonstrates how to get settings from Alkami's data scope abstraction
/// It is sometimes useful to have a service's settings within a widget or another service.
/// This service may be one part of a feature, but it's possible to store all feature settings within a single service
/// Any other widgets or services that are a part of this feature can easily get all the settings they need by calling this method
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
[OperationContract]
Task<SettingsResponse> GetSettingsAsync(GetSettingsRequest request);
/// <summary>
/// Get something from a third party service or API
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
[OperationContract]
Task<CustomObjectResponse> GetDataAsync(GetSomethingRequest request);
/// <summary>
/// Part of the FirstService feature set that determines if a loan can be approved
/// The request will contain member application details and a decision tree will be executed to determine if a loan amount has been approved
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
[OperationContract]
Task<LoanDecisionResponse> GetLoanDecisionAsync(GetLoanDecisionRequest request);
}
}

Updating the Service Client

The service client is the proxy between the caller and the host service. We’ll need to stub out a proxy method similar to the one that was added by the template.

Add the new GetLoanDecisionAsync() method stub so that your class looks like the one below

FirstServiceServiceClient.cs

using Alkami.MicroServices.Settings.ProviderBasedClient;
using System.Threading.Tasks;
using USBFI.MS.FirstService.Contracts;
using USBFI.MS.FirstService.Contracts.Requests;
using USBFI.MS.FirstService.Contracts.Responses;
namespace USBFI.MS.FirstService.Service.Client
{
/// <inheritdoc />
public class FirstServiceServiceClient : ProviderBasedClient<IFirstServiceServiceContract>, IFirstServiceServiceContract
{
/// <summary>
/// The unique provider type designator for this service
/// </summary>
private const string ProviderType = "USBFI";
/// <summary>
/// ProviderBased constructor
/// </summary>
public FirstServiceServiceClient() : base(ProviderType)
{ }
/// <summary>
/// ProviderBased constructor override
/// </summary>
/// <param name="providerId"></param>
public FirstServiceServiceClient(long providerId) : base(ProviderType, providerId)
{ }
/// <inheritdoc />
public Task<SettingsResponse> GetSettingsAsync(GetSettingsRequest request)
{
return ProxyCall((operation, inner) => operation.GetSettingsAsync(inner), request);
}
/// <inheritdoc />
public Task<CustomObjectResponse> GetDataAsync(GetSomethingRequest request)
{
return ProxyCall((operation, inner) => operation.GetDataAsync(inner), request);
}
/// <inheritdoc />
public Task<LoanDecisionResponse> GetLoanDecisionAsync(GetLoanDecisionRequest request)
{
return ProxyCall((operation, inner) => operation.GetLoanDecisionAsync(inner), request);
}
}
}

Updating ServiceImp

ServiceImp.cs

using Alkami.Data.Validations;
using Common.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using USBFI.MS.FirstService.Contracts;
using USBFI.MS.FirstService.Contracts.Requests;
using USBFI.MS.FirstService.Contracts.Responses;
using USBFI.MS.FirstService.Data;
using USBFI.MS.FirstService.Data.ProviderSettings;
namespace USBFI.MS.FirstService.Service
{
/// <inheritdoc />
public partial class ServiceImp : IFirstServiceServiceContract
{
private static readonly ILog Logger = LogManager.GetLogger<ServiceImp>();
/// <inheritdoc />
public async Task<SettingsResponse> GetSettingsAsync(GetSettingsRequest request)
{
// Create a new response object that encapsulates the data type we'll be returning
var response = new SettingsResponse();
// We want some local variables that are available outside of the data scope
string firstSetting = string.Empty;
string secondSetting = string.Empty;
var maxLoanAmount = 0;
var minLoanAmount = 0;
// GetScopeAsync() is how we retrieve settings using the request type of this service
using (var scope = await GetScopeAsync(request))
{
// Assigning the settings to our local variables
firstSetting = scope.GetSettingOrDefault<string>(SettingNames.FirstProviderSetting);
secondSetting = scope.GetSettingOrDefault<string>(SettingNames.SecondProviderSetting);
int.TryParse(scope.GetSettingOrDefault<string>(SettingNames.MaxLoanAmount), out maxLoanAmount);
int.TryParse(scope.GetSettingOrDefault<string>(SettingNames.MinLoanAmount), out minLoanAmount);
}
// It's always good to add a trace log for future troubleshooting
Logger.Trace($"{nameof(GetSettingsAsync)} | First Setting: [{firstSetting}] | Second Setting: [{secondSetting}]");
// Populate the details of the Setting object with this service's two template settings
// The response object's "ItemList" property is an enumerable list of the type we passed into the class definition of SettingsResponse
response.ItemList.Add(new Setting
{
Name = SettingNames.FirstProviderSetting,
DefaultValue = DefaultSettings()[SettingNames.FirstProviderSetting],
CurrentValue = firstSetting,
Description = SettingDescriptors().FirstOrDefault(x => x.Name == SettingNames.FirstProviderSetting)?.Description
});
// We'll do the same for the second setting, adding another instance of the Setting to the response's ItemList
response.ItemList.Add(new Setting
{
Name = SettingNames.SecondProviderSetting,
DefaultValue = DefaultSettings()[SettingNames.SecondProviderSetting],
CurrentValue = secondSetting,
Description = SettingDescriptors().FirstOrDefault(x => x.Name == SettingNames.SecondProviderSetting)?.Description
});
// Return the response using an awaited task
return await Task.FromResult(response);
}
/// <summary>
/// We can get something from the web
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
public async Task<CustomObjectResponse> GetDataAsync(GetSomethingRequest request)
{
// A service GET is going to retrieve some data from some other source and populate a response
// In this case we have a CustomObjectResponse that contains an ItemList of CustomDataObjects
// For the sake of demonstration a response has been populated with constants and returned to the caller
return await Task.FromResult(new CustomObjectResponse
{
ItemList = new List<CustomDataObject>
{
new CustomDataObject
{
AnotherPropertyThatsAnInt = 9999,
ChildrenObjects = new List<CustomChildObject>
{
new CustomChildObject
{
DateTime = DateTime.Now,
Int = 007,
String = "James Bond"
},
new CustomChildObject
{
DateTime = DateTime.Today,
Int = 006,
String = "Alec Trevelyn"
}
},
OneOfYourObjects = "MI6"
}
}
});
}
}
}

Updating the Service Host

The final place we need to stub out our new contract method is within the DistributedService itself. This is also where we want to add our two validatiors we created earlier.

Add a new method stub for GetLoanDecisionAsync() and add two new entity validators so that your class now looks like the one below

DistributedService.cs

using Alkami.Data.Validations;
using Alkami.MicroServices.Settings.ProviderBasedService;
using System;
using System.Threading.Tasks;
using USBFI.MS.FirstService.Contracts;
using USBFI.MS.FirstService.Contracts.Requests;
using USBFI.MS.FirstService.Contracts.Responses;
using USBFI.MS.FirstService.Data.Validations;
namespace USBFI.MS.FirstService.Service.Host
{
public class DistributedService : ProviderBasedService<IFirstServiceServiceContract, ServiceImp>, IFirstServiceServiceContract
{
private IFirstServiceServiceContract _serviceContract; //This doesn't need to be injected, since the service will ALWAYS run the instance
private readonly string _providerName;
private readonly string _providerType;
/// <summary>
/// The new provider based service will require additional parameters during construction
/// </summary>
/// <param name="friendlyName"></param>
/// <param name="providerName"></param>
/// <param name="providerType"></param>
public DistributedService(string friendlyName, string providerName, string providerType) : base(friendlyName, providerName, providerType)
{
_providerName = providerName;
_providerType = providerType;
}
public void OnStart()
{
_serviceContract = new ServiceImp(_providerType, _providerName);
// TODO: Add validators here.
EntityValidator.AddValidator(new AddOrUpdateSomethingRequestValidator());
EntityValidator.AddValidator(new GetSomethingRequestValidator());
EntityValidator.AddValidator(new CustomDataObjectFilterValidator());
EntityValidator.AddValidator(new CustomDataObjectValidator());
Alkami.Broker.ZeroMq.Setup.PublishUsingZeroMqLocally();
Alkami.Broker.ZeroMq.Setup.SubscribeUsingZeroMqLocally(_serviceCancellationToken.Token);
Alkami.Broker.App.Subscription.InitializeSubscriber();
base.Start();
}
public void OnStop(TimeSpan fromSeconds)
{
base.Stop(fromSeconds);
}
/// <inheritdoc />
public Task<SettingsResponse> GetSettingsAsync(GetSettingsRequest request)
{
return _serviceContract.GetSettingsAsync(request);
}
/// <inheritdoc />
public Task<CustomObjectResponse> GetDataAsync(GetSomethingRequest request)
{
return _serviceContract.GetDataAsync(request);
}
public Task<LoanDecisionResponse> GetLoanDecisionAsync(GetLoanDecisionRequest request)
{
return _serviceContract.GetLoanDecisionAsync(request);
}
}
}

Testing the New Loan Decision Feature

We have all the implementation details in place! Now it’s time to test our new contract method using the Micro Service Tester.

Start the service in Visual Studio and select the new GetLoanDecision contract method. Open the ApplicationDetail property and populate it’s members with some test data.

Click OK and then click Send Message setup screen

The response will have our new decision details! This data was approved for a $15000 loan, congrats! setup screen

Identifying Additional Settings

It’s always good to look at your implementation and determine if there are additional variables that can be converted to configurable settings.

I’ve purposefully left some “magic numbers” in the example ServiceImp.LoanDecision.cs partial class and I’ve circled them below in the screen shot. These are great candidates for configurable settings and can be added right along with the min and max settings. setup screen