Finbuckle / Finbuckle.MultiTenant

Finbuckle.MultiTenant is an open-source multitenancy middleware library for .NET. It enables tenant resolution, per-tenant app behavior, and per-tenant data isolation.
https://www.finbuckle.com/multitenant
Apache License 2.0
1.32k stars 267 forks source link

How to use other tenant-specific config parameters? #248

Closed ZedZipDev closed 4 years ago

ZedZipDev commented 4 years ago

I understand how to use ConnectionString

// ToDoDbContext.cs
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlite(ConnectionString);
            base.OnConfiguring(optionsBuilder);
        }

There is ConnectionString from parent class used. I have in appsettings.json 2 tenant specific parameters

ConnectionString S3ConnectionString

How I can pass the tenants S3ConnectionString?

AndrewTriesToCode commented 4 years ago

@Oleg26Dev I'm sorry for the late reply.

Your best bet here is to not derive from MultiTenantDbContext, but to instead implement IMultiTenantDbContext per the documentation -- with this option you'll have total control and can use the connection strings you want. Let me know if you run into any problems.

ZedZipDev commented 4 years ago

The matter is: I do not use DbContext when work with AWS S3. I use service, register it in ConfigureServices and pass the connection object. The same about using Redis: I need connection string or connection object and service to work. No EF context, migrations etc.

In case of multi-tenant environment need to have a possibility to get tenants config. But... I can do it inside of service but need to recognize what is the current tenant. Sorry for the description, but I am trying to catch an idea how to work with non-db sources.

AndrewTriesToCode commented 4 years ago

@Oleg26Dev Does the typed TenatInfo in the preview for v6 look like it will address this for you?

win32nipuh commented 4 years ago

@Oleg26Dev I'm sorry for the late reply.

Your best bet here is to not derive from MultiTenantDbContext, but to instead implement IMultiTenantDbContext per the documentation -- with this option you'll have total control and can use the connection strings you want. Let me know if you run into any problems.

Hi Andrew, I'm still wandering in the dark :-) Ok, I can create my own class implements IMultiTenantDbContext, including ConnectionString and S3 Connection parameters. Ok, what is the correct ConfigureServices in this case? Does a sample of code exists how to do it?

AndrewTriesToCode commented 4 years ago

hi @win32nipuh are you using the preview for version 6? I realize that in version 5 in the DbContext you can't get the TenantInfo directly, but in V6 I changed that so if you have a custom TenantInfo with extra properties like yours, in the db context configuration method you can access the TenantInfo and use those properties however you want.

win32nipuh commented 4 years ago

Great, thank you. Will try. Btw, it will be great to test some examples to see "How to". Did you add/modify any samples?What about the doc about how to use this approach?

win32nipuh commented 4 years ago

How to edit these functions to make they working? This way I receive the syntax error. I see something is incorrect.

public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages();
            services.AddServerSideBlazor();
            if (tenantSettings == null)
                tenantSettings = new TenantStrategy();
            services.AddSingleton<S3ConfigService>();
            Configuration.GetSection("TenantStrategy").Bind(tenantSettings);
            services.AddSingleton<WeatherForecastService>();

            if (tenantSettings.RouteStrategy)
            {
                services.AddMultiTenant<TenantInfo>()
                .WithConfigurationStore()
                .WithRouteStrategy();
            }
            else
            {
                services.AddMultiTenant<DerivedTenantInfo>()
                    .WithConfigurationStore()
                    //.WithConfigurationStore<ConfigurationStore<DerivedTenantInfo>>(ServiceLifetime.Singleton)
                    //.WithStore<ConfigurationStore<DerivedTenantInfo>>(ServiceLifetime.Singleton)
                    .WithHostStrategy(tenantSettings.HostTemplate)
                    .WithPerTenantOptions<S3ConfigOptions>((options, tenantInfo) =>
                    {
                        //(t.S3ConfigOptions)
                        tenantInfo.S3CustomOptions = options;
                    });

            }

            //Allows accessing HttpContext in Blazor
            services.AddHttpContextAccessor();

            services.AddSingleton<ITenantStrategy>(Configuration.GetSection("TenantStrategy").Get<TenantStrategy>());
            services.AddTransient<IConfigHelper, ConfigHelper>();
            services.AddScoped<Classes.ContextHelper>();
            services.AddDbContext<ToDoDbContext>();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.EnvironmentName == "Development")
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseHttpsRedirection();

            app.UseStaticFiles();
            app.UseRouting();
            app.UseMultiTenant<DerivedTenantInfo>();

            if (tenantSettings.RouteStrategy)
            {
                app.UseEndpoints(endpoints =>
                {
                    endpoints.MapBlazorHub();
                    endpoints.MapControllerRoute("default", tenantSettings.DefaultRoute);
                    endpoints.MapFallbackToPage("/_Host");
                });
            }
            else
            {
                app.UseEndpoints(endpoints =>
                {
                    endpoints.MapBlazorHub();
                    endpoints.MapControllers();
                    endpoints.MapFallbackToPage("/_Host");
                });
            }

            SetupDb();
        }
