From bcaf5aa142f8d528143c8ed40004bd47209fd4cc Mon Sep 17 00:00:00 2001 From: Bart Koelman <10324372+bkoelman@users.noreply.github.com> Date: Tue, 23 Apr 2024 10:55:36 +0200 Subject: [PATCH] Demonstrate how to choose DbContext per request. Note this only works properly if the EF Core models (not the database tables) are identical. --- .../Data/PostgreSqlSampleDbContext.cs | 11 +++ ...eDbContext.cs => SqliteSampleDbContext.cs} | 2 +- src/Examples/GettingStarted/DatabaseType.cs | 7 ++ .../GettingStarted/DbAwareLinkBuilder.cs | 33 +++++++++ .../GettingStarted/GettingStarted.csproj | 1 + src/Examples/GettingStarted/Program.cs | 69 ++++++++++++++++--- .../QueryStringDbContextResolver.cs | 43 ++++++++++++ 7 files changed, 156 insertions(+), 10 deletions(-) create mode 100644 src/Examples/GettingStarted/Data/PostgreSqlSampleDbContext.cs rename src/Examples/GettingStarted/Data/{SampleDbContext.cs => SqliteSampleDbContext.cs} (68%) create mode 100644 src/Examples/GettingStarted/DatabaseType.cs create mode 100644 src/Examples/GettingStarted/DbAwareLinkBuilder.cs create mode 100644 src/Examples/GettingStarted/QueryStringDbContextResolver.cs diff --git a/src/Examples/GettingStarted/Data/PostgreSqlSampleDbContext.cs b/src/Examples/GettingStarted/Data/PostgreSqlSampleDbContext.cs new file mode 100644 index 0000000000..d6a49f3d82 --- /dev/null +++ b/src/Examples/GettingStarted/Data/PostgreSqlSampleDbContext.cs @@ -0,0 +1,11 @@ +using GettingStarted.Models; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; + +namespace GettingStarted.Data; + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +public class PostgreSqlSampleDbContext(DbContextOptions options) : DbContext(options) +{ + public DbSet Books => Set(); +} diff --git a/src/Examples/GettingStarted/Data/SampleDbContext.cs b/src/Examples/GettingStarted/Data/SqliteSampleDbContext.cs similarity index 68% rename from src/Examples/GettingStarted/Data/SampleDbContext.cs rename to src/Examples/GettingStarted/Data/SqliteSampleDbContext.cs index 5e65f8466e..58b9664dc9 100644 --- a/src/Examples/GettingStarted/Data/SampleDbContext.cs +++ b/src/Examples/GettingStarted/Data/SqliteSampleDbContext.cs @@ -5,7 +5,7 @@ namespace GettingStarted.Data; [UsedImplicitly(ImplicitUseTargetFlags.Members)] -public class SampleDbContext(DbContextOptions options) : DbContext(options) +public class SqliteSampleDbContext(DbContextOptions options) : DbContext(options) { public DbSet Books => Set(); } diff --git a/src/Examples/GettingStarted/DatabaseType.cs b/src/Examples/GettingStarted/DatabaseType.cs new file mode 100644 index 0000000000..347319902f --- /dev/null +++ b/src/Examples/GettingStarted/DatabaseType.cs @@ -0,0 +1,7 @@ +namespace GettingStarted; + +internal enum DatabaseType +{ + Sqlite, + PostgreSql +} diff --git a/src/Examples/GettingStarted/DbAwareLinkBuilder.cs b/src/Examples/GettingStarted/DbAwareLinkBuilder.cs new file mode 100644 index 0000000000..2c72925ec4 --- /dev/null +++ b/src/Examples/GettingStarted/DbAwareLinkBuilder.cs @@ -0,0 +1,33 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Queries; +using JsonApiDotNetCore.Queries.Parsing; +using JsonApiDotNetCore.Serialization.Response; +using Microsoft.Extensions.Primitives; + +namespace GettingStarted; + +public sealed class DbAwareLinkBuilder( + IJsonApiOptions options, IJsonApiRequest request, IPaginationContext paginationContext, IHttpContextAccessor httpContextAccessor, + LinkGenerator linkGenerator, IControllerResourceMapping controllerResourceMapping, IPaginationParser paginationParser, + IDocumentDescriptionLinkProvider documentDescriptionLinkProvider) : LinkBuilder(options, request, paginationContext, httpContextAccessor, linkGenerator, + controllerResourceMapping, paginationParser, documentDescriptionLinkProvider) +{ + private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor; + + protected override string? RenderLinkForAction(string? controllerName, string actionName, IDictionary routeValues) + { + if (!routeValues.ContainsKey("dbType")) + { + HttpContext? httpContext = _httpContextAccessor.HttpContext; + + if (httpContext != null) + { + StringValues dbType = httpContext.Request.Query["dbType"]; + routeValues.Add("dbType", dbType); + } + } + + return base.RenderLinkForAction(controllerName, actionName, routeValues); + } +} diff --git a/src/Examples/GettingStarted/GettingStarted.csproj b/src/Examples/GettingStarted/GettingStarted.csproj index 1f4645f323..0fd74b4e64 100644 --- a/src/Examples/GettingStarted/GettingStarted.csproj +++ b/src/Examples/GettingStarted/GettingStarted.csproj @@ -13,5 +13,6 @@ + diff --git a/src/Examples/GettingStarted/Program.cs b/src/Examples/GettingStarted/Program.cs index 9ce6beda08..03b61c8adb 100644 --- a/src/Examples/GettingStarted/Program.cs +++ b/src/Examples/GettingStarted/Program.cs @@ -1,7 +1,10 @@ using System.Diagnostics; +using GettingStarted; using GettingStarted.Data; using GettingStarted.Models; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Repositories; +using JsonApiDotNetCore.Serialization.Response; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; @@ -9,17 +12,32 @@ // Add services to the container. -builder.Services.AddDbContext(options => +builder.Services.AddDbContext(options => { options.UseSqlite("Data Source=SampleDb.db;Pooling=False"); SetDbContextDebugOptions(options); }); -builder.Services.AddJsonApi(options => +builder.Services.AddDbContext(options => +{ + options.UseNpgsql("Host=localhost;Database=ExampleDb;User ID=postgres;Password=postgres;Include Error Detail=true"); + SetDbContextDebugOptions(options); +}); + +// EntityFrameworkCoreRepository injects IDbContextResolver to obtain the DbContext during a request. +builder.Services.AddHttpContextAccessor(); +builder.Services.AddScoped(); + +// Make rendered links contain the dbType query string parameter. +builder.Services.AddScoped(); + +// DbContext is used to scan the model at app startup. Pick any, since their entities are identical. +builder.Services.AddJsonApi(options => { options.Namespace = "api"; options.UseRelativeLinks = true; options.IncludeTotalResourceCount = true; + options.AllowUnknownQueryStringParameters = true; #if DEBUG options.IncludeExceptionStackTraceInErrors = true; @@ -36,7 +54,8 @@ app.UseJsonApi(); app.MapControllers(); -await CreateDatabaseAsync(app.Services); +await CreateSqliteDatabaseAsync(app.Services); +await CreatePostgreSqlDatabaseAsync(app.Services); app.Run(); @@ -48,21 +67,19 @@ static void SetDbContextDebugOptions(DbContextOptionsBuilder options) options.ConfigureWarnings(builder => builder.Ignore(CoreEventId.SensitiveDataLoggingEnabledWarning)); } -static async Task CreateDatabaseAsync(IServiceProvider serviceProvider) +static async Task CreateSqliteDatabaseAsync(IServiceProvider serviceProvider) { await using AsyncServiceScope scope = serviceProvider.CreateAsyncScope(); - var dbContext = scope.ServiceProvider.GetRequiredService(); + var dbContext = scope.ServiceProvider.GetRequiredService(); await dbContext.Database.EnsureDeletedAsync(); await dbContext.Database.EnsureCreatedAsync(); - await CreateSampleDataAsync(dbContext); + await CreateSqliteSampleDataAsync(dbContext); } -static async Task CreateSampleDataAsync(SampleDbContext dbContext) +static async Task CreateSqliteSampleDataAsync(SqliteSampleDbContext dbContext) { - // Note: The generate-examples.ps1 script (to create example requests in documentation) depends on these. - dbContext.Books.AddRange(new Book { Title = "Frankenstein", @@ -91,3 +108,37 @@ static async Task CreateSampleDataAsync(SampleDbContext dbContext) await dbContext.SaveChangesAsync(); } + +static async Task CreatePostgreSqlDatabaseAsync(IServiceProvider serviceProvider) +{ + await using AsyncServiceScope scope = serviceProvider.CreateAsyncScope(); + + var dbContext = scope.ServiceProvider.GetRequiredService(); + await dbContext.Database.EnsureDeletedAsync(); + await dbContext.Database.EnsureCreatedAsync(); + + await CreatePostgreSqlSampleDataAsync(dbContext); +} + +static async Task CreatePostgreSqlSampleDataAsync(PostgreSqlSampleDbContext dbContext) +{ + dbContext.Books.AddRange(new Book + { + Title = "Wolf Hall", + PublishYear = 2009, + Author = new Person + { + Name = "Hilary Mantel" + } + }, new Book + { + Title = "Gilead", + PublishYear = 2004, + Author = new Person + { + Name = "Marilynne Robinson" + } + }); + + await dbContext.SaveChangesAsync(); +} diff --git a/src/Examples/GettingStarted/QueryStringDbContextResolver.cs b/src/Examples/GettingStarted/QueryStringDbContextResolver.cs new file mode 100644 index 0000000000..d63ba7af63 --- /dev/null +++ b/src/Examples/GettingStarted/QueryStringDbContextResolver.cs @@ -0,0 +1,43 @@ +using System.Net; +using GettingStarted.Data; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Repositories; +using JsonApiDotNetCore.Serialization.Objects; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Primitives; + +namespace GettingStarted; + +public sealed class QueryStringDbContextResolver(IHttpContextAccessor httpContextAccessor, SqliteSampleDbContext startupDbContext) : IDbContextResolver +{ + private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor; + private readonly SqliteSampleDbContext _startupDbContext = startupDbContext; + + public DbContext GetContext() + { + HttpContext? httpContext = _httpContextAccessor.HttpContext; + + if (httpContext == null) + { + // DbContext is used to scan the model at app startup. Pick any, since their entities are identical. + return _startupDbContext; + } + + StringValues dbType = httpContext.Request.Query["dbType"]; + + if (!Enum.TryParse(dbType, true, out DatabaseType databaseType)) + { + throw new JsonApiException(new ErrorObject(HttpStatusCode.BadRequest) + { + Title = "The 'dbType' query string parameter is missing or invalid." + }); + } + + return databaseType switch + { + DatabaseType.Sqlite => httpContext.RequestServices.GetRequiredService(), + DatabaseType.PostgreSql => httpContext.RequestServices.GetRequiredService(), + _ => throw new NotSupportedException("Unknown database type.") + }; + } +}