Don’t Break Consumers with Optional Parameters

Adding a optional parameter is a breaking change.  This topic came up more frequently around the time the feature was introduced but, I haven’t seen it brought up in a while. I mentioned it on a code review recently so, now it’s fresh on my mind. The “breaking” nature of optional parameters is a fun one because it is not entirely obvious.  How is it breaking when the consuming code doesn’t need to change? Well, like anything in software development, there is a logical explanation.

Lets take a look at the following code.  Imagine we have a library that contains a message queue.  We might have something like the IMessageQueue interface below.

Next, as part of our business requirements, we need to have the ability to send emails.  We remember our super cool message queue so, we decide to use it.  This leaves us with the following EmailService.

Some time goes by and we want to enhance our message queue.  We know that there are a couple other libraries that are using it so, we make sure to add default values to all our new parameters.  With the following code, our consumers can still invoke the Publish method exactly as they have before.

So where’s the breaking change?!? The code looks exactly the same.  The secret lies in the IL code that is generated.  Let’s look at the the following line of code

_queue.Publish(new Message<Email>(email));

Prior to our code changes, we get IL that looks like the following

IL_0001: ldarg.0      // this
    
IL_0002: ldfld        class EspressoCoder.Samples.Defaults.IMessageQueue EspressoCoder.Samples.Defaults.EmailService::_queue
    
IL_0007: ldarg.1      // email
    
IL_0008: newobj       instance void class EspressoCoder.Samples.Defaults.Message`1<class EspressoCoder.Samples.Defaults.Email>::.ctor(!0/*class EspressoCoder.Samples.Defaults.Email*/)
    
IL_000d: callvirt     instance void EspressoCoder.Samples.Defaults.IMessageQueue::Publish<class EspressoCoder.Samples.Defaults.Email>(class EspressoCoder.Samples.Defaults.Message`1<!!0/*class EspressoCoder.Samples.Defaults.Email*/>)
    
IL_0012: nop

After we add the default parameter, we notice the IL changes

IL_0001: ldarg.0      // this
    
IL_0002: ldfld        class EspressoCoder.Samples.Defaults.IMessageQueue EspressoCoder.Samples.Defaults.EmailService::_queue
    
IL_0007: ldarg.1      // email
    
IL_0008: newobj       instance void class EspressoCoder.Samples.Defaults.Message`1<class EspressoCoder.Samples.Defaults.Email>::.ctor(!0/*class EspressoCoder.Samples.Defaults.Email*/)
    
IL_000d: ldc.i4.1     
    
IL_000e: callvirt     instance void EspressoCoder.Samples.Defaults.IMessageQueue::Publish<class EspressoCoder.Samples.Defaults.Email>(class EspressoCoder.Samples.Defaults.Message`1<!!0/*class EspressoCoder.Samples.Defaults.Email*/>, valuetype EspressoCoder.Samples.Defaults.MessagePriority)
    
IL_0013: nop

Why does .NET compile these changes changes differently?  It is because .NET determines the default values at compile time.  When the function is invoked, it is called with all default parameter values even though you do not see this in code.

Pay special attention to the line IL_000d: ldc.i4.1.  The last character in this line is ‘1’ which is the numeric representation of the MessagePriority.Normal default value. On the following line, we can then see that the full function (with all parameters) is invoked.

What does this mean? It means anytime we modify the parameter list, with required OR optional parameters, the consumers need to be updated. Even if that means merely recompiling against the updated function. For example, if these two classes are in the same project or even the same solution you will not have an issue.  Unfortunately, this is not always possible.  Compiled libraries are often delivered as nuget packages from either internal or external repositories.  With the previous example, it would be perfectly reasonable to have our two classes in separate nuget packages. In this case, a runtime error will occur.

This doesn’t mean these sorts of changes CANT or SHOULDN’T be made.  What it does mean is if they are, appropriate versioning must be made otherwise, pain will ensue.

One thought on “Don’t Break Consumers with Optional Parameters

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s