0

Here is my setup: Blazor Server app with .NET 7.

I have the following question. I'm using built-in dependency injection from .NET.

...
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();

//Models
builder.Services.AddScoped<IEnvironment, ApplicationLogic.Models.Environment>();

//Database
builder.Services.AddTransient<IDbConnectionString, DbConnectionString>();
builder.Services.AddTransient<IIMP_GG_Context, IMP_GG_Context>();
...

I'm using Entity Framework Core for the connection to the database. Now I have the situation, that in my application the user is able to choose between different environments ("Live", "Test", "Development"). A change of the environment should change the connection to the database which is being used. The selected enviroment is being save in an Enviroment object and has only one property which is an enum. The environment setup is Scoped.

builder.Services.AddScoped<IEnvironment, ApplicationLogic.Models.Environment>();

So here is the current structure of my application based on one example (ProductStatus). The UI have injected a controller object. Here is an example (see _ProductStatusController):

public partial class Filter
{
    ...
    [Inject] private IProductStatusController _ProductStatusController { get; set; }
    
    protected override void OnInitialized()
    {
        base.OnInitialized();
        GetAllProductStatus();
    }

    private void GetAllProductStatus()
    {
        var productStatus = _ProductStatusController.GetAllProductStatus();
        ...
    }
    ...
}

The controller has a repository injected.

public class ProductStatusController : IProductStatusController
{
    private IProductStatusRepository _ProductStatusRepository;

    public ProductStatusController(IProductStatusRepository productStatusRepository)
    {
        _ProductStatusRepository = productStatusRepository;
    }

    public List<Vorlauf> GetAllProductStatus()
    {
        return _ProductStatusRepository.GetAll();
    }
}

The Repository (ProductStatusRepository) looks like this. Here is the Entity framework core context object (_Context) (also injected).

public class ProductStatusRepository : IProductStatusRepository
{
    private IIMP_GG_Context _Context;

    public ProductStatusRepository(IIMP_GG_Context impGgContext)
    {
        _Context = impGgContext;
    }

    public List<Vorlauf> GetAll()
    {
        return _Context.VorlaufTable.Where((vorlauf) => vorlauf.Art == 62).ToList();
    }
}

Which connection is being used, depends on the object Environment. Here is the Context class:

public class IMP_GG_Context : DbContext, IIMP_GG_Context
{
    public DbSet<Vorlauf> VorlaufTable { get; set; }
    ... //more tables

    DbConnection _Connection;
    IDbConnectionString _DbConnectionString;
    IEnvironment _Environment;

    public IMP_GG_Context(IDbConnectionString dbConnectionString)
    {
        _DbConnectionString = dbConnectionString;
        Database.SetCommandTimeout(600);
    }


    public IMP_GG_Context(DbConnection connection, IDbConnectionString dbConnectionString)
    {
        _DbConnectionString = dbConnectionString;
        _Connection = connection;
    }

    ~IMP_GG_Context()
    {
    }

    private void OnEnvironmentChanged(object sender, PropertyChangedEventArgs e)
    {
        OnConfiguring(new DbContextOptionsBuilder());
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        if (_Connection == null)
        {
            string connection = _DbConnectionString.GetImpGgConnectionString();
            optionsBuilder.UseSqlServer(connection);
        }
        else
        {
            optionsBuilder.UseSqlServer(_Connection);
        }
    }
}

The _DbConnectionString property offers a method to get the correct connection string based on the chosen environment.

I think, the issue I have now, is that the _ProductStatusController object is once created when the UI component is being created. So the database connection will be created only once and not being updated when the environment changes. Is there any solution to this? What can I do to re-create the _ProductStatusController object? Many thanks in advance.


UPDATE

I tried the provided solution of Svyatoslav Danyliv. But it actually resets my selected environment to the default (="Test").

This is the new code I'm using in my Filter UI:

private void GetAllProductStatus()
{
    using (var scope = _ServiceProvider.CreateScope())
    {
        var environment = scope.ServiceProvider.GetRequiredService<IEnvironment>();
        environment = _Environment;
        var productStatusController = scope.ServiceProvider.GetRequiredService<IProductStatusController>();
        var productStatus = productStatusController.GetAllProductStatus();
        ...
    }
}

As you can see the environment is set correctly at this stage (="Dev"):

enter image description here

But when I execute the next step

var productStatusController = scope.ServiceProvider.GetRequiredService<IProductStatusController>();

...the environment is being resetted to "Test":

enter image description here

Am I doing here something wrong?

dns_nx
  • 3,651
  • 4
  • 37
  • 66
  • Start from fact that Blazor applications [do not create scope](https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/dependency-injection?view=aspnetcore-7.0#service-lifetime) – Svyatoslav Danyliv May 09 '23 at 08:25
  • With Balzor you have to use `IDbContexFactory`, or create and dispose Scope manually. – Svyatoslav Danyliv May 09 '23 at 08:26
  • @SvyatoslavDanyliv It is not a Blazor WebAssembly solution, but Blazor Server. – dns_nx May 09 '23 at 08:29
  • @SvyatoslavDanyliv I saw that there is an option `IDbContextFactory`, but will it solve my issue? How to create or dispose the scope manually? – dns_nx May 09 '23 at 08:31
  • [IServiceProvider.CreateScope()](https://stackoverflow.com/a/43722904/10646316) – Svyatoslav Danyliv May 09 '23 at 09:05
  • Sorry, I did not get it run. If I want to use `IDbContextFactory` I got the error `System.InvalidOperationException: 'Cannot resolve scoped service 'ApplicationLogic.Models.IEnvironment' from root provider.'`. If want to use `CreateScope()` I got an error `System.InvalidOperationException: 'No constructor for type 'ApplicationLogic.Database.IMP_GG_Context' can be instantiated using services from the service container and default values.'`. Can you please provide an answer where I need to change the code? Many thanks. – dns_nx May 09 '23 at 09:54
  • @dns_nx the docs already explain how to use EF Core in Blazor Server. Your code doesn't even register the DbContext correctly though. There's a *very* good reason `AddDbContext` regisgters contexts as scoped. It also allows you to change the underlying provider and connection string of the context instead of hard-coding it. Check [ASP.NET Core Blazor Server with Entity Framework Core (EF Core)](https://learn.microsoft.com/en-us/aspnet/core/blazor/blazor-server-ef-core?view=aspnetcore-7.0) before trying to "abstract" the high-level Unit-of-Work that is DbContext below a low-level "repository" – Panagiotis Kanavos May 09 '23 at 13:20
  • You should probably read [Repositories and Unit-of-Work Don't Mix](https://robconery.com/databases/repositories-on-top-unitofwork-are-not-a-good-idea/) and [No Need for Repositories and Unit-of-Work with EF Core](https://gunnarpeipman.com/ef-core-repository-unit-of-work/). The question's code shows some confusion too. Blazor Server has no controllers, so what's the point of `ProductStatusController`? EF Core isn't a data driver, it's the high-level abstraction. It doesn't need `IDbConnectionString ` or `IEnvironment` because `AddDbContext` will always pass the current settings and config. – Panagiotis Kanavos May 09 '23 at 13:25
  • I *strongly* suggest creating a Blazor Server application using the tutorials *first* and only consider "fixes" if you actually encounter problems. The environment is determined during startup for example and doesn't change while an application runs. The correct configuration is loaded automatically. What's the point of `IEnvironment` then? .NET Core configuration settings *can* change, but that won't affect EF Core objects that are created only when needed and treated as a UoW. When you create a new DbContext it will retrieve the connection string from the *current* configuration – Panagiotis Kanavos May 09 '23 at 13:29
  • @PanagiotisKanavos Many thanks for your comments and links. I will go through this links to have a better overview about the interaction of Blazor, dependency injection and Entity Framework Core. – dns_nx May 09 '23 at 13:45
  • You should probably start with a Blazor Server tutorial in general. The question mixes up Blazor Server, MVC *and* Razor Pages. These are 3 completely different ways to generate server-rendered web sites. Try to create a simple Blazor Server application first. – Panagiotis Kanavos May 09 '23 at 13:48
  • I already did a Blazor Server tutorial, but as I learned it makes sense to don't mix application logic with UI. All the UI stuff is in the UI project. All app logic is in application logic project. But I agree to read further on using EFCore. – dns_nx May 09 '23 at 13:52
  • @PanagiotisKanavos Now I read all your links and I can say, that, yes, I'm using repositories, but not that way described in your provided links. In the project I have complex queries, which seems to be similar as the described `Query` classes. However, your comments do not address my question. It is about the fact that I have changing environments (SQL Server connections). Accordingly, there must be a distinction somewhere in the application that, in my eyes, justifies the classes mentioned (`Environment` and `DbConnectionString`). If you know another solution to this, I am open to it. – dns_nx May 09 '23 at 14:26
  • The docs already answer this - you don't have to "fix" anything. Just use EF Core as you see in the docs. The environment is the concern of the *ASP.NET Configuration middleware* which ensures you can retrieve the correct, up-to-date settings through `IConfiguration`. Which has a `GetConnectionString` as well. The `AddDbContext` and `AddDbContextFactory` methods have access to `IServiceProvider` so they too can retrieve up-to-date configuration settings and connection strings. The Blazor Server docs show you to inject DbContextFactory and create a DbContext instance as needed – Panagiotis Kanavos May 09 '23 at 14:30
  • More importantly a DbContext IS NOT a database connection. It *uses* an ADO.NET connection only to load or save data. It's ADO.NET that abstracts the db connection through the IDbConnection and DbConnection-derived classes. A DbContext is your multi-entity Repository and UoW, constructing queries, mapping results and detecting changes. – Panagiotis Kanavos May 09 '23 at 14:32
  • @PanagiotisKanavos I don't know, if you saw my final solution (please see answer). I would be interested, if you would say, that this is a conventional way. Many thanks for your feedback on this in advance. – dns_nx May 11 '23 at 04:50

2 Answers2

0

You can control scope lifetime by yourself.

Make everything instead of Filter scoped:

//Models
builder.Services.AddScoped<IEnvironment, ApplicationLogic.Models.Environment>();

//Database
builder.Services.AddScoped<IDbConnectionString, DbConnectionString>();
builder.Services.AddScoped<IIMP_GG_Context, IMP_GG_Context>();

//Filter
builder.Services.AddTransient<IFilter, Filter>();

Inject into your service IServiceProvider

public class Filter
{
    IServiceProvider _serviceProvider;

    public Filter(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider
    }

    public void GetAllProductStatus()
    {
        using (var scope = _serviceProvider.CreateScope())
        {
            // istantiate IProductStatusController manually
            var statusController = scope.ServiceProvider.GetRequiredService<IProductStatusController>();
            var productStatus = statusController.GetAllProductStatus();
            ...

            // after disposing Scope - IProductStatusController, IIMP_GG_Context, IProductStatusRepository ect. will be disposed automatically
        }        
    }
}

Anyway, looks like you have another problem not related to Blazor - how to properly configure DbContext. Just calling OnConfiguring by yourself gives nothing.

Add proper constructor. Other and OnConfiguring override should be removed.

public class IMP_GG_Context: DbContext, IIMP_GG_Context
{
    public IMP_GG_Context(IDbConnectionString dbConnectionString) : base(GetOptions(dbConnectionString))
    {
    }

    private static DbContextOptions<IMP_GG_Context> GetOptions(IDbConnectionString dbConnectionString)
    {
        return new DbContextOptionsBuilder<IMP_GG_Context>()
            .UseSqlServer(dbConnectionString.GetImpGgConnectionString())
            .Options;
    }
}
Svyatoslav Danyliv
  • 21,911
  • 3
  • 16
  • 32
  • Thanks for your detailed answer. But one question is left. Actually your code works (no errors being thrown), BUT the environment is being reset to "default" Test. Does `using (var scope = _ServiceProvider.CreateScope())` ignoring the already setted value like 'Development' in Environment object and creates a new object of Environment with the default value? – dns_nx May 09 '23 at 12:15
  • At first request Environment Object, change environment value, then request other services. – Svyatoslav Danyliv May 09 '23 at 12:16
  • Yes, that's how it is being done. But when I get the new status controller object, the environment is being reset to default. – dns_nx May 09 '23 at 12:30
  • That's right. Always when you request new scope, set desired configuration. – Svyatoslav Danyliv May 09 '23 at 12:36
  • I have made an update in my original posting (see "Update"). I would be very grateful if you could take another look at what I did wrong. – dns_nx May 09 '23 at 13:12
  • Set breakpoint in Environment's constructor and check when it is created, ensure that Environment il registered as Scoped. – Svyatoslav Danyliv May 09 '23 at 13:31
0

I've got it work now. This is my final solution:

First I did some changes to my DbContext:

public class IMP_GG_Context : DbContext, IIMP_GG_Context
{
    public DbSet<Vorlauf> VorlaufTable { get; set; }
    ... //more tables

    IDbConnectionString _DbConnectionString;

    [ActivatorUtilitiesConstructor]
    public IMP_GG_Context(DbContextOptions<IMP_GG_Context> options, IDbConnectionString dbConnectionString) : base(options)
    {
        _DbConnectionString = dbConnectionString;
    }

    ~IMP_GG_Context()
    {
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(_DbConnectionString.GetImpGgConnectionString());
    }
}

Then I changed the configuration in Program.cs to use AddDbContextFactory:

...
builder.Services.AddDbContextFactory<IMP_GG_Context>(options =>
{
    var environment = new ApplicationLogic.Models.Environment();
    DbConnectionString dbConnectionString = new DbConnectionString(environment);
   options.UseSqlServer(dbConnectionString.GetImpGgConnectionString());
}, ServiceLifetime.Scoped);
...

...and then last, I changed the Repositories to get a "fresh" instance with the help of IDbContextFactory when loading data:

public class ProductStatusRepository : IProductStatusRepository
{
    private IIMP_GG_Context _Context;
    private IDbContextFactory<IMP_GG_Context> _ContextFactory;

    public ProductStatusRepository(IDbContextFactory<IMP_GG_Context> dbContextFactory)
    {
        _ContextFactory = dbContextFactory;
    }

    public List<Vorlauf> GetAll()
    {
        _Context = _ContextFactory.CreateDbContext();
        return _Context.VorlaufTable.Where((vorlauf) => vorlauf.Art == 62).ToList();
    }
}

With these changes, the correct environment will be used. I actually don't know, if that is the recommended way of using different database servers / environments.

dns_nx
  • 3,651
  • 4
  • 37
  • 66