Repository: serilog/serilog

Issue:
`WriteTo.Batched()` and `IBatchedLogEventSink`
_Serilog.Sinks.PeriodicBatching_ is used by many, many publicly-available Serilog sinks. Although currently managed as a separate package, it's a fundamental part of the ecosystem.

Over the past two years we've been trying to update _Serilog.Sinks.PeriodicBatching_ to work more efficiently with async and tasks, but moving things forward cleanly has been [hampered by ten years of downstream consuming packages](https://github.com/serilog/serilog-sinks-periodicbatching/issues/74) in various states of (non-) maintenance.

I've just [drafted a PR to revert _Serilog.Sinks.PeriodicBatching_ to its original API, again](https://github.com/serilog/serilog-sinks-periodicbatching/pull/75). My proposal is that after merging it, we put _Serilog.Sinks.PeriodicBatching_ into legacy/maintenance mode, and move `IBatchedLogEventSink` into _Serilog_ itself.

Along with the interface, we'd add a `WriteTo.Batched()` public API on `LoggerConfiguration`, along with a public "options" type to parameterize it, and an internal `BatchingSink` implementation. The full cost here would be ~4 types, and a _System.Threading.Channels_ dependency (supported on all current TFMs).

This would present the opportunity we need to nail down a minimal, stable, evolvable API, while promoting batch-oriented sinks to a first-class part of Serilog. Maintained sinks would migrate and drop their _PeriodicBatching_ dependency, while unmaintained sinks could continue using the older package.

Although it may not be in scope for the initial version of the feature, the underlying implementation of the updated batching sink in Serilog could also drive a built-in `WriteTo.Async()`, given the current _Serilog.Sinks.Async_ is expected to migrate to a very similar implementation based on _System.Threading.Channels_ in the future.


Hints:

Patch:
diff --git a/Serilog.sln.DotSettings b/Serilog.sln.DotSettings
index b9aebc18f..26729a031 100644
--- a/Serilog.sln.DotSettings
+++ b/Serilog.sln.DotSettings
@@ -171,6 +171,21 @@
 	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=StaticReadonly/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
 	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=TypeParameters/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="T" Suffix="" Style="AaBb" /&gt;</s:String>
 	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=TypesAndNamespaces/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
+	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=15b5b1f1_002D457c_002D4ca6_002Db278_002D5615aedc07d3/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static readonly fields (private)"&gt;&lt;ElementKinds&gt;&lt;Kind Name="READONLY_FIELD" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;&lt;/Policy&gt;</s:String>
+	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=236f7aa5_002D7b06_002D43ca_002Dbf2a_002D9b31bfcff09a/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Any" AccessRightKinds="Private" Description="Constant fields (private)"&gt;&lt;ElementKinds&gt;&lt;Kind Name="CONSTANT_FIELD" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;&lt;/Policy&gt;</s:String>
+	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=2c62818f_002D621b_002D4425_002Dadc9_002D78611099bfcb/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Any" AccessRightKinds="Any" Description="Type parameters"&gt;&lt;ElementKinds&gt;&lt;Kind Name="TYPE_PARAMETER" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="T" Suffix="" Style="AaBb" /&gt;&lt;/Policy&gt;</s:String>
+	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=4a98fdf6_002D7d98_002D4f5a_002Dafeb_002Dea44ad98c70c/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"&gt;&lt;ElementKinds&gt;&lt;Kind Name="FIELD" /&gt;&lt;Kind Name="READONLY_FIELD" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /&gt;&lt;/Policy&gt;</s:String>
+	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=53eecf85_002Dd821_002D40e8_002Dac97_002Dfdb734542b84/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Instance" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Instance fields (not private)"&gt;&lt;ElementKinds&gt;&lt;Kind Name="FIELD" /&gt;&lt;Kind Name="READONLY_FIELD" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;&lt;/Policy&gt;</s:String>
+	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=61a991a4_002Dd0a3_002D4d19_002D90a5_002Df8f4d75c30c1/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Any" AccessRightKinds="Any" Description="Local variables"&gt;&lt;ElementKinds&gt;&lt;Kind Name="LOCAL_VARIABLE" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;&lt;/Policy&gt;</s:String>
+	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=669e5282_002Dfb4b_002D4e90_002D91e7_002D07d269d04b60/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Any" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Constant fields (not private)"&gt;&lt;ElementKinds&gt;&lt;Kind Name="CONSTANT_FIELD" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;&lt;/Policy&gt;</s:String>
+	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=70345118_002D4b40_002D4ece_002D937c_002Dbbeb7a0b2e70/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Static" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Static fields (not private)"&gt;&lt;ElementKinds&gt;&lt;Kind Name="FIELD" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;&lt;/Policy&gt;</s:String>
+	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=8a85b61a_002D1024_002D4f87_002Db9ef_002D1fdae19930a1/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Any" AccessRightKinds="Any" Description="Parameters"&gt;&lt;ElementKinds&gt;&lt;Kind Name="PARAMETER" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;&lt;/Policy&gt;</s:String>
+	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=8b8504e3_002Df0be_002D4c14_002D9103_002Dc732f2bddc15/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Any" AccessRightKinds="Any" Description="Enum members"&gt;&lt;ElementKinds&gt;&lt;Kind Name="ENUM_MEMBER" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;&lt;/Policy&gt;</s:String>
+	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=a0b4bc4d_002Dd13b_002D4a37_002Db37e_002Dc9c6864e4302/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Any" AccessRightKinds="Any" Description="Types and namespaces"&gt;&lt;ElementKinds&gt;&lt;Kind Name="NAMESPACE" /&gt;&lt;Kind Name="CLASS" /&gt;&lt;Kind Name="STRUCT" /&gt;&lt;Kind Name="ENUM" /&gt;&lt;Kind Name="DELEGATE" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;&lt;/Policy&gt;</s:String>
+	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=a4f433b8_002Dabcd_002D4e55_002Da08f_002D82e78cef0f0c/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Any" AccessRightKinds="Any" Description="Local constants"&gt;&lt;ElementKinds&gt;&lt;Kind Name="LOCAL_CONSTANT" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;&lt;/Policy&gt;</s:String>
+	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=a7a3339e_002D4e89_002D4319_002D9735_002Da9dc4cb74cc7/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Any" AccessRightKinds="Any" Description="Interfaces"&gt;&lt;ElementKinds&gt;&lt;Kind Name="INTERFACE" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="I" Suffix="" Style="AaBb" /&gt;&lt;/Policy&gt;</s:String>
+	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=c873eafb_002Dd57f_002D481d_002D8c93_002D77f6863c2f88/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Static" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Static readonly fields (not private)"&gt;&lt;ElementKinds&gt;&lt;Kind Name="READONLY_FIELD" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;&lt;/Policy&gt;</s:String>
+	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=f9fce829_002De6f4_002D4cb2_002D80f1_002D5497c44f51df/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static fields (private)"&gt;&lt;ElementKinds&gt;&lt;Kind Name="FIELD" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /&gt;&lt;/Policy&gt;</s:String>
 	<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FCONSTRUCTOR/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
 	<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FFUNCTION/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
 	<s:String x:Key="/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FGLOBAL_005FVARIABLE/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
@@ -207,6 +222,7 @@
 	<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002ECSharpPlaceAttributeOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateThisQualifierSettings/@EntryIndexedValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EPredefinedNamingRulesToUserRulesUpgrade/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002EJavaScript_002ECodeStyle_002ESettingsUpgrade_002EJsCodeFormatterSettingsUpgrader/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002EJavaScript_002ECodeStyle_002ESettingsUpgrade_002EJsParsFormattingSettingsUpgrader/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002EJavaScript_002ECodeStyle_002ESettingsUpgrade_002EJsWrapperSettingsUpgrader/@EntryIndexedValue">True</s:Boolean>
diff --git a/src/Serilog/Configuration/BatchingOptions.cs b/src/Serilog/Configuration/BatchingOptions.cs
new file mode 100644
index 000000000..fe7ac7cf2
--- /dev/null
+++ b/src/Serilog/Configuration/BatchingOptions.cs
@@ -0,0 +1,46 @@
+// Copyright © Serilog Contributors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+namespace Serilog.Core.Sinks.Batching;
+
+/// <summary>
+/// Initialization options for <see cref="BatchingSink"/>.
+/// </summary>
+public class BatchingOptions
+{
+    /// <summary>
+    /// Eagerly emit a batch containing the first received event, regardless of
+    /// the target batch size or batching time. This helps with perceived "liveness"
+    /// when running/debugging applications interactively. The default is <c>true</c>.
+    /// </summary>
+    public bool EagerlyEmitFirstEvent { get; set; } = true;
+
+    /// <summary>
+    /// The maximum number of events to include in a single batch. The default is <c>1000</c>.
+    /// </summary>
+    public int BatchSizeLimit { get; set; } = 1000;
+
+    /// <summary>
+    /// The maximum delay between event batches. The default is two seconds. If a batch can be filled
+    /// before the buffering time limit is reached, it will be emitted without waiting.
+    /// </summary>
+    public TimeSpan BufferingTimeLimit { get; set; } = TimeSpan.FromSeconds(2);
+
+    /// <summary>
+    /// Maximum number of events to hold in the sink's internal queue, or <c>null</c>
+    /// for an unbounded queue. The default is <c>100000</c>. When the limit is exceeded,
+    /// backpressure is applied.
+    /// </summary>
+    public int? QueueLimit { get; set; } = 100000;
+}
diff --git a/src/Serilog/Configuration/LoggerSinkConfiguration.cs b/src/Serilog/Configuration/LoggerSinkConfiguration.cs
index 8f869972f..28feb9ebf 100644
--- a/src/Serilog/Configuration/LoggerSinkConfiguration.cs
+++ b/src/Serilog/Configuration/LoggerSinkConfiguration.cs
@@ -12,6 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+// ReSharper disable MergeCastWithTypeCheck
+
+using Serilog.Core.Sinks.Batching;
+
 namespace Serilog.Configuration;
 
 /// <summary>
@@ -29,7 +33,7 @@ internal LoggerSinkConfiguration(LoggerConfiguration loggerConfiguration, Action
     }
 
     /// <summary>
-    /// Write log events to the specified <see cref="ILogEventSink"/>.
+    /// Write log events to an <see cref="ILogEventSink"/>.
     /// </summary>
     /// <param name="logEventSink">The sink.</param>
     /// <param name="restrictedToMinimumLevel">The minimum level for
@@ -46,7 +50,7 @@ public LoggerConfiguration Sink(
     }
 
     /// <summary>
-    /// Write log events to the specified <see cref="ILogEventSink"/>.
+    /// Write log events to an <see cref="ILogEventSink"/>.
     /// </summary>
     /// <param name="logEventSink">The sink.</param>
     /// <param name="restrictedToMinimumLevel">The minimum level for
