Procházet zdrojové kódy

Updates to aspnetcore PG database access (#8005)

* Some code cleanup

* Updates to aspnetcore PG database access

* Use batching with Sync error barriers in Updates
* Use NpgsqlDataSource
* Use typed NpgsqlParameter<T> everywhere
* Use positional parameter placeholders everywhere ($1, $2)

* Stop UTF8 decoding/reencoding in Fortunes platform

* Update Fortunes to use Razor templating

* Turn of SQL parsing/rewriting for Fortunes
Shay Rojansky před 2 roky
rodič
revize
38ee29013e
23 změnil soubory, kde provedl 573 přidání a 231 odebrání
  1. 1 1
      frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.Caching.cs
  2. 41 31
      frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.Fortunes.cs
  3. 12 1
      frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.HttpConnection.cs
  4. 1 1
      frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.Json.cs
  5. 3 3
      frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.MultipleQueries.cs
  6. 2 2
      frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.SingleQuery.cs
  7. 3 3
      frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.Updates.cs
  8. 62 42
      frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.cs
  9. 1 1
      frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkConfigurationHelpers.cs
  10. 2 2
      frameworks/CSharp/aspnetcore/PlatformBenchmarks/BufferWriter.cs
  11. 241 0
      frameworks/CSharp/aspnetcore/PlatformBenchmarks/ChunkedBufferWriter.cs
  12. 13 9
      frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/BatchUpdateString.cs
  13. 3 3
      frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/FortuneUtf16.cs
  14. 22 0
      frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/FortuneUtf8.cs
  15. 4 4
      frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/Providers/RawDbMySqlConnector.cs
  16. 111 107
      frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/Providers/RawDbNpgsql.cs
  17. 1 4
      frameworks/CSharp/aspnetcore/PlatformBenchmarks/DateHeader.cs
  18. 2 1
      frameworks/CSharp/aspnetcore/PlatformBenchmarks/PlatformBenchmarks.csproj
  19. 33 15
      frameworks/CSharp/aspnetcore/PlatformBenchmarks/Program.cs
  20. 2 0
      frameworks/CSharp/aspnetcore/PlatformBenchmarks/Templates/FortunesUtf16.cshtml
  21. 2 0
      frameworks/CSharp/aspnetcore/PlatformBenchmarks/Templates/FortunesUtf8.cshtml
  22. 9 0
      frameworks/CSharp/aspnetcore/PlatformBenchmarks/Templates/_ViewImports.cshtml
  23. 2 1
      frameworks/CSharp/aspnetcore/PlatformBenchmarks/appsettings.json

+ 1 - 1
frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.Caching.cs

@@ -10,7 +10,7 @@ namespace PlatformBenchmarks;
 
 
 public partial class BenchmarkApplication
 public partial class BenchmarkApplication
 {
 {
-    private async Task Caching(PipeWriter pipeWriter, int count)
+    private static async Task Caching(PipeWriter pipeWriter, int count)
     {
     {
         OutputMultipleQueries(pipeWriter, await Db.LoadCachedQueries(count), SerializerContext.CachedWorldArray);
         OutputMultipleQueries(pipeWriter, await Db.LoadCachedQueries(count), SerializerContext.CachedWorldArray);
     }
     }

+ 41 - 31
frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.Fortunes.cs

@@ -3,53 +3,63 @@
 
 
 #if DATABASE
 #if DATABASE
 
 
-using System.Collections.Generic;
 using System.IO.Pipelines;
 using System.IO.Pipelines;
-using System.Text.Encodings.Web;
-using System.Threading.Tasks;
+using System.Runtime.CompilerServices;
+using RazorSlices;
 
 
 namespace PlatformBenchmarks
 namespace PlatformBenchmarks
 {
 {
     public partial class BenchmarkApplication
     public partial class BenchmarkApplication
     {
     {
-        private static ReadOnlySpan<byte> _fortunesPreamble =>
-            "HTTP/1.1 200 OK\r\n"u8 +
-            "Server: K\r\n"u8 +
-            "Content-Type: text/html; charset=UTF-8\r\n"u8 +
-            "Content-Length: "u8;
-
         private async Task Fortunes(PipeWriter pipeWriter)
         private async Task Fortunes(PipeWriter pipeWriter)
         {
         {
-            OutputFortunes(pipeWriter, await Db.LoadFortunesRows());
+            await OutputFortunes(pipeWriter, await Db.LoadFortunesRows(), FortunesTemplateFactory);
         }
         }
 
 
-        private void OutputFortunes(PipeWriter pipeWriter, List<Fortune> model)
+        private ValueTask OutputFortunes<TModel>(PipeWriter pipeWriter, TModel model, SliceFactory<TModel> templateFactory)
         {
         {
-            var writer = GetWriter(pipeWriter, sizeHint: 1600); // in reality it's 1361
-
-            writer.Write(_fortunesPreamble);
-
-            var lengthWriter = writer;
-            writer.Write(_contentLengthGap);
+            // Render headers
+            var preamble = """
+                HTTP/1.1 200 OK
+                Server: K
+                Content-Type: text/html; charset=utf-8
+                Transfer-Encoding: chunked
+                """u8;
+            var headersLength = preamble.Length + DateHeader.HeaderBytes.Length;
+            var headersSpan = pipeWriter.GetSpan(headersLength);
+            preamble.CopyTo(headersSpan);
+            DateHeader.HeaderBytes.CopyTo(headersSpan[preamble.Length..]);
+            pipeWriter.Advance(headersLength);
 
 
-            // Date header
-            writer.Write(DateHeader.HeaderBytes);
+            // Render body
+            var template = templateFactory(model);
+            // Kestrel PipeWriter span size is 4K, headers above already written to first span & template output is ~1350 bytes,
+            // so 2K chunk size should result in only a single span and chunk being used.
+            var chunkedWriter = GetChunkedWriter(pipeWriter, chunkSizeHint: 2048);
+            var renderTask = template.RenderAsync(chunkedWriter, null, HtmlEncoder);
 
 
-            var bodyStart = writer.Buffered;
-            // Body
-            writer.Write(_fortunesTableStart);
-            foreach (var item in model)
+            if (renderTask.IsCompletedSuccessfully)
             {
             {
-                writer.Write(_fortunesRowStart);
-                writer.WriteNumeric((uint)item.Id);
-                writer.Write(_fortunesColumn);
-                writer.WriteUtf8String(HtmlEncoder.Encode(item.Message));
-                writer.Write(_fortunesRowEnd);
+                renderTask.GetAwaiter().GetResult();
+                EndTemplateRendering(chunkedWriter, template);
+                return ValueTask.CompletedTask;
             }
             }
-            writer.Write(_fortunesTableEnd);
-            lengthWriter.WriteNumeric((uint)(writer.Buffered - bodyStart));
 
 
-            writer.Commit();
+            return AwaitTemplateRenderTask(renderTask, chunkedWriter, template);
+        }
+
+        private static async ValueTask AwaitTemplateRenderTask(ValueTask renderTask, ChunkedBufferWriter<WriterAdapter> chunkedWriter, RazorSlice template)
+        {
+            await renderTask;
+            EndTemplateRendering(chunkedWriter, template);
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private static void EndTemplateRendering(ChunkedBufferWriter<WriterAdapter> chunkedWriter, RazorSlice template)
+        {
+            chunkedWriter.End();
+            ReturnChunkedWriter(chunkedWriter);
+            template.Dispose();
         }
         }
     }
     }
 }
 }

+ 12 - 1
frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.HttpConnection.cs

@@ -251,7 +251,18 @@ public partial class BenchmarkApplication : IHttpConnection
 
 
     [MethodImpl(MethodImplOptions.AggressiveInlining)]
     [MethodImpl(MethodImplOptions.AggressiveInlining)]
     private static BufferWriter<WriterAdapter> GetWriter(PipeWriter pipeWriter, int sizeHint)
     private static BufferWriter<WriterAdapter> GetWriter(PipeWriter pipeWriter, int sizeHint)
-        => new(new WriterAdapter(pipeWriter), sizeHint);
+        => new(new(pipeWriter), sizeHint);
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    private static ChunkedBufferWriter<WriterAdapter> GetChunkedWriter(PipeWriter pipeWriter, int chunkSizeHint)
+    {
+        var writer = ChunkedWriterPool.Get();
+        writer.SetOutput(new WriterAdapter(pipeWriter), chunkSizeHint);
+        return writer;
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    private static void ReturnChunkedWriter(ChunkedBufferWriter<WriterAdapter> writer) => ChunkedWriterPool.Return(writer);
 
 
     private struct WriterAdapter : IBufferWriter<byte>
     private struct WriterAdapter : IBufferWriter<byte>
     {
     {

+ 1 - 1
frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.Json.cs

@@ -25,7 +25,7 @@ public partial class BenchmarkApplication
 
 
         writer.Commit();
         writer.Commit();
 
 
-        Utf8JsonWriter utf8JsonWriter = t_writer ??= new Utf8JsonWriter(bodyWriter, new JsonWriterOptions { SkipValidation = true });
+        var utf8JsonWriter = t_writer ??= new Utf8JsonWriter(bodyWriter, new JsonWriterOptions { SkipValidation = true });
         utf8JsonWriter.Reset(bodyWriter);
         utf8JsonWriter.Reset(bodyWriter);
 
 
         // Body
         // Body

+ 3 - 3
frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.MultipleQueries.cs

@@ -12,7 +12,7 @@ namespace PlatformBenchmarks
 {
 {
     public partial class BenchmarkApplication
     public partial class BenchmarkApplication
     {
     {
-        private async Task MultipleQueries(PipeWriter pipeWriter, int count)
+        private static async Task MultipleQueries(PipeWriter pipeWriter, int count)
         {
         {
             OutputMultipleQueries(pipeWriter, await Db.LoadMultipleQueriesRows(count), SerializerContext.WorldArray);
             OutputMultipleQueries(pipeWriter, await Db.LoadMultipleQueriesRows(count), SerializerContext.WorldArray);
         }
         }
@@ -31,11 +31,11 @@ namespace PlatformBenchmarks
 
 
             writer.Commit();
             writer.Commit();
 
 
-            Utf8JsonWriter utf8JsonWriter = t_writer ??= new Utf8JsonWriter(pipeWriter, new JsonWriterOptions { SkipValidation = true });
+            var utf8JsonWriter = t_writer ??= new Utf8JsonWriter(pipeWriter, new JsonWriterOptions { SkipValidation = true });
             utf8JsonWriter.Reset(pipeWriter);
             utf8JsonWriter.Reset(pipeWriter);
 
 
             // Body
             // Body
-            JsonSerializer.Serialize<TWorld[]>(utf8JsonWriter, rows, jsonTypeInfo);
+            JsonSerializer.Serialize(utf8JsonWriter, rows, jsonTypeInfo);
 
 
             // Content-Length
             // Content-Length
             lengthWriter.WriteNumeric((uint)utf8JsonWriter.BytesCommitted);
             lengthWriter.WriteNumeric((uint)utf8JsonWriter.BytesCommitted);

+ 2 - 2
frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.SingleQuery.cs

@@ -11,7 +11,7 @@ namespace PlatformBenchmarks
 {
 {
     public partial class BenchmarkApplication
     public partial class BenchmarkApplication
     {
     {
-        private async Task SingleQuery(PipeWriter pipeWriter)
+        private static async Task SingleQuery(PipeWriter pipeWriter)
         {
         {
             OutputSingleQuery(pipeWriter, await Db.LoadSingleQueryRow());
             OutputSingleQuery(pipeWriter, await Db.LoadSingleQueryRow());
         }
         }
@@ -30,7 +30,7 @@ namespace PlatformBenchmarks
 
 
             writer.Commit();
             writer.Commit();
 
 
-            Utf8JsonWriter utf8JsonWriter = t_writer ??= new Utf8JsonWriter(pipeWriter, new JsonWriterOptions { SkipValidation = true });
+            var utf8JsonWriter = t_writer ??= new Utf8JsonWriter(pipeWriter, new JsonWriterOptions { SkipValidation = true });
             utf8JsonWriter.Reset(pipeWriter);
             utf8JsonWriter.Reset(pipeWriter);
 
 
             // Body
             // Body

+ 3 - 3
frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.Updates.cs

@@ -11,7 +11,7 @@ namespace PlatformBenchmarks
 {
 {
     public partial class BenchmarkApplication
     public partial class BenchmarkApplication
     {
     {
-        private async Task Updates(PipeWriter pipeWriter, int count)
+        private static async Task Updates(PipeWriter pipeWriter, int count)
         {
         {
             OutputUpdates(pipeWriter, await Db.LoadMultipleUpdatesRows(count));
             OutputUpdates(pipeWriter, await Db.LoadMultipleUpdatesRows(count));
         }
         }
@@ -30,11 +30,11 @@ namespace PlatformBenchmarks
 
 
             writer.Commit();
             writer.Commit();
 
 
-            Utf8JsonWriter utf8JsonWriter = t_writer ??= new Utf8JsonWriter(pipeWriter, new JsonWriterOptions { SkipValidation = true });
+            var utf8JsonWriter = t_writer ??= new Utf8JsonWriter(pipeWriter, new JsonWriterOptions { SkipValidation = true });
             utf8JsonWriter.Reset(pipeWriter);
             utf8JsonWriter.Reset(pipeWriter);
 
 
             // Body
             // Body
-            JsonSerializer.Serialize( utf8JsonWriter, rows, SerializerContext.WorldArray);
+            JsonSerializer.Serialize(utf8JsonWriter, rows, SerializerContext.WorldArray);
 
 
             // Content-Length
             // Content-Length
             lengthWriter.WriteNumeric((uint)utf8JsonWriter.BytesCommitted);
             lengthWriter.WriteNumeric((uint)utf8JsonWriter.BytesCommitted);

+ 62 - 42
frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkApplication.cs

@@ -9,6 +9,8 @@ using System.Text.Json.Serialization;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 
 
 using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
 using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
+using Microsoft.Extensions.ObjectPool;
+using RazorSlices;
 
 
 namespace PlatformBenchmarks;
 namespace PlatformBenchmarks;
 
 
@@ -34,31 +36,49 @@ public sealed partial class BenchmarkApplication
         "Content-Length: "u8;
         "Content-Length: "u8;
 
 
     private static ReadOnlySpan<byte> _plainTextBody => "Hello, World!"u8;
     private static ReadOnlySpan<byte> _plainTextBody => "Hello, World!"u8;
+    private static ReadOnlySpan<byte> _contentLengthGap => "    "u8;
 
 
-    private static readonly JsonContext SerializerContext = JsonContext.Default;
+#if DATABASE
+        public static RawDb Db { get; set; }
+#endif
 
 
-    [JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Serialization, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
-    [JsonSerializable(typeof(JsonMessage))]
-    [JsonSerializable(typeof(CachedWorld[]))]
-    [JsonSerializable(typeof(World[]))]
-    private sealed partial class JsonContext : JsonSerializerContext
+    private static readonly DefaultObjectPool<ChunkedBufferWriter<WriterAdapter>> ChunkedWriterPool
+        = new(new ChunkedWriterObjectPolicy());
+
+    private sealed class ChunkedWriterObjectPolicy : IPooledObjectPolicy<ChunkedBufferWriter<WriterAdapter>>
     {
     {
-    }
+        public ChunkedBufferWriter<WriterAdapter> Create() => new();
 
 
-    private static ReadOnlySpan<byte> _fortunesTableStart => "<!DOCTYPE html><html><head><title>Fortunes</title></head><body><table><tr><th>id</th><th>message</th></tr>"u8;
-    private static ReadOnlySpan<byte> _fortunesRowStart => "<tr><td>"u8;
-    private static ReadOnlySpan<byte> _fortunesColumn => "</td><td>"u8;
-    private static ReadOnlySpan<byte> _fortunesRowEnd => "</td></tr>"u8;
-    private static ReadOnlySpan<byte> _fortunesTableEnd => "</table></body></html>"u8;
-    private static ReadOnlySpan<byte> _contentLengthGap => "    "u8;
+        public bool Return(ChunkedBufferWriter<WriterAdapter> writer)
+        {
+            writer.Reset();
+            return true;
+        }
+    }
 
 
 #if DATABASE
 #if DATABASE
-        public static RawDb Db { get; set; }
+#if NPGSQL
+    private readonly static SliceFactory<List<FortuneUtf8>> FortunesTemplateFactory = RazorSlice.ResolveSliceFactory<List<FortuneUtf8>>("/Templates/FortunesUtf8.cshtml");
+#elif MYSQLCONNECTOR
+    private readonly static SliceFactory<List<FortuneUtf16>> FortunesTemplateFactory = RazorSlice.ResolveSliceFactory<List<FortuneUtf16>>("/Templates/FortunesUtf16.cshtml");
+#else
+#error "DATABASE defined by neither NPGSQL nor MYSQLCONNECTOR are defined"
+#endif
 #endif
 #endif
 
 
     [ThreadStatic]
     [ThreadStatic]
     private static Utf8JsonWriter t_writer;
     private static Utf8JsonWriter t_writer;
 
 
+    private static readonly JsonContext SerializerContext = JsonContext.Default;
+
+    [JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Serialization, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
+    [JsonSerializable(typeof(JsonMessage))]
+    [JsonSerializable(typeof(CachedWorld[]))]
+    [JsonSerializable(typeof(World[]))]
+    private sealed partial class JsonContext : JsonSerializerContext
+    {
+    }
+
     public static class Paths
     public static class Paths
     {
     {
         public static ReadOnlySpan<byte> Json => "/json"u8;
         public static ReadOnlySpan<byte> Json => "/json"u8;
@@ -78,41 +98,41 @@ public sealed partial class BenchmarkApplication
         _requestType = versionAndMethod.Method == Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod.Get ? GetRequestType(startLine.Slice(targetPath.Offset, targetPath.Length), ref _queries) : RequestType.NotRecognized;
         _requestType = versionAndMethod.Method == Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod.Get ? GetRequestType(startLine.Slice(targetPath.Offset, targetPath.Length), ref _queries) : RequestType.NotRecognized;
     }
     }
 
 
-    private RequestType GetRequestType(ReadOnlySpan<byte> path, ref int queries)
+    private static RequestType GetRequestType(ReadOnlySpan<byte> path, ref int queries)
     {
     {
 #if !DATABASE
 #if !DATABASE
         if (path.Length == 10 && path.SequenceEqual(Paths.Plaintext))
         if (path.Length == 10 && path.SequenceEqual(Paths.Plaintext))
         {
         {
             return RequestType.PlainText;
             return RequestType.PlainText;
         }
         }
-        else if (path.Length == 5 && path.SequenceEqual(Paths.Json))
+        if (path.Length == 5 && path.SequenceEqual(Paths.Json))
         {
         {
             return RequestType.Json;
             return RequestType.Json;
         }
         }
 #else
 #else
-            if (path.Length == 3 && path[0] == '/' && path[1] == 'd' && path[2] == 'b')
-            {
-                return RequestType.SingleQuery;
-            }
-            else if (path.Length == 9 && path[1] == 'f' && path.SequenceEqual(Paths.Fortunes))
-            {
-                return RequestType.Fortunes;
-            }
-            else if (path.Length >= 15 && path[1] == 'c' && path.StartsWith(Paths.Caching))
-            {
-                queries = ParseQueries(path.Slice(15));
-                return RequestType.Caching;
-            }
-            else if (path.Length >= 9 && path[1] == 'u' && path.StartsWith(Paths.Updates))
-            {
-                queries = ParseQueries(path.Slice(9));
-                return RequestType.Updates;
-            }
-            else if (path.Length >= 9 && path[1] == 'q' && path.StartsWith(Paths.MultipleQueries))
-            {
-                queries = ParseQueries(path.Slice(9));
-                return RequestType.MultipleQueries;
-            }
+        if (path.Length == 3 && path[0] == '/' && path[1] == 'd' && path[2] == 'b')
+        {
+            return RequestType.SingleQuery;
+        }
+        if (path.Length == 9 && path[1] == 'f' && path.SequenceEqual(Paths.Fortunes))
+        {
+            return RequestType.Fortunes;
+        }
+        if (path.Length >= 15 && path[1] == 'c' && path.StartsWith(Paths.Caching))
+        {
+            queries = ParseQueries(path.Slice(15));
+            return RequestType.Caching;
+        }
+        if (path.Length >= 9 && path[1] == 'u' && path.StartsWith(Paths.Updates))
+        {
+            queries = ParseQueries(path.Slice(9));
+            return RequestType.Updates;
+        }
+        if (path.Length >= 9 && path[1] == 'q' && path.StartsWith(Paths.MultipleQueries))
+        {
+            queries = ParseQueries(path.Slice(9));
+            return RequestType.MultipleQueries;
+        }
 #endif
 #endif
         return RequestType.NotRecognized;
         return RequestType.NotRecognized;
     }
     }
@@ -138,13 +158,13 @@ public sealed partial class BenchmarkApplication
 
 
         private static int ParseQueries(ReadOnlySpan<byte> parameter)
         private static int ParseQueries(ReadOnlySpan<byte> parameter)
         {
         {
-            if (!Utf8Parser.TryParse(parameter, out int queries, out _) || queries < 1)
+            if (!Utf8Parser.TryParse(parameter, out int queries, out _))
             {
             {
                 queries = 1;
                 queries = 1;
             }
             }
-            else if (queries > 500)
+            else
             {
             {
-                queries = 500;
+                queries = Math.Clamp(queries, 1, 500);
             }
             }
 
 
             return queries;
             return queries;

+ 1 - 1
frameworks/CSharp/aspnetcore/PlatformBenchmarks/BenchmarkConfigurationHelpers.cs

@@ -14,7 +14,7 @@ public static class BenchmarkConfigurationHelpers
 
 
         builder.UseSockets(options =>
         builder.UseSockets(options =>
         {
         {
-            if (int.TryParse(builder.GetSetting("threadCount"), out int threadCount))
+            if (int.TryParse(builder.GetSetting("threadCount"), out var threadCount))
             {
             {
                 options.IOQueueCount = threadCount;
                 options.IOQueueCount = threadCount;
             }
             }

+ 2 - 2
frameworks/CSharp/aspnetcore/PlatformBenchmarks/BufferWriter.cs

@@ -44,7 +44,7 @@ public ref struct BufferWriter<T> where T : IBufferWriter<byte>
     }
     }
 
 
     [MethodImpl(MethodImplOptions.AggressiveInlining)]
     [MethodImpl(MethodImplOptions.AggressiveInlining)]
-    public void Write(ReadOnlySpan<byte> source)
+    public void Write(scoped ReadOnlySpan<byte> source)
     {
     {
         if (_span.Length >= source.Length)
         if (_span.Length >= source.Length)
         {
         {
@@ -77,7 +77,7 @@ public ref struct BufferWriter<T> where T : IBufferWriter<byte>
         _span = _output.GetSpan(count);
         _span = _output.GetSpan(count);
     }
     }
 
 
-    private void WriteMultiBuffer(ReadOnlySpan<byte> source)
+    private void WriteMultiBuffer(scoped ReadOnlySpan<byte> source)
     {
     {
         while (source.Length > 0)
         while (source.Length > 0)
         {
         {

+ 241 - 0
frameworks/CSharp/aspnetcore/PlatformBenchmarks/ChunkedBufferWriter.cs

@@ -0,0 +1,241 @@
+// 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.Buffers;
+using System.Buffers.Text;
+using System.Diagnostics;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+
+namespace PlatformBenchmarks;
+
+internal sealed class ChunkedBufferWriter<TWriter> : IBufferWriter<byte> where TWriter : IBufferWriter<byte>
+{
+    private const int DefaultChunkSizeHint = 2048;
+    private static readonly StandardFormat DefaultHexFormat = GetHexFormat(DefaultChunkSizeHint);
+    private static ReadOnlySpan<byte> ChunkTerminator => "\r\n"u8;
+
+    private TWriter _output;
+    private int _chunkSizeHint;
+    private StandardFormat _hexFormat = DefaultHexFormat;
+    private Memory<byte> _currentFullChunk;
+    private Memory<byte> _currentChunk;
+    private int _buffered;
+    private bool _ended = false;
+
+    public Memory<byte> Memory => _currentChunk;
+
+    public TWriter Output => _output;
+
+    public int Buffered => _buffered;
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void SetOutput(TWriter output, int chunkSizeHint = DefaultChunkSizeHint)
+    {
+        _buffered = 0;
+        _chunkSizeHint = chunkSizeHint;
+        _output = output;
+
+        StartNewChunk(chunkSizeHint, isFirst: true);
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void Reset()
+    {
+        _buffered = 0;
+        _output = default;
+        _ended = false;
+        _hexFormat = DefaultHexFormat;
+        _currentFullChunk = default;
+        _currentChunk = default;
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void Advance(int count)
+    {
+        ThrowIfEnded();
+
+        _buffered += count;
+        _currentChunk = _currentChunk[count..];
+    }
+
+    public Memory<byte> GetMemory(int sizeHint = 0)
+    {
+        ThrowIfEnded();
+
+        if (_currentChunk.Length <= sizeHint)
+        {
+            EnsureMore(sizeHint);
+        }
+        return _currentChunk;
+    }
+
+    public Span<byte> GetSpan(int sizeHint = 0) => GetMemory(sizeHint).Span;
+
+    public void End()
+    {
+        ThrowIfEnded();
+
+        CommitCurrentChunk(isFinal: true);
+
+        _ended = true;
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    private static StandardFormat GetHexFormat(int maxValue)
+    {
+        var hexDigitCount = CountHexDigits(maxValue);
+
+        return new StandardFormat('X', (byte)hexDigitCount);
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    private static int CountHexDigits(int n) => n <= 16 ? 1 : (BitOperations.Log2((uint)n) >> 2) + 1;
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    private void StartNewChunk(int sizeHint, bool isFirst = false)
+    {
+        ThrowIfEnded();
+
+        // Header is like:
+        // 520\r\n
+
+        var oldFullChunkHexLength = -1;
+        if (!isFirst)
+        {
+            oldFullChunkHexLength = CountHexDigits(_currentFullChunk.Length);
+        }
+        _currentFullChunk = _output.GetMemory(Math.Max(_chunkSizeHint, sizeHint));
+        var newFullChunkHexLength = CountHexDigits(_currentFullChunk.Length);
+
+        var currentFullChunkSpan = _currentFullChunk.Span;
+
+        // Write space for HEX digits
+        currentFullChunkSpan[..newFullChunkHexLength].Fill(48); // 48 == '0'
+
+        // Write header terminator
+        var terminator = "\r\n"u8;
+        terminator.CopyTo(currentFullChunkSpan[newFullChunkHexLength..]);
+        var chunkHeaderLength = newFullChunkHexLength + terminator.Length;
+        _currentChunk = _currentFullChunk[chunkHeaderLength..];
+
+        if ((!isFirst && oldFullChunkHexLength != newFullChunkHexLength) || (isFirst && DefaultChunkSizeHint != _chunkSizeHint))
+        {
+            // Update HEX format if changed
+            _hexFormat = GetHexFormat(_currentFullChunk.Length);
+        }
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    private void CommitCurrentChunk(bool isFinal = false, int sizeHint = 0)
+    {
+        ThrowIfEnded();
+
+        var contentLength = _buffered;
+
+        if (contentLength > 0)
+        {
+            // Update the chunk header
+            var chunkLengthHexDigitsLength = CountHexDigits(contentLength);
+            var span = _currentFullChunk.Span;
+            if (!Utf8Formatter.TryFormat(contentLength, span, out var bytesWritten, _hexFormat))
+            {
+                throw new NotSupportedException("Chunk size too large");
+            }
+            Debug.Assert(chunkLengthHexDigitsLength == bytesWritten, "HEX formatting math problem.");
+            var headerLength = chunkLengthHexDigitsLength + 2;
+
+            // Total chunk length: content length as HEX string + \r\n + content + \r\n
+            var spanOffset = headerLength + contentLength;
+            var chunkTotalLength = spanOffset + ChunkTerminator.Length;
+
+            Debug.Assert(span.Length >= chunkTotalLength, "Bad chunk size calculation.");
+
+            // Write out the chunk terminator
+            ChunkTerminator.CopyTo(span[spanOffset..]);
+            spanOffset = chunkTotalLength;
+
+            if (!isFinal)
+            {
+                _output.Advance(chunkTotalLength);
+                StartNewChunk(sizeHint);
+            }
+            else
+            {
+                // Write out final chunk (zero-length chunk)
+                var terminator = "0\r\n\r\n"u8;
+                if ((spanOffset + terminator.Length) <= span.Length)
+                {
+                    // There's space for the final chunk in the current span
+                    terminator.CopyTo(span[spanOffset..]);
+                    _output.Advance(chunkTotalLength + terminator.Length);
+                }
+                else
+                {
+                    // Final chunk doesn't fit in current span so just write it directly after advancing the writer
+                    _output.Advance(chunkTotalLength);
+                    _output.Write(terminator);
+                }
+            }
+
+            _buffered = 0;
+        }
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void Write(ReadOnlySpan<byte> source)
+    {
+        ThrowIfEnded();
+
+        if (_currentChunk.Length >= (source.Length + ChunkTerminator.Length))
+        {
+            source.CopyTo(_currentChunk.Span);
+            Advance(source.Length);
+        }
+        else
+        {
+            WriteMultiBuffer(source);
+        }
+    }
+
+    [MethodImpl(MethodImplOptions.NoInlining)]
+    private void EnsureMore(int count = 0)
+    {
+        if (count > (_currentChunk.Length - _buffered - ChunkTerminator.Length))
+        {
+            if (_buffered > 0)
+            {
+                CommitCurrentChunk(isFinal: false, count);
+            }
+            else
+            {
+                StartNewChunk(count);
+            }
+        }
+    }
+
+    private void WriteMultiBuffer(ReadOnlySpan<byte> source)
+    {
+        while (source.Length > 0)
+        {
+            if ((_currentChunk.Length - ChunkTerminator.Length) == 0)
+            {
+                EnsureMore();
+            }
+
+            var writable = Math.Min(source.Length, _currentChunk.Length - ChunkTerminator.Length);
+            source[..writable].CopyTo(_currentChunk.Span);
+            source = source[writable..];
+            Advance(writable);
+        }
+    }
+
+    private void ThrowIfEnded()
+    {
+        if (_ended)
+        {
+            throw new InvalidOperationException("Cannot use the writer after calling End().");
+        }
+    }
+}

+ 13 - 9
frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/BatchUpdateString.cs

@@ -15,21 +15,25 @@ internal sealed class BatchUpdateString
     private static readonly string[] _queries = new string[MaxBatch + 1];
     private static readonly string[] _queries = new string[MaxBatch + 1];
 
 
     public static string Query(int batchSize)
     public static string Query(int batchSize)
-    {
-        if (_queries[batchSize] != null)
-        {
-            return _queries[batchSize];
-        }
-
-        var lastIndex = batchSize - 1;
+        => _queries[batchSize] is null
+            ? CreateBatch(batchSize)
+            : _queries[batchSize];
 
 
+    private static string CreateBatch(int batchSize)
+    {
         var sb = StringBuilderCache.Acquire();
         var sb = StringBuilderCache.Acquire();
 
 
         if (DatabaseServer == DatabaseServer.PostgreSql)
         if (DatabaseServer == DatabaseServer.PostgreSql)
         {
         {
             sb.Append("UPDATE world SET randomNumber = temp.randomNumber FROM (VALUES ");
             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");
+            var c = 1;
+            for (var i = 0; i < batchSize; i++)
+            {
+                if (i > 0)
+                    sb.Append(", ");
+                sb.Append($"(${c++}, ${c++})");
+            }
+            sb.Append(" ORDER BY 1) AS temp(id, randomNumber) WHERE temp.id = world.id");
         }
         }
         else
         else
         {
         {

+ 3 - 3
frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/Fortune.cs → frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/FortuneUtf16.cs

@@ -3,9 +3,9 @@
 
 
 namespace PlatformBenchmarks;
 namespace PlatformBenchmarks;
 
 
-public readonly struct Fortune : IComparable<Fortune>, IComparable
+public readonly struct FortuneUtf16 : IComparable<FortuneUtf16>, IComparable
 {
 {
-    public Fortune(int id, string message)
+    public FortuneUtf16(int id, string message)
     {
     {
         Id = id;
         Id = id;
         Message = message;
         Message = message;
@@ -18,5 +18,5 @@ public readonly struct Fortune : IComparable<Fortune>, IComparable
     public int CompareTo(object obj) => throw new InvalidOperationException("The non-generic CompareTo should not be used");
     public int CompareTo(object obj) => throw new InvalidOperationException("The non-generic CompareTo should not be used");
 
 
     // Performance critical, using culture insensitive comparison
     // Performance critical, using culture insensitive comparison
-    public int CompareTo(Fortune other) => string.CompareOrdinal(Message, other.Message);
+    public int CompareTo(FortuneUtf16 other) => string.CompareOrdinal(Message, other.Message);
 }
 }

+ 22 - 0
frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/FortuneUtf8.cs

@@ -0,0 +1,22 @@
+// 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 readonly struct FortuneUtf8 : IComparable<FortuneUtf8>, IComparable
+{
+    public FortuneUtf8(int id, byte[] message)
+    {
+        Id = id;
+        Message = message;
+    }
+
+    public int Id { get; }
+
+    public byte[] 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(FortuneUtf8 other) => Message.AsSpan().SequenceCompareTo(other.Message.AsSpan());
+}

+ 4 - 4
frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/Providers/RawDbMySqlConnector.cs

@@ -185,9 +185,9 @@ namespace PlatformBenchmarks
             return results;
             return results;
         }
         }
 
 
-        public async Task<List<Fortune>> LoadFortunesRows()
+        public async Task<List<FortuneUtf16>> LoadFortunesRows()
         {
         {
-            var result = new List<Fortune>();
+            var result = new List<FortuneUtf16>();
 
 
             using (var db = new MySqlConnection(_connectionString))
             using (var db = new MySqlConnection(_connectionString))
             {
             {
@@ -202,7 +202,7 @@ namespace PlatformBenchmarks
                         while (await rdr.ReadAsync())
                         while (await rdr.ReadAsync())
                         {
                         {
                             result.Add(
                             result.Add(
-                                new Fortune
+                                new FortuneUtf16
                                 (
                                 (
                                     id: rdr.GetInt32(0),
                                     id: rdr.GetInt32(0),
                                     message: rdr.GetString(1)
                                     message: rdr.GetString(1)
@@ -212,7 +212,7 @@ namespace PlatformBenchmarks
                 }
                 }
             }
             }
 
 
-            result.Add(new Fortune(id: 0, message: "Additional fortune added at request time." ));
+            result.Add(new FortuneUtf16(id: 0, message: "Additional fortune added at request time." ));
             result.Sort();
             result.Sort();
 
 
             return result;
             return result;

+ 111 - 107
frameworks/CSharp/aspnetcore/PlatformBenchmarks/Data/Providers/RawDbNpgsql.cs

@@ -3,11 +3,7 @@
 
 
 #if NPGSQL
 #if NPGSQL
 
 
-using System;
-using System.Collections.Generic;
-using System.Linq;
 using System.Runtime.CompilerServices;
 using System.Runtime.CompilerServices;
-using System.Threading.Tasks;
 using Microsoft.Extensions.Caching.Memory;
 using Microsoft.Extensions.Caching.Memory;
 using Npgsql;
 using Npgsql;
 
 
@@ -18,53 +14,63 @@ namespace PlatformBenchmarks
     public sealed class RawDb
     public sealed class RawDb
     {
     {
         private readonly ConcurrentRandom _random;
         private readonly ConcurrentRandom _random;
-        private readonly string _connectionString;
         private readonly MemoryCache _cache = new(
         private readonly MemoryCache _cache = new(
             new MemoryCacheOptions
             new MemoryCacheOptions
             {
             {
                 ExpirationScanFrequency = TimeSpan.FromMinutes(60)
                 ExpirationScanFrequency = TimeSpan.FromMinutes(60)
             });
             });
 
 
+        private readonly NpgsqlDataSource _dataSource;
+
         public RawDb(ConcurrentRandom random, AppSettings appSettings)
         public RawDb(ConcurrentRandom random, AppSettings appSettings)
         {
         {
             _random = random;
             _random = random;
-            _connectionString = appSettings.ConnectionString;
+            _dataSource = NpgsqlDataSource.Create(appSettings.ConnectionString);
         }
         }
 
 
         public async Task<World> LoadSingleQueryRow()
         public async Task<World> LoadSingleQueryRow()
         {
         {
-            using (var db = new NpgsqlConnection(_connectionString))
-            {
-                await db.OpenAsync();
+            using var db = _dataSource.CreateConnection();
+            await db.OpenAsync();
 
 
-                var (cmd, _) = CreateReadCommand(db);
-                using (cmd)
-                {
-                    return await ReadSingleRow(cmd);
-                }
-            }
+            var (cmd, _) = CreateReadCommand(db);
+            using var command = cmd;
+            return await ReadSingleRow(cmd);
         }
         }
 
 
         public async Task<World[]> LoadMultipleQueriesRows(int count)
         public async Task<World[]> LoadMultipleQueriesRows(int count)
         {
         {
-            var result = new World[count];
+            var results = new World[count];
+
+            using var connection = await _dataSource.OpenConnectionAsync();
 
 
-            using (var db = new NpgsqlConnection(_connectionString))
+            using var batch = new NpgsqlBatch(connection)
             {
             {
-                await db.OpenAsync();
+                // Inserts a PG Sync message between each statement in the batch, required for compliance with
+                // TechEmpower general test requirement 7
+                // https://github.com/TechEmpower/FrameworkBenchmarks/wiki/Project-Information-Framework-Tests-Overview
+                EnableErrorBarriers = true
+            };
 
 
-                var (cmd, idParameter) = CreateReadCommand(db);
-                using (cmd)
+            for (var i = 0; i < count; i++)
+            {
+                batch.BatchCommands.Add(new()
                 {
                 {
-                    for (int i = 0; i < result.Length; i++)
-                    {
-                        result[i] = await ReadSingleRow(cmd);
-                        idParameter.TypedValue = _random.Next(1, 10001);
-                    }
-                }
+                    CommandText = "SELECT id, randomnumber FROM world WHERE id = $1",
+                    Parameters = { new NpgsqlParameter<int> { TypedValue = _random.Next(1, 10001) } }
+                });
             }
             }
 
 
-            return result;
+            using var reader = await batch.ExecuteReaderAsync();
+
+            for (var i = 0; i < count; i++)
+            {
+                await reader.ReadAsync();
+                results[i] = new World { Id = reader.GetInt32(0), RandomNumber = reader.GetInt32(1) };
+                await reader.NextResultAsync();
+            }
+
+            return results;
         }
         }
 
 
         public Task<CachedWorld[]> LoadCachedQueries(int count)
         public Task<CachedWorld[]> LoadCachedQueries(int count)
@@ -77,7 +83,7 @@ namespace PlatformBenchmarks
             {
             {
                 var id = random.Next(1, 10001);
                 var id = random.Next(1, 10001);
                 var key = cacheKeys[id];
                 var key = cacheKeys[id];
-                if (cache.TryGetValue(key, out object cached))
+                if (cache.TryGetValue(key, out var cached))
                 {
                 {
                     result[i] = (CachedWorld)cached;
                     result[i] = (CachedWorld)cached;
                 }
                 }
@@ -91,32 +97,25 @@ namespace PlatformBenchmarks
 
 
             static async Task<CachedWorld[]> LoadUncachedQueries(int id, int i, int count, RawDb rawdb, CachedWorld[] result)
             static async Task<CachedWorld[]> LoadUncachedQueries(int id, int i, int count, RawDb rawdb, CachedWorld[] result)
             {
             {
-                using (var db = new NpgsqlConnection(rawdb._connectionString))
-                {
-                    await db.OpenAsync();
+                using var db = rawdb._dataSource.CreateConnection();
+                await db.OpenAsync();
 
 
-                    var (cmd, idParameter) = rawdb.CreateReadCommand(db);
-                    using (cmd)
-                    {
-                        Func<ICacheEntry, Task<CachedWorld>> create = async _ =>
-                        {
-                            return await rawdb.ReadSingleRow(cmd);
-                        };
+                var (cmd, idParameter) = rawdb.CreateReadCommand(db);
+                using var command = cmd;
+                async Task<CachedWorld> create(ICacheEntry _) => await ReadSingleRow(cmd);
 
 
-                        var cacheKeys = _cacheKeys;
-                        var key = cacheKeys[id];
+                var cacheKeys = _cacheKeys;
+                var key = cacheKeys[id];
 
 
-                        idParameter.TypedValue = id;
+                idParameter.TypedValue = id;
 
 
-                        for (; i < result.Length; i++)
-                        {
-                            result[i] = await rawdb._cache.GetOrCreateAsync(key, create);
+                for (; i < result.Length; i++)
+                {
+                    result[i] = await rawdb._cache.GetOrCreateAsync(key, create);
 
 
-                            id = rawdb._random.Next(1, 10001);
-                            idParameter.TypedValue = id;
-                            key = cacheKeys[id];
-                        }
-                    }
+                    id = rawdb._random.Next(1, 10001);
+                    idParameter.TypedValue = id;
+                    key = cacheKeys[id];
                 }
                 }
 
 
                 return result;
                 return result;
@@ -125,21 +124,17 @@ namespace PlatformBenchmarks
 
 
         public async Task PopulateCache()
         public async Task PopulateCache()
         {
         {
-            using (var db = new NpgsqlConnection(_connectionString))
-            {
-                await db.OpenAsync();
+            using var db = _dataSource.CreateConnection();
+            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));
-                    }
-                }
+            var (cmd, idParameter) = CreateReadCommand(db);
+            using var command = 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");
             Console.WriteLine("Caching Populated");
@@ -149,70 +144,81 @@ namespace PlatformBenchmarks
         {
         {
             var results = new World[count];
             var results = new World[count];
 
 
-            using (var db = new NpgsqlConnection(_connectionString))
+            using var connection = _dataSource.CreateConnection();
+            await connection.OpenAsync();
+
+            using (var batch = new NpgsqlBatch(connection))
             {
             {
-                await db.OpenAsync();
+                // Inserts a PG Sync message between each statement in the batch, required for compliance with
+                // TechEmpower general test requirement 7
+                // https://github.com/TechEmpower/FrameworkBenchmarks/wiki/Project-Information-Framework-Tests-Overview
+                batch.EnableErrorBarriers = true;
 
 
-                var (queryCmd, queryParameter) = CreateReadCommand(db);
-                using (queryCmd)
+                for (var i = 0; i < count; i++)
                 {
                 {
-                    for (int i = 0; i < results.Length; i++)
+                    batch.BatchCommands.Add(new()
                     {
                     {
-                        results[i] = await ReadSingleRow(queryCmd);
-                        queryParameter.TypedValue = _random.Next(1, 10001);
-                    }
+                        CommandText = "SELECT id, randomnumber FROM world WHERE id = $1",
+                        Parameters = { new NpgsqlParameter<int> { TypedValue = _random.Next(1, 10001) } }
+                    });
                 }
                 }
 
 
-                using (var updateCmd = new NpgsqlCommand(BatchUpdateString.Query(count), db))
-                {
-                    var ids = BatchUpdateString.Ids;
-                    var randoms = BatchUpdateString.Randoms;
+                using var reader = await batch.ExecuteReaderAsync();
 
 
-                    for (int i = 0; i < results.Length; i++)
-                    {
-                        var randomNumber = _random.Next(1, 10001);
+                for (var i = 0; i < count; i++)
+                {
+                    await reader.ReadAsync();
+                    results[i] = new World { Id = reader.GetInt32(0), RandomNumber = reader.GetInt32(1) };
+                    await reader.NextResultAsync();
+                }
+            }
 
 
-                        updateCmd.Parameters.Add(new NpgsqlParameter<int>(parameterName: ids[i], value: results[i].Id));
-                        updateCmd.Parameters.Add(new NpgsqlParameter<int>(parameterName: randoms[i], value: randomNumber));
+            using (var updateCmd = new NpgsqlCommand(BatchUpdateString.Query(count), connection))
+            {
+                for (int i = 0; i < results.Length; i++)
+                {
+                    var randomNumber = _random.Next(1, 10001);
 
 
-                        results[i].RandomNumber = randomNumber;
-                    }
+                    updateCmd.Parameters.Add(new NpgsqlParameter<int> { TypedValue = results[i].Id });
+                    updateCmd.Parameters.Add(new NpgsqlParameter<int> { TypedValue = randomNumber });
 
 
-                    await updateCmd.ExecuteNonQueryAsync();
+                    results[i].RandomNumber = randomNumber;
                 }
                 }
+
+                await updateCmd.ExecuteNonQueryAsync();
             }
             }
 
 
             return results;
             return results;
         }
         }
 
 
-        public async Task<List<Fortune>> LoadFortunesRows()
+        public async Task<List<FortuneUtf8>> LoadFortunesRows()
         {
         {
-            var result = new List<Fortune>();
+            var result = new List<FortuneUtf8>();
 
 
-            using (var db = new NpgsqlConnection(_connectionString))
+            using (var db = _dataSource.CreateConnection())
             {
             {
                 await db.OpenAsync();
                 await db.OpenAsync();
 
 
-                using (var cmd = new NpgsqlCommand("SELECT id, message FROM fortune", db))
-                using (var rdr = await cmd.ExecuteReaderAsync())
+                using var cmd = new NpgsqlCommand("SELECT id, message FROM fortune", db);
+                using var rdr = await cmd.ExecuteReaderAsync();
+                while (await rdr.ReadAsync())
                 {
                 {
-                    while (await rdr.ReadAsync())
-                    {
-                        result.Add(new Fortune
-                        (
-                            id:rdr.GetInt32(0),
-                            message: rdr.GetString(1)
-                        ));
-                    }
+                    result.Add(new FortuneUtf8
+                    (
+                        id:rdr.GetInt32(0),
+                        message: rdr.GetFieldValue<byte[]>(1)
+                    ));
                 }
                 }
             }
             }
 
 
-            result.Add(new Fortune(id: 0, message: "Additional fortune added at request time." ));
+            result.Add(new FortuneUtf8(id: 0, AdditionalFortune));
             result.Sort();
             result.Sort();
 
 
             return result;
             return result;
         }
         }
 
 
+        private readonly byte[] AdditionalFortune = "Additional fortune added at request time."u8.ToArray();
+
         private (NpgsqlCommand readCmd, NpgsqlParameter<int> idParameter) CreateReadCommand(NpgsqlConnection connection)
         private (NpgsqlCommand readCmd, NpgsqlParameter<int> idParameter) CreateReadCommand(NpgsqlConnection connection)
         {
         {
             var cmd = new NpgsqlCommand("SELECT id, randomnumber FROM world WHERE id = $1", connection);
             var cmd = new NpgsqlCommand("SELECT id, randomnumber FROM world WHERE id = $1", connection);
@@ -224,18 +230,16 @@ namespace PlatformBenchmarks
         }
         }
 
 
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
-        private async Task<World> ReadSingleRow(NpgsqlCommand cmd)
+        private static async Task<World> ReadSingleRow(NpgsqlCommand cmd)
         {
         {
-            using (var rdr = await cmd.ExecuteReaderAsync(System.Data.CommandBehavior.SingleRow))
-            {
-                await rdr.ReadAsync();
+            using var rdr = await cmd.ExecuteReaderAsync(System.Data.CommandBehavior.SingleRow);
+            await rdr.ReadAsync();
 
 
-                return new World
-                {
-                    Id = rdr.GetInt32(0),
-                    RandomNumber = rdr.GetInt32(1)
-                };
-            }
+            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();
         private static readonly object[] _cacheKeys = Enumerable.Range(0, 10001).Select(i => new CacheKey(i)).ToArray();

+ 1 - 4
frameworks/CSharp/aspnetcore/PlatformBenchmarks/DateHeader.cs

@@ -17,10 +17,7 @@ internal static class DateHeader
     const int suffixLength = 2; // crlf
     const int suffixLength = 2; // crlf
     const int suffixIndex = dateTimeRLength + prefixLength;
     const int suffixIndex = dateTimeRLength + prefixLength;
 
 
-    private static readonly Timer s_timer = new((s) =>
-    {
-        SetDateValues(DateTimeOffset.UtcNow);
-    }, null, 1000, 1000);
+    private static readonly Timer s_timer = new(_ => SetDateValues(DateTimeOffset.UtcNow), null, 1000, 1000);
 
 
     private static byte[] s_headerBytesMaster = new byte[prefixLength + dateTimeRLength + 2 * suffixLength];
     private static byte[] s_headerBytesMaster = new byte[prefixLength + dateTimeRLength + 2 * suffixLength];
     private static byte[] s_headerBytesScratch = new byte[prefixLength + dateTimeRLength + 2 * suffixLength];
     private static byte[] s_headerBytesScratch = new byte[prefixLength + dateTimeRLength + 2 * suffixLength];

+ 2 - 1
frameworks/CSharp/aspnetcore/PlatformBenchmarks/PlatformBenchmarks.csproj

@@ -22,7 +22,8 @@
   </PropertyGroup>
   </PropertyGroup>
   
   
   <ItemGroup>
   <ItemGroup>
-    <PackageReference Condition=" '$(DatabaseProvider)' == 'Npgsql' " Include="Npgsql" Version="6.0.0" />
+    <PackageReference Include="RazorSlices" Version="0.3.0" />
+    <PackageReference Condition=" '$(DatabaseProvider)' == 'Npgsql' " Include="Npgsql" Version="7.0.2" />
     <PackageReference Condition=" '$(DatabaseProvider)' == 'MySqlConnector' " Include="MySqlConnector" Version="2.0.0" />
     <PackageReference Condition=" '$(DatabaseProvider)' == 'MySqlConnector' " Include="MySqlConnector" Version="2.0.0" />
   </ItemGroup>
   </ItemGroup>
 
 

+ 33 - 15
frameworks/CSharp/aspnetcore/PlatformBenchmarks/Program.cs

@@ -14,15 +14,23 @@ public sealed class Program
     {
     {
         Args = args;
         Args = args;
 
 
+#if NPGSQL
+        // This disables SQL parsing/rewriting, which requires using positional parameters and NpgsqlBatch everywhere.
+        // This helps commands where there are no parameters (Fortunes); when there are parameters, their ParameterName
+        // being null already triggers positional parameters and disables parsing)
+        // Note that Dapper and EF aren't yet compatible with this mode.
+        AppContext.SetSwitch("Npgsql.EnableSqlRewriting", false);
+#endif
+
         Console.WriteLine(Encoding.UTF8.GetString(BenchmarkApplication.ApplicationName));
         Console.WriteLine(Encoding.UTF8.GetString(BenchmarkApplication.ApplicationName));
 #if !DATABASE
 #if !DATABASE
         Console.WriteLine(Encoding.UTF8.GetString(BenchmarkApplication.Paths.Plaintext));
         Console.WriteLine(Encoding.UTF8.GetString(BenchmarkApplication.Paths.Plaintext));
         Console.WriteLine(Encoding.UTF8.GetString(BenchmarkApplication.Paths.Json));
         Console.WriteLine(Encoding.UTF8.GetString(BenchmarkApplication.Paths.Json));
 #else
 #else
-            Console.WriteLine(Encoding.UTF8.GetString(BenchmarkApplication.Paths.Fortunes));
-            Console.WriteLine(Encoding.UTF8.GetString(BenchmarkApplication.Paths.SingleQuery));
-            Console.WriteLine(Encoding.UTF8.GetString(BenchmarkApplication.Paths.Updates));
-            Console.WriteLine(Encoding.UTF8.GetString(BenchmarkApplication.Paths.MultipleQueries));
+        Console.WriteLine(Encoding.UTF8.GetString(BenchmarkApplication.Paths.Fortunes));
+        Console.WriteLine(Encoding.UTF8.GetString(BenchmarkApplication.Paths.SingleQuery));
+        Console.WriteLine(Encoding.UTF8.GetString(BenchmarkApplication.Paths.Updates));
+        Console.WriteLine(Encoding.UTF8.GetString(BenchmarkApplication.Paths.MultipleQueries));
 #endif
 #endif
         DateHeader.SyncDateTimer();
         DateHeader.SyncDateTimer();
 
 
@@ -30,7 +38,14 @@ public sealed class Program
         var config = (IConfiguration)host.Services.GetService(typeof(IConfiguration));
         var config = (IConfiguration)host.Services.GetService(typeof(IConfiguration));
         BatchUpdateString.DatabaseServer = config.Get<AppSettings>().Database;
         BatchUpdateString.DatabaseServer = config.Get<AppSettings>().Database;
 #if DATABASE
 #if DATABASE
+        try
+        {
             await BenchmarkApplication.Db.PopulateCache();
             await BenchmarkApplication.Db.PopulateCache();
+        }
+        catch (Exception ex)
+        {
+            Console.WriteLine($"Error trying to populate database cache: {ex}");
+        }
 #endif
 #endif
         await host.RunAsync();
         await host.RunAsync();
     }
     }
@@ -39,6 +54,9 @@ public sealed class Program
     {
     {
         var config = new ConfigurationBuilder()
         var config = new ConfigurationBuilder()
             .AddJsonFile("appsettings.json")
             .AddJsonFile("appsettings.json")
+#if DEBUG
+            .AddUserSecrets<Program>()
+#endif
             .AddEnvironmentVariables()
             .AddEnvironmentVariables()
             .AddEnvironmentVariables(prefix: "ASPNETCORE_")
             .AddEnvironmentVariables(prefix: "ASPNETCORE_")
             .AddCommandLine(args)
             .AddCommandLine(args)
@@ -46,18 +64,18 @@ public sealed class Program
 
 
         var appSettings = config.Get<AppSettings>();
         var appSettings = config.Get<AppSettings>();
 #if DATABASE
 #if DATABASE
-            Console.WriteLine($"Database: {appSettings.Database}");
-            Console.WriteLine($"ConnectionString: {appSettings.ConnectionString}");
+        Console.WriteLine($"Database: {appSettings.Database}");
+        Console.WriteLine($"ConnectionString: {appSettings.ConnectionString}");
 
 
-            if (appSettings.Database is DatabaseServer.PostgreSql
-                                     or DatabaseServer.MySql)
-            {
-                BenchmarkApplication.Db = new RawDb(new ConcurrentRandom(), appSettings);
-            }
-            else
-            {
-                throw new NotSupportedException($"{appSettings.Database} is not supported");
-            }
+        if (appSettings.Database is DatabaseServer.PostgreSql
+                                 or DatabaseServer.MySql)
+        {
+            BenchmarkApplication.Db = new RawDb(new ConcurrentRandom(), appSettings);
+        }
+        else
+        {
+            throw new NotSupportedException($"{appSettings.Database} is not supported");
+        }
 #endif
 #endif
 
 
         var hostBuilder = new WebHostBuilder()
         var hostBuilder = new WebHostBuilder()

+ 2 - 0
frameworks/CSharp/aspnetcore/PlatformBenchmarks/Templates/FortunesUtf16.cshtml

@@ -0,0 +1,2 @@
+@inherits RazorSlice<List<FortuneUtf16>>
+<!DOCTYPE html><html><head><title>Fortunes</title></head><body><table><tr><th>id</th><th>message</th></tr>@foreach (var item in Model){<tr><td>@item.Id</td><td>@item.Message</td></tr>}</table></body></html>

+ 2 - 0
frameworks/CSharp/aspnetcore/PlatformBenchmarks/Templates/FortunesUtf8.cshtml

@@ -0,0 +1,2 @@
+@inherits RazorSlice<List<FortuneUtf8>>
+<!DOCTYPE html><html><head><title>Fortunes</title></head><body><table><tr><th>id</th><th>message</th></tr>@foreach (var item in Model){<tr><td>@item.Id</td><td>@item.Message</td></tr>}</table></body></html>

+ 9 - 0
frameworks/CSharp/aspnetcore/PlatformBenchmarks/Templates/_ViewImports.cshtml

@@ -0,0 +1,9 @@
+@inherits RazorSlice
+
+@using System.Globalization;
+@using Microsoft.AspNetCore.Razor;
+@using RazorSlices;
+@using PlatformBenchmarks;
+
+@tagHelperPrefix __disable_tagHelpers__:
+@removeTagHelper *, Microsoft.AspNetCore.Mvc.Razor

+ 2 - 1
frameworks/CSharp/aspnetcore/PlatformBenchmarks/appsettings.json

@@ -1,3 +1,4 @@
 {
 {
-  "ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=aspnetcore-Benchmarks;Trusted_Connection=True;MultipleActiveResultSets=true"
+  "ConnectionString": "Server=localhost;Database=fortunes;User Id=test;Password=test",
+  "Database": "PostgreSQL"
 }
 }