In most of my applications I think my core code is quite good organized and uses only dependency injection for everything. However as far as the initiailzation is concerned I find it's a huge chaos that I'm not sure how to organize. As a consequence I can test every module but I cannot write a test that would simulate running the application or exchange any modules registered in the IoC container.
In this question I'd like you to ask you for your suggestions about the below code. It's taken from one of my tools. Basically what it does is:
- initialize logging
- initialize configuration
- initialize IoC (it uses some
async
code just to play around with) - load json files (globals and test configuration) (here a test is a kind of an assert for a table in a database)
- initialize global variables
- resolve the
TestRunner
and run the test files (here the application actually starts its work)
My main question is: how to clean up this mess? What kind of modules would you suggest? Do you have any estabilished patterns that would make also this part of the code testable?
internal class Program
{
public static readonly string Name = Assembly.GetAssembly(typeof(Program)).GetName().Name;
public static readonly string Version = "1.0.0";
private static readonly ILogger Logger;
static Program()
{
Logger = InitializeLogging();
Configuration = InitializeConfiguration();
}
public static readonly string GlobalFileName = "_Global.json";
public static Configuration Configuration { get; }
// We need this before the IoC is created.
public static readonly IPathResolver PathResolver = new PathResolver();
private static int Main(string[] args)
{
try
{
var container = InitializeContainer().GetAwaiter().GetResult();
using (var scope = container.BeginLifetimeScope())
{
var variableBuilder = container.Resolve<IVariableBuilder>();
var targetsDirectoryName = PathResolver.ResolveDirectoryPath(Configuration.Load<Program, Workspace>().Targets);
LogEntry.New().Debug().Message($"Targets directory: \"{targetsDirectoryName}\".").Log(Logger);
var globalFileName = Path.Combine(targetsDirectoryName, GlobalFileName);
var globalFile = LoadGlobalFile(globalFileName);
VariableValidator.ValidateNamesNotReserved(globalFile.Globals, variableBuilder.Names);
var globals = VariableResolver.Empty
.MergeWith(globalFile.Globals)
.MergeWith(variableBuilder.BuildVariables(Configuration.Load<Program, Workspace>()));
var testFileNames = GetTestFileNames(targetsDirectoryName);
var testFiles = LoadTestFiles(testFileNames, container, container.Resolve<IVariableBuilder>().Names).ToList();
LogEntry.New().Debug().Message($"Test files ({testFiles.Count}) loaded.").Log(Logger);
LogEntry.New().Info().Message($"*** {Name} v{Version} started. ***").Log(Logger);
scope.Resolve<TestRunner>().RunTestFiles(testFiles, args, globals);
}
return 0;
}
// Exception should already be logged elsewhere and rethrown to exit the application.
catch (Exception ex)
{
LogEntry.New().Fatal().Message($"*** {Name} v{Version} crashed. ***").Exception(ex).Log(Logger);
return 1;
}
finally
{
LogEntry.New().Info().Message($"*** {Name} v{Version} exited. ***").Log(Logger);
}
}
[NotNull, ItemNotNull]
private static IEnumerable<string> GetTestFileNames(string directoryName)
{
return
from fullName in Directory.GetFiles(directoryName, "*.json")
where !Path.GetFileName(fullName).StartsWith("_", StringComparison.OrdinalIgnoreCase)
select fullName;
}
#region Initialization
private static ILogger InitializeLogging()
{
Reusable.Logging.NLog.Tools.LayoutRenderers.InvariantPropertiesLayoutRenderer.Register();
Reusable.Logging.Logger.ComputedProperties.Add(new Reusable.Logging.ComputedProperties.AppSetting(name: "Environment", key: "Environment"));
Reusable.Logging.Logger.ComputedProperties.Add(new Reusable.Logging.ComputedProperties.ElapsedSeconds());
Reusable.Logging.LoggerFactory.Initialize<Reusable.Logging.Adapters.NLogFactory>();
var logger = LoggerFactory.CreateLogger(nameof(Program));
LogEntry.New().Debug().Message("Logging initialized.").Log(logger);
return logger;
}
private static Configuration InitializeConfiguration()
{
try
{
return new Configuration(new AppSettings());
}
catch (Exception ex)
{
throw new InitializationException("Could not initialize configuration.", ex);
}
}
private static async Task<IContainer> InitializeContainer()
{
try
{
var containerBuilder = new ContainerBuilder();
var variableBuilderTask = Task.Run(() =>
new VariableBuilder()
.AddVariables<TestFile>(
x => x.FullName,
x => x.FileName)
.AddVariables<IDataSource>(
x => x.Elapsed)
.AddVariables<TestCase>(
x => x.Severity,
x => x.Message,
x => x.Elapsed)
.AddVariables<Workspace>(
x => x.Environment,
x => x.AppName));
containerBuilder
.RegisterType<TestRunner>()
.WithParameter(new TypedParameter(typeof(ILogger), LoggerFactory.CreateLogger(nameof(TestRunner))));
containerBuilder
.RegisterType<Data.SqlClient.TableOrView>()
.WithParameter(new TypedParameter(typeof(ILogger), LoggerFactory.CreateLogger(nameof(Data.SqlClient.TableOrView))));
containerBuilder
.RegisterType<HtmlEmail>()
.WithParameter(new TypedParameter(typeof(ILogger), LoggerFactory.CreateLogger(nameof(HtmlEmail))));
#region Initialize reporting modules
containerBuilder
.RegisterType<Report>()
.As<IReport>();
containerBuilder
.RegisterType<TestCaseInfo>();
containerBuilder
.RegisterType<DataSourceInfo>();
containerBuilder
.RegisterType<DataSummary>();
#endregion
#region Initialize renderers
containerBuilder
.RegisterType<GreetingRenderer>()
.As<ModuleRenderer>();
containerBuilder
.RegisterType<TableRenderer>()
.As<ModuleRenderer>();
containerBuilder
.RegisterType<SignatureRenderer>()
.As<ModuleRenderer>();
#endregion
containerBuilder
.RegisterInstance(await variableBuilderTask)
.As<IVariableBuilder>();
containerBuilder
.RegisterInstance(PathResolver)
.As<IPathResolver>();
containerBuilder
.RegisterType<CssInliner>();
containerBuilder
.RegisterType<SimpleCssParser>()
.As<ICssParser>();
containerBuilder
.Register<Func<string, Css>>(c =>
{
var context = c.Resolve<IComponentContext>();
return cssFileName =>
{
cssFileName = Path.Combine(Configuration.Load<Program, Workspace>().Themes, cssFileName);
var cssFullName = context.Resolve<IPathResolver>().ResolveFilePath(cssFileName);
var css = context.Resolve<ICssParser>().Parse(File.ReadAllText(cssFullName));
return css;
};
});
LogEntry.New().Debug().Message("IoC initialized.").Log(Logger);
return containerBuilder.Build();
}
catch (Exception ex)
{
throw new InitializationException("Could not initialize container.", ex);
}
}
private static GlobalFile LoadGlobalFile(string fileName)
{
if (!File.Exists(fileName)) { return new GlobalFile(); }
try
{
var globalFileJson = File.ReadAllText(fileName);
var globalFile = JsonConvert.DeserializeObject<GlobalFile>(globalFileJson);
LogEntry.New().Debug().Message($"{Path.GetFileName(fileName)} loaded.").Log(Logger);
return globalFile;
}
catch (Exception ex)
{
throw new InitializationException($"Could not load {Path.GetFileName(fileName)}.", ex);
}
}
[NotNull, ItemNotNull]
private static IEnumerable<TestFile> LoadTestFiles(IEnumerable<string> fileNames, IContainer container, IEnumerable<string> reservedNames)
{
LogEntry.New().Debug().Message("Initializing tests...").Log(Logger);
return fileNames.Select(LoadTest).Where(Conditional.IsNotNull);
TestFile LoadTest(string fileName)
{
var logEntry = LogEntry.New().Info();
try
{
var json = File.ReadAllText(fileName);
var testFile = JsonConvert.DeserializeObject<TestFile>(json, new JsonSerializerSettings
{
ContractResolver = new AutofacContractResolver(container),
DefaultValueHandling = DefaultValueHandling.Populate,
TypeNameHandling = TypeNameHandling.Auto,
});
testFile.FullName = fileName;
VariableValidator.ValidateNamesNotReserved(testFile.Locals, reservedNames);
logEntry.Message($"Test initialized: {fileName}");
return testFile;
}
catch (Exception ex)
{
logEntry.Error().Message($"Could not initialize test: {fileName}").Exception(ex);
return null;
}
finally
{
logEntry.Log(Logger);
}
}
}
#endregion
}
internal class InitializationException : Exception
{
public InitializationException(string message, Exception innerException)
: base(message, innerException)
{ }
}