Dynamic Types

By the Syncfusion DevTutorials team · Last updated

Resolve members at runtime with dynamic: runtime binding, dynamic member access and calls, ExpandoObject, DynamicObject, unknown data, reflection vs dynamic and performance.

Introduction to Dynamic Types

Think about two ways to send a parcel.

At a post office counter, the clerk checks everything before the parcel leaves: the address, the weight, the stamps. If something is wrong, you fix it right there.

With a drop box, no one checks anything. You drop the parcel and leave. If the address is wrong, you find out days later when it comes back.

Normal C# works like the counter. The compiler checks every method call before your program runs. dynamic works like the drop box. The compiler skips the checks. The method call is checked only when that line actually runs.

In simple words: dynamic tells the compiler, "I will handle this at runtime."

This helps when the compiler cannot know the type:

  • data from JSON or a database that has no matching C# class,
  • objects from COM libraries like Excel automation,
  • code that calls Python or JavaScript.

Easy Way to Remember: a normal type is checked before you ship. dynamic is checked when it arrives.

What You Will Learn

In this lesson, you will learn:

  1. What the dynamic keyword does.
  2. How types are checked when the program runs.
  3. How to read and use members when type is unknown.
  4. How to build objects on the fly with ExpandoObject.
  5. How to make your own dynamic objects with DynamicObject.
  6. How to work with data of unknown type.
  7. When to use reflection and when to use dynamic.
  8. What dynamic costs in speed and safety.

All examples on this page run in your browser. The browser runtime includes the C# runtime binder that dynamic needs. Examples using COM, like Excel automation, only work on Windows with Office installed, so they are shown as plain code.

Start With a Working Example

Press Run.

One variable, three different types

using System;

dynamic value = 10;
Console.WriteLine($"{value} is {value.GetType().Name}, plus 5 = {value + 5}");

value = "Chennai";
Console.WriteLine($"{value} is {value.GetType().Name}, length = {value.Length}");

value = new DateTime(2025, 8, 15);
Console.WriteLine($"{value:dd MMM yyyy} is {value.GetType().Name}, year = {value.Year}");

The same variable holds an int, then a string, then a DateTime. Each time, value.Length or value.Year is looked up while your program runs, on whatever object is inside at that moment.

Try This

Add Console.WriteLine(value.Length); as the last line and run. The compiler accepts it. At runtime it fails, because a DateTime has no Length. You will see why in the next sections.

dynamic Keyword

When you declare a variable as dynamic, the compiler stops checking what you do with it.

Compare dynamic with var and object. They look similar, but work very differently:

varobjectdynamic
Type decidedAt build time, from the valueAt build time: always objectWhen the program runs, from the real value
Can call value.Length?Yes, if that type has itNo, build errorYes, checked when this line runs
Mistakes foundWhen you buildWhen you buildWhen the program runs
IntelliSense help?Yes, full listOnly object membersNo, none

var, object and dynamic side by side

using System;

var a = "hello";      // the compiler knows a is a string
object b = "hello";   // the compiler only knows b is an object
dynamic c = "hello";  // the compiler does not check; it waits until runtime

Console.WriteLine(a.Length);           // checked when you build
// Console.WriteLine(b.Length);        // build error: object has no Length
Console.WriteLine(((string)b).Length); // you must cast it first
Console.WriteLine(c.Length);           // checked when this line runs

// a = 5;  // build error: a is a string forever
c = 5;     // fine: c can hold any type
Console.WriteLine(c * 2);

In simple words: var is a shortcut for a real type. object is a type that hides details. dynamic turns off type checking.

dynamic has no IntelliSense and no build-time checks. A typo like value.Lenght builds without error and fails only when that line runs. Use dynamic in small, clearly marked places, not everywhere.

Runtime Type Resolution

When your code uses a dynamic value, the compiler writes a small call site. When that line runs, the call site does three things:

  1. It looks at the actual type of the value right now.
  2. It asks the runtime binder to find a member that matches, using C# rules.
  3. It remembers the answer, so the next call with the same type is faster.

