Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

${AssemblyVersion}: add type (File, Assembly, Informational) option #2487

Merged
merged 16 commits into from
Jan 4, 2018
Merged
10 changes: 10 additions & 0 deletions src/NLog/Internal/ReflectionHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,16 @@ public static TAttr GetCustomAttribute<TAttr>(this PropertyInfo info)
#endif
}

public static TAttr GetCustomAttribute<TAttr>(this Assembly assembly)
where TAttr : Attribute
{
#if NETSTANDARD1_0
return assembly.GetCustomAttributes(typeof(TAttr)).FirstOrDefault() as TAttr;
#else
return (TAttr)Attribute.GetCustomAttribute(assembly, typeof(TAttr));
#endif
}

public static IEnumerable<TAttr> GetCustomAttributes<TAttr>(this Type type, bool inherit) where TAttr : Attribute
{
#if NETSTANDARD1_0
Expand Down
126 changes: 105 additions & 21 deletions src/NLog/LayoutRenderers/AssemblyVersionLayoutRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,60 +31,144 @@
// THE POSSIBILITY OF SUCH DAMAGE.
//


namespace NLog.LayoutRenderers
{
using System.Reflection;
using System.ComponentModel;
using System.Text;
using NLog.Config;
using NLog.Internal;

/// <summary>
/// Assembly version.
/// Renders the assembly version information for the entry assembly or a named assembly.
/// </summary>
/// <remarks>The entry assembly can't be found in some cases e.g. ASP.NET, Unit tests etc.</remarks>
/// <remarks>
/// As this layout renderer uses reflection and version information is unlikely to change during application execution,
/// it is recommended to use it in conjunction with the <see cref="NLog.LayoutRenderers.Wrappers.CachedLayoutRendererWrapper"/>.
/// </remarks>
/// <remarks>
/// The entry assembly can't be found in some cases e.g. ASP.NET, unit tests, etc.
/// </remarks>
[LayoutRenderer("assembly-version")]
public class AssemblyVersionLayoutRenderer : LayoutRenderer
{
/// <summary>
/// Initializes a new instance of the <see cref="AssemblyVersionLayoutRenderer" /> class.
/// </summary>
public AssemblyVersionLayoutRenderer()
{
Type = AssemblyVersionType.Assembly;
}

/// <summary>
/// The (full) name of the assembly. If <c>null</c>, using the entry assembly.
/// </summary>
[DefaultParameter]
public string Name { get; set; }

/// <summary>
/// Renders assembly version and appends it to the specified <see cref="StringBuilder" />.
/// Gets or sets the type of assembly version to retrieve.
/// </summary>
/// <remarks>
/// Some version type and platform combinations are not fully supported.
/// - UWP earlier than .NET Standard 1.5: Value for <see cref="AssemblyVersionType.Assembly"/> is always returned unless the <see cref="Name"/> parameter is specified.
/// - Silverlight: Value for <see cref="AssemblyVersionType.Assembly"/> is always returned.
/// </remarks>
/// <docgen category='Rendering Options' order='10' />
[DefaultValue(nameof(AssemblyVersionType.Assembly))]
public AssemblyVersionType Type { get; set; }

/// <summary>
/// Renders an assembly version and appends it to the specified <see cref="StringBuilder" />.
/// </summary>
/// <param name="builder">The <see cref="StringBuilder"/> to append the rendered data to.</param>
/// <param name="logEvent">Logging event.</param>
protected override void Append(StringBuilder builder, LogEventInfo logEvent)
{
string assemblyVersion = GetAssemblyApplicationVersion(Name);
if (string.IsNullOrEmpty(assemblyVersion))
var version = GetVersion();

if (string.IsNullOrEmpty(version))
{
assemblyVersion = $"Could not find {(string.IsNullOrEmpty(Name) ? "entry" : Name)} assembly";
version = $"Could not find value for {(string.IsNullOrEmpty(Name) ? "entry" : Name)} assembly and version type {Type}";
}
builder.Append(assemblyVersion);

builder.Append(version);
}

string GetAssemblyApplicationVersion(string assemblyName)
#if SILVERLIGHT

private string GetVersion()
{
if (!string.IsNullOrEmpty(assemblyName))
var assemblyName = GetAssemblyName();
return assemblyName.Version.ToString();
}

private System.Reflection.AssemblyName GetAssemblyName()
{
if (string.IsNullOrEmpty(Name))
{
#if SILVERLIGHT
return new AssemblyName(assemblyName).Version.ToString();
#else
return Assembly.Load(new AssemblyName(assemblyName))?.GetName().Version.ToString();
#endif
return new System.Reflection.AssemblyName(System.Windows.Application.Current.GetType().Assembly.FullName);
}
else
{
#if SILVERLIGHT
return new AssemblyName(System.Windows.Application.Current.GetType().Assembly.FullName).Version.ToString();
#elif WINDOWS_UWP
return new System.Reflection.AssemblyName(Name);
}
}

#elif WINDOWS_UWP && !NETSTANDARD1_5

private string GetVersion()
{
if (string.IsNullOrEmpty(Name))
{
return Microsoft.Extensions.PlatformAbstractions.PlatformServices.Default.Application.ApplicationVersion;
}
else
{
var assembly = GetAssembly();
return assembly?.GetName().Version.ToString();
}
}

private System.Reflection.Assembly GetAssembly()
{
return System.Reflection.Assembly.Load(new System.Reflection.AssemblyName(Name));
}

#else
return Assembly.GetEntryAssembly()?.GetName().Version.ToString();
#endif

private string GetVersion()
{
var assembly = GetAssembly();
return GetVersion(assembly);
}

private System.Reflection.Assembly GetAssembly()
{
if (string.IsNullOrEmpty(Name))
{
return System.Reflection.Assembly.GetEntryAssembly();
}
else
{
return System.Reflection.Assembly.Load(new System.Reflection.AssemblyName(Name));
}
}
}

private string GetVersion(System.Reflection.Assembly assembly)
{
switch (Type)
{
case AssemblyVersionType.File:
return assembly?.GetCustomAttribute<System.Reflection.AssemblyFileVersionAttribute>()?.Version;

case AssemblyVersionType.Informational:
return assembly?.GetCustomAttribute<System.Reflection.AssemblyInformationalVersionAttribute>()?.InformationalVersion;

default:
return assembly?.GetName().Version?.ToString();
}
}
#endif
}
}
56 changes: 56 additions & 0 deletions src/NLog/LayoutRenderers/AssemblyVersionType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// Copyright (c) 2004-2017 Jaroslaw Kowalski <jaak@jkowalski.net>, Kim Christensen, Julian Verdurmen
//
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// * Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// * Neither the name of Jaroslaw Kowalski nor the names of its
// contributors may be used to endorse or promote products derived from this
// software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
// THE POSSIBILITY OF SUCH DAMAGE.
//