@@ -62,6 +66,8 @@ public LoggerConfiguration Sink(
         // ReSharper disable once MethodOverloadWithOptionalParameter
         LoggingLevelSwitch? levelSwitch = null)
     {
+        Guard.AgainstNull(logEventSink);
+
         var sink = logEventSink;
         if (levelSwitch != null)
         {
@@ -96,6 +102,50 @@ public LoggerConfiguration Sink<TSink>(
         return Sink(new TSink(), restrictedToMinimumLevel, levelSwitch);
     }
 
+    /// <summary>
+    /// Write log events to an <see cref="IBatchedLogEventSink"/>. Events will be internally buffered, and
+    /// written to the sink in batches.
+    /// </summary>
+    /// <param name="batchedLogEventSink">The batched sink to receive events.</param>
+    /// <param name="batchingOptions">Options that control batch sizes, buffering time, and backpressure.</param>
+    /// <param name="restrictedToMinimumLevel">The minimum level for
+    /// events passed through the sink. Ignored when <paramref name="levelSwitch"/> is specified.</param>
+    /// <param name="levelSwitch">A switch allowing the pass-through minimum level
+    /// to be changed at runtime.</param>
+    /// <returns>Configuration object allowing method chaining.</returns>
+    public LoggerConfiguration Sink(
+        IBatchedLogEventSink batchedLogEventSink,
+        BatchingOptions batchingOptions,
+        LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum,
+        LoggingLevelSwitch? levelSwitch = null)
+    {
+        Guard.AgainstNull(batchedLogEventSink);
+        Guard.AgainstNull(batchingOptions);
+
+        return Sink(new BatchingSink(batchedLogEventSink, batchingOptions), restrictedToMinimumLevel, levelSwitch);
+    }
+
+    /// <summary>
+    /// Write log events to an <see cref="IBatchedLogEventSink"/>. Events will be internally buffered, and
+    /// written to the sink in batches.
+    /// </summary>
+    /// <typeparam name="TSink">The type of a batched sink to receive events. The sink must provide a public,
+    /// parameterless constructor.</param>
+    /// <param name="batchingOptions">Options that control batch sizes, buffering time, and backpressure.</param>
+    /// <param name="restrictedToMinimumLevel">The minimum level for
+    /// events passed through the sink. Ignored when <paramref name="levelSwitch"/> is specified.</param>
+    /// <param name="levelSwitch">A switch allowing the pass-through minimum level
+    /// to be changed at runtime.</param>
+    /// <returns>Configuration object allowing method chaining.</returns>
+    public LoggerConfiguration Sink<TSink>(
+        BatchingOptions batchingOptions,
+        LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum,
+        LoggingLevelSwitch? levelSwitch = null)
+        where TSink : IBatchedLogEventSink, new()
+    {
+        return Sink(new TSink(), batchingOptions, restrictedToMinimumLevel, levelSwitch);
+    }
+
     /// <summary>
     /// Write log events to a sub-logger, where further processing may occur. Events through
     /// the sub-logger will be constrained by filters and enriched by enrichers that are
@@ -197,7 +247,7 @@ public LoggerConfiguration Conditional(Func<LogEvent, bool> condition, Action<Lo
 
         // Level aliases and so on don't need to be accepted here; if the user wants both a condition and leveling, they
         // can specify `restrictedToMinimumLevel` etc in the wrapped sink configuration.
-        return Wrap(this, s => new ConditionalSink(s, condition), configureSink, LevelAlias.Minimum, null);
+        return Wrap(this, s => new ConditionalSink(s, condition), configureSink);
     }
 
     /// <summary>
@@ -251,7 +301,7 @@ public static LoggerConfiguration Wrap(
 #if FEATURE_ASYNCDISPOSABLE
                 or IAsyncDisposable
 #endif
-                )
+           )
         {
             wrapper = new DisposeDelegatingSink(wrapper, enclosed as IDisposable
 #if FEATURE_ASYNCDISPOSABLE
diff --git a/src/Serilog/Core/IBatchedLogEventSink.cs b/src/Serilog/Core/IBatchedLogEventSink.cs
new file mode 100644
index 000000000..5b548c1ed
--- /dev/null
+++ b/src/Serilog/Core/IBatchedLogEventSink.cs
@@ -0,0 +1,46 @@
+// Copyright © Serilog Contributors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+namespace Serilog.Core;
+
+/// <summary>
+/// A destination that accepts events in batches. Many sinks gain a performance advantage by handling events in batches,
+/// for example to combine multiple events into a single network request to a remote collector. Because the client
+/// application cannot wait for every event to be flushed when batching is used, batched sinks normally work
+/// asynchronously to conserve local resources while batches are sent.
+/// </summary>
+/// <seealso cref="ILogEventSink"/>
+public interface IBatchedLogEventSink
+{
+    /// <summary>
+    /// Emit a batch of log events, asynchronously.
+    /// </summary>
+    /// <param name="batch">The batch of events to emit.</param>
+    /// <remarks>Implementers should allow exceptions to propagate when batches fail. The batching infrastructure
+    /// handles exception handling, diagnostics, and retries.</remarks>
+    Task EmitBatchAsync(IReadOnlyCollection<LogEvent> batch);
+
+    /// <summary>
+    /// Allows sinks to perform periodic work without requiring additional threads
+    /// or timers (thus avoiding additional flush/shut-down complexity).
+    /// </summary>
+    Task OnEmptyBatchAsync()
+#if FEATURE_DEFAULT_INTERFACE
+    {
+        return Task.CompletedTask;
+    }
+#else
+        ;
+#endif
+}
\ No newline at end of file
diff --git a/src/Serilog/Core/ILogEventSink.cs b/src/Serilog/Core/ILogEventSink.cs
index 85db94a1d..b2d34c948 100644
--- a/src/Serilog/Core/ILogEventSink.cs
+++ b/src/Serilog/Core/ILogEventSink.cs
@@ -1,4 +1,4 @@
-// Copyright 2013-2015 Serilog Contributors
+// Copyright © Serilog Contributors
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -17,11 +17,15 @@ namespace Serilog.Core;
 /// <summary>
 /// A destination for log events.
 /// </summary>
+/// <seealso cref="ILogEventSink"/>
 public interface ILogEventSink
 {
     /// <summary>
     /// Emit the provided log event to the sink.
     /// </summary>
     /// <param name="logEvent">The log event to write.</param>
+    /// <seealso cref="IBatchedLogEventSink"/>
+    /// <remarks>Implementers should allow exceptions to propagate when event emission fails. The logger will handle
+    /// exceptions and produce diagnostics appropriately.
     void Emit(LogEvent logEvent);
 }
diff --git a/src/Serilog/Core/Logger.cs b/src/Serilog/Core/Logger.cs
index 04214787f..ddb344bbc 100644
--- a/src/Serilog/Core/Logger.cs
+++ b/src/Serilog/Core/Logger.cs
@@ -213,7 +213,7 @@ public void Write<T>(LogEventLevel level, string messageTemplate, T propertyValu
             span[0] = propertyValue;
             Write(level, messageTemplate, span);
 #else
-            Write(level, messageTemplate, new object?[] { propertyValue });
+            Write(level, messageTemplate, [propertyValue]);
 #endif
         }
     }
@@ -238,7 +238,7 @@ public void Write<T0, T1>(LogEventLevel level, string messageTemplate, T0 proper
             span[1] = propertyValue1;
             Write(level, messageTemplate, span);
 #else
-            Write(level, messageTemplate, new object?[] { propertyValue0, propertyValue1 });
+            Write(level, messageTemplate, [propertyValue0, propertyValue1]);
 #endif
         }
     }
@@ -265,7 +265,7 @@ public void Write<T0, T1, T2>(LogEventLevel level, string messageTemplate, T0 pr
             span[2] = propertyValue2;
             Write(level, messageTemplate, span);
 #else
-            Write(level, messageTemplate, new object?[] { propertyValue0, propertyValue1, propertyValue2 });
+            Write(level, messageTemplate, [propertyValue0, propertyValue1, propertyValue2]);
 #endif
         }
     }
@@ -339,7 +339,7 @@ public void Write<T>(LogEventLevel level, Exception? exception, string messageTe
             span[0] = propertyValue;
             Write(level, exception, messageTemplate, span);
 #else
-            Write(level, exception, messageTemplate, new object?[] { propertyValue });
+            Write(level, exception, messageTemplate, [propertyValue]);
 #endif
         }
     }
@@ -365,7 +365,7 @@ public void Write<T0, T1>(LogEventLevel level, Exception? exception, string mess
             span[1] = propertyValue1;
             Write(level, exception, messageTemplate, span);
 #else
-            Write(level, exception, messageTemplate, new object?[] { propertyValue0, propertyValue1 });
+            Write(level, exception, messageTemplate, [propertyValue0, propertyValue1]);
 #endif
         }
     }
@@ -393,7 +393,7 @@ public void Write<T0, T1, T2>(LogEventLevel level, Exception? exception, string
             span[2] = propertyValue2;
             Write(level, exception, messageTemplate, span);
 #else
-            Write(level, exception, messageTemplate, new object?[] { propertyValue0, propertyValue1, propertyValue2 });
+            Write(level, exception, messageTemplate, [propertyValue0, propertyValue1, propertyValue2]);
 #endif
         }
     }
@@ -414,7 +414,7 @@ public void Write(LogEventLevel level, Exception? exception, string messageTempl
         // Catch a common pitfall when a single non-object array is cast to object[]
         if (propertyValues != null &&
             propertyValues.GetType() != typeof(object[]))
-            propertyValues = new object[] { propertyValues };
+            propertyValues = [propertyValues];
 
         var logTimestamp = DateTimeOffset.Now;
 #if FEATURE_SPAN
diff --git a/src/Serilog/Core/Sinks/AggregateSink.cs b/src/Serilog/Core/Sinks/AggregateSink.cs
index c314391b6..ea419241c 100644
--- a/src/Serilog/Core/Sinks/AggregateSink.cs
+++ b/src/Serilog/Core/Sinks/AggregateSink.cs
@@ -37,7 +37,7 @@ public void Emit(LogEvent logEvent)
             catch (Exception ex)
             {
                 SelfLog.WriteLine("Caught exception while emitting to sink {0}: {1}", sink, ex);
-                exceptions ??= new();
+                exceptions ??= [];
                 exceptions.Add(ex);
             }
         }
