Advent is a season of anticipation. Of course, there is a core spiritual anticipation many of us feel in the run-up to Christmas. There's also the anticipation of time off to relax and rejoice with our families in our traditions.
In the spirit of C# Advent, allow me to share how my family will maintain its traditions in these challenging times. This year I built a Blazor app to virtualize our annual Secret Santa game. It allows participants to register with their phone number and messages them with whom they are to send a gift.
The Problem
Every year we have a Secret Santa gift exchange. My wife reaches out to every member of our family to see who would like to participate. The number fluctuates based on how much engagement we get from the younger members of the family. Typically by the end of it, we have about fifteen participants.
Theresa then purchases cards for each participant, fills in the game rules. For example, spend no more than $xx. She then puts all the cards in envelopes, and on Thanksgiving (the fourth Thursday in November), we gather together to distribute the cards.
Each year we run into an issue, invariably a couple of participants aren't present at Thanksgiving dinner, our family is distributed evenly between New York, New Jersey, and Florida. This issue creates a practical problem as we can't randomly assign someone who isn't present a Secret Santa, as they could potentially get themselves. In the past, we've always been able to work around this by having one person, who isn't participating, selecting anyone who isn't present to ensure they are not receiving themselves. After that, my wife addresses all the envelopes to the appropriate people and sends them along.
This year, rather than one or two people being absent, we were missing a dozen, and with no one present to arbitrate the selection process to ensure no one got themselves, we needed to get creative.
Skip Straight to the Code
If you want to skip over this tutorial and want to run your own Secret Santa game, all the code is available in GitHub
Prerequisites
- You'll need the latest .NET SDK
- I use Visual Studio 2019 for this demo. You could use VS Code if you'd like
- I'm going to use Postgres for this demo. I'll assume for purposes of this demo that you have the server stood up. You can use whatever database you'd like, so long as you update the database context for it
- I deployed this to Azure - if you choose to do so, that's up to you, but to run this, you only need IIS Express or
Vonage API Account
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.
This tutorial also uses a virtual phone number. To purchase one, go to Numbers > Buy Numbers and search for one that meets your needs.

