Contents
data/authors/Paul Logan.json

Mobile app communication with on-prem Enterprise Application

The Brief

Develop an app that employees can access on their mobile phone to submit holiday requests.

The app needs to communicate with the internal HR web system and backing database to submit requests and return approval decisions.

Requirements

Communication between the employee app and the internal HR system needs to by async in nature - the HR system is occassionally offline for system updates and maintenance.

To ensure a smooth user experience, the app must not rely on the HR system being online to serve requests.

The user needs to see their existing holiday bookings, outstanding requests and approval decisions.

Architecture

Employee App = Progressive Web App hosted in Azure as a Static Web App. Requests from the PWA will be processed by the SWA’s function and sent to an Azure Service Bus message queue. The internal HR system will be updated with Azure.Messaging.ServiceBus functionality, allowing it to receive messages from the queue. Manager decisions are currently emailed automatically from the HR system to the employee - this will continue and will be the “proof” that a holiday was approved or not. Alongside the email, the HR system will send a message to the queue for the employee app to receive and display the decision in the app/notification.

Modernising the MVC App

The existing HR system is an ASP.Net MVC Framework 4.5.1 web app.

Step 1 is to migrate to the latest .Net Framework version, 4.8. In a new branch, I increased the framework version number in the 3 projects: UI, backend and unit test. Upgrading this way failed due to conflicts in assemblies and other unpalatable issues. Then I came across this post recommending Target Framework Migrator. The migrator is documented as only working on VS 2019 - thankfully, I still have VS 2019 installed on my machine and the migrator worked first time.

Next, add the Azure Messaging Service Bus package to the UI project.

    Install-Package Azure.Messaging.ServiceBus -Version 7.17.2

Next, add the Azure Messaging Service Bus package to the UI project.

using Azure.Messaging.ServiceBus;
using System.Configuration;
using System.Threading.Tasks;

namespace PaulLogan.Infrastructure
{
    public static class ServiceBus
    {

        static string connectionString = ConfigurationManager.AppSettings["HRServiceBusConnString"];
        static string queueName = ConfigurationManager.AppSettings["HolidayRequestQueueName"];
        static ServiceBusClient client;
        static ServiceBusProcessor processor;
        static ServiceBusClient client;
        static ServiceBusProcessor processor;

        public static async Task Initialise()
        {
            var clientOptions = new ServiceBusClientOptions() { TransportType = ServiceBusTransportType.AmqpWebSockets };
            client = new ServiceBusClient(connectionString, clientOptions);
            processor = client.CreateProcessor(queueName, new ServiceBusProcessorOptions());
            processor.ProcessMessageAsync += MessageHandler;
            processor.ProcessErrorAsync += ErrorHandler;
            await processor.StartProcessingAsync();
        }

        static async Task MessageHandler(ProcessMessageEventArgs args)
        {
            new MessageRouter(args.Message);
            await args.CompleteMessageAsync(args.Message);
        }

        static Task ErrorHandler(ProcessErrorEventArgs args)
        {
            return Task.CompletedTask;
        }

        public async static Task Dispose()
        {
            var clientOptions = new ServiceBusClientOptions() { TransportType = ServiceBusTransportType.AmqpWebSockets };
            client = new ServiceBusClient(connectionString, clientOptions);
            processor = client.CreateProcessor(queueName, new ServiceBusProcessorOptions());
            await processor.DisposeAsync();
            await client.DisposeAsync();
        }
    }
}

Sending a message from the Azure portal to test resulted in an “Invalid TLS version TrackingId” being reported in the ErrorHandler above. The solution to this error was to enable TLS 1.2 before connecting to the Service Bus (first line in the updated Initialise function below):

        public static async Task Initialise()
        {
            System.Net.ServicePointManager.SecurityProtocol = System.Net.SecurityProtocolType.Tls12;
            var clientOptions = new ServiceBusClientOptions() { TransportType = ServiceBusTransportType.AmqpWebSockets };
            client = new ServiceBusClient(connectionString, clientOptions);
            processor = client.CreateProcessor(queueName, new ServiceBusProcessorOptions());
            processor.ProcessMessageAsync += MessageHandler;
            processor.ProcessErrorAsync += ErrorHandler;
            await processor.StartProcessingAsync();
        }

Creating the Static Web App

I have previously developed other SWAs that use Azure Functions based on HTTP Triggers to send messages to an Azure Service Bus Queue:

using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;
using System.IO;
using System.Text.Json;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Azure.Messaging.ServiceBus;
using Microsoft.Extensions.Configuration;

