Skip to content

Demonstrate how to choose DbContext per request #1535

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/Examples/GettingStarted/Data/PostgreSqlSampleDbContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using GettingStarted.Models;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;

namespace GettingStarted.Data;

[UsedImplicitly(ImplicitUseTargetFlags.Members)]
public class PostgreSqlSampleDbContext(DbContextOptions<PostgreSqlSampleDbContext> options) : DbContext(options)
{
public DbSet<Book> Books => Set<Book>();
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace GettingStarted.Data;

[UsedImplicitly(ImplicitUseTargetFlags.Members)]
public class SampleDbContext(DbContextOptions<SampleDbContext> options) : DbContext(options)
public class SqliteSampleDbContext(DbContextOptions<SqliteSampleDbContext> options) : DbContext(options)
{
public DbSet<Book> Books => Set<Book>();
}
7 changes: 7 additions & 0 deletions src/Examples/GettingStarted/DatabaseType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace GettingStarted;

internal enum DatabaseType
{
Sqlite,
PostgreSql
}
33 changes: 33 additions & 0 deletions src/Examples/GettingStarted/DbAwareLinkBuilder.cs
Original file line number Diff line number Diff line change
@@ -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<string, object?> 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);
}
}
1 change: 1 addition & 0 deletions src/Examples/GettingStarted/GettingStarted.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="$(EntityFrameworkCoreVersion)" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="$(EntityFrameworkCoreVersion)" />
</ItemGroup>
</Project>
69 changes: 60 additions & 9 deletions src/Examples/GettingStarted/Program.cs
Original file line number Diff line number Diff line change
@@ -1,25 +1,43 @@
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;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddDbContext<SampleDbContext>(options =>
builder.Services.AddDbContext<SqliteSampleDbContext>(options =>
{
options.UseSqlite("Data Source=SampleDb.db;Pooling=False");
SetDbContextDebugOptions(options);
});

builder.Services.AddJsonApi<SampleDbContext>(options =>
builder.Services.AddDbContext<PostgreSqlSampleDbContext>(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<IDbContextResolver, QueryStringDbContextResolver>();

// Make rendered links contain the dbType query string parameter.
builder.Services.AddScoped<ILinkBuilder, DbAwareLinkBuilder>();

// DbContext is used to scan the model at app startup. Pick any, since their entities are identical.
builder.Services.AddJsonApi<SqliteSampleDbContext>(options =>
{
options.Namespace = "api";
options.UseRelativeLinks = true;
options.IncludeTotalResourceCount = true;
options.AllowUnknownQueryStringParameters = true;

#if DEBUG
options.IncludeExceptionStackTraceInErrors = true;
Expand All @@ -36,7 +54,8 @@
app.UseJsonApi();
app.MapControllers();

await CreateDatabaseAsync(app.Services);
await CreateSqliteDatabaseAsync(app.Services);
await CreatePostgreSqlDatabaseAsync(app.Services);

app.Run();

Expand All @@ -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<SampleDbContext>();
var dbContext = scope.ServiceProvider.GetRequiredService<SqliteSampleDbContext>();
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",
Expand Down Expand Up @@ -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<PostgreSqlSampleDbContext>();
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();
}
43 changes: 43 additions & 0 deletions src/Examples/GettingStarted/QueryStringDbContextResolver.cs
Original file line number Diff line number Diff line change
@@ -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<SqliteSampleDbContext>(),
DatabaseType.PostgreSql => httpContext.RequestServices.GetRequiredService<PostgreSqlSampleDbContext>(),
_ => throw new NotSupportedException("Unknown database type.")
};
}
}
Loading