Using Multiple Namespaces to Increase Active Devices with Azure Notification Hubs

Introduction

This is a follow up to my previous 3-post blog series on Learning Azure Notification Hubs. At the time of writing, on the free tier, there is a limit of only 500 active devices per namespace. The basic plan increases this to 200 000 devices for about $10 (USD) a month. While $10 is not much, when still starting out, or working on a side-project, every cent counts.

The pricing states that you can have up to 100 namespaces per tier (Free, Basic or Standard). This got me thinking if it was possible to create multiple namespaces for an application and then distribute device registrations amongst these namespaces. This would increase the maximum number of active devices you could support for free.

This approach can also be used if you are paying for the Basic or Standard tier but reach the device limits for namespaces in those tiers (200 000 and 10 000 00 respectively at the time of writing).

Create additional namespaces

To take advantage of this you will need to create multiple Notification Hub namespaces in Azure, together with a Hub for each namespace. I recommend using the original namespace name, and appending a number to it such as:

  • MyNamespaceName1
  • MyNamespaceName2
  • MyNamespaceName3
  • MyNamespaceName4
  • MyNamespaceName5
  • Etc…

You will also have to copy the Apple (APNS), Google (GCM/FCM) and Windows (WNS) settings across to the new namespaces.

Modify the code to work with multiple namespaces

This code is available from my GitHub account.

The original code before these modifications is available here. If you want to follow along, I suggest checking out this code first and then making the modifications shown below to it.

The code after these modifications is available here.

All these changes will be made within the API project. We now have multiple Notification Hubs instead of just one. Modify the appsettings.Development.json file so that it can provide the application with the access details for all the new Notification Hubs. We will also need two more properties for the number of namespaces we have and how many devices a namespace supports. This will allow us to easily adapt to changes in the limits by Microsoft, or if we create or remove the namespaces in the future.

"NotificationHubService": {
    "Namespaces": 10,
    "DevicesPerNamespace": 500,
    "Namespace1": {
      "NamespaceName": "<YOUR_NAMESPACE_NAME>",
      "HubName": "<YOUR_HUB_NAME>",
      "SasKeyName": "<YOUR_SAS_KEY_NAME>",
      "SasKey": "<YOUR_SAS_KEY>"
    },
    "Namespace2": {
      "NamespaceName": "<YOUR_NAMESPACE_NAME>",
      "HubName": "<YOUR_HUB_NAME>",
      "SasKeyName": "<YOUR_SAS_KEY_NAME>",
      "SasKey": "<YOUR_SAS_KEY>"
    },
    "Namespace3": {
      "NamespaceName": "<YOUR_NAMESPACE_NAME>",
      "HubName": "<YOUR_HUB_NAME>",
      "SasKeyName": "<YOUR_SAS_KEY_NAME>",
      "SasKey": "<YOUR_SAS_KEY>"
    },
    "Namespace4": {
      "NamespaceName": "<YOUR_NAMESPACE_NAME>",
      "HubName": "<YOUR_HUB_NAME>",
      "SasKeyName": "<YOUR_SAS_KEY_NAME>",
      "SasKey": "<YOUR_SAS_KEY>"
    },
    "Namespace5": {
      "NamespaceName": "<YOUR_NAMESPACE_NAME>",
      "HubName": "<YOUR_HUB_NAME>",
      "SasKeyName": "<YOUR_SAS_KEY_NAME>",
      "SasKey": "<YOUR_SAS_KEY>"
    },
    "Namespace6": {
      "NamespaceName": "<YOUR_NAMESPACE_NAME>",
      "HubName": "<YOUR_HUB_NAME>",
      "SasKeyName": "<YOUR_SAS_KEY_NAME>",
      "SasKey": "<YOUR_SAS_KEY>"
    },
    "Namespace7": {
      "NamespaceName": "<YOUR_NAMESPACE_NAME>",
      "HubName": "<YOUR_HUB_NAME>",
      "SasKeyName": "<YOUR_SAS_KEY_NAME>",
      "SasKey": "<YOUR_SAS_KEY>"
    },
    "Namespace8": {
      "NamespaceName": "<YOUR_NAMESPACE_NAME>",
      "HubName": "<YOUR_HUB_NAME>",
      "SasKeyName": "<YOUR_SAS_KEY_NAME>",
      "SasKey": "<YOUR_SAS_KEY>"
    },
    "Namespace9": {
      "NamespaceName": "<YOUR_NAMESPACE_NAME>",
      "HubName": "<YOUR_HUB_NAME>",
      "SasKeyName": "<YOUR_SAS_KEY_NAME>",
      "SasKey": "<YOUR_SAS_KEY>"
    },
    "Namespace10": {
      "NamespaceName": "<YOUR_NAMESPACE_NAME>",
      "HubName": "<YOUR_HUB_NAME>",
      "SasKeyName": "<YOUR_SAS_KEY_NAME>",
      "SasKey": "<YOUR_SAS_KEY>"
    }
  }

