diff --git a/.github/workflows/generate-diagrams.yml b/.github/workflows/generate-diagrams.yml
index 26d2cc3..35bff66 100644
--- a/.github/workflows/generate-diagrams.yml
+++ b/.github/workflows/generate-diagrams.yml
@@ -16,7 +16,7 @@ jobs:
run: dotnet build -c Release
- name: Generate a NuGet package
- run: dotnet run -p src/DepTree.Console applicationconfig.json ./src/DepTree/bin/Release/net5.0/DepTree.dll
+ run: dotnet run -p src/DepTree.Console --config applicationconfig.json -a ./src/DepTree/bin/Release/net5.0/DepTree.dll -i None
- uses: EndBug/add-and-commit@v7
with:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 97acd98..ed6b5cb 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -17,7 +17,7 @@ jobs:
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
- createpackage:
+ build:
runs-on: ubuntu-latest
needs: create-tag
env:
@@ -61,7 +61,7 @@ jobs:
create-release:
name: Create Release
runs-on: ubuntu-latest
- needs: [create-tag, createpackage]
+ needs: [create-tag, build]
steps:
- name: Checkout code
uses: actions/checkout@master
diff --git a/Dockerfile b/Dockerfile
index fe8a3a3..ed47937 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -17,6 +17,14 @@ ARG ASSEMBLY_LOCATION
ENV ASSEMBLY_LOCATION=$ASSEMBLY_LOCATION
ARG APPLICATION_CONFIG_LOCATION
ENV APPLICATION_CONFIG_LOCATION=$APPLICATION_CONFIG_LOCATION
+ARG ROOT_TYPE
+ENV ROOT_TYPE=$ROOT_TYPE
+ARG SKIP_TYPE
+ENV SKIP_TYPE=$SKIP_TYPE
+ARG ASSEMBLY_CONFIG_LOCATION
+ENV ASSEMBLY_CONFIG_LOCATION=$ASSEMBLY_CONFIG_LOCATION
+ARG INTERFACE_RESOLVER
+ENV INTERFACE_RESOLVER=$INTERFACE_RESOLVER
FROM mcr.microsoft.com/dotnet/aspnet:5.0-focal
COPY --from=build-env /out .
diff --git a/README.md b/README.md
index 3d16642..5f2cd8e 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,56 @@
# Dependency Tree
-Create a self contained assembly
+Dependency Tree will load an assembly then, starting with the root type, recursively read the constructor parameters to create a tree of dependencies.
-`dotnet publish -r osx.10.12-x64 -p:PublishSingleFile=true --self-contained true`
+By default it uses the Startup file to load and resolve which type is registered to an interface.
+
+## Using the github action
+
+The GitHub action uses the [Dockerfile](./Dockerfile) in the root of the project.
+
+| Name | Environment Variable | CLI setting | | Required |
+| -- | -- | -- | -- | -- |
+| Assembly Location | ASSEMBLY_LOCATION | -a --assembly | The location of the assembly to read | Yes |
+| Root types | ROOT_TYPES | -t --root-types | The root type to use for the dependency tree, multiple values can be used as a csv input | Yes |
+| Skip types | SKIP_TYPES | -s --skip-types | Types to skip, multiple values can be used as a csv input | No |
+| Assembly Config Location | ASSEMBLY_CONFIG_LOCATION | -n --assembly-config | The location of the configuration file required to build IConfiguration for Startup | No |
+| Interface Resolver | INTERFACE_RESOLVER | -i --interface-resolver | Method for resolving interfaces, Allowed Values: None, Startup. Default: Startup. | No |
+| Application Config Location | APPLICATION_CONFIG_LOCATION | -n --assembly-config | The location of application config file | No |
+
+### Application config
+
+There is support for passing in a config file for multiple `Skip` or `Generate` (root) types.
+
+[Example](./applicationconfig.json)
+
+### Interface resolver
+
+If `Interface Resolver` is `Startup` (default) then the application will look for a class named Startup.
+
+If the Startup file has an IConfiguration constructor then set the `Assembly Config Location` to the path of an example config file that has enough data in it to build the configuration.
+
+If there is no startup then set `Interface Resolver` to `None`.
+
+### Example applications
+
+#### Pokedex - dotnet5 web api
+
+Needed to publish a single file application
+
+`dotnet publish -r linux-x64 -p:PublishSingleFile=true -p:UseAppHost=true --self-contained`
+
+[Example output](https://github.com/maisiesadler/pokedex/blob/main/DependencyTree.md)
+
+
+
+[Example workflow](https://github.com/maisiesadler/pokedex/blob/main/.github/workflows/dependencytree.yml)
+
+- Need to add RuntimeIdentifiers [here](https://github.com/maisiesadler/pokedex/blob/main/src/Pokedex/Pokedex.csproj#L5).
+
+#### Endpoints - doesn't use Startup
+
+[Example output](https://github.com/maisiesadler/Endpoints/blob/master/Dependencies.md)
+
+
+
+[Example workflow action](https://github.com/maisiesadler/Endpoints/blob/master/.github/workflows/dependencytree.yml)
diff --git a/run.sh b/run.sh
index 51b407f..966ea4a 100755
--- a/run.sh
+++ b/run.sh
@@ -1,5 +1,12 @@
-# dotnet build src/Example
-dotnet run -p src/DepTree.Console \
- applicationconfig.json \
- /Users/maisiesadler/comparison/property-service/src/PropertyService.Api/bin/Release/net5.0/osx.10.12-x64/PropertyService.Api.dll \
- /Users/maisiesadler/comparison/property-service/src/PropertyService.Test.Integration/testsettings.json
+dotnet publish ./src/DepTree.Console/DepTree.Console.csproj -c Release -o out --no-self-contained
+
+ASSEMBLY_LOCATION=/Users/maisiesadler/repos/endpoints/src/Endpoints.Api/bin/Release/net5.0/osx.10.12-x64/Endpoints.Api.dll
+ROOT_TYPES=Pokedex.Controllers.PokemonController
+
+ASSEMBLY_LOCATION=$ASSEMBLY_LOCATION \
+ APPLICATION_CONFIG_LOCATION=$APPLICATION_CONFIG_LOCATION \
+ ROOT_TYPES=$ROOT_TYPES \
+ SKIP_TYPES=$SKIP_TYPES \
+ ASSEMBLY_CONFIG_LOCATION=$ASSEMBLY_CONFIG_LOCATION \
+ INTERFACE_RESOLVER=$INTERFACE_RESOLVER \
+ dotnet out/DepTree.Console.dll
diff --git a/runindocker.sh b/runindocker.sh
index c22c12d..468d6d6 100755
--- a/runindocker.sh
+++ b/runindocker.sh
@@ -1,6 +1,12 @@
#!/bin/bash
-result=$(dotnet /DepTree.Console.dll $APPLICATION_CONFIG_LOCATION $ASSEMBLY_LOCATION)
+result=$(ASSEMBLY_LOCATION=$ASSEMBLY_LOCATION \
+ APPLICATION_CONFIG_LOCATION=$APPLICATION_CONFIG_LOCATION \
+ ROOT_TYPES=$ROOT_TYPES \
+ SKIP_TYPES=$SKIP_TYPES \
+ ASSEMBLY_CONFIG_LOCATION=$ASSEMBLY_CONFIG_LOCATION \
+ INTERFACE_RESOLVER=$INTERFACE_RESOLVER \
+ dotnet /DepTree.Console.dll)
r=$?
if [ $r -ne 0 ]; then
diff --git a/src/DepTree.Console.Tests/ConfigurationTests.cs b/src/DepTree.Console.Tests/ConfigurationTests.cs
new file mode 100644
index 0000000..ca957e0
--- /dev/null
+++ b/src/DepTree.Console.Tests/ConfigurationTests.cs
@@ -0,0 +1,61 @@
+using Xunit;
+using DepTree.Console.Configuration;
+
+namespace DepTree.Console.Tests
+{
+ public class ConfigurationTests
+ {
+ [Fact]
+ public void CanBuildApplicationConfigFromArgs()
+ {
+ var args = new[] { "-a", "assembly-location", "-t", "type", "-c", "config-location" };
+
+ var (config, ok) = ApplicationConfiguration.Build(args);
+
+ Assert.NotNull(config);
+ var type = Assert.Single(config.Generate);
+ Assert.Equal("type", type);
+ Assert.Equal("assembly-location", config.AssemblyLocation);
+ }
+
+ [Fact]
+ public void InvalidAssemblyLocationProducesError()
+ {
+ var args = new[] { "-a", "assembly-location", "-t", "type", "-c", "config-location" };
+
+ var (config, ok) = ApplicationConfiguration.Build(args);
+
+ Assert.False(ok);
+ Assert.Contains("Assembly Location 'assembly-location' is missing or invalid", config.Errors);
+ }
+
+ [Fact]
+ public void ConfigFileIsParsed()
+ {
+ var args = new[] { "-a", "assembly-location", "-c", "exampleappconfig.json" };
+
+ var (config, ok) = ApplicationConfiguration.Build(args);
+
+ Assert.NotNull(config);
+ Assert.Equal(2, config.Generate.Count);
+ Assert.Equal("DepTree.DependencyTree", config.Generate[0]);
+ Assert.Equal("DepTree.AnotherRootType", config.Generate[1]);
+ var skip = Assert.Single(config.Skip);
+ Assert.Equal("Serilog.IDiagnosticContext", skip);
+ }
+
+ [Fact]
+ public void CsvTypesAreAdded()
+ {
+ var args = new[] { "-a", "assembly-location", "-t", "type1,type2" };
+
+ var (config, ok) = ApplicationConfiguration.Build(args);
+
+ Assert.NotNull(config);
+ Assert.Equal(2, config.Generate.Count);
+ Assert.Equal("type1", config.Generate[0]);
+ Assert.Equal("type2", config.Generate[1]);
+ Assert.Equal("assembly-location", config.AssemblyLocation);
+ }
+ }
+}
diff --git a/src/DepTree.Console.Tests/DepTree.Console.Tests.csproj b/src/DepTree.Console.Tests/DepTree.Console.Tests.csproj
new file mode 100644
index 0000000..e053f7a
--- /dev/null
+++ b/src/DepTree.Console.Tests/DepTree.Console.Tests.csproj
@@ -0,0 +1,30 @@
+
+
+
+ net5.0
+
+ false
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/DepTree.Console.Tests/exampleappconfig.json b/src/DepTree.Console.Tests/exampleappconfig.json
new file mode 100644
index 0000000..b1e4536
--- /dev/null
+++ b/src/DepTree.Console.Tests/exampleappconfig.json
@@ -0,0 +1,9 @@
+{
+ "Skip": [
+ "Serilog.IDiagnosticContext"
+ ],
+ "Generate": [
+ "DepTree.DependencyTree",
+ "DepTree.AnotherRootType"
+ ]
+}
diff --git a/src/DepTree.Console/Configuration/ApplicationConfiguration.cs b/src/DepTree.Console/Configuration/ApplicationConfiguration.cs
new file mode 100644
index 0000000..0fd7d5e
--- /dev/null
+++ b/src/DepTree.Console/Configuration/ApplicationConfiguration.cs
@@ -0,0 +1,136 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text.Json;
+using CommandLine;
+using Microsoft.Extensions.Configuration;
+
+namespace DepTree.Console.Configuration
+{
+ public class ApplicationConfiguration
+ {
+ private ApplicationConfiguration() { }
+
+ private string _assemblyConfigLocation;
+ private string _configLocation;
+
+ public IConfiguration AssemblyConfiguration { get; private set; }
+ public string AssemblyLocation { get; private set; }
+ public string InterfaceResolver { get; private set; }
+ public HashSet Skip { get; private set; } = new HashSet();
+ public List Generate { get; private set; } = new List();
+ public List Errors { get; private set; } = new List();
+ public bool IsValid => Errors.Count == 0;
+
+ public static (ApplicationConfiguration, bool) Build(string[] args)
+ {
+ var config = new ApplicationConfiguration();
+
+ config.ReadEnvironmentVariables();
+
+ config.TryReadArgs(args);
+ config.TryReadFileConfig();
+
+ config.TryBuildAssemblyConfiguration();
+
+ config.Validate();
+
+ return (config, config.IsValid);
+ }
+
+ private void Validate()
+ {
+ if (string.IsNullOrEmpty(AssemblyLocation) || !File.Exists(AssemblyLocation))
+ {
+ Errors.Add($"Assembly Location '{AssemblyLocation}' is missing or invalid");
+ }
+ }
+
+ private void ReadEnvironmentVariables()
+ {
+ AssemblyLocation = Environment.GetEnvironmentVariable("ASSEMBLY_LOCATION");
+ _assemblyConfigLocation = Environment.GetEnvironmentVariable("ASSEMBLY_CONFIG_LOCATION");
+ _configLocation = Environment.GetEnvironmentVariable("APPLICATION_CONFIG_LOCATION");
+ InterfaceResolver = Environment.GetEnvironmentVariable("INTERFACE_RESOLVER");
+
+ var rootTypes = Environment.GetEnvironmentVariable("ROOT_TYPES");
+ if (!string.IsNullOrWhiteSpace(rootTypes))
+ {
+ var types = rootTypes.Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ Generate.AddRange(types);
+ }
+
+ var skipTypes = Environment.GetEnvironmentVariable("SKIP_TYPES");
+ if (!string.IsNullOrWhiteSpace(skipTypes))
+ {
+ var skip = skipTypes.Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ foreach (var s in skip) Skip.Add(s);
+ }
+ }
+
+ private void TryReadArgs(string[] args)
+ {
+ var parser = Parser.Default.ParseArguments(() => new(), args);
+ parser.WithParsed(inputs =>
+ {
+ if (!string.IsNullOrWhiteSpace(inputs.AssemblyLocation))
+ AssemblyLocation = inputs.AssemblyLocation;
+
+ if (!string.IsNullOrWhiteSpace(inputs.InterfaceResolver))
+ InterfaceResolver = inputs.InterfaceResolver;
+
+ if (!string.IsNullOrWhiteSpace(inputs.RootTypes))
+ {
+ var types = inputs.RootTypes.Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ Generate.AddRange(types);
+ }
+
+ if (!string.IsNullOrWhiteSpace(inputs.SkipTypes))
+ {
+ var skip = inputs.SkipTypes.Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ foreach (var s in skip) Skip.Add(s);
+ }
+
+ if (!string.IsNullOrWhiteSpace(inputs.AssemblyConfigLocation))
+ _assemblyConfigLocation = inputs.AssemblyConfigLocation;
+ if (!string.IsNullOrWhiteSpace(inputs.ConfigLocation))
+ _configLocation = inputs.ConfigLocation;
+ });
+
+ parser.WithNotParsed(errors =>
+ {
+ Errors.Add("Invalid CLI inputs");
+ });
+ }
+
+ private void TryReadFileConfig()
+ {
+ try
+ {
+ if (string.IsNullOrWhiteSpace(_configLocation)) return;
+ var configContents = File.ReadAllText(_configLocation);
+ var fileConfig = JsonSerializer.Deserialize(configContents);
+
+ if (fileConfig.Skip != null)
+ foreach (var s in fileConfig.Skip) Skip.Add(s);
+
+ if (fileConfig.Generate != null)
+ Generate.AddRange(fileConfig.Generate);
+ }
+ catch (Exception ex)
+ {
+ System.Console.WriteLine($"Exception reading config: {ex.Message}.");
+ }
+ }
+
+ private void TryBuildAssemblyConfiguration()
+ {
+ if (!string.IsNullOrWhiteSpace(_assemblyConfigLocation) && File.Exists(_assemblyConfigLocation))
+ {
+ var cfgBuilder = new ConfigurationBuilder();
+ cfgBuilder.AddJsonFile(_assemblyConfigLocation, optional: false, reloadOnChange: false);
+ AssemblyConfiguration = cfgBuilder.Build();
+ }
+ }
+ }
+}
diff --git a/src/DepTree.Console/Configuration/CommandLineInputs.cs b/src/DepTree.Console/Configuration/CommandLineInputs.cs
new file mode 100644
index 0000000..e92c77d
--- /dev/null
+++ b/src/DepTree.Console/Configuration/CommandLineInputs.cs
@@ -0,0 +1,37 @@
+using CommandLine;
+
+namespace DepTree.Console.Configuration
+{
+ public class CommandLineInputs
+ {
+ [Option('a', "assembly",
+ Required = false,
+ HelpText = "The location of the assembly to read")]
+ public string AssemblyLocation { get; set; } = null!;
+
+ [Option('n', "assembly-config",
+ Required = false,
+ HelpText = "The location of the configuration file required to build IConfiguration for Startup")]
+ public string AssemblyConfigLocation { get; set; } = null!;
+
+ [Option('t', "root-types",
+ Required = false,
+ HelpText = "The root type to use for the dependency tree, multiple values can be used as a csv input")]
+ public string RootTypes { get; set; } = null!;
+
+ [Option('c', "config",
+ Required = false,
+ HelpText = "The location of application config file")]
+ public string ConfigLocation { get; set; } = null!;
+
+ [Option('s', "skip-types",
+ Required = false,
+ HelpText = "Types to skip, multiple values can be used as a csv input")]
+ public string SkipTypes { get; set; } = null!;
+
+ [Option('i', "interface-resolver",
+ Required = false,
+ HelpText = "Interface Resolver type to use, Allowed Values: None, Startup. Default: Startup.")]
+ public string InterfaceResolver { get; set; } = null!;
+ }
+}
diff --git a/src/DepTree.Console/ApplicationConfig.cs b/src/DepTree.Console/Configuration/FileConfiguration.cs
similarity index 67%
rename from src/DepTree.Console/ApplicationConfig.cs
rename to src/DepTree.Console/Configuration/FileConfiguration.cs
index c89523d..71ae057 100644
--- a/src/DepTree.Console/ApplicationConfig.cs
+++ b/src/DepTree.Console/Configuration/FileConfiguration.cs
@@ -1,8 +1,8 @@
using System.Collections.Generic;
-namespace DepTree.Console
+namespace DepTree.Console.Configuration
{
- public class ApplicationConfig
+ public class FileConfiguration
{
public HashSet Skip { get; set; }
public IList Generate { get; set; }
diff --git a/src/DepTree.Console/DepTree.Console.csproj b/src/DepTree.Console/DepTree.Console.csproj
index 99140a5..56b78e9 100644
--- a/src/DepTree.Console/DepTree.Console.csproj
+++ b/src/DepTree.Console/DepTree.Console.csproj
@@ -6,6 +6,7 @@
+
diff --git a/src/DepTree.Console/Program.cs b/src/DepTree.Console/Program.cs
index 9505d5e..4ffc341 100644
--- a/src/DepTree.Console/Program.cs
+++ b/src/DepTree.Console/Program.cs
@@ -1,10 +1,7 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
+using System.Collections.Generic;
using System.Reflection;
-using System.Text.Json;
using DepTree.Diagrams;
-using Microsoft.Extensions.Configuration;
+using DepTree.Console.Configuration;
namespace DepTree.Console
{
@@ -12,39 +9,20 @@ class Program
{
static int Main(string[] args)
{
- var expectedArgs = "Expected .";
- if (args.Length != 2 && args.Length != 3)
- {
- System.Console.WriteLine($"Incorrect number of args. {expectedArgs}");
- return -1;
- }
-
- var configLocation = args[0];
- if (!File.Exists(configLocation))
- {
- System.Console.WriteLine($"Application Config location '{configLocation}' does not exist'. {expectedArgs}");
- return -1;
- }
-
- var (applicationConfig, ok) = ReadConfig(configLocation);
+ // System.Console.WriteLine(string.Join(", ", args));
+ var (applicationConfig, ok) = ApplicationConfiguration.Build(args);
if (!ok)
{
- System.Console.WriteLine($"Application Config at '{configLocation}' not valid'. {expectedArgs}");
- return -1;
- }
-
- var assemblyLocation = args[1];
- if (!File.Exists(assemblyLocation))
- {
- System.Console.WriteLine($"Assembly location '{assemblyLocation}' does not exist'. {expectedArgs}");
+ System.Console.WriteLine($"Application Config is not valid. {string.Join(", ", applicationConfig.Errors)}.");
return -1;
}
- var iconfiguration = TryBuildConfiguration(args);
+ // System.Console.WriteLine(JsonSerializer.Serialize(applicationConfig));
- var ass = Assembly.LoadFrom(assemblyLocation);
- var config = new DependencyTreeConfig(ass, iconfiguration, skipAssemblies: applicationConfig.Skip);
- config.InterfaceResolverType = Resolvers.InterfaceResolverType.None;
+ var assembly = Assembly.LoadFrom(applicationConfig.AssemblyLocation);
+ var config = new DependencyTreeConfig(assembly, applicationConfig.AssemblyConfiguration, skipAssemblies: applicationConfig.Skip);
+ if (applicationConfig.InterfaceResolver == "None")
+ config.InterfaceResolverType = Resolvers.InterfaceResolverType.None;
var nodes = new List();
var tree = new DependencyTree(config);
@@ -63,34 +41,6 @@ static int Main(string[] args)
return 0;
}
- private static IConfiguration TryBuildConfiguration(string[] args)
- {
- if (args.Length > 2
- && args[2] is string configLocation
- && File.Exists(configLocation))
- {
- var cfgBuilder = new ConfigurationBuilder();
- cfgBuilder.AddJsonFile(configLocation, optional: false, reloadOnChange: false);
- return cfgBuilder.Build();
- }
-
- return null;
- }
-
- private static (ApplicationConfig, bool) ReadConfig(string configLocation)
- {
- try
- {
- var configContents = File.ReadAllText(configLocation);
- return (JsonSerializer.Deserialize(configContents), true);
- }
- catch (Exception ex)
- {
- System.Console.WriteLine($"Could not read config. Exception: {ex.Message}.");
- return (null, false);
- }
- }
-
private static void Print(DependencyTreeNode node, string indent = "")
{
System.Console.WriteLine($"{indent}{node.Name}");