win32nipuh commented 4 years ago

Hi Andrew, may be it is simpler: could you please show the piece of code how to use the MultiTenantDbContext and how to use TenantInfo in DBContext directly on the DataIsolationSample? Thanx!

AndrewTriesToCode commented 4 years ago

hi @win32nipuh I will take a closer look today and try to give you a better response

AndrewTriesToCode commented 4 years ago

@win32nipuh I don't have updated docs yet but I think I can help you out.

On the data isolation sample I am using TenantInfo, but lets assume you have a different ITenantInfo called MyCustomTenantInfo with extra properties on it such as ConnectionString2, AlternateConnectionString, or whatever.

Then in your db context OnConfiguring you can access these properties and use them however you want:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    // The TenantInfo property in the dbcontext is an `ITenantInfo` so we cast it to our
    // custom type to access our properties.
    var myTenantInfo = TenantInfo as MyCustomTenantInfo; 

    if(...)
    {
        optionsBuilder.UseSqlite(myTenantInfo.ConnectionString2);
    } else if(...)
    {
        optionsBuilder.UseSqlServer(myTenantInfo.ConnectionString);
    }
    else if(...)
    {
        optionsBuilder.UseOtherDbProvider(myTenantInfo.AlternateConnectionString);
    }

    base.OnConfiguring(optionsBuilder);
}

You'll need to make sure your tenants have values for these properties. If you are using the configuration store you'd need to add the Json to add the property values for each tenant.

Does that help?

ZedZipDev commented 4 years ago

Hi Andrew, it helps but partially ;-) I use your OnConfiguring sample. In my case all additional parameters are null.

There is my derived tenant class

    public class DerivedTenantInfo : IMultiTenantDbContext //TenantInfo
    {
        public S3ConfigOptions S3CustomOptions { get; set; }
        public ITenantInfo TenantInfo { get; }
        public TenantMismatchMode TenantMismatchMode { get; set; } = TenantMismatchMode.Throw;
        public TenantNotSetMode TenantNotSetMode { get; set; } = TenantNotSetMode.Throw;
    }

There is my code in the ConfigureServices() ...

 services.AddMultiTenant<DerivedTenantInfo>() // <------here is an error if I use DerivedTenantInfo
// error CS0400: The type or namespace name 'DataIsolationBlazor2' could not be found in the //global namespace
                    .WithConfigurationStore()
(ServiceLifetime.Singleton)
                    .WithHostStrategy(tenantSettings.HostTemplate)
                    .WithPerTenantOptions<S3ConfigOptions>((options, tenantInfo) =>
                    {
                        tenantInfo.S3CustomOptions = options;
                    });

Could you please give a piece of code how to correctly derive my own custom class instead of TenantInfo which can have additional config parameters besides ConnectionString? and how to use it in the ConfigureServices?

Thank you.

AndrewTriesToCode commented 4 years ago

