Skip to content

Commit

Permalink
Merge pull request #349 from PHOENIXCONTACT/bugfix/db-controller-disp…
Browse files Browse the repository at this point in the history
…osed-object-exception

Fix `DisposedObjectException` in DatabaseController
  • Loading branch information
seveneleven authored Oct 27, 2023
2 parents 10a3c7c + cfaf02e commit 4787f7c
Show file tree
Hide file tree
Showing 12 changed files with 208 additions and 23 deletions.
13 changes: 10 additions & 3 deletions MORYX-Framework.sln
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moryx.Notifications.Tests",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moryx.Resources.Management.Tests", "src\Tests\Moryx.Resources.Management.Tests\Moryx.Resources.Management.Tests.csproj", "{FEB3BA44-2CD9-445A-ABF2-C92378C443F7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Moryx.Runtime.Endpoints.Tests", "src\Tests\Moryx.Runtime.Endpoints.Tests\Moryx.Runtime.Endpoints.Tests.csproj", "{7792C4E0-6D07-42C9-AC29-BAB76836FC11}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moryx.Runtime.Endpoints.Tests", "src\Tests\Moryx.Runtime.Endpoints.Tests\Moryx.Runtime.Endpoints.Tests.csproj", "{7792C4E0-6D07-42C9-AC29-BAB76836FC11}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Moryx.Runtime.Endpoints.IntegrationTests", "src\Tests\Moryx.Runtime.Endpoints.IntegrationTests\Moryx.Runtime.Endpoints.IntegrationTests.csproj", "{4FFB98A7-9A4C-476F-8BCC-C19B7F757BF8}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -290,6 +292,10 @@ Global
{7792C4E0-6D07-42C9-AC29-BAB76836FC11}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7792C4E0-6D07-42C9-AC29-BAB76836FC11}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7792C4E0-6D07-42C9-AC29-BAB76836FC11}.Release|Any CPU.Build.0 = Release|Any CPU
{4FFB98A7-9A4C-476F-8BCC-C19B7F757BF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4FFB98A7-9A4C-476F-8BCC-C19B7F757BF8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4FFB98A7-9A4C-476F-8BCC-C19B7F757BF8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4FFB98A7-9A4C-476F-8BCC-C19B7F757BF8}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -333,10 +339,11 @@ Global
{E84A977E-6C8C-4603-B5C5-3EA99C72DAFC} = {0A466330-6ED6-4861-9C94-31B1949CDDB9}
{FEB3BA44-2CD9-445A-ABF2-C92378C443F7} = {0A466330-6ED6-4861-9C94-31B1949CDDB9}
{7792C4E0-6D07-42C9-AC29-BAB76836FC11} = {0A466330-6ED6-4861-9C94-31B1949CDDB9}
{4FFB98A7-9A4C-476F-8BCC-C19B7F757BF8} = {8517D209-5BC1-47BD-A7C7-9CF9ADD9F5B6}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
RESX_ShowErrorsInErrorList = True
RESX_TaskErrorCategory = Message
SolutionGuid = {36EFC961-F4E7-49DC-A36A-99594FFB8243}
RESX_TaskErrorCategory = Message
RESX_ShowErrorsInErrorList = True
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Moryx.Model;
using Moryx.Model.Repositories;
using Moryx.Model.Sqlite;

Expand All @@ -15,11 +16,26 @@ public static class InMemoryUnitOfWorkFactoryBuilder
{

/// <summary>
/// Instanciate a `UnitOfWorkFactory` of type `T`
/// Instantiate a `UnitOfWorkFactory` of type `T`
/// </summary>
public static UnitOfWorkFactory<T> Sqlite<T>() where T : DbContext
{
return SqliteDbContextManager().Factory<T>();
}

/// <summary>
/// Instantiate a `UnitOfWorkFactory` of type `T`
/// </summary>
public static UnitOfWorkFactory<T> Factory<T>(this IDbContextManager dbContextManager) where T : DbContext
{
return new UnitOfWorkFactory<T>(dbContextManager);
}

/// <summary>
/// Instantiate an in memory `SqliteDbContextProvider`
/// </summary>
public static IDbContextManager SqliteDbContextManager()
{
// The in memory tests using SQLite need a permanently opened connection. Otherwise,
// opening a database connection would result in creating a new in memory database
var connectionStringBuilder = new SqliteConnectionStringBuilder
Expand All @@ -30,7 +46,7 @@ public static UnitOfWorkFactory<T> Sqlite<T>() where T : DbContext
var inMemoryDbConnection = new SqliteConnection(connectionStringBuilder.ConnectionString);
inMemoryDbConnection.Open();

return new UnitOfWorkFactory<T>(new SqliteDbContextManager(inMemoryDbConnection));
return new SqliteDbContextManager(inMemoryDbConnection);
}

/// <summary>
Expand All @@ -46,5 +62,6 @@ public static UnitOfWorkFactory<T> EnsureDbIsCreated<T>(this UnitOfWorkFactory<T
uow.DbContext.Database.EnsureCreated();
return factory;
}

}
}
1 change: 1 addition & 0 deletions src/Moryx.Model.Sqlite/Moryx.Model.Sqlite.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Moryx.Runtime.Kernel\Moryx.Runtime.Kernel.csproj" />
<ProjectReference Include="..\Moryx\Moryx.csproj" />
<ProjectReference Include="..\Moryx.Model\Moryx.Model.csproj" />
</ItemGroup>
Expand Down
27 changes: 23 additions & 4 deletions src/Moryx.Model.Sqlite/SqliteDbContextManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Moryx.Model.Configuration;
using Moryx.Runtime.Kernel;

