diff --git a/Beeline.sln b/Beeline.sln index 9edd6aa..c55a1d2 100644 --- a/Beeline.sln +++ b/Beeline.sln @@ -13,6 +13,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Beeline.Tests", "test\Beeli EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Beeline.Benchmarks", "test\Beeline.Benchmarks\Beeline.Benchmarks.csproj", "{0045104E-E9BC-4566-8324-3D1E4099CD6E}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{0A575C00-110D-46B7-A30D-E69A25961CCE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Beeline.AspNetCoreBenchmarks", "samples\Beeline.AspNetCoreBenchmarks\Beeline.AspNetCoreBenchmarks.csproj", "{DF8CE83B-0BF0-412D-A5B6-BBE27F58E675}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -62,10 +66,23 @@ Global {0045104E-E9BC-4566-8324-3D1E4099CD6E}.Release|x64.Build.0 = Release|x64 {0045104E-E9BC-4566-8324-3D1E4099CD6E}.Release|x86.ActiveCfg = Release|x86 {0045104E-E9BC-4566-8324-3D1E4099CD6E}.Release|x86.Build.0 = Release|x86 + {DF8CE83B-0BF0-412D-A5B6-BBE27F58E675}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DF8CE83B-0BF0-412D-A5B6-BBE27F58E675}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DF8CE83B-0BF0-412D-A5B6-BBE27F58E675}.Debug|x64.ActiveCfg = Debug|x64 + {DF8CE83B-0BF0-412D-A5B6-BBE27F58E675}.Debug|x64.Build.0 = Debug|x64 + {DF8CE83B-0BF0-412D-A5B6-BBE27F58E675}.Debug|x86.ActiveCfg = Debug|x86 + {DF8CE83B-0BF0-412D-A5B6-BBE27F58E675}.Debug|x86.Build.0 = Debug|x86 + {DF8CE83B-0BF0-412D-A5B6-BBE27F58E675}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DF8CE83B-0BF0-412D-A5B6-BBE27F58E675}.Release|Any CPU.Build.0 = Release|Any CPU + {DF8CE83B-0BF0-412D-A5B6-BBE27F58E675}.Release|x64.ActiveCfg = Release|x64 + {DF8CE83B-0BF0-412D-A5B6-BBE27F58E675}.Release|x64.Build.0 = Release|x64 + {DF8CE83B-0BF0-412D-A5B6-BBE27F58E675}.Release|x86.ActiveCfg = Release|x86 + {DF8CE83B-0BF0-412D-A5B6-BBE27F58E675}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(NestedProjects) = preSolution {B7F309C8-75FC-41FE-BF10-32B727AF9EB4} = {FD1C9140-EAE4-4C8B-8B93-985C07992FF7} {EBD6AEB9-1E0B-4651-83CD-5D4956C19EA3} = {D20967AB-999E-44C6-9F01-BF0638F12D1E} {0045104E-E9BC-4566-8324-3D1E4099CD6E} = {D20967AB-999E-44C6-9F01-BF0638F12D1E} + {DF8CE83B-0BF0-412D-A5B6-BBE27F58E675} = {0A575C00-110D-46B7-A30D-E69A25961CCE} EndGlobalSection EndGlobal diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000..6f666e0 --- /dev/null +++ b/nuget.config @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/samples/Beeline.AspNetCoreBenchmarks/Beeline.AspNetCoreBenchmarks.csproj b/samples/Beeline.AspNetCoreBenchmarks/Beeline.AspNetCoreBenchmarks.csproj new file mode 100644 index 0000000..e949be1 --- /dev/null +++ b/samples/Beeline.AspNetCoreBenchmarks/Beeline.AspNetCoreBenchmarks.csproj @@ -0,0 +1,19 @@ + + + netcoreapp2.1 + 2.1.0-preview1-26103-03 + 2.1.0-preview1-26103-03 + 7.2 + + + Microsoft.NETCore.App + + + + + + + + + + \ No newline at end of file diff --git a/samples/Beeline.AspNetCoreBenchmarks/Configuration/AppSettings.cs b/samples/Beeline.AspNetCoreBenchmarks/Configuration/AppSettings.cs new file mode 100644 index 0000000..80079a6 --- /dev/null +++ b/samples/Beeline.AspNetCoreBenchmarks/Configuration/AppSettings.cs @@ -0,0 +1,7 @@ +namespace Beeline.AspNetCoreBenchmarks.Configuration +{ + public class AppSettings + { + public string ConnectionString { get; set; } + } +} \ No newline at end of file diff --git a/samples/Beeline.AspNetCoreBenchmarks/Controllers/ValuesController.cs b/samples/Beeline.AspNetCoreBenchmarks/Controllers/ValuesController.cs new file mode 100644 index 0000000..632ca7d --- /dev/null +++ b/samples/Beeline.AspNetCoreBenchmarks/Controllers/ValuesController.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + +namespace Beeline.AspNetCoreBenchmarks.Controllers +{ + [Route("api/[controller]")] + public class ValuesController : Controller + { + // GET api/values + [HttpGet] + public IEnumerable Get() + { + return new string[] { "value1", "value2" }; + } + + // GET api/values/5 + [HttpGet("{id}")] + public string Get(int id) + { + return "value"; + } + + // POST api/values + [HttpPost] + public void Post([FromBody]string value) + { + } + + // PUT api/values/5 + [HttpPut("{id}")] + public void Put(int id, [FromBody]string value) + { + } + + // DELETE api/values/5 + [HttpDelete("{id}")] + public void Delete(int id) + { + } + } +} diff --git a/samples/Beeline.AspNetCoreBenchmarks/Data/BeelineDb.cs b/samples/Beeline.AspNetCoreBenchmarks/Data/BeelineDb.cs new file mode 100644 index 0000000..188e8cd --- /dev/null +++ b/samples/Beeline.AspNetCoreBenchmarks/Data/BeelineDb.cs @@ -0,0 +1,61 @@ +using System; +using System.Buffers; +using System.Data; +using System.Data.Common; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Beeline.AspNetCoreBenchmarks.Configuration; +using Microsoft.Extensions.Options; + +namespace Beeline.AspNetCoreBenchmarks.Data +{ + public class BeelineDb + { + private readonly IRandom _random; + private readonly DbProviderFactory _dbProvider; + private readonly string _connectionString; + private ObjectWriter _objectWriter; + + public BeelineDb(IRandom random, DbProviderFactory dbProvider, IOptions appSettings) + { + _random = random; + _dbProvider = dbProvider; + _connectionString = appSettings.Value.ConnectionString; + } + + public async Task LoadSingleQueryRow(byte[] buffer, CancellationToken ct = default) + { + using (var db = _dbProvider.CreateConnection()) + { + Debug.Assert(db != null); + db.ConnectionString = _connectionString; + using (var cmd = CreateSingleQueryCommand(db, _random.Next(1, 10001))) + { + await db.OpenAsync(ct); + using (var reader = await cmd.ExecuteReaderAsync(ct)) + { + if (_objectWriter is null) + { + _objectWriter = new ObjectWriter(RowSerializer.For(reader, true), 1024); + } + + return await _objectWriter.WriteSingle(reader, buffer, ct); + } + } + } + } + + private static DbCommand CreateSingleQueryCommand(DbConnection db, int id) + { + var cmd = db.CreateCommand(); + cmd.CommandText = "SELECT id, randomnumber FROM world WHERE id = @id"; + var idParameter = cmd.CreateParameter(); + idParameter.ParameterName = "id"; + idParameter.DbType = DbType.Int32; + idParameter.Value = id; + cmd.Parameters.Add(idParameter); + return cmd; + } + } +} \ No newline at end of file diff --git a/samples/Beeline.AspNetCoreBenchmarks/Data/DatabaseInitializer.cs b/samples/Beeline.AspNetCoreBenchmarks/Data/DatabaseInitializer.cs new file mode 100644 index 0000000..68156b6 --- /dev/null +++ b/samples/Beeline.AspNetCoreBenchmarks/Data/DatabaseInitializer.cs @@ -0,0 +1,44 @@ +using System; +using Npgsql; +using NpgsqlTypes; + +namespace Beeline.AspNetCoreBenchmarks.Data +{ + public static class DatabaseInitializer + { + private const string DropTable = "drop table if exists world"; + private const string CreateTable = @"create table world +( + id integer not null + constraint world_pkey + primary key, + randomnumber integer +)"; + + private const string Insert = "INSERT INTO world (id, randomnumber) VALUES (@id, @randomnumber)"; + + public static void Initialize(string connectionString) + { + var random = new Random(42); + using (var connection = new NpgsqlConnection(connectionString)) + using (var cmd = connection.CreateCommand()) + { + connection.Open(); + cmd.CommandText = DropTable; + cmd.ExecuteNonQuery(); + cmd.CommandText = CreateTable; + cmd.ExecuteNonQuery(); + cmd.CommandText = Insert; + var @id = cmd.Parameters.Add("id", NpgsqlDbType.Integer); + var @randomnumber = cmd.Parameters.Add("randomnumber", NpgsqlDbType.Integer); + cmd.Prepare(); + for (int i = 1; i < 10001; i++) + { + @id.Value = i; + @randomnumber.Value = random.Next(1, 999_999_999); + cmd.ExecuteNonQuery(); + } + } + } + } +} \ No newline at end of file diff --git a/samples/Beeline.AspNetCoreBenchmarks/Data/DefaultRandom.cs b/samples/Beeline.AspNetCoreBenchmarks/Data/DefaultRandom.cs new file mode 100644 index 0000000..c4e2e4f --- /dev/null +++ b/samples/Beeline.AspNetCoreBenchmarks/Data/DefaultRandom.cs @@ -0,0 +1,17 @@ +using System; +using System.Threading; + +namespace Beeline.AspNetCoreBenchmarks.Data +{ + public class DefaultRandom : IRandom + { + private static int _nextSeed = 0; + // Random isn't thread safe + private static readonly ThreadLocal Random = new ThreadLocal(() => new Random(Interlocked.Increment(ref _nextSeed))); + + public int Next(int minValue, int maxValue) + { + return Random.Value.Next(minValue, maxValue); + } + } +} \ No newline at end of file diff --git a/samples/Beeline.AspNetCoreBenchmarks/Data/IRandom.cs b/samples/Beeline.AspNetCoreBenchmarks/Data/IRandom.cs new file mode 100644 index 0000000..a614650 --- /dev/null +++ b/samples/Beeline.AspNetCoreBenchmarks/Data/IRandom.cs @@ -0,0 +1,7 @@ +namespace Beeline.AspNetCoreBenchmarks.Data +{ + public interface IRandom + { + int Next(int minValue, int maxValue); + } +} \ No newline at end of file diff --git a/samples/Beeline.AspNetCoreBenchmarks/Middleware/SingleQueryBeelineMiddleware.cs b/samples/Beeline.AspNetCoreBenchmarks/Middleware/SingleQueryBeelineMiddleware.cs new file mode 100644 index 0000000..ff93ea3 --- /dev/null +++ b/samples/Beeline.AspNetCoreBenchmarks/Middleware/SingleQueryBeelineMiddleware.cs @@ -0,0 +1,58 @@ +using System; +using System.Buffers; +using System.Threading.Tasks; +using Beeline.AspNetCoreBenchmarks.Data; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Beeline.AspNetCoreBenchmarks.Middleware +{ + public class SingleQueryBeelineMiddleware + { + private static readonly PathString Path = new PathString("/db/beeline"); + + private readonly RequestDelegate _next; + + public SingleQueryBeelineMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task Invoke(HttpContext httpContext) + { + if (httpContext.Request.Path.StartsWithSegments(Path, StringComparison.Ordinal)) + { + var db = httpContext.RequestServices.GetService(); + var buffer = ArrayPool.Shared.Rent(128); + try + { + var length = await db.LoadSingleQueryRow(buffer, httpContext.RequestAborted); + + httpContext.Response.StatusCode = StatusCodes.Status200OK; + httpContext.Response.ContentType = "application/json"; + httpContext.Response.ContentLength = length; + await httpContext.Response.Body.WriteAsync(buffer, 0, length, httpContext.RequestAborted); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + + return; + } + + await _next(httpContext); + } + } + + public static class SingleQueryDapperMiddlewareExtensions + { + public static IApplicationBuilder UseSingleQueryBeeline(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} \ No newline at end of file diff --git a/samples/Beeline.AspNetCoreBenchmarks/Program.cs b/samples/Beeline.AspNetCoreBenchmarks/Program.cs new file mode 100644 index 0000000..6b91ef6 --- /dev/null +++ b/samples/Beeline.AspNetCoreBenchmarks/Program.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Beeline.AspNetCoreBenchmarks +{ + [UsedImplicitly] + public class Program + { + public static void Main(string[] args) + { + BuildWebHost(args).Run(); + } + + private static IWebHost BuildWebHost(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup() + .Build(); + } +} diff --git a/samples/Beeline.AspNetCoreBenchmarks/Startup.cs b/samples/Beeline.AspNetCoreBenchmarks/Startup.cs new file mode 100644 index 0000000..121184d --- /dev/null +++ b/samples/Beeline.AspNetCoreBenchmarks/Startup.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; +using System.Threading.Tasks; +using Beeline.AspNetCoreBenchmarks.Configuration; +using Beeline.AspNetCoreBenchmarks.Data; +using Beeline.AspNetCoreBenchmarks.Middleware; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Npgsql; + +namespace Beeline.AspNetCoreBenchmarks +{ + [UsedImplicitly] + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + private IConfiguration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + DatabaseInitializer.Initialize(Configuration.GetValue("ConnectionString")); + services.Configure(Configuration); + services.AddSingleton(); + services.AddSingleton(NpgsqlFactory.Instance); + services.AddSingleton(); + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + app.UseSingleQueryBeeline(); + } + } +} diff --git a/samples/Beeline.AspNetCoreBenchmarks/appsettings.Development.json b/samples/Beeline.AspNetCoreBenchmarks/appsettings.Development.json new file mode 100644 index 0000000..3bb1924 --- /dev/null +++ b/samples/Beeline.AspNetCoreBenchmarks/appsettings.Development.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + }, + "ConnectionString": "Host=localhost;Database=beeline;Username=beeline;Password=S3cr3tSqu1rr3l" +} diff --git a/samples/Beeline.AspNetCoreBenchmarks/appsettings.json b/samples/Beeline.AspNetCoreBenchmarks/appsettings.json new file mode 100644 index 0000000..d8379f9 --- /dev/null +++ b/samples/Beeline.AspNetCoreBenchmarks/appsettings.json @@ -0,0 +1,16 @@ +{ + "Logging": { + "IncludeScopes": false, + "Debug": { + "LogLevel": { + "Default": "Warning" + } + }, + "Console": { + "LogLevel": { + "Default": "Warning" + } + } + }, + "ConnectionString": "Host=localhost;Database=beeline;Username=beeline;Password=S3cr3tSqu1rr3l" +} diff --git a/src/Beeline/ArrayWriter.cs b/src/Beeline/ArrayWriter.cs index 9e7b947..47870d7 100644 --- a/src/Beeline/ArrayWriter.cs +++ b/src/Beeline/ArrayWriter.cs @@ -1,5 +1,6 @@ using System; using System.Buffers; +using System.ComponentModel; using System.Data.Common; using System.IO; using System.Threading; @@ -27,7 +28,6 @@ public async Task Write(DbDataReader reader, Memory buffer, Cancellat { int rowCount = 0; int pos = 0; - bool first = true; buffer.Span[pos++] = OpenBracket; if (!await reader.ReadAsync(ct).ConfigureAwait(false)) return 0; diff --git a/src/Beeline/ObjectWriter.cs b/src/Beeline/ObjectWriter.cs index 787d50d..f771c22 100644 --- a/src/Beeline/ObjectWriter.cs +++ b/src/Beeline/ObjectWriter.cs @@ -22,8 +22,7 @@ public async Task WriteSingle(DbDataReader reader, Memory buffer, Can { if (await reader.ReadAsync(ct).ConfigureAwait(false)) { - int bytes = _serializer.Write(reader, buffer.Span); - return 1; + return _serializer.Write(reader, buffer.Span); } return 0; diff --git a/test/Beeline.Benchmarks/Beeline.Benchmarks.csproj b/test/Beeline.Benchmarks/Beeline.Benchmarks.csproj index 84723d8..1cf341a 100644 --- a/test/Beeline.Benchmarks/Beeline.Benchmarks.csproj +++ b/test/Beeline.Benchmarks/Beeline.Benchmarks.csproj @@ -4,6 +4,7 @@ netcoreapp2.1 2.1.0-preview1-26016-05 2.1.0-preview1-26016-05 + 7.2