My development of this Windows service stemmed from the need to automate and simplify client notifications, such as a notification about a prescription being ready for pickup, while ensuring both reliability and flexibility by seamlessly integrating with my custom API to process and deliver notifications via SMS, voice, and email using my Twilio based API endpoint.
By integrating with my custom API, the Windows service retrieves notification records from the database, validates the data, and triggers the appropriate API delivery method based on the message type (SMS, voice, email).
Note: TheWinService is a placeholder for the actual Windows background service name. I replaced the real name for clarity and confidentiality.
The Windows background service is composed of the following components:
App.config
- Stores application settings, connection strings, and configuration details, such as the API endpoint required for instantiation.
Twilio.cs
- Includes static classes, general utility functions, and logic for retrieving records to process and handling general SQL logging.
Program.cs
- Acts as the entry point for the Windows service. This file configures and initializes the service as a Windows background process, setting up essential dependencies like logging and the core service logic.
WindowsBackgroundService.cs
- Implements the primary background task responsible for periodic processing of notifications, including messaging operations.
App.config
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<appSettings>
<add key="service_timer_minutes" value="2" />
<add key="RequestUri" value="https://the_base_url/Controller/APIEndpoint" />
<add key="SQL" value="intergrated authentication and connection info" />
<add key="HashKey" value="a randomly generated client specific hashkey" />
</appSettings>
</configuration>
Twilio.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Twilio;
using Twilio.TwiML;
using Twilio.Rest.Api.V2010.Account;
using Twilio.Types;
using Twilio.AspNet.Core;
using Twilio.AspNet.Common;
using Microsoft.AspNetCore.Http;
using static Microsoft.Extensions.Logging.EventSource.LoggingEventSource;
using Twilio.Http;
using Twilio.TwiML.Messaging;
using System.Reflection;
using Newtonsoft.Json.Linq;
using Microsoft.Extensions.Logging;
using System.Diagnostics;
using static TheWinService.GeneralFunctions;
using static TheWinService.WindowsBackgroundService;
namespace TheWinService
{
public class TwilioRecordToProcess
{
public string? unique_patent_id { get; set; }
public string? to { get; set; }
public string? from { get; set; }
public string? pkid { get; set; }
public string? msgtype { get; set; }
public string? msgtypevalue { get; set; }
}
public class GeneralFunctions
{
public static bool IsNull([System.Diagnostics.CodeAnalysis.MaybeNullWhen(true)] object? obj) => obj == null;
public static bool IsNotNull([System.Diagnostics.CodeAnalysis.NotNullWhen(true)] object? obj) => obj != null;
public static class Globals
{
public static string? _sql_con_str = System.Configuration.ConfigurationManager.AppSettings["SQL"].Replace(@"\\\\", @"\\");
public static string? hashkey = System.Configuration.ConfigurationManager.AppSettings["HashKey"];
public static string? RequestUri = System.Configuration.ConfigurationManager.AppSettings["RequestUri"];
}
public static TwilioRecordToProcess GetRecordToProcess()
{
if (OperatingSystem.IsWindows()) { EventLog.WriteEntry("TheWinService", "GetRecordToProcess"); }
TwilioRecordToProcess _TwilInfo = new();
try
{
string? _sql_con_str = Globals._sql_con_str;
System.Data.SqlClient.SqlConnection _sql_con = new System.Data.SqlClient.SqlConnection(_sql_con_str);
System.Data.SqlClient.SqlCommand _sql_cmd = new System.Data.SqlClient.SqlCommand("GetTheRecToProcess ", _sql_con);
_sql_cmd.CommandType = System.Data.CommandType.StoredProcedure;
_sql_con.Open();
System.Data.SqlClient.SqlDataReader _sql_dr = _sql_cmd.ExecuteReader();
while (_sql_dr.Read())
{
_TwilInfo.to = (string?)_sql_dr["destination"];
_TwilInfo.unique_patent_id = (string?)_sql_dr["unique_patent_id"].ToString();
_TwilInfo.from = (string?)_sql_dr["twilio_from_number"];
_TwilInfo.pkid = (string?)_sql_dr["pkid"].ToString();
_TwilInfo.msgtype = (string?)_sql_dr["msgtype"].ToString();
_TwilInfo.msgtypevalue = (string?)_sql_dr["msgtypevalue"].ToString();
}
_sql_dr.Close();
_sql_con.Close();
}
catch (Exception ex)
{
if (OperatingSystem.IsWindows())
{
EventLog.WriteEntry("TheWinService", "GetRecordToProcess - ERROR");
EventLog.WriteEntry("TheWinService", $"GetRecordToProcess - ERROR: {ex.Message.ToString()}");
}
}
return _TwilInfo;
}
public static string LogEndpointCall(string endpoint_name, string payload, string ParticipantMessagingBinding)
{
if (IsNull(payload)) { payload = ""; }
try
{
string? _sql_con_str = Globals._sql_con_str;
System.Data.SqlClient.SqlConnection _sql_con = new System.Data.SqlClient.SqlConnection(_sql_con_str);
System.Data.SqlClient.SqlCommand _sql_cmd = new System.Data.SqlClient.SqlCommand("DoTheSQLLogging", _sql_con);
_sql_cmd.CommandType = System.Data.CommandType.StoredProcedure;
_sql_cmd.Parameters.AddWithValue("@p_partner_id", 0);
_sql_cmd.Parameters.AddWithValue("@p_endpoint_name", endpoint_name);
_sql_cmd.Parameters.AddWithValue("@p_payload", payload);
_sql_cmd.Parameters.AddWithValue("@p_ParticipantMessagingBinding", ParticipantMessagingBinding);
_sql_con.Open();
_sql_cmd.ExecuteNonQuery();
_sql_con.Close();
}
catch (Exception ex)
{
throw new Exception(ex.Message.ToString());
}
return "logged";
}
}
}
Program.cs
using TheWinService;
using Microsoft.Extensions.Logging.Configuration;
using Microsoft.Extensions.Logging.EventLog;
IHostBuilder builder = Host.CreateDefaultBuilder(args)
.UseWindowsService(options =>
{
options.ServiceName = "TheWinService";
})
.ConfigureServices((context, services) =>
{
LoggerProviderOptions.RegisterProviderOptions<
EventLogSettings, EventLogLoggerProvider>(services);
services.AddSingleton<CPTwilio>();
services.AddHostedService<WindowsBackgroundService>();
services.AddLogging(builder =>
{
builder.AddConfiguration(
context.Configuration.GetSection("Logging"));
});
});
IHost host = builder.Build();
host.Run();
WindowsBackgroundService.cs
using System.Diagnostics;
using System.Security.Cryptography.Xml;
using System.Security.Cryptography;
using System.Text;
using System.Net.Http;
using System.Net;
using Newtonsoft.Json;
using System;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
namespace TheWinService ;
public sealed class WindowsBackgroundService : BackgroundService
{
private readonly CPTwilio _TheWinService; // used in IsCancellationRequested
private readonly ILogger<WindowsBackgroundService> _logger; // windows log
public class SendTwilioSMSBody
{
public string? to { get; set; }
public string? unique_patent_id { get; set; }
public string? from { get; set; }
}
public WindowsBackgroundService(
CPTwilio TheWinService ,
ILogger<WindowsBackgroundService> logger) =>
(_TheWinService , _logger) = (TheWinService , logger);
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
try
{
if (OperatingSystem.IsWindows()) { EventLog.WriteEntry("TheWinService", "TheWinService Start"); }
}
catch (Exception)
{ }
while (!stoppingToken.IsCancellationRequested)
{
if (OperatingSystem.IsWindows()) { EventLog.WriteEntry("TheWinService", "GetRecordToProcess"); }
dynamic SendTwilioSMSBody = GeneralFunctions.GetRecordToProcess();
GeneralFunctions.LogEndpointCall("Begin ExecuteAsync TheWinService", "", "");
if (GeneralFunctions.IsNotNull(SendTwilioSMSBody.unique_patent_id )) // we must have a value
{
var ret_list = Newtonsoft.Json.JsonConvert.SerializeObject(SendTwilioSMSBody); // convert class object into JSON
if (!SendTwilioSMSBody.to.ToString().StartsWith("+1"))
{
if (OperatingSystem.IsWindows()) { EventLog.WriteEntry("TheWinService", "[to] must begin with +1 " + ret_list); }
GeneralFunctions.LogEndpointCall("ExecuteAsync TheWinService", "[to] must begin with +1: " + ret_list, "");
return;
}
if (SendTwilioSMSBody.to.ToString().Length != 12)
{
if (OperatingSystem.IsWindows()) { EventLog.WriteEntry("TheWinService", "[to] is incorrect length: " + ret_list); }
GeneralFunctions.LogEndpointCall("ExecuteAsync TheWinService", "[to] is incorrect length: " + ret_list, "");
return;
}
string ParticipantMessagingBinding = SendTwilioSMSBody.@from +"-"+ SendTwilioSMSBody.@to;
var result = string.Empty;
string? signature = string.Empty;
string? timestamp = DateTime.Now.ToString("yyyy'-'MM'-'dd 'T'HH':'mm':'ss'.'fff'Z'");
byte[] keyByte = Encoding.UTF8.GetBytes(TheWinService .GeneralFunctions.Globals.hashkey!);
using (var hmacsha256 = new HMACSHA256(keyByte))
{
byte[] textBytes = System.Text.Encoding.UTF8.GetBytes($"{timestamp}:{TheWinService .GeneralFunctions.Globals.hashkey!}");
byte[] hashBytes = hmacsha256.ComputeHash(textBytes);
string hash = BitConverter.ToString(hashBytes).Replace("-", String.Empty);
signature = hash.ToUpper();
}
// the body stuff
var content = new StringContent(ret_list, Encoding.UTF8, "application/json"); // add payload to content as Body
content.Headers.Add("cp_webhook_timestamp", timestamp);
content.Headers.Add("cp_webhook_signature", signature);
HttpClient httpClient = new();
var httpRequestMessage = new HttpRequestMessage();
httpRequestMessage.Method = HttpMethod.Post;
// add the content to the response
httpRequestMessage.Content = content; // required for requestSignature == signature in PartnerRegisteredEndpoint
// define the URI
string? RequestUri = TheWinService .GeneralFunctions.Globals.RequestUri!;
httpRequestMessage.RequestUri = new Uri(RequestUri!); // set the partner_registered_endpoint
if (OperatingSystem.IsWindows()) { EventLog.WriteEntry("TheWinService", RequestUri); }
// call the API TwilioEntryPoint endpoint
try
{
var response = await httpClient.PostAsync(httpRequestMessage.RequestUri.ToString(), content);
if ((int)response.StatusCode != 200)
{
string resp = response.StatusCode.ToString() + " - " + response.ToString() + " - " + RequestUri;
if (OperatingSystem.IsWindows()) { EventLog.WriteEntry("TheWinService", resp); }
return;
}
}
catch (Exception)
{
if (OperatingSystem.IsWindows()) { EventLog.WriteEntry("TheWinService", "API endpoint did not respond."); }
return;
}
if (OperatingSystem.IsWindows()) { EventLog.WriteEntry("TheWinService", "API SendTwilioSMS endpoint call complete"); }
}
string? _minute_timer = System.Configuration.ConfigurationManager.AppSettings["service_timer_minutes"];
int minutes = int.Parse(_minute_timer!);
await Task.Delay(TimeSpan.FromMinutes(minutes), stoppingToken);
}
}
catch (TaskCanceledException)
{
// When the stopping token is canceled, for example, a call made from services.msc,
// we shouldn't exit with a non-zero exit code. In other words, this is expected...
}
catch (Exception ex)
{
_logger.LogError(ex, "{Message}", ex.Message); // _logger is defined as WindowsBackgroundService
// Terminates this process and returns an exit code to the operating system.
// This is required to avoid the 'BackgroundServiceExceptionBehavior', which
// performs one of two scenarios:
// 1. When set to "Ignore": will do nothing at all, errors cause zombie services.
// 2. When set to "StopHost": will cleanly stop the host, and log errors.
//
// In order for the Windows Service Management system to leverage configured
// recovery options, we need to terminate the process with a non-zero exit code.
if (OperatingSystem.IsWindows()) { EventLog.WriteEntry("TheWinService", "TheWinService Stop"); }
Environment.Exit(1);
}
}
}
Overview: Twilio Windows Background Service
This Windows Background Service, implemented in C#, is designed to automate the delivery of notifications using Twilio. The service continuously monitors a SQL database for new records indicating messages to be sent, such as a notification about a prescription being ready for pickup. The key features and workflow of this service are as follows:
General Features
- Customizable Interval: The service polls the SQL database at a configurable interval (default: 2 minutes) to check for new records.
- SQL Integration: Retrieves relevant data from the database for preparation.
- Custom API Integration: Constructs the message body and initiates an endpoint call to my custom API which, based on message type, sends messages to recipients using Twilio integration.
- Error Handling and Logging: Logs activity and results in both the SQL database and the Windows Event Log. Detects and logs common issues, such as invalid phone numbers or failed API calls.
- Secure Communication: Generates secure signatures for API calls using HMAC-SHA256 to authenticate requests.
- Graceful Shutdown: Handles task cancellations when the service is stopped, ensuring a clean exit.
General Workflow
Initialization:
- The service starts and writes an entry to the Windows Event Log.
- Retrieves the polling interval and endpoint configuration from app settings.
Polling Loop:
- Queries the SQL database for a record indicating a message to process.
- If a record exists:
- Serializes the record into JSON for use in the request payload.
- Validates the recipient's phone number (e.g., must begin with +1 and be 12 characters long).
- Constructs the API request:
- Generates a timestamp and HMAC signature for secure communication.
- Sets headers and body content for the HTTP request.
- Communicates with the custom API endpoint to send message data
using Twilio integration and handles the response:
- Logs any errors or issues with the API call to the Windows Event Log and database.
Logging and Delays:
- Logs success or failure of each operation to the database and Windows Event Log.
- Waits for the configured interval before polling the database again.
Shutdown:
- Monitors for cancellation requests (e.g., service stop from Windows).
- Performs cleanup tasks and ensures a clean exit.
Detailed Workflow
1.Host Configuration:
- The Host.CreateDefaultBuilder initializes the default host for the application.
- The UseWindowsService method sets up the application as a Windows Service and specifies its name.
2.Service Registration:
- The ConfigureServices method is used to register required services and the hosted background service:
- WindowsBackgroundService: The main background task that performs periodic processing.
- Configures logging to use the Windows Event Log with customizable settings.
3.Service Execution:
- The host.Run() method starts the service, transitioning it into the active state where it begins processing messaging tasks.
Key Features
1.Windows Service Configuration:
- The service is configured to run as a Windows Service with the name "TheWinSerice".
- This enables the service to integrate seamlessly with the Windows Service Manager. 2.Dependency Injection:
- Adds the WindowsBackgroundService as a hosted service, which contains the core logic for Twilio message processing. 3.Logging:
- Configures logging using the Windows Event Log.
- Allows further customization by reading logging configurations from the application settings (appsettings.json). 4.Host Creation:
- Uses the Host.CreateDefaultBuilder method to set up the service's default environment, including configuration and dependency injection.
- The UseWindowsService method specifies that this application is designed to run as a Windows Service.
Overview of the Integration and Workflow
Windows Service for Orchestration: The Windows service acts as the primary orchestrator, continuously monitoring the database for new notifications. It operates at a configurable interval (e.g., every 2 minutes), fetching records that require processing. For each record:
- It validates the data.
- Prepares the payload with details like recipient, message type, and content.
- Sends the data to the TwilioEntryPoint API endpoint for further processing.
Centralized API Endpoint: The custom API's "TwilioEntryPoint" endpoint serves as the hub for processing notifications. Upon receiving a request:
- It parses the payload to extract key information (e.g., recipient details, message type).
- Based on the message type (msgtype), the API routes the request to the appropriate handler:
- SMS notifications are sent using Twilio's messaging services.
- Voice notifications initiate a call using the Twilio Voice API.
- Email notifications are processed via a dedicated email controller.
Global Configuration for Flexibility: A static global variable (RequestUri) stores the client-specific API entry endpoint. This ensures the solution can adapt to varying configurations across different deployments, allowing effortless customization without code changes.
Secure Communication: Each API call is signed with a unique HMACSHA256 signature and timestamp, ensuring secure and authenticated communication between the Windows service and the API.
Error Handling and Logging: Robust error handling and logging mechanisms are in place to ensure reliability:
- The Windows service logs errors and status updates to the Windows Event Log.
- The API validates incoming requests, ensuring essential parameters are present and logging any discrepancies.
Benefits of the Approach
- Scalability: Handles high volumes of notifications efficiently.
- Flexibility: Easily adapts to client-specific configurations and requirements.
- Reliability: Ensures secure, error-resistant communication between components.
- Comprehensive Logging: Provides clear insights into the processing flow and status.
Conclusion
The Program.cs file serves as the backbone of the messaging Windows Service, managing its initialization, configuration, and integration with the Windows Service infrastructure. By leveraging dependency injection and structured logging, it ensures modularity, maintainability, and ease of monitoring. Overall, this custom Twilio Windows service provides a robust solution for automating client notifications across multiple channels, including SMS, voice calls, and emails. By seamlessly integrating a custom API with database interactions and the Twilio API, the service guarantees efficient, reliable, and timely communication.
Top comments (0)