用户登录时,ASP.NET Core更改EF连接字符串

ozz*_*ald 10 asp.net asp.net-mvc asp.net-identity entity-framework-core asp.net-core

经过几个小时的研究,发现无法做到这一点; 是时候提问了.

我有一个使用EF Core和MVC的ASP.NET Core 1.1项目,供多个客户使用.每个客户都有自己的数据库,具有完全相同的模式.该项目目前是一个迁移到Web的Windows应用程序.在登录屏幕上,用户有三个字段,公司代码,用户名和密码.我需要能够在用户尝试根据他们在公司代码输入中键入的内容进行登录时更改连接字符串,然后在整个会话期间记住他们的输入.

我找到了一些方法可以使用一个数据库和多个模式执行此操作,但没有一个使用相同模式的多个数据库.

我解决这个问题的方法不是问题的实际解决方案,而是一个解决这个问题的方法.我的数据库和应用程序托管在Azure上.我对此的修复是将我的应用服务升级到支持插槽的计划(5个插槽每月只需额外支付20美元).每个插槽都有相同的程序,但保存连接字符串的环境变量是公司特定的.这样我也可以根据需要对每个公司进行子域名访问.虽然这种做法可能不像其他人那样做,但对我来说这是最具成本效益的.发布到每个插槽比在花费时间进行其他无法正常工作的编程更容易.在微软轻松更改连接字符串之前,这是我的解决方案.

回应Herzl的回答

这似乎可行.我试图让它实现.我正在做的一件事是使用访问我的上下文的存储库类.我的控制器将注入的存储库注入其中以调用访问上下文的存储库中的方法.我如何在存储库类中执行此操作.我的存储库中没有OnActionExecuting重载.此外,如果会话持续存在,当用户再次向应用程序打开浏览器并且仍然使用持续7天的cookie登录时会发生什么?这不是新会议吗?听起来应用程序会抛出异常,因为会话变量将为null,因此没有完整的连接字符串.我想我也可以将它存储为一个Claim,如果session变量为null,则使用Claim.

这是我的存储库类.IDbContextService是ProgramContext,但我开始添加你的建议,试着让它工作.

public class ProjectRepository : IProjectRepository
{
    private IDbContextService _context;
    private ILogger<ProjectRepository> _logger;
    private UserManager<ApplicationUser> _userManager;

    public ProjectRepository(IDbContextService context,
                            ILogger<ProjectRepository> logger,
                            UserManager<ApplicationUser> userManger)
    {
        _context = context;
        _logger = logger;
        _userManager = userManger;
    }

    public async Task<bool> SaveChangesAsync()
    {
        return (await _context.SaveChangesAsync()) > 0;
    }
}
Run Code Online (Sandbox Code Playgroud)

回应FORCE JB的回答

我试图实现你的方法.我在Program.cs上得到一个例外

host.Run();
Run Code Online (Sandbox Code Playgroud)

这是我的'Program.cs'课程.不变.

using System.IO;
using Microsoft.AspNetCore.Hosting;

namespace Project
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = new WebHostBuilder()
                .UseKestrel()
                .UseContentRoot(Directory.GetCurrentDirectory())
                .UseIISIntegration()
                .UseStartup<Startup>()
                .Build();

            host.Run();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

还有我的'Startup.cs'课程.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Serialization;
using System;
using System.Collections.Generic;
using Project.Entities;
using Project.Services;

namespace Project
{
    public class Startup
    {
        private IConfigurationRoot _config;

        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json")
                .AddEnvironmentVariables();