namespace PaulLogan.PreviousProject
{
    public static class SubmitTransferToBus
    {
        [FunctionName("SubmitTransferToBus")]
        [return: ServiceBus("%TransferQueueName%", Connection = "ServiceBusConnection")]
        public static async Task<ServiceBusMessage> ServiceBusOutput([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)]
        HttpRequest req,
        ILogger log,
        ExecutionContext context)
        {

            var config = new ConfigurationBuilder()
                .SetBasePath(context.FunctionAppDirectory)
                .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
                .AddJsonFile("secret.settings.json", optional: true, reloadOnChange: true)
                .AddEnvironmentVariables()
                .Build();

            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
            var document = JsonDocument.Parse(requestBody);
            JsonElement root = document.RootElement;
            JsonElement transType = root.GetProperty("Type");
            ServiceBusMessage message = new ServiceBusMessage(requestBody);
            message.ApplicationProperties.Add("tranType", transType.ToString());
            return message;
        }
    }
}

So a Copy & Paste into the new project, tweak the queue name and connection string, and all should go well.

Firing up my debuggers, within the function I can see the body properly attached:

Message with body for sending
Message with body for sending

At the receiving end though, an empty object in the Body property of the ServiceBusReceivedMessage parameter:

Message received with no body
Message received with no body

Debugging the SWA function

Running the SWA CLI by itself will not allow you to debug the function.

In a VS Code terminal, cd to the function’s api folder and F5 or func host start in the terminal.

Then, in another terminal start the SWA, specifying the backend dev server above:

npx swa start --api-devserver-url http://localhost:7071

Be sure to cd back into the root folder before using the SWA CLI again. Also, when adding new nuget packages for use in the functions, you need to cd back into the api folder, so the proj file can be found.

The previous app was in .net 6, whereas this app was using latest and greatest .net 8. While opting to use .net 8, I also selected the recommended isolated worker process for Azure functions.

Unbeknownst to me at the time, with the move to the isolated worker process, the .Net team have done away with supporting SDK types for output binding - the ServiceBusMessage type in this case.

Here is the stackoverflow post, which links to this github discussion on the matter.

After changing the output binding type to string, the working version of the function declaration is now:

using System.Text.Json;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;

namespace PaulLogan.NewProject
{
    public class SendHolidayRequestFunctions
    {
        private readonly ILogger<SendHolidayRequestFunctions> _logger;

        public SendHolidayRequestFunctions(ILogger<SendHolidayRequestFunctions> logger) => _logger = logger;

        [Function(nameof(SendHolidayRequest))]
        [ServiceBusOutput("holidayrequest", Connection = "ServiceBusConnection")]
        public async Task<string> SendHolidayRequest([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequestData req)
        {
            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
            Dictionary<string, object> props = new Dictionary<string, object>() { { "Subject", "HolidayRequest" } };
            var message = new Message()
            {
                Body = requestBody,
                Subject = "HolidayRequest",
                ApplicationProperties = props,
                EnqueuedTime = DateTime.Now,
            };
            return JsonSerializer.Serialize(message);
        }
    }

