.NET Core 3.1でLog4netを使う

`.NET Core` のアプリで、`.NET Framework`同様に`log4net`を使いたかったのですが、多少迷うところがありました。<br>
同様の情報は見つけられなかったので記しておきます。<br>

# TL;DR

下記のようにアセンブリ属性を記載することにより`.NET Core 3.1`でも楽に`log4net`が利用できます。

```cs:Program.cs
using log4net;

[assembly: log4net.Config.XmlConfigurator(ConfigFile="log4net.config",Watch=true)]

namespace console1
{
    class Program
    {
        static ILog logger = LogManager.GetLogger(typeof(Program));

        static void Main(string[] args)
        {
            logger.Info("Hello World!");
        }
    }
}
```

```xml:log4net.config
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <log4net>
        <appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender" >
            <layout type="log4net.Layout.PatternLayout">
                <conversionPattern value="%date [%thread] %-5level %logger - %message%newline" />
            </layout>
        </appender>
        <root>
            <level value="ALL" />
            <appender-ref ref="ConsoleAppender" />
        </root>
    </log4net>
</configuration>
```

# 環境

- .NET Core SDK 3.1.100
- Visual Studio Code 1.41.1
   - C# Plugin
- log4net 2.0.8
- Windows 10 バージョン 2004 (OS ビルド 1941.21)

# 長文

## ログが出力されない。

`dotnet create console` でプロジェクトを作成し、`nuget`で`log4net`をインポートして、さっそくログを出力しようとしましたが、出力されません。<br>
生成されたプロジェクトには`AssemblyInfo.cs`はなく、`.NET Framework`で`log4net`を使うときに書く`XmlConfigurator`属性等の記載を行っていないのでそれが原因かと思われました。<br>

## ワークアラウンドを探す。

