diff --git a/eng/MSBuild/ProjectStaging.props b/eng/MSBuild/ProjectStaging.props
index 95fed0f1e31..9e3abad4a7c 100644
--- a/eng/MSBuild/ProjectStaging.props
+++ b/eng/MSBuild/ProjectStaging.props
@@ -11,12 +11,6 @@
-->
<_IsStable Condition="('$(Stage)' != 'dev' and '$(Stage)' != 'preview') Or '$(MSBuildProjectName)' == 'Microsoft.AspNetCore.Testing'">true
-
- release
-
$(NoWarn);LA0003
false
+
+
+
+
true
@@ -74,7 +83,7 @@
10.0.0-preview.3.25151.1
10.0.0-preview.3.25151.1
- 9.0.3
+ 9.0.4
10.0.0-beta.25126.4
diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/IncomingRequestLogBuffer.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/IncomingRequestLogBuffer.cs
new file mode 100644
index 00000000000..1b5da2e7f3d
--- /dev/null
+++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/IncomingRequestLogBuffer.cs
@@ -0,0 +1,164 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#if NET9_0_OR_GREATER
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using Microsoft.Extensions.Diagnostics.Buffering;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.ObjectPool;
+using Microsoft.Extensions.Options;
+using Microsoft.Shared.Diagnostics;
+using Microsoft.Shared.Pools;
+
+namespace Microsoft.AspNetCore.Diagnostics.Buffering;
+
+internal sealed class IncomingRequestLogBuffer : IDisposable
+{
+ private const int MaxBatchSize = 256;
+ private static readonly ObjectPool> _recordsToEmitListPool =
+ PoolFactory.CreateListPoolWithCapacity(MaxBatchSize);
+
+ private readonly IBufferedLogger _bufferedLogger;
+ private readonly LogBufferingFilterRuleSelector _ruleSelector;
+ private readonly IOptionsMonitor _options;
+ private readonly TimeProvider _timeProvider = TimeProvider.System;
+ private readonly LogBufferingFilterRule[] _filterRules;
+ private readonly Lock _bufferSwapLock = new();
+ private volatile bool _disposed;
+ private ConcurrentQueue _activeBuffer = new();
+ private ConcurrentQueue _standbyBuffer = new();
+ private int _activeBufferSize;
+ private DateTimeOffset _lastFlushTimestamp;
+
+ public IncomingRequestLogBuffer(
+ IBufferedLogger bufferedLogger,
+ string category,
+ LogBufferingFilterRuleSelector ruleSelector,
+ IOptionsMonitor options)
+ {
+ _bufferedLogger = bufferedLogger;
+ _ruleSelector = ruleSelector;
+ _options = options;
+ _filterRules = LogBufferingFilterRuleSelector.SelectByCategory(_options.CurrentValue.Rules.ToArray(), category);
+ }
+
+ public bool TryEnqueue(LogEntry logEntry)
+ {
+ if (_timeProvider.GetUtcNow() < _lastFlushTimestamp + _options.CurrentValue.AutoFlushDuration)
+ {
+ return false;
+ }
+
+ IReadOnlyList>? attributes = logEntry.State as IReadOnlyList>;
+ if (attributes is null)
+ {
+ // we expect state to be either ModernTagJoiner or LegacyTagJoiner
+ // which both implement IReadOnlyList>
+ // and if not, we throw an exception
+ Throw.InvalidOperationException(
+ $"Unsupported type of log state detected: {typeof(TState)}, expected IReadOnlyList>");
+ }
+
+ if (_ruleSelector.Select(_filterRules, logEntry.LogLevel, logEntry.EventId, attributes) is null)
+ {
+ // buffering is not enabled for this log entry,
+ // return false to indicate that the log entry should be logged normally.
+ return false;
+ }
+
+ SerializedLogRecord serializedLogRecord = SerializedLogRecordFactory.Create(
+ logEntry.LogLevel,
+ logEntry.EventId,
+ _timeProvider.GetUtcNow(),
+ attributes,
+ logEntry.Exception,
+ logEntry.Formatter(logEntry.State, logEntry.Exception));
+
+ if (serializedLogRecord.SizeInBytes > _options.CurrentValue.MaxLogRecordSizeInBytes)
+ {
+ SerializedLogRecordFactory.Return(serializedLogRecord);
+ return false;
+ }
+
+ lock (_bufferSwapLock)
+ {
+ _activeBuffer.Enqueue(serializedLogRecord);
+ _ = Interlocked.Add(ref _activeBufferSize, serializedLogRecord.SizeInBytes);
+
+ }
+
+ TrimExcessRecords();
+
+ return true;
+ }
+
+ public void Flush()
+ {
+ _lastFlushTimestamp = _timeProvider.GetUtcNow();
+
+ ConcurrentQueue tempBuffer;
+ int numItemsToEmit;
+ lock (_bufferSwapLock)
+ {
+ tempBuffer = _activeBuffer;
+ _activeBuffer = _standbyBuffer;
+ _standbyBuffer = tempBuffer;
+
+ numItemsToEmit = tempBuffer.Count;
+
+ _ = Interlocked.Exchange(ref _activeBufferSize, 0);
+ }
+
+ for (int offset = 0; offset < numItemsToEmit && !tempBuffer.IsEmpty; offset += MaxBatchSize)
+ {
+ int currentBatchSize = Math.Min(MaxBatchSize, numItemsToEmit - offset);
+ List recordsToEmit = _recordsToEmitListPool.Get();
+ try
+ {
+ for (int i = 0; i < currentBatchSize && tempBuffer.TryDequeue(out SerializedLogRecord bufferedRecord); i++)
+ {
+ recordsToEmit.Add(new DeserializedLogRecord(
+ bufferedRecord.Timestamp,
+ bufferedRecord.LogLevel,
+ bufferedRecord.EventId,
+ bufferedRecord.Exception,
+ bufferedRecord.FormattedMessage,
+ bufferedRecord.Attributes));
+ }
+
+ _bufferedLogger.LogRecords(recordsToEmit);
+ }
+ finally
+ {
+ _recordsToEmitListPool.Return(recordsToEmit);
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+
+ _ruleSelector.InvalidateCache();
+ }
+
+ private void TrimExcessRecords()
+ {
+ while (_activeBufferSize > _options.CurrentValue.MaxPerRequestBufferSizeInBytes &&
+ _activeBuffer.TryDequeue(out SerializedLogRecord item))
+ {
+ _ = Interlocked.Add(ref _activeBufferSize, -item.SizeInBytes);
+ SerializedLogRecordFactory.Return(item);
+ }
+ }
+}
+#endif
diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/IncomingRequestLogBufferHolder.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/IncomingRequestLogBufferHolder.cs
new file mode 100644
index 00000000000..e46869a083b
--- /dev/null
+++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/IncomingRequestLogBufferHolder.cs
@@ -0,0 +1,40 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+#if NET9_0_OR_GREATER
+using System;
+using System.Collections.Concurrent;
+
+namespace Microsoft.AspNetCore.Diagnostics.Buffering;
+
+internal sealed class IncomingRequestLogBufferHolder : IDisposable
+{
+ private readonly ConcurrentDictionary _buffers = new();
+ private bool _disposed;
+
+ public IncomingRequestLogBuffer GetOrAdd(string category, Func valueFactory) =>
+ _buffers.GetOrAdd(category, valueFactory);
+
+ public void Flush()
+ {
+ foreach (IncomingRequestLogBuffer buffer in _buffers.Values)
+ {
+ buffer.Flush();
+ }
+ }
+
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+
+ foreach (IncomingRequestLogBuffer buffer in _buffers.Values)
+ {
+ buffer.Dispose();
+ }
+ }
+}
+#endif
diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerIncomingRequestLoggingBuilderExtensions.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerIncomingRequestLoggingBuilderExtensions.cs
new file mode 100644
index 00000000000..8d3f7411367
--- /dev/null
+++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerIncomingRequestLoggingBuilderExtensions.cs
@@ -0,0 +1,124 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+#if NET9_0_OR_GREATER
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.AspNetCore.Diagnostics.Buffering;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Diagnostics.Buffering;
+using Microsoft.Extensions.Options;
+using Microsoft.Shared.DiagnosticIds;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Extensions.Logging;
+
+///
+/// Lets you register per incoming request log buffering in a dependency injection container.
+///
+[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)]
+public static class PerIncomingRequestLoggingBuilderExtensions
+{
+ ///
+ /// Adds per incoming request log buffering to the logging infrastructure.
+ ///
+ /// The .
+ /// The to add.
+ /// The value of .
+ /// or is .
+ ///
+ /// Matched logs will be buffered in a buffer specific to each incoming request
+ /// and can optionally be flushed and emitted during the request lifetime.
+ ///
+ public static ILoggingBuilder AddPerIncomingRequestBuffer(this ILoggingBuilder builder, IConfiguration configuration)
+ {
+ _ = Throw.IfNull(builder);
+ _ = Throw.IfNull(configuration);
+
+ _ = builder.Services
+ .AddSingleton>(new PerRequestLogBufferingConfigureOptions(configuration))
+ .AddOptionsWithValidateOnStart()
+ .Services.AddOptionsWithValidateOnStart();
+
+ return builder
+ .AddPerRequestBufferManager()
+ .AddGlobalBuffer(configuration);
+ }
+
+ ///
+ /// Adds per incoming request log buffering to the logging infrastructure.
+ ///
+ /// The .
+ /// The buffering options configuration delegate.
+ /// The value of .
+ /// or is .
+ ///
+ /// Matched logs will be buffered in a buffer specific to each incoming request
+ /// and can optionally be flushed and emitted during the request lifetime.
+ ///
+ public static ILoggingBuilder AddPerIncomingRequestBuffer(this ILoggingBuilder builder, Action configure)
+ {
+ _ = Throw.IfNull(builder);
+ _ = Throw.IfNull(configure);
+
+ _ = builder.Services
+ .AddOptionsWithValidateOnStart()
+ .Services.AddOptionsWithValidateOnStart()
+ .Configure(configure);
+
+ PerRequestLogBufferingOptions options = new PerRequestLogBufferingOptions();
+ configure(options);
+
+ return builder
+ .AddPerRequestBufferManager()
+ .AddGlobalBuffer(opts => opts.Rules = options.Rules);
+ }
+
+ ///
+ /// Adds per incoming request log buffering to the logging infrastructure.
+ ///
+ /// The .
+ /// The level (and below) of logs to buffer.
+ /// The value of .
+ /// is .
+ ///
+ /// Matched logs will be buffered in a buffer specific to each incoming request
+ /// and can optionally be flushed and emitted during the request lifetime.
+ ///
+ public static ILoggingBuilder AddPerIncomingRequestBuffer(this ILoggingBuilder builder, LogLevel? logLevel = null)
+ {
+ _ = Throw.IfNull(builder);
+
+ _ = builder.Services
+ .AddOptionsWithValidateOnStart()
+ .Services.AddOptionsWithValidateOnStart()
+ .Configure(options =>
+ {
+ options.Rules.Add(new LogBufferingFilterRule(logLevel: logLevel));
+ });
+
+ return builder
+ .AddPerRequestBufferManager()
+ .AddGlobalBuffer(logLevel);
+ }
+
+ private static ILoggingBuilder AddPerRequestBufferManager(this ILoggingBuilder builder)
+ {
+ builder.Services.TryAddScoped();
+ builder.Services.TryAddSingleton();
+ builder.Services.TryAddSingleton(sp =>
+ {
+ var globalBufferManager = sp.GetRequiredService();
+ return ActivatorUtilities.CreateInstance(sp, globalBufferManager);
+ });
+ builder.Services.TryAddSingleton(sp => sp.GetRequiredService());
+ builder.Services.TryAddSingleton(sp => sp.GetRequiredService());
+
+ return builder;
+ }
+}
+
+#endif
diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerRequestLogBufferManager.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerRequestLogBufferManager.cs
new file mode 100644
index 00000000000..de51e26eb58
--- /dev/null
+++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerRequestLogBufferManager.cs
@@ -0,0 +1,60 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+#if NET9_0_OR_GREATER
+
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.Buffering;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Diagnostics.Buffering;
+
+internal sealed class PerRequestLogBufferManager : PerRequestLogBuffer
+{
+ private readonly GlobalLogBuffer _globalBuffer;
+ private readonly IHttpContextAccessor _httpContextAccessor;
+ private readonly LogBufferingFilterRuleSelector _ruleSelector;
+ private readonly IOptionsMonitor _options;
+
+ public PerRequestLogBufferManager(
+ GlobalLogBuffer globalBuffer,
+ IHttpContextAccessor httpContextAccessor,
+ LogBufferingFilterRuleSelector ruleSelector,
+ IOptionsMonitor options)
+ {
+ _globalBuffer = globalBuffer;
+ _httpContextAccessor = httpContextAccessor;
+ _ruleSelector = ruleSelector;
+ _options = options;
+ }
+
+ public override void Flush()
+ {
+ _httpContextAccessor.HttpContext?.RequestServices.GetService()?.Flush();
+ _globalBuffer.Flush();
+ }
+
+ public override bool TryEnqueue(IBufferedLogger bufferedLogger, in LogEntry logEntry)
+ {
+ HttpContext? httpContext = _httpContextAccessor.HttpContext;
+ if (httpContext is null)
+ {
+ return _globalBuffer.TryEnqueue(bufferedLogger, logEntry);
+ }
+
+ string category = logEntry.Category;
+ IncomingRequestLogBufferHolder? bufferHolder =
+ httpContext.RequestServices.GetService();
+ IncomingRequestLogBuffer? buffer = bufferHolder?.GetOrAdd(category, _ =>
+ new IncomingRequestLogBuffer(bufferedLogger, category, _ruleSelector, _options));
+
+ if (buffer is null)
+ {
+ return _globalBuffer.TryEnqueue(bufferedLogger, logEntry);
+ }
+
+ return buffer.TryEnqueue(logEntry);
+ }
+}
+#endif
diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerRequestLogBufferingConfigureOptions.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerRequestLogBufferingConfigureOptions.cs
new file mode 100644
index 00000000000..54e1e2dc674
--- /dev/null
+++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerRequestLogBufferingConfigureOptions.cs
@@ -0,0 +1,49 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+#if NET9_0_OR_GREATER
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Diagnostics.Buffering;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Diagnostics.Buffering;
+
+internal sealed class PerRequestLogBufferingConfigureOptions : IConfigureOptions
+{
+ private const string ConfigSectionName = "PerIncomingRequestLogBuffering";
+ private readonly IConfiguration _configuration;
+
+ public PerRequestLogBufferingConfigureOptions(IConfiguration configuration)
+ {
+ _configuration = configuration;
+ }
+
+ public void Configure(PerRequestLogBufferingOptions options)
+ {
+ if (_configuration is null)
+ {
+ return;
+ }
+
+ IConfigurationSection section = _configuration.GetSection(ConfigSectionName);
+ if (!section.Exists())
+ {
+ return;
+ }
+
+ var parsedOptions = section.Get();
+ if (parsedOptions is null)
+ {
+ return;
+ }
+
+ options.MaxLogRecordSizeInBytes = parsedOptions.MaxLogRecordSizeInBytes;
+ options.MaxPerRequestBufferSizeInBytes = parsedOptions.MaxPerRequestBufferSizeInBytes;
+ options.AutoFlushDuration = parsedOptions.AutoFlushDuration;
+
+ foreach (LogBufferingFilterRule rule in parsedOptions.Rules)
+ {
+ options.Rules.Add(rule);
+ }
+ }
+}
+#endif
diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerRequestLogBufferingOptions.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerRequestLogBufferingOptions.cs
new file mode 100644
index 00000000000..0b281f06edb
--- /dev/null
+++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerRequestLogBufferingOptions.cs
@@ -0,0 +1,79 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+#if NET9_0_OR_GREATER
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Extensions.Diagnostics.Buffering;
+using Microsoft.Shared.Data.Validation;
+using Microsoft.Shared.DiagnosticIds;
+
+namespace Microsoft.AspNetCore.Diagnostics.Buffering;
+
+///
+/// The options for log buffering per each incoming request.
+///
+[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)]
+public class PerRequestLogBufferingOptions
+{
+ private const int DefaultPerRequestBufferSizeInBytes = 500 * 1024 * 1024; // 500 MB.
+ private const int DefaultMaxLogRecordSizeInBytes = 50 * 1024; // 50 KB.
+
+ private const int MinimumAutoFlushDuration = 0;
+ private const int MaximumAutoFlushDuration = 1000 * 60 * 60 * 24; // 1 day.
+
+ private const long MinimumPerRequestBufferSizeInBytes = 1;
+ private const long MaximumPerRequestBufferSizeInBytes = 10L * 1024 * 1024 * 1024; // 10 GB.
+
+ private const long MinimumLogRecordSizeInBytes = 1;
+ private const long MaximumLogRecordSizeInBytes = 10 * 1024 * 1024; // 10 MB.
+
+ private static readonly TimeSpan _defaultAutoFlushDuration = TimeSpan.FromSeconds(30);
+
+ ///
+ /// Gets or sets the time to do automatic flushing after manual flushing was triggered.
+ ///
+ ///
+ /// Use this to temporarily suspend buffering after a flush, e.g. in case of an incident you may want all logs to be emitted immediately,
+ /// so the buffering will be suspended for the time.
+ ///
+ [TimeSpan(MinimumAutoFlushDuration, MaximumAutoFlushDuration)]
+ public TimeSpan AutoFlushDuration { get; set; } = _defaultAutoFlushDuration;
+
+ ///
+ /// Gets or sets the maximum size of each individual log record in bytes.
+ ///
+ ///
+ /// If the size of a log record exceeds this limit, it won't be buffered.
+ ///
+ [Range(MinimumLogRecordSizeInBytes, MaximumLogRecordSizeInBytes)]
+ public int MaxLogRecordSizeInBytes { get; set; } = DefaultMaxLogRecordSizeInBytes;
+
+ ///
+ /// Gets or sets the maximum size of each per request buffer in bytes.
+ ///
+ ///
+ /// If adding a new log entry would cause the buffer size to exceed this limit,
+ /// the oldest buffered log records will be dropped to make room.
+ ///
+ [Range(MinimumPerRequestBufferSizeInBytes, MaximumPerRequestBufferSizeInBytes)]
+ public int MaxPerRequestBufferSizeInBytes { get; set; } = DefaultPerRequestBufferSizeInBytes;
+
+#pragma warning disable CA2227 // Collection properties should be read only - setter is necessary for options pattern
+ ///
+ /// Gets or sets the collection of used for filtering log messages for the purpose of further buffering.
+ ///
+ ///
+ /// If a log entry matches a rule, it will be buffered for the lifetime and scope of the respective incoming request.
+ /// Consequently, it will later be emitted when the buffer is flushed.
+ /// When the request finishes, and flush has not happened, buffered log entries of that specific request will be dropped.
+ /// If a log entry does not match any rule, it will be emitted normally.
+ /// If the buffer size limit is reached, the oldest buffered log entries will be dropped (not emitted!) to make room for new ones.
+ /// If a log entry size is greater than , it will not be buffered and will be emitted normally.
+ ///
+ public IList Rules { get; set; } = [];
+#pragma warning restore CA2227
+}
+#endif
diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerRequestLogBufferingOptionsCustomValidator.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerRequestLogBufferingOptionsCustomValidator.cs
new file mode 100644
index 00000000000..9db4c1ec183
--- /dev/null
+++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerRequestLogBufferingOptionsCustomValidator.cs
@@ -0,0 +1,35 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+#if NET9_0_OR_GREATER
+
+using System;
+using Microsoft.Extensions.Diagnostics.Buffering;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Diagnostics.Buffering;
+
+internal sealed class PerRequestLogBufferingOptionsCustomValidator : IValidateOptions
+{
+ private const char WildcardChar = '*';
+
+ public ValidateOptionsResult Validate(string? name, PerRequestLogBufferingOptions options)
+ {
+ ValidateOptionsResultBuilder resultBuilder = new();
+ foreach (LogBufferingFilterRule rule in options.Rules)
+ {
+ if (rule.CategoryName is null)
+ {
+ continue;
+ }
+
+ int wildcardIndex = rule.CategoryName.IndexOf(WildcardChar, StringComparison.Ordinal);
+ if (wildcardIndex >= 0 && rule.CategoryName.IndexOf(WildcardChar, wildcardIndex + 1) >= 0)
+ {
+ resultBuilder.AddError("Only one wildcard character is allowed in category name.", nameof(options.Rules));
+ }
+ }
+
+ return resultBuilder.Build();
+ }
+}
+#endif
diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerRequestLogBufferingOptionsValidator.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerRequestLogBufferingOptionsValidator.cs
new file mode 100644
index 00000000000..6077472e928
--- /dev/null
+++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/PerRequestLogBufferingOptionsValidator.cs
@@ -0,0 +1,13 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+#if NET9_0_OR_GREATER
+
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Diagnostics.Buffering;
+
+[OptionsValidator]
+internal sealed partial class PerRequestLogBufferingOptionsValidator : IValidateOptions
+{
+}
+#endif
diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Microsoft.AspNetCore.Diagnostics.Middleware.csproj b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Microsoft.AspNetCore.Diagnostics.Middleware.csproj
index 4cc1cbfeff3..e9c7b9b521f 100644
--- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Microsoft.AspNetCore.Diagnostics.Middleware.csproj
+++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Microsoft.AspNetCore.Diagnostics.Middleware.csproj
@@ -10,19 +10,19 @@
$(NetCoreTargetFrameworks)
true
true
- false
- false
+ true
true
false
false
true
false
- true
+ false
+ false
normal
- 100
+ 98
85
diff --git a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/components/App.tsx b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/components/App.tsx
index 7cb0b26558d..da256800221 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/components/App.tsx
+++ b/src/Libraries/Microsoft.Extensions.AI.Evaluation.Reporting/TypeScript/components/App.tsx
@@ -55,7 +55,7 @@ function App() {
const toggleSettings = () => setIsSettingsOpen(!isSettingsOpen);
const closeSettings = () => setIsSettingsOpen(false);
- const downloadDataset = () => {
+ const downloadDataset = () => {
// create a stringified JSON of the dataset
const dataStr = JSON.stringify(dataset, null, 2);
diff --git a/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogger.cs b/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogger.cs
index 56973c9e78d..20b4fde8512 100644
--- a/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogger.cs
+++ b/src/Libraries/Microsoft.Extensions.Diagnostics.Testing/Logging/FakeLogger.cs
@@ -6,6 +6,11 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
+#if NET9_0_OR_GREATER
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Shared.DiagnosticIds;
+#endif
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Extensions.Logging.Testing;
@@ -17,7 +22,11 @@ namespace Microsoft.Extensions.Logging.Testing;
/// This type is intended for use in unit tests. It captures all the log state to memory and lets you inspect it
/// to validate that your code is logging what it should.
///
+#if NET9_0_OR_GREATER
+public class FakeLogger : ILogger, IBufferedLogger
+#else
public class FakeLogger : ILogger
+#endif
{
private readonly ConcurrentDictionary _disabledLevels = new(); // used as a set, the value is ignored
@@ -105,6 +114,27 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except
///
public string? Category { get; }
+#if NET9_0_OR_GREATER
+ ///
+ [Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)]
+ public void LogRecords(IEnumerable records)
+ {
+ _ = Throw.IfNull(records);
+
+ var l = new List