The INotificationHubService interface needs to change. Most of the methods, except for the SendNotification method will need to change to work with multiple notification hubs. The interface will now look like:

public interface INotificationHubService
{
    Task<(string registrationId, string notificationHubNamespaceName)> CreateRegistration(SignInUserDto signInUserDto);
    Task UpdateRegistration(SignInUserDto signInDeviceDto, string registrationId, string namespaceName);
    Task DeleteRegistration(string registrationId, string namespaceName);
    Task SendNotification(string message, string tag);
}

All the methods that work with Notification Hub registrations now require a namespaceName. The CreateRegistration method will now return a tuple that includes the registrationId and the Notification Hub namespace that was used.

Before we can implement these changes in NotificationHubService, first create a new class in the Services/Azure folder called NotificationHubNamespaceCredentials. This class will contain all the information about a Notification Hub namespace. Its constructor will also set up the class by reading the values stored in the configuration file.

public class NotificationHubNamespaceCredentials
{
    public string NamespaceName { get; }
    public string HubName { get; }
    public string SasKeyName { get; }
    public string SasKey { get; }

    public NotificationHubNamespaceCredentials(int count, IConfiguration configuration)
    {
        var notificationHubServiceConfiguration = configuration.GetSection("NotificationHubService");

        NamespaceName = notificationHubServiceConfiguration.GetValue<string>($"Namespace{count}:NamespaceName");
        HubName = notificationHubServiceConfiguration.GetValue<string>($"Namespace{count}:HubName");
        SasKeyName = notificationHubServiceConfiguration.GetValue<string>($"Namespace{count}:SasKeyName");
        SasKey = notificationHubServiceConfiguration.GetValue<string>($"Namespace{count}:SasKey");
    }
}

Next, you can start implementing the changes in the NotificationHubService class. The first thing is to change the fields. The class will now store how many devices a namespace can have as well as a list of the namespaces as private readonly fields, that will be populated in the constructor.

private readonly int _devicesPerNamespace;
private readonly List<NotificationHubNamespaceCredentials> _notificationHubNamespaces = new List<NotificationHubNamespaceCredentials>();

public NotificationHubService(IConfiguration configuration)
{
    var notificationHubServiceConfiguration = configuration.GetSection("NotificationHubService");

    var namespaceCount = notificationHubServiceConfiguration.GetValue<int>("Namespaces");
    _devicesPerNamespace = notificationHubServiceConfiguration.GetValue<int>("DevicesPerNamespace");

    for (var count = 1; count <= namespaceCount; count++)
    {
        _notificationHubNamespaces.Add(new NotificationHubNamespaceCredentials(count, configuration));
    }
}

When creating a registration, we need to be able to inspect how many devices are already within a namespace, to decide if we should switch to the next one. To obtain this information, you need to create a new private method called GetRegistrations that will obtain this information from the Notification Hub API. We also need to implement another class within the Services/Azure folder that maps a Notification Hub’s XML response for a registration to a C# class. Create a new file in the Services/Azure folder called NotificationHubRegistration.cs and add the following code within it:

// NOTE: Generated code may require at least .NET Framework 4.5 or .NET Core/Standard 2.0.
/// <remarks/>
[Serializable()]
[System.ComponentModel.DesignerCategory("code")]
[System.Xml.Serialization.XmlType(AnonymousType = true, Namespace = "http://www.w3.org/2005/Atom")]
[System.Xml.Serialization.XmlRoot(Namespace = "http://www.w3.org/2005/Atom", IsNullable = false)]
public partial class feed
{

    private feedTitle titleField;

    private string idField;

    private DateTime updatedField;

    private feedLink linkField;

    private feedEntry[] entryField;

    /// <remarks/>
    public feedTitle title
    {
        get
        {
            return this.titleField;
        }
        set
        {
            this.titleField = value;
        }
    }

