I'm new to ASP.NET Core and I'm trying to solve this problem for a week now.
I have a solution with two projects.
And when I start the porject the browser just says:
InvalidOperationException: Unable to resolve service for type 'TSM_Programm.Data.TSMContext' while attempting to activate 'TSM_Programm.Controllers.ResourcesController'.
The first part of the solution is my API-Layer that passes data to a user (currently via postman).
The second project is my Data Access Layer.
This Layer contains several Controllers, all of them using the same constructor, which is the following:
public TSMContext _context;
public ResourcesController(TSMContext context)
{
_context = context;
}
The TSMContext Class is the following:
namespace TSM_Programm.Data
{
public class TSMContext : DbContext
{
public TSMContext(DbContextOptions<TSMContext> options)
: base(options)
{
}
public DbSet<Resource> Resources { get; set; }
public DbSet<Parameter> Parameters { get; set; }
public DbSet<ResourceToParameter> ResourceToParameters { get; set; }
public DbSet<Reservation> Reservations { get; set; }
}
So far so god, but when I am trying to start the program the controllerof the API-Layer does not seem to be able to handle the constructor.
This is my API-Conrtoller:
namespace TSM_API.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class APIController : ControllerBase //Base Class without View Support
{
//Troublemaker
public ResourcesController _resources;
public ParametersController _parameters;
public ReservationsController _reservations;
public APIController(ResourcesController resources, ParametersController parameters, ReservationsController reservations)
{
_resources = resources;
_parameters = parameters;
_reservations = reservations;
}
//Function to check if controller works
//GET: api/API
[HttpGet]
public IEnumerable<string> Get()
{
// ResourcesController controller = new ResourcesController();
return new string[] { "value1", "value2" };
}
The API-Controller was not able to use its own constructors, that's why I changed the Startup.cs.
namespace TSM_API
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddMvc().AddApplicationPart(typeof(ResourcesController).Assembly).AddControllersAsServices();
services.AddMvc().AddApplicationPart(typeof(ParametersController).Assembly).AddControllersAsServices();
services.AddMvc().AddApplicationPart(typeof(ReservationsController).Assembly).AddControllersAsServices();
services.AddMvc().AddApplicationPart(typeof(TSMContext).Assembly).AddControllersAsServices();
}
I'm simply out of ideas on how to solve the problem, since I can't add the TSMContext class a service.
Any idea how to solve it?
Thank you.
I see you have not registered your dbcontext as a dependency injection. Your issue might be due to ResourceController trying to access _context as a DI but it is not registered. To use the context as a dependency injection, register it in the startup.cs as following.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<TSMContext>(options => options.UseSqlServer(Configuration.GetConnectionString("YOUR_CONNECTION_STRING")));
//If you have any services that should be used as DI, then they also must be registered as like this
services.AddScoped<Interface, Class>(); //Interface refer to the service interface while class is the actual service you will use.
}
I have a .net Core application and I've been attempting to read from my local instance of SQL Server (2014) with Windows Authentication and continue to run into a repeat error about my connection string. I've been reviewing the MSDN docs as well as connectionstrings.com and thought I had everything configured correctly.
This is my error:
"System.ArgumentException: 'Format of the initialization string does
not conform to specification starting at index 0.'"
Which I take to mean the very start of my connection string.
I have read the other posts related to this exact issue but haven't been able to use them to find a solution.
Here is what I attempt when the error occurs:
public class HomeController : Controller
{
private ModelContext _context;
public HomeController()
{}
public IActionResult Index()
{
var viewModel = new HomeViewModel();
var optionsBuilder = new DbContextOptionsBuilder<ModelContext>();
optionsBuilder.UseSqlServer("DefaultConnection");
using (_context = new ModelContext(optionsBuilder.Options))
{
>>>>>> viewModel.List = _context.TableName.ToList(); <<<<<<<<
I have the following in my "appsettings.json" file:
"ConnectionStrings": {
"DefaultConnection": "Server=MyComputerName; Database=DBName; IntegratedSecurity=SSPI;"
},
In my "ModelContext.cs" file
public class ModelContext : DbContext
{
public ModelContext(DbContextOptions<ModelContext> options)
:base(options)
{ }
[<Table Properties>]
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("DefaultConnection");
}
And "Startup.cs" file:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddDbContext<ModelContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
}
Thanks for taking a look!
After much contemplation and re-review of the MS Documents site for EF Core DbContext, I found that I was attempting to implement ALL 3 methods of DbContext configuration: Constructor Argument, OnConfiguring, and dependency injection.
Decided to go with OnConfiguring to get the app moving.
I have the custom AuthorizeAttribute where I need to use one of the business layer services to validate some data in the database before giving user a permission to view the resource. In order to be able to allocate this service within the my AuthorizeAttribute I decided to use service location "anti-pattern", this is the code:
internal class AuthorizeGetGroupByIdAttribute : AuthorizeAttribute
{
private readonly IUserGroupService _userGroupService;
public AuthorizeGetGroupByIdAttribute()
{
_userGroupService = ServiceLocator.Instance.Resolve<IUserGroupService>();
}
//In this method I'm validating whether the user is a member of a group.
//If they are not they won't get a permission to view the resource, which is decorated with this attribute.
protected override bool IsAuthorized(HttpActionContext actionContext)
{
Dictionary<string, string> parameters = actionContext.Request.GetQueryNameValuePairs().ToDictionary(x => x.Key, x => x.Value);
int groupId = int.Parse(parameters["groupId"]);
int currentUserId = HttpContext.Current.User.Identity.GetUserId();
return _userGroupService.IsUserInGroup(currentUserId, groupId);
}
protected override void HandleUnauthorizedRequest(HttpActionContext actionContex)
{
if (!HttpContext.Current.User.Identity.IsAuthenticated)
{
base.HandleUnauthorizedRequest(actionContex);
}
else
{
actionContex.Response = new HttpResponseMessage(HttpStatusCode.Forbidden);
}
}
}
I have couple of other attributes like this in my application. Using service locator is probably not a good approach. After searching the web a little bit I found some people suggesting to use IAuthorizationFilter with dependency injection instead. But I don't know how to write this kind of IAuthorizationFilter. Can you help me writing IAuthorizationFilter that will do the same thing that the AuthorizeAttribute above?
So after struggling for a while I think I managed to resolve this issue. Here are the steps you have to do in order to that:
1) First you have to make GetGroupByIdAttribute passive, and by passive I mean an empty attribute without any logic within it (it will be used strictly for decoration purposes)
public class GetGroupByIdAttribute : Attribute
{
}
2) Then you have to mark a controller method, for which you want to add authorization, with this attribute.
[HttpPost]
[GetGroupById]
public IHttpActionResult GetGroupById(int groupId)
{
//Some code
}
3) In order to write your own IAuthorizationFilter you have to implement its method ExecuteAuthorizationFilterAsync. Here is the full class (I included comments to guide you through the code):
public class GetGroupByIdAuthorizationFilter : IAuthorizationFilter
{
public bool AllowMultiple { get; set; }
private readonly IUserGroupService _userGroupService;
//As you can see I'm using a constructor injection here
public GetGroupByIdAuthorizationFilter(IUserGroupService userGroupService)
{
_userGroupService = userGroupService;
}
public Task<HttpResponseMessage> ExecuteAuthorizationFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
{
//First I check whether the method is marked with the attribute, if it is then check whether the current user has a permission to use this method
if (actionContext.ActionDescriptor.GetCustomAttributes<GetGroupByIdAttribute>().SingleOrDefault() != null)
{
Dictionary<string, string> parameters = actionContext.Request.GetQueryNameValuePairs().ToDictionary(x => x.Key, x => x.Value);
int groupId = int.Parse(parameters["groupId"]);
int currentUserId = HttpContext.Current.User.Identity.GetUserId();
//If the user is not allowed to view view the resource, then return 403 status code forbidden
if (!_userGroupService.IsUserInGroup(currentUserId, groupId))
{
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.Forbidden));
}
}
//If this line was reached it means the user is allowed to use this method, so just return continuation() which basically means continue processing
return continuation();
}
}
4) The last step is to register your filter in the WebApiConfig.
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Here I am registering Dependency Resolver
config.DependencyResolver = ServiceLocator.Instance.DependencyResolver;
//Then I resolve the service I want to use (which should be fine because this is basically the start of the application)
var userGroupService = ServiceLocator.Instance.Resolve<IUserGroupService>();
//And finally I'm registering the IAuthorizationFilter I created
config.Filters.Add(new GetGroupByIdAuthorizationFilter(userGroupService));
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
Now, if needed, I can create additional IActionFilters that use IUserGroupService and then inject this service at the start of the application, from WebApiConfig class, into all filters.
Perhaps try it like shown here:
Add the following public method to your class.
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
// gets the dependecies from the serviceProvider
// and creates an instance of the filter
return new GetGroupByIdAuthorizationFilter(
(IUserGroupService )serviceProvider.GetService(typeof(IUserGroupService )));
}
Also Add interface IFilterMetadata to your class.
Now when your class is to be created the DI notices that there is a CreateInstance method and will use that rather then the constructor.
Alternatively you can get the interface directly from the DI in your method by calling
context.HttpContext.Features.Get<IUserGroupService>()
After a few hours of research and finding no way to do this; it's time to ask the question.
I have an ASP.NET Core 1.1 project using EF Core and MVC that is used by multiple customers. Each customer has their own database with the exact same schema. This project is currently a Windows application being migrated to the web. At the login screen the user has three fields, Company Code, Username and Password. I need to be able to change the connection string when the user attempts to login based on what they type in the Company Code input then remember their input throughout the session duration.
I found some ways to do this with one database and multiple schema, but none with multiple databases using the same schema.
The way I solved this problem isn't an actual solution to the problem, but a work around that worked for me. My databases and app are hosted on Azure. My fix to this was to upgrade my app service to a plan that supports slots (only an extra $20 a month for 5 slots). Each slot has the same program but the environment variable that holds the connection string is company specific. This way I can also subdomain each companies access if I want. While this approach may not be what others would do, it was the most cost effective to me. It is easier to publish to each slot than to spend the hours doing the other programming that doesn't work right. Until Microsoft makes it easy to change the connection string this is my solution.
In response to Herzl's answer
This seems like it could work. I have tried to get it implemented. One thing I am doing though is using a repository class that accesses my context. My controllers get the repository injected into them to call methods in the repository that access the context. How do I do this in a repository class. There is no OnActionExecuting overload in my repository. Also, if this persists for the session, what happens when a user opens their browser to the app again and is still logged in with a cookie that lasts 7 days? Isn't this a new session? Sounds like the app would throw an exception because the session variable would be null and therefor not have a complete connection string. I guess I could also store it as a Claim and use the Claim if the session variable is null.
Here is my repository class. IDbContextService was ProgramContext but I started adding your suggestions to try and get it to work.
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
I tried to implement your approach. I get an exception in Program.cs on line
host.Run();
Here is my 'Program.cs' class. Untouched.
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();
}
}
}
And my 'Startup.cs' class.
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" }
);
});
}
}
}
And the exception:
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]'.
Not sure what to do here.
Partial success edit
Okay I got your example working. I can set the connection string in my repository constructor using a different id. My problem now is logging in and choosing the right database. I thought about having the repository pull from a session or claim, whatever wasn't null. But I can't set the value before using the SignInManager in the Login controller because SignInManager is injected into the controller which creates a context before I update the session variable. The only way I can think of is to have a two page login. The first page will ask for the company code and update the session variable. The second page will use the SignInManager and have the repository injected into the controllers constructor. This would happen after the first page updates the session variable. This may actually be more visually appealing with animations between both login views. Unless anyone has any ideas on a way to do this without two login views I am going to try and implement the two page login and post the code if it works.
It is actually broken
When it was working, it is because I still had a valid cookie. I would run the project and it would skip the login. Now I get the exception InvalidOperationException: No database provider has been configured for this DbContext after clearing my cache. I have stepped through it all and the context is being created correctly. My guess is that Identity is having some sort of issues. Could the below code adding the entity framework stores in ConfigureServices be causing the issue?
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>();
Edit
I verified Identity is the problem. I pulled data from my repository before executing PasswordSignInAsync and it pulled the data just fine. How is the DbContext created for Identity?
Create a DbContext factory
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
In 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);
}
Usage
var dbContext= DbContextFactory.Create("DB1");
According to your question, I going to provide a solution assuming some things:
First, I've created three databases in my local SQL Server instance:
create database CompanyFoo
go
create database CompanyBar
go
create database CompanyZaz
go
Then, I going to create one table with one row in each database:
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
Next step is create an user with SQL Authentication and grant access to read the databases, in my case my user name is johnd and password is 123.
Once we have these steps completed, we proceed to create an MVC application in ASP.NET Core, I used MultipleCompany as project name, I have two controllers: Home and Administration, the goal is to show a login view first and then redirect to another view to show data according to selected database in "login" view.
To accomplish your requirement, you'll need to use session on ASP.NET Core application you can change this way to storage and read data later, for now this is for concept test only.
HomeController code:
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 code:
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);
}
}
}
Code for Home view:
#{
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>
Code for Administration view:
#{
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 code:
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 code:
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 code:
using System;
namespace MultipleCompany.Models
{
public class ConfigurationValue
{
public Int32? Id { get; set; }
public String Name { get; set; }
public String Desc { get; set; }
}
}
AppSettings code:
using System;
namespace MultipleCompany.Models
{
public class AppSettings
{
public String CompanyConnectionString { get; set; }
}
}
IDbContextService code:
using MultipleCompany.Models;
namespace MultipleCompany.Services
{
public interface IDbContextService
{
CompanyDbContext CreateCompanyDbContext(LoginModel model);
}
}
DbContextService code:
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;
}
}
}
Startup code:
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?}");
});
}
}
}
I've added this packages for my project:
"Microsoft.EntityFrameworkCore": "1.0.1",
"Microsoft.EntityFrameworkCore.SqlServer": "1.0.1",
"Microsoft.AspNetCore.Session": "1.0.0"
My appsettings.json file:
{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
},
"AppSettings": {
"CompanyConnectionString": "server=(local);database={database};user id={user id};password={password}"
}
}
Please get focus on the concept about to connect to selected database in home view, you can change any part of this code as an improvement, please remember I'm providing this solution making some assumptions according to your brief question, please feel free to ask about any exposed aspect in this solution to improve this piece of code according to your requirements.
Basically, we need to define a service to create the instance of db context according to selected database, that's IDbContextService interface and DbContextService it's the implementation for that interface.
As you can see on DbContextService code, we replace the values inside of {} to build different connection string, in this case I've added the database names in drop down list but in real development please avoid this way because for security reasons it's better to don't expose the real names of your databases and other configurations; you can have a parity table from controller's side to resolve the company code according to selected database.
One improvement for this solution, it would be to add some code to serialize login model as json into session instead of store each value in separate way.
Please let me know if this answer is useful.
PD: Let me know in comments if you want the full code to upload in one drive
You found your answer but maybe my post can be helpful for someones. I had a similar problem like this question. I have had to change my entity framework connectionstring to connect different database server after user logged in. And for solution first I deleted this function from my context class,
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...");
}
because I couldn't called this function from outside. And I had this auto generated constructor
public ClientContext(DbContextOptions<ClientContext> options)
: base(options)
{
}
After deletion, I added this code to my context class.
public ClientContext CreateConnectionFromOut(string connectionString)
{
var optionsBuilder = new DbContextOptionsBuilder<Esdesk_ClientContext>();
optionsBuilder.UseSqlServer(connectionString);
var context = new ClientContext(optionsBuilder.Options);
return context;
}
Now finally, I can change my connection string from wherever I want. It is just like that,
ClientContext cc = new ClientContext();
var db = cc.CreateConnectionFromOut("your connection string");
Hope this may be fine for someone.
Since you are building a multi-tenant web application, you have to first decide how will you distinguish between tenants. Are you going to use differnent URL? or maybe the same URL but adding a part in the URL?
Assuming that you chose the latter, so tenant 1 would have a URL similar to this: http://localhost:9090/tenant1/orders
Tenant 2 would have a URL like: http://localhost:9090/tenant2/orders
You can do that using URL routing:
routes.MapRoute(
name: "Multitenant",
url: "{tenant}/{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
As for the connection string, you need a class to decide the connection string based on the URL, and inject this class into the DB context.
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
}
}
In your DB Context:
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);
}
}
Inspired by (Link to article) this is the way I implemented it
Configuration:
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();
services.AddTransient<ClaimsPrincipal>(provider =>
provider.GetService<IHttpContextAccessor>().HttpContext.User
);
services.AddDbContext<MyDbContext>();
...
}
DbContext:
public class MyDbContext: DbContext
{
readonly ClaimsPrincipal _claimsPrincipal;
public MyDbContext(DbContextOptions options, ClaimsPrincipal claimsPrincipal) : base(options)
{
_claimsPrincipal = claimsPrincipal;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var email = _claimsPrincipal.Claims
.FirstOrDefault(e => e.Type == ClaimTypes.Email).Value;
var dbName = string.Concat(
email.Split(Path.GetInvalidFileNameChars())
);
var connectionString = $"Filename=./{dbName}.sqlite";
optionsBuilder.UseSqlite(connectionString);
base.OnConfiguring(optionsBuilder);
}
}
Update to pass connection string
To pass the dynamically generated connection to your context, create a partial class in the same context as your context-partial class would ensure it stays intact if someone ran the custom tool (for edmx), the auto generated code will be wiped out and regenerated. If you have this code in a partial class it will not be wiped out. For code first, this will not apply. Here is the code:
public class YourContext : DbContext
{
public YourContext(string connString)
{
}
}
The way I have done this in the past is to have one database where the accounts (usernames, passwords) of all the clients are stored. The account the application is running under would be used to communicate with this database to authenticate the client who is logging (CompanyID, Password).
Afterwards, once authenticated, a token is generated. Afterwards, the authenticated user will be interacting with that client's (Company) database. For this part, you can create the connection on the fly as shown here but I will copy and paste it her as well:
// 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());
You will need to provide your own csdl, ssdl, and msl names in the above code. If you are using Code First, then your connection string will not need the metadata.
You could try the following while creating your context instance:
// 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;
}
Then make a constructor in your context class which accepts string as a parameter and use it as:
var db = DBHelper.GetDbContext(name of database to connect);
It's been a long time since I posted this question, and I never shared the solution I developed, so I figured I should.
I ended up going the route of using different subdomains for my tenants. Because of this, I simply created a TenantService that checked the url and returned a connection string from config. Inside my DbContext's OnConfiguring method, I simply called the tenant service and used the returned connection string. Here is some sample code:
Tenant Service
public class Tenant
{
public string Name { get; set; }
public string Hostname { get; set; }
public string ConnectionString { get; set; }
}
public interface ITenantService
{
Tenant GetCurrentTenant();
List<Tenant> GetTenantList();
}
public class TenantService : ITenantService
{
private readonly ILogger<TenantService> _logger;
private readonly IHttpContextAccessor _httpContext;
private readonly IConfiguration _configuration;
public TenantService(
ILogger<TenantService> logger,
IHttpContextAccessor httpContext,
IConfiguration configuration)
{
_logger = logger;
_httpContext = httpContext;
_configuration = configuration;
}
/// <summary>
/// Gets the current tenant from the host.
/// </summary>
/// <returns>The tenant.</returns>
public Tenant GetCurrentTenant()
{
Tenant tenant;
var host = _httpContext.HttpContext.Request.Host;
var tenants = GetTenantList();
tenant = tenants.SingleOrDefault(t => t.Hostname == host.Value);
if (tenant == null)
{
_logger.LogCritical("Could not find tenant from host: {host}", host);
throw new ArgumentException($"Could not find tenant from host: {host}");
}
return tenant;
}
/// <summary>
/// Gets a list of tenants in configuration.
/// </summary>
/// <returns>The list of tenants.</returns>
public List<Tenant> GetTenantList()
{
var tenants = new List<Tenant>();
_configuration.GetSection("Tenants").Bind(tenants);
return tenants;
}
}
DbContext
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
if (!optionsBuilder.IsConfigured)
{
if (_tenantService == null)
{
throw new ArgumentNullException(nameof(_tenantService));
}
optionsBuilder.UseSqlServer(_tenantService.GetCurrentTenant().ConnectionString);
}
}
I can DI app setting in the controller like this
private IOptions<AppSettings> appSettings;
public CompanyInfoController(IOptions<AppSettings> appSettings)
{
this.appSettings = appSettings;
}
But how to DI that in my custom class like this
private IOptions<AppSettings> appSettings;
public PermissionFactory(IOptions<AppSettings> appSetting)
{
this.appSettings = appSettings;
}
my register in Startup.cs is
services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));
The "proper" way
Register your custom class in the DI, the same way you register other dependencies in ConfigureServices method, for example:
services.AddTransient<PermissionFactory>();
(Instead of AddTransient, you can use AddScoped, or any other lifetime that you need)
Then add this dependency to the constructor of your controller:
public CompanyInfoController(IOptions<AppSettings> appSettings, PermissionFactory permFact)
Now, DI knows about PermissionFactory, can instantiate it and will inject it into your controller.
If you want to use PermissionFactory in Configure method, just add it to it's parameter list:
Configure(IApplicationBuilder app, PermissionFactory prov)
Aspnet will do it's magic and inject the class there.
The "nasty" way
If you want to instantiate PermissionFactory somewhere deep in your code, you can also do it in a little nasty way - store reference to IServiceProvider in Startup class:
internal static IServiceProvider ServiceProvider { get;set; }
Configure(IApplicationBuilder app, IServiceProvider prov) {
ServiceProvider = prov;
...
}
Now you can access it like this:
var factory = Startup.ServiceProvider.GetService<PermissionFactory>();
Again, DI will take care of injecting IOptions<AppSettings> into PermissionFactory.
Asp.Net 5 Docs in Dependency Injection
I recommend not passing AppSettings. A class shouldn't depend on something vague - it should depend on exactly what it needs, or close to it. ASP.NET Core makes it easier to move away from the old pattern of depending on AppSettings. If your class depends on AppSettings then you can't really see from the constructor what it depends on. It could depend on any key. If it depends on a more specific interface then its dependency is clearer, more explicit, and you can mock that interface when unit testing.
You can create an interface with the specific settings that your class needs (or something less specific but not too broad) and a class that implements it - for example,
public interface IFooSettings
{
string Name { get; }
IEnumerable Foos { get; }
}
public interface IFoo
{
string Color { get; }
double BarUnits { get; }
}
public class FooSettings : IFooSettings
{
public string Name { get; set; }
public List<Foo> FooList { get; set; }
public IEnumerable Foos
{
get
{
if (FooList == null) FooList = new List<Foo>();
return FooList.Cast<IFoo>();
}
}
}
public class Foo : IFoo
{
public string Color { get; set; }
public double BarUnits { get; set; }
}
Then add a .json file, fooSettings.json:
{
"FooSettings": {
"Name": "MyFooSettings",
"FooList": [
{
"Color": "Red",
"BarUnits": "1.5"
}, {
"Color": "Blue",
"BarUnits": "3.14159'"
}, {
"Color": "Green",
"BarUnits": "-0.99999"
}
]
}
}
Then, in Startup() (in Startup.cs) where we specify what goes into our Configuration, add fooSettings.json:
var builder = new ConfigurationBuilder(appEnv.ApplicationBasePath)
.AddJsonFile("config.json")
.AddJsonFile($"config.{env.EnvironmentName}.json", optional: true)
.AddJsonFile("fooSettings.json");
Finally, in ConfigureServices() (also in Startup.cs) tell it to load an instance of FooSettings, cast it as IFooSettings (so the properties appear read-only) and supply that single instance for all dependencies on IFooSettings:
var fooSettings = (IFooSettings)ConfigurationBinder.Bind<FooSettings>(
Configuration.GetConfigurationSection("FooSettings"));
services.AddInstance(typeof (IFooSettings), fooSettings);
Now your class - controller, filter, or anything else created by the DI container - can have a dependency on IFooSettings and it will be supplied from the .json file. But you can mock IFooSettings for unit testing.
Original blog post - it's mine so I'm not plagiarizing.
You can do dependency injection in your non-controller classes as well.
In your startup class,
public class Startup
{
public IConfigurationRoot Configuration { get; set; }
public Startup(IHostingEnvironment env)
{
// Set up configuration sources.
var builder = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.AddEnvironmentVariables();
Configuration = builder.Build();
}
public void ConfigureServices(IServiceCollection services)
{
// register other dependencies also here
services.AddInstance<IConfiguration>(Configuration);
}
}
Now in your custom class, Have the constructor accept an implementation of IConfiguration
private IConfiguration configuration;
public PermissionFactory(IConfiguration configuration)
{
this.configuration = configuration;
}
public void SomeMethod()
{
var someSection = this.configuration.GetSection("SomeSection");
var someValue= this.configuration.Get<string>("YourItem:SubItem");
}
If you want to DI to action filter reference to Action filters, service filters and type filters in ASP.NET 5 and MVC 6 service filter part.