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 fromobject
. - 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 anobject
, 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 anint
), you'll get a runtime error.
- Role:
- 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.
- Role: The
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).
- Casting: Before you can call any method or property on an
- 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 theobject
type. This can make operations slower because there's a conversion process taking place.
- Boxing/Unboxing: This is the process of converting value types (like
- 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 thanobject
when accessing members because of this extra runtime checking. Withobject
, once you've cast correctly, accessing members is straightforward. Withdynamic
, every member access requires a lookup.
- Runtime Binder: The .NET runtime needs to figure out the operations on
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:
- Errors at Runtime: Any mistakes only show up when your application is running.
- Maintainability: Overusing
dynamic
can make the code harder to read and maintain, as the type ambiguities can confuse developers. - 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.