Skip to content

Commit

Permalink
Dependency ordering between stateful resources. Closes GH-81
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremydmiller committed Sep 23, 2024
1 parent 178f49f commit c712b98
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 1 deletion.
8 changes: 8 additions & 0 deletions docs/guide/host/resources.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,15 @@ public static async Task usages_for_testing(IHost host)
<sup><a href='https://github.com/JasperFx/oakton/blob/master/src/Tests/Resources/ResourceHostExtensionsTests.cs#L62-L78' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_programmatically_control_resources' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

## Dependencies between Resources

Sometimes you'd like to enforce some ordering between your resources because there might be some
dependency from one resource to another -- with the original scenario being the need to run one set of database migrations before running a resource that depends on that original database setup.
That can be done by implementing this interface for your
stateful resources:

snippet: sample_IStatefulResourceWithDependencies
~~~~
## "resources" Command
::: tip
Expand Down
15 changes: 15 additions & 0 deletions src/Oakton/Resources/IStatefulResource.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Spectre.Console.Rendering;

namespace Oakton.Resources;

#region sample_IStatefulResourceWithDependencies

/// <summary>
/// Use to create dependencies between
/// </summary>
public interface IStatefulResourceWithDependencies : IStatefulResource
{
// Given all the known stateful resources in your system -- including the current resource!
// tell Oakton which resources are dependencies of this resource that should be setup first
IEnumerable<IStatefulResource> FindDependencies(IReadOnlyList<IStatefulResource> others);
}

#endregion

#region sample_IStatefulResource

/// <summary>
Expand Down
13 changes: 12 additions & 1 deletion src/Oakton/Resources/ResourcesCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,18 @@ internal static IList<IStatefulResource> FindResources(IServiceProvider services
list = list.Where(x => x.Type.EqualsIgnoreCase(typeName)).ToList();
}

return list.OrderBy(x => x.Type).ThenBy(x => x.Name).ToList();
// Initial sort
list = list.OrderBy(x => x.Type).ThenBy(x => x.Name).ToList();

if (!list.OfType<IStatefulResourceWithDependencies>().Any()) return list;

IEnumerable<IStatefulResource> FindDependencies(IStatefulResource resource) =>
resource is IStatefulResourceWithDependencies x
? x.FindDependencies(list)
: Array.Empty<IStatefulResource>();

// Again on dependencies
return list.TopologicalSort(FindDependencies).ToList();
}

internal class ResourceRecord
Expand Down
52 changes: 52 additions & 0 deletions src/Tests/Resources/ResourceCommandContext.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using JasperFx.Core;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using NSubstitute;
using Oakton.Resources;
using Shouldly;
using Spectre.Console.Rendering;

namespace Tests.Resources
{
Expand Down Expand Up @@ -77,6 +80,18 @@ internal IStatefulResource AddResource(string name, string type = "Resource")
_services.AddSingleton<IStatefulResource>(resource);
return resource;
}

internal IStatefulResource AddResourceWithDependencies(string name, string type, params string[] dependencyNames)
{
var resource = new ResourceWithDependencies
{
Name = name,
Type = type,
DependencyNames = dependencyNames
};
_services.AddSingleton<IStatefulResource>(resource);
return resource;
}

public class ResourceCollection : IStatefulResourceSource
{
Expand All @@ -102,4 +117,41 @@ public IReadOnlyList<IStatefulResource> FindResources()
}
}
}
}

public class ResourceWithDependencies : IStatefulResourceWithDependencies
{
public string Type { get; set; }
public string Name { get; set; }
public string[] DependencyNames { get; set; }

public Task Check(CancellationToken token)
{
return Task.CompletedTask;
}

public Task ClearState(CancellationToken token)
{
return Task.CompletedTask;
}

public Task Teardown(CancellationToken token)
{
return Task.CompletedTask;
}

public Task Setup(CancellationToken token)
{
return Task.CompletedTask;
}

public Task<IRenderable> DetermineStatus(CancellationToken token)
{
throw new NotImplementedException();
}

public IEnumerable<IStatefulResource> FindDependencies(IReadOnlyList<IStatefulResource> others)
{
return others.Where(x => DependencyNames.Contains(x.Name));
}
}
30 changes: 30 additions & 0 deletions src/Tests/Resources/resource_ordering.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System.Linq;
using Shouldly;
using Xunit;

namespace Tests.Resources;

public class resource_ordering : ResourceCommandContext
{
[Fact]
public void respect_ordering_by_dependencies()
{
var one = AddResourceWithDependencies("one", "system", "blue", "red");
var two = AddResourceWithDependencies("two", "system", "blue", "red", "one");

var blue = AddResource("blue", "color");
var red = AddResource("red", "color");

var tx = AddResource("tx", "state");
var ar = AddResource("ar", "state");

var resources = applyTheResourceFiltering();

resources.Count.ShouldBe(6);

resources.Last().ShouldBe(two);

resources.Select(x => x.Name).ShouldBe(new string[] { "blue", "red", "ar", "tx", "one", "two" });

}
}

0 comments on commit c712b98

Please sign in to comment.