Skip to content

Commit bcaf5aa

Browse files
committed
Demonstrate how to choose DbContext per request.
Note this only works properly if the EF Core models (not the database tables) are identical.
1 parent 9c51fe3 commit bcaf5aa

7 files changed

+156
-10
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using GettingStarted.Models;
2+
using JetBrains.Annotations;
3+
using Microsoft.EntityFrameworkCore;
4+
5+
namespace GettingStarted.Data;
6+
7+
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
8+
public class PostgreSqlSampleDbContext(DbContextOptions<PostgreSqlSampleDbContext> options) : DbContext(options)
9+
{
10+
public DbSet<Book> Books => Set<Book>();
11+
}

src/Examples/GettingStarted/Data/SampleDbContext.cs renamed to src/Examples/GettingStarted/Data/SqliteSampleDbContext.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
namespace GettingStarted.Data;
66

77
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
8-
public class SampleDbContext(DbContextOptions<SampleDbContext> options) : DbContext(options)
8+
public class SqliteSampleDbContext(DbContextOptions<SqliteSampleDbContext> options) : DbContext(options)
99
{
1010
public DbSet<Book> Books => Set<Book>();
1111
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace GettingStarted;
2+
3+
internal enum DatabaseType
4+
{
5+
Sqlite,
6+
PostgreSql
7+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using JsonApiDotNetCore.Configuration;
2+
using JsonApiDotNetCore.Middleware;
3+
using JsonApiDotNetCore.Queries;
4+
using JsonApiDotNetCore.Queries.Parsing;
5+
using JsonApiDotNetCore.Serialization.Response;
6+
using Microsoft.Extensions.Primitives;
7+
8+
namespace GettingStarted;
9+
10+
public sealed class DbAwareLinkBuilder(
11+
IJsonApiOptions options, IJsonApiRequest request, IPaginationContext paginationContext, IHttpContextAccessor httpContextAccessor,
12+
LinkGenerator linkGenerator, IControllerResourceMapping controllerResourceMapping, IPaginationParser paginationParser,
13+
IDocumentDescriptionLinkProvider documentDescriptionLinkProvider) : LinkBuilder(options, request, paginationContext, httpContextAccessor, linkGenerator,
14+
controllerResourceMapping, paginationParser, documentDescriptionLinkProvider)
15+
{
16+
private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor;
17+
18+
protected override string? RenderLinkForAction(string? controllerName, string actionName, IDictionary<string, object?> routeValues)
19+
{
20+
if (!routeValues.ContainsKey("dbType"))
21+
{
22+
HttpContext? httpContext = _httpContextAccessor.HttpContext;
23+
24+
if (httpContext != null)
25+
{
26+
StringValues dbType = httpContext.Request.Query["dbType"];
27+
routeValues.Add("dbType", dbType);
28+
}
29+
}
30+
31+
return base.RenderLinkForAction(controllerName, actionName, routeValues);
32+
}
33+
}

src/Examples/GettingStarted/GettingStarted.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@
1313

1414
<ItemGroup>
1515
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="$(EntityFrameworkCoreVersion)" />
16+
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="$(EntityFrameworkCoreVersion)" />
1617
</ItemGroup>
1718
</Project>

src/Examples/GettingStarted/Program.cs

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,43 @@
11
using System.Diagnostics;
2+
using GettingStarted;
23
using GettingStarted.Data;
34
using GettingStarted.Models;
45
using JsonApiDotNetCore.Configuration;
6+
using JsonApiDotNetCore.Repositories;
7+
using JsonApiDotNetCore.Serialization.Response;
58
using Microsoft.EntityFrameworkCore;
69
using Microsoft.EntityFrameworkCore.Diagnostics;
710

811
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
912

1013
// Add services to the container.
1114

12-
builder.Services.AddDbContext<SampleDbContext>(options =>
15+
builder.Services.AddDbContext<SqliteSampleDbContext>(options =>
1316
{
1417
options.UseSqlite("Data Source=SampleDb.db;Pooling=False");
1518
SetDbContextDebugOptions(options);
1619
});
1720

18-
builder.Services.AddJsonApi<SampleDbContext>(options =>
21+
builder.Services.AddDbContext<PostgreSqlSampleDbContext>(options =>
22+
{
23+
options.UseNpgsql("Host=localhost;Database=ExampleDb;User ID=postgres;Password=postgres;Include Error Detail=true");
24+
SetDbContextDebugOptions(options);
25+
});
26+
27+
// EntityFrameworkCoreRepository injects IDbContextResolver to obtain the DbContext during a request.
28+
builder.Services.AddHttpContextAccessor();
29+
builder.Services.AddScoped<IDbContextResolver, QueryStringDbContextResolver>();
30+
31+
// Make rendered links contain the dbType query string parameter.
32+
builder.Services.AddScoped<ILinkBuilder, DbAwareLinkBuilder>();
33+
34+
// DbContext is used to scan the model at app startup. Pick any, since their entities are identical.
35+
builder.Services.AddJsonApi<SqliteSampleDbContext>(options =>
1936
{
2037
options.Namespace = "api";
2138
options.UseRelativeLinks = true;
2239
options.IncludeTotalResourceCount = true;
40+
options.AllowUnknownQueryStringParameters = true;
2341

2442
#if DEBUG
2543
options.IncludeExceptionStackTraceInErrors = true;
@@ -36,7 +54,8 @@
3654
app.UseJsonApi();
3755
app.MapControllers();
3856

39-
await CreateDatabaseAsync(app.Services);
57+
await CreateSqliteDatabaseAsync(app.Services);
58+
await CreatePostgreSqlDatabaseAsync(app.Services);
4059

4160
app.Run();
4261

@@ -48,21 +67,19 @@ static void SetDbContextDebugOptions(DbContextOptionsBuilder options)
4867
options.ConfigureWarnings(builder => builder.Ignore(CoreEventId.SensitiveDataLoggingEnabledWarning));
4968
}
5069

51-
static async Task CreateDatabaseAsync(IServiceProvider serviceProvider)
70+
static async Task CreateSqliteDatabaseAsync(IServiceProvider serviceProvider)
5271
{
5372
await using AsyncServiceScope scope = serviceProvider.CreateAsyncScope();
5473

55-
var dbContext = scope.ServiceProvider.GetRequiredService<SampleDbContext>();
74+
var dbContext = scope.ServiceProvider.GetRequiredService<SqliteSampleDbContext>();
5675
await dbContext.Database.EnsureDeletedAsync();
5776
await dbContext.Database.EnsureCreatedAsync();
5877

59-
await CreateSampleDataAsync(dbContext);
78+
await CreateSqliteSampleDataAsync(dbContext);
6079
}
6180

62-
static async Task CreateSampleDataAsync(SampleDbContext dbContext)
81+
static async Task CreateSqliteSampleDataAsync(SqliteSampleDbContext dbContext)
6382
{
64-
// Note: The generate-examples.ps1 script (to create example requests in documentation) depends on these.
65-
6683
dbContext.Books.AddRange(new Book
6784
{
6885
Title = "Frankenstein",
@@ -91,3 +108,37 @@ static async Task CreateSampleDataAsync(SampleDbContext dbContext)
91108

92109
await dbContext.SaveChangesAsync();
93110
}
111+
112+
static async Task CreatePostgreSqlDatabaseAsync(IServiceProvider serviceProvider)
113+
{
114+
await using AsyncServiceScope scope = serviceProvider.CreateAsyncScope();
115+
116+
var dbContext = scope.ServiceProvider.GetRequiredService<PostgreSqlSampleDbContext>();
117+
await dbContext.Database.EnsureDeletedAsync();
118+
await dbContext.Database.EnsureCreatedAsync();
119+
120+
await CreatePostgreSqlSampleDataAsync(dbContext);
121+
}
122+
123+
static async Task CreatePostgreSqlSampleDataAsync(PostgreSqlSampleDbContext dbContext)
124+
{
125+
dbContext.Books.AddRange(new Book
126+
{
127+
Title = "Wolf Hall",
128+
PublishYear = 2009,
129+
Author = new Person
130+
{
131+
Name = "Hilary Mantel"
132+
}
133+
}, new Book
134+
{
135+
Title = "Gilead",
136+
PublishYear = 2004,
137+
Author = new Person
138+
{
139+
Name = "Marilynne Robinson"
140+
}
141+
});
142+
143+
await dbContext.SaveChangesAsync();
144+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using System.Net;
2+
using GettingStarted.Data;
3+
using JsonApiDotNetCore.Errors;
4+
using JsonApiDotNetCore.Repositories;
5+
using JsonApiDotNetCore.Serialization.Objects;
6+
using Microsoft.EntityFrameworkCore;
7+
using Microsoft.Extensions.Primitives;
8+
9+
namespace GettingStarted;
10+
11+
public sealed class QueryStringDbContextResolver(IHttpContextAccessor httpContextAccessor, SqliteSampleDbContext startupDbContext) : IDbContextResolver
12+
{
13+
private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor;
14+
private readonly SqliteSampleDbContext _startupDbContext = startupDbContext;
15+
16+
public DbContext GetContext()
17+
{
18+
HttpContext? httpContext = _httpContextAccessor.HttpContext;
19+
20+
if (httpContext == null)
21+
{
22+
// DbContext is used to scan the model at app startup. Pick any, since their entities are identical.
23+
return _startupDbContext;
24+
}
25+
26+
StringValues dbType = httpContext.Request.Query["dbType"];
27+
28+
if (!Enum.TryParse(dbType, true, out DatabaseType databaseType))
29+
{
30+
throw new JsonApiException(new ErrorObject(HttpStatusCode.BadRequest)
31+
{
32+
Title = "The 'dbType' query string parameter is missing or invalid."
33+
});
34+
}
35+
36+
return databaseType switch
37+
{
38+
DatabaseType.Sqlite => httpContext.RequestServices.GetRequiredService<SqliteSampleDbContext>(),
39+
DatabaseType.PostgreSql => httpContext.RequestServices.GetRequiredService<PostgreSqlSampleDbContext>(),
40+
_ => throw new NotSupportedException("Unknown database type.")
41+
};
42+
}
43+
}

0 commit comments

Comments
 (0)