@Oleg26Dev Normally the derived TenantInfo would implement ITenantInfo but you are implementing IMultiTenantDbContext. Are you purposefully trying to combine an EFCore database context and the TenantInfo?

A more typical setup would look like

public class MyTenantInfo : ITenantInfo // or derive from TenatInfo class
{
    string Id { get; set; }; //etc
    string S3Options { get; set; }
}

Then if your EFCore database context in OnConfiguring you can access the Tenant S3 options like so:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    // The TenantInfo property in the dbcontext is an `ITenantInfo` so we cast it to our
    // custom type to access our properties.
    var myTenantInfo = TenantInfo as MyCustomTenantInfo; 

    if(...)
    {
        optionsBuilder.UseSqlite(myTenantInfo.ConnectionString2);
    } else if(...)
    {
        optionsBuilder.UseSqlServer(myTenantInfo.ConnectionString);
    }
    else if(...)
    {
        optionsBuilder.UseOtherDbProvider(myTenantInfo.AlternateConnectionString);
    }

    base.OnConfiguring(optionsBuilder);
}

As an aside,I still don't understand why in PerTenantOptions you are set the tenant options from the regular options-- it's usually the other way around. Then later when you sue the "regular" options in your app they will magically be adjusted specific to the tenant. In your setup the regular options will not be adjusted at all. How are you using the S3 options? Maybe if I understand your app I can help better.

ZedZipDev commented 4 years ago

Ok, you are right. I am developing a Blazor app. Each Tenant works with database and with a documents(files) storage. They are independent. E.g.

Tenant(i) works with his own database and file storage ->DatabaseContext ->Databse(i) ->S3FileService->File storage(i) (S3 or MinIO)

You can check the https://min.io/product/overview, the MinIO is compatible with AWS S3.

To connect to the File storage (S3 or MinIO) need to have connection parameters. That is my appsettings.json

{
  "S3FileSvcConfiguration": {
    "Profile": "default",
    "ServiceURL": "http://oleg11:9000",
    "Region": "us-east-1",
    "AccessKey": "minio",
    "SecretKey": "olegminio"
  },
  "DataDBConfiguration": {
    "ConnectionString": "Server=localhost;Port=5434;Database=pm10;User Id=postgres;Password=postgres;CommandTimeout=20;Pooling=False; Minimum Pool Size=0; Maximum Pool Size=0;"
  },
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}

