Entity Framework Code First ignoring connection string, using IIS instead - entity-framework

I have a web app that I've created using Entity Framework Code First. In setting it up I have managed to match my DB connection string to my DBContext by specifying the full namespace and class of the DBContext as the name of the connection string.
<add name="MyClassProject.EfDbContext" connectionString="Data Source=.\SQLEXPRESS;Initial Catalog=MyDatabase;Integrated Security=true;User Id=MyUsername;Password=MyPassword;" providerName="System.Data.SqlClient"/>
Initially when I set up the project, I just had it created in c:\inetpub\wwwroot, and just ran it through Visual Studio. Everything worked fine.
Now I'm trying to have the code build to a separate website folder, and have the website run as it's own website and app pool in IIS. I've set up the website, and my hosts file but when I went to run it I received the following error.
Cannot open database "MyDatabase" requested by the login. The login failed.
Login failed for user 'IIS APPPOOL\MyAppPool'.
I'm wondering why this is happening, as I seem to be specifying the security username and password to use for the DB in my connection string....so why is it trying to connect as the app pool that my website is running in?
Also, how can I fix this, without having to give MyAppPool (or Network Service if I changed it to that) DB permissions in SQL Server?
Update: I should've mentioned that I initialise my DBContext class using:
namespace MyClassProject
{
public class EfDbContext : DbContext
{
public EfDbContext() : base ("MyDatabase")
{
}
}
}

I found the issue.
When I initialise my DBContext class with : base("MyDatabase"), it overrides the connection string specified in the web.config.
Removing that from my DBContext class, with the database already existing, the site now works in IIS.
However, if I don't have the database created already, (or if I have my database initialiser use DropCreateDatabaseWhenModelChanges or DropCreateDatabaseAlways so that it'll needs to recreate the DB), the initialiser will fail, as it'll try to use an SQL user that doesn't have permissions to create the DB.
My way around it is to use the : base("MyDatabase") and run from Visual Studio initially so the database is created.
Then remove it from code, add the specified user to the DB security in SQL Server, and it'll allow my site to run in IIS thereafter.

Remove Integrated Security=true;. That is the setting that passes the current user off.

When using Integrated Security, the DB is given a token from the user who is currently running the process. In all likelihood, you run Visual Studio from your user account, which likely has Admin permissions on your SQL Server instance.
When IIS runs your app, it uses something called an Application Pool (or App pool). You can have multiple apps in a single pool to be managed together. The app pool also runs under a special user account named for the pool. App pool users exist under a container called "IIS AppPool", so the local user for the DefaultAppPool is IIS AppPool\DefaultAppPool. If you want to grant access to a resource on your local system (including file permissions), you can also grant it to the app pool user or local group IIS_IUSRS to grant it to all app pools.
Remember that these are local accounts, so they will not cross network boundaries. To grant permissions on a different server, you'll need to either use a domain user (or even better, a domain Managed Service Account) or you can set the app pool user to NETWORK SERVICE and then you can grant permissions to MyDomain\MyWebServer$ (the dollar sign is important).

You can use Web.config Transform to have Local connection stirng different from Remote (say in Release mode). To start using it you need to publish your Web App using One-Click Publish from Visual Studio. That's really very handy way to publish web apps!
Looks like that's what you're looking for.
Or set connection string name base on a condition:
public EfDbContext() : base (GetConnectionStringName())
{
}
private static GetConnectionStringName()
{
return RunLocally() : "LocalDatabase" : "RemoteDatabase";
}
private static bool RunLocally()
{
// implement some how
}

Related

Error opening MDB file on network location with EntityFrameworkCore.jet.oledb 3.1

I have a project opening up MS Access DBs on a network folder. The project is a .net core 3.1 webapi.
EDIT: I'm using EntityFrameworkCore.Jet.OleDb v3.1 with provider in connection string Provider=Microsoft.ACE.OLEDB.12.0.
It very simply updates a list of boards based on new ones
public void SyncBoards(List<Board> boards)
{
_cutriteDbContext.RemoveRange(boards);
_cutriteDbContext.SaveChanges();
_cutriteDbContext.AddRange(boards);
_cutriteDbContext.SaveChanges();
}
I'm getting the error (sanitized)
System.Data.OleDb.OleDbException (0x80004005): The Microsoft Access
database engine cannot open or write to the file
'\{SHAREDFOLDER}{PATH_TO_FILE}\imatv11.mdb'. It is already opened
exclusively by another user, or you need permission to view and write
its data.
This works fine in IIS Express when debugging from VS 2019. I believe this is because the API doesn't have the credentials to access the file. The DBs do not have password protection. Is there a way to provide credentials to the file?
I had to set the identity in the IIS application pool in advanced settings.

Managed Identity Sql Auth with EF Core - Login failed for user '<token-identified principal>'

