
Refit is a great little library for simplifying consumption of REST Web APIs, that even allows authentication via support for HTTP authorization header injection in Refit’s settings during set up.
Content relating to general C# development using the .NET / .NET Framework.
Refit is a great little library for simplifying consumption of REST Web APIs, that even allows authentication via support for HTTP authorization header injection in Refit’s settings during set up.
Blazor is a .NET-based Component Architecture platform that is offered as a potential alternative to other modern frameworks such as Angular or ReactJS for responsive web development.
If you are familiar with other Component Architecture frameworks and are also a .NET developer then moving to Blazor should be fairly straightforward. But, there are some caveats and design constraints you should consider in Blazor that you might not worry about in other frameworks.
Most modern cloud authentication providers (i.e. Microsoft Entra ID, Auth0, Okta, etc.) provide a claims-based user management capability. A user can be assigned roles and permissions and these are surfaced in .NET’s user identity classes as ‘claims’.
Although, most of the time, claims can be configured and managed in the identity provider’s management console there may be times when you want to inject additional claims, and this article discusses how that can be done.
With Microsoft iminently shuttering the capability to send emails programmatically via MS Exchange in Azure, developers need to switch to a mechanism that will be supported moving forwards. Enter Microsoft Graph. This article describes how the MS Graph API can be leveraged to replace MS Exchange based mailing for the programmatic sending of emails.
In this article:
Microsoft Graph is a REST API originally developed as the officially supported API for Office 365 (now Microsoft 365 (MS365)). Since then, it has been massively expanded to cater for SharePoint Online, MS365’s Power Platform, Azure, Azure Active Directory (Azure Entra ID), and other targets.
This diagram is from Microsoft’s developer center website to give you an idea how it is organised:
Microsoft distribute NuGet packages to support the development of MS Graph interactions in .NET so we don’t have to resort to the raw REST API calls most of the time.
In this walkthrough we’ll design the solution as a abstracted service contract and vendor specific service implementation, so that Microsoft Graph doesn’t have to be the only solution available (although we won’t develop any alternatives) to the consuming code – the specific mailing service instance can be dependency injected at runtime.
EmailService
.Entities
and Services
.Class1.cs
file to Credentials.cs
and accept the renaming of the class name when prompted. Move the file to the Entities folder, and accept the namespace changes when prompted.Attachment.cs
and Email.cs
.IEmailService.cs
.using System.Security;
namespace EmailService.Entities
{
/// <summary>
/// Specifies the credentials that the email service will use.
/// </summary>
public class Credentials
{
/// <summary>
/// Represents the type of authentication the email service expects.
/// </summary>
public enum AuthenticationType
{
/// <summary>
/// The email service doesn't require authentication.
/// <para>(This can be the case for some localised SMTP servers.)</para>
/// </summary>
None,
/// <summary>
/// The email service requires an Azure app registration based client ID and secret.
/// </summary>
AzureClientSecret,
/// <summary>
/// The email service requires simple username and password based authentication.
/// </summary>
UsernameAndPassword
}
/// <summary>
/// Creates a new instance of the class, specifying that authentication is not required.
/// </summary>
public Credentials()
{
AuthMode = AuthenticationType.None;
ClientIdOrUsername = "";
ClientSecretOrPassword = null;
TenantId = null;
Scopes = null;
Domain = null;
}
/// <summary>
/// Creates a new instance of the class, configuring the credentials to target an Azure App Registration.
/// </summary>
/// <param name="clientId">The client ID of the app registration.</param>
/// <param name="secret">The client secret associated with the app registration.</param>
/// <param name="tenantId">The ID of the tenant the app registration is specified on.</param>
/// <param name="scopes">The authentication scopes to request, or an empty collection or null to ignore.</param>
/// <param name="domain">The tenant's domain name, or an empty string or null to ignore.</param>
public Credentials(
string clientId,
SecureString secret,
string tenantId,
IList<string>? scopes = null,
string? domain = null)
{
AuthMode = AuthenticationType.AzureClientSecret;
ClientIdOrUsername = clientId;
ClientSecretOrPassword = secret;
TenantId = tenantId;
Scopes = scopes;
Domain = domain;
}
/// <summary>
/// Creates a new instance of the class, configuring the credentials to target a simple username and password based login.
/// </summary>
/// <param name="username">The account username to use.</param>
/// <param name="password">The account password to use.</param>
/// <param name="domain">The domain the account is associated with, if any, or an empty string or null to ignore.</param>
public Credentials(string username, SecureString password, string? domain = null)
{
AuthMode = AuthenticationType.UsernameAndPassword;
ClientIdOrUsername = username;
ClientSecretOrPassword = password;
TenantId = null;
Scopes = null;
Domain = domain;
}
public AuthenticationType AuthMode { get; }
public string ClientIdOrUsername { get; }
public SecureString? ClientSecretOrPassword { get; }
public string? TenantId { get; }
public IList<string>? Scopes { get; }
public string? Domain { get; }
}
}
Code language: JavaScript (javascript)
namespace EmailService.Entities;
/// <summary>
/// Specifies an attachment that can be added to an email message.
/// </summary>
public class Attachment
{
/// <summary>
/// Creates a new instance of the class.
/// </summary>
public Attachment()
{
Filename = "";
Data = [];
}
/// <summary>
/// Creates a new instance of the class, initialising the filename and raw data properties.
/// </summary>
/// <param name="name">The filename to set.</param>
/// <param name="data">The attachment's raw data.</param>
public Attachment(string name, byte[] data)
{
Filename = name;
Data = data;
}
public string Filename { get; set; }
public byte[] Data { get; set; }
}
Code language: JavaScript (javascript)
using System.Collections.Generic;
namespace EmailService.Entities;
/// <summary>
/// Describes an email message being sent.
/// </summary>
public class Email
{
/// <summary>
/// Gets or sets the main recipients of the message.
/// <para>At least one of <see cref="To"/>, <see cref="CarbonCopy"/>, or <see cref="BlindCopy"/> must contain a recipient.</para>
/// </summary>
public IList<string> To { get; set; } = new List<string>();
/// <summary>
/// Gets or sets the recipients, if any, that should be carbon-copied (CC'd) the message.
/// <para>At least one of <see cref="To"/>, <see cref="CarbonCopy"/>, or <see cref="BlindCopy"/> must contain a recipient.</para>
/// </summary>
public IList<string> CarbonCopy { get; set; } = new List<string>();
/// <summary>
/// Gets or sets the recipients, if any, that should be blind-copied (BCC'd) the message.
/// <para>At least one of <see cref="To"/>, <see cref="CarbonCopy"/>, or <see cref="BlindCopy"/> must contain a recipient.</para>
/// </summary>
public IList<string> BlindCopy { get; set; } = new List<string>();
/// <summary>
/// Gets or sets the email address from which the message is being sent.
/// <para>This may be used to identify the sender's mailbox by some email provider implementations.</para>
/// </summary>
public string From { get; set; } = "";
/// <summary>
/// Gets or sets the subject (title) of the email.
/// </summary>
public string Subject { get; set; } = "";
/// <summary>
/// Gets or sets the message body content being sent.
/// </summary>
public string Message { get; set; } = "";
/// <summary>
/// Gets or sets a value indicating if the message is HTML formatted.
/// <para>(If not HTML, the email will be sent as plain text.)</para>
/// </summary>
public bool IsHtml { get; set; } = false;
/// <summary>
/// Gets or sets a collection of zero or more attachments being included with the email.
/// </summary>
public IList<Attachment> Attachments { get; set; } = new List<Attachment>();
/// <summary>
/// Gets or sets a collection of zero or more recipients any replies should be directed to.
/// <para>If omitted, the sender will be set as the recipient of any replies.</para>
/// </summary>
public IList<string>? ReplyTo { get; set; }
}
Code language: HTML, XML (xml)
using EmailService.Entities;
namespace EmailService.Services;
/// <summary>
/// Defines the contract for a service that can be used for sending emails.
/// </summary>
public interface IEmailService
{
/// <summary>
/// Gets or sets the authentication credentials the email service requires.
/// <para>This must be set before calling <see cref="SendAsync(Email, bool)"/>.</para>
/// </summary>
Credentials Auth { get; set; }
/// <summary>
/// Sends a specified email message.
/// </summary>
/// <param name="message">The email message to send.</param>
/// <param name="saveInSentFolder">(Optional, default: false) if asserted, saves the email message to the Sent emails folder of the mailbox being used.</param>
Task SendAsync(Email message, bool saveInSentFolder = false);
}
Code language: HTML, XML (xml)
Now that the entities and service contract has been defined, let’s get on with developing the Microsoft Graph based mailing service implementation.
EmailService.MicrosoftGraph
.EmailService
project as a dependency. (Right click Dependencies in Solution Explorer, choose ‘Add Project Reference’ and then select the EmailService project.)Class1.cs
file to MicrosoftGraphEmailService.cs
and accept the class name change when prompted.IEmailService
, and implement the members.using System.Runtime.InteropServices;
using System.Security;
using Azure.Identity;
using EmailService.Entities;
using EmailService.Services;
using Microsoft.Graph;
using Microsoft.Graph.Models;
using Microsoft.Graph.Users.Item.SendMail;
namespace EmailService.MicrosoftGraph;
/// <summary>
/// Provides an implementation of <see cref="IEmailService"/> allowing
/// consuming code to send emails via the Microsoft Graph API.
/// </summary>
public class MicrosoftGraphEmailService : IEmailService
{
/// <inheritdoc />
public Credentials Auth { get; set; } = new();
/// <inheritdoc />
public async Task SendAsync(Email message, bool saveInSentFolder = false)
{
GraphServiceClient client = CreateClient();
Message email = ConvertEmailToGraphMessage(message);
try
{
await client.Users[message.From].SendMail.PostAsync(
new SendMailPostRequestBody
{
Message = email,
SaveToSentItems = saveInSentFolder
});
}
catch (AuthenticationFailedException authEx)
{
throw new Exception("Failed to authenticate against MS Graph API.", authEx);
}
catch (Exception ex)
{
throw new Exception("An error occurred while attempting to send an email via the MS Graph API.", ex);
}
}
private Message ConvertEmailToGraphMessage(Email email)
{
List<Recipient> to = new();
List<Recipient> carbonCopy = new();
List<Recipient> blindCopy = new();
List<Recipient> replyTo = new();
if (email.To.Count > 0)
{
to.AddRange(
email.To
.Where(x => !string.IsNullOrWhiteSpace(x))
.Select(address => new Recipient
{
EmailAddress = new EmailAddress { Address = address.Trim() }
}));
}
if (email.CarbonCopy.Count > 0)
{
carbonCopy.AddRange(
email.CarbonCopy
.Where(x => !string.IsNullOrWhiteSpace(x))
.Select(address => new Recipient
{
EmailAddress = new EmailAddress { Address = address.Trim() }
}));
}
if (email.BlindCopy.Count > 0)
{
blindCopy.AddRange(
email.BlindCopy
.Where(x => !string.IsNullOrWhiteSpace(x))
.Select(address => new Recipient
{
EmailAddress = new EmailAddress { Address = address.Trim() }
}));
}
if (email.ReplyTo != null)
{
replyTo.AddRange(
email.ReplyTo
.Where(x => !string.IsNullOrWhiteSpace(x))
.Select(address => new Recipient
{
EmailAddress = new EmailAddress { Address = address.Trim() }
}));
}
if (!to.Any() && !carbonCopy.Any() && !blindCopy.Any())
{
throw new ArgumentException("At least one email recipient must be specified.", nameof(email));
}
Message message = new()
{
ToRecipients = to,
CcRecipients = carbonCopy,
BccRecipients = blindCopy,
ReplyTo = replyTo,
Subject = email.Subject,
Body = new ItemBody
{
ContentType = email.IsHtml ? BodyType.Html : BodyType.Text,
Content = email.Message
}
};
if (email.Attachments.Count > 0)
{
List<Microsoft.Graph.Models.Attachment> attachments = new();
foreach (Attachment attachment in email.Attachments)
{
FileAttachment file = new()
{
Name = attachment.Filename,
ContentBytes = attachment.Data
};
attachments.Add(file);
}
message.Attachments = attachments;
}
return message;
}
private GraphServiceClient CreateClient()
{
ValidateAuth(Auth);
GraphServiceClient client;
try
{
ClientSecretCredential credentials = new(
Auth.TenantId,
Auth.ClientIdOrUsername,
MicrosoftGraphEmailService.FromSecureString(Auth.ClientSecretOrPassword)
);
client = new GraphServiceClient(credentials);
}
catch (ServiceException ex)
{
throw new Exception("An error occurred while initialising a new MS Graph client instance.", ex);
}
return client;
}
private void ValidateAuth(Credentials? credentials)
{
if (credentials == null) throw new ArgumentException("No credentials were supplied.", nameof(credentials));
if (credentials.AuthMode != Credentials.AuthenticationType.AzureClientSecret)
{
throw new InvalidOperationException(
"This email service implementation requires Azure authentication settings.");
}
if (string.IsNullOrWhiteSpace(credentials.ClientIdOrUsername))
{
throw new ArgumentException("A valid client ID must be supplied.", nameof(credentials));
}
if (string.IsNullOrWhiteSpace(credentials.TenantId))
{
throw new ArgumentException("A value tenant ID must be supplied.", nameof(credentials));
}
if (credentials.ClientSecretOrPassword == null || credentials.ClientSecretOrPassword.Length == 0)
{
throw new ArgumentException("A client secret must be supplied.", nameof(credentials));
}
}
/// <summary>
/// Returns a plain text representation of the supplied <see cref="SecureString"/> object.
/// </summary>
/// <param name="encrypted">The <see cref="SecureString"/> value to parse.</param>
private static string FromSecureString(SecureString? encrypted)
{
if (encrypted == null) return "";
IntPtr pointer = IntPtr.Zero;
try
{
pointer = Marshal.SecureStringToGlobalAllocUnicode(encrypted);
return Marshal.PtrToStringUni(pointer) ?? "";
}
finally
{
Marshal.ZeroFreeGlobalAllocUnicode(pointer);
}
}
}
Code language: HTML, XML (xml)
That’s it. You can now add the code projects / assemblies to your solutions and send emails using Microsoft Graph.
NOTE: this tutorial didn’t include details on how to configure an Azure App Registration with the correct permissions because that is fairly straightforward to do, Microsoft have articles on it, and it would be time consuming to write up here.
When working with layers of abstraction, especially in modern design structures like Clean Architecture, it can easily become complicated getting a simple database query from the UI to the physcial database provider being queried due to all the abstraction and indirection. An obvious way of abstracting an application from any single underlying data provider is by implementing the Repository pattern.
Loading (well, reloading) workflows that include Visual Basic expressions in a WCF hosted service is not as straightforward as one might expect. If you have created your XAML workflow based on an ActivityBuilder or a DynamicActivity then the Visual Basic settings aren’t configured automatically.
There may be occasions when you’ll want to save the XAML version of a workflow (root activity) while persisting it. In my case this is normally to ensure that when I persist a workflow and later want to reload it, I’ve got the exact workflow structure I started with. This article discusses how to do just that.
One of the clear advantages of using Visual Studio for editing workflows over a custom implementation is the readily available auto-completion features of the IDE. A similar style of auto-completion can be added to your re-hosted workflow designer by implementing your own ‘editor service’ and publishing it to the workflow designer control.
The basic re-hosted workflow designer solution described in re-hosting the workflow designer article provides a baseline for a bespoke application that can be used for designing workflows, but without some further enhancements it is quite limited. By default it will only allow you to create and edit workflows that use the Framework’s built-in set of activities.
The basic re-hosted workflow designer solution described in this article provides a baseline for a bespoke application that can be used for designing workflows, but without some further enhancements it is quite limited. A relatively straightforward addition to a re-hosted workflow designer is visual debugging.