There is single tenant settings. I need ConnectionString and S3FileSvcConfiguration for every tenant. My S3FileService does not work via DBContext. It is service, razor pages use it to List, Upload, Download, Delete files in the remote storage. But naturally as you see I need to get S3FileSvcConfiguration for the current tenant. There is the my S3 Service single-Tenant sample, I use the common config private readonly IS3FileSvcConfiguration _S3FileSvcConfiguration; But I need to use here tenants config :

    public interface IS3FileSvc
    {
        Task UploadAsync(IFileListEntry fileEntry, string folderName, string bucketName, bool bCreateBucket = true, bool bCreateFolder = false);
        public Task UploadFileToS3(IFileListEntry fileEntry, string bucketName);
        public Task ListObjects2Async(string folderName, string bucketName, string key);

        public Task CreateBucketAsync(string bucketName);
    }
    public class S3FileSvc : IS3FileSvc
    {
        private readonly IWebHostEnvironment _environment;
        private readonly IS3FileSvcConfiguration _S3FileSvcConfiguration;

        private readonly IConfiguration _Configuration;
        ILogger<S3FileSvcConfiguration> _logger;

        public S3FileSvc(IConfiguration configuration, IS3FileSvcConfiguration s3FileSvcConfig, ILogger<S3FileSvcConfiguration> logger, IWebHostEnvironment env)
        {
            _environment = env;
            _Configuration = configuration;
            _S3FileSvcConfiguration = s3FileSvcConfig;
            _logger = logger;
        }
        public async Task UploadAsync(IFileListEntry fileEntry, string folderName, string bucketName, bool bCreateBucket = true, bool bCreateFolder=false)
        {
            if (_S3FileSvcConfiguration != null)
                _S3FileSvcConfiguration.RegionEndPoint = RegionEndpoint.GetBySystemName(_S3FileSvcConfiguration.Region);

            var s3Config = new AmazonS3Config
            {
                RegionEndpoint = _S3FileSvcConfiguration.RegionEndPoint,
                ServiceURL = _S3FileSvcConfiguration.ServiceURL,
                ForcePathStyle = true
            };
            var s3Client = new AmazonS3Client(_S3FileSvcConfiguration.AccessKey, _S3FileSvcConfiguration.SecretKey, s3Config);

            string objName = "Unknown";
            string contentType2 = "text/plain";
            string contentType = fileEntry.Type;
            string bucketName2 = bucketName;

            try
            {

               await CreateBucketAsyncInternal(s3Client, bucketName);

                objName = Path.GetFileName(fileEntry.Name);

                if (!string.IsNullOrEmpty(folderName))
                {
                    objName = folderName + @"/" + objName;
                }
                if (bCreateFolder && !string.IsNullOrEmpty(folderName))
                {
                    PutObjectRequest request = new PutObjectRequest
                    {
                        BucketName = bucketName,
                        Key = folderName,
                        ContentType = contentType2
                    };

                    PutObjectResponse putResponse;
                    try
                    {
                        putResponse = await s3Client.PutObjectAsync(request);
                        _logger.LogInformation("S3FileSvc::UploadAsync:Create folder: response.HttpStatusCode={0}", putResponse.HttpStatusCode);
                    }
                    catch (Exception x)
                    {
                        _logger.LogError("S3FileSvc::UploadAsync: " + x.Message);
                    }
                }
                string strFileName = @"E:\Books\log2.txt";
                strFileName = fileEntry.Name;
                var ms = new MemoryStream();
                await fileEntry.Data.CopyToAsync(ms);

                PutObjectRequest putRequest = new PutObjectRequest
                {
                    BucketName = bucketName,
                    Key = objName,
                    InputStream = ms,
                    ContentType = contentType

                };
                PutObjectResponse response;
                try
                {
                    response = await s3Client.PutObjectAsync(putRequest);
                    _logger.LogInformation("S3FileSvc::UploadAsync: response.HttpStatusCode={0}", response.HttpStatusCode);
                }
                catch (Exception x)
                {
                    _logger.LogError("S3FileSvc::UploadAsync: " + x.Message);
                }

            }
            catch (AmazonS3Exception amazonS3Exception)
            {
                if (amazonS3Exception.ErrorCode != null &&
                    (amazonS3Exception.ErrorCode.Equals("InvalidAccessKeyId")
                    ||
                    amazonS3Exception.ErrorCode.Equals("InvalidSecurity")))
                {
                    _logger.LogError("S3FileSvc::UploadAsync: Check the provided AWS Credentials.");
                }
                else
                {
                    _logger.LogError("S3FileSvc::UploadAsync: Error occurred: " + amazonS3Exception.Message);
                }
            }
            catch (Exception x)
            {
                _logger.LogError("S3FileSvc::UploadAsync2: " + x.Message);
            }
            if (!(_logger is null))
            {
                _logger.LogInformation("S3FileSvc::UploadAsync: Completed");
            }
        }
...
win32nipuh commented 4 years ago

@Oleg26Dev Normally the derived TenantInfo would implement ITenantInfo but you are implementing IMultiTenantDbContext. Are you purposefully trying to combine an EFCore database context and the TenantInfo?

A more typical setup would look like

public class MyTenantInfo : ITenantInfo // or derive from TenatInfo class
{
    string Id { get; set; }; //etc
    string S3Options { get; set; }
}

Then if your EFCore database context in OnConfiguring you can access the Tenant S3 options like so:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    // The TenantInfo property in the dbcontext is an `ITenantInfo` so we cast it to our
    // custom type to access our properties.
    var myTenantInfo = TenantInfo as MyCustomTenantInfo; 

    if(...)
    {
        optionsBuilder.UseSqlite(myTenantInfo.ConnectionString2);
    } else if(...)
    {
        optionsBuilder.UseSqlServer(myTenantInfo.ConnectionString);
    }
    else if(...)
    {
        optionsBuilder.UseOtherDbProvider(myTenantInfo.AlternateConnectionString);
    }

    base.OnConfiguring(optionsBuilder);
}

