diff --git a/src/Umbraco.Core/DatabaseContext.cs b/src/Umbraco.Core/DatabaseContext.cs index 585f1d51cf..f8f31ee695 100644 --- a/src/Umbraco.Core/DatabaseContext.cs +++ b/src/Umbraco.Core/DatabaseContext.cs @@ -1,10 +1,10 @@ using System; +using System.Collections.Generic; using System.Configuration; using System.Data.SqlServerCe; using System.IO; using System.Linq; using System.Web; -using System.Web.Configuration; using System.Xml.Linq; using Semver; using Umbraco.Core.Configuration; @@ -30,7 +30,6 @@ namespace Umbraco.Core private readonly ILogger _logger; private readonly SqlSyntaxProviders _syntaxProviders; private bool _configured; - private readonly object _locker = new object(); private string _connectionString; private string _providerName; private DatabaseSchemaResult _result; @@ -40,7 +39,7 @@ namespace Umbraco.Core : this(factory, LoggerResolver.Current.Logger, new SqlSyntaxProviders(new ISqlSyntaxProvider[] { new MySqlSyntaxProvider(LoggerResolver.Current.Logger), - new SqlCeSyntaxProvider(), + new SqlCeSyntaxProvider(), new SqlServerSyntaxProvider() })) { @@ -80,21 +79,55 @@ namespace Umbraco.Core _configured = true; } +#if DEBUG_DATABASES + public List Databases + { + get + { + var factory = _factory as DefaultDatabaseFactory; + if (factory == null) throw new NotSupportedException(); + return factory.Databases; + } + } +#endif + public ISqlSyntaxProvider SqlSyntax { get; private set; } /// - /// Gets the object for doing CRUD operations - /// against custom tables that resides in the Umbraco database. + /// Gets the object for doing CRUD operations against custom tables that resides in the Umbraco database. /// /// - /// This should not be used for CRUD operations or queries against the - /// standard Umbraco tables! Use the Public services for that. + /// Should not be used for operation against standard Umbraco tables; as services should be used instead. + /// Gets or creates an "ambient" database that is either stored in http context + available for the whole + /// request + auto-disposed at the end of the request, or in call context if there is no http context - in which + /// case it needs to be explicitely disposed so it is removed from call context. /// public virtual UmbracoDatabase Database { get { return _factory.CreateDatabase(); } } + /// + /// Replaces the "ambient" database (if any) and installs a new object. + /// + /// + /// The returned IDisposable *must* be diposed in order to properly disposed the safe database and + /// restore the original "ambient" database (if any). + /// This is to be used in background tasks to ensure that they have an "ambient" database which + /// will be properly removed from call context and does not interfere with anything else. In most case + /// it is not replacing anything, just temporarily installing a database in context. + /// + public virtual IDisposable UsingSafeDatabase + { + get + { + var factory = _factory as DefaultDatabaseFactory; + if (factory == null) throw new NotSupportedException(); + var database = factory.CreateDatabaseInstance(DefaultDatabaseFactory.ContextOwner.None); + return new UsedDatabase(database, _logger); + } + } + /// /// Boolean indicating whether the database has been configured /// @@ -189,7 +222,7 @@ namespace Umbraco.Core // since it's been like this for quite some time //using (var engine = new SqlCeEngine(connectionString)) //{ - // engine.CreateDatabase(); + // engine.CreateDatabase(); //} } @@ -230,14 +263,14 @@ namespace Umbraco.Core /// Database Password /// Type of the provider to be used (Sql, Sql Azure, Sql Ce, MySql) public void ConfigureDatabaseConnection(string server, string databaseName, string user, string password, string databaseProvider) - { + { string providerName; var connectionString = GetDatabaseConnectionString(server, databaseName, user, password, databaseProvider, out providerName); SaveConnectionString(connectionString, providerName); Initialize(providerName); } - + public string GetDatabaseConnectionString(string server, string databaseName, string user, string password, string databaseProvider, out string providerName) { providerName = Constants.DatabaseProviders.SqlServer; @@ -268,18 +301,18 @@ namespace Umbraco.Core public string GetIntegratedSecurityDatabaseConnectionString(string server, string databaseName) { - return String.Format("Server={0};Database={1};Integrated Security=true", server, databaseName); + return String.Format("Server={0};Database={1};Integrated Security=true", server, databaseName); } internal string BuildAzureConnectionString(string server, string databaseName, string user, string password) { if (server.Contains(".") && ServerStartsWithTcp(server) == false) server = string.Format("tcp:{0}", server); - + if (server.Contains(".") == false && ServerStartsWithTcp(server)) { - string serverName = server.Contains(",") - ? server.Substring(0, server.IndexOf(",", StringComparison.Ordinal)) + string serverName = server.Contains(",") + ? server.Substring(0, server.IndexOf(",", StringComparison.Ordinal)) : server; var portAddition = string.Empty; @@ -289,10 +322,10 @@ namespace Umbraco.Core server = string.Format("{0}.database.windows.net{1}", serverName, portAddition); } - + if (ServerStartsWithTcp(server) == false) server = string.Format("tcp:{0}.database.windows.net", server); - + if (server.Contains(",") == false) server = string.Format("{0},1433", server); @@ -336,7 +369,7 @@ namespace Umbraco.Core _connectionString = connectionString; _providerName = providerName; - + var fileName = IOHelper.MapPath(string.Format("{0}/web.config", SystemDirectories.Root)); var xml = XDocument.Load(fileName, LoadOptions.PreserveWhitespace); var connectionstrings = xml.Root.DescendantsAndSelf("connectionStrings").Single(); @@ -381,7 +414,7 @@ namespace Umbraco.Core connString = ConfigurationManager.ConnectionStrings[GlobalSettings.UmbracoConnectionName].ConnectionString; } Initialize(providerName, connString); - + } else if (ConfigurationManager.AppSettings.ContainsKey(GlobalSettings.UmbracoConnectionName) && string.IsNullOrEmpty(ConfigurationManager.AppSettings[GlobalSettings.UmbracoConnectionName]) == false) { @@ -418,7 +451,7 @@ namespace Umbraco.Core //Remove the legacy connection string, so we don't end up in a loop if something goes wrong. GlobalSettings.RemoveSetting(GlobalSettings.UmbracoConnectionName); - + } else { @@ -437,15 +470,15 @@ namespace Umbraco.Core { if (_syntaxProviders != null) { - SqlSyntax = _syntaxProviders.GetByProviderNameOrDefault(providerName); + SqlSyntax = _syntaxProviders.GetByProviderNameOrDefault(providerName); } else if (SqlSyntax == null) { throw new InvalidOperationException("No " + typeof(ISqlSyntaxProvider) + " specified or no " + typeof(SqlSyntaxProviders) + " instance specified"); } - + SqlSyntaxContext.SqlSyntaxProvider = SqlSyntax; - + _configured = true; } catch (Exception e) @@ -485,7 +518,7 @@ namespace Umbraco.Core } internal Result CreateDatabaseSchemaAndData(ApplicationContext applicationContext) - { + { try { var readyForInstall = CheckReadyForInstall(); @@ -503,7 +536,7 @@ namespace Umbraco.Core // If MySQL, we're going to ensure that database calls are maintaining proper casing as to remove the necessity for checks // for case insensitive queries. In an ideal situation (which is what we're striving for), all calls would be case sensitive. - /* + /* var supportsCaseInsensitiveQueries = SqlSyntax.SupportsCaseInsensitiveQueries(database); if (supportsCaseInsensitiveQueries == false) { @@ -521,9 +554,9 @@ namespace Umbraco.Core message = GetResultMessageForMySql(); var schemaResult = ValidateDatabaseSchema(); - + var installedSchemaVersion = schemaResult.DetermineInstalledVersion(); - + //If Configuration Status is empty and the determined version is "empty" its a new install - otherwise upgrade the existing if (string.IsNullOrEmpty(GlobalSettings.ConfigurationStatus) && installedSchemaVersion.Equals(new Version(0, 0, 0))) { @@ -542,9 +575,9 @@ namespace Umbraco.Core message = "

Upgrading database, this may take some time...

"; return new Result { - RequiresUpgrade = true, - Message = message, - Success = true, + RequiresUpgrade = true, + Message = message, + Success = true, Percentage = "30" }; } @@ -572,7 +605,7 @@ namespace Umbraco.Core _logger.Info("Database upgrade started"); var database = new UmbracoDatabase(_connectionString, ProviderName, _logger); - //var supportsCaseInsensitiveQueries = SqlSyntax.SupportsCaseInsensitiveQueries(database); + //var supportsCaseInsensitiveQueries = SqlSyntax.SupportsCaseInsensitiveQueries(database); var message = GetResultMessageForMySql(); @@ -580,32 +613,32 @@ namespace Umbraco.Core var installedSchemaVersion = new SemVersion(schemaResult.DetermineInstalledVersion()); - var installedMigrationVersion = schemaResult.DetermineInstalledVersionByMigrations(migrationEntryService); - + var installedMigrationVersion = schemaResult.DetermineInstalledVersionByMigrations(migrationEntryService); + var targetVersion = UmbracoVersion.Current; - + //In some cases - like upgrading from 7.2.6 -> 7.3, there will be no migration information in the database and therefore it will - // return a version of 0.0.0 and we don't necessarily want to run all migrations from 0 -> 7.3, so we'll just ensure that the + // return a version of 0.0.0 and we don't necessarily want to run all migrations from 0 -> 7.3, so we'll just ensure that the // migrations are run for the target version if (installedMigrationVersion == new SemVersion(new Version(0, 0, 0)) && installedSchemaVersion > new SemVersion(new Version(0, 0, 0))) { //set the installedMigrationVersion to be one less than the target so the latest migrations are guaranteed to execute installedMigrationVersion = new SemVersion(targetVersion.SubtractRevision()); } - + //Figure out what our current installed version is. If the web.config doesn't have a version listed, then we'll use the minimum - // version detected between the schema installed and the migrations listed in the migration table. + // version detected between the schema installed and the migrations listed in the migration table. // If there is a version in the web.config, we'll take the minimum between the listed migration in the db and what // is declared in the web.config. - + var currentInstalledVersion = string.IsNullOrEmpty(GlobalSettings.ConfigurationStatus) //Take the minimum version between the detected schema version and the installed migration version ? new[] {installedSchemaVersion, installedMigrationVersion}.Min() //Take the minimum version between the installed migration version and the version specified in the config : new[] { SemVersion.Parse(GlobalSettings.ConfigurationStatus), installedMigrationVersion }.Min(); - //Ok, another edge case here. If the current version is a pre-release, - // then we want to ensure all migrations for the current release are executed. + //Ok, another edge case here. If the current version is a pre-release, + // then we want to ensure all migrations for the current release are executed. if (currentInstalledVersion.Prerelease.IsNullOrWhiteSpace() == false) { currentInstalledVersion = new SemVersion(currentInstalledVersion.GetVersion().SubtractRevision()); @@ -740,7 +773,7 @@ namespace Umbraco.Core { var datasource = dataSourcePart.Replace("|DataDirectory|", AppDomain.CurrentDomain.GetData("DataDirectory").ToString()); var filePath = datasource.Replace("Data Source=", string.Empty); - sqlCeDatabaseExists = File.Exists(filePath); + sqlCeDatabaseExists = File.Exists(filePath); } } @@ -754,5 +787,32 @@ namespace Umbraco.Core return true; } + + private class UsedDatabase : IDisposable + { + private readonly UmbracoDatabase _database; + private readonly ILogger _logger; + + public UsedDatabase(UmbracoDatabase database, ILogger logger) + { + _logger = logger; + + // replace with ours + _database = DefaultDatabaseFactory.DetachDatabase(); + _logger.Debug("Detached db " + (_database == null ? "x" : _database.InstanceSid) + "."); + DefaultDatabaseFactory.AttachDatabase(database); + _logger.Debug("Attached db " + database.InstanceSid + "."); + } + + public void Dispose() + { + // restore the original DB + var db = DefaultDatabaseFactory.DetachDatabase(); + _logger.Debug("Detached db " + db.InstanceSid + "."); + db.Dispose(); + DefaultDatabaseFactory.AttachDatabase(_database); + _logger.Debug("Attached db " + (_database == null ? "x" : _database.InstanceSid) + "."); + } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/DefaultDatabaseFactory.cs b/src/Umbraco.Core/Persistence/DefaultDatabaseFactory.cs index 4ad425d9c9..5139da20d8 100644 --- a/src/Umbraco.Core/Persistence/DefaultDatabaseFactory.cs +++ b/src/Umbraco.Core/Persistence/DefaultDatabaseFactory.cs @@ -75,69 +75,168 @@ namespace Umbraco.Core.Persistence public UmbracoDatabase CreateDatabase() { - // no http context, create the call context object - // NOTHING is going to track the object and it is the responsibility of the caller to release it! - // using the ReleaseDatabase method. + UmbracoDatabase database; + + // gets or creates a database, using either the call context (if no http context) or + // the current request context (http context) to store it. once done using the database, + // it should be disposed - which will remove it from whatever context it is currently + // stored in. this is automatic with http context because UmbracoDatabase implements + // IDisposeOnRequestEnd, but NOT with call context. + if (HttpContext.Current == null) { - LogHelper.Debug("Get NON http [T" + Environment.CurrentManagedThreadId + "]"); - var value = NonContextValue; - if (value != null) return value; - lock (Locker) + database = NonContextValue; + if (database == null) { - value = NonContextValue; - if (value != null) return value; - - LogHelper.Debug("Create NON http [T" + Environment.CurrentManagedThreadId + "]"); - NonContextValue = value = string.IsNullOrEmpty(ConnectionString) == false && string.IsNullOrEmpty(ProviderName) == false - ? new UmbracoDatabase(ConnectionString, ProviderName, _logger) - : new UmbracoDatabase(_connectionStringName, _logger); - - return value; + lock (Locker) + { + database = NonContextValue; + if (database == null) + { + database = CreateDatabaseInstance(ContextOwner.CallContext); + NonContextValue = database; + } +#if DEBUG_DATABASES + else + { + Log("Get lcc", database); + } +#endif + } } +#if DEBUG_DATABASES + else + { + Log("Get lcc", database); + } +#endif + return database; } - // we have an http context, so only create one per request. - // UmbracoDatabase is marked IDisposeOnRequestEnd and therefore will be disposed when - // UmbracoModule attempts to dispose the relevant HttpContext items. so we DO dispose - // connections at the end of each request. no need to call ReleaseDatabase. - LogHelper.Debug("Get http [T" + Environment.CurrentManagedThreadId + "]"); - if (HttpContext.Current.Items.Contains(typeof(DefaultDatabaseFactory)) == false) - { - LogHelper.Debug("Create http [T" + Environment.CurrentManagedThreadId + "]"); - HttpContext.Current.Items.Add(typeof(DefaultDatabaseFactory), - string.IsNullOrEmpty(ConnectionString) == false && string.IsNullOrEmpty(ProviderName) == false - ? new UmbracoDatabase(ConnectionString, ProviderName, _logger) - : new UmbracoDatabase(_connectionStringName, _logger)); + if (HttpContext.Current.Items.Contains(typeof (DefaultDatabaseFactory)) == false) + { + database = CreateDatabaseInstance(ContextOwner.HttpContext); + HttpContext.Current.Items.Add(typeof (DefaultDatabaseFactory), database); + } + else + { + database = (UmbracoDatabase) HttpContext.Current.Items[typeof(DefaultDatabaseFactory)]; +#if DEBUG_DATABASES + Log("Get ctx", database); +#endif } - return (UmbracoDatabase)HttpContext.Current.Items[typeof(DefaultDatabaseFactory)]; + + return database; + } + + // called by UmbracoDatabase when disposed, so that the factory can de-list it from context + internal void OnDispose(UmbracoDatabase disposing) + { + var value = disposing; + switch (disposing.ContextOwner) + { + case ContextOwner.CallContext: + value = NonContextValue; + break; + case ContextOwner.HttpContext: + value = (UmbracoDatabase) HttpContext.Current.Items[typeof (DefaultDatabaseFactory)]; + break; + } + + if (value != null && value.InstanceId != disposing.InstanceId) throw new Exception("panic: wrong db."); + + switch (disposing.ContextOwner) + { + case ContextOwner.CallContext: + NonContextValue = null; + break; + case ContextOwner.HttpContext: + HttpContext.Current.Items.Remove(typeof(DefaultDatabaseFactory)); + break; + } + + disposing.ContextOwner = ContextOwner.None; + +#if DEBUG_DATABASES + _databases.Remove(value); +#endif } - // releases the "context" database - public void ReleaseDatabase() +#if DEBUG_DATABASES + // helps identifying when non-httpContext databases are created by logging the stack trace + private void LogCallContextStack() { - if (HttpContext.Current == null) - { - var value = NonContextValue; - if (value != null) value.Dispose(); - NonContextValue = null; - } + var trace = Environment.StackTrace; + if (trace.IndexOf("ScheduledPublishing") > 0) + LogHelper.Debug("CallContext: Scheduled Publishing"); + else if (trace.IndexOf("TouchServerTask") > 0) + LogHelper.Debug("CallContext: Server Registration"); + else if (trace.IndexOf("LogScrubber") > 0) + LogHelper.Debug("CallContext: Log Scrubber"); else - { - var db = (UmbracoDatabase)HttpContext.Current.Items[typeof(DefaultDatabaseFactory)]; - if (db != null) db.Dispose(); - HttpContext.Current.Items.Remove(typeof(DefaultDatabaseFactory)); - } + LogHelper.Debug("CallContext: " + Environment.StackTrace); } + private readonly List _databases = new List(); + + // helps identifying database leaks by keeping track of all instances + public List Databases { get { return _databases; } } + + private static void Log(string message, UmbracoDatabase database) + { + LogHelper.Debug(message + " (" + (database == null ? "" : database.InstanceSid) + ")."); + } +#endif + + internal enum ContextOwner + { + None, + HttpContext, + CallContext + } + + internal UmbracoDatabase CreateDatabaseInstance(ContextOwner contextOwner) + { + var database = string.IsNullOrEmpty(ConnectionString) == false && string.IsNullOrEmpty(ProviderName) == false + ? new UmbracoDatabase(ConnectionString, ProviderName, _logger) + : new UmbracoDatabase(_connectionStringName, _logger); + database.ContextOwner = contextOwner; + database.DatabaseFactory = this; + //database.EnableSqlTrace = true; +#if DEBUG_DATABASES + Log("Create " + contextOwner, database); + if (contextOwner == ContextOwner.CallContext) + LogCallContextStack(); + _databases.Add(database); +#endif + return database; + } + protected override void DisposeResources() { - ReleaseDatabase(); - } + UmbracoDatabase database; + + if (HttpContext.Current == null) + { + database = NonContextValue; +#if DEBUG_DATABASES + Log("Release lcc", database); +#endif + } + else + { + database = (UmbracoDatabase) HttpContext.Current.Items[typeof (DefaultDatabaseFactory)]; +#if DEBUG_DATABASES + Log("Release ctx", database); +#endif + } + + if (database != null) database.Dispose(); // removes it from call context + } // during tests, the thread static var can leak between tests // this method provides a way to force-reset the variable - internal void ResetForTests() + internal void ResetForTests() { var value = NonContextValue; if (value != null) value.Dispose(); @@ -156,26 +255,29 @@ namespace Umbraco.Core.Persistence // detaches the current database // ie returns the database and remove it from whatever is "context" - private static UmbracoDatabase DetachDatabase() + internal static UmbracoDatabase DetachDatabase() { + UmbracoDatabase database; + if (HttpContext.Current == null) { - var db = NonContextValue; + database = NonContextValue; NonContextValue = null; - return db; } else { - var db = (UmbracoDatabase)HttpContext.Current.Items[typeof(DefaultDatabaseFactory)]; - HttpContext.Current.Items.Remove(typeof(DefaultDatabaseFactory)); - return db; + database = (UmbracoDatabase) HttpContext.Current.Items[typeof (DefaultDatabaseFactory)]; + HttpContext.Current.Items.Remove(typeof (DefaultDatabaseFactory)); } + + if (database != null) database.ContextOwner = ContextOwner.None; + return database; } // attach a current database // ie assign it to whatever is "context" // throws if there already is a database - private static void AttachDatabase(object o) + internal static void AttachDatabase(object o) { var database = o as UmbracoDatabase; if (o != null && database == null) throw new ArgumentException("Not an UmbracoDatabase.", "o"); @@ -183,12 +285,18 @@ namespace Umbraco.Core.Persistence if (HttpContext.Current == null) { if (NonContextValue != null) throw new InvalidOperationException(); - if (database != null) NonContextValue = database; + if (database == null) return; + + NonContextValue = database; + database.ContextOwner = ContextOwner.CallContext; } else { - if (HttpContext.Current.Items[typeof(DefaultDatabaseFactory)] != null) throw new InvalidOperationException(); - if (database != null) HttpContext.Current.Items[typeof(DefaultDatabaseFactory)] = database; + if (HttpContext.Current.Items[typeof (DefaultDatabaseFactory)] != null) throw new InvalidOperationException(); + if (database == null) return; + + HttpContext.Current.Items[typeof (DefaultDatabaseFactory)] = database; + database.ContextOwner = ContextOwner.HttpContext; } } diff --git a/src/Umbraco.Core/Persistence/UmbracoDatabase.cs b/src/Umbraco.Core/Persistence/UmbracoDatabase.cs index 7ff9cabe84..62cbfa78c4 100644 --- a/src/Umbraco.Core/Persistence/UmbracoDatabase.cs +++ b/src/Umbraco.Core/Persistence/UmbracoDatabase.cs @@ -1,7 +1,9 @@ using System; using System.Data; using System.Data.Common; +using System.Data.SqlClient; using System.Text; +using MySql.Data.MySqlClient; using StackExchange.Profiling; using Umbraco.Core.Logging; using Umbraco.Core.Persistence.SqlSyntax; @@ -21,6 +23,12 @@ namespace Umbraco.Core.Persistence private readonly ILogger _logger; private readonly Guid _instanceId = Guid.NewGuid(); private bool _enableCount; +#if DEBUG_DATABASES + private int _spid = -1; +#endif + + internal DefaultDatabaseFactory.ContextOwner ContextOwner = DefaultDatabaseFactory.ContextOwner.None; + internal DefaultDatabaseFactory DatabaseFactory = null; /// /// Used for testing @@ -30,6 +38,18 @@ namespace Umbraco.Core.Persistence get { return _instanceId; } } + public string InstanceSid + { + get + { +#if DEBUG_DATABASES + return _instanceId.ToString("N").Substring(0, 8) + ":" + _spid; +#else + return _instanceId.ToString("N").Substring(0, 8); +#endif + } + } + /// /// Generally used for testing, will output all SQL statements executed to the logger /// @@ -113,13 +133,44 @@ namespace Umbraco.Core.Persistence { // propagate timeout if none yet +#if DEBUG_DATABASES + if (DatabaseType == DBType.MySql) + { + using (var command = connection.CreateCommand()) + { + command.CommandText = "SELECT CONNECTION_ID()"; + _spid = Convert.ToInt32(command.ExecuteScalar()); + } + } + else if (DatabaseType == DBType.SqlServer) + { + using (var command = connection.CreateCommand()) + { + command.CommandText = "SELECT @@SPID"; + _spid = Convert.ToInt32(command.ExecuteScalar()); + } + } + else + { + // includes SqlCE + _spid = 0; + } +#endif + // wrap the connection with a profiling connection that tracks timings return new StackExchange.Profiling.Data.ProfiledDbConnection(connection as DbConnection, MiniProfiler.Current); } +#if DEBUG_DATABASES + public override void OnConnectionClosing(IDbConnection conn) + { + _spid = -1; + } +#endif + public override void OnException(Exception x) { - _logger.Error("Database exception occurred", x); + _logger.Error("Exception (" + InstanceSid + ").", x); base.OnException(x); } @@ -132,14 +183,18 @@ namespace Umbraco.Core.Persistence if (EnableSqlTrace) { var sb = new StringBuilder(); +#if DEBUG_DATABASES + sb.Append(InstanceSid); + sb.Append(": "); +#endif sb.Append(cmd.CommandText); foreach (DbParameter p in cmd.Parameters) { sb.Append(" - "); sb.Append(p.Value); } - - _logger.Debug(sb.ToString()); + + _logger.Debug(sb.ToString().Replace("{", "{{").Replace("}", "}}")); } base.OnExecutingCommand(cmd); @@ -185,9 +240,15 @@ namespace Umbraco.Core.Persistence } } } - + //use the defaults base.BuildSqlDbSpecificPagingQuery(databaseType, skip, take, sql, sqlSelectRemoved, sqlOrderBy, ref args, out sqlPage); } + + protected override void Dispose(bool disposing) + { + LogHelper.Debug("Dispose (" + InstanceSid + ")."); + if (DatabaseFactory != null) DatabaseFactory.OnDispose(this); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/UmbracoApplicationBase.cs b/src/Umbraco.Core/UmbracoApplicationBase.cs index e4574a7972..c6cdfb072a 100644 --- a/src/Umbraco.Core/UmbracoApplicationBase.cs +++ b/src/Umbraco.Core/UmbracoApplicationBase.cs @@ -16,7 +16,7 @@ namespace Umbraco.Core /// The abstract class for the Umbraco HttpApplication /// /// - /// This is exposed in the core so that we can have the IApplicationEventHandler in the core project so that + /// This is exposed in the core so that we can have the IApplicationEventHandler in the core project so that /// IApplicationEventHandler's can fire/execute outside of the web contenxt (i.e. in console applications) /// public abstract class UmbracoApplicationBase : System.Web.HttpApplication @@ -35,7 +35,7 @@ namespace Umbraco.Core /// internal void StartApplication(object sender, EventArgs e) { - //take care of unhandled exceptions - there is nothing we can do to + //take care of unhandled exceptions - there is nothing we can do to // prevent the entire w3wp process to go down but at least we can try // and log the exception AppDomain.CurrentDomain.UnhandledException += (_, args) => @@ -59,6 +59,11 @@ namespace Umbraco.Core //And now we can dispose of our startup handlers - save some memory ApplicationEventsResolver.Current.Dispose(); + + // after Umbraco has started there is a database in "context" and that context is + // going to stay there and never get destroyed nor reused, so we have to ensure that + // the database is disposed (which will auto-remove it from context). + ApplicationContext.Current.DatabaseContext.Database.Dispose(); } /// @@ -121,7 +126,7 @@ namespace Umbraco.Core throw; } } - + } /// @@ -188,7 +193,7 @@ namespace Umbraco.Core { return; } - + Logger.Error("An unhandled exception occurred", exc); OnApplicationError(sender, e);