diff --git a/tests/Benchmarks/Benchmark.Api/Benchmark.Api.csproj b/tests/Benchmarks/Benchmark.Api/Benchmark.Api.csproj
new file mode 100644
index 00000000..ecc56fce
--- /dev/null
+++ b/tests/Benchmarks/Benchmark.Api/Benchmark.Api.csproj
@@ -0,0 +1,21 @@
+
+
+
+ netstandard2.1
+ SpaceEngineers.Core.Benchmark.Api
+ SpaceEngineers.Core.Benchmark.Api
+ false
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
diff --git a/tests/Benchmarks/Benchmark.Api/Benchmark.cs b/tests/Benchmarks/Benchmark.Api/Benchmark.cs
new file mode 100644
index 00000000..c8cf9ffa
--- /dev/null
+++ b/tests/Benchmarks/Benchmark.Api/Benchmark.cs
@@ -0,0 +1,181 @@
+namespace SpaceEngineers.Core.Benchmark.Api
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Globalization;
+ using System.Linq;
+ using BenchmarkDotNet.Columns;
+ using BenchmarkDotNet.Reports;
+ using BenchmarkDotNet.Running;
+ using BenchmarkDotNet.Validators;
+ using Perfolizer.Horology;
+
+ ///
+ /// Benchmark entry point
+ ///
+ [SuppressMessage("Analysis", "CA1724", Justification = "desired name")]
+ public static class Benchmark
+ {
+ ///
+ /// Benchmark entry point method
+ ///
+ /// Output
+ /// TSource type-argument
+ /// Benchmark summary
+ public static Summary Run(Action output)
+ {
+ var summary = BenchmarkRunner.Run();
+
+ output(summary.Title);
+ output($"TotalTime: {summary.TotalTime.ToString()}");
+ output($"Details in: {summary.LogFilePath}");
+
+ if (summary.HasCriticalValidationErrors)
+ {
+ var errors = ((IEnumerable)summary.ValidationErrors)
+ .Select(error => new InvalidOperationException(error.ToString()))
+ .ToList();
+
+ throw new AggregateException(errors);
+ }
+
+ return summary;
+ }
+
+ ///
+ /// Gets time measure in seconds from benchmark summary
+ ///
+ /// Summary
+ /// Measure record name
+ /// Measure
+ /// Output
+ /// Seconds time measure
+ public static decimal SecondMeasure(
+ this Summary summary,
+ string measureRecordName,
+ Measure measure,
+ Action output)
+ {
+ return summary.TimeMeasure(
+ TimeUnit.Second,
+ measureRecordName,
+ measure,
+ output);
+ }
+
+ ///
+ /// Gets time measure in milliseconds from benchmark summary
+ ///
+ /// Summary
+ /// Measure record name
+ /// Measure
+ /// Output
+ /// Milliseconds time measure
+ public static decimal MillisecondMeasure(
+ this Summary summary,
+ string measureRecordName,
+ Measure measure,
+ Action output)
+ {
+ return summary.TimeMeasure(
+ TimeUnit.Millisecond,
+ measureRecordName,
+ measure,
+ output);
+ }
+
+ ///
+ /// Gets time measure in nanoseconds from benchmark summary
+ ///
+ /// Summary
+ /// Measure record name
+ /// Measure
+ /// Output
+ /// Nanoseconds time measure
+ public static decimal NanosecondMeasure(
+ this Summary summary,
+ string measureRecordName,
+ Measure measure,
+ Action output)
+ {
+ return summary.TimeMeasure(
+ TimeUnit.Nanosecond,
+ measureRecordName,
+ measure,
+ output);
+ }
+
+ ///
+ /// Gets time measure from benchmark summary
+ ///
+ /// Summary
+ /// TimeUnit
+ /// Measure record name
+ /// Measure
+ /// Output
+ /// Time measure
+ public static decimal TimeMeasure(
+ this Summary summary,
+ TimeUnit timeUnit,
+ string measureRecordName,
+ Measure measure,
+ Action output)
+ {
+ var timeMeasure = summary.TimeMeasures(measure, timeUnit)[measureRecordName];
+
+ output($"{measureRecordName} -> {measure} -> {timeMeasure} {timeUnit.Name}");
+
+ return timeMeasure;
+ }
+
+ private static IDictionary TimeMeasures(
+ this Summary summary,
+ Measure measure,
+ TimeUnit timeUnit)
+ {
+ var measureColumnName = measure.ToString();
+
+ var methodColumn = summary.Column("Method");
+ var measureColumn = summary.Column(measureColumnName);
+
+ if (!measureColumn.IsNumeric)
+ {
+ throw new InvalidOperationException($"{measureColumnName} isn't numeric");
+ }
+
+ var style = new SummaryStyle(CultureInfo.InvariantCulture,
+ false,
+ SizeUnit.B,
+ timeUnit,
+ false,
+ true);
+
+ return summary
+ .BenchmarksCases
+ .ToDictionary(benchmarksCase => methodColumn.GetValue(summary, benchmarksCase),
+ ParseTime(summary, methodColumn, measureColumn, measureColumnName, timeUnit, style));
+
+ static Func ParseTime(
+ Summary summary,
+ IColumn methodColumn,
+ IColumn measureColumn,
+ string measureColumnName,
+ TimeUnit timeUnit,
+ SummaryStyle style)
+ {
+ return benchmarkCase =>
+ {
+ var measure = measureColumn.GetValue(summary, benchmarkCase, style);
+
+ return decimal.Parse(measure, CultureInfo.InvariantCulture);
+ };
+ }
+ }
+
+ private static IColumn Column(this Summary summary, string columnName)
+ {
+ return summary.GetColumns().Single(col => col.ColumnName == columnName);
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Benchmarks/Benchmark.Api/Measure.cs b/tests/Benchmarks/Benchmark.Api/Measure.cs
new file mode 100644
index 00000000..93ba933c
--- /dev/null
+++ b/tests/Benchmarks/Benchmark.Api/Measure.cs
@@ -0,0 +1,13 @@
+namespace SpaceEngineers.Core.Benchmark.Api
+{
+ ///
+ /// Measure
+ ///
+ public enum Measure
+ {
+ ///
+ /// Mean
+ ///
+ Mean
+ }
+}
\ No newline at end of file
diff --git a/tests/Benchmarks/Benchmark.Api/Properties/AssemblyInfo.cs b/tests/Benchmarks/Benchmark.Api/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..31adb06d
--- /dev/null
+++ b/tests/Benchmarks/Benchmark.Api/Properties/AssemblyInfo.cs
@@ -0,0 +1,5 @@
+using System.Reflection;
+
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
+[assembly: AssemblyInformationalVersion("1.0.0.0")]
\ No newline at end of file
diff --git a/tests/Benchmarks/GenericHost.Benchmark/Benchmarks.cs b/tests/Benchmarks/GenericHost.Benchmark/Benchmarks.cs
new file mode 100644
index 00000000..b7255b7b
--- /dev/null
+++ b/tests/Benchmarks/GenericHost.Benchmark/Benchmarks.cs
@@ -0,0 +1,154 @@
+namespace SpaceEngineers.Core.GenericHost.Benchmark
+{
+ using System.Threading.Tasks;
+ using Core.Benchmark.Api;
+ using Sources;
+ using Test.Api;
+ using Test.Api.ClassFixtures;
+ using Xunit;
+ using Xunit.Abstractions;
+
+ ///
+ /// Benchmarks
+ ///
+ // TODO: #136 - remove magic numbers and use adaptive approach -> store test artifacts in DB and search performance change points
+ public class Benchmarks : TestBase
+ {
+ /// .cctor
+ /// ITestOutputHelper
+ /// TestFixture
+ public Benchmarks(ITestOutputHelper output, TestFixture fixture)
+ : base(output, fixture)
+ {
+ }
+
+ [Fact(Timeout = 300_000)]
+ internal void DatabaseConnectionProviderBenchmark()
+ {
+ var summary = Benchmark.Run(Output.WriteLine);
+
+ var read = summary.MillisecondMeasure(
+ nameof(DatabaseConnectionProviderBenchmarkSource.Read),
+ Measure.Mean,
+ Output.WriteLine);
+
+ var insert = summary.MillisecondMeasure(
+ nameof(DatabaseConnectionProviderBenchmarkSource.Insert),
+ Measure.Mean,
+ Output.WriteLine);
+
+ var delete = summary.MillisecondMeasure(
+ nameof(DatabaseConnectionProviderBenchmarkSource.Delete),
+ Measure.Mean,
+ Output.WriteLine);
+
+ Assert.True(read < 25m);
+ Assert.True(insert < 25m);
+ Assert.True(delete < 25m);
+ }
+
+ [Fact(Timeout = 300_000)]
+ internal static async Task DatabaseConnectionProviderBenchmarkTest()
+ {
+ var source = new DatabaseConnectionProviderBenchmarkSource();
+
+ try
+ {
+ source.GlobalSetup();
+
+ for (var i = 0; i < 1000; i++)
+ {
+ source.IterationSetup();
+
+ await source.Read().ConfigureAwait(false);
+ await source.Insert().ConfigureAwait(false);
+ await source.Delete().ConfigureAwait(false);
+
+ source.IterationCleanup();
+ }
+ }
+ finally
+ {
+ source.GlobalCleanup();
+ }
+ }
+
+ [Fact(Timeout = 300_000)]
+ internal void MessageHandlerMiddlewareBenchmark()
+ {
+ var summary = Benchmark.Run(Output.WriteLine);
+
+ var compositeMiddleware = summary.MillisecondMeasure(
+ nameof(MessageHandlerMiddlewareBenchmarkSource.RunCompositeMiddleware),
+ Measure.Mean,
+ Output.WriteLine);
+
+ var tracingMiddleware = summary.MillisecondMeasure(
+ nameof(MessageHandlerMiddlewareBenchmarkSource.RunTracingMiddleware),
+ Measure.Mean,
+ Output.WriteLine);
+
+ var errorHandlingMiddleware = summary.MillisecondMeasure(
+ nameof(MessageHandlerMiddlewareBenchmarkSource.RunErrorHandlingMiddleware),
+ Measure.Mean,
+ Output.WriteLine);
+
+ var authorizationMiddleware = summary.MillisecondMeasure(
+ nameof(MessageHandlerMiddlewareBenchmarkSource.RunAuthorizationMiddleware),
+ Measure.Mean,
+ Output.WriteLine);
+
+ var unitOfWorkMiddleware = summary.MillisecondMeasure(
+ nameof(MessageHandlerMiddlewareBenchmarkSource.RunUnitOfWorkMiddleware),
+ Measure.Mean,
+ Output.WriteLine);
+
+ var handledByEndpointMiddleware = summary.MillisecondMeasure(
+ nameof(MessageHandlerMiddlewareBenchmarkSource.RunHandledByEndpointMiddleware),
+ Measure.Mean,
+ Output.WriteLine);
+
+ var requestReplyMiddleware = summary.MillisecondMeasure(
+ nameof(MessageHandlerMiddlewareBenchmarkSource.RunRequestReplyMiddleware),
+ Measure.Mean,
+ Output.WriteLine);
+
+ Assert.True(compositeMiddleware < 50m);
+ Assert.True(tracingMiddleware < 1m);
+ Assert.True(errorHandlingMiddleware < 1m);
+ Assert.True(authorizationMiddleware < 1m);
+ Assert.True(unitOfWorkMiddleware < 25m);
+ Assert.True(handledByEndpointMiddleware < 1m);
+ Assert.True(requestReplyMiddleware < 1m);
+ }
+
+ [Fact(Timeout = 300_000)]
+ internal static async Task MessageHandlerMiddlewareBenchmarkTest()
+ {
+ var source = new MessageHandlerMiddlewareBenchmarkSource();
+
+ try
+ {
+ source.GlobalSetup();
+
+ for (var i = 0; i < 1000; i++)
+ {
+ source.IterationSetup();
+
+ await source.RunTracingMiddleware().ConfigureAwait(false);
+ await source.RunErrorHandlingMiddleware().ConfigureAwait(false);
+ await source.RunAuthorizationMiddleware().ConfigureAwait(false);
+ await source.RunUnitOfWorkMiddleware().ConfigureAwait(false);
+ await source.RunHandledByEndpointMiddleware().ConfigureAwait(false);
+ await source.RunRequestReplyMiddleware().ConfigureAwait(false);
+
+ source.IterationCleanup();
+ }
+ }
+ finally
+ {
+ source.GlobalCleanup();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Benchmarks/GenericHost.Benchmark/GenericHost.Benchmark.csproj b/tests/Benchmarks/GenericHost.Benchmark/GenericHost.Benchmark.csproj
new file mode 100644
index 00000000..ccd12055
--- /dev/null
+++ b/tests/Benchmarks/GenericHost.Benchmark/GenericHost.Benchmark.csproj
@@ -0,0 +1,73 @@
+
+
+
+ net7.0
+ GenericHost.Benchmark
+ SpaceEngineers.Core.GenericHost.Benchmark
+ false
+ false
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ MessageHandlerMiddlewareBenchmarkSource.cs
+
+
+ MessageHandlerMiddlewareBenchmarkSource.cs
+
+
+ MessageHandlerMiddlewareBenchmarkSource.cs
+
+
+ DatabaseConnectionProviderBenchmarkSource.cs
+
+
+ DatabaseConnectionProviderBenchmarkSource.cs
+
+
+ DatabaseConnectionProviderBenchmarkSource.cs
+
+
+ DatabaseConnectionProviderBenchmarkSource.cs
+
+
+ DatabaseConnectionProviderBenchmarkSource.cs
+
+
+ DatabaseConnectionProviderBenchmarkSource.cs
+
+
+ DatabaseConnectionProviderBenchmarkSource.cs
+
+
+ DatabaseConnectionProviderBenchmarkSource.cs
+
+
+
diff --git a/tests/Benchmarks/GenericHost.Benchmark/Program.cs b/tests/Benchmarks/GenericHost.Benchmark/Program.cs
new file mode 100644
index 00000000..a93a5b53
--- /dev/null
+++ b/tests/Benchmarks/GenericHost.Benchmark/Program.cs
@@ -0,0 +1,17 @@
+namespace SpaceEngineers.Core.GenericHost.Benchmark
+{
+ ///
+ /// Program
+ ///
+ public static class Program
+ {
+ /// Main
+ /// args
+ public static void Main(string[] args)
+ {
+ Benchmarks
+ .MessageHandlerMiddlewareBenchmarkTest()
+ .Wait();
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Benchmarks/GenericHost.Benchmark/Properties/AssemblyInfo.cs b/tests/Benchmarks/GenericHost.Benchmark/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..31adb06d
--- /dev/null
+++ b/tests/Benchmarks/GenericHost.Benchmark/Properties/AssemblyInfo.cs
@@ -0,0 +1,5 @@
+using System.Reflection;
+
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
+[assembly: AssemblyInformationalVersion("1.0.0.0")]
\ No newline at end of file
diff --git a/tests/Benchmarks/GenericHost.Benchmark/Settings/DatabaseConnectionProviderBenchmarkSource/appsettings.json b/tests/Benchmarks/GenericHost.Benchmark/Settings/DatabaseConnectionProviderBenchmarkSource/appsettings.json
new file mode 100644
index 00000000..ec5785bb
--- /dev/null
+++ b/tests/Benchmarks/GenericHost.Benchmark/Settings/DatabaseConnectionProviderBenchmarkSource/appsettings.json
@@ -0,0 +1,52 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Debug",
+ "System": "Information",
+ "Microsoft": "Information",
+ "Npgsql": "Information"
+ }
+ },
+ "Transports": {
+ "RabbitMqIntegrationTransport": {
+ "RabbitMqSettings": {
+ "Hosts": [
+ "localhost"
+ ],
+ "Port": "5672",
+ "HttpApiPort": "15672",
+ "User": "guest",
+ "Password": "guest",
+ "VirtualHost": "DatabaseConnectionProviderBenchmarkSource",
+ "ApplicationName": "DatabaseConnectionProviderBenchmarkSource",
+ "ConsumerPrefetchCount": 100,
+ "QueueMaxLengthBytes": 1048576,
+ "ConsumerPriority": 0
+ }
+ },
+ "InMemoryIntegrationTransport": { }
+ },
+ "Endpoints": {
+ "DatabaseConnectionProviderBenchmarkSource": {
+ "GenericEndpointSettings": {
+ "RpcRequestSecondsTimeout": 60
+ },
+ "OrmSettings": {
+ "CommandSecondsTimeout": 10
+ },
+ "OutboxSettings": {
+ "OutboxDeliverySecondsInterval": 60
+ },
+ "SqlDatabaseSettings": {
+ "ApplicationName": "DatabaseConnectionProviderBenchmarkSource",
+ "Host": "localhost",
+ "Port": 5432,
+ "Database": "DatabaseConnectionProviderBenchmarkSource",
+ "IsolationLevel": "ReadCommitted",
+ "Username": "postgres",
+ "Password": "Password12!",
+ "ConnectionPoolSize": 1
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Benchmarks/GenericHost.Benchmark/Settings/MessageHandlerMiddlewareBenchmarkSource/appsettings.json b/tests/Benchmarks/GenericHost.Benchmark/Settings/MessageHandlerMiddlewareBenchmarkSource/appsettings.json
new file mode 100644
index 00000000..bf204b9c
--- /dev/null
+++ b/tests/Benchmarks/GenericHost.Benchmark/Settings/MessageHandlerMiddlewareBenchmarkSource/appsettings.json
@@ -0,0 +1,59 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Debug",
+ "System": "Information",
+ "Microsoft": "Information",
+ "Npgsql": "Information"
+ }
+ },
+ "Transports": {
+ "RabbitMqIntegrationTransport": {
+ "RabbitMqSettings": {
+ "Hosts": [
+ "localhost"
+ ],
+ "Port": "5672",
+ "HttpApiPort": "15672",
+ "User": "guest",
+ "Password": "guest",
+ "VirtualHost": "MessageHandlerMiddlewareBenchmarkSource",
+ "ApplicationName": "MessageHandlerMiddlewareBenchmarkSource",
+ "ConsumerPrefetchCount": 100,
+ "QueueMaxLengthBytes": 1048576,
+ "ConsumerPriority": 0
+ }
+ },
+ "InMemoryIntegrationTransport": { }
+ },
+ "Endpoints": {
+ "AuthEndpoint": {
+ "Authorization": {
+ "Issuer": "Test",
+ "Audience": "Test",
+ "PrivateKey": "db3OIsj+BXE9NZDy0t8W3TcNekrF+2d/1sFnWG4HnV8TZY30iTOdtVWJG8abWvB1GlOgJuQZdcF2Luqm/hccMw=="
+ }
+ },
+ "MessageHandlerMiddlewareBenchmarkSource": {
+ "GenericEndpointSettings": {
+ "RpcRequestSecondsTimeout": 60
+ },
+ "OrmSettings": {
+ "CommandSecondsTimeout": 60
+ },
+ "OutboxSettings": {
+ "OutboxDeliverySecondsInterval": 60
+ },
+ "SqlDatabaseSettings": {
+ "ApplicationName": "MessageHandlerMiddlewareBenchmarkSource",
+ "Host": "localhost",
+ "Port": 5432,
+ "Database": "MessageHandlerMiddlewareBenchmarkSource",
+ "IsolationLevel": "ReadCommitted",
+ "Username": "postgres",
+ "Password": "Password12!",
+ "ConnectionPoolSize": 1
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Benchmarks/GenericHost.Benchmark/Sources/Blog.cs b/tests/Benchmarks/GenericHost.Benchmark/Sources/Blog.cs
new file mode 100644
index 00000000..3dd71663
--- /dev/null
+++ b/tests/Benchmarks/GenericHost.Benchmark/Sources/Blog.cs
@@ -0,0 +1,25 @@
+namespace SpaceEngineers.Core.GenericHost.Benchmark.Sources
+{
+ using System;
+ using System.Collections.Generic;
+ using DataAccess.Orm.Sql.Model;
+ using DataAccess.Orm.Sql.Model.Attributes;
+
+ [Schema(nameof(GenericHost) + nameof(Test))]
+ internal record Blog : BaseDatabaseEntity
+ {
+ public Blog(
+ Guid primaryKey,
+ string theme,
+ IReadOnlyCollection posts)
+ : base(primaryKey)
+ {
+ Theme = theme;
+ Posts = posts;
+ }
+
+ public string Theme { get; set; }
+
+ public IReadOnlyCollection Posts { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/tests/Benchmarks/GenericHost.Benchmark/Sources/Community.cs b/tests/Benchmarks/GenericHost.Benchmark/Sources/Community.cs
new file mode 100644
index 00000000..858e1cf6
--- /dev/null
+++ b/tests/Benchmarks/GenericHost.Benchmark/Sources/Community.cs
@@ -0,0 +1,25 @@
+namespace SpaceEngineers.Core.GenericHost.Benchmark.Sources
+{
+ using System;
+ using System.Collections.Generic;
+ using DataAccess.Orm.Sql.Model;
+ using DataAccess.Orm.Sql.Model.Attributes;
+
+ [Schema(nameof(GenericHost) + nameof(Test))]
+ internal record Community : BaseDatabaseEntity
+ {
+ public Community(
+ Guid primaryKey,
+ string name,
+ IReadOnlyCollection participants)
+ : base(primaryKey)
+ {
+ Name = name;
+ Participants = participants;
+ }
+
+ public string Name { get; set; }
+
+ public IReadOnlyCollection Participants { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/tests/Benchmarks/GenericHost.Benchmark/Sources/ComplexDatabaseEntity.cs b/tests/Benchmarks/GenericHost.Benchmark/Sources/ComplexDatabaseEntity.cs
new file mode 100644
index 00000000..3ea092aa
--- /dev/null
+++ b/tests/Benchmarks/GenericHost.Benchmark/Sources/ComplexDatabaseEntity.cs
@@ -0,0 +1,160 @@
+namespace SpaceEngineers.Core.GenericHost.Benchmark.Sources
+{
+ using System;
+ using System.Diagnostics.CodeAnalysis;
+ using DataAccess.Orm.Sql.Model;
+ using DataAccess.Orm.Sql.Model.Attributes;
+ using GenericEndpoint.Contract.Abstractions;
+
+ [SuppressMessage("Analysis", "SA1011", Justification = "space between square brackets and nullable symbol")]
+ [Schema(nameof(GenericHost) + nameof(Test))]
+ internal record ComplexDatabaseEntity : BaseDatabaseEntity
+ {
+ public ComplexDatabaseEntity(Guid primaryKey)
+ : base(primaryKey)
+ {
+ }
+
+ public double Number { get; set; }
+
+ public double? NullableNumber { get; set; }
+
+ public Guid Identifier { get; set; }
+
+ public Guid? NullableIdentifier { get; set; }
+
+ public bool Boolean { get; set; }
+
+ public bool? NullableBoolean { get; set; }
+
+ public DateTime DateTime { get; set; }
+
+ public DateTime? NullableDateTime { get; set; }
+
+ public TimeSpan TimeSpan { get; set; }
+
+ public TimeSpan? NullableTimeSpan { get; set; }
+
+ public DateOnly DateOnly { get; set; }
+
+ public DateOnly? NullableDateOnly { get; set; }
+
+ public TimeOnly TimeOnly { get; set; }
+
+ public TimeOnly? NullableTimeOnly { get; set; }
+
+ public byte[] ByteArray { get; set; } = default!;
+
+ public string String { get; set; } = default!;
+
+ public string? NullableString { get; set; }
+
+ public EnEnum Enum { get; set; }
+
+ public EnEnum? NullableEnum { get; set; }
+
+ public EnEnumFlags EnumFlags { get; set; }
+
+ public EnEnumFlags? NullableEnumFlags { get; set; }
+
+ public EnEnum[] EnumArray { get; set; } = Array.Empty();
+
+ public EnEnum?[] NullableEnumArray { get; set; } = Array.Empty();
+
+ public string[] StringArray { get; set; } = Array.Empty();
+
+ public string?[] NullableStringArray { get; set; } = Array.Empty();
+
+ public DateTime[] DateTimeArray { get; set; } = Array.Empty();
+
+ public DateTime?[] NullableDateTimeArray { get; set; } = Array.Empty();
+
+ [JsonColumn]
+ public IIntegrationMessage Json { get; set; } = default!;
+
+ [JsonColumn]
+ public IIntegrationMessage? NullableJson { get; set; }
+
+ [ForeignKey(EnOnDeleteBehavior.NoAction)]
+ public Blog Relation { get; set; } = default!;
+
+ [ForeignKey(EnOnDeleteBehavior.NoAction)]
+ public Blog? NullableRelation { get; set; }
+
+ public static ComplexDatabaseEntity Generate(IIntegrationMessage json, Blog relation)
+ {
+ return new ComplexDatabaseEntity(Guid.NewGuid())
+ {
+ Number = 42,
+ NullableNumber = 42,
+ Identifier = Guid.NewGuid(),
+ NullableIdentifier = Guid.NewGuid(),
+ Boolean = true,
+ NullableBoolean = true,
+ DateTime = DateTime.Today,
+ NullableDateTime = DateTime.Today,
+ TimeSpan = TimeSpan.FromHours(3),
+ NullableTimeSpan = TimeSpan.FromHours(3),
+ DateOnly = DateOnly.FromDateTime(DateTime.Today),
+ NullableDateOnly = DateOnly.FromDateTime(DateTime.Today),
+ TimeOnly = TimeOnly.FromTimeSpan(TimeSpan.FromHours(3)),
+ NullableTimeOnly = TimeOnly.FromTimeSpan(TimeSpan.FromHours(3)),
+ ByteArray = new byte[] { 1, 2, 3 },
+ String = "SomeString",
+ NullableString = "SomeNullableString",
+ Enum = EnEnum.Three,
+ NullableEnum = EnEnum.Three,
+ EnumFlags = EnEnumFlags.A | EnEnumFlags.B,
+ NullableEnumFlags = EnEnumFlags.A | EnEnumFlags.B,
+ EnumArray = new[] { EnEnum.One, EnEnum.Two, EnEnum.Three },
+ NullableEnumArray = new EnEnum?[] { EnEnum.One, EnEnum.Two, EnEnum.Three },
+ StringArray = new[] { "SomeString", "AnotherString" },
+ NullableStringArray = new[] { "SomeString", "AnotherString" },
+ DateTimeArray = new[] { DateTime.MaxValue, DateTime.MaxValue },
+ NullableDateTimeArray = new DateTime?[] { DateTime.MaxValue, DateTime.MaxValue },
+ Json = json,
+ NullableJson = json,
+ Relation = relation,
+ NullableRelation = relation
+ };
+ }
+
+ public static ComplexDatabaseEntity GenerateWithNulls(IIntegrationMessage json, Blog relation)
+ {
+ return new ComplexDatabaseEntity(Guid.NewGuid())
+ {
+ Number = 42,
+ NullableNumber = null,
+ Identifier = Guid.NewGuid(),
+ NullableIdentifier = null,
+ Boolean = true,
+ NullableBoolean = null,
+ DateTime = DateTime.Today,
+ NullableDateTime = null,
+ TimeSpan = TimeSpan.FromHours(3),
+ NullableTimeSpan = null,
+ DateOnly = DateOnly.FromDateTime(DateTime.Today),
+ NullableDateOnly = null,
+ TimeOnly = TimeOnly.FromTimeSpan(TimeSpan.FromHours(3)),
+ NullableTimeOnly = null,
+ ByteArray = new byte[] { 1, 2, 3 },
+ String = "SomeString",
+ NullableString = null,
+ Enum = EnEnum.Three,
+ NullableEnum = null,
+ EnumFlags = EnEnumFlags.A | EnEnumFlags.B,
+ NullableEnumFlags = null,
+ EnumArray = new[] { EnEnum.One, EnEnum.Two, EnEnum.Three },
+ NullableEnumArray = new EnEnum?[] { EnEnum.One, null, EnEnum.Three },
+ StringArray = new[] { "SomeString", "AnotherString" },
+ NullableStringArray = new[] { "SomeString", null, "AnotherString" },
+ DateTimeArray = new[] { DateTime.MaxValue, DateTime.MaxValue },
+ NullableDateTimeArray = new DateTime?[] { DateTime.MaxValue, null, DateTime.MaxValue },
+ Json = json,
+ NullableJson = null,
+ Relation = relation,
+ NullableRelation = null
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Benchmarks/GenericHost.Benchmark/Sources/DatabaseConnectionProviderBenchmarkSource.cs b/tests/Benchmarks/GenericHost.Benchmark/Sources/DatabaseConnectionProviderBenchmarkSource.cs
new file mode 100644
index 00000000..8e854fba
--- /dev/null
+++ b/tests/Benchmarks/GenericHost.Benchmark/Sources/DatabaseConnectionProviderBenchmarkSource.cs
@@ -0,0 +1,249 @@
+namespace SpaceEngineers.Core.GenericHost.Benchmark.Sources
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Linq;
+ using System.Reflection;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Basics;
+ using BenchmarkDotNet.Attributes;
+ using BenchmarkDotNet.Engines;
+ using CompositionRoot;
+ using DataAccess.Orm.Sql.Linq;
+ using DataAccess.Orm.Sql.Model;
+ using DataAccess.Orm.Sql.Transaction;
+ using GenericEndpoint.Contract;
+ using GenericEndpoint.DataAccess.Sql.Postgres.Host;
+ using GenericEndpoint.Host;
+ using IntegrationTransport.Host;
+ using Test.Api.ClassFixtures;
+ using IHost = Microsoft.Extensions.Hosting.IHost;
+
+ ///
+ /// IDatabaseConnectionProvider query benchmark source
+ ///
+ [SimpleJob(RunStrategy.Throughput)]
+ [SuppressMessage("Analysis", "CA1506", Justification = "application composition root")]
+ [SuppressMessage("Analysis", "CA1001", Justification = "benchmark source")]
+ public class DatabaseConnectionProviderBenchmarkSource
+ {
+ private CancellationTokenSource? _cts;
+ private IHost? _host;
+ private IDependencyContainer? _dependencyContainer;
+
+ ///
+ /// GlobalSetup
+ ///
+ [GlobalSetup]
+ public void GlobalSetup()
+ {
+ _cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
+
+ var solutionFileDirectory = SolutionExtensions.SolutionFile().Directory
+ ?? throw new InvalidOperationException("Solution directory wasn't found");
+
+ var settingsDirectory = solutionFileDirectory
+ .StepInto("tests")
+ .StepInto("Benchmarks")
+ .StepInto("GenericHost.Benchmark")
+ .StepInto("Settings")
+ .StepInto(nameof(DatabaseConnectionProviderBenchmarkSource));
+
+ var databaseEntities = new[]
+ {
+ typeof(ComplexDatabaseEntity),
+ typeof(Blog),
+ typeof(Post),
+ typeof(User),
+ typeof(Community),
+ typeof(Participant)
+ };
+
+ var startupActions = new[]
+ {
+ typeof(RecreatePostgreSqlDatabaseHostedServiceStartupAction)
+ };
+
+ var additionalOurTypes = databaseEntities
+ .Concat(startupActions)
+ .ToArray();
+
+ var transportIdentity = IntegrationTransport.InMemory.Identity.TransportIdentity();
+
+ var endpointIdentity = new EndpointIdentity(
+ nameof(DatabaseConnectionProviderBenchmarkSource),
+ Assembly.GetEntryAssembly() ?? throw new InvalidOperationException("Unable to get entry assembly"));
+
+ _host = new TestFixture()
+ .CreateHostBuilder()
+ .UseInMemoryIntegrationTransport(transportIdentity)
+ .UseEndpoint(endpointIdentity,
+ builder => builder
+ .WithPostgreSqlDataAccess(options => options
+ .ExecuteMigrations())
+ .ModifyContainerOptions(options => options
+ .WithAdditionalOurTypes(additionalOurTypes))
+ .BuildOptions())
+ .BuildHost(settingsDirectory);
+
+ _host.StartAsync(_cts.Token).Wait(_cts.Token);
+
+ _dependencyContainer = _host.GetEndpointDependencyContainer(endpointIdentity);
+ }
+
+ ///
+ /// GlobalCleanup
+ ///
+ [GlobalCleanup]
+ public void GlobalCleanup()
+ {
+ _host.StopAsync(_cts.Token).Wait();
+ _host.Dispose();
+ _cts.Dispose();
+ }
+
+ ///
+ /// IterationSetup
+ ///
+ [IterationSetup]
+ public void IterationSetup()
+ {
+ Insert().Wait(_cts.Token);
+ }
+
+ ///
+ /// IterationCleanup
+ ///
+ [IterationCleanup]
+ public void IterationCleanup()
+ {
+ Delete().Wait(_cts.Token);
+ }
+
+ /// Read
+ /// Ongoing operation
+ [Benchmark(Description = nameof(Read), Baseline = true)]
+ public async Task Read()
+ {
+ await _dependencyContainer!
+ .InvokeWithinTransaction(true, Producer, _cts.Token)
+ .ConfigureAwait(false);
+
+ static Task Producer(
+ IDatabaseContext transaction,
+ CancellationToken token)
+ {
+ return transaction
+ .All()
+ .CachedExpression("ECF915DD-4CDA-4B6C-ACF5-E497B418D813")
+ .ToListAsync(token);
+ }
+ }
+
+ /// Insert
+ /// Ongoing operation
+ [Benchmark(Description = nameof(Insert))]
+ public async Task Insert()
+ {
+ var user = new User(Guid.NewGuid(), "SpaceEngineer");
+ var posts = new List();
+ var blog = new Blog(Guid.NewGuid(), "MilkyWay", posts);
+ var post = new Post(Guid.NewGuid(), blog, user, DateTime.Now, "PostContent");
+ posts.Add(post);
+
+ var communities = new List();
+ var participants = new List();
+ var community = new Community(Guid.NewGuid(), "AmazingCommunity", participants);
+ var participant = new Participant(Guid.NewGuid(), "RegularParticipant", communities);
+ communities.Add(community);
+ participants.Add(participant);
+
+ var message = new Request();
+ var complexDatabaseEntity = ComplexDatabaseEntity.Generate(message, blog);
+
+ var entities = new IDatabaseEntity[]
+ {
+ user,
+ blog,
+ post,
+ complexDatabaseEntity,
+ community,
+ participant
+ };
+
+ await _dependencyContainer!
+ .InvokeWithinTransaction(true, entities, Producer, _cts.Token)
+ .ConfigureAwait(false);
+
+ static Task Producer(
+ IDatabaseContext transaction,
+ IDatabaseEntity[] entities,
+ CancellationToken token)
+ {
+ return transaction
+ .Insert(entities, EnInsertBehavior.Default)
+ .CachedExpression("22046C75-F23F-4C21-B143-A98687105410")
+ .Invoke(token);
+ }
+ }
+
+ /// Delete
+ /// Ongoing operation
+ [Benchmark(Description = nameof(Delete))]
+ public async Task Delete()
+ {
+ await _dependencyContainer!
+ .InvokeWithinTransaction(true, Producer, _cts.Token)
+ .ConfigureAwait(false);
+
+ static async Task Producer(
+ IDatabaseContext transaction,
+ CancellationToken token)
+ {
+ await transaction
+ .Delete()
+ .Where(_ => true)
+ .CachedExpression("14CBC111-F1B3-41AB-A816-47BF6C88B4EF")
+ .Invoke(token)
+ .ConfigureAwait(false);
+
+ await transaction
+ .Delete()
+ .Where(_ => true)
+ .CachedExpression("14A30D13-91D1-4CDA-8500-5F36C625B2F5")
+ .Invoke(token)
+ .ConfigureAwait(false);
+
+ await transaction
+ .Delete()
+ .Where(_ => true)
+ .CachedExpression("C9A9FAA9-911E-4E7A-AF1F-34E16C36C168")
+ .Invoke(token)
+ .ConfigureAwait(false);
+
+ await transaction
+ .Delete()
+ .Where(_ => true)
+ .CachedExpression("D60B12D2-B738-4E86-BCAA-AE5D701FF8C0")
+ .Invoke(token)
+ .ConfigureAwait(false);
+
+ await transaction
+ .Delete()
+ .Where(_ => true)
+ .CachedExpression("9379254D-97D6-4A96-854C-237220E4DD1E")
+ .Invoke(token)
+ .ConfigureAwait(false);
+
+ await transaction
+ .Delete()
+ .Where(_ => true)
+ .CachedExpression("248F4251-18A0-400B-BC94-62348F78DA71")
+ .Invoke(token)
+ .ConfigureAwait(false);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Benchmarks/GenericHost.Benchmark/Sources/EnEnum.cs b/tests/Benchmarks/GenericHost.Benchmark/Sources/EnEnum.cs
new file mode 100644
index 00000000..39b6900f
--- /dev/null
+++ b/tests/Benchmarks/GenericHost.Benchmark/Sources/EnEnum.cs
@@ -0,0 +1,20 @@
+namespace SpaceEngineers.Core.GenericHost.Benchmark.Sources
+{
+ internal enum EnEnum
+ {
+ ///
+ /// One
+ ///
+ One = 1,
+
+ ///
+ /// Two
+ ///
+ Two = 2,
+
+ ///
+ /// Three
+ ///
+ Three = 4
+ }
+}
\ No newline at end of file
diff --git a/tests/Benchmarks/GenericHost.Benchmark/Sources/EnEnumFlags.cs b/tests/Benchmarks/GenericHost.Benchmark/Sources/EnEnumFlags.cs
new file mode 100644
index 00000000..e4bb05e5
--- /dev/null
+++ b/tests/Benchmarks/GenericHost.Benchmark/Sources/EnEnumFlags.cs
@@ -0,0 +1,23 @@
+namespace SpaceEngineers.Core.GenericHost.Benchmark.Sources
+{
+ using System;
+
+ [Flags]
+ internal enum EnEnumFlags
+ {
+ ///
+ /// A
+ ///
+ A = 1 << 0,
+
+ ///
+ /// B
+ ///
+ B = 1 << 1,
+
+ ///
+ /// c
+ ///
+ C = 1 << 2
+ }
+}
\ No newline at end of file
diff --git a/tests/Benchmarks/GenericHost.Benchmark/Sources/MessageHandlerMiddlewareBenchmarkSource.cs b/tests/Benchmarks/GenericHost.Benchmark/Sources/MessageHandlerMiddlewareBenchmarkSource.cs
new file mode 100644
index 00000000..7ae0ebf7
--- /dev/null
+++ b/tests/Benchmarks/GenericHost.Benchmark/Sources/MessageHandlerMiddlewareBenchmarkSource.cs
@@ -0,0 +1,270 @@
+namespace SpaceEngineers.Core.GenericHost.Benchmark.Sources
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Linq;
+ using System.Reflection;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Basics;
+ using BenchmarkDotNet.Attributes;
+ using BenchmarkDotNet.Engines;
+ using CompositionRoot;
+ using GenericEndpoint.Authorization;
+ using GenericEndpoint.Authorization.Host;
+ using GenericEndpoint.Contract;
+ using GenericEndpoint.DataAccess.Sql.Postgres.Host;
+ using GenericEndpoint.EventSourcing.Host;
+ using GenericEndpoint.Host;
+ using GenericEndpoint.Messaging;
+ using GenericEndpoint.Messaging.MessageHeaders;
+ using GenericEndpoint.Pipeline;
+ using GenericEndpoint.Telemetry;
+ using GenericEndpoint.Telemetry.Host;
+ using IntegrationTransport.Host;
+ using IntegrationTransport.RabbitMQ;
+ using JwtAuthentication;
+ using Test.Api.ClassFixtures;
+ using IHost = Microsoft.Extensions.Hosting.IHost;
+
+ ///
+ /// IMessageHandlerMiddleware benchmark source
+ ///
+ [SimpleJob(RunStrategy.Throughput)]
+ [SuppressMessage("Analysis", "CA1506", Justification = "application composition root")]
+ [SuppressMessage("Analysis", "CA1001", Justification = "benchmark source")]
+ public class MessageHandlerMiddlewareBenchmarkSource
+ {
+ private CancellationTokenSource? _cts;
+ private IHost? _host;
+ private IntegrationMessage? _request;
+ private Func? _messageHandler;
+ private IDependencyContainer? _dependencyContainer;
+ private IMessageHandlerMiddlewareComposite? _messageHandlerMiddleware;
+ private IMessageHandlerMiddleware? _tracingMiddleware;
+ private IMessageHandlerMiddleware? _errorHandlingMiddleware;
+ private IMessageHandlerMiddleware? _authorizationMiddleware;
+ private IMessageHandlerMiddleware? _unitOfWorkMiddleware;
+ private IMessageHandlerMiddleware? _handledByEndpointMiddleware;
+ private IMessageHandlerMiddleware? _requestReplyMiddleware;
+
+ ///
+ /// GlobalSetup
+ ///
+ [GlobalSetup]
+ public void GlobalSetup()
+ {
+ _cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
+
+ var solutionFileDirectory = SolutionExtensions.SolutionFile().Directory
+ ?? throw new InvalidOperationException("Solution directory wasn't found");
+
+ var settingsDirectory = solutionFileDirectory
+ .StepInto("tests")
+ .StepInto("Benchmarks")
+ .StepInto("GenericHost.Benchmark")
+ .StepInto("Settings")
+ .StepInto(nameof(MessageHandlerMiddlewareBenchmarkSource));
+
+ var transportIdentity = IntegrationTransport.InMemory.Identity.TransportIdentity();
+
+ var endpointIdentity = new EndpointIdentity(
+ nameof(MessageHandlerMiddlewareBenchmarkSource),
+ Assembly.GetEntryAssembly() ?? throw new InvalidOperationException("Unable to get entry assembly"));
+
+ _host = new TestFixture()
+ .CreateHostBuilder()
+ .UseInMemoryIntegrationTransport(transportIdentity)
+ .UseEndpoint(endpointIdentity,
+ builder => builder
+ .WithPostgreSqlDataAccess(options => options
+ .ExecuteMigrations())
+ .WithSqlEventSourcing()
+ .WithJwtAuthentication(builder.Context.Configuration)
+ .WithAuthorization()
+ .WithOpenTelemetry()
+ .ModifyContainerOptions(options => options
+ .WithAdditionalOurTypes(typeof(RecreatePostgreSqlDatabaseHostedServiceStartupAction)))
+ .BuildOptions())
+ .UseOpenTelemetry()
+ .BuildHost(settingsDirectory);
+
+ _host.StartAsync(_cts.Token).Wait(_cts.Token);
+
+ _dependencyContainer = _host.GetEndpointDependencyContainer(endpointIdentity);
+
+ var username = "qwerty";
+
+ var authorizationToken = _dependencyContainer
+ .Resolve()
+ .GenerateToken(
+ username,
+ new[]
+ {
+ nameof(MessageHandlerMiddlewareBenchmarkSource)
+ },
+ TimeSpan.FromSeconds(300));
+
+ _request = _dependencyContainer
+ .Resolve()
+ .CreateGeneralMessage(
+ new Request(),
+ typeof(Request),
+ new[] { new Authorization(authorizationToken) },
+ null);
+
+ _messageHandler = static (context, token) => context.Reply((Request)context.Message.Payload, new Reply(), token);
+
+ _messageHandlerMiddleware = _dependencyContainer.Resolve();
+
+ var middlewares = _dependencyContainer.ResolveCollection().ToList();
+
+ _tracingMiddleware = middlewares.Single(middleware => middleware.GetType() == typeof(TracingMiddleware));
+ _errorHandlingMiddleware = middlewares.Single(middleware => middleware.GetType() == typeof(ErrorHandlingMiddleware));
+ _authorizationMiddleware = middlewares.Single(middleware => middleware.GetType() == typeof(AuthorizationMiddleware));
+ _unitOfWorkMiddleware = middlewares.Single(middleware => middleware.GetType() == typeof(UnitOfWorkMiddleware));
+ _handledByEndpointMiddleware = middlewares.Single(middleware => middleware.GetType() == typeof(HandledByEndpointMiddleware));
+ _requestReplyMiddleware = middlewares.Single(middleware => middleware.GetType() == typeof(RequestReplyMiddleware));
+ }
+
+ ///
+ /// GlobalCleanup
+ ///
+ [GlobalCleanup]
+ public void GlobalCleanup()
+ {
+ _host.StopAsync(_cts.Token).Wait();
+ _host.Dispose();
+ _cts.Dispose();
+ }
+
+ ///
+ /// IterationSetup
+ ///
+ [IterationSetup]
+ public void IterationSetup()
+ {
+ _ = _dependencyContainer;
+ }
+
+ ///
+ /// IterationCleanup
+ ///
+ [IterationCleanup]
+ public void IterationCleanup()
+ {
+ var headers = (Dictionary)_request.Headers;
+
+ _ = headers.Remove(typeof(ActualDeliveryDate));
+ _ = headers.Remove(typeof(DeliveryTag));
+ _ = headers.Remove(typeof(HandledBy));
+ _ = headers.Remove(typeof(RejectReason));
+ }
+
+ /// RunCompositeMiddleware
+ /// Ongoing operation
+ [Benchmark(Description = nameof(RunCompositeMiddleware), Baseline = true)]
+ public async Task RunCompositeMiddleware()
+ {
+ await using (_dependencyContainer.OpenScopeAsync().ConfigureAwait(false))
+ {
+ var exclusiveContext = _dependencyContainer.Resolve(_request!);
+
+ await _messageHandlerMiddleware
+ .Handle(exclusiveContext, _messageHandler!, _cts.Token)
+ .ConfigureAwait(false);
+ }
+ }
+
+ /// RunTracingMiddleware
+ /// Ongoing operation
+ [Benchmark(Description = nameof(RunTracingMiddleware))]
+ public async Task RunTracingMiddleware()
+ {
+ await using (_dependencyContainer.OpenScopeAsync().ConfigureAwait(false))
+ {
+ var exclusiveContext = _dependencyContainer.Resolve(_request!);
+
+ await _tracingMiddleware
+ .Handle(exclusiveContext, _messageHandler!, _cts.Token)
+ .ConfigureAwait(false);
+ }
+ }
+
+ /// RunErrorHandlingMiddleware
+ /// Ongoing operation
+ [Benchmark(Description = nameof(RunErrorHandlingMiddleware))]
+ public async Task RunErrorHandlingMiddleware()
+ {
+ await using (_dependencyContainer.OpenScopeAsync().ConfigureAwait(false))
+ {
+ var exclusiveContext = _dependencyContainer.Resolve(_request!);
+
+ await _errorHandlingMiddleware
+ .Handle(exclusiveContext, _messageHandler!, _cts.Token)
+ .ConfigureAwait(false);
+ }
+ }
+
+ /// RunAuthorizationMiddleware
+ /// Ongoing operation
+ [Benchmark(Description = nameof(RunAuthorizationMiddleware))]
+ public async Task RunAuthorizationMiddleware()
+ {
+ await using (_dependencyContainer.OpenScopeAsync().ConfigureAwait(false))
+ {
+ var exclusiveContext = _dependencyContainer.Resolve(_request!);
+
+ await _authorizationMiddleware
+ .Handle(exclusiveContext, _messageHandler!, _cts.Token)
+ .ConfigureAwait(false);
+ }
+ }
+
+ /// RunUnitOfWorkMiddleware
+ /// Ongoing operation
+ [Benchmark(Description = nameof(RunUnitOfWorkMiddleware))]
+ public async Task RunUnitOfWorkMiddleware()
+ {
+ await using (_dependencyContainer.OpenScopeAsync().ConfigureAwait(false))
+ {
+ var exclusiveContext = _dependencyContainer.Resolve(_request!);
+
+ await _unitOfWorkMiddleware
+ .Handle(exclusiveContext, _messageHandler!, _cts.Token)
+ .ConfigureAwait(false);
+ }
+ }
+
+ /// RunHandledByEndpointMiddleware
+ /// Ongoing operation
+ [Benchmark(Description = nameof(RunHandledByEndpointMiddleware))]
+ public async Task RunHandledByEndpointMiddleware()
+ {
+ await using (_dependencyContainer.OpenScopeAsync().ConfigureAwait(false))
+ {
+ var exclusiveContext = _dependencyContainer.Resolve(_request!);
+
+ await _handledByEndpointMiddleware
+ .Handle(exclusiveContext, _messageHandler!, _cts.Token)
+ .ConfigureAwait(false);
+ }
+ }
+
+ /// RunRequestReplyMiddleware
+ /// Ongoing operation
+ [Benchmark(Description = nameof(RunRequestReplyMiddleware))]
+ public async Task RunRequestReplyMiddleware()
+ {
+ await using (_dependencyContainer.OpenScopeAsync().ConfigureAwait(false))
+ {
+ var exclusiveContext = _dependencyContainer.Resolve(_request!);
+
+ await _requestReplyMiddleware
+ .Handle(exclusiveContext, _messageHandler!, _cts.Token)
+ .ConfigureAwait(false);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Benchmarks/GenericHost.Benchmark/Sources/Participant.cs b/tests/Benchmarks/GenericHost.Benchmark/Sources/Participant.cs
new file mode 100644
index 00000000..63a38a47
--- /dev/null
+++ b/tests/Benchmarks/GenericHost.Benchmark/Sources/Participant.cs
@@ -0,0 +1,25 @@
+namespace SpaceEngineers.Core.GenericHost.Benchmark.Sources
+{
+ using System;
+ using System.Collections.Generic;
+ using DataAccess.Orm.Sql.Model;
+ using DataAccess.Orm.Sql.Model.Attributes;
+
+ [Schema(nameof(GenericHost) + nameof(Test))]
+ internal record Participant : BaseDatabaseEntity
+ {
+ public Participant(
+ Guid primaryKey,
+ string name,
+ IReadOnlyCollection communities)
+ : base(primaryKey)
+ {
+ Name = name;
+ Communities = communities;
+ }
+
+ public string Name { get; set; }
+
+ public IReadOnlyCollection Communities { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/tests/Benchmarks/GenericHost.Benchmark/Sources/Post.cs b/tests/Benchmarks/GenericHost.Benchmark/Sources/Post.cs
new file mode 100644
index 00000000..cef60a48
--- /dev/null
+++ b/tests/Benchmarks/GenericHost.Benchmark/Sources/Post.cs
@@ -0,0 +1,34 @@
+namespace SpaceEngineers.Core.GenericHost.Benchmark.Sources
+{
+ using System;
+ using DataAccess.Orm.Sql.Model;
+ using DataAccess.Orm.Sql.Model.Attributes;
+
+ [Schema(nameof(GenericHost) + nameof(Test))]
+ internal record Post : BaseDatabaseEntity
+ {
+ public Post(
+ Guid primaryKey,
+ Blog blog,
+ User user,
+ DateTime dateTime,
+ string text)
+ : base(primaryKey)
+ {
+ Blog = blog;
+ User = user;
+ DateTime = dateTime;
+ Text = text;
+ }
+
+ [ForeignKey(EnOnDeleteBehavior.Cascade)]
+ public Blog Blog { get; set; }
+
+ [ForeignKey(EnOnDeleteBehavior.Restrict)]
+ public User User { get; set; }
+
+ public DateTime DateTime { get; set; }
+
+ public string Text { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/tests/Benchmarks/GenericHost.Benchmark/Sources/RecreatePostgreSqlDatabaseHostedServiceStartupAction.cs b/tests/Benchmarks/GenericHost.Benchmark/Sources/RecreatePostgreSqlDatabaseHostedServiceStartupAction.cs
new file mode 100644
index 00000000..138e6ee1
--- /dev/null
+++ b/tests/Benchmarks/GenericHost.Benchmark/Sources/RecreatePostgreSqlDatabaseHostedServiceStartupAction.cs
@@ -0,0 +1,98 @@
+namespace SpaceEngineers.Core.GenericHost.Benchmark.Sources
+{
+ using System;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using AutoRegistration.Api.Abstractions;
+ using AutoRegistration.Api.Attributes;
+ using AutoRegistration.Api.Enumerations;
+ using Basics;
+ using CompositionRoot;
+ using CrossCuttingConcerns.Settings;
+ using DataAccess.Orm.Sql.Connection;
+ using DataAccess.Orm.Sql.Settings;
+ using DataAccess.Orm.Sql.Translation;
+ using Npgsql;
+
+ [Component(EnLifestyle.Singleton)]
+ internal class RecreatePostgreSqlDatabaseHostedServiceStartupAction : IHostedServiceStartupAction,
+ ICollectionResolvable,
+ ICollectionResolvable,
+ IResolvable
+ {
+ private const string CommandText = @"create extension if not exists dblink;
+
+drop database if exists ""{0}"" with (FORCE);
+create database ""{0}"";
+grant all privileges on database ""{0}"" to ""{1}"";";
+
+ private readonly SqlDatabaseSettings _sqlDatabaseSettings;
+ private readonly IDependencyContainer _dependencyContainer;
+ private readonly IDatabaseConnectionProvider _connectionProvider;
+
+ public RecreatePostgreSqlDatabaseHostedServiceStartupAction(
+ ISettingsProvider sqlDatabaseSettingsProvider,
+ IDependencyContainer dependencyContainer,
+ IDatabaseConnectionProvider connectionProvider)
+ {
+ _sqlDatabaseSettings = sqlDatabaseSettingsProvider.Get();
+
+ _dependencyContainer = dependencyContainer;
+ _connectionProvider = connectionProvider;
+ }
+
+ [SuppressMessage("Analysis", "CA2000", Justification = "IDbConnection will be disposed in outer scope by client")]
+ public async Task Run(CancellationToken token)
+ {
+ var command = new SqlCommand(
+ CommandText.Format(_sqlDatabaseSettings.Database, _sqlDatabaseSettings.Username),
+ Array.Empty());
+
+ var connectionStringBuilder = new NpgsqlConnectionStringBuilder
+ {
+ Host = _sqlDatabaseSettings.Host,
+ Port = _sqlDatabaseSettings.Port,
+ Database = "postgres",
+ Username = _sqlDatabaseSettings.Username,
+ Password = _sqlDatabaseSettings.Password
+ };
+
+ var npgSqlConnection = new NpgsqlConnection(connectionStringBuilder.ConnectionString);
+
+ try
+ {
+ await npgSqlConnection.OpenAsync(token).ConfigureAwait(false);
+
+ _ = await _connectionProvider
+ .Execute(npgSqlConnection, command, token)
+ .ConfigureAwait(false);
+ }
+ finally
+ {
+ npgSqlConnection.Dispose();
+ }
+
+ NpgsqlConnection.ClearPool(npgSqlConnection);
+
+ while (true)
+ {
+ var doesDatabaseExist = await _dependencyContainer
+ .Resolve()
+ .DoesDatabaseExist(token)
+ .ConfigureAwait(false);
+
+ if (!doesDatabaseExist)
+ {
+ await Task
+ .Delay(TimeSpan.FromMilliseconds(100), token)
+ .ConfigureAwait(false);
+ }
+ else
+ {
+ break;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Benchmarks/GenericHost.Benchmark/Sources/Reply.cs b/tests/Benchmarks/GenericHost.Benchmark/Sources/Reply.cs
new file mode 100644
index 00000000..346578e0
--- /dev/null
+++ b/tests/Benchmarks/GenericHost.Benchmark/Sources/Reply.cs
@@ -0,0 +1,13 @@
+namespace SpaceEngineers.Core.GenericHost.Benchmark.Sources
+{
+ using GenericEndpoint.Contract.Abstractions;
+ using GenericEndpoint.Contract.Attributes;
+
+ ///
+ /// Reply
+ ///
+ [Feature(nameof(MessageHandlerMiddlewareBenchmarkSource))]
+ public record Reply : IIntegrationReply
+ {
+ }
+}
\ No newline at end of file
diff --git a/tests/Benchmarks/GenericHost.Benchmark/Sources/Request.cs b/tests/Benchmarks/GenericHost.Benchmark/Sources/Request.cs
new file mode 100644
index 00000000..5340b045
--- /dev/null
+++ b/tests/Benchmarks/GenericHost.Benchmark/Sources/Request.cs
@@ -0,0 +1,14 @@
+namespace SpaceEngineers.Core.GenericHost.Benchmark.Sources
+{
+ using GenericEndpoint.Contract.Abstractions;
+ using GenericEndpoint.Contract.Attributes;
+
+ ///
+ /// Request
+ ///
+ [OwnedBy(nameof(MessageHandlerMiddlewareBenchmarkSource))]
+ [Feature(nameof(MessageHandlerMiddlewareBenchmarkSource))]
+ public record Request : IIntegrationRequest
+ {
+ }
+}
\ No newline at end of file
diff --git a/tests/Benchmarks/GenericHost.Benchmark/Sources/User.cs b/tests/Benchmarks/GenericHost.Benchmark/Sources/User.cs
new file mode 100644
index 00000000..ceffa56a
--- /dev/null
+++ b/tests/Benchmarks/GenericHost.Benchmark/Sources/User.cs
@@ -0,0 +1,18 @@
+namespace SpaceEngineers.Core.GenericHost.Benchmark.Sources
+{
+ using System;
+ using DataAccess.Orm.Sql.Model;
+ using DataAccess.Orm.Sql.Model.Attributes;
+
+ [Schema(nameof(GenericHost) + nameof(Test))]
+ internal record User : BaseDatabaseEntity
+ {
+ public User(Guid primaryKey, string nickname)
+ : base(primaryKey)
+ {
+ Nickname = nickname;
+ }
+
+ public string Nickname { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/tests/Benchmarks/Modules.Benchmark/Benchmarks.cs b/tests/Benchmarks/Modules.Benchmark/Benchmarks.cs
new file mode 100644
index 00000000..406ea903
--- /dev/null
+++ b/tests/Benchmarks/Modules.Benchmark/Benchmarks.cs
@@ -0,0 +1,92 @@
+namespace SpaceEngineers.Core.Modules.Benchmark
+{
+ using Core.Benchmark.Api;
+ using Sources;
+ using Test.Api;
+ using Test.Api.ClassFixtures;
+ using Xunit;
+ using Xunit.Abstractions;
+
+ ///
+ /// Benchmarks
+ ///
+ // TODO: #136 - remove magic numbers and use adaptive approach -> store test artifacts in DB and search performance change points
+ public class Benchmarks : TestBase
+ {
+ /// .cctor
+ /// ITestOutputHelper
+ /// TestFixture
+ public Benchmarks(ITestOutputHelper output, TestFixture fixture)
+ : base(output, fixture)
+ {
+ }
+
+ [Fact(Timeout = 300_000)]
+ internal void DeepCopyBenchmark()
+ {
+ var summary = Benchmark.Run(Output.WriteLine);
+
+ var bySerialization = summary.MillisecondMeasure(
+ nameof(DeepCopyBenchmarkSource.DeepCopyBySerialization),
+ Measure.Mean,
+ Output.WriteLine);
+
+ var byReflection = summary.MillisecondMeasure(
+ nameof(DeepCopyBenchmarkSource.DeepCopyByReflection),
+ Measure.Mean,
+ Output.WriteLine);
+
+ var multiplier = bySerialization / byReflection;
+ Output.WriteLine($"{nameof(multiplier)}: {multiplier:N}");
+
+ Assert.True(bySerialization <= 100m);
+ Assert.True(byReflection <= 50m);
+ }
+
+ [Fact(Timeout = 300_000)]
+ internal void AssembliesExtensionsBelowBenchmark()
+ {
+ var summary = Benchmark.Run(Output.WriteLine);
+
+ var measure = summary.MillisecondMeasure(
+ nameof(AssembliesExtensionsBelowBenchmarkSource.Below),
+ Measure.Mean,
+ Output.WriteLine);
+
+ Assert.True(measure <= 25m);
+ }
+
+ [Fact(Timeout = 300_000)]
+ internal void CompositionRootStartupBenchmark()
+ {
+ var summary = Benchmark.Run(Output.WriteLine);
+
+ var create = summary.MillisecondMeasure(
+ nameof(CompositionRootStartupBenchmarkSource.Create),
+ Measure.Mean,
+ Output.WriteLine);
+
+ Assert.True(create <= 1000m);
+ }
+
+ [Fact(Timeout = 300_000)]
+ internal void StreamCopyVersusStreamReadBenchmark()
+ {
+ var summary = Benchmark.Run(Output.WriteLine);
+
+ var copyTo = summary.MillisecondMeasure(
+ nameof(StreamCopyVersusStreamReadBenchmarkSource.CopyTo),
+ Measure.Mean,
+ Output.WriteLine);
+
+ Assert.True(copyTo <= 10m);
+
+ var read = summary.MillisecondMeasure(
+ nameof(StreamCopyVersusStreamReadBenchmarkSource.Read),
+ Measure.Mean,
+ Output.WriteLine);
+
+ Assert.True(read <= 10m);
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Benchmarks/Modules.Benchmark/Modules.Benchmark.csproj b/tests/Benchmarks/Modules.Benchmark/Modules.Benchmark.csproj
new file mode 100644
index 00000000..6e5ef324
--- /dev/null
+++ b/tests/Benchmarks/Modules.Benchmark/Modules.Benchmark.csproj
@@ -0,0 +1,38 @@
+
+
+
+ net7.0
+ Modules.Benchmark
+ SpaceEngineers.Core.Modules.Benchmark
+ false
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Benchmarks/Modules.Benchmark/Properties/AssemblyInfo.cs b/tests/Benchmarks/Modules.Benchmark/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..31adb06d
--- /dev/null
+++ b/tests/Benchmarks/Modules.Benchmark/Properties/AssemblyInfo.cs
@@ -0,0 +1,5 @@
+using System.Reflection;
+
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
+[assembly: AssemblyInformationalVersion("1.0.0.0")]
\ No newline at end of file
diff --git a/tests/Benchmarks/Modules.Benchmark/Settings/appsettings.json b/tests/Benchmarks/Modules.Benchmark/Settings/appsettings.json
new file mode 100644
index 00000000..7086180d
--- /dev/null
+++ b/tests/Benchmarks/Modules.Benchmark/Settings/appsettings.json
@@ -0,0 +1,10 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "System": "Information",
+ "Microsoft": "Information",
+ "Npgsql": "Information"
+ }
+ }
+}
diff --git a/tests/Benchmarks/Modules.Benchmark/Sources/AssembliesExtensionsBelowBenchmarkSource.cs b/tests/Benchmarks/Modules.Benchmark/Sources/AssembliesExtensionsBelowBenchmarkSource.cs
new file mode 100644
index 00000000..81a7f26a
--- /dev/null
+++ b/tests/Benchmarks/Modules.Benchmark/Sources/AssembliesExtensionsBelowBenchmarkSource.cs
@@ -0,0 +1,39 @@
+namespace SpaceEngineers.Core.Modules.Benchmark.Sources
+{
+ using System;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Reflection;
+ using Basics;
+ using BenchmarkDotNet.Attributes;
+ using BenchmarkDotNet.Engines;
+
+ ///
+ /// AssembliesExtensions.Below(assembly) benchmark source
+ ///
+ [SimpleJob(RunStrategy.Throughput)]
+ public class AssembliesExtensionsBelowBenchmarkSource
+ {
+ [SuppressMessage("Analysis", "SA1011", Justification = "space between square brackets and nullable symbol")]
+ private Assembly[]? _allAssemblies;
+ private Assembly? _belowAssembly;
+
+ private Assembly[] AllAssemblies => _allAssemblies ?? throw new InvalidOperationException(nameof(_allAssemblies));
+
+ private Assembly BelowAssembly => _belowAssembly ?? throw new InvalidOperationException(nameof(_belowAssembly));
+
+ ///
+ /// GlobalSetup
+ ///
+ [GlobalSetup]
+ public void GlobalSetup()
+ {
+ _allAssemblies = AssembliesExtensions.AllAssembliesFromCurrentDomain();
+ _belowAssembly = GetType().Assembly;
+ }
+
+ /// AssembliesExtensions.Below
+ /// Below assemblies
+ [Benchmark(Description = nameof(Below), Baseline = true)]
+ public Assembly[] Below() => AllAssemblies.Below(BelowAssembly);
+ }
+}
\ No newline at end of file
diff --git a/tests/Benchmarks/Modules.Benchmark/Sources/CompositionRootStartupBenchmarkSource.cs b/tests/Benchmarks/Modules.Benchmark/Sources/CompositionRootStartupBenchmarkSource.cs
new file mode 100644
index 00000000..700eeb03
--- /dev/null
+++ b/tests/Benchmarks/Modules.Benchmark/Sources/CompositionRootStartupBenchmarkSource.cs
@@ -0,0 +1,68 @@
+namespace SpaceEngineers.Core.Modules.Benchmark.Sources
+{
+ using System;
+ using System.Diagnostics.CodeAnalysis;
+ using System.IO;
+ using System.Reflection;
+ using Basics;
+ using BenchmarkDotNet.Attributes;
+ using BenchmarkDotNet.Engines;
+ using CompositionRoot;
+ using CrossCuttingConcerns.Settings;
+
+ ///
+ /// Creates DependencyContainer and measures cold start time
+ ///
+ [SimpleJob(RunStrategy.ColdStart)]
+ public class CompositionRootStartupBenchmarkSource
+ {
+ private DirectoryInfo? _settingsDirectory;
+
+ [SuppressMessage("Analysis", "SA1011", Justification = "space between square brackets and nullable symbol")]
+ private Assembly[]? _assemblies;
+
+ private Assembly[] Assemblies => _assemblies ?? throw new InvalidOperationException(nameof(_assemblies));
+
+ ///
+ /// GlobalSetup
+ ///
+ [GlobalSetup]
+ public void GlobalSetup()
+ {
+ var solutionFileDirectory = SolutionExtensions.SolutionFile().Directory
+ ?? throw new InvalidOperationException("Solution directory wasn't found");
+
+ _settingsDirectory = solutionFileDirectory
+ .StepInto("tests")
+ .StepInto("Benchmarks")
+ .StepInto("Modules.Benchmark")
+ .StepInto("Settings");
+
+ _assemblies = new[]
+ {
+ AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SpaceEngineers), nameof(Core), nameof(CrossCuttingConcerns))),
+
+ AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SpaceEngineers), nameof(Core), nameof(DataImport))),
+ AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SpaceEngineers), nameof(Core), nameof(DataExport))),
+
+ AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SpaceEngineers), nameof(Core), nameof(Dynamic))),
+
+ AssembliesExtensions.FindRequiredAssembly(AssembliesExtensions.BuildName(nameof(SpaceEngineers), nameof(Core), nameof(CliArgumentsParser))),
+
+ AssembliesExtensions.FindRequiredAssembly("System.Private.CoreLib")
+ };
+ }
+
+ /// CreateExactlyBounded
+ /// IDependencyContainer
+ [Benchmark(Description = nameof(Create))]
+ public IDependencyContainer Create()
+ {
+ var options = new DependencyContainerOptions()
+ .WithPluginAssemblies(Assemblies)
+ .WithManualRegistrations(new SettingsDirectoryProviderManualRegistration(new SettingsDirectoryProvider(_settingsDirectory!)));
+
+ return DependencyContainer.Create(options);
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Benchmarks/Modules.Benchmark/Sources/DeepCopyBenchmarkSource.cs b/tests/Benchmarks/Modules.Benchmark/Sources/DeepCopyBenchmarkSource.cs
new file mode 100644
index 00000000..85dffa2f
--- /dev/null
+++ b/tests/Benchmarks/Modules.Benchmark/Sources/DeepCopyBenchmarkSource.cs
@@ -0,0 +1,38 @@
+namespace SpaceEngineers.Core.Modules.Benchmark.Sources
+{
+ using System;
+ using Basics;
+ using Basics.Test.DeepCopy;
+ using BenchmarkDotNet.Attributes;
+ using BenchmarkDotNet.Engines;
+
+ ///
+ /// ObjectExtensions.DeepCopy benchmark source
+ ///
+ [SimpleJob(RunStrategy.Throughput)]
+ public class DeepCopyBenchmarkSource
+ {
+ private TestReferenceWithoutSystemTypes? _original;
+
+ private TestReferenceWithoutSystemTypes Original => _original ?? throw new InvalidOperationException(nameof(_original));
+
+ ///
+ /// GlobalSetup
+ ///
+ [GlobalSetup]
+ public void GlobalSetup()
+ {
+ _original = TestReferenceWithoutSystemTypes.CreateOrInit();
+ }
+
+ /// DeepCopyBySerialization
+ /// Copy
+ [Benchmark(Description = nameof(DeepCopyBySerialization), Baseline = true)]
+ public TestReferenceWithoutSystemTypes DeepCopyBySerialization() => Original.DeepCopyBySerialization();
+
+ /// DeepCopyByReflection
+ /// Copy
+ [Benchmark(Description = nameof(DeepCopyByReflection))]
+ public TestReferenceWithoutSystemTypes DeepCopyByReflection() => Original.DeepCopy();
+ }
+}
\ No newline at end of file
diff --git a/tests/Benchmarks/Modules.Benchmark/Sources/StreamCopyVersusStreamReadBenchmarkSource.cs b/tests/Benchmarks/Modules.Benchmark/Sources/StreamCopyVersusStreamReadBenchmarkSource.cs
new file mode 100644
index 00000000..8e747dcf
--- /dev/null
+++ b/tests/Benchmarks/Modules.Benchmark/Sources/StreamCopyVersusStreamReadBenchmarkSource.cs
@@ -0,0 +1,66 @@
+namespace SpaceEngineers.Core.Modules.Benchmark.Sources
+{
+ using System;
+ using System.IO;
+ using Basics;
+ using BenchmarkDotNet.Attributes;
+ using BenchmarkDotNet.Engines;
+
+ ///
+ /// Measures Stream.CopyTo vs. Stream.Read
+ ///
+ [SimpleJob(RunStrategy.Throughput)]
+ public class StreamCopyVersusStreamReadBenchmarkSource
+ {
+ private FileInfo? _file;
+
+ ///
+ /// GlobalSetup
+ ///
+ [GlobalSetup]
+ public void GlobalSetup()
+ {
+ var solutionFileDirectory = SolutionExtensions.SolutionFile().Directory
+ ?? throw new InvalidOperationException("Unable to find solution directory");
+
+ _file = solutionFileDirectory
+ .StepInto("tests")
+ .StepInto("Benchmarks")
+ .StepInto("Modules.Benchmark")
+ .StepInto("Settings")
+ .GetFile("appsettings", ".json");
+ }
+
+ /// CopyTo
+ /// Copy
+ [Benchmark(Description = nameof(CopyTo), Baseline = true)]
+ public ReadOnlyMemory CopyTo()
+ {
+ using (var stream = File.Open(_file.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
+ using (var memoryStream = new MemoryStream())
+ {
+ stream.CopyTo(memoryStream);
+
+ return memoryStream.AsBytes();
+ }
+ }
+
+ /// Read
+ /// Copy
+ [Benchmark(Description = nameof(Read))]
+ public ReadOnlyMemory Read()
+ {
+ using (var stream = File.Open(_file.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
+ {
+ var length = (int)stream.Length;
+ var buffer = new byte[length];
+
+ stream.Position = 0;
+
+ _ = stream.Read(buffer);
+
+ return buffer.AsMemory();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Tests/Basics.Test/AsyncSynchronizationPrimitivesTest.cs b/tests/Tests/Basics.Test/AsyncSynchronizationPrimitivesTest.cs
new file mode 100644
index 00000000..f2320c4f
--- /dev/null
+++ b/tests/Tests/Basics.Test/AsyncSynchronizationPrimitivesTest.cs
@@ -0,0 +1,184 @@
+namespace SpaceEngineers.Core.Basics.Test
+{
+ using System;
+ using System.Linq;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Primitives;
+ using Xunit;
+ using Xunit.Abstractions;
+
+ ///
+ /// Test for async synchronization primitives
+ ///
+ public class AsyncSynchronizationPrimitivesTest : BasicsTestBase
+ {
+ private static readonly TimeSpan TestTimeout = TimeSpan.FromSeconds(1);
+
+ /// .cctor
+ /// ITestOutputHelper
+ public AsyncSynchronizationPrimitivesTest(ITestOutputHelper output)
+ : base(output)
+ {
+ }
+
+ [Fact]
+ internal async Task TaskCancellationCompletionSourceTest()
+ {
+ try
+ {
+ using (var cts = new CancellationTokenSource())
+ using (var tcs = new TaskCancellationCompletionSource