As an aside,I still don't understand why in PerTenantOptions you are set the tenant options from the regular options-- it's usually the other way around. Then later when you sue the "regular" options in your app they will magically be adjusted specific to the tenant. In your setup the regular options will not be adjusted at all. How are you using the S3 options? Maybe if I understand your app I can help better.

Hi Andrew, I do not see the ConnectionString2, AlternateConnectionString in the myTenantInfo. They should be defined. Can you please show the myTenantInfo for this sample? And how lokks ConfigureServices to use this myTenantInfo ? How these strings read from appsettings.json?

AndrewTriesToCode commented 4 years ago

hi @win32nipuh and @Oleg26Dev I'm doing my best to help -- sorry for any confusion. It seems that there are two different discussions going on.

@win32nipuh can you please post a new issue with your specific situation and details?

@Oleg26Dev thanks for that detail. I think I understand now. It looks like you are accessing the app configuration directly instead of using the Options pattern.

If you modify it to use the Options pattern then Finbuckle's WithPerTenantOptions will let you modify the options that get passed to the S3 service based on the current tenant. This would look something like this:

// First define your tenant info class with any properties unique to each tenant
public class MyTenatInfo : ITenantInfo
{
  // Normal properties:
  public string Id { get; set; }
  ...

  // Custom S3 tenant-specific properties that will "override" the S3 options:
  // I'm not sure which S3 settings you need to change per tenant so this is just an example.
  public string S3ServiceUrl { get; set; }
}

// Then create your options class that will hold the default S3 configuration
public class S3Options
{
  public string Profile { get; set; }
  public string ServiceURL { get; set; }
  public string Region { get; set; }
  public string AccessKey { get; set; }
  public string SecretKey { get; set; }
}

// Then in your ConfigureServices method you register the options class:
services.Configure<S3Options>(Configuration);

// and also in ConfigureServices use WithPerTenantOptions to have the options adjusted per tenant:
services.AddMultiTenant<MyTenantInfo>()...WithPerTenantOptions<S3Options>((tenantInfo, options) =>
{
  options.ServiceURL = tenantInfo.S3ServiceUrl;
});

// Now in your S3 service in the constructor inject IOptions<S3Options> and the service url in the options object will be for the current tenant:
 public S3FileSvc(IOptions<S3Options> options, ...)
{
  _options = options;
  ...
}

Then later in your service you reference _options to get the values you need.

To recap: Finbuckle is designed to work with the Options pattern and not IConfigurations directly. Options are usually initialized from IConfiguration, and then Finbuckle can override per-tenant.

It raises the question: why use Options over Configuration? And that is a whole other topic. Options can be set via configuration but also via code and offer other functionality. Most of ASP.NET Core is written using Options so that's why I focused on it.

I hope this help!

ZedZipDev commented 4 years ago

Andrew, thank you very much. I'll try it. By the way: Can I use object S3Options in the class?