    /// <remarks/>
    public string id
    {
        get
        {
            return this.idField;
        }
        set
        {
            this.idField = value;
        }
    }

    /// <remarks/>
    public DateTime updated
    {
        get
        {
            return this.updatedField;
        }
        set
        {
            this.updatedField = value;
        }
    }

    /// <remarks/>
    public feedLink link
    {
        get
        {
            return this.linkField;
        }
        set
        {
            this.linkField = value;
        }
    }

    /// <remarks/>
    [System.Xml.Serialization.XmlElement("entry")]
    public feedEntry[] entry
    {
        get
        {
            return this.entryField;
        }
        set
        {
            this.entryField = value;
        }
    }
}

/// <remarks/>
[Serializable()]
[System.ComponentModel.DesignerCategory("code")]
[System.Xml.Serialization.XmlType(AnonymousType = true, Namespace = "http://www.w3.org/2005/Atom")]
public partial class feedTitle
{

    private string typeField;

    private string valueField;

    /// <remarks/>
    [System.Xml.Serialization.XmlAttribute()]
    public string type
    {
        get
        {
            return this.typeField;
        }
        set
        {
            this.typeField = value;
        }
    }

    /// <remarks/>
    [System.Xml.Serialization.XmlText()]
    public string Value
    {
        get
        {
            return this.valueField;
        }
        set
        {
            this.valueField = value;
        }
    }
}

/// <remarks/>
[Serializable()]
[System.ComponentModel.DesignerCategory("code")]
[System.Xml.Serialization.XmlType(AnonymousType = true, Namespace = "http://www.w3.org/2005/Atom")]
public partial class feedLink
{

    private string relField;

    private string hrefField;

    /// <remarks/>
    [System.Xml.Serialization.XmlAttribute()]
    public string rel
    {
        get
        {
            return this.relField;
        }
        set
        {
            this.relField = value;
        }
    }

    /// <remarks/>
    [System.Xml.Serialization.XmlAttribute()]
    public string href
    {
        get
        {
            return this.hrefField;
        }
        set
        {
            this.hrefField = value;
        }
    }
}

/// <remarks/>
[Serializable()]
[System.ComponentModel.DesignerCategory("code")]
[System.Xml.Serialization.XmlType(AnonymousType = true, Namespace = "http://www.w3.org/2005/Atom")]
public partial class feedEntry
{

    private string idField;

    private feedEntryTitle titleField;

    private DateTime publishedField;

    private DateTime updatedField;

    private feedEntryLink linkField;

    private feedEntryContent contentField;

    private string etagField;

    /// <remarks/>
    public string id
    {
        get
        {
            return this.idField;
        }
        set
        {
            this.idField = value;
        }
    }

    /// <remarks/>
    public feedEntryTitle title
    {
        get
        {
            return this.titleField;
        }
        set
        {
            this.titleField = value;
        }
    }

    /// <remarks/>
    public DateTime published
    {
        get
        {
            return this.publishedField;
        }
        set
        {
            this.publishedField = value;
        }
    }

    /// <remarks/>
    public DateTime updated
    {
        get
        {
            return this.updatedField;
        }
        set
        {
            this.updatedField = value;
        }
    }

    /// <remarks/>
    public feedEntryLink link
    {
        get
        {
            return this.linkField;
        }
        set
        {
            this.linkField = value;
        }
    }

    /// <remarks/>
    public feedEntryContent content
    {
        get
        {
            return this.contentField;
        }
        set
        {
            this.contentField = value;
        }
    }

    /// <remarks/>
    [System.Xml.Serialization.XmlAttribute(Form = System.Xml.Schema.XmlSchemaForm.Qualified, Namespace = "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata")]
    public string etag
    {
        get
        {
            return this.etagField;
        }
        set
        {
            this.etagField = value;
        }
    }
}

/// <remarks/>
[Serializable()]
[System.ComponentModel.DesignerCategory("code")]
[System.Xml.Serialization.XmlType(AnonymousType = true, Namespace = "http://www.w3.org/2005/Atom")]
public partial class feedEntryTitle
{

    private string typeField;

    private string valueField;

    /// <remarks/>
    [System.Xml.Serialization.XmlAttribute()]
    public string type
    {
        get
        {
            return this.typeField;
        }
        set
        {
            this.typeField = value;
        }
    }

