Dapper
Dapper copied to clipboard
C# 9 records
Are there any plans to add support for records (added in C# 9/.NET 5), more specifically positional records?
well, we'd need to think about what that means, since positional records still have names; do we bind by constructor parameter name (which would be my expectation)? or by ordinal position? what would you expect to happen here?
By name sounds most logical to me. See here on how it can be done.
I'm very familiar; right now, for nominal matching, it needs a parameterless constructor, which means that (if we assume the columns aren't an exact match) this doesn't work:
private record PositionalCarRecord(int Age, Car.TrapEnum Trap, string Name)
but this does:
private record PositionalCarRecord(int Age, Car.TrapEnum Trap, string Name)
{
public PositionalCarRecord() : this(default, default, default) { }
}
I'll have to look to see how much work is involved in handling the first case.
I did it by name in spanjson, fwiw it was more work to get mixed records (with extra normal properties) to work than the positional stuff. Additionally https://stackoverflow.com/questions/63097273/c-sharp-9-0-records-reflection-and-generic-constraints might help, but Marc probably already knows about it.
👋🏻 G'Day @mgravell (and others). Is there any news on this? Boy this would be hella-sweet if this was implemented!
I just tried this (and assploded) ..
private record MyData(int Id, string Name, string Address, string Blah);
...
using (var db = new SqlConnection(connectionString))
{
var results = await db.QueryAsync<MyData>(query, new { Name = request.Name });
}
so can't wait to see if this can come into Dapper.
(Side note: I just don't like using dynamic
, which the above code works 💯 fine. I just prefer strongly typed stuff)
In what way exactly did it "assplode". Also, what was query
, and how did that match to the type members? The reason I ask is: we have tests for both nominal and positional records in the current test suite, and they work fine. Also, what version are you testing against?
It may also we worth testing against the myget feed, in case the problem is simply that we haven't pushed it to NuGet: https://www.myget.org/F/dapper/api/v3/index.json
I'm an silly sausage. I tried (myget 2.0.82) + a bugfix (on my side) it worked. I dropped back to 2.0.78 and it also worked.
So it could have always worked.
This was the error message I received:
A parameterless default constructor or one matching signature (System.Int32
, System.String , System.String , System.String , System.String , System.Int32 , System.Int32 , System.Int32 , System.String , System.String , System.Int32 ) is required for materialization
So it seemed to be a strong-type mismatch between my result record set the ctor.
I'm usually a bit more careful than this (im guessing i didn't read the 2nd half of the error message + late night programming + 💤 ) so I'm sorry for potentially wasting your time. 😞
TL;DR; for the rest of the internet: Dapper 2.0.78 is working nicely with records :)
@PureKrome doesn't appear to work with .NET 3.1, complains about parameterless constructor. :sob:
@PureKrome doesn't appear to work with .NET 3.1, complains about parameterless constructor. 😭
AFAIK, records are a C# 9 feature which only works with .NET 5 and above.
You can use records in 3.1, they just don't work 100% like they do in 5.0 it appears.
https://btburnett.com/csharp/2020/12/11/csharp-9-records-and-init-only-setters-without-dotnet5.html
I have found an interesting case that using Class works but Record doesn't:
public record Foo(string EffectiveDate); public record Foo() { public string EffectiveDate { get; set; } }
public class Foo { public string EffectiveDate { get; set; } }
When the table is using Date / Datetime / Datetime2 data type, dapper will throw an exception said it can't match the Record which works fine when I switched back to Class.
I noticed you need to take care about extraneous fields in the record coming from the DB. While with classes or even default-constructable records, this works fine, for positionally-constructed records, you get an error message about a parameterless constructor matching certain arguments and if you look at the message closely, you will notice what's the issue.
Example: Say the type is:
public record Event
{
public Guid Id { get; init; }
public EventType Type { get; init; }
public Guid MapId { get; init; }
}
and you do a query like
select
Id, Type, Timestamp, MapId
from
MyTable
everything will work just fine. But if you change Event
to
public record Event(Guid Id, EventType Type, Guid MapId);
the very same query will lead to an error telling you it needs a constructor matching all four fields.
Now this may seem like a stupid mistake to make, but it's much less obvious if you do something like select *
or, like me, include a field in the query just to sort by (but are not interested in retrieving that field, yet the number of records is small enough you can afford to opt for simplicity over performance), like:
(select
MapCommandId as Id,
InsertDate as Timestamp,
MapId,
0 as Type
from MapCommand
where MapId in @mapIds)
union
(select
Id,
UpdateDate as Timestamp,
RelatedMapId as MapId,
2 as Type
from Task
where RelatedMapId in @mapIds)
union
(select
Id,
UpdateDate as Timestamp,
ChannelId as MapId,
1 as Type
from ChatEntry
where ChannelId in @mapIds)
order by Timestamp asc
A work-around in my case is to wrap the query into another select that just selects the three fields in the record.
So whoever comes here to look at this issue, maybe this will help them. Also, it might not hurt to mention this somewhere prominently in the docs.
I hit theDateTime
parsing issue when database rows are mapped to positional record models. Same as @AnsonWooBizCloud mentions.
I created a source generator for one of my projects that adds a private parameterless constructor to get around the warning
A parameterless default constructor [...] is required
namespace X;
[Srcgen.DapperConstructor]
partial record TestResult(string A, string B)
{
public int C { get; set; }
}
which generates
namespace X
{
public partial record TestResult
{
private TestResult() : this(default!, default!) { }
}
}
Gist with the full code is here: https://gist.github.com/SaahilClaypool/b1aca690f154ec0fe9ed49751988e701