Login
Phone Number:
@if (_numberExists == false)
{
}
Register here
```
Next, we're going to add some logic to perform the login. We're going to start a verify request, which will send a code to the user if verification hasn't begun. Otherwise, it will route them to the confirmation page to enter their OTP.
@code {
private string _phoneNumber;
private bool? _numberExists;
private async Task StartLogin()
{
if (!_phoneNumber.StartsWith("1"))
{
_phoneNumber = "1" + _phoneNumber;
}
var user = Database.Participants.Where(x => x.PhoneNumber == _phoneNumber).FirstOrDefault();
if (user != null)
{
try
{
var verifyId = await SecretSantaService.StartVeriy(_phoneNumber);
user.RequestId = verifyId;
await Database.SaveChangesAsync();
NavigationManager.NavigateTo($"/confirmCode/{verifyId}");
}
catch (VonageVerifyResponseException ex)
{
if (ex.Response.Status == "10")
{
NavigationManager.NavigateTo($"/confirmCode/{user.RequestId}");
}
}
}
else
{
_numberExists = false;
}
}
}
Add Registration Page
We want to allow new users to register their phone number with the app. To do this, create a new RegistrationPage.razor
file, and add the following form to it:
@inject SecretSantaContext dbContext;
@inject SecretSantaService SecretSantaService
@inject NavigationManager NavigationManager
@inject Microsoft.Extensions.Configuration.IConfiguration Config
@using Vonage.Verify
@page "/registration"
<h3>Register</h3>
<div class="row">
<div class="col-md-4">
<EditForm Model="@participant" OnValidSubmit="CreateUser">
<DataAnnotationsValidator />
<ValidationSummary />
Your Phone Number*:<br /><InputText id="phone" @bind-Value="participant.PhoneNumber" /><br />
Your Name*:<br /><InputText id="Name" @bind-Value="participant.Name" /><br />
Your Mailing Address*:<br /><InputText id="Address" @bind-Value="participant.Address" /><br />
Gift ideas (For You!):<br /><InputText id="GiftIdeas" @bind-Value="participant.GiftIdeas" /><br />
<button type="submit">Register</button>
<p>*Required fields</p>
</EditForm>
</div>
</div>
@if (_userExistsAlready == true)
{
<p>User exists already try <a href="/">logging in</a></p>
}
@if (!string.IsNullOrEmpty(_error))
{
<p><b>Error Encountered:</b> @_error</p>
}
Next, we'll add a create user method to check the database to see if the proposed user already exists. If it doesn't, it will insert the user and start a verify event. Otherwise, it will tell you that the creation failed because the user already exists. Also, I have an admin phone number that I'm going to configure the app for; that phone number will decide who the game's admin is. When they are registered, their role is admin
.
@code {
private SecretSantaParticipant participant = new SecretSantaParticipant();
private bool? _userExistsAlready;
private string _error = "";
private async Task CreateUser()
{
participant.PhoneNumber = participant.PhoneNumber.Replace("(", "");
participant.PhoneNumber = participant.PhoneNumber.Replace(")", "");
participant.PhoneNumber = participant.PhoneNumber.Replace("-", "");
if (!participant.PhoneNumber.StartsWith("1"))
{
participant.PhoneNumber = "1" + participant.PhoneNumber;
}
var userExists = dbContext.Participants.Where(x => x.PhoneNumber == participant.PhoneNumber).Any();
if (!userExists)
{
try
{
if (participant.PhoneNumber == Config["ADMIN_NUMBER"])
{
participant.Role = "admin";
}
else
{
participant.Role = "user";
}
var verifyRequestId = await SecretSantaService.StartVeriy(participant.PhoneNumber);
participant.RequestId = verifyRequestId;
dbContext.Participants.Add(participant);
dbContext.SaveChanges();
NavigationManager.NavigateTo($"/confirmCode/{verifyRequestId}");
}
catch (VonageVerifyResponseException ex)
{
_error = ex.Response.ErrorText;
}
}
else
{
_userExistsAlready = true;
}
}
}
Finalize Login
After someone creates an account or tries to log in, we need to forward them to a page where they'll validate that they have the correct code. You'll notice that after sending out the Verify request, the NavigationManager
routes the user to a new URL: NavigationManager.NavigateTo($"/confirmCode/{verifyRequestId}");
- this act will open up a new page. Create a new Razor component called CodeConfirmationPage.razor
. This page will take a parameter through the path - the verification ID, which will eventually attempt the code confirmation. This page will have a simple input box and button to have us try the confirmation. If the Authentication succeeds, we will navigate back to the root page, which will now be able to route to the remainder of the app.
@using Microsoft.AspNetCore.Identity
@inject SecretSantaService VonageService
@inject SecretSantaContext db
@inject NavigationManager NavigationManager
@inject AuthenticationStateProvider Provider
@page "/confirmCode/{VerifyRequestId}"
<h3>Confirm Code!</h3>
<input class="input-group-text" @bind="_code" />
<button class="btn-group-lg" @onclick="CheckCode">Confirm</button>
@if (_authenticated == false)
{
<p>Authentication Successful</p>
}
@code {
[Parameter]
public string VerifyRequestId { get; set; }
private string _code;
private bool? _authenticated;
private async Task CheckCode()
{
_authenticated = await VonageService.ConfirmCode(VerifyRequestId, _code);
if (_authenticated == true)
{
var user = db.Participants.Where(x => x.RequestId == VerifyRequestId).FirstOrDefault();
((AuthProvider)Provider).AuthorizeUser(user);
NavigationManager.NavigateTo("/");
}
}
}
Account Page
The last page we need to add is the actual account page. Now this will actually have a difference in access level based on whether the user is an admin or not. When the actual game is set to start the Admin will get to validate that everyone is in correctly, and will have the ability to send the message out to everyone that the game has started.
In the display portion of this page we'll show everyone their name and account info, as well as who they are matched with (assuming a match isn't still pending.) We will also leave the address and gift idea fields editable so that if someone chooses to update them, we can support that. Create a razor file called AccountComponent.razor
and add the following:
@inject SecretSantaContext Db
@inject AuthenticationStateProvider Provider
@inject NavigationManager NavigationManager
@inject SecretSantaService SecretSantaService
@using System.Security.Claims
<h3>Accounts</h3>
<table class="table-bordered">
<tr>
<th>Your Name</th>
<td>@participant.Name</td>
</tr>
<tr>
<th>Your Address</th>
<td><input class="input-group" @bind="participant.Address" /></td>
</tr>
<tr>
<th>Gift Ideas For You</th>
<td><input class="input-group" @bind="participant.GiftIdeas" /></td>
</tr>
@if (participant.Match != null)
{
<tr>
<th>Your Secret Santa</th>
<td>@participant.Match.Name</td>
</tr>
<tr>
<th>Gift Ideas</th>
<td>@participant.Match.GiftIdeas</td>
</tr>
<tr>
<th>Your Secret Santa's Address</th>
<td>@participant.Match.Address</td>
</tr>
}
else
{
<tr>
<th>Status</th>
<td>Pending match</td>
</tr>
}
</table>
@if (_isAdmin)
{
<h3>Admin Section</h3>
<table class="table-bordered">
<thead>
<tr>
<th>Name</th>
<th>Phone Number</th>
<th>Address</th>
<th>Gift ideas</th>
<th>Matched?</th>
</tr>
</thead>
@foreach (var par in _participants)
{
<tr>
<td>@par.Name</td>
<td>@par.PhoneNumber</td>
<td>@par.Address</td>
<td>@par.GiftIdeas</td>
<td>@(par.Match!=null)</td>
</tr>
}
</table>
<button @onclick="Shuffle" class="btn-group-lg">Shuffle And Send</button>
<button @onclick="SecretSantaService.NotifyUsers">Notify Participants</button>
<br />
}
<p>@msg</p>
<button style="cursor:pointer" @onclick="UpdateAccount">Update Account</button>
<a @onclick="Logout" href="">Log Out</a>
Now in for the code bit of this, we'll have a method to initialize the page for us. Suppose our authentication state's identity is an admin. In that case, we'll also query the database to get the list of current participants in the game and display them in a separate table. We'll have a button to shuffle the users and assign them a Secret Santa, and then we'll have the admin's button to start the contest, which will have Santa send the greeting out to everyone.
@code {
SecretSantaParticipant participant = new SecretSantaParticipant();
List<SecretSantaParticipant> _participants = new List<SecretSantaParticipant>();
string msg;
bool _isAdmin;
protected override async Task OnInitializedAsync()
{
var user = (await ((AuthProvider)Provider).GetAuthenticationStateAsync()).User.Claims.First(x => x.Type == ClaimTypes.Name).Value;
participant = Db.Participants.ToList().FirstOrDefault(x => x.PhoneNumber == user);
_isAdmin = participant.Role == "admin";
if (_isAdmin)
{
_participants = Db.Participants.ToList();
}
StateHasChanged();
}
private async void Shuffle()
{
await SecretSantaService.ShuffleUsers();
}
private void Logout()
{
((AuthProvider)Provider).LogOutUser();
}
private async void UpdateAccount()
{
var par = Db.Participants.FirstOrDefault(x => x.PhoneNumber == participant.PhoneNumber);
par = participant;
await Db.SaveChangesAsync();
msg = "Account Updated";
try
{
await SecretSantaService.NotifyUserOfUpdate(par);
}
catch (Exception ex) { msg = ex.Message; }
StateHasChanged();
}
}
Set up the Routes in the Index
The last code related thing we need to do to get the app setup is to replace the contents of the Index.razor
file with an AuthorizeView
, which will show the account page if the Auth sate is authorized, and the login page otherwise. In the index.razor file add the following:
@page "/"
<AuthorizeView>
<Authorized>
<AccountComponent></AccountComponent>
</Authorized>
<NotAuthorized>
<Login></Login>
</NotAuthorized>
</AuthorizeView>
Configuration
Now we've written the app, we need to set up the database and add the appropriate environment variables to the app.
appsettings.json
Open your appsettings.json
file and add the following keys to it:
"CONNECTION_STRING": "Host=localhost;Database=secretsantausers;User Id=username;Password=password;Port=5432",
"API_KEY": "API_KEY",
"API_SECRET": "API_SECRET",
"VONAGE_NUMBER": "VONAGE_NUMBER"
Set the API_KEY
and API_SECRET
with your API key and secret from your Vonage Dashboard. Set the VONAGE_NUMBER
to one of your Vonage virtual numbers. Set the admin number to the cellphone number of whoever you want to be the admin of the game (presumably yourself). Finally, set the CONNECTION_STRING
to whatever your database's connection string.
Migrate the Database
Finally, we'll need to migrate the database. To do this, you're going to need the entity framework tool:
dotnet tool install --global dotnet-ef
Then run the fluent tool to create the migration:
dotnet ef migrations add initial_create
Finally, run the update tool from fluent to apply all the appropriate migrations:
dotnet ef database update
Conclusion
And that's it! Now you can run the app by either hitting the play button in IIS Express or with the dotnet run
command from your terminal. Regardless you're all set!
Other Resources
- A comprehensive overview of the Verify API is available on our documentation website.
- You can see further uses of our SMS API here
- All the source code from this demo can be found in GitHub