namespace Moryx.Model.Sqlite
{
Expand All @@ -14,6 +17,7 @@ namespace Moryx.Model.Sqlite
/// </summary>
public sealed class SqliteDbContextManager : IDbContextManager
{
private Dictionary<Type, IModelConfigurator> _configurators;
private readonly string _connectionString;
private readonly SqliteConnection _sqliteConnection;

Expand All @@ -24,6 +28,7 @@ public sealed class SqliteDbContextManager : IDbContextManager
public SqliteDbContextManager(string connectionString)
{
_connectionString = connectionString;
_configurators = new Dictionary<Type, IModelConfigurator>();
}

/// <summary>
Expand All @@ -33,21 +38,23 @@ public SqliteDbContextManager(string connectionString)
public SqliteDbContextManager(SqliteConnection sqliteConnection)
{
_sqliteConnection = sqliteConnection;
_configurators = new Dictionary<Type, IModelConfigurator>();
}

/// <inheritdoc />
public IReadOnlyCollection<Type> Contexts => Array.Empty<Type>();
public IReadOnlyCollection<Type> Contexts => _configurators.Select(c => c.Key).ToArray();

/// <inheritdoc />
public IModelConfigurator GetConfigurator(Type contextType)
{
throw new NotImplementedException();
return _configurators[contextType];
}

/// <inheritdoc />
public IModelSetupExecutor GetSetupExecutor(Type contextType)
{
throw new NotImplementedException();
var setupExecutorType = typeof(ModelSetupExecutor<>).MakeGenericType(contextType);
return (IModelSetupExecutor)Activator.CreateInstance(setupExecutorType, this);
}

/// <inheritdoc />
Expand Down Expand Up @@ -76,7 +83,10 @@ public TContext Create<TContext>(IDatabaseConfig config) where TContext : DbCont
}

// Create instance of context
var context = (TContext)Activator.CreateInstance(typeof(TContext), options);
var configurator = new SqliteModelConfigurator();
configurator.Initialize(typeof(TContext), CreateConfigManager(), null);
var context = (TContext)configurator.CreateContext(typeof(TContext), options);
_configurators.TryAdd(context.GetType(), configurator);
return context;
}