public class MyTenatInfo : ITenantInfo { // Normal properties: public string Id { get; set; } ...

// Custom S3 tenant-specific properties that will "override" the S3 options: // I'm not sure which S3 settings you need to change per tenant so this is just an example.

// public string S3ServiceUrl { get; set; } public S3Options {get;set;} //<-----------------? }

ZedZipDev commented 4 years ago

And here : // and also in ConfigureServices use WithPerTenantOptions to have the options adjusted per tenant: services.AddMultiTenant()...WithPerTenantOptions((tenantInfo, options) => { //options.ServiceURL = tenantInfo.S3ServiceUrl;// instead of this tenantInfo.S3ServiceUrl=options.ServiceURL;.//<-------? });

AndrewTriesToCode commented 4 years ago

public S3Options {get;set;} //<-----------------?

Yes, you would need to make sure the json where you have the tenant details matches accordingly. It would need a nested object named S3Options with internal values for the properties of S3Options, per tenant.

However in WithPerTenantOptions you would still need to adjust the option 1 property at a time:

// This will not work because the option reference is passed-by-value
options = tenantInfo.S3Options;

// Instead set each property as needed.
options.ServiceUrl = tenatnInfo.S3Options.ServiceUrl;
options.AnotherS3Option = tenatnInfo.S3Options.AnotherS3Option; // etc...

//options.ServiceURL = tenantInfo.S3ServiceUrl;// instead of this tenantInfo.S3ServiceUrl=options.ServiceURL;.//<-------?

This won't work because you aren't changing the option at all, so in the S3 service constructor when IOptions<S3Options> is injected it won't reflect the tenant specific values assigned in WithPerTenantOptions.

You might ask "Why bother with these options, why not just inject the tenantinfo and use its properties to set up the S3 service?"

Yes you can do this and it works. However if you use other libraries or rely on part of .NET Core itself you can't inject tenant info into that code. But if they internally use the options pattern (most do) then WithPerTenantOptions lets you adjust their behavior per-tenant by modifying the options they already inject.

ZedZipDev commented 4 years ago

Hi Andrew, I moved forward using your decription. Thank you. Yet another small problem appears: That is my class:

public class DerivedTenantInfo : ITenantInfo
{
    public string Id { get; set; }
    public string Identifier { get; set; }
    public string Name { get; set; }
    public string ConnectionString { get; set; }
    public string ConnectionString2 { get; set; }
    public string AlternateConnectionString { get; set; }
    public string S3Profile { get; set; }
    public string S3ServiceURL { get; set; }
    public string S3Region { get; set; }
    public string S3AccessKey { get; set; }
    public string S3SecretKey { get; set; }
}

Here the ti is null and naturally I have an error System.InvalidOperationException: No database provider has been configured for this DbContext. A provider can be configured by overriding the DbContext.OnConfiguring method or by using AddDbContext on the application service provider. If AddDbContext is used, then also ensure that your DbContext type accepts a DbContextOptions object in its constructor and passes it to the base constructor for DbContext.

public class ToDoDbContext : MultiTenantDbContext
    {
        public ToDoDbContext(ITenantInfo tenantInfo) : base(tenantInfo)
        {
        }

        public ToDoDbContext(ITenantInfo tenantInfo, DbContextOptions<ToDoDbContext> options) : base(tenantInfo, options)
        {
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            var ti = TenantInfo as DerivedTenantInfo;
            if(ti!=null)  //<-----------------------
                optionsBuilder.UseSqlite(TenantInfo.ConnectionString);
            base.OnConfiguring(optionsBuilder);
        }

On start the app it works fine when I call in the Program.Main

using (var db = new ToDoDbContext(new DerivedTenantInfo { ConnectionString = "Data Source=Data/ToDoList.db" }))
                {
                    db.Database.MigrateAsync().Wait();
                }

                using (var db = new ToDoDbContext(new DerivedTenantInfo { ConnectionString = "Data Source=Data/Initech_ToDoList.db" }))
                {
                    db.Database.MigrateAsync().Wait();
                }
                using (var db = new ToDoDbContext(new DerivedTenantInfo { ConnectionString = "Data Source=Data/Local_ToDoList.db" }))
                {
                    db.Database.MigrateAsync().Wait();
                }

and in the SetupDb in Startup.Configure

But when I call a page to show the ToDo items (DataIsolationSample) from the page it ==null

That is my ConfigureServices

 public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddServerSideBlazor();
    if (tenantSettings == null)
        tenantSettings = new TenantStrategy();
    services.AddSingleton<S3ConfigService>();
    Configuration.GetSection("TenantStrategy").Bind(tenantSettings);
    services.AddSingleton<WeatherForecastService>();

