THE GOAL
I'm trying to have a class library shared between an ASP.NET Core web app and other projects/solutions and the class should be able to interact with whatever database is being used by the calling process/environment.
THE PROBLEM
I have a class library that is throwing up a weird error when used in my ASP.NET Core 3.1 web app. The class library is actually shared between the front end (the website) and the backend app that takes care of some recurring, heavy load processes. I'm using EF Core with both front and back ends and the database is on Azure, not my local machine. Yet, when the web app tries to do some work I am getting the following error:
An attempt to attach an auto-named database for file C:...\bin\Debug\netcoreapp3.1\aspnetdb.mdf failed. A database with the same name exists, or specified file cannot be opened, or it is located on UNC share.
This doesn't make sense to me at all since the DB is on Azure. Also, calling the same exact method in the library using the backend app doesn't throw up this error. The connection string is stored in appsettings.json for the website and app.config for the backend.
This is block of code that is throwing the error, but again this is only happening on the ASP.NET Core project on the SaveChanges() call:
public static void AddLogEvent(int Severity, DateTime EventTime, string EventType, string User, string Message)
{
DBEntities context = new DBEntities();
DbSet<LogEvent> dbSet = context.Set<LogEvent>();
LogEvent NewRecord = new LogEvent();
NewRecord.Id = Guid.NewGuid();
NewRecord.Severity = Severity;
NewRecord.EventTime = EventTime;
NewRecord.EventType = EventType;
NewRecord.User = User;
NewRecord.Message = Message;
dbSet.Add(NewRecord);
context.SaveChanges();
}
Within DBEntities, I am overriding the OnConfiguring() method to ensure proper connection for whatever environment is making the call as such:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
bool FoundValidConnection = false;
if (ConfigurationManager.ConnectionStrings.Count > 0)
{
foreach (ConnectionStringSettings connstr in ConfigurationManager.ConnectionStrings)
{
if (FoundValidConnection == false)
{
if (string.Compare(connstr.Name.Trim().ToUpper(), "DefaultConnection".ToUpper()) == 0)
{
optionsBuilder.UseSqlServer(connstr.ConnectionString);
FoundValidConnection = true;
break;
}
}
}
foreach (ConnectionStringSettings connstr in ConfigurationManager.ConnectionStrings)
{
if (FoundValidConnection == false)
{
if (string.Compare(connstr.Name.Trim().ToUpper(), "DBEntities".ToUpper()) == 0)
{
optionsBuilder.UseSqlServer(connstr.ConnectionString);
FoundValidConnection = true;
break;
}
}
}
if (FoundValidConnection == false)
{
//if still haven't found one of the expected connection string names, then take whatever the first one is.
optionsBuilder.UseSqlServer(ConfigurationManager.ConnectionStrings[0].ConnectionString);
FoundValidConnection = true;
}
}
else
{
//nothing to do. there are no connection strings in the ConfigurationManager
}
}
}
Lastly, when I step through debugging on the website, I can see that the only connection string located in ConfigurationManager.ConnectionStrings is 1 with a name of LocalSqlServer and a connection string set to:
data source=.\SQLEXPRESS;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|aspnetdb.mdf;User Instance=true
Which seems to be my local testing SQLExpress instance but is not used anywhere in the web app. All references or connection strings to the local testing database in SQLExpress have been removed so I am confused as to how this is showing up and the one in appsettings.json is being ignored. I also don't understand how optionsBuilder.IsConfigured is returning FALSE on the web app. I expected that context to already be configured.
I ended up changing the last if statement to the following:
if (FoundValidConnection == false)
{
try
{
//read from appsettings.json directly instead
string connString = new ConfigurationBuilder().SetBasePath(System.IO.Directory.GetCurrentDirectory()).AddJsonFile("appsettings.json").Build().GetSection("ConnectionStrings").GetSection("DefaultConnection").Value;
optionsBuilder.UseSqlServer(connString);
FoundValidConnection = true;
}
catch (System.Exception ex)
{
}
}
As Ivan noted in the comments above, ConfigurationManager from System.Configuration only works with app.config, which is XML based. ASP.NET Core utilizes appsettings.json which is, obviously, JSON based. So the solution was to modify the last check to be such that if the app.config checks fail and produce nothing then we assume the code is being called by something using appsettings.json and that one line uses ConfigurationBuilder to get the app's directory, build the configuration based on that file and get the expected connection by it's section and name.
References to Microsoft.Extensions.Configuration.FileExtensions and Microsoft.Extensions.Configuration.Json were required for SetBasePath() and AddJsonFile(), respectively.
The try/catch block is needed in the event there is some unforeseen error and the appsettings/json doesn't contain the expected section/name. Although, an error would eventually get thrown up somewhere when trying to interact with the database if nothing was set.
Related
I have tried various ways to get my application to work and have tried searching here for a solution to the various messages that I received but none have worked. So, I decided to start from scratch and ask for the proper way to connect my 2 applications.
I have a Solution with 2 projects inside it:
Application
Application is a stand alone app that has login/logout, user management
capabilities etc.
Application is connected to a database
ApplicationApi
ApplicationApi is a console application that references Application
ApplicationApi's main task is to receive any incoming requests via its
ApiController and then pass that to Application who will then return a result
to it
ApplicationApi is not connected to a database
This is the function in ApplicationApi that I want to call a function in Application
namespace ApplicationApi.Controllers
{
public class TokenController:ApiController
{
public String Get()
{
var auth = new AuthenticationController();
return auth.AuthenticateUser("user#one.com", "P#ssw0rd").ToString();
}
}
}
This is the function that is called by ApplicationApi
namespace Application.Controllers.API
{
public class AuthenticationController : Controller
{
ApplicationDbContext dbContext = new ApplicationDbContext();
Logger log = LogManager.GetCurrentClassLogger();
PasswordHasher passwordHasher = new PasswordHasher();
public bool AuthenticateUser(String username, String password)
{
try
{
var user = dbContext.Users.FirstOrDefault(u => u.UserName == username);
if (user == null)
{
log.Error(username + " not found");
return false;
}
else
{
var result = passwordHasher.VerifyHashedPassword(user.PasswordHash, password);
if (result == PasswordVerificationResult.Success)
{
return true;
}
else
{
log.Error("Invalid password for user: " + username);
return false;
}
}
}
catch (Exception e)
{
log.Error(e, "Exception found for user: " + username);
return false;
}
}
}
}
At the moment, when I make a request from Postman, I get this error from Application when debug is on:
{"No Entity Framework provider found for the ADO.NET provider with invariant name 'System.Data.SqlClient'. Make sure the provider is registered in the 'entityFramework' section of the application config file. See http://go.microsoft.com/fwlink/?LinkId=260882 for more information."}
This looks like its saying that my ApplicationApi needs to have EntityFramework or needs to refer to it to work?
That is where I get confused. ApplicationApi is only supposed to ask Application for a result. Why does this error happen? ApplicationApi does not have any database connections. Nor does it need to to. Its only a transport layer.
Maybe I've started wrong so I hope someone can guide me on how to actually do this.
EDIT: My question is not a duplicate of that one because I'm asking why I need EntityFramework in an Api Project that won't connect to a database?
For each site in a single port, I need to connect to different server hosts.
While connecting for the first time, no issues. But while trying to register (RegisterDestinationConfiguration) with another connection pool name, it throws an exception. Destination configuration is already initialized for ConnectionPoolName1 so I can't register for ConnectionPoolName2.
function() {
if (SAPDestination == null) {
SAPDestination = SAPConnection(ApplicationSite);
RfcSessionManager.BeginContext(SAPDestination);
}
rfcTravelfunc = SAPDestination.Repository.CreateFunction("FunctionName");
}
private RfcDestination SAPConnection(SPSite ApplicationSite) {
RfcDestination SAPConnect = null;
try {
DestinationConfig objConfig = new DestinationConfig();
SAPConnect = objConfig.TryGetDestination(ConnectionPoolName); // If connection doesnt exist with this connection pool name returns null
if (SAPConnect == null) {
DestinationConfig configObj = new DestinationConfig();
DestinationConfig.ApplicationSite = ApplicationSite;
RfcDestinationManager.RegisterDestinationConfiguration(configObj); // Throws exception when trying to register for new connection pool name
SAPConnect = RfcDestinationManager.GetDestination(ConnectionPoolName);
}
}
catch(Exception ex) {
}
return SAPConnect;
}
RfcDestinationManager.RegisterDestinationConfiguration() is a global static method. You can only register once. It should be set in a static context (like a static class constructor) or you can use RfcDestinationManager.IsDestinationConfigurationRegistered() to check if already registered. The exception is thrown to prevent incorrect usage.
The registration object needs to implement SAP.Middleware.Connector.IDestinationConfiguration.
It has a method RfcConfigParameters GetParameters(string destinationName); This should return connection parameters for the requested destination.
You should use this way only if you need to look up the connection parameters from an external store. The simpler method is to store the connection parameters in the app.config/web.config (can store multiple) and not use RfcDestinationManager.RegisterDestinationConfiguration().
Taken from the SAP NCo Tutorial Code StepByStepClient.cs
The .Net Connector 3.0 introduces a new destination-oriented concept. Applications work with destination instances, which are configured per default in the application configuration file (app.config) or which can alternatively be defined by explicitly registering an IDestinationConfiguration object. A destination identifies the backend to which connections can be opened.
With Advanced Installer, I'm trying to make a Custom Action, that at installationtime, encrypt the Connection String.
I seems like I can't use "~" here. (I moved my working code from the MVC project, to here).
Is there a simple alternative to that line or am I forced to make a complete rewrite and use e.g. a solution that uses somekind of Stream (like this Modifying Web.Config During Installation
Exception thrown by custom action:
System.Reflection.TargetInvocationException: Exception has been thrown by the
target of an invocation. ---> System.ArgumentException:
The application relative virtual path '~' is not allowed here.
Custom Action:
[CustomAction]
public static ActionResult EncryptConnStr(Session session)
{
try
{
var config = WebConfigurationManager.OpenWebConfiguration("~");
var section = (ConnectionStringsSection)config.GetSection("connectionStrings");
var cms = section.ConnectionStrings[GetConnectionStringName()];
var connStr = BuildConnStr(session["CONN_STR_SERVER"], session["CONN_STR_DATABASE"], session["CONN_STR_USERNAME"], session["CONN_STR_PASSWORD"]);
if (cms == null)
{
// Add new Connection String
section.ConnectionStrings.Add(new ConnectionStringSettings(GetConnectionStringName(), connStr));
}
else
{
// Update existing Connection String
cms.ConnectionString = connStr;
}
// Encrypt
section.SectionInformation.ProtectSection(ConnStrEncryptionKey);
// Save the configuration file.
config.Save(ConfigurationSaveMode.Modified);
return ActionResult.Success;
}
catch (Exception ex)
{
MessageBox.Show(ex.StackTrace, ex.Message);
throw;
}
}
The solution to the path issue, is to use ConfigurationManager a long with some mapping, like this, instead of the web version WebConfigurationManager.
var map = new ExeConfigurationFileMap { ExeConfigFilename = path };
Configuration config = ConfigurationManager.OpenMappedExeConfiguration(map, ConfigurationUserLevel.None);
The encryption works fine as the code is, but the issue with save is still not solved because the execution time is to early. The installation isn't finished and the web.config isn't yet copyed to the APPDIR.
At my company we use the IBM DB2 for i5/OS .NET provider dll version 12.0.7.3. in a web application that makes database calls to the iSeries. We encountered a situation where a method which would create a connection then pass that connection to subroutines would seem to access the wrong data sometimes. An example case is as follows: Assume the server has just spun up. Client A calls the "GetContractPdf" and it succeeds. Client B then calls the same method and it fails. The server is restarted and Client B calls the "GetContractPdf" method which then succeeds. Client A calls it and it fails.
I pulled my hair out trying to figure this out but it appears that the code isn't disposing of or handling the database connections properly. It is reproducible in my development environment and when I do reproduce it the connection object shows the right library list for the client that I used to call it, but after inserting a test query that would yield data that could easily identify the client database being touched it was absolutely getting the wrong data. The databases in question are running on the same physical AS/400 machine with different library lists. A third client who has their own AS/400 is unaffected by this problem.
I have been changing any method that accepts a connection as an argument to create their own connection and hoped that it won't cause too big of a performance hit.
Here is the method that generates the connections:
public static iDB2Connection GetConnection(string ClientCode)
{
string decrypted = "";
try
{
decrypted = System.Configuration.ConfigurationManager.ConnectionStrings[ClientCode].ConnectionString;
}
catch (Exception)
{
throw new FaultException(string.Format("Client connection string not found for {0}.", ClientCode));
}
try
{
decrypted = EncryptDecrypt.Decrypt(decrypted);
}
catch (Exception)
{
throw new FaultException(string.Format("Error determining connection string for client {0}.", ClientCode));
}
try
{
return new iDB2Connection(decrypted);
}
catch (Exception)
{
throw new FaultException("Error accessing database.");
}
}
And here is some pseudocode that shows how it is used when the problem behavior arises:
public static ContractObject GetContract(clientCode,contractId){
using(iDb2Connection conn = GetConnection(clientCode)){
ContractObject contractObject = new ContractObject(){
Id = contractId
};
GetSomeData(contractObject,conn);
GetOtherData(contractObject,conn);
return contractObject;
}
}
public static void GetSomeData(ContractObject contract, iDb2Connection connection){
string commandText = "Select data from table where conditions";
using(iDb2Command cmd = new iDb2Command(commandText,connection)){
using(iDb2DataReader reader = cmd.ExecuteReader()){
if(reader.HasRows){
contract.Foo = reader["ColumnName"].ToString();
}
}
}
}
public static void GetOtherData(ContractObject contract, iDb2Connection connection){
string commandText = "Select data from table where conditions";
using(iDb2Command cmd = new iDb2Command(commandText,connection)){
using(iDb2DataReader reader = cmd.ExecuteReader()){
if(reader.HasRows){
contract.Bar = reader["ColumnName"].ToString();
}
}
}
}
If I change the methods up so that a new connection object is created for every query the behavior is eliminated.
As I see it there are two possible problems, either I have done something horribly wrong (which I certainly could have), or the IBM dll does not handle passing around connection objects well (which you would think is less likely but could happen). I'm sure there are other possibilities that I'm too inexperienced / code blind to see.
Do any of you have any ideas of what could be causing this behavior or questions that could lead us to figuring out what causes this behavior?
Thanks in advance for your time.
I have a installer class which is taking a vale from the user at the time of installation.Now as per my requirement if the value provided is not correct i have to cancel the installation but i am not getting how to get this ..
Here is my installer.cs file..
public override void Install(System.Collections.IDictionary stateSaver)
{
base.Install(stateSaver);
// Retrieve configuration settings
string targetSite = Context.Parameters["targetsite"];
string targetVDir = Context.Parameters["targetvdir"];
string targetDirectory = Context.Parameters["targetdir"];
string value = Context.Parameters["value"];
string connectionstring = Context.Parameters["db"];
if (targetSite == null)
throw new InstallException("IIS Site Name Not Specified!");
if (targetSite.StartsWith("/LM/"))
targetSite = targetSite.Substring(4);
if (connectionstring == null)
throw new InstallException("You did not specify a database to use!");
if (value.Equals("123"))
{
RegisterScriptMaps(targetSite, targetVDir);
ConfigureDatabase(targetSite, targetVDir, connectionstring);
}
else {
Rollback(stateSaver);
}
}
Inspite of Rollback(stateSaver); my setup gets installed..
Please help me..
Just calling InstallException will do the rollback. That's the usual way and it works, so if it doesn't then something else is going on.
You should think about using another tool that lets you validate the data when it gets entered. VS custom actions always run after all the files have been installed, so basically you're installing the entire app and then rolling it back rather than using an MSI build tool that lets you validate when it's entered.