diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 45cefd4..10e97ea 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,7 +16,7 @@ jobs: run: dotnet tool install dotnet-script --global - name: Install dotnet-ilverify - run: dotnet tool install dotnet-ilverify --global + run: dotnet tool install dotnet-ilverify --global --version 8.0.0 - name: Run build script run: dotnet-script build/build.csx diff --git a/readme.md b/readme.md index df755b4..7a27f88 100644 --- a/readme.md +++ b/readme.md @@ -273,14 +273,14 @@ ON c.CustomerId = @CustomerID ``` -| CustomerId|CompanyName|OrderId|OrderDate -| ----------|-----------|-------|--------- -|ALFKI|Alfreds Futterkiste|10643|1997-08-25 -|ALFKI|Alfreds Futterkiste|10692|1997-10-03 -|ALFKI|Alfreds Futterkiste|10702|1997-10-13 -|ALFKI|Alfreds Futterkiste|10835|1998-01-15 -|ALFKI|Alfreds Futterkiste|10952|1998-03-16 -|ALFKI|Alfreds Futterkiste|11011|1998-04-09 +| CustomerId | CompanyName | OrderId | OrderDate | +| ---------- | ------------------- | ------- | ---------- | +| ALFKI | Alfreds Futterkiste | 10643 | 1997-08-25 | +| ALFKI | Alfreds Futterkiste | 10692 | 1997-10-03 | +| ALFKI | Alfreds Futterkiste | 10702 | 1997-10-13 | +| ALFKI | Alfreds Futterkiste | 10835 | 1998-01-15 | +| ALFKI | Alfreds Futterkiste | 10952 | 1998-03-16 | +| ALFKI | Alfreds Futterkiste | 11011 | 1998-04-09 | As we can see from the result we now have six rows. One for each *Order* and the *Customer* columns are duplicated for each *Order*. @@ -550,9 +550,42 @@ This instructs **DbReader** to use our custom read delegate whenever it encounte The *Guid* also needs to be converted back into a byte array when passing a *Guid* value as a parameter to a query. ``` -DbReaderOptions.WhenPassing().Use((parameter, guid) => parameter.Value = guid.ToByArray()); +DbReaderOptions.WhenPassing().Use((parameter, guid) => parameter.Value = guid.ToByArray() ``` +> Note that the conversion function is only invoked if the value from the database is NOT `DBNull` + +### Default values + +When using a custom conversion function (WhenReading) it is possible to define a default for the value to be used when value from the database is `DBNull`. + +Say that we have a class with a property of type `CustomValueType` + +```c# + +public class CustomValueType +{ + public CustomValueType(int value) + { + Value = value; + } + + public int Value { get; } +} + +public class MyClass +{ + public CustomValueType SomeProperty { get; set; } +} +``` + +When the property `SomeProperty` we can specify what to return in the case of the field (SomeProperty) being `DBNull` from the `IDataRecord` + +```C# +DbReaderOptions.WhenReading().Use((dr, i) => new CustomValueType(dr.GetInt32(i))).WithDefaultValue(new CustomValueType(42)); +``` + + ## Simple Types Sometimes we just want to get a list of a simple types such as `string` or maybe an `integer` diff --git a/src/DbReader.Tests/InstanceReaderTests.cs b/src/DbReader.Tests/InstanceReaderTests.cs index 2436a4a..50d4506 100644 --- a/src/DbReader.Tests/InstanceReaderTests.cs +++ b/src/DbReader.Tests/InstanceReaderTests.cs @@ -400,6 +400,29 @@ public void ShouldHandleNullValuesInNavigationChain() result.Children.ShouldBeEmpty(); } + [Fact] + public void ShouldUseDefaultValue() + { + var dataRecord = new { Id = 1, Property = DBNull.Value }.ToDataRecord(); + DbReaderOptions.WhenReading().Use((dr, i) => new CustomValueType(dr.GetInt32(i))).WithDefaultValue(new CustomValueType(42)); + var reader = GetReader>(); + var instance = reader.Read(dataRecord, string.Empty); + instance.Property.Value.ShouldBe(42); + } + + + public class CustomValueType + { + public CustomValueType(int value) + { + Value = value; + } + + public int Value { get; } + } + + + [Fact] public void Test() { diff --git a/src/DbReader/Construction/PropertyReaderMethodBuilder.cs b/src/DbReader/Construction/PropertyReaderMethodBuilder.cs index 3b80ec6..609e72a 100644 --- a/src/DbReader/Construction/PropertyReaderMethodBuilder.cs +++ b/src/DbReader/Construction/PropertyReaderMethodBuilder.cs @@ -140,6 +140,14 @@ private void EmitPropertySetter(ILGenerator il, PropertyInfo property, int prope EmitGetValue(il, propertyIndex, getMethod, property.PropertyType); EmitCallPropertySetterMethod(il, property); il.MarkLabel(endLabel); + if (ValueConverter.HasDefaultValue(property.PropertyType)) + { + var openGenericGetDefaultValueMethod = typeof(ValueConverter).GetMethod(nameof(ValueConverter.GetDefaultValue), BindingFlags.Static | BindingFlags.NonPublic); + var getDefaultValueMethod = openGenericGetDefaultValueMethod.MakeGenericMethod(property.PropertyType); + LoadInstance(il, instanceVariable); + il.Emit(OpCodes.Call, getDefaultValueMethod); + EmitCallPropertySetterMethod(il, property); + } } } } \ No newline at end of file diff --git a/src/DbReader/ReadDelegate.cs b/src/DbReader/ReadDelegate.cs index 18ccc85..3f15a9e 100644 --- a/src/DbReader/ReadDelegate.cs +++ b/src/DbReader/ReadDelegate.cs @@ -13,9 +13,26 @@ public class ReadDelegate /// Specifies the to be used to read a value of type /// /// The function to be used to read the value. - public void Use(Func readFunction) + public DefaultValue Use(Func readFunction) { - ValueConverter.RegisterReadDelegate(readFunction); + ValueConverter.RegisterReadDelegate(readFunction); + return new DefaultValue(); + } + } + + /// + /// Specifies the default value to be used when the value from the database is . + /// + /// The type of property for which to specify a default value. + public class DefaultValue + { + /// + /// Specifies the to be used when the value from the database is . + /// + /// The value to be used when the value from the database is . + public void WithDefaultValue(TProperty defaultValue) + { + ValueConverter.RegisterDefaultValue(defaultValue); } } } \ No newline at end of file diff --git a/src/DbReader/ValueConverter.cs b/src/DbReader/ValueConverter.cs index b570d3b..2a770fb 100644 --- a/src/DbReader/ValueConverter.cs +++ b/src/DbReader/ValueConverter.cs @@ -36,6 +36,8 @@ public static class ValueConverter private static readonly ConcurrentDictionary ReadDelegates = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary DefaultValues = new ConcurrentDictionary(); + /// /// Registers a function delegate that creates a value of from an /// at the specified ordinal (column index). @@ -47,6 +49,12 @@ public static void RegisterReadDelegate(Func convertFunc ReadDelegates.AddOrUpdate(typeof(T), type => convertFunction, (type, del) => convertFunction); } + public static void RegisterDefaultValue(T defaultValue) + { + DefaultValues.AddOrUpdate(typeof(T), type => defaultValue, (type, value) => defaultValue); + } + + /// /// Determines if the given can be converted. /// @@ -57,6 +65,22 @@ internal static bool CanConvert(Type type) return ReadDelegates.ContainsKey(type); } + /// + /// Determines if the given has a default value to be used when the value from the database is null. + /// + /// The type to be checked for a default value. + /// true, if there is a default value registration for the given type, otherwise, false. + internal static bool HasDefaultValue(Type type) + { + return DefaultValues.ContainsKey(type); + } + + internal static T GetDefaultValue() + { + return (T)DefaultValues[typeof(T)]; + } + + /// /// Converts the value from the at the given /// to an instance of .