The runtime binder is the part of .NET that does what the compiler normally does, but while your program runs. It is in Microsoft.CSharp.dll.

Because this lookup happens at runtime, overload resolution uses the actual type, not the declared type. This creates a useful trick.

The real type picks the overload

using System;

object[] items = { 42, "Asha", 3.5, true };

Console.WriteLine("Declared as object:");
foreach (object item in items)
    Console.WriteLine($"  {Describer.Describe(item)}");

Console.WriteLine("Cast to dynamic:");
foreach (object item in items)
    Console.WriteLine($"  {Describer.Describe((dynamic)item)}");

static class Describer
{
    public static string Describe(object value) => "some object";
    public static string Describe(int value) => $"an int: {value}";
    public static string Describe(string value) => $"a string: {value}";
    public static string Describe(double value) => $"a double: {value}";
}

With object, the compiler always picks Describe(object). With (dynamic)item, the runtime binder looks at the actual value and picks the best match. true has no bool match, so it falls back to Describe(object).

What Happens When There Is No Match

If the runtime binder cannot find a matching member, it throws RuntimeBinderException. This is like the drop box parcel coming back.

A missing member fails at runtime

using System;
using Microsoft.CSharp.RuntimeBinder;

dynamic city = "Mumbai";

try
{
    Console.WriteLine(city.ToUpper());     // string has ToUpper: works
    Console.WriteLine(city.Population);    // string has no Population
}
catch (RuntimeBinderException ex)
{
    Console.WriteLine($"Runtime error: {ex.Message}");
}

Dynamic Member Access

With dynamic, you read properties and fields with a simple dot. No GetProperty, no GetValue.

This works well with anonymous types, which have no name you can write. Anonymous types are internal, so this works when the code that reads them is in the same project.

Read anonymous objects without knowing their type

using System;
using System.Collections.Generic;

var rows = new List<dynamic>
{
    new { Name = "Asha", City = "Pune", Orders = 12 },
    new { Name = "Ravi", City = "Chennai", Orders = 4 },
    new { Name = "Meera", City = "Delhi", Orders = 9 },
};

foreach (dynamic row in rows)
{
    string level = row.Orders >= 9 ? "Gold" : "Silver";
    Console.WriteLine($"{row.Name,-6} {row.City,-8} {level}");
}

Every row.Orders and row.Name is looked up while the program runs. If you change the anonymous type later, nothing reminds you to update this code. That is the trade-off.

Dynamic Method Invocation

Calling methods works the same way: value.MethodName(arguments).

The runtime binder finds a method on the actual type with that name and matching arguments. This is called duck typing: "if it walks like a duck and quacks like a duck, treat it as a duck." The objects do not need to share an interface. They just need a method with the right name.

Duck typing with dynamic

using System;

dynamic[] devices = { new Printer(), new Speaker(), new Lamp() };

foreach (dynamic device in devices)
{
    try
    {
        device.TurnOn();
        Console.WriteLine($"  status: {device.Status()}");
    }
    catch (Microsoft.CSharp.RuntimeBinder.RuntimeBinderException)
    {
        Console.WriteLine($"  {device.GetType().Name} cannot report a status");
    }
}

class Printer
{
    public void TurnOn() => Console.WriteLine("Printer warming up");
    public string Status() => "ready, 120 sheets";
}

class Speaker
{
    public void TurnOn() => Console.WriteLine("Speaker on");
    public string Status() => "volume 40%";
}

class Lamp
{
    public void TurnOn() => Console.WriteLine("Lamp on");
}

Printer, Speaker and Lamp share no interface, but one loop calls them all. Lamp has no Status method, so that one call fails when the line runs.

In your own code, an interface is almost always better than duck typing. The compiler checks it, the editor completes it, and missing methods are caught before the program runs. Use dynamic calls for types you cannot change.

ExpandoObject

An ExpandoObject is an object you can add properties to while your program runs. It starts empty. When you set obj.Anything = value, the property Anything is created.

