Recently, I have been working on OpenEvents – an open source event registration system.
Because I am going to have several talks about microservices architecture in upcoming months, I have chosen different approach to build this app so it can work as a demo.
I have separated the event management part and the registration part in two services and used Azure Service Bus to do messaging between these two parts.
Which package?
There are two Nuget packages which you can use:
- WindowsAzure.ServiceBus is the older one. It only support full .NET Framework.
- Microsoft.Azure.ServiceBus is the new one. It targets .NET Standard and it was the only option for me as my project is built with .NET Core 2.0.
Both packages contain the queue or topic client classes and all API you need to send, receive and work with the messages.
The old package also contained the NamespaceManager class which could be used to manage topics, subscriptions and queues inside the Service Bus namespace.
It was very useful because the application could provision these resources at startup and didn’t require to prepare all the queues and topics manually. Especially if you had multiple environments (dev, test, staging, production), it is easier to let the app create what is needs than to maintain four environments.
Resource Manager
The new package doesn’t include any API to manage these resources. The only way to create topics, subscriptions and queues programmatically is to use Azure Resource Manager.
Azure Resource Manager exposes a REST API and there is a Nuget package with API client for almost every Azure service. The package name always starts with Microsoft.Azure.Management
, so we need to use Microsoft.Azure.Management.ServiceBus
.
Because consuming the raw REST API is not so convenient, some Azure services also offer a package with Fluent
suffix. These packages contain wrappers which make using the API more convenient.
In my app, I have used Microsoft.Azure.Management.ServiceBus.Fluent Nuget package.
First, I need to authenticate and create the ServiceBusManager
object so we can work with the resources inside the Service Bus namespace.
var credentials = SdkContext.AzureCredentialsFactory.FromServicePrincipal(CLIENT_ID, CLIENT_SECRET, TENANT_ID, AzureEnvironment.AzureGlobalCloud);
var serviceBusManager = ServiceBusManager.Authenticate(credentials, SUBSCRIPTION_ID);
serviceBusNamespace = serviceBusManager.Namespaces.GetByResourceGroup(RESOURCE_GROUP, RESOURCE_NAME);
Creating the topic is then quite easy:
public async Task<ITopic> EnsureTopicExists(string topicName)
{
var topics = await serviceBusNamespace.Topics.ListAsync();
var topic = topics.FirstOrDefault(t => t.Name == topicName);
if (topic == null)
{
topic = await serviceBusNamespace.Topics.Define(topicName)
.WithSizeInMB(1)
... // other configuration
.CreateAsync();
}
return topic;
}
You can use the same approach for queues, subscriptions and other resources.
Authentication
The last thing you need to handle is the authentication for the Resource Manager API. To do that, I have created a service principal and given it a permissions to manage the Service Bus namespace.
1. Install the Azure PowerShell.
2. Login to the Azure account:
Login-AzureRmAccount
3. If you have multiple subscriptions registered in your account, select the correct one. I am using the name of the subscription to identify it:
Select-AzureRmSubscription -SubscriptionName "SUBSCRIPTION_NAME"
Get-AzureRmSubscription -SubscriptionName "SUBSCRIPTION_NAME"
4. Note the TenantId and Id of the subscription from the output of the previous step – you will need it later.
5. Now, create the service principal. Make sure the password is strong enough and the name describes what is the purpose of the principal. The password of the principal will be the ClientSecret – do not lose it as you will need it later.
$p = ConvertTo-SecureString -asplaintext -string "PRINCIPAL_PASSWORD" -force
$sp = New-AzureRmADServicePrincipal -DisplayName "PRINCIPAL_NAME" -Password $p
6. Retrieve the ApplicationId of the principal. It is often called ClientId – you will need it later.
$sp.ApplicationId
7. Assign the Contributor role to the service principal for the Service Bus resource. Use the name of the resource group and exact name of the Service Bus.
New-AzureRmRoleAssignment -ServicePrincipalName $sp.ApplicationId -ResourceGroupName RESOURCE_GROUP -ResourceName RESOURCE_NAME -RoleDefinitionName Contributor -ResourceType Microsoft.ServiceBus/namespaces
No we should have all the information we need to make our code work.
Configuration
Keeping these information in the code is not a good idea. I am using the Microsoft.Extensions.Configuration
to store the Service Bus configuration and secrets. I have created a class that represents my configuration:
public class ServiceBusConfiguration
{
public string ConnectionString { get; set; }
public string ResourceGroup { get; set; }
public string NamespaceName { get; set; }
public string ClientId { get; set; }
public string ClientSecret { get; set; }
public string SubscriptionId { get; set; }
public string TenantId { get; set; }
}
I have added the following section in the appconfig.json
file in my application:
{
...
"serviceBus": {
"resourceGroup": "RESOURCE_GROUP",
"namespaceName": "RESOURCE_NAME",
"connectionString": "",
"clientId": "",
"clientSecret": "",
"subscriptionId": "",
"tenantId": ""
},
...
}
To get the configuration object for Service Bus, I can use the following code:
var config = Configuration.GetSection("serviceBus").Get<ServiceBusConfiguration>();
Working with User Secrets
Since I don’t want to keep the secrets in the source control, I am using User Secrets.
I have added the following section at the top of my .csproj
file…
<PropertyGroup>
<UserSecretsId>openevents</UserSecretsId>
</PropertyGroup>
…and the following section at the bottom:
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.Extensions.SecretManager.Tools" Version="2.0.0" />
</ItemGroup>
Now I can use command line to store the secret configuration values outside of my project folder.
dotnet user-secrets set serviceBus:connectionString "SERVICE_BUS_CONNECTION_STRING"
dotnet user-secrets set serviceBus:subscriptionId SUBSCRIPTION_ID
dotnet user-secrets set serviceBus:tenantId TENANT_ID
dotnet user-secrets set serviceBus:clientId CLIENT_ID
dotnet user-secrets set serviceBus:clientSecret CLIENT_SECRET
Remember that CLIENT_ID is the ApplicationId value you have printed out in step 6. The CLIENT_SECRET is the password of the service principal.
The last thing you need to do is to add user secrets in the configuration builder so the secrets will override the values from the appsettings.json
file.
var builder = new ConfigurationBuilder()
.SetBasePath(hostingEnvironment.ContentRootPath)
.AddJsonFile("appsettings.json")
.AddJsonFile($"appsettings.{hostingEnvironment.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
if (hostingEnvironment.IsDevelopment())
{
builder.AddUserSecrets<Startup>();
}
Configuration = builder.Build();
By default, they are stored in the following location: c:\Users\USER_NAME\AppData\Roaming\Microsoft\UserSecrets
Remember that user secrets are designed for development purposes, they should not be used in production environment.