Skip to content

Commit

Permalink
DatabaseTarget - Added AllowDbNull for easier support for nullable pa… (
Browse files Browse the repository at this point in the history
#4076)

* DatabaseTarget - Added AllowDbNull for easier support for nullable parameters

* DatabaseTarget - Added AllowDbNull for easier support for nullable parameters (support empty string as raw value)
  • Loading branch information
snakefoot authored Aug 14, 2020
1 parent 34afe70 commit e441fa8
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 32 deletions.
7 changes: 7 additions & 0 deletions src/NLog/Targets/DatabaseParameterInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,13 @@ public DatabaseParameterInfo(string parameterName, Layout parameterLayout)
[DefaultValue(null)]
public CultureInfo Culture { get; set; }

/// <summary>
/// Gets or sets whether empty value should translate into DbNull. Requires database column to allow NULL values.
/// </summary>
/// <docgen category='Parameter Options' order='8' />
[DefaultValue(false)]
public bool AllowDbNull { get; set; }

internal bool SetDbType(IDbDataParameter dbParameter)
{
if (!string.IsNullOrEmpty(DbType))
Expand Down
76 changes: 49 additions & 27 deletions src/NLog/Targets/DatabaseTarget.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1094,43 +1094,38 @@ protected virtual IDbDataParameter CreateDatabaseParameter(IDbCommand command, D
/// <param name="parameterInfo">Parameter configuration info.</param>
protected internal virtual object GetDatabaseParameterValue(LogEventInfo logEvent, DatabaseParameterInfo parameterInfo)
{
return RenderObjectValue(logEvent, parameterInfo.Name, parameterInfo.Layout, parameterInfo.ParameterType, parameterInfo.Format, parameterInfo.Culture);
return RenderObjectValue(logEvent, parameterInfo.Name, parameterInfo.Layout, parameterInfo.ParameterType, parameterInfo.Format, parameterInfo.Culture, parameterInfo.AllowDbNull);
}

private object GetDatabaseObjectPropertyValue(LogEventInfo logEvent, DatabaseObjectPropertyInfo connectionInfo)
{
return RenderObjectValue(logEvent, connectionInfo.Name, connectionInfo.Layout, connectionInfo.PropertyType, connectionInfo.Format, connectionInfo.Culture);
return RenderObjectValue(logEvent, connectionInfo.Name, connectionInfo.Layout, connectionInfo.PropertyType, connectionInfo.Format, connectionInfo.Culture, false);
}

private object RenderObjectValue(LogEventInfo logEvent, string propertyName, Layout valueLayout, Type valueType, string valueFormat, IFormatProvider formatProvider)
private object RenderObjectValue(LogEventInfo logEvent, string propertyName, Layout valueLayout, Type valueType, string valueFormat, IFormatProvider formatProvider, bool allowDbNull)
{
if (string.IsNullOrEmpty(valueFormat) && valueType == typeof(string))
if (string.IsNullOrEmpty(valueFormat) && valueType == typeof(string) && !allowDbNull)
{
return RenderLogEvent(valueLayout, logEvent) ?? string.Empty;
}

formatProvider = formatProvider ?? logEvent.FormatProvider ?? LoggingConfiguration?.DefaultCultureInfo;

if (valueLayout.TryGetRawValue(logEvent, out var rawValue))
try
{
try
if (TryRenderObjectRawValue(logEvent, valueLayout, valueType, valueFormat, formatProvider, allowDbNull, out var rawValue))
{
if (ReferenceEquals(rawValue, DBNull.Value))
{
return rawValue;
}

return PropertyTypeConverter.Convert(rawValue, valueType, valueFormat, formatProvider) ?? CreateDefaultValue(valueType);
return rawValue;
}
catch (Exception ex)
{
if (ex.MustBeRethrownImmediately())
throw;
}
catch (Exception ex)
{
if (ex.MustBeRethrownImmediately())
throw;

InternalLogger.Warn(ex, " DatabaseTarget: Failed to convert raw value for '{0}' into {1}", propertyName, valueType);
if (ExceptionMustBeRethrown(ex))
throw;
}
InternalLogger.Warn(ex, " DatabaseTarget: Failed to convert raw value for '{0}' into {1}", propertyName, valueType);
if (ExceptionMustBeRethrown(ex))
throw;
}

try
Expand All @@ -1139,10 +1134,10 @@ private object RenderObjectValue(LogEventInfo logEvent, string propertyName, Lay
string parameterValue = RenderLogEvent(valueLayout, logEvent);
if (string.IsNullOrEmpty(parameterValue))
{
return CreateDefaultValue(valueType);
return CreateDefaultValue(valueType, allowDbNull);
}

return PropertyTypeConverter.Convert(parameterValue, valueType, valueFormat, formatProvider) ?? DBNull.Value;
return PropertyTypeConverter.Convert(parameterValue, valueType, valueFormat, formatProvider) ?? CreateDefaultValue(valueType, allowDbNull);
}
catch (Exception ex)
{
Expand All @@ -1154,18 +1149,45 @@ private object RenderObjectValue(LogEventInfo logEvent, string propertyName, Lay
if (ExceptionMustBeRethrown(ex))
throw;

return CreateDefaultValue(valueType);
return CreateDefaultValue(valueType, allowDbNull);
}
}

private bool TryRenderObjectRawValue(LogEventInfo logEvent, Layout valueLayout, Type valueType, string valueFormat, IFormatProvider formatProvider, bool allowDbNull, out object rawValue)
{
if (valueLayout.TryGetRawValue(logEvent, out rawValue))
{
if (ReferenceEquals(rawValue, DBNull.Value))
{
return true;
}

if (rawValue == null)
{
rawValue = CreateDefaultValue(valueType, allowDbNull);
return true;
}

if (valueType == typeof(string))
{
return rawValue is string;
}

rawValue = PropertyTypeConverter.Convert(rawValue, valueType, valueFormat, formatProvider) ?? CreateDefaultValue(valueType, allowDbNull);
return true;
}

return false;
}

/// <summary>
/// Create Default Value of Type
/// </summary>
/// <param name="dbParameterType"></param>
/// <returns></returns>
private static object CreateDefaultValue(Type dbParameterType)
private static object CreateDefaultValue(Type dbParameterType, bool allowDbNull)
{
if (dbParameterType == typeof(string))
if (allowDbNull)
return DBNull.Value;
else if (dbParameterType == typeof(string))
return string.Empty;
else if (dbParameterType.IsValueType())
return Activator.CreateInstance(dbParameterType);
Expand Down
26 changes: 21 additions & 5 deletions tests/NLog.UnitTests/Targets/DatabaseTargetTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -591,7 +591,7 @@ Add Parameter Parameter #1
[InlineData("${counter}", DbType.Int32, 1)]
[InlineData("${counter}", DbType.Int64, (long)1)]
[InlineData("${counter:norawvalue=true}", DbType.Int16, (short)1)] //fallback
[InlineData("${counter}", DbType.VarNumeric, 1, true)]
[InlineData("${counter}", DbType.VarNumeric, 1, false, true)]
[InlineData("${counter}", DbType.AnsiString, "1")]
[InlineData("${level}", DbType.AnsiString, "Debug")]
[InlineData("${level}", DbType.Int32, 1)]
Expand All @@ -607,12 +607,23 @@ Add Parameter Parameter #1
[InlineData("${event-properties:almostAsIntProp}", DbType.Int32, 124)]
[InlineData("${event-properties:almostAsIntProp}", DbType.Int64, (long)124)]
[InlineData("${event-properties:almostAsIntProp}", DbType.AnsiString, " 124 ")]
public void GetParameterValueTest(string layout, DbType dbtype, object expected, bool convertToDecimal = false)
[InlineData("${event-properties:emptyprop}", DbType.AnsiString, "")]
[InlineData("${event-properties:emptyprop}", DbType.AnsiString, "", true)]
[InlineData("${event-properties:NullRawValue}", DbType.AnsiString, "")]
[InlineData("${event-properties:NullRawValue}", DbType.Int32, 0)]
[InlineData("${event-properties:NullRawValue}", DbType.AnsiString, null, true)]
[InlineData("${event-properties:NullRawValue}", DbType.Int32, null, true)]
[InlineData("${event-properties:NullRawValue}", DbType.Guid, null, true)]
[InlineData("", DbType.AnsiString, null, true)]
[InlineData("", DbType.Int32, null, true)]
[InlineData("", DbType.Guid, null, true)]
public void GetParameterValueTest(string layout, DbType dbtype, object expected, bool allowDbNull = false, bool convertToDecimal = false)
{
// Arrange
var logEventInfo = new LogEventInfo(LogLevel.Debug, "logger1", "message 2");
logEventInfo.Properties["intprop"] = 123;
logEventInfo.Properties["boolprop"] = true;
logEventInfo.Properties["emptyprop"] = "";
logEventInfo.Properties["almostAsIntProp"] = " 124 ";
logEventInfo.Properties["dateprop"] = new DateTime(2018, 12, 30, 13, 34, 56);

Expand All @@ -622,6 +633,7 @@ public void GetParameterValueTest(string layout, DbType dbtype, object expected,
DbType = dbtype.ToString(),
Layout = layout,
Name = parameterName,
AllowDbNull = allowDbNull,
};
databaseParameterInfo.SetDbType(new MockDbConnection().CreateCommand().CreateParameter());

Expand All @@ -635,12 +647,12 @@ public void GetParameterValueTest(string layout, DbType dbtype, object expected,
expected = (decimal)(int)expected;
}

Assert.Equal(expected, result);
Assert.Equal(expected ?? DBNull.Value, result);
}

[Theory]
[MemberData(nameof(ConvertFromStringTestCases))]
public void GetParameterValueFromStringTest(string value, DbType dbType, object expected, string format = null, CultureInfo cultureInfo = null)
public void GetParameterValueFromStringTest(string value, DbType dbType, object expected, string format = null, CultureInfo cultureInfo = null, bool? allowDbNull = null)
{

var culture = System.Threading.Thread.CurrentThread.CurrentCulture;
Expand All @@ -655,6 +667,7 @@ public void GetParameterValueFromStringTest(string value, DbType dbType, object
Format = format,
DbType = dbType.ToString(),
Culture = cultureInfo,
AllowDbNull = allowDbNull ?? false,
};
databaseParameterInfo.SetDbType(new MockDbConnection().CreateCommand().CreateParameter());

Expand Down Expand Up @@ -704,7 +717,10 @@ public static IEnumerable<object[]> ConvertFromStringTestCases()
yield return new object[] { "${db-null}", DbType.DateTime, DBNull.Value };
yield return new object[] { "${event-properties:userid}", DbType.Int32, 0 };
yield return new object[] { "${date:universalTime=true:format=yyyy-MM:norawvalue=true}", DbType.DateTime, DateTime.SpecifyKind(DateTime.UtcNow.Date.AddDays(-DateTime.UtcNow.Day + 1), DateTimeKind.Unspecified) };
yield return new object[] { "${shortdate:universalTime=true}", DbType.DateTime, DateTime.UtcNow.Date };
yield return new object[] { "${shortdate:universalTime=true}", DbType.DateTime, DateTime.UtcNow.Date, null, null, true };
yield return new object[] { "${shortdate:universalTime=true}", DbType.DateTime, DateTime.UtcNow.Date, null, null, false };
yield return new object[] { "${shortdate:universalTime=true}", DbType.String, DateTime.UtcNow.Date.ToString("yyyy-MM-dd"), null, null, true };
yield return new object[] { "${shortdate:universalTime=true}", DbType.String, DateTime.UtcNow.Date.ToString("yyyy-MM-dd"), null, null, false };
}

[Fact]
Expand Down

0 comments on commit e441fa8

Please sign in to comment.