It lives in System.Dynamic, and you must use it with a dynamic variable.

Add properties on the fly

using System;
using System.Dynamic;

dynamic product = new ExpandoObject();
product.Name = "Wireless Mouse";
product.Price = 799m;
product.Tags = new[] { "computer", "sale" };

Console.WriteLine($"{product.Name}: Rs. {product.Price}");
Console.WriteLine($"Tags: {string.Join(", ", product.Tags)}");

// You can even add a method, as a delegate
product.PriceWithTax = (Func<decimal>)(() => product.Price * 1.18m);
Console.WriteLine($"With tax: Rs. {product.PriceWithTax():0.00}");

ExpandoObject Is Also a Dictionary

Inside, an ExpandoObject is a dictionary of property names and values. It implements IDictionary<string, object?>, so you can:

  • add a property whose name you only have as text,
  • loop over every property,
  • check if a property exists before you read it.

Use an ExpandoObject as a dictionary

using System;
using System.Collections.Generic;
using System.Dynamic;

dynamic person = new ExpandoObject();
person.Name = "Priya";

var fields = (IDictionary<string, object?>)person;
fields["City"] = "Hyderabad";          // name chosen at runtime
fields["Joined"] = 2021;

Console.WriteLine($"Has City? {fields.ContainsKey("City")}");
Console.WriteLine($"Has Phone? {fields.ContainsKey("Phone")}");
Console.WriteLine($"City via dot: {person.City}");

foreach (var (key, value) in fields)
    Console.WriteLine($"  {key} = {value}");

fields.Remove("Joined");
Console.WriteLine($"Properties left: {fields.Count}");

If you read a property that does not exist, it throws RuntimeBinderException. Use ContainsKey first when you are not sure.

Dynamic Objects

ExpandoObject just stores values. Sometimes you want an object that decides what to do when someone reads a property it does not have.

For that, inherit from DynamicObject and override the methods you need:

OverrideRuns when someone...
TryGetMemberreads obj.Something
TrySetMemberwrites obj.Something = value
TryInvokeMembercalls obj.Something(args)

Each method returns true if it handled the request, or false to make the runtime throw.

A settings object that reads from a dictionary

using System;
using System.Collections.Generic;
using System.Dynamic;

var raw = new Dictionary<string, string>
{
    ["theme"] = "dark",
    ["fontsize"] = "16",
};

dynamic settings = new Settings(raw);

Console.WriteLine(settings.Theme);       // case does not matter here
Console.WriteLine(settings.FontSize);
Console.WriteLine(settings.Language);    // missing: returns a default

settings.Language = "ta-IN";
Console.WriteLine(settings.Language);

class Settings : DynamicObject
{
    private readonly Dictionary<string, string> _values;

    public Settings(Dictionary<string, string> values) =>
        _values = new Dictionary<string, string>(values, StringComparer.OrdinalIgnoreCase);

    public override bool TryGetMember(GetMemberBinder binder, out object? result)
    {
        result = _values.TryGetValue(binder.Name, out var text) ? text : "(not set)";
        return true;
    }

    public override bool TrySetMember(SetMemberBinder binder, object? value)
    {
        _values[binder.Name] = value?.ToString() ?? "";
        return true;
    }
}

binder.Name is the property name the caller wrote. The object turns every settings.Something into a dictionary lookup, ignores upper and lower case, and never throws when a key is missing.

Handling Method Calls

TryInvokeMember handles method calls. This example builds tiny "query" methods from just the method name.

Methods that do not exist until you call them

using System;
using System.Dynamic;
using System.Linq;

var people = new[]
{
    new Person("Asha", "Pune"),
    new Person("Ravi", "Chennai"),
    new Person("Kiran", "Pune"),
};

dynamic finder = new Finder(people);

// Save the dynamic result to a real type before using it.
Person[] inPune = finder.FindByCity("Pune");
Person[] named = finder.FindByName("Ravi");

Console.WriteLine(string.Join(", ", inPune));
Console.WriteLine(string.Join(", ", named));

