diff --git a/Dapper/DbString.cs b/Dapper/DbString.cs index 14dc4612b..15b97d6bc 100644 --- a/Dapper/DbString.cs +++ b/Dapper/DbString.cs @@ -28,6 +28,17 @@ public DbString() Length = -1; IsAnsi = IsAnsiDefault; } + + /// + /// Create a new DbString + /// + public DbString(string? value, int length = -1) + { + Value = value; + Length = length; + IsAnsi = IsAnsiDefault; + } + /// /// Ansi vs Unicode /// @@ -44,12 +55,13 @@ public DbString() /// The value of the string /// public string? Value { get; set; } - + /// /// Gets a string representation of this DbString. /// - public override string ToString() => - $"Dapper.DbString (Value: '{Value}', Length: {Length}, IsAnsi: {IsAnsi}, IsFixedLength: {IsFixedLength})"; + public override string ToString() => Value is null + ? $"Dapper.DbString (Value: null, Length: {Length}, IsAnsi: {IsAnsi}, IsFixedLength: {IsFixedLength})" + : $"Dapper.DbString (Value: '{Value}', Length: {Length}, IsAnsi: {IsAnsi}, IsFixedLength: {IsFixedLength})"; /// /// Add the parameter to the command... internal use only diff --git a/Dapper/PublicAPI.Shipped.txt b/Dapper/PublicAPI.Shipped.txt index 4c5417a6c..c1b08ea99 100644 --- a/Dapper/PublicAPI.Shipped.txt +++ b/Dapper/PublicAPI.Shipped.txt @@ -30,6 +30,7 @@ Dapper.CustomPropertyTypeMap.GetMember(string! columnName) -> Dapper.SqlMapper.I Dapper.DbString Dapper.DbString.AddParameter(System.Data.IDbCommand! command, string! name) -> void Dapper.DbString.DbString() -> void +Dapper.DbString.DbString(string? value, int length = -1) -> void Dapper.DbString.IsAnsi.get -> bool Dapper.DbString.IsAnsi.set -> void Dapper.DbString.IsFixedLength.get -> bool @@ -323,6 +324,7 @@ static Dapper.SqlMapper.Settings.UseSingleRowOptimization.set -> void static Dapper.SqlMapper.SetTypeMap(System.Type! type, Dapper.SqlMapper.ITypeMap? map) -> void static Dapper.SqlMapper.SetTypeName(this System.Data.DataTable! table, string! typeName) -> void static Dapper.SqlMapper.ThrowDataException(System.Exception! ex, int index, System.Data.IDataReader! reader, object? value) -> void +static Dapper.SqlMapper.ThrowNullCustomQueryParameter(string! name) -> void static Dapper.SqlMapper.TypeHandlerCache.Parse(object! value) -> T? static Dapper.SqlMapper.TypeHandlerCache.SetValue(System.Data.IDbDataParameter! parameter, object! value) -> void static Dapper.SqlMapper.TypeMapProvider -> System.Func! \ No newline at end of file diff --git a/Dapper/SqlMapper.cs b/Dapper/SqlMapper.cs index fb6ac88e6..fb151758e 100644 --- a/Dapper/SqlMapper.cs +++ b/Dapper/SqlMapper.cs @@ -2637,6 +2637,16 @@ internal static IList GetLiteralTokens(string sql) { il.Emit(OpCodes.Ldloc, typedParameterLocal); // stack is now [parameters] [typed-param] il.Emit(callOpCode, prop.GetGetMethod()!); // stack is [parameters] [custom] + if (!prop.PropertyType.IsValueType) + { + // throw if null + var notNull = il.DefineLabel(); + il.Emit(OpCodes.Dup); // stack is [parameters] [custom] [custom] + il.Emit(OpCodes.Brtrue_S, notNull); // stack is [parameters] [custom] + il.Emit(OpCodes.Ldstr, prop.Name); // stack is [parameters] [custom] [name] + il.EmitCall(OpCodes.Call, typeof(SqlMapper).GetMethod(nameof(ThrowNullCustomQueryParameter))!, null); // stack is [parameters] [custom] + il.MarkLabel(notNull); + } il.Emit(OpCodes.Ldarg_0); // stack is now [parameters] [custom] [command] il.Emit(OpCodes.Ldstr, prop.Name); // stack is now [parameters] [custom] [command] [name] il.EmitCall(OpCodes.Callvirt, prop.PropertyType.GetMethod(nameof(ICustomQueryParameter.AddParameter))!, null); // stack is now [parameters] @@ -3859,6 +3869,14 @@ private static void FlexibleConvertBoxedFromHeadOfStack(ILGenerator il, Type fro return null; } + /// + /// For internal use only + /// + [Obsolete(ObsoleteInternalUsageOnly, false)] + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public static void ThrowNullCustomQueryParameter(string name) + => throw new InvalidOperationException($"Member '{name}' is an {nameof(ICustomQueryParameter)} and cannot be null"); + /// /// Throws a data exception, only used internally /// @@ -3867,6 +3885,7 @@ private static void FlexibleConvertBoxedFromHeadOfStack(ILGenerator il, Type fro /// The reader the exception occurred in. /// The value that caused the exception. [Obsolete(ObsoleteInternalUsageOnly, false)] + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] public static void ThrowDataException(Exception ex, int index, IDataReader reader, object? value) { Exception toThrow; diff --git a/tests/Dapper.Tests/MiscTests.cs b/tests/Dapper.Tests/MiscTests.cs index 24f7bf2bd..aaaf8e978 100644 --- a/tests/Dapper.Tests/MiscTests.cs +++ b/tests/Dapper.Tests/MiscTests.cs @@ -657,6 +657,27 @@ public void TestDbString() Assert.Equal(10, (int)obj.f); } + [Fact] + public void DbStringNullHandling() + { + // without lengths + var obj = new { x = new DbString("abc"), y = (DbString?)new DbString(null) }; + var row = connection.QuerySingle<(string? x,string? y)>("select @x as x, @y as y", obj); + Assert.Equal("abc", row.x); + Assert.Null(row.y); + + // with lengths + obj = new { x = new DbString("abc", 200), y = (DbString?)new DbString(null, 200) }; + row = connection.QuerySingle<(string? x, string? y)>("select @x as x, @y as y", obj); + Assert.Equal("abc", row.x); + Assert.Null(row.y); + + // null raw value - give clear message, at least + obj = obj with { y = null }; + var ex = Assert.Throws(() => connection.QuerySingle<(string? x, string? y)>("select @x as x, @y as y", obj)); + Assert.Equal("Member 'y' is an ICustomQueryParameter and cannot be null", ex.Message); + } + [Fact] public void TestDbStringToString() { @@ -668,6 +689,8 @@ public void TestDbStringToString() new DbString { Value = "abcde", IsFixedLength = false, Length = 10, IsAnsi = true }.ToString()); Assert.Equal("Dapper.DbString (Value: 'abcde', Length: 10, IsAnsi: False, IsFixedLength: False)", new DbString { Value = "abcde", IsFixedLength = false, Length = 10, IsAnsi = false }.ToString()); + Assert.Equal("Dapper.DbString (Value: null, Length: -1, IsAnsi: False, IsFixedLength: False)", + new DbString { Value = null }.ToString()); Assert.Equal("Dapper.DbString (Value: 'abcde', Length: -1, IsAnsi: True, IsFixedLength: False)", new DbString { Value = "abcde", IsAnsi = true }.ToString());