jonwagner / Insight.Database

Fast, lightweight .NET micro-ORM
Other
859 stars 145 forks source link

Support for C# 9 records #450

Closed bartecargo closed 3 years ago

bartecargo commented 3 years ago

Support for C# 9 records

C# 9 introduced records, which is a concise way to write a POCO. Since we use a lot of simple types to read data using Insight.Database, this would be a heavily used use-case for us.

Sample code:

Program.cs:

using Insight.Database;
using Microsoft.Data.SqlClient;

var db = new SqlConnection("Trusted_connection=true");
await db.QuerySqlAsync<SimpleType>("select 1");

record SimpleType(int Value);

And the project file:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
    <LangVersion>default</LangVersion>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Insight.Database" Version="6.3.3" />
  </ItemGroup>

</Project>

Right now, this program fails with the following error:


> dotnet run
Unhandled exception. System.InvalidOperationException: Cannot find a default constructor for type SimpleType, and there was more than one constructor, but no DbConstructorAttribute was specified.
   at Insight.Database.CodeGenerator.ClassDeserializerGenerator.SelectConstructor(Type type)
   at Insight.Database.CodeGenerator.ClassDeserializerGenerator.CreateClassDeserializerDynamicMethod(Type type, IDataReader reader, IRecordStructure structure, Int32 startColumn, Int32 columnCount, Boolean createNewObject, Boolean isRootObject, Boolean allowBindChild, Boolean checkForAllDbNull)
   at Insight.Database.CodeGenerator.ClassDeserializerGenerator.CreateClassDeserializer(Type type, IDataReader reader, IRecordStructure structure, Int32 startColumn, Int32 columnCount, Boolean createNewObject)
   at Insight.Database.CodeGenerator.ClassDeserializerGenerator.CreateDeserializer(IDataReader reader, Type type, IRecordStructure structure, SchemaMappingType mappingType)
   at Insight.Database.CodeGenerator.DbReaderDeserializer.<>c__DisplayClass8_0.<GetDeserializer>b__0(SchemaMappingIdentity key)
   at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
   at Insight.Database.CodeGenerator.DbReaderDeserializer.GetDeserializer(IDataReader reader, Type type, IRecordStructure structure, SchemaMappingType mappingType)
   at Insight.Database.CodeGenerator.DbReaderDeserializer.GetDeserializer[T](IDataReader reader, IRecordStructure structure)
   at Insight.Database.OneToOne`1.GetRecordReader(IDataReader reader)
   at Insight.Database.DBConnectionExtensions.AsyncReader`1.MoveNextAsync()
   at Insight.Database.DBConnectionExtensions.ToListAsync[T](IDataReader reader, IRecordReader`1 recordReader, CancellationToken cancellationToken, Boolean firstRecordOnly)
   at Insight.Database.Structure.ListReader`1.ReadAsync(IDbCommand command, IDataReader reader, CancellationToken cancellationToken)
   at Insight.Database.DBConnectionExtensions.ExecuteAsyncAndAutoClose[T](IDbConnection connection, Object parameters, Func`2 getCommand, Boolean callGetReader, Func`3 translate, CommandBehavior commandBehavior, CancellationToken cancellationToken, Object outputParameters)
   at <Program>$.<<Main>$>d__0.MoveNext() in D:\temp\repro\Program.cs:line 5
--- End of stack trace from previous location ---
   at <Program>$.<Main>(String[] args)```
jonwagner commented 3 years ago

I was just thinking that I hadn't seen a new request in a while... I agree. This would be a nice feature to add.

I haven't seen the IL that C# generates for the record classes. It's going to be either very easy or very hard. I'll try to find some upcoming time to work on it. Meanwhile, if someone can run ILDASM on a small record class and post it, that would be really helpful.

bartecargo commented 3 years ago

The following code:

record SimpleType(int Value);

Generates (in the Rider IDE):

.class private auto ansi beforefieldinit
  SimpleType
    extends [System.Runtime]System.Object
    implements class [System.Runtime]System.IEquatable`1<class SimpleType>
{
  .custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor([in] unsigned int8)
    = (01 00 01 00 00 ) // .....
    // unsigned int8(1) // 0x01
  .custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor([in] unsigned int8)
    = (01 00 00 00 00 ) // .....
    // unsigned int8(0) // 0x00
  .interfaceimpl type class [System.Runtime]System.IEquatable`1<class SimpleType>
    .custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor([in] unsigned int8)
      = (01 00 00 00 00 ) // .....
      // unsigned int8(0) // 0x00

  .field private initonly int32 '<Value>k__BackingField'
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
      = (01 00 00 00 )
    .custom instance void [System.Runtime]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [System.Runtime]System.Diagnostics.DebuggerBrowsableState)
      = (01 00 00 00 00 00 00 00 ) // ........
      // int32(0) // 0x00000000

  .method public hidebysig specialname rtspecialname instance void
    .ctor(
      int32 Value
    ) cil managed
  {
    .maxstack 8

    // [1 19 - 1 28]
    IL_0000: ldarg.0      // this
    IL_0001: ldarg.1      // Value
    IL_0002: stfld        int32 SimpleType::'<Value>k__BackingField'

    // [1 1 - 1 30]
    IL_0007: ldarg.0      // this
    IL_0008: call         instance void [System.Runtime]System.Object::.ctor()
    IL_000d: nop
    IL_000e: ret

  } // end of method SimpleType::.ctor

  .method family hidebysig virtual newslot specialname instance class [System.Runtime]System.Type
    get_EqualityContract() cil managed
  {
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
      = (01 00 00 00 )
    .maxstack 8

    IL_0000: ldtoken      SimpleType
    IL_0005: call         class [System.Runtime]System.Type [System.Runtime]System.Type::GetTypeFromHandle(valuetype [System.Runtime]System.RuntimeTypeHandle)
    IL_000a: ret

  } // end of method SimpleType::get_EqualityContract

  .method public hidebysig specialname instance int32
    get_Value() cil managed
  {
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
      = (01 00 00 00 )
    .maxstack 8

    // [1 19 - 1 28]
    IL_0000: ldarg.0      // this
    IL_0001: ldfld        int32 SimpleType::'<Value>k__BackingField'
    IL_0006: ret

  } // end of method SimpleType::get_Value

  .method public hidebysig specialname instance void modreq ([System.Runtime]System.Runtime.CompilerServices.IsExternalInit)
    set_Value(
      int32 'value'
    ) cil managed
  {
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
      = (01 00 00 00 )
    .maxstack 8

    // [1 19 - 1 28]
    IL_0000: ldarg.0      // this
    IL_0001: ldarg.1      // 'value'
    IL_0002: stfld        int32 SimpleType::'<Value>k__BackingField'
    IL_0007: ret

  } // end of method SimpleType::set_Value

  .method public hidebysig virtual instance string
    ToString() cil managed
  {
    .custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor([in] unsigned int8)
      = (01 00 00 00 00 ) // .....
      // unsigned int8(0) // 0x00
    .maxstack 2
    .locals init (
      [0] class [System.Runtime]System.Text.StringBuilder V_0
    )

    IL_0000: newobj       instance void [System.Runtime]System.Text.StringBuilder::.ctor()
    IL_0005: stloc.0      // V_0
    IL_0006: ldloc.0      // V_0
    IL_0007: ldstr        "SimpleType"
    IL_000c: callvirt     instance class [System.Runtime]System.Text.StringBuilder [System.Runtime]System.Text.StringBuilder::Append(string)
    IL_0011: pop
    IL_0012: ldloc.0      // V_0
    IL_0013: ldstr        " { "
    IL_0018: callvirt     instance class [System.Runtime]System.Text.StringBuilder [System.Runtime]System.Text.StringBuilder::Append(string)
    IL_001d: pop
    IL_001e: ldarg.0      // this
    IL_001f: ldloc.0      // V_0
    IL_0020: callvirt     instance bool SimpleType::PrintMembers(class [System.Runtime]System.Text.StringBuilder)
    IL_0025: brfalse.s    IL_0033
    IL_0027: ldloc.0      // V_0
    IL_0028: ldstr        " "
    IL_002d: callvirt     instance class [System.Runtime]System.Text.StringBuilder [System.Runtime]System.Text.StringBuilder::Append(string)
    IL_0032: pop
    IL_0033: ldloc.0      // V_0
    IL_0034: ldstr        "}"
    IL_0039: callvirt     instance class [System.Runtime]System.Text.StringBuilder [System.Runtime]System.Text.StringBuilder::Append(string)
    IL_003e: pop
    IL_003f: ldloc.0      // V_0
    IL_0040: callvirt     instance string [System.Runtime]System.Object::ToString()
    IL_0045: ret

  } // end of method SimpleType::ToString

  .method family hidebysig virtual newslot instance bool
    PrintMembers(
      class [System.Runtime]System.Text.StringBuilder builder
    ) cil managed
  {
    .maxstack 2
    .locals init (
      [0] int32 V_0
    )

    IL_0000: ldarg.1      // builder
    IL_0001: ldstr        "Value"
    IL_0006: callvirt     instance class [System.Runtime]System.Text.StringBuilder [System.Runtime]System.Text.StringBuilder::Append(string)
    IL_000b: pop
    IL_000c: ldarg.1      // builder
    IL_000d: ldstr        " = "
    IL_0012: callvirt     instance class [System.Runtime]System.Text.StringBuilder [System.Runtime]System.Text.StringBuilder::Append(string)
    IL_0017: pop
    IL_0018: ldarg.1      // builder
    IL_0019: ldarg.0      // this
    IL_001a: call         instance int32 SimpleType::get_Value()
    IL_001f: stloc.0      // V_0
    IL_0020: ldloca.s     V_0
    IL_0022: constrained. [System.Runtime]System.Int32
    IL_0028: callvirt     instance string [System.Runtime]System.Object::ToString()
    IL_002d: callvirt     instance class [System.Runtime]System.Text.StringBuilder [System.Runtime]System.Text.StringBuilder::Append(string)
    IL_0032: pop
    IL_0033: ldc.i4.1
    IL_0034: ret

  } // end of method SimpleType::PrintMembers

  .method public hidebysig static specialname bool
    op_Inequality(
      class SimpleType r1,
      class SimpleType r2
    ) cil managed
  {
    .custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor([in] unsigned int8)
      = (01 00 02 00 00 ) // .....
      // unsigned int8(2) // 0x02
    .maxstack 8

    IL_0000: ldarg.0      // r1
    IL_0001: ldarg.1      // r2
    IL_0002: call         bool SimpleType::op_Equality(class SimpleType, class SimpleType)
    IL_0007: ldc.i4.0
    IL_0008: ceq
    IL_000a: ret

  } // end of method SimpleType::op_Inequality

  .method public hidebysig static specialname bool
    op_Equality(
      class SimpleType r1,
      class SimpleType r2
    ) cil managed
  {
    .custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor([in] unsigned int8)
      = (01 00 02 00 00 ) // .....
      // unsigned int8(2) // 0x02
    .maxstack 8

    IL_0000: ldarg.0      // r1
    IL_0001: ldarg.1      // r2
    IL_0002: beq.s        IL_0013
    IL_0004: ldarg.0      // r1
    IL_0005: brfalse.s    IL_0010
    IL_0007: ldarg.0      // r1
    IL_0008: ldarg.1      // r2
    IL_0009: callvirt     instance bool SimpleType::Equals(class SimpleType)
    IL_000e: br.s         IL_0011
    IL_0010: ldc.i4.0
    IL_0011: br.s         IL_0014
    IL_0013: ldc.i4.1
    IL_0014: ret

  } // end of method SimpleType::op_Equality

  .method public hidebysig virtual instance int32
    GetHashCode() cil managed
  {
    .maxstack 8

    IL_0000: call         class [System.Collections]System.Collections.Generic.EqualityComparer`1<!0/*class [System.Runtime]System.Type*/> class [System.Collections]System.Collections.Generic.EqualityComparer`1<class [System.Runtime]System.Type>::get_Default()
    IL_0005: ldarg.0      // this
    IL_0006: callvirt     instance class [System.Runtime]System.Type SimpleType::get_EqualityContract()
    IL_000b: callvirt     instance int32 class [System.Collections]System.Collections.Generic.EqualityComparer`1<class [System.Runtime]System.Type>::GetHashCode(!0/*class [System.Runtime]System.Type*/)
    IL_0010: ldc.i4       -1521134295 // 0xa5555529
    IL_0015: mul
    IL_0016: call         class [System.Collections]System.Collections.Generic.EqualityComparer`1<!0/*int32*/> class [System.Collections]System.Collections.Generic.EqualityComparer`1<int32>::get_Default()
    IL_001b: ldarg.0      // this
    IL_001c: ldfld        int32 SimpleType::'<Value>k__BackingField'
    IL_0021: callvirt     instance int32 class [System.Collections]System.Collections.Generic.EqualityComparer`1<int32>::GetHashCode(!0/*int32*/)
    IL_0026: add
    IL_0027: ret

  } // end of method SimpleType::GetHashCode

  .method public hidebysig virtual instance bool
    Equals(
      object obj
    ) cil managed
  {
    .custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor([in] unsigned int8)
      = (01 00 02 00 00 ) // .....
      // unsigned int8(2) // 0x02
    .maxstack 8

    IL_0000: ldarg.0      // this
    IL_0001: ldarg.1      // obj
    IL_0002: isinst       SimpleType
    IL_0007: callvirt     instance bool SimpleType::Equals(class SimpleType)
    IL_000c: ret

  } // end of method SimpleType::Equals

  .method public hidebysig virtual newslot instance bool
    Equals(
      class SimpleType other
    ) cil managed
  {
    .custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor([in] unsigned int8)
      = (01 00 02 00 00 ) // .....
      // unsigned int8(2) // 0x02
    .maxstack 8

    IL_0000: ldarg.1      // other
    IL_0001: brfalse.s    IL_002e
    IL_0003: ldarg.0      // this
    IL_0004: callvirt     instance class [System.Runtime]System.Type SimpleType::get_EqualityContract()
    IL_0009: ldarg.1      // other
    IL_000a: callvirt     instance class [System.Runtime]System.Type SimpleType::get_EqualityContract()
    IL_000f: call         bool [System.Runtime]System.Type::op_Equality(class [System.Runtime]System.Type, class [System.Runtime]System.Type)
    IL_0014: brfalse.s    IL_002e
    IL_0016: call         class [System.Collections]System.Collections.Generic.EqualityComparer`1<!0/*int32*/> class [System.Collections]System.Collections.Generic.EqualityComparer`1<int32>::get_Default()
    IL_001b: ldarg.0      // this
    IL_001c: ldfld        int32 SimpleType::'<Value>k__BackingField'
    IL_0021: ldarg.1      // other
    IL_0022: ldfld        int32 SimpleType::'<Value>k__BackingField'
    IL_0027: callvirt     instance bool class [System.Collections]System.Collections.Generic.EqualityComparer`1<int32>::Equals(!0/*int32*/, !0/*int32*/)
    IL_002c: br.s         IL_002f
    IL_002e: ldc.i4.0
    IL_002f: ret

  } // end of method SimpleType::Equals

  .method public hidebysig virtual newslot instance class SimpleType
    '<Clone>$'() cil managed
  {
    .maxstack 8

    IL_0000: ldarg.0      // this
    IL_0001: newobj       instance void SimpleType::.ctor(class SimpleType)
    IL_0006: ret

  } // end of method SimpleType::'<Clone>$'

  .method family hidebysig specialname rtspecialname instance void
    .ctor(
      class SimpleType original
    ) cil managed
  {
    .maxstack 8

    IL_0000: ldarg.0      // this
    IL_0001: call         instance void [System.Runtime]System.Object::.ctor()
    IL_0006: nop
    IL_0007: ldarg.0      // this
    IL_0008: ldarg.1      // original
    IL_0009: ldfld        int32 SimpleType::'<Value>k__BackingField'
    IL_000e: stfld        int32 SimpleType::'<Value>k__BackingField'
    IL_0013: ret

  } // end of method SimpleType::.ctor

  .method public hidebysig instance void
    Deconstruct(
      [out] int32& Value
    ) cil managed
  {
    .maxstack 8

    IL_0000: ldarg.1      // Value
    IL_0001: ldarg.0      // this
    IL_0002: call         instance int32 SimpleType::get_Value()
    IL_0007: stind.i4
    IL_0008: ret

  } // end of method SimpleType::Deconstruct

  .property instance class [System.Runtime]System.Type EqualityContract()
  {
    .get instance class [System.Runtime]System.Type SimpleType::get_EqualityContract()
  } // end of property SimpleType::EqualityContract

  .property instance int32 Value()
  {
    .get instance int32 SimpleType::get_Value()
    .set instance void modreq ([System.Runtime]System.Runtime.CompilerServices.IsExternalInit) SimpleType::set_Value(int32)
  } // end of property SimpleType::Value
} // end of class SimpleType
bartecargo commented 3 years ago