    services.Configure<S3ConfigOptions>(Configuration);
    services.AddMultiTenant<DerivedTenantInfo>()
        .WithStore<ConfigurationStore<DerivedTenantInfo>>(ServiceLifetime.Singleton)
        .WithHostStrategy(tenantSettings.HostTemplate)
        .WithPerTenantOptions<S3ConfigOptions>((options, tenantInfo) =>
        {
            options.ServiceURL = tenantInfo.S3ServiceURL;
        });

    //Allows accessing HttpContext in Blazor
    services.AddHttpContextAccessor();

    services.AddSingleton<ITenantStrategy>(Configuration.GetSection("TenantStrategy").Get<TenantStrategy>());
    services.AddTransient<IConfigHelper, ConfigHelper>();
    services.AddScoped<Classes.ContextHelper>();
    services.AddDbContext<ToDoDbContext>();
}
AndrewTriesToCode commented 4 years ago

@Oleg26Dev when you request a page and get the error what is the URL of the request?

Also can you show the controller or page code?

AndrewTriesToCode commented 4 years ago

@Oleg26Dev thanks for sending me your project. I loaded it up and it had some compilation errors, but once I got it running I think the issue is that you have the host strategy in place. If you plug in static strategy instead of host strategy (just as a test) does the correct tenant load?

Do you have entries in your hosts file for the subdomain if you want to use host strategy on your own machine?

ZedZipDev commented 4 years ago

Thnx, I am going to use host strategy in my real app. To test host strategy I use DNS like xip.io https://megacorp.127.0.0.1.xip.io:5001 https://finbuckle.127.0.0.1.xip.io:5001 https://initech.127.0.0.1.xip.io:5001 It is more easy and does not require any editing of the hosts file.

For example: I run VS, it runs Chrome by default: https://localhost:5001 I run Opera with the query https://megacorp.127.0.0.1.xip.io:5001 other page in the Chrome https://finbuckle.127.0.0.1.xip.io:5001 etc

ZedZipDev commented 4 years ago

I have made the test as you wrote:

  1. The hosts file contains 127.0.0.1 finbuckle

  2. I use Static strategy and query : https://finbuckle:5001

 services.AddMultiTenant<DerivedTenantInfo>()
                    .WithConfigurationStore()
(ServiceLifetime.Singleton)
                    .WithStaticStrategy("finbuckle")
                    .WithPerTenantOptions<S3ConfigOptions>((options, tenantInfo) =>
                    {
                        options.ServiceURL = tenantInfo.S3ServiceURL;
                    });
  1. I use this context class:
    public class ToDoDbContext : MultiTenantDbContext
    {
        public ToDoDbContext(ITenantInfo tenantInfo) : base(tenantInfo)
        {
        }
        public ToDoDbContext(ITenantInfo tenantInfo, DbContextOptions<ToDoDbContext> options) : base(tenantInfo, options)
        {
        }
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            var ti = TenantInfo as DerivedTenantInfo; 
            if (ti != null) //**<------------------------------------ it is null when I call from the page**
                optionsBuilder.UseSqlite(TenantInfo.ConnectionString);
            else
            {
                int x = 0;
            }
            base.OnConfiguring(optionsBuilder);
        }
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<ToDoItem>().IsMultiTenant();
            base.OnModelCreating(modelBuilder);
        }
        public DbSet<ToDoItem> ToDoItems { get; set; }
    }

    All as I described before

AndrewTriesToCode commented 4 years ago

@Oleg26Dev I have tracked the problem down to how I cached the tenant and how Blazor handles scoped services. I should have a preview3 out tonight or tomorrow.

ZedZipDev commented 4 years ago

@AndrewTriesToCode Thank you!!!! I am waiting

ZedZipDev commented 4 years ago

Hi Andrew, thanx! I have replaced the NuGet preview2 with the latest preview3 and it seems the app works fine, e.g.

 protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            var ti = TenantInfo as DerivedTenantInfo;
            if(ti!=null)  //<-----------------------             now it is NOT NULL, it is OK!
                optionsBuilder.UseSqlite(TenantInfo.ConnectionString);
            base.OnConfiguring(optionsBuilder);
        }

I will test it yet in details but it works.

stale[bot] commented 4 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.