If you’ve been following my Twitter feed, you may have noticed I’ve been building some demo apps with SMS along parallel tracks, with Blazor and pure MVC. There are three primary functionalities we need to think through when we’re talking SMS.
Now that those are all out of the way - it seemed like a good point to stop and build something more complicated. That is what we are going to be doing in this tutorial.
We’re going to be using Blazor WebAssembly, MVC, the Entity Framework Core, and SignalR to build a Single-Page-App(SPA).
This application will be able to Send SMS messages of-course. It will also be able to receive SMS messages; each message it receives will be stored using the Entity Framework and push straight to our client applications using SignalR. It will do the same thing for all delivery receipts that it receives. This project might seem a bit ambitious, but I promise you it’s not. So without further adieu, let’s commence our adventure.
If you want to see the code for this demo, you can check it out in GitHub.
To complete this tutorial, you will need a Vonage API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the Vonage API Dashboard.
I’m going to be throwing everything on localhost:5000
, running ngrok will allow us to publicly access localhost:5000
.
ngrok http --host-header=localhost:5000 5000
Take a note of the URL that ngrok is running on. In my case, it’s running on http://fb09abd3c106.ngrok.io
. That URL is going to be the base URL for my webhooks going forward.
Let’s create our Solution:
After you create your project, you are going to have a solution with three projects:
You will need to add the following NuGet packages to your respective projects.
Microsoft.AspNetCore.SignalR.Core
Nexmo.Csharp.Client
Microsoft.EntityFrameworkCore.Sqlite
Microsoft.EntityFrameworkCore.Design
Microsoft.AspNetCore.SignalR.Client
System.ComponentModel.Annotations
We are going to be using EntityFramework Core with SQLite to house our database. You could just use the data-structures from the Nexmo.Csharp.Client as the model, but since we are using WebAssembly, that would involve having to send the whole Nexmo Csharp SDK down to the client browser, which will involve quadrupling the size of the wasm. To avoid that, we’re going to add Plain Old CLR Objects(POCOs) to our VonageSmsDashboard.Shared
project. Add the following classes to that project:
public class MessageBase
{
[Key]
public string MessageId { get; set; }
public string To { get; set; }
public string MessageTimestamp { get; set; }
public string Msisdn { get; set; }
}
public class DeliveryReceiptModel : MessageBase
{
public string Status { get; set; }
}
public class InboundSmsModel : MessageBase
{
public string Text { get; set; }
}
public class OutboundSms
{
[Key]
public string MessageId { get; set; }
public string To { get; set; }
public string From { get; set; }
public string Status { get; set; }
public string MessagePrice { get; set; }
}
Switch over to VonageSmsDashboard.Server
and add the file SmsContext.cs
to the project. Add a DbSet
for our Inbound Messages and Dlrs. Then override the OnConfiguring
method to point at a Sqlite data source.
public DbSet<InboundSmsModel> InboundSms { get; set; }
public DbSet<DeliveryReceiptModel> Dlrs { get; set; }
public DbSet<OutboundSms> OutboundSms { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseSqlite("Data Source=VonageSms.db");
Build your project quickly, or dotnet ef
is liable to remove your dependant packages when you perform the migration. Now in your console, navigate to the VonageSmsDashboard.Server and run the following:
dotnet tool install --global dotnet-ef
dotnet ef migrations add InitialCreate
dotnet ef database update
This will output Migrations\SmsContextModelSnapshot.cs
and Migrations\A_TIMESTAMP_InitialCreate.cs
after this, your model is created, we will now add our SignalR Hub.
In our VonageSmsDashboard.Server
project add a folder called Hubs
, then add an SmsHub.cs
file to that folder. Have the SmsHub extend Hub
.
public class SmsHub : Hub{}
We’re going to need to set up a couple of pieces of middleware for ourselves to help us get set up with Entity/SignalR.
In Startup.cs
find the ConfigureServices
method and add the following two lines to it:
services.AddSignalR();
services.AddDbContext<SmsDashboardContext>();
Next, in the Configure
method, add a endpoints.MapHub<Hubs.SmsHub>("/smshub")
to the UseEndpoints
delegate, which will now look like:
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapControllers();
endpoints.MapFallbackToFile("index.html");
endpoints.MapHub<Hubs.SmsHub>("/smshub");
});
With our model built and all our middleware setup we will build our controller. In VonageSmsDashboard.Server
right click on the Controllers
folder -> add -> Controller -> API Controller - Empty
-> Add -> SmsController.cs
Let’s inject our dependencies into the SMS controller. We are going to need an IConfiguration
to pull out our API_KEY/API_SECRET. We will need an IHubContext
to manage the connection with our clients. And we are going to need a SmsContext
which will be our access to our database. Inject them all into the Controller’s constructor like so:
private readonly IConfiguration _config;
private readonly IHubContext<Hubs.SmsHub> _hubContext;
private readonly SmsContext _dbContext;
public SmsController(IConfiguration config, IHubContext<Hubs.SmsHub> context, SmsContext dbContext)
{
_hubContext = context;
_config = config;
_dbContext = dbContext;
}
We’ll now add an action to send an SMS; this will take an OutboundSms object, pull our ApiKey/secret from the config, send an SMS, then add the Message Id, price, and timestamp to our request object, and return it to the requestor.
[HttpPost]
[Route("[controller]/sendsms")]
public async Task<ActionResult<OutboundSms>> SendSms([FromBody] OutboundSms sms)
{
var apiKey = _config["API_KEY"];
var apiSecret = _config["API_SECRET"];
var credentials = Credentials.FromApiKeyAndSecret(apiKey, apiSecret);
var request = new SendSmsRequest { To = sms.To, From = sms.From, Text = sms.Text };
var client = new SmsClient(credentials);
var response = client.SendAnSms(request);
sms.MessagePrice = response.Messages[0].MessagePrice;
sms.Status = response.Messages[0].Status;
sms.MessageId = response.Messages[0].MessageId;
_dbContext.OutboundSms.Add(sms);
await _dbContext.SaveChangesAsync();
return sms;
}
Next, We’ll add Get Methods for each of our model types that will just read them out of the database and return it to the requestor.
[HttpGet]
[Route("[controller]/getInboundSms")]
public ActionResult<List<InboundSmsModel>> GetInboundSms()
{
return _dbContext.InboundSms.ToList();
}
[HttpGet]
[Route("[controller]/getDlr")]
public ActionResult<List<DeliveryReceiptModel>> GetDlr()
{
return _dbContext.Dlrs.ToList();
}
[HttpGet]
[Route("[controller]/getOutboundSms")]
public ActionResult<List<OutboundSms>> GetOutboundSms()
{
return _dbContext.OutboundSms.ToList();
}
Now we need a route to handle the SMS and DLR messages inbound to our app. These requests will pull the body out of the stream and deserialize it into an Inbound SMS and DLR object. Then it will map the critical fields into our model objects. Then it will push it down to the SmsHub clients. Finally, it will save the new inbound/Dlr to our database.
[HttpPost]
[Route("webhooks/inbound-sms")]
public async Task<IActionResult> ReceiveSms()
{
using (StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8))
{
var json = await reader.ReadToEndAsync();
var inboundSms = JsonConvert.DeserializeObject<InboundSms>(json);
var inboundSmsModel = new InboundSmsModel { Msisdn = inboundSms.Msisdn, To = inboundSms.To, MessageId = inboundSms.MessageId, Text = inboundSms.Text, MessageTimestamp = inboundSms.MessageTimestamp };
await _hubContext.Clients.All.SendAsync("ReceiveMessage", inboundSms);
_dbContext.InboundSms.Add(inboundSmsModel);
await _dbContext.SaveChangesAsync();
}
return NoContent();
}
[HttpPost]
[Route("webhooks/dlr")]
public async Task<IActionResult> ReceiveDlr()
{
using (StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8))
{
var json = await reader.ReadToEndAsync();
var dlr = JsonConvert.DeserializeObject<DeliveryReceipt>(json);
var dlrModel = new DeliveryReceiptModel { Msisdn = dlr.Msisdn, To = dlr.To, MessageId = dlr.MessageId, Status = dlr.StringStatus, MessageTimestamp = dlr.MessageTimestamp };
await _hubContext.Clients.All.SendAsync("ReceiveDlr", dlrModel);
_dbContext.Dlrs.Add(dlrModel);
await _dbContext.SaveChangesAsync();
}
return NoContent();
}
The last thing we need to do is build the frontend. Ordinarily, this would involve bringing together HTML/js/CSS until we got everything just the way we like it. The beauty of Blazor wasm is that we only need to think about one type of unit - our razor components. Let’s create a Components
folder in the VonageSmsDashboard.Client
directory.
First, we’re going to need to load stuff from our controller dynamically, so we’ll need to disable the caching of the HttpClient
. Open Program.cs
and replace the builder.Services.AddTransient
call to:
builder.Services.AddTransient(sp =>
{
var client = new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) };
client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue { NoCache = true };
return client;
});
In the VonageSmsDashboard.Client\Components
folder, add a new razor component “DeliveredMessages” this is where we are going to display DLRs as they come into our app. At the head of this add using statements for the Microsoft.AspNetCore.SignalR.Client
and VonageSmsDashboard.Shared
namespaces. Then inject a NavigationManager and an HttpClient. Also, as we’re going to be using a HubConnection
to manage the push from SignalR, we’re going to need to implement IDisposable
to clean it up afterward.
@using Microsoft.AspNetCore.SignalR.Client
@using VonageSmsDashboard.Shared;
@inject NavigationManager NavigationManager
@inject HttpClient Http
@implements IDisposable
Now that we have our dependencies let’s go ahead and add the visual part of our component. It will just be a table populated from the list of messages that we will get from the server.
<div class="x-display-table">
<h2>Delivered Messages</h2>
<table class="table" id="dlrList">
<thead>
<tr>
<th>To</th>
<th>From</th>
<th>Message Id</th>
<th>TimeStamp</th>
<th>Status</th>
</tr>
</thead>
<tbody>
@for (var i = _messages.Count - 1; i >= 0; i--)
{
var message = _messages[i];
<tr>
<td>@message.Msisdn</td>
<td>@message.To</td> @*note the to = the number you sent from*@
<td>@message.MessageId</td>
<td>@message.MessageTimestamp</td>
<td>@message.String</td>
</tr>
}
</tbody>
</table>
</div>
Now we must add a couple fields _hubConnection
and _messages
to hold our SignalR hub connection and the DLR messages, respectively. We need to add an override to OnInitializedAsync
to initialize the HubConnection
with a delegate that will update the messages list, and do an initial update of the messages list with the currently available DLRs. We will also need to add a Dispose
method to dispose of the HubConnection
when the component finalizes. Put all of this in the code block, which should look like:
@code {
private HubConnection _hubConnection;
private List<DeliveryReceipt> _messages = new List<DeliveryReceipt>();
protected override async Task OnInitializedAsync()
{
_hubConnection = new HubConnectionBuilder()
.WithUrl(NavigationManager.ToAbsoluteUri("/SmsHub"))
.Build();
_hubConnection.On<DeliveryReceipt>("ReceiveDlr", (dlr) =>
{
_messages.Add(dlr);
StateHasChanged();
});
await _hubConnection.StartAsync();
var response = await Http.GetAsync("/sms/getDlr");
var json = await response.Content.ReadAsStringAsync();
_messages.AddRange(Newtonsoft.Json.JsonConvert.DeserializeObject<List<DeliveryReceipt>>(json));
}
public bool IsConnected => _hubConnection.State == HubConnectionState.Connected;
public void Dispose()
{
_ = _hubConnection.DisposeAsync();
}
}
Next, we’ll build the InboundSms
Component - we’re going to start the same way we started the DeliveredMessages
Component, create a razor component called InboundSms
and add the dependencies to it.
@using Microsoft.AspNetCore.SignalR.Client
@using VonageSmsDashboard.Shared
@inject NavigationManager NavigationManager
@inject HttpClient Http
@implements IDisposable
Then we’re going to add a very similar looking table to it, which will display the data from our webhook.
<div class="x-display-table">
<h2 >Received MessagesMessages</h2>
<table class="table" id="messageList">
<thead>
<tr>
<th>From</th>
<th>To</th>
<th>Time</th>
<th>Message Id</th>
<th>Message</th>
</tr>
</thead>
<tbody>
@for (var i = _messages.Count - 1; i >= 0; i--)
{
var message = _messages[i];
<tr>
<td>@message.Msisdn</td>
<td>@message.To</td>
<td>@message.MessageTimestamp</td>
<td>@message.MessageId</td>
<td style="word-wrap: break-word;">@message.Text</td>
</tr>
}
</tbody>
</table>
</div>
Finally, we’ll update our code block to fetch the inbound messages when the component initializes and setup the hub connection. When the component finalizes, it will dispose of the hub connection.
@code {
private HubConnection _hubConnection;
private List<InboundSmsModel> _messages = new List<InboundSmsModel>();
protected override async Task OnInitializedAsync()
{
_hubConnection = new HubConnectionBuilder()
.WithUrl(NavigationManager.ToAbsoluteUri("/SmsHub"))
.Build();
_hubConnection.On<InboundSmsModel>("ReceiveMessage", (sms) =>
{
_messages.Add(sms);
StateHasChanged();
});
await _hubConnection.StartAsync();
_messages = await Http.GetFromJsonAsync<List<InboundSmsModel>>("/sms/getinboundsms");
}
public bool IsConnected => _hubConnection.State == HubConnectionState.Connected;
public void Dispose()
{
_ = _hubConnection.DisposeAsync();
}
}
Now we need to build the frontend for sending SMS messages. We will make a separate component that we will call MessageSender
, go ahead and create a component with that name, inject a HttpClient and include the shared project.
@inject HttpClient Http
@using System.Text.Json
@using VonageSmsDashboard.Shared
Next, we will bind an OutboundSms
object to three input fields, a to
, from
, and text
field. Also, we’ll display our last sent message id exists. We will show it.
To:
<input id="to" @bind="@Message.To" placeholder="To Number" class="input-group-text" />
From:
<input id="from" @bind="@Message.From" placeholder="From Number" class="input-group-text" />
Text:
<input id="text" @bind="@Message.Text" placeholder="Text" class="input-group-text" />
<br />
<button class="btn btn-primary" @onclick="SendSms">Send SMS</button>
@if (LastMessageId != null)
{
<br />
<h2>Most Recently Sent Message: @LastMessageId</h2>
}
Finally, we must send the message. We will post a request to our controller, and pull the Message Id out of the response and store it in LastMessageId
.
@code {
OutboundSms Message { get; set; } = new OutboundSms();
string LastMessageId { get; set; }
private async Task SendSms()
{
var response = await Http.PostAsJsonAsync<OutboundSms>("/sms/sendsms", Message);
var json = await response.Content.ReadAsStringAsync();
LastMessageId = JsonSerializer.Deserialize<OutboundSms>(json).MessageId;
}
}
Now let’s lay out the frontend. Luckily we’ve split everything into components, so we just need to include the VonageSmsDashboard.Client.Components
in the index.razor
file and add the three new components into a div together.
@page "/"
@using VonageSmsDashboard.Client.Components
<div style="height: 100%">
<DeliveredMessages></DeliveredMessages>
<InboundSms></InboundSms>
<MessageSender></MessageSender>
</div>
Let’s add just a bit of style to the wwwroot\css\app.css
file so that this renders a bit nicer:
.x-display-table {
width: 90%;
max-height: 400px;
overflow-y: auto;
}
The last thing we need to do before we test is to configure our webhooks. We must point the webhooks at the endpoints we want to receive our inbound messages and DLRs on. Earlier, we ran a ngrok command which loaded ngrok into http://fb09abd3c106.ngrok.io
- the random string before ngrok.io
will be different. The path we need to point to for our inbound SMS messages is http://fb09abd3c106.ngrok.io/webhooks/inbound-sms
and http://fb09abd3c106.ngrok.io/webhooks/dlr
for DLR messages. We now need to set those URLs and set the HTTP Method to POST-JSON
in the dashboard
The last thing we need to do before we test is to add our API_KEY
, and API_SECRET
to the configuration. Open up VonageSmsDashboard.Server\appsettings.json
and add your API key and API secret to it, your config will look something like:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"API_KEY": "API_KEY",
"API_SECRET": "API_SECRET"
}
We also assumed when we set up ngrok that we would be listening on port 5000, you can set this by opening up the properties\launchsettings.json
file and changing the applicationUrl
to http://localhost:5000
for IIS express, if you are using IIS express, and change the SSL port to 0. Or, if you are using Kestrel, you can drop the https://localhost:5001
endpoint.
You’re all ready to test now. Go ahead and fire up the app in IIS Express or Kestrel, and you’re off to the races. You will see a dashboard that looks something like this:
It’s amazing what the combination of Blazor and the Vonage APIs can enable us to do with some HTML and a touch of C# code. To review, we’ve built a feature-rich SPA app with ZERO JavaScript in minutes.