Sending oneself Good Morning Emails using Azure Communication Service, Azure Functions, and Rider IDE

Sending oneself Good Morning Emails using Azure Communication Service, Azure Functions, and Rider IDE

This blog post introduces a way to send regular emails to a single recipient. The solution uses Azure Communication Services to send emails. It uses Azure Functions to automate email delivery on a daily schedule. It uses Rider IDE for code development and deployment.

Setting up Azure Resources

This section details the process of setting up Azure resources via the Azure Portal. It assumes an Azure account and a subscription are already available.

Communication Resources

Communication Service is an Azure resource that manages all kinds of communication channels, including voice and video calling, SMS, and email. Email is configured via a separate resource called Email Communication Service. An application connects to a Communication Service resource, which in turn interacts with an Email Communication Service resource to facilitate email sending. Both services are global. This means that their resources are not tied to any particular region.

Here is the five-step process to set it up:

  • Step 1: Create a resource group. One way to name it is rg-communication where rg specifies the resource type (resource group) and communication specifies its purpose.
  • Step 2: Create an Email Communication Service resource within the resource group. Example name is communication-services-email-xxxx where communication-services-email specifies the resource type, and xxxx is a random suffix to ensure a unique name.
  • Step 3: Create a Communication Service resource. Example name is communication-service-xxxx.
  • Step 4: Enable a domain in the Email Communication Service resource. An email domain is part of an email address that comes after the @ symbol. Azure-managed domain is structured as xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.azurecomm.net where xs represent randomly generated characters.
  • Step 5: Connect the domain to the Communication Service resource.

Once everything is configured, the resource group contains the following resources:
rg-communication-contents.png

Azure Communication Service provides a Try Email feature. It can be used to send a test email manually to ensure that everything is configured correctly before automating it.

Azure Functions

Azure Functions is a serverless compute service. When instantiated, the resources are referred to as Function Apps. Each Function App contains zero or more functions. Each function has a trigger condition and an execution logic.

This project uses a single Function App with a single function. The function's trigger is a CRON expression. The execution logic is a piece of C# code.

A Function App must belong to a resource group. Here, the resource group is named rg-morningwisher. It follows the same naming convention as before (rg for resource type and morningwisher for application purpose). This name also matches the GitHub repository name: morning-wisher.

Azure Functions offers various hosting plans. These are Consumption, Premium, and Dedicated plans[1]. The Consumption plan is the only plan on which an app can scale down to zero instances and thus incur no compute costs. This makes it suitable for scenarios with infrequent function executions such as is the case here.

A Function App requires a globally unique name. One way to approach naming it is to combine its purpose with a random suffix, such as morningwisher-xxxx. As of May 2024, the latest version of .NET runtime stack is "8 (LTS), isolated worker model". The choice between Linux and Windows operating systems is irrelevant in this case.

When creating Function App via Azure Portal, Azure automatically provisions dependent resources: App Service Plan and Storage. App Service Plan defines the compute specifics such as hosting plan and operating system. For example, "Pricing Plan: Y1" signifies Consumption plan. The storage account stores the app's code and settings.

In the end, the resource group contains the following resources:
morning-wisher-resource-group-contents.png

Pricing

The solution has associated costs to consider. As of May 2024:

  • Communication resources are free.
  • Email pricing is 0.00025 USD per email + 0.00012 USD per megabyte of data.
  • Azure Functions consumption plan includes 1 million free executions per month.
  • Azure Functions also incur storage account costs.

Setting up the project

The project is available on GitHub at https://github.com/yamesant/morning-wisher/tree/checkpoints/checkpoint-0

This section provides a commit-by-commit breakdown of how the project has been put together.

Step 0: Install Azure Toolkit for Rider

The Azure Toolkit for Rider plugin simplifies development of Function Apps. It provides debugging and deployment capabilities, and project templates.

The plugin can be verified to be installed and enabled by navigating to Rider's settings. A quick way to access Rider's settings is to press a shift key twice and search for "plugins".

It should look similar to this:
azure-toolkit-for-rider.png

Step 1: Initialise the Project

The template code is committed as is.

Step 2: Configure Azurite Emulator

Local development of an Azure App with time-triggered functions requires a storage emulator to simulate Azure Storage behaviour. One example of such emulator is Azurite. One way to enable it is to run it in a Docker container. The command is [2]

docker run -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite

Once a storage emulator is running, azure functions can be started for debugging:
debuggable-azure-function.png

The option RunOnStartup=true is required when debugging time-triggered functions locally. Without this setting, the debugger has to wait until 10pm UTC for the scheduled trigger to fire.

Step 3: Configure Email Communication Settings

The email communication configuration includes the sender email address, recipient email address, and the connection string to the Communication Service resource. These are all considered secrets and should not be publicly exposed.

The sender email address and connection string can be found via the Azure portal in the Try Email section of the Communication Service resource.

There are five parts making up the project configuration setup.

