经过几个小时的研究,发现无法做到这一点;是时候提问了 .
我有一个使用EF Core和MVC的ASP.NET Core 1.1项目,供多个客户使用 . 每个客户都有自己的数据库,具有完全相同的模式 . 该项目目前是一个迁移到Web的Windows应用程序 . 在登录屏幕上,用户有三个字段,公司代码,用户名和密码 . 我需要能够在用户尝试根据他们在公司代码输入中键入的内容进行登录时更改连接字符串,然后在整个会话期间记住他们的输入 .
我找到了一些方法可以使用一个数据库和多个模式执行此操作,但没有一个使用相同模式的多个数据库 .
我解决这个问题的方法并不是问题的实际解决方案,但是解决这个问题对我有用 . 我的数据库和应用程序托管在Azure上 . 我对此的修复是将我的应用服务升级到支持插槽的计划(5个插槽每月只需额外支付20美元) . 每个插槽都有相同的程序,但保存连接字符串的环境变量是公司特定的 . 这样我也可以根据需要对每个公司进行子域名访问 . 虽然这种做法可能不像其他人那样做,但对我来说这是最具成本效益的 . 发布到每个插槽比在花费时间进行其他无法正常工作的编程更容易 . 在微软轻松更改连接字符串之前,这是我的解决方案 .
In response to Herzl's answer
这似乎可行 . 我试图让它实现 . 我正在做的一件事是使用访问我的上下文的存储库类 . 我的控制器将注入的存储库注入其中以调用访问上下文的存储库中的方法 . 我如何在存储库类中执行此操作 . 我的存储库中没有OnActionExecuting重载 . 此外,如果会话持续存在,当用户再次向应用程序打开浏览器并且仍然使用持续7天的cookie登录时会发生什么?这不是新 Session 吗?听起来应用程序会抛出异常,因为会话变量将为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;
}
}
In response to The FORCE JB's answer
我试图实现你的方法 . 我在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并将存储库注入到控制器构造函数中 . 这将在第一页更新会话变量后发生 . 对于两个登录视图之间的动画,这实际上可能更具视觉吸引力 . 除非有任何想法在没有两个登录视图的情况下执行此操作,否则我将尝试实现两页登录并发布代码(如果有效) .
It is actually broken
当它工作时,这是因为我仍然有一个有效的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?
5 回答
创建DbContext工厂
初始化DbContext工厂
在startup.cs中
用法
根据你的问题,我将提供一些解决方案,假设有些事情:
首先,我在本地SQL Server实例中创建了三个数据库:
然后,我将在每个数据库中创建一个包含一行的表:
下一步是创建一个具有SQL身份验证的用户并授予读取数据库的权限,在我的情况下,我的用户名是johnd,密码是123 .
一旦我们完成这些步骤,我们继续在ASP.NET中创建核心MVC应用程序,我用MultipleCompany作为项目名称,我有两个控制器:Home和管理,我们的目标是首先显示一个登录视图,然后重定向到另一个查看以在“登录”视图中根据所选数据库显示数据 .
为了满足您的要求,您需要在ASP.NET Core应用程序上使用会话,您可以将此方式更改为存储并稍后读取数据,现在这仅用于概念测试 .
HomeController代码:
AdministrationController代码:
主页视图代码:
管理代码视图:
LoginModel代码:
CompanyDbContext代码:
ConfigurationValue代码:
AppSettings代码:
IDbContextService代码:
DbContextService代码:
启动代码:
我为我的项目添加了这个包:
我的appsettings.json文件:
请关注在家庭视图中连接到选定数据库的概念,您可以更改此代码的任何部分作为改进,请记住我提供此解决方案根据您的简短问题做出一些假设,请随时询问关于此解决方案中任何暴露的方面,以根据您的要求改进这段代码 .
基本上,我们需要定义一个服务来根据选定的数据库创建db context的实例,即IDbContextService接口和DbContextService,它是该接口的实现 .
正如你可以在DbContextService代码中看到的,我们更换的{}里面的 Value 观来 Build 不同的连接字符串,在这种情况下,我在下拉列表中,但在真正的发展增加了数据库名称请尽量避免这种方式,因为出于安全考虑,最好不要公开数据库的真实名称和其他配置;您可以从控制器端获得一个奇偶校验表,以根据所选数据库解析公司代码 .
对此解决方案的一个改进是,添加一些代码将登录模型序列化为json进入会话,而不是以单独的方式存储每个值 .
如果这个答案有用,请告诉我 . PD:如果您希望在一个驱动器中上传完整代码,请在评论中告诉我
由于您正在构建多租户Web应用程序,因此您必须首先决定如何区分租户 . 你打算使用不同的URL吗?或者可能是相同的URL,但在URL中添加了一个部分?
假设您选择了后者,那么租户1将具有与此类似的URL:http://localhost:9090/tenant1/orders
租户2的网址如下:http://localhost:9090/tenant2/orders
您可以使用URL路由执行此操作:
对于连接字符串,您需要一个类来根据URL决定连接字符串,并将此类注入到DB上下文中 .
在您的数据库环境中:
Update to pass connection string
要将动态生成的连接传递给您的上下文,请在相同的上下文中创建一个部分类,因为如果有人运行自定义工具(对于edmx),上下文部分类将确保它保持不变,自动生成的代码将被清除并重新生成 . 如果你在部分类中有这个代码,它将不会被删除 . 对于代码优先,这将不适用 . 这是代码:
我过去这样做的方法是有一个数据库,其中存储了所有客户端的帐户(用户名,密码) . 运行应用程序的帐户将用于与此数据库通信,以验证正在记录的客户端(CompanyID,Password) .
之后,一旦经过身份验证,就会生成令牌 . 之后,经过身份验证的用户将与该客户端(公司)数据库进行交互 . 对于此部分,您可以动态创建连接,如图所示here但我也将其复制并粘贴到她:
您需要在上面的代码中提供自己的csdl,ssdl和msl名称 . 如果您使用的是Code First,那么您的连接字符串将不需要元数据 .
您可以在创建时尝试以下操作您的上下文实例:
然后在上下文类中创建一个构造函数,该构造函数接受字符串作为参数并将其用作: