Introduction
This is a second part to the first article I wrote some time ago on how to create a custom SmartObject Service and how to deploy it. It was completely based off Dynamic SQL Service located in the K2 blackmarket. The thing was the service was geared completely towards generating the interface based on the schema of a SQL Database. So I was lead down the path that I had to follow this pattern to define my SmartObject Service. Well I was playing around the other day and I saw a different project on the K2 blackmarket called the AD Interop. I read the code and I was flabbergasted. Apparently it is possible to create a regular class and then decorate the class, properties and methods with attributes to describe it. I dropped Colin an email saying to check this out and we both agreed this is a much cleaner way of building SmartObject Services which are not being generated dynamically.
I read through the AD Interop project and there were a few things which the project owner did not finish out like how to handle exceptions and how to properly pass configuration values from the ServiceBroker to the SmartObject Service. Below is a proof of concept I created for an employee SmartObject Service. Now it is not the most verbose implementation but should lead you in the right direction when trying to create a SmartObject Service.
Employee Object
First I actually went back to my first article and following my instructions for create a project. Then I created an Employee object. Notice it does not inherit from anything special. Notice the SourceCode.SmartObjects.Services.ServiceSDK.Objects.ServiceObject attribute which has been added to the class. It describes the Employee object.
using System;
using System.Collections.Generic;
using System.Text;
using SourceCode.SmartObjects.Services.ServiceSDK.Attributes;
using SourceCode.SmartObjects.Services.ServiceSDK.Objects;
using SourceCode.SmartObjects.Services.ServiceSDK.Types;
namespace CustomTestService
{
[ServiceObject("EmployeeServiceObject",
"Employee Service Object",
"This is a test Employee Service Object.")]
class Employee
{
}
}
Next I created some attributes and properties for the Employee object. Again nothing special. All of the properties are decorated with SourceCode.SmartObjects.Services.ServiceSDK.Objects.Property which describes name of the property, the type, a display name for the property and the description. The only property of any special interest is the DatabaseConnection property. This will only be used by the service broker. The service broker will set the database connection value that was set in the K2 workspace. Note that properties that are not exposed through a method are not visible in a SmartObject definition and cannot be set. This was a concern of mine because I did not want to expose this property publically.
#region attributes
private string _databaseConnection = "";
private int _employeeNumber;
private string _department = "";
private string _firstName = "";
private string _lastName = "";
private string _email = "";
#endregion
#region properties
[Property("EmployeeNumber", SoType.Number,
"Employee Number", "Employee Number of employee")]
public int EmployeeNumber {
get {
return _employeeNumber;
}
set {
_employeeNumber = value;
}
}
[Property("Department", SoType.Text,
"Department", "Department of employee")]
public string Department
{
get
{
return _department;
}
set
{
_department = value;
}
}
[Property("FirstName", SoType.Text,
"First Name", "First Name of employee")]
public string FirstName {
get {
return _firstName;
}
set {
_firstName = value;
}
}
[Property("LastName", SoType.Text,
"Last Name", "Last Name of employee")]
public string LastName {
get {
return _lastName;
}
set {
_lastName = value;
}
}
[Property("Email", SoType.Text,
"Email", "Email of employee")]
public string Email {
get {
return _email;
}
set {
_email = value;
}
}
/// <summary>
/// Note this property was created to pass a value from the
/// EmployeeServiceBroker to the Employee object. This property
/// is not exposed through any of the methods.
/// </summary>
[Property("DatabaseConnection", SoType.Text,
"Database Connection", "Connection string to the database.")]
public string DatabaseConnection
{
get
{
return _databaseConnection;
}
set
{
_databaseConnection = value;
}
}
#endregion
Finally I created three methods: GetEmployee, HireEmployee, and GetEmployees. These are stubs and with no real implementation. Notice that each method is decorated with SourceCode.SmartObjects.Services.ServiceSDK.Objects.Method which takes the name of the method, the method type, a display name, and description. As well, there are three arrays which define the required fields, input fields and output fields. Notice the methods always return itself and any property that is defined as an input field to the method will have a value populated into it which can be used to perform operations (like searching for an employee based on their employee number). I was really happy to see I was able to easily return a collection of Employee objects for a List operation.
#region Methods
/// <summary>
/// This method will get an Employee based on the Employee Number.
/// The Employee number is required.
/// </summary>
/// <returns></returns>
[Method("GetEmployee", MethodType.Read, "Get Employee",
"Method will get Employee based on Employee ID.",
new string[] {"EmployeeNumber"},
new string[] {"EmployeeNumber"},
new string[] {"FirstName", "LastName", "Email", "Department"})]
public Employee GetEmployee()
{
try
{
///Write code here to fill in the object from where ever.
///this.EmployeeNumber; will contain the employee number
///value that was sent by the caller.
///Filling in some test data.
FirstName = "Jason";
LastName = "Apergis";
Department = "Professional Services";
Email = "jason@foobar.com";
}
catch (Exception ex)
{
throw new Exception("Error Getting an Employee >> " + ex.Message);
}
return this; //return the Employee object to the Execute() call
}
/// <summary>
/// This method will create an Employee. First name, last name
/// and department are required. An employee number and email
/// address will be generated as a result of the creation of the employee.
/// These two values will be returned.
/// </summary>
/// <returns></returns>
[Method("HireEmployee", MethodType.Execute, "Hire Employee",
"Method will complete the hire of an email creating a Employee Number and Email Address.",
new string[] {"FirstName", "LastName", "Department"},
new string[] {"FirstName", "LastName", "Department"},
new string[] {"EmployeeNumber", "Email"})]
public Employee HireEmployee()
{
Employee employee = new Employee();
try {
///Write code here to save the object to where ever.
///this.FirstName, etc. to get values provided by the caller.
///returning some test values
EmployeeNumber = 1;
Email = "jason@foobar.com";
}
catch (Exception ex)
{
throw new Exception("Error Hiring an Employee >> " + ex.Message);
}
return this; //return the Employee object to the Execute() call
}
/// <summary>
/// This method will get a list of employees. Providing no value
/// will get all employees. Providing a Department name will get
/// all employees for the department. This is because the Department
/// field is not required.
/// </summary>
/// <returns></returns>
[Method("GetEmployees", MethodType.List, "Get Employees",
"Method will get Employees.",
new string[] { },
new string[] {"Department" },
new string[] {"EmployeeNumber", "FirstName", "LastName", "Email" })]
public List<Employee> GetEmployees()
{
List<Employee> employees = new List<Employee>();
try
{
///Check of there is a Department value
if (String.IsNullOrEmpty(Department))
{
///Get all Employees
}
else {
///Get Employees based on department id
}
///Filling in some test data.
Employee emp1 = new Employee();
emp1.EmployeeNumber = 0;
emp1.FirstName = "Jason";
emp1.LastName = "Apergis";
emp1.Department = "Professional Services";
emp1.Email = "jason@foobar.com";
employees.Add(emp1);
Employee emp2 = new Employee();
emp2.EmployeeNumber = 1;
emp2.FirstName = "Ethan";
emp2.LastName = "Apergis";
emp2.Department = "Professional Services";
emp2.Email = "ethan@foobar.com";
employees.Add(emp2);
}
catch (Exception ex)
{
throw new Exception("Error Getting Employees >> " + ex.Message);
}
return employees; //return the Employee objects to the Execute() call
}
#endregion
Now below is the EmployeeServiceBroker which inherits from ServiceAssemblyBase. In the GetConfigSection() override is where the DatabaseConnection service configuration is defined. When the service instance is created in the K2 Workspace, the administrator will be required to set a connection string which will be associated to the specific service instance. In the DescribeSchema() method notice that the Employee object is add the Service's SmartObjects list. Unlike my first article, nothing more needs to be done in this method because the definition is on the Employee object. Finally I had to override the Execute() method. The Execute() method actually does not have to be overridden, however I am overriding it so that I can pass the database connection string for the SmartObject service instance to the Employee object. As well, I wanted to properly send exception messages using the ServicePackage.
using System;
using System.Collections.Generic;
using System.Text;
using SourceCode.SmartObjects.Services.ServiceSDK;
using SourceCode.SmartObjects.Services.ServiceSDK.Objects;
using SourceCode.SmartObjects.Services.ServiceSDK.Types;
using System.Data;
using System.Data.SqlClient;
namespace CustomTestService
{
public class EmployeeServiceBroker : ServiceAssemblyBase
{
public EmployeeServiceBroker()
{
}
public override string GetConfigSection()
{
this.Service.ServiceConfiguration.Add("DatabaseConnection", true, "Default Value");
return base.GetConfigSection();
}
public override string DescribeSchema()
{
//Custom service object
Type employeeType = typeof(Employee);
base.Service.ServiceObjects.Add(new ServiceObject(employeeType));
return base.DescribeSchema();
}
/// <summary>
/// This method does not have to be implemented. However it is used to set
/// configuration values that are set in the K2 Workspace. As well, exceptions
/// from the Employee object are handled here.
/// </summary>
public override void Execute()
{
try
{
foreach (ServiceObject so in base.Service.ServiceObjects)
{
if (so.Name == "EmployeeServiceObject")
{
string server = base.Service.ServiceConfiguration["DatabaseConnection"].ToString();
so.Properties["DatabaseConnection"].Value = server;
}
}
base.Execute();
}
catch (Exception ex) {
string errorMsg = Service.MetaData.DisplayName + " Error >> " + ex.Message;
this.ServicePackage.ServiceMessages.Add(errorMsg, MessageSeverity.Error);
this.ServicePackage.IsSuccessful = false;
}
}
public override void Extend()
{
//throw new Exception("The method or operation is not implemented.");
}
}
}
Next all I needed to do was deploy the SmartObject service which I describe how to do here.
Employee SmartObject
Now that I have my Employee SmartObject Service deployed, I needed to create an Employee SmartObject. The following is a screenshot of a quick SmartObject that I threw together.
Test Stub
Finally I created a little command line application to test the SmartObject and Service that I had created. I added references to SourceCode.SmartObjects.Client and SourceCode.Hosting.Client and I was off and running.
using System;
using System.Collections.Generic;
using System.Text;
using SourceCode.SmartObjects.Client;
using SourceCode.Hosting.Client.BaseAPI;
namespace TestSO
{
class Program
{
static void Main(string[] args)
{
SmartObjectClientServer server = new SmartObjectClientServer();
try
{
SCConnectionStringBuilder cb = new SCConnectionStringBuilder();
cb.Host = "BLACKPEARL";
cb.Port = 5555;
cb.Integrated = true;
cb.IsPrimaryLogin = true;
//Connect to server
server.CreateConnection();
server.Connection.Open(cb.ToString());
//--------------------------
//Hire the Employee
//Get SmartObject Definition
SmartObject newEmployee = server.GetSmartObject("CustomTestEmployee");
//Hire Employee
newEmployee.Properties["FirstName"].Value = "Jason";
newEmployee.Properties["LastName"].Value = "Apergis";
newEmployee.Properties["Department"].Value = "Professional Services";
//Hire the employee
newEmployee.MethodToExecute = "HireEmployee";
server.ExecuteScalar(newEmployee);
//values returned
System.Diagnostics.Trace.WriteLine(
newEmployee.Properties["ID"].Value);
System.Diagnostics.Trace.WriteLine(
newEmployee.Properties["Email"].Value);
//--------------------------
//Get the Employee
//Get SmartObject Definition
SmartObject employee = server.GetSmartObject("CustomTestEmployee");
//Set properties
employee.Properties["ID"].Value = "1";
//Get the record
employee.MethodToExecute = "GetEmployee";
server.ExecuteScalar(employee);
System.Diagnostics.Trace.WriteLine(
employee.Properties["FirstName"].Value);
System.Diagnostics.Trace.WriteLine(
employee.Properties["LastName"].Value);
System.Diagnostics.Trace.WriteLine(
employee.Properties["Email"].Value);
//--------------------------
//Get employees using a Department Name
//Get SmartObject Definition
employee = server.GetSmartObject("CustomTestEmployee");
//Set properties
employee.Properties["Department"].Value = "Professional Services";
//Get the records
employee.MethodToExecute = "GetEmployees";
SmartObjectList employees = server.ExecuteList(employee);
//Loop over the return employee values
foreach (SmartObject so in employees.SmartObjectsList) {
foreach (SmartProperty property in so.Properties) {
System.Diagnostics.Debug.Write(
property.Name + "=" + property.Value);
}
}
}
catch (Exception ex)
{
throw new Exception("Error Creating Request >> " + ex.Message);
}
}
}
}
Conclusions
I was extremely happy with how clean this was. I was able to create an Employee object, decorate it and then it was deployed as a Service which K2 or other applications across my enterprise can use. The implementation is extremely decoupled and I had to put no thought to it at all. Going down this path, it is possible to re-use Domain Layers that may already be written and expose them to the enterprise on the large.