    /// <remarks/>
    [System.Xml.Serialization.XmlText()]
    public string Value
    {
        get
        {
            return this.valueField;
        }
        set
        {
            this.valueField = value;
        }
    }
}

/// <remarks/>
[Serializable()]
[System.ComponentModel.DesignerCategory("code")]
[System.Xml.Serialization.XmlType(AnonymousType = true, Namespace = "http://www.w3.org/2005/Atom")]
public partial class feedEntryLink
{

    private string relField;

    private string hrefField;

    /// <remarks/>
    [System.Xml.Serialization.XmlAttribute()]
    public string rel
    {
        get
        {
            return this.relField;
        }
        set
        {
            this.relField = value;
        }
    }

    /// <remarks/>
    [System.Xml.Serialization.XmlAttribute()]
    public string href
    {
        get
        {
            return this.hrefField;
        }
        set
        {
            this.hrefField = value;
        }
    }
}

/// <remarks/>
[Serializable()]
[System.ComponentModel.DesignerCategory("code")]
[System.Xml.Serialization.XmlType(AnonymousType = true, Namespace = "http://www.w3.org/2005/Atom")]
public partial class feedEntryContent
{

    private GcmRegistrationDescription gcmRegistrationDescriptionField;

    private WindowsRegistrationDescription windowsRegistrationDescriptionField;

    private string typeField;

    /// <remarks/>
    [System.Xml.Serialization.XmlElement(Namespace = "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect")]
    public GcmRegistrationDescription GcmRegistrationDescription
    {
        get
        {
            return this.gcmRegistrationDescriptionField;
        }
        set
        {
            this.gcmRegistrationDescriptionField = value;
        }
    }

    /// <remarks/>
    [System.Xml.Serialization.XmlElement(Namespace = "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect")]
    public WindowsRegistrationDescription WindowsRegistrationDescription
    {
        get
        {
            return this.windowsRegistrationDescriptionField;
        }
        set
        {
            this.windowsRegistrationDescriptionField = value;
        }
    }

    /// <remarks/>
    [System.Xml.Serialization.XmlAttribute()]
    public string type
    {
        get
        {
            return this.typeField;
        }
        set
        {
            this.typeField = value;
        }
    }
}

/// <remarks/>
[Serializable()]
[System.ComponentModel.DesignerCategory("code")]
[System.Xml.Serialization.XmlType(AnonymousType = true, Namespace = "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect")]
[System.Xml.Serialization.XmlRoot(Namespace = "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", IsNullable = false)]
public partial class GcmRegistrationDescription
{

    private byte eTagField;

    private DateTime expirationTimeField;

    private string registrationIdField;

    private string tagsField;

    private string gcmRegistrationIdField;

    /// <remarks/>
    public byte ETag
    {
        get
        {
            return this.eTagField;
        }
        set
        {
            this.eTagField = value;
        }
    }

    /// <remarks/>
    public DateTime ExpirationTime
    {
        get
        {
            return this.expirationTimeField;
        }
        set
        {
            this.expirationTimeField = value;
        }
    }

    /// <remarks/>
    public string RegistrationId
    {
        get
        {
            return this.registrationIdField;
        }
        set
        {
            this.registrationIdField = value;
        }
    }

    /// <remarks/>
    public string Tags
    {
        get
        {
            return this.tagsField;
        }
        set
        {
            this.tagsField = value;
        }
    }

    /// <remarks/>
    public string GcmRegistrationId
    {
        get
        {
            return this.gcmRegistrationIdField;
        }
        set
        {
            this.gcmRegistrationIdField = value;
        }
    }
}

/// <remarks/>
[Serializable()]
[System.ComponentModel.DesignerCategory("code")]
[System.Xml.Serialization.XmlType(AnonymousType = true, Namespace = "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect")]
[System.Xml.Serialization.XmlRoot(Namespace = "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", IsNullable = false)]
public partial class WindowsRegistrationDescription
{

    private byte eTagField;

    private DateTime expirationTimeField;

    private string registrationIdField;

    private string channelUriField;

    /// <remarks/>
    public byte ETag
    {
        get
        {
            return this.eTagField;
        }
        set
        {
            this.eTagField = value;
        }
    }

    /// <remarks/>
    public DateTime ExpirationTime
    {
        get
        {
            return this.expirationTimeField;
        }
        set
        {
            this.expirationTimeField = value;
        }
    }