The first is the local.settings.json file. The file is excluded from git so the settings never leave local machine. The file is structured this way:

{  
    "IsEncrypted": false,  
    "Values": {  
        "AzureWebJobsStorage": "UseDevelopmentStorage=true",  
        "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated"  
    },  
    "EmailCommunicationConfig":{  
        "SenderAddress":"sender",  
        "RecipientAddress":"recipient",  
        "ConnectionString":"connectionString"  
    }  
}

The second is the EmailCommunicationConfig class that represents the configuration data within C#. The class and property names must match the names in the local.settings.json file. The properties must have public setters.

The third is the configuration validator EmailCommunicationConfigValidator class. In this case it checks that all configuration values are present (not empty). This approach causes the application to crash during startup if invalid configuration is detected, rather than during function execution.

The fourth is the configuration in the Program.cs file. The code in Program.cs first adds the local.settings.json file as a configuration source. This is necessary because the ConfigureFunctionsWorkerDefaults extension method while adds environment variables and command line arguments as configuration sources, it does not include local.settings.json file (as seen from the screenshot below). Secondly, it registers EmailCommunicationConfig and EmailCommunicationConfigValidator for dependency injection.

configure-functions-worker-defaults-documentation.png

The fifth is the dependency injection of IOptions<EmailCommunicationConfig> into the SendGoodMorningWishesEmail class to access the configuration values.

Once everything is set up, the function can access the configuration values:
access-config-from-azure-function.png

Step 4: Emailing Logic

The C# SDK for Azure Communication Services for Email is available through the Azure.Communication.Email NuGet package.

The function instantiates an email client and uses it to send an email. The function ignores logging and error handling because if something goes wrong it's no big deal if an email fails to get sent.

The email body gets filled with auto-generated text. Here's an example LLM prompt that can be used to generate such content:

Generate a 4 line poem. Give 5 examples. Output in a numbered table. Simple words. Rhyme. Morning. Motivation. Persistence. Positive.

Step 5: Pre-deployment

This section describes the final few steps before the app could be deployed.

The RunOnStartup parameter should be set to false in production to avoid sending emails at unpredictable times due to app restarts or scaling. One way to achieve it is by using a C# preprocessor directive[3]. This approach works because the default value of RunOnStartup is false, and it is set to true only in debug mode.

        #if DEBUG
        ,RunOnStartup = true
        #endif

The second requirement is to prepare the email communication configuration. Locally, it's stored in the local.settings.json file. In Azure, one among many ways to configure it is to use application settings[4]. These settings can be accessed via Environment Variables section under Settings on Function App page in Azure portal.

email-communication-config-as-app-settings-in-azure.png

The final requirement is to enable FTP access to the Function App. It can be enabled by ticking "on" for "FTP Basic Auth Publishing Credentials" and "SCM Basic Auth Publishing Credentials" and saving the changes in General settings in Configuration in Settings on Function App page. FTP stands for File Transfer Protocol. It is a method to transfer files from one computer to another over a network. In this case, Azure Toolkit for Rider uses FTP for deployment - i.e. moving application code from the local development machine to the Azure machine that runs the Function App.

Step 6: Deployment

The Azure Toolkit for Rider plugin enables developers to deploy the application by right clicking on the project (i.e. MorningWisher.Functions) and selecting Publish and choosing to Azure.

Conclusion

The result is the following email coming in daily to the recipient's inbox:
result-email.png

Revision questions:

Which six Azure resources does this solution provision?1) Email Communication Service 2) Email Communication Services Domain 3) Communication Service 4) Function App 5) Storage account 6) App Service plan
What naming convention is used for Azure resources in this solution?Resource type + resource purpose + optional random suffix
What three hosting plans does Azure Functions provide?1) Consumption 2) Premium 3) Dedicated
What is the name of the Rider extension that helps working with Azure Functions?Azure Toolkit for Rider
Why is the local.settings.json filename included in .gitignore?It contains secrets, and secrets should not be available in the source code .
Name the five parts making up the project configuration setup.1) local.settings.json 2) config class 3) config validator class 4) program.cs configuration 5) dependency injection
What is the name of the NuGet package that provides C# client library for Azure Communication Services Email?Azure.Communication.Email
Why "RunOnStartup" should be set to false in production?To avoid function triggers whenever the app restarts or scales.
When Rider fails to deploy the code and responds with code 401 and message Unauthorized, what could be a possible reason for the failure?FTP basic authentication has not been enabled.

  1. Azure Functions hosting plans: https://learn.microsoft.com/en-us/azure/azure-functions/functions-scale ↩︎

  2. Docker Storage Emulator: https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azurite?tabs=docker-hub%2Cblob-storage ↩︎

  3. C# preprocessor directive: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/preprocessor-directives ↩︎

  4. Azure Functions App Settings: https://learn.microsoft.com/en-us/azure/azure-functions/functions-app-settings ↩︎

Read more