record Person(string Name, string City)
{
    public override string ToString() => $"{Name} ({City})";
}

class Finder : DynamicObject
{
    private readonly Person[] _people;
    public Finder(Person[] people) => _people = people;

    public override bool TryInvokeMember(InvokeMemberBinder binder, object?[]? args, out object? result)
    {
        result = null;
        if (!binder.Name.StartsWith("FindBy") || args is not { Length: 1 }) return false;

        string field = binder.Name.Substring("FindBy".Length);   // "City" or "Name"
        var property = typeof(Person).GetProperty(field);
        if (property == null) return false;

        result = _people.Where(p => Equals(property.GetValue(p), args[0])).ToArray();
        return true;
    }
}

FindByCity and FindByName are not written anywhere. The object reads the method name, then uses reflection to find the matching property. Old ORMs used this exact trick.

Working With Unknown Types

The most common real reason for dynamic is data whose shape you learn only when the program runs, like JSON from an API.

System.Text.Json does not give you dynamic objects directly. A small helper can turn JSON into ExpandoObjects, and then you can use simple dots.

Turn JSON into dynamic objects

using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Text.Json;

string json = """
{
  "orderId": 5012,
  "customer": { "name": "Meera", "city": "Kochi" },
  "items": [
    { "title": "Notebook", "qty": 3 },
    { "title": "Pen", "qty": 10 }
  ]
}
""";

dynamic order = ToDynamic(JsonDocument.Parse(json).RootElement);

Console.WriteLine($"Order {order.orderId} for {order.customer.name} in {order.customer.city}");
foreach (dynamic item in order.items)
    Console.WriteLine($"  {item.qty} x {item.title}");

static object? ToDynamic(JsonElement e) => e.ValueKind switch
{
    JsonValueKind.Object => e.EnumerateObject().Aggregate(
        (IDictionary<string, object?>)new ExpandoObject(),
        (obj, p) => { obj[p.Name] = ToDynamic(p.Value); return obj; }),
    JsonValueKind.Array => e.EnumerateArray().Select(ToDynamic).ToList(),
    JsonValueKind.Number => e.TryGetInt64(out long n) ? n : e.GetDouble(),
    JsonValueKind.String => e.GetString(),
    JsonValueKind.True => true,
    JsonValueKind.False => false,
    _ => null,
};

Safer Choices for JSON

dynamic is quick to write, but safer choices are usually better:

  • Know the shape? Write a record and use JsonSerializer.Deserialize<Order>(json). You get full type checking.
  • Unknown shape? Use JsonNode: node["customer"]?["name"]?.GetValue<string>(). It is clear about missing values.
  • Quick script or prototype? dynamic is fine.

COM Interop

Before C# 4, automating Excel or Word from C# meant long casts on every line. dynamic made it short. This only runs on Windows with Office, so here it is as plain code:

// Windows only, with Microsoft Excel installed.
dynamic excel = Activator.CreateInstance(Type.GetTypeFromProgID("Excel.Application")!)!;
excel.Visible = true;
dynamic book = excel.Workbooks.Add();
book.ActiveSheet.Cells[1, 1].Value = "Sales";
book.ActiveSheet.Cells[1, 2].Value = 125000;

Reflection vs Dynamic

Reflection and dynamic both work with types the compiler does not know. They solve different problems.

  • Reflection answers "what is there?" It lists members, reads attributes and finds private members.
  • dynamic answers "just call it." You must know the member name when you write the code.

The same job two ways

using System;
using System.Reflection;

object box = new Parcel { Weight = 2.5, City = "Jaipur" };

// Reflection: clear, can discover members, more code
PropertyInfo weightInfo = box.GetType().GetProperty("Weight")!;
double w1 = (double)weightInfo.GetValue(box)!;
Console.WriteLine($"Reflection: {w1} kg");

// dynamic: short, but you must know the name when writing the code
dynamic d = box;
double w2 = d.Weight;
Console.WriteLine($"dynamic:    {w2} kg");