    /// <remarks/>
    public string RegistrationId
    {
        get
        {
            return this.registrationIdField;
        }
        set
        {
            this.registrationIdField = value;
        }
    }

    /// <remarks/>
    public string ChannelUri
    {
        get
        {
            return this.channelUriField;
        }
        set
        {
            this.channelUriField = value;
        }
    }
}

This class is used with the GetRegistrations method. I created this class by copying the XML from the API’s response and then using Visual Studio’s Edit -> Paste Special -> Paste XML as Classes feature.  The GetRegistrations method is as follows:

private async Task<List<string>> GetRegistrations(NotificationHubNamespaceCredentials notificationHubNamespace)
{
    string resourceUri = $"https://{notificationHubNamespace.NamespaceName}.servicebus.windows.net/{notificationHubNamespace.HubName}/registrations/?api-version=2015-01";
    HttpRequestMessage request = new HttpRequestMessage()
    {
        RequestUri = new Uri(resourceUri),
        Method = HttpMethod.Get
    };

    string sasToken = Azure.AzureUtililities.GenerateSASToken(resourceUri, notificationHubNamespace.SasKeyName, notificationHubNamespace.SasKey);
    request.Headers.TryAddWithoutValidation("Authorization", sasToken);

    HttpClient httpClient = new HttpClient();
    HttpResponseMessage response = await httpClient.SendAsync(request);

    response.EnsureSuccessStatusCode();

    XmlSerializer xmlSerializer = new XmlSerializer(typeof(Azure.feed));
    Stream stream = await response.Content.ReadAsStreamAsync();

    Azure.feed feedVar = xmlSerializer.Deserialize(stream) as Azure.feed;

    // The feed contains a list of entries. Each entry has a 'title' that is its ID.
    List<string> entries = new List<string>();
    if (feedVar.entry is not null)
    {
        foreach (Azure.feedEntry entry in feedVar.entry)
        {
            entries.Add(entry.title.Value);
        }
    }

    return entries;
}

The CreateRegistration class will change to find the first Notification Hub namespace that has space for a new device. If a namespace is not found, the class will throw an exception. Other than this change, the method is mostly the same as it previously was, except for the return statement. It will now return the namespace that is being used for the registration in addition to the registrationId.

public async Task<(string registrationId, string notificationHubNamespaceName)> CreateRegistration(SignInUserDto signInUserDto)
{
    // Find the first namespace with a free slot.
    NotificationHubNamespaceCredentials usableNotificationHubNamespace = null;
    foreach (var notificationHubNamespace in _notificationHubNamespaces)
    {
        var registrations = await GetRegistrations(notificationHubNamespace);
        if (registrations.Count() < _devicesPerNamespace)
        {
            usableNotificationHubNamespace = notificationHubNamespace;
            break;
        }
    }

    if (usableNotificationHubNamespace is null)
    {
        throw new Exception("All the Notification Hub namespaces are full.");
    }    

    string createRegistration = CreateRegistrationBody(signInUserDto);

    string resourceUri = $"https://{usableNotificationHubNamespace.NamespaceName}.servicebus.windows.net/{usableNotificationHubNamespace.HubName}/registrations?api-version=2015-01";
    HttpRequestMessage request = new HttpRequestMessage()
    {
        RequestUri = new Uri(resourceUri),
        Method = HttpMethod.Post,
        Content = new StringContent(createRegistration, Encoding.UTF8, "application/atom+xml")
    };

    string sasToken = Azure.AzureUtililities.GenerateSASToken(resourceUri, usableNotificationHubNamespace.SasKeyName, usableNotificationHubNamespace.SasKey);
    request.Headers.TryAddWithoutValidation("Authorization", sasToken);

    HttpClient httpClient = new HttpClient();
    HttpResponseMessage response = await httpClient.SendAsync(request);

    response.EnsureSuccessStatusCode();

    string responseContent = await response.Content.ReadAsStringAsync();

    XmlDocument xmlDocument = new XmlDocument();
    xmlDocument.LoadXml(responseContent);

    string registrationId = xmlDocument.GetElementsByTagName("title")[0].InnerText;

    return (registrationId, usableNotificationHubNamespace.NamespaceName);
}

The UpdateRegistration and DeleteRegistration, methods are mostly the same as before. The only change required in them is to take in an argument indicating which namespace the registration exists on. The methods then find this namespace from the list of namespaces and use it.