namespace NLog.LayoutRenderers
{
/// <summary>
/// Type of assembly version to retrieve.
/// </summary>
public enum AssemblyVersionType
{
/// <summary>
/// Gets the assembly version.
/// </summary>
Assembly,

/// <summary>
/// Gets the file version.
/// </summary>
File,

/// <summary>
/// Gets additional version information.
/// </summary>
Informational
}
}
136 changes: 132 additions & 4 deletions tests/NLog.UnitTests/LayoutRenderers/AssemblyVersionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,30 @@
// THE POSSIBILITY OF SUCH DAMAGE.
//

using System.Reflection;

namespace NLog.UnitTests.LayoutRenderers
{
using System;
using System.Reflection;
using NLog.LayoutRenderers;
using Xunit;
using Xunit.Abstractions;

public class AssemblyVersionTests : NLogTestBase
{
private readonly ITestOutputHelper _testOutputHelper;

public AssemblyVersionTests(ITestOutputHelper testOutputHelper)
{
_testOutputHelper = testOutputHelper;
}

[Fact]
public void EntryAssemblyVersionTest()
{
var assembly = Assembly.GetEntryAssembly();
var assemblyVersion = assembly == null ? "Could not find entry assembly" : assembly.GetName().Version.ToString();
var assemblyVersion = assembly == null
? $"Could not find value for entry assembly and version type {nameof(AssemblyVersionType.Assembly)}"
: assembly.GetName().Version.ToString();
AssertLayoutRendererOutput("${assembly-version}", assemblyVersion);
}

Expand All @@ -52,5 +63,122 @@ public void AssemblyNameVersionTest()
{
AssertLayoutRendererOutput("${assembly-version:NLogAutoLoadExtension}", "2.0.0.0");
}

[Fact]
public void AssemblyNameVersionTypeTest()
{
AssertLayoutRendererOutput("${assembly-version:name=NLogAutoLoadExtension:type=assembly}", "2.0.0.0");
AssertLayoutRendererOutput("${assembly-version:name=NLogAutoLoadExtension:type=file}", "2.0.0.0");
AssertLayoutRendererOutput("${assembly-version:name=NLogAutoLoadExtension:type=informational}", "1.0.0");
}

#if !NETSTANDARD
private const string AssemblyVersionTest = "1.1.1.1";
private const string AssemblyFileVersionTest = "1.1.1.2";
private const string AssemblyInformationalVersionTest = "Version 1";

[Theory]
[InlineData(AssemblyVersionType.Assembly, AssemblyVersionTest)]
[InlineData(AssemblyVersionType.File, AssemblyFileVersionTest)]
[InlineData(AssemblyVersionType.Informational, AssemblyInformationalVersionTest)]
public void AssemblyVersionTypeTest(AssemblyVersionType type, string expected)
{
LogManager.Configuration = CreateConfigurationFromString(@"
<nlog>
<targets><target name='debug' type='Debug' layout='${message:withException=true}|${assembly-version:type=" + type.ToString().ToLower() + @"}' /></targets>
<rules><logger name='*' minlevel='Debug' writeTo='debug' /></rules>
</nlog>");
var logger = LogManager.GetLogger("SomeLogger");
var compiledAssembly = GenerateTestAssembly();
var testLoggerType = compiledAssembly.GetType("LogTester.LoggerTest");
var logMethod = testLoggerType.GetMethod("TestLog");
var testLoggerInstance = Activator.CreateInstance(testLoggerType);

logMethod.Invoke(testLoggerInstance, new object[] { logger, compiledAssembly });

var lastMessage = GetDebugLastMessage("debug");
var messageParts = lastMessage.Split('|');
var logMessage = messageParts[0];
var logVersion = messageParts[1];
if (logMessage.StartsWith("Skip:"))
{
_testOutputHelper.WriteLine(logMessage);
}
else
{
Assert.StartsWith("Pass:", logMessage);
Assert.Equal(expected, logVersion);
}
}

private static Assembly GenerateTestAssembly()
{
const string code = @"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!

using System;
using System.Reflection;

[assembly: AssemblyVersion(""" + AssemblyVersionTest + @""")]
[assembly: AssemblyFileVersion(""" + AssemblyFileVersionTest + @""")]
[assembly: AssemblyInformationalVersion(""" + AssemblyInformationalVersionTest + @""")]

namespace LogTester
{
public class LoggerTest
{
public void TestLog(NLog.Logger logger, Assembly assembly)
{
if (System.Reflection.Assembly.GetEntryAssembly() == null)
{
// In some unit testing scenarios we cannot find the entry assembly
// So we attempt to force this to be set, which can also still fail
// This is not expected to be necessary in Visual Studio
// See https://github.com/Microsoft/vstest/issues/649
try
{
SetEntryAssembly(assembly);
}
catch (InvalidOperationException ioex)
{
logger.Debug(ioex, ""Skip: No entry assembly"");
return;
}
}

logger.Debug(""Pass: Test fully executed"");
}

private static void SetEntryAssembly(Assembly assembly)
{
var manager = new AppDomainManager();

var domain = AppDomain.CurrentDomain;
if (domain == null)
throw new InvalidOperationException(""Current app domain is null"");

var entryAssemblyField = manager.GetType().GetField(""m_entryAssembly"", BindingFlags.Instance | BindingFlags.NonPublic);
if (entryAssemblyField == null)
throw new InvalidOperationException(""Unable to find field m_entryAssembly"");
entryAssemblyField.SetValue(manager, assembly);

var domainManagerField = domain.GetType().GetField(""_domainManager"", BindingFlags.Instance | BindingFlags.NonPublic);
if (domainManagerField == null)
throw new InvalidOperationException(""Unable to find field _domainManager"");
domainManagerField.SetValue(domain, manager);
}
}
}";

var provider = new Microsoft.CSharp.CSharpCodeProvider();
var parameters = new System.CodeDom.Compiler.CompilerParameters
{
GenerateInMemory = true,
GenerateExecutable = false,
ReferencedAssemblies = {"NLog.dll"}
};
System.CodeDom.Compiler.CompilerResults results = provider.CompileAssemblyFromSource(parameters, code);
var compiledAssembly = results.CompiledAssembly;
return compiledAssembly;
}
#endif
}
}
}