Team Debugging with NServiceBus Azure ServiceBus
In this article I disclose how my team manages concurrent development with Azure Service Bus transport using NServiceBus.
Particular's NServiceBus is an amazing library for implementing messaging in .NET. It is smart, helpful, and takes away a lot of the work needed to get messaging up and running quick.
While working with Ryan to design the Sightly TargetView platform we decided early on that the system should be run in the cloud. We chose Microsoft Azure and Azure Service Bus. Fortunately the hard working developers of Particular have an implementation of the Azure Service Bus transport for NServiceBus. We quickly ran into a delimma regarding how to independently debug our systems without interfering with each other.
In it's default usage, which is where my experience started, NServiceBus is used with Microsoft's MSMQ. This makes local debugging easy as the system creates each hosts queues locally if you use the queue name only configuration for the Endpoint name in MessageEndpointMappings.
<MessageEndpointMappings>
<add Assembly="assembly" Endpoint="queue" />
</MessageEndpointMappings>
Using config transforms the debug config can be set to route using local queues on your development box and allows you to run and debug multiple NServiceBus hosts without collisions with other developers.
Unlike MSMQ, Azure Service Bus has no local option. It is not yet supported by the Storage Emulator so debugging must be performed using an Azure Service Bus hosted in Azure. The service bus is configured by convention using this connection string:
<connectionStrings>
<!-- Azure ServiceBus -->
<add name="NServiceBus/Transport"
connectionString="Endpoint=sb://[namespace].servicebus.windows.net;
SharedSecretIssuer=owner;
SharedSecretValue=someSecret"/>
</connectionStrings>
The knee jerk solution was to create a build configuration for each developer and use config transformations, perhaps with some SlowCheetah thrown in for our worker roles.
This just smells bad. Build configurations already add a measure of risk (albiet necessary) with your project. Creating one for each developer is excessive. In addition it creates another administrative burden as your team expands and contracts. We wanted to find another way.
Our system employs 3 environmental Service Buses, each set in a corresponding configuration tranform for each host. The debug configuration transform, as well as the main config (app/web.config) intentionally omit this connection string. Only the other build configurations set the connection string.
As a result we need only setup a way to set the connection string while debugging.
One of the many things I enjoy about using NServiceBus is it's multitude of extensibility points. One such way is an interface for overriding the default configuration source for NServiceBus settings: IProvideConfiguration<T>
.
The IProvideConfiguration interface is simple enough:
/// <summary>
/// Abstraction of a configuration source for a given piece of configuration data.
/// </summary>
public interface IProvideConfiguration<T>
{
/// <summary>
/// Returns configuration data for the given type.
/// </summary>
T GetConfiguration();
}
For our purposes we need to override configuration of the Azure Service Bus connection string, which leads us to AzureServiceBusQueueConfig
. Let's start with a skeleton implementation for IProvideConfiguration<AzureServiceBusConfig>
:
/// <summary>
/// Configures Azure Service Bus connection string for debugging
/// </summary>
public class DebuggableAzureServiceBusQueueConfigurationProvider :
IProvideConfiguration<AzureServiceBusQueueConfig>
{
/// <summary>
/// Retrieves the config
/// </summary>
/// <returns></returns>
public AzureServiceBusQueueConfig GetConfiguration()
{
var config = ConfigurationManager.GetSection("AzureServiceBusQueueConfig") as AzureServiceBusQueueConfig ?? new AzureServiceBusQueueConfig();
return config;
}
}
This essentially replicates the existing functionality within NServiceBus Azure. It reads the configuration from the app.config and returns it to NServiceBus when the transport is configured. The AzureServiceBusQueueConfig
class includes a property called ConnectionString. Curiously, this is not set by default. When empty, it defaults to the convention defined above. Fair enough, we need only set it:
var config = ConfigurationManager.GetSection("AzureServiceBusQueueConfig") as AzureServiceBusQueueConfig ?? new AzureServiceBusQueueConfig();
config.ConnectionString = "Endpoint=sb://[namespace].servicebus.windows.net;
SharedSecretIssuer=owner;
SharedSecretValue=someSecret";
return config;
The million dollar question is: Where do we load the connection string from?
We thought of several options:
- Add a connection string to the Debug config for each developer, using the developers machine name or user name as the name.
- Set the connection string in the Registry of each developers workstation, using a path/key name set by convention.
- Set the connection string in the machine.config file on the developers workstation, using a connection string named by convention.
- Set an environment variable on each developers workstation, again named by convention.
- Use some sort of configuration file in the file system, path set by convention.
In our case we chose to use the machine.config file. We name the connection string NServiceBus/Transport/Azure
. We also decided that this connection string should not override any existing connection string (in the case that we are debugging using one of the other build configuration where the connection string is defined). Additionally it should only apply while actively debugging from Visual Studio.
With these constraints in mind, and some defensive programming we ended up with this:
/// <summary>
/// Configures Azure Service Bus connection string for debugging
/// </summary>
public class DebuggableAzureServiceBusQueueConfigurationProvider :
IProvideConfiguration<AzureServiceBusQueueConfig>
{
private const string DebugConnectionStringName = "NServiceBus/Transport/Azure";
private const string DefaultConnectionStringName = "NServiceBus/Transport";
private static AzureServiceBusQueueConfig Config { get; set; }
/// <summary>
/// Retrieves the config
/// </summary>
/// <returns></returns>
public AzureServiceBusQueueConfig GetConfiguration()
{
var asbqConfig = ConfigurationManager.GetSection("AzureServiceBusQueueConfig") as AzureServiceBusQueueConfig ?? new AzureServiceBusQueueConfig();
var defaultNsbConnectionString = ConfigurationManager.ConnectionStrings[DefaultConnectionStringName];
if (!Debugger.IsAttached ||
!string.IsNullOrWhiteSpace(asbqConfig.ConnectionString) ||
defaultNsbConnectionString != null) return asbqConfig;
Debug.WriteLine("Debugger attached and no connection string is specified. Using machine.config connection string", "NServiceBusConfig");
if (Config != null) return Config;
var connectionStringSetting = ConfigurationManager.ConnectionStrings[DebugConnectionStringName];
if (connectionStringSetting == null)
throw new InvalidOperationException(string.Format("Specify a connection string in your machine.config called '{0}' for debugging on your development workstation.", DebugConnectionStringName));
var newConfig = new AzureServiceBusQueueConfig
{
BackoffTimeInSeconds = asbqConfig.BackoffTimeInSeconds,
BatchSize = asbqConfig.BatchSize,
ConnectionString = connectionStringSetting.ConnectionString,
ConnectivityMode = asbqConfig.ConnectivityMode,
DefaultMessageTimeToLive = asbqConfig.DefaultMessageTimeToLive,
DuplicateDetectionHistoryTimeWindow = asbqConfig.DuplicateDetectionHistoryTimeWindow,
EnableBatchedOperations = asbqConfig.EnableBatchedOperations,
EnableDeadLetteringOnMessageExpiration = asbqConfig.EnableDeadLetteringOnMessageExpiration,
EnablePartitioning = asbqConfig.EnablePartitioning,
IssuerKey = asbqConfig.IssuerKey,
IssuerName = asbqConfig.IssuerName,
LockDuration = asbqConfig.LockDuration,
MaxDeliveryCount = asbqConfig.MaxDeliveryCount,
MaxSizeInMegabytes = asbqConfig.MaxSizeInMegabytes,
QueueName = asbqConfig.QueueName,
QueuePerInstance = asbqConfig.QueuePerInstance,
RequiresDuplicateDetection = asbqConfig.RequiresDuplicateDetection,
RequiresSession = asbqConfig.RequiresSession,
ServerWaitTime = asbqConfig.ServerWaitTime,
ServiceNamespace = asbqConfig.ServiceNamespace
};
Config = newConfig;
return Config;
}
}
No configuration is necessary. Simply add the class and NServiceBus will find and utilize it.
Each developer is created his/her own Azure Service Bus for debugging purposes, and sets the connection string in both of the machine.config files:
C:\Windows\Microsoft.NET\Framework\v4.0.30319\Config\machine.config
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Config\machine.config
I am not completely satisfied with this choice. While the machine.config makes an easy implementation, all .NET applications running on the developers workstation have access to this connection string. I have no idea to what nefarious uses a compromised Azure Service Bus used solely for debugging could be used, it still strikes me that there should be a better way to do this.
Any thoughts?