From 60fa8cade8d077fd644947f5794d90da2d4cef66 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?C=C3=A9dric=20Luthi?= <cedric.luthi@gmail.com>
Date: Fri, 1 Oct 2021 21:53:04 +0200
Subject: [PATCH 01/14] Add support for IMessageSink

This enables logging in extensibility classes such as fixtures and discoverers. See https://xunit.net/docs/capturing-output#output-in-extensions for more information.

All classes were modeled after the similar classes for `ITestOutputHelper`. The only method impossible to replicate was `public static ILoggingBuilder AddXUnit(this ILoggingBuilder builder)`.
---
 Directory.Packages.props                      |   3 +-
 src/Logging.XUnit/IMessageSinkAccessor.cs     |  18 +
 src/Logging.XUnit/IMessageSinkExtensions.cs   |  50 +++
 .../MartinCostello.Logging.XUnit.csproj       |   1 +
 src/Logging.XUnit/MessageSinkAccessor.cs      |  31 ++
 src/Logging.XUnit/XUnitLogger.IMessageSink.cs |  66 ++++
 .../XUnitLogger.ITestOutputHelper.cs          |  49 +++
 src/Logging.XUnit/XUnitLogger.cs              |  58 +--
 .../XUnitLoggerExtensions.IMessageSink.cs     | 342 ++++++++++++++++++
 ...UnitLoggerExtensions.ITestOutputHelper.cs} |   2 +-
 src/Logging.XUnit/XUnitLoggerOptions.cs       |   8 +
 .../XUnitLoggerProvider.IMessageSink.cs       |  47 +++
 .../XUnitLoggerProvider.ITestOutputHelper.cs  |  47 +++
 src/Logging.XUnit/XUnitLoggerProvider.cs      |  49 +--
 tests/Logging.XUnit.Tests/Constructor.cs      |  11 +
 .../Integration/DatabaseFixture.cs            |  40 ++
 .../Integration/DatabaseTests.cs              |  24 ++
 .../Integration/PrintableDiagnosticMessage.cs |  20 +
 .../XUnitLoggerExtensionsTests.cs             |  65 +++-
 .../XUnitLoggerProviderTests.cs               |  32 +-
 tests/Logging.XUnit.Tests/XUnitLoggerTests.cs |  33 +-
 tests/Logging.XUnit.Tests/xunit.runner.json   |   2 +
 22 files changed, 907 insertions(+), 91 deletions(-)
 create mode 100644 src/Logging.XUnit/IMessageSinkAccessor.cs
 create mode 100644 src/Logging.XUnit/IMessageSinkExtensions.cs
 create mode 100644 src/Logging.XUnit/MessageSinkAccessor.cs
 create mode 100644 src/Logging.XUnit/XUnitLogger.IMessageSink.cs
 create mode 100644 src/Logging.XUnit/XUnitLogger.ITestOutputHelper.cs
 create mode 100644 src/Logging.XUnit/XUnitLoggerExtensions.IMessageSink.cs
 rename src/Logging.XUnit/{XUnitLoggerExtensions.cs => XUnitLoggerExtensions.ITestOutputHelper.cs} (99%)
 create mode 100644 src/Logging.XUnit/XUnitLoggerProvider.IMessageSink.cs
 create mode 100644 src/Logging.XUnit/XUnitLoggerProvider.ITestOutputHelper.cs
 create mode 100644 tests/Logging.XUnit.Tests/Constructor.cs
 create mode 100644 tests/Logging.XUnit.Tests/Integration/DatabaseFixture.cs
 create mode 100644 tests/Logging.XUnit.Tests/Integration/DatabaseTests.cs
 create mode 100644 tests/Logging.XUnit.Tests/Integration/PrintableDiagnosticMessage.cs

diff --git a/Directory.Packages.props b/Directory.Packages.props
index 132ae7ed..4c3d48f5 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -11,7 +11,8 @@
   </ItemGroup>
   <ItemGroup Condition=" '$(IsTestProject)' != 'true' ">
     <PackageVersion Include="Microsoft.Extensions.Logging" Version="2.0.0" />
-    <PackageVersion Include="xunit.abstractions" Version="2.0.1" />
+    <PackageVersion Include="xunit.abstractions" Version="2.0.3" />
+    <PackageVersion Include="xunit.extensibility.execution" Version="2.4.1" />
   </ItemGroup>
   <ItemGroup Condition=" '$(IsTestProject)' == 'true' ">
     <PackageVersion Include="Microsoft.Extensions.Logging" Version="5.0.0" />
diff --git a/src/Logging.XUnit/IMessageSinkAccessor.cs b/src/Logging.XUnit/IMessageSinkAccessor.cs
new file mode 100644
index 00000000..724961ed
--- /dev/null
+++ b/src/Logging.XUnit/IMessageSinkAccessor.cs
@@ -0,0 +1,18 @@
+// Copyright (c) Martin Costello, 2018. All rights reserved.
+// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
+
+using Xunit.Abstractions;
+
+namespace MartinCostello.Logging.XUnit
+{
+    /// <summary>
+    /// Defines a property for accessing an <see cref="IMessageSink"/>.
+    /// </summary>
+    public interface IMessageSinkAccessor
+    {
+        /// <summary>
+        /// Gets or sets the <see cref="IMessageSink"/> to use.
+        /// </summary>
+        IMessageSink? MessageSink { get; set; }
+    }
+}
diff --git a/src/Logging.XUnit/IMessageSinkExtensions.cs b/src/Logging.XUnit/IMessageSinkExtensions.cs
new file mode 100644
index 00000000..34ff47ec
--- /dev/null
+++ b/src/Logging.XUnit/IMessageSinkExtensions.cs
@@ -0,0 +1,50 @@
+// Copyright (c) Martin Costello, 2018. All rights reserved.
+// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
+
+using System;
+using System.ComponentModel;
+using Microsoft.Extensions.Logging;
+
+namespace Xunit.Abstractions
+{
+    /// <summary>
+    /// A class containing extension methods for the <see cref="IMessageSink"/> interface. This class cannot be inherited.
+    /// </summary>
+    [EditorBrowsable(EditorBrowsableState.Never)]
+    public static class IMessageSinkExtensions
+    {
+        /// <summary>
+        /// Returns an <see cref="ILoggerFactory"/> that logs to the message sink.
+        /// </summary>
+        /// <param name="messageSink">The <see cref="IMessageSink"/> to create the logger factory from.</param>
+        /// <returns>
+        /// An <see cref="ILoggerFactory"/> that writes messages to the message sink.
+        /// </returns>
+        /// <exception cref="ArgumentNullException">
+        /// <paramref name="messageSink"/> is <see langword="null"/>.
+        /// </exception>
+        public static ILoggerFactory ToLoggerFactory(this IMessageSink messageSink)
+        {
+            if (messageSink == null)
+            {
+                throw new ArgumentNullException(nameof(messageSink));
+            }
+
+            return new LoggerFactory().AddXUnit(messageSink);
+        }
+
+        /// <summary>
+        /// Returns an <see cref="ILogger{T}"/> that logs to the message sink.
+        /// </summary>
+        /// <typeparam name="T">The type of the logger to create.</typeparam>
+        /// <param name="messageSink">The <see cref="IMessageSink"/> to create the logger from.</param>
+        /// <returns>
+        /// An <see cref="ILogger{T}"/> that writes messages to the message sink.
+        /// </returns>
+        /// <exception cref="ArgumentNullException">
+        /// <paramref name="messageSink"/> is <see langword="null"/>.
+        /// </exception>
+        public static ILogger<T> ToLogger<T>(this IMessageSink messageSink)
+            => messageSink.ToLoggerFactory().CreateLogger<T>();
+    }
+}
diff --git a/src/Logging.XUnit/MartinCostello.Logging.XUnit.csproj b/src/Logging.XUnit/MartinCostello.Logging.XUnit.csproj
index c715777d..fc133a4c 100644
--- a/src/Logging.XUnit/MartinCostello.Logging.XUnit.csproj
+++ b/src/Logging.XUnit/MartinCostello.Logging.XUnit.csproj
@@ -17,5 +17,6 @@
   <ItemGroup>
     <PackageReference Include="Microsoft.Extensions.Logging" />
     <PackageReference Include="xunit.abstractions" />
+    <PackageReference Include="xunit.extensibility.execution" />
   </ItemGroup>
 </Project>
