Asp.net

使用者登錄時 ASP.NET Core 更改 EF 連接字元串

  • September 20, 2021

經過幾個小時的研究,發現沒有辦法做到這一點;是時候問這個問題了。

我有一個使用 EF Core 和 MVC 的 ASP.NET Core 1.1 項目,供多個客戶使用。每個客戶都有自己的數據庫,其架構完全相同。該項目目前是一個正在遷移到 Web 的 Windows 應用程序。在登錄螢幕上,使用者具有三個欄位,公司程式碼、使用者名和密碼。當使用者嘗試根據他們在公司程式碼輸入中鍵入的內容登錄時,我需要能夠更改連接字元串,然後在整個會話期間記住他們的輸入。

我找到了一些使用一個數據庫和多個架構的方法,但沒有使用相同架構的多個數據庫。

我解決這個問題的方法並不是問題的實際解決方案,而是對我有用的解決方法。我的數據庫和應用程序託管在 Azure 上。我對此的解決方法是將我的應用服務升級到支持插槽的計劃(5 個插槽每月只需額外支付 20 美元)。每個插槽都有相同的程序,但保存連接字元串的環境變數是公司特定的。這樣,如果我願意,我還可以為每個公司訪問子域。雖然這種方法可能不是其他人會做的,但它對我來說是最具成本效益的。發佈到每個插槽比花費數小時進行其他無法正常工作的程式更容易。在 Microsoft 使更改連接字元串變得容易之前,這是我的解決方案。

回應赫茨爾的回答

這似乎可以工作。我試圖讓它實現。我正在做的一件事是使用訪問我的上下文的儲存庫類。我的控制器將儲存庫注入其中,以呼叫儲存庫中訪問上下文的方法。如何在儲存庫類中執行此操作。我的儲存庫中沒有 OnActionExecuting 重載。此外,如果會話持續存在,當使用者再次打開瀏覽器訪問應用程序並且仍然使用持續 7 天的 cookie 登錄時會發生什麼?這不是新的會議嗎?聽起來應用程序會拋出異常,因為會話變數將為空,因此沒有完整的連接字元串。我想我也可以將其儲存為聲明並在會話變數為空時使用聲明。

這是我的儲存庫類。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;
   }
}

回應 The 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 在我的儲存庫建構子中設置連接字元串。我現在的問題是登錄並選擇正確的數據庫。我想過讓儲存庫從會話或聲明中提取,無論是否為空。但是我無法在登錄控制器中使用 SignInManager 之前設置該值,因為 SignInManager 被注入到控制器中,該控制器在我更新會話變數之前創建了一個上下文。我能想到的唯一方法是進行兩頁登錄。第一頁將詢問公司程式碼並更新會話變數。第二個頁面將使用 SignInManager 並將儲存庫注入到控制器建構子中。這將在第一頁更新會話變數後發生。兩個登錄視圖之間的動畫實際上可能更具視覺吸引力。除非有人對沒有兩個登錄視圖的方法有任何想法,否則我將嘗試實現兩個頁面登錄並發布程式碼(如果它有效)。

它實際上已經壞了

當它工作時,這是因為我仍然有一個有效的 cookie。我會執行該項目,它會跳過登錄。現在我InvalidOperationException: No database provider has been configured for this DbContext在清除記憶體後得到了異常。我已經完成了這一切,並且正在正確創建上下文。我的猜測是 Identity 存在某種問題。下面添加實體框架儲存的程式碼是否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 工廠

在啟動.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 應用程序上使用會話,您可以稍後更改這種方式來儲存和讀取數據,目前這僅用於概念測試。

家庭控制器程式碼:

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

管理控制器程式碼:

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>

登錄型號程式碼:

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

配置值程式碼:

using System;

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

       public String Name { get; set; }

       public String Desc { get; set; }
   }
}

應用設置程式碼:

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

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

基本上,我們需要定義一個服務來根據選擇的數據庫創建數據庫上下文的實例,即 IDbContextService 介面和 DbContextService 它是該介面的實現。

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

此解決方案的一項改進是添加一些程式碼以將登錄模型作為 json 序列化到會話中,而不是以單獨的方式儲存每個值。

請讓我知道這個答案是否有用。PD:如果您想在一個驅動器中上傳完整程式碼,請在評論中告訴我

引用自:https://stackoverflow.com/questions/40836102