Let's take a close look at an interesting part of C#: the dynamic keyword.

Unpacking 'Dynamic'

In the C# world, which loves its strict types, dynamic stands out. It's a type that C# 4.0 introduced, allowing some flexibility. In situations where you can't know the type until runtime, dynamic shines. But what do we get with it?

Dynamic vs. Object: Two Different Beasts

At first, dynamic and object might seem pretty similar. But they have some key differences:

1. How They Work:

  • object:
    • Role: object is the mother of all .NET types. Every type in .NET, whether it’s an int, string, custom class, and even array, eventually inherits from object.
    • At Coding Time: When you store a value in an object variable, it loses its original type's unique identity. For instance, if you assign an integer to an object, you can't just call integer methods or properties without letting the code know it's actually an integer.
    • At Runtime: When you're ready to get the original type back, you need to cast it. If the cast is wrong (say you cast it to a string but it's an int), you'll get a runtime error.
  • dynamic:
    • Role: The dynamic type bypasses compile-time type checking. Essentially, the compiler trusts you to get things right.
    • At Coding Time: While writing your code, you won’t get IntelliSense support, because the type is unknown. This means you can call any method or property on a dynamic type and the compiler won't complain.
    • At Runtime: Only when your application is running will it try to figure out if the method or property you called on a dynamic variable exists. If it doesn’t, you'll get a runtime error.

2. Using Them in Code:

  • object:
    • Casting: Before you can call any method or property on an object, you have to cast it to its actual type. For example, ((string)someObj).ToUpper().
    • Error Checking: If you do something wrong, like casting to the wrong type or calling a method that doesn’t exist on that type, you'll get a compile-time error (if casting is wrong) or a runtime error (for incorrect method calls after casting).
  • dynamic:
    • Casting: You can directly call methods or access properties on it without casting.
    • Error Checking: Errors only come up at runtime. There's no compile-time check, so you might run into more unexpected issues when the application is running if you're not careful.

3. Speed and Performance:

  • object:
    • Boxing/Unboxing: This is the process of converting value types (like int, char, bool) to and from the object type. This can make operations slower because there's a conversion process taking place.
  • dynamic:
    • Runtime Binder: The .NET runtime needs to figure out the operations on dynamic types as your application runs. This means checking what methods or properties are available, which can take time.
    • In comparison: The dynamic type tends to be slower than object when accessing members because of this extra runtime checking. With object, once you've cast correctly, accessing members is straightforward. With dynamic, every member access requires a lookup.

Benefits and Downsides:

  • Benefits of object:
    • Predictability: You handle type-related issues at compile-time, which can make your application more robust.
    • Flexibility: Can store any value.
  • Downsides of object:
    • Manual Casting: Requires you to remember and cast to the original type.
    • Performance: Boxing and unboxing can slow things down.
  • Benefits of dynamic:
    • No Casting: Directly call methods or properties.
    • Interop Scenarios: Especially useful when working with COM objects or other dynamic languages in .NET.
  • Downsides of dynamic:
    • Errors: All issues surface at runtime, which can lead to unexpected crashes.
    • Performance: The runtime lookup makes it slower than direct access with known types.

Seeing 'Dynamic' in Action

Okay, let's look at a real-world example.

Imagine a system where you get messages, like UserCreated or UserUpdated. These messages come in a JSON format, and you only figure out their real type at runtime. Here's how the message might look:

{
    "metadata": {
        "type": "Messaging.User.UserCreated",
        ...
    },
    "payload": {
        "id": 1,
        "firstName": "Mario",
        "lastName": "Smolcic",
        ...
    }
}

Each message, when it arrives in the OnMessageReceivedAsync method, is just a string in this JSON format. Within the metadata, there's a type property that tells us the full name of the type (like "Messaging.User.UserCreated"). We can use this, with the power of reflection (using assembly.GetType), to determine the actual message type from our code.

Here's the magic:

private readonly IMapper _mapper;
private readonly IMediator _mediator;

protected async Task OnMessageReceivedAsync(
    Message message, 
    CancellationToken cancellationToken)
{
    Assembly assembly = typeof(Message).Assembly;
    Type messageType = assembly.GetType(message.Metadata.Type);
    object messagePayload = JsonConvert.DeserializeObject(message.Payload, messageType);
    await HandleMessageAsync((dynamic)messagePayload, cancellationToken);
}

private async Task HandleMessageAsync<T>(
    T messagePayload,
    CancellationToken cancellationToken)
    where T : MessageBase
{
    UpsertUserCommand command = _mapper.Map<T, UpsertUserCommand>(messagePayload);
    await _mediator.Send(command, cancellationToken);
}

By casting messagePayload as dynamic in OnMessageReceivedAsync, we're allowing the runtime to determine the actual type. This means HandleMessageAsync will be called with the specific type that messagePayload was deserialized to. For instance, if messagePayload was deserialized to a type called UserCreated, then the method HandleMessageAsync<UserCreated> will be invoked. If we had a different logic in the HandleMessageAsync method for different message types, we could have multiple method overloads. By casting messagePayload as dynamic, we would invoke the correct overload per type. This pattern offers a flexible way to handle various message types in a loosely-coupled and maintainable manner, aiding in the creation of scalable, extendable systems without the need for huge if-else or switch-case constructs.

Using 'Dynamic' Carefully

Though dynamic has its benefits, it's essential to be aware of the pitfalls:

  1. Errors at Runtime: Any mistakes only show up when your application is running.
  2. Maintainability: Overusing dynamic can make the code harder to read and maintain, as the type ambiguities can confuse developers.
  3. Performance: As mentioned, it can be slower because of the runtime lookups.

Wrapping Up

The dynamic keyword provides a way to bring flexibility to the rigid type system of C#. However, like any powerful tool, it's crucial to understand when and how to use it. Always weigh its advantages against the potential pitfalls and determine if it's the right fit for your specific scenario. Honestly, you won't need it often, but it's nice to know it's there if you will.