Dapper icon indicating copy to clipboard operation
Dapper copied to clipboard

C# 9 records

Open silkfire opened this issue 3 years ago • 16 comments

Are there any plans to add support for records (added in C# 9/.NET 5), more specifically positional records?

silkfire avatar Nov 16 '20 11:11 silkfire

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?

mgravell avatar Nov 16 '20 12:11 mgravell

By name sounds most logical to me. See here on how it can be done.

silkfire avatar Nov 16 '20 13:11 silkfire

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.

mgravell avatar Nov 16 '20 15:11 mgravell

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.

Tornhoof avatar Nov 22 '20 19:11 Tornhoof

👋🏻 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)

PureKrome avatar Jan 15 '21 11:01 PureKrome

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?

mgravell avatar Jan 15 '21 14:01 mgravell

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

mgravell avatar Jan 15 '21 14:01 mgravell

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 avatar Jan 18 '21 02:01 PureKrome

@PureKrome doesn't appear to work with .NET 3.1, complains about parameterless constructor. :sob:

phillip-haydon avatar Mar 24 '21 07:03 phillip-haydon

@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.

silkfire avatar Mar 24 '21 08:03 silkfire

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

phillip-haydon avatar Mar 24 '21 08:03 phillip-haydon

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.

AnsonWooBizCloud avatar Jun 03 '21 15:06 AnsonWooBizCloud

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.

ModernRonin avatar Dec 02 '21 18:12 ModernRonin

I hit theDateTime parsing issue when database rows are mapped to positional record models. Same as @AnsonWooBizCloud mentions.

hmih avatar Apr 06 '23 07:04 hmih

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

SaahilClaypool avatar May 31 '23 19:05 SaahilClaypool