            _config = builder.Build();
        }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton(_config);
            services.AddIdentity<ApplicationUser, IdentityRole>(config =>
            {
                config.User.RequireUniqueEmail = true;
                config.Password.RequireDigit = true;
                config.Password.RequireLowercase = true;
                config.Password.RequireUppercase = true;
                config.Password.RequireNonAlphanumeric = false;
                config.Password.RequiredLength = 8;
                config.Cookies.ApplicationCookie.LoginPath = "/Auth/Login";
                config.Cookies.ApplicationCookie.ExpireTimeSpan = new TimeSpan(7, 0, 0, 0); // Cookies last 7 days
            })
            .AddEntityFrameworkStores<ProjectContext>();
            services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, AppClaimsPrincipalFactory>();
            services.AddScoped<IProjectRepository, ProjectRepository>();
            services.AddTransient<MiscService>();
            services.AddLogging();
            services.AddMvc()
            .AddJsonOptions(config =>
            {
                config.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
            });
        }

        public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
        {
            Dictionary<string, string> connStrs = new Dictionary<string, string>();
            connStrs.Add("company1", "1stconnectionstring"));
            connStrs.Add("company2", "2ndconnectionstring";
            DbContextFactory.SetDConnectionString(connStrs);
            //app.UseDefaultFiles();

            app.UseStaticFiles();
            app.UseIdentity();
            app.UseMvc(config =>
            {
                config.MapRoute(
                    name: "Default",
                    template: "{controller}/{action}/{id?}",
                    defaults: new { controller = "Auth", action = "Login" }
                    );
            });
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

例外情况:

InvalidOperationException: Unable to resolve service for type 'Project.Entities.ProjectContext' while attempting to activate 'Microsoft.AspNetCore.Identity.EntityFrameworkCore.UserStore`4[Project.Entities.ApplicationUser,Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole,Project.Entities.ProjectContext,System.String]'.
Run Code Online (Sandbox Code Playgroud)

不知道该怎么做.

部分成功编辑

好的,我让你的例子工作了.我可以使用不同的id在我的存储库构造函数中设置连接字符串.我现在的问题是登录并选择正确的数据库.我想过从会话或声明中获取存储库,无论是非空的.但是在登录控制器中使用SignInManager之前我无法设置值,因为在更新会话变量之前,SignInManager被注入到控制器中,该控制器创建了一个上下文.我能想到的唯一方法是登录两页.第一页将询问公司代码并更新会话变量.第二页将使用SignInManager并将存储库注入到控制器构造函数中.这将在第一页更新会话变量后发生.对于两个登录视图之间的动画,这实际上可能更具视觉吸引力.除非有任何想法在没有两个登录视图的情况下执行此操作,否则我将尝试实现两页登录并发布代码(如果有效).

它实际上是破碎的

当它工作时,这是因为我仍然有一个有效的cookie.我会运行该项目,它会跳过登录.现在我InvalidOperationException: No database provider has been configured for this DbContext在清除缓存后得到了异常.我已经完成了所有操作并且正确地创建了上下文.我的猜测是身份存在某种问题.添加实体框架存储的下面代码是否可能ConfigureServices导致问题?

services.AddIdentity<ApplicationUser, IdentityRole>(config =>
{
    config.User.RequireUniqueEmail = true;
    config.Password.RequireDigit = true;
    config.Password.RequireLowercase = true;
    config.Password.RequireUppercase = true;
    config.Password.RequireNonAlphanumeric = false;
    config.Password.RequiredLength = 8;
    config.Cookies.ApplicationCookie.LoginPath = "/Company/Login";
    config.Cookies.ApplicationCookie.ExpireTimeSpan = new TimeSpan(7, 0, 0, 0); // Cookies last 7 days
})
.AddEntityFrameworkStores<ProgramContext>();
Run Code Online (Sandbox Code Playgroud)

编辑

我验证Identity了这个问题.我在执行之前从我的存储库中PasswordSignInAsync提取了数据,它将数据拉得很好.如何为Identity创建DbContext?

The*_* JB 19

创建一个DbContext工厂

public static class DbContextFactory
{
    public static Dictionary<string, string> ConnectionStrings { get; set; }

    public static void SetConnectionString(Dictionary<string, string> connStrs)
    {
        ConnectionStrings = connStrs;
    }

    public static MyDbContext Create(string connid)
    {
        if (!string.IsNullOrEmpty(connid))
        {
            var connStr = ConnectionStrings[connid];
            var optionsBuilder = new DbContextOptionsBuilder<MyDbContext>();
            optionsBuilder.UseSqlServer(connStr);
            return new MyDbContext(optionsBuilder.Options);
        }
        else
        {
            throw new ArgumentNullException("ConnectionId");
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

初始化DbContext工厂

在startup.cs中

public void Configure()
{
  Dictionary<string, string> connStrs = new Dictionary<string, string>();
  connStrs.Add("DB1", Configuration["Data:DB1Connection:ConnectionString"]);
  connStrs.Add("DB2", Configuration["Data:DB2Connection:ConnectionString"]);
  DbContextFactory.SetConnectionString(connStrs);
}
Run Code Online (Sandbox Code Playgroud)

用法

var dbContext= DbContextFactory.Create("DB1");
Run Code Online (Sandbox Code Playgroud)


H. *_*rzl 5

根据你的问题,我将提供一个假设一些事情的解决方案:

首先,我在本地 SQL Server 实例中创建了三个数据库:

create database CompanyFoo
go

create database CompanyBar
go

create database CompanyZaz
go
Run Code Online (Sandbox Code Playgroud)

然后,我将在每个数据库中创建一个包含一行的表:

use CompanyFoo
go

drop table ConfigurationValue
go

create table ConfigurationValue
(
    Id int not null identity(1, 1),
    Name varchar(255) not null,
    [Desc] varchar(max) not null
)
go

insert into ConfigurationValue values ('Company name', 'Foo Company')
go

use CompanyBar
go

drop table ConfigurationValue
go

create table ConfigurationValue
(
    Id int not null identity(1, 1),
    Name varchar(255) not null,
    [Desc] varchar(max) not null
)
go

insert into ConfigurationValue values ('Company name', 'Bar Company')
go

use CompanyZaz
go

drop table ConfigurationValue
go

create table ConfigurationValue
(
    Id int not null identity(1, 1),
    Name varchar(255) not null,
    [Desc] varchar(max) not null
)
go

insert into ConfigurationValue values ('Company name', 'Zaz Company')
go
Run Code Online (Sandbox Code Playgroud)

下一步是使用 SQL 身份验证创建一个用户并授予读取数据库的权限,在我的情况下,我的用户名是 johnd,密码是 123。

完成这些步骤后,我们继续在 ASP.NET Core 中创建一个 MVC 应用程序,我使用 MultipleCompany 作为项目名称,我有两个控制器:Home 和 Administration,目标是先显示登录视图,然后重定向到另一个视图根据“登录”视图中选定的数据库显示数据。

为了完成您的要求,您需要在 ASP.NET Core 应用程序上使用会话,您可以将这种方式更改为稍后存储和读取数据,目前这仅用于概念测试。

家庭控制器代码:

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using MultipleCompany.Models;

namespace MultipleCompany.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }

        [HttpPost]
        public IActionResult Index(LoginModel model)
        {
            HttpContext.Session.SetString("CompanyCode", model.CompanyCode);
            HttpContext.Session.SetString("UserName", model.UserName);
            HttpContext.Session.SetString("Password", model.Password);

            return RedirectToAction("Index", "Administration");
        }

        public IActionResult Error()
        {
            return View();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

管理控制器代码:

using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using MultipleCompany.Models;
using MultipleCompany.Services;

namespace MultipleCompany.Controllers
{
    public class AdministrationController : Controller
    {
        protected IDbContextService DbContextService;
        protected CompanyDbContext DbContext;

        public AdministrationController(IDbContextService dbContextService)
        {
            DbContextService = dbContextService;
        }

        public override void OnActionExecuting(ActionExecutingContext context)
        {
            DbContext = DbContextService.CreateCompanyDbContext(HttpContext.Session.CreateLoginModelFromSession());

            base.OnActionExecuting(context);
        }

        public IActionResult Index()
        {
            var model = DbContext.ConfigurationValue.ToList();

            return View(model);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

主页视图代码:

@{
    ViewData["Title"] = "Home Page";
}

<form action="/home" method="post">
    <fieldset>
        <legend>Log in</legend>

        <div>
            <label for="CompanyCode">Company code</label>
            <select name="CompanyCode">
                <option value="CompanyFoo">Foo</option>
                <option value="CompanyBar">Bar</option>
                <option value="CompanyZaz">Zaz</option>
            </select>
        </div>

        <div>
            <label for="UserName">User name</label>
            <input type="text" name="UserName" />
        </div>

        <div>
            <label for="Password">Password</label>
            <input type="password" name="Password" />
        </div>

        <button type="submit">Log in</button>
    </fieldset>
</form>
Run Code Online (Sandbox Code Playgroud)

管理视图代码:

@{
    ViewData["Title"] = "Home Page";
}

<h1>Welcome!</h1>

<table class="table">
    <tr>
        <th>Name</th>
        <th>Desc</th>
    </tr>
    @foreach (var item in Model)
    {
        <tr>
            <td>@item.Name</td>
            <td>@item.Desc</td>
        </tr>
    }
</table>
Run Code Online (Sandbox Code Playgroud)

登录型号代码:

using System;
using Microsoft.AspNetCore.Http;

namespace MultipleCompany.Models
{
    public class LoginModel
    {
        public String CompanyCode { get; set; }

        public String UserName { get; set; }

        public String Password { get; set; }
    }

    public static class LoginModelExtensions
    {
        public static LoginModel CreateLoginModelFromSession(this ISession session)
        {
            var companyCode = session.GetString("CompanyCode");
            var userName = session.GetString("UserName");
            var password = session.GetString("Password");

            return new LoginModel
            {
                CompanyCode = companyCode,
                UserName = userName,
                Password = password
            };
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

CompanyDbContext 代码:

using System;
using Microsoft.EntityFrameworkCore;

namespace MultipleCompany.Models
{
    public class CompanyDbContext : Microsoft.EntityFrameworkCore.DbContext
    {
        public CompanyDbContext(String connectionString)
        {
            ConnectionString = connectionString;
        }

        public String ConnectionString { get; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(ConnectionString);

            base.OnConfiguring(optionsBuilder);
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
        }

        public DbSet<ConfigurationValue> ConfigurationValue { get; set; }
    }
}
Run Code Online (Sandbox Code Playgroud)

配置值代码:

using System;

namespace MultipleCompany.Models
{
    public class ConfigurationValue
    {
        public Int32? Id { get; set; }

        public String Name { get; set; }

        public String Desc { get; set; }
    }
}
Run Code Online (Sandbox Code Playgroud)

应用设置代码:

using System;

namespace MultipleCompany.Models
{
    public class AppSettings
    {
        public String CompanyConnectionString { get; set; }
    }
}
Run Code Online (Sandbox Code Playgroud)

IDbContextService 代码:

using MultipleCompany.Models;

namespace MultipleCompany.Services
{
    public interface IDbContextService
    {
        CompanyDbContext CreateCompanyDbContext(LoginModel model);
    }
}
Run Code Online (Sandbox Code Playgroud)

DbContextService 代码:

using System;
using Microsoft.Extensions.Options;
using MultipleCompany.Models;

namespace MultipleCompany.Services
{
    public class DbContextService : IDbContextService
    {
        public DbContextService(IOptions<AppSettings> appSettings)
        {
            ConnectionString = appSettings.Value.CompanyConnectionString;
        }

        public String ConnectionString { get; }

        public CompanyDbContext CreateCompanyDbContext(LoginModel model)
        {
            var connectionString = ConnectionString.Replace("{database}", model.CompanyCode).Replace("{user id}", model.UserName).Replace("{password}", model.Password);

            var dbContext = new CompanyDbContext(connectionString);

            return dbContext;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

启动代码:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using MultipleCompany.Models;
using MultipleCompany.Services;

namespace MultipleCompany
{
    public class Startup
    {
        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
                .AddEnvironmentVariables();
            Configuration = builder.Build();
        }

        public IConfigurationRoot Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            // Add framework services.
            services.AddMvc();

            services.AddEntityFrameworkSqlServer().AddDbContext<CompanyDbContext>();

            services.AddScoped<IDbContextService, DbContextService>();

            services.AddDistributedMemoryCache();
            services.AddSession();

            services.AddOptions();

            services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));

            services.AddSingleton<IConfiguration>(Configuration);
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            loggerFactory.AddDebug();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseBrowserLink();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }

            app.UseStaticFiles();

            app.UseSession();

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我为我的项目添加了这个包:

"Microsoft.EntityFrameworkCore": "1.0.1",
"Microsoft.EntityFrameworkCore.SqlServer": "1.0.1",
"Microsoft.AspNetCore.Session":  "1.0.0"
Run Code Online (Sandbox Code Playgroud)

我的 appsettings.json 文件:

{
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  },
  "AppSettings": {
    "CompanyConnectionString": "server=(local);database={database};user id={user id};password={password}"
  }
}
Run Code Online (Sandbox Code Playgroud)

请关注有关在主视图中连接到所选数据库的概念,您可以更改此代码的任何部分作为改进,请记住我提供此解决方案是根据您的简短问题做出一些假设,请随时提问关于此解决方案中任何暴露的方面,以根据您的要求改进这段代码。

基本上,我们需要定义一个服务来根据选定的数据库创建数据库上下文的实例,即 IDbContextService 接口和 DbContextService 它是该接口的实现。

正如您在 DbContextService 代码中看到的,我们替换了 {} 中的值以构建不同的连接字符串,在这种情况下,我在下拉列表中添加了数据库名称,但在实际开发中请避免这种方式,因为出于安全原因,这样做会更好不要暴露你的数据库和其他配置的真实名称;您可以从控制器端获得一个奇偶校验表,以根据选定的数据库解析公司代码。

此解决方案的一项改进是添加一些代码以将登录模型作为 json 序列化到会话中,而不是以单独的方式存储每个值。

请让我知道这个答案是否有用。PD:如果您希望将完整代码上传到一个驱动器中,请在评论中告诉我


ozz*_*ald 0

自从我发布这个问题以来已经很长时间了,我从未分享过我开发的解决方案,所以我认为我应该这样做。

我最终选择为我的租户使用不同的子域。因此,我只是创建了一个TenantService检查 url 并从 config.json 返回一个连接字符串的方法。在 DbContext 的OnConfiguring方法中,我只是调用租户服务并使用返回的连接字符串。这是一些示例代码:

租户服务

public class Tenant
{
    public string Name { get; set; }

    public string Hostname { get; set; }

    public string ConnectionString { get; set; }
}

public interface ITenantService
{
    Tenant GetCurrentTenant();

    List<Tenant> GetTenantList();
}

public class TenantService : ITenantService
{
    private readonly ILogger<TenantService> _logger;
    private readonly IHttpContextAccessor _httpContext;
    private readonly IConfiguration _configuration;

    public TenantService(
        ILogger<TenantService> logger,
        IHttpContextAccessor httpContext,
        IConfiguration configuration)
    {
        _logger = logger;
        _httpContext = httpContext;
        _configuration = configuration;
    }

    /// <summary>
    /// Gets the current tenant from the host.
    /// </summary>
    /// <returns>The tenant.</returns>
    public Tenant GetCurrentTenant()
    {
        Tenant tenant;
        var host = _httpContext.HttpContext.Request.Host;
        var tenants = GetTenantList();

        tenant = tenants.SingleOrDefault(t => t.Hostname == host.Value);
        if (tenant == null)
        {
            _logger.LogCritical("Could not find tenant from host: {host}", host);
            throw new ArgumentException($"Could not find tenant from host: {host}");
        }
        return tenant;
    }

    /// <summary>
    /// Gets a list of tenants in configuration.
    /// </summary>
    /// <returns>The list of tenants.</returns>
    public List<Tenant> GetTenantList()
    {
        var tenants = new List<Tenant>();

        _configuration.GetSection("Tenants").Bind(tenants);

        return tenants;
    }
}
Run Code Online (Sandbox Code Playgroud)

数据库上下文

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    base.OnConfiguring(optionsBuilder);

    if (!optionsBuilder.IsConfigured)
    {
        if (_tenantService == null)
        {
            throw new ArgumentNullException(nameof(_tenantService));
        }
        optionsBuilder.UseSqlServer(_tenantService.GetCurrentTenant().ConnectionString);
    }
}
Run Code Online (Sandbox Code Playgroud)