diff --git a/.gitignore b/.gitignore index 4ce6fdd..804e333 100644 --- a/.gitignore +++ b/.gitignore @@ -337,4 +337,5 @@ ASALocalRun/ .localhistory/ # BeatPulse healthcheck temp database -healthchecksdb \ No newline at end of file +healthchecksdb +/TA.Utils.Logging.Nlog/TA.Utils.Logging.Nlog.xml diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..5c7247b --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,7 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..7c6dd3d --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,37 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "shell", + "args": [ + "build", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + }, + { + "label": "clean", + "command": "dotnet", + "type": "shell", + "args": [ + "clean", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": [] + } + ] +} \ No newline at end of file diff --git a/Push-Packages.ps1 b/Push-Packages.ps1 index bec31f8..7725f34 100644 --- a/Push-Packages.ps1 +++ b/Push-Packages.ps1 @@ -2,6 +2,12 @@ # Assumes that the API key for the relevant feeds has been installed in NuGet. # Searches the current directory and child directories recursively. +param ( + [string]$ApiKey = $null + ) + +if ($ApiKey) { $setApiKey = "-ApiKey " + $ApiKey } + $packageFeed = "https://www.myget.org/F/tigra-astronomy/api/v2/package" $symbolFeed = "https://www.myget.org/F/tigra-astronomy/api/v3/index.json" @@ -11,10 +17,10 @@ foreach ($package in $releasePackages) { if ($package.Name -like "*.snupkg") { - NuGet.exe push -Source $symbolFeed $package + NuGet.exe push -Source $symbolFeed $package $setApiKey } else { - NuGet.exe push -Source $packageFeed $package + NuGet.exe push -Source $packageFeed $package $setApiKey } } diff --git a/ReadMe.md b/ReadMe.md new file mode 100644 index 0000000..e11687e --- /dev/null +++ b/ReadMe.md @@ -0,0 +1,288 @@ +# Tigra Astronomy Commonly Used Helpers and Utilities # + +This library represents a collection of classes factored out of our production projects, that we found were being used over and over again. +Rather than re-using the code at source level, it is now collected together in this package as a general purpose reusable library and made freely available for you to use. + +This was always the promise of _Object Oriented Design_, but it was not until the advent of [NuGet][nuget] and its widespread adoption that this became a practical reality. +It is easy to overlook the impact of [NuGet][nuget], as it seems so obvious and natural once you've used it. + +> Dependency management is the key challenge in software at every scale +> **Donald Knuth**, _The Art of Computer Programming_ + +NuGet has essentially solved a large chunk of the dependency management problem. +At Tigra Astronomy, we use NuGet it as a key component in our software design strategy. +We publish our open source code on a [public MyGet feed][myget]. +We push both prerelease and release versions to [MyGet][myget]. +When we make an official release, we promote that package from [MyGet][myget] to [NuGet][nuget]. +You can consume our packages from either location, but if you want betas and release candidates, then you'll need to use [our MyGet feed][myget]. + +## Licensing ## + +This software is released under the [Tigra MIT License][mit], which (in summary) means: +"Anyone can do anything at all with this software without limitation, but it's not our fault if anything goes wrong". + +Our [philosophy of open source][yt-oss] is to [give wholeheartedly with no strings attached][yt-oss]. +We have no time for "copyleft" licenses which we find irksome. + +So here it is, for you to use however you like, no strings attached. + +I tend to use "we" and "our" when talking about the company, but Tigra Astronomy is a one-man operation run by me, Tim Long. +I hope you find the software useful, and if you feel that my efforts are worth supporting, then it would make my day if you would [buying me some coffee][coffee]. +I also wouldn't mind you giving us a mention, if you feel you are able to, as it helps the company grow. Donations and mentions really make a difference, so please think about it and do what you can. + +If you are a company and need some work done, then consider hiring me as a freelance developer. I have decades of experience in product design, firmware development for embedded systems and PC driver and software development. I'm a professional; I believe in doing what's right, not what's expedient and I support my software. + +## Description of Classes ## + +### Versioning ### + +Tigra Astronomy has settled on a versioning strategy based on [Semantic Versioning 2.0.0][semver]. + +We give all of our software a semantic version, which we display to the user in the About box and write out to log files on startup. +We use [GitVersion][gitversion] to [automatically assign a version number to every build][yt-gitversion] (even in [Arduino projects][yt-gitversion-arduino]). +We never manually set the version number, it happens as part of the build process. +So we can never forget to "bump the version" and we can never forget to set it. +Total automation. +If you examine one of our log files, you may well find something like this: + +``` lang=log +21:16:59.2909|INFO |Server |Git Commit ID: "229c1acc4a7bda494f78a8c7cc811c2a4d8e9132" +21:16:59.3069|INFO |Server |Git Short ID: "229c1ac" +21:16:59.3069|INFO |Server |Commit Date: "2020-07-11" +21:16:59.3069|INFO |Server |Semantic version: "0.1.0-alpha.1" +21:16:59.3069|INFO |Server |Full Semantic version: "0.1.0-alpha.1" +21:16:59.3069|INFO |Server |Build metadata: "Branch.develop.Sha.229c1acc4a7bda494f78a8c7cc811c2a4d8e9132" +21:16:59.3069|INFO |Server |Informational Version: "0.1.0-alpha.1+Branch.develop.Sha.229c1acc4a7bda494f78a8c7cc811c2a4d8e9132" +``` + +There's no mistaking where that build came from. + +Our `GitVersion` class contains static properties for getting your semantic version metadata at runtime. We use it to write the log entries as shown above. + +### Readability and Intention-revealing Code ### + +#### Maybe? #### + +One of the most insideous bug producers in .NET code is the null value. +Do you return `null` to mean "no value"? +What's the caller supposed to do with that? +Did you mean there was an error? +Did you mean there wans't an error but you can't give an answer? +Did you mean the answer was empty? +Or did someone just forget to initialize the variable? + +The ambiguity around "error" vs. "no value" is why we created `Maybe`. + +`Maybe` is a type that either has a value, or doesn't, but it is never null. +The idea is that by using a `Maybe` you clearly communicate your intentions to the caller. +By returning `Maybe` you nail down the ambiguity: "there might not be a value and you have to check". + +Strictly, a `Maybe` is an `IEnumerable` which is either empty (no value) or has exactly one element. +Because it is `IEnumerable` you can use certain LINQ operators: + +- `maybe.Any()` will be true if there is a value. +- `maybe.Single()` gets you the value. +- `maybe.SingleOrDefault()` gets you the value or `null`. +- extension method `maybe.None()` is `true` if there's no value. + +Creating a maybe can be done by: + +- `object.AsMaybe();` - convert a reference type. +- `Maybe.From(7);` - works with value types. +- `Maybe.Empty` - a maybe without a value. + +`Maybe` has a `ToString()` method so you can write it to a stream or use in a string interpolation, and you will get the serialized value or "`{no value}`". + +Try returning a `Maybe` whenever you have a situation wehere there may be no value, instead of `null`. +You may find that your bug count tends to diminish. + +### Bit Manipulation ### + +#### Octet #### + +An `Octet` is an immutable type that represents 8 bits, or a byte. +In most cases, it can be directly used in place of a `byte` as there are implicit conversions to and from a `byte`. +There are also explicit conversions to and from `int` and `uint`. +The latter are explicit because there is potentially data loss, so use with care. + +In an `Octet`, each bit position is directly addressable. +You can access `octet[0]` through `octet[7]`. + +You can set a bit with `octet.WithBitSetTo(n, state)`. +Remember `Octet` is immutable so this gives you a new `Octet` and leaves the original unchanged. + +You can perform logical bitwise operations using the `&` anf `|` operators. + +### Diagnostics ### + +#### ASCII Mnemonic Expansion #### + +When dealing with streams of ASCII-encoded data, it is often helpful to be able to see non-printing and white space characters. +This is especially useful when logging. +The `ExpandAscii()` extension method makes this simple. +Us `string.ExpansAscii()` and cahacters such as carriage return, for example, will be rendered as `` instead of causing an ugly line break in your log output. + +`ExpandAscii()` uses the mnemonics defined in the `AsciiSymbols` enumerated type. + +#### Display Equivalence for Enumerated Types #### + +The `[DisplayEquivalent("text")]` Attribute works with the `EnumExtensions.DisplayEquivalent()` extension method. +This can be useful for building drop-down lists and Combo box contents for enumerated types. +You can always get the equivalent human-readable display text for an enumerated value using `value.DisplayEquivalent()`. +This will return the display text if it has been set, or the name of the enum value otherwise. +Set the display text by dropping a `[DisplayEquivalent("text")]` attribute on each field of the enum. + +### Asynchrony and Threading ### + +#### ConfigureAwait #### + +There is an extension method in .NET used to configure awaitable tasks, called `ConfigureAwait(bool)`. +THe method affects how the task awaiter schedules its continuation. +With `ConfigureAwait(true)` the tasks continues on the current synchronization context. +That usually means on the same thread, and is particularly relevant when the awaiter is a user interface thread. +Conversely, `ConfigureAwait(false)` means that continuation can happen on any thread, and usually that will be a thread pool worker thread. +The implications are quite profound. Consider the following method: + +``` lang=cs +public async Task SomeMethod() + { + Console.WriteLine("Starting on thread {0}", Thread.CurrentThread.ManagedThreadId); + await Task.Delay(1000).ConfigureAwait(false); + Console.WriteLine("Continuing on thread {0}", Thread.CurrentThread.ManagedThreadId); + } +``` + +When you run this, you may get something like + +> Starting on thread 14 +> Continuing on thread 11 + +But it is not at all ovious how `ConfigureAwait()` should be used. +What if you don't specifiy? +Is the await configured or unconfigured? +Does `ConfigureAwait(false)` mean you don't want to configure it, or that you want to configure it not to do something? +It's just horrible, you can't read the code and instantly understand what it does, and that violates the _Principle of Least Astonishment_. + +So we made some extension methods that essentially do the same thing, but make more sense. Our aync method now becomes: + +``` lang=cs +public async Task SomeMethod() + { + Console.WriteLine("Starting on thread {0}", Thread.CurrentThread.ManagedThreadId); + await Task.Delay(1000).ContinueOnAnyThread(); + Console.WriteLine("Continuing on thread {0}", Thread.CurrentThread.ManagedThreadId); + } +``` + +and we get + +> Starting on thread 15 +> Continuing on thread 13 + +Alternatively: + +``` lang=cs +public async Task SomeMethod() + { + Console.WriteLine("Starting on thread {0}", Thread.CurrentThread.ManagedThreadId); + await Task.Delay(1000).ContinueInCurrentContext(); + Console.WriteLine("Continuing on thread {0}", Thread.CurrentThread.ManagedThreadId); + } +``` + +The await captures the current `SynchronizationContext` and uses it to schedule the continuation. +What happens next depends on the application model and how it implements `SynchronizationContext`. +For a user interface application, the UI generally runs in a Single Threaded Apartment (STA thread). +In this model, asynchronous operations are posted to the message queue of the STA thread. +The continuation will then happen on the UI thread once teh thread is idel and the message pump runs. +In a free-threaded application model such as a console application, the continuation will likely +still happen on a different thread. + +Here you can see the danger of this option. +If the continuation is queued in the message queue waiting for messages to be pumped, but the UI is blocked waiting for the task to complete, then the continuation may never get to run. +The task is prevented from completing and we are in deadlock. +Therefore, best practice for library writers is to always use `ContinueOnAnyThread()`. + +#### Cancel Culture #### + +One final extension method is `Task.WithCancellation(token)`. +This takes a task that is not cancellable and adds cancellation to it. +Note that this doesn't stop the task from running and it may still run to completion, +but it means you don't have to wait for it. + +#### Logging #### + +The `TA.Utils.Core.Diagnostics` namespace defines a pair of interfaces, `ILog` and `IFluentLogBuilder`, that define an abstract logging service. + +Libraries can perform logging through these interfaces without ever taking a dependency on any logging imnplementation. +The actual implementation can be injected at runtime, typically in a constructor parameter. +The policy decision about which logging engine to use can be taken in the top level composition root of the application. + +The fluent interface defined in `IFluentLogBuilder` was modeled on the NLog fluent interface, so it is a very natural fit. +However, the interface has enough flexibility to adapt to other logging backends without too much trouble.] + +A null implementation is provided in `DegenerateLoggerService` and `DegenerateLogBuilder`. +The two classes do essentially nothing and produce no output; they are a data sink. +Libraries can choose to use this as their default logging implementation, which is easier than checking +whether the logger is null every time it is used. + +The interface supports semantic logging. You can use a simple format string like so: + +``` lang=cs +log.Info().Message("Sending data {0}", data).Write(); +log.Error().Message("Exception {0} occurred with error code {1}", ex.Message, errorCode).Write(); +``` + +But this leaves useful information on the table. Extra rich information can be included like so: + +``` lang=cs +log.Info().Message("Sending data {data}", data).Write(); +log.Error() + .Message("Exception {exception} occurred with error code {error}") + .Property("exception", ex.Message) + .Property("error", errorCode) + .Exception(ex) + .Write(); +``` + +In both statements, we are adding property-value pairs to the log. +In the first `Log.Info()` statement this is implicit, whereas in the `Log.Error()` statement it is made explicit. +This extra information may or may not be used by the log renderer, but if its not there then it can't be used! +So if in doubt, include extra information where it is appropriate. + +Again, this feature set is native to NLog so makes for a very lightweight adaptor. +When developing adaptors for other logging frameworks, every attempt shouldbe made to preserve as much of the information as possible. + +### Two-stage Approach to Logging ### + +Think of logging as occurring in two distinct stages. + +1. You build the log entry using `IFluentLogBuilder`, adding all of the relevant information as _Properties_ of the log entry. +2. You send the log entry to the backend to be rendered. + +The renderer may use none, some or all of the information you provided and it may even augment it with additional metadata. +As a library developer, you shouldn't be concerned with how the entry will be rendered, stored or how it will be formatted. +You should concentrate only on including as much relevant information as is appropriate. + +Multiple renderers may be in use and different renderers will produce different output from the exact same log entry. +For example: + +- A file renderer may include a timestamp and perform log file rotation so that a new file is created each day. +- A debug output stream may include the name of the class where the log entry originated and print only the message portion. +- A console logger may write different lines in different colours accoring to the severity level. +- A syslog rendere may include the host name of the originating computer. +- A NoSQL database renderer may write out all of the properties as a JSON document. + +In most cases, the way in which log data is ultimately rendered is controlled by the application, often using a configuration file. +As a library developer, you must accept that you have little to no control over this. +Just concentrate in including appropriate and useful information and don't think about formatting or storage. + +[mit]: https://tigra.mit-license.org "Tigra MIT License" +[semver]: https://semver.org/ "the rules of semantic versioning" +[gitversion]: https://gitversion.net/docs/ "GitVersion documentation" +[nuget]: https://www.nuget.org/ "NuGet gallery" +[myget]: https://www.myget.org/feed/Packages/tigra-astronomy "Tigra Astronomy public package feed" +[yt-gitversion]: https://www.youtube.com/watch?v=8WKDk8yPMUA "Automatically versioning your code based on Git commit history" +[yt-gitversion-arduino]: https://www.youtube.com/watch?v=P4B6PTP6aAk "Automatic version in Arduino code with GitVersion" +[yt-oss]: https://www.youtube.com/watch?v=kloweL2fw7Q "Set your software free" +[coffee]: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=ARU8ANQKU2SN2&source=url "Support our open source projects with a donation" diff --git a/TA.Utils.Core/AsyncExtensions.cs b/TA.Utils.Core/AsyncExtensions.cs index db2cbd6..5f98a02 100644 --- a/TA.Utils.Core/AsyncExtensions.cs +++ b/TA.Utils.Core/AsyncExtensions.cs @@ -36,6 +36,7 @@ public static async Task WithCancellation(this Task task, CancellationT /// /// Configures a task to schedule its completion on any available thread. Use this when awaiting /// tasks in a user interface thread to avoid deadlock issues. + /// This is the recommended best practice for library writers. /// /// The type of the result. /// The task to configure. @@ -48,6 +49,7 @@ public static ConfiguredTaskAwaitable ContinueOnAnyThread(this /// /// Configures a task to schedule its completion on any available thread. Use this when awaiting /// tasks in a user interface thread to avoid deadlock issues. + /// This is the recommended best practice for library writers. /// /// The task to configure. /// An awaitable object that may schedule continuation on any thread. @@ -66,9 +68,29 @@ public static ConfiguredTaskAwaitable ContinueOnAnyThread(this Task task) /// The task. /// ConfiguredTaskAwaitable. /// + [Obsolete("Use ContinueInCurrentContext() instead")] public static ConfiguredTaskAwaitable ContinueOnCurrentThread(this Task task) { return task.ConfigureAwait(continueOnCapturedContext: true); } + + /// + /// Configures a task awaiter to schedule continuation on the captured synchronization context. + /// What happens next depends on the current synchronization context. + /// In a Single Threaded Apartment (STA thread) such as a UI thread, the continuation should + /// execute on the same thread. However in a free threaded context, the continuation can + /// still happen on a different thread. This can be risky when the awaiter is a single + /// threaded apartment (STA) thread. If the awaiter blocks waiting for the task, then + /// the continuation may never execute, preventign completion and resulting in deadlock. + /// Use with care. + /// + /// The task. + /// ConfiguredTaskAwaitable. + /// + + public static ConfiguredTaskAwaitable ContinueInCurrentContext(this Task task) + { + return task.ConfigureAwait(continueOnCapturedContext: true); + } } } \ No newline at end of file diff --git a/TA.Utils.Core/Diagnostics/DegenerateLogBuilder.cs b/TA.Utils.Core/Diagnostics/DegenerateLogBuilder.cs new file mode 100644 index 0000000..969f543 --- /dev/null +++ b/TA.Utils.Core/Diagnostics/DegenerateLogBuilder.cs @@ -0,0 +1,66 @@ +// This file is part of the TA.Ascom.ReactiveCommunications project +// +// Copyright © 2015-2020 Tigra Astronomy, all rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so. The Software comes with no warranty of any kind. +// You make use of the Software entirely at your own risk and assume all liability arising from your use thereof. +// +// File: DegenerateLogBuilder.cs Last modified: 2020-07-14@09:03 by Tim Long + +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace TA.Utils.Core.Diagnostics + { + /// + /// A degenerate log builder that does nothing and produces no output. + /// Can be used as a default log builder when logging is disabled. + /// Implements the + /// + /// + public sealed class DegenerateLogBuilder : IFluentLogBuilder + { + /// + public IFluentLogBuilder Exception(Exception exception) => this; + + /// + public IFluentLogBuilder LoggerName(string loggerName) => this; + + /// + public IFluentLogBuilder Message(string message) => this; + + /// + public IFluentLogBuilder Message(string format, params object[] args) => this; + + /// + public IFluentLogBuilder Message(IFormatProvider provider, string format, params object[] args) => this; + + /// + public IFluentLogBuilder Property(string name, object value) => this; + + /// + public IFluentLogBuilder Properties(IDictionary properties) => this; + + /// + public IFluentLogBuilder TimeStamp(DateTime timeStamp) => this; + + /// + public IFluentLogBuilder StackTrace(StackTrace stackTrace, int userStackFrame) => this; + + /// + public void Write(string callerMemberName = null, string callerFilePath = null, + int callerLineNumber = default) { } + + /// + public void WriteIf(Func condition, string callerMemberName = null, string callerFilePath = null, + int callerLineNumber = default) { } + + /// + public void WriteIf(bool condition, string callerMemberName = null, string callerFilePath = null, + int callerLineNumber = default) { } + } + } \ No newline at end of file diff --git a/TA.Utils.Core/Diagnostics/DegenerateLoggerService.cs b/TA.Utils.Core/Diagnostics/DegenerateLoggerService.cs new file mode 100644 index 0000000..e79f448 --- /dev/null +++ b/TA.Utils.Core/Diagnostics/DegenerateLoggerService.cs @@ -0,0 +1,43 @@ +// This file is part of the TA.Ascom.ReactiveCommunications project +// +// Copyright © 2015-2020 Tigra Astronomy, all rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so. The Software comes with no warranty of any kind. +// You make use of the Software entirely at your own risk and assume all liability arising from your use thereof. +// +// File: DegenerateLoggerService.cs Last modified: 2020-07-14@09:02 by Tim Long + +namespace TA.Utils.Core.Diagnostics + { + /// + /// This is the default logging service used if non is supplied by the user. The service does + /// nothing and produces no output. It is essentially "logging disabled". + /// + public sealed class DegenerateLoggerService : ILog + { + private static IFluentLogBuilder builder = new DegenerateLogBuilder(); + /// + public IFluentLogBuilder Trace(string callerFilePath = null) => builder; + + /// + public IFluentLogBuilder Debug(string callerFilePath = null) => builder; + + /// + public IFluentLogBuilder Info(string callerFilePath = null) => builder; + + /// + public IFluentLogBuilder Warn(string callerFilePath = null) => builder; + + /// + public IFluentLogBuilder Error(string callerFilePath = null) => builder; + + /// + public IFluentLogBuilder Fatal(string callerFilePath = null) => builder; + + /// + public void Shutdown() { } + } + } \ No newline at end of file diff --git a/TA.Utils.Core/Diagnostics/IFluentLogBuilder.cs b/TA.Utils.Core/Diagnostics/IFluentLogBuilder.cs new file mode 100644 index 0000000..25c2d3f --- /dev/null +++ b/TA.Utils.Core/Diagnostics/IFluentLogBuilder.cs @@ -0,0 +1,96 @@ +// This file is part of the TA.Ascom.ReactiveCommunications project +// +// Copyright © 2015-2020 Tigra Astronomy, all rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so. The Software comes with no warranty of any kind. +// You make use of the Software entirely at your own risk and assume all liability arising from your use thereof. +// +// File: IFluentLogBuilder.cs Last modified: 2020-07-14@07:01 by Tim Long + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace TA.Utils.Core.Diagnostics + { + /// + /// Fluent Log Builder + /// + public interface IFluentLogBuilder + { + /// + /// Add an exception to the log entry. + /// + IFluentLogBuilder Exception(Exception exception); + + /// + /// Set the log (source) name. By default, this is the name of the source file. + /// + IFluentLogBuilder LoggerName(string loggerName); + + /// + /// Sets the message template for the log entry. + /// The message may be a simple plain text string, + /// it may contain numbered substitution tokens like string.Format, + /// or it may contain named substitution tokens enclosed in {braces} + /// + IFluentLogBuilder Message(string message); + + /// + /// Sets the message template and property values for the log entry. + /// The format string may use numbered positional placeholders like string.Format, + /// or it may contain named substitution tokens enclosed in {braces}. + /// + IFluentLogBuilder Message(string format, params object[] args); + + /// + /// Sets the message template and property values for the log entry. + /// The format provider will be used when rendering the property values. + /// + IFluentLogBuilder Message(IFormatProvider provider, string format, params object[] args); + + /// + /// Adds a named property and value pair to the log entry. + /// + IFluentLogBuilder Property(string name, object value); + + /// + /// Adds a collection of property/value pairs to the log entry. + /// + IFluentLogBuilder Properties(IDictionary properties); + + /// + /// Sets the time stamp of the log entry. + /// If not set, the log entry will be timed at the moment Write() was called. + /// + IFluentLogBuilder TimeStamp(DateTime timeStamp); + + /// + /// Adds a stack trace to the log entry. + /// + IFluentLogBuilder StackTrace(StackTrace stackTrace, int userStackFrame); + + /// + /// Writes the log entry. + /// + void Write([CallerMemberName] string callerMemberName = null, [CallerFilePath] string callerFilePath = null, + [CallerLineNumber] int callerLineNumber = default); + + /// + /// Writes the log entry if the supplied predicate is true. + /// + void WriteIf(Func condition, [CallerMemberName] string callerMemberName = null, + [CallerFilePath] string callerFilePath = null, [CallerLineNumber] int callerLineNumber = default); + + /// + /// Writes the log entry if the boolean condition is true. + /// + void WriteIf(bool condition, [CallerMemberName] string callerMemberName = null, + [CallerFilePath] string callerFilePath = null, [CallerLineNumber] int callerLineNumber = default); + } + } \ No newline at end of file diff --git a/TA.Utils.Core/Diagnostics/ILog.cs b/TA.Utils.Core/Diagnostics/ILog.cs new file mode 100644 index 0000000..0a75423 --- /dev/null +++ b/TA.Utils.Core/Diagnostics/ILog.cs @@ -0,0 +1,70 @@ +// This file is part of the TA.Ascom.ReactiveCommunications project +// +// Copyright © 2015-2020 Tigra Astronomy, all rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so. The Software comes with no warranty of any kind. +// You make use of the Software entirely at your own risk and assume all liability arising from your use thereof. +// +// File: ILog.cs Last modified: 2020-07-14@06:59 by Tim Long + +using System.Runtime.CompilerServices; + +namespace TA.Utils.Core.Diagnostics + { + /// + /// Logging service interface + /// + public interface ILog + { + /// + /// Creates a log builder for a log entry with Trace severity. + /// + /// The caller file path. + /// IFluentLogBuilder. + IFluentLogBuilder Trace([CallerFilePath] string callerFilePath = null); + + /// + /// Debugs the specified caller file path. + /// + /// The caller file path. + /// IFluentLogBuilder. + IFluentLogBuilder Debug([CallerFilePath] string callerFilePath = null); + + /// + /// Informations the specified caller file path. + /// + /// The caller file path. + /// IFluentLogBuilder. + IFluentLogBuilder Info([CallerFilePath] string callerFilePath = null); + + /// + /// Warnings the specified caller file path. + /// + /// The caller file path. + /// IFluentLogBuilder. + IFluentLogBuilder Warn([CallerFilePath] string callerFilePath = null); + + /// + /// Errors the specified caller file path. + /// + /// The caller file path. + /// IFluentLogBuilder. + IFluentLogBuilder Error([CallerFilePath] string callerFilePath = null); + + /// + /// Fatals the specified caller file path. + /// + /// The caller file path. + /// IFluentLogBuilder. + IFluentLogBuilder Fatal([CallerFilePath] string callerFilePath = null); + + /// + /// Instructs the logging service to shut down. + /// This should flush any buffered log entries and close any open files or streams. + /// + void Shutdown(); + } + } \ No newline at end of file diff --git a/TA.Utils.Core/TA.Utils.Core.csproj b/TA.Utils.Core/TA.Utils.Core.csproj index 31c1916..1b546dc 100644 --- a/TA.Utils.Core/TA.Utils.Core.csproj +++ b/TA.Utils.Core/TA.Utils.Core.csproj @@ -5,9 +5,9 @@ true Tim Long Tigra Astronomy - TA.Utils + TA.Utilities A collection of commonly used utility and helper classes - Copyright 2020 Tigra Astronomy, all rights reserved + Copyright © 2015-2020 Tigra Astronomy, all rights reserved MIT https://github.com/Tigra-Astronomy/TA.Utilities https://github.com/Tigra-Astronomy/TA.Utilities @@ -23,7 +23,7 @@ snupkg - + TA.Utils.Core.xml diff --git a/TA.Utils.Core/TA.Utils.Core.xml b/TA.Utils.Core/TA.Utils.Core.xml index b304b1a..e7431dd 100644 --- a/TA.Utils.Core/TA.Utils.Core.xml +++ b/TA.Utils.Core/TA.Utils.Core.xml @@ -43,6 +43,7 @@ Configures a task to schedule its completion on any available thread. Use this when awaiting tasks in a user interface thread to avoid deadlock issues. + This is the recommended best practice for library writers. The type of the result. The task to configure. @@ -52,6 +53,7 @@ Configures a task to schedule its completion on any available thread. Use this when awaiting tasks in a user interface thread to avoid deadlock issues. + This is the recommended best practice for library writers. The task to configure. An awaitable object that may schedule continuation on any thread. @@ -68,6 +70,217 @@ ConfiguredTaskAwaitable. + + + Configures a task awaiter to schedule continuation on the captured synchronization context. + What happens next depends on the current synchronization context. + In a Single Threaded Apartment (STA thread) such as a UI thread, the continuation should + execute on the same thread. However in a free threaded context, the continuation can + still happen on a different thread. This can be risky when the awaiter is a single + threaded apartment (STA) thread. If the awaiter blocks waiting for the task, then + the continuation may never execute, preventign completion and resulting in deadlock. + Use with care. + + The task. + ConfiguredTaskAwaitable. + + + + + A degenerate log builder that does nothing and produces no output. + Can be used as a default log builder when logging is disabled. + Implements the + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This is the default logging service used if non is supplied by the user. The service does + nothing and produces no output. It is essentially "logging disabled". + + + + + + + + + + + + + + + + + + + + + + + + + + Fluent Log Builder + + + + + Add an exception to the log entry. + + + + + Set the log (source) name. By default, this is the name of the source file. + + + + + Sets the message template for the log entry. + The message may be a simple plain text string, + it may contain numbered substitution tokens like string.Format, + or it may contain named substitution tokens enclosed in {braces} + + + + + Sets the message template and property values for the log entry. + The format string may use numbered positional placeholders like string.Format, + or it may contain named substitution tokens enclosed in {braces}. + + + + + Sets the message template and property values for the log entry. + The format provider will be used when rendering the property values. + + + + + Adds a named property and value pair to the log entry. + + + + + Adds a collection of property/value pairs to the log entry. + + + + + Sets the time stamp of the log entry. + If not set, the log entry will be timed at the moment Write() was called. + + + + + Adds a stack trace to the log entry. + + + + + Writes the log entry. + + + + + Writes the log entry if the supplied predicate is true. + + + + + Writes the log entry if the boolean condition is true. + + + + + Logging service interface + + + + + Creates a log builder for a log entry with Trace severity. + + The caller file path. + IFluentLogBuilder. + + + + Debugs the specified caller file path. + + The caller file path. + IFluentLogBuilder. + + + + Informations the specified caller file path. + + The caller file path. + IFluentLogBuilder. + + + + Warnings the specified caller file path. + + The caller file path. + IFluentLogBuilder. + + + + Errors the specified caller file path. + + The caller file path. + IFluentLogBuilder. + + + + Fatals the specified caller file path. + + The caller file path. + IFluentLogBuilder. + + + + Instructs the logging service to shut down. + This should flush any buffered log entries and close any open files or streams. + + When applied to an enum member or field, specifies a string that should be used for display diff --git a/TA.Utils.Logging.NLog.SampleConsoleApp/NLog.config b/TA.Utils.Logging.NLog.SampleConsoleApp/NLog.config new file mode 100644 index 0000000..cb97530 --- /dev/null +++ b/TA.Utils.Logging.NLog.SampleConsoleApp/NLog.config @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${pad:padding=-5:inner=${uppercase:${level}}}|${pad:padding=-16:fixedLength=true:alignmentOnTruncation=right:inner=${callsite:className=true:fileName=false:includeSourcePath=false:methodName=false:includeNamespace=false}}| ${message} + + + + + + + + + \ No newline at end of file diff --git a/TA.Utils.Logging.NLog.SampleConsoleApp/Program.cs b/TA.Utils.Logging.NLog.SampleConsoleApp/Program.cs new file mode 100644 index 0000000..9e50547 --- /dev/null +++ b/TA.Utils.Logging.NLog.SampleConsoleApp/Program.cs @@ -0,0 +1,77 @@ +// This file is part of the TA.Utils project +// Copyright © 2016-2020 Tigra Astronomy, all rights reserved. +// File: Program.cs Last modified: 2020-07-16@19:24 by Tim Long + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using TA.Utils.Core; + +namespace TA.Utils.Logging.NLog.SampleConsoleApp + { + /// + /// This program demonstrates how to use the logging service + /// and examples of structured or semantic logging. Traditional logging usually + /// involves writing out a simple string, but with semantic logging we can + /// include all sorts of data in every log event. For example, when logging an + /// exception the full exception data including the stack trace is saved to the log. + /// In this example we are using the NLog back-end and sending the log entries + /// to the console (with color highlighting) as well as to a cloud log collector. + /// NLog is configured in the NLog.config file. + /// + class Program + { + static readonly List SuperstitiousNumbers = new List {13, 7, 666, 3, 8, 88, 888}; + + async static Task Main(string[] args) + { + var log = new LoggingService(); + log.Info() + .Message("Application stating - version {Version}", GitVersion.GitInformationalVersion) + .Property("SemVer", GitVersion.GitFullSemVer) + .Property("GitCommit", GitVersion.GitCommitSha) + .Property("CommitDate", GitVersion.GitCommitDate) + .Write(); + var seed = DateTime.Now.Millisecond; + var gameOfChance = new Random(seed); + log.Debug().Property("seed",seed).Write(); + + for (int i = 0; i < 1000; i++) + { + try + { + log.Debug().Message("Starting iteration {iteration}", i).Write(); + + /* + * The program doesn't like numbers associated with superstition, + * and will flag them up as warnings. + */ + if (SuperstitiousNumbers.Contains(i)) + { + throw new SuperstitiousNumberException($"Skipping {i} because it is a superstitious number"); + } + + // There's a small chance of a random "failure" + if (gameOfChance.Next(100) < 3) + throw new ApplicationException("Random failure"); + } + catch (SuperstitiousNumberException ex) + { + log.Warn() + .Message("Superstitious looking number: {number}", i) + .Exception(ex) + .Property("SuperstitiousNumbers", SuperstitiousNumbers) + .Write(); + } + catch (ApplicationException ae) + { + log.Error().Exception(ae).Message("Failed iteration {iteration}", i).Write(); + } + await Task.Delay(TimeSpan.FromMilliseconds(1000)); + log.Debug().Message("Finished iteration {iteration}", i).Write(); + } + log.Info().Message("Program terminated").Write(); + log.Shutdown(); + } + } + } \ No newline at end of file diff --git a/TA.Utils.Logging.NLog.SampleConsoleApp/SuperstitiousNumberException.cs b/TA.Utils.Logging.NLog.SampleConsoleApp/SuperstitiousNumberException.cs new file mode 100644 index 0000000..8687bd7 --- /dev/null +++ b/TA.Utils.Logging.NLog.SampleConsoleApp/SuperstitiousNumberException.cs @@ -0,0 +1,30 @@ +// This file is part of the TA.Utils project +// Copyright © 2016-2020 Tigra Astronomy, all rights reserved. +// File: SuperstitiousNumberException.cs Last modified: 2020-07-16@20:38 by Tim Long + +using System; +using System.Runtime.Serialization; + +namespace TA.Utils.Logging.NLog.SampleConsoleApp + { + [Serializable] + public class SuperstitiousNumberException : Exception + { + // + // For guidelines regarding the creation of new exception types, see + // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpgenref/html/cpconerrorraisinghandlingguidelines.asp + // and + // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dncscol/html/csharp07192001.asp + // + + public SuperstitiousNumberException() { } + + public SuperstitiousNumberException(string message) : base(message) { } + + public SuperstitiousNumberException(string message, Exception inner) : base(message, inner) { } + + protected SuperstitiousNumberException( + SerializationInfo info, + StreamingContext context) : base(info, context) { } + } + } \ No newline at end of file diff --git a/TA.Utils.Logging.NLog.SampleConsoleApp/TA.Utils.Logging.NLog.SampleConsoleApp.csproj b/TA.Utils.Logging.NLog.SampleConsoleApp/TA.Utils.Logging.NLog.SampleConsoleApp.csproj new file mode 100644 index 0000000..548adc7 --- /dev/null +++ b/TA.Utils.Logging.NLog.SampleConsoleApp/TA.Utils.Logging.NLog.SampleConsoleApp.csproj @@ -0,0 +1,27 @@ + + + + Exe + netcoreapp3.1 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + Always + + + + diff --git a/TA.Utils.Logging.Nlog/AssemlyAttributes.cs b/TA.Utils.Logging.Nlog/AssemlyAttributes.cs new file mode 100644 index 0000000..c978bea --- /dev/null +++ b/TA.Utils.Logging.Nlog/AssemlyAttributes.cs @@ -0,0 +1,7 @@ +// This file is part of the TA.Utils project +// Copyright © 2016-2020 Tigra Astronomy, all rights reserved. +// File: AssemlyAttributes.cs Last modified: 2020-07-17@04:24 by Tim Long + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("TA.Utils.Specifications")] \ No newline at end of file diff --git a/TA.Utils.Logging.Nlog/LogBuilder.cs b/TA.Utils.Logging.Nlog/LogBuilder.cs new file mode 100644 index 0000000..fc091c8 --- /dev/null +++ b/TA.Utils.Logging.Nlog/LogBuilder.cs @@ -0,0 +1,167 @@ +// This file is part of the TA.LoggingService project +// Copyright © 2016-2020 Tigra Astronomy, all rights reserved. +// File: NLogLogBuilder.cs Last modified: 2020-07-14@03:27 by Tim Long + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using NLog; +using TA.Utils.Core.Diagnostics; + +namespace TA.Utils.Logging.NLog +{ + internal sealed class LogBuilder : IFluentLogBuilder + { + private readonly LogEventInfo logEvent; + private readonly ILogger logger; + + public LogBuilder(ILogger logger, LogLevel level) + { + if (logger == null) + throw new ArgumentNullException(nameof(logger)); + if (level == null) + throw new ArgumentNullException(nameof(level)); + this.logger = logger; + logEvent = new LogEventInfo { LoggerName = logger.Name, Level = level }; + } + + /// + public IFluentLogBuilder Exception(Exception exception) + { + logEvent.Exception = exception; + return this; + } + + /// + public IFluentLogBuilder LoggerName(string loggerName) + { + logEvent.LoggerName = loggerName; + return this; + } + + /// + public IFluentLogBuilder Message(string message) + { + logEvent.Message = message; + return this; + } + + /// + public IFluentLogBuilder Message(string format, params object[] args) + { + logEvent.Message = format; + logEvent.Parameters = args; + return this; + } + + /// + public IFluentLogBuilder Message(IFormatProvider provider, string format, params object[] args) + { + logEvent.FormatProvider = provider; + logEvent.Message = format; + logEvent.Parameters = args; + return this; + } + + /// + public IFluentLogBuilder Property(string name, object value) + { + logEvent.Properties.Add(name, value); + return this; + } + + /// + public IFluentLogBuilder Properties(IDictionary properties) + { + var logProperties = properties.Select(p => new KeyValuePair(p.Key, p.Value)); + foreach (var keyValuePair in logProperties) + { + logEvent.Properties.Add(keyValuePair); + } + return this; + } + + /// + public IFluentLogBuilder Property(object name, object value) + { + if (name == null) + throw new ArgumentNullException(nameof(name)); + logEvent.Properties[name] = value; + return this; + } + + /// + public IFluentLogBuilder Properties(IDictionary properties) + { + if (properties == null) + throw new ArgumentNullException(nameof(properties)); + foreach (var key in properties.Keys) + { + logEvent.Properties[key] = properties[key]; + } + return this; + } + + /// + public IFluentLogBuilder TimeStamp(DateTime timeStamp) + { + logEvent.TimeStamp = timeStamp; + return this; + } + + /// + public IFluentLogBuilder StackTrace(StackTrace stackTrace, int userStackFrame) + { + logEvent.SetStackTrace(stackTrace, userStackFrame); + return this; + } + + /// + public void Write([CallerMemberName] string callerMemberName = null, + [CallerFilePath] string callerFilePath = null, + [CallerLineNumber] int callerLineNumber = default) + { + if (!logger.IsEnabled(logEvent.Level)) return; + SetCallerInfo(callerMemberName, callerFilePath, callerLineNumber); + logger.Log(logEvent); + } + + /// + public void WriteIf(Func condition, + [CallerMemberName] string callerMemberName = null, + [CallerFilePath] string callerFilePath = null, + [CallerLineNumber] int callerLineNumber = 0) + { + if (condition == null || !condition() || !logger.IsEnabled(logEvent.Level)) + return; + SetCallerInfo(callerMemberName, callerFilePath, callerLineNumber); + logger.Log(logEvent); + } + + /// + public void WriteIf(bool condition, + [CallerMemberName] string callerMemberName = null, + [CallerFilePath] string callerFilePath = null, + [CallerLineNumber] int callerLineNumber = 0) + { + if (condition == false || !logger.IsEnabled(logEvent.Level)) + return; + SetCallerInfo(callerMemberName, callerFilePath, callerLineNumber); + logger.Log(logEvent); + } + + private void SetCallerInfo(string callerMethodName, string callerFilePath, int callerLineNumber) + { + if (callerMethodName != null || callerFilePath != null || callerLineNumber != 0) + logEvent.SetCallerInfo(null, callerMethodName, callerFilePath, callerLineNumber); + } + + /// + /// Builds and returns the without writing it to the log. + /// + internal LogEventInfo Build() => logEvent; + } +} \ No newline at end of file diff --git a/TA.Utils.Logging.Nlog/LoggingService.cs b/TA.Utils.Logging.Nlog/LoggingService.cs new file mode 100644 index 0000000..ae51c52 --- /dev/null +++ b/TA.Utils.Logging.Nlog/LoggingService.cs @@ -0,0 +1,76 @@ +// This file is part of the TA.Utils project +// Copyright © 2016-2020 Tigra Astronomy, all rights reserved. +// File: LoggingService.cs Last modified: 2020-07-16@20:09 by Tim Long + +using System.IO; +using System.Runtime.CompilerServices; +using NLog; +using TA.Utils.Core.Diagnostics; + +namespace TA.Utils.Logging.NLog + { + /// + /// A logging service that uses NLog as the back-end logger. Implements the + /// + /// + /// + public sealed class LoggingService : ILog + { + private static readonly ILogger DefaultLogger = LogManager.GetCurrentClassLogger(); + + /// Static initializer can be used to perform 1-time NLog configurations. + static LoggingService() + { + LogManager.AutoShutdown = true; + } + + /// + public IFluentLogBuilder Trace([CallerFilePath] string callerFilePath = null) + { + return CreateLogBuilder(LogLevel.Trace, callerFilePath); + } + + /// + public IFluentLogBuilder Debug([CallerFilePath] string callerFilePath = null) + { + return CreateLogBuilder(LogLevel.Debug, callerFilePath); + } + + /// + public IFluentLogBuilder Info([CallerFilePath] string callerFilePath = null) + { + return CreateLogBuilder(LogLevel.Info, callerFilePath); + } + + /// + public IFluentLogBuilder Warn([CallerFilePath] string callerFilePath = null) + { + return CreateLogBuilder(LogLevel.Warn, callerFilePath); + } + + /// + public IFluentLogBuilder Error([CallerFilePath] string callerFilePath = null) + { + return CreateLogBuilder(LogLevel.Error, callerFilePath); + } + + /// + public IFluentLogBuilder Fatal([CallerFilePath] string callerFilePath = null) + { + return CreateLogBuilder(LogLevel.Fatal, callerFilePath); + } + + private IFluentLogBuilder CreateLogBuilder(LogLevel logLevel, string callerFilePath) + { + string name = !string.IsNullOrWhiteSpace(callerFilePath) + ? Path.GetFileNameWithoutExtension(callerFilePath) + : null; + var logger = string.IsNullOrWhiteSpace(name) ? DefaultLogger : LogManager.GetLogger(name); + var builder = new LogBuilder(logger, logLevel); + return builder; + } + + /// + public void Shutdown() => LogManager.Shutdown(); + } + } \ No newline at end of file diff --git a/TA.Utils.Logging.Nlog/ReadMe.md b/TA.Utils.Logging.Nlog/ReadMe.md new file mode 100644 index 0000000..140b1b0 --- /dev/null +++ b/TA.Utils.Logging.Nlog/ReadMe.md @@ -0,0 +1,45 @@ +# NLog Logging Service # + +This is a logging service implementation that uses *NLog* as the back-end. + +The fluent interface defined in `TA.Utils.Diagnostics.IFluentLogBuilder` was modeled on the NLog fluent interface, +so it is a very natural fit. +However, the interface has enough flexibility to adapr to other logging backends without too much trouble. + +NLog supports semantic logging. You can use a simple format string like so: + +``` lang=cs +log.Info().Message("Sending data {0}", data).Write(); +log.Error().Message("Exception {0} occurred with error code {1}", ex.Message, errorCode).Write(); +``` + +But this leaves some functionality on the table. Extra rich information can be included like so: + +``` lang=cs +log.Info().Message("Sending data {data}", data).Write(); +log.Error() + .Message("Exception {exception} occurred with error code {error}") + .Property("exception", ex.Message) + .Property("error", errorCode) + .Exception(ex) + .Write(); +``` + +When using NLog, you never need to be concerned about output formatting or where the log messages will go. +This is all handled by NLog and the `NLog.config` file in the application directory. +There is a lot of flexibility in this approach. +As the developer, just concentrate on putting rich information into your log entries. + +By default, the log name (source) is set to the file name of the source file where the log entry is created. +This is fine for most purposes. +This behaviour can be everridden either by specifying the log name along with the level: + +``` lang=cs +log.Debug("Custom Source").Message("Hello log").Write(); +``` + +or by adding an explicit name entry when building the log entry: + +``` lang=cs +log.Debug().Message("Hello log").LoggerName("Custom Source").Write(); +``` diff --git a/TA.Utils.Logging.Nlog/TA.Utils.Logging.Nlog.csproj b/TA.Utils.Logging.Nlog/TA.Utils.Logging.Nlog.csproj new file mode 100644 index 0000000..ba1bcab --- /dev/null +++ b/TA.Utils.Logging.Nlog/TA.Utils.Logging.Nlog.csproj @@ -0,0 +1,40 @@ + + + + netstandard2.0 + true + 0.0.0 + Tim Long + Tigra Astronomy + TA.Utilities + A logging service that uses NLog as the back-end. + Copyright © 2020 Tigra Astronomy, all rights reserved + MIT + https://github.com/Tigra-Astronomy/TA.Utilities + https://github.com/Tigra-Astronomy/TA.Utilities + git + log,logging,nlog,service,utility + + + + true + snupkg + + + + TA.Utils.Logging.Nlog.xml + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/TA.Utils.Specifications/AsciiExpansionSpecs.cs b/TA.Utils.Specifications/AsciiExpansionSpecs.cs new file mode 100644 index 0000000..d74a128 --- /dev/null +++ b/TA.Utils.Specifications/AsciiExpansionSpecs.cs @@ -0,0 +1,34 @@ + +using Machine.Specifications; +using TA.Utils.Core; + +namespace TA.Utils.Specifications + { + [Subject(typeof(AsciiExtensions))] + internal class when_expanding_individual_characters + { + It should_expand_cr = () => '\r'.ExpandAscii().ShouldEqual(""); + It should_expand_lf = () => '\n'.ExpandAscii().ShouldEqual(""); + It should_expand_ff = () => '\f'.ExpandAscii().ShouldEqual(""); + It should_expand_htab = () => '\t'.ExpandAscii().ShouldEqual(""); + It should_expand_vtab = () => '\v'.ExpandAscii().ShouldEqual(""); + It should_expand_bell = () => '\a'.ExpandAscii().ShouldEqual(""); + It should_not_expand_space = () => ' '.ExpandAscii().ShouldEqual(" "); + } + + [Subject(typeof(AsciiExtensions), "printing characters")] + internal class when_expanding_a_string_containing_only_printing_characters + { + const string expected = + @"1234567890-=qwertyuiop[]\asdfghjkl;'zxcvbnm,./!@##$%^&*()_+QWERTYUIOP{}|ASDFGHJKL:""ZXCVBNM<>?"; + It should_not_expand = () => expected.ExpandAscii().ShouldEqual(expected); + } + + [Subject(typeof(AsciiExtensions), "whitespace characters")] + internal class when_expanding_a_string_containing_nonprinting_characters + { + const string source = "The\tquick brown fox\r\njumps over the lazy dog\a"; + const string expected = "Thequick brown foxjumps over the lazy dog"; + It should_replace_nonprinting_characters = () => source.ExpandAscii().ShouldEqual(expected); + } + } \ No newline at end of file diff --git a/TA.Utils.Specifications/NLogLogServiceSpecs.cs b/TA.Utils.Specifications/NLogLogServiceSpecs.cs new file mode 100644 index 0000000..0d3bbeb --- /dev/null +++ b/TA.Utils.Specifications/NLogLogServiceSpecs.cs @@ -0,0 +1,49 @@ +// This file is part of the TA.Utils project +// Copyright © 2016-2020 Tigra Astronomy, all rights reserved. +// File: NLogLogServiceSpecs.cs Last modified: 2020-07-17@04:55 by Tim Long + +using System.IO; +using System.Runtime.CompilerServices; +using Machine.Specifications; +using TA.Utils.Logging.NLog; + +internal class with_caller_info_context + { + protected static string CallerFileNameWithoutExtenstion([CallerFilePath] string callerFilePath = null) + { + const string unknown = "(unknown)"; + if (string.IsNullOrWhiteSpace(callerFilePath)) + return unknown; + var fileName = Path.GetFileNameWithoutExtension(callerFilePath); + return string.IsNullOrWhiteSpace(fileName) ? unknown : fileName; + } + } + +internal class when_building_a_default_logger : with_caller_info_context + { + Establish context = () => builder = (LogBuilder) new LoggingService().Info(); + It should_have_the_file_name = () => builder.Build().LoggerName.ShouldEqual(CallerFileNameWithoutExtenstion()); + static LogBuilder builder; + } + +internal class when_creating_a_named_logger : with_caller_info_context + { + Establish context = () => builder = (LogBuilder) new LoggingService().Info("Roger"); + It should_change_the_name = () => builder.Build().LoggerName.ShouldEqual("Roger"); + static LogBuilder builder; + } + +internal class when_creating_and_building_a_named_logger : with_caller_info_context + { + Establish context = () => builder = (LogBuilder) new LoggingService().Info("Roger"); + Because of = () => builder.LoggerName("Jim"); + It should_change_the_name = () => builder.Build().LoggerName.ShouldEqual("Jim"); + static LogBuilder builder; + } +internal class when_building_a_named_logger : with_caller_info_context + { + Establish context = () => builder = (LogBuilder) new LoggingService().Info(); + Because of = () => builder.LoggerName("Jim"); + It should_change_the_name = () => builder.Build().LoggerName.ShouldEqual("Jim"); + static LogBuilder builder; + } \ No newline at end of file diff --git a/TA.Utils.Specifications/TA.Utils.Specifications.csproj b/TA.Utils.Specifications/TA.Utils.Specifications.csproj index f805e32..3bf6560 100644 --- a/TA.Utils.Specifications/TA.Utils.Specifications.csproj +++ b/TA.Utils.Specifications/TA.Utils.Specifications.csproj @@ -6,10 +6,14 @@ + + + + diff --git a/TA.Utils.sln b/TA.Utils.sln index 8dcbcea..74bc82e 100644 --- a/TA.Utils.sln +++ b/TA.Utils.sln @@ -3,15 +3,28 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.30128.36 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TA.Utils.Core", "TA.Utils.Core\TA.Utils.Core.csproj", "{4D49A7E7-DC29-4292-8D19-0B2F9D24BDE6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TA.Utils.Core", "TA.Utils.Core\TA.Utils.Core.csproj", "{4D49A7E7-DC29-4292-8D19-0B2F9D24BDE6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TA.Utils.Specifications", "TA.Utils.Specifications\TA.Utils.Specifications.csproj", "{8B3A2531-3295-4037-95EC-0BAD0DB4E972}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TA.Utils.Specifications", "TA.Utils.Specifications\TA.Utils.Specifications.csproj", "{8B3A2531-3295-4037-95EC-0BAD0DB4E972}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{22935A98-C6AD-4424-B288-56C89E864321}" ProjectSection(SolutionItems) = preProject + .gitattributes = .gitattributes + .gitignore = .gitignore Push-Packages.ps1 = Push-Packages.ps1 + TA.Utils.sln.DotSettings = TA.Utils.sln.DotSettings + TA.Utils.v3.ncrunchsolution = TA.Utils.v3.ncrunchsolution EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TA.Utils.Logging.Nlog", "TA.Utils.Logging.Nlog\TA.Utils.Logging.Nlog.csproj", "{57DD1805-E4A4-4D7C-94CD-E01498222640}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Documentation", "Documentation", "{00332C45-7CC4-4B40-A7CC-A7578524E16E}" + ProjectSection(SolutionItems) = preProject + ReadMe.md = ReadMe.md + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TA.Utils.Logging.NLog.SampleConsoleApp", "TA.Utils.Logging.NLog.SampleConsoleApp\TA.Utils.Logging.NLog.SampleConsoleApp.csproj", "{48B93DCE-0944-4A34-BF42-B7B5AF0DFAA0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -26,6 +39,14 @@ Global {8B3A2531-3295-4037-95EC-0BAD0DB4E972}.Debug|Any CPU.Build.0 = Debug|Any CPU {8B3A2531-3295-4037-95EC-0BAD0DB4E972}.Release|Any CPU.ActiveCfg = Release|Any CPU {8B3A2531-3295-4037-95EC-0BAD0DB4E972}.Release|Any CPU.Build.0 = Release|Any CPU + {57DD1805-E4A4-4D7C-94CD-E01498222640}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {57DD1805-E4A4-4D7C-94CD-E01498222640}.Debug|Any CPU.Build.0 = Debug|Any CPU + {57DD1805-E4A4-4D7C-94CD-E01498222640}.Release|Any CPU.ActiveCfg = Release|Any CPU + {57DD1805-E4A4-4D7C-94CD-E01498222640}.Release|Any CPU.Build.0 = Release|Any CPU + {48B93DCE-0944-4A34-BF42-B7B5AF0DFAA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {48B93DCE-0944-4A34-BF42-B7B5AF0DFAA0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {48B93DCE-0944-4A34-BF42-B7B5AF0DFAA0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {48B93DCE-0944-4A34-BF42-B7B5AF0DFAA0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE