经过几个小时的研究,发现无法做到这一点;是时候提问了。
我有一个使用EF Core和MVC的ASP.NET Core 1.1项目,供多个客户使用。每个客户都有自己的数据库,具有完全相同的模式。该项目目前是一个迁移到Web的Windows应用程序。在登录屏幕上,用户有三个字段,公司代码,用户名和密码。当用户尝试根据他们在公司代码输入中输入的内容进行登录时,我需要能够更改连接字符串,然后在整个会话期间记住他们的输入。
我找到了一些方法可以使用一个数据库和多个模式执行此操作,但没有一个使用相同模式的多个数据库。
我解决这个问题的方法并不是问题的实际解决方案,但解决方案对我有用。我的数据库和应用程序托管在Azure上。我对此的修复是将我的应用服务升级到支持插槽的计划(5个插槽每月只需额外支付20美元)。每个插槽都有相同的程序,但保存连接字符串的环境变量是公司特定的。这样我也可以根据需要对每个公司进行子域名访问。虽然这种做法可能不像其他人那样做,但对我来说这是最具成本效益的。发布到每个插槽比在花费时间进行其他无法正常工作的编程更容易。直到Microsoft能够轻松更改连接字符串,这才是我的解决方案。
回应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();
这是我的&#39; Program.cs&#39;类。不变。
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();
}
}
}
我的&#39; Startup.cs&#39;类。
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
。我已经完成了所有操作并且正确地创建了上下文。我的猜测是身份存在某种问题。以下代码在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?
答案 0 :(得分:18)
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");
}
}
}
在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");
答案 1 :(得分:4)
根据你的问题,我将提供一些解决方案:
首先,我在本地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:如果您希望在一个驱动器中上传完整代码,请在评论中告诉我们
答案 2 :(得分:1)
由于您正在构建多租户Web应用程序,因此您必须首先决定如何区分租户。你打算使用不同的URL吗?或者可能是相同的URL,但在URL中添加了一部分?
假设您选择了后者,那么租户1将拥有与此类似的网址:http://localhost:9090/tenant1/orders
租户2的网址类似于:http://localhost:9090/tenant2/orders
您可以使用网址路由执行此操作:
scala.util.Try
对于连接字符串,您需要一个类来根据URL决定连接字符串,并将此类注入到DB上下文中。
routes.MapRoute(
name: "Multitenant",
url: "{tenant}/{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
在您的数据库上下文中:
public interface ITenantIdentifier
{
string GetCurrentTenantId();
}
public class UrlTenantIdentifier : ITenantIdentifier
{
public string GetCurrentTenantId()
{
//Get the current Http Context and get the URL, you should have a table or configration that maps the URL to the tenant ID and connection string
}
}
答案 3 :(得分:1)
您找到了答案,但是也许我的帖子对某人可能有所帮助。我有类似这样的问题。用户登录后,我必须更改实体框架连接字符串以连接其他数据库服务器。对于解决方案,我首先从上下文类中删除了此功能,
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
#warning To protect potentially sensitive information in your connection string, you should move it out of source code. See http://go.microsoft.com/fwlink/?LinkId=723263 for guidance on storing connection strings.
optionsBuilder.UseSqlServer("your connectionstring...");
}
因为我无法从外部调用此函数。我有这个自动生成的构造函数
public ClientContext(DbContextOptions<ClientContext> options)
: base(options)
{
}
删除后,我将此代码添加到上下文类中。
public ClientContext CreateConnectionFromOut(string connectionString)
{
var optionsBuilder = new DbContextOptionsBuilder<Esdesk_ClientContext>();
optionsBuilder.UseSqlServer(connectionString);
var context = new ClientContext(optionsBuilder.Options);
return context;
}
现在,终于可以从任何位置更改连接字符串了。就是这样,
ClientContext cc = new ClientContext();
var db = cc.CreateConnectionFromOut("your connection string");
希望这对某人可能很好。
答案 4 :(得分:0)
更新以传递连接字符串
要将动态生成的连接传递给您的上下文,请在相同的上下文中创建一个部分类,因为如果有人运行自定义工具(对于edmx),上下文部分类将确保它保持不变,将自动生成的代码将被擦除出来并重新生成。如果你在部分类中有这个代码,它将不会被删除。对于代码优先,这将不适用。这是代码:
public class YourContext : DbContext
{
public YourContext(string connString)
{
}
}
我过去这样做的方法是有一个数据库,其中存储了所有客户端的帐户(用户名,密码)。运行应用程序的帐户将用于与此数据库通信,以验证正在记录的客户端(CompanyID,Password)。
之后,一旦经过身份验证,就会生成令牌。之后,经过身份验证的用户将与该客户端(公司)数据库进行交互。对于此部分,您可以动态创建连接,如here所示,但我也会将其复制并粘贴到她身上:
// Specify the provider name, server and database.
string providerName = "System.Data.SqlClient";
string serverName = ".";
string databaseName = "AdventureWorks";
// Initialize the connection string builder for the
// underlying provider.
SqlConnectionStringBuilder sqlBuilder =
new SqlConnectionStringBuilder();
// Set the properties for the data source.
sqlBuilder.DataSource = serverName;
sqlBuilder.InitialCatalog = databaseName;
sqlBuilder.IntegratedSecurity = true;
// Build the SqlConnection connection string.
string providerString = sqlBuilder.ToString();
// Initialize the EntityConnectionStringBuilder.
EntityConnectionStringBuilder entityBuilder =
new EntityConnectionStringBuilder();
//Set the provider name.
entityBuilder.Provider = providerName;
// Set the provider-specific connection string.
entityBuilder.ProviderConnectionString = providerString;
// Set the Metadata location.
entityBuilder.Metadata = @"res://*/AdventureWorksModel.csdl|
res://*/AdventureWorksModel.ssdl|
res://*/AdventureWorksModel.msl";
Console.WriteLine(entityBuilder.ToString());
您需要在上面的代码中提供自己的csdl,ssdl和msl名称。如果您使用的是Code First,那么您的连接字符串将不需要元数据。
答案 5 :(得分:0)
您可以在创建上下文实例时尝试以下操作:
// in class DBHelper
public static YourEntities GetDbContext(string tenantName)
{
var connectionStringTemplate =
@"metadata=res://*/yourModel.csdl|res://*/yourModel.ssdl|res://*/yourModel.msl;" +
@"provider=System.Data.SqlClient;" +
@"provider connection string=""data source=.;" +
@"initial catalog={0};" +
@"user id=sa;password=pwd;" +
@"MultipleActiveResultSets=True;App=EntityFramework"";";
var connectionString = string.Format(connection, tenantName);
var db = new YourEntities(connectionString);
return db;
}
然后在上下文类中创建一个构造函数,该构造函数接受字符串作为参数并将其用作:
var db = DBHelper.GetDbContext(name of database to connect);