From b9dae91ac9c9260a469c5322e4e443ef34c98eed Mon Sep 17 00:00:00 2001 From: ig-sinicyn Date: Sun, 29 Dec 2019 00:04:42 +0300 Subject: [PATCH 1/2] + Connection string parser/formatter class (based on DbConnectionStringBuilder) + string.ToXxx extension methods * improve targeting for ReflectionExtensions --- .../CodeJam.Blocks.Tests.csproj | 2 +- CodeJam.Main.Tests/CodeJam.Main.Tests.csproj | 2 +- .../ConnectionStringTests.cs | 224 +++++++++++++++ CodeJam.Main/CodeJam.Main.csproj | 7 + .../ConnectionStrings/ConnectionStringBase.cs | 258 ++++++++++++++++++ .../Reflection/ReflectionExtensions.cs | 13 +- .../Strings/StringExtensions.ToXxx.cs | 103 +++++-- .../StringExtensions.ToXxx.generated.cs | 22 +- .../Strings/StringExtensions.ToXxx.tt | 2 +- 9 files changed, 590 insertions(+), 43 deletions(-) create mode 100644 CodeJam.Main.Tests/ConnectionStrings/ConnectionStringTests.cs create mode 100644 CodeJam.Main/ConnectionStrings/ConnectionStringBase.cs diff --git a/CodeJam.Blocks.Tests/CodeJam.Blocks.Tests.csproj b/CodeJam.Blocks.Tests/CodeJam.Blocks.Tests.csproj index d604cf911..7ad4469dc 100644 --- a/CodeJam.Blocks.Tests/CodeJam.Blocks.Tests.csproj +++ b/CodeJam.Blocks.Tests/CodeJam.Blocks.Tests.csproj @@ -13,7 +13,7 @@ - + diff --git a/CodeJam.Main.Tests/CodeJam.Main.Tests.csproj b/CodeJam.Main.Tests/CodeJam.Main.Tests.csproj index 9bbf276e7..f59961aeb 100644 --- a/CodeJam.Main.Tests/CodeJam.Main.Tests.csproj +++ b/CodeJam.Main.Tests/CodeJam.Main.Tests.csproj @@ -14,7 +14,7 @@ - + diff --git a/CodeJam.Main.Tests/ConnectionStrings/ConnectionStringTests.cs b/CodeJam.Main.Tests/ConnectionStrings/ConnectionStringTests.cs new file mode 100644 index 000000000..a1c8d5345 --- /dev/null +++ b/CodeJam.Main.Tests/ConnectionStrings/ConnectionStringTests.cs @@ -0,0 +1,224 @@ +using System; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; + +using NUnit.Framework; + +using static NUnit.Framework.Assert; + +namespace CodeJam.ConnectionStrings +{ + [SuppressMessage("ReSharper", "ObjectCreationAsStatement")] + public class ConnectionStringTests + { + private static readonly DateTimeOffset _defaultDateTimeOffset = new DateTimeOffset(2010, 11, 12, 0, 0, 0, TimeSpan.Zero); + + public class BaseConnectionString : ConnectionStringBase + { + public BaseConnectionString(string connectionString) : base(connectionString) { } + +#if NET35_OR_GREATER || TARGETS_NETCOREAPP + [Required] +#endif + public string RequiredValue + { + get => TryGetValue(nameof(RequiredValue)); + set => SetValue(nameof(RequiredValue), value); + } + + public bool BooleanValue + { + get => TryGetBooleanValue(nameof(BooleanValue)); + set => SetValue(nameof(BooleanValue), value); + } + + public int? Int32Value + { + get => TryGetInt32Value(nameof(Int32Value)); + set => SetValue(nameof(Int32Value), value); + } + } + + public class DerivedConnectionString : BaseConnectionString + { + public DerivedConnectionString(string connectionString) : base(connectionString) { } + + public new string RequiredValue + { + get => TryGetValue(nameof(RequiredValue)); + set => SetValue(nameof(RequiredValue), value); + } + + public DateTimeOffset? DateTimeOffsetValue + { + get => TryGetDateTimeOffsetValue(nameof(DateTimeOffsetValue)); + set => SetValue(nameof(DateTimeOffsetValue), value); + } + } + + public class NonBrowsableConnectionString : BaseConnectionString + { + public NonBrowsableConnectionString(string connectionString) : base(connectionString) { } + + [Browsable(false)] + public new string RequiredValue + { + get => TryGetValue(nameof(RequiredValue)); + set => SetValue(nameof(RequiredValue), value); + } + } + + [Test] + public void TestConnectionStringValidation() + { + DoesNotThrow(() => new BaseConnectionString(null)); + DoesNotThrow(() => new BaseConnectionString("")); + DoesNotThrow(() => new BaseConnectionString("requiredValue=aaa")); + DoesNotThrow(() => new BaseConnectionString("RequiredValue=aaa;IgnoredValue=123")); + DoesNotThrow(() => new BaseConnectionString("") { ConnectionString = null }); + DoesNotThrow(() => new BaseConnectionString("") { ConnectionString = "" }); + DoesNotThrow(() => new BaseConnectionString("") { ConnectionString = "requiredValue=aaa" }); + DoesNotThrow(() => new BaseConnectionString("") { ConnectionString = "RequiredValue=aaa;IgnoredValue=123" }); + +#if NET35_OR_GREATER || TARGETS_NETCOREAPP + var ex = Throws(() => new BaseConnectionString("IgnoredValue=123")); + That(ex.Message, Does.Contain(nameof(BaseConnectionString.RequiredValue))); + ex = Throws( + () => new BaseConnectionString("") + { + ConnectionString = "IgnoredValue = 123" + }); + That(ex.Message, Does.Contain(nameof(BaseConnectionString.RequiredValue))); +#else + DoesNotThrow(() => new BaseConnectionString("IgnoredValue=123")); + DoesNotThrow( + () => new BaseConnectionString("") + { + ConnectionString = "IgnoredValue = 123" + });; +#endif + } + + [Test] + public void TestConnectionStringValidationOverride() + { + DoesNotThrow(() => new DerivedConnectionString("IgnoredValue=123")); + DoesNotThrow( + () => new DerivedConnectionString("") + { + ConnectionString = "IgnoredValue=123" + }); + } + + [Test] + public void TestGetProperties() + { + var x = new BaseConnectionString("requiredValue=aaa"); + AreEqual(x.RequiredValue, "aaa"); + AreEqual(x.BooleanValue, false); + AreEqual(x.Int32Value, null); + + IsTrue(x.ContainsKey(nameof(x.RequiredValue))); + IsTrue(x.TryGetValue(nameof(x.RequiredValue), out _)); + AreEqual(x[nameof(x.RequiredValue)], "aaa"); + + x = new BaseConnectionString("requiredValue='aa; a'"); + AreEqual(x.RequiredValue, "aa; a"); + + x = new BaseConnectionString("requiredValue=\"aa; a\""); + AreEqual(x.RequiredValue, "aa; a"); + + // test for input string format + x = new BaseConnectionString(@" + RequiredValue=""aaa"" ; +BooleanValue=true; + Int32Value = 112"); + AreEqual(x.RequiredValue, "aaa"); + AreEqual(x.BooleanValue, true); + AreEqual(x.Int32Value, 112); + } + + [Test] + public void TestGetPropertiesDerived() + { + var x = new DerivedConnectionString("IgnoredValue=aaa"); + AreEqual(x.RequiredValue, null); + AreEqual(x.BooleanValue, false); + AreEqual(x.Int32Value, null); + AreEqual(x.DateTimeOffsetValue, null); + + x = new DerivedConnectionString("DateTimeOffsetValue=2010-11-12Z"); + AreEqual(x.DateTimeOffsetValue, _defaultDateTimeOffset); + } + + [Test] + public void TestPropertiesRoundtrip() + { + var x = new DerivedConnectionString("") + { + RequiredValue = "A; B=C'\"", + BooleanValue = true, + Int32Value = -1024, + DateTimeOffsetValue = _defaultDateTimeOffset + }; + + var s = x.ToString(); + AreEqual(s, @"RequiredValue=""A; B=C'"""""";DateTimeOffsetValue=""11/12/2010 00:00:00 +00:00"";BooleanValue=True;Int32Value=-1024"); + + var x2 = new DerivedConnectionString(s); + AreEqual(x2.RequiredValue, x.RequiredValue); + AreEqual(x2.BooleanValue, x.BooleanValue); + AreEqual(x2.Int32Value, x.Int32Value); + AreEqual(x2.DateTimeOffsetValue, x.DateTimeOffsetValue); + AreEqual(s, x2.ToString()); + } + + [Test] + public void TestIgnoredProperties() + { + var x = new DerivedConnectionString(""); + IsFalse(x.ContainsKey("IgnoredValue")); + IsFalse(x.TryGetValue("IgnoredValue", out var ignored)); + IsNull(ignored); + var ex = Throws(() => x["IgnoredValue"].ToString()); + That(ex.Message, Does.Contain("IgnoredValue")); + + x = new DerivedConnectionString("IgnoredValue=123"); + IsFalse(x.ContainsKey("IgnoredValue")); + IsFalse(x.TryGetValue("IgnoredValue", out ignored)); + IsNull(ignored); + ex = Throws(() => x["IgnoredValue"].ToString()); + That(ex.Message, Does.Contain("IgnoredValue")); + } + + [Test] + public void TestNonBrowsableProperties() + { + var x = new NonBrowsableConnectionString("") + { + RequiredValue = "A", + BooleanValue = true, + Int32Value = -1024 + }; + + var s = x.GetBrowsableConnectionString(true); + AreEqual(s, @"RequiredValue=...;BooleanValue=True;Int32Value=-1024"); + + var x2 = new NonBrowsableConnectionString(s); + AreEqual(x2.RequiredValue, "..."); + AreEqual(x2.BooleanValue, x.BooleanValue); + AreEqual(x2.Int32Value, x.Int32Value); + AreEqual(s, x2.ToString()); + + s = x.GetBrowsableConnectionString(false); + AreEqual(s, @"BooleanValue=True;Int32Value=-1024"); + + x2 = new NonBrowsableConnectionString(s); + AreEqual(x2.RequiredValue, null); + AreEqual(x2.BooleanValue, x.BooleanValue); + AreEqual(x2.Int32Value, x.Int32Value); + AreEqual(s, x2.ToString()); + } + } +} \ No newline at end of file diff --git a/CodeJam.Main/CodeJam.Main.csproj b/CodeJam.Main/CodeJam.Main.csproj index d108a4348..2c8327fb2 100644 --- a/CodeJam.Main/CodeJam.Main.csproj +++ b/CodeJam.Main/CodeJam.Main.csproj @@ -68,6 +68,7 @@ + @@ -82,6 +83,8 @@ + + @@ -93,6 +96,8 @@ + + @@ -110,6 +115,8 @@ + + diff --git a/CodeJam.Main/ConnectionStrings/ConnectionStringBase.cs b/CodeJam.Main/ConnectionStrings/ConnectionStringBase.cs new file mode 100644 index 000000000..8b654e83d --- /dev/null +++ b/CodeJam.Main/ConnectionStrings/ConnectionStringBase.cs @@ -0,0 +1,258 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data.Common; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading; + +using CodeJam.Collections; +using CodeJam.Reflection; +using CodeJam.Strings; +using CodeJam.Targeting; + +using JetBrains.Annotations; + +namespace CodeJam.ConnectionStrings +{ + /// + /// Base class for connection strings + /// + [PublicAPI] + public abstract class ConnectionStringBase : DbConnectionStringBuilder + { + private const string _nonBrowsableValue = "..."; + + /// + /// Descriptor for connection string keyword + /// + protected class KeywordDescriptor + { + /// + /// Initializes a new instance of the class. + /// + public KeywordDescriptor(string name, Type valueType, bool isRequired, bool isBrowsable) + { + Name = name; + ValueType = valueType; + IsRequired = isRequired; + IsBrowsable = isBrowsable; + } + + /// + /// Gets the keyword name. + /// + public string Name { get; } + + /// + /// Gets expected type of the keyword value. + /// + public Type ValueType { get; } + + /// + /// Gets a value indicating whether this keyword is a mandatory keyword. + /// + public bool IsRequired { get; } + + /// + /// Gets a value indicating whether this keyword is browsable (safe to log / display). + /// + public bool IsBrowsable { get; } + } + + private static IReadOnlyDictionary GetDescriptorsCore(Type type) + { + KeywordDescriptor GetDescriptor(PropertyInfo property) => + new KeywordDescriptor( + property.Name, + property.PropertyType, +#if NET35_OR_GREATER || TARGETS_NETSTANDARD || TARGETS_NETCOREAPP + property.IsRequired(), +#else + false, +#endif + property.IsBrowsable()); + + // Explicit ordering from most derived to base. Explanation: + // The GetProperties method does not return properties in a particular order, such as alphabetical or declaration order. + // Your code must not depend on the order in which properties are returned, because that order varies. + var typeChain = Sequence.CreateWhileNotNull( + type, + t => t.GetBaseType() is var baseType && baseType != typeof(DbConnectionStringBuilder) + ? baseType + : null); + var properties = typeChain + .SelectMany(t => t.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly)); +#if NET45_OR_GREATER || TARGETS_NETSTANDARD || TARGETS_NETCOREAPP + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); +#else + var result = new DictionaryEx(StringComparer.OrdinalIgnoreCase); +#endif + foreach (var prop in properties) + { + // DONTTOUCH: most derived property wins + if (result.ContainsKey(prop.Name)) + continue; + + result[prop.Name] = GetDescriptor(prop); + } + return result; + } + + private static readonly Func> _keywordsCache = Algorithms.Memoize( + (Type type) => GetDescriptorsCore(type), + LazyThreadSafetyMode.ExecutionAndPublication); + + /// Initializes a new instance of the class. + /// The connection string. + protected ConnectionStringBase([CanBeNull] string connectionString) + { + if (connectionString != null) + ConnectionString = connectionString; + } + + /// + /// Gets all supported keywords for current connection. + /// + [NotNull] + protected IReadOnlyDictionary Keywords => _keywordsCache(GetType()); + + /// + /// Gets or sets the connection string associated with the . + /// + [NotNull] + public new string ConnectionString + { + get => base.ConnectionString; + set + { + base.ConnectionString = value; + if (value.NotNullNorEmpty()) + { + foreach (var nameRequiredPair in Keywords.Where(p => p.Value.IsRequired)) + { + if (!ContainsKey(nameRequiredPair.Key)) + throw CodeExceptions.Argument( + nameof(ConnectionString), + $"The value of required {nameRequiredPair.Key} connection string parameter is empty."); + } + } + } + } + + /// + /// Gets the browsable connection string. + /// + /// If set to true, non browsable values will be . + /// + [NotNull, MustUseReturnValue] + public string GetBrowsableConnectionString(bool includeNonBrowsable = false) + { + var builder = new StringBuilder(); + foreach (var browsablePair in Keywords) + { + if (!browsablePair.Value.IsBrowsable && !includeNonBrowsable) + continue; + + if (ShouldSerialize(browsablePair.Key) && TryGetValue(browsablePair.Key, out var value)) + { + if (!browsablePair.Value.IsBrowsable) + value = _nonBrowsableValue; + var keyValue = Convert.ToString(value, CultureInfo.InvariantCulture); + AppendKeyValuePair(builder, browsablePair.Key, keyValue); + } + } + + return builder.ToString(); + } + + #region Use only allowed keywords + /// + public override ICollection Keys => _keywordsCache(GetType()).Keys.ToArray(); + + /// + public override object this[string keyword] + { + get + { + return base[keyword]; + } + set + { + if (Keywords.ContainsKey(keyword)) + base[keyword] = value; + } + } + #endregion + + /// Gets the value for the keyword. + /// Name of keyword + /// Value for the keyword + [CanBeNull, MustUseReturnValue] + protected string TryGetValue(string keyword) => ContainsKey(keyword) ? (string)base[keyword] : null; + + /// Set value for the keyword. + /// Name of keyword + /// The value. + protected void SetValue(string keyword, object value) => + base[keyword] = value switch + { + DateTimeOffset x => x.ToInvariantString(), + Guid x => x.ToInvariantString(), +#if NET40_OR_GREATER || TARGETS_NETSTANDARD || TARGETS_NETCOREAPP + TimeSpan x => x.ToInvariantString(), +#else + TimeSpan x => x.ToString(), +#endif + Uri x => x.ToString(), + _ => value + }; + + /// Gets the value for the keyword. + /// Name of keyword + /// Value for the keyword + [MustUseReturnValue] + protected bool TryGetBooleanValue(string keyword) => ContainsKey(keyword) && Convert.ToBoolean(base[keyword]); + + /// Gets the value for the keyword. + /// Name of keyword + /// Value for the keyword + [CanBeNull, MustUseReturnValue] + protected int? TryGetInt32Value(string keyword) => ContainsKey(keyword) ? Convert.ToInt32(base[keyword]) : default(int?); + + /// Gets the value for the keyword. + /// Name of keyword + /// Value for the keyword + [CanBeNull, MustUseReturnValue] + protected long? TryGetInt64Value(string keyword) => ContainsKey(keyword) ? Convert.ToInt64(base[keyword]) : default(long?); + + + /// Gets the value for the keyword. + /// Name of keyword + /// Value for the keyword + [CanBeNull, MustUseReturnValue] + protected DateTimeOffset? TryGetDateTimeOffsetValue(string keyword) => TryGetValue(keyword).ToDateTimeOffsetInvariant(); + +#if NET40_OR_GREATER || TARGETS_NETSTANDARD || TARGETS_NETCOREAPP // PUBLIC_API_CHANGES + /// Gets the value for the keyword. + /// The value for the keyword. + /// Value for the keyword + [CanBeNull, MustUseReturnValue] + protected Guid? TryGetGuidValue(string keyword) => TryGetValue(keyword).ToGuid(); + + /// Gets the value for the keyword. + /// The value for the keyword. + /// Value for the keyword + [CanBeNull, MustUseReturnValue] + protected TimeSpan? TryGetTimeSpanValue(string keyword) => TryGetValue(keyword).ToTimeSpanInvariant(); + + /// Gets the value for the keyword. + /// The value for the keyword. + /// Value for the keyword. + [CanBeNull, MustUseReturnValue] + protected Uri TryGetUriValue(string keyword) => TryGetValue(keyword).ToUri(); +#endif + } +} \ No newline at end of file diff --git a/CodeJam.Main/Reflection/ReflectionExtensions.cs b/CodeJam.Main/Reflection/ReflectionExtensions.cs index 9d0dd3780..03741599e 100644 --- a/CodeJam.Main/Reflection/ReflectionExtensions.cs +++ b/CodeJam.Main/Reflection/ReflectionExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.ComponentModel.DataAnnotations; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; @@ -630,7 +631,16 @@ public static bool IsDefined([NotNull] this MemberInfo member) where [Pure] public static bool IsCompilerGenerated([NotNull] this MemberInfo member) => member.IsDefined(); -#if TARGETS_NET || NETSTANDARD20_OR_GREATER || NETCOREAPP20_OR_GREATER // PUBLIC_API_CHANGES +#if NET35_OR_GREATER || TARGETS_NETSTANDARD || TARGETS_NETCOREAPP // PUBLIC_API_CHANGES + /// + /// Checks if is is compiler generated member. + /// + /// Member to check. + /// True, if is generated by compiler. + [Pure] + public static bool IsRequired([NotNull] this MemberInfo member) => member.IsDefined(); +#endif + /// /// Checks if has no defined /// -or- @@ -652,6 +662,5 @@ public static bool IsBrowsable([NotNull] this Type type) => [Pure] public static bool IsBrowsable([NotNull] this MemberInfo member) => member.GetCustomAttribute()?.Browsable ?? true; -#endif } } \ No newline at end of file diff --git a/CodeJam.Main/Strings/StringExtensions.ToXxx.cs b/CodeJam.Main/Strings/StringExtensions.ToXxx.cs index f798c3b95..9e12d351c 100644 --- a/CodeJam.Main/Strings/StringExtensions.ToXxx.cs +++ b/CodeJam.Main/Strings/StringExtensions.ToXxx.cs @@ -137,27 +137,35 @@ public static int LastIndexOfInvariant( str.LastIndexOf(value, startIndex, count, StringComparison.InvariantCulture); #endif - #region DateTime /// - /// Converts the string representation of a number in a specified style and culture-specific format to its - /// equivalent. A return value indicates whether the conversion succeeded. + ///Tries to convert the specified string representation of a logical value to its equivalent. + /// + /// The string to convert. + /// A structure that contains the value that was parsed. + [Pure] + public static bool? ToBoolean([CanBeNull] this string str) => + bool.TryParse(str, out var result) ? (bool?)result : null; + + /// + /// Converts the string representation of a date in a specified style and culture-invariant format to its + /// equivalent. A return value indicates whether the conversion succeeded. /// /// - /// A string containing a number to convert. The string is interpreted using the style specified by + /// A string containing a date to convert. The string is interpreted using the style specified by /// . /// /// /// A bitwise combination of enumeration values that indicates the style elements that can be present in - /// . Default value is Integer. + /// . Default value is DateTimeStyles.None. /// /// /// An object that supplies culture-specific formatting information about . /// /// - /// When this method returns, contains the value equivalent of the number contained in + /// When this method returns, contains the value equivalent of the date contained in /// , if the conversion succeeded, or null if the conversion failed. The conversion fails if /// the parameter is null or String.Empty, is not in a format compliant with style, or - /// represents a number less than or greater than . + /// represents a date less than or greater than . /// [Pure] public static DateTime? ToDateTime( @@ -167,51 +175,49 @@ public static int LastIndexOfInvariant( DateTime.TryParse(str, provider, dateStyle, out var result) ? (DateTime?)result : null; /// - /// Converts the string representation of a number in a specified style and culture-invariant format to its + /// Converts the string representation of a date in a specified style and culture-invariant format to its /// equivalent. A return value indicates whether the conversion succeeded. /// /// - /// A string containing a number to convert. The string is interpreted using the style specified by + /// A string containing a date to convert. The string is interpreted using the style specified by /// . /// /// /// A bitwise combination of enumeration values that indicates the style elements that can be present in - /// . Default value is Integer. + /// . Default value is DateTimeStyles.None. /// /// - /// When this method returns, contains the value equivalent of the number contained in + /// When this method returns, contains the value equivalent of the date contained in /// , if the conversion succeeded, or null if the conversion failed. The conversion fails if /// the parameter is null or String.Empty, is not in a format compliant with style, or - /// represents a number less than or greater than . + /// represents a date less than or greater than . /// [Pure] public static DateTime? ToDateTimeInvariant( [CanBeNull] this string str, DateTimeStyles dateStyle = DateTimeStyles.None) => DateTime.TryParse(str, CultureInfo.InvariantCulture, dateStyle, out var result) ? (DateTime?)result : null; - #endregion - #region DateTimeOffset /// - /// Converts the string representation of a number in a specified style and culture-specific format to its - /// equivalent. A return value indicates whether the conversion succeeded. + /// Converts the string representation of a date in a specified style and culture-invariant format to its + /// equivalent. A return value indicates whether the conversion succeeded. /// /// - /// A string containing a number to convert. The string is interpreted using the style specified by + /// A string containing a date to convert. The string is interpreted using the style specified by /// . /// /// /// A bitwise combination of enumeration values that indicates the style elements that can be present in - /// . Default value is Integer. + /// . Default value is DateTimeStyles.None. /// /// /// An object that supplies culture-specific formatting information about . /// /// - /// When this method returns, contains the value equivalent of the number contained in + /// When this method returns, contains the value equivalent of the date contained in /// , if the conversion succeeded, or null if the conversion failed. The conversion fails if /// the parameter is null or String.Empty, is not in a format compliant with style, or - /// represents a number less than or greater than . + /// represents a date less than or greater than . /// [Pure] public static DateTimeOffset? ToDateTimeOffset( @@ -221,28 +227,71 @@ public static int LastIndexOfInvariant( DateTimeOffset.TryParse(str, provider, dateStyle, out var result) ? (DateTimeOffset?)result : null; /// - /// Converts the string representation of a number in a specified style and culture-invariant format to its + /// Converts the string representation of a date in a specified style and culture-invariant format to its /// equivalent. A return value indicates whether the conversion succeeded. /// /// - /// A string containing a number to convert. The string is interpreted using the style specified by + /// A string containing a date to convert. The string is interpreted using the style specified by /// . /// /// /// A bitwise combination of enumeration values that indicates the style elements that can be present in - /// . Default value is Integer. + /// . Default value is DateTimeStyles.None. /// /// - /// When this method returns, contains the value equivalent of the number contained in + /// When this method returns, contains the value equivalent of the date contained in /// , if the conversion succeeded, or null if the conversion failed. The conversion fails if /// the parameter is null or String.Empty, is not in a format compliant with style, or - /// represents a number less than or greater than . + /// represents a date less than or greater than . /// [Pure] public static DateTimeOffset? ToDateTimeOffsetInvariant( [CanBeNull] this string str, DateTimeStyles dateStyle = DateTimeStyles.None) => - DateTimeOffset.TryParse(str, CultureInfo.InvariantCulture, dateStyle, out var result) ? (DateTimeOffset?)result : null; - #endregion + DateTimeOffset.TryParse(str, CultureInfo.InvariantCulture, dateStyle, out var result) + ? (DateTimeOffset?)result + : null; + + /// + /// Creates a new using the specified instance + /// and a . + /// + /// The representing the . + /// The type of the Uri. DefaultValue is . + /// Constructed . + [Pure, CanBeNull] + public static Uri ToUri([CanBeNull] this string str, UriKind uriKind = UriKind.RelativeOrAbsolute) => + Uri.TryCreate(str, uriKind, out var result) ? result : null; + +#if NET40_OR_GREATER || TARGETS_NETSTANDARD || TARGETS_NETCOREAPP // PUBLIC_API_CHANGES + /// + /// Converts the string representation of a GUID to the equivalent structure. + /// + /// The string to convert. + /// A structure that contains the value that was parsed. + [Pure] + public static Guid? ToGuid([CanBeNull] this string str) => + Guid.TryParse(str, out var result) ? (Guid?)result : null; + + /// + /// Converts the string representation of a time interval to its equivalent + /// by using the specified culture-specific format information. + /// A string that specifies the time interval to convert. + /// An object that supplies culture-specific formatting information. + /// A time interval that corresponds to , as specified by . + [Pure] + public static TimeSpan? ToTimeSpan([CanBeNull] this string str, IFormatProvider formatProvider) => + TimeSpan.TryParse(str, formatProvider, out var result) ? (TimeSpan?)result : null; + + /// + /// Converts the string representation of a time interval to its equivalent + /// by using invariant culture format information. + /// A string that specifies the time interval to convert. + /// A time interval that corresponds to . + [Pure] + public static TimeSpan? ToTimeSpanInvariant([CanBeNull] this string str) => + TimeSpan.TryParse(str, CultureInfo.InvariantCulture, out var result) ? (TimeSpan?)result : null; +#endif + } } \ No newline at end of file diff --git a/CodeJam.Main/Strings/StringExtensions.ToXxx.generated.cs b/CodeJam.Main/Strings/StringExtensions.ToXxx.generated.cs index 87351f463..7c63238f1 100644 --- a/CodeJam.Main/Strings/StringExtensions.ToXxx.generated.cs +++ b/CodeJam.Main/Strings/StringExtensions.ToXxx.generated.cs @@ -71,7 +71,7 @@ partial class StringExtensions NumberStyles numberStyle = NumberStyles.Integer) { byte result; - return byte.TryParse(str, numberStyle, CultureInfo.InvariantCulture, out result) ? (byte?)result : null; + return byte.TryParse(str, numberStyle, CultureInfo.InvariantCulture, out result) ? (byte?)result : null; } #endregion @@ -131,7 +131,7 @@ partial class StringExtensions NumberStyles numberStyle = NumberStyles.Integer) { sbyte result; - return sbyte.TryParse(str, numberStyle, CultureInfo.InvariantCulture, out result) ? (sbyte?)result : null; + return sbyte.TryParse(str, numberStyle, CultureInfo.InvariantCulture, out result) ? (sbyte?)result : null; } #endregion @@ -191,7 +191,7 @@ partial class StringExtensions NumberStyles numberStyle = NumberStyles.Number) { short result; - return short.TryParse(str, numberStyle, CultureInfo.InvariantCulture, out result) ? (short?)result : null; + return short.TryParse(str, numberStyle, CultureInfo.InvariantCulture, out result) ? (short?)result : null; } #endregion @@ -251,7 +251,7 @@ partial class StringExtensions NumberStyles numberStyle = NumberStyles.Integer) { ushort result; - return ushort.TryParse(str, numberStyle, CultureInfo.InvariantCulture, out result) ? (ushort?)result : null; + return ushort.TryParse(str, numberStyle, CultureInfo.InvariantCulture, out result) ? (ushort?)result : null; } #endregion @@ -311,7 +311,7 @@ partial class StringExtensions NumberStyles numberStyle = NumberStyles.Integer) { int result; - return int.TryParse(str, numberStyle, CultureInfo.InvariantCulture, out result) ? (int?)result : null; + return int.TryParse(str, numberStyle, CultureInfo.InvariantCulture, out result) ? (int?)result : null; } #endregion @@ -371,7 +371,7 @@ partial class StringExtensions NumberStyles numberStyle = NumberStyles.Integer) { uint result; - return uint.TryParse(str, numberStyle, CultureInfo.InvariantCulture, out result) ? (uint?)result : null; + return uint.TryParse(str, numberStyle, CultureInfo.InvariantCulture, out result) ? (uint?)result : null; } #endregion @@ -431,7 +431,7 @@ partial class StringExtensions NumberStyles numberStyle = NumberStyles.Integer) { long result; - return long.TryParse(str, numberStyle, CultureInfo.InvariantCulture, out result) ? (long?)result : null; + return long.TryParse(str, numberStyle, CultureInfo.InvariantCulture, out result) ? (long?)result : null; } #endregion @@ -491,7 +491,7 @@ partial class StringExtensions NumberStyles numberStyle = NumberStyles.Integer) { ulong result; - return ulong.TryParse(str, numberStyle, CultureInfo.InvariantCulture, out result) ? (ulong?)result : null; + return ulong.TryParse(str, numberStyle, CultureInfo.InvariantCulture, out result) ? (ulong?)result : null; } #endregion @@ -551,7 +551,7 @@ partial class StringExtensions NumberStyles numberStyle = NumberStyles.Float) { float result; - return float.TryParse(str, numberStyle, CultureInfo.InvariantCulture, out result) ? (float?)result : null; + return float.TryParse(str, numberStyle, CultureInfo.InvariantCulture, out result) ? (float?)result : null; } #endregion @@ -611,7 +611,7 @@ partial class StringExtensions NumberStyles numberStyle = NumberStyles.Float) { double result; - return double.TryParse(str, numberStyle, CultureInfo.InvariantCulture, out result) ? (double?)result : null; + return double.TryParse(str, numberStyle, CultureInfo.InvariantCulture, out result) ? (double?)result : null; } #endregion @@ -671,7 +671,7 @@ partial class StringExtensions NumberStyles numberStyle = NumberStyles.Number) { decimal result; - return decimal.TryParse(str, numberStyle, CultureInfo.InvariantCulture, out result) ? (decimal?)result : null; + return decimal.TryParse(str, numberStyle, CultureInfo.InvariantCulture, out result) ? (decimal?)result : null; } #endregion } diff --git a/CodeJam.Main/Strings/StringExtensions.ToXxx.tt b/CodeJam.Main/Strings/StringExtensions.ToXxx.tt index da55c99a8..365890f65 100644 --- a/CodeJam.Main/Strings/StringExtensions.ToXxx.tt +++ b/CodeJam.Main/Strings/StringExtensions.ToXxx.tt @@ -84,7 +84,7 @@ namespace CodeJam.Strings NumberStyles numberStyle = NumberStyles.<#=type.Style#>) { <#=type.Lang#> result; - return <#=type.Lang#>.TryParse(str, numberStyle, CultureInfo.InvariantCulture, out result) ? (<#=type.Lang#>?)result : null; + return <#=type.Lang#>.TryParse(str, numberStyle, CultureInfo.InvariantCulture, out result) ? (<#=type.Lang#>?)result : null; } <# } From 18f58b542604fe10e083178b44dc4c9e71b51b6d Mon Sep 17 00:00:00 2001 From: ig-sinicyn Date: Sun, 29 Dec 2019 10:31:40 +0300 Subject: [PATCH 2/2] + ConnectionStringBase.ConnectionWrapper --- .../ConnectionStringTests.cs | 36 ++- .../ConnectionStringBase.ConnectionWrapper.cs | 166 +++++++++++ .../ConnectionStrings/ConnectionStringBase.cs | 272 +++++++++--------- 3 files changed, 330 insertions(+), 144 deletions(-) create mode 100644 CodeJam.Main/ConnectionStrings/ConnectionStringBase.ConnectionWrapper.cs diff --git a/CodeJam.Main.Tests/ConnectionStrings/ConnectionStringTests.cs b/CodeJam.Main.Tests/ConnectionStrings/ConnectionStringTests.cs index a1c8d5345..05b11e894 100644 --- a/CodeJam.Main.Tests/ConnectionStrings/ConnectionStringTests.cs +++ b/CodeJam.Main.Tests/ConnectionStrings/ConnectionStringTests.cs @@ -2,6 +2,9 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; +using System.Linq; + +using JetBrains.Annotations; using NUnit.Framework; @@ -12,7 +15,9 @@ namespace CodeJam.ConnectionStrings [SuppressMessage("ReSharper", "ObjectCreationAsStatement")] public class ConnectionStringTests { - private static readonly DateTimeOffset _defaultDateTimeOffset = new DateTimeOffset(2010, 11, 12, 0, 0, 0, TimeSpan.Zero); + private static readonly DateTimeOffset _defaultDateTimeOffset = new DateTimeOffset( + new DateTime(2010, 11, 12), + TimeSpan.Zero); public class BaseConnectionString : ConnectionStringBase { @@ -50,6 +55,14 @@ public DerivedConnectionString(string connectionString) : base(connectionString) set => SetValue(nameof(RequiredValue), value); } + // Never filled. Used to test roundtrip scenario + [UsedImplicitly] + public string OptionalValue + { + get => TryGetValue(nameof(OptionalValue)); + set => SetValue(nameof(OptionalValue), value); + } + public DateTimeOffset? DateTimeOffsetValue { get => TryGetDateTimeOffsetValue(nameof(DateTimeOffsetValue)); @@ -144,6 +157,7 @@ public void TestGetPropertiesDerived() { var x = new DerivedConnectionString("IgnoredValue=aaa"); AreEqual(x.RequiredValue, null); + AreEqual(x.OptionalValue, null); AreEqual(x.BooleanValue, false); AreEqual(x.Int32Value, null); AreEqual(x.DateTimeOffsetValue, null); @@ -164,14 +178,19 @@ public void TestPropertiesRoundtrip() }; var s = x.ToString(); - AreEqual(s, @"RequiredValue=""A; B=C'"""""";DateTimeOffsetValue=""11/12/2010 00:00:00 +00:00"";BooleanValue=True;Int32Value=-1024"); + AreEqual( + s, + @"RequiredValue=""A; B=C'"""""";BooleanValue=True;Int32Value=-1024;DateTimeOffsetValue=""11/12/2010 00:00:00 +00:00"""); var x2 = new DerivedConnectionString(s); AreEqual(x2.RequiredValue, x.RequiredValue); AreEqual(x2.BooleanValue, x.BooleanValue); AreEqual(x2.Int32Value, x.Int32Value); AreEqual(x2.DateTimeOffsetValue, x.DateTimeOffsetValue); + AreEqual(s, x2.ToString()); + IsTrue(x.EquivalentTo(x2)); + AreEqual(x.OrderBy(p => p.Key), x2.OrderBy(p => p.Key)); } [Test] @@ -192,6 +211,15 @@ public void TestIgnoredProperties() That(ex.Message, Does.Contain("IgnoredValue")); } + [Test] + public void TestInvalidProperties() + { + var x = new DerivedConnectionString(@"BooleanValue=aaa;Int32Value=bbb;DateTimeOffsetValue=ccc"); + Throws(() => x.BooleanValue.ToString()); + Throws(() => x.Int32Value?.ToString()); + Throws(() => x.DateTimeOffsetValue?.ToString()); + } + [Test] public void TestNonBrowsableProperties() { @@ -210,6 +238,8 @@ public void TestNonBrowsableProperties() AreEqual(x2.BooleanValue, x.BooleanValue); AreEqual(x2.Int32Value, x.Int32Value); AreEqual(s, x2.ToString()); + IsFalse(x.EquivalentTo(x2)); + AreNotEqual(x.ToArray(), x2.ToArray()); s = x.GetBrowsableConnectionString(false); AreEqual(s, @"BooleanValue=True;Int32Value=-1024"); @@ -219,6 +249,8 @@ public void TestNonBrowsableProperties() AreEqual(x2.BooleanValue, x.BooleanValue); AreEqual(x2.Int32Value, x.Int32Value); AreEqual(s, x2.ToString()); + IsFalse(x.EquivalentTo(x2)); + AreNotEqual(x.ToArray(), x2.ToArray()); } } } \ No newline at end of file diff --git a/CodeJam.Main/ConnectionStrings/ConnectionStringBase.ConnectionWrapper.cs b/CodeJam.Main/ConnectionStrings/ConnectionStringBase.ConnectionWrapper.cs new file mode 100644 index 000000000..5729df3d8 --- /dev/null +++ b/CodeJam.Main/ConnectionStrings/ConnectionStringBase.ConnectionWrapper.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading; + +using CodeJam.Collections; +using CodeJam.Reflection; +using CodeJam.Strings; +using CodeJam.Targeting; + +using JetBrains.Annotations; + +namespace CodeJam.ConnectionStrings +{ + partial class ConnectionStringBase + { + private class StringBuilderWrapper : DbConnectionStringBuilder + { + private const string _nonBrowsableValue = "..."; + + private static IReadOnlyDictionary GetDescriptorsCore(Type type) + { + KeywordDescriptor GetDescriptor(PropertyInfo property) => + new KeywordDescriptor( + property.Name, + property.PropertyType, +#if NET35_OR_GREATER || TARGETS_NETSTANDARD || TARGETS_NETCOREAPP + property.IsRequired(), +#else + false, +#endif + property.IsBrowsable()); + + // Explicit ordering from most derived to base. Explanation: + // The GetProperties method does not return properties in a particular order, such as alphabetical or declaration order. + // Your code must not depend on the order in which properties are returned, because that order varies. + var typeChain = Sequence.CreateWhileNotNull( + type, + t => t.GetBaseType() is var baseType && baseType != typeof(ConnectionStringBase) + ? baseType + : null); + var properties = typeChain + .SelectMany(t => t.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly)); +#if NET45_OR_GREATER || TARGETS_NETSTANDARD || TARGETS_NETCOREAPP + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); +#else + var result = new DictionaryEx(StringComparer.OrdinalIgnoreCase); +#endif + foreach (var prop in properties) + { + // DONTTOUCH: most derived property wins + if (result.ContainsKey(prop.Name)) + continue; + + result[prop.Name] = GetDescriptor(prop); + } + return result; + } + + private static readonly Func> _keywordsCache = Algorithms + .Memoize( + (Type type) => GetDescriptorsCore(type), + LazyThreadSafetyMode.ExecutionAndPublication); + + private readonly Type _descriptorType; + + public StringBuilderWrapper(string connectionString, Type descriptorType) + { + _descriptorType = descriptorType; + if (connectionString != null) + ConnectionString = connectionString; + } + + [NotNull] + public IReadOnlyDictionary Keywords => _keywordsCache(_descriptorType); + + [NotNull] + public new string ConnectionString + { + get => base.ConnectionString; + set + { + base.ConnectionString = value; + if (value.NotNullNorEmpty()) + { + foreach (var nameRequiredPair in Keywords.Where(p => p.Value.IsRequired)) + { + if (!ContainsKey(nameRequiredPair.Key)) + throw CodeExceptions.Argument( + nameof(ConnectionString), + $"The value of required {nameRequiredPair.Key} connection string parameter is empty."); + } + } + } + } + + /// + [NotNull, MustUseReturnValue] + public string GetBrowsableConnectionString(bool includeNonBrowsable = false) + { + var builder = new StringBuilder(); + foreach (var browsablePair in Keywords) + { + if (!browsablePair.Value.IsBrowsable && !includeNonBrowsable) + continue; + + if (ShouldSerialize(browsablePair.Key) && TryGetValue(browsablePair.Key, out var value)) + { + if (!browsablePair.Value.IsBrowsable) + value = _nonBrowsableValue; + var keyValue = Convert.ToString(value, CultureInfo.InvariantCulture); + AppendKeyValuePair(builder, browsablePair.Key, keyValue); + } + } + + return builder.ToString(); + } + + public string GetStringValue(string keyword) => (string)this[keyword]; + + public bool TryGetStringValue(string keyword, out string value) + { + value = GetStringValue(keyword); + return value != null; + } + + #region Use only allowed keywords + /// + public override object this[string keyword] + { + get + { + if (Keywords.ContainsKey(keyword)) + { + TryGetValue(keyword, out var value); + return value; + } + return base[keyword]; // exception for not supported keyword. + } + set + { + if (Keywords.TryGetValue(keyword, out var descriptor)) + { + base[descriptor.Name] = value switch + { + DateTimeOffset x => x.ToInvariantString(), + Guid x => x.ToInvariantString(), +#if NET40_OR_GREATER || TARGETS_NETSTANDARD || TARGETS_NETCOREAPP + TimeSpan x => x.ToInvariantString(), +#else + TimeSpan x => x.ToString(), +#endif + Uri x => x.ToString(), + _ => value + }; + } + } + } + #endregion + } + } +} \ No newline at end of file diff --git a/CodeJam.Main/ConnectionStrings/ConnectionStringBase.cs b/CodeJam.Main/ConnectionStrings/ConnectionStringBase.cs index 8b654e83d..5bb420a1b 100644 --- a/CodeJam.Main/ConnectionStrings/ConnectionStringBase.cs +++ b/CodeJam.Main/ConnectionStrings/ConnectionStringBase.cs @@ -1,17 +1,8 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Data.Common; using System.Globalization; using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading; - -using CodeJam.Collections; -using CodeJam.Reflection; -using CodeJam.Strings; -using CodeJam.Targeting; using JetBrains.Annotations; @@ -21,10 +12,8 @@ namespace CodeJam.ConnectionStrings /// Base class for connection strings /// [PublicAPI] - public abstract class ConnectionStringBase : DbConnectionStringBuilder + public abstract partial class ConnectionStringBase : IDictionary { - private const string _nonBrowsableValue = "..."; - /// /// Descriptor for connection string keyword /// @@ -62,197 +51,196 @@ public KeywordDescriptor(string name, Type valueType, bool isRequired, bool isBr public bool IsBrowsable { get; } } - private static IReadOnlyDictionary GetDescriptorsCore(Type type) - { - KeywordDescriptor GetDescriptor(PropertyInfo property) => - new KeywordDescriptor( - property.Name, - property.PropertyType, -#if NET35_OR_GREATER || TARGETS_NETSTANDARD || TARGETS_NETCOREAPP - property.IsRequired(), -#else - false, -#endif - property.IsBrowsable()); - - // Explicit ordering from most derived to base. Explanation: - // The GetProperties method does not return properties in a particular order, such as alphabetical or declaration order. - // Your code must not depend on the order in which properties are returned, because that order varies. - var typeChain = Sequence.CreateWhileNotNull( - type, - t => t.GetBaseType() is var baseType && baseType != typeof(DbConnectionStringBuilder) - ? baseType - : null); - var properties = typeChain - .SelectMany(t => t.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly)); -#if NET45_OR_GREATER || TARGETS_NETSTANDARD || TARGETS_NETCOREAPP - var result = new Dictionary(StringComparer.OrdinalIgnoreCase); -#else - var result = new DictionaryEx(StringComparer.OrdinalIgnoreCase); -#endif - foreach (var prop in properties) - { - // DONTTOUCH: most derived property wins - if (result.ContainsKey(prop.Name)) - continue; - - result[prop.Name] = GetDescriptor(prop); - } - return result; - } - - private static readonly Func> _keywordsCache = Algorithms.Memoize( - (Type type) => GetDescriptorsCore(type), - LazyThreadSafetyMode.ExecutionAndPublication); + private readonly StringBuilderWrapper _wrapper; /// Initializes a new instance of the class. /// The connection string. protected ConnectionStringBase([CanBeNull] string connectionString) { - if (connectionString != null) - ConnectionString = connectionString; + _wrapper = new StringBuilderWrapper(connectionString, GetType()); } /// /// Gets all supported keywords for current connection. /// [NotNull] - protected IReadOnlyDictionary Keywords => _keywordsCache(GetType()); + protected IReadOnlyDictionary Keywords => _wrapper.Keywords; /// /// Gets or sets the connection string associated with the . /// [NotNull] - public new string ConnectionString + public string ConnectionString { - get => base.ConnectionString; - set - { - base.ConnectionString = value; - if (value.NotNullNorEmpty()) - { - foreach (var nameRequiredPair in Keywords.Where(p => p.Value.IsRequired)) - { - if (!ContainsKey(nameRequiredPair.Key)) - throw CodeExceptions.Argument( - nameof(ConnectionString), - $"The value of required {nameRequiredPair.Key} connection string parameter is empty."); - } - } - } + get => _wrapper.ConnectionString; + set => _wrapper.ConnectionString = value; } - /// - /// Gets the browsable connection string. - /// - /// If set to true, non browsable values will be . - /// - [NotNull, MustUseReturnValue] - public string GetBrowsableConnectionString(bool includeNonBrowsable = false) - { - var builder = new StringBuilder(); - foreach (var browsablePair in Keywords) - { - if (!browsablePair.Value.IsBrowsable && !includeNonBrowsable) - continue; - - if (ShouldSerialize(browsablePair.Key) && TryGetValue(browsablePair.Key, out var value)) - { - if (!browsablePair.Value.IsBrowsable) - value = _nonBrowsableValue; - var keyValue = Convert.ToString(value, CultureInfo.InvariantCulture); - AppendKeyValuePair(builder, browsablePair.Key, keyValue); - } - } - - return builder.ToString(); - } - - #region Use only allowed keywords - /// - public override ICollection Keys => _keywordsCache(GetType()).Keys.ToArray(); - - /// - public override object this[string keyword] - { - get - { - return base[keyword]; - } - set - { - if (Keywords.ContainsKey(keyword)) - base[keyword] = value; - } - } - #endregion - /// Gets the value for the keyword. /// Name of keyword /// Value for the keyword [CanBeNull, MustUseReturnValue] - protected string TryGetValue(string keyword) => ContainsKey(keyword) ? (string)base[keyword] : null; + protected string TryGetValue(string keyword) => _wrapper.GetStringValue(keyword); /// Set value for the keyword. /// Name of keyword /// The value. - protected void SetValue(string keyword, object value) => - base[keyword] = value switch - { - DateTimeOffset x => x.ToInvariantString(), - Guid x => x.ToInvariantString(), -#if NET40_OR_GREATER || TARGETS_NETSTANDARD || TARGETS_NETCOREAPP - TimeSpan x => x.ToInvariantString(), -#else - TimeSpan x => x.ToString(), -#endif - Uri x => x.ToString(), - _ => value - }; + protected void SetValue(string keyword, object value) => _wrapper[keyword] = value; /// Gets the value for the keyword. /// Name of keyword /// Value for the keyword [MustUseReturnValue] - protected bool TryGetBooleanValue(string keyword) => ContainsKey(keyword) && Convert.ToBoolean(base[keyword]); + protected bool TryGetBooleanValue(string keyword) => + _wrapper.TryGetValue(keyword, out var item) && Convert.ToBoolean(item); /// Gets the value for the keyword. /// Name of keyword /// Value for the keyword [CanBeNull, MustUseReturnValue] - protected int? TryGetInt32Value(string keyword) => ContainsKey(keyword) ? Convert.ToInt32(base[keyword]) : default(int?); + protected int? TryGetInt32Value(string keyword) => + _wrapper.TryGetValue(keyword, out var item) ? Convert.ToInt32(item) : default(int?); /// Gets the value for the keyword. /// Name of keyword /// Value for the keyword [CanBeNull, MustUseReturnValue] - protected long? TryGetInt64Value(string keyword) => ContainsKey(keyword) ? Convert.ToInt64(base[keyword]) : default(long?); - + protected long? TryGetInt64Value(string keyword) => + _wrapper.TryGetValue(keyword, out var item) ? Convert.ToInt64(item) : default(long?); /// Gets the value for the keyword. /// Name of keyword /// Value for the keyword [CanBeNull, MustUseReturnValue] - protected DateTimeOffset? TryGetDateTimeOffsetValue(string keyword) => TryGetValue(keyword).ToDateTimeOffsetInvariant(); + protected DateTimeOffset? TryGetDateTimeOffsetValue(string keyword) => + _wrapper.TryGetStringValue(keyword, out var item) + ? DateTimeOffset.Parse(item, CultureInfo.InvariantCulture, DateTimeStyles.None) + : default(DateTimeOffset?); -#if NET40_OR_GREATER || TARGETS_NETSTANDARD || TARGETS_NETCOREAPP // PUBLIC_API_CHANGES /// Gets the value for the keyword. /// The value for the keyword. /// Value for the keyword [CanBeNull, MustUseReturnValue] - protected Guid? TryGetGuidValue(string keyword) => TryGetValue(keyword).ToGuid(); + protected Guid? TryGetGuidValue(string keyword) => + _wrapper.TryGetStringValue(keyword, out var item) ? new Guid(item) : default(Guid?); /// Gets the value for the keyword. /// The value for the keyword. /// Value for the keyword [CanBeNull, MustUseReturnValue] - protected TimeSpan? TryGetTimeSpanValue(string keyword) => TryGetValue(keyword).ToTimeSpanInvariant(); + protected TimeSpan? TryGetTimeSpanValue(string keyword) => + _wrapper.TryGetStringValue(keyword, out var item) ? TimeSpan.Parse(item) : default(TimeSpan?); /// Gets the value for the keyword. /// The value for the keyword. /// Value for the keyword. [CanBeNull, MustUseReturnValue] - protected Uri TryGetUriValue(string keyword) => TryGetValue(keyword).ToUri(); -#endif + protected Uri TryGetUriValue(string keyword) => + _wrapper.TryGetStringValue(keyword, out var item) ? new Uri(item) : null; + + /// + /// Compares the connection information in this object with the connection information in the supplied object.. + /// + /// The other connection string. + /// true if the connection information in both objects causes an equivalent connection string; otherwise false. + public bool EquivalentTo(ConnectionStringBase other) => _wrapper.EquivalentTo(other._wrapper); + + /// + /// Gets the browsable connection string. + /// + /// If set to true, non browsable values will be . + /// Browsable connection string + [NotNull, MustUseReturnValue] + public string GetBrowsableConnectionString(bool includeNonBrowsable = false) => + _wrapper.GetBrowsableConnectionString(includeNonBrowsable); + + #region Implementation of IEnumerable + /// + public IEnumerator> GetEnumerator() => _wrapper + .Cast>() + .GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + #endregion + + #region Implementation of ICollection> + /// + void ICollection>.Add(KeyValuePair item) => + _wrapper.Add(item.Key, item.Value); + + /// + void ICollection>.Clear() => _wrapper.Clear(); + + /// + bool ICollection>.Contains(KeyValuePair item) => + _wrapper.TryGetValue(item.Key, out var value) && Equals(item.Value, value); + + /// + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + Code.NotNull(array, nameof(array)); + Code.ValidIndex(arrayIndex, nameof(arrayIndex), array.Length); + Code.ValidIndexAndCount( + arrayIndex, + nameof(arrayIndex), + Count, + nameof(Count), + array.Length); + + var index = arrayIndex; + foreach (var pair in this) + { + array[index++] = pair; + } + } + + /// + bool ICollection>.Remove(KeyValuePair item) + { + if (_wrapper.TryGetValue(item.Key, out var value) && Equals(item.Value, value)) + return _wrapper.Remove(item.Key); + + return false; + } + + /// + public int Count => _wrapper.Count; + + /// + bool ICollection>.IsReadOnly => false; + #endregion + + #region Implementation of IDictionary + /// + public bool ContainsKey(string key) => _wrapper.ContainsKey(key); + + /// + public void Add(string key, object value) => _wrapper.Add(key, value); + + /// + public bool Remove(string key) => _wrapper.Remove(key); + + /// + public bool TryGetValue(string key, out object value) => _wrapper.TryGetValue(key, out value); + + /// + public object this[string key] + { + get => _wrapper[key]; + set => _wrapper[key] = value; + } + + /// + ICollection IDictionary.Keys => (ICollection)_wrapper.Keys; + + /// + ICollection IDictionary.Values => (ICollection)_wrapper.Values; + #endregion + + #region Overrides of Object + /// + public override string ToString() => _wrapper.ToString(); + #endregion } } \ No newline at end of file