Skip to content

Commit

Permalink
fixed IDatabaseInitializer sorting with InitializeAfterAttribute (was…
Browse files Browse the repository at this point in the history
… only comparing neighbors) & added support for transitive dependencies
  • Loading branch information
martinzima committed May 17, 2022
1 parent 3cbef32 commit fdf7628
Show file tree
Hide file tree
Showing 9 changed files with 238 additions and 33 deletions.
3 changes: 3 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## [1.30.0] - 2022-05-10

### Fixed
- fixed IDatabaseInitializer sorting with InitializeAfterAttribute (was only comparing neighbors) & added support for transitive dependencies

### Changed
- Now targeting .NET 6.0 and .NET Standard 2.0 (dropping support for .NET Framework, .NET 3.1 and .NET Standard 2.0)
- Upgraded to ASP.NET Core 6.0, Entity Framework Core 6.0, AutoMapper 11
Expand Down
4 changes: 2 additions & 2 deletions Revo.Infrastructure/DataAccess/DataAccessModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ public override void Load()
.To<DatabaseInitializerDiscovery>()
.InSingletonScope();

Bind<IDatabaseInitializerComparer>()
.To<DatabaseInitializerDependencyComparer>()
Bind<IDatabaseInitializerSorter>()
.To<DatabaseInitializerDependencySorter>()
.InSingletonScope();
}
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace Revo.Infrastructure.DataAccess
{
public class DatabaseInitializerDependencySorter : IDatabaseInitializerSorter
{
public List<IDatabaseInitializer> GetSorted(IReadOnlyCollection<IDatabaseInitializer> initializers)
{
var result = new List<IDatabaseInitializer>();
var remaining = initializers.ToList();

while (remaining.Count > 0)
{
var next = remaining
.Where(x => GetDependencies(x)
.All(depType => result.Any(depType.IsInstanceOfType)))
.ToArray();
if (next.Length == 0)
{
throw new InvalidOperationException(
$"Unable to sort IDatabaseInitializers - either you have cyclic dependencies or some dependencies were not found: ${string.Join(", ", remaining.Select(x => x.GetType().Name))}");
}

result.AddRange(next);
foreach (var di in next)
{
remaining.Remove(di);
}
}

return result;
}

private IEnumerable<Type> GetDependencies(IDatabaseInitializer databaseInitializer)
{
return databaseInitializer.GetType()
.GetCustomAttributes<InitializeAfterAttribute>()
.Select(x => x.InitializerType);
}
}
}
9 changes: 4 additions & 5 deletions Revo.Infrastructure/DataAccess/DatabaseInitializerLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,18 @@ namespace Revo.Infrastructure.DataAccess
public class DatabaseInitializerLoader : IDatabaseInitializerLoader, IApplicationStartedListener
{
private readonly IDatabaseInitializerDiscovery databaseInitializerDiscovery;
private readonly IDatabaseInitializerComparer comparer;
private readonly IDatabaseInitializerSorter sorter;
private readonly Func<IRepository> repositoryFunc; // using func factories for late resolving in the scope of different tasks
private readonly Func<IUnitOfWorkFactory> unitOfWorkFactoryFunc;
private readonly Func<CommandContextStack> commandContextStackFunc;
private bool isInitialized = false;

public DatabaseInitializerLoader(IDatabaseInitializerDiscovery databaseInitializerDiscovery,
IDatabaseInitializerComparer comparer, Func<IRepository> repositoryFunc,
IDatabaseInitializerSorter sorter, Func<IRepository> repositoryFunc,
Func<IUnitOfWorkFactory> unitOfWorkFactoryFunc, Func<CommandContextStack> commandContextStackFunc)
{
this.databaseInitializerDiscovery = databaseInitializerDiscovery;
this.comparer = comparer;
this.sorter = sorter;
this.repositoryFunc = repositoryFunc;
this.unitOfWorkFactoryFunc = unitOfWorkFactoryFunc;
this.commandContextStackFunc = commandContextStackFunc;
Expand All @@ -41,8 +41,7 @@ public void EnsureDatabaseInitialized()
isInitialized = true;

var initializers = databaseInitializerDiscovery.DiscoverDatabaseInitializers();
var sortedInitializers = initializers.ToList();
sortedInitializers.Sort(comparer);
var sortedInitializers = sorter.GetSorted(initializers.ToArray());

foreach (var initializer in sortedInitializers)
{
Expand Down

This file was deleted.

9 changes: 9 additions & 0 deletions Revo.Infrastructure/DataAccess/IDatabaseInitializerSorter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Collections.Generic;

namespace Revo.Infrastructure.DataAccess
{
public interface IDatabaseInitializerSorter
{
List<IDatabaseInitializer> GetSorted(IReadOnlyCollection<IDatabaseInitializer> initializers);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
using FluentAssertions;
using MoreLinq;
using Revo.Infrastructure.DataAccess;
using Revo.Infrastructure.Repositories;
using System;
using System.Linq;
using System.Threading.Tasks;
using Xunit;

namespace Revo.Infrastructure.Tests.DataAccess;

public class DatabaseInitializerDependencySorterTests
{
private DatabaseInitializerDependencySorter sut = new();

[Fact]
public void Sort()
{
var initializers = new IDatabaseInitializer[]
{
new TestDiA1(),
new TestDiA(),
};

var result = sut.GetSorted(initializers);
result.Should().BeEquivalentTo(new IDatabaseInitializer[]
{
initializers.OfType<TestDiA>().First(),
initializers.OfType<TestDiA1>().First(),
});
}

[Fact]
public void Sort_Transitive()
{
var initializers = new IDatabaseInitializer[]
{
new TestDiA1A(),
new TestDiA1(),
new TestDiA(),
};

var result = sut.GetSorted(initializers);
result.Should().BeEquivalentTo(new IDatabaseInitializer[]
{
initializers.OfType<TestDiA>().First(),
initializers.OfType<TestDiA1>().First(),
initializers.OfType<TestDiA1A>().First()
});
}

[Fact]
public void Sort_MultipleDependencies()
{
var initializers = new IDatabaseInitializer[]
{
new TestDiC(),
new TestDiA1A(),
new TestDiA1(),
new TestDiA(),
new TestDiB(),
new TestDiB1()
};

var result = sut.GetSorted(initializers);
result.Should().Contain(initializers);

result.IndexOf(initializers.OfType<TestDiA>().First()).Should()
.BeLessThan(result.IndexOf(initializers.OfType<TestDiA1>().First()));
result.IndexOf(initializers.OfType<TestDiA1>().First()).Should()
.BeLessThan(result.IndexOf(initializers.OfType<TestDiA1A>().First()));
result.IndexOf(initializers.OfType<TestDiB>().First()).Should()
.BeLessThan(result.IndexOf(initializers.OfType<TestDiB1>().First()));
result.IndexOf(initializers.OfType<TestDiA1A>().First()).Should()
.BeLessThan(result.IndexOf(initializers.OfType<TestDiC>().First()));
result.IndexOf(initializers.OfType<TestDiB1>().First()).Should()
.BeLessThan(result.IndexOf(initializers.OfType<TestDiC>().First()));
}

[Fact]
public void Sort_ThrowsOnCyclicDependency()
{
var initializers = new IDatabaseInitializer[]
{
new TestDiD(),
new TestDiE()
};

sut.Invoking(x => x.GetSorted(initializers))
.Should().Throw<InvalidOperationException>();
}

[Fact]
public void Sort_ThrowsOnMissingDependency()
{
var initializers = new IDatabaseInitializer[]
{
new TestDiD()
};

sut.Invoking(x => x.GetSorted(initializers))
.Should().Throw<InvalidOperationException>();
}

public class TestDiA : IDatabaseInitializer
{
public Task InitializeAsync(IRepository repository)
{
throw new System.NotImplementedException();
}
}

public class TestDiB : IDatabaseInitializer
{
public Task InitializeAsync(IRepository repository)
{
throw new System.NotImplementedException();
}
}

[InitializeAfter(typeof(TestDiA))]
public class TestDiA1 : IDatabaseInitializer
{
public Task InitializeAsync(IRepository repository)
{
throw new System.NotImplementedException();
}
}

[InitializeAfter(typeof(TestDiB))]
public class TestDiB1 : IDatabaseInitializer
{
public Task InitializeAsync(IRepository repository)
{
throw new System.NotImplementedException();
}
}

[InitializeAfter(typeof(TestDiA1))]
public class TestDiA1A : IDatabaseInitializer
{
public Task InitializeAsync(IRepository repository)
{
throw new System.NotImplementedException();
}
}

[InitializeAfter(typeof(TestDiA1A))]
[InitializeAfter(typeof(TestDiB1))]
public class TestDiC : IDatabaseInitializer
{
public Task InitializeAsync(IRepository repository)
{
throw new System.NotImplementedException();
}
}

[InitializeAfter(typeof(TestDiE))]
public class TestDiD : IDatabaseInitializer
{
public Task InitializeAsync(IRepository repository)
{
throw new System.NotImplementedException();
}
}

[InitializeAfter(typeof(TestDiD))]
public class TestDiE : IDatabaseInitializer
{
public Task InitializeAsync(IRepository repository)
{
throw new System.NotImplementedException();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public DatabaseInitializerLoaderTests()
repository = Substitute.For<IRepository>();
commandContextStack = new CommandContextStack();

sut = new DatabaseInitializerLoader(databaseInitializerDiscovery, new DatabaseInitializerDependencyComparer(),
sut = new DatabaseInitializerLoader(databaseInitializerDiscovery, new DatabaseInitializerDependencySorter(),
() => repository, () => unitOfWorkFactory, () => commandContextStack);
}

Expand Down

0 comments on commit fdf7628

Please sign in to comment.