public async Task<(string registrationId, string notificationHubNamespaceName)> CreateRegistration(SignInUserDto signInUserDto)
{
    // Find the first namespace with a free slot.
    NotificationHubNamespaceCredentials usableNotificationHubNamespace = null;
    foreach (var notificationHubNamespace in _notificationHubNamespaces)
    {
        var registrations = await GetRegistrations(notificationHubNamespace);
        if (registrations.Count() < _devicesPerNamespace)
        {
            usableNotificationHubNamespace = notificationHubNamespace;
            break;
        }
    }

    if (usableNotificationHubNamespace is null)
    {
        throw new Exception("All the Notification Hub namespaces are full.");
    }    

    string createRegistration = CreateRegistrationBody(signInUserDto);

    string resourceUri = $"https://{usableNotificationHubNamespace.NamespaceName}.servicebus.windows.net/{usableNotificationHubNamespace.HubName}/registrations?api-version=2015-01";
    HttpRequestMessage request = new HttpRequestMessage()
    {
        RequestUri = new Uri(resourceUri),
        Method = HttpMethod.Post,
        Content = new StringContent(createRegistration, Encoding.UTF8, "application/atom+xml")
    };

    string sasToken = Azure.AzureUtililities.GenerateSASToken(resourceUri, usableNotificationHubNamespace.SasKeyName, usableNotificationHubNamespace.SasKey);
    request.Headers.TryAddWithoutValidation("Authorization", sasToken);

    HttpClient httpClient = new HttpClient();
    HttpResponseMessage response = await httpClient.SendAsync(request);

    response.EnsureSuccessStatusCode();

    string responseContent = await response.Content.ReadAsStringAsync();

    XmlDocument xmlDocument = new XmlDocument();
    xmlDocument.LoadXml(responseContent);

    string registrationId = xmlDocument.GetElementsByTagName("title")[0].InnerText;

    return (registrationId, usableNotificationHubNamespace.NamespaceName);
}

SendNotification has the same method signature as before, but there are some changes within the method itself. As we now have multiple namespaces, and a user could have a device in any namespace, or multiple devices spread over the namespaces, we need to send the notification to all of them. As we’re using a tag with the user’s username, it’s safe to do this – if a user doesn’t have a device on a namespace, there will be no registration matching the tag and no notification will be sent. The method makes use of Tasks, and Task.WhenAll to allow the namespaces to receive the notification requests and then wait for all the requests to complete before continuing. This is more efficient than awaiting each call to a namespace. Afterwards, all requests are inspected to see if they were successful.

public async Task SendNotification(string message, string tag)
{
    // Send the notification to each namespace.
    var sendNotificationTasks = new List<Task<HttpResponseMessage>>();
    foreach (var notificationHubNamespace in _notificationHubNamespaces)
    {
        string notificationBody = "{\"message\":\"" + message + "\"}";

        string resourceUri = $"https://{notificationHubNamespace.NamespaceName}.servicebus.windows.net/{notificationHubNamespace.HubName}/messages/?api-version=2015-01";
        HttpRequestMessage request = new HttpRequestMessage()
        {
            RequestUri = new Uri(resourceUri),
            Method = HttpMethod.Post,
            Content = new StringContent(notificationBody, Encoding.UTF8, "application/json")
        };

        string sasToken = Azure.AzureUtililities.GenerateSASToken(resourceUri, notificationHubNamespace.SasKeyName, notificationHubNamespace.SasKey);
        request.Headers.TryAddWithoutValidation("Authorization", sasToken);
        request.Headers.TryAddWithoutValidation("ServiceBusNotification-Tags", tag);
        request.Headers.TryAddWithoutValidation("ServiceBusNotification-Format", "template");

        HttpClient httpClient = new HttpClient();

        sendNotificationTasks.Add(httpClient.SendAsync(request));              
    }

    await Task.WhenAll(sendNotificationTasks);

    foreach (var sendNotificationTask in sendNotificationTasks)
    {
        sendNotificationTask.Result.EnsureSuccessStatusCode();
    }
}

The Device model also needs to be modified to store which namespace the registration for this device is within. In the Models folder, open the Device.cs file and add the NotificationHubNamespaceName property:

public string NotificationHubNamespaceName { get; set; }

Next, open a terminal window in the API project and run the following command to create a migration.

dotnet ef migrations add "NotificationHubNamespaceName"

After running the above command you’ll see a new XXX_NotificationHubNamespaceName.cs file within the Migrations folder. You can open this to see what changes will be applied to the database. Run the following command to apply those changes.