diff --git a/src/Serilog/Core/Sinks/Batching/BatchingSink.cs b/src/Serilog/Core/Sinks/Batching/BatchingSink.cs
new file mode 100644
index 000000000..2a15f1448
--- /dev/null
+++ b/src/Serilog/Core/Sinks/Batching/BatchingSink.cs
@@ -0,0 +1,296 @@
+// Copyright © Serilog Contributors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System.Threading.Channels;
+
+// ReSharper disable UnusedParameter.Global, ConvertIfStatementToConditionalTernaryExpression, MemberCanBePrivate.Global, UnusedMember.Global, VirtualMemberNeverOverridden.Global, ClassWithVirtualMembersNeverInherited.Global, SuspiciousTypeConversion.Global
+
+namespace Serilog.Core.Sinks.Batching;
+
+/// <summary>
+/// Buffers log events into batches for background flushing.
+/// </summary>
+sealed class BatchingSink : ILogEventSink, IDisposable
+#if FEATURE_ASYNCDISPOSABLE
+        , IAsyncDisposable
+#endif
+{
+    // Buffers events from the write- to the read side.
+    readonly Channel<LogEvent> _queue;
+
+    // These fields are used by the write side to signal shutdown.
+    // A mutex is required because the queue writer `Complete()` call is not idempotent and will throw if
+    // called multiple times, e.g. via multiple `Dispose()` calls on this sink.
+    readonly object _stateLock = new();
+    // Needed because the read loop needs to observe shutdown even when the target batched (remote) sink is
+    // unable to accept events (preventing the queue from being drained and completion being observed).
+    readonly CancellationTokenSource _shutdownSignal = new();
+    // The write side can wait on this to ensure shutdown has completed.
+    readonly Task _runLoop;
+
+    // Used only by the read side.
+    readonly IBatchedLogEventSink _targetSink;
+    readonly int _batchSizeLimit;
+    readonly bool _eagerlyEmitFirstEvent;
+    readonly FailureAwareBatchScheduler _batchScheduler;
+    readonly Queue<LogEvent> _currentBatch = new();
+    readonly Task _waitForShutdownSignal;
+    Task<bool>? _cachedWaitToRead;
+
+    /// <summary>
+    /// Construct a <see cref="BatchingSink"/>.
+    /// </summary>
+    /// <param name="batchedSink">A <see cref="IBatchedLogEventSink"/> to send log event batches to. Batches and empty
+    /// batch notifications will not be sent concurrently. When the <see cref="BatchingSink"/> is disposed,
+    /// it will dispose this object if possible.</param>
+    /// <param name="options">Options controlling behavior of the sink.</param>
+    public BatchingSink(IBatchedLogEventSink batchedSink, BatchingOptions options)
+    {
+        if (options == null) throw new ArgumentNullException(nameof(options));
+        if (options.BatchSizeLimit <= 0)
+            throw new ArgumentOutOfRangeException(nameof(options), "The batch size limit must be greater than zero.");
+        if (options.BufferingTimeLimit <= TimeSpan.Zero)
+            throw new ArgumentOutOfRangeException(nameof(options), "The period must be greater than zero.");
+
+        _targetSink = batchedSink ?? throw new ArgumentNullException(nameof(batchedSink));
+        _batchSizeLimit = options.BatchSizeLimit;
+        _queue = options.QueueLimit is { } limit
+            ? Channel.CreateBounded<LogEvent>(new BoundedChannelOptions(limit) { SingleReader = true })
+            : Channel.CreateUnbounded<LogEvent>(new UnboundedChannelOptions { SingleReader = true });
+        _batchScheduler = new FailureAwareBatchScheduler(options.BufferingTimeLimit);
+        _eagerlyEmitFirstEvent = options.EagerlyEmitFirstEvent;
+        _waitForShutdownSignal = Task.Delay(Timeout.InfiniteTimeSpan, _shutdownSignal.Token)
+            .ContinueWith(e => e.Exception, TaskContinuationOptions.OnlyOnFaulted);
+
+        // The conditional here is no longer required in .NET 8+ (dotnet/runtime#82912)
+        using (ExecutionContext.IsFlowSuppressed() ? (IDisposable?)null : ExecutionContext.SuppressFlow())
+        {
+            _runLoop = Task.Run(LoopAsync);
+        }
+    }
+
+    /// <summary>
+    /// Emit the provided log event to the sink. If the sink is being disposed or
+    /// the app domain unloaded, then the event is ignored.
+    /// </summary>
+    /// <param name="logEvent">Log event to emit.</param>
+    /// <exception cref="ArgumentNullException">The event is null.</exception>
+    /// <remarks>
+    /// The sink implements the contract that any events whose Emit() method has
+    /// completed at the time of sink disposal will be flushed (or attempted to,
+    /// depending on app domain state).
+    /// </remarks>
+    public void Emit(LogEvent logEvent)
+    {
+        if (logEvent == null) throw new ArgumentNullException(nameof(logEvent));
+
+        if (_shutdownSignal.IsCancellationRequested)
+            return;
+
+        _queue.Writer.TryWrite(logEvent);
+    }
+
+    async Task LoopAsync()
+    {
+        var isEagerBatch = _eagerlyEmitFirstEvent;
+        do
+        {
+            // Code from here through to the `try` block is expected to be infallible. It's structured this way because
+            // any failure modes within it haven't been accounted for in the rest of the sink design, and would need
+            // consideration in order for the sink to function robustly (i.e. to avoid hot/infinite looping).
+
+            var fillBatch = Task.Delay(_batchScheduler.NextInterval);
+            do
+            {
+                while (_currentBatch.Count < _batchSizeLimit &&
+                       !_shutdownSignal.IsCancellationRequested &&
+                       _queue.Reader.TryRead(out var next))
+                {
+                    _currentBatch.Enqueue(next);
+                }
+            } while ((_currentBatch.Count < _batchSizeLimit && !isEagerBatch || _currentBatch.Count == 0) &&
+                     !_shutdownSignal.IsCancellationRequested &&
+                     await TryWaitToReadAsync(_queue.Reader, fillBatch, _shutdownSignal.Token).ConfigureAwait(false));
+
+            try
+            {
+                if (_currentBatch.Count == 0)
+                {
+                    await _targetSink.OnEmptyBatchAsync().ConfigureAwait(false);
+                }
+                else
+                {
+                    isEagerBatch = false;
+
+                    await _targetSink.EmitBatchAsync(_currentBatch).ConfigureAwait(false);
+
+                    _currentBatch.Clear();
+                    _batchScheduler.MarkSuccess();
+                }
+            }
+            catch (Exception ex)
+            {
+                WriteToSelfLog("failed emitting a batch", ex);
+                _batchScheduler.MarkFailure();
+
+                if (_batchScheduler.ShouldDropBatch)
+                {
+                    WriteToSelfLog("dropping the current batch");
+                    _currentBatch.Clear();
+                }
+
+                if (_batchScheduler.ShouldDropQueue)
+                {
+                    WriteToSelfLog("dropping all queued events");
+
+                    // Not ideal, uses some CPU capacity unnecessarily and doesn't complete in bounded time. The goal is
+                    // to reduce memory pressure on the client if the server is offline for extended periods. May be
+                    // worth reviewing and possibly abandoning this.
+                    while (_queue.Reader.TryRead(out _) && !_shutdownSignal.IsCancellationRequested) { }
+                }
+
+                // Wait out the remainder of the batch fill time so that we don't overwhelm the server. With each
+                // successive failure the interval will increase. Needs special handling so that we don't need to
+                // make `fillBatch` cancellable (and thus fallible).
+                await Task.WhenAny(fillBatch, _waitForShutdownSignal).ConfigureAwait(false);
+            }
+        }
+        while (!_shutdownSignal.IsCancellationRequested);
+
+        // At this point:
+        //  - The sink is being disposed
+        //  - The queue has been completed
+        //  - The queue may or may not be empty
+        //  - The waiting batch may or may not be empty
+        //  - The target sink may or may not be accepting events
+
+        // Try flushing the rest of the queue, but bail out on any failure. Shutdown time is unbounded, but it
+        // doesn't make sense to pick an arbitrary limit - a future version might add a new option to control this.
+        try
+        {
+            while (_queue.Reader.TryPeek(out _))
+            {
+                while (_currentBatch.Count < _batchSizeLimit &&
+                       _queue.Reader.TryRead(out var next))
+                {
+                    _currentBatch.Enqueue(next);
+                }
+
+                if (_currentBatch.Count != 0)
+                {
+                    await _targetSink.EmitBatchAsync(_currentBatch).ConfigureAwait(false);
+                    _currentBatch.Clear();
+                }
+            }
+        }
+        catch (Exception ex)
+        {
+            WriteToSelfLog("failed emitting a batch during shutdown; dropping remaining queued events", ex);
+        }
+    }
+
+    // Wait until `reader` has items to read. Returns `false` if the `timeout` task completes, or if the reader is cancelled.
+    async Task<bool> TryWaitToReadAsync(ChannelReader<LogEvent> reader, Task timeout, CancellationToken cancellationToken)
+    {
+        var waitToRead = _cachedWaitToRead ?? reader.WaitToReadAsync(cancellationToken).AsTask();
+        _cachedWaitToRead = null;
+
+        var completed = await Task.WhenAny(timeout, waitToRead).ConfigureAwait(false);
+
+        // Avoid unobserved task exceptions in the cancellation and failure cases. Note that we may not end up observing
+        // read task cancellation exceptions during shutdown, may be some room to improve.
+        if (completed is { Exception: not null, IsCanceled: false })
+        {
+            WriteToSelfLog($"could not read from queue: {completed.Exception}");
+        }
+
+        if (completed == timeout)
+        {
+            // Dropping references to `waitToRead` will cause it and some supporting objects to leak; disposing it
+            // will break the channel and cause future attempts to read to fail. So, we cache and reuse it next time
+            // around the loop.
+
+            _cachedWaitToRead = waitToRead;
+            return false;
+        }
+
+        if (waitToRead.Status is not TaskStatus.RanToCompletion)
+            return false;
+
+        return await waitToRead;
+    }
+
+    /// <inheritdoc/>
+    public void Dispose()
+    {
+        SignalShutdown();
+
+        try
+        {
+            _runLoop.Wait();
+        }
+        catch (Exception ex)
+        {
+            // E.g. the task was canceled before ever being run, or internally failed and threw
+            // an unexpected exception.
+            WriteToSelfLog("caught exception during disposal", ex);
+        }
+
+        (_targetSink as IDisposable)?.Dispose();
+    }
+
+#if FEATURE_ASYNCDISPOSABLE
+        /// <inheritdoc/>
+        public async ValueTask DisposeAsync()
+        {
+            SignalShutdown();
+
+            try
+            {
+                await _runLoop.ConfigureAwait(false);
+            }
+            catch (Exception ex)
+            {
+                // E.g. the task was canceled before ever being run, or internally failed and threw
+                // an unexpected exception.
+                WriteToSelfLog("caught exception during async disposal", ex);
+            }
+
+            if (_targetSink is IAsyncDisposable asyncDisposable)
+                await asyncDisposable.DisposeAsync().ConfigureAwait(false);
+            else
+                (_targetSink as IDisposable)?.Dispose();
+        }
+#endif
+
+    void SignalShutdown()
+    {
+        lock (_stateLock)
+        {
+            if (!_shutdownSignal.IsCancellationRequested)
+            {
+                // Relies on synchronization via `_stateLock`: once the writer is completed, subsequent attempts to
+                // complete it will throw.
+                _queue.Writer.Complete();
+                _shutdownSignal.Cancel();
+            }
+        }
+    }
+
+    void WriteToSelfLog(string message, Exception? exception = null)
+    {
+        var ex = exception != null ? $"{Environment.NewLine}{exception}" : "";
+        SelfLog.WriteLine($"BatchingSink ({_targetSink}): {message}{ex}");
+    }
+}
diff --git a/src/Serilog/Core/Sinks/Batching/FailureAwareBatchScheduler.cs b/src/Serilog/Core/Sinks/Batching/FailureAwareBatchScheduler.cs
new file mode 100644
index 000000000..ff9b934ec
--- /dev/null
+++ b/src/Serilog/Core/Sinks/Batching/FailureAwareBatchScheduler.cs
@@ -0,0 +1,88 @@
+// Copyright © Serilog Contributors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+namespace Serilog.Core.Sinks.Batching;
+
+/// <summary>
+/// Manages reconnection period and transient fault response for <see cref="BatchingSink"/>.
+/// During normal operation an object of this type will simply echo the configured batch transmission
+/// period. When availability fluctuates, the class tracks the number of failed attempts, each time
+/// increasing the interval before reconnection is attempted (up to a set maximum) and at predefined
+/// points indicating that either the current batch, or entire waiting queue, should be dropped. This
+/// Serves two purposes - first, a loaded receiver may need a temporary reduction in traffic while coming
+/// back online. Second, the sender needs to account for both bad batches (the first fault response) and
+/// also overproduction (the second, queue-dropping response). In combination these should provide a
+/// reasonable delivery effort but ultimately protect the sender from memory exhaustion.
+/// </summary>
+class FailureAwareBatchScheduler
+{
+    static readonly TimeSpan MinimumBackoffPeriod = TimeSpan.FromSeconds(5);
+    static readonly TimeSpan MaximumBackoffInterval = TimeSpan.FromMinutes(10);
+
+    const int FailuresBeforeDroppingBatch = 8;
+    const int FailuresBeforeDroppingQueue = 10;
+
+    readonly TimeSpan _period;
+
+    int _failuresSinceSuccessfulBatch;
+
+    public FailureAwareBatchScheduler(TimeSpan period)
+    {
+        if (period < TimeSpan.Zero)
+            throw new ArgumentOutOfRangeException(nameof(period), "The batching period must be a positive timespan.");
+
+        _period = period;
+    }
+
+    public void MarkSuccess()
+    {
+        _failuresSinceSuccessfulBatch = 0;
+    }
+
+    public void MarkFailure()
+    {
+        ++_failuresSinceSuccessfulBatch;
+    }
+
+    public TimeSpan NextInterval
+    {
+        get
+        {
+            // Available, and first failure, just try the batch interval
+            if (_failuresSinceSuccessfulBatch <= 1) return _period;
+
+            // Second failure, start ramping up the interval - first 2x, then 4x, ...
+            var backoffFactor = Math.Pow(2, _failuresSinceSuccessfulBatch - 1);
+
+            // If the period is ridiculously short, give it a boost so we get some
+            // visible backoff.
+            var backoffPeriod = Math.Max(_period.Ticks, MinimumBackoffPeriod.Ticks);
+
+            // The "ideal" interval
+            var backedOff = (long) (backoffPeriod * backoffFactor);
+
+            // Capped to the maximum interval
+            var cappedBackoff = Math.Min(MaximumBackoffInterval.Ticks, backedOff);
+
+            // Unless that's shorter than the period, in which case we'll just apply the period
+            var actual = Math.Max(_period.Ticks, cappedBackoff);
+
+            return TimeSpan.FromTicks(actual);
+        }
+    }
+
+    public bool ShouldDropBatch => _failuresSinceSuccessfulBatch >= FailuresBeforeDroppingBatch;
+
+    public bool ShouldDropQueue => _failuresSinceSuccessfulBatch >= FailuresBeforeDroppingQueue;
+}
diff --git a/src/Serilog/Core/Sinks/DisposingAggregateSink.cs b/src/Serilog/Core/Sinks/DisposingAggregateSink.cs
index 7336a58c7..5427baf23 100644
--- a/src/Serilog/Core/Sinks/DisposingAggregateSink.cs
+++ b/src/Serilog/Core/Sinks/DisposingAggregateSink.cs
@@ -39,7 +39,7 @@ public void Emit(LogEvent logEvent)
             catch (Exception ex)
             {
                 SelfLog.WriteLine("Caught exception while emitting to sink {0}: {1}", sink, ex);
-                exceptions ??= new();
+                exceptions ??= [];
                 exceptions.Add(ex);
             }
         }