Webでワークアラウンドを探しました。<br>
過去の記事ですが、自前でXmlConfiguratorを利用し、Loggerのインスタンスをセットアップするパターンの情報が見つかりました。<br>
このやり方は[公式のマニュアル](https://logging.apache.org/log4net/release/manual/configuration.html)にも記載があります。
この方法で確かに動作しますが、log4netの普段の使い勝手と比べると納得できません。<br>

**ワークアラウンド**

```cs:Program.cs
using log4net;

namespace console1
{
    class Program
    {
        static ILog logger;

        static void Main(string[] args)
        {
            //log4net使う準備ここから
            XmlDocument log4netConfig = new XmlDocument();
            log4netConfig.Load(File.OpenRead(LOG_CONFIG_FILE));
            var repo = LogManager.CreateRepository(  
             Assembly.GetEntryAssembly(), 
             typeof(log4net.Repository.Hierarchy.Hierarchy));
            log4net.Config.XmlConfigurator.Configure(repo, log4netConfig["log4net"]);
            logger = LogManager.GetLogger(type);
            //log4net使う準備ここまで

            logger.Info("Hello World!");
        }
    }
}
```

参考

[How to Configure log4net for .NET Core](https://stackify.com/making-log4net-net-core-work/)

[How To Use Log4Net In ASP.NET Core Application](https://www.c-sharpcorner.com/blogs/how-to-use-log4net-in-asp-net-core-application)


**期待するコード**

```cs
using log4net;

namespace console1
{
    class Program
    {
        static ILog logger = LogManager.GetLogger(typeof(Program));

        static void Main(string[] args)
        {
            logger.Info("Hello World!");
        }
    }
}
```

## そもそも .NET Frameworkではlog4netはどうやって動いていた？

上記のワークアラウンドから推測すると、同等の処理をどこかでやっていたのではないかと思われます。<br>
.NET Frameworkではどのようにlog4netをセットアップしているのか調べてみました。

<details>
<summary>結局 LogManager.GetLogger() からの呼び出して、アセンブリに付与した XmlConfiguratorAttribute に設定したファイルを見ていることがわかりました。</summary>
<div>

- LogManager.GetLogger(Type) -> LogManager.GetLogger(Assembly,String)

**このリンクは2020/02/06時点のURLです。masterブランチが更新されると役に立たないリンクになります。**

https://github.com/apache/logging-log4net/blob/master/src/LogManager.cs#L260

```cs
		/// <summary>
		/// Shorthand for <see cref="M:LogManager.GetLogger(string)"/>.
		/// </summary>
		/// <remarks>
		/// Get the logger for the fully qualified name of the type specified.
		/// </remarks>
		/// <param name="type">The full name of <paramref name="type"/> will be used as the name of the logger to retrieve.</param>
		/// <returns>The logger with the name specified.</returns>
		public static ILog GetLogger(Type type) 
		{
#if NETSTANDARD1_3
			return GetLogger(type.GetTypeInfo().Assembly, type.FullName);
#else
			return GetLogger(Assembly.GetCallingAssembly(), type.FullName);
#endif
		}
```

- LogManager.GetLogger(Assembly,String) -> LoggerManager.GetLogger(Assembly, String)

https://github.com/apache/logging-log4net/blob/master/src/LogManager.cs#L244

```cs
		/// <summary>
		/// Retrieves or creates a named logger.
		/// </summary>
		/// <remarks>
		/// <para>
		/// Retrieve a logger named as the <paramref name="name"/>
		/// parameter. If the named logger already exists, then the
		/// existing instance will be returned. Otherwise, a new instance is
		/// created.
		/// </para>
		/// <para>
		/// By default, loggers do not have a set level but inherit
		/// it from the hierarchy. This is one of the central features of
		/// log4net.
		/// </para>
		/// </remarks>
		/// <param name="repositoryAssembly">The assembly to use to lookup the repository.</param>
		/// <param name="name">The name of the logger to retrieve.</param>
		/// <returns>The logger with the name specified.</returns>
		public static ILog GetLogger(Assembly repositoryAssembly, string name)
		{
			return WrapLogger(LoggerManager.GetLogger(repositoryAssembly, name));
		}
```

- LoggerManager.GetLogger(Assembly, String) -> RepositorySelector.GetRepository(Assembly)

https://github.com/apache/logging-log4net/blob/master/src/Core/LoggerManager.cs#L406

```cs
		/// <summary>
		/// Retrieves or creates a named logger.
		/// </summary>
		/// <param name="repositoryAssembly">The assembly to use to lookup the repository.</param>
		/// <param name="name">The name of the logger to retrieve.</param>
		/// <returns>The logger with the name specified.</returns>
		/// <remarks>
		/// <para>
		/// Retrieves a logger named as the <paramref name="name"/>
		/// parameter. If the named logger already exists, then the
		/// existing instance will be returned. Otherwise, a new instance is
		/// created.
		/// </para>
		/// <para>
		/// By default, loggers do not have a set level but inherit
		/// it from the hierarchy. This is one of the central features of
		/// log4net.
		/// </para>
		/// </remarks>
		public static ILogger GetLogger(Assembly repositoryAssembly, string name)
		{
			if (repositoryAssembly == null)
			{
				throw new ArgumentNullException("repositoryAssembly");
			}
			if (name == null)
			{
				throw new ArgumentNullException("name");
			}
			return RepositorySelector.GetRepository(repositoryAssembly).GetLogger(name);
		}	
```

- RepositorySelector.GetRepository(Assembly) -> RepositorySelector.CreateRepository(Assembly, Type)

https://github.com/apache/logging-log4net/blob/master/src/Core/DefaultRepositorySelector.cs#L143

```cs
		/// <summary>
		/// Gets the <see cref="ILoggerRepository"/> for the specified assembly.
		/// </summary>
		/// <param name="repositoryAssembly">The assembly use to lookup the <see cref="ILoggerRepository"/>.</param>
		/// <remarks>
		/// <para>
		/// The type of the <see cref="ILoggerRepository"/> created and the repository 
		/// to create can be overridden by specifying the <see cref="log4net.Config.RepositoryAttribute"/> 
		/// attribute on the <paramref name="repositoryAssembly"/>.
		/// </para>
		/// <para>
		/// The default values are to use the <see cref="log4net.Repository.Hierarchy.Hierarchy"/> 
		/// implementation of the <see cref="ILoggerRepository"/> interface and to use the
		/// <see cref="AssemblyName.Name"/> as the name of the repository.
		/// </para>
		/// <para>
		/// The <see cref="ILoggerRepository"/> created will be automatically configured using 
		/// any <see cref="log4net.Config.ConfiguratorAttribute"/> attributes defined on
		/// the <paramref name="repositoryAssembly"/>.
		/// </para>
		/// </remarks>
		/// <returns>The <see cref="ILoggerRepository"/> for the assembly</returns>
		/// <exception cref="ArgumentNullException"><paramref name="repositoryAssembly"/> is <see langword="null" />.</exception>
		public ILoggerRepository GetRepository(Assembly repositoryAssembly)
		{
			if (repositoryAssembly == null)
			{
				throw new ArgumentNullException("repositoryAssembly");
			}
			return CreateRepository(repositoryAssembly, m_defaultRepositoryType);
		}
```

- RepositorySelector.CreateRepository(Assembly, Type) -> RepositorySelector.CreateRepository(Assembly, Type, string, bool)

https://github.com/apache/logging-log4net/blob/master/src/Core/DefaultRepositorySelector.cs#L220

```cs
		/// <summary>
		/// Create a new repository for the assembly specified 
		/// </summary>
		/// <param name="repositoryAssembly">the assembly to use to create the repository to associate with the <see cref="ILoggerRepository"/>.</param>
		/// <param name="repositoryType">The type of repository to create, must implement <see cref="ILoggerRepository"/>.</param>
		/// <returns>The repository created.</returns>
		/// <remarks>
		/// <para>
		/// The <see cref="ILoggerRepository"/> created will be associated with the repository
		/// specified such that a call to <see cref="M:GetRepository(Assembly)"/> with the
		/// same assembly specified will return the same repository instance.
		/// </para>
		/// <para>
		/// The type of the <see cref="ILoggerRepository"/> created and
		/// the repository to create can be overridden by specifying the
		/// <see cref="log4net.Config.RepositoryAttribute"/> attribute on the 
		/// <paramref name="repositoryAssembly"/>.  The default values are to use the 
		/// <paramref name="repositoryType"/> implementation of the 
		/// <see cref="ILoggerRepository"/> interface and to use the
		/// <see cref="AssemblyName.Name"/> as the name of the repository.
		/// </para>
		/// <para>
		/// The <see cref="ILoggerRepository"/> created will be automatically
		/// configured using any <see cref="log4net.Config.ConfiguratorAttribute"/> 
		/// attributes defined on the <paramref name="repositoryAssembly"/>.
		/// </para>
		/// <para>
		/// If a repository for the <paramref name="repositoryAssembly"/> already exists
		/// that repository will be returned. An error will not be raised and that 
		/// repository may be of a different type to that specified in <paramref name="repositoryType"/>.
		/// Also the <see cref="log4net.Config.RepositoryAttribute"/> attribute on the
		/// assembly may be used to override the repository type specified in 
		/// <paramref name="repositoryType"/>.
		/// </para>
		/// </remarks>
		/// <exception cref="ArgumentNullException"><paramref name="repositoryAssembly"/> is <see langword="null" />.</exception>
		public ILoggerRepository CreateRepository(Assembly repositoryAssembly, Type repositoryType)
		{
			return CreateRepository(repositoryAssembly, repositoryType, DefaultRepositoryName, true);
		}
```

- RepositorySelector.CreateRepository(Assembly, Type, string, bool) -> RepositorySelector.ConfigureRepository(Assembly, ILoggerRepository)

https://github.com/apache/logging-log4net/blob/master/src/Core/DefaultRepositorySelector.cs#L313

```cs
		/// <summary>
		/// Creates a new repository for the assembly specified.
		/// </summary>
		/// <param name="repositoryAssembly">the assembly to use to create the repository to associate with the <see cref="ILoggerRepository"/>.</param>
		/// <param name="repositoryType">The type of repository to create, must implement <see cref="ILoggerRepository"/>.</param>
		/// <param name="repositoryName">The name to assign to the created repository</param>
		/// <param name="readAssemblyAttributes">Set to <c>true</c> to read and apply the assembly attributes</param>
		/// <returns>The repository created.</returns>
		/// <remarks>
		/// <para>
		/// The <see cref="ILoggerRepository"/> created will be associated with the repository
		/// specified such that a call to <see cref="M:GetRepository(Assembly)"/> with the
		/// same assembly specified will return the same repository instance.
		/// </para>
		/// <para>
		/// The type of the <see cref="ILoggerRepository"/> created and
		/// the repository to create can be overridden by specifying the
		/// <see cref="log4net.Config.RepositoryAttribute"/> attribute on the 
		/// <paramref name="repositoryAssembly"/>.  The default values are to use the 
		/// <paramref name="repositoryType"/> implementation of the 
		/// <see cref="ILoggerRepository"/> interface and to use the
		/// <see cref="AssemblyName.Name"/> as the name of the repository.
		/// </para>
		/// <para>
		/// The <see cref="ILoggerRepository"/> created will be automatically
		/// configured using any <see cref="log4net.Config.ConfiguratorAttribute"/> 
		/// attributes defined on the <paramref name="repositoryAssembly"/>.
		/// </para>
		/// <para>
		/// If a repository for the <paramref name="repositoryAssembly"/> already exists
		/// that repository will be returned. An error will not be raised and that 
		/// repository may be of a different type to that specified in <paramref name="repositoryType"/>.
		/// Also the <see cref="log4net.Config.RepositoryAttribute"/> attribute on the
		/// assembly may be used to override the repository type specified in 
		/// <paramref name="repositoryType"/>.
		/// </para>
		/// </remarks>
		/// <exception cref="ArgumentNullException"><paramref name="repositoryAssembly"/> is <see langword="null" />.</exception>
		public ILoggerRepository CreateRepository(Assembly repositoryAssembly, Type repositoryType, string repositoryName, bool readAssemblyAttributes)
		{
			if (repositoryAssembly == null)
			{
				throw new ArgumentNullException("repositoryAssembly");
			}

			// If the type is not set then use the default type
			if (repositoryType == null)
			{
				repositoryType = m_defaultRepositoryType;
			}

			lock(this)
			{
				// Lookup in map
				ILoggerRepository rep = m_assembly2repositoryMap[repositoryAssembly] as ILoggerRepository;
				if (rep == null)
				{
					// Not found, therefore create
					LogLog.Debug(declaringType, "Creating repository for assembly [" + repositoryAssembly + "]");

					// Must specify defaults
					string actualRepositoryName = repositoryName;
					Type actualRepositoryType = repositoryType;

					if (readAssemblyAttributes)
					{
						// Get the repository and type from the assembly attributes
						GetInfoForAssembly(repositoryAssembly, ref actualRepositoryName, ref actualRepositoryType);
					}

					LogLog.Debug(declaringType, "Assembly [" + repositoryAssembly + "] using repository [" + actualRepositoryName + "] and repository type [" + actualRepositoryType + "]");

					// Lookup the repository in the map (as this may already be defined)
					rep = m_name2repositoryMap[actualRepositoryName] as ILoggerRepository;
					if (rep == null)
					{
						// Create the repository
						rep = CreateRepository(actualRepositoryName, actualRepositoryType);

						if (readAssemblyAttributes)
						{
							try
							{
								// Look for aliasing attributes
								LoadAliases(repositoryAssembly, rep);

								// Look for plugins defined on the assembly
								LoadPlugins(repositoryAssembly, rep);

								// Configure the repository using the assembly attributes
								ConfigureRepository(repositoryAssembly, rep);
							}
							catch (Exception ex)
							{
								LogLog.Error(declaringType, "Failed to configure repository [" + actualRepositoryName + "] from assembly attributes.", ex);
							}
						}
					}
					else
					{
						LogLog.Debug(declaringType, "repository [" + actualRepositoryName + "] already exists, using repository type [" + rep.GetType().FullName + "]");

						if (readAssemblyAttributes)
						{
							try
							{
								// Look for plugins defined on the assembly
								LoadPlugins(repositoryAssembly, rep);
							}
							catch (Exception ex)
							{
								LogLog.Error(declaringType, "Failed to configure repository [" + actualRepositoryName + "] from assembly attributes.", ex);
							}
						}
					}
					m_assembly2repositoryMap[repositoryAssembly] = rep;
				}
				return rep;
			}
		}
```

- RepositorySelector.ConfigureRepository(Assembly, ILoggerRepository) -> XmlConfiguratorAttribute.Configure(Assembly, ILoggerRepository)

https://github.com/apache/logging-log4net/blob/master/src/Core/DefaultRepositorySelector.cs#L683

```cs
		/// <summary>
		/// Configures the repository using information from the assembly.
		/// </summary>
		/// <param name="assembly">The assembly containing <see cref="log4net.Config.ConfiguratorAttribute"/>
		/// attributes which define the configuration for the repository.</param>
		/// <param name="repository">The repository to configure.</param>
		/// <exception cref="ArgumentNullException">
		///	<para><paramref name="assembly" /> is <see langword="null" />.</para>
		///	<para>-or-</para>
		///	<para><paramref name="repository" /> is <see langword="null" />.</para>
		/// </exception>
		private void ConfigureRepository(Assembly assembly, ILoggerRepository repository)
		{
			if (assembly == null)
			{
				throw new ArgumentNullException("assembly");
			}
			if (repository == null)
			{
				throw new ArgumentNullException("repository");
			}

			// Look for the Configurator attributes (e.g. XmlConfiguratorAttribute) on the assembly
#if NETSTANDARD1_3
			object[] configAttributes = assembly.GetCustomAttributes(typeof(log4net.Config.ConfiguratorAttribute)).ToArray();
#else
			object[] configAttributes = Attribute.GetCustomAttributes(assembly, typeof(log4net.Config.ConfiguratorAttribute), false);
#endif
			if (configAttributes != null && configAttributes.Length > 0)
			{
				// Sort the ConfiguratorAttributes in priority order
				Array.Sort(configAttributes);

				// Delegate to the attribute the job of configuring the repository
				foreach(log4net.Config.ConfiguratorAttribute configAttr in configAttributes)
				{
					if (configAttr != null)
					{
						try
						{
							configAttr.Configure(assembly, repository);
						}
						catch (Exception ex)
						{
							LogLog.Error(declaringType, "Exception calling ["+configAttr.GetType().FullName+"] .Configure method.", ex);
						}
					}
				}
			}

			if (repository.Name == DefaultRepositoryName)
			{
				// Try to configure the default repository using an AppSettings specified config file
				// Do this even if the repository has been configured (or claims to be), this allows overriding
				// of the default config files etc, if that is required.

				string repositoryConfigFile = SystemInfo.GetAppSetting("log4net.Config");
				if (repositoryConfigFile != null && repositoryConfigFile.Length > 0)
				{
					string applicationBaseDirectory = null;
					try
					{
						applicationBaseDirectory = SystemInfo.ApplicationBaseDirectory;
					}
					catch(Exception ex)
					{
						LogLog.Warn(declaringType, "Exception getting ApplicationBaseDirectory. appSettings log4net.Config path ["+repositoryConfigFile+"] will be treated as an absolute URI", ex);
					}

                    string repositoryConfigFilePath = repositoryConfigFile;
                    if (applicationBaseDirectory != null)
                    {
                        repositoryConfigFilePath = Path.Combine(applicationBaseDirectory, repositoryConfigFile);
                    }

                    // Determine whether to watch the file or not based on an app setting value:
				    bool watchRepositoryConfigFile = false;
#if NET_2_0 || MONO_2_0 || MONO_3_5 || MONO_4_0 || NETSTANDARD1_3
				    Boolean.TryParse(SystemInfo.GetAppSetting("log4net.Config.Watch"), out watchRepositoryConfigFile);
#else
                                    {
                                        string watch = SystemInfo.GetAppSetting("log4net.Config.Watch");
                                        if (watch != null && watch.Length > 0)
                                        {
                                            try
                                            {
                                                watchRepositoryConfigFile = Boolean.Parse(watch);
                                            }
                                            catch (FormatException)
                                            {
                                                // simply not a Boolean
                                            }
                                        }
                                    }
#endif

					if (watchRepositoryConfigFile)
					{
 						// As we are going to watch the config file it is required to resolve it as a 
 						// physical file system path pass that in a FileInfo object to the Configurator
						FileInfo repositoryConfigFileInfo = null;
						try
						{
							repositoryConfigFileInfo = new FileInfo(repositoryConfigFilePath);
						}
						catch (Exception ex)
						{
                            LogLog.Error(declaringType, "DefaultRepositorySelector: Exception while parsing log4net.Config file physical path [" + repositoryConfigFilePath + "]", ex);
						}
						try
						{
                            LogLog.Debug(declaringType, "Loading and watching configuration for default repository from AppSettings specified Config path [" + repositoryConfigFilePath + "]");

                            XmlConfigurator.ConfigureAndWatch(repository, repositoryConfigFileInfo);
						}
						catch (Exception ex)
						{
                            LogLog.Error(declaringType, "DefaultRepositorySelector: Exception calling XmlConfigurator.ConfigureAndWatch method with ConfigFilePath [" + repositoryConfigFilePath + "]", ex);
						}
					}
					else
					{
                    // As we are not going to watch the config file it is easiest to just resolve it as a 
					// URI and pass that to the Configurator
					Uri repositoryConfigUri = null;
					try
					{
					    repositoryConfigUri = new Uri(repositoryConfigFilePath);
					}
					catch(Exception ex)
					{
						LogLog.Error(declaringType, "Exception while parsing log4net.Config file path ["+repositoryConfigFile+"]", ex);
					}

					if (repositoryConfigUri != null)
					{
						LogLog.Debug(declaringType, "Loading configuration for default repository from AppSettings specified Config URI ["+repositoryConfigUri.ToString()+"]");

						try
						{
							// TODO: Support other types of configurator
							XmlConfigurator.Configure(repository, repositoryConfigUri);
						}
						catch (Exception ex)
						{
							LogLog.Error(declaringType, "Exception calling XmlConfigurator.Configure method with ConfigUri ["+repositoryConfigUri+"]", ex);
						}
					}
                    }
				}
			}
		}
```

- XmlConfiguratorAttribute.Configure(Assembly, ILoggerRepository) -> XmlConfiguratorAttribute.ConfigureFromFile(Assembly, ILoggerRepository)

https://github.com/apache/logging-log4net/blob/master/src/Config/XmlConfiguratorAttribute.cs#L218

```cs
		#region Override ConfiguratorAttribute

		/// <summary>
		/// Configures the <see cref="ILoggerRepository"/> for the specified assembly.
		/// </summary>
		/// <param name="sourceAssembly">The assembly that this attribute was defined on.</param>
		/// <param name="targetRepository">The repository to configure.</param>
		/// <remarks>
		/// <para>
		/// Configure the repository using the <see cref="XmlConfigurator"/>.
		/// The <paramref name="targetRepository"/> specified must extend the <see cref="Hierarchy"/>
		/// class otherwise the <see cref="XmlConfigurator"/> will not be able to
		/// configure it.
		/// </para>
		/// </remarks>
        /// <exception cref="ArgumentOutOfRangeException">The <paramref name="targetRepository" /> does not extend <see cref="Hierarchy"/>.</exception>
		override public void Configure(Assembly sourceAssembly, ILoggerRepository targetRepository)
		{
            IList configurationMessages = new ArrayList();

            using (new LogLog.LogReceivedAdapter(configurationMessages))
            {
                string applicationBaseDirectory = null;
                try
                {
                    applicationBaseDirectory = SystemInfo.ApplicationBaseDirectory;
                }
                catch
                {
                    // Ignore this exception because it is only thrown when ApplicationBaseDirectory is a file
                    // and the application does not have PathDiscovery permission
                }

                if (applicationBaseDirectory == null || (new Uri(applicationBaseDirectory)).IsFile)
                {
                    ConfigureFromFile(sourceAssembly, targetRepository);
                }
                else
                {
                    ConfigureFromUri(sourceAssembly, targetRepository);
                }
            }

            targetRepository.ConfigurationMessages = configurationMessages;
		}

		#endregion
```

- XmlConfiguratorAttribute.ConfigureFromFile(Assembly, ILoggerRepository) -> ConfigureFromFile(ILoggerRepository, FileInfo)

https://github.com/apache/logging-log4net/blob/master/src/Config/XmlConfiguratorAttribute.cs#L305

```cs
		/// <summary>
		/// Attempt to load configuration from the local file system
		/// </summary>
		/// <param name="sourceAssembly">The assembly that this attribute was defined on.</param>
		/// <param name="targetRepository">The repository to configure.</param>
		private void ConfigureFromFile(Assembly sourceAssembly, ILoggerRepository targetRepository)
		{
			// Work out the full path to the config file
			string fullPath2ConfigFile = null;
			
			// Select the config file
			if (m_configFile == null || m_configFile.Length == 0)
			{
				if (m_configFileExtension == null || m_configFileExtension.Length == 0)
				{
					// Use the default .config file for the AppDomain
					try
					{
						fullPath2ConfigFile = SystemInfo.ConfigurationFileLocation;
					}
					catch(Exception ex)
					{
						LogLog.Error(declaringType, "XmlConfiguratorAttribute: Exception getting ConfigurationFileLocation. Must be able to resolve ConfigurationFileLocation when ConfigFile and ConfigFileExtension properties are not set.", ex);
					}
				}
				else
				{
					// Force the extension to start with a '.'
					if (m_configFileExtension[0] != '.')
					{
						m_configFileExtension = "." + m_configFileExtension;
					}

					string applicationBaseDirectory = null;
					try
					{
						applicationBaseDirectory = SystemInfo.ApplicationBaseDirectory;
					}
					catch(Exception ex)
					{
						LogLog.Error(declaringType, "Exception getting ApplicationBaseDirectory. Must be able to resolve ApplicationBaseDirectory and AssemblyFileName when ConfigFileExtension property is set.", ex);
					}

					if (applicationBaseDirectory != null)
					{
						fullPath2ConfigFile = Path.Combine(applicationBaseDirectory, SystemInfo.AssemblyFileName(sourceAssembly) + m_configFileExtension);
					}
				}
			}
			else
			{
				string applicationBaseDirectory = null;
				try
				{
					applicationBaseDirectory = SystemInfo.ApplicationBaseDirectory;
				}
				catch(Exception ex)
				{
					LogLog.Warn(declaringType, "Exception getting ApplicationBaseDirectory. ConfigFile property path ["+m_configFile+"] will be treated as an absolute path.", ex);
				}

				if (applicationBaseDirectory != null)
				{
					// Just the base dir + the config file
					fullPath2ConfigFile = Path.Combine(applicationBaseDirectory, m_configFile);
				}
				else
				{
					fullPath2ConfigFile = m_configFile;
				}
			}

			if (fullPath2ConfigFile != null)
			{
				ConfigureFromFile(targetRepository, new FileInfo(fullPath2ConfigFile));
			}
		}
```

- ConfigureFromFile(ILoggerRepository, FileInfo) -> XmlConfigurator.Configure(ILoggerRepository, FileInfo)

https://github.com/apache/logging-log4net/blob/master/src/Config/XmlConfiguratorAttribute.cs#L330

```cs
		/// <summary>
		/// Configure the specified repository using a <see cref="FileInfo"/>
		/// </summary>
		/// <param name="targetRepository">The repository to configure.</param>
		/// <param name="configFile">the FileInfo pointing to the config file</param>
		private void ConfigureFromFile(ILoggerRepository targetRepository, FileInfo configFile)
		{
#if (SSCLI)
			if (m_configureAndWatch)
			{
				LogLog.Warn(declaringType, "XmlConfiguratorAttribute: Unable to watch config file not supported on SSCLI");
			}
			XmlConfigurator.Configure(targetRepository, configFile);
#else
			// Do we configure just once or do we configure and then watch?
			if (m_configureAndWatch)
			{
				XmlConfigurator.ConfigureAndWatch(targetRepository, configFile);
			}
			else
			{
				XmlConfigurator.Configure(targetRepository, configFile);
			}
#endif
		}
```
</div>
</details>

## `XmlConfiguratorAttribute`を定義したいが、`.NET Core`の`AssemblyInfo.cs`はどこへいった？

結局のところ、`XmlConfigurator`属性をアセンブリに付与することで`.NET Core`でも動作するであろうことがわかりました。<br>

では、`.NET Core`では`AssemblyInfo.cs`はどこへ行ったのでしょうか。<br>

`.NET Core`のドキュメントを見ると`.NET Framework`で、`AssemblyInfo.cs`に記載していたような情報は、`.NET Core`ではビルド時に各所から情報を集めて自動生成されるようになったとのこと。<br>

参考：[.NET Core の csproj 形式に追加されたもの](https://docs.microsoft.com/ja-jp/dotnet/core/tools/csproj#assemblyinfo-properties)

>通常、AssemblyInfo ファイル内に存在していたアセンブリ属性は、プロパティから自動的に生成されるようになりました。

`obj`フォルダ配下に`AssemblyInfo`という名前のファイルが生成されているのでそういうことなのだと理解しました。<br>

objファイル配下のファイル

```txt
└─obj
    │  console1.csproj.nuget.cache
    │  console1.csproj.nuget.dgspec.json
    │  console1.csproj.nuget.g.props
    │  console1.csproj.nuget.g.targets
    │  project.assets.json
    │
    └─Debug
        └─netcoreapp3.1
                console1.AssemblyInfo.cs
                console1.AssemblyInfoInputs.cache
                console1.assets.cache
                console1.csproj.CopyComplete
                console1.csproj.FileListAbsolute.txt
                console1.csprojAssemblyReference.cache
                console1.dll
                console1.exe
                console1.pdb
```

## それでは、`.NET Core`ではアセンブリ属性はどこに記載すればよいのか？

`.NET Standard`の[ドキュメント](https://docs.microsoft.com/en-us/dotnet/standard/assembly/set-attributes)を見ると、結局のところコードで下記のように記載すればアセンブリ属性が定義できることがわかりました。特に`AssemblyInfo.cs`に書く必要は無いようです。

> [assembly:AssemblyKeyFileAttribute("myKey.snk")]
> [assembly:AssemblyDelaySignAttribute(true)]

ドキュメントに倣い、プログラムを下記のように修正すると無事`log4net`が動作するようになりました。<br>
__遠回りしたわりには答えは近くにありましたというオチでした。__

```cs:Program.cs
using log4net;

[assembly: log4net.Config.XmlConfigurator(ConfigFile="log4net.config",Watch=true)]

namespace console1
{
    class Program
    {
        static ILog logger = LogManager.GetLogger(typeof(Program));

        static void Main(string[] args)
        {
            logger.Info("Hello World!");
        }
    }
}
```

実行結果

```ps
PS C:\projects\core_with_log4net\console1> dotnet run
2020-02-08 23:13:09,447 [1] INFO  console1.Program - Hello World!
PS C:\projects\core_with_log4net\console1> 
```