Georgi Rangelov
Georgi Rangelov

Reputation: 307

How can Dapper set a read-only property?

I noticed the following behavior related to Dapper and C# read-only auto-properties (introduced in C# 6).

Dapper is able to map an instance of a class with a read-only property set, which according to my understanding of how read-only properties work should not be possible.

The following code demonstrates this in action:

using Dapper;
using Npgsql;
using System;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class Model
    {
        public long Id { get; }
    }

    class Program
    {
        static async Task<Model> GetData()
        {
            using (var connection = new NpgsqlConnection("..."))
            {
                await connection.OpenAsync();

                var sql = @"
                    SELECT 22 AS Id";

                return await connection.QuerySingleAsync<Model>(sql);
            }
        }

        static async Task Main(string[] args)
        {
            var model = await GetData();
            Console.WriteLine(model.Id);

            var model2 = new Model();
            //model2.Id = 22;

            var model3 = new Model
            {
                //Id = 22;
            };
        }
    }
}

The commented out lines produce compile-time error CS0200 as expected.

No database setup is needed, just a working SQL connection (I'm using Postgres). The model returned from GetData has it's Id property set, if I try to manually create an instance of the Model class I can't set it's Id property as it's read-only.

I was wondering how Dapper can set the property anyway ? I tried following the code in the Dapper repo but it's above my level of comprehension.

I managed to follow up-to SqlMapper.cs#L1530 which is where I think the magic is happening, but beyond that point it's just too hard to follow.

If someone could explain in simple terms I would be grateful :)

Thank you.

Upvotes: 3

Views: 1770

Answers (1)

Under the hood, your declaration:

public long Id { get; }

is actually compiled to the following C# code:

[CompilerGenerated]
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly long <Id>k__BackingField;

public long Id
{
    [CompilerGenerated]
    get
    {
        return <Id>k__BackingField;
    }
}

With Reflection, you can do some pretty crazy things. And one of those things is to set the value of a field marked as readonly. (This has some interesting implications, like being able to set string.Empty to whatever you want!)

In Dapper's case, it's effectively doing the following, but with a lot more error checking:

Model m = <instance of your Model class>;
Type t = m.GetType();
string propertyName = "Id"; // read using Reflection
FieldInfo fi = t.GetField("<" + propertyName + ">k__BackingField",
    BindingFlags.Instance | BindingFlags.NonPublic);
if (fi != null)
    fi.SetValue(m, 22); // 22 = value read from DB

Upvotes: 4

Related Questions