MessageReleaseThread: Invoke Thread.Sleep only when milliseconds > 0

Fixes: #264

--- src/Helsenorge.Messaging/ServiceBus/MessageReleaseThread.cs
+++ src/Helsenorge.Messaging/ServiceBus/MessageReleaseThread.cs
@@ -45,7 +45,10 @@

                 logger.LogDebug($"MessageReleaseThread-AwaitRelease: MessageId: {messageId}. Awaiting {millisecondsDelay} before releasing message.");

-                Thread.Sleep(millisecondsDelay);
+                if (millisecondsDelay > 0)
+                {
+                    Thread.Sleep(millisecondsDelay);
+                }

                 message.Modify(deliveryFailed: true);


Show login overlay when attempting to update a beatmap while logged out

--- osu.Game/Screens/Select/Carousel/UpdateBeatmapSetButton.cs
+++ osu.Game/Screens/Select/Carousel/UpdateBeatmapSetButton.cs
@@ -11,6 +11,8 @@
 using osu.Game.Graphics;
 using osu.Game.Graphics.Sprites;
 using osu.Game.Graphics.UserInterface;
+using osu.Game.Online.API;
+using osu.Game.Overlays;
 using osuTK;
 using osuTK.Graphics;

@@ -22,6 +24,15 @@
         private SpriteIcon icon = null!;
         private Box progressFill = null!;

+        [Resolved]
+        private BeatmapModelDownloader beatmapDownloader { get; set; } = null!;
+
+        [Resolved]
+        private IAPIProvider api { get; set; } = null!;
+
+        [Resolved(canBeNull: true)]
+        private LoginOverlay? loginOverlay { get; set; }
+
         public UpdateBeatmapSetButton(BeatmapSetInfo beatmapSetInfo)
         {
             this.beatmapSetInfo = beatmapSetInfo;
@@ -31,9 +42,6 @@
             Anchor = Anchor.CentreLeft;
             Origin = Anchor.CentreLeft;
         }
-
-        [Resolved]
-        private BeatmapModelDownloader beatmapDownloader { get; set; } = null!;

         [BackgroundDependencyLoader]
         private void load()
@@ -90,6 +98,12 @@

             Action = () =>
             {
+                if (!api.IsLoggedIn)
+                {
+                    loginOverlay?.Show();
+                    return;
+                }
+
                 beatmapDownloader.DownloadAsUpdate(beatmapSetInfo);
                 attachExistingDownload();
             };


fix: first result displayed in out of order

--- src/Mshudx.OscarNite.Web/Controllers/ReportingController.cs
+++ src/Mshudx.OscarNite.Web/Controllers/ReportingController.cs
@@ -24,7 +24,7 @@

         public IActionResult Index()
         {
-            var question = dbContext.Questions.First();
+            var question = dbContext.Questions.OrderBy(q => q.Order).ThenBy(q => q.Id).First();
             var votes = dbContext.Votes.Count();
             ViewData["totalVotes"] = votes;
             ViewData["questionId"] = question.Id;


Simplified loop and parse expression

Simplified loop and parse expression

--- BlackVueDownloader.PCL/BlackVueDownloader.cs
+++ BlackVueDownloader.PCL/BlackVueDownloader.cs
@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using System.Diagnostics;
+using System.Linq;
 using System.Net;
 using Flurl.Http;

@@ -51,17 +52,8 @@
         /// <returns>Normalized list of files</returns>
         public IList<string> GetListOfFilesFromResponse(string body)
         {
-            var fileList = new List<string>();
-
             // Parse each element of the body by the separator, which fortunately is a 'space'
-            var element = body.Split(' ');
-            foreach (var e in element)
-            {
-                // Parse only what we need off of it by replacing what we don't with nothing.
-                fileList.Add(e.Replace("n:/Record/", "").Replace(",s:1000000", ""));
-            }
-
-            return fileList;
+            return body.Split(' ').Select(e => e.Replace("n:/Record/", "").Replace(",s:1000000", "")).ToList();
         }

         /// <summary>


Test for NH-571

Test for NH-571

SVN: 488784591515bd4cdaa016be4ec9b172dc4e7caf@2087

--- src/NHibernate.Test/JoinedSubclass/JoinedSubclassFixture.cs
+++ src/NHibernate.Test/JoinedSubclass/JoinedSubclassFixture.cs
@@ -157,6 +157,31 @@
 			s.Flush();
 			s.Close();
 		}
+
+		[Test]
+		public void SelectByClass()
+		{
+			using (ISession s = OpenSession())
+			using( ITransaction t = s.BeginTransaction() )
+			{
+				Person wally = new Person();
+				wally.Name = "wally";
+
+				Employee dilbert = new Employee();
+				dilbert.Name = "dilbert";
+				dilbert.Title = "office clown";
+
+				s.Save(wally);
+				s.Save(dilbert);
+				s.Flush();
+
+				Assert.AreEqual(1, s.CreateQuery("select p from Person p where p.class = Person").List().Count);
+
+				s.Delete(wally);
+				s.Delete(dilbert);
+				t.Commit();
+			}
+		}

 		/// <summary>
 		/// Test the ability to insert a new row with a User Assigned Key


Make alias check for exact match only

Make alias check for exact match only

