I'm rewriting an application from .net MVC to Blazor.
In my old application I resolved DbContext and added current User in a BaseController.
With Blazor I need to resolve the Context in each Component, otherwice several Components will use the same DBContext at the same time, thus resulting in an error. According to MS Docs I should use the IDbContextFactory. But...It registers as a Singleton and I need the logged in user.
My Context has this Constructor:
public class ApplicationDbContext : IdentityDbContext
{
private readonly AuthenticationStateProvider _authenticationStateProvider;
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options, AuthenticationStateProvider authenticationStateProvider)
: base(options)
{
_authenticationStateProvider = authenticationStateProvider;
}
public override int SaveChanges()
{
var user = _authenticationStateProvider.GetAuthenticationStateAsync().Result.User.Identity.Name;
LoggStuff(user);
return base.SaveChanges();
}
If I register a factory like so:
builder.Services.AddDbContextFactory<ApplicationDbContext>(options => options.UseSqlServer(connectionString));
I get an error since the DBContext is not scoped but the AuthenticationStateProvider is.
I could change the factory to create the Context to be scoped I guess, but how would I pass the connectionString as an option then? Or is this the right way to go at all?
CodePudding user response:
This is based on the standard template weatherforcast context. I'm writing an article on a similar subject so quickly adapted the existing code for your answer. It's a bit long, but...
Basically you have a UI <=> ViewService <=> DataService <=> Datastore/ORM data pipeline (there would normally be an interface between the View and Data services, but I've kept it simple). The View service is Scoped
so has access to the registered AuthenticationStateProvider
. You can get whatever user object you want (I've just used Name). I use request and result objects to pass data into the data layer and get results back, so pass the user information as part of the request.
Even if you've decided on a different course it will give you an alternative perspective.
This is my DbContext:
public class InMemoryWeatherDbContext
: DbContext
{
public DbSet<WeatherForecast> WeatherForecast { get; set; } = default!;
public string UserId { get; set; } = string.Empty;
public InMemoryWeatherDbContext(DbContextOptions<InMemoryWeatherDbContext> options) : base(options) { }
}
And my request and result objects:
public record ListRequest
{
public int StartIndex { get; init; } = 0;
public int PageSize { get; init; } = 10000;
public string User { get; init; } = string.Empty;
}
public record ListResult<TRecord>
where TRecord : class
{
public IEnumerable<TRecord> Data { get; init; } = Enumerable.Empty<TRecord>();
public bool Success { get; init; }
}
This is my Data Service, It's registered as a Singleton.
public class WeatherForecastDataService
{
private readonly IDbContextFactory<InMemoryWeatherDbContext> _factory;
public WeatherForecastDataService(IDbContextFactory<InMemoryWeatherDbContext> factory)
=> _factory = factory;
public async Task<ListResult<WeatherForecast>> GetForecastsAsync(ListRequest request)
{
using var context = _factory.CreateDbContext();
context.UserId = request.User;
// Emulate logging
Debug.WriteLine($"User : {request.User} requested a list of Weather Forecasts at {DateTime.Now.ToLongTimeString()}");
IQueryable<WeatherForecast> query = context.Set<WeatherForecast>();
if (request.PageSize > 0)
{
query = query
.Skip(request.StartIndex)
.Take(request.PageSize);
}
var set = await query.ToListAsync();
return new ListResult<WeatherForecast> { Data = set, Success = true };
}
}
And my View Service. It's registered as Scoped, note getting the registered AuthenticationStateProvider
on initialization.
public class WeatherForecastViewService
{
private WeatherForecastDataService _weatherForecastDataService;
private AuthenticationStateProvider _authProvider;
public IEnumerable<WeatherForecast> Records { get; private set; } = Enumerable.Empty<WeatherForecast>();
public WeatherForecastViewService(WeatherForecastDataService weatherForecastDataService, AuthenticationStateProvider authenticationStateProvider)
{
_weatherForecastDataService = weatherForecastDataService;
_authProvider = authenticationStateProvider;
}
public async Task<bool> GetForecastAsync()
{
var user = await this.GetUserAsync();
var request = new ListRequest { User = user };
var result = await _weatherForecastDataService.GetForecastsAsync(request);
this.Records = result.Data;
return this.Records.Count() > 0;
}
private async Task<string> GetUserAsync()
{
var state = await _authProvider.GetAuthenticationStateAsync();
return state is null
? "Anonymous"
: state.User.Identity?.Name ?? "Anonymous";
}
}
FetchData then looks like this:
@page "/fetchdata"
<PageTitle>Weather forecast</PageTitle>
@using BlazorApp6.Data
@inject WeatherForecastViewService ForecastService
<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from a service.</p>
@if (this.ForecastService.Records == null)
{
<p><em>Loading...</em></p>
}
else
{
//.....
}
@code {
protected override async Task OnInitializedAsync()
{
await ForecastService.GetForecastAsync();
}
}
Appendix
For completeness.
I have a singleton pattern TestDataProvider
:
public class WeatherTestDataProvider
{
private int RecordsToGenerate;
public IEnumerable<WeatherForecast> WeatherForecasts { get; private set; } = Enumerable.Empty<WeatherForecast>();
private WeatherTestDataProvider()
=> this.Load();
public void LoadDbContext<TDbContext>(IDbContextFactory<TDbContext> factory) where TDbContext : DbContext
{
using var dbContext = factory.CreateDbContext();
var weatherForcasts = dbContext.Set<WeatherForecast>();
// Check if we already have a full data set
// If not clear down any existing data and start again
if (weatherForcasts.Count() == 0)
{
dbContext.RemoveRange(weatherForcasts.ToList());
dbContext.SaveChanges();
dbContext.AddRange(this.WeatherForecasts);
dbContext.SaveChanges();
}
}
public void Load(int records = 100)
{
RecordsToGenerate = records;
this.LoadForecasts();
}
private void LoadForecasts()
{
var forecasts = new List<WeatherForecast>();
forecasts
.AddRange(Enumerable
.Range(1, RecordsToGenerate)
.Select(index => new WeatherForecast
{
Id = Guid.NewGuid(),
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
);
this.WeatherForecasts = forecasts;
}
private static WeatherTestDataProvider? _weatherTestData;
public static WeatherTestDataProvider Instance()
{
if (_weatherTestData is null)
_weatherTestData = new WeatherTestDataProvider();
return _weatherTestData;
}
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
}
The service registration code:
public static class WeatherAppServices
{
public static void AddWeatherAppServices<TDbContext>(this IServiceCollection services, Action<DbContextOptionsBuilder> options) where TDbContext : DbContext
{
services.AddDbContextFactory<TDbContext>(options);
services.AddSingleton<WeatherForecastDataService>();
services.AddScoped<WeatherForecastViewService>();
}
public static void AddTestData(IServiceProvider provider)
{
var factory = provider.GetService<IDbContextFactory<InMemoryWeatherDbContext>>();
if (factory is not null)
WeatherTestDataProvider.Instance().LoadDbContext<InMemoryWeatherDbContext>(factory);
}
And then Program
looks like this:
//.....
builder.Services.AddServerSideBlazor();
builder.Services.AddWeatherAppServices<InMemoryWeatherDbContext>(options
=> options.UseInMemoryDatabase($"WeatherDatabase-{Guid.NewGuid().ToString()}"));
var app = builder.Build();
// Add the test data to the InMemory Db
WeatherAppServices.AddTestData(app.Services);
//.....
CodePudding user response:
I ended up with a Custom factory that I register like this in program.cs:
builder.Services.AddScoped<AuthenticationStateProvider, RevalidatingIdentityAuthenticationStateProvider<IdentityUser>>();
builder.Services.AddScoped<IDbContextFactory<ApplicationDbContext>>(provider => new
ContextFactory(options.Options, provider));
Since the factory is not Scoped and not a Singleton I can initate the DbContext from here with the current logged in user provided by the AuthenticationStateProvider. I've tested this to some extent and it seems to be working.