I have a dotnet 5 (isolated) Azure Function app that needs to access an Azure Sql Server database via EF Core 5. I would like to use the managed identity of the function app when making the sql server requests.
What I tried
I followed the instructions here.
I created a new AD account called "smsrouterdb" and made this the Azure Sql Admin.
The name of my function app is "func-smsrouter-msdn-01". So after logging into the DB via SSMS as "smsrouterdb", I created a contained user as below:
CREATE USER [func-smsrouter-msdn-01] FROM EXTERNAL PROVIDER
ALTER ROLE db_datawriter ADD MEMBER [func-smsrouter-msdn-01]
ALTER ROLE db_datareader ADD MEMBER [func-smsrouter-msdn-01]
I then triggered my function app via an http request.
What happened
I got the following error from the function app:
One or more errors occurred. (Invalid value for key 'authentication'.) ---> System.ArgumentException: Invalid value for key 'authentication'. at Microsoft.Data.Common.DbConnectionStringBuilderUtil.ConvertToAuthenticationType(String keyword, Object value)
I realised that this was because an old version of the nuget package Microsoft.Data.SqlClient was being referenced. So, I explicitly added a reference to v3.0.0.
I then got the following error
Login failed for user '<token-identified principal>'
However, if I change the connection string's authentication property to "Active Directory Interactive" and promote the object id of the managed identity for the function app to be Sql Admin using the following command:
az sql server ad-admin create --resource-group <tg name> --server-name <server name> --display-name MSIAzureAdmin --object-id "id of managed identity here"
then the rows are written correctly. My concern is that the managed identity should not need to be a sql admin.
Config
The nuget packages of the project containing the dbcontext are:
"Microsoft.Azure.Services.AppAuthentication" Version="1.6.1"
"Microsoft.EntityFrameworkCore" Version="5.0.6"
"Microsoft.EntityFrameworkCore.Design" Version="5.0.6"
"Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.6"
"WindowsAzure.Storage" Version="9.3.3" />
From the main Azure Function project, I have references to the following nuget packages:
Microsoft.Data.SqlClient 3.0.0
Serilog.Sinks.MSSqlServer 5.6.0
The only code in my db context is:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
SqlConnection connection = new SqlConnection();
logger.LogInformation($"SqlSvrConString=[{sharedConfig[ConfigConstants.SqlSvrConnString]}]");
connection.ConnectionString = sharedConfig[ConfigConstants.SqlSvrConnString];
optionsBuilder.UseSqlServer(connection);
}
My connection string is:
Server=servernamehere.database.windows.net;Initial
Catalog=dbnamehere;Authentication=Active Directory Managed Identity;
Can anyone explain why this fails unless the managed identity is made sql admin?
I think the root cause of the problem was that when I'd issued the command: CREATE USER [function name here] FROM EXTERNAL PROVIDER, although the function name was spelled correctly, the case was incorrect.
One easy way to work around these kind of errors is to use the guid of the functions identity instead of the name. This works as well.
CREATE USER [abcdef-1234-5678-ghijkl] FROM EXTERNAL PROVIDER

Azure database works on localhost, but not when used with azure service app

