diff --git a/.gitignore b/.gitignore
index e285430cb..fd2a2ba46 100644
--- a/.gitignore
+++ b/.gitignore
@@ -34,7 +34,6 @@ launchSettings.json
BenchmarkDotNet.Artifacts/
BDN.Generated/
binaries/
-global.json
.vscode/
*.binlog
build/feed
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 0977689f4..f8e0a7a8e 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -4,7 +4,7 @@
9.0.0-preview.5.24306.11
8.0.6
7.0.5
- 6.0.11
+ 6.0.33
2.63.0
1.6.0
1.8.1
@@ -57,21 +57,20 @@
-
+
-
+
-
+
-
-
+
@@ -80,10 +79,9 @@
-
-
+
diff --git a/examples/Certifier/Client/Program.cs b/examples/Certifier/Client/Program.cs
index 0435a7b64..846ad92ae 100644
--- a/examples/Certifier/Client/Program.cs
+++ b/examples/Certifier/Client/Program.cs
@@ -73,7 +73,7 @@ static HttpClientHandler CreateHttpHandler(bool includeClientCertificate)
// Load client certificate
var basePath = Path.GetDirectoryName(typeof(Program).Assembly.Location);
var certPath = Path.Combine(basePath!, "Certs", "client.pfx");
- var clientCertificate = new X509Certificate2(certPath, "1111");
+ var clientCertificate = X509CertificateLoader.LoadPkcs12FromFile(certPath, "1111");
handler.ClientCertificates.Add(clientCertificate);
}
diff --git a/examples/Container/Frontend/Frontend.csproj b/examples/Container/Frontend/Frontend.csproj
index cd0fc7b99..73a9dd89a 100644
--- a/examples/Container/Frontend/Frontend.csproj
+++ b/examples/Container/Frontend/Frontend.csproj
@@ -1,7 +1,10 @@
-
+
net9.0
+
+
+ $(NoWarn);CS0618
diff --git a/examples/Container/deploy.ps1 b/examples/Container/deploy.ps1
index 299a9dd85..141db2a89 100644
--- a/examples/Container/deploy.ps1
+++ b/examples/Container/deploy.ps1
@@ -1,8 +1,8 @@
-docker-compose -f .\docker-compose.yml build container-frontend
-docker-compose -f .\docker-compose.yml build container-backend
+docker compose -f .\docker-compose.yml build container-frontend
+docker compose -f .\docker-compose.yml build container-backend
kubectl delete -f .\Kubernetes\deploy-backend.yml
kubectl apply -f .\Kubernetes\deploy-backend.yml
kubectl delete -f .\Kubernetes\deploy-frontend.yml
-kubectl apply -f .\Kubernetes\deploy-frontend.yml
\ No newline at end of file
+kubectl apply -f .\Kubernetes\deploy-frontend.yml
diff --git a/global.json b/global.json
index 334b825de..67d91a4f9 100644
--- a/global.json
+++ b/global.json
@@ -1,6 +1,6 @@
{
"sdk": {
- "version": "9.0.100-preview.5.24305.3",
+ "version": "9.0.100-preview.7.24407.12",
"rollForward": "latestFeature"
}
}
diff --git a/grpcweb_interoptests.sh b/grpcweb_interoptests.sh
index e24d41b2e..eb7fd89c7 100755
--- a/grpcweb_interoptests.sh
+++ b/grpcweb_interoptests.sh
@@ -17,11 +17,11 @@ set -ex
echo "Starting gRPC-Web interop test containers"
-docker-compose -f docker-compose.yml build grpcweb-server
-docker-compose -f docker-compose.yml build grpcweb-client
+docker compose -f docker-compose.yml build grpcweb-server
+docker compose -f docker-compose.yml build grpcweb-client
-docker-compose -f docker-compose.yml up -d grpcweb-server
-docker-compose -f docker-compose.yml up -d grpcweb-client
+docker compose -f docker-compose.yml up -d grpcweb-server
+docker compose -f docker-compose.yml up -d grpcweb-client
sleep 5
@@ -34,6 +34,6 @@ cd ../../..
echo "Remove all containers"
-docker-compose down
+docker compose down
-echo "gRPC-Web interop tests finished"
\ No newline at end of file
+echo "gRPC-Web interop tests finished"
diff --git a/perf/Grpc.AspNetCore.Microbenchmarks/Grpc.AspNetCore.Microbenchmarks.csproj b/perf/Grpc.AspNetCore.Microbenchmarks/Grpc.AspNetCore.Microbenchmarks.csproj
index d4107b395..22c7d13f7 100644
--- a/perf/Grpc.AspNetCore.Microbenchmarks/Grpc.AspNetCore.Microbenchmarks.csproj
+++ b/perf/Grpc.AspNetCore.Microbenchmarks/Grpc.AspNetCore.Microbenchmarks.csproj
@@ -25,7 +25,6 @@
-
diff --git a/perf/benchmarkapps/GrpcClient/Program.cs b/perf/benchmarkapps/GrpcClient/Program.cs
index 8c277b140..9517079bb 100644
--- a/perf/benchmarkapps/GrpcClient/Program.cs
+++ b/perf/benchmarkapps/GrpcClient/Program.cs
@@ -476,11 +476,8 @@ private static ChannelBase CreateChannel(string target)
{
var basePath = Path.GetDirectoryName(AppContext.BaseDirectory);
var certPath = Path.Combine(basePath!, "Certs", "client.pfx");
- var clientCertificate = new X509Certificate2(certPath, "1111");
- httpClientHandler.SslOptions.ClientCertificates = new X509CertificateCollection
- {
- clientCertificate
- };
+ var clientCertificates = X509CertificateLoader.LoadPkcs12CollectionFromFile(certPath, "1111");
+ httpClientHandler.SslOptions.ClientCertificates = clientCertificates;
}
#if NET5_0_OR_GREATER
if (!string.IsNullOrEmpty(_options.UdsFileName))
diff --git a/test/FunctionalTests/Balancer/ConnectionTests.cs b/test/FunctionalTests/Balancer/ConnectionTests.cs
index 72cb7dbdf..9476b5126 100644
--- a/test/FunctionalTests/Balancer/ConnectionTests.cs
+++ b/test/FunctionalTests/Balancer/ConnectionTests.cs
@@ -432,7 +432,11 @@ Task UnaryMethod(HelloRequest request, ServerCallContext context)
// even after specifying the correct host override.
var basePath = Path.GetDirectoryName(typeof(InProcessTestServer).Assembly.Location);
var certPath = Path.Combine(basePath!, "localhost.pfx");
+#if NET9_0_OR_GREATER
+ var cert = X509CertificateLoader.LoadPkcs12FromFile(certPath, "11111");
+#else
var cert = new X509Certificate2(certPath, "11111");
+#endif
// Arrange
using var endpoint1 = BalancerHelpers.CreateGrpcEndpoint(UnaryMethod, nameof(UnaryMethod), HttpProtocols.Http1AndHttp2, isHttps: true, certificate: cert);
diff --git a/test/FunctionalTests/Grpc.AspNetCore.FunctionalTests.csproj b/test/FunctionalTests/Grpc.AspNetCore.FunctionalTests.csproj
index 764ecff82..91868df0f 100644
--- a/test/FunctionalTests/Grpc.AspNetCore.FunctionalTests.csproj
+++ b/test/FunctionalTests/Grpc.AspNetCore.FunctionalTests.csproj
@@ -27,6 +27,7 @@
+
@@ -37,8 +38,6 @@
-
-
PreserveNewest
diff --git a/test/Grpc.AspNetCore.Server.ClientFactory.Tests/Grpc.AspNetCore.Server.ClientFactory.Tests.csproj b/test/Grpc.AspNetCore.Server.ClientFactory.Tests/Grpc.AspNetCore.Server.ClientFactory.Tests.csproj
index 23b0efbc1..3c5fe497b 100644
--- a/test/Grpc.AspNetCore.Server.ClientFactory.Tests/Grpc.AspNetCore.Server.ClientFactory.Tests.csproj
+++ b/test/Grpc.AspNetCore.Server.ClientFactory.Tests/Grpc.AspNetCore.Server.ClientFactory.Tests.csproj
@@ -20,13 +20,13 @@
+
-
diff --git a/test/Grpc.AspNetCore.Server.Tests/Grpc.AspNetCore.Server.Tests.csproj b/test/Grpc.AspNetCore.Server.Tests/Grpc.AspNetCore.Server.Tests.csproj
index 90d3b6664..3c52d8c27 100644
--- a/test/Grpc.AspNetCore.Server.Tests/Grpc.AspNetCore.Server.Tests.csproj
+++ b/test/Grpc.AspNetCore.Server.Tests/Grpc.AspNetCore.Server.Tests.csproj
@@ -27,6 +27,7 @@
+
@@ -36,9 +37,6 @@
-
-
-
diff --git a/test/Grpc.AspNetCore.Server.Tests/GrpcProtocolHelpersTests.cs b/test/Grpc.AspNetCore.Server.Tests/GrpcProtocolHelpersTests.cs
index 7273ceafc..6fd6339a9 100644
--- a/test/Grpc.AspNetCore.Server.Tests/GrpcProtocolHelpersTests.cs
+++ b/test/Grpc.AspNetCore.Server.Tests/GrpcProtocolHelpersTests.cs
@@ -1,4 +1,4 @@
-#region Copyright notice and license
+#region Copyright notice and license
// Copyright 2019 The gRPC Authors
//
@@ -30,7 +30,7 @@ public class GrpcProtocolHelpersTests
public void CreateAuthContext_CertWithAlternativeNames_UseAlternativeNamesAsPeerIdentity()
{
// Arrange
- X509Certificate2 certificate = new X509Certificate2(TestHelpers.ResolvePath(@"Certs/outlookcom.crt"));
+ var certificate = LoadCertificate(TestHelpers.ResolvePath(@"Certs/outlookcom.crt"));
// Act
var authContext = GrpcProtocolHelpers.CreateAuthContext(certificate);
@@ -57,7 +57,7 @@ public void CreateAuthContext_CertWithAlternativeNames_UseAlternativeNamesAsPeer
public void CreateAuthContext_CertWithCommonName_UseCommonNameAsPeerIdentity()
{
// Arrange
- var certificate = new X509Certificate2(TestHelpers.ResolvePath(@"Certs/client.crt"));
+ var certificate = LoadCertificate(TestHelpers.ResolvePath(@"Certs/client.crt"));
// Act
var authContext = GrpcProtocolHelpers.CreateAuthContext(certificate);
@@ -99,4 +99,13 @@ public void TryDecodeTimeout_WithVariousUnits_ShouldMatchExpected(string timeout
Assert.AreEqual(expectedSuccesfullyDecoded, successfullyDecoded);
Assert.AreEqual(expectedTimespan, timeSpan);
}
+
+ public static X509Certificate2 LoadCertificate(string path)
+ {
+#if NET9_0_OR_GREATER
+ return X509CertificateLoader.LoadCertificateFromFile(path);
+#else
+ return new X509Certificate2(path);
+#endif
+ }
}
diff --git a/test/Grpc.AspNetCore.Server.Tests/HttpContextServerCallContextTests.cs b/test/Grpc.AspNetCore.Server.Tests/HttpContextServerCallContextTests.cs
index 2730119bb..63f357cff 100644
--- a/test/Grpc.AspNetCore.Server.Tests/HttpContextServerCallContextTests.cs
+++ b/test/Grpc.AspNetCore.Server.Tests/HttpContextServerCallContextTests.cs
@@ -1,4 +1,4 @@
-#region Copyright notice and license
+#region Copyright notice and license
// Copyright 2019 The gRPC Authors
//
@@ -537,7 +537,7 @@ public void AuthContext_HasClientCertificate_Authenticated()
{
// Arrange
var httpContext = new DefaultHttpContext();
- var certificate = new X509Certificate2(TestHelpers.ResolvePath(@"Certs/client.crt"));
+ var certificate = GrpcProtocolHelpersTests.LoadCertificate(TestHelpers.ResolvePath(@"Certs/client.crt"));
httpContext.Connection.ClientCertificate = certificate;
var serverCallContext = CreateServerCallContext(httpContext);
diff --git a/test/Grpc.Net.Client.Tests/Grpc.Net.Client.Tests.csproj b/test/Grpc.Net.Client.Tests/Grpc.Net.Client.Tests.csproj
index a04c10f08..d83170b49 100644
--- a/test/Grpc.Net.Client.Tests/Grpc.Net.Client.Tests.csproj
+++ b/test/Grpc.Net.Client.Tests/Grpc.Net.Client.Tests.csproj
@@ -32,6 +32,7 @@
+
@@ -40,7 +41,6 @@
-
diff --git a/test/Grpc.Net.ClientFactory.Tests/Grpc.Net.ClientFactory.Tests.csproj b/test/Grpc.Net.ClientFactory.Tests/Grpc.Net.ClientFactory.Tests.csproj
index d134fd140..bdd09a4c2 100644
--- a/test/Grpc.Net.ClientFactory.Tests/Grpc.Net.ClientFactory.Tests.csproj
+++ b/test/Grpc.Net.ClientFactory.Tests/Grpc.Net.ClientFactory.Tests.csproj
@@ -23,12 +23,12 @@
+
-
diff --git a/test/Shared/Logging/BeginScopeContext.cs b/test/Shared/Logging/BeginScopeContext.cs
new file mode 100644
index 000000000..5e93ee511
--- /dev/null
+++ b/test/Shared/Logging/BeginScopeContext.cs
@@ -0,0 +1,14 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable disable
+
+// Copied with permission from https://github.com/dotnet/aspnetcore/tree/08b60af1bca8cffff8ba0a72164fb7505ffe114d/src/Testing/src/Logging
+namespace Microsoft.Extensions.Logging.Testing;
+
+public class BeginScopeContext
+{
+ public object Scope { get; set; }
+
+ public string LoggerName { get; set; }
+}
diff --git a/test/Shared/Logging/ITestSink.cs b/test/Shared/Logging/ITestSink.cs
new file mode 100644
index 000000000..546795f14
--- /dev/null
+++ b/test/Shared/Logging/ITestSink.cs
@@ -0,0 +1,29 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable disable
+
+using System;
+using System.Collections.Concurrent;
+
+// Copied with permission from https://github.com/dotnet/aspnetcore/tree/08b60af1bca8cffff8ba0a72164fb7505ffe114d/src/Testing/src/Logging
+namespace Microsoft.Extensions.Logging.Testing;
+
+public interface ITestSink
+{
+ event Action MessageLogged;
+
+ event Action ScopeStarted;
+
+ Func WriteEnabled { get; set; }
+
+ Func BeginEnabled { get; set; }
+
+ IProducerConsumerCollection Scopes { get; set; }
+
+ IProducerConsumerCollection Writes { get; set; }
+
+ void Write(WriteContext context);
+
+ void Begin(BeginScopeContext context);
+}
diff --git a/test/Shared/Logging/LogLevelAttribute.cs b/test/Shared/Logging/LogLevelAttribute.cs
new file mode 100644
index 000000000..65d90d30d
--- /dev/null
+++ b/test/Shared/Logging/LogLevelAttribute.cs
@@ -0,0 +1,20 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable disable
+
+using System;
+
+// Copied with permission from https://github.com/dotnet/aspnetcore/tree/08b60af1bca8cffff8ba0a72164fb7505ffe114d/src/Testing/src/Logging
+namespace Microsoft.Extensions.Logging.Testing;
+
+[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly, AllowMultiple = false)]
+public class LogLevelAttribute : Attribute
+{
+ public LogLevelAttribute(LogLevel logLevel)
+ {
+ LogLevel = logLevel;
+ }
+
+ public LogLevel LogLevel { get; }
+}
diff --git a/test/Shared/Logging/TestLogger.cs b/test/Shared/Logging/TestLogger.cs
new file mode 100644
index 000000000..70efb0a1c
--- /dev/null
+++ b/test/Shared/Logging/TestLogger.cs
@@ -0,0 +1,78 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable disable
+
+using System;
+
+// Copied with permission from https://github.com/dotnet/aspnetcore/tree/08b60af1bca8cffff8ba0a72164fb7505ffe114d/src/Testing/src/Logging
+namespace Microsoft.Extensions.Logging.Testing;
+
+public class TestLogger : ILogger
+{
+ private object _scope;
+ private readonly ITestSink _sink;
+ private readonly string _name;
+ private readonly Func _filter;
+
+ public TestLogger(string name, ITestSink sink, bool enabled)
+ : this(name, sink, _ => enabled)
+ {
+ }
+
+ public TestLogger(string name, ITestSink sink, Func filter)
+ {
+ _sink = sink;
+ _name = name;
+ _filter = filter;
+ }
+
+ public string Name { get; set; }
+
+ public IDisposable BeginScope(TState state)
+ {
+ _scope = state;
+
+ _sink.Begin(new BeginScopeContext()
+ {
+ LoggerName = _name,
+ Scope = state,
+ });
+
+ return TestDisposable.Instance;
+ }
+
+ public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
+ {
+ if (!IsEnabled(logLevel))
+ {
+ return;
+ }
+
+ _sink.Write(new WriteContext()
+ {
+ LogLevel = logLevel,
+ EventId = eventId,
+ State = state,
+ Exception = exception,
+ Formatter = (s, e) => formatter((TState)s, e),
+ LoggerName = _name,
+ Scope = _scope
+ });
+ }
+
+ public bool IsEnabled(LogLevel logLevel)
+ {
+ return logLevel != LogLevel.None && _filter(logLevel);
+ }
+
+ private sealed class TestDisposable : IDisposable
+ {
+ public static readonly TestDisposable Instance = new TestDisposable();
+
+ public void Dispose()
+ {
+ // intentionally does nothing
+ }
+ }
+}
diff --git a/test/Shared/Logging/TestLoggerFactory.cs b/test/Shared/Logging/TestLoggerFactory.cs
new file mode 100644
index 000000000..f22b44600
--- /dev/null
+++ b/test/Shared/Logging/TestLoggerFactory.cs
@@ -0,0 +1,32 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable disable
+
+// Copied with permission from https://github.com/dotnet/aspnetcore/tree/08b60af1bca8cffff8ba0a72164fb7505ffe114d/src/Testing/src/Logging
+namespace Microsoft.Extensions.Logging.Testing;
+
+public class TestLoggerFactory : ILoggerFactory
+{
+ private readonly ITestSink _sink;
+ private readonly bool _enabled;
+
+ public TestLoggerFactory(ITestSink sink, bool enabled)
+ {
+ _sink = sink;
+ _enabled = enabled;
+ }
+
+ public ILogger CreateLogger(string name)
+ {
+ return new TestLogger(name, _sink, _enabled);
+ }
+
+ public void AddProvider(ILoggerProvider provider)
+ {
+ }
+
+ public void Dispose()
+ {
+ }
+}
diff --git a/test/Shared/Logging/TestLoggerProvider.cs b/test/Shared/Logging/TestLoggerProvider.cs
new file mode 100644
index 000000000..b3d10ea01
--- /dev/null
+++ b/test/Shared/Logging/TestLoggerProvider.cs
@@ -0,0 +1,26 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable disable
+
+// Copied with permission from https://github.com/dotnet/aspnetcore/tree/08b60af1bca8cffff8ba0a72164fb7505ffe114d/src/Testing/src/Logging
+namespace Microsoft.Extensions.Logging.Testing;
+
+public class TestLoggerProvider : ILoggerProvider
+{
+ private readonly ITestSink _sink;
+
+ public TestLoggerProvider(ITestSink sink)
+ {
+ _sink = sink;
+ }
+
+ public ILogger CreateLogger(string categoryName)
+ {
+ return new TestLogger(categoryName, _sink, enabled: true);
+ }
+
+ public void Dispose()
+ {
+ }
+}
diff --git a/test/Shared/Logging/TestLoggerT.cs b/test/Shared/Logging/TestLoggerT.cs
new file mode 100644
index 000000000..f7b3fe8cb
--- /dev/null
+++ b/test/Shared/Logging/TestLoggerT.cs
@@ -0,0 +1,39 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable disable
+
+using System;
+
+// Copied with permission from https://github.com/dotnet/aspnetcore/tree/08b60af1bca8cffff8ba0a72164fb7505ffe114d/src/Testing/src/Logging
+namespace Microsoft.Extensions.Logging.Testing;
+
+public class TestLogger : ILogger
+{
+ private readonly ILogger _logger;
+
+ public TestLogger(TestLoggerFactory factory)
+ {
+ _logger = factory.CreateLogger();
+ }
+
+ public IDisposable BeginScope(TState state)
+ {
+ return _logger.BeginScope(state);
+ }
+
+ public bool IsEnabled(LogLevel logLevel)
+ {
+ return _logger.IsEnabled(logLevel);
+ }
+
+ public void Log(
+ LogLevel logLevel,
+ EventId eventId,
+ TState state,
+ Exception exception,
+ Func formatter)
+ {
+ _logger.Log(logLevel, eventId, state, exception, formatter);
+ }
+}
diff --git a/test/Shared/Logging/TestSink.cs b/test/Shared/Logging/TestSink.cs
new file mode 100644
index 000000000..7d85e63b3
--- /dev/null
+++ b/test/Shared/Logging/TestSink.cs
@@ -0,0 +1,67 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable disable
+
+using System;
+using System.Collections.Concurrent;
+
+// Copied with permission from https://github.com/dotnet/aspnetcore/tree/08b60af1bca8cffff8ba0a72164fb7505ffe114d/src/Testing/src/Logging
+namespace Microsoft.Extensions.Logging.Testing;
+
+public class TestSink : ITestSink
+{
+ private ConcurrentQueue _scopes;
+ private ConcurrentQueue _writes;
+
+ public TestSink(
+ Func writeEnabled = null,
+ Func beginEnabled = null)
+ {
+ WriteEnabled = writeEnabled;
+ BeginEnabled = beginEnabled;
+
+ _scopes = new ConcurrentQueue();
+ _writes = new ConcurrentQueue();
+ }
+
+ public Func WriteEnabled { get; set; }
+
+ public Func BeginEnabled { get; set; }
+
+ public IProducerConsumerCollection Scopes { get => _scopes; set => _scopes = new ConcurrentQueue(value); }
+
+ public IProducerConsumerCollection Writes { get => _writes; set => _writes = new ConcurrentQueue(value); }
+
+ public event Action MessageLogged;
+
+ public event Action ScopeStarted;
+
+ public void Write(WriteContext context)
+ {
+ if (WriteEnabled == null || WriteEnabled(context))
+ {
+ _writes.Enqueue(context);
+ }
+ MessageLogged?.Invoke(context);
+ }
+
+ public void Begin(BeginScopeContext context)
+ {
+ if (BeginEnabled == null || BeginEnabled(context))
+ {
+ _scopes.Enqueue(context);
+ }
+ ScopeStarted?.Invoke(context);
+ }
+
+ public static bool EnableWithTypeName(WriteContext context)
+ {
+ return context.LoggerName.Equals(typeof(T).FullName);
+ }
+
+ public static bool EnableWithTypeName(BeginScopeContext context)
+ {
+ return context.LoggerName.Equals(typeof(T).FullName);
+ }
+}
diff --git a/test/Shared/Logging/WriteContext.cs b/test/Shared/Logging/WriteContext.cs
new file mode 100644
index 000000000..f3b9770bd
--- /dev/null
+++ b/test/Shared/Logging/WriteContext.cs
@@ -0,0 +1,39 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable disable
+
+using System;
+
+// Copied with permission from https://github.com/dotnet/aspnetcore/tree/08b60af1bca8cffff8ba0a72164fb7505ffe114d/src/Testing/src/Logging
+namespace Microsoft.Extensions.Logging.Testing;
+
+public class WriteContext
+{
+ public LogLevel LogLevel { get; set; }
+
+ public EventId EventId { get; set; }
+
+ public object State { get; set; }
+
+ public Exception Exception { get; set; }
+
+ public Func