diff --git a/src/Logging.XUnit/MessageSinkAccessor.cs b/src/Logging.XUnit/MessageSinkAccessor.cs
new file mode 100644
index 00000000..a932867d
--- /dev/null
+++ b/src/Logging.XUnit/MessageSinkAccessor.cs
@@ -0,0 +1,31 @@
+// Copyright (c) Martin Costello, 2018. All rights reserved.
+// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
+
+using System;
+using Xunit.Abstractions;
+
+namespace MartinCostello.Logging.XUnit
+{
+    /// <summary>
+    /// A class representing the default implementation of <see cref="IMessageSinkAccessor"/>. This class cannot be inherited.
+    /// </summary>
+    internal sealed class MessageSinkAccessor : IMessageSinkAccessor
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="MessageSinkAccessor"/> class.
+        /// </summary>
+        /// <param name="messageSink">The <see cref="IMessageSink"/> to use.</param>
+        /// <exception cref="ArgumentNullException">
+        /// <paramref name="messageSink"/> is <see langword="null"/>.
+        /// </exception>
+        internal MessageSinkAccessor(IMessageSink messageSink)
+        {
+            MessageSink = messageSink ?? throw new ArgumentNullException(nameof(messageSink));
+        }
+
+        /// <summary>
+        /// Gets or sets the current <see cref="IMessageSink"/>.
+        /// </summary>
+        public IMessageSink? MessageSink { get; set; }
+    }
+}
diff --git a/src/Logging.XUnit/XUnitLogger.IMessageSink.cs b/src/Logging.XUnit/XUnitLogger.IMessageSink.cs
new file mode 100644
index 00000000..9cb27d67
--- /dev/null
+++ b/src/Logging.XUnit/XUnitLogger.IMessageSink.cs
@@ -0,0 +1,66 @@
+// Copyright (c) Martin Costello, 2018. All rights reserved.
+// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
+
+using System;
+using Microsoft.Extensions.Logging;
+using Xunit.Abstractions;
+
+namespace MartinCostello.Logging.XUnit
+{
+    /// <summary>
+    /// A class representing an <see cref="ILogger"/> to use with xunit.
+    /// </summary>
+    public partial class XUnitLogger
+    {
+        /// <summary>
+        /// The <see cref="IMessageSinkAccessor"/> to use. This field is read-only.
+        /// </summary>
+        private readonly IMessageSinkAccessor? _messageSinkAccessor;
+
+        /// <summary>
+        /// Gets or sets the message sink message factory to use when writing to a <see cref="IMessageSink"/>.
+        /// </summary>
+        private Func<string, IMessageSinkMessage> _messageSinkMessageFactory;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="XUnitLogger"/> class.
+        /// </summary>
+        /// <param name="name">The name for messages produced by the logger.</param>
+        /// <param name="messageSink">The <see cref="IMessageSink"/> to use.</param>
+        /// <param name="options">The <see cref="XUnitLoggerOptions"/> to use.</param>
+        /// <exception cref="ArgumentNullException">
+        /// <paramref name="name"/> or <paramref name="messageSink"/> is <see langword="null"/>.
+        /// </exception>
+        public XUnitLogger(string name, IMessageSink messageSink, XUnitLoggerOptions? options)
+            : this(name, new MessageSinkAccessor(messageSink), options)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="XUnitLogger"/> class.
+        /// </summary>
+        /// <param name="name">The name for messages produced by the logger.</param>
+        /// <param name="accessor">The <see cref="IMessageSinkAccessor"/> to use.</param>
+        /// <param name="options">The <see cref="XUnitLoggerOptions"/> to use.</param>
+        /// <exception cref="ArgumentNullException">
+        /// <paramref name="name"/> or <paramref name="accessor"/> is <see langword="null"/>.
+        /// </exception>
+        public XUnitLogger(string name, IMessageSinkAccessor accessor, XUnitLoggerOptions? options)
+            : this(name, options)
+        {
+            _messageSinkAccessor = accessor ?? throw new ArgumentNullException(nameof(accessor));
+        }
+
+        /// <summary>
+        /// Gets or sets the message sink message factory to use when writing to a <see cref="IMessageSink"/>.
+        /// </summary>
+        /// <exception cref="ArgumentNullException">
+        /// <paramref name="value"/> is <see langword="null"/>.
+        /// </exception>
+        public Func<string, IMessageSinkMessage> MessageSinkMessageFactory
+        {
+            get { return _messageSinkMessageFactory; }
+            set { _messageSinkMessageFactory = value ?? throw new ArgumentNullException(nameof(value)); }
+        }
+    }
+}
diff --git a/src/Logging.XUnit/XUnitLogger.ITestOutputHelper.cs b/src/Logging.XUnit/XUnitLogger.ITestOutputHelper.cs
new file mode 100644
index 00000000..84630e6b
--- /dev/null
+++ b/src/Logging.XUnit/XUnitLogger.ITestOutputHelper.cs
@@ -0,0 +1,49 @@
+// Copyright (c) Martin Costello, 2018. All rights reserved.
+// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
+
+using System;
+using Microsoft.Extensions.Logging;
+using Xunit.Abstractions;
+
+namespace MartinCostello.Logging.XUnit
+{
+    /// <summary>
+    /// A class representing an <see cref="ILogger"/> to use with xunit.
+    /// </summary>
+    public partial class XUnitLogger
+    {
+        /// <summary>
+        /// The <see cref="ITestOutputHelperAccessor"/> to use. This field is read-only.
+        /// </summary>
+        private readonly ITestOutputHelperAccessor? _outputHelperAccessor;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="XUnitLogger"/> class.
+        /// </summary>
+        /// <param name="name">The name for messages produced by the logger.</param>
+        /// <param name="outputHelper">The <see cref="ITestOutputHelper"/> to use.</param>
+        /// <param name="options">The <see cref="XUnitLoggerOptions"/> to use.</param>
+        /// <exception cref="ArgumentNullException">
+        /// <paramref name="name"/> or <paramref name="outputHelper"/> is <see langword="null"/>.
+        /// </exception>
+        public XUnitLogger(string name, ITestOutputHelper outputHelper, XUnitLoggerOptions? options)
+            : this(name, new TestOutputHelperAccessor(outputHelper), options)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="XUnitLogger"/> class.
+        /// </summary>
+        /// <param name="name">The name for messages produced by the logger.</param>
+        /// <param name="accessor">The <see cref="ITestOutputHelperAccessor"/> to use.</param>
+        /// <param name="options">The <see cref="XUnitLoggerOptions"/> to use.</param>
+        /// <exception cref="ArgumentNullException">
+        /// <paramref name="name"/> or <paramref name="accessor"/> is <see langword="null"/>.
+        /// </exception>
+        public XUnitLogger(string name, ITestOutputHelperAccessor accessor, XUnitLoggerOptions? options)
+            : this(name, options)
+        {
+            _outputHelperAccessor = accessor ?? throw new ArgumentNullException(nameof(accessor));
+        }
+    }
+}
diff --git a/src/Logging.XUnit/XUnitLogger.cs b/src/Logging.XUnit/XUnitLogger.cs
index c108e621..94f035fc 100644
--- a/src/Logging.XUnit/XUnitLogger.cs
+++ b/src/Logging.XUnit/XUnitLogger.cs
@@ -2,19 +2,18 @@
 // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
 
 using System;
-using System.Collections;
 using System.Collections.Generic;
-using System.Linq;
 using System.Text;
 using Microsoft.Extensions.Logging;
 using Xunit.Abstractions;
