ASP.NET Core изменить строку подключения EF при входе пользователя в систему
после нескольких часов исследований и не найдя способ сделать это, пришло время задать вопрос.
у меня есть ASP.NET проект Core 1.1 с использованием EF Core и MVC, который используется несколькими клиентами. Каждый клиент имеет свою собственную базу данных с точно такой же схемой. В настоящее время этот проект является приложением Windows, которое переносится в интернет. На экране входа в систему пользователь имеет три поля: код компании, имя пользователя и пароль. Мне нужно иметь возможность изменить строку соединения, когда пользователь пытается войти в систему на основе того, что они вводят в код компании, а затем запоминают их ввод на протяжении всего сеанса.
я нашел несколько способов сделать это с одной базой данных и несколькими схемами, но ни с несколькими базами данных, использующими одну и ту же схему.
я решил эту проблему не решить проблему, а обойти, что работал для меня. Мои базы данных и приложение размещены в Azure. Мое исправление к этому было обновить мое приложение обслуживание плана, который поддерживает слоты (только дополнительные $20 в месяц для 5 слотов). Каждый слот имеет одну и ту же программу, но переменная среды, содержащая строку подключения, специфична для компании. Таким образом, я также могу подчинить доступ каждой компании, если захочу. Хотя этот подход не может быть тем, что сделали бы другие, он был наиболее экономически эффективным для меня. Легче публиковать в каждом слоте, чем тратить часы на другое программирование, которое работает неправильно. Пока Microsoft не упростит изменение строки подключения это мое решение.
в ответ на ответ
кажется, это может сработать. Я попытался его реализовать. Одна вещь, которую я делаю, - это использование класса репозитория, который обращается к моему контексту. Мои контроллеры получают репозиторий, введенный в них, чтобы вызвать методы в репозитории, которые обращаются к контексту. Как это сделать в классе репозитория. Нет перегрузки OnActionExecuting в моем хранилище. Кроме того, если это сохраняется для сеанса, что происходит, когда пользователь снова открывает свой браузер для приложения и все еще входит в систему с файлом cookie, который длится 7 дней? Разве это не новый сеанс? Похоже, приложение выдаст исключение, потому что переменная сеанса будет равна null и поэтому не будет иметь полной строки подключения. Думаю, я также могу сохранить его как утверждение и использовать утверждение, если переменная сеанса равна null.
вот мой класс репозитория. 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;
}
}
в ответ на ответ силы JB
я попытался реализовать ваш подход. Я получаю исключение в программе.cs on line
host.Run();
вот моя программа.класс 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();
}
}
}
и мой " запуск.класс 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]'.
не уверен, что делать здесь.
частичный успех edit
хорошо, я получил ваш пример работы. Я могу установить строку подключения в моем конструкторе репозитория, используя другой идентификатор. Теперь моя проблема-войти в систему и выбрать правильную базу данных. Я подумал о том, что репозиторий вытащит из сеанса или утверждения, что бы ни было null. Но я не могу установить значение перед использованием SignInManager в контроллере входа в систему, потому что SignInManager вводится в контроллер, который создает контекст перед обновлением переменной сеанса. Единственный способ, который я могу придумать, - это иметь двухстраничный логин. На первой странице будет запрошен код компании и обновлена переменная сеанса. На второй странице будет использоваться SignInManager и репозиторий будет введен в конструктор контроллеров. Это произойдет после того, как первая страница обновит переменную сеанса. Это может быть более визуально привлекательным с анимацией между обоими представлениями входа. Если только у кого-нибудь нет идей, как сделайте это без двух представлений входа я попытаюсь реализовать двухстраничный вход и опубликовать код, если он работает.
он на самом деле сломан
когда он работал, это потому, что у меня все еще был действительный cookie. Я бы запустил проект, и он пропустил бы логин. Теперь я получаю исключение InvalidOperationException: No database provider has been configured for this DbContext
после очистки кэша. Я прошел через все это, и контекст создается правильно. Я предполагаю, что у идентичности есть какие-то проблемы. Может ли приведенный ниже код добавить хранилища Entity framework в 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
и он вытащил данные просто отлично. Как создается DbContext для идентификации?
5 ответов
создать фабрику 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");
}
}
}
Intialize DbContext factory
в автозагрузку.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.
как только у нас есть эти шаги завершив, мы приступаем к созданию приложения MVC в ASP.NET Core, я использовал MultipleCompany как имя проекта, у меня есть два контроллера: Home и Administration, цель состоит в том, чтобы сначала показать представление входа, а затем перенаправить на другое представление, чтобы показать данные в соответствии с выбранной базой данных в представлении "вход".
чтобы выполнить ваше требование, вам нужно будет использовать сеанс на ASP.NET Core application вы можете изменить этот способ хранения и чтения данных позже, пока это для тестирования концепции только.
код 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}"
}
}
пожалуйста, сосредоточьтесь на концепции подключения к выбранной базе данных в домашнем представлении, вы можете изменить любую часть этого кода в качестве улучшения, пожалуйста, помните, что я предоставляю это решение, делая некоторые предположения в соответствии с вашим кратким вопрос, пожалуйста чувствуйте свободным спросить о любом, который подвергли действию аспекте в этом решении улучшить эту часть кода согласно вашим требованиям.
в принципе, нам нужно определить службу для создания экземпляра контекста БД в соответствии с выбранной базой данных, это интерфейс IDbContextService и DbContextService это реализация для этого интерфейса.
как вы можете видеть в коде DbContextService, мы заменяем значения внутри {} для построения другой строки подключения, в этом случае я добавил имена баз данных в раскрывающемся списке, но в реальной разработке, пожалуйста, избегайте этого, потому что по соображениям безопасности лучше не раскрывать реальные имена ваших баз данных и других конфигураций; вы можете иметь таблицу четности со стороны контроллера, чтобы разрешить код компании в соответствии с выбранной базой данных.
одним из улучшений для этого решения было бы добавить некоторый код для сериализации модели входа в систему как json в сеанс вместо хранения каждого значения в раздельный способ.
пожалуйста, дайте мне знать, если этот ответ полезен. PD: Дайте мне знать в комментариях, если вы хотите, чтобы полный код был загружен на одном диске
поскольку вы строите многопользовательское веб-приложение, вы должны сначала решить, как вы будете различать арендаторов. Вы собираетесь использовать differnent URL-адрес? или, может быть, тот же URL, но добавление части в URL?
предполагая, что вы выбрали последнее, поэтому у арендатора 1 будет URL-адрес, подобный этому:http://localhost:9090/tenant1/orders
арендатор 2 будет иметь URL-адрес, например:http://localhost:9090/tenant2/orders
вы можно сделать это с помощью маршрутизации URL:
routes.MapRoute(
name: "Multitenant",
url: "{tenant}/{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
Что касается строки подключения, вам нужен класс, чтобы решить строку подключения на основе URL-адреса и ввести этот класс в контекст БД.
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
}
}
в контексте вашей БД:
public class MyDbContext: DbContext
{
public MyDbContext(ITenantIdentifier tenantIdentifier)
{
var connectionStringName = "TenantConnectionString"+tenantIdentifier.GetCurrentTenantId(); //here assuming that you are following a pattern, each tenant has a connection string in the shape of TenantConnectionString+ID
var connectionString = //get connection string
base(connectionString);
}
}
обновление для передачи строки подключения
чтобы передать динамически сгенерированное соединение в контекст, создайте частичный класс в том же контексте, что и ваш контекст-частичный класс гарантирует, что он останется нетронутым, если кто-то запустил пользовательский инструмент (для edmx), автоматически сгенерированный код будет уничтожен и восстановлен. Если у вас есть этот код в частичном классе, он не будет уничтожен. Для кода first это не будет применяться. Вот код:
public class YourContext : DbContext
{
public YourContext(string connString)
{
}
}
путь Я сделал это в прошлом, чтобы иметь одну базу данных, где хранятся учетные записи (имена пользователей, пароли) всех клиентов. Учетная запись, под которой работает приложение, будет использоваться для связи с этой базой данных для аутентификации клиента, который ведет журнал (CompanyID, Password).
после этого, после аутентификации, генерируется токен. После этого аутентифицированный пользователь будет взаимодействовать с базой данных этого клиента (компании). Для этой части можно создать соединение на муха, как показано здесь но я скопирую и вставлю ее тоже:
// 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 в приведенном выше коде. Если сначала используется код, то строка подключения не будет нуждаться в метаданных.
вы можете попробовать следующее при создании экземпляра контекста:
// 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;
}
затем создайте конструктор в своем классе контекста, который принимает string в качестве параметра и использует его как:
var db = DBHelper.GetDbContext(name of database to connect);