--- src/NadekoBot/Modules/Utility/Services/CommandMapService.cs
+++ src/NadekoBot/Modules/Utility/Services/CommandMapService.cs
@@ -46,9 +46,7 @@
                     foreach (var k in keys)
                     {
                         string newInput;
-                        if (input.StartsWith(k + " "))
-                            newInput = maps[k] + input.Substring(k.Length, input.Length - k.Length);
-                        else if (input == k)
+                        if (input == k)
                             newInput = maps[k];
                         else
                             continue;


feat(auth): 将功能权限的缓存有效期由无限期改为7±1天

feat(auth): 将功能权限的缓存有效期由无限期改为7±1天

--- src/OSharp.Authorization.Functions/FunctionAuthCacheBase.cs
+++ src/OSharp.Authorization.Functions/FunctionAuthCacheBase.cs
@@ -40,6 +40,7 @@
         where TUser : UserBase<TUserKey>
         where TUserKey : IEquatable<TUserKey>
     {
+        private readonly Random _random = new Random();
         private readonly IServiceProvider _serviceProvider;
         private readonly IDistributedCache _cache;
         private readonly ILogger _logger;
@@ -147,7 +148,10 @@
                 IRepository<TRole, TRoleKey> roleRepository = provider.GetService<IRepository<TRole, TRoleKey>>();
                 roleNames = roleRepository.QueryAsNoTracking(m => roleIds.Contains(m.Id)).Select(m => m.Name).Distinct().ToArray();
             }
-            _cache.Set(key, roleNames);
+
+            // 有效期为 7 ± 1 天
+            int seconds = 7 * 24 * 3600 + _random.Next(-24 * 2600, 24 * 3600);
+            _cache.Set(key, roleNames, seconds);
             _logger.LogDebug($"添加功能“{functionId}”的“Function-Roles[]”缓存，角色数：{roleNames.Length}");

             serviceScope?.Dispose();


Fix id for drop down

Fix id for drop down

--- CRP.Mvc/Views/Shared/_QuestionFormNew.cshtml
+++ CRP.Mvc/Views/Shared/_QuestionFormNew.cshtml
@@ -73,7 +73,7 @@
         case "Drop Down":
             @*@this.Select(".Answer").Options(Model.Question.Options.OrderBy(a => a.Name), x => x.Name, x => x.Name).Class("indexedControl " + Model.Question.ValidationClasses).Selected(Model.Answer ?? string.Empty).FirstOption("--Select--")*@
             <label id="_Answer" class="indexedControl">@Model.Question.Name</label>
-            <select class="form-control indexedControl @Model.Question.ValidationClasses"  id=".Answer">
+            <select class="form-control indexedControl @Model.Question.ValidationClasses"  id="_Answer">
                 <option>--Select--</option>
                 @foreach (var o in Model.Question.Options.OrderBy(a => a.Name))
                 {


Fix RenderTextureZbuffer touch positioning

--- tests/tests/classes/tests/RenderTextureTest/RenderTextureZbuffer.cs
+++ tests/tests/classes/tests/RenderTextureTest/RenderTextureZbuffer.cs
@@ -85,7 +85,10 @@

         public override void OnEnter()
         {
-base.OnEnter(); CCSize windowSize = Layer.VisibleBoundsWorldspace.Size;
+
+            base.OnEnter();
+
+            CCSize windowSize = Layer.VisibleBoundsWorldspace.Size;

             label.Position = new CCPoint(windowSize.Width / 2, windowSize.Height * 0.25f);
             label2.Position = new CCPoint(windowSize.Width / 2, windowSize.Height * 0.5f);
@@ -126,7 +129,7 @@
         {
             foreach (CCTouch touch in touches)
             {
-                CCPoint location = touch.LocationOnScreen;
+                CCPoint location = touch.Location;

                 sp1.Position = location;
                 sp2.Position = location;
@@ -144,7 +147,7 @@
         {
             foreach (CCTouch touch in touches)
             {
-                CCPoint location = touch.LocationOnScreen;
+                CCPoint location = touch.Location;

                 sp1.Position = location;
                 sp2.Position = location;


Fix https://github.com/KirillOsenkov/MSBuildStructuredLog/issues/242.

There's a design flaw in the BuildLog format. I took a shortcut
and for Folder nodes I just write the name of the folder instead
of specifying that the element is a Folder in the first place.
Unfortunately I didn't think about Folders named "Property",
"Target", etc.
So the deserialization logic when it sees a string called "Property"
it assumes we have a property here, instead of a Folder named
"Property". But properties have children! We're in a pickle now.
Longer term I need to modify the format to not do this optimization
and always write "Folder" for folders and write the name separately.
For now I don't have time to do this right, so put in the dirty
hack to recover from this situation. If it says it's a "Property"
but expects children, it means it's actually a folder with name
"Property".

--- src/StructuredLogger/Serialization/Binary/BuildLogReader.cs
+++ src/StructuredLogger/Serialization/Binary/BuildLogReader.cs
@@ -90,6 +90,29 @@
             int childrenCount = reader.ReadInt32();
             if (childrenCount > 0)
             {
+                if (!(node is TreeNode))
+                {
+                    // OK we got ourselves into a situation here.
+                    // https://github.com/KirillOsenkov/MSBuildStructuredLog/issues/242
+                    // There's a design flaw in the BuildLog format. I took a shortcut
+                    // and for Folder nodes I just write the name of the folder instead
+                    // of specifying that the element is a Folder in the first place.
+                    // Unfortunately I didn't think about Folders named "Property",
+                    // "Target", etc.
+                    // So the deserialization logic when it sees a string called "Property"
+                    // it assumes we have a property here, instead of a Folder named
+                    // "Property". But properties have children! We're in a pickle now.
+                    // Longer term I need to modify the format to not do this optimization
+                    // and always write "Folder" for folders and write the name separately.
+                    // For now I don't have time to do this right, so put in the dirty
+                    // hack to recover from this situation. If it says it's a "Property"
+                    // but expects children, it means it's actually a folder with name
+                    // "Property".
+                    folder = new Folder();
+                    folder.Name = Serialization.GetNodeName(node);
+                    node = folder;
+                }
+
                 var treeNode = (TreeNode)node;
                 for (int i = 0; i < childrenCount; i++)
                 {


Fix inconsistent exception message in `-replace` operator (#12388)

--- src/System.Management.Automation/engine/lang/parserutils.cs
+++ src/System.Management.Automation/engine/lang/parserutils.cs
@@ -927,7 +927,7 @@
                 {
                     // only allow 1 or 2 arguments to -replace
                     throw InterpreterError.NewInterpreterException(rval, typeof(RuntimeException), errorPosition,
-                        "BadReplaceArgument", ParserStrings.BadReplaceArgument, ignoreCase ? "-ireplace" : "-replace", rList.Count);
+                        "BadReplaceArgument", ParserStrings.BadReplaceArgument, errorPosition.Text, rList.Count);
                 }

                 if (rList.Count > 0)


Fix CookieContainer dead code masquerading as a race condition

A coverity scan suggested that _domainTable might be unsafe to be locked on as it's replaced.  But closer inspection shows that it's only re-assigned to in code that's actually dead, as the condition guarding the if block containing the assignment will never be true.  This commit just replaces the dead code with some asserts and makes the lockable field readonly so as to highlight that it won't change.

--- src/System.Net.Primitives/src/System/Net/CookieContainer.cs
+++ src/System.Net.Primitives/src/System/Net/CookieContainer.cs
@@ -99,7 +99,7 @@
         };

         // NOTE: all accesses of _domainTable must be performed with _domainTable locked.
-        private Dictionary<string, PathList> _domainTable = new Dictionary<string, PathList>();
+        private readonly Dictionary<string, PathList> _domainTable = new Dictionary<string, PathList>();
         private int _maxCookieSize = DefaultCookieLengthLimit;
         private int _maxCookies = DefaultCookieLimit;
         private int _maxCookiesPerDomain = DefaultPerDomainCookieLimit;
@@ -356,13 +356,8 @@
         // Param. 'domain' == null means to age in the whole container.
         private bool AgeCookies(string domain)
         {
-            // Border case: shrunk to zero
-            if (_maxCookies == 0 || _maxCookiesPerDomain == 0)
-            {
-                _domainTable = new Dictionary<string, PathList>();
-                _count = 0;
-                return false;
-            }
+            Debug.Assert(_maxCookies != 0);
+            Debug.Assert(_maxCookiesPerDomain != 0);

             int removed = 0;
             DateTime oldUsed = DateTime.MaxValue;


Fix comment on Solution.Options to explain what this will be

--- src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs
+++ src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs
@@ -999,14 +999,14 @@
         public ProjectDependencyGraph GetProjectDependencyGraph() => _state.GetProjectDependencyGraph();

         /// <summary>
-        /// Returns the options that should be applied to this solution. This consists of global options from <see cref="Workspace.Options"/>,
-        /// merged with any settings the user has specified at the solution level.
+        /// Returns the options that should be applied to this solution. This is equivalent to <see cref="Workspace.Options" /> when the <see cref="Solution"/>
+        /// instance was created.
         /// </summary>
         public OptionSet Options
         {
             get
             {
-                // TODO: merge with solution-specific options
+                // TODO: actually make this a snapshot
                 return this.Workspace.Options;
             }
         }


Fix more parsing errors

Fix more parsing errors

--- src/JsonParser.cs
+++ src/JsonParser.cs
@@ -14,8 +14,8 @@
             int start = Math.Max(0, _start - 10);
             int length = Math.Min(20, _input.Length - start);

-            string error = "Error while parsing: " + message + "; context = \"" +
-                _input.Substring(start, length) + "\"";
+            string error = "Error while parsing: " + message + "; context = <" +
+                _input.Substring(start, length) + ">";
             return JsonFailure.Fail(error);
         }

@@ -103,8 +103,6 @@

             switch (Character()) {
                 case '\\': TryMoveNext(); escaped = '\\'; return JsonFailure.Success;
-                case '/': TryMoveNext(); escaped = '/'; return JsonFailure.Success;
-                case '\'': TryMoveNext(); escaped = '\''; return JsonFailure.Success;
                 case '"': TryMoveNext(); escaped = '\"'; return JsonFailure.Success;
                 case 'a': TryMoveNext(); escaped = '\a'; return JsonFailure.Success;
                 case 'b': TryMoveNext(); escaped = '\b'; return JsonFailure.Success;
@@ -125,6 +123,7 @@
                         TryMoveNext();
                         TryMoveNext();
                         TryMoveNext();
+                        TryMoveNext();

                         escaped = (char)codePoint;
                         return JsonFailure.Success;
@@ -236,7 +235,7 @@
             }

             // read until the next "
-            while (Character() != '\"') {
+            while (HasValue() && Character() != '\"') {
                 char c = Character();

                 // escape if necessary
@@ -254,17 +253,17 @@
                 // no escaping necessary
                 else {
                     result.Append(c);
-                }
-
-                // get the next character
-                if (TryMoveNext() == false) {
-                    str = string.Empty;
-                    return MakeFailure("Unexpected end of input when reading a string");
+
+                    // get the next character
+                    if (TryMoveNext() == false) {
+                        str = string.Empty;
+                        return MakeFailure("Unexpected end of input when reading a string");
+                    }
                 }
             }

             // skip the first "
-            if (Character() != '"' || TryMoveNext() == false) {
+            if (HasValue() == false || Character() != '"' || TryMoveNext() == false) {
                 str = string.Empty;
                 return MakeFailure("No closing \" when parsing a string");
             }


Changed urls for new identity area

Changed urls for new identity area

--- CmsEngine.Ui/Views/Shared/Cms/_Layout.cshtml
+++ CmsEngine.Ui/Views/Shared/Cms/_Layout.cshtml
@@ -76,8 +76,8 @@
                     @*

                             Account

*@
-                     Profile
-                     Change password
+                     Profile
+                     Change password
                     @* Updates42
                          Messages42
                          Tasks42
@@ -85,10 +85,10 @@
                     @*

                             Settings

*@
-                     Settings
+                    @* Settings*@

                     @* Lock Account*@
-                     Logout
+                     Logout


Discard tempVar as we don't care

Discard tempVar as we don't care

--- ConsoleScraper/ConsoleScraper/IWebCrawler.cs
+++ ConsoleScraper/ConsoleScraper/IWebCrawler.cs
@@ -132,7 +132,7 @@
 			foreach (var vehiclePageLink in vehiclePageLinks)
 			{
 				// Remove the current node so that the other threads don't reprocess it
-				vehiclePageLinks.TryRemove(vehiclePageLink.Key, out HtmlNode tempNode);
+				vehiclePageLinks.TryRemove(vehiclePageLink.Key, out HtmlNode _);

 				// Fetch page information
 				HtmlNode linkNode = vehiclePageLink.Value;


Fix ToString

Fix ToString

--- src/QTKit/Defs.cs
+++ src/QTKit/Defs.cs
@@ -49,7 +49,10 @@

 		public override string ToString ()
 		{
-			return String.Format ("[TimeValue={0} scale={2} Flags={3}]", TimeValue, TimeScale, Flags);
+			if (Flags == 0)
+				return String.Format ("[TimeValue={0} scale={1}]", TimeValue, TimeScale);
+			else
+				return String.Format ("[TimeValue={0} scale={1} Flags={2}]", TimeValue, TimeScale, Flags);
 		}
 	}


Added Title and Version properties to Content

--- Dapplo.Confluence.Shared/Entities/Content.cs
+++ Dapplo.Confluence.Shared/Entities/Content.cs
@@ -42,8 +42,14 @@
 		[DataMember(Name = "type")]
 		public string Type { get; set; }

+		[DataMember(Name = "title")]
+		public string Title { get; set; }
+
 		[DataMember(Name = "space")]
 		public Space Space { get; set; }
+
+		[DataMember(Name = "version")]
+		public Version Version { get; set; }

 		[DataMember(Name = "_expandable")]
 		public IDictionary<string, string> Expandables { get; set; }


Finalize redesign of the Ini class.

Finalize redesign of the Ini class.

--- src/SilDev/Ini.cs
+++ src/SilDev/Ini.cs
@@ -5,7 +5,7 @@
 // ==============================================
 //
 // Filename: Ini.cs
-// Version:  2020-01-21 20:42
+// Version:  2020-01-24 20:49
 //
 // Copyright (c) 2020, Si13n7 Developments(tm)
 // All rights reserved.
@@ -17,57 +17,77 @@
 {
     using System;
     using System.Collections.Generic;
+    using System.Collections.ObjectModel;
     using System.IO;
     using System.Linq;
+    using System.Runtime.Serialization;
+    using System.Security;
     using System.Text;
     using System.Text.RegularExpressions;
+    using System.Web.Script.Serialization;
+    using IDocumentDictionary = System.Collections.Generic.IDictionary<string, System.Collections.Generic.IDictionary<string, System.Collections.Generic.IList<string>>>;
+    using IKeyValueDictionary = System.Collections.Generic.IDictionary<string, System.Collections.Generic.IList<string>>;
+    using IReadOnlyDocumentDictionary = System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IReadOnlyList<string>>>;
+    using IReadOnlyKeyValueDictionary = System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IReadOnlyList<string>>;

     /// <summary>
     ///     Provides the functionality to handle the INI format.
     /// </summary>
-    public sealed class Ini
+    [Serializable]
+    public sealed class Ini : ISerializable, IEquatable<Ini>
     {
         /// <summary>
         ///     The default pattern for parsing an INI document that allows blank sections.
         /// </summary>
+        [ScriptIgnore]
         public const string ParsePatternDefault = @"^((?:\[)(?<Section>[^\]]*)(?:\])(?:[\r\n]{0,}|\Z))((?!\[)(?<Key>[^=]*?)(?:=)(?<Value>[^\r\n]*)(?:[\r\n]{0,4}))*";

         /// <summary>
         ///     The pattern for parsing an INI document that does not allow blank sections.
         /// </summary>
+        [ScriptIgnore]
         public const string ParsePatternStrict = @"^((?:\[)(?<Section>[^\]]*)(?:\])(?:[\r\n]{0,}|\Z))((?!\[)(?<Key>[^=]*?)(?:=)(?<Value>[^\r\n]*)(?:[\r\n]{0,4}))+";

+        [ScriptIgnore]
         private const string EncodeTagOpen = "<base64 reason=\"LineSeparator\">",
                              EncodeTagClose = "</base64>";

+        private readonly object _identifier = new object();
+        private IDocumentDictionary _document;
+
         /// <summary>
         ///     Gets the raw object that is used to manage the complete data of this
-        ///     <see cref="Ini"/> instance.
-        /// </summary>
-        public IReadOnlyDictionary<string, IReadOnlyDictionary<string, IReadOnlyList<string>>> ReadOnlyDocument =>
-            Document.ToDictionary(p => p.Key, p => (IReadOnlyDictionary<string, IReadOnlyList<string>>)p.Value.ToDictionary(x => x.Key, x => (IReadOnlyList<string>)x.Value));
+        ///     instance.
+        ///     <para>
+        ///         Please use the indexer of this instance to update the content of this
+        ///         <see cref="Document"/>.
+        ///     </para>
+        /// </summary>
+        public IReadOnlyDocumentDictionary Document =>
+            _document.ToDictionary(p => p.Key, p => p.Value.ToDictionary(x => x.Key, x => (x.Value as List<string>)?.AsReadOnly() as IReadOnlyList<string>) as IReadOnlyKeyValueDictionary);

         /// <summary>
         ///     Gets the value that determines how sections and keys are compared
-        ///     internally in this <see cref="Ini"/> instance to allow duplicates if
-        ///     case-sensitive, or to avoid if case-insensitive.
-        /// </summary>
+        ///     internally in this instance to allow duplicates if case-sensitive, or to
+        ///     avoid if case-insensitive.
+        /// </summary>
+        [ScriptIgnore]
         public StringComparer Comparer { get; private set; }

         /// <summary>
-        ///     Gets the value that determines whether this <see cref="Ini"/> instance is
-        ///     saved with ordered sections and keys.
+        ///     Gets the value that determines whether this instance is saved with ordered
+        ///     sections and keys.
         /// </summary>
         public bool Sorted { get; private set; }

         /// <summary>
-        ///     Gets or sets the file path of this <see cref="Ini"/> instance.
+        ///     Gets or sets the file path of this instance.
         /// </summary>
         public string FilePath { get; set; }

         /// <summary>
         ///     Gets or sets the value under the specified index of the specified key in
-        ///     the specified section of this <see cref="Ini"/> instance.
+        ///     the specified section of this instance.
         /// </summary>
         /// <param name="section">
         ///     The name of the section. Must be <see langword="null"/> to handle values of
@@ -87,7 +107,7 @@

         /// <summary>
         ///     Gets or sets the value under the first index of the specified key in the
-        ///     specified section of this <see cref="Ini"/> instance.
+        ///     specified section of this instance.
         /// </summary>
         /// <param name="section">
         ///     The name of the section. Must be <see langword="null"/> to handle values of
@@ -102,11 +122,58 @@
             set => AddOrUpdate(section, key, value);
         }

+        /// <summary>
+        ///     Gets or sets the object value under the specified index of the specified
+        ///     key in the specified section of this instance.
+        /// </summary>
+        /// <param name="section">
+        ///     The name of the section. Must be <see langword="null"/> to handle values of
+        ///     a non-section key.
+        /// </param>
+        /// <param name="key">
+        ///     The name of the key in the section.
+        /// </param>
+        /// <param name="index">
+        ///     The index of the key whose the value is associated.
+        /// </param>
+        /// <param name="defValue">
+        ///     The default value that is returned if no value is found.
+        ///     <para>
+        ///         Should be <see langword="null"/> if a new object value is to be set.
+        ///     </para>
+        /// </param>
+        public object this[string section, string key, int index, object defValue]
+        {
+            get => GetValue(section, key, index) ?? defValue;
+            set => AddOrUpdate(section, key, index, value?.ToString());
+        }
+
+        /// <summary>
+        ///     Gets or sets the object value under the first index of the specified key in
+        ///     the specified section of this instance.
+        /// </summary>
+        /// <param name="section">
+        ///     The name of the section. Must be <see langword="null"/> to handle values of
+        ///     a non-section key.
+        /// </param>
+        /// <param name="key">
+        ///     The name of the key in the section.
+        /// </param>
+        /// <param name="defValue">
+        ///     The default value that is returned if no value is found.
+        ///     <para>
+        ///         Should be <see langword="null"/> if a new object value is to be set.
+        ///     </para>
+        /// </param>
+        public object this[string section, string key, object defValue]
+        {
+            get => GetValue(section, key) ?? defValue;
+            set => AddOrUpdate(section, key, value?.ToString());
+        }
+
         private static string NonSectionKey { get; } = Guid.NewGuid().Encrypt();

         private static AlphaNumericComparer SortComparer { get; } = new AlphaNumericComparer();
-
-        private IDictionary<string, IDictionary<string, IList<string>>> Document { get; set; }

         /// <summary>
         ///     Initializes a new instance of the <see cref="Ini"/> class with the
@@ -131,9 +198,37 @@
         /// </summary>
         public Ini() : this(null) { }

-        /// <summary>
-        ///     Loads the full content of an INI file or an INI file formatted string value
-        ///     into this <see cref="Ini"/> instance.
+        private Ini(SerializationInfo info, StreamingContext context)
+        {
+            if (info == null)
+                throw new ArgumentNullException(nameof(info));
+
+            if (Log.DebugMode > 1)
+                Log.Write($"{nameof(Ini)}.ctor({nameof(SerializationInfo)}, {nameof(StreamingContext)}) => info: {Json.Serialize(info)}, context: {Json.Serialize(context)}");
+
+            _document = (IDocumentDictionary)info.GetValue(nameof(Document), typeof(IDocumentDictionary));
+            Comparer = (StringComparer)info.GetValue(nameof(Comparer), typeof(StringComparer));
+            Sorted = info.GetBoolean(nameof(Sorted));
+            FilePath = info.GetString(nameof(FilePath));
+        }
+
+        /// <summary>
+        ///     Gets the regular expression used to parse the INI document in an accessible
+        ///     format.
+        /// </summary>
+        /// <param name="allowEmptySection">
+        ///     <see langword="true"/> to allow key value pairs without section; otherwise,
+        ///     <see langword="false"/>.
+        /// </param>
+        public static Regex GetParseRegex(bool allowEmptySection = true, bool ignoreCases = true)
+        {
+            var options = (ignoreCases ? RegexOptions.IgnoreCase : RegexOptions.None) | RegexOptions.Multiline;
+            return new Regex(allowEmptySection ? ParsePatternDefault : ParsePatternStrict, options);
+        }
+
+        /// <summary>
+        ///     Converts the content of an INI file or an INI file formatted string value
+        ///     to a <see cref="IDictionary{TKey, TValue}"/> object.
         /// </summary>
         /// <param name="fileOrContent">
         ///     The path or content of an INI file.
@@ -146,274 +241,7 @@
         ///     <see langword="true"/> to sort the sections and keys; otherwise,
         ///     <see langword="false"/>.
         /// </param>
-        public void Load(string fileOrContent, bool ignoreCase = true, bool sorted = true)
-        {
-            if (!string.IsNullOrEmpty(fileOrContent))
-            {
-                if (FileEx.Exists(fileOrContent))
-                    FilePath = fileOrContent;
-                Comparer = ignoreCase ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;
-                Sorted = sorted;
-                if (TryParse(fileOrContent, ignoreCase, sorted, out var content))
-                {
-                    Document = content;
-                    return;
-                }
-            }
-            Document = new Dictionary<string, IDictionary<string, IList<string>>>(Comparer);
-        }
-
-        /// <summary>
-        ///     Loads the content of the file at <see cref="FilePath"/> into this
-        ///     <see cref="Ini"/> instance.
-        /// </summary>
-        /// <param name="ignoreCase">
-        ///     <see langword="true"/> to ignore the case of sections and keys; otherwise,
-        ///     <see langword="false"/>.
-        /// </param>
-        /// <param name="sorted">
-        ///     <see langword="true"/> to sort the sections and keys; otherwise,
-        ///     <see langword="false"/>.
-        /// </param>
-        public void Load(bool ignoreCase = true, bool sorted = true) =>
-            Load(FilePath, ignoreCase, sorted);
-
-        /// <summary>
-        ///     Saves content of this <see cref="Ini"/> instance to the specified path.
-        /// </summary>
-        /// <param name="path">
-        ///     The path to file to be written.
-        /// </param>
-        /// <param name="setNewFilePath">
-        ///     <see langword="true"/> to set a new value of the <see cref="FilePath"/>
-        ///     property; otherwise, <see langword="false"/>.
-        /// </param>
-        public void Save(string path, bool setNewFilePath = false)
-        {
-            if (setNewFilePath)
-                FilePath = path;
-            WriteAll(Document, path, Sorted);
-        }
-
-        /// <summary>
-        ///     Saves content of this <see cref="Ini"/> instance to the path at
-        ///     <see cref="FilePath"/>.
-        /// </summary>
-        public void Save() =>
-            WriteAll(Document, FilePath, Sorted);
-
-        /// <summary>
-        ///     Gets a collection of all sections of this <see cref="Ini"/> instance.
-        /// </summary>
-        public ICollection<string> GetSections() =>
-            Document.Keys;
-
-        /// <summary>
-        ///     Gets a collection of all keys under the specified section of this
-        ///     <see cref="Ini"/> instance.
-        /// </summary>
-        /// <param name="section">
-        ///     The name of the section to get the keys. Must be <see langword="null"/> to
-        ///     retrieve the non-section keys.
-        /// </param>
-        public ICollection<string> GetKeys(string section) =>
-            Document?.ContainsKey(section ??= string.Empty) == true ? Document[section].Keys : Array.Empty<string>();
-
-        /// <summary>
-        ///     Gets a list of all values under the specified key of the specified section
-        ///     of this <see cref="Ini"/> instance.
-        /// </summary>
-        /// <param name="section">
-        ///     The name of the section containing the key. Must be <see langword="null"/>
-        ///     to retrieve the values of a non-section key.
-        /// </param>
-        /// <param name="key">
-        ///     The name of the key in the section.
-        /// </param>
-        public IList<string> GetValues(string section, string key) =>
-            Document.ContainsKey(section ??= string.Empty) && Document[section].ContainsKey(key) ? Document[section][key] : Array.Empty<string>();
-
-        /// <summary>
-        ///     Gets the value under the specified index of the specified key in the
-        ///     specified section of this <see cref="Ini"/> instance.
-        /// </summary>
-        /// <param name="section">
-        ///     The name of the section. Must be <see langword="null"/> to retrieve the
-        ///     values of a non-section key.
-        /// </param>
-        /// <param name="key">
-        ///     The name of the key in the section.
-        /// </param>
-        /// <param name="index">
-        ///     The zero-based index of the value.
-        /// </param>
-        public string GetValue(string section, string key, int index = 0) =>
-            Document.ContainsKey(section ??= string.Empty) && Document[section].ContainsKey(key) && Document[section][key].Count > index ? Document[section][key][index] : string.Empty;
-
-        /// <summary>
-        ///     Sets the specified value under the index for the specified key in the
-        ///     specified section of this <see cref="Ini"/> instance.
-        /// </summary>
-        /// <param name="section">
-        ///     The name of the section. Must be <see langword="null"/> to retrieve the
-        ///     values of a non-section key.
-        /// </param>
-        /// <param name="key">
-        ///     The name of the key in the section.
-        ///     <para>
-        ///         If <see langword="null"/>, the entire section is removed.
-        ///     </para>
-        /// </param>
-        /// <param name="index">
-        ///     The zero-based index of the value.
-        ///     <para>
-        ///         If <see langword="null"/>, the entire key is removed.
-        ///     </para>
-        ///     <para>
-        ///         If out of range, a new value is added to the list of values.
-        ///     </para>
-        /// </param>
-        /// <param name="value">
-        ///     The value to be set.
-        /// </param>
-        /// <exception cref="ArgumentInvalidException">
-        ///     section or key contains invalid characters.
-        /// </exception>
-        public void AddOrUpdate(string section, string key, int index, string value)
-        {
-            if ((section ??= string.Empty).Any(TextEx.IsLineSeparator))
-                throw new ArgumentInvalidException(nameof(section));
-            if (key?.Any(TextEx.IsLineSeparator) == true)
-                throw new ArgumentInvalidException(nameof(key));
-            if (string.IsNullOrEmpty(key))
-            {
-                Remove(section);
-                return;
-            }
-            if (string.IsNullOrEmpty(value))
-            {
-                RemoveAt(section, key, index);
-                return;
-            }
-            if (!Document.ContainsKey(section))
-                Document.Add(section, new Dictionary<string, IList<string>>(Comparer));
-            if (!Document[section].ContainsKey(key))
-                Document[section].Add(key, new List<string>());
-            if (!Document[section][key].Any() || Document[section][key].Count < index)
-            {
-                Document[section][key].Add(value);
-                return;
-            }
-            Document[section][key][index] = value;
-        }
-
-        /// <summary>
-        ///     Sets the specified value under the first index for the specified key in the
-        ///     specified section of this <see cref="Ini"/> instance.
-        /// </summary>
-        /// <param name="section">
-        ///     The name of the section. Must be <see langword="null"/> to retrieve the
-        ///     values of a non-section key.
-        /// </param>
-        /// <param name="key">
-        ///     The name of the key in the section.
-        /// </param>
-        /// <param name="value">
-        ///     The value to be set.
-        /// </param>
-        public void AddOrUpdate(string section, string key, string value) =>
-            AddOrUpdate(section, key, 0, value);
-
-        /// <summary>
-        ///     Removes the value at the specified index under the specified key in the
-        ///     specified section of this <see cref="Ini"/> instance.
-        /// </summary>
-        /// <param name="section">
-        ///     The name of the section. Must be <see langword="null"/> to handle
-        ///     non-section keys.
-        /// </param>
-        /// <param name="key">
-        ///     The name of the key in the section.
-        /// </param>
-        /// <param name="index">
-        ///     The zero-based index of the value to remove.
-        /// </param>
-        public void RemoveAt(string section, string key, int index)
-        {
-            if (Document == default || !Document.ContainsKey(section ??= string.Empty) || !Document[section].ContainsKey(key) || Document[section][key].Count <= index)
-                return;
-            Document[section][key].RemoveAt(index);
-        }
-
-        /// <summary>
-        ///     Removes all values from the specified key in the specified section of this
-        ///     <see cref="Ini"/> instance.
-        /// </summary>
-        /// <param name="section">
-        ///     The name of the section. Must be <see langword="null"/> to handle
-        ///     non-section keys.
-        /// </param>
-        /// <param name="key">
-        ///     The name of the key in the section.
-        /// </param>
-        public void Remove(string section, string key)
-        {
-            if (!Document.ContainsKey(section ??= string.Empty) || !Document[section].ContainsKey(key))
-                return;
-            Document[section].Remove(key);
-        }
-
-        /// <summary>
-        ///     Removes all keys and their associated values from the specified section of
-        ///     this <see cref="Ini"/> instance.
-        /// </summary>
-        /// <param name="section">
-        ///     The name of the section. Must be <see langword="null"/> to handle
-        ///     non-section keys.
-        /// </param>
-        public void Remove(string section)
-        {
-            if (!Document.ContainsKey(section ??= string.Empty))
-                return;
-            Document.Remove(section);
-        }
-
-        /// <summary>
-        ///     Removes all sections from this <see cref="Ini"/> instance.
-        /// </summary>
-        public void Clear() =>
-            Document?.Clear();
-
-        /// <summary>
-        ///     Gets the regular expression used to parse the INI document in an accessible
-        ///     format.
-        /// </summary>
-        /// <param name="allowEmptySection">
-        ///     <see langword="true"/> to allow key value pairs without section; otherwise,
-        ///     <see langword="false"/>.
-        /// </param>
-        public static Regex GetParseRegex(bool allowEmptySection = true, bool ignoreCases = true)
-        {
-            var options = (ignoreCases ? RegexOptions.IgnoreCase : RegexOptions.None) | RegexOptions.Multiline;
-            return new Regex(allowEmptySection ? ParsePatternDefault : ParsePatternStrict, options);
-        }
-
-        /// <summary>
-        ///     Converts the content of an INI file or an INI file formatted string value
-        ///     to a <see cref="IDictionary{TKey, TValue}"/> object.
-        /// </summary>
-        /// <param name="fileOrContent">
-        ///     The path or content of an INI file.
-        /// </param>
-        /// <param name="ignoreCase">
-        ///     <see langword="true"/> to ignore the case of sections and keys; otherwise,
-        ///     <see langword="false"/>.
-        /// </param>
-        /// <param name="sorted">
-        ///     <see langword="true"/> to sort the sections and keys; otherwise,
-        ///     <see langword="false"/>.
-        /// </param>
-        public static IDictionary<string, IDictionary<string, IList<string>>> Parse(string fileOrContent, bool ignoreCase = true, bool sorted = true)
+        public static IDocumentDictionary Parse(string fileOrContent, bool ignoreCase = true, bool sorted = true)
         {
             var source = fileOrContent;
             if (string.IsNullOrEmpty(source))
@@ -424,7 +252,7 @@
             source = ForceFormat(source);

             var comparer = ignoreCase ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;
-            var content = new Dictionary<string, IDictionary<string, IList<string>>>(comparer);
+            var content = new Dictionary<string, IKeyValueDictionary>(comparer);
             foreach (var match in GetParseRegex(true, ignoreCase).Matches(source).Cast<Match>())
             {
                 var section = match.Groups["Section"]?.Value.Trim();
@@ -472,7 +300,7 @@
         ///     <see langword="true"/> to sort the sections and keys; otherwise,
         ///     <see langword="false"/>.
         /// </param>
-        public static bool TryParse(string fileOrContent, bool ignoreCase, bool sorted, out IDictionary<string, IDictionary<string, IList<string>>> value)
+        public static bool TryParse(string fileOrContent, bool ignoreCase, bool sorted, out IDocumentDictionary value)
         {
             try
             {
@@ -498,7 +326,7 @@
         ///     <see langword="true"/> to ignore the case of sections and keys; otherwise,
         ///     <see langword="false"/>.
         /// </param>
-        public static bool TryParse(string fileOrContent, bool ignoreCase, out IDictionary<string, IDictionary<string, IList<string>>> value) =>
+        public static bool TryParse(string fileOrContent, bool ignoreCase, out IDocumentDictionary value) =>
             TryParse(fileOrContent, ignoreCase, true, out value);

         /// <summary>
@@ -508,79 +336,103 @@
         /// <param name="fileOrContent">
         ///     The path or content of an INI file.
         /// </param>
-        public static bool TryParse(string fileOrContent, out IDictionary<string, IDictionary<string, IList<string>>> value) =>
+        public static bool TryParse(string fileOrContent, out IDocumentDictionary value) =>
             TryParse(fileOrContent, true, true, out value);

         /// <summary>
-        ///     Writes the specified content to an file on the disk.
-        /// </summary>
-        /// <param name="content">
-        ///     The content based on <see cref="Parse(string, bool, bool)"/>.
-        /// </param>
-        /// <param name="file">
-        ///     The file path of an INI file.
+        ///     Writes the specified dictionary representation of an INI file to the
+        ///     specified stream.
+        /// </summary>
+        /// <param name="source">
+        ///     The dictionary representation of an INI file.
+        /// </param>
+        /// <param name="destination">
+        ///     The stream to write.
         /// </param>
         /// <param name="sorted">
         ///     <see langword="true"/> to sort the sections and keys; otherwise,
         ///     <see langword="false"/>.
         /// </param>
-        public static bool WriteAll(IDictionary<string, IDictionary<string, IList<string>>> content, string file, bool sorted = true)
-        {
-            try
-            {
-                if (content == null)
-                    throw new ArgumentNullException(nameof(content));
-                if (!content.Any() || !content.Values.Any() || content.Values.All(x => x?.Any() != true))
-                    throw new ArgumentInvalidException(nameof(content));
-
-                var path = PathEx.Combine(file);
-                if (string.IsNullOrEmpty(path))
-                    throw new ArgumentNullException(nameof(file));
-                if (!PathEx.IsValidPath(path))
-                    throw new ArgumentInvalidException(nameof(file));
-
-                var source = content;
-                if (sorted)
-                    source = SortHelper(source);
-
-                var hash = File.Exists(path) ? path.EncryptFile(ChecksumAlgorithm.Crc32) : null;
-                var temp = FileEx.GetUniqueTempPath("tmp", ".ini");
-                using (var sw = new StreamWriter(temp, true))
-                    foreach (var dict in source)
+        /// <exception cref="ArgumentNullException">
+        ///     source or destination is null.
+        /// </exception>
+        public static void WriteToStream(IDocumentDictionary source, Stream destination, bool sorted = true)
+        {
+            if (source == null)
+                throw new ArgumentNullException(nameof(source));
+            if (destination == null)
+                throw new ArgumentNullException(nameof(destination));
+            var document = source;
+            if (sorted)
+                document = SortHelper(document);
+            using var sw = new StreamWriter(destination, new UTF8Encoding(false, true), 4096, true);
+            foreach (var dict in document)
+            {
+                if (!dict.Value.Any() || dict.Key.Any(TextEx.IsLineSeparator))
+                    continue;
+                if (!string.IsNullOrEmpty(dict.Key))
+                {
+                    sw.Write('[');
+                    sw.Write(dict.Key.Trim());
+                    sw.Write(']');
+                    sw.WriteLine();
+                }
+                foreach (var pair in dict.Value)
+                {
+                    if (string.IsNullOrWhiteSpace(pair.Key) || pair.Key.Any(TextEx.IsLineSeparator) || !pair.Value.Any())
+                        continue;
+                    foreach (var value in pair.Value)
                     {
-                        if (!dict.Value.Any() || dict.Key.Any(TextEx.IsLineSeparator))
+                        if (string.IsNullOrWhiteSpace(value))
                             continue;
-                        if (!string.IsNullOrEmpty(dict.Key))
+                        sw.Write(pair.Key.Trim());
+                        sw.Write('=');
+                        if (!value.Any(TextEx.IsLineSeparator))
+                            sw.Write(value.Trim());
+                        else
                         {
-                            sw.Write('[');
-                            sw.Write(dict.Key.Trim());
-                            sw.Write(']');
-                            sw.WriteLine();
-                        }
-                        foreach (var pair in dict.Value)
-                        {
-                            if (string.IsNullOrWhiteSpace(pair.Key) || pair.Key.Any(TextEx.IsLineSeparator) || !pair.Value.Any())
-                                continue;
-                            foreach (var value in pair.Value)
-                            {
-                                if (string.IsNullOrWhiteSpace(value))
-                                    continue;
-                                sw.Write(pair.Key.Trim());
-                                sw.Write('=');
-                                if (!value.Any(TextEx.IsLineSeparator))
-                                    sw.Write(value.Trim());
-                                else
-                                {
-                                    sw.Write(EncodeTagOpen);
-                                    sw.Write(value.Encode());
-                                    sw.Write(EncodeTagClose);
-                                }
-                                sw.WriteLine();
-                            }
+                            sw.Write(EncodeTagOpen);
+                            sw.Write(value.Encode());
+                            sw.Write(EncodeTagClose);
                         }
                         sw.WriteLine();
                     }
-
+                }
+                sw.WriteLine();
+            }
+        }
+
+        /// <summary>
+        ///     Writes the specified dictionary representation of an INI file to the
+        ///     specified destination file.
+        /// </summary>
+        /// <param name="source">
+        ///     The dictionary representation of an INI file.
+        /// </param>
+        /// <param name="destination">
+        ///     The file to write.
+        /// </param>
+        /// <param name="sorted">
+        ///     <see langword="true"/> to sort the sections and keys; otherwise,
+        ///     <see langword="false"/>.
+        /// </param>
+        public static bool WriteToFile(IDocumentDictionary source, string destination, bool sorted = true)
+        {
+            try
+            {
+                if (source == null)
+                    throw new ArgumentNullException(nameof(source));
+                if (!source.Any() || !source.Values.Any() || source.Values.All(x => x?.Any() != true))
+                    throw new ArgumentInvalidException(nameof(source));
+                var path = PathEx.Combine(destination);
+                if (string.IsNullOrEmpty(path))
+                    throw new ArgumentNullException(nameof(destination));
+                if (!PathEx.IsValidPath(path))
+                    throw new ArgumentInvalidException(nameof(destination));
+                var hash = File.Exists(path) ? path.EncryptFile(ChecksumAlgorithm.Crc32) : null;
+                var temp = FileEx.GetUniqueTempPath("tmp", ".ini");
+                using (var fs = new FileStream(temp, FileMode.Create))
+                    WriteToStream(source, fs, sorted);
                 if (hash == temp.EncryptFile(ChecksumAlgorithm.Crc32))
                 {
                     File.Delete(temp);
@@ -596,6 +448,26 @@
                 Log.Write(ex);
             }
             return false;
+        }
+
+        /// <summary>
+        ///     Writes the specified read-only dictionary representation of an INI file to
+        ///     the specified file.
+        /// </summary>
+        /// <param name="source">
+        ///     The read-only dictionary representation of an INI file.
+        /// </param>
+        /// <param name="destination">
+        ///     The file path of an INI file.
+        /// </param>
+        /// <param name="sorted">
+        ///     <see langword="true"/> to sort the sections and keys; otherwise,
+        ///     <see langword="false"/>.
+        /// </param>
+        public static bool WriteToFile(IReadOnlyDocumentDictionary source, string destination, bool sorted = true)
+        {
+            var document = source?.ToDictionary(d => d.Key, d => d.Value?.ToDictionary(p => p.Key, p => p.Value?.ToList() as IList<string>) as IKeyValueDictionary);
+            return WriteToFile(document, destination, sorted);
         }

         /// <summary>
@@ -712,10 +584,373 @@
             }
         }

-        private static IDictionary<string, IDictionary<string, IList<string>>> SortHelper(IDictionary<string, IDictionary<string, IList<string>>> source) =>
+        /// <summary>
+        ///     Loads the full content of an INI file or an INI file formatted string value
+        ///     into this instance.
+        /// </summary>
+        /// <param name="fileOrContent">
+        ///     The path or content of an INI file.
+        /// </param>
+        /// <param name="ignoreCase">
+        ///     <see langword="true"/> to ignore the case of sections and keys; otherwise,
+        ///     <see langword="false"/>.
+        /// </param>
+        /// <param name="sorted">
+        ///     <see langword="true"/> to sort the sections and keys; otherwise,
+        ///     <see langword="false"/>.
+        /// </param>
+        public void Load(string fileOrContent, bool ignoreCase = true, bool sorted = true)
+        {
+            if (!string.IsNullOrEmpty(fileOrContent))
+            {
+                FilePath = FileEx.Exists(fileOrContent) ? fileOrContent : FileEx.GetUniqueTempPath("tmp", ".ini");
+                Comparer = ignoreCase ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;
+                Sorted = sorted;
+                if (TryParse(fileOrContent, ignoreCase, sorted, out var document))
+                {
+                    _document = document;
+                    return;
+                }
+            }
+            _document = new Dictionary<string, IKeyValueDictionary>(Comparer);
+        }
+
+        /// <summary>
+        ///     Loads the content of the file at <see cref="FilePath"/> into this instance.
+        /// </summary>
+        /// <param name="ignoreCase">
+        ///     <see langword="true"/> to ignore the case of sections and keys; otherwise,
+        ///     <see langword="false"/>.
+        /// </param>
+        /// <param name="sorted">
+        ///     <see langword="true"/> to sort the sections and keys; otherwise,
+        ///     <see langword="false"/>.
+        /// </param>
+        public void Load(bool ignoreCase = true, bool sorted = true) =>
+            Load(FilePath, ignoreCase, sorted);
+
+        /// <summary>
+        ///     Saves the string representation of this instance to the specified path.
+        /// </summary>
+        /// <param name="path">
+        ///     The path to file to be written.
+        /// </param>
+        /// <param name="setNewFilePath">
+        ///     <see langword="true"/> to set a new value of the <see cref="FilePath"/>
+        ///     property; otherwise, <see langword="false"/>.
+        /// </param>
+        public void Save(string path, bool setNewFilePath = false)
+        {
+            if (setNewFilePath)
+                FilePath = path;
+            WriteToFile(_document, path, Sorted);
+        }
+
+        /// <summary>
+        ///     Saves the string representation of this instance to <see cref="FilePath"/>.
+        /// </summary>
+        public void Save() =>
+            WriteToFile(_document, FilePath, Sorted);
+
+        /// <summary>
+        ///     Sets the <see cref="SerializationInfo"/> object for this instance.
+        /// </summary>
+        /// <param name="info">
+        ///     The object that holds the serialized object data.
+        /// </param>
+        /// <param name="context">
+        ///     The contextual information about the source or destination.
+        /// </param>
+        [SecurityCritical]
+        public void GetObjectData(SerializationInfo info, StreamingContext context)
+        {
+            if (info == null)
+                throw new ArgumentNullException(nameof(info));
+
+            if (Log.DebugMode > 1)
+                Log.Write($"{nameof(Ini)}.get({nameof(SerializationInfo)}, {nameof(StreamingContext)}) => info: {Json.Serialize(info)}, context: {Json.Serialize(context)}");
+
+            info.AddValue(nameof(Document), _document);
+            info.AddValue(nameof(Comparer), Comparer);
+            info.AddValue(nameof(Sorted), Sorted);
+            info.AddValue(nameof(FilePath), FilePath);
+        }
+
+        /// <summary>
+        ///     Gets a collection of all sections of this instance.
+        /// </summary>
+        public ICollection<string> GetSections()
+        {
+            var sections = _document.Keys;
+            if (Sorted && sections.Count > 1)
+                return sections.OrderBy(s => !string.IsNullOrEmpty(s)).ThenBy(s => s, SortComparer).ToList();
+            return sections;
+        }
+
+        /// <summary>
+        ///     Gets a collection of all keys under the specified section of this instance.
+        /// </summary>
+        /// <param name="section">
+        ///     The name of the section to get the keys. Must be <see langword="null"/> to
+        ///     retrieve the non-section keys.
+        /// </param>
+        public ICollection<string> GetKeys(string section)
+        {
+            var keys = _document.ContainsKey(section ??= string.Empty) ? _document[section].Keys : new Collection<string>();
+            if (Sorted && keys.Count > 1)
+                return keys.OrderBy(s => s, SortComparer).ToList();
+            return keys;
+        }
+
+        /// <summary>
+        ///     Gets a list of all values under the specified key of the specified section
+        ///     of this instance.
+        /// </summary>
+        /// <param name="section">
+        ///     The name of the section containing the key. Must be <see langword="null"/>
+        ///     to retrieve the values of a non-section key.
+        /// </param>
+        /// <param name="key">
+        ///     The name of the key in the section.
+        /// </param>
+        public IList<string> GetValues(string section, string key) =>
+            _document.ContainsKey(section ??= string.Empty) && _document[section].ContainsKey(key) ? _document[section][key] : new Collection<string>();
+
+        /// <summary>
+        ///     Gets the value under the specified index of the specified key in the
+        ///     specified section of this instance.
+        /// </summary>
+        /// <param name="section">
+        ///     The name of the section. Must be <see langword="null"/> to retrieve the
+        ///     values of a non-section key.
+        /// </param>
+        /// <param name="key">
+        ///     The name of the key in the section.
+        /// </param>
+        /// <param name="index">
+        ///     The zero-based index of the value.
+        /// </param>
+        public string GetValue(string section, string key, int index = 0) =>
+            _document.ContainsKey(section ??= string.Empty) && _document[section].ContainsKey(key) && _document[section][key].Count > index ? _document[section][key][index] : null;
+
+        /// <summary>
+        ///     Copies everything of the specified <see cref="Ini"/> instance to this
+        ///     instance.
+        ///     <para>
+        ///         Please note that all existing values will be overwritten.
+        ///     </para>
+        /// </summary>
+        /// <param name="ini">
+        ///     The <see cref="Ini"/> instance that is merged.
+        /// </param>
+        public void AddOrUpdate(Ini ini)
+        {
+            if (ini == null)
+                return;
+            foreach (var section in ini.GetSections())
+            {
+                foreach (var key in ini.GetKeys(section))
+                {
+                    var values = ini.GetValues(section, key);
+                    if (values?.Any() != true)
+                        continue;
+                    for (var i = 0; i < values.Count; i++)
+                        AddOrUpdate(section, key, i, values[0]);
+                }
+            }
+        }
+
+        /// <summary>
+        ///     Sets the specified value under the index for the specified key in the
+        ///     specified section of this instance.
+        /// </summary>
+        /// <param name="section">
+        ///     The name of the section. Must be <see langword="null"/> to retrieve the
+        ///     values of a non-section key.
+        /// </param>
+        /// <param name="key">
+        ///     The name of the key in the section.
+        ///     <para>
+        ///         If <see langword="null"/>, the entire section is removed.
+        ///     </para>
+        /// </param>
+        /// <param name="index">
+        ///     The zero-based index of the value.
+        ///     <para>
+        ///         If <see langword="null"/>, the entire key is removed.
+        ///     </para>
+        ///     <para>
+        ///         If out of range, a new value is added to the list of values.
+        ///     </para>
+        /// </param>
+        /// <param name="value">
+        ///     The value to be set.
+        /// </param>
+        /// <exception cref="ArgumentInvalidException">
+        ///     section or key contains invalid characters.
+        /// </exception>
+        /// <exception cref="ArgumentOutOfRangeException">
+        ///     index is below zero.
+        /// </exception>
+        public void AddOrUpdate(string section, string key, int index, string value)
+        {
+            if (index < 0)
+                throw new ArgumentOutOfRangeException(nameof(index));
+            if (key?.Any(TextEx.IsLineSeparator) == true)
+                throw new ArgumentInvalidException(nameof(key));
+            if ((section ??= string.Empty).Any(TextEx.IsLineSeparator))
+                throw new ArgumentInvalidException(nameof(section));
+            if (string.IsNullOrEmpty(key))
+            {
+                Remove(section);
+                return;
+            }
+            if (string.IsNullOrEmpty(value))
+            {
+                RemoveAt(section, key, index);
+                return;
+            }
+            if (!_document.ContainsKey(section))
+                _document.Add(section, new Dictionary<string, IList<string>>(Comparer));
+            if (!_document[section].ContainsKey(key))
+                _document[section].Add(key, new List<string>());
+            if (!_document[section][key].Any() || _document[section][key].Count < index)
+            {
+                _document[section][key].Add(value);
+                return;
+            }
+            _document[section][key][index] = value;
+        }
+
+        /// <summary>
+        ///     Sets the specified value under the first index for the specified key in the
+        ///     specified section of this instance.
+        /// </summary>
+        /// <param name="section">
+        ///     The name of the section. Must be <see langword="null"/> to retrieve the
+        ///     values of a non-section key.
+        /// </param>
+        /// <param name="key">
+        ///     The name of the key in the section.
+        /// </param>
+        /// <param name="value">
+        ///     The value to be set.
+        /// </param>
+        /// <exception cref="ArgumentInvalidException">
+        ///     section or key contains invalid characters.
+        /// </exception>
+        public void AddOrUpdate(string section, string key, string value) =>
+            AddOrUpdate(section, key, 0, value);
+
+        /// <summary>
+        ///     Removes the value at the specified index under the specified key in the
+        ///     specified section of this instance.
+        /// </summary>
+        /// <param name="section">
+        ///     The name of the section. Must be <see langword="null"/> to handle
+        ///     non-section keys.
+        /// </param>
+        /// <param name="key">
+        ///     The name of the key in the section.
+        /// </param>
+        /// <param name="index">
+        ///     The zero-based index of the value to remove.
+        /// </param>
+        public void RemoveAt(string section, string key, int index)
+        {
+            if (_document == default || index < 0 || !_document.ContainsKey(section ??= string.Empty) || !_document[section].ContainsKey(key) || _document[section][key].Count <= index)
+                return;
+            _document[section][key].RemoveAt(index);
+        }
+
+        /// <summary>
+        ///     Removes all values from the specified key in the specified section of this
+        ///     instance.
+        /// </summary>
+        /// <param name="section">
+        ///     The name of the section. Must be <see langword="null"/> to handle
+        ///     non-section keys.
+        /// </param>
+        /// <param name="key">
+        ///     The name of the key in the section.
+        /// </param>
+        public void Remove(string section, string key)
+        {
+            if (!_document.ContainsKey(section ??= string.Empty) || !_document[section].ContainsKey(key))
+                return;
+            _document[section].Remove(key);
+        }
+
+        /// <summary>
+        ///     Removes all keys and their associated values from the specified section of
+        ///     this instance.
+        /// </summary>
+        /// <param name="section">
+        ///     The name of the section. Must be <see langword="null"/> to handle
+        ///     non-section keys.
+        /// </param>
+        public void Remove(string section)
+        {
+            if (!_document.ContainsKey(section ??= string.Empty))
+                return;
+            _document.Remove(section);
+        }
+
+        /// <summary>
+        ///     Removes all sections from this instance.
+        /// </summary>
+        public void Clear() =>
+            _document?.Clear();
+
+        /// <summary>
+        ///     Returns the hash code for this instance.
+        /// </summary>
+        public override int GetHashCode() =>
+            _identifier.GetHashCode();
+
+        /// <summary>
+        ///     Determines whether this instance have same values as the specified
+        ///     <see cref="Ini"/> instance.
+        /// </summary>
+        /// <param name="other">
+        ///     The <see cref="Ini"/> instance to compare.
+        /// </param>
+        public bool Equals(Ini other)
+        {
+            if (other == null || Comparer != other.Comparer || Sorted != other.Sorted || FilePath != other.FilePath || _document.Count != other._document.Count)
+                return false;
+            return this.EncryptRaw() == other.EncryptRaw();
+        }
+
+        /// <summary>
+        ///     Determines whether this instance have same values as the specified
+        ///     <see cref="object"/>.
+        /// </summary>
+        /// <param name="other">
+        ///     The  <see cref="object"/> to compare.
+        /// </param>
+        public override bool Equals(object other)
+        {
+            if (!(other is Ini ini))
+                return false;
+            return Equals(ini);
+        }
+
+        /// <summary>
+        ///     Returns a string that represents the current <see cref="Ini"/> object.
+        /// </summary>
+        public override string ToString()
+        {
+            using var ms = new MemoryStream();
+            WriteToStream(_document, ms, Sorted);
+            ms.Position = 0;
+            return ms.ToArray().ToStringDefault();
+        }
+
+        private static IDocumentDictionary SortHelper(IDocumentDictionary source) =>
             source.OrderBy(p => !string.IsNullOrEmpty(p.Key)).ThenBy(p => p.Key, SortComparer).ToDictionary(p => p.Key, p => SortHelper(p.Value));

-        private static IDictionary<string, IList<string>> SortHelper(IDictionary<string, IList<string>> source) =>
+        private static IKeyValueDictionary SortHelper(IKeyValueDictionary source) =>
             source.OrderBy(p => p.Key, SortComparer).ToDictionary(p => p.Key, p => p.Value);

         private static bool LineIsValid(string str)
@@ -754,5 +989,31 @@
             builder.Insert(0, '[');
             return builder.ToStringThenClear();
         }
+
+        /// <summary>
+        ///     Determines whether two specified <see cref="Ini"/> instances have same
+        ///     values.
+        /// </summary>
+        /// <param name="left">
+        ///     The first <see cref="Ini"/> instance to compare.
+        /// </param>
+        /// <param name="right">
+        ///     The second <see cref="Ini"/> instance to compare.
+        /// </param>
+        public static bool operator ==(Ini left, Ini right) =>
+            left?.Equals(right) ?? right is null;
+
+        /// <summary>
+        ///     Determines whether two specified <see cref="Ini"/> instances have different
+        ///     values.
+        /// </summary>
+        /// <param name="left">
+        ///     The first <see cref="Ini"/> instance to compare.
+        /// </param>
+        /// <param name="right">
+        ///     The second <see cref="Ini"/> instance to compare.
+        /// </param>
+        public static bool operator !=(Ini left, Ini right) =>
+            !(left == right);
     }
 }


Simplify expand number check

Simplify expand number check

--- osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs
+++ osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs
@@ -126,8 +126,8 @@
                             break;

                         case OsuSkinConfiguration.ExpandNumberPiece:
-                            decimal legacyVersion = source.GetConfig<LegacySetting, decimal>(LegacySetting.Version)?.Value ?? 1.0m;
-                            return SkinUtils.As<TValue>(new BindableBool(legacyVersion < 2.0m));
+                            bool expand = source.GetConfig<LegacySetting, decimal>(LegacySetting.Version)?.Value < 2.0m;
+                            return SkinUtils.As<TValue>(new BindableBool(expand));
                     }

                     break;


Remove IHasFilterableChildren from class

--- osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs
+++ osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs
@@ -15,7 +15,7 @@

 namespace osu.Game.Screens.Multi.Lounge.Components
 {
-    public class RoomsContainer : CompositeDrawable, IHasFilterableChildren
+    public class RoomsContainer : CompositeDrawable
     {
         public Action<Room> JoinRequested;

@@ -114,11 +114,5 @@

             selectedRoom.Value = room;
         }
-
-        public IEnumerable<string> FilterTerms => Enumerable.Empty<string>();
-
-        public IEnumerable<IFilterable> FilterableChildren => InternalChildren.OfType<IFilterable>();
-
-        public bool MatchingFilter { set { } }
     }
 }


[packages,C] Fixed debug symbols, omit frame pointer and optimization for other configurations

--- packages/C/bam/Scripts/Defaults/ICommonCompilerSettings.cs
+++ packages/C/bam/Scripts/Defaults/ICommonCompilerSettings.cs
@@ -39,9 +39,9 @@
             Bam.Core.Module module)
         {
             settings.Bits = (module as CModule).BitDepth;
-            settings.DebugSymbols = module.BuildEnvironment.Configuration == Bam.Core.EConfiguration.Debug;
-            settings.OmitFramePointer = module.BuildEnvironment.Configuration != Bam.Core.EConfiguration.Debug;
-            settings.Optimization = module.BuildEnvironment.Configuration == Bam.Core.EConfiguration.Debug ? EOptimization.Off : EOptimization.Speed;
+            settings.DebugSymbols = (0 != (module.BuildEnvironment.Configuration & (Bam.Core.EConfiguration.Debug | Bam.Core.EConfiguration.Profile)));
+            settings.OmitFramePointer = (0 != (module.BuildEnvironment.Configuration & Bam.Core.EConfiguration.NotDebug));
+            settings.Optimization = (0 != (module.BuildEnvironment.Configuration & Bam.Core.EConfiguration.NotDebug)) ? EOptimization.Speed : EOptimization.Off;
             settings.OutputType = ECompilerOutput.CompileOnly;
             settings.PreprocessorDefines.Add(System.String.Format("D_BAM_CONFIGURATION_{0}", module.BuildEnvironment.Configuration.ToString().ToUpper()));
             if (module.BuildEnvironment.Platform.Includes(Bam.Core.EPlatform.Windows))


[packages,Publisher] Fixing Xcode publishing of files with no reference

--- packages/Publisher/bam/XcodeBuilder/XcodeCollatedObject.cs
+++ packages/Publisher/bam/XcodeBuilder/XcodeCollatedObject.cs
@@ -61,7 +61,8 @@
                 return;
             }

-            if (sender.SourceModule.PackageDefinition == sender.Reference.PackageDefinition)
+            if ((null != sender.Reference) && (null != sender.SourceModule) &&
+                (sender.SourceModule.PackageDefinition == sender.Reference.PackageDefinition))
             {
                 // same package has the same output folder, so don't bother copying
                 // TODO: does the destination directory need to be set?


Added Null-Check for EfFlush (#119)

Added Null-Check for EfFlush (#119)

* Added Null-Check for EfFlush

* Move null check

Co-authored-by: Florian Markus <eafc8bd05a0435ef14d31bbff29687dd7b71c3ca@anic-gmbh.com>

--- src/implementations/Backend.Fx.EfCorePersistence/EfFlush.cs
+++ src/implementations/Backend.Fx.EfCorePersistence/EfFlush.cs
@@ -1,4 +1,5 @@
 using System;
+using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Reflection;
 using System.Security.Principal;
@@ -105,7 +106,7 @@
                      .ToArray()
                      .ForAll(entry =>
                      {
-                         EntityEntry aggregateRootEntry = FindAggregateRootEntry(DbContext.ChangeTracker, entry);
+                         EntityEntry aggregateRootEntry = GetAggregateRootEntry(DbContext.ChangeTracker, entry);
                          if (aggregateRootEntry.State == EntityState.Unchanged) aggregateRootEntry.State = EntityState.Modified;
                      });

@@ -141,14 +142,16 @@
                      });
             if (count > 0) Logger.Debug($"Tracked {count} entities as created/changed on {utcNow:u} by {userId}");
         }
-
+
         /// <summary>
         ///     This method finds the EntityEntry<AggregateRoot> of an EntityEntry<Entity>
         ///     assuming it has been loaded and is being tracked by the change tracker.
         /// </summary>
-        private static EntityEntry FindAggregateRootEntry(ChangeTracker changeTracker, EntityEntry entry)
+        [return: NotNull]
+        private static EntityEntry GetAggregateRootEntry(ChangeTracker changeTracker, EntityEntry entry)
         {
-            Logger.Debug($"Searching aggregate root of {entry.Entity.GetType().Name}[{(entry.Entity as Identified)?.Id}]");
+            var entityIdentifier = $"{entry.Entity.GetType().Name}[{(entry.Entity as Identified)?.Id}]";
+            Logger.Debug($"Searching aggregate root of {entityIdentifier}");
             foreach (NavigationEntry navigation in entry.Navigations)
             {
                 TypeInfo navTargetTypeInfo = navigation.Metadata.TargetEntityType.ClrType.GetTypeInfo();
@@ -181,10 +184,10 @@

                 // recurse in case of "Entity -> Entity -> AggregateRoot"
                 Logger.Debug("Recursing...");
-                return FindAggregateRootEntry(changeTracker, navigationTargetEntry);
+                return GetAggregateRootEntry(changeTracker, navigationTargetEntry);
             }

-            return null;
+            throw new InvalidOperationException($"Could not find aggregate root of {entityIdentifier}");
         }
     }
 }


Some event fixes that make sense

Some event fixes that make sense

--- src/tests/TestUtils/Events/IRepositoryManagerListener.cs
+++ src/tests/TestUtils/Events/IRepositoryManagerListener.cs
@@ -9,8 +9,8 @@
     interface IRepositoryManagerListener
     {
         void OnStatusUpdate(GitStatus status);
-        void OnActiveBranchChanged();
-        void OnActiveRemoteChanged();
+        void OnActiveBranchChanged(ConfigBranch? branch);
+        void OnActiveRemoteChanged(ConfigRemote? remote);
         void OnHeadChanged();
         void OnLocalBranchListChanged();
         void OnRemoteBranchListChanged();
@@ -25,7 +25,6 @@
         public AutoResetEvent OnStatusUpdate { get; } = new AutoResetEvent(false);
         public EventWaitHandle OnActiveBranchChanged { get; } = new ManualResetEvent(false);
         public EventWaitHandle OnActiveRemoteChanged { get; } = new ManualResetEvent(false);
-        public EventWaitHandle OnHeadChanged { get; } = new ManualResetEvent(false);
         public EventWaitHandle OnLocalBranchListChanged { get; } = new ManualResetEvent(false);
         public EventWaitHandle OnRemoteBranchListChanged { get; } = new ManualResetEvent(false);
         public EventWaitHandle OnLocksUpdated { get; } = new ManualResetEvent(false);
@@ -34,13 +33,11 @@
         {
             OnIsBusy.Reset();
             OnIsNotBusy.Reset();
-            OnRepositoryChanged.Reset();
+            OnStatusUpdate.Reset();
             OnActiveBranchChanged.Reset();
             OnActiveRemoteChanged.Reset();
-            OnHeadChanged.Reset();
             OnLocalBranchListChanged.Reset();
             OnRemoteBranchListChanged.Reset();
-            OnRemoteOrTrackingChanged.Reset();
             OnLocksUpdated.Reset();
         }
     }
@@ -61,28 +58,22 @@
                     managerEvents?.OnIsNotBusy.Set();
             };

-            repositoryManager.Repository.OnStatusUpdated += status => {
+            repositoryManager.OnStatusUpdated += status => {
                 logger?.Debug("OnStatusUpdated: {0}", status);
                 listener.OnStatusUpdate(status);
-                managerAutoResetEvent?.OnRepositoryChanged.Set();
+                managerEvents?.OnStatusUpdate.Set();
             };

-            repositoryManager.OnActiveBranchChanged += () => {
+            repositoryManager.OnActiveBranchChanged += (branch) => {
                 logger?.Trace($"OnActiveBranchChanged {branch}");
                 listener.OnActiveBranchChanged(branch);
                 managerEvents?.OnActiveBranchChanged.Set();
             };

-            repositoryManager.OnActiveRemoteChanged += () => {
+            repositoryManager.OnActiveRemoteChanged += (remote) => {
                 logger?.Trace($"OnActiveRemoteChanged {(remote.HasValue ? remote.Value.Name : null)}");
                 listener.OnActiveRemoteChanged(remote);
                 managerEvents?.OnActiveRemoteChanged.Set();
-            };
-
-            repositoryManager.OnHeadChanged += () => {
-                logger?.Trace($"OnHeadChanged");
-                listener.OnHeadChanged();
-                managerEvents?.OnHeadChanged.Set();
             };

             repositoryManager.OnLocalBranchListChanged += () => {
@@ -97,12 +88,6 @@
                 managerEvents?.OnRemoteBranchListChanged.Set();
             };

-            repositoryManager.OnRemoteOrTrackingChanged += () => {
-                logger?.Trace("OnRemoteOrTrackingChanged");
-                listener.OnRemoteOrTrackingChanged();
-                managerEvents?.OnRemoteOrTrackingChanged.Set();
-            };
-
             repositoryManager.OnLocksUpdated += locks => {
                 var lockArray = locks.ToArray();
                 logger?.Trace("OnLocksUpdated Count:{0}", lockArray.Length);
@@ -114,7 +99,7 @@
         public static void AssertDidNotReceiveAnyCalls(this IRepositoryManagerListener repositoryManagerListener)
         {
             repositoryManagerListener.DidNotReceive().OnStatusUpdate(Args.GitStatus);
-            repositoryManagerListener.DidNotReceive().OnActiveBranchChanged(Args.String);
+            repositoryManagerListener.DidNotReceive().OnActiveBranchChanged(Arg.Any<ConfigBranch?>());
             repositoryManagerListener.DidNotReceive().OnActiveRemoteChanged(Arg.Any<ConfigRemote?>());
             repositoryManagerListener.DidNotReceive().OnHeadChanged();
             repositoryManagerListener.DidNotReceive().OnLocalBranchListChanged();


Update checker unit test: Return when an event is raised

--- pass-winmenu-tests/UdateChecking/UpdateCheckerUnitTests.cs
+++ pass-winmenu-tests/UdateChecking/UpdateCheckerUnitTests.cs
@@ -105,9 +105,12 @@
 			};
 			checker.Start();

-			// Allow the update checker to raise the event.
-			Thread.Sleep(100);
-
+			// Wait a while to see if the event is raised.
+			for (var i = 0; i < 20; i++)
+			{
+				Thread.Sleep(50);
+				if (raised) return true;
+			}
 			return raised;
 		}


Restore temperature data after changing a tile.

--- src/TabGodMode.cs
+++ src/TabGodMode.cs
@@ -138,6 +138,7 @@
                 }

                 _gameData.GodModeData.SetupTile();
+                LogTemperatureInfo(_gameData.GodModeData.SelectedTileId);
             }

             if (ListingStandard.ButtonText("Redraw Map"))


refactor: ConfiguringPropertySetterSpecs

--- tests/FakeItEasy.Specs/ConfiguringPropertySetterSpecs.cs
+++ tests/FakeItEasy.Specs/ConfiguringPropertySetterSpecs.cs
@@ -18,7 +18,6 @@
             [SuppressMessage("Microsoft.Design", "CA1023:IndexersShouldNotBeMultidimensional", Justification = "Required for testing.")]
             bool this[string genus, string species] { get; set; }

-            [SuppressMessage("Microsoft.Design", "CA1023:IndexersShouldNotBeMultidimensional", Justification = "Required for testing.")]
             int this[string commonName] { get; set; }

             string this[int count] { get; }
@@ -33,9 +32,11 @@
             "When assignment of a property is configured using a null expression"
                 .x(() => exception = Record.Exception(() => A.CallToSet<int>(null)));

-            "Then the call to configure throws"
-                .x(() => exception.Should().BeAnExceptionOfType<ArgumentNullException>()
-                    .And.ParamName.Should().Be("propertySpecification"));
+            "Then an argument null exception is thrown"
+                .x(() => exception.Should().BeAnExceptionOfType<ArgumentNullException>());
+
+            "And the parameter name is 'propertySpecification'"
+                .x(() => exception.As<ArgumentNullException>().ParamName.Should().Be("propertySpecification"));
         }

         [Scenario]
@@ -46,10 +47,10 @@
             "Given a Fake with a property that can't be configured"
                 .x(() => subject = A.Fake<ClassWithInterestingProperties>());

-            "When assignment of the property is configured for any value"
+            "When assignment of the property is configured"
                 .x(() => exception = Record.Exception(() => A.CallToSet(() => subject.NonConfigurableProperty).DoesNothing()));

-            "Then the call to configure throws"
+            "Then a fake configuration exception is thrown"
                 .x(() => exception.Should().BeAnExceptionOfType<FakeConfigurationException>());
         }

@@ -61,12 +62,15 @@
             "Given a Fake with a read-only property"
                 .x(() => subject = A.Fake<IHaveInterestingProperties>());

-            "When assignment of the property is configured for any value"
+            "When assignment of the property is configured"
                 .x(() => exception = Record.Exception(() => A.CallToSet(() => subject.ReadOnlyProperty).DoesNothing()));

-            "Then the call to configure throws"
-                .x(() => exception.Should().BeAnExceptionOfType<ArgumentException>()
-                .And.Message.Should().Be("The property '" + nameof(IHaveInterestingProperties.ReadOnlyProperty) + "' does not have a setter."));
+            "Then an argument exception is thrown"
+                .x(() => exception.Should().BeAnExceptionOfType<ArgumentException>());
+
+            "And the exception message indicates that the property is read-only"
+                .x(() => exception.Message.Should().Be(
+                    $"The property '{nameof(IHaveInterestingProperties.ReadOnlyProperty)}' does not have a setter."));
         }

         [Scenario]
@@ -78,11 +82,14 @@
                 .x(() => subject = A.Fake<IHaveInterestingProperties>());

             "When assignment of the property is configured using a method call expression"
-                .x(() => exception = Record.Exception(() => A.CallToSet(() => subject.MethodThatLooksLikeAPropertyGetter()).DoesNothing()));
-
-            "Then the call to configure throws"
-                .x(() => exception.Should().BeAnExceptionOfType<ArgumentException>()
-                .And.Message.Should().EndWith("' must refer to a property or indexer getter, but doesn't."));
+                .x(() => exception = Record.Exception(() =>
+                    A.CallToSet(() => subject.MethodThatLooksLikeAPropertyGetter()).DoesNothing()));
+
+            "Then an argument exception is thrown"
+                .x(() => exception.Should().BeAnExceptionOfType<ArgumentException>());
+
+            "And the exception message indicates that the expression refers to an incorrect member type"
+                .x(() => exception.Message.Should().EndWith("' must refer to a property or indexer getter, but doesn't."));
         }

         [Scenario]
@@ -96,13 +103,21 @@
             "When assignment of the property is configured using a field access expression"
                 .x(() => exception = Record.Exception(() => A.CallToSet(() => subject.Field).DoesNothing()));

-            "Then the call to configure throws"
-                .x(() => exception.Should().BeAnExceptionOfType<ArgumentException>()
-                .And.Message.Should().Be("The specified expression is not a method call or property getter."));
-        }
-
-        [Scenario]
+            "Then an argument exception is thrown"
+                .x(() => exception.Should().BeAnExceptionOfType<ArgumentException>());
+
+            "And the exception message indicates that the expression refers to an incorrect member type"
+                .x(() => exception.Message.Should().Be("The specified expression is not a method call or property getter."));
+        }
+
+        [Scenario]
+        [Example(int.MinValue)]
+        [Example(-42)]
+        [Example(0)]
+        [Example(42)]
+        [Example(int.MaxValue)]
         public static void ConfiguringSetterForAnyValue(
+            int value,
             IHaveInterestingProperties subject,
             bool wasConfiguredBehaviorUsed)
         {
@@ -112,8 +127,8 @@
             "And assignment of the property is configured for any value"
                 .x(() => A.CallToSet(() => subject.ReadWriteProperty).Invokes(call => wasConfiguredBehaviorUsed = true));

-            "When I assign the property"
-                .x(() => subject.ReadWriteProperty = 7);
+            $"When I assign the property to {value}"
+                .x(() => subject.ReadWriteProperty = value);

             "Then the configured behavior is used"
                 .x(() => wasConfiguredBehaviorUsed.Should().BeTrue());
@@ -127,7 +142,7 @@
             "Given a Fake with an indexer"
                 .x(() => subject = A.Fake<IHaveInterestingProperties>());

-            "And assignment of the indexer is configured for any value"
+            "And assignment of the indexer is configured"
                 .x(() => A.CallToSet(() => subject["Choeropsis", "liberiensis"]).Invokes(call => wasConfiguredBehaviorUsed = true));

             "When I assign the property using the wrong indexes"
@@ -145,7 +160,7 @@
             "Given a Fake with an indexer"
                 .x(() => subject = A.Fake<IHaveInterestingProperties>());

-            "And assignment of the indexer is configured for any value"
+            "And assignment of the indexer is configured"
                 .x(() => A.CallToSet(() => subject["Choeropsis", "liberiensis"]).Invokes(call => wasConfiguredBehaviorUsed = true));

             "When I assign the property using matching indexes"
@@ -164,10 +179,10 @@
             "Given a Fake with an overloaded indexer"
                 .x(() => subject = A.Fake<IHaveInterestingProperties>());

-            "And assignment of the indexer is configured for one signature and any value"
+            "And assignment of the indexer is configured for one signature"
                 .x(() => A.CallToSet(() => subject["Choeropsis", "liberiensis"]).Invokes(call => wasFirstConfiguredBehaviorUsed = true));

-            "And assignment of the indexer is configured for the other signature and any value"
+            "And assignment of the indexer is configured for the other signature"
                 .x(() => A.CallToSet(() => subject["Pygmy hippopotamus"]).Invokes(call => wasSecondConfiguredBehaviorUsed = true));

             "When I assign the property using one signature"
@@ -191,12 +206,14 @@
             "Given a Fake with a read-only indexer"
                 .x(() => subject = A.Fake<IHaveInterestingProperties>());

-            "When assignment of the indexer is configured for any value"
+            "When assignment of the indexer is configured"
                 .x(() => exception = Record.Exception(() => A.CallToSet(() => subject[7]).DoesNothing()));

-            "Then the call to configure throws"
-               .x(() => exception.Should().BeAnExceptionOfType<ArgumentException>()
-               .And.Message.Should().EndWith("refers to an indexed property that does not have a setter."));
+            "Then an argument exception is thrown"
+               .x(() => exception.Should().BeAnExceptionOfType<ArgumentException>());
+
+            "And the exception message indicates that the property is read only"
+                .x(() => exception.Message.Should().EndWith("refers to an indexed property that does not have a setter."));
         }

         [Scenario]
@@ -207,7 +224,7 @@
             "Given a Fake with an indexer"
                 .x(() => subject = A.Fake<IHaveInterestingProperties>());

-            "And assignment of the indexer is configured for any value using WhenArgumentsMatch"
+            "And assignment of the indexer is configured using WhenArgumentsMatch"
                 .x(() => A.CallToSet(() => subject["Choeropsis", "liberiensis"])
                     .WhenArgumentsMatch(arguments => arguments.Get<string>("genus") == "Canis")
                     .Invokes(call => wasConfiguredBehaviorUsed = true));
@@ -267,7 +284,7 @@
             "Given a Fake with an indexer"
                 .x(() => subject = A.Fake<IHaveInterestingProperties>());

-            "And assignment of the indexer is configured for any value and overridden with WithAnyArguments"
+            "And assignment of the indexer is configured for specific arguments and overridden with WithAnyArguments"
                 .x(() => A.CallToSet(() => subject["Choeropsis", "liberiensis"])
                     .WithAnyArguments()
                     .Invokes(call => wasConfiguredBehaviorUsed = true));


Fix TestApp constants declaration

Fix TestApp constants declaration

--- Assets/Adjust/Test/TestApp.cs
+++ Assets/Adjust/Test/TestApp.cs
@@ -9,11 +9,7 @@
     {
         public static readonly string TAG = "[TestApp]";

-#if (UNITY_STANDALONE_WIN)
-        private const string PORT = ":8080";
-        private const string PROTOCOL = "http://";
-        private const string IP = "localhost";
-#elif UNITY_ANDROID
+#if UNITY_ANDROID
         private const string PORT = ":8443";
         private const string PROTOCOL = "https://";
         private const string IP = "192.168.86.80";
@@ -22,6 +18,10 @@
         private const string PROTOCOL = "http://";
         private const string IP = "192.168.86.80";
         private TestLibraryiOS _testLibraryiOS;
+#else
+        private const string PORT = ":8080";
+        private const string PROTOCOL = "http://";
+        private const string IP = "localhost";
 #endif
         private const string BASE_URL = PROTOCOL + IP + PORT;
         private const string GDPR_URL = PROTOCOL + IP + PORT;


Fix bots not destroyed when off side of bullet

--- Assets/BulletGridGenerator.cs
+++ Assets/BulletGridGenerator.cs
@@ -108,6 +108,10 @@
         {
             return;
         }
+        if (position == null)
+        {
+            return;
+        }
         GameObject newBot = Instantiate(nanobot.gameObject);
         GridPositionComponent gridPosition = newBot.GetComponent<GridPositionComponent>();
         setBotPosition(newBot, gridPosition, position);