Expand All @@ -85,5 +95,14 @@ public void UpdateConfig(Type dbContextType, Type configuratorType, IDatabaseCon
{
throw new NotImplementedException();
}

private static ConfigManager CreateConfigManager()
{
var configManager = new ConfigManager
{
ConfigDirectory = ""
};
return configManager;
}
}
}
6 changes: 3 additions & 3 deletions src/Moryx.Model/ModelSetupExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
namespace Moryx.Model
{
/// <inheritdoc />
internal class ModelSetupExecutor<TContext> : IModelSetupExecutor
public class ModelSetupExecutor<TContext> : IModelSetupExecutor
where TContext : DbContext
{
private readonly IDbContextManager _dbContextManager;
Expand All @@ -41,12 +41,12 @@ public ModelSetupExecutor(IDbContextManager dbContextManager)
public IReadOnlyList<IModelSetup> GetAllSetups() => _setups;

/// <inheritdoc />
public Task Execute(IDatabaseConfig config, IModelSetup setup, string setupData)
public async Task Execute(IDatabaseConfig config, IModelSetup setup, string setupData)
{
var unitOfWorkFactory = new UnitOfWorkFactory<TContext>(_dbContextManager);
using var uow = unitOfWorkFactory.Create(config);

return setup.Execute(uow, setupData);
await setup.Execute(uow, setupData);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ public async Task<ActionResult<DatabaseMigrationSummary>> MigrateDatabaseModel(s

[HttpPost("{targetModel}/setup")]
[Authorize(Policy = RuntimePermissions.DatabaseCanSetup)]
public ActionResult<InvocationResponse> ExecuteSetup(string targetModel, ExecuteSetupRequest request)
public async Task<ActionResult<InvocationResponse>> ExecuteSetup(string targetModel, ExecuteSetupRequest request)
{
var contextType = _dbContextManager.Contexts.First(c => TargetModelName(c) == targetModel);
var targetConfigurator = _dbContextManager.GetConfigurator(contextType);
Expand All @@ -250,7 +250,7 @@ public ActionResult<InvocationResponse> ExecuteSetup(string targetModel, Execute
// ReSharper disable once SuspiciousTypeConversion.Global
try
{
setupExecutor.Execute(config, targetSetup, request.Setup.SetupData);
await setupExecutor.Execute(config, targetSetup, request.Setup.SetupData);
return new InvocationResponse();
}
catch (Exception ex)
Expand Down
35 changes: 35 additions & 0 deletions src/Moryx.TestTools.Test.Model/Setups/DisposedObjectSetup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright (c) 2023, Phoenix Contact GmbH & Co. KG
// Licensed under the Apache License, Version 2.0

using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Moryx.Model;
using Moryx.Model.Attributes;
using Moryx.Model.Repositories;

namespace Moryx.TestTools.Test.Model
{
[ModelSetup(typeof(TestModelContext))]
public class DisposedObjectSetup : IModelSetup
{
public int SortOrder => 1;

public string Name => "Raw SQL Setup";

public string Description => "";

public string SupportedFileRegex => string.Empty;

public async Task Execute(IUnitOfWork openContext, string setupData)
{
// If the caller wouldn't await this method,
// the DbContext might be disposed before it's
// being used in here.
// Adding a delay to ensure that the is actually
// gone then.
await Task.Delay(50);
await openContext.DbContext.Database.ExecuteSqlRawAsync("SELECT * FROM \"Cars\"");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ public class ProductStorageSqliteTests : ProductStorageTests
{
protected override UnitOfWorkFactory<ProductsContext> BuildUnitOfWorkFactory()
{
return InMemoryUnitOfWorkFactoryBuilder
.Sqlite<ProductsContext>()
.EnsureDbIsCreated();
var uowFactory = InMemoryUnitOfWorkFactoryBuilder
.Sqlite<ProductsContext>();
uowFactory.EnsureDbIsCreated();

return uowFactory;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ public class RecipeStorageSqliteTests : RecipeStorageTests

protected override UnitOfWorkFactory<ProductsContext> BuildUnitOfWorkFactory()
{
return InMemoryUnitOfWorkFactoryBuilder
.Sqlite<ProductsContext>()
.EnsureDbIsCreated();
var uowFactory = InMemoryUnitOfWorkFactoryBuilder
.Sqlite<ProductsContext>();
uowFactory.EnsureDbIsCreated();

return uowFactory;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ public class ResourceManagerSqliteTests : ResourceManagerTests
{
protected override UnitOfWorkFactory<ResourcesContext> BuildUnitOfWorkFactory()
{
return InMemoryUnitOfWorkFactoryBuilder
.Sqlite<ResourcesContext>()
.EnsureDbIsCreated();
var uowFactory = InMemoryUnitOfWorkFactoryBuilder
.Sqlite<ResourcesContext>();
uowFactory.EnsureDbIsCreated();

return uowFactory;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright (c) 2023, Phoenix Contact GmbH & Co. KG
// Licensed under the Apache License, Version 2.0

using Moryx.Model;
using Moryx.TestTools.Test.Model;
using NUnit.Framework;
using System.Threading.Tasks;
using Moryx.Runtime.Endpoints.Databases.Endpoint;
using Moryx.AbstractionLayer.TestTools;
using System;
using System.Collections.Generic;

namespace Moryx.Runtime.Endpoints.IntegrationTests.Databases.Controller
{
internal class ModelSetupTests
{
private IDbContextManager? _dbContextManager;
private DatabaseController _databaseController;
private readonly List<Exception> _exceptions = new List<Exception>();

[SetUp]
public void Setup()
{
_dbContextManager = InMemoryUnitOfWorkFactoryBuilder
.SqliteDbContextManager();

_dbContextManager
.Factory<TestModelContext>()
.EnsureDbIsCreated();

_databaseController = new DatabaseController(_dbContextManager);

_exceptions.Clear();
}

[Test]
public async Task ExecuteSetupDoesNotThrowDisposedObjectException()
{
// Add unobserved task exceptions to a list, to be checked later.
TaskScheduler.UnobservedTaskException += (sender, e) =>
{
_exceptions.Add(e.Exception);
};

var result = await _databaseController!.ExecuteSetup("Moryx.TestTools.Test.Model.TestModelContext", new()
{
Config = new()
{
ConfiguratorTypename = "1",
Entries = new() { { "ConnectionString", "DataSource=:memory:" } }
},
Setup = new Endpoints.Databases.Endpoint.Models.SetupModel { Fullname = "Moryx.TestTools.Test.Model.DisposedObjectSetup" }
});

// ExecuteSetup lead to unobserved task exceptions. We give them
// some time to be thrown.
Task.Delay(100).Wait();
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

Assert.That(result?.Value?.Success, Is.True);
Assert.That(_exceptions.Count, Is.EqualTo(0));
}
}
}
Loading

0 comments on commit 4787f7c

Please sign in to comment.