Browse Source

Initial cut (#6022)

Ben Adams 4 years ago
parent
commit
88d4b99d85

+ 37 - 0
frameworks/CSharp/ben/PlatformBenchmarks/BenchmarkApplication.cs

@@ -0,0 +1,37 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Threading.Tasks;
+using PlatformExtensions;
+
+namespace PlatformBenchmarks
+{
+    public partial class BenchmarkApplication
+    {
+#if !DATABASE
+        [Route("/plaintext")]
+        static string PlainText() => "Hello, World!";
+
+        [Route("/json")]
+        static JsonMessage Json() => new JsonMessage { message = "Hello, World!" };
+#else
+        [Route("/db")]
+        static Task<World> SingleQuery() => Db.GetRow();
+
+        [Route("/queries/{count}")]
+        static Task<World[]> MultipleQueries(int count) => Db.GetRows(count);
+
+        [Route("/cached-worlds/{count}")]
+        static Task<World[]> Caching(int count) => Db.GetCachedRows(count);
+
+        [Route("/updates/{count}")]
+        static Task<World[]> Updates(int count) => Db.UpdateAndGetRows(count);
+#endif
+        public static RawDb Db { get; set; }
+    }
+
+    public struct JsonMessage
+    {
+        public string message { get; set; }
+    }
+}

+ 40 - 0
frameworks/CSharp/ben/PlatformBenchmarks/BenchmarkConfigurationHelpers.cs

@@ -0,0 +1,40 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Net;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Configuration;
+using System.IO.Pipelines;
+
+namespace PlatformBenchmarks
+{
+    public static class BenchmarkConfigurationHelpers
+    {
+        public static IPEndPoint CreateIPEndPoint(this IConfiguration config)
+        {
+            var url = config["server.urls"] ?? config["urls"];
+
+            if (string.IsNullOrEmpty(url))
+            {
+                return new IPEndPoint(IPAddress.Loopback, 8080);
+            }
+
+            var address = BindingAddress.Parse(url);
+
+            IPAddress ip;
+
+            if (string.Equals(address.Host, "localhost", StringComparison.OrdinalIgnoreCase))
+            {
+                ip = IPAddress.Loopback;
+            }
+            else if (!IPAddress.TryParse(address.Host, out ip))
+            {
+                ip = IPAddress.IPv6Any;
+            }
+
+            return new IPEndPoint(ip, address.Port);
+        }
+    }
+}

+ 20 - 0
frameworks/CSharp/ben/PlatformBenchmarks/Configuration/AppSettings.cs

@@ -0,0 +1,20 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace PlatformBenchmarks
+{
+    public class AppSettings
+    {
+        public string ConnectionString { get; set; }
+
+        public DatabaseServer Database { get; set; } = DatabaseServer.None;
+    }
+
+    public enum DatabaseServer
+    {
+        None,
+        SqlServer,
+        PostgreSql,
+        MySql
+    }
+}

+ 44 - 0
frameworks/CSharp/ben/PlatformBenchmarks/Data/BatchUpdateString.cs

@@ -0,0 +1,44 @@
+// Copyright (c) .NET Foundation. All rights reserved. 
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 
+
+using System.Linq;
+
+namespace PlatformBenchmarks
+{
+    internal class BatchUpdateString
+    {
+        private const int MaxBatch = 500;
+
+        public static DatabaseServer Database;
+
+        internal static readonly string[] Ids = Enumerable.Range(0, MaxBatch).Select(i => $"@Id_{i}").ToArray();
+        internal static readonly string[] Randoms = Enumerable.Range(0, MaxBatch).Select(i => $"@Random_{i}").ToArray();
+
+        private static string[] _queries = new string[MaxBatch + 1];
+
+        public static string Query(int batchSize)
+        {
+            if (_queries[batchSize] != null)
+            {
+                return _queries[batchSize];
+            }
+
+            var lastIndex = batchSize - 1;
+
+            var sb = StringBuilderCache.Acquire();
+
+            if (Database == DatabaseServer.PostgreSql)
+            {
+                sb.Append("UPDATE world SET randomNumber = temp.randomNumber FROM (VALUES ");
+                Enumerable.Range(0, lastIndex).ToList().ForEach(i => sb.Append($"(@Id_{i}, @Random_{i}), "));
+                sb.Append($"(@Id_{lastIndex}, @Random_{lastIndex}) ORDER BY 1) AS temp(id, randomNumber) WHERE temp.id = world.id");
+            }
+            else
+            {
+                Enumerable.Range(0, batchSize).ToList().ForEach(i => sb.Append($"UPDATE world SET randomnumber = @Random_{i} WHERE id = @Id_{i};"));
+            }
+
+            return _queries[batchSize] = StringBuilderCache.GetStringAndRelease(sb);
+        }
+    }
+}

+ 17 - 0
frameworks/CSharp/ben/PlatformBenchmarks/Data/CachedWorld.cs

@@ -0,0 +1,17 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Runtime.InteropServices;
+
+namespace PlatformBenchmarks
+{
+    public class CachedWorld
+    {
+        public int Id { get; set; }
+
+        public int RandomNumber { get; set; }
+
+        public static implicit operator World(CachedWorld world) => new World { Id = world.Id, RandomNumber = world.RandomNumber };
+        public static implicit operator CachedWorld(World world) => new CachedWorld { Id = world.Id, RandomNumber = world.RandomNumber };
+    }
+}

+ 25 - 0
frameworks/CSharp/ben/PlatformBenchmarks/Data/Fortune.cs

@@ -0,0 +1,25 @@
+// Copyright (c) .NET Foundation. All rights reserved. 
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 
+
+using System;
+
+namespace PlatformBenchmarks
+{
+    public readonly struct Fortune : IComparable<Fortune>, IComparable
+    {
+        public Fortune(int id, string message)
+        {
+            Id = id;
+            Message = message;
+        }
+
+        public int Id { get; }
+
+        public string Message { get; }
+
+        public int CompareTo(object obj) => throw new InvalidOperationException("The non-generic CompareTo should not be used");
+
+        // Performance critical, using culture insensitive comparison
+        public int CompareTo(Fortune other) => string.CompareOrdinal(Message, other.Message);
+    }
+}

+ 32 - 0
frameworks/CSharp/ben/PlatformBenchmarks/Data/Random.cs

@@ -0,0 +1,32 @@
+// Copyright (c) .NET Foundation. All rights reserved. 
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 
+
+using System;
+using System.Runtime.CompilerServices;
+using System.Threading;
+
+namespace PlatformBenchmarks
+{
+    public class ConcurrentRandom
+    {
+        private static int nextSeed = 0;
+
+        // Random isn't thread safe
+        [ThreadStatic]
+        private static Random _random;
+
+        private static Random Random => _random ?? CreateRandom();
+
+        [MethodImpl(MethodImplOptions.NoInlining)]
+        private static Random CreateRandom()
+        {
+            _random = new Random(Interlocked.Increment(ref nextSeed));
+            return _random;
+        }
+
+        public int Next(int minValue, int maxValue)
+        {
+            return Random.Next(minValue, maxValue);
+        }
+    }
+}

+ 262 - 0
frameworks/CSharp/ben/PlatformBenchmarks/Data/RawDb.cs

@@ -0,0 +1,262 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Caching.Memory;
+using Npgsql;
+
+namespace PlatformBenchmarks
+{
+    public class RawDb
+    {
+        private readonly ConcurrentRandom _random;
+        private readonly string _connectionString;
+        private readonly MemoryCache _cache = new MemoryCache(
+            new MemoryCacheOptions()
+            {
+                ExpirationScanFrequency = TimeSpan.FromMinutes(60)
+            });
+
+        public RawDb(ConcurrentRandom random, AppSettings appSettings)
+        {
+            _random = random;
+            _connectionString = appSettings.ConnectionString;
+        }
+
+        public async Task<World> GetRow()
+        {
+            using (var db = new NpgsqlConnection(_connectionString))
+            {
+                await db.OpenAsync();
+
+                var (cmd, _) = CreateReadCommand(db);
+                using (cmd)
+                {
+                    return await ReadSingleRow(cmd);
+                }
+            }
+        }
+
+        public async Task<World[]> GetRows(int count)
+        {
+            var result = new World[count];
+
+            using (var db = new NpgsqlConnection(_connectionString))
+            {
+                await db.OpenAsync();
+
+                var (cmd, idParameter) = CreateReadCommand(db);
+                using (cmd)
+                {
+                    for (int i = 0; i < result.Length; i++)
+                    {
+                        result[i] = await ReadSingleRow(cmd);
+                        idParameter.TypedValue = _random.Next(1, 10001);
+                    }
+                }
+            }
+
+            return result;
+        }
+
+        public Task<World[]> GetCachedRows(int count)
+        {
+            var result = new World[count];
+            var cacheKeys = _cacheKeys;
+            var cache = _cache;
+            var random = _random;
+            for (var i = 0; i < result.Length; i++)
+            {
+                var id = random.Next(1, 10001);
+                var key = cacheKeys[id];
+                var data = cache.Get<CachedWorld>(key);
+
+                if (data != null)
+                {
+                    result[i] = data;
+                }
+                else
+                {
+                    return GetUncachedRows(id, i, count, this, result);
+                }
+            }
+
+            return Task.FromResult(result);
+
+            static async Task<World[]> GetUncachedRows(int id, int i, int count, RawDb rawdb, World[] result)
+            {
+                using (var db = new NpgsqlConnection(rawdb._connectionString))
+                {
+                    await db.OpenAsync();
+
+                    var (cmd, idParameter) = rawdb.CreateReadCommand(db);
+                    using (cmd)
+                    {
+                        Func<ICacheEntry, Task<CachedWorld>> create = async (entry) => 
+                        {
+                            return await rawdb.ReadSingleRow(cmd);
+                        };
+
+                        var cacheKeys = _cacheKeys;
+                        var key = cacheKeys[id];
+
+                        idParameter.TypedValue = id;
+
+                        for (; i < result.Length; i++)
+                        {
+                            var data = await rawdb._cache.GetOrCreateAsync<CachedWorld>(key, create);
+                            result[i] = data;
+
+                            id = rawdb._random.Next(1, 10001);
+                            idParameter.TypedValue = id;
+                            key = cacheKeys[id];
+                        }
+                    }
+                }
+
+                return result;
+            }
+        }
+
+        public async Task PopulateCache()
+        {
+            using (var db = new NpgsqlConnection(_connectionString))
+            {
+                await db.OpenAsync();
+
+                var (cmd, idParameter) = CreateReadCommand(db);
+                using (cmd)
+                {
+                    var cacheKeys = _cacheKeys;
+                    var cache = _cache;
+                    for (var i = 1; i < 10001; i++)
+                    {
+                        idParameter.TypedValue = i;
+                        cache.Set<CachedWorld>(cacheKeys[i], await ReadSingleRow(cmd));
+                    }
+                }
+            }
+
+            Console.WriteLine("Caching Populated");
+        }
+
+        public async Task<World[]> UpdateAndGetRows(int count)
+        {
+            var results = new World[count];
+
+            using (var db = new NpgsqlConnection(_connectionString))
+            {
+                await db.OpenAsync();
+
+                var (queryCmd, queryParameter) = CreateReadCommand(db);
+                using (queryCmd)
+                {
+                    for (int i = 0; i < results.Length; i++)
+                    {
+                        results[i] = await ReadSingleRow(queryCmd);
+                        queryParameter.TypedValue = _random.Next(1, 10001);
+                    }
+                }
+
+                using (var updateCmd = new NpgsqlCommand(BatchUpdateString.Query(count), db))
+                {
+                    var ids = BatchUpdateString.Ids;
+                    var randoms = BatchUpdateString.Randoms;
+
+                    for (int i = 0; i < results.Length; i++)
+                    {
+                        var randomNumber = _random.Next(1, 10001);
+
+                        updateCmd.Parameters.Add(new NpgsqlParameter<int>(parameterName: ids[i], value: results[i].Id));
+                        updateCmd.Parameters.Add(new NpgsqlParameter<int>(parameterName: randoms[i], value: randomNumber));
+
+                        results[i].RandomNumber = randomNumber;
+                    }
+
+                    await updateCmd.ExecuteNonQueryAsync();
+                }
+            }
+
+            return results;
+        }
+
+        public async Task<List<Fortune>> LoadFortunesRows()
+        {
+            var result = new List<Fortune>(20);
+
+            using (var db = new NpgsqlConnection(_connectionString))
+            {
+                await db.OpenAsync();
+
+                using (var cmd = new NpgsqlCommand("SELECT id, message FROM fortune", db))
+                using (var rdr = await cmd.ExecuteReaderAsync())
+                {
+                    while (await rdr.ReadAsync())
+                    {
+                        result.Add(new Fortune
+                        (
+                            id:rdr.GetInt32(0),
+                            message: rdr.GetString(1)
+                        ));
+                    }
+                }
+            }
+
+            result.Add(new Fortune(id: 0, message: "Additional fortune added at request time." ));
+            result.Sort();
+
+            return result;
+        }
+
+        private (NpgsqlCommand readCmd, NpgsqlParameter<int> idParameter) CreateReadCommand(NpgsqlConnection connection)
+        {
+            var cmd = new NpgsqlCommand("SELECT id, randomnumber FROM world WHERE id = @Id", connection);
+            var parameter = new NpgsqlParameter<int>(parameterName: "@Id", value: _random.Next(1, 10001));
+
+            cmd.Parameters.Add(parameter);
+
+            return (cmd, parameter);
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private async Task<World> ReadSingleRow(NpgsqlCommand cmd)
+        {
+            using (var rdr = await cmd.ExecuteReaderAsync(System.Data.CommandBehavior.SingleRow))
+            {
+                await rdr.ReadAsync();
+
+                return new World
+                {
+                    Id = rdr.GetInt32(0),
+                    RandomNumber = rdr.GetInt32(1)
+                };
+            }
+        }
+
+        private static readonly object[] _cacheKeys = Enumerable.Range(0, 10001).Select((i) => new CacheKey(i)).ToArray();
+
+        public sealed class CacheKey : IEquatable<CacheKey>
+        {
+            private readonly int _value;
+
+            public CacheKey(int value)
+                => _value = value;
+
+            public bool Equals(CacheKey key)
+                => key._value == _value;
+
+            public override bool Equals(object obj) 
+                => ReferenceEquals(obj, this);
+
+            public override int GetHashCode()
+                => _value;
+
+            public override string ToString()
+                => _value.ToString();
+        }
+    }
+}

+ 59 - 0
frameworks/CSharp/ben/PlatformBenchmarks/Data/StringBuilderCache.cs

@@ -0,0 +1,59 @@
+// Copyright (c) .NET Foundation. All rights reserved. 
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 
+
+using System;
+using System.Text;
+
+namespace PlatformBenchmarks
+{
+    internal static class StringBuilderCache
+    {
+        private const int DefaultCapacity = 1386;
+        private const int MaxBuilderSize = DefaultCapacity * 3;
+
+        [ThreadStatic]
+        private static StringBuilder t_cachedInstance;
+
+        /// <summary>Get a StringBuilder for the specified capacity.</summary>
+        /// <remarks>If a StringBuilder of an appropriate size is cached, it will be returned and the cache emptied.</remarks>
+        public static StringBuilder Acquire(int capacity = DefaultCapacity)
+        {
+            if (capacity <= MaxBuilderSize)
+            {
+                StringBuilder sb = t_cachedInstance;
+                if (capacity < DefaultCapacity)
+                {
+                    capacity = DefaultCapacity;
+                }
+
+                if (sb != null)
+                {
+                    // Avoid stringbuilder block fragmentation by getting a new StringBuilder
+                    // when the requested size is larger than the current capacity
+                    if (capacity <= sb.Capacity)
+                    {
+                        t_cachedInstance = null;
+                        sb.Clear();
+                        return sb;
+                    }
+                }
+            }
+            return new StringBuilder(capacity);
+        }
+
+        public static void Release(StringBuilder sb)
+        {
+            if (sb.Capacity <= MaxBuilderSize)
+            {
+                t_cachedInstance = sb;
+            }
+        }
+
+        public static string GetStringAndRelease(StringBuilder sb)
+        {
+            string result = sb.ToString();
+            Release(sb);
+            return result;
+        }
+    }
+}

+ 15 - 0
frameworks/CSharp/ben/PlatformBenchmarks/Data/World.cs

@@ -0,0 +1,15 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Runtime.InteropServices;
+
+namespace PlatformBenchmarks
+{
+    [StructLayout(LayoutKind.Sequential, Size = 8)]
+    public struct World
+    {
+        public int Id { get; set; }
+
+        public int RandomNumber { get; set; }
+    }
+}

+ 26 - 0
frameworks/CSharp/ben/PlatformBenchmarks/PlatformBenchmarks.csproj

@@ -0,0 +1,26 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+  <PropertyGroup>
+    <TargetFramework>net5.0</TargetFramework>
+    <OutputType>Exe</OutputType>
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    <LangVersion>preview</LangVersion>
+    <NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
+  </PropertyGroup>
+  
+  <PropertyGroup>
+    <DefineConstants Condition=" '$(IsDatabase)' == 'true' ">$(DefineConstants);DATABASE</DefineConstants>
+  </PropertyGroup>
+  
+  <ItemGroup>
+    <None Include="appsettings.json" CopyToOutputDirectory="PreserveNewest" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Npgsql" Version="5.0.0-alpha1" />
+    <PackageReference Include="Ben.AspNetCore.PlatformExtensions" Version="0.0.4">
+      <PrivateAssets>all</PrivateAssets>
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+    </PackageReference>
+  </ItemGroup>
+</Project>

+ 86 - 0
frameworks/CSharp/ben/PlatformBenchmarks/Program.cs

@@ -0,0 +1,86 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Threading.Tasks;
+
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+#if DATABASE
+using Npgsql;
+#endif
+
+using PlatformExtensions;
+
+namespace PlatformBenchmarks
+{
+    public class Program
+    {
+        public static async Task Main(string[] args)
+        {
+            Console.WriteLine("ASP.NET Core Platform Level");
+            foreach (var path in BenchmarkApplication.Paths.Enabled)
+            {
+                Console.WriteLine(path);
+            }
+
+            var host = BuildWebHost(args);
+            var config = (IConfiguration)host.Services.GetService(typeof(IConfiguration));
+            BatchUpdateString.Database = config.Get<AppSettings>().Database;
+#if DATABASE
+            await BenchmarkApplication.Db.PopulateCache();
+#endif
+            await host.RunAsync();
+        }
+
+        public static IWebHost BuildWebHost(string[] args)
+        {
+            var config = new ConfigurationBuilder()
+                .AddJsonFile("appsettings.json")
+                .AddEnvironmentVariables()
+                .AddEnvironmentVariables(prefix: "ASPNETCORE_")
+                .AddCommandLine(args)
+                .Build();
+
+            var appSettings = config.Get<AppSettings>();
+#if DATABASE
+            Console.WriteLine($"Database: {appSettings.Database}");
+            Console.WriteLine($"ConnectionString: {appSettings.ConnectionString}");
+
+            if (appSettings.Database == DatabaseServer.PostgreSql)
+            {
+                BenchmarkApplication.Db = new RawDb(new ConcurrentRandom(), appSettings);
+            }
+            else
+            {
+                throw new NotSupportedException($"{appSettings.Database} is not supported");
+            }
+#endif
+
+            var host = new WebHostBuilder()
+                .UseSockets(options =>
+                {
+                    options.WaitForDataBeforeAllocatingBuffer = false;
+                })
+                .UseKestrel((context, options) =>
+                {
+                    var endPoint = context.Configuration.CreateIPEndPoint();
+
+                    options.Listen(endPoint, builder =>
+                    {
+                        builder.UseHttpApplication<BenchmarkApplication>();
+                    });
+                })
+                .UseStartup<Program>()
+                .Build();
+
+            return host;
+        }
+
+        public void Configure(IApplicationBuilder app)
+        {
+
+        }
+    }
+}

+ 3 - 0
frameworks/CSharp/ben/PlatformBenchmarks/appsettings.json

@@ -0,0 +1,3 @@
+{
+  "ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=aspnetcore-Benchmarks;Trusted_Connection=True;MultipleActiveResultSets=true"
+}

+ 4 - 0
frameworks/CSharp/ben/PlatformBenchmarks/appsettings.postgresql.json

@@ -0,0 +1,4 @@
+{
+  "ConnectionString": "Server=tfb-database;Database=hello_world;User Id=benchmarkdbuser;Password=benchmarkdbpass;Maximum Pool Size=2;NoResetOnClose=true;Enlist=false;Max Auto Prepare=4;Multiplexing=true;Write Coalescing Delay Us=500;Write Coalescing Buffer Threshold Bytes=5500",
+  "Database": "postgresql"
+}

+ 4 - 0
frameworks/CSharp/ben/PlatformBenchmarks/appsettings.postgresql.updates.json

@@ -0,0 +1,4 @@
+{
+  "ConnectionString": "Server=tfb-database;Database=hello_world;User Id=benchmarkdbuser;Password=benchmarkdbpass;Maximum Pool Size=2;NoResetOnClose=true;Enlist=false;Max Auto Prepare=3;Multiplexing=true;Write Coalescing Delay Us=500;Write Coalescing Buffer Threshold Bytes=5500",
+  "Database": "postgresql"
+}

+ 35 - 0
frameworks/CSharp/ben/README.md

@@ -0,0 +1,35 @@
+# ASP.NET Core Source Generator Test
+
+See [.NET Core](http://dot.net) and [ASP.NET Core](https://github.com/aspnet) for more information.
+
+Using C# source generators to transform `Route` [annotated methods](PlatformBenchmarks/BenchmarkApplication.cs)
+into a Platform level test.
+
+## Infrastructure Software Versions
+
+**Language**
+
+* C# 9.0
+
+**Platforms**
+
+* .NET 5.0 (Windows and Linux)
+
+**Web Servers**
+
+* [Kestrel](https://github.com/dotnet/aspnetcore/tree/master/src/Servers/Kestrel)
+
+**Web Stack**
+
+* ASP.NET Core
+* [Ben.AspNetCore.PlatformExtensions](https://github.com/benaadams/Ben.AspNetCore.PlatformExtensions)
+
+## Paths & Source for Tests
+
+All tests are in a single file
+
+* [Plaintext](PlatformBenchmarks/BenchmarkApplication.cs): "/plaintext"
+* [JSON Serialization](PlatformBenchmarks/BenchmarkApplication.cs): "/json"
+* [Single Query Raw](PlatformBenchmarks/BenchmarkApplication.cs): "/db"
+* [Multiple Queries Raw](PlatformBenchmarks/BenchmarkApplication.cs): "/queries/"
+* [Data Updates Raw](PlatformBenchmarks/BenchmarkApplication.cs): "/updates/"

+ 12 - 0
frameworks/CSharp/ben/ben-pg-up.dockerfile

@@ -0,0 +1,12 @@
+FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
+WORKDIR /app
+COPY PlatformBenchmarks .
+RUN dotnet publish -c Release -o out /p:IsDatabase=true
+
+FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS runtime
+ENV ASPNETCORE_URLS http://+:8080
+WORKDIR /app
+COPY --from=build /app/out ./
+COPY PlatformBenchmarks/appsettings.postgresql.updates.json ./appsettings.json
+
+ENTRYPOINT ["dotnet", "PlatformBenchmarks.dll"]

+ 12 - 0
frameworks/CSharp/ben/ben-pg.dockerfile

@@ -0,0 +1,12 @@
+FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
+WORKDIR /app
+COPY PlatformBenchmarks .
+RUN dotnet publish -c Release -o out /p:IsDatabase=true
+
+FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS runtime
+ENV ASPNETCORE_URLS http://+:8080
+WORKDIR /app
+COPY --from=build /app/out ./
+COPY PlatformBenchmarks/appsettings.postgresql.json ./appsettings.json
+
+ENTRYPOINT ["dotnet", "PlatformBenchmarks.dll"]

+ 11 - 0
frameworks/CSharp/ben/ben.dockerfile

@@ -0,0 +1,11 @@
+FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
+WORKDIR /app
+COPY PlatformBenchmarks .
+RUN dotnet publish -c Release -o out
+
+FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS runtime
+ENV ASPNETCORE_URLS http://+:8080
+WORKDIR /app
+COPY --from=build /app/out ./
+
+ENTRYPOINT ["dotnet", "PlatformBenchmarks.dll"]

+ 62 - 0
frameworks/CSharp/ben/benchmark_config.json

@@ -0,0 +1,62 @@
+{
+  "framework": "ben",
+  "tests": [{
+    "default": {
+      "plaintext_url": "/plaintext",
+      "json_url": "/json",
+      "port": 8080,
+      "approach": "Stripped",
+      "classification": "Platform",
+      "database": "None",
+      "framework": "ASP.NET Core",
+      "language": "C#",
+      "orm": "Raw",
+      "platform": ".NET",
+      "flavor": "CoreCLR",
+      "webserver": "Kestrel",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "ASP.NET Core",
+      "notes": "",
+      "versus": "aspcore"
+    },
+    "pg": {
+      "db_url": "/db",
+      "query_url": "/queries/",
+      "cached_query_url": "/cached-worlds/",
+      "port": 8080,
+      "approach": "Stripped",
+      "classification": "Platform",
+      "database": "Postgres",
+      "framework": "ASP.NET Core",
+      "language": "C#",
+      "orm": "Raw",
+      "platform": ".NET",
+      "flavor": "CoreCLR",
+      "webserver": "Kestrel",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "ASP.NET Core, ADO.NET",
+      "notes": "",
+      "versus": "aspcore-ado-pg"
+    },
+    "pg-up": {
+      "update_url": "/updates/",
+      "port": 8080,
+      "approach": "Stripped",
+      "classification": "Platform",
+      "database": "Postgres",
+      "framework": "ASP.NET Core",
+      "language": "C#",
+      "orm": "Raw",
+      "platform": ".NET",
+      "flavor": "CoreCLR",
+      "webserver": "Kestrel",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "ASP.NET Core, ADO.NET",
+      "notes": "",
+      "versus": "aspcore-ado-pg-up"
+    }
+  }]
+}