+using Xunit.Sdk;
 
 namespace MartinCostello.Logging.XUnit
 {
     /// <summary>
     /// A class representing an <see cref="ILogger"/> to use with xunit.
     /// </summary>
-    public class XUnitLogger : ILogger
+    public partial class XUnitLogger : ILogger
     {
         //// Based on https://github.com/aspnet/Logging/blob/master/src/Microsoft.Extensions.Logging.Console/ConsoleLogger.cs
 
@@ -39,11 +38,6 @@ public class XUnitLogger : ILogger
         [ThreadStatic]
         private static StringBuilder? _logBuilder;
 
-        /// <summary>
-        /// The <see cref="ITestOutputHelperAccessor"/> to use. This field is read-only.
-        /// </summary>
-        private readonly ITestOutputHelperAccessor _accessor;
-
         /// <summary>
         /// Gets or sets the filter to use.
         /// </summary>
@@ -53,31 +47,13 @@ public class XUnitLogger : ILogger
         /// Initializes a new instance of the <see cref="XUnitLogger"/> class.
         /// </summary>
         /// <param name="name">The name for messages produced by the logger.</param>
-        /// <param name="outputHelper">The <see cref="ITestOutputHelper"/> to use.</param>
-        /// <param name="options">The <see cref="XUnitLoggerOptions"/> to use.</param>
-        /// <exception cref="ArgumentNullException">
-        /// <paramref name="name"/> or <paramref name="outputHelper"/> is <see langword="null"/>.
-        /// </exception>
-        public XUnitLogger(string name, ITestOutputHelper outputHelper, XUnitLoggerOptions? options)
-            : this(name, new TestOutputHelperAccessor(outputHelper), options)
-        {
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="XUnitLogger"/> class.
-        /// </summary>
-        /// <param name="name">The name for messages produced by the logger.</param>
-        /// <param name="accessor">The <see cref="ITestOutputHelperAccessor"/> to use.</param>
         /// <param name="options">The <see cref="XUnitLoggerOptions"/> to use.</param>
-        /// <exception cref="ArgumentNullException">
-        /// <paramref name="name"/> or <paramref name="accessor"/> is <see langword="null"/>.
-        /// </exception>
-        public XUnitLogger(string name, ITestOutputHelperAccessor accessor, XUnitLoggerOptions? options)
+        private XUnitLogger(string name, XUnitLoggerOptions? options)
         {
             Name = name ?? throw new ArgumentNullException(nameof(name));
-            _accessor = accessor ?? throw new ArgumentNullException(nameof(accessor));
 
-            _filter = options?.Filter ?? ((category, logLevel) => true);
+            _filter = options?.Filter ?? ((_, _) => true);
+            _messageSinkMessageFactory = options?.MessageSinkMessageFactory ?? (message => new DiagnosticMessage(message));
             IncludeScopes = options?.IncludeScopes ?? false;
         }
 
@@ -152,7 +128,7 @@ public void Log<TState>(LogLevel logLevel, EventId eventId, TState? state, Excep
         }
 
         /// <summary>
-        /// Writes a message to the <see cref="ITestOutputHelper"/> associated with the instance.
+        /// Writes a message to the <see cref="ITestOutputHelper"/> or <see cref="IMessageSink"/> associated with the instance.
         /// </summary>
         /// <param name="logLevel">The message to write will be written on this level.</param>
         /// <param name="eventId">The Id of the event.</param>
@@ -160,13 +136,6 @@ public void Log<TState>(LogLevel logLevel, EventId eventId, TState? state, Excep
         /// <param name="exception">The exception related to this message.</param>
         public virtual void WriteMessage(LogLevel logLevel, int eventId, string? message, Exception? exception)
         {
-            ITestOutputHelper? outputHelper = _accessor.OutputHelper;
-
-            if (outputHelper == null)
-            {
-                return;
-            }
-
             StringBuilder? logBuilder = _logBuilder;
             _logBuilder = null;
 
@@ -213,7 +182,20 @@ public virtual void WriteMessage(LogLevel logLevel, int eventId, string? message
 
             try
             {
-                outputHelper.WriteLine($"[{Clock():u}] {logLevelString}{formatted}");
+                ITestOutputHelper? outputHelper = _outputHelperAccessor?.OutputHelper;
+                IMessageSink? messageSink = _messageSinkAccessor?.MessageSink;
+
+                var line = $"[{Clock():u}] {logLevelString}{formatted}";
+                if (outputHelper != null)
+                {
+                    outputHelper.WriteLine(line);
+                }
+
+                if (messageSink != null)
+                {
+                    var sinkMessage = MessageSinkMessageFactory(line);
+                    messageSink.OnMessage(sinkMessage);
+                }
             }
 #pragma warning disable CA1031
             catch (InvalidOperationException)
diff --git a/src/Logging.XUnit/XUnitLoggerExtensions.IMessageSink.cs b/src/Logging.XUnit/XUnitLoggerExtensions.IMessageSink.cs
new file mode 100644
index 00000000..cdfcaa56
--- /dev/null
+++ b/src/Logging.XUnit/XUnitLoggerExtensions.IMessageSink.cs
@@ -0,0 +1,342 @@
+// Copyright (c) Martin Costello, 2018. All rights reserved.
+// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
+
+using System;
+using MartinCostello.Logging.XUnit;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Xunit.Abstractions;
+
+namespace Microsoft.Extensions.Logging
+{
+    /// <summary>
+    /// A class containing extension methods for configuring logging to xunit. This class cannot be inherited.
+    /// </summary>
+    public static partial class XUnitLoggerExtensions
+    {
+        /// <summary>
+        /// Adds an xunit logger to the logging builder.
+        /// </summary>
+        /// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param>
+        /// <param name="accessor">The <see cref="IMessageSinkAccessor"/> to use.</param>
+        /// <returns>
+        /// The instance of <see cref="ILoggingBuilder"/> specified by <paramref name="builder"/>.
+        /// </returns>
+        /// <exception cref="ArgumentNullException">
+        /// <paramref name="builder"/> or <paramref name="accessor"/> is <see langword="null"/>.
+        /// </exception>
+        public static ILoggingBuilder AddXUnit(this ILoggingBuilder builder, IMessageSinkAccessor accessor)
+        {
+            if (builder == null)
+            {
+                throw new ArgumentNullException(nameof(builder));
+            }
+
+            if (accessor == null)
+            {
+                throw new ArgumentNullException(nameof(accessor));
+            }
+
+            return builder.AddXUnit(accessor, (_) => { });
+        }
+
+        /// <summary>
+        /// Adds an xunit logger to the logging builder.
+        /// </summary>
+        /// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param>
+        /// <param name="accessor">The <see cref="IMessageSinkAccessor"/> to use.</param>
+        /// <param name="configure">A delegate to a method to use to configure the logging options.</param>
+        /// <returns>
+        /// The instance of <see cref="ILoggingBuilder"/> specified by <paramref name="builder"/>.
+        /// </returns>
+        /// <exception cref="ArgumentNullException">
+        /// <paramref name="builder"/>, <paramref name="accessor"/> OR <paramref name="configure"/> is <see langword="null"/>.
+        /// </exception>
+        public static ILoggingBuilder AddXUnit(this ILoggingBuilder builder, IMessageSinkAccessor accessor, Action<XUnitLoggerOptions> configure)
+        {
+            if (builder == null)
+            {
+                throw new ArgumentNullException(nameof(builder));
+            }
+
+            if (accessor == null)
+            {
+                throw new ArgumentNullException(nameof(accessor));
+            }
+
+            if (configure == null)
+            {
+                throw new ArgumentNullException(nameof(configure));
+            }
+
+            var options = new XUnitLoggerOptions();
+
+            configure(options);
+
+#pragma warning disable CA2000
+            builder.AddProvider(new XUnitLoggerProvider(accessor, options));
+#pragma warning restore CA2000
+
+            builder.Services.TryAddSingleton(accessor);
+
+            return builder;
+        }
+
+        /// <summary>
+        /// Adds an xunit logger to the logging builder.
+        /// </summary>
+        /// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param>
+        /// <param name="messageSink">The <see cref="IMessageSink"/> to use.</param>
+        /// <returns>
+        /// The instance of <see cref="ILoggingBuilder"/> specified by <paramref name="builder"/>.
+        /// </returns>
+        /// <exception cref="ArgumentNullException">
+        /// <paramref name="builder"/> or <paramref name="messageSink"/> is <see langword="null"/>.
+        /// </exception>
+        public static ILoggingBuilder AddXUnit(this ILoggingBuilder builder, IMessageSink messageSink)
+        {
+            if (builder == null)
+            {
+                throw new ArgumentNullException(nameof(builder));
+            }
+
+            if (messageSink == null)
+            {
+                throw new ArgumentNullException(nameof(messageSink));
+            }
+
+            return builder.AddXUnit(messageSink, (_) => { });
+        }
+
+        /// <summary>
+        /// Adds an xunit logger to the logging builder.
+        /// </summary>
+        /// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param>
+        /// <param name="messageSink">The <see cref="IMessageSink"/> to use.</param>
+        /// <param name="configure">A delegate to a method to use to configure the logging options.</param>
+        /// <returns>
+        /// The instance of <see cref="ILoggingBuilder"/> specified by <paramref name="builder"/>.
+        /// </returns>
+        /// <exception cref="ArgumentNullException">
+        /// <paramref name="builder"/>, <paramref name="messageSink"/> OR <paramref name="configure"/> is <see langword="null"/>.
+        /// </exception>
+        public static ILoggingBuilder AddXUnit(this ILoggingBuilder builder, IMessageSink messageSink, Action<XUnitLoggerOptions> configure)
+        {
+            if (builder == null)
+            {
+                throw new ArgumentNullException(nameof(builder));
+            }
+
+            if (messageSink == null)
+            {
+                throw new ArgumentNullException(nameof(messageSink));
+            }
+
+            if (configure == null)
+            {
+                throw new ArgumentNullException(nameof(configure));
+            }
+
+            var options = new XUnitLoggerOptions();
+
+            configure(options);
+
+#pragma warning disable CA2000
+            return builder.AddProvider(new XUnitLoggerProvider(messageSink, options));
+#pragma warning restore CA2000
+        }
+
+        /// <summary>
+        /// Adds an xunit logger to the factory.
+        /// </summary>
+        /// <param name="factory">The <see cref="ILoggerFactory"/> to use.</param>
+        /// <param name="messageSink">The <see cref="IMessageSink"/> to use.</param>
+        /// <param name="minLevel">The minimum <see cref="LogLevel"/> to be logged.</param>
+        /// <returns>
+        /// The instance of <see cref="ILoggerFactory"/> specified by <paramref name="factory"/>.
+        /// </returns>
+        /// <exception cref="ArgumentNullException">
+        /// <paramref name="factory"/> or <paramref name="messageSink"/> is <see langword="null"/>.
+        /// </exception>
+        public static ILoggerFactory AddXUnit(this ILoggerFactory factory, IMessageSink messageSink, LogLevel minLevel)
+        {
+            if (factory == null)
+            {
+                throw new ArgumentNullException(nameof(factory));
+            }
+
+            if (messageSink == null)
+            {
+                throw new ArgumentNullException(nameof(messageSink));
+            }
+
+            return factory.AddXUnit(messageSink, (_, level) => level >= minLevel);
+        }
+
+        /// <summary>
+        /// Adds an xunit logger to the factory.
+        /// </summary>
+        /// <param name="factory">The <see cref="ILoggerFactory"/> to use.</param>
+        /// <param name="messageSink">The <see cref="IMessageSink"/> to use.</param>
+        /// <param name="filter">The category filter to apply to logs.</param>
+        /// <returns>
+        /// The instance of <see cref="ILoggerFactory"/> specified by <paramref name="factory"/>.
+        /// </returns>
+        /// <exception cref="ArgumentNullException">
+        /// <paramref name="factory"/>, <paramref name="messageSink"/> or <paramref name="filter"/> is <see langword="null"/>.
+        /// </exception>
+        public static ILoggerFactory AddXUnit(this ILoggerFactory factory, IMessageSink messageSink, Func<string?, LogLevel, bool> filter)
+        {
+            if (factory == null)
+            {
+                throw new ArgumentNullException(nameof(factory));
+            }
+
+            if (messageSink == null)
+            {
+                throw new ArgumentNullException(nameof(messageSink));
+            }
+
+            if (filter == null)
+            {
+                throw new ArgumentNullException(nameof(filter));
+            }
+
+            return factory.AddXUnit(messageSink, (options) => options.Filter = filter);
+        }
+
+        /// <summary>
+        /// Adds an xunit logger to the factory.
+        /// </summary>
+        /// <param name="factory">The <see cref="ILoggerFactory"/> to use.</param>
+        /// <param name="messageSink">The <see cref="IMessageSink"/> to use.</param>
+        /// <returns>
+        /// The instance of <see cref="ILoggerFactory"/> specified by <paramref name="factory"/>.
+        /// </returns>
+        /// <exception cref="ArgumentNullException">
+        /// <paramref name="factory"/> or <paramref name="messageSink"/> is <see langword="null"/>.
+        /// </exception>
+        public static ILoggerFactory AddXUnit(this ILoggerFactory factory, IMessageSink messageSink)
+        {
+            if (factory == null)
+            {
+                throw new ArgumentNullException(nameof(factory));
+            }
+
+            if (messageSink == null)
+            {
+                throw new ArgumentNullException(nameof(messageSink));
+            }
+
+            return factory.AddXUnit(messageSink, (_) => { });
+        }
+
+        /// <summary>
+        /// Adds an xunit logger to the factory.
+        /// </summary>
+        /// <param name="factory">The <see cref="ILoggerFactory"/> to use.</param>
+        /// <param name="messageSink">The <see cref="IMessageSink"/> to use.</param>
+        /// <param name="options">The options to use for logging to xunit.</param>
+        /// <returns>
+        /// The instance of <see cref="ILoggerFactory"/> specified by <paramref name="factory"/>.
+        /// </returns>
+        /// <exception cref="ArgumentNullException">
+        /// <paramref name="factory"/>, <paramref name="messageSink"/> OR <paramref name="options"/> is <see langword="null"/>.
+        /// </exception>
+        public static ILoggerFactory AddXUnit(this ILoggerFactory factory, IMessageSink messageSink, XUnitLoggerOptions options)
+        {
+            if (factory == null)
+            {
+                throw new ArgumentNullException(nameof(factory));
+            }
+
+            if (messageSink == null)
+            {
+                throw new ArgumentNullException(nameof(messageSink));
+            }
+
+            if (options == null)
+            {
+                throw new ArgumentNullException(nameof(options));
+            }
+
+            return factory.AddXUnit(messageSink, () => options);
+        }
+
+        /// <summary>
+        /// Adds an xunit logger to the factory.
+        /// </summary>
+        /// <param name="factory">The <see cref="ILoggerFactory"/> to use.</param>
+        /// <param name="messageSink">The <see cref="IMessageSink"/> to use.</param>
+        /// <param name="configure">A delegate to a method to use to configure the logging options.</param>
+        /// <returns>
+        /// The instance of <see cref="ILoggerFactory"/> specified by <paramref name="factory"/>.
+        /// </returns>
+        /// <exception cref="ArgumentNullException">
+        /// <paramref name="factory"/>, <paramref name="messageSink"/> OR <paramref name="configure"/> is <see langword="null"/>.
+        /// </exception>
+        public static ILoggerFactory AddXUnit(this ILoggerFactory factory, IMessageSink messageSink, Action<XUnitLoggerOptions> configure)
+        {
+            if (factory == null)
+            {
+                throw new ArgumentNullException(nameof(factory));
+            }
+
+            if (messageSink == null)
+            {
+                throw new ArgumentNullException(nameof(messageSink));
+            }
+
+            if (configure == null)
+            {
+                throw new ArgumentNullException(nameof(configure));
+            }
+
+            return factory.AddXUnit(
+                messageSink,
+                () =>
+                {
+                    var options = new XUnitLoggerOptions();
+                    configure(options);
+                    return options;
+                });
+        }
+
+        /// <summary>
+        /// Adds an xunit logger to the factory.
+        /// </summary>
+        /// <param name="factory">The <see cref="ILoggerFactory"/> to use.</param>
+        /// <param name="messageSink">The <see cref="IMessageSink"/> to use.</param>
+        /// <param name="configure">A delegate to a method that returns a configured <see cref="XUnitLoggerOptions"/> to use.</param>
+        /// <returns>
+        /// The instance of <see cref="ILoggerFactory"/> specified by <paramref name="factory"/>.
+        /// </returns>
+        /// <exception cref="ArgumentNullException">
+        /// <paramref name="factory"/>, <paramref name="messageSink"/> or <paramref name="configure"/> is <see langword="null"/>.
+        /// </exception>
+        public static ILoggerFactory AddXUnit(this ILoggerFactory factory, IMessageSink messageSink, Func<XUnitLoggerOptions> configure)
+        {
+            if (factory == null)
+            {
+                throw new ArgumentNullException(nameof(factory));
+            }
+
+            if (messageSink == null)
+            {
+                throw new ArgumentNullException(nameof(messageSink));
+            }
+
+            if (configure == null)
+            {
+                throw new ArgumentNullException(nameof(configure));
+            }
+
+            var options = configure();
+
+#pragma warning disable CA2000
+            factory.AddProvider(new XUnitLoggerProvider(messageSink, options));
+#pragma warning restore CA2000
+
+            return factory;
+        }
+    }
+}
diff --git a/src/Logging.XUnit/XUnitLoggerExtensions.cs b/src/Logging.XUnit/XUnitLoggerExtensions.ITestOutputHelper.cs
similarity index 99%
rename from src/Logging.XUnit/XUnitLoggerExtensions.cs
rename to src/Logging.XUnit/XUnitLoggerExtensions.ITestOutputHelper.cs
index ea9027dc..5bd73b22 100644
--- a/src/Logging.XUnit/XUnitLoggerExtensions.cs
+++ b/src/Logging.XUnit/XUnitLoggerExtensions.ITestOutputHelper.cs
@@ -13,7 +13,7 @@ namespace Microsoft.Extensions.Logging
     /// A class containing extension methods for configuring logging to xunit. This class cannot be inherited.
     /// </summary>
     [EditorBrowsable(EditorBrowsableState.Never)]
-    public static class XUnitLoggerExtensions
+    public static partial class XUnitLoggerExtensions
     {
         /// <summary>
         /// Adds an xunit logger to the logging builder.
diff --git a/src/Logging.XUnit/XUnitLoggerOptions.cs b/src/Logging.XUnit/XUnitLoggerOptions.cs
index ecc71fb1..f306b577 100644
--- a/src/Logging.XUnit/XUnitLoggerOptions.cs
+++ b/src/Logging.XUnit/XUnitLoggerOptions.cs
@@ -3,6 +3,8 @@
 
 using System;
 using Microsoft.Extensions.Logging;
+using Xunit.Abstractions;
+using Xunit.Sdk;
 
 namespace MartinCostello.Logging.XUnit
 {
@@ -23,6 +25,12 @@ public XUnitLoggerOptions()
         /// </summary>
         public Func<string?, LogLevel, bool> Filter { get; set; } = (c, l) => true; // By default log everything
 
+        /// <summary>
+        /// Gets or sets the message sink message factory to use when writing to a <see cref="IMessageSink"/>.
+        /// By default, creates a <see cref="DiagnosticMessage"/>.
+        /// </summary>
+        public Func<string, IMessageSinkMessage> MessageSinkMessageFactory { get; set; } = m => new DiagnosticMessage(m);
+
         /// <summary>
         /// Gets or sets a value indicating whether to include scopes.
         /// </summary>
diff --git a/src/Logging.XUnit/XUnitLoggerProvider.IMessageSink.cs b/src/Logging.XUnit/XUnitLoggerProvider.IMessageSink.cs
new file mode 100644
index 00000000..41c1c656
--- /dev/null
+++ b/src/Logging.XUnit/XUnitLoggerProvider.IMessageSink.cs
@@ -0,0 +1,47 @@
+// Copyright (c) Martin Costello, 2018. All rights reserved.
+// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
+
+using System;
+using Microsoft.Extensions.Logging;
+using Xunit.Abstractions;
+
+namespace MartinCostello.Logging.XUnit
+{
+    /// <summary>
+    /// A class representing an <see cref="ILoggerProvider"/> to use with xunit.
+    /// </summary>
+    public partial class XUnitLoggerProvider
+    {
+        /// <summary>
+        /// The <see cref="IMessageSinkAccessor"/> to use. This field is readonly.
+        /// </summary>
+        private readonly IMessageSinkAccessor? _messageSinkAccessor;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="XUnitLoggerProvider"/> class.
+        /// </summary>
+        /// <param name="messageSink">The <see cref="IMessageSink"/> to use.</param>
+        /// <param name="options">The options to use for logging to xunit.</param>
+        /// <exception cref="ArgumentNullException">
+        /// <paramref name="messageSink"/> or <paramref name="options"/> is <see langword="null"/>.
+        /// </exception>
+        public XUnitLoggerProvider(IMessageSink messageSink, XUnitLoggerOptions options)
+            : this(new MessageSinkAccessor(messageSink), options)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="XUnitLoggerProvider"/> class.
+        /// </summary>
+        /// <param name="accessor">The <see cref="IMessageSinkAccessor"/> to use.</param>
+        /// <param name="options">The options to use for logging to xunit.</param>
+        /// <exception cref="ArgumentNullException">
+        /// <paramref name="accessor"/> or <paramref name="options"/> is <see langword="null"/>.
+        /// </exception>
+        public XUnitLoggerProvider(IMessageSinkAccessor accessor, XUnitLoggerOptions options)
+        {
+            _messageSinkAccessor = accessor ?? throw new ArgumentNullException(nameof(accessor));
+            _options = options ?? throw new ArgumentNullException(nameof(options));
+        }
+    }
+}
diff --git a/src/Logging.XUnit/XUnitLoggerProvider.ITestOutputHelper.cs b/src/Logging.XUnit/XUnitLoggerProvider.ITestOutputHelper.cs
new file mode 100644
index 00000000..d2e34ea9
--- /dev/null
+++ b/src/Logging.XUnit/XUnitLoggerProvider.ITestOutputHelper.cs
@@ -0,0 +1,47 @@
+// Copyright (c) Martin Costello, 2018. All rights reserved.
+// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
+
+using System;
+using Microsoft.Extensions.Logging;
+using Xunit.Abstractions;
+
+namespace MartinCostello.Logging.XUnit
+{
+    /// <summary>
+    /// A class representing an <see cref="ILoggerProvider"/> to use with xunit.
+    /// </summary>
+    public partial class XUnitLoggerProvider
+    {
+        /// <summary>
+        /// The <see cref="ITestOutputHelperAccessor"/> to use. This field is readonly.
+        /// </summary>
+        private readonly ITestOutputHelperAccessor? _outputHelperAccessor;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="XUnitLoggerProvider"/> class.
+        /// </summary>
+        /// <param name="outputHelper">The <see cref="ITestOutputHelper"/> to use.</param>
+        /// <param name="options">The options to use for logging to xunit.</param>
+        /// <exception cref="ArgumentNullException">
+        /// <paramref name="outputHelper"/> or <paramref name="options"/> is <see langword="null"/>.
+        /// </exception>
+        public XUnitLoggerProvider(ITestOutputHelper outputHelper, XUnitLoggerOptions options)
+            : this(new TestOutputHelperAccessor(outputHelper), options)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="XUnitLoggerProvider"/> class.
+        /// </summary>
+        /// <param name="accessor">The <see cref="ITestOutputHelperAccessor"/> to use.</param>
+        /// <param name="options">The options to use for logging to xunit.</param>
+        /// <exception cref="ArgumentNullException">
+        /// <paramref name="accessor"/> or <paramref name="options"/> is <see langword="null"/>.
+        /// </exception>
+        public XUnitLoggerProvider(ITestOutputHelperAccessor accessor, XUnitLoggerOptions options)
+        {
+            _outputHelperAccessor = accessor ?? throw new ArgumentNullException(nameof(accessor));
+            _options = options ?? throw new ArgumentNullException(nameof(options));
+        }
+    }
+}
diff --git a/src/Logging.XUnit/XUnitLoggerProvider.cs b/src/Logging.XUnit/XUnitLoggerProvider.cs
index c6ccaab5..d72cb165 100644
--- a/src/Logging.XUnit/XUnitLoggerProvider.cs
+++ b/src/Logging.XUnit/XUnitLoggerProvider.cs
@@ -10,45 +10,13 @@ namespace MartinCostello.Logging.XUnit
     /// <summary>
     /// A class representing an <see cref="ILoggerProvider"/> to use with xunit.
     /// </summary>
-    public class XUnitLoggerProvider : ILoggerProvider
+    public partial class XUnitLoggerProvider : ILoggerProvider
     {
-        /// <summary>
-        /// The <see cref="ITestOutputHelperAccessor"/> to use. This field is readonly.
-        /// </summary>
-        private readonly ITestOutputHelperAccessor _accessor;
-
         /// <summary>
         /// The <see cref="XUnitLoggerOptions"/> to use. This field is readonly.
         /// </summary>
         private readonly XUnitLoggerOptions _options;
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="XUnitLoggerProvider"/> class.
-        /// </summary>
-        /// <param name="outputHelper">The <see cref="ITestOutputHelper"/> to use.</param>
-        /// <param name="options">The options to use for logging to xunit.</param>
-        /// <exception cref="ArgumentNullException">
-        /// <paramref name="outputHelper"/> or <paramref name="options"/> is <see langword="null"/>.
-        /// </exception>
-        public XUnitLoggerProvider(ITestOutputHelper outputHelper, XUnitLoggerOptions options)
-            : this(new TestOutputHelperAccessor(outputHelper), options)
-        {
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="XUnitLoggerProvider"/> class.
-        /// </summary>
-        /// <param name="accessor">The <see cref="ITestOutputHelperAccessor"/> to use.</param>
-        /// <param name="options">The options to use for logging to xunit.</param>
-        /// <exception cref="ArgumentNullException">
-        /// <paramref name="accessor"/> or <paramref name="options"/> is <see langword="null"/>.
-        /// </exception>
-        public XUnitLoggerProvider(ITestOutputHelperAccessor accessor, XUnitLoggerOptions options)
-        {
-            _accessor = accessor ?? throw new ArgumentNullException(nameof(accessor));
-            _options = options ?? throw new ArgumentNullException(nameof(options));
-        }
-
         /// <summary>
         /// Finalizes an instance of the <see cref="XUnitLoggerProvider"/> class.
         /// </summary>
@@ -58,7 +26,20 @@ public XUnitLoggerProvider(ITestOutputHelperAccessor accessor, XUnitLoggerOption
         }
 
         /// <inheritdoc />
-        public virtual ILogger CreateLogger(string categoryName) => new XUnitLogger(categoryName, _accessor, _options);
+        public virtual ILogger CreateLogger(string categoryName)
+        {
+            if (_outputHelperAccessor != null)
+            {
+                return new XUnitLogger(categoryName, _outputHelperAccessor, _options);
+            }
+
+            if (_messageSinkAccessor != null)
+            {
+                return new XUnitLogger(categoryName, _messageSinkAccessor, _options);
+            }
+
+            throw new InvalidOperationException($"Either {nameof(_outputHelperAccessor)} or {nameof(_messageSinkAccessor)} must not be null.");
+        }
 
         /// <inheritdoc />
         public void Dispose()
diff --git a/tests/Logging.XUnit.Tests/Constructor.cs b/tests/Logging.XUnit.Tests/Constructor.cs
new file mode 100644
index 00000000..21aae94c
--- /dev/null
+++ b/tests/Logging.XUnit.Tests/Constructor.cs
@@ -0,0 +1,11 @@
+// Copyright (c) Martin Costello, 2018. All rights reserved.
+// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
+
+namespace MartinCostello.Logging.XUnit
+{
+    public enum Constructor
+    {
+        ITestOutputHelper,
+        IMessageSink,
+    }
+}
diff --git a/tests/Logging.XUnit.Tests/Integration/DatabaseFixture.cs b/tests/Logging.XUnit.Tests/Integration/DatabaseFixture.cs
new file mode 100644
index 00000000..730b8e5a
--- /dev/null
+++ b/tests/Logging.XUnit.Tests/Integration/DatabaseFixture.cs
@@ -0,0 +1,40 @@
+// Copyright (c) Martin Costello, 2018. All rights reserved.
+// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
+
+using System;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace MartinCostello.Logging.XUnit.Integration
+{
+    public sealed class DatabaseFixture : IAsyncLifetime
+    {
+        private readonly ILogger _loggerInitialize;
+        private readonly ILogger _loggerDispose;
+        private string? _connectionString;
+
+        public DatabaseFixture(IMessageSink messageSink)
+        {
+            using var loggerFactory = new LoggerFactory();
+            _loggerInitialize = loggerFactory.AddXUnit(messageSink, c => c.MessageSinkMessageFactory = m => new PrintableDiagnosticMessage(m)).CreateLogger<DatabaseFixture>();
+            _loggerDispose = messageSink.ToLogger<DatabaseFixture>();
+        }
+
+        public string ConnectionString => _connectionString ?? throw new InvalidOperationException("The connection string is only available after InitializeAsync has completed.");
+
+        Task IAsyncLifetime.InitializeAsync()
+        {
+            _loggerInitialize.LogInformation("Initializing database");
+            _connectionString = "Server=localhost";
+            return Task.CompletedTask;
+        }
+
+        Task IAsyncLifetime.DisposeAsync()
+        {
+            _loggerDispose.LogInformation("Disposing database");
+            return Task.CompletedTask;
+        }
+    }
+}
diff --git a/tests/Logging.XUnit.Tests/Integration/DatabaseTests.cs b/tests/Logging.XUnit.Tests/Integration/DatabaseTests.cs
new file mode 100644
index 00000000..cad38552
--- /dev/null
+++ b/tests/Logging.XUnit.Tests/Integration/DatabaseTests.cs
@@ -0,0 +1,24 @@
+// Copyright (c) Martin Costello, 2018. All rights reserved.
+// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
+
+using Shouldly;
+using Xunit;
+
+namespace MartinCostello.Logging.XUnit.Integration
+{
+    public class DatabaseTests : IClassFixture<DatabaseFixture>
+    {
+        public DatabaseTests(DatabaseFixture databaseFixture)
+        {
+            DatabaseFixture = databaseFixture;
+        }
+
+        public DatabaseFixture DatabaseFixture { get; }
+
+        [Fact]
+        public void Run_Database_Test()
+        {
+            DatabaseFixture.ConnectionString.ShouldNotBeEmpty();
+        }
+    }
+}
diff --git a/tests/Logging.XUnit.Tests/Integration/PrintableDiagnosticMessage.cs b/tests/Logging.XUnit.Tests/Integration/PrintableDiagnosticMessage.cs
new file mode 100644
index 00000000..de817298
--- /dev/null
+++ b/tests/Logging.XUnit.Tests/Integration/PrintableDiagnosticMessage.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Martin Costello, 2018. All rights reserved.
+// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
+
+using Xunit.Sdk;
+
+namespace MartinCostello.Logging.XUnit.Integration
+{
+    /// <summary>
+    /// See https://github.com/xunit/xunit/pull/2148#issuecomment-839838421
+    /// </summary>
+    internal class PrintableDiagnosticMessage : DiagnosticMessage
+    {
+        public PrintableDiagnosticMessage(string message)
+            : base(message)
+        {
+        }
+
+        public override string ToString() => Message;
+    }
+}
diff --git a/tests/Logging.XUnit.Tests/XUnitLoggerExtensionsTests.cs b/tests/Logging.XUnit.Tests/XUnitLoggerExtensionsTests.cs
index f81c20f1..74ec7936 100644
--- a/tests/Logging.XUnit.Tests/XUnitLoggerExtensionsTests.cs
+++ b/tests/Logging.XUnit.Tests/XUnitLoggerExtensionsTests.cs
@@ -13,7 +13,7 @@ namespace MartinCostello.Logging.XUnit
     public static class XUnitLoggerExtensionsTests
     {
         [Fact]
-        public static void AddXUnit_For_ILoggerBuilder_Validates_Parameters()
+        public static void AddXUnit_TestOutputHelper_For_ILoggerBuilder_Validates_Parameters()
         {
             // Arrange
             var builder = Mock.Of<ILoggingBuilder>();
@@ -35,7 +35,28 @@ public static void AddXUnit_For_ILoggerBuilder_Validates_Parameters()
         }
 
         [Fact]
-        public static void AddXUnit_For_ILoggerFactory_Validates_Parameters()
+        public static void AddXUnit_MessageSink_For_ILoggerBuilder_Validates_Parameters()
+        {
+            // Arrange
+            var builder = Mock.Of<ILoggingBuilder>();
+            var messageSink = Mock.Of<IMessageSink>();
+            var accessor = Mock.Of<IMessageSinkAccessor>();
+
+            // Act and Assert
+            Assert.Throws<ArgumentNullException>("builder", () => (null as ILoggingBuilder) !.AddXUnit(messageSink));
+            Assert.Throws<ArgumentNullException>("builder", () => (null as ILoggingBuilder) !.AddXUnit(messageSink, ConfigureAction));
+            Assert.Throws<ArgumentNullException>("builder", () => (null as ILoggingBuilder) !.AddXUnit(accessor));
+            Assert.Throws<ArgumentNullException>("builder", () => (null as ILoggingBuilder) !.AddXUnit(accessor, ConfigureAction));
+            Assert.Throws<ArgumentNullException>("accessor", () => builder.AddXUnit((null as IMessageSinkAccessor) !));
+            Assert.Throws<ArgumentNullException>("accessor", () => builder.AddXUnit((null as IMessageSinkAccessor) !, ConfigureAction));
+            Assert.Throws<ArgumentNullException>("messageSink", () => builder.AddXUnit((null as IMessageSink) !));
+            Assert.Throws<ArgumentNullException>("messageSink", () => builder.AddXUnit((null as IMessageSink) !, ConfigureAction));
+            Assert.Throws<ArgumentNullException>("configure", () => builder.AddXUnit(messageSink, (null as Action<XUnitLoggerOptions>) !));
+            Assert.Throws<ArgumentNullException>("configure", () => builder.AddXUnit(accessor, (null as Action<XUnitLoggerOptions>) !));
+        }
+
+        [Fact]
+        public static void AddXUnit_TestOutputHelper_For_ILoggerFactory_Validates_Parameters()
         {
             // Arrange
             ILoggerFactory factory = NullLoggerFactory.Instance;
@@ -51,25 +72,55 @@ public static void AddXUnit_For_ILoggerFactory_Validates_Parameters()
             Assert.Throws<ArgumentNullException>("factory", () => (null as ILoggerFactory) !.AddXUnit(outputHelper, Filter));
             Assert.Throws<ArgumentNullException>("factory", () => (null as ILoggerFactory) !.AddXUnit(outputHelper, logLevel));
             Assert.Throws<ArgumentNullException>("outputHelper", () => factory.AddXUnit((null as ITestOutputHelper) !));
-            Assert.Throws<ArgumentNullException>("outputHelper", () => factory.AddXUnit(null!, ConfigureAction));
-            Assert.Throws<ArgumentNullException>("outputHelper", () => factory.AddXUnit(null!, ConfigureFunction));
-            Assert.Throws<ArgumentNullException>("outputHelper", () => factory.AddXUnit(null!, Filter));
-            Assert.Throws<ArgumentNullException>("outputHelper", () => factory.AddXUnit(null!, logLevel));
-            Assert.Throws<ArgumentNullException>("outputHelper", () => factory.AddXUnit(null!, options));
+            Assert.Throws<ArgumentNullException>("outputHelper", () => factory.AddXUnit((null as ITestOutputHelper) !, ConfigureAction));
+            Assert.Throws<ArgumentNullException>("outputHelper", () => factory.AddXUnit((null as ITestOutputHelper) !, ConfigureFunction));
+            Assert.Throws<ArgumentNullException>("outputHelper", () => factory.AddXUnit((null as ITestOutputHelper) !, Filter));
+            Assert.Throws<ArgumentNullException>("outputHelper", () => factory.AddXUnit((null as ITestOutputHelper) !, logLevel));
+            Assert.Throws<ArgumentNullException>("outputHelper", () => factory.AddXUnit((null as ITestOutputHelper) !, options));
             Assert.Throws<ArgumentNullException>("options", () => factory.AddXUnit(outputHelper, (null as XUnitLoggerOptions) !));
             Assert.Throws<ArgumentNullException>("configure", () => factory.AddXUnit(outputHelper, (null as Action<XUnitLoggerOptions>) !));
             Assert.Throws<ArgumentNullException>("configure", () => factory.AddXUnit(outputHelper, (null as Func<XUnitLoggerOptions>) !));
             Assert.Throws<ArgumentNullException>("filter", () => factory.AddXUnit(outputHelper, (null as Func<string, LogLevel, bool>) !));
         }
 
+        [Fact]
+        public static void AddXUnit_MessageSink_For_ILoggerFactory_Validates_Parameters()
+        {
+            // Arrange
+            ILoggerFactory factory = NullLoggerFactory.Instance;
+            var logLevel = LogLevel.Information;
+            var messageSink = Mock.Of<IMessageSink>();
+            var options = new XUnitLoggerOptions();
+
+            // Act and Assert
+            Assert.Throws<ArgumentNullException>("factory", () => (null as ILoggerFactory) !.AddXUnit(messageSink));
+            Assert.Throws<ArgumentNullException>("factory", () => (null as ILoggerFactory) !.AddXUnit(messageSink, options));
+            Assert.Throws<ArgumentNullException>("factory", () => (null as ILoggerFactory) !.AddXUnit(messageSink, ConfigureAction));
+            Assert.Throws<ArgumentNullException>("factory", () => (null as ILoggerFactory) !.AddXUnit(messageSink, ConfigureFunction));
+            Assert.Throws<ArgumentNullException>("factory", () => (null as ILoggerFactory) !.AddXUnit(messageSink, Filter));
+            Assert.Throws<ArgumentNullException>("factory", () => (null as ILoggerFactory) !.AddXUnit(messageSink, logLevel));
+            Assert.Throws<ArgumentNullException>("messageSink", () => factory.AddXUnit((null as IMessageSink) !));
+            Assert.Throws<ArgumentNullException>("messageSink", () => factory.AddXUnit((null as IMessageSink) !, ConfigureAction));
+            Assert.Throws<ArgumentNullException>("messageSink", () => factory.AddXUnit((null as IMessageSink) !, ConfigureFunction));
+            Assert.Throws<ArgumentNullException>("messageSink", () => factory.AddXUnit((null as IMessageSink) !, Filter));
+            Assert.Throws<ArgumentNullException>("messageSink", () => factory.AddXUnit((null as IMessageSink) !, logLevel));
+            Assert.Throws<ArgumentNullException>("messageSink", () => factory.AddXUnit((null as IMessageSink) !, options));
+            Assert.Throws<ArgumentNullException>("options", () => factory.AddXUnit(messageSink, (null as XUnitLoggerOptions) !));
+            Assert.Throws<ArgumentNullException>("configure", () => factory.AddXUnit(messageSink, (null as Action<XUnitLoggerOptions>) !));
+            Assert.Throws<ArgumentNullException>("configure", () => factory.AddXUnit(messageSink, (null as Func<XUnitLoggerOptions>) !));
+            Assert.Throws<ArgumentNullException>("filter", () => factory.AddXUnit(messageSink, (null as Func<string, LogLevel, bool>) !));
+        }
+
         [Fact]
         public static void ToLoggerFactory_Validates_Parameters()
         {
             // Arrange
             ITestOutputHelper? outputHelper = null;
+            IMessageSink? messageSink = null;
 
             // Act and Assert
             Assert.Throws<ArgumentNullException>("outputHelper", () => outputHelper!.ToLoggerFactory());
+            Assert.Throws<ArgumentNullException>("messageSink", () => messageSink!.ToLoggerFactory());
         }
 
         private static void ConfigureAction(XUnitLoggerOptions options)
diff --git a/tests/Logging.XUnit.Tests/XUnitLoggerProviderTests.cs b/tests/Logging.XUnit.Tests/XUnitLoggerProviderTests.cs
index 15b0e3fe..15620325 100644
--- a/tests/Logging.XUnit.Tests/XUnitLoggerProviderTests.cs
+++ b/tests/Logging.XUnit.Tests/XUnitLoggerProviderTests.cs
@@ -13,7 +13,7 @@ namespace MartinCostello.Logging.XUnit
     public static class XUnitLoggerProviderTests
     {
         [Fact]
-        public static void XUnitLoggerProvider_Constructor_Validates_Parameters()
+        public static void XUnitLoggerProvider_TestOutputHelper_Constructor_Validates_Parameters()
         {
             // Arrange
             var outputHelper = Mock.Of<ITestOutputHelper>();
@@ -28,15 +28,38 @@ public static void XUnitLoggerProvider_Constructor_Validates_Parameters()
         }
 
         [Fact]
-        public static void XUnitLoggerProvider_Creates_Logger()
+        public static void XUnitLoggerProvider_MessageSink_Constructor_Validates_Parameters()
         {
             // Arrange
-            var outputHelper = Mock.Of<ITestOutputHelper>();
+            var messageSink = Mock.Of<IMessageSink>();
+            var accessor = Mock.Of<IMessageSinkAccessor>();
+            var options = new XUnitLoggerOptions();
+
+            // Act and Assert
+            Assert.Throws<ArgumentNullException>("messageSink", () => new XUnitLoggerProvider((null as IMessageSink) !, options));
+            Assert.Throws<ArgumentNullException>("accessor", () => new XUnitLoggerProvider((null as IMessageSinkAccessor) !, options));
+            Assert.Throws<ArgumentNullException>("options", () => new XUnitLoggerProvider(messageSink, null!));
+            Assert.Throws<ArgumentNullException>("options", () => new XUnitLoggerProvider(accessor, null!));
+        }
+
+        [Theory]
+        [InlineData(Constructor.ITestOutputHelper)]
+        [InlineData(Constructor.IMessageSink)]
+        public static void XUnitLoggerProvider_Creates_Logger(Constructor constructor)
+        {
+            // Arrange
+            var testOutputHelper = Mock.Of<ITestOutputHelper>();
+            var messageSink = Mock.Of<IMessageSink>();
             var options = new XUnitLoggerOptions();
 
             string categoryName = "MyLogger";
 
-            using var target = new XUnitLoggerProvider(outputHelper, options);
+            using var target = constructor switch
+            {
+                Constructor.ITestOutputHelper => new XUnitLoggerProvider(testOutputHelper, options),
+                Constructor.IMessageSink => new XUnitLoggerProvider(messageSink, options),
+                _ => throw new ArgumentOutOfRangeException(nameof(constructor), constructor, null)
+            };
 
             // Act
             ILogger actual = target.CreateLogger(categoryName);
@@ -47,6 +70,7 @@ public static void XUnitLoggerProvider_Creates_Logger()
             var xunit = actual.ShouldBeOfType<XUnitLogger>();
             xunit.Name.ShouldBe(categoryName);
             xunit.Filter.ShouldBeSameAs(options.Filter);
+            xunit.MessageSinkMessageFactory.ShouldBeSameAs(options.MessageSinkMessageFactory);
             xunit.IncludeScopes.ShouldBeFalse();
         }
     }
diff --git a/tests/Logging.XUnit.Tests/XUnitLoggerTests.cs b/tests/Logging.XUnit.Tests/XUnitLoggerTests.cs
index 751b39d6..9a4df491 100644
--- a/tests/Logging.XUnit.Tests/XUnitLoggerTests.cs
+++ b/tests/Logging.XUnit.Tests/XUnitLoggerTests.cs
@@ -8,6 +8,7 @@
 using Shouldly;
 using Xunit;
 using Xunit.Abstractions;
+using Xunit.Sdk;
 
 namespace MartinCostello.Logging.XUnit
 {
@@ -28,7 +29,9 @@ public static void XUnitLogger_Validates_Parameters()
             // Act and Assert
             Assert.Throws<ArgumentNullException>("name", () => new XUnitLogger(null!, outputHelper, options));
             Assert.Throws<ArgumentNullException>("outputHelper", () => new XUnitLogger(name, (null as ITestOutputHelper) !, options));
+            Assert.Throws<ArgumentNullException>("messageSink", () => new XUnitLogger(name, (null as IMessageSink) !, options));
             Assert.Throws<ArgumentNullException>("accessor", () => new XUnitLogger(name, (null as ITestOutputHelperAccessor) !, options));
+            Assert.Throws<ArgumentNullException>("accessor", () => new XUnitLogger(name, (null as IMessageSinkAccessor) !, options));
 
             // Arrange
             var logger = new XUnitLogger(name, outputHelper, options);
@@ -37,33 +40,49 @@ public static void XUnitLogger_Validates_Parameters()
             Assert.Throws<ArgumentNullException>("value", () => logger.Filter = null!);
         }
 
-        [Fact]
-        public static void XUnitLogger_Constructor_Initializes_Instance()
+        [Theory]
+        [InlineData(Constructor.ITestOutputHelper)]
+        [InlineData(Constructor.IMessageSink)]
+        public static void XUnitLogger_Constructor_Initializes_Instance(Constructor constructor)
         {
             // Arrange
             string name = "MyName";
-            var outputHelper = Mock.Of<ITestOutputHelper>();
-
+            var testOutputHelper = Mock.Of<ITestOutputHelper>();
+            var messageSink = Mock.Of<IMessageSink>();
             var options = new XUnitLoggerOptions()
             {
                 Filter = FilterTrue,
+                MessageSinkMessageFactory = DiagnosticMessageFactory,
                 IncludeScopes = true,
             };
 
+            XUnitLogger CreateLogger(XUnitLoggerOptions? opts)
+            {
+                return constructor switch
+                {
+                    Constructor.ITestOutputHelper => new XUnitLogger(name, testOutputHelper, opts),
+                    Constructor.IMessageSink => new XUnitLogger(name, messageSink, opts),
+                    _ => throw new ArgumentOutOfRangeException(nameof(constructor), constructor, null)
+                };
+            }
+
             // Act
-            var actual = new XUnitLogger(name, outputHelper, options);
+            var actual = CreateLogger(options);
 
             // Assert
             actual.Filter.ShouldBeSameAs(options.Filter);
+            actual.MessageSinkMessageFactory.ShouldBeSameAs(options.MessageSinkMessageFactory);
             actual.IncludeScopes.ShouldBeTrue();
             actual.Name.ShouldBe(name);
 
             // Act
-            actual = new XUnitLogger(name, outputHelper, null);
+            actual = CreateLogger(null);
 
             // Assert
             actual.Filter.ShouldNotBeNull();
             actual.Filter(null, LogLevel.None).ShouldBeTrue();
+            actual.MessageSinkMessageFactory.ShouldNotBeNull();
+            actual.MessageSinkMessageFactory("message").ShouldBeOfType<DiagnosticMessage>();
             actual.IncludeScopes.ShouldBeFalse();
             actual.Name.ShouldBe(name);
         }
@@ -655,6 +674,8 @@ public static void XUnitLogger_Log_Logs_Message_If_Scopes_Included_And_There_Is_
 
         private static DateTimeOffset StaticClock() => new DateTimeOffset(2018, 08, 19, 17, 12, 16, TimeSpan.FromHours(1));
 
+        private static IMessageSinkMessage DiagnosticMessageFactory(string message) => new DiagnosticMessage(message);
+
         private static bool FilterTrue(string? categoryName, LogLevel level) => true;
 
         private static bool FilterFalse(string? categoryName, LogLevel level) => false;
diff --git a/tests/Logging.XUnit.Tests/xunit.runner.json b/tests/Logging.XUnit.Tests/xunit.runner.json
index 89e7724d..ea67ff35 100644
--- a/tests/Logging.XUnit.Tests/xunit.runner.json
+++ b/tests/Logging.XUnit.Tests/xunit.runner.json
@@ -1,4 +1,6 @@
 {
+  "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
+  "diagnosticMessages": true,
   "methodDisplay": "method",
   "methodDisplayOptions": "replaceUnderscoreWithSpace"
 }

From 0b75f6aad21fe72bece9e7a24c79fafd1caacc7c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?C=C3=A9dric=20Luthi?= <cedric.luthi@gmail.com>
Date: Sat, 2 Oct 2021 14:58:10 +0200
Subject: [PATCH 02/14] Use the lowest versions possible for xunit dependencies

xunit.abstractions 2.0.2 and xunit.extensibility.execution 2.4.0 are the first versions to support .NET Standard 2.0
---
 Directory.Packages.props | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Directory.Packages.props b/Directory.Packages.props
index 4c3d48f5..7b90576b 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -11,8 +11,8 @@
   </ItemGroup>
   <ItemGroup Condition=" '$(IsTestProject)' != 'true' ">
     <PackageVersion Include="Microsoft.Extensions.Logging" Version="2.0.0" />
-    <PackageVersion Include="xunit.abstractions" Version="2.0.3" />
-    <PackageVersion Include="xunit.extensibility.execution" Version="2.4.1" />
+    <PackageVersion Include="xunit.abstractions" Version="2.0.2" />
+    <PackageVersion Include="xunit.extensibility.execution" Version="2.4.0" />
   </ItemGroup>
   <ItemGroup Condition=" '$(IsTestProject)' == 'true' ">
     <PackageVersion Include="Microsoft.Extensions.Logging" Version="5.0.0" />

From 68ca3f3cbb909750b00a771457cf961ffbb16b1f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?C=C3=A9dric=20Luthi?= <cedric.luthi@gmail.com>
Date: Sat, 2 Oct 2021 15:00:04 +0200
Subject: [PATCH 03/14] Fix typo

Co-authored-by: Martin Costello <martin@martincostello.com>
---
 src/Logging.XUnit/XUnitLogger.IMessageSink.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Logging.XUnit/XUnitLogger.IMessageSink.cs b/src/Logging.XUnit/XUnitLogger.IMessageSink.cs
index 9cb27d67..d17f91a4 100644
--- a/src/Logging.XUnit/XUnitLogger.IMessageSink.cs
+++ b/src/Logging.XUnit/XUnitLogger.IMessageSink.cs
@@ -52,7 +52,7 @@ public XUnitLogger(string name, IMessageSinkAccessor accessor, XUnitLoggerOption
         }
 
         /// <summary>
-        /// Gets or sets the message sink message factory to use when writing to a <see cref="IMessageSink"/>.
+        /// Gets or sets the message sink message factory to use when writing to an <see cref="IMessageSink"/>.
         /// </summary>
         /// <exception cref="ArgumentNullException">
         /// <paramref name="value"/> is <see langword="null"/>.

From 300a51cfbe30f85ee50482a67beb92507f0dcf97 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?C=C3=A9dric=20Luthi?= <cedric.luthi@gmail.com>
Date: Sat, 2 Oct 2021 15:00:20 +0200
Subject: [PATCH 04/14] Fix typo

Co-authored-by: Martin Costello <martin@martincostello.com>
---
 src/Logging.XUnit/XUnitLogger.IMessageSink.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Logging.XUnit/XUnitLogger.IMessageSink.cs b/src/Logging.XUnit/XUnitLogger.IMessageSink.cs
index d17f91a4..cb363850 100644
--- a/src/Logging.XUnit/XUnitLogger.IMessageSink.cs
+++ b/src/Logging.XUnit/XUnitLogger.IMessageSink.cs
@@ -18,7 +18,7 @@ public partial class XUnitLogger
         private readonly IMessageSinkAccessor? _messageSinkAccessor;
 
         /// <summary>
-        /// Gets or sets the message sink message factory to use when writing to a <see cref="IMessageSink"/>.
+        /// Gets or sets the message sink message factory to use when writing to an <see cref="IMessageSink"/>.
         /// </summary>
         private Func<string, IMessageSinkMessage> _messageSinkMessageFactory;
 

From 0ea106f403fe20aa5d2bc5c0e1cb9afb82453714 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?C=C3=A9dric=20Luthi?= <cedric.luthi@gmail.com>
Date: Sat, 2 Oct 2021 15:04:12 +0200
Subject: [PATCH 05/14] Bail out earlier if the output helper and the sink are
 both null

---
 src/Logging.XUnit/XUnitLogger.cs | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)

diff --git a/src/Logging.XUnit/XUnitLogger.cs b/src/Logging.XUnit/XUnitLogger.cs
index 94f035fc..d32d1985 100644
--- a/src/Logging.XUnit/XUnitLogger.cs
+++ b/src/Logging.XUnit/XUnitLogger.cs
@@ -136,6 +136,14 @@ public void Log<TState>(LogLevel logLevel, EventId eventId, TState? state, Excep
         /// <param name="exception">The exception related to this message.</param>
         public virtual void WriteMessage(LogLevel logLevel, int eventId, string? message, Exception? exception)
         {
+            ITestOutputHelper? outputHelper = _outputHelperAccessor?.OutputHelper;
+            IMessageSink? messageSink = _messageSinkAccessor?.MessageSink;
+
+            if (outputHelper is null && messageSink is null)
+            {
+                return;
+            }
+
             StringBuilder? logBuilder = _logBuilder;
             _logBuilder = null;
 
@@ -182,9 +190,6 @@ public virtual void WriteMessage(LogLevel logLevel, int eventId, string? message
 
             try
             {
-                ITestOutputHelper? outputHelper = _outputHelperAccessor?.OutputHelper;
-                IMessageSink? messageSink = _messageSinkAccessor?.MessageSink;
-
                 var line = $"[{Clock():u}] {logLevelString}{formatted}";
                 if (outputHelper != null)
                 {

From 4361d4549b5e0224b281d690aac9ee754656f390 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?C=C3=A9dric=20Luthi?= <cedric.luthi@gmail.com>
Date: Sat, 2 Oct 2021 15:05:48 +0200
Subject: [PATCH 06/14] Use the _messageSinkMessageFactory field directly

---
 src/Logging.XUnit/XUnitLogger.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Logging.XUnit/XUnitLogger.cs b/src/Logging.XUnit/XUnitLogger.cs
index d32d1985..05273625 100644
--- a/src/Logging.XUnit/XUnitLogger.cs
+++ b/src/Logging.XUnit/XUnitLogger.cs
@@ -198,7 +198,7 @@ public virtual void WriteMessage(LogLevel logLevel, int eventId, string? message
 
                 if (messageSink != null)
                 {
-                    var sinkMessage = MessageSinkMessageFactory(line);
+                    var sinkMessage = _messageSinkMessageFactory(line);
                     messageSink.OnMessage(sinkMessage);
                 }
             }

From e41b0fa4fdc67b27120343e3a44b30c044c204e6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?C=C3=A9dric=20Luthi?= <cedric.luthi@gmail.com>
Date: Sat, 2 Oct 2021 15:06:29 +0200
Subject: [PATCH 07/14] Fix typo

Co-authored-by: Martin Costello <martin@martincostello.com>
---
 src/Logging.XUnit/XUnitLoggerExtensions.IMessageSink.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Logging.XUnit/XUnitLoggerExtensions.IMessageSink.cs b/src/Logging.XUnit/XUnitLoggerExtensions.IMessageSink.cs
index cdfcaa56..4d90a237 100644
--- a/src/Logging.XUnit/XUnitLoggerExtensions.IMessageSink.cs
+++ b/src/Logging.XUnit/XUnitLoggerExtensions.IMessageSink.cs
@@ -49,7 +49,7 @@ public static ILoggingBuilder AddXUnit(this ILoggingBuilder builder, IMessageSin
         /// The instance of <see cref="ILoggingBuilder"/> specified by <paramref name="builder"/>.
         /// </returns>
         /// <exception cref="ArgumentNullException">
-        /// <paramref name="builder"/>, <paramref name="accessor"/> OR <paramref name="configure"/> is <see langword="null"/>.
+        /// <paramref name="builder"/>, <paramref name="accessor"/> or <paramref name="configure"/> is <see langword="null"/>.
         /// </exception>
         public static ILoggingBuilder AddXUnit(this ILoggingBuilder builder, IMessageSinkAccessor accessor, Action<XUnitLoggerOptions> configure)
         {

From 1229d8f6d700affdaeefd8bcf0bde30706af38d8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?C=C3=A9dric=20Luthi?= <cedric.luthi@gmail.com>
Date: Sat, 2 Oct 2021 15:11:50 +0200
Subject: [PATCH 08/14] Fix typo

Co-authored-by: Martin Costello <martin@martincostello.com>
---
 src/Logging.XUnit/XUnitLoggerExtensions.IMessageSink.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Logging.XUnit/XUnitLoggerExtensions.IMessageSink.cs b/src/Logging.XUnit/XUnitLoggerExtensions.IMessageSink.cs
index 4d90a237..8f8ecc8a 100644
--- a/src/Logging.XUnit/XUnitLoggerExtensions.IMessageSink.cs
+++ b/src/Logging.XUnit/XUnitLoggerExtensions.IMessageSink.cs
@@ -117,7 +117,7 @@ public static ILoggingBuilder AddXUnit(this ILoggingBuilder builder, IMessageSin
         /// The instance of <see cref="ILoggingBuilder"/> specified by <paramref name="builder"/>.
         /// </returns>
         /// <exception cref="ArgumentNullException">
-        /// <paramref name="builder"/>, <paramref name="messageSink"/> OR <paramref name="configure"/> is <see langword="null"/>.
+        /// <paramref name="builder"/>, <paramref name="messageSink"/> or <paramref name="configure"/> is <see langword="null"/>.
         /// </exception>
         public static ILoggingBuilder AddXUnit(this ILoggingBuilder builder, IMessageSink messageSink, Action<XUnitLoggerOptions> configure)
         {

From a8f6216cfc8acd79d41f4986f3d715aef5b07b3f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?C=C3=A9dric=20Luthi?= <cedric.luthi@gmail.com>
Date: Sat, 2 Oct 2021 15:12:42 +0200
Subject: [PATCH 09/14] Fix typo

Co-authored-by: Martin Costello <martin@martincostello.com>
---
 src/Logging.XUnit/XUnitLoggerExtensions.IMessageSink.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Logging.XUnit/XUnitLoggerExtensions.IMessageSink.cs b/src/Logging.XUnit/XUnitLoggerExtensions.IMessageSink.cs
index 8f8ecc8a..5d75fd2b 100644
--- a/src/Logging.XUnit/XUnitLoggerExtensions.IMessageSink.cs
+++ b/src/Logging.XUnit/XUnitLoggerExtensions.IMessageSink.cs
@@ -272,7 +272,7 @@ public static ILoggerFactory AddXUnit(this ILoggerFactory factory, IMessageSink
         /// The instance of <see cref="ILoggerFactory"/> specified by <paramref name="factory"/>.
         /// </returns>
         /// <exception cref="ArgumentNullException">
-        /// <paramref name="factory"/>, <paramref name="messageSink"/> OR <paramref name="configure"/> is <see langword="null"/>.
+        /// <paramref name="factory"/>, <paramref name="messageSink"/> or <paramref name="configure"/> is <see langword="null"/>.
         /// </exception>
         public static ILoggerFactory AddXUnit(this ILoggerFactory factory, IMessageSink messageSink, Action<XUnitLoggerOptions> configure)
         {

From eb5a96b0dbe85988a4b1caf5d8e09146bf1989c7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?C=C3=A9dric=20Luthi?= <cedric.luthi@gmail.com>
Date: Sat, 2 Oct 2021 15:13:35 +0200
Subject: [PATCH 10/14] Fix misleading documentation

Co-authored-by: Martin Costello <martin@martincostello.com>
---
 src/Logging.XUnit/XUnitLoggerOptions.cs | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/Logging.XUnit/XUnitLoggerOptions.cs b/src/Logging.XUnit/XUnitLoggerOptions.cs
index f306b577..af1dade4 100644
--- a/src/Logging.XUnit/XUnitLoggerOptions.cs
+++ b/src/Logging.XUnit/XUnitLoggerOptions.cs
@@ -27,7 +27,6 @@ public XUnitLoggerOptions()
 
         /// <summary>
         /// Gets or sets the message sink message factory to use when writing to a <see cref="IMessageSink"/>.
-        /// By default, creates a <see cref="DiagnosticMessage"/>.
         /// </summary>
         public Func<string, IMessageSinkMessage> MessageSinkMessageFactory { get; set; } = m => new DiagnosticMessage(m);
 

From 095a649903f023288e925ddc53e62076efaf4c60 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?C=C3=A9dric=20Luthi?= <cedric.luthi@gmail.com>
Date: Sat, 2 Oct 2021 15:17:30 +0200
Subject: [PATCH 11/14] Tweak logger names

---
 .../Integration/DatabaseFixture.cs                   | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/tests/Logging.XUnit.Tests/Integration/DatabaseFixture.cs b/tests/Logging.XUnit.Tests/Integration/DatabaseFixture.cs
index 730b8e5a..9820ce70 100644
--- a/tests/Logging.XUnit.Tests/Integration/DatabaseFixture.cs
+++ b/tests/Logging.XUnit.Tests/Integration/DatabaseFixture.cs
@@ -11,29 +11,29 @@ namespace MartinCostello.Logging.XUnit.Integration
 {
     public sealed class DatabaseFixture : IAsyncLifetime
     {
-        private readonly ILogger _loggerInitialize;
-        private readonly ILogger _loggerDispose;
+        private readonly ILogger _initializeLogger;
+        private readonly ILogger _disposeLogger;
         private string? _connectionString;
 
         public DatabaseFixture(IMessageSink messageSink)
         {
             using var loggerFactory = new LoggerFactory();
-            _loggerInitialize = loggerFactory.AddXUnit(messageSink, c => c.MessageSinkMessageFactory = m => new PrintableDiagnosticMessage(m)).CreateLogger<DatabaseFixture>();
-            _loggerDispose = messageSink.ToLogger<DatabaseFixture>();
+            _initializeLogger = loggerFactory.AddXUnit(messageSink, c => c.MessageSinkMessageFactory = m => new PrintableDiagnosticMessage(m)).CreateLogger<DatabaseFixture>();
+            _disposeLogger = messageSink.ToLogger<DatabaseFixture>();
         }
 
         public string ConnectionString => _connectionString ?? throw new InvalidOperationException("The connection string is only available after InitializeAsync has completed.");
 
         Task IAsyncLifetime.InitializeAsync()
         {
-            _loggerInitialize.LogInformation("Initializing database");
+            _initializeLogger.LogInformation("Initializing database");
             _connectionString = "Server=localhost";
             return Task.CompletedTask;
         }
 
         Task IAsyncLifetime.DisposeAsync()
         {
-            _loggerDispose.LogInformation("Disposing database");
+            _disposeLogger.LogInformation("Disposing database");
             return Task.CompletedTask;
         }
     }

From a752efb30e8988ef3c6cc1b12f89e8c12b29b4b7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?C=C3=A9dric=20Luthi?= <cedric.luthi@gmail.com>
Date: Sat, 2 Oct 2021 16:17:20 +0200
Subject: [PATCH 12/14] Improve the InvalidOperationException message for
 unreachable code path

---
 src/Logging.XUnit/XUnitLoggerProvider.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Logging.XUnit/XUnitLoggerProvider.cs b/src/Logging.XUnit/XUnitLoggerProvider.cs
index d72cb165..1f36f0b6 100644
--- a/src/Logging.XUnit/XUnitLoggerProvider.cs
+++ b/src/Logging.XUnit/XUnitLoggerProvider.cs
@@ -38,7 +38,7 @@ public virtual ILogger CreateLogger(string categoryName)
                 return new XUnitLogger(categoryName, _messageSinkAccessor, _options);
             }
 
-            throw new InvalidOperationException($"Either {nameof(_outputHelperAccessor)} or {nameof(_messageSinkAccessor)} must not be null.");
+            throw new InvalidOperationException("INTERNAL ERROR. This code path is not reachable since XUnitLoggerProvider is initialized with either a non null _outputHelperAccessor or a non null _messageSinkAccessor.");
         }
 
         /// <inheritdoc />

From 4d1b153e015ef213fd0cb955880d646c8f893632 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?C=C3=A9dric=20Luthi?= <cedric.luthi@gmail.com>
Date: Sat, 2 Oct 2021 16:20:31 +0200
Subject: [PATCH 13/14] Delete unused import

---
 src/Logging.XUnit/XUnitLoggerProvider.cs | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/Logging.XUnit/XUnitLoggerProvider.cs b/src/Logging.XUnit/XUnitLoggerProvider.cs
index 1f36f0b6..be337a77 100644
--- a/src/Logging.XUnit/XUnitLoggerProvider.cs
+++ b/src/Logging.XUnit/XUnitLoggerProvider.cs
@@ -3,7 +3,6 @@
 
 using System;
 using Microsoft.Extensions.Logging;
-using Xunit.Abstractions;
 
 namespace MartinCostello.Logging.XUnit
 {

From 144c55a5e4be472750cf803744f3279ababa7712 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?C=C3=A9dric=20Luthi?= <cedric.luthi@gmail.com>
Date: Sat, 2 Oct 2021 16:41:24 +0200
Subject: [PATCH 14/14] Add some XUnitLoggerExtensions tests

---
 .../XUnitLoggerExtensionsTests.cs             | 79 +++++++++++++++++++
 1 file changed, 79 insertions(+)

diff --git a/tests/Logging.XUnit.Tests/XUnitLoggerExtensionsTests.cs b/tests/Logging.XUnit.Tests/XUnitLoggerExtensionsTests.cs
index 74ec7936..1827fb24 100644
--- a/tests/Logging.XUnit.Tests/XUnitLoggerExtensionsTests.cs
+++ b/tests/Logging.XUnit.Tests/XUnitLoggerExtensionsTests.cs
@@ -2,9 +2,11 @@
 // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
 
 using System;
+using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging.Abstractions;
 using Moq;
+using Shouldly;
 using Xunit;
 using Xunit.Abstractions;
 
@@ -123,6 +125,83 @@ public static void ToLoggerFactory_Validates_Parameters()
             Assert.Throws<ArgumentNullException>("messageSink", () => messageSink!.ToLoggerFactory());
         }
 
+        [Fact]
+        public static void AddXUnit_Registers_Services()
+        {
+            // Arrange
+            var services = new ServiceCollection();
+
+            // Act
+            services.AddLogging(c => c.AddXUnit());
+
+            // Assert
+            var serviceProvider = services.BuildServiceProvider();
+            serviceProvider.GetService<ILoggerProvider>().ShouldBeOfType<XUnitLoggerProvider>();
+            serviceProvider.GetService<ITestOutputHelperAccessor>().ShouldBeOfType<AmbientTestOutputHelperAccessor>();
+        }
+
+        [Fact]
+        public static void AddXUnit_ITestOutputHelperAccessor_Registers_Services()
+        {
+            // Arrange
+            var services = new ServiceCollection();
+            var accessor = Mock.Of<ITestOutputHelperAccessor>();
+
+            // Act
+            services.AddLogging(c => c.AddXUnit(accessor));
+
+            // Assert
+            var serviceProvider = services.BuildServiceProvider();
+            serviceProvider.GetService<ILoggerProvider>().ShouldBeOfType<XUnitLoggerProvider>();
+            serviceProvider.GetService<ITestOutputHelperAccessor>().ShouldBe(accessor);
+        }
+
+        [Fact]
+        public static void AddXUnit_IMessageSinkAccessor_Registers_Services()
+        {
+            // Arrange
+            var services = new ServiceCollection();
+            var accessor = Mock.Of<IMessageSinkAccessor>();
+
+            // Act
+            services.AddLogging(c => c.AddXUnit(accessor));
+
+            // Assert
+            var serviceProvider = services.BuildServiceProvider();
+            serviceProvider.GetService<ILoggerProvider>().ShouldBeOfType<XUnitLoggerProvider>();
+            serviceProvider.GetService<IMessageSinkAccessor>().ShouldBe(accessor);
+        }
+
+        [Fact]
+        public static void AddXUnit_ITestOutputHelper_Registers_Services()
+        {
+            // Arrange
+            var services = new ServiceCollection();
+            var testOutputHelper = Mock.Of<ITestOutputHelper>();
+
+            // Act
+            services.AddLogging(c => c.AddXUnit(testOutputHelper));
+
+            // Assert
+            var serviceProvider = services.BuildServiceProvider();
+            serviceProvider.GetService<ILoggerProvider>().ShouldBeOfType<XUnitLoggerProvider>();
+        }
+
+        [Fact]
+        public static void AddXUnit_IMessageSink_Registers_Services()
+        {
+            // Arrange
+            var services = new ServiceCollection();
+            var messageSink = Mock.Of<IMessageSink>();
+
+            // Act
+            services.AddLogging(c => c.AddXUnit(messageSink));
+
+            // Assert
+            var serviceProvider = services.BuildServiceProvider();
+            serviceProvider.GetService<ILoggerProvider>().ShouldBeOfType<XUnitLoggerProvider>();
+        }
+
         private static void ConfigureAction(XUnitLoggerOptions options)
         {
         }