// Only reflection can answer "what properties exist?"
foreach (var p in box.GetType().GetProperties())
    Console.WriteLine($"  found property: {p.Name}");

class Parcel
{
    public double Weight { get; set; }
    public string City { get; set; } = "";
}
QuestionReflectiondynamic
List all members of a type?YesNo
Read attributes?YesNo
Use a member name stored as text?Yes, GetProperty(name)No, the name is in the code
Access private members?Yes, with BindingFlagsNo, uses normal C# rules
Short and easy to read?NoYes
Choose the overload by actual type?You do it manuallyAutomatic
Works with DynamicObject and COM?NoYes

Easy Way to Remember: reflection is a map for exploring. dynamic is a shortcut for when you know where you are going.

Dynamic Type Performance Considerations

dynamic has two costs: speed and safety.

Speed

The first time a dynamic call runs, the runtime binder does the work the compiler normally does. That first call is slow. The runtime then remembers the answer for that type, so later calls are much faster, but still slower than a normal call.

Typed, dynamic and reflection calls compared

using System;
using System.Diagnostics;

var item = new Item { Price = 10 };
dynamic dyn = item;
var info = typeof(Item).GetProperty("Price")!;
const int Rounds = 200_000;
long sum = 0;

var watch = Stopwatch.StartNew();
for (int i = 0; i < Rounds; i++) sum += item.Price;
Console.WriteLine($"Typed:      {watch.ElapsedMilliseconds} ms");

watch.Restart();
for (int i = 0; i < Rounds; i++) sum += dyn.Price;
Console.WriteLine($"dynamic:    {watch.ElapsedMilliseconds} ms");

watch.Restart();
for (int i = 0; i < Rounds; i++) sum += (int)info.GetValue(item)!;
Console.WriteLine($"Reflection: {watch.ElapsedMilliseconds} ms");

class Item { public int Price { get; set; } }

Your numbers will change between runs. Normal typed code is always fastest. A dynamic call site that has been called before is usually as fast as cached reflection.

Safety

The bigger cost is safety:

  • Typos are found only when the line runs, possibly after you ship to production.
  • Renaming a property in Visual Studio does not update dynamic code that uses it.
  • Anything touching dynamic becomes dynamic. If you write var total = order.Price * 2;, then total is also dynamic. The problem spreads.

Stop the spread by saving the result to a real type right away:

decimal total = order.Price * 2;   // now total is typed and checked from here on

A good rule: keep dynamic at the edge of your program, where new data comes in. Convert it to real types immediately, and keep the rest of your code fully typed.

Where Dynamic Types Are Used

  • COM automation: Excel, Word and Outlook from C#.
  • Interop with dynamic languages: IronPython and JavaScript engines.
  • Quick scripts: reading JSON or CSV with a shape that changes.
  • Older ASP.NET MVC: ViewBag.Title = "Home"; is a dynamic object.
  • Testing tools that call private or generated members by name.

Common mistakes

MistakeSymptomFix
Typo in a member nameRuntimeBinderException at runtimeUse a real type when you know it
Extension method on dynamicRuntimeBinderExceptionCast first, or call the static method directly
Reading a missing ExpandoObject propertyRuntimeBinderExceptionCheck with ContainsKey first
var result of a dynamic expressiondynamic spreads everywhereAssign to a real type instead
dynamic inside a hot loopSlow codeUse typed code or a cached delegate

What to Remember

  • dynamic turns off build-time checks. Members are found when the line runs.
  • var is fully typed. object hides the type. dynamic decides at runtime.
  • A missing member throws RuntimeBinderException.
  • With dynamic, the actual type picks the method overload.
  • ExpandoObject lets you add properties at runtime and is also an IDictionary<string, object?>.
  • Inherit from DynamicObject to decide what happens when someone reads, writes or calls.
  • The runtime binder cannot see extension methods. Cast to a real type first.
  • Reflection explores types. dynamic calls members you already know by name.
  • dynamic costs speed on first call and safety always. Keep it at the edges.

Next: Expression Trees