So I've been trying to publish my first project to azure. I've got everything set-up, a service app and a sql database.
My initial page loads properly(It's the standard view for a .net core web application).
The first thing I need to do is register a new user. Whenever I try through my azure app (myapp.azurewebsites.net) it fails and the logs says it's db related.
However I try the same thing by running the application on my machine in production environment, again connected to the azure sql server and everything works perfectly. I can register users, I can create posts, I can edit them. The allow access to azure services option is turned on. This error is from the eventlogs. I have not included the stacktrace.
Category: Microsoft.EntityFrameworkCore.Query EventId: 10100 RequestId: 800001be-0000-ba00-b63f-
84710c7967bb RequestPath: /Identity/Account/Register SpanId: |1e5a93ae-43f424904f38ea9f. TraceId:
1e5a93ae-43f424904f38ea9f ParentId: ActionId: c3430236-e61c-4785-a3c3-4f60ba115b6e ActionName:
/Account/Register An exception occurred while iterating over the results of a query for context type
'MyApp.Data.ApplicationDbContext'. Microsoft.Data.SqlClient.SqlException (0x80131904): Server
name cannot be determined. It must appear as the first segment of the server's dns name
(servername.database.windows.net). Some libraries do not send the server name, in which case the
server name must be included as part of the user name (username#servername). In addition, if both
formats are used, the server names must match.
Those are the different ways I tried to add the connection string to the appsettings.json file. (Server name, catalog, user and password have been replaced, they are written correctly in the appsettings file)
Server=tcp:servername.database.windows.net,1433;Initial Catalog=db;Persist Security Info=False;
User ID=user#server;Password=mypassword;
MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;
Server=tcp:servername.database.windows.net,1433;Initial Catalog=db;Persist Security Info=False;
User ID=user;Password=mypassword;
MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;
Data Source=tcp:server.database.windows.net,1433;
Initial Catalog=db;User Id=#server.database.windows.net;Password=password;
Alright so after a day and a half, I finally managed to fix it. The solution is rather simple and it is most likely my newbie mistake, that caused so much trouble.
I was following a tutorial for setting up the application and database connection after that. In the tutorial, the connection string that was being used, was the default one, found in the "myApp -> Configuration -> Connection strings", the format was:
Data Source=tcp:server.database.windows.net,1433;
Initial Catalog=db;User Id=#server.database.windows.net;Password=password;
This one was working in the guide, but not for me. So what I did, was go to my "sqldb -> connection strings" and copied the one provided there. I then went back to the app configuration and added it as a new configuration string using SqlServer as the Type.
This string was in the format:
Server=tcp:servername.database.windows.net,1433;Initial Catalog=db;Persist Security Info=False;
User ID=user;Password=mypassword;
MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;
After that, the app started working properly.

How to handle EF code-first migrations from my local machine when deploying to Azure?

I finally figured out how to get web.config transformations working, so that locally I have one connection (the one in my default web.config), and then when I publish to Azure, the "debug" transformation is applied so that an Azure-SQL database connection string is used.
That much is working, but now I'm running into a problem with database migrations.
In my Configuration:
protected override void Seed(MG.Context.MentorContext context)
{
System.Data.Entity.Database.SetInitializer(new MigrateDatabaseToLatestVersion<MentorContext, Configuration>());
if (!WebSecurity.Initialized)
WebSecurity.InitializeDatabaseConnection("DefaultConnection",
"User", "UserId", "Username", autoCreateTables: true);
}
Now, when I'm running locally and want to update my local database, I open up Package Manager Console and type in 'update-database' and everything works wonderfully.
Sometimes I want to update the remote Azure-SQL database though - so in the past I've done this:
Update-Database -ConnectionString "azure connection string here" -verbose
which was working when I was manually updating my local web.config. Now that I'm using the above transformations, even though I specify a connectionString, DefaultConnection in my Seed method resolves to the un-transformed connection string (my local db), so the Membership tables never get created on the Azure database.
This can be solved by manually updating the default web.config, but that defeats the purpose of using these transformations.
How can I have these transformations applied so that the Seed method of my EF migrations uses the Azure connection strings - OR - how can I tell update-database to use the azure connection string?
I'm trying to avoid manually swapping the connection strings if I can.
You mention a Web.config and "publishing" to Azure; are you using Azure Web Sites?
If so, look at this article. In short, if you configure a connection string on the K/V store of Azure Web Sites with the same name as your connection string, the value you set on Azure will automatically take precedence:
Connection strings work in a similar fashion, with a small additional
requirement. Remember from earlier that there is a connection string
called “example-config_db” that has been associated with the website.
If the website’s web.config file references the same connection string
in the configuration section, then Windows Azure
Web Sites will automatically update the connection string at runtime
using the value shown in the portal.
This should ensure that you Seed method attempts to connect to the right database.

Need ServiceConfiguration.cscfg to populate web.config sessionstate and connection strings

I need to propagate connection string changes for entity framework, asp.net membership (which are both in the connectionstrings section of web.config) and session state (which is in sessonstate's sqlconnectionstring) in web.config when I adjust these settings in windows azure's service configuration.
During development we test our app as a standard asp.net webforms app, but once it is deployed it is running in azure. So we need to allow for the site running in both non-azure and an azure context. That's why we're just relying upon the values in web.config for now.Since these connection strings are not called directly in my code writing a utility class which grabs from azure service config if that is available or otherwise grabs from web.config is not a possibility for these values.
I realize that editing web.config would cause a disruption in service - and i only plan to do this during off hours.
I believe that the best approach is to wrap your configuration information in a service. Then, in the service, use RoleEnvironment to determine which settings to use. For example
public static class Config
{
public static string ConnStr
{
get
{
if (RoleEnvironment.IsAvailable)
return RoleEnvironment.GetConfigurationSettingValue("ConnStr");
return ConfigurationManager.AppSettings["ConnStr"];
}
}
}
If that doesn't work, and you need to change the actual web.config (for instance, using named connection strings), then you'll need to modify the config at runtime. In your role start, do something like the following:
var config = WebConfigurationManager.OpenWebConfiguration(null);
var connStrs = WebConfigurationManager.OpenWebConfiguration(null).GetSection("connectionStrings") as ConnectionStringsSection;
connStrs.ConnectionStrings["ConnStr"].ConnectionString = RoleEnvironment.GetConfigurationSettingValue("ConnStr");
config.Save();
To handle when the configuration changes after the role is running, just call the same code as above from the RoleEnvironment.Changing event.
Good luck,
Erick