    public class Message
    {
        public string Body { get; set; } = "";
        public string Subject { get; set; } = "";
        public IDictionary<string, object>? ApplicationProperties { get; set; }
        public DateTime EnqueuedTime { get; internal set; }
    }
}

and here is the proof of the pudding:

A message with body and properties
A message with body and properties

It works on my machine

The next day, back into the work office, I pull down the commits I made yesterday at home, fire up the apps and book a holiday from the employee app and wait. The debug point in the function is hit and all looks well, so I press F5 to send the message on its way - but the message never made it to the queue. After waiting a while, the terminal for the employee app shows the following warning message:

Function timeout

A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond

GoogleBinging the warning shows that it most likely due to running the app behind the corporate firewall. There were a number of proposed solutions:

The last item above got it working for me, relying on the AMQP protocol over WebSockets as it will use port 443 and may be routed through a proxy, if needed.

This post then enlightened me that the host.json file option relates to the published application, not the local dev app. This post made it even clearer.

It works on my machine part 2

Now I go for a dark release to the live server.

System error when published to live

The type ‘System.Object’ is defined in an assembly that is not referenced. You must add a reference to assembly ‘System.Runtime, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a

Merging the new branch into the main was not the way to go. I had to uncommit the merge, by git reset HEAD Then I used the Target Framework Migrator again on the main branch, followed by a merge of the feature branch.

I then uninstalled the Nuget System.ValueTuple package.

The live internal HR system now worked and processed a message I sent from the employee app.

Refactorings

I now have test and live HR systems that are communicating with Azure Service Bus. This means that I could end up with one of my test bookings being processed by the live system. I create a new Queue in the portal, called holidayrequest_test. In the SendHolidayRequest function declaration, I replace the hard-coded name of the queue with a setting name declared in the local.settings.json file, HolidayRequestQueueName:

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "FUNCTIONS_EXTENSION_VERSION": "~4",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    "ServiceBusConnection": "Endpoint=sb:xyz;",
    "HolidayRequestQueueName": "holidayrequest_test",
    "AzureFunctionsJobHost:extensions:ServiceBus:TransportType": "AmqpWebSockets"
  }
}
        [Function(nameof(SendHolidayRequest))]
        [ServiceBusOutput("%HolidayRequestQueueName%", Connection = "ServiceBusConnection")]
        public async Task<string> SendHolidayRequest([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequestData req)
        {
            ...
            return JsonSerializer.Serialize(message);
        }

Gotcha

The Azure functions that are managed by an SWA are “Managed Functions”, and have some constraints.

I had created another prototype function to receive messages from the HR system when a holiday request had been approved or rejected.

        [Function(nameof(ReceiveHolidayDecision))]
        public void ReceiveHolidayDecision([ServiceBusTrigger("%HolidayApprovalQueueName%", Connection = "ServiceBusConnection")] ServiceBusReceivedMessage message)
        {
            _logger.LogInformation("Message ID: {id}", message.MessageId);
            _logger.LogInformation("Message Body: {body}", message.Body);
            _logger.LogInformation("Message Content-Type: {contentType}", message.ContentType);

            var outputMessage = $"Output message created at {DateTime.Now}";
        }

When testing locally, I could see the function being hit and could step through it with the debugger:

System error when published to live
Executing ‘Functions.ReceiveHolidayDecision’ (Reason='(null)', Id=52999148-0c99-4ed9-aec5-7ad7b7539e2c) Trigger Details: MessageId: cebea874b36e47edafa467b70737ff8b, SequenceNumber: 1, DeliveryCount: 1, EnqueuedTimeUtc: 2024-02-01T11:49:07.3540000+00:00, LockedUntilUtc: 2024-02-01T12:13:05.6210000+00:00, SessionId: (null) Message Body: { Message Content-Type: (null) “Email”: “PAULHJLOGAN@GMAIL.COM”, Message ID: cebea874b36e47edafa467b70737ff8b “Date”: “14/05/2024”, “Approved”: false } Executed ‘Functions.ReceiveHolidayDecision’ (Succeeded, Id=52999148-0c99-4ed9-aec5-7ad7b7539e2c, Duration=297ms)

The terminal did show the following message

System error when published to live
Function app contains non-HTTP triggered functions. Azure Static Web Apps managed functions only support HTTP functions. To use this function app with Static Web Apps, see ‘Bring your own function app’.

I thought I would take a chance, seeing as it worked locally. I ploughed on and committed the changes, however, I got alerted to a build failure:

System error when published to live
Finished building function app with Oryx Found functions.metadata file Error in processing api build artifacts: the file ‘functions.metadata’ has specified an invalid trigger of type ‘serviceBusTrigger’ and direction ‘In’. Currently, only httpTriggers are supported.

The solution was to substitute the ServiceBusTrigger with an HttpTrigger as done here in this post The HR system would POST to this endpoint and the function would proceed. The use of messages is more a necessity in the other direction, allowing the HR system to pick them up when it is ready to.

        [Function(nameof(ReceiveHolidayDecision))]
        public static async Task ReceiveHolidayDecision([HttpTrigger(AuthorizationLevel.Anonymous, "post", `Route = "holidaydecision")] HttpRequestData req)
        {
            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
            var hol = JsonSerializer.Deserialize<HolidayBooking>(requestBody);
            ......
        }

According to the documentation, I can remove the Route property in the function signature and rely on the default of using the function name in the calling client:

        [Function(nameof(ReceiveHolidayDecision))]
        public static async Task ReceiveHolidayDecision([HttpTrigger(AuthorizationLevel.Anonymous, "post"] HttpRequestData req)
        {
            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
            var hol = JsonSerializer.Deserialize<HolidayBooking>(requestBody);
            ......
        }

It works on my other machine

Back in the home office, I pull down yesterdays commits and hit F5 to start the function debugger.

The first gotcha is related to the local.settings.json file. As it contains sensitive information, the default convention is to exclude it from version control (and rightly so). However, this means that other developers or the same developer working on different computers will not get the current settings. Also, there is no traceability/history of changes that are made to the file if it is excluded from git.

This has cropped up before on previous apps, and I implement a very simple solution. I add a second settings json file, secret.settings.json, that will be ignored by git and will hold the secret values. The local.settings.json file will have the same settings but with values set to “–SECRET–”. I cannot remember where I initially discovered this technique, but it could well have been here.

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "FUNCTIONS_EXTENSION_VERSION": "~4",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    "AzureFunctionsJobHost:extensions:ServiceBus:TransportType": "AmqpWebSockets",
    "ServiceBusConnection": "--SECRET--",
    "EmployeeDataTableURI": "--SECRET--",
    "TSKaccountName": "--SECRET--",
    "TSKaccountKey": "--SECRET--",
    "HolidayRequestQueueName": "holidayrequest_test",
    "HolidayApprovalQueueName": "holidayapproval_test"
  }
}

After updating the local settings file, it will need to be removed from the .gitignore file. Then, to actually get the repository to stop ignoring the fil, use the following command to add the file to git:

git add -f api/local.settings.json

What is different this time, is the C# code to read the new configuration file.

Previously, in .net 6 world:

    public static class SubmitTransferToBus
    {
        [FunctionName("SubmitTransferToBus")]
        [return: ServiceBus("%TransferQueueName%", Connection = "ServiceBusConnection")]
        public static async Task<ServiceBusMessage> ServiceBusOutput([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)]
        HttpRequest req,
        ILogger log,
        ExecutionContext context)
        {

            var config = new ConfigurationBuilder()
                .SetBasePath(context.FunctionAppDirectory)
                .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
                .AddJsonFile("secret.settings.json", optional: true, reloadOnChange: true)
                .AddEnvironmentVariables()
                .Build();

            .........    

In .net8, ExecutionContext does not have a FunctionAppDirectory property. So the code now becomes:

        [Function(nameof(MyHolidays))]
        public static string MyHolidays([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "myholidays/{emailaddress}")] HttpRequestData req,
        string emailaddress)
        {
            var config = new ConfigurationBuilder()
                            .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
                            .AddJsonFile("secret.settings.json", optional: true, reloadOnChange: true)
                            .AddEnvironmentVariables()
                            .Build();

            var accName = config.GetValue<string>("TSKaccountName");
            var accKey = config.GetValue<string>("TSKaccountKey");

            ..........

Test & Production Queues

I want to have separate queues for development and production.

        [Function(nameof(SendHolidayRequest))]
        [ServiceBusOutput("%HolidayRequestQueueName%", Connection = "ServiceBusConnection")]
        public async Task<string> SendHolidayRequest([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequestData req)
        {
            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
            Dictionary<string, object> props = new Dictionary<string, object>() { { "Subject", "HolidayRequest" } };
            var message = new Message()
            {
                Body = requestBody,
                Subject = "HolidayRequest",
                ApplicationProperties = props,
                EnqueuedTime = DateTime.Now,
            };
            return JsonSerializer.Serialize(message);
        }
        [Function(nameof(SendHolidayRequest))]
        public async Task<string> SendHolidayRequest([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequestData req)
        {

            var config = new ConfigurationBuilder()
                            .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
                            .AddJsonFile("secret.settings.json", optional: true, reloadOnChange: true)
                            .AddEnvironmentVariables()
                            .Build();
            var queueName = config.GetValue<string>("HolidayRequestQueueName");
            var svcBusConnection = config.GetValue<string>("ServiceBusConnection");

            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
            var message = new ServiceBusMessage(requestBody) { Subject = "HolidayRequest" };
            await new ServiceBusClient(svcBusConnection, new ServiceBusClientOptions
            {
                TransportType = ServiceBusTransportType.AmqpWebSockets
            }).CreateSender(queueName).SendMessageAsync(message);
            return JsonSerializer.Serialize(message);
        }

Decoupling the App

The employee app and the internal HR system are two different services, and any coupling between them should be avoided. Sending messsages to a Service Bus instead of using a REST call directly to the HR system is the first step in this direction.

Now, I want to ensure that the employee app will show the latest holiday approvals on startup - providing a consistent user experience by leveraging the eventual consistency model. I am introducing an Azure Data Table to record the holidays per employee in the cloud, that will provide the holidays to the employee app directly instead of querying the internal system.

Azure Table Client commands

CD into the api folder where the functions are stored, otherwise the next command will return:

Could not find any project in D:\Source\MyApp\

Addd the required package.

dotnet add package Azure.Data.Tables

System error when published to live

Can’t determine project language from files. Please use one of [–csharp, –javascript, –typescript, –java, –python, –powershell, –custom]

No job functions found. Try making your job classes and methods public. If you’re using binding extensions (e.g. Azure Storage, ServiceBus, Timers, etc.) make sure you’ve called the registration method for the extension(s) in your startup code (e.g. builder.AddAzureStorage(), builder.AddServiceBus(), builder.AddTimers(), etc.).

  • Deleted bin and obj folder
  • Downgraded Microsoft.Azure.Functions.Worker.Sdk in apo.csproj from 1.16.3 to 1.15.1 This removed the “No job functions found.” error, leaving the “Can’t determine project language from files.”.
  • Perform dotnet clean
  • From terminal, func host start —csharp This removed the “Can’t determine project language from files.” message.

After tweaking the local.settings.json file, I found the cause was due to a new JSON setting I had added to the Values array - as reported here, values must be strings, and not JSON objects or arrays. I had mistakenly believed that I could use a JSON object after reading about dot notation in the documentation.

Azure Service Bus client library for .NET: Stackoverflow ques tion SWA function debugging