diff --git a/src/Serilog/Formatting/Display/LevelOutputFormat.cs b/src/Serilog/Formatting/Display/LevelOutputFormat.cs
index 2a9173b10..3776fb39d 100644
--- a/src/Serilog/Formatting/Display/LevelOutputFormat.cs
+++ b/src/Serilog/Formatting/Display/LevelOutputFormat.cs
@@ -22,32 +22,35 @@ namespace Serilog.Formatting.Display;
 /// </summary>
 static class LevelOutputFormat
 {
-    static readonly string[][] _titleCaseLevelMap = {
-        new []{ "V", "Vb", "Vrb", "Verb", "Verbo", "Verbos", "Verbose" },
-        new []{ "D", "De", "Dbg", "Dbug", "Debug" },
-        new []{ "I", "In", "Inf", "Info", "Infor", "Inform", "Informa", "Informat", "Informati", "Informatio", "Information" },
-        new []{ "W", "Wn", "Wrn", "Warn", "Warni", "Warnin", "Warning" },
-        new []{ "E", "Er", "Err", "Eror", "Error" },
-        new []{ "F", "Fa", "Ftl", "Fatl", "Fatal" }
-    };
+    static readonly string[][] _titleCaseLevelMap =
+    [
+        ["V", "Vb", "Vrb", "Verb", "Verbo", "Verbos", "Verbose"],
+        ["D", "De", "Dbg", "Dbug", "Debug"],
+        ["I", "In", "Inf", "Info", "Infor", "Inform", "Informa", "Informat", "Informati", "Informatio", "Information"],
+        ["W", "Wn", "Wrn", "Warn", "Warni", "Warnin", "Warning"],
+        ["E", "Er", "Err", "Eror", "Error"],
+        ["F", "Fa", "Ftl", "Fatl", "Fatal"]
+    ];
 
-    static readonly string[][] _lowerCaseLevelMap = {
-        new []{ "v", "vb", "vrb", "verb", "verbo", "verbos", "verbose" },
-        new []{ "d", "de", "dbg", "dbug", "debug" },
-        new []{ "i", "in", "inf", "info", "infor", "inform", "informa", "informat", "informati", "informatio", "information" },
-        new []{ "w", "wn", "wrn", "warn", "warni", "warnin", "warning" },
-        new []{ "e", "er", "err", "eror", "error" },
-        new []{ "f", "fa", "ftl", "fatl", "fatal" }
-    };
+    static readonly string[][] _lowerCaseLevelMap =
+    [
+        ["v", "vb", "vrb", "verb", "verbo", "verbos", "verbose"],
+        ["d", "de", "dbg", "dbug", "debug"],
+        ["i", "in", "inf", "info", "infor", "inform", "informa", "informat", "informati", "informatio", "information"],
+        ["w", "wn", "wrn", "warn", "warni", "warnin", "warning"],
+        ["e", "er", "err", "eror", "error"],
+        ["f", "fa", "ftl", "fatl", "fatal"]
+    ];
 
-    static readonly string[][] _upperCaseLevelMap = {
-        new []{ "V", "VB", "VRB", "VERB", "VERBO", "VERBOS", "VERBOSE" },
-        new []{ "D", "DE", "DBG", "DBUG", "DEBUG" },
-        new []{ "I", "IN", "INF", "INFO", "INFOR", "INFORM", "INFORMA", "INFORMAT", "INFORMATI", "INFORMATIO", "INFORMATION" },
-        new []{ "W", "WN", "WRN", "WARN", "WARNI", "WARNIN", "WARNING" },
-        new []{ "E", "ER", "ERR", "EROR", "ERROR" },
-        new []{ "F", "FA", "FTL", "FATL", "FATAL" }
-    };
+    static readonly string[][] _upperCaseLevelMap =
+    [
+        ["V", "VB", "VRB", "VERB", "VERBO", "VERBOS", "VERBOSE"],
+        ["D", "DE", "DBG", "DBUG", "DEBUG"],
+        ["I", "IN", "INF", "INFO", "INFOR", "INFORM", "INFORMA", "INFORMAT", "INFORMATI", "INFORMATIO", "INFORMATION"],
+        ["W", "WN", "WRN", "WARN", "WARNI", "WARNIN", "WARNING"],
+        ["E", "ER", "ERR", "EROR", "ERROR"],
+        ["F", "FA", "FTL", "FATL", "FATAL"]
+    ];
 
     public static string GetLevelMoniker(LogEventLevel value, string? format = null)
     {
diff --git a/src/Serilog/ILogger.cs b/src/Serilog/ILogger.cs
index 95ac8d67f..87733721a 100644
--- a/src/Serilog/ILogger.cs
+++ b/src/Serilog/ILogger.cs
@@ -12,7 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#if FEATURE_DEFAULT_INTERFACE
 using System.Diagnostics;
+#endif
 
 namespace Serilog;
 
@@ -162,7 +164,7 @@ void Write<T>(LogEventLevel level, string messageTemplate, T propertyValue)
         // Avoid the array allocation and any boxing allocations when the level isn't enabled
         if (IsEnabled(level))
         {
-            Write(level, messageTemplate, new object?[] { propertyValue });
+            Write(level, messageTemplate, [propertyValue]);
         }
     }
 #else
@@ -183,7 +185,7 @@ void Write<T0, T1>(LogEventLevel level, string messageTemplate, T0 propertyValue
         // Avoid the array allocation and any boxing allocations when the level isn't enabled
         if (IsEnabled(level))
         {
-            Write(level, messageTemplate, new object?[] { propertyValue0, propertyValue1 });
+            Write(level, messageTemplate, [propertyValue0, propertyValue1]);
         }
     }
 #else
@@ -205,7 +207,7 @@ void Write<T0, T1, T2>(LogEventLevel level, string messageTemplate, T0 propertyV
         // Avoid the array allocation and any boxing allocations when the level isn't enabled
         if (IsEnabled(level))
         {
-            Write(level, messageTemplate, new object?[] { propertyValue0, propertyValue1, propertyValue2 });
+            Write(level, messageTemplate, [propertyValue0, propertyValue1, propertyValue2]);
         }
     }
 #else
@@ -259,7 +261,7 @@ void Write<T>(LogEventLevel level, Exception? exception, string messageTemplate,
         // Avoid the array allocation and any boxing allocations when the level isn't enabled
         if (IsEnabled(level))
         {
-            Write(level, exception, messageTemplate, new object?[] { propertyValue });
+            Write(level, exception, messageTemplate, [propertyValue]);
         }
     }
 #else
@@ -281,7 +283,7 @@ void Write<T0, T1>(LogEventLevel level, Exception? exception, string messageTemp
         // Avoid the array allocation and any boxing allocations when the level isn't enabled
         if (IsEnabled(level))
         {
-            Write(level, exception, messageTemplate, new object?[] { propertyValue0, propertyValue1 });
+            Write(level, exception, messageTemplate, [propertyValue0, propertyValue1]);
         }
     }
 #else
@@ -304,7 +306,7 @@ void Write<T0, T1, T2>(LogEventLevel level, Exception? exception, string message
         // Avoid the array allocation and any boxing allocations when the level isn't enabled
         if (IsEnabled(level))
         {
-            Write(level, exception, messageTemplate, new object?[] { propertyValue0, propertyValue1, propertyValue2 });
+            Write(level, exception, messageTemplate, [propertyValue0, propertyValue1, propertyValue2]);
         }
     }
 #else
@@ -331,7 +333,7 @@ void Write(LogEventLevel level, Exception? exception, string messageTemplate, pa
         // Catch a common pitfall when a single non-object array is cast to object[]
         if (propertyValues != null &&
             propertyValues.GetType() != typeof(object[]))
-            propertyValues = new object[] { propertyValues };
+            propertyValues = [propertyValues];
 
         var logTimestamp = DateTimeOffset.Now;
         if (BindMessageTemplate(messageTemplate, propertyValues, out var parsedTemplate, out var boundProperties))
diff --git a/src/Serilog/LoggerConfiguration.cs b/src/Serilog/LoggerConfiguration.cs
index d6e7f1d15..cef595654 100644
--- a/src/Serilog/LoggerConfiguration.cs
+++ b/src/Serilog/LoggerConfiguration.cs
@@ -19,13 +19,13 @@ namespace Serilog;
 /// </summary>
 public class LoggerConfiguration
 {
-    readonly List<ILogEventSink> _logEventSinks = new();
-    readonly List<ILogEventSink> _auditSinks = new();
-    readonly List<ILogEventEnricher> _enrichers = new();
-    readonly List<ILogEventFilter> _filters = new();
-    readonly List<Type> _additionalScalarTypes = new();
-    readonly HashSet<Type> _additionalDictionaryTypes = new();
-    readonly List<IDestructuringPolicy> _additionalDestructuringPolicies = new();
+    readonly List<ILogEventSink> _logEventSinks = [];
+    readonly List<ILogEventSink> _auditSinks = [];
+    readonly List<ILogEventEnricher> _enrichers = [];
+    readonly List<ILogEventFilter> _filters = [];
+    readonly List<Type> _additionalScalarTypes = [];
+    readonly HashSet<Type> _additionalDictionaryTypes = [];
+    readonly List<IDestructuringPolicy> _additionalDestructuringPolicies = [];
     readonly Dictionary<string, LoggingLevelSwitch> _overrides = new();
     LogEventLevel _minimumLevel = LogEventLevel.Information;
     LoggingLevelSwitch? _levelSwitch;
diff --git a/src/Serilog/Policies/SimpleScalarConversionPolicy.cs b/src/Serilog/Policies/SimpleScalarConversionPolicy.cs
index 6c030adc0..c802653f7 100644
--- a/src/Serilog/Policies/SimpleScalarConversionPolicy.cs
+++ b/src/Serilog/Policies/SimpleScalarConversionPolicy.cs
@@ -20,7 +20,7 @@ class SimpleScalarConversionPolicy : IScalarConversionPolicy
 
     public SimpleScalarConversionPolicy(IEnumerable<Type> scalarTypes)
     {
-        _scalarTypes = new(scalarTypes);
+        _scalarTypes = [..scalarTypes];
     }
 
     public bool TryConvertToScalar(object value, [NotNullWhen(true)] out ScalarValue? result)
diff --git a/src/Serilog/Serilog.csproj b/src/Serilog/Serilog.csproj
index 690d922e7..39306d9b8 100644
--- a/src/Serilog/Serilog.csproj
+++ b/src/Serilog/Serilog.csproj
@@ -57,15 +57,18 @@
 
   <ItemGroup Condition=" '$(TargetFramework)' == 'net462' ">
     <PackageReference Include="System.ValueTuple" Version="4.5.0" />
-    <PackageReference Include="System.Diagnostics.DiagnosticSource" Version="7.0.2" />
+    <PackageReference Include="System.Diagnostics.DiagnosticSource" Version="8.0.1" />
+    <PackageReference Include="System.Threading.Channels" Version="8.0.0" />
   </ItemGroup>
 
   <ItemGroup Condition=" '$(TargetFramework)' == 'net471' ">
-    <PackageReference Include="System.Diagnostics.DiagnosticSource" Version="7.0.2" />
+    <PackageReference Include="System.Diagnostics.DiagnosticSource" Version="8.0.1" />
+    <PackageReference Include="System.Threading.Channels" Version="8.0.0" />
   </ItemGroup>
 
   <ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
-    <PackageReference Include="System.Diagnostics.DiagnosticSource" Version="7.0.2" />
+    <PackageReference Include="System.Diagnostics.DiagnosticSource" Version="8.0.1" />
+    <PackageReference Include="System.Threading.Channels" Version="8.0.0" />
   </ItemGroup>
 
 </Project>
diff --git a/src/Serilog/Settings/KeyValuePairs/KeyValuePairSettings.cs b/src/Serilog/Settings/KeyValuePairs/KeyValuePairSettings.cs
index ccca2ea19..2c1bbd476 100644
--- a/src/Serilog/Settings/KeyValuePairs/KeyValuePairSettings.cs
+++ b/src/Serilog/Settings/KeyValuePairs/KeyValuePairSettings.cs
@@ -37,7 +37,7 @@ class KeyValuePairSettings : ILoggerSettings
     const string LevelSwitchNameRegex = @"^\$[A-Za-z]+[A-Za-z0-9]*$";
 
     static readonly string[] _supportedDirectives =
-    {
+    [
         UsingDirective,
         LevelSwitchDirective,
         AuditToDirective,
@@ -48,7 +48,7 @@ class KeyValuePairSettings : ILoggerSettings
         EnrichWithDirective,
         FilterDirective,
         DestructureDirective
-    };
+    ];
 
     static readonly Dictionary<string, Type> CallableDirectiveReceiverTypes = new()
     {


Test patch:
diff --git a/test/Serilog.ApprovalTests/Serilog.approved.txt b/test/Serilog.ApprovalTests/Serilog.approved.txt
index 71a596147..0613a5fa3 100644
--- a/test/Serilog.ApprovalTests/Serilog.approved.txt
+++ b/test/Serilog.ApprovalTests/Serilog.approved.txt
@@ -75,8 +75,11 @@ namespace Serilog.Configuration
         public Serilog.LoggerConfiguration Logger(Serilog.ILogger logger, bool attemptDispose = false, Serilog.Events.LogEventLevel restrictedToMinimumLevel = 0, Serilog.Core.LoggingLevelSwitch? levelSwitch = null) { }
         public Serilog.LoggerConfiguration Sink(Serilog.Core.ILogEventSink logEventSink, Serilog.Events.LogEventLevel restrictedToMinimumLevel) { }
         public Serilog.LoggerConfiguration Sink(Serilog.Core.ILogEventSink logEventSink, Serilog.Events.LogEventLevel restrictedToMinimumLevel = 0, Serilog.Core.LoggingLevelSwitch? levelSwitch = null) { }
+        public Serilog.LoggerConfiguration Sink(Serilog.Core.IBatchedLogEventSink batchedLogEventSink, Serilog.Core.Sinks.Batching.BatchingOptions batchingOptions, Serilog.Events.LogEventLevel restrictedToMinimumLevel = 0, Serilog.Core.LoggingLevelSwitch? levelSwitch = null) { }
         public Serilog.LoggerConfiguration Sink<TSink>(Serilog.Events.LogEventLevel restrictedToMinimumLevel = 0, Serilog.Core.LoggingLevelSwitch? levelSwitch = null)
             where TSink : Serilog.Core.ILogEventSink, new () { }
+        public Serilog.LoggerConfiguration Sink<TSink>(Serilog.Core.Sinks.Batching.BatchingOptions batchingOptions, Serilog.Events.LogEventLevel restrictedToMinimumLevel = 0, Serilog.Core.LoggingLevelSwitch? levelSwitch = null)
+            where TSink : Serilog.Core.IBatchedLogEventSink, new () { }
         public static Serilog.LoggerConfiguration Wrap(Serilog.Configuration.LoggerSinkConfiguration loggerSinkConfiguration, System.Func<Serilog.Core.ILogEventSink, Serilog.Core.ILogEventSink> wrapSink, System.Action<Serilog.Configuration.LoggerSinkConfiguration> configureWrappedSink, Serilog.Events.LogEventLevel restrictedToMinimumLevel = 0, Serilog.Core.LoggingLevelSwitch? levelSwitch = null) { }
     }
 }
@@ -98,6 +101,11 @@ namespace Serilog.Core
     {
         public const string SourceContextPropertyName = "SourceContext";
     }
+    public interface IBatchedLogEventSink
+    {
+        System.Threading.Tasks.Task EmitBatchAsync(System.Collections.Generic.IReadOnlyCollection<Serilog.Events.LogEvent> batch);
+        System.Threading.Tasks.Task OnEmptyBatchAsync();
+    }
     public interface IDestructuringPolicy
     {
         bool TryDestructure(object value, Serilog.Core.ILogEventPropertyValueFactory propertyValueFactory, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out Serilog.Events.LogEventPropertyValue? result);
@@ -305,6 +313,17 @@ namespace Serilog.Core.Enrichers
         public void Enrich(Serilog.Events.LogEvent logEvent, Serilog.Core.ILogEventPropertyFactory propertyFactory) { }
     }
 }
+namespace Serilog.Core.Sinks.Batching
+{
+    public class BatchingOptions
+    {
+        public BatchingOptions() { }
+        public int BatchSizeLimit { get; set; }
+        public System.TimeSpan BufferingTimeLimit { get; set; }
+        public bool EagerlyEmitFirstEvent { get; set; }
+        public int? QueueLimit { get; set; }
+    }
+}
 namespace Serilog.Data
 {
     public abstract class LogEventPropertyValueRewriter<TState> : Serilog.Data.LogEventPropertyValueVisitor<TState, Serilog.Events.LogEventPropertyValue>
diff --git a/test/Serilog.PerformanceTests/BindingBenchmark.cs b/test/Serilog.PerformanceTests/BindingBenchmark.cs
index a5d1844f0..84fce7a93 100644
--- a/test/Serilog.PerformanceTests/BindingBenchmark.cs
+++ b/test/Serilog.PerformanceTests/BindingBenchmark.cs
@@ -36,8 +36,8 @@ public void Setup()
             .CreateLogger();
 
         _zero = Array.Empty<object>();
-        _one = new object[] { 1 };
-        _five = new object[] { 1, 2, 3, 4, 5 };
+        _one = [1];
+        _five = [1, 2, 3, 4, 5];
     }
 
     // The benchmarks run p.Count() to force enumeration; this will be representative of how the API
diff --git a/test/Serilog.PerformanceTests/SourceContextMatchBenchmark.cs b/test/Serilog.PerformanceTests/SourceContextMatchBenchmark.cs
index ffdc8ad7b..d02d701cf 100644
--- a/test/Serilog.PerformanceTests/SourceContextMatchBenchmark.cs
+++ b/test/Serilog.PerformanceTests/SourceContextMatchBenchmark.cs
@@ -6,14 +6,14 @@ public class SourceContextMatchBenchmark
 {
     readonly LevelOverrideMap _levelOverrideMap;
     readonly Logger _loggerWithOverrides;
-    readonly List<ILogger> _loggersWithFilters = new();
+    readonly List<ILogger> _loggersWithFilters = [];
     readonly LogEvent _event = Some.InformationEvent();
     readonly string[] _contexts;
 
     public SourceContextMatchBenchmark()
     {
-        _contexts = new[]
-        {
+        _contexts =
+        [
             "Serilog",
             "MyApp",
             "MyAppSomething",
@@ -23,7 +23,7 @@ public SourceContextMatchBenchmark()
             "MyApp.Api.Controllers.AboutController",
             "MyApp.Api.Controllers.HomeController",
             "Api.Controllers.HomeController"
-        };
+        ];
 
         var overrides = new Dictionary<string, LoggingLevelSwitch>
         {
diff --git a/test/Serilog.Tests/Core/BatchingSinkTests.cs b/test/Serilog.Tests/Core/BatchingSinkTests.cs
new file mode 100644
index 000000000..5fd8f61ac
--- /dev/null
+++ b/test/Serilog.Tests/Core/BatchingSinkTests.cs
@@ -0,0 +1,140 @@
+using Serilog.Core.Sinks.Batching;
+
+namespace Serilog.Core.Tests;
+
+public class BatchingSinkTests
+{
+    static readonly TimeSpan TinyWait = TimeSpan.FromMilliseconds(200);
+    static readonly TimeSpan MicroWait = TimeSpan.FromMilliseconds(1);
+
+    [Fact]
+    public void WhenAnEventIsEnqueuedItIsWrittenToABatchOnDispose()
+    {
+        var bs = new InMemoryBatchedSink(TimeSpan.Zero);
+        var pbs = new BatchingSink(bs, new() { BatchSizeLimit = 2, BufferingTimeLimit = TinyWait, EagerlyEmitFirstEvent = true });
+        var evt = Some.InformationEvent();
+        pbs.Emit(evt);
+        pbs.Dispose();
+        Assert.Single(bs.Batches);
+        Assert.Single(bs.Batches[0]);
+        Assert.Same(evt, bs.Batches[0][0]);
+        Assert.True(bs.IsDisposed);
+        Assert.False(bs.WasCalledAfterDisposal);
+    }
+
+#if FEATURE_ASYNCDISPOSABLE
+        [Fact]
+        public async Task WhenAnEventIsEnqueuedItIsWrittenToABatchOnDisposeAsync()
+        {
+            var bs = new InMemoryBatchedSink(TimeSpan.Zero);
+            var pbs = new BatchingSink(
+                bs, new()
+                {
+                    BatchSizeLimit = 2, BufferingTimeLimit = TinyWait, EagerlyEmitFirstEvent = true
+                });
+            var evt = Some.InformationEvent();
+            pbs.Emit(evt);
+            await pbs.DisposeAsync();
+            Assert.Single(bs.Batches);
+            Assert.Single(bs.Batches[0]);
+            Assert.Same(evt, bs.Batches[0][0]);
+            Assert.True(bs.IsDisposed);
+            Assert.True(bs.IsDisposedAsync);
+            Assert.False(bs.WasCalledAfterDisposal);
+        }
+#endif
+
+    [Fact]
+    public void WhenAnEventIsEnqueuedItIsWrittenToABatchOnTimer()
+    {
+        var bs = new InMemoryBatchedSink(TimeSpan.Zero);
+        var pbs = new BatchingSink(
+            bs,
+            new()
+            {
+                BatchSizeLimit = 2, BufferingTimeLimit = TinyWait, EagerlyEmitFirstEvent = true
+            });
+        var evt = Some.InformationEvent();
+        pbs.Emit(evt);
+        Thread.Sleep(TinyWait + TinyWait);
+        bs.Stop();
+        pbs.Dispose();
+        Assert.Single(bs.Batches);
+        Assert.True(bs.IsDisposed);
+        Assert.False(bs.WasCalledAfterDisposal);
+    }
+
+    [Fact]
+    public void WhenAnEventIsEnqueuedItIsWrittenToABatchOnDisposeWhileRunning()
+    {
+        var bs = new InMemoryBatchedSink(TinyWait + TinyWait);
+        var pbs = new BatchingSink(bs, new() { BatchSizeLimit = 2, BufferingTimeLimit = MicroWait, EagerlyEmitFirstEvent = true });
+        var evt = Some.InformationEvent();
+        pbs.Emit(evt);
+        Thread.Sleep(TinyWait);
+        pbs.Dispose();
+        Assert.Single(bs.Batches);
+        Assert.True(bs.IsDisposed);
+        Assert.False(bs.WasCalledAfterDisposal);
+    }
+
+    [Fact]
+    public void ExecutionContextDoesNotFlowToBatchedSink()
+    {
+        var local = new AsyncLocal<int>
+        {
+            Value = 5
+        };
+
+        var observed = 17;
+        var bs = new CallbackBatchedSink(_ =>
+        {
+            observed = local.Value;
+            return Task.CompletedTask;
+        });
+
+        var pbs = new BatchingSink(bs, new());
+        var evt = Some.InformationEvent();
+        pbs.Emit(evt);
+        pbs.Dispose();
+
+        Assert.Equal(default, observed);
+    }
+
+    [Theory]
+    [InlineData(true)]
+    [InlineData(false)]
+    public async Task EagerlyEmitFirstEventCausesQuickInitialBatch(bool eagerlyEmit)
+    {
+        var x = new ManualResetEvent(false);
+        var bs = new CallbackBatchedSink(_ =>
+        {
+            x.Set();
+            return Task.CompletedTask;
+        });
+
+        var options = new BatchingOptions
+        {
+            BufferingTimeLimit = TimeSpan.FromSeconds(3),
+            EagerlyEmitFirstEvent = eagerlyEmit,
+            BatchSizeLimit = 10,
+            QueueLimit = 1000
+        };
+
+        var pbs = new BatchingSink(bs, options);
+
+        var evt = Some.InformationEvent();
+        pbs.Emit(evt);
+
+        await Task.Delay(1900);
+        Assert.Equal(eagerlyEmit, x.WaitOne(0));
+
+#if FEATURE_ASYNCDISPOSABLE
+        await pbs.DisposeAsync();
+#else
+        pbs.Dispose();
+#endif
+
+        Assert.True(x.WaitOne(0));
+    }
+}
diff --git a/test/Serilog.Tests/Core/ChildLoggerKnownLimitationsTests.cs b/test/Serilog.Tests/Core/ChildLoggerKnownLimitationsTests.cs
index d1e6de96f..11e3b3f26 100644
--- a/test/Serilog.Tests/Core/ChildLoggerKnownLimitationsTests.cs
+++ b/test/Serilog.Tests/Core/ChildLoggerKnownLimitationsTests.cs
@@ -66,7 +66,7 @@ public void SpecifyingMinimumLevelOverridesInWriteToLoggerWritesWarningToSelfLog
         //
         static object?[] T(string? rs, int? rl, string cs, int? cl)
         {
-            return new object?[] { rs, rl, cs, cl };
+            return [rs, rl, cs, cl];
         }
         // numbers are relative to incoming event level
         // Information + 1 = Warning
diff --git a/test/Serilog.Tests/Core/ChildLoggerTests.cs b/test/Serilog.Tests/Core/ChildLoggerTests.cs
index 10dccf2db..0defe52c5 100644
--- a/test/Serilog.Tests/Core/ChildLoggerTests.cs
+++ b/test/Serilog.Tests/Core/ChildLoggerTests.cs
@@ -11,7 +11,7 @@ public class ChildLoggerTests
         //
         static object?[] T(LogEventLevel el, int? rl, int? rt, int? cl, bool r)
         {
-            return new object?[] { el, rl, rt, cl, r };
+            return [el, rl, rt, cl, r];
         }
         // numbers are relative to incoming event level
         // Information + 1 = Warning
@@ -144,7 +144,7 @@ public void WriteToLoggerMinimumLevelInheritanceScenarios(
         //
         static object?[] T(string? rs, int? rl, string? cs, int? cl, bool r, LogEventLevel dl = LevelAlias.Minimum)
         {
-            return new object?[] { dl, rs, rl, cs, cl, r };
+            return [dl, rs, rl, cs, cl, r];
         }
         // numbers are relative to incoming event level
         // Information + 1 = Warning
diff --git a/test/Serilog.Tests/Core/FailureAwareBatchSchedulerTests.cs b/test/Serilog.Tests/Core/FailureAwareBatchSchedulerTests.cs
new file mode 100644
index 000000000..1f5dbe2ee
--- /dev/null
+++ b/test/Serilog.Tests/Core/FailureAwareBatchSchedulerTests.cs
@@ -0,0 +1,104 @@
+using Serilog.Core.Sinks.Batching;
+
+namespace Serilog.Core.Tests;
+
+public class FailureAwareBatchSchedulerTests
+{
+    static TimeSpan Period => TimeSpan.FromSeconds(2);
+    FailureAwareBatchScheduler Scheduler { get; } = new(Period);
+
+    [Fact]
+    public void WhenNoFailuresHaveOccurredTheInitialIntervalIsUsed()
+    {
+        Assert.Equal(Period, Scheduler.NextInterval);
+    }
+
+    [Fact]
+    public void WhenOneFailureHasOccurredTheInitialIntervalIsUsed()
+    {
+        Scheduler.MarkFailure();
+        Assert.Equal(Period, Scheduler.NextInterval);
+    }
+
+    [Fact]
+    public void WhenTwoFailuresHaveOccurredTheIntervalBacksOff()
+    {
+        Scheduler.MarkFailure();
+        Scheduler.MarkFailure();
+        Assert.Equal(TimeSpan.FromSeconds(10), Scheduler.NextInterval);
+    }
+
+    [Fact]
+    public void WhenABatchSucceedsTheStatusResets()
+    {
+        Scheduler.MarkFailure();
+        Scheduler.MarkFailure();
+        Scheduler.MarkSuccess();
+        Assert.Equal(Period, Scheduler.NextInterval);
+    }
+
+    [Fact]
+    public void WhenThreeFailuresHaveOccurredTheIntervalBacksOff()
+    {
+        Scheduler.MarkFailure();
+        Scheduler.MarkFailure();
+        Scheduler.MarkFailure();
+        Assert.Equal(TimeSpan.FromSeconds(20), Scheduler.NextInterval);
+        Assert.False(Scheduler.ShouldDropBatch);
+    }
+
+    [Fact]
+    public void WhenEightFailuresHaveOccurredTheIntervalBacksOffAndBatchIsDropped()
+    {
+        for (var i = 0; i < 8; ++i)
+        {
+            Assert.False(Scheduler.ShouldDropBatch);
+            Scheduler.MarkFailure();
+        }
+        Assert.Equal(TimeSpan.FromMinutes(10), Scheduler.NextInterval);
+        Assert.True(Scheduler.ShouldDropBatch);
+        Assert.False(Scheduler.ShouldDropQueue);
+    }
+
+    [Fact]
+    public void WhenTenFailuresHaveOccurredTheQueueIsDropped()
+    {
+        for (var i = 0; i < 10; ++i)
+        {
+            Assert.False(Scheduler.ShouldDropQueue);
+            Scheduler.MarkFailure();
+        }
+        Assert.True(Scheduler.ShouldDropQueue);
+    }
+
+    [Fact]
+    public void AtTheDefaultIntervalRetriesForTenMinutesBeforeDroppingBatch()
+    {
+        var cumulative = TimeSpan.Zero;
+        do
+        {
+            Scheduler.MarkFailure();
+
+            if (!Scheduler.ShouldDropBatch)
+                cumulative += Scheduler.NextInterval;
+        } while (!Scheduler.ShouldDropBatch);
+
+        Assert.False(Scheduler.ShouldDropQueue);
+        Assert.Equal(TimeSpan.Parse("00:10:32", CultureInfo.InvariantCulture), cumulative);
+    }
+
+    [Fact]
+    public void AtTheDefaultIntervalRetriesForThirtyMinutesBeforeDroppingQueue()
+    {
+        var cumulative = TimeSpan.Zero;
+        do
+        {
+            Scheduler.MarkFailure();
+
+            if (!Scheduler.ShouldDropQueue)
+                cumulative += Scheduler.NextInterval;
+        } while (!Scheduler.ShouldDropQueue);
+
+        Assert.Equal(TimeSpan.Parse("00:30:32", CultureInfo.InvariantCulture), cumulative);
+    }
+}
diff --git a/test/Serilog.Tests/Core/LogEventPropertyCapturingTests.cs b/test/Serilog.Tests/Core/LogEventPropertyCapturingTests.cs
index 43a67e4ac..d6f87579e 100644
--- a/test/Serilog.Tests/Core/LogEventPropertyCapturingTests.cs
+++ b/test/Serilog.Tests/Core/LogEventPropertyCapturingTests.cs
@@ -86,8 +86,8 @@ public void WillCaptureSequenceOfScalarsWhenAdditionalNamedPropsNotInTemplateAre
             {
                 new LogEventProperty("who", new ScalarValue("who")),
                 new LogEventProperty("what", new ScalarValue("what")),
-                new LogEventProperty("__2", new SequenceValue(new[] { new ScalarValue("__2") })),
-                new LogEventProperty("__3", new SequenceValue(new[] { new ScalarValue("__3") })),
+                new LogEventProperty("__2", new SequenceValue([new ScalarValue("__2")])),
+                new LogEventProperty("__3", new SequenceValue([new ScalarValue("__3")])),
             },
             Capture("Hello {who} {what} where}", "who", "what", new[] { "__2" }, new[] { "__3" }),
             new LogEventPropertyStructuralEqualityComparer());
@@ -114,10 +114,11 @@ public void CapturingIntArrayAsScalarSequenceValuesWorks()
         var templateArguments = new[] { 1, 2, 3, };
         var expected = new[] {
             new LogEventProperty("sequence",
-                new SequenceValue(new[] {
+                new SequenceValue([
                     new ScalarValue(1),
                     new ScalarValue(2),
-                    new ScalarValue(3) })) };
+                    new ScalarValue(3)
+                ])) };
 
         Assert.Equal(expected, Capture(template, templateArguments),
             new LogEventPropertyStructuralEqualityComparer());
@@ -130,12 +131,13 @@ public void CapturingDestructuredStringArrayAsScalarSequenceValuesWorks()
         var templateArguments = new[] { "1", "2", "3", };
         var expected = new[] {
             new LogEventProperty("sequence",
-                new SequenceValue(new[] {
+                new SequenceValue([
                     new ScalarValue("1"),
                     new ScalarValue("2"),
-                    new ScalarValue("3") })) };
+                    new ScalarValue("3")
+                ])) };
 
