7
\$\begingroup\$

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)
    { }
}
\$\endgroup\$
4
\$\begingroup\$

I generally have a separate class for IoC container configuration but your code is certainly sane. The main method is a bit too long for me so I'd look at pulling out some additional methods:

private static void LoadAndRunTests(/* don't know the types to list the params... */)
{
    var testFileNames = GetTestFileNames(targetsDirectoryName);
    var testFiles = LoadTestFiles(testFileNames, container, variableBuilder.Names).ToList();

    LogEntry.New().Debug().Message($"Test files ({testFiles.Count}) loaded.").Log(Logger);
    LogEntry.New().Info().Message($"*** {Name} v{Version} started. ***").Log(Logger);
    runner.RunTestFiles(testFiles, args, globals);
}

Why do you resolve two instances of IVariableBuilder? It's not clear from the code why you need both.


Your logging code is really weird. It's not what I'd expect at all, is it something you've written yourself? Normally, one would expect to see something like:

 Logger.Debug("message");

It's seems really backwards to have a LogEntry that knows how to write itself to a given Logger. I'd strongly recommend revisiting this API if you can, familiarity is much better than clever IMO.


In summary: code is good, just split out a couple more methods/classes and you're fine.

\$\endgroup\$
  • \$\begingroup\$ Yes, the logging layer is my own invention becasue all logging frameworks are too limited (no auto fields like something constant as an environment name, no thread context fields, no custom properties, no automatic string interpolation into message based on other fields, no computed fileds like a stopwatch). It allows me to set the logging level independently of the message which I can change during runtime if necessary. The log-entry does not actually know how to log itself (it's just a dictionary with a bunch of helper extension), Log is a helper extension for Logger.Log(logEntry). \$\endgroup\$ – t3chb0t Jun 21 '17 at 16:11
  • \$\begingroup\$ @t3chb0t - those sound like great additions to a logger. Have you considered the simpler approach of a Logger.Log(level, entry) with convenience methods for Info, Debug etc? Seems more discoverable than having to call a method on the log entry \$\endgroup\$ – RobH Jun 21 '17 at 16:19
  • \$\begingroup\$ The trick with the extensions is that once learned they work with any logger. I have an adpter for NLog and log4net that under the hood do exactly that, call the logger with their own entry. Another trick is that I have T4 scripts that generate extensions for custom columns automatically if they are not any of the standard ones like log-level, message or exception. The extensions are pretty brainless. You don't have to think about the logging framework you use. Acutally the final logging should look like this: gist \$\endgroup\$ – t3chb0t Jun 21 '17 at 16:31
  • \$\begingroup\$ Sometimes I need to log a session or transaction id per thread into every log entry to be able to group them toghether later so a thread context field that I do not have to pass into each worker method is a great help :-) Once set it gets resolved automatically for every log entry. \$\endgroup\$ – t3chb0t Jun 21 '17 at 16:33
  • \$\begingroup\$ Back to the question... I have to say, it's a real brain twister to build it in such a way that I actually can for example simulate a missing file with an integration test and see if the application actually exits as inteded or skips another one that is not readable and can be ignored and doesn't crash but logs this event. \$\endgroup\$ – t3chb0t Jun 21 '17 at 18:19
2
\$\begingroup\$

It wasn't easy but I managed to move all code that needs to be tested into a new TehApplicashun class - I know, lolcat speak :-) that now receives a bunch of serivces and is created by Autofac too. Everything is abstracted and everything can be replaced for testing. Now I can test the application's behavior for example to verfiy it exits if one of the requried files is missing or doesn't crash if an optional one is invalid.

[SettingName("TehApp")]
internal class TehApplicashun
{
    public static readonly string Name = Assembly.GetAssembly(typeof(Program)).GetName().Name;
    public static readonly string Version = "2.0.0";
    private static readonly string GlobalFileName = "_Global.json";

    private readonly ILogger _logger;
    private readonly IPathResolver _pathResolver;
    private readonly IFileSystem _fileSystem;
    private readonly IVariableBuilder _variableBuilder;
    private readonly AutofacContractResolver _autofacContractResolver;
    private readonly TestRunner _testRunner;

    public TehApplicashun(
        ILogger logger,
        IPathResolver pathResolver,
        IFileSystem fileSystem,
        IVariableBuilder variableBuilder,
        AutofacContractResolver autofacContractResolver,
        TestRunner testRunner)
    {
        _logger = logger;
        _pathResolver = pathResolver;
        _fileSystem = fileSystem;
        _variableBuilder = variableBuilder;
        _autofacContractResolver = autofacContractResolver;
        _testRunner = testRunner;
    }

    public Workspace Workspace { get; set; }

    public void Start(string[] args)
    {
        var globalFile = LoadGlobalFile();

        var globals = VariableResolver.Empty
            .MergeWith(globalFile.Globals)
            .MergeWith(_variableBuilder.BuildVariables(Workspace));

        var testFiles = LoadTestFiles().ToList();

        LogEntry.New().Debug().Message($"Test files ({testFiles.Count}) loaded.").Log(_logger);
        LogEntry.New().Info().Message($"*** {Name} v{Version} started. ***").Log(_logger);

        _testRunner.RunTestFiles(testFiles, args, globals);
    }

    private GlobalFile LoadGlobalFile()
    {
        var targetsDirectoryName = _pathResolver.ResolveDirectoryPath(Workspace.Targets);
        var fileName = Path.Combine(targetsDirectoryName, GlobalFileName);

        if (!File.Exists(fileName)) { return new GlobalFile(); }

        try
        {
            var globalFileJson = _fileSystem.ReadAllText(fileName);
            var globalFile = JsonConvert.DeserializeObject<GlobalFile>(globalFileJson);

            VariableValidator.ValidateNamesNotReserved(globalFile.Globals, _variableBuilder.Names);

            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 IEnumerable<TestFile> LoadTestFiles()
    {
        LogEntry.New().Debug().Message("Initializing tests...").Log(_logger);

        return 
            GetTestFileNames()
                .Select(LoadTest)
                .Where(Conditional.IsNotNull);
    }

    [NotNull, ItemNotNull]
    private IEnumerable<string> GetTestFileNames()
    {
        var targetsDirectoryName = _pathResolver.ResolveDirectoryPath(Workspace.Targets);

        return
            from fullName in _fileSystem.GetFiles(targetsDirectoryName, "*.json")
            where !Path.GetFileName(fullName).StartsWith("_", StringComparison.OrdinalIgnoreCase)
            select fullName;
    }

    [CanBeNull]
    private TestFile LoadTest(string fileName)
    {
        var logEntry = LogEntry.New().Info();
        try
        {
            var json = _fileSystem.ReadAllText(fileName);
            var testFile = JsonConvert.DeserializeObject<TestFile>(json, new JsonSerializerSettings
            {
                ContractResolver = _autofacContractResolver,
                DefaultValueHandling = DefaultValueHandling.Populate,
                TypeNameHandling = TypeNameHandling.Auto,
            });
            testFile.FullName = fileName;

            VariableValidator.ValidateNamesNotReserved(testFile.Locals, _variableBuilder.Names);

            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);
        }
    }
}

Additionaly I had to create a new small service to be able to fake the file-system and simulate missing or invalid file.

[PublicAPI]
internal interface IFileSystem
{
    string ReadAllText(string fileName);
    string[] GetFiles(string path, string searchPattern);
}

internal class FileSystem : IFileSystem
{
    public string ReadAllText(string fileName)
    {
        return File.ReadAllText(fileName);
    }

    public string[] GetFiles(string path, string searchPattern)
    {
        return Directory.GetFiles(path, searchPattern);
    }
}

The new Main is now nice and small. Jut resolve TehApplicashun and Start.

private static int Main(string[] args)
{
    var mainLogEntry = 
        LogEntry
            .New()
            .MessageBuilder(sb => sb.Append($"*** {TehApplicashun.Name} v{TehApplicashun.Version}"))
            .Stopwatch(sw => sw.Start());

    try
    {
        var container = InitializeContainer().GetAwaiter().GetResult();
        using (var scope = container.BeginLifetimeScope())
        {
            var tehApp = scope.Resolve<TehApplicashun>();
            tehApp.Start(args);                    
        }

        mainLogEntry.Info().MessageBuilder(sb => sb.Append("completed."));
        return 0;
    }
    catch (Exception ex)
    {
        mainLogEntry.Fatal().MessageBuilder(sb => sb.Append("crashed.")).Exception(ex);
        return 1;
    }
    finally
    {
        mainLogEntry.MessageBuilder(sb => sb.Append(" ***")).Log(Logger);
    }
}

I still need to clean-up the container configuration but this shouldn't be easy done with autofac-modules also exchangable for testing.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.