Bind Azure Functions to SAP Event Mesh … What?
Tags: serverless, azure, sap, azurefunctions
Published: 2021-09-06 06:37:06

### Introduction
In a lot of business processes in some point in time we must touch SAP system as the ERP backbone of many companies. There are tons of use-cases with their specific requirements to feed data into SAP and there are also a lot of options to achieve this with corresponding pros and cons.

For sure we can directly call exposed APIs of the SAP systems or connect via integration services like [SAP Cloud Integration](https://help.sap.com/viewer/368c481cd6954bdfa5d0435479fd4eaf/Cloud/en-US/e12c09cc8e9b4574b092d8964b049ce6.html). 

In addition, there is a (quite) new player in the game called [SAP Event Mesh]( https://help.sap.com/viewer/bf82e6b26456494cbdd197057c09979f/Cloud/en-US/df532e8735eb4322b00bfc7e42f84e8d.html) that allows us to send messages and events to via different channels. That sounds like a nice building block to setup a resilient and extensible way to communicate with SAP solutions with the big advantage that we do not have to care about which systems to call exactly. We send a message in a queue and the relevant subscribers can get the access to the needed information.  

Let us assume the starting point of our process is outside of SAP as we have developed a serverless app using Azure Functions and at some point in the process we want to send a message to the SAP system making use of the SAP Event Mesh to trigger follow-up processes in the ERP.

This picture gives you a rough overview of an example flow (some components on BTP side might be missing depending on how to consume and process the messages):

![Overview Message Flow](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k5m397ci1qmaunfnsymc.png)

How can we do that? There is a REST API that allows us to interact with the SAP Event Mesh. But before calling the REST API we must do the OAuth dance and fetch the bearer token to authenticate our call. Okay we can do that in our Azure Function. Problem solved.

Wait a second: in a real-world scenario, there will be not only one business process or one Azure Function app that communicates with the SAP Event Mesh but there will be several. One solution would be the brute force one – every app implements the flow (OAuth + call of the REST API) by themselves. It’s not rocket-science but cumbersome and in case of changes e. g. in the API every app must adopt its code. This is for several reasons like maintainability not the best approach.

Is there a better way to centralize the steps to send a message to the SAP Event Mesh and make the consumption of this functionality in an Azure Function as smooth as possible? And indeed, there is: Azure Functions allow the implementation of [custom bindings]( https://github.com/Azure/azure-webjobs-sdk/wiki/Creating-custom-input-and-output-bindings) to exactly achieve such a flow (or to be more precise this functionality is delivered by the underlying _WebJobsSDK_). 

Let’s explore how we can do that and how complicated it is to achieve a first working setup.

### Setup

To test the scenario, we need an SAP Event Mesh. Luckily this service is part of the [SAP Business Technology Platform trial](https://account.hanatrial.ondemand.com/) and available for free. 

However, the quality of the documentation as well as the one of the management UI has room for improvement. The trial account offers a (deprecated) version of the SAP Event Mesh in the so called _dev plan_. For the inital setup follow along the description for the _[deprecated lite service plan] (https://help.sap.com/viewer/bf82e6b26456494cbdd197057c09979f/Cloud/en-US/7e504322652d4791ade0ca2bbc5a6443.html)_ (you get the fun, I guess: looking for the setup of the dev plan will not guide you to the correct documentation). 

This version might be discontinued in the future, but this is the easiest setup and with respect to the implementation of the binding there are no differences.  

After we have executed the initial setup according to the documentation and created an instance of the SAP Event Mesh we must create so called _service keys_ to get access to the to it e. g. via the REST APIs:
 
![SAP Event Mesh Create Service Keys](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h30i96uwlyfqcfdo1as6.png)

The service key contains the relevant REST endpoints for the OAuth token, credentials and the base URL of the SAP Event Mesh:

![SAP Event Mesh - Service Key](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3ltdgen77opq64a6iesr.png)

As last step of the setup we must navigate to the dashboard of the SAP Event Mesh from the service instance:

![Navigation to SAP Event Mesh Dashboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/flylc1zwms9zeyi9snfj.png)

and create a queue to put our messages into:

![SAP Event Mesh Queue](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6s3seiz05czd0074nun7.png)

From the SAP Event Mesh side everything is put in place to start the implementation of the custom binding.

>> 💡 _Additional Info_: If you want to dive deeper into the topic of the SAP Event Mesh I highly recommend the [YouTube playlist](https://www.youtube.com/playlist?list=PL6RpkC85SLQCf--P9o7DtfjEcucimapUf) of DJ Adams that provides a  lot more details and brings in some CLI magic on top – this helped me a lot!

### The Custom Binding

Azure Functions come along with the concept of [bindings]( https://docs.microsoft.com/en-us/azure/azure-functions/functions-triggers-bindings?tabs=csharp) which allows us to connect to resources outside of the function in a declarative way. 
There are three types of bindings:

- _Trigger_: connection to a resource that triggers the execution of the function
- _Input_: fetching data from a resource as input for the function
- _Output_: sending data to a resource as output of the function

Azure Functions come with some bindings out of the box, but of course not for the SAP Event Mesh. Luckily bindings can be extended via own implementations that can be consumed from an Azure Function via the standard binding mechanisms.

For our scenario we need an output binding that we use to propagate a message to a queue in the SAP Event Mesh. The binding must take a message from the function, do the authentication and then propagate the message to the SAP Event Mesh via the REST endpoint.

What do we have to do to make this reality? Let’s walk through it.

>> _Remark_: These are my first steps into the .NET and C# area, so bear with me with respect to the quality of the code. Happy for any input on improving in that area.

#### Step 1 – Project Setup

We create a directory (e.g. `EventMeshCustomBinding`) and in it we setup the basic skeleton of the custom binding via the dotnet CLI:

```powershell
dotnet new classlib
```
Make sure that the target framework in the project properties  is set to .NET Core 3.1.

Next we add a `.gitignore` file via CLI.

```powershell
dotnet new gitignore
```
Scaffolding done, let's move on to the code.

#### Step 2 – Definition of Binding Data

Now we create the classes needed for the declarative definition of the binding parameters (like credentials and endpoint) as well as the data we expect from the Azure Function to be handed to the SAP Event Mesh.

We define a class `EventMeshMessage.cs` that represents our message that we receive from the Azure Function. We assume that just the message is handed over, so the class looks like this:

```csharp
namespace EventMeshCustomBinding
{ 
    public class EventMeshMessage
    {
        public string message { get; set; } = default!;
    }
}
```
We do the same for the declarative definition of the binding parameters by creating the class ` EventMeshAttribute `. 

The class contains the parameters that we expect from the configuration of the binding as property. It inherits from the `Attribute` base class:

```csharp
using Microsoft.Azure.WebJobs.Description;
using System;

namespace EventMeshCustomBinding
{
    [Binding]
    [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.ReturnValue)]

    public class EventMeshAttribute : Attribute
    {
        [AutoResolve]
        public string EventMeshTokenEndpoint {  get; set; } = default!;

        [AutoResolve]
        public string EventMeshClientId { get; set; } = default!;

        [AutoResolve]
        public string EventMeshClientSecret { get; set; } = default!;

        [AutoResolve]
        public string EventMeshGrantType { get; set; } = default!;

        [AutoResolve]
        public string EventMeshRestBaseEndpoint { get; set; } = default!;

        [AutoResolve]
        public string EventMeshQueueName { get; set; } = default!;
    }
}
```

We added some attributes to the class namely the `[Binding]` attribute to mark it as a binding for the WebJobSDK as well as the usage of the defined attributes via `[AttributeUsage]`.

In addition, we annotate every property of the class with the `[AutoResolve]` attribute to wire it up with the function i. e. with the parameters defined in the function.

#### Step 3 – The Async Collector aka the Core Part

The application logic behind the binding is implemented using the interface  ` IAsyncCollector `. Here is the complete code of the class:

```csharp
using Microsoft.Azure.WebJobs;
using Newtonsoft.Json;
using RestSharp;
using System;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Web;

namespace EventMeshCustomBinding
{
    internal class EventMeshAsyncCollector : IAsyncCollector<EventMeshMessage>
    { 
        private string _eventMeshTokenEndpoint; 
        private string _eventMeshClientId;
        private string _eventMeshClientSecret;
        private string _eventMeshGrantType;
        private string _eventMeshRestBaseEndpoint;
        private string _eventMeshQueueName;

        public EventMeshAsyncCollector(EventMeshBinding config, EventMeshAttribute attr)
        {
            _eventMeshTokenEndpoint = attr.EventMeshTokenEndpoint;
            _eventMeshClientId = attr.EventMeshClientId;
            _eventMeshClientSecret = attr.EventMeshClientSecret;
            _eventMeshGrantType = attr.EventMeshGrantType;
            _eventMeshRestBaseEndpoint = attr.EventMeshRestBaseEndpoint;
            _eventMeshQueueName = attr.EventMeshQueueName;
        }

        public async Task AddAsync(EventMeshMessage message, CancellationToken cancellationToken = default)
        {
            try
            {

                var token = await GetOauthToken();
                
                var restEndPointWithQueue = $"{_eventMeshRestBaseEndpoint}/messagingrest/v1/queues/{HttpUtility.UrlEncode(_eventMeshQueueName)}/messages"; 
                
                var client = new RestClient(restEndPointWithQueue);

                client.Timeout = -1;

                var request = new RestRequest(Method.POST);

                request.AddHeader("x-qos", "0");

                request.AddHeader("Content-Type", "application/json");

                request.AddHeader("Authorization", $"{token.TokenType} {token.AccessToken}");

                var body = JsonConvert.SerializeObject(message, Formatting.Indented);

                request.AddParameter("application/json", body, ParameterType.RequestBody);
                
                IRestResponse response = await client.ExecuteAsync(request);

            }
            catch (Exception e)
            {

                Console.WriteLine(e.Message);
            }
        }

        public Task FlushAsync(CancellationToken cancellationToken = default)
        {
            return Task.CompletedTask;
        }


        private async Task<Token> GetOauthToken( )
        {

            var tokenUrl = $"{_eventMeshTokenEndpoint}?grant_type={_eventMeshGrantType}&response_type=token";

            var client = new RestClient(tokenUrl);
   
            client.Timeout = -1;
            
            var request = new RestRequest(Method.POST);
            
            var base64EncodedCredentials = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{_eventMeshClientId}:{_eventMeshClientSecret}"));

            request.AddHeader("Authorization", $"Basic {base64EncodedCredentials}");
            
            var body = @"";
            
            request.AddParameter("text/plain", body, ParameterType.RequestBody);
            
            IRestResponse response = await client.ExecuteAsync(request);
            
            Token bearerToken = JsonConvert.DeserializeObject<Token>(response.Content);

            return bearerToken;
        }

        internal class Token
        {
            [JsonProperty("access_token")]
            public string AccessToken { get; set; } = default!;

            [JsonProperty("token_type")]
            public string TokenType { get; set; } = default!;

            [JsonProperty("expires_in")]
            public int ExpiresIn { get; set; } = default!;

            [JsonProperty("refresh_token")]
            public string RefreshToken { get; set; } = default!;
        }
    }
}

```

Let’s walk through it step by step. The class consists of three main parts:

- The constructor
- The `AddAsync` method
- The `FlushAsync` method

The `constructor` receives the attribute values of the binding i. e. the configuration parameters that we store in private properties of the class:

```csharp
public EventMeshAsyncCollector(EventMeshBinding config, EventMeshAttribute attr)
        {
            _eventMeshTokenEndpoint = attr.EventMeshTokenEndpoint;
            _eventMeshClientId = attr.EventMeshClientId;
            _eventMeshClientSecret = attr.EventMeshClientSecret;
            _eventMeshGrantType = attr.EventMeshGrantType;
            _eventMeshRestBaseEndpoint = attr.EventMeshRestBaseEndpoint;
            _eventMeshQueueName = attr.EventMeshQueueName;
        }
```

The `FlushAsync` method returns the successful completed task and needs to be implemented as part of the `IAsyncCollector` interface, no real magic in there:

```csharp
public Task FlushAsync(CancellationToken cancellationToken = default)
        {
            return Task.CompletedTask;
        }
```

The main logic is located   in the method `AddAsync`. In our case it contains the logic to get the OAuth token via the private method `GetOauthToken`:

```csharp
private async Task<Token> GetOauthToken( )
        {

            var tokenUrl = $"{_eventMeshTokenEndpoint}?grant_type={_eventMeshGrantType}&response_type=token";

            var client = new RestClient(tokenUrl);
   
            client.Timeout = -1;
            
            var request = new RestRequest(Method.POST);
            
            var base64EncodedCredentials = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{_eventMeshClientId}:{_eventMeshClientSecret}"));

            request.AddHeader("Authorization", $"Basic {base64EncodedCredentials}");
            
            var body = @"";
            
            request.AddParameter("text/plain", body, ParameterType.RequestBody);
            
            IRestResponse response = await client.ExecuteAsync(request);
            
            Token bearerToken = JsonConvert.DeserializeObject<Token>(response.Content);

            return bearerToken;
        }
```

and the call of the SAP Event Mesh REST endpoint to transfer the message to authenticating via the Bearer token.

```csharp
{
            try
            {

                var token = await GetOauthToken();
                
                var restEndPointWithQueue = $"{_eventMeshRestBaseEndpoint}/messagingrest/v1/queues/{HttpUtility.UrlEncode(_eventMeshQueueName)}/messages"; 
                
                var client = new RestClient(restEndPointWithQueue);

                client.Timeout = -1;

                var request = new RestRequest(Method.POST);

                request.AddHeader("x-qos", "0");

                request.AddHeader("Content-Type", "application/json");

                request.AddHeader("Authorization", $"{token.TokenType} {token.AccessToken}");

                var body = JsonConvert.SerializeObject(message, Formatting.Indented);

                request.AddParameter("application/json", body, ParameterType.RequestBody);
                
                IRestResponse response = await client.ExecuteAsync(request);

            }
            catch (Exception e)
            {

                Console.WriteLine(e.Message);
            }
        }
```

We use an internal class `Token` to map the token data from the REST call to an object. In this class we use the attribute ` JsonProperty` on the properties to "automagically" transfer the right data to the properties when serializing the JSON string from the REST call of the OAuth endpoint:

```csharp
internal class Token
        {
            [JsonProperty("access_token")]
            public string AccessToken { get; set; } = default!;

            [JsonProperty("token_type")]
            public string TokenType { get; set; } = default!;

            [JsonProperty("expires_in")]
            public int ExpiresIn { get; set; } = default!;

            [JsonProperty("refresh_token")]
            public string RefreshToken { get; set; } = default!;
        }
```

Some points to put attention to:

- For the OAuth token we need to transfer the `clientid` and the `clientsecret` _base64_ encoded.
- The queue name is part of the URL of the SAP Event Mesh. We must therefore _URL encode_ the string.

Now it is time to wire things up.

>> 💡 _Additional Info_: When implementing the binding I first made sure that the REST calls are working properly. For that I executed the calls via Postman. Postman offers an option to create the code for different languages and libraries which made the transfer to the C# world much easier for me 😊

#### Step 4 – Connecting the Pieces

To integrate the logic into the binding mechanism of Azure Functions we need to implement two more classes:

First we need a config provider class to wire the message and the configuration together with the async collector. We achieve this with few lines of code implementing the interface ` IExtensionConfigProvider ` in the class `EventMeshBinding`:

```csharp
using Microsoft.Azure.WebJobs.Description;
using Microsoft.Azure.WebJobs.Host.Config;

namespace EventMeshCustomBinding
{
    [Extension("EventMeshBinding")]

    
public class EventMeshBinding : IExtensionConfigProvider
    {
        

        public void Initialize(ExtensionConfigContext context)
        {
            context.AddConverter<string, EventMeshMessage>(input => new EventMeshMessage { message = input });
            context.AddBindingRule<EventMeshAttribute>().BindToCollector(attr => new EventMeshAsyncCollector(this, attr));
        }
    }
}

```
We mark the class as an extension using the corresponding attribute and we use the `Initialize` method:

- to convert the input delivered as a string to the EventMessage object. 
- to bind the async collector and the configuration attributes together

>> 💡 _Additional Info_: There is much more that you can do there like validating the input, but let’s start with this setup.

Having this glue code in place we need to make the runtime aware of the extension. This is done via a startup class that we call `EventMeshBindingStartup`:

```csharp
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Hosting;
using EventMeshCustomBinding;

[assembly: WebJobsStartup(typeof(EventMeshBindingStartup))]

namespace EventMeshCustomBinding
{
    public class EventMeshBindingStartup : IWebJobsStartup
    {
        public void Configure(IWebJobsBuilder builder)
        {

            builder.AddExtension<EventMeshBinding>();

        }

    }
}
```
Now we published our newly created SAP Event Mesh binding to the WebJob builder. 

Code-wise everything is in place, so let's build the `dll`

#### Step 5 – Building the DLL

As we have the code in place, we must build the project via 

```
dotnet build
```

This creates the necessary `*.dll` file for our Azure Function binding.

### Usage of the Custom Binding in an Azure Function

We create an HTTP triggered Azure Function in TypeScript in order to wire this one up with our new custom binding. 

Azure Functions in non-.NET languages use the so called _extension bundle_ mechanism to install the needed bindings. As we want to install our own binding we need to bypass this mechanism and bring in our dll. Here are the steps to achieve this:

- We add a folder `extension` to your Azure Functions project.
- We copy the `dll` file of the custom binding into the folder.
- We remove the `extensionbundle` section from the `host.json` file. This switches off the extension bundle mechanism
- We initialize the Azure Functions project with a `extensions.csproj` file via the CLI command `func extensions sync`
- We add the needed dependencies of our custom binding to the `extensions.csproj` file namely for our extension namely:

```xml
 <PackageReference Include="Microsoft.Azure.WebJobs" Version="3.0.27" />
 <PackageReference Include="RestSharp" Version="106.12.0" />
```

- We execute `func extensions install` to install our binding extension. To check if this was successful we take a look into the file `extension.json` in the `bin` directory. There should be an entry for the SAP Event Mesh binding in there.
- We add the output binding to the function in its `function.json` file:

```json
{
    "type": "EventMesh",
    "direction": "out",
    "name": "EventMeshMessage",
    "EventMeshTokenEndpoint": "%EventMeshTokenEndpoint%",
    "EventMeshClientId": "%EventMeshClientId%",
    "EventMeshClientSecret": "%EventMeshClientSecret%",
    "EventMeshGrantType": "%EventMeshGrantType%",
    "EventMeshRestBaseEndpoint": "%EventMeshRestBaseEndpoint%",
    "EventMeshQueueName": "%EventMeshQueueName%"
  }
```

- We add the connection values for the SAP Event Mesh into the `local settings.json` like:

 ```json
 {
 "IsEncrypted": false,
 "Values": {
   "AzureWebJobsStorage": "",
   "FUNCTIONS_WORKER_RUNTIME": "node",
   "EventMeshTokenEndpoint": "<YOUR EVENT MESH TOKEN ENDPOINT>",
   "EventMeshClientId": "<YOUR EVENT MESH CLIENT ID>",
   "EventMeshClientSecret": "<YOUR EVENT MESH CLIENT SECRET>",
   "EventMeshGrantType": "client_credentials",
   "EventMeshRestBaseEndpoint": "<YOUR EVENT MESH REST ENDPOINT>",
   "EventMeshQueueName": "<YOUR EVENT MESH QUEUE NAME>"
   }
 }
 ```
This setup is due to the `[AutoResolve]` attribute in our binding. 

We adjust the functions code to send a simple message to the queue:

```javascript
import { AzureFunction, Context, HttpRequest } from "@azure/functions"

const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {
    context.log('HTTP trigger function processed a request.');
    
    let responseMessage: string

    if (req.body && req.body.message) {

        context.bindings.EventMeshMessage = req.body.message

         responseMessage = `This HTTP triggered function executed successfully. Message was: ${req.body.message}.`;

    }
    else {
        responseMessage = "No message to be sent"
    }

    context.res = {
        body: responseMessage
    }
}

export default httpTrigger
```

Now we can start the function and in `verbose` mode we see that the extension gets picked up:

![Azure Function Startup](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bl1k3nicqdtrdpuoyht2.png)

Let us send a message to the function that will then be propagated to the SAP Event Mesh:

![Send Message to SAP Event Mesh](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vxdg85d3iiyqk9d3rur6.png)

and it will wait there to get consumed:

![SAP Event Mesh - Message in Queue](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ig015n4mun1kmu2hsgdj.png)

By consuming it via a Postman we see that this was the one we sent before: 

![Consume message via Postman](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w117rjgeh8nzhqgjtdbw.png)

Cool, our first custom output binding is up and running!

### Summary

Talking to an SAP system is usually inevitable when developing business applications. There have been and there will be a lot of options to do that. 

One cloud-native approach is available via the SAP Event Mesh. In this blog post we walked through the setup to combine Azure Functions with the SAP Event Mesh. To make this combination convenient for a developer we developed a custom binding to keep the code (and the mind of the developer) of the Azure Function free of any burden from authenticating against the SAP Event Mesh as well as the details on how to send a message to it.

Although this is only a first little step it shows that bridging the gap between the two worlds is not that complicated and custom applications implemented via Azure Functions can be combined with the SAP sphere without breaking your hands. 

As an outlook: if we look at the code, we see that it still has some room for improvement (like error handling, unit testing and validations), so some more .NET to learn for me 😉.

In addition, it is certainly also interesting to have a custom trigger binding for the SAP Event Mesh. That levels up the difficulty at least for me, but something I will certainly have a closer look at. 

### Addendum 1 - The Code

All the code is available on GitHub:
- Custom Binding to SAP Event Mesh: <https://github.com/lechnerc77/SAPEventMeshCustomBinding>
- Azure Function using the Custom Binding: <https://github.com/lechnerc77/SAPEventMeshCustomBindingSampleFunction>

### Addendum 2 - Extension Bundles

Extensions are the way Azure Functions delivers the different bindings and keeps them out of the Azure Functions Core (keeping the core (c)lean - does that ring a bell reader from the SAP world?). 

There are only two extensions backed into the core (HTTP and CRON). For all other ones like Kafka the extension needs to be installed in addition.

For non-.NET languages there is a mechanism that makes the live as developer easier when it comes to install the right extension, called _extension bundle_. This mechanism automatically installs the right extension in the version that is also used in the hosted environment of Azure. The mechanism is configured in the `host.json` file of the Azure Function and tells the runtime to install what is necessary (details which version needs to be installed are available here: <https://github.com/Azure/azure-functions-extension-bundles/blob/v2.x/src/Microsoft.Azure.Functions.ExtensionBundle/extensions.json>).

Technically the Azure Function host reaches out to a CDN and installs the function based on some constants hidden in the Azure Functions Host (see <https://github.com/Azure/azure-functions-host/blob/9bdb40b2d517f32c5052956ba8c9cf662ea36a9e/src/WebJobs.Script/ScriptConstants.cs> => search for `ExtensionBundleDefaultSourceUri`)

We maybe do not want to reach out to the CDN, use our own CDN or we want to pin a specific version or maybe want to switch to a later version than the CDN provides (updates of the versions in the ExtensionBundles do not occur as regularly as updates of extensions). How can we achieve that?

In the blog post we used a kind of brute-force approach to get our extension installed, which is okay for development purposes. But in a professional setup we may provide our own NuGet feed.