This article was originally published at blogs.siliconorchid.com on 19-May-2019
This is part two of a three-part series that shows you a way to combine several Serverless technologies, creating an online chat system that sends messages between users of WhatsApp and a real-time web app.
In part 1 of this series, we introduce you to the scenario and walk through the configuration of an entire solution, ready for local testing.
In part 2 of this series, we look at the various pieces of code that make up our solution in detail.
In part 3 of this series, we walk through the additional steps needed to set up and deploy the solution to the cloud.
Let's look at the code
In the previous article, we spent a while looking at how to set up and run the project. Let's look at some code next!
If you haven't done so already, you should clone this repository.
Our Visual Studio project is comprised of two projects - an Azure Function project (WhatsAppSignalRDemo.Function
) and an ASP.NET Core Web Application (WhatsAppSignalRDemo.Web
).
Parts of the code in this demo originated from the example provided in the Microsoft SignalR Quickstart guide, so this would be a recommended read.
Another recommended resource from Microsoft is the tutorial Get started with ASP.NET Core SignalR
- whilst it doesn't cover SignalR Service specifically, it does show you how to write a chat client using "Hosted SignalR" and introduces most of the concepts you need to know.
The web client project
In this demo, we've gone out of our way to make the Web Client as simple as possible.
The Web App consists purely of static web content. There is no MVC, Razor, Controllers nor any other server-side logic. Instead, we have a single index.html
and a small amount of accompanying static content (such as JavaScript and CSS).
The purpose of doing this is twofold:-
We don't want to distract you with any implementation details that are specific to a UI framework (whether that be ASP.NET MVC or one of the many JavaScript frameworks/libraries such as Angular or React). We want to focus upon the essentials, meaning that we have a very basic UI and minimal code (which includes any code needed to interact with SignalR).
We specifically wanted to have purely static web content (i.e. just
.html
,.css
and.js
files) so that, later in the article when we come to publish the Web App to the cloud, we can show you how to use Azure Storage Static Websites. One considerable benefit to this approach is that the hosting cost is a fraction of what it could otherwise be.
For development and testing purposes, we will still need a development web server, so we have opted to use an "ASP.NET Core Web Application" project as a starting point. This is still essentially the same as you would find for a conventional MVC or RazorPages project, except it has been pared back to the absolute minimum.
- For example, there is no longer a
Pages
folder (for.cshtml
views, in general) and none of the usual MVC constructs (such as_Layout.cshtml
). - Static content has no need for configuration, so
appsettings.json
has been discarded. - The template includes the JQuery library, but that is not being used so has been removed from the
wwwroot/lib
folder. - We have retained the
site.css
stylesheet and the Bootstrap library. - We needed to include the SignalR Javascript Client library. You can find that in
wwwroot/lib/signalr
Moving on to the small amount of C# code in this project:-
-
Program.cs
remains untouched -
Startup.cs
has significant changes:-- Pretty much the entire content that was provided by the new-project Template has been discarded.
- We need to explicitly tell ASP.NET core to use the
index.html
and we need to explicitly tell it to serve up static content. The revised code looks like this:-
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
namespace WhatsAppSignalRDemo.Web
{
public class Startup
{
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
DefaultFilesOptions options = new DefaultFilesOptions();
options.DefaultFileNames.Add("index.html");
app.UseDefaultFiles(options);
app.UseStaticFiles();
}
}
}
Javascript code on the client
The script that runs in the browser can be located here:
….\WhatsAppSignalRDemo\src\WhatsAppSignalRDemo.Web\wwwroot\js\chat.js
We briefly looked at this piece of code earlier in the article, when we checked that the API endpoint serviceEndpoint
was correctly defined.
This particular piece of code started off life as being a copy of the sample code presented in the aforementioned Microsoft chat-application Tutorial Get started with ASP.NET Core SignalR
, so any similarities are due to this. That original sample was intended to work with Hosted SignalR, so along with other customisations, we've adapted the code to work with our SignalR Service.
As an overview, the code uses the Microsoft SignalR JavaScript library to create a “connection” object (named signalRconnection
in this demo). The MS library does all the heavy-lifting work for us.
The code found in ....\WhatsAppSignalRDemo\src\WhatsAppSignalRDemo.Web\wwwroot\js\chat.js
does the following:-
- Defines a handful of configuration values, identifying the API endpoint of the service and a specific API name to be used for broadcasting messages.
- Calls the
signalR.HubConnectionBuilder()
function to establish a connection to either a SignalR Hub or SignalR Service (we use the same JavaScript client, regardless of how the backend is implemented).- The "HubConnectionBuilder" code is hardcoded to look for an API which is called
negotiate
by convention. We need to provide this in our Azure Function backend. This part is quite an important point to be aware of. You can read about it on Microsoft: Azure Functions development and configuration with Azure SignalR Service .
- The "HubConnectionBuilder" code is hardcoded to look for an API which is called
var signalRconnection = new signalR.HubConnectionBuilder()
.withUrl(serviceEndpoint)
.build();
- We have an event listener, which reacts to messages being broadcast from the SignalR Service. When they arrive, the message object is separated into component parts (a username and user message) and simply appended to a UI list:-
signalRconnection.on(signalRTargetGroupName, function (broadcastMessage) {
console.log(broadcastMessage);
var encodedMsg = "<b>" + broadcastMessage.User + "</b> : "" + broadcastMessage.Message + """;
var li = document.createElement("li");
li.innerHTML = encodedMsg;
document.getElementById("messagesList").appendChild(li);
});
- We have a startup handler, that is responsible for updating the UI when the client established a connection to the SignalR Service (it enables the "send" button and removes the "please wait" message):-
signalRconnection.start().then(function () {
document.getElementById("sendButton").disabled = false;
document.getElementById("connectingMessage").style.display = "none";
}).catch(function (err) {
return console.error(err.toString());
});
With SignalR Service, instead of "invoking" Hub methods, we send HTTP messages to a restful API endpoint.
The way we interact with SignalR Service is different from traditional Hosted SignalR.
Previously, with Hosted SignalR we would have a server-side "Hub" that contained [C# proxy] methods. In our client-side [JavaScript] code, we would "invoke" these methods using the SignalR
connection object itself.
Such a piece of code would have looked like this (where "SendMessage" would be a corresponding C# method of the Hub class):-
signalRconnection.invoke("SendMessage", user, message).catch(function (err) {
return console.error(err.toString());
});
In contrast, when using the "serverless mode" of SignalR Service, we still have the concept of Hubs (e.g. will still need to identify a "HubName" to group connections together) - but those Hubs are generic and no longer contain our [C# proxy] code. This means that there is no longer anything to "invoke".
Instead, we need our [web] client to POST separate HTTP messages to an API that we provide ourselves. In our serverless architecture, this would be another HTTP-triggered Azure Function.
Any APIs we create that provide SignalR functionality will have a connection string tying that Azure Function to our SignalR Service. We'll talk about the C# API code itself, later in the series.
This change is worth underlining because, at the time of writing, I felt that this was slightly ambiguous in the MS documentation. When researching this article, I wasn't confident that I should have been abandoning the use of the
invoke
function of thesignalRConnection
object - having to make separate REST calls felt like I awkwardly working around their solution.To be sure that I have presented the correct solution in this article, I asked Microsoft Cloud Advocates directly. Thank you Brady Gaster and Anthony Chu for clarifying that I was indeed, doing it right!!
Something interesting that they pointed out, was that they are currently looking for a way to harmonise the experience, so there is a good chance that this current approach may change again in the near future.
Returning to the code in this demo, the need to make separate HTTP call(s) represents the most significant shift away from the Microsoft sample code that this file was originally based upon.
- The following encapsulates a vanilla JavaScript
fetch
request into the functionpostMessage
. In the future, if our code expands to require other HTTP calls, we could then easily reuse this function.
document.getElementById("sendButton").addEventListener("click", function (event) {
var userMessage = new Object();
userMessage.User = document.getElementById("userInput").value;
userMessage.Message = document.getElementById("messageInput").value;
postMessage(serviceEndpoint + signalRBroadcastApiMethod, userMessage);
event.preventDefault();
});
function postMessage(url = ``, data = {}) {
// Default options are marked with *
return fetch(url, {
method: "POST", // *GET, POST, PUT, DELETE, etc.
mode: "cors", // no-cors, cors, *same-origin
cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
credentials: "same-origin", // include, *same-origin, omit
headers: {
"Content-Type": "application/json",
// "Content-Type": "application/x-www-form-urlencoded",
},
redirect: "follow", // manual, *follow, error
referrer: "no-referrer", // no-referrer, *client
body: JSON.stringify(data), // body data type must match "Content-Type" header
})
.then(response => response.json()); // parses JSON response into native Javascript objects
}
The Azure Function project
Broadly speaking this project is comprised of just three main elements:-
-
Models - there are only two models in the project, a container for some configuration items
GenericConfig.cs
and a very simple container for user messagesUserMessage.cs
- Common Code - this represents a handful of classes that are shared across all functions.
- Functions - there are four HTTP-triggered Azure Functions .
If you have worked with Azure Functions in the past, you may be expecting to declare each function as a
static
. A very recent change to the SDK means that this is no longer a requirement and you can create function as an instance class. This is all due to scoping changes being made in order to accommodate the upcoming introduction of Dependency Injection.
Project Dependencies
There are only a handful of dependencies to other libraries. It would be simplest to add these dependencies using the NuGet Package Manager, but here is the library listing as seen in the ....\src\WhatsAppSignalRDemo.Function\WhatsAppSignalRDemo.Function.csproj
file:-
<ItemGroup>
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.SignalRService" Version="1.0.0" />
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="1.0.24" />
<PackageReference Include="twilio" Version="5.28.2" />
</ItemGroup>
GlobalConstants.cs
One of the things that struck me when looking at much of the sample code available for both Azure Functions and SignalR, is that there seems to be a heavy reliance on string literals for identifying things in the code.
For example, functions typically have a [FunctionName="xyz"]
attribute to define their exposed API name.
Similarly, if we wanted to pass messages between functions (over HTTP), we find ourselves identifying the function we want by the URL - again, another form of string.
I feel that these various examples make for brittle code - it would only take a typo for things to break.
Additionally, by using string-literals, we lose things such as compile-time checks and intellisense [to hint at mistakes]. If there is a problem, it will only manifest at runtime.
Therefore I have implemented a simple solution to address this issue. We create the static class GlobalConstants
and within this, define a small selection of string constants. The globalConstants.cs
class looks like this:-
namespace WhatsAppSignalRDemo.Function.Common
{
public static class GlobalConstants
{
public const string SignalR_HubName = "Chat";
public const string SignalR_TargetGroup = "AllUsers";
public const string FunctionName_Negotiate = "negotiate";
public const string FunctionName_BroadcastSignalRMessage = "BroadcastSignalRMessage";
public const string FunctionName_SendWhatsAppMessage = "SendWhatsAppMessage";
public const string FunctionName_ReceiveWhatsAppMessage = "ReceiveWhatsAppMessage";
public const string Config_TwilioAccountSid = "TwilioAccountSid";
public const string Config_TwilioAuthToken = "TwilioAuthToken";
public const string Config_TwilioWhatsAppPhoneNumber = "TwilioWhatsAppPhoneNumber";
}
}
Creating and using this class means that we can define string values in just one single place and then we can use those strongly-typed values across the rest of the project.
Aside from greatly reducing the possibility of making a silly mistake caused by a typo, it means that if you want to do something such as rename a keyname in your configuration, you don't have to then make this change all over your codebase.
To use these values, we can substitute them anywhere where a string literal would otherwise be used. So where previously we would have code that looks like this:-
public class SendWhatsAppMessageFunction
{
[FunctionName("SendWhatsAppMessage")]
public async Task<IActionResult> Run(
...
We can instead replace it with the following:-
public class SendWhatsAppMessageFunction
{
[FunctionName(GlobalConstants.FunctionName_SendWhatsAppMessage)]
public async Task<IActionResult> Run(
...
HttpHelper.cs
This class is a component that helps our functions make HTTP requests. It is a common, reusable component that can be shared by several separate Azure Functions.
Making RESTful calls is one mechanism for communicating between different functions (there are other, more reliable, ways such as using a queuing mechanism, but that is beyond the scope of this article). For example, in this demo, when a message is received from WhatsApp to the ReceiveWhatsAppMessageFunction
, the message is relayed onward to the BroadcastSignalRMessageFunction
by way of an HTTP call.
using System.IO;
using System.Net;
using Newtonsoft.Json;
namespace WhatsAppSignalRDemo.Function.Common
{
public static class HttpHelper
{
public static void PostMessage (string apiUrl, object message)
{
//send request
var httpWebRequest = (HttpWebRequest)WebRequest.Create(apiUrl);
httpWebRequest.ContentType = "application/json";
httpWebRequest.Method = "POST";
using (var streamWriter = new StreamWriter(httpWebRequest.GetRequestStream()))
{
streamWriter.Write(JsonConvert.SerializeObject(message));
streamWriter.Flush();
streamWriter.Close();
}
//get response
var httpResponse = (HttpWebResponse)httpWebRequest.GetResponse();
using (var streamReader = new StreamReader(httpResponse.GetResponseStream()))
{
var result = streamReader.ReadToEnd();
}
}
}
}
StartupHelper.cs
Azure Functions are self-contained units that are instanced as demand requires. This is why they can scale without issue.
In contrast, ASP.NET APIs exist on a common platform - a constantly provisioned App Server.
One benefit of using an ASP.NET Web Application is that you only need a single application startup (with that code literally being in the Startup.cs
file). This means that any setup and configuration needs happen only once and the results can then be made available until the application is recycled.
In contrast, an Azure Function requires you to run configuration on each and every use. If you look at many of the code examples for Azure Functions, you tend to see often those examples repeating boilerplate code for things like configuration.
Looking at an Azure Function in isolation, this probably doesn't appear to be much of an issue. However, as soon as you have a suite of different functions, you'll start to see a pattern of repetition, which isn't ideal.
As developers we aspire to produce, what we hope to be well-considered code, therefore, we should try and follow software design principles where possible. Amongst these recommendations is the DRY Principle - which basically says "don't repeat the same code everywhere - try and reuse it".
So, the code in ....\src\WhatsAppSignalRDemo.Function\Common\StartupHelper.cs
is an effort to try and encapsulate some of that code which is common between each of the four Functions.
using System;
using Microsoft.Azure.WebJobs;
using WhatsAppSignalRDemo.Function.Models;
namespace WhatsAppSignalRDemo.Function.Common
{
public static class StartupHelper
{
public static GenericConfig GetConfig(ExecutionContext executionContext)
{
string functionHostName = Environment.GetEnvironmentVariable("WEBSITE_HOSTNAME");
return new GenericConfig
{
TwilioAccountSid = Environment.GetEnvironmentVariable(GlobalConstants.Config_TwilioAccountSid),
TwilioAuthToken = Environment.GetEnvironmentVariable(GlobalConstants.Config_TwilioAuthToken),
TwilioWhatsAppPhoneNumber = Environment.GetEnvironmentVariable(GlobalConstants.Config_TwilioWhatsAppPhoneNumber),
ApiEndpoint = $"http://{functionHostName}/api/"
};
}
}
}
Within the Function class (such as ReceiveWhatsAppMessageFunction
), the StartupHelper
is made available a little bit like an ASP.NET Core Configuration Item. It would look like this (partial code, for brevity):-
...
private GenericConfig _genericConfig;
...
_genericConfig = StartupHelper.GetConfig(executionContext);
...
HttpHelper.PostMessage(
_genericConfig.ApiEndpoint + GlobalConstants.FunctionName_BroadcastSignalRMessage,
userMessage);
...
The intent of the StartupHelper
class is to demonstrate a way to encapsulate all of the requests for configuration information, into one place (which can then be re-used by multiple different functions). The mindset behind this idea is to emulate the effect of having all of your configuration code in a startup class.
The practical use of the above class is to define and populate the field ApiEndpoint
, which is genuinely re-used in three of the four functions.
The inclusion of the three Twilio configuration items into StartupHelper
, is a little bit more tenuous to justify. Those particular fields are only actually used in SendWhatsAppMessageFunction
, so you could argue that those configuration items may have been better placed directly into just SendWhatsAppMessageFunction
, using Environment.GetEnvironmentVariables
as appropriate.
The point of this article is to talk about and demonstrate some ideas, so take all of this into consideration and choose a solution that suits your purposes best.
NegotiateFunction.cs
This function is fundamental to the operation of the SignalR Service, so you need to know that it's both required and how works.
Previously, with Hosted SignalR, the [web] client would negotiate directly with the Hub. This would all be managed transparently by the SignalR JavaScript client code - we only needed to provide the hostname of the service.
With SignalR Service, the concept of a Hub still exists, but it is all dynamically provisioned and managed by Azure. The upshot is that our [web] client can't make the first connection directly. To make this work, we need to provide a static HTTP API, which will negotiate with the SignalR Service, and return connection details and a user-access token.
The good news is that the SignalR JavaScript client software, provided by Microsoft, understands the differences between working with Hosted and Service modes, so will explicitly be expecting you to provide an API called "negotiate". You can read about that in their own documentation here.
When it comes to writing the C# for the "negotiate" API, fortunately, the code itself is really straightforward and we didn't even need to write it ourselves, as it comes directly cut+paste from Microsoft's documentation.
In our demo, we've tweaked it slightly, so as to provide consistency and make use of our GlobalConstants
:-
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Extensions.SignalRService;
using Microsoft.AspNetCore.Http;
using WhatsAppSignalRDemo.Function.Common;
namespace WhatsAppSignalRDemo.Function
{
public class NegotiateFunction
{
[FunctionName(GlobalConstants.FunctionName_Negotiate)]
public SignalRConnectionInfo Negotiate(
[HttpTrigger(AuthorizationLevel.Anonymous)]HttpRequest req,
[SignalRConnectionInfo(HubName = GlobalConstants.SignalR_HubName)]SignalRConnectionInfo connectionInfo)
{
return connectionInfo;
}
}
}
One part of the code to draw your attention to is the need to be consistent with your use of the SignalR HubName
. This needs to be shared across any of the references that you make - so in the case of this demo, be conscious that this HubName is also used in the BroadcastSignalRMessageFunction
(which is another good reason to use string constants, rather than literals)
BroadcastSignalRMessageFunction.cs
The BroadcastSignalRMessageFunction
class is an HTTP-triggered Azure Function, which receives a message from the web client and relays it onto both:-
- The SignalR Service, which In turn, broadcasts the message to every connected client.
- The
SendWhatsAppMessageFunction
API, which in turn, broadcasts the message to every subscribed WhatsApp user.
Earlier in this article, we've explained that although SignalR Service has Hubs, they no longer accommodate having methods written against them.
The BroadcastSignalRMessageFunction
is the stand-alone HTTP API, that effectively replaces code that would previously have been a method within the Hub class.
The SignalR Service extension is added as an argument to our Function and the object is named signalRMessages
. This handles all the operational details, such as locating a connection string, automatically for us.
We send messages into the Service by calling signalRMessages.AddAsync(...)
like this:-
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Extensions.SignalRService;
using WhatsAppSignalRDemo.Function.Common;
using WhatsAppSignalRDemo.Function.Models;
namespace WhatsAppSignalRDemo.Function
{
public class BroadcastSignalRMessageFunction
{
private GenericConfig _genericConfig;
[FunctionName(GlobalConstants.FunctionName_BroadcastSignalRMessage)]
public Task SendMessage(
[HttpTrigger(AuthorizationLevel.Anonymous, "post")]object message,
[SignalR(HubName = GlobalConstants.SignalR_HubName)]IAsyncCollector<SignalRMessage> signalRMessages,
ExecutionContext executionContext)
{
_genericConfig = StartupHelper.GetConfig(executionContext);
// relay the message to the WhatsApp API
HttpHelper.PostMessage(_genericConfig.ApiEndpoint + GlobalConstants.FunctionName_SendWhatsAppMessage, message);
//relay the message to the SignalR Service
return signalRMessages.AddAsync(
new SignalRMessage
{
Target = GlobalConstants.SignalR_TargetGroup,
Arguments = new[] { message }
});
}
}
}
SendWhatsAppMessageFunction.cs
This Function is not intended to be called directly from the [web] client, but instead triggered by the BroadcastSignalRMessageFunction
Function.
This particular Function is probably the most complex in terms of different tasks that it performs:-
- The incoming JSON message is deserialised and ultimately end ups in the C#
dynamic
type (for simplicity) namedrequestObject
. - The Twilio client is created and initialised using credentials originating in our configuration.
- Using the Twilio library method
MessageResource.ReadAsync(...)
we obtain a list of all WhatsApp messages that have been added to our sandbox in this session. This will contain a list of every single message, including each phone number of the user involved. This collection is namedmessageCollection
. - We use a LINQ query to filter the list of every phone number in
messageCollection
, down to a list of unique phone numbers. This resultant collection is nameddistinctPhoneNumbers
. - We iterate over the list of unique phone numbers, using the Twilio library method
MessageResource.Create(...)
to send a message to each.- We test that the phone number of the recipient is not the same as the phone number in the message (i.e. the sender) - we don't want to send a WhatsApp message straight back to the user who sent it!
using System.IO;
using System.Threading.Tasks;
using System.Linq;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
using Twilio;
using Twilio.Base;
using Twilio.Types;
using Twilio.Rest.Api.V2010.Account;
using WhatsAppSignalRDemo.Function.Models;
using WhatsAppSignalRDemo.Function.Common;
namespace WhatsAppSignalRDemo.Function
{
public class SendWhatsAppMessageFunction
{
private GenericConfig _genericConfig;
[FunctionName(GlobalConstants.FunctionName_SendWhatsAppMessage)]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)]HttpRequest httpRequest,
ExecutionContext executionContext
)
{
_genericConfig = StartupHelper.GetConfig(executionContext);
string requestBody = await new StreamReader(httpRequest.Body).ReadToEndAsync();
dynamic requestObject = JsonConvert.DeserializeObject(requestBody);
// connect to Twilio
TwilioClient.Init(_genericConfig.TwilioAccountSid, _genericConfig.TwilioAuthToken);
// query twilio api ... and get distinct phone numbers of whatsapp users subscribed to chat
ResourceSet<MessageResource> messageCollection = await MessageResource.ReadAsync(to: _genericConfig.TwilioWhatsAppPhoneNumber);
IEnumerable<PhoneNumber> distinctPhoneNumbers = messageCollection.GroupBy(a => a.From.ToString()).Select(b => b.FirstOrDefault().From);
foreach(var phoneNumber in distinctPhoneNumbers)
{
if (phoneNumber.ToString() == requestObject.User.ToString())
{
// if the message originated from whatsapp, don't send a copy straight back to sender
continue;
}
// send message to whatsapp user
var message = MessageResource.Create(
from: new Twilio.Types.PhoneNumber(_genericConfig.TwilioWhatsAppPhoneNumber),
body: $"{requestObject.User} : {requestObject.Message}",
to: phoneNumber
);
}
return (ActionResult)new OkResult();
}
}
}
ReceiveWhatsAppMessageFunction.cs
This Function is the endpoint we identified in the Twilio Dashboard. Its purpose is to simply receive a message sent from Twilio and extract some of the fields that we receive.
That extracted information is then used to populate our Model Class UserMessage
. The fields are simply a username (the WhatsApp phone number) and message.
The processed message is then sent onward, via an HTTP POST, to BroadcastSignalRMessage
.
using System.IO;
using System.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using WhatsAppSignalRDemo.Function.Models;
using WhatsAppSignalRDemo.Function.Common;
namespace WhatsAppSignalRDemo.Function
{
public class ReceiveWhatsAppMessageFunction
{
private GenericConfig _genericConfig;
[FunctionName(GlobalConstants.FunctionName_ReceiveWhatsAppMessage)]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)]
HttpRequest httpRequest,
ExecutionContext executionContext)
{
_genericConfig = StartupHelper.GetConfig(executionContext);
string requestBody = await new StreamReader(httpRequest.Body).ReadToEndAsync();
UserMessage userMessage = new UserMessage
{
User = HttpUtility.ParseQueryString(requestBody).Get("From"),
Message = HttpUtility.ParseQueryString(requestBody).Get("Body")
};
HttpHelper.PostMessage(
_genericConfig.ApiEndpoint + GlobalConstants.FunctionName_BroadcastSignalRMessage,
userMessage);
return new OkResult();
}
}
}
Summary
We've now had an in-depth look at the code and you should be in a good place with your understanding of what's needed in the system.
In the final article of this series, we'll look into the additional steps needed to get our development system published to the cloud.
See you there!
Top comments (0)