用戶登錄時,ASP.NET Core更改EF連接字符串

asp.net asp.net-core asp.net-identity asp.net-mvc entity-framework-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;
    }
}

回應FORCE JB的回答

我試圖實施你的方法。我在Program.cs上得到一個例外

host.Run();

這是我的'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();
        }
    }
}

還有我的'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" }
                    );
            });
        }
    }
}

例外:

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]'.

不知道該怎麼做。

部分成功編輯

好的,我讓你的例子工作了。我可以使用不同的id在我的存儲庫構造函數中設置連接字符串。我現在的問題是登錄並選擇正確的數據庫。我想過從會話或聲明中獲取存儲庫,無論是非空的。但是我無法在Login控制器中使用SignInManager之前設置該值,因為SignInManager被注入到控制器中,在我更新會話變量之前創建了一個上下文。我能想到的唯一方法是登錄兩頁。第一頁將詢問公司代碼並更新會話變量。第二頁將使用SignInManager並將存儲庫注入到控制器構造函數中。這將在第一頁更新會話變量後發生。這實際上可能在兩個登錄視圖之間使用動畫更具視覺吸引力。除非有任何想法在沒有兩個登錄視圖的情況下執行此操作,否則我將嘗試實現兩頁登錄並發布代碼(如果有效)。

它實際上是破碎的

當它工作時,這是因為我仍然有一個有效的cookie。我會運行該項目,它將跳過登錄。現在我收到異常InvalidOperationException: No database provider has been configured for this DbContext清除緩存後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>();

編輯

我驗證了Identity是問題所在。我在執行PasswordSignInAsync之前從我的存儲庫中提取數據,並且它提取數據就好了。如何為Identity創建DbContext?

一般承認的答案

創建一個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");
        }
    }
}

初始化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);
}

用法

var dbContext= DbContextFactory.Create("DB1");

熱門答案

根據你的問題,我將提供一些解決方案,假設有些事情:

首先,我在本地SQL Server實例中創建了三個數據庫:

create database CompanyFoo
go

create database CompanyBar
go

create database CompanyZaz
go

然後,我將在每個數據庫中創建一個包含一行的表:

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

下一步是創建一個具有SQL身份驗證的用戶並授予讀取數據庫的訪問權限,在我的情況下,我的用戶名是johnd,密碼是123。

完成這些步驟後,我們繼續在ASP.NET Core中創建一個MVC應用程序,我使用MultipleCompany作為項目名稱,我有兩個控制器:Home和Administration,目標是首先顯示登錄視圖然後重定向到另一個查看以在“登錄”視圖中根據所選數據庫顯示數據。

為了滿足您的要求,您需要在ASP.NET Core應用程序上使用會話,您可以將此方式更改為存儲並稍後讀取數據,現在這僅用於概念測試。

HomeController代碼:

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();
        }
    }
}

AdministrationController代碼:

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);
        }
    }
}

主頁視圖代碼:

@{
    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>

管理代碼視圖:

@{
    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>

LoginModel代碼:

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
            };
        }
    }
}

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; }
    }
}

ConfigurationValue代碼:

using System;

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

        public String Name { get; set; }

        public String Desc { get; set; }
    }
}

AppSettings代碼:

using System;

namespace MultipleCompany.Models
{
    public class AppSettings
    {
        public String CompanyConnectionString { get; set; }
    }
}

IDbContextService代碼:

using MultipleCompany.Models;

namespace MultipleCompany.Services
{
    public interface IDbContextService
    {
        CompanyDbContext CreateCompanyDbContext(LoginModel model);
    }
}

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;
        }
    }
}

啟動代碼:

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?}");
            });
        }
    }
}

我為我的項目添加了這個包:

"Microsoft.EntityFrameworkCore": "1.0.1",
"Microsoft.EntityFrameworkCore.SqlServer": "1.0.1",
"Microsoft.AspNetCore.Session":  "1.0.0"

我的appsettings.json文件:

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

請關注在家庭視圖中連接到選定數據庫的概念,您可以更改此代碼的任何部分作為改進,請記住我提供此解決方案根據您的簡短問題做出一些假設,請隨時詢問關於此解決方案中任何暴露的方面,以根據您的要求改進這段代碼。

基本上,我們需要定義一個服務來根據選定的數據庫創建db context的實例,即IDbContextService接口和DbContextService,它是該接口的實現。

正如您在DbContextService代碼中看到的那樣,我們替換{}內部的值來構建不同的連接字符串,在這種情況下,我在下拉列表中添加了數據庫名稱,但在實際開發中請避免這種情況,因為出於安全原因,它更好不公開數據庫的真實名稱和其他配置;您可以從控制器端獲得一個奇偶校驗表,以根據所選數據庫解析公司代碼。

這個解決方案的一個改進,就是添加一些代碼來將登錄模型序列化為json到session中,而不是以單獨的方式存儲每個值。

如果這個答案有用,請告訴我。 PD:如果您希望在一個驅動器中上傳完整代碼,請在評論中告訴我



Related

許可下: CC-BY-SA with attribution
不隸屬於 Stack Overflow
許可下: CC-BY-SA with attribution
不隸屬於 Stack Overflow