Reputation: 307
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
Reputation: 29868
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