Skip to content

Commit

Permalink
Update docs for DbContext pooling with state
Browse files Browse the repository at this point in the history
To have a proper, scoped ITenant service.

Closes #3787
  • Loading branch information
roji committed Apr 19, 2022
1 parent 561f986 commit c6f7d50
Show file tree
Hide file tree
Showing 5 changed files with 43 additions and 17 deletions.
12 changes: 8 additions & 4 deletions entity-framework/core/performance/advanced-performance-topics.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,20 @@ Context pooling works by reusing the same context instance across requests; this

A typical scenario involving context state would be a multi-tenant ASP.NET Core application, where the context instance has a *tenant ID* which is taken into account by queries (see [Global Query Filters](xref:core/querying/filters) for more details). Since the tenant ID needs to change with each web request, we need to have go through some extra steps to make it all work with context pooling.

First, register a pooling context factory as a Singleton service, as usual:
Let's assume that your application registers a scoped `ITenant` service, which wraps the tenant ID and any other tenant-related information:

[!code-csharp[Main](../../../samples/core/Performance/AspNetContextPoolingWithState/Program.cs#TenantResolution)]

As written above, pay special attention to where you get the tenant ID from: this is an important aspect of your application's security.

Once we have our scoped `ITenant` service, register a pooling context factory as a Singleton service, as usual:

[!code-csharp[Main](../../../samples/core/Performance/AspNetContextPoolingWithState/Program.cs#RegisterSingletonContextFactory)]

Next, write a custom context factory which gets a pooled context from the Singleton factory we registered, finds the tenant ID in the web request's `HttpContext`, and injects the ID into the context:
Next, write a custom context factory which gets a pooled context from the Singleton factory we registered, and injects the tenant ID into context instances it hands out:

[!code-csharp[Main](../../../samples/core/Performance/AspNetContextPoolingWithState/WeatherForecastScopedFactory.cs#WeatherForecastScopedFactory)]

As written above, pay special attention to where you get the tenant ID from: this is an important aspect of your application's security.

Once we have our custom context factory, register it as a Scoped service:

[!code-csharp[Main](../../../samples/core/Performance/AspNetContextPoolingWithState/Program.cs#RegisterScopedContextFactory)]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Performance.AspNetContextPoolingWithState;

public interface ITenant
{
public int TenantId { get; set; }
}
20 changes: 16 additions & 4 deletions samples/core/Performance/AspNetContextPoolingWithState/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.Extensions.Primitives;
using Performance.AspNetContextPoolingWithState;

var builder = WebApplication.CreateBuilder(args);
Expand All @@ -8,6 +8,21 @@
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

#region TenantResolution
// Below is a minimal tenant resolution strategy, which registers a scoped ITenant service in DI.
// In this sample, we simply accept the tenant ID as a request query, which means that a client can impersonate any
// tenant. In a real application, the tenant ID would be set based on secure authentication data.
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<ITenant>(sp =>
{
var tenantIdString = sp.GetRequiredService<IHttpContextAccessor>().HttpContext.Request.Query["TenantId"];
return tenantIdString != StringValues.Empty && int.TryParse(tenantIdString, out var tenantId)
? new Tenant(tenantId)
: null;
});
#endregion

// First, register a pooling context factory as a Singleton service, as usual:
#region RegisterSingletonContextFactory
builder.Services.AddPooledDbContextFactory<WeatherForecastContext>(
Expand All @@ -17,7 +32,6 @@
// Register an additional context factory as a Scoped service, which gets a pooled context from the Singleton factory we registered above,
// finds the tenant ID in the web request's `HttpContext`, and injects the ID into the context:
#region RegisterScopedContextFactory
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<WeatherForecastScopedFactory>();
#endregion

Expand All @@ -36,9 +50,7 @@
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Performance.AspNetContextPoolingWithState;

public class Tenant : ITenant
{
public Tenant(int tenantId)
=> TenantId = tenantId;

public int TenantId { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,17 @@ namespace Performance.AspNetContextPoolingWithState;
#region WeatherForecastScopedFactory
public class WeatherForecastScopedFactory : IDbContextFactory<WeatherForecastContext>
{
private const int DefaultTenantId = -1;

private readonly IDbContextFactory<WeatherForecastContext> _pooledFactory;
private readonly int _tenantId;

public WeatherForecastScopedFactory(
IDbContextFactory<WeatherForecastContext> pooledFactory,
IHttpContextAccessor httpContextAccessor)
ITenant tenant)
{
_pooledFactory = pooledFactory;

// In this sample, we simply accept the tenant ID as a request query, which means that a client can impersonate any tenant.
// In a real application, the tenant ID would be set based on secure authentication data.
var tenantIdString = httpContextAccessor.HttpContext.Request.Query["TenantId"];
if (tenantIdString != StringValues.Empty && int.TryParse(tenantIdString, out var tenantId))
{
_tenantId = tenantId;
}
_tenantId = tenant?.TenantId ?? DefaultTenantId;
}

public WeatherForecastContext CreateDbContext()
Expand Down

0 comments on commit c6f7d50

Please sign in to comment.