-        Assert.Equal(expected, Capture(template, new object[] { templateArguments }),
+        Assert.Equal(expected, Capture(template, [templateArguments]),
             new LogEventPropertyStructuralEqualityComparer());
     }
 
diff --git a/test/Serilog.Tests/Core/LoggerTests.cs b/test/Serilog.Tests/Core/LoggerTests.cs
index f469021cb..5307b4cc5 100644
--- a/test/Serilog.Tests/Core/LoggerTests.cs
+++ b/test/Serilog.Tests/Core/LoggerTests.cs
@@ -112,7 +112,7 @@ public void MessageTemplatesCanBeBound(Type loggerType)
         var log = CreateLogger(loggerType, lc => lc);
 
         // ReSharper disable StructuredMessageTemplateProblem
-        Assert.True(log.BindMessageTemplate("Hello, {Name}!", new object[] { "World" }, out var template, out var properties));
+        Assert.True(log.BindMessageTemplate("Hello, {Name}!", ["World"], out var template, out var properties));
         // ReSharper restore StructuredMessageTemplateProblem
 
         Assert.Equal("Hello, {Name}!", template.Text);
@@ -203,7 +203,7 @@ public void DelegatingLoggerShouldDelegateCallsToInnerLogger()
         Assert.Equal(42, collectingSink.SingleEvent.Properties["number"].LiteralValue());
         Assert.Equal(
             // ReSharper disable once CoVariantArrayConversion
-            expected: new SequenceValue(new[] { new ScalarValue(1), new ScalarValue(2), new ScalarValue(3) }),
+            expected: new SequenceValue([new ScalarValue(1), new ScalarValue(2), new ScalarValue(3)]),
             actual: (SequenceValue)collectingSink.SingleEvent.Properties["values"],
             comparer: new LogEventPropertyValueComparer());
 
