Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why/how does dynamic work with generics?

tl;dr How does dynamic work going into a method taking a IQueryable<T> generic? And why does it then not work trying to use methods available to IQueryable<T> directly against the dynamic instance?

For a quick background, I was working on a test project to check our Entity Framework context against a database, to be better prepared whenever we move environments. I initially came up with this simple test just to see if the entity would load according to the existing schema:

private static bool CheckSchema<T>(IQueryable<T> dbSet)
{
    try
    {
        dbSet.FirstOrDefault();
        return true;
    }
    catch
    {
        return false;
    }
}

private async void TestSchemaButton_Click(object sender, RoutedEventArgs e)
{
    LayoutRoot.IsEnabled = false;
    ResultsPane.Text = "";

    var cStrings = LoadEnvironmentConnectionStrings();
    var results = new StringBuilder();

    ResultsPane.Text += "--- Testing " + nameof(MyProject.MyDbContext1) + "...";
    await Task.Run(() =>
    {
        using (var db = new MyProject.MyDbContext1())
        {
            db.Database.Connection.ConnectionString = cStrings.Item1;

            if (!CheckSchema(db.MyDbSetProperty1)) results.AppendLine(nameof(MyProject.MyDbContext1.MyDbSetProperty1) + " has invalid schema.");
            // ...
            if (!CheckSchema(db.MyDbSetPropertyN)) results.AppendLine(nameof(MyProject.MyDbContext1.MyDbSetPropertyN) + " has invalid schema.");
        }
    });
    ResultsPane.Text += Environment.NewLine + results.ToString() + 
                        "--- End " + nameof(MyProject.MyDbContext1) + Environment.NewLine + Environment.NewLine;
}

This is pretty simple, and accomplished what I was after. However, I have multiple DB contexts to test, some with many, many entities. I'm a programmer, so I'm of course lazy, and even though it's mostly copy-paste, there's still a lot of renaming to do. Since performance wasn't a concern for this manually-run, test project, I figured I could just get all the entities to test via reflection.

It was at this point I became initially stuck, since I needed my property to be an IQueryable<T>, but I have no idea how to cast when a generic is involved. I realized I could cheat a bit by using dynamic and thus my Task body could become:

using (var db = new MyProject.MyDbContext1())
{
    db.Database.Connection.ConnectionString = connectionString;

    var dbSetProps = db.GetType().GetProperties().Where(pi => pi.PropertyType.Name == "DbSet`1");

    foreach (var dbSetProp in dbSetProps)
    {
        dynamic dbSetVal = dbSetProp.GetValue(db);
        if (!CheckSchema(dbSetVal))
            results.AppendLine(dbSetProp.Name + " has invalid schema.");
    }
}

I was pleasantly surprised to find that this simply worked. What I don't get is how? I figured that even as versatile as generics can be, they are still a compile-time construct.

Additionally, I figured that I could change my whole CheckSchema method to do all the work that was in the body of the Task, so I updated as so:

private static string CheckSchema<T>(string connectionString) where T : DbContext, new()
{
    var invalidSchemas = new StringBuilder();
    using (var db = new T())
    {
        db.Database.Connection.ConnectionString = connectionString;

        var dbSetProps = db.GetType().GetProperties().Where(pi => pi.PropertyType.Name == "DbSet`1");

        foreach (var dbSetProp in dbSetProps)
        {
            dynamic dbSetVal = dbSetProp.GetValue(db);
            try
            {
                dbSetVal.FirstOrDefault();
            }
            catch
            {
                invalidSchemas.AppendLine(dbSetProp.Name + " has invalid schema.");
            }
        }
    }
    return invalidSchemas.ToString();
}

But now it always encounters an exception at dbSetVal.FirstOrDefault() saying that "DbSet<MyProject.MyDbContext1.MyDbSetPropertyNEntityType> does not contain a method FirstOrDefault." I'm fine going back to my second implementation, as it is still pretty compact. However, it's got me wondering, what's going on?

like image 436
Mike Guthrie Avatar asked Mar 31 '26 16:03

Mike Guthrie


1 Answers

dynamic essentially tells the compiler to bypass static checks. This means that all binding must be done at run-time, not at compile time.

IQueryable<Person> people = GetPeople();
dynamic dPerson = people;

Here, dPerson is still an IQueryable<Person>, but we've specifically asked the compiler not to create any compile-time assumptions about it. If one were to write dPerson.ToString(), the method ToString would only be resolved as it's being called.

The consequence of this is, that you lose the syntactic sugar of extension methods. Writing the following:

people.FirstOrDefault() is actually writing Queryable.FirstOrDefault(people);

You cannot use extension methods on dynamic objects. At runtime, it will look for the method FirstOrDefault on the object itself, and will not check for possible extension methods.

You need to write, instead, Queryable.FirstOrDefault(dbSetVal);

like image 134
Rob Avatar answered Apr 03 '26 17:04

Rob



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!