diff --git a/src/NLog/Targets/DatabaseParameterInfo.cs b/src/NLog/Targets/DatabaseParameterInfo.cs index 24e1684129..17d353d0f4 100644 --- a/src/NLog/Targets/DatabaseParameterInfo.cs +++ b/src/NLog/Targets/DatabaseParameterInfo.cs @@ -134,6 +134,13 @@ public DatabaseParameterInfo(string parameterName, Layout parameterLayout) [DefaultValue(null)] public CultureInfo Culture { get; set; } + /// + /// Gets or sets whether empty value should translate into DbNull. Requires database column to allow NULL values. + /// + /// + [DefaultValue(false)] + public bool AllowDbNull { get; set; } + internal bool SetDbType(IDbDataParameter dbParameter) { if (!string.IsNullOrEmpty(DbType)) diff --git a/src/NLog/Targets/DatabaseTarget.cs b/src/NLog/Targets/DatabaseTarget.cs index 391c927d06..1cc311345f 100644 --- a/src/NLog/Targets/DatabaseTarget.cs +++ b/src/NLog/Targets/DatabaseTarget.cs @@ -1094,43 +1094,38 @@ protected virtual IDbDataParameter CreateDatabaseParameter(IDbCommand command, D /// Parameter configuration info. 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 @@ -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) { @@ -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; + } + /// /// Create Default Value of Type /// - /// - /// - 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); diff --git a/tests/NLog.UnitTests/Targets/DatabaseTargetTests.cs b/tests/NLog.UnitTests/Targets/DatabaseTargetTests.cs index 37ed97fe11..a1ace521f3 100644 --- a/tests/NLog.UnitTests/Targets/DatabaseTargetTests.cs +++ b/tests/NLog.UnitTests/Targets/DatabaseTargetTests.cs @@ -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)] @@ -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); @@ -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()); @@ -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; @@ -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()); @@ -704,7 +717,10 @@ public static IEnumerable 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]