dotnet ef database update

Finally, we need to update the SignUserIn and SignUserOut methods of the AuthController to work with the changes made to NotificationHubService, and to store the NotificationHubNamespaceName for a device. After the changes are implemented, these two methods should look like:

[HttpPost("SignUserIn")]
public async Task<IActionResult> SignUserIn(SignInUserDto signInUserDto)
{        
    // Check if this user exists.
    User user = _applicationDbContext.Users.FirstOrDefault(u => u.Username == signInUserDto.Username);

    // If the user does not exist, create the user and the device.
    if (user is null)
    {
        user = new User
        {
            Username = signInUserDto.Username,
        };

        await _applicationDbContext.Users.AddAsync(user);

        // Register with Azure Notification Hub
        var installationIdAndNamespaceName = await _notificationHubService.CreateRegistration(signInUserDto);

        Device device = new Device
        {
            Id = signInUserDto.DeviceId.Value,
            Platform = signInUserDto.Platform.Value,
            PnsToken = signInUserDto.PnsToken,
            Username = signInUserDto.Username,
            RegistrationId = installationIdAndNamespaceName.registrationId,
            NotificationHubNamespaceName =installationIdAndNamespaceName.notificationHubNamespaceName
        };

        await _applicationDbContext.Devices.AddAsync(device);
    }

    // Check if this device has been assigned to the user.
    else
    {

        Device device = await _applicationDbContext.Devices
            .FirstOrDefaultAsync(d => d.Id == signInUserDto.DeviceId && d.Username == signInUserDto.Username);

        // If the device has not been assigned, assign it.
        if (device is null)
        {
            var installationIdAndNamespaceName = await _notificationHubService.CreateRegistration(signInUserDto);

            device = new Device
            {
                Id = signInUserDto.DeviceId.Value,
                Platform = signInUserDto.Platform.Value,
                PnsToken = signInUserDto.PnsToken,
                Username = signInUserDto.Username,
                RegistrationId = installationIdAndNamespaceName.registrationId,
                NotificationHubNamespaceName = installationIdAndNamespaceName.notificationHubNamespaceName
            };

            await _applicationDbContext.Devices.AddAsync(device);
        }
        else
        // Update the registration
        {
            await _notificationHubService.UpdateRegistration(signInUserDto, device.RegistrationId, device.NotificationHubNamespaceName);

            device.RegistrationId = signInUserDto.PnsToken;
        }
    }

    // Finally, save the changes.
    await _applicationDbContext.SaveChangesAsync();

    return Ok();
}
[HttpPost("SignUserOut")]
public async Task<IActionResult> SignUserOut(SignOutUserDto signOutUserDto)
{

    // Check if this user exists.
    User user = _applicationDbContext.Users.FirstOrDefault(u => u.Username == signOutUserDto.Username);

    // If the user does not exist, return BadRequest
    if (user is null)
    {
        return BadRequest("The user does not exist.");
    }

    // Check if this device has been assigned to the user.
    Device device = await _applicationDbContext.Devices
        .FirstOrDefaultAsync(d => d.Id == signOutUserDto.DeviceId && d.Username == signOutUserDto.Username);

    // If the device has not been assigned, unassign it.
    if (device is not null)
    {
        await _notificationHubService.DeleteRegistration(device.RegistrationId, device.NotificationHubNamespaceName);

        _applicationDbContext.Devices.Remove(device);
        await _applicationDbContext.SaveChangesAsync();
    }

    // If the user has no more devices left, delete them.
    int deviceCount = await _applicationDbContext.Devices
        .Where(d => d.Username == signOutUserDto.Username)
        .CountAsync();

    if (deviceCount == 0)
    {
        _applicationDbContext.Remove(user);
    }

    // Finally, save the changes.
    await _applicationDbContext.SaveChangesAsync();
        
    return Ok();
}

Conclusion

We’ve modified the code to work with multiple namespaces allowing us to increase the number of active devices we can have registered. This method will allow you to have more active devices than the 500 limit on the free plan. If your application is being used in production, you should consider moving up to a paid plan instead of trying to squeeze as many devices out of the free plan as possible. For a relatively low monthly cost, the Basic plan offers a lot more than just more active devices. It allows more notification’s to be sent and comes with an SLA.

Leave a comment

Your email address will not be published. Required fields are marked *