Skip to content

Commit

Permalink
Merge pull request #52 from seesharper/feature-record-types
Browse files Browse the repository at this point in the history
Added support for C# record types
  • Loading branch information
seesharper committed May 31, 2021
2 parents b3ff15d + 3d60db2 commit eb72df1
Show file tree
Hide file tree
Showing 9 changed files with 120 additions and 16 deletions.
27 changes: 23 additions & 4 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,20 @@ var args = new ArgumentsBuilder()
connection.Read<Customer>("SELECT * FROM Customers WHERE Country = @Country AND City = @City",
```

## Record types

**DbReader** supports [C# records](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record) meaning that we can ensure immutability and it also makes it possible to create a class representing the result of a query with minimum effort.
```c#
public record Customer(string CustomerId, string CompanyName)
```

To define key "properties", we can decorate the constructor arguments with the `KeyAttribute`

```c#
public record Customer([Key]string CustomerId, string CompanyName)
```



## List Parameters
Expand Down Expand Up @@ -398,10 +412,10 @@ ON

## Keys

The only requirement with regards to metadata is that a class must declare a property that uniquely identifies an instance of this class. This information is used to determine if we should create a new instance of a class or retrieve it from the query cache. The query cache makes sure that we don't eagerly create new instances of classes that has already been read. The default convention here is that each class must declare a property named *Id* or *[classname]Id*.
For instance there might be desirable to be more explicit about the key properties and there also might be needed to define composite keys. One solution here is to take advantage of the *KeyAttribute* defined in the System.Components.DataAnnotations namespace.
The only requirement with regards to metadata is that a class must declare a property that uniquely identifies an instance of this class. This information is used to determine if we should create a new instance of a class or retrieve it from the query cache. The query cache makes sure that we don't eagerly create new instances of classes that has already been read. The default convention here is that each class must declare a property named *Id*, *[classname]Id* or decorate the key property with the `KeyAttribute` from the `DbReader.Annotations` namespace.

```

```c#
public class OrderDetail
{
[Key]
Expand All @@ -412,10 +426,15 @@ public class OrderDetail
}
```
The convention can easily be changes through the DbReaderOptions class.
```
```c#
DbReaderOptions.KeyConvention = (property) => property.IsDefined(typeof(KeyAttribute));
```

Another possibility is to supply this information using the DbReaderOptions`KeySelector` method.

```c#
DbReaderOptions.KeySelector<Customer>(c => c.CustomerId)
```

## Prefixes

Expand Down
2 changes: 1 addition & 1 deletion src/DbReader.Tests/DbReader.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<PackageReference Include="System.Reflection.Emit.Lightweight" Version="4.7.0" />
</ItemGroup>
<PropertyGroup>
<TargetFrameworks>netcoreapp2.1;net462</TargetFrameworks>
<TargetFrameworks>net50;net462</TargetFrameworks>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
</PropertyGroup>
<ItemGroup>
Expand Down
38 changes: 38 additions & 0 deletions src/DbReader.Tests/IntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ namespace DbReader.Tests
using Xunit;
using System.Text;
using System.Data.SQLite;
using DbReader.Annotations;

public class IntegrationTests
{
Expand Down Expand Up @@ -139,6 +140,33 @@ public void ShouldReadCustomersAndOrders()
}
}

[Fact]
public void ShouldReadCustomersAsRecordType()
{
using (var connection = CreateConnection())
{
var test = SQL.Customers;
var customers = connection.Read<CustomerRecord>("SELECT CustomerID, CompanyName FROM Customers");
customers.Count().ShouldBe(93);
}
}

[Fact]
public void ShouldReadCustomersAndOrdersAsRecordType()
{

// DbReaderOptions.KeySelector<CustomerRecordWithOrders>(c => c.CustomerId);
// DbReaderOptions.KeySelector<OrderRecord>(o => o.OrderId);

using (var connection = CreateConnection())
{
var test = SQL.Customers;
var customers = connection.Read<CustomerRecordWithOrders>("SELECT c.CustomerID, c.CompanyName, o.OrderId as Orders_OrderId, o.OrderDate as Orders_OrderDate FROM Customers c INNER JOIN Orders o ON o.CustomerId = c.CustomerId");
customers.Count().ShouldBe(89);
customers.SelectMany(c => c.Orders).Count().ShouldBe(830);
}
}

[Fact]
public async Task ShouldReadCustomersAndOrdersAsync()
{
Expand Down Expand Up @@ -559,5 +587,15 @@ public CustomScalarValue(long value)

public long Value { get; }
}

public record CustomerRecord(string CustomerId, string CompanyName)
{
}

public record CustomerRecordWithOrders([Key] string CustomerId, string CompanyName, ICollection<OrderRecord> Orders)
{
}

public record OrderRecord([Key] long OrderId, DateTime? OrderDate);
}
#endif
9 changes: 9 additions & 0 deletions src/DbReader/Annotations/KeyAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System;

namespace DbReader.Annotations
{
[AttributeUsage(validOn: AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)]
public class KeyAttribute : Attribute
{
}
}
31 changes: 23 additions & 8 deletions src/DbReader/Construction/PropertyReaderMethodBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,19 @@
using System.Data;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using Extensions;
using Readers;
using Selectors;

/// <summary>
/// A class that is capable of creating a delegate that creates and populates an instance of <typeparamref name="T"/> from an
/// A class that is capable of creating a delegate that creates and populates an instance of <typeparamref name="T"/> from an
/// <see cref="IDataRecord"/>.
/// </summary>
/// <typeparam name="T">The type of object to be created.</typeparam>
public class PropertyReaderMethodBuilder<T> : ReaderMethodBuilder<T>
{
/// <typeparam name="T">The type of object to be created.</typeparam>
public class PropertyReaderMethodBuilder<T> : ReaderMethodBuilder<T>
{
private readonly IPropertySelector simplePropertySelector;

private readonly IPropertySelector oneToManyPropertySelector;
Expand All @@ -27,7 +29,7 @@ public class PropertyReaderMethodBuilder<T> : ReaderMethodBuilder<T>
/// </summary>
/// <param name="methodSkeletonFactory">The <see cref="IMethodSkeletonFactory"/> that is responsible for
/// creating an <see cref="IMethodSkeleton"/>.</param>
/// <param name="methodSelector">The <see cref="IMethodSelector"/> that is responsible for selecting the <see cref="IDataRecord"/>
/// <param name="methodSelector">The <see cref="IMethodSelector"/> that is responsible for selecting the <see cref="IDataRecord"/>
/// get method that corresponds to the property type.</param>
/// <param name="simplePropertySelector">The <see cref="IPropertySelector"/> that is responsible for selecting the target properties.</param>
/// <param name="oneToManyPropertySelector"></param>
Expand Down Expand Up @@ -76,7 +78,7 @@ private void InitializeEnumerableProperty(ILGenerator il, LocalBuilder instanceV
var collectionType = typeof(Collection<>).MakeGenericType(projectionType);
var collectionConstructor = collectionType.GetConstructor(Type.EmptyTypes);
var setMethod = property.GetSetMethod();
LoadInstance(il, instanceVariable);
LoadInstance(il, instanceVariable);
il.Emit(OpCodes.Newobj, collectionConstructor);
il.Emit(OpCodes.Callvirt, setMethod);
}
Expand All @@ -95,8 +97,21 @@ private static void EmitCallPropertySetterMethod(ILGenerator il, PropertyInfo pr

private static LocalBuilder EmitNewInstance(ILGenerator il, ConstructorInfo constructorInfo)
{
il.Emit(OpCodes.Newobj, constructorInfo);
LocalBuilder instanceVariable = il.DeclareLocal(typeof(T));
if (typeof(T).IsRecordType())
{
var getUninitializedObjectMethod = typeof(FormatterServices).GetMethod(nameof(FormatterServices.GetUninitializedObject), BindingFlags.Static | BindingFlags.Public);
var getTypeFromHandleMethod = typeof(Type).GetMethod(nameof(Type.GetTypeFromHandle), BindingFlags.Static | BindingFlags.Public);
il.Emit(OpCodes.Ldtoken, typeof(T));
il.Emit(OpCodes.Call, getTypeFromHandleMethod);
il.Emit(OpCodes.Call, getUninitializedObjectMethod);
il.Emit(OpCodes.Castclass, typeof(T));
}
else
{
il.Emit(OpCodes.Newobj, constructorInfo);

}
il.Emit(OpCodes.Stloc, instanceVariable);
return instanceVariable;
}
Expand Down Expand Up @@ -125,6 +140,6 @@ private void EmitPropertySetter(ILGenerator il, PropertyInfo property, int prope
EmitGetValue(il, propertyIndex, getMethod, property.PropertyType);
EmitCallPropertySetterMethod(il, property);
il.MarkLabel(endLabel);
}
}
}
}
2 changes: 1 addition & 1 deletion src/DbReader/DbReader.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<PropertyGroup>
<TargetFrameworks>netcoreapp2.1;netstandard2.0;net462</TargetFrameworks>
<!-- <TargetFramework>netcoreapp2.0</TargetFramework> -->
<Version>2.3.7</Version>
<Version>2.4.0</Version>
<Authors>Bernhard Richter</Authors>
<PackageProjectUrl>https://github.com/seesharper/DbReader</PackageProjectUrl>
<RepositoryType>git</RepositoryType>
Expand Down
10 changes: 9 additions & 1 deletion src/DbReader/DbReaderOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using System.Threading;
using System.Threading.Tasks;
using Database;
using DbReader.Annotations;
using DbReader.Extensions;
using Interfaces;
using Selectors;
Expand All @@ -36,11 +37,18 @@ static DbReaderOptions()
{
KeyConvention = p =>
p.Name.Equals("Id", StringComparison.OrdinalIgnoreCase)
|| p.Name.Equals(p.DeclaringType.Name + "Id", StringComparison.OrdinalIgnoreCase);
|| p.Name.Equals(p.DeclaringType.Name + "Id", StringComparison.OrdinalIgnoreCase) || p.IsDefined(typeof(KeyAttribute), true) || HasConstructorWithKeyParameterMatchingProperty(p);
ParameterParser = new RegExParameterParser(@"(:\w+)|(@\w+)", @"IN\s*\((\s*(?:@|:)\w+)\s*\)");
}

private static bool HasConstructorWithKeyParameterMatchingProperty(PropertyInfo property)
{
var constructors = property.DeclaringType.GetConstructors();
return constructors.SelectMany(c => c.GetParameters()).Any(c => c.Name.Equals(property.Name, StringComparison.Ordinal) && c.IsDefined(typeof(KeyAttribute), true));
}



/// <summary>
/// Allows a custom conversion specified when reading a property of type <typeparamref name="TProperty"/>.
/// </summary>
Expand Down
10 changes: 10 additions & 0 deletions src/DbReader/Extensions/TypeReflectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,16 @@ public static Type GetProjectionType(this Type type)
return ProjectionTypeCache.GetOrAdd(type, ResolveProjectionType);
}

/// <summary>
/// Determines if the given <paramref name="type"/> is a record type.
/// </summary>
/// <param name="type">The <see cref="Type"/> to be checked.</param>
/// <returns>true if the type if a record type, otherwise false.</returns>
public static bool IsRecordType(this Type type)
{
return type.GetMethods().Any(m => m.Name == "<Clone>$");
}

private static bool IsEnumerableOfT(Type @interface)
{
return @interface.GetTypeInfo().IsGenericType && @interface.GetGenericTypeDefinition() == typeof(IEnumerable<>);
Expand Down
7 changes: 6 additions & 1 deletion src/DbReader/Selectors/ParameterlessConstructorSelector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
{
using System;
using System.Reflection;
using DbReader.Extensions;

/// <summary>
/// An <see cref="IConstructorSelector"/> that looks for a parameterless
/// An <see cref="IConstructorSelector"/> that looks for a parameterless
/// constructor for a given <see cref="Type"/>.
/// </summary>
public class ParameterlessConstructorSelector : IConstructorSelector
Expand All @@ -16,6 +17,10 @@ public class ParameterlessConstructorSelector : IConstructorSelector
/// <returns><see cref="ConstructorInfo"/></returns>
public ConstructorInfo Execute(Type type)
{
if (type.IsRecordType())
{
return type.GetConstructors()[0];
}
return type.GetConstructor(Type.EmptyTypes);
}
}
Expand Down

0 comments on commit eb72df1

Please sign in to comment.