A simplified overview of the above IL code generated by ILSpy from the assembly:

// SimpleType
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text;

internal class SimpleType : IEquatable<SimpleType>
{
    protected virtual Type EqualityContract
    {
        [CompilerGenerated]
        get
        {
            return typeof(SimpleType);
        }
    }

    public int Value
    {
        get;
        set/*init*/;
    }

    public SimpleType(int Value)
    {
        this.Value = Value;
        base..ctor();
    }

    public override string ToString()
    {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.Append("SimpleType");
        stringBuilder.Append(" { ");
        if (PrintMembers(stringBuilder))
        {
            stringBuilder.Append(" ");
        }
        stringBuilder.Append("}");
        return stringBuilder.ToString();
    }

    protected virtual bool PrintMembers(StringBuilder builder)
    {
        builder.Append("Value");
        builder.Append(" = ");
        builder.Append(Value.ToString());
        return true;
    }

    [System.Runtime.CompilerServices.NullableContext(2)]
    public static bool operator !=(SimpleType? r1, SimpleType? r2)
    {
        return !(r1 == r2);
    }

    [System.Runtime.CompilerServices.NullableContext(2)]
    public static bool operator ==(SimpleType? r1, SimpleType? r2)
    {
        return (object)r1 == r2 || (r1?.Equals(r2) ?? false);
    }

    public override int GetHashCode()
    {
        return EqualityComparer<Type>.Default.GetHashCode(EqualityContract) * -1521134295 + EqualityComparer<int>.Default.GetHashCode(Value);
    }

    public override bool Equals(object? obj)
    {
        return Equals(obj as SimpleType);
    }

    public virtual bool Equals(SimpleType? other)
    {
        return (object)other != null && EqualityContract == other!.EqualityContract && EqualityComparer<int>.Default.Equals(Value, other!.Value);
    }

    public virtual SimpleType <Clone>$()
    {
        return new SimpleType(this);
    }

    protected SimpleType(SimpleType original)
    {
        Value = original.Value;
    }

    public void Deconstruct(out int Value)
    {
        Value = this.Value;
    }
}
jonwagner commented 3 years ago

Interesting...if that's the generated code it should just work. Guess I'll just have to do a little debugging.

bartecargo commented 3 years ago

Is the additional protected constructor tripping it up?

jonwagner commented 3 years ago

Oh yeah that's it. :thinking: how best to handle that. Oh wait can just filter on the public one without breaking things I think.

jonwagner commented 3 years ago

It looks like the simple fix in the c9-records branch will do it, but I have to get my computer fully working with net5.0 before I release it.

jonwagner commented 3 years ago

This should work in v6.3.4. There may be a few other scenarios where we need some tweaks but I think it should all work. Let me know how it goes.

nzbart commented 3 years ago

That works perfectly, thanks!

Jaxelr commented 3 years ago

Closing then.