diff --git a/test/Serilog.Tests/Core/MessageTemplateTests.cs b/test/Serilog.Tests/Core/MessageTemplateTests.cs
index 148a4536a..9d289f630 100755
--- a/test/Serilog.Tests/Core/MessageTemplateTests.cs
+++ b/test/Serilog.Tests/Core/MessageTemplateTests.cs
@@ -8,7 +8,7 @@ class Chair
     {
         // ReSharper disable UnusedMember.Local
         public string Back => "straight";
-        public int[] Legs => new[] { 1, 2, 3, 4 };
+        public int[] Legs => [1, 2, 3, 4];
         // ReSharper restore UnusedMember.Local
         public override string ToString() => "a chair";
     }
diff --git a/test/Serilog.Tests/Data/LogEventPropertyValueRewriterTests.cs b/test/Serilog.Tests/Data/LogEventPropertyValueRewriterTests.cs
index 97e0a4f7d..bed2a3741 100644
--- a/test/Serilog.Tests/Data/LogEventPropertyValueRewriterTests.cs
+++ b/test/Serilog.Tests/Data/LogEventPropertyValueRewriterTests.cs
@@ -21,13 +21,12 @@ public class LogEventPropertyValueRewriterTests
     [Fact]
     public void StatePropagatesAndNestedStructuresAreRewritten()
     {
-        var value = new SequenceValue(new[]
-        {
+        var value = new SequenceValue([
             new StructureValue(new[]
             {
                 new LogEventProperty("S", new ScalarValue("abcde"))
             })
-        });
+        ]);
 
         var limiter = new LimitingRewriter();
         var limited = limiter.LimitStringLength(value, 3);
@@ -50,13 +49,12 @@ public void StatePropagatesAndNestedStructuresAreRewritten()
     [Fact]
     public void WhenNoRewritingTakesPlaceAllElementsAreUnchanged()
     {
-        var value = new SequenceValue(new[]
-        {
+        var value = new SequenceValue([
             new StructureValue(new[]
             {
                 new LogEventProperty("S", new ScalarValue("abcde"))
             })
-        });
+        ]);
         var limiter = new LimitingRewriter();
         var unchanged = limiter.LimitStringLength(value, 10);
         Assert.Same(value, unchanged);
diff --git a/test/Serilog.Tests/Debugging/SelfLogTests.cs b/test/Serilog.Tests/Debugging/SelfLogTests.cs
index 5c6efbf0b..2460c04d3 100644
--- a/test/Serilog.Tests/Debugging/SelfLogTests.cs
+++ b/test/Serilog.Tests/Debugging/SelfLogTests.cs
@@ -8,7 +8,7 @@ public class SelfLogTests
     [Fact]
     public void MessagesAreWrittenWhenOutputIsSet()
     {
-        Messages = new();
+        Messages = [];
         SelfLog.Enable(m =>
         {
             Messages.Add(m);
@@ -28,7 +28,7 @@ public void MessagesAreWrittenWhenOutputIsSet()
     [Fact]
     public void SelfLogReportsErrorWhenPositionalParameterCountIsMismatched()
     {
-        Messages = new();
+        Messages = [];
         SelfLog.Enable(m =>
         {
             Messages.Add(m);
@@ -49,7 +49,7 @@ public void SelfLogReportsErrorWhenPositionalParameterCountIsMismatched()
     [Fact]
     public void SelfLogDoesNotReportErrorWhenPositionalParameterIsRepeated()
     {
-        Messages = new();
+        Messages = [];
         SelfLog.Enable(m =>
         {
             Messages.Add(m);
@@ -69,7 +69,7 @@ public void SelfLogDoesNotReportErrorWhenPositionalParameterIsRepeated()
     [Fact]
     public void WritingToUndeclaredSinkWritesToSelfLog()
     {
-        Messages = new();
+        Messages = [];
         SelfLog.Enable(m =>
         {
             Messages.Add(m);
diff --git a/test/Serilog.Tests/Events/DictionaryValueTests.cs b/test/Serilog.Tests/Events/DictionaryValueTests.cs
index 7cb012378..b13432637 100644
--- a/test/Serilog.Tests/Events/DictionaryValueTests.cs
+++ b/test/Serilog.Tests/Events/DictionaryValueTests.cs
@@ -9,7 +9,7 @@ public void ADictionaryValueRendersAsMappingOfKeysToValues()
             new KeyValuePair<ScalarValue, LogEventPropertyValue>(
                 new ScalarValue(1), new ScalarValue("hello")),
             new KeyValuePair<ScalarValue, LogEventPropertyValue>(
-                new ScalarValue("world"), new SequenceValue(new [] { new ScalarValue(1.2) }))
+                new ScalarValue("world"), new SequenceValue([new ScalarValue(1.2)]))
         });
 
         var sw = new StringWriter();
diff --git a/test/Serilog.Tests/Formatting/Json/JsonFormatterTests.cs b/test/Serilog.Tests/Formatting/Json/JsonFormatterTests.cs
index f0afd5f5b..2db48757e 100644
--- a/test/Serilog.Tests/Formatting/Json/JsonFormatterTests.cs
+++ b/test/Serilog.Tests/Formatting/Json/JsonFormatterTests.cs
@@ -268,7 +268,7 @@ public void SequencesOfSequencesAreSerialized()
     {
         var p = new MessageTemplateParser();
         var e = new LogEvent(Some.OffsetInstant(), Information, null,
-            p.Parse("{@AProperty}"), new[] { new LogEventProperty("AProperty", new SequenceValue(new[] { new SequenceValue(new[] { new ScalarValue("Hello") }) })) });
+            p.Parse("{@AProperty}"), new[] { new LogEventProperty("AProperty", new SequenceValue([new SequenceValue([new ScalarValue("Hello")])])) });
 
         var d = FormatEvent(e);
 
diff --git a/test/Serilog.Tests/Formatting/Json/JsonValueFormatterTests.cs b/test/Serilog.Tests/Formatting/Json/JsonValueFormatterTests.cs
index a4b5329c8..89a38ba1f 100644
--- a/test/Serilog.Tests/Formatting/Json/JsonValueFormatterTests.cs
+++ b/test/Serilog.Tests/Formatting/Json/JsonValueFormatterTests.cs
@@ -157,7 +157,7 @@ public void ScalarPropertiesFormatAsLiteralValues()
     [Fact]
     public void SequencePropertiesFormatAsArrayValue()
     {
-        var f = Format(new SequenceValue(new[] { new ScalarValue(123), new ScalarValue(456) }));
+        var f = Format(new SequenceValue([new ScalarValue(123), new ScalarValue(456)]));
         Assert.Equal("[123,456]", f);
     }
 
@@ -183,7 +183,7 @@ public void DictionaryWithScalarKeyFormatsAsAnObject()
     [Fact]
     public void SequencesOfSequencesAreFormatted()
     {
-        var s = new SequenceValue(new[] { new SequenceValue(new[] { new ScalarValue("Hello") }) });
+        var s = new SequenceValue([new SequenceValue([new ScalarValue("Hello")])]);
 
         var f = Format(s);
         Assert.Equal("[[\"Hello\"]]", f);
diff --git a/test/Serilog.Tests/MethodOverloadConventionTests.cs b/test/Serilog.Tests/MethodOverloadConventionTests.cs
index 852bbf122..425407881 100644
--- a/test/Serilog.Tests/MethodOverloadConventionTests.cs
+++ b/test/Serilog.Tests/MethodOverloadConventionTests.cs
@@ -247,7 +247,7 @@ public void ValidateWriteEventLogMethods(Type loggerType)
 
         var logger = GetLogger(loggerType, out var sink);
 
-        InvokeMethod(writeMethod, logger, new object[] { Some.LogEvent(DateTimeOffset.Now, level) });
+        InvokeMethod(writeMethod, logger, [Some.LogEvent(DateTimeOffset.Now, level)]);
 
         //handle silent logger special case i.e. no result validation
         if (loggerType == typeof(SilentLogger))
@@ -283,7 +283,7 @@ public void ValidateForContextMethods(Type loggerType)
             {
                 try
                 {
-                    testMethod.Invoke(this, new object[] { method });
+                    testMethod.Invoke(this, [method]);
 
                     signatureMatchAndInvokeSuccess = true;
 
@@ -365,7 +365,7 @@ public void ValidateBindMessageTemplateMethods(Type loggerType)
         Assert.True(result as bool?);
 
         //test null arg path
-        var falseResult = InvokeMethod(method, logger, new object?[] { null, null, null, null });
+        var falseResult = InvokeMethod(method, logger, [null, null, null, null]);
 
         Assert.IsType<bool>(falseResult);
         Assert.False(falseResult as bool?);
@@ -421,7 +421,7 @@ public void ValidateBindPropertyMethods(Type loggerType)
         Assert.True(result as bool?);
 
         //test null arg path/ invalid property name
-        var falseResult = InvokeMethod(method, logger, new object?[] { " ", null, false, null });
+        var falseResult = InvokeMethod(method, logger, [" ", null, false, null]);
 
         Assert.IsType<bool>(falseResult);
         Assert.False(falseResult as bool?);
@@ -451,12 +451,12 @@ public void ValidateIsEnabledMethods(Type loggerType)
 
         var logger = GetLogger(loggerType, out _, Information);
 
-        var falseResult = InvokeMethod(method, logger, new object[] { Verbose });
+        var falseResult = InvokeMethod(method, logger, [Verbose]);
 
         Assert.IsType<bool>(falseResult);
         Assert.False(falseResult as bool?);
 
-        var trueResult = InvokeMethod(method, logger, new object[] { Warning });
+        var trueResult = InvokeMethod(method, logger, [Warning]);
 
         Assert.IsType<bool>(trueResult);
 
@@ -492,7 +492,7 @@ void ForContextMethod0(MethodInfo method)
 
         var logEnricher = new DummyThreadIdEnricher();
 
-        var enrichedLogger = InvokeMethod(method, logger, new object[] { logEnricher });
+        var enrichedLogger = InvokeMethod(method, logger, [logEnricher]);
 
         TestForContextResult(method, logger, normalResult: enrichedLogger);
     }
@@ -525,7 +525,7 @@ void ForContextMethod1(MethodInfo method)
         var logEnricher = new DummyThreadIdEnricher();
 
         var enrichedLogger = InvokeMethod(method, logger,
-            new object[] { new ILogEventEnricher[] { logEnricher, logEnricher } });
+            [new ILogEventEnricher[] { logEnricher, logEnricher }]);
 
         TestForContextResult(method, logger, normalResult: enrichedLogger);
     }
@@ -564,7 +564,7 @@ void ForContextMethod2(MethodInfo method)
         var propertyName = "SomeString";
         var propertyValue = "someString";
 
-        var enrichedLogger = InvokeMethod(method, logger, new object[] { propertyName, propertyValue, false });
+        var enrichedLogger = InvokeMethod(method, logger, [propertyName, propertyValue, false]);
 
         Assert.NotNull(enrichedLogger);
         Assert.True(enrichedLogger is ILogger);
@@ -576,7 +576,7 @@ void ForContextMethod2(MethodInfo method)
         Assert.NotSame(logger, enrichedLogger);
 
         //invalid args path
-        var sameLogger = InvokeMethod(method, logger, new object?[] { null, null, false });
+        var sameLogger = InvokeMethod(method, logger, [null, null, false]);
 
         Assert.NotNull(sameLogger);
         Assert.True(sameLogger is ILogger);
@@ -611,7 +611,7 @@ void ForContextMethod3(MethodInfo method)
 
         var logger = GetLogger(method.DeclaringType!);
 
-        var enrichedLogger = InvokeMethod(method, logger, null, new[] { typeof(object) });
+        var enrichedLogger = InvokeMethod(method, logger, null, [typeof(object)]);
 
         Assert.NotNull(enrichedLogger);
         Assert.True(enrichedLogger is ILogger);
@@ -640,7 +640,7 @@ void ForContextMethod4(MethodInfo method)
 
         var logger = GetLogger(method.DeclaringType!);
 
-        var enrichedLogger = InvokeMethod(method, logger, new object[] { typeof(object) });
+        var enrichedLogger = InvokeMethod(method, logger, [typeof(object)]);
 
         TestForContextResult(method, logger, normalResult: enrichedLogger);
     }
@@ -657,7 +657,7 @@ static void TestForContextResult(MethodInfo method, ILogger? logger, object norm
         Assert.NotSame(logger, normalResult);
 
         //if invoked with null args it should return the same instance
-        var sameLogger = InvokeMethod(method, logger, new object?[] { null });
+        var sameLogger = InvokeMethod(method, logger, [null]);
 
         Assert.NotNull(sameLogger);
 
@@ -716,7 +716,7 @@ void ValidateConventionForMethodSet(
                     else
                         invokeTestMethod = InvokeConventionMethod;
 
-                    testMethod.Invoke(this, new object[] { method, invokeTestMethod });
+                    testMethod.Invoke(this, [method, invokeTestMethod]);
 
                     signatureMatchAndInvokeSuccess = true;
 
diff --git a/test/Serilog.Tests/Support/CallbackBatchedSink.cs b/test/Serilog.Tests/Support/CallbackBatchedSink.cs
new file mode 100644
index 000000000..626ddd1ca
--- /dev/null
+++ b/test/Serilog.Tests/Support/CallbackBatchedSink.cs
@@ -0,0 +1,14 @@
+namespace Serilog.Tests.Support;
+
+class CallbackBatchedSink(Func<IEnumerable<LogEvent>, Task> callback) : IBatchedLogEventSink
+{
+    public Task EmitBatchAsync(IReadOnlyCollection<LogEvent> batch)
+    {
+        return callback(batch);
+    }
+
+    public Task OnEmptyBatchAsync()
+    {
+        return Task.CompletedTask;
+    }
+}
diff --git a/test/Serilog.Tests/Support/CollectingEnricher.cs b/test/Serilog.Tests/Support/CollectingEnricher.cs
index 4782ae940..1bfdb4b59 100644
--- a/test/Serilog.Tests/Support/CollectingEnricher.cs
+++ b/test/Serilog.Tests/Support/CollectingEnricher.cs
@@ -2,7 +2,7 @@ namespace Serilog.Tests.Support;
 
 class CollectingEnricher : ILogEventEnricher
 {
-    public List<LogEvent> Events { get; } = new();
+    public List<LogEvent> Events { get; } = [];
 
     public LogEvent SingleEvent => Events.Single();
 
diff --git a/test/Serilog.Tests/Support/CollectingSink.cs b/test/Serilog.Tests/Support/CollectingSink.cs
index 2fd4b59f2..2cf41b8b7 100644
--- a/test/Serilog.Tests/Support/CollectingSink.cs
+++ b/test/Serilog.Tests/Support/CollectingSink.cs
@@ -2,7 +2,7 @@ namespace Serilog.Tests.Support;
 
 class CollectingSink : ILogEventSink
 {
-    public List<LogEvent> Events { get; } = new();
+    public List<LogEvent> Events { get; } = [];
 
     public LogEvent SingleEvent => Events.Single();
 
diff --git a/test/Serilog.Tests/Support/InMemoryBatchedSink.cs b/test/Serilog.Tests/Support/InMemoryBatchedSink.cs
new file mode 100644
index 000000000..0ae904f50
--- /dev/null
+++ b/test/Serilog.Tests/Support/InMemoryBatchedSink.cs
@@ -0,0 +1,62 @@
+namespace Serilog.Tests.Support;
+
+sealed class InMemoryBatchedSink(TimeSpan batchEmitDelay) : IBatchedLogEventSink, IDisposable
+#if FEATURE_ASYNCDISPOSABLE
+    , IAsyncDisposable
+#endif
+{
+    readonly object _stateLock = new();
+    bool _stopped;
+
+    // Postmortem only
+    public bool WasCalledAfterDisposal { get; private set; }
+    public IList<IList<LogEvent>> Batches { get; } = new List<IList<LogEvent>>();
+    public bool IsDisposed { get; private set; }
+
+    public void Stop()
+    {
+        lock (_stateLock)
+        {
+            _stopped = true;
+        }
+    }
+
+    public Task EmitBatchAsync(IReadOnlyCollection<LogEvent> events)
+    {
+        lock (_stateLock)
+        {
+            if (_stopped)
+                return Task.CompletedTask;
+
+            if (IsDisposed)
+                WasCalledAfterDisposal = true;
+
+            Thread.Sleep(batchEmitDelay);
+            Batches.Add(events.ToList());
+        }
+
+        return Task.CompletedTask;
+    }
+
+    public Task OnEmptyBatchAsync() => Task.CompletedTask;
+
+    public void Dispose()
+    {
+        lock (_stateLock)
+            IsDisposed = true;
+    }
+
+#if FEATURE_ASYNCDISPOSABLE
+    public bool IsDisposedAsync { get; private set; }
+
+    public ValueTask DisposeAsync()
+    {
+        lock (_stateLock)
+        {
+            IsDisposedAsync = true;
+            Dispose();
+            return default;
+        }
+    }
+#endif
+}
diff --git a/test/Serilog.Tests/Support/LogEventPropertyStructuralEqualityComparerTests.cs b/test/Serilog.Tests/Support/LogEventPropertyStructuralEqualityComparerTests.cs
index b84b0e901..0e03c6fbc 100644
--- a/test/Serilog.Tests/Support/LogEventPropertyStructuralEqualityComparerTests.cs
+++ b/test/Serilog.Tests/Support/LogEventPropertyStructuralEqualityComparerTests.cs
@@ -23,10 +23,10 @@ public void LogEventPropertyStructuralEqualityComparerWorksForSequences()
         var sequenceOfScalarsIntStringDoubleA = new LogEventProperty("a", new SequenceValue(intStringDoubleScalarArray));
 
         var sequenceOfScalarsIntStringDoubleAStructurallyEqual = new LogEventProperty("a",
-            new SequenceValue(new[] { new ScalarValue(1), new ScalarValue("2"), new ScalarValue(3.0), }));
+            new SequenceValue([new ScalarValue(1), new ScalarValue("2"), new ScalarValue(3.0)]));
 
         var sequenceOfScalarsIntStringDoubleAStructurallyNotEqual = new LogEventProperty("a",
-            new SequenceValue(new[] { new ScalarValue(1), new ScalarValue("2"), new ScalarValue(3.1), }));
+            new SequenceValue([new ScalarValue(1), new ScalarValue("2"), new ScalarValue(3.1)]));
 
         var sequenceOfScalarsIntStringFloatA = new LogEventProperty("a", new ScalarValue(intStringFloatScalarArray));
 
diff --git a/test/TestDummies/DummyRollingFileAuditSink.cs b/test/TestDummies/DummyRollingFileAuditSink.cs
index f9365e016..86a73b012 100644
--- a/test/TestDummies/DummyRollingFileAuditSink.cs
+++ b/test/TestDummies/DummyRollingFileAuditSink.cs
@@ -5,7 +5,7 @@ public class DummyRollingFileAuditSink : ILogEventSink
     [ThreadStatic]
     static List<LogEvent>? _emitted;
 
-    public static List<LogEvent> Emitted => _emitted ??= new();
+    public static List<LogEvent> Emitted => _emitted ??= [];
 
     public void Emit(LogEvent logEvent)
     {
diff --git a/test/TestDummies/DummyRollingFileSink.cs b/test/TestDummies/DummyRollingFileSink.cs
index f822e9bcd..372b688a6 100644
--- a/test/TestDummies/DummyRollingFileSink.cs
+++ b/test/TestDummies/DummyRollingFileSink.cs
@@ -5,7 +5,7 @@ public class DummyRollingFileSink : ILogEventSink
     [ThreadStatic]
     static List<LogEvent>? _emitted;
 
-    public static List<LogEvent> Emitted => _emitted ??= new();
+    public static List<LogEvent> Emitted => _emitted ??= [];
 
     public void Emit(LogEvent logEvent)
     {
diff --git a/test/TestDummies/DummyWithLevelSwitchSink.cs b/test/TestDummies/DummyWithLevelSwitchSink.cs
index c8d19c9b2..4ffbc9f7a 100644
--- a/test/TestDummies/DummyWithLevelSwitchSink.cs
+++ b/test/TestDummies/DummyWithLevelSwitchSink.cs
@@ -12,7 +12,7 @@ public DummyWithLevelSwitchSink(LoggingLevelSwitch? loggingControlLevelSwitch)
 
     [ThreadStatic]
     // ReSharper disable ThreadStaticFieldHasInitializer
-    public static List<LogEvent> Emitted = new();
+    public static List<LogEvent> Emitted = [];
     // ReSharper restore ThreadStaticFieldHasInitializer
 
     public void Emit(LogEvent logEvent)
diff --git a/test/TestDummies/DummyWrappingSink.cs b/test/TestDummies/DummyWrappingSink.cs
index 5b8f782fc..e7ef9c34d 100644
--- a/test/TestDummies/DummyWrappingSink.cs
+++ b/test/TestDummies/DummyWrappingSink.cs
@@ -5,7 +5,7 @@ public class DummyWrappingSink : ILogEventSink
     [ThreadStatic]
     static List<LogEvent>? _emitted;
 
-    public static List<LogEvent> Emitted => _emitted ??= new();
+    public static List<LogEvent> Emitted => _emitted ??= [];
 
     readonly ILogEventSink _sink;