Avatar

Blog (pg. 6)

  • Published on
    When you have a class that is populated using "configuration.Bind" you might find it useful to test that the properties are correctly set. This confirms that the property names are correctly aligned with your expected keys and that all your bound properties have accessible "setters" to be called by the binding engine. (Given that the interface that my config files implement are usually "read-only" then the "set" is not enforced) See the example class:
    
    public class SomeSettings
    {
        public SomeSettings(IConfiguration configuration)
        {
            if (configuration == null) throw new ArgumentNullException(nameof(configuration));
            configuration.Bind("SomeSettings", this);
        }
    
        public int SomeIntSetting { get; set; }
        public IReadOnlyList<SomeChildSetting> SomeListOfObjects { get; set; }
        public IReadOnlyList<string> SomeListOfValues { get; set; }
    }
    
    public class SomeChildSetting
    {
        public string SomeChildItem { get; set; }
    }
    
    Your appsettings.json file might look like:
    {
        "SomeSettings": {
             "SomeIntSetting": 1,
             "SomeListOfObjects": [
                 {
                      "SomeChildItem": "hello"
                 },
                 {
                      "SomeChildItem": "world"
                 }
              ],
           "SomeListOfValues": [ "this", "is", "great" ]
        }
    }
    
    The above should work, however renaming a property on the class would break the binding or removing the "set" against a property would break the binding, but there would be no errors during compilation or runtime, it would simply ignore the things that couldn't be bound. Therefore, it's worth adding unit tests to protect against such bugs, which can be achieved using the "ConfigurationBuilder" which supports in memory collections, as shown below:
    
    [TestFixture]
    public class SomeSettingsTests
    {
        private readonly Fixture _fixture = new Fixture();
        private Dictionary<string, string> _stubConfigs;
        private IConfigurationBuilder _configurationBuilder;
    
        [SetUp]
        public void SetUp()
        {
            _stubConfigs = new Dictionary<string, string>();
            _configurationBuilder = new ConfigurationBuilder().AddInMemoryCollection(_stubConfigs);
        }
    
        [Test]
        public void Ctor_ConfigurationNull_ThrowsException()
        {
            Func<SomeSettings> act = () => new SomeSettings(null);
    
            act.Should().Throw<ArgumentNullException>().And.ParamName.Should().Be("configuration");
        }
    
        [Test]
        public void SomeIntSetting_WhenConfigured_IsExpected()
        {
            var value = _fixture.Create<int>();
            _stubConfigs.Add("SomeSettings:SomeIntSetting", value.ToString());
    
            var result = GetDefaultSut().SomeIntSetting;
    
            result.Should().Be(value);
        }
    
        [Test]
        public void SomeListOfObjects_WhenConfigured_IsExpected()
        {
            var childSettings = _fixture.CreateMany<SomeChildSetting>().ToList();
    
            for (var i = 0; i < childSettings.Count; i++)
            {
                foreach (var propertyInfo in typeof(SomeChildSetting).GetProperties())
                {
                    _stubConfigs.Add($"SomeSettings:SomeListOfObjects:{i}:{propertyInfo.Name}", propertyInfo.GetGetMethod().Invoke(childSettings[i], null).ToString());
                }
            }
    
            var result = GetDefaultSut().SomeListOfObjects;
    
            result.Should().BeEquivalentTo(childSettings);
        }
    
        [Test]
        public void SomeListOfValues_WhenConfigured_IsExpected()
        {
            var values = _fixture.CreateMany<string>().ToList();
    
            for (var i = 0; i < values.Count; i++)
            {
                _stubConfigs.Add($"SomeSettings:SomeListOfValues:{i}", values[i]);
            }
    
            var result = GetDefaultSut().SomeListOfValues;
    
            result.Should().BeEquivalentTo(values);
        }
    
        private SomeSettings GetDefaultSut() => new SomeSettings(_configurationBuilder.Build());
    }
    
  • Published on
    This is a difficult one to describe in terms of using the correct terminology for exactly what problem I was solving when I came up with this code. I think the following example is the best way to convey what problem this solution is designed to solve. Imagine you have a sort of 2-dimensional jagged array (in my case a list of lists) where the x dimension represents the passing of time and the y dimension represents the various options/forks in the data which could be used in that segment. e.g.
    | 0 | 1 | 2 |
    | A | A | A |
    |   | B | B |
    |   | C |   |
    
    In the above, segment 0 of time can only use option "A", segment 1 can use "A", "B" or "C", segment 2 can use "A" or "B". Given this above set of data, there are a finite number of possible combinations the data can be used (which is equal to the multiple aggregate value of the counts of the y values) i.e.: 1 * 3 * 2 = 6 combinations And I wanted a way to have a single pass at the data and build the truth table of possible permutations by filling in the gaps left by lack of any option e.g.:
    | 0 | 1 | 2 |
    | A | A | A |
    | A | A | B |
    | A | B | A |
    | A | B | B |
    | A | C | A |
    | A | C | B |
    
    My idea was that, ahead of time for a given permutation, you know how many times the input options of each segment should be repeated into the output matrix in order to end up with all the permutations. At the same time, you must occasionally reverse the output order in order not generate a mirror image of an existing permutation. The code I came up with, an example of which can be seen below, can be used with any combination of x and y counts and returns the value containing all distinct permutations:
    
    private static List<string>[] GetFullCombinations(List<List<string>> segmentOptions)
    {
    	var totalPermutations = segmentOptions.Aggregate(1, (x, y) => x * y.Count);
    	var combos = new List<string>[totalPermutations];
    	var repetitions = totalPermutations;
    
    	foreach (var options in segmentOptions)
    	{
    		repetitions /= options.Count;
    		var optionIndex = 0;
    		for (var permutation = 0; permutation < totalPermutations; permutation++)
    		{
    			if ((permutation + 1) % repetitions == 0)
    				optionIndex = (optionIndex + 1) % options.Count;
    
    			var option = options[optionIndex];
    			if (combos[permutation] == null)
    			{
    				combos[permutation] = new List<string>(segmentOptions.Count);
    			}
    
    			combos[permutation].Add(option);
    		}
    	}
    
    	return combos;
    }
    
    Due to the "no mirror images" modular arithmetic, the output is actually in a slightly different order to how a human might have ordered it (in my first table), nevertheless all combinations are returned:
    
    [Test]
    public void GetFullCombinations_WhenInputSegmentsHaveOptions_ReturnsAllDistinctPermutations()
    {
    	var input = new List<List<string>>
    	{
    		new List<string>
    		{
    			"A"
    		},
    		new List<string>
    		{
    			"A",
    			"B",
    			"C"
    		},
    		new List<string>
    		{
    			"A",
    			"B"
    		}
    	};
    	var expectedPermutations = new[]
    	{
    		new [] { "A", "A", "A" },
    		new [] { "A", "A", "B"},
    		new [] { "A", "B", "A"},
    		new [] { "A", "B", "B"},
    		new [] { "A", "C", "A"},
    		new [] { "A", "C", "B"}
    	};
    
    	var result = GetFullCombinations(input);
    
    	using (new AssertionScope())
    	{
    		foreach (var expectedPermutation in expectedPermutations)
    		{
    			result.Should().ContainEquivalentOf(expectedPermutation, cfg => cfg.WithStrictOrdering());
    		}
    	}
    }
    
  • Published on
    I try to maintain a toolkit of useful apps for doing my daily development tasks. Some of these I use very frequently, others not so much but they are useful to know about. I thought I'd catalogue them on my blog so that I remember them when I'm setting up a new machine :)
    Tool Name Description
    Microsoft Visual Studio I think this one goes without saying, but if anyone getting into development needs to choose an IDE I'd highly recommend starting here! It pretty much does everything you need (solutions, projects, code editing, compiling, debugging, NuGet package management, profiling, source control and more) and at the time of writing is available for Windows and Mac. There are free editions of Visual Studio suitable for most people.
    Recommended plugins:
    JetBrains Rider If you like Visual Studio + Re-Sharper and have the dotUltimate JetBrains license I'd invite you to try the JetBrains IDE Rider. I tried this out as it's fully cross platform and I liked the idea of being able to seamlessly switch OS without any noticeable difference in my IDE experience and I have to say I think I now prefer it to using Visual Studio!
    Notepad++ A free text editor which is well maintained and comes with a lot of features for working with text files. It's not a "code editor", as such although it supports syntax highlighting, but it's useful for quickly viewing or editing all kinds of text files.
    VS Code A free cross platform and very extensible IDE/text editor by Microsoft. For me, this is the middle ground between opening Notepad++ and opening a full blown IDE such as Visual Studio or Rider.
    Sourcetree A free GUI for Git. One of the best I've tried and adds real value vs using the Visual Studio plugin or going fully command line. Linux alternative: Git-Cola
    Docker Installing Docker desktop opens up a whole world of containerised apps ready for you to integrate with in your code, such as Redis caches, Kafka instances, SQL server, FTP servers - pretty much run anything with a simple command!
    Fiddler A free tool to aid debugging web based application. It can capture web traffic as well as reply packets, intercept calls and more.
    Wireshark A free tool to aid debugging network traffic. Generally I use this when Fiddler can't intercept the traffic and I need something a little further down the network stack for capturing traffic.
    dotPeek A free tool for decompiling .NET assembles by JetBrains. Can be used standalone or if you use Rider/Re-Sharper you can have the code auto-disassemble if you F12 into a compiled reference.
    Rambox Desktop app for managing all the various web based tools that you use. This one is more useful if you're a consultant with more than one client, as using Rambox you can organize all of your apps/sites into "workspaces" each with it's own set of stored credentials.
    mRemoteNG A free tool for managing connections to remote machines including RDP, SSH and Web interfaces. Linux alternative: Remmina
    WinMerge A free tool for comparing and merging files and folders. Linux alternative: Meld
    Conduktor A free GUI for inspecting the data in a Kafka instance
    Microsoft PowerToys Several useful extensions for Windows to increase productivity Linux alternatives: Ulauncher - like PowerToys Run
  • Published on
    I've had issues with both VS2017 and now VS2019 where applying my custom fonts/colour scheme is not maintained between sessions. The same trick worked in VS2019 as what I discovered in VS2017, so this time I'm blogging it! Basically, import your custom colour scheme as usual using the "Import and Export Settings" wizard. Now go to Tools > Options > General and switch the "Color Theme" to any other theme than the current one. Now switch the theme back. That's it! For some reason this seems to persist your customisation of the theme whereas without switching themes the changes get lost.
  • Published on
    XML is pretty old tech and without a schema is a bit of a pain to work with! A semi saving grace is using Visual Studio's "Paste XML as Classes" option (Paste Special) which will generate C# classes capable of representing the XML you had on the clipboard (using the XmlSerializer). However the caveat to this is that it only generates code for the exact xml you have used, so any optional attributes/elements or collections that only have 1 item in them will be generated incorrectly and will silently start dropping information when you deserialize another file with slightly different xml content. To combat this, I wrote a simple XmlSchemaChecker class which takes the content of an XML file and it's deserialized equivalent and ensures that every piece of data from the file is represented within the instance. It logs these problems when running with Debug logging enabled and is called from the class responsible for deserializing files.
    
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Xml;
    using System.Xml.Serialization;
    using Microsoft.Extensions.Logging;
    
    namespace Deserialization
    {
        public class XmlSchemaChecker : IXmlSchemaChecker
        {
            private readonly ILogger<XmlSchemaChecker> _logger;
    
            public XmlSchemaChecker(ILogger<XmlSchemaChecker> logger)
            {
                _logger = logger ?? throw new ArgumentNullException(nameof(logger));
            }
    
            public void LogSchemaWarnings<T>(string originalXmlFilePath, T deserialized)
            {
                if (!_logger.IsEnabled(LogLevel.Debug)) return;
    
                var originalXml = File.ReadAllText(originalXmlFilePath);
                var newXml = ReSerialize(deserialized);
    
                var originalValues = GetXmlValues(originalXml);
                var newValues = GetXmlValues(newXml);
    
                var missingItems = originalValues.Except(newValues).ToList();
    
                if (missingItems.Any())
                {
                    _logger.LogDebug("Schema for {filename} was not fully deserialized. Missing items: {missingItems}", originalXmlFilePath, missingItems);
                }
            }
    
            private static void ProcessNodes(ISet<string> values, Stack<string> paths, IEnumerable nodes)
            {
                foreach (var node in nodes)
                {
                    switch (node)
                    {
                        case XmlDeclaration _:
                            continue;
                        case XmlElement element:
                            {
                                paths.Push(element.Name);
    
                                foreach (var att in element.Attributes)
                                {
                                    if (att is XmlAttribute xmlAttribute && xmlAttribute.Name != "xmlns:xsd" && xmlAttribute.Name != "xmlns:xsi")
                                    {
                                        values.Add($"{string.Join(":", paths.Reverse())}:{xmlAttribute.Name}:{CleanseValue(xmlAttribute.Value)}");
                                    }
                                }
    
                                if (element.HasChildNodes)
                                {
                                    ProcessNodes(values, paths, element.ChildNodes);
                                }
    
                                paths.Pop();
                                break;
                            }
                        case XmlText text:
                            {
                                values.Add($"{string.Join(":", paths.Reverse())}:{text.ParentNode.Name}:{CleanseValue(text.InnerText)}");
                                break;
                            }
                    }
                }
            }
    
            private static string CleanseValue(string value)
            {
                return value.Replace("\r\n", "\n").Replace("\t", "").Trim(' ', '\n');
            }
    
            private static IEnumerable<string> GetXmlValues(string xml)
            {
                var values = new HashSet<string>();
                var paths = new Stack<string>();
                var doc = new XmlDocument();
                doc.LoadXml(xml);
    
                ProcessNodes(values, paths, doc.ChildNodes);
    
                return values;
            }
    
            private static string ReSerialize<T>(T item)
            {
                var xmlSerializer = new XmlSerializer(typeof(T));
                var output = new System.Text.StringBuilder();
    
                using (var outputStream = new StringWriter(output))
                {
                    xmlSerializer.Serialize(outputStream, item);
                }
    
                return output.ToString();
            }
        }
    }