From f5e2b959334d3b62166dc0f68c3a035e30d76283 Mon Sep 17 00:00:00 2001 From: Rolf Kristensen Date: Sat, 25 Mar 2023 12:27:51 +0100 Subject: [PATCH] Added support for MailHeaders (align with NLog MailTarget) --- azure-pipelines.yml | 2 +- src/NLog.MailKit/MailTarget.cs | 249 +++++++-------- src/NLog.MailKit/NLog.MailKit.csproj | 8 +- src/NLog.MailKit/Util/ExceptionHelper.cs | 107 ------- src/NLog.MailKit/Util/SortHelpers.cs | 366 ----------------------- 5 files changed, 111 insertions(+), 621 deletions(-) delete mode 100644 src/NLog.MailKit/Util/ExceptionHelper.cs delete mode 100644 src/NLog.MailKit/Util/SortHelpers.cs diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 0398ccf..6cf68d2 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -13,7 +13,7 @@ variables: Solution: 'src/NLog.MailKit.sln' BuildPlatform: 'Any CPU' BuildConfiguration: 'Release' - Version: '5.0.2' + Version: '5.1.3' FullVersion: '$(Version).$(Build.BuildId)' steps: diff --git a/src/NLog.MailKit/MailTarget.cs b/src/NLog.MailKit/MailTarget.cs index 1eacb3b..af26068 100644 --- a/src/NLog.MailKit/MailTarget.cs +++ b/src/NLog.MailKit/MailTarget.cs @@ -34,7 +34,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Text; using MailKit.Net.Smtp; using MailKit.Security; @@ -43,26 +43,24 @@ using NLog.Common; using NLog.Config; using NLog.Layouts; -using NLog.MailKit.Util; using NLog.Targets; namespace NLog.MailKit { /// - /// Sends log messages by email using SMTP protocol with MailKit. + /// Sends log messages by email using SMTP protocol. /// + /// + /// See NLog Wiki + /// /// Documentation on NLog Wiki /// ///

- /// To set up the target in the configuration file, + /// To set up the target in the configuration file, /// use the following syntax: ///

/// ///

- /// This assumes just one target and a single rule. More configuration - /// options are described here. - ///

- ///

/// To set up the log target programmatically use code like this: ///

/// @@ -71,7 +69,7 @@ namespace NLog.MailKit /// which lets you send multiple log messages in single mail ///

///

- /// To set up the buffered mail target in the configuration file, + /// To set up the buffered mail target in the configuration file, /// use the following syntax: ///

/// @@ -81,7 +79,7 @@ namespace NLog.MailKit /// ///
[Target("Mail")] - [SuppressMessage("ReSharper", "RedundantStringFormatCall")] + [Target("MailKit")] public class MailTarget : TargetWithLayoutHeaderAndFooter { private static readonly Encoding DefaultEncoding = System.Text.Encoding.UTF8; @@ -93,24 +91,18 @@ public class MailTarget : TargetWithLayoutHeaderAndFooter /// Initializes a new instance of the class. /// /// - /// The default value of the layout is: ${longdate}|${level:uppercase=true}|${logger}|${message} + /// The default value of the layout is: ${longdate}|${level:uppercase=true}|${logger}|${message:withexception=true} /// public MailTarget() { Body = "${message}${newline}"; - Subject = "Message from NLog on ${machinename}"; - Encoding = DefaultEncoding; - SmtpPort = 25; - SmtpAuthentication = SmtpAuthenticationMode.None; - SecureSocketOption = DefaultSecureSocketOption; - Timeout = 10000; } /// /// Initializes a new instance of the class. /// /// - /// The default value of the layout is: ${longdate}|${level:uppercase=true}|${logger}|${message} + /// The default value of the layout is: ${longdate}|${level:uppercase=true}|${logger}|${message:withexception=true} /// /// Name of the target. public MailTarget(string name) : this() @@ -122,6 +114,7 @@ public MailTarget(string name) : this() /// Gets or sets sender's email address (e.g. joe@domain.com). /// /// + [RequiredParameter] public Layout From { get; set; } /// @@ -147,24 +140,21 @@ public MailTarget(string name) : this() /// Gets or sets a value indicating whether to add new lines between log entries. /// /// A value of true if new lines should be added; otherwise, false. - /// - [DefaultValue(false)] - public Layout AddNewLines { get; set; } + /// + public bool AddNewLines { get; set; } /// /// Gets or sets the mail subject. /// /// - [DefaultValue("Message from NLog on ${machinename}")] [RequiredParameter] - public Layout Subject { get; set; } + public Layout Subject { get; set; } = "Message from NLog on ${machinename}"; /// /// Gets or sets mail message body (repeated for each log message send in one mail). /// /// Alias for the Layout property. /// - [DefaultValue("${message}${newline}")] public Layout Body { get => Layout; @@ -174,29 +164,27 @@ public Layout Body /// /// Gets or sets encoding to be used for sending e-mail. /// - /// - [DefaultValue("UTF8")] - public Layout Encoding { get; set; } + /// + public Layout Encoding { get; set; } = DefaultEncoding; /// /// Gets or sets a value indicating whether to send message as HTML instead of plain text. /// - /// - [DefaultValue(false)] + /// public Layout Html { get; set; } /// /// Gets or sets SMTP Server to be used for sending. /// /// + [RequiredParameter] public Layout SmtpServer { get; set; } /// /// Gets or sets SMTP Authentication mode. /// /// - [DefaultValue("None")] - public Layout SmtpAuthentication { get; set; } + public Layout SmtpAuthentication { get; set; } = SmtpAuthenticationMode.None; /// /// Gets or sets the username used to connect to SMTP server (used when is set to "basic"). @@ -215,9 +203,7 @@ public Layout Body /// /// See also /// - /// - /// . - [DefaultValue(false)] + /// . public Layout EnableSsl { get; set; } /// @@ -227,80 +213,75 @@ public Layout Body /// [DefaultValue(DefaultSecureSocketOption)] [CLSCompliant(false)] - public Layout SecureSocketOption { get; set; } + public Layout SecureSocketOption { get; set; } = DefaultSecureSocketOption; /// /// Gets or sets the port number that SMTP Server is listening on. /// /// - [DefaultValue(25)] - public Layout SmtpPort { get; set; } + public Layout SmtpPort { get; set; } = 25; /// /// Gets or sets a value indicating whether SmtpClient should ignore invalid certificate. /// /// - /// . - [DefaultValue(false)] public Layout SkipCertificateValidation { get; set; } /// /// Gets or sets the priority used for sending mails. /// + /// public Layout Priority { get; set; } /// - /// Gets or sets a value indicating whether NewLine characters in the body should be replaced with
tags. + /// Gets or sets a value indicating whether NewLine characters in the body should be replaced with
tags. ///
- /// Only happens when is set to true. - [DefaultValue(false)] + /// Only happens when is set to true. public Layout ReplaceNewlineWithBrTagInHtml { get; set; } /// /// Gets or sets a value indicating the SMTP client timeout. /// /// Warning: zero is not infinite waiting - [DefaultValue(10000)] - public Layout Timeout { get; set; } + public Layout Timeout { get; set; } = 10000; /// - /// Renders the logging event message and adds it to the internal ArrayList of log messages. + /// Gets the array of email headers that are transmitted with this email message /// - /// The logging event. + /// + [ArrayParameter(typeof(MethodCallParameter), "mailheader")] + public IList MailHeaders { get; } = new List(); + + /// protected override void Write(AsyncLogEventInfo logEvent) { Write(new[] { logEvent }); } - /// - /// Renders an array logging events. - /// - /// Array of logging events. + /// protected override void Write(IList logEvents) { - var buckets = logEvents.BucketSort(c => GetSmtpSettingsKey(c.LogEvent)); - foreach (var bucket in buckets) + if (logEvents.Count == 1) { - var eventInfos = bucket.Value; - ProcessSingleMailMessage(eventInfos); + ProcessSingleMailMessage(logEvents); + } + else + { + var buckets = logEvents.GroupBy(l => GetSmtpSettingsKey(l.LogEvent)); + foreach (var bucket in buckets) + { + var eventInfos = bucket; + ProcessSingleMailMessage(eventInfos); + } } } - /// - /// Initializes the target. Can be used by inheriting classes - /// to initialize logging. - /// + /// protected override void InitializeTarget() { InternalLogger.Debug("Init mailtarget with mailkit"); CheckRequiredParameters(); - var smtpAuthentication = RenderLogEvent(SmtpAuthentication, LogEventInfo.CreateNullEvent()); - if (smtpAuthentication == SmtpAuthenticationMode.Ntlm) - { - throw new NLogConfigurationException("NTLM not yet supported"); - } - base.InitializeTarget(); } @@ -308,18 +289,17 @@ protected override void InitializeTarget() /// Create mail and send with SMTP /// /// event printed in the body of the event - private void ProcessSingleMailMessage(IList events) + private void ProcessSingleMailMessage(IEnumerable events) { try { - if (events.Count == 0) + LogEventInfo firstEvent = events.FirstOrDefault().LogEvent; + LogEventInfo lastEvent = events.LastOrDefault().LogEvent; + if (firstEvent is null || lastEvent is null) { throw new NLogRuntimeException("We need at least one event."); } - var firstEvent = events[0].LogEvent; - var lastEvent = events[events.Count - 1].LogEvent; - // unbuffered case, create a local buffer, append header, body and footer var bodyBuffer = CreateBodyBuffer(events, firstEvent, lastEvent); @@ -327,7 +307,6 @@ private void ProcessSingleMailMessage(IList events) using (var client = new SmtpClient()) { - CheckRequiredParameters(); client.Timeout = RenderLogEvent(Timeout, lastEvent); var renderedHost = SmtpServer.Render(lastEvent); @@ -383,13 +362,10 @@ private void ProcessSingleMailMessage(IList events) catch (Exception exception) { //always log - InternalLogger.Error(exception, "Error sending mail."); + InternalLogger.Error(exception, "{0}: Error sending mail.", this); - if (exception.MustBeRethrown()) - { + if (LogManager.ThrowExceptions) throw; - } - foreach (var ev in events) { @@ -411,80 +387,56 @@ private StringBuilder CreateBodyBuffer(IEnumerable events, Lo var addNewLines = RenderLogEvent(AddNewLines, firstEvent, false); if (Header != null) { - bodyBuffer.Append(Header.Render(firstEvent)); + bodyBuffer.Append(RenderLogEvent(Header, firstEvent)); if (addNewLines) { - bodyBuffer.Append("\n"); + bodyBuffer.Append('\n'); } } foreach (var eventInfo in events) { - bodyBuffer.Append(Layout.Render(eventInfo.LogEvent)); + bodyBuffer.Append(RenderLogEvent(Layout, eventInfo.LogEvent)); if (addNewLines) { - bodyBuffer.Append("\n"); + bodyBuffer.Append('\n'); } } if (Footer != null) { - bodyBuffer.Append(Footer.Render(lastEvent)); + bodyBuffer.Append(RenderLogEvent(Footer, lastEvent)); if (addNewLines) { - bodyBuffer.Append("\n"); + bodyBuffer.Append('\n'); } } - return bodyBuffer; } private void CheckRequiredParameters() { - if (SmtpServer == null) - { - throw new NLogConfigurationException(string.Format(RequiredPropertyIsEmptyFormat, nameof(SmtpServer))); - } - - if (From == null) + var smtpAuthentication = RenderLogEvent(SmtpAuthentication, LogEventInfo.CreateNullEvent()); + if (smtpAuthentication == SmtpAuthenticationMode.Ntlm) { - throw new NLogConfigurationException(string.Format(RequiredPropertyIsEmptyFormat, nameof(From))); + throw new NLogConfigurationException("NTLM not yet supported"); } } /// /// Create key for grouping. Needed for multiple events in one mail message /// - /// event for rendering layouts + /// event for rendering layouts /// string to group on private string GetSmtpSettingsKey(LogEventInfo logEvent) { - var sb = new StringBuilder(); - - AppendLayout(sb, logEvent, From); - AppendLayout(sb, logEvent, To); - AppendLayout(sb, logEvent, Cc); - AppendLayout(sb, logEvent, Bcc); - AppendLayout(sb, logEvent, SmtpServer); - AppendLayout(sb, logEvent, SmtpPassword); - AppendLayout(sb, logEvent, SmtpUserName); - - return sb.ToString(); - } - - /// - /// Append rendered layout to the stringbuilder - /// - /// append to this - /// event for rendering - /// append if not null - private static void AppendLayout(StringBuilder sb, LogEventInfo logEvent, Layout layout) - { - sb.Append("|"); - if (layout != null) - { - sb.Append(layout.Render(logEvent)); - } + return $@"{RenderLogEvent(From, logEvent)} +{RenderLogEvent(To, logEvent)} +{RenderLogEvent(Cc, logEvent)} +{RenderLogEvent(Bcc, logEvent)} +{RenderLogEvent(SmtpServer, logEvent)} +{RenderLogEvent(SmtpPassword, logEvent)} +{RenderLogEvent(SmtpUserName, logEvent)}"; } /// @@ -494,13 +446,12 @@ private MimeMessage CreateMailMessage(LogEventInfo lastEvent, string body) { var msg = new MimeMessage(); - var renderedFrom = From?.Render(lastEvent); + var renderedFrom = RenderLogEvent(From, lastEvent); if (string.IsNullOrEmpty(renderedFrom)) { throw new NLogRuntimeException(string.Format(RequiredPropertyIsEmptyFormat, "From")); } - msg.From.Add(MailboxAddress.Parse(renderedFrom)); var addedTo = AddAddresses(msg.To, To, lastEvent); @@ -512,7 +463,7 @@ private MimeMessage CreateMailMessage(LogEventInfo lastEvent, string body) throw new NLogRuntimeException(string.Format(RequiredPropertyIsEmptyFormat, "To/Cc/Bcc")); } - msg.Subject = Subject == null ? string.Empty : Subject.Render(lastEvent).Trim(); + msg.Subject = (RenderLogEvent(Subject, lastEvent) ?? string.Empty).Trim(); if (Priority != null) { @@ -520,25 +471,33 @@ private MimeMessage CreateMailMessage(LogEventInfo lastEvent, string body) msg.Priority = ParseMessagePriority(renderedPriority); } - TextPart CreateBodyPart() + var newBody = body; + var html = RenderLogEvent(Html, lastEvent); + var replaceNewlineWithBrTagInHtml = RenderLogEvent(ReplaceNewlineWithBrTagInHtml, lastEvent); + if (html && replaceNewlineWithBrTagInHtml) { - var newBody = body; - var html = RenderLogEvent(Html, lastEvent); - var replaceNewlineWithBrTagInHtml = RenderLogEvent(ReplaceNewlineWithBrTagInHtml, lastEvent); - if (html && replaceNewlineWithBrTagInHtml) - { - newBody = newBody?.Replace(Environment.NewLine, "
"); - } + newBody = newBody?.Replace(Environment.NewLine, "
"); + } - var encoding = RenderLogEvent(Encoding, lastEvent, DefaultEncoding); - return new TextPart(html ? TextFormat.Html : TextFormat.Plain) + var encoding = RenderLogEvent(Encoding, lastEvent, DefaultEncoding); + msg.Body = new TextPart(html ? TextFormat.Html : TextFormat.Plain) + { + Text = newBody, + ContentType = { Charset = encoding?.WebName } + }; + + + if (MailHeaders?.Count > 0) + { + for (int i = 0; i < MailHeaders.Count; i++) { - Text = newBody, - ContentType = { Charset = encoding?.WebName } - }; - } + string headerValue = RenderLogEvent(MailHeaders[i].Layout, lastEvent); + if (headerValue is null) + continue; - msg.Body = CreateBodyPart(); + msg.Headers.Add(MailHeaders[i].Name, headerValue); + } + } return msg; } @@ -584,18 +543,22 @@ internal static MessagePriority ParseMessagePriority(string priority) /// layout with addresses, ; separated /// event for rendering the /// added a address? - private static bool AddAddresses(InternetAddressList mailAddressCollection, Layout layout, LogEventInfo logEvent) + private bool AddAddresses(InternetAddressList mailAddressCollection, Layout layout, LogEventInfo logEvent) { - if (layout == null) - { - return false; - } - var added = false; - foreach (var mail in layout.Render(logEvent).Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)) + var mailAddresses = RenderLogEvent(layout, logEvent); + + if (!string.IsNullOrEmpty(mailAddresses)) { - mailAddressCollection.Add(MailboxAddress.Parse(mail)); - added = true; + foreach (string mail in mailAddresses.Split(';')) + { + var mailAddress = mail.Trim(); + if (string.IsNullOrEmpty(mailAddress)) + continue; + + mailAddressCollection.Add(MailboxAddress.Parse(mail)); + added = true; + } } return added; diff --git a/src/NLog.MailKit/NLog.MailKit.csproj b/src/NLog.MailKit/NLog.MailKit.csproj index afe9b1a..49b1a8b 100644 --- a/src/NLog.MailKit/NLog.MailKit.csproj +++ b/src/NLog.MailKit/NLog.MailKit.csproj @@ -29,9 +29,9 @@ If the mail target was already available on your platform, this package will ove NLog.snk false -- NLog 5 compatible -- All properties are now layoutable. See 'NLog Layout for everything' on https://nlog-project.org/2021/08/25/nlog-5-0-preview1-ready.html -- Updated MailKit +- Added support for email headers +- Added target-alias mailkit +- Updated to NLog v5.1.3 See https://github.com/NLog/NLog.MailKit/releases @@ -50,7 +50,7 @@ See https://github.com/NLog/NLog.MailKit/releases - + diff --git a/src/NLog.MailKit/Util/ExceptionHelper.cs b/src/NLog.MailKit/Util/ExceptionHelper.cs deleted file mode 100644 index 4980862..0000000 --- a/src/NLog.MailKit/Util/ExceptionHelper.cs +++ /dev/null @@ -1,107 +0,0 @@ -// -// Copyright (c) 2004-2022 Jaroslaw Kowalski , Kim Christensen, Julian Verdurmen -// -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions -// are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// * Neither the name of Jaroslaw Kowalski nor the names of its -// contributors may be used to endorse or promote products derived from this -// software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF -// THE POSSIBILITY OF SUCH DAMAGE. -// - -using System; -using NLog.Common; - -namespace NLog.MailKit.Util -{ - /// - /// Helper class for dealing with exceptions. - /// - internal static class ExceptionHelper - { - private const string LoggedKey = "NLog.ExceptionLoggedToInternalLogger"; - - /// - /// Is this exception logged to the ? - /// - /// - /// trueif the has been logged to the . - public static bool IsLoggedToInternalLogger(this Exception exception) - { - if (exception != null) - { - return exception.Data[LoggedKey] as bool? ?? false; - } - return false; - } - - - /// - /// Determines whether the exception must be rethrown and logs the error to the if is false. - /// - /// Advised to log first the error to the before calling this method. - /// - /// The exception to check. - /// trueif the must be rethrown, false otherwise. - public static bool MustBeRethrown(this Exception exception) - { - if (exception.MustBeRethrownImmediately()) - { - //no further logging, because it can make severe exceptions only worse. - return true; - } - - var isConfigError = exception is NLogConfigurationException; - - //we throw always configuration exceptions (historical) - if (!exception.IsLoggedToInternalLogger()) - { - var level = isConfigError ? LogLevel.Warn : LogLevel.Error; - InternalLogger.Log(exception, level, "Error has been raised."); - } - - //if ThrowConfigExceptions == null, use ThrowExceptions - var shallRethrow = isConfigError ? (LogManager.ThrowConfigExceptions ?? LogManager.ThrowExceptions) : LogManager.ThrowExceptions; - return shallRethrow; - } - - /// - /// Determines whether the exception must be rethrown immediately, without logging the error to the . - /// - /// Only used this method in special cases. - /// - /// The exception to check. - /// trueif the must be rethrown, false otherwise. - public static bool MustBeRethrownImmediately(this Exception exception) - { - if (exception is OutOfMemoryException) - { - return true; - } - - return false; - } - } -} diff --git a/src/NLog.MailKit/Util/SortHelpers.cs b/src/NLog.MailKit/Util/SortHelpers.cs deleted file mode 100644 index 2d3bbd1..0000000 --- a/src/NLog.MailKit/Util/SortHelpers.cs +++ /dev/null @@ -1,366 +0,0 @@ -// -// Copyright (c) 2004-2022 Jaroslaw Kowalski , Kim Christensen, Julian Verdurmen -// -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions -// are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// * Neither the name of Jaroslaw Kowalski nor the names of its -// contributors may be used to endorse or promote products derived from this -// software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF -// THE POSSIBILITY OF SUCH DAMAGE. -// - -using System; -using System.Collections; -using System.Collections.Generic; -using NLog.Common; - -namespace NLog.MailKit.Util -{ - /// - /// Provides helpers to sort log events and associated continuations. - /// - internal static class SortHelpers - { - /// - /// Key selector delegate. - /// - /// The type of the value. - /// The type of the key. - /// Value to extract key information from. - /// Key selected from log event. - internal delegate TKey KeySelector(TValue value); - - /// - /// Performs bucket sort (group by) on an array of items and returns a dictionary for easy traversal of the result set. - /// - /// The type of the value. - /// The type of the key. - /// The inputs. - /// The key selector function. - /// - /// Dictionary where keys are unique input keys, and values are lists of . - /// - public static ReadOnlySingleBucketDictionary> BucketSort(this IList inputs, KeySelector keySelector) - { - Dictionary> buckets = null; - bool singleBucketFirstKey = false; - TKey singleBucketKey = default; - EqualityComparer c = EqualityComparer.Default; - for (int i = 0; i < inputs.Count; i++) - { - TKey keyValue = keySelector(inputs[i]); - if (!singleBucketFirstKey) - { - singleBucketFirstKey = true; - singleBucketKey = keyValue; - } - else if (buckets == null) - { - if (!c.Equals(singleBucketKey, keyValue)) - { - // Multiple buckets needed, allocate full dictionary - buckets = new Dictionary>(); - var bucket = new List(i); - for (int j = 0; j < i; j++) - { - bucket.Add(inputs[j]); - } - buckets[singleBucketKey] = bucket; - bucket = new List { inputs[i] }; - buckets[keyValue] = bucket; - } - } - else - { - if (!buckets.TryGetValue(keyValue, out var eventsInBucket)) - { - eventsInBucket = new List(); - buckets.Add(keyValue, eventsInBucket); - } - eventsInBucket.Add(inputs[i]); - } - } - - if (buckets != null) - { - return new ReadOnlySingleBucketDictionary>(buckets); - } - else - { - return new ReadOnlySingleBucketDictionary>(new KeyValuePair>(singleBucketKey, inputs)); - } - } - - /// - /// Single-Bucket optimized readonly dictionary. Uses normal internally Dictionary if multiple buckets are needed. - /// - /// Avoids allocating a new dictionary, when all items are using the same bucket - /// - /// The type of the key. - /// The type of the value. - public struct ReadOnlySingleBucketDictionary : IDictionary - { - readonly KeyValuePair? _singleBucket; - readonly Dictionary _multiBucket; - readonly IEqualityComparer _comparer; - - public ReadOnlySingleBucketDictionary(KeyValuePair singleBucket) - : this(singleBucket, EqualityComparer.Default) - { - } - - public ReadOnlySingleBucketDictionary(Dictionary multiBucket) - : this(multiBucket, EqualityComparer.Default) - { - } - - public ReadOnlySingleBucketDictionary(KeyValuePair singleBucket, IEqualityComparer comparer) - { - _comparer = comparer; - _multiBucket = null; - _singleBucket = singleBucket; - } - - public ReadOnlySingleBucketDictionary(Dictionary multiBucket, IEqualityComparer comparer) - { - _comparer = comparer; - _multiBucket = multiBucket; - _singleBucket = default; - } - - /// - public int Count { get { if (_multiBucket != null) return _multiBucket.Count; else if (_singleBucket.HasValue) return 1; else return 0; } } - - /// - public ICollection Keys - { - get - { - if (_multiBucket != null) - return _multiBucket.Keys; - if (_singleBucket.HasValue) - return new[] { _singleBucket.Value.Key }; - return Array.Empty(); - } - } - - /// - public ICollection Values - { - get - { - if (_multiBucket != null) - return _multiBucket.Values; - if (_singleBucket.HasValue) - return new[] { _singleBucket.Value.Value }; - return Array.Empty(); - } - } - - /// - public bool IsReadOnly => true; - - /// - /// Allows direct lookup of existing keys. If trying to access non-existing key exception is thrown. - /// Consider to use instead for better safety. - /// - /// Key value for lookup - /// Mapped value found - public TValue this[TKey key] - { - get - { - if (_multiBucket != null) - return _multiBucket[key]; - if (_singleBucket.HasValue && _comparer.Equals(_singleBucket.Value.Key, key)) - return _singleBucket.Value.Value; - throw new KeyNotFoundException(); - } - set - { - throw new NotSupportedException("Readonly"); - } - } - - /// - /// Non-Allocating struct-enumerator - /// - public struct Enumerator : IEnumerator> - { - bool _singleBucketFirstRead; - readonly KeyValuePair _singleBucket; - readonly IEnumerator> _multiBuckets; - - internal Enumerator(Dictionary multiBucket) - { - _singleBucketFirstRead = false; - _singleBucket = default; - _multiBuckets = multiBucket.GetEnumerator(); - } - - internal Enumerator(KeyValuePair singleBucket) - { - _singleBucketFirstRead = false; - _singleBucket = singleBucket; - _multiBuckets = null; - } - - public KeyValuePair Current - { - get - { - if (_multiBuckets != null) - return new KeyValuePair(_multiBuckets.Current.Key, _multiBuckets.Current.Value); - else - return new KeyValuePair(_singleBucket.Key, _singleBucket.Value); - } - } - - object IEnumerator.Current => Current; - - public void Dispose() - { - _multiBuckets?.Dispose(); - } - - public bool MoveNext() - { - if (_multiBuckets != null) - return _multiBuckets.MoveNext(); - else if (_singleBucketFirstRead) - return false; - else - return _singleBucketFirstRead = true; - - } - - public void Reset() - { - if (_multiBuckets != null) - _multiBuckets.Reset(); - else - _singleBucketFirstRead = false; - } - } - - public Enumerator GetEnumerator() - { - if (_multiBucket != null) - return new Enumerator(_multiBucket); - if (_singleBucket.HasValue) - return new Enumerator(_singleBucket.Value); - return new Enumerator(new Dictionary()); - } - - /// - IEnumerator> IEnumerable>.GetEnumerator() - { - return GetEnumerator(); - } - - /// - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - /// - public bool ContainsKey(TKey key) - { - if (_multiBucket != null) - return _multiBucket.ContainsKey(key); - if (_singleBucket.HasValue) - return _comparer.Equals(_singleBucket.Value.Key, key); - return false; - } - - /// Will always throw, as dictionary is readonly - public void Add(TKey key, TValue value) - { - throw new NotSupportedException(); // Readonly - } - - /// Will always throw, as dictionary is readonly - public bool Remove(TKey key) - { - throw new NotSupportedException(); // Readonly - } - - /// - public bool TryGetValue(TKey key, out TValue value) - { - if (_multiBucket != null) - { - return _multiBucket.TryGetValue(key, out value); - } - if (_singleBucket.HasValue && _comparer.Equals(_singleBucket.Value.Key, key)) - { - value = _singleBucket.Value.Value; - return true; - } - - value = default; - return false; - } - - /// Will always throw, as dictionary is readonly - public void Add(KeyValuePair item) - { - throw new NotSupportedException(); // Readonly - } - - /// Will always throw, as dictionary is readonly - public void Clear() - { - throw new NotSupportedException(); // Readonly - } - - /// - public bool Contains(KeyValuePair item) - { - if (_multiBucket != null) - return ((IDictionary)_multiBucket).Contains(item); - if (_singleBucket.HasValue) - return _comparer.Equals(_singleBucket.Value.Key, item.Key) && EqualityComparer.Default.Equals(_singleBucket.Value.Value, item.Value); - return false; - } - - /// - public void CopyTo(KeyValuePair[] array, int arrayIndex) - { - if (_multiBucket != null) - ((IDictionary)_multiBucket).CopyTo(array, arrayIndex); - else if (_singleBucket.HasValue) - array[arrayIndex] = _singleBucket.Value; - } - - /// Will always throw, as dictionary is readonly - public bool Remove(KeyValuePair item) - { - throw new NotSupportedException(); // Readonly - } - } - } -}