diff --git a/eng/Versions.props b/eng/Versions.props index 4a749666efcc6e..1e6381dfafe1a1 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -8,6 +8,7 @@ 0 preview 2 + $(PreReleaseVersionIteration)-runtime $(MajorVersion).$(MinorVersion).0.0 diff --git a/src/libraries/Directory.Build.props b/src/libraries/Directory.Build.props index 7464cbe3360da4..9817cb63337041 100644 --- a/src/libraries/Directory.Build.props +++ b/src/libraries/Directory.Build.props @@ -323,6 +323,9 @@ $(ArtifactsBinDir)pkg\$(NetCoreAppCurrent)\ref $(ArtifactsBinDir)pkg\$(NetCoreAppCurrent)\lib + + $(ArtifactsBinDir)pkg\aspnetcoreapp\ref + $(ArtifactsBinDir)pkg\aspnetcoreapp\lib $([MSBuild]::NormalizeDirectory('$(ArtifactsBinDir)', 'testhost', '$(BuildConfiguration)')) diff --git a/src/libraries/Directory.Build.targets b/src/libraries/Directory.Build.targets index a977d74811354d..d856ddee960978 100644 --- a/src/libraries/Directory.Build.targets +++ b/src/libraries/Directory.Build.targets @@ -66,6 +66,10 @@ ILLinkTrimAssembly=true + + $(ASPNETCoreAppPackageRuntimePath) + $(ASPNETCoreAppPackageRefPath) + diff --git a/src/libraries/Microsoft.Extensions.Primitives/Directory.Build.props b/src/libraries/Microsoft.Extensions.Primitives/Directory.Build.props new file mode 100644 index 00000000000000..e6b08fc74ca605 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Primitives/Directory.Build.props @@ -0,0 +1,7 @@ + + + + MicrosoftAspNetCore + true + + \ No newline at end of file diff --git a/src/libraries/Microsoft.Extensions.Primitives/Microsoft.Extensions.Primitives.sln b/src/libraries/Microsoft.Extensions.Primitives/Microsoft.Extensions.Primitives.sln new file mode 100644 index 00000000000000..7fab925f1a9f88 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Primitives/Microsoft.Extensions.Primitives.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29521.150 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D69C4529-128C-4A51-AD5A-659872A4F405}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Primitives", "src\Microsoft.Extensions.Primitives.csproj", "{31785605-3555-4D71-8B24-59DC43F7439A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0EB2C914-0873-4AF8-9262-75B1405C842A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "ref", "{33BE7927-9C98-425D-97F5-D68510163F6C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Primitives", "ref\Microsoft.Extensions.Primitives.csproj", "{7A3952AC-BA1B-4A00-B45B-92DE4D3B36E5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Primitives.Tests", "tests\Microsoft.Extensions.Primitives.Tests.csproj", "{E5CBC6DD-9C09-483A-A27A-F1F1FA2DFBAA}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {31785605-3555-4D71-8B24-59DC43F7439A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {31785605-3555-4D71-8B24-59DC43F7439A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {31785605-3555-4D71-8B24-59DC43F7439A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {31785605-3555-4D71-8B24-59DC43F7439A}.Release|Any CPU.Build.0 = Release|Any CPU + {7A3952AC-BA1B-4A00-B45B-92DE4D3B36E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7A3952AC-BA1B-4A00-B45B-92DE4D3B36E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7A3952AC-BA1B-4A00-B45B-92DE4D3B36E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7A3952AC-BA1B-4A00-B45B-92DE4D3B36E5}.Release|Any CPU.Build.0 = Release|Any CPU + {E5CBC6DD-9C09-483A-A27A-F1F1FA2DFBAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E5CBC6DD-9C09-483A-A27A-F1F1FA2DFBAA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E5CBC6DD-9C09-483A-A27A-F1F1FA2DFBAA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E5CBC6DD-9C09-483A-A27A-F1F1FA2DFBAA}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {31785605-3555-4D71-8B24-59DC43F7439A} = {D69C4529-128C-4A51-AD5A-659872A4F405} + {7A3952AC-BA1B-4A00-B45B-92DE4D3B36E5} = {33BE7927-9C98-425D-97F5-D68510163F6C} + {E5CBC6DD-9C09-483A-A27A-F1F1FA2DFBAA} = {0EB2C914-0873-4AF8-9262-75B1405C842A} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {EC9E15A7-A8F7-486B-B7EA-59341203A4D3} + EndGlobalSection +EndGlobal diff --git a/src/libraries/Microsoft.Extensions.Primitives/pkg/Microsoft.Extensions.Primitives.pkgproj b/src/libraries/Microsoft.Extensions.Primitives/pkg/Microsoft.Extensions.Primitives.pkgproj new file mode 100644 index 00000000000000..07273ae288cef3 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Primitives/pkg/Microsoft.Extensions.Primitives.pkgproj @@ -0,0 +1,9 @@ + + + + + net461;netcoreapp2.0;uap10.0.16299;$(AllXamarinFrameworks) + + + + \ No newline at end of file diff --git a/src/libraries/Microsoft.Extensions.Primitives/ref/Microsoft.Extensions.Primitives.cs b/src/libraries/Microsoft.Extensions.Primitives/ref/Microsoft.Extensions.Primitives.cs new file mode 100644 index 00000000000000..37f8f8a869baf7 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Primitives/ref/Microsoft.Extensions.Primitives.cs @@ -0,0 +1,198 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. +// ------------------------------------------------------------------------------ +// Changes to this file must follow the https://aka.ms/api-review process. +// ------------------------------------------------------------------------------ + +namespace Microsoft.Extensions.Primitives +{ + public partial class CancellationChangeToken : Microsoft.Extensions.Primitives.IChangeToken + { + public CancellationChangeToken(System.Threading.CancellationToken cancellationToken) { } + public bool ActiveChangeCallbacks { get { throw null; } } + public bool HasChanged { get { throw null; } } + public System.IDisposable RegisterChangeCallback(System.Action callback, object state) { throw null; } + } + public static partial class ChangeToken + { + public static System.IDisposable OnChange(System.Func changeTokenProducer, System.Action changeTokenConsumer) { throw null; } + public static System.IDisposable OnChange(System.Func changeTokenProducer, System.Action changeTokenConsumer, TState state) { throw null; } + } + public partial class CompositeChangeToken : Microsoft.Extensions.Primitives.IChangeToken + { + public CompositeChangeToken(System.Collections.Generic.IReadOnlyList changeTokens) { } + public bool ActiveChangeCallbacks { get { throw null; } } + public System.Collections.Generic.IReadOnlyList ChangeTokens { get { throw null; } } + public bool HasChanged { get { throw null; } } + public System.IDisposable RegisterChangeCallback(System.Action callback, object state) { throw null; } + } + public static partial class Extensions + { + public static System.Text.StringBuilder Append(this System.Text.StringBuilder builder, Microsoft.Extensions.Primitives.StringSegment segment) { throw null; } + } + public partial interface IChangeToken + { + bool ActiveChangeCallbacks { get; } + bool HasChanged { get; } + System.IDisposable RegisterChangeCallback(System.Action callback, object state); + } + public partial struct InplaceStringBuilder + { + private object _dummy; + private int _dummyPrimitive; + public InplaceStringBuilder(int capacity) { throw null; } + public int Capacity { get { throw null; } set { } } + public void Append(Microsoft.Extensions.Primitives.StringSegment segment) { } + public void Append(char c) { } + public void Append(string value) { } + public void Append(string value, int offset, int count) { } + public override string ToString() { throw null; } + } + public readonly partial struct StringSegment : System.IEquatable, System.IEquatable + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public static readonly Microsoft.Extensions.Primitives.StringSegment Empty; + public StringSegment(string buffer) { throw null; } + public StringSegment(string buffer, int offset, int length) { throw null; } + public string Buffer { get { throw null; } } + public bool HasValue { get { throw null; } } + public char this[int index] { get { throw null; } } + public int Length { get { throw null; } } + public int Offset { get { throw null; } } + public string Value { get { throw null; } } + public System.ReadOnlyMemory AsMemory() { throw null; } + public System.ReadOnlySpan AsSpan() { throw null; } + public static int Compare(Microsoft.Extensions.Primitives.StringSegment a, Microsoft.Extensions.Primitives.StringSegment b, System.StringComparison comparisonType) { throw null; } + public bool EndsWith(string text, System.StringComparison comparisonType) { throw null; } + public bool Equals(Microsoft.Extensions.Primitives.StringSegment other) { throw null; } + public static bool Equals(Microsoft.Extensions.Primitives.StringSegment a, Microsoft.Extensions.Primitives.StringSegment b, System.StringComparison comparisonType) { throw null; } + public bool Equals(Microsoft.Extensions.Primitives.StringSegment other, System.StringComparison comparisonType) { throw null; } + public override bool Equals(object obj) { throw null; } + public bool Equals(string text) { throw null; } + public bool Equals(string text, System.StringComparison comparisonType) { throw null; } + public override int GetHashCode() { throw null; } + public int IndexOf(char c) { throw null; } + public int IndexOf(char c, int start) { throw null; } + public int IndexOf(char c, int start, int count) { throw null; } + public int IndexOfAny(char[] anyOf) { throw null; } + public int IndexOfAny(char[] anyOf, int startIndex) { throw null; } + public int IndexOfAny(char[] anyOf, int startIndex, int count) { throw null; } + public static bool IsNullOrEmpty(Microsoft.Extensions.Primitives.StringSegment value) { throw null; } + public int LastIndexOf(char value) { throw null; } + public static bool operator ==(Microsoft.Extensions.Primitives.StringSegment left, Microsoft.Extensions.Primitives.StringSegment right) { throw null; } + public static implicit operator System.ReadOnlyMemory (Microsoft.Extensions.Primitives.StringSegment segment) { throw null; } + public static implicit operator System.ReadOnlySpan (Microsoft.Extensions.Primitives.StringSegment segment) { throw null; } + public static implicit operator Microsoft.Extensions.Primitives.StringSegment (string value) { throw null; } + public static bool operator !=(Microsoft.Extensions.Primitives.StringSegment left, Microsoft.Extensions.Primitives.StringSegment right) { throw null; } + public Microsoft.Extensions.Primitives.StringTokenizer Split(char[] chars) { throw null; } + public bool StartsWith(string text, System.StringComparison comparisonType) { throw null; } + public Microsoft.Extensions.Primitives.StringSegment Subsegment(int offset) { throw null; } + public Microsoft.Extensions.Primitives.StringSegment Subsegment(int offset, int length) { throw null; } + public string Substring(int offset) { throw null; } + public string Substring(int offset, int length) { throw null; } + public override string ToString() { throw null; } + public Microsoft.Extensions.Primitives.StringSegment Trim() { throw null; } + public Microsoft.Extensions.Primitives.StringSegment TrimEnd() { throw null; } + public Microsoft.Extensions.Primitives.StringSegment TrimStart() { throw null; } + } + public partial class StringSegmentComparer : System.Collections.Generic.IComparer, System.Collections.Generic.IEqualityComparer + { + internal StringSegmentComparer() { } + public static Microsoft.Extensions.Primitives.StringSegmentComparer Ordinal { get { throw null; } } + public static Microsoft.Extensions.Primitives.StringSegmentComparer OrdinalIgnoreCase { get { throw null; } } + public int Compare(Microsoft.Extensions.Primitives.StringSegment x, Microsoft.Extensions.Primitives.StringSegment y) { throw null; } + public bool Equals(Microsoft.Extensions.Primitives.StringSegment x, Microsoft.Extensions.Primitives.StringSegment y) { throw null; } + public int GetHashCode(Microsoft.Extensions.Primitives.StringSegment obj) { throw null; } + } + public readonly partial struct StringTokenizer : System.Collections.Generic.IEnumerable, System.Collections.IEnumerable + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public StringTokenizer(Microsoft.Extensions.Primitives.StringSegment value, char[] separators) { throw null; } + public StringTokenizer(string value, char[] separators) { throw null; } + public Microsoft.Extensions.Primitives.StringTokenizer.Enumerator GetEnumerator() { throw null; } + System.Collections.Generic.IEnumerator System.Collections.Generic.IEnumerable.GetEnumerator() { throw null; } + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } + public partial struct Enumerator : System.Collections.Generic.IEnumerator, System.Collections.IEnumerator, System.IDisposable + { + private object _dummy; + private int _dummyPrimitive; + public Enumerator(ref Microsoft.Extensions.Primitives.StringTokenizer tokenizer) { throw null; } + public readonly Microsoft.Extensions.Primitives.StringSegment Current { get { throw null; } } + object System.Collections.IEnumerator.Current { get { throw null; } } + public void Dispose() { } + public bool MoveNext() { throw null; } + public void Reset() { } + } + } + public readonly partial struct StringValues : System.Collections.Generic.ICollection, System.Collections.Generic.IEnumerable, System.Collections.Generic.IList, System.Collections.Generic.IReadOnlyCollection, System.Collections.Generic.IReadOnlyList, System.Collections.IEnumerable, System.IEquatable, System.IEquatable, System.IEquatable + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public static readonly Microsoft.Extensions.Primitives.StringValues Empty; + public StringValues(string value) { throw null; } + public StringValues(string[] values) { throw null; } + public int Count { get { throw null; } } + public string this[int index] { get { throw null; } } + bool System.Collections.Generic.ICollection.IsReadOnly { get { throw null; } } + string System.Collections.Generic.IList.this[int index] { get { throw null; } set { } } + public static Microsoft.Extensions.Primitives.StringValues Concat(Microsoft.Extensions.Primitives.StringValues values1, Microsoft.Extensions.Primitives.StringValues values2) { throw null; } + public static Microsoft.Extensions.Primitives.StringValues Concat(in Microsoft.Extensions.Primitives.StringValues values, string value) { throw null; } + public static Microsoft.Extensions.Primitives.StringValues Concat(string value, in Microsoft.Extensions.Primitives.StringValues values) { throw null; } + public bool Equals(Microsoft.Extensions.Primitives.StringValues other) { throw null; } + public static bool Equals(Microsoft.Extensions.Primitives.StringValues left, Microsoft.Extensions.Primitives.StringValues right) { throw null; } + public static bool Equals(Microsoft.Extensions.Primitives.StringValues left, string right) { throw null; } + public static bool Equals(Microsoft.Extensions.Primitives.StringValues left, string[] right) { throw null; } + public override bool Equals(object obj) { throw null; } + public bool Equals(string other) { throw null; } + public static bool Equals(string left, Microsoft.Extensions.Primitives.StringValues right) { throw null; } + public bool Equals(string[] other) { throw null; } + public static bool Equals(string[] left, Microsoft.Extensions.Primitives.StringValues right) { throw null; } + public Microsoft.Extensions.Primitives.StringValues.Enumerator GetEnumerator() { throw null; } + public override int GetHashCode() { throw null; } + public static bool IsNullOrEmpty(Microsoft.Extensions.Primitives.StringValues value) { throw null; } + public static bool operator ==(Microsoft.Extensions.Primitives.StringValues left, Microsoft.Extensions.Primitives.StringValues right) { throw null; } + public static bool operator ==(Microsoft.Extensions.Primitives.StringValues left, object right) { throw null; } + public static bool operator ==(Microsoft.Extensions.Primitives.StringValues left, string right) { throw null; } + public static bool operator ==(Microsoft.Extensions.Primitives.StringValues left, string[] right) { throw null; } + public static bool operator ==(object left, Microsoft.Extensions.Primitives.StringValues right) { throw null; } + public static bool operator ==(string left, Microsoft.Extensions.Primitives.StringValues right) { throw null; } + public static bool operator ==(string[] left, Microsoft.Extensions.Primitives.StringValues right) { throw null; } + public static implicit operator string (Microsoft.Extensions.Primitives.StringValues values) { throw null; } + public static implicit operator string[] (Microsoft.Extensions.Primitives.StringValues value) { throw null; } + public static implicit operator Microsoft.Extensions.Primitives.StringValues (string value) { throw null; } + public static implicit operator Microsoft.Extensions.Primitives.StringValues (string[] values) { throw null; } + public static bool operator !=(Microsoft.Extensions.Primitives.StringValues left, Microsoft.Extensions.Primitives.StringValues right) { throw null; } + public static bool operator !=(Microsoft.Extensions.Primitives.StringValues left, object right) { throw null; } + public static bool operator !=(Microsoft.Extensions.Primitives.StringValues left, string right) { throw null; } + public static bool operator !=(Microsoft.Extensions.Primitives.StringValues left, string[] right) { throw null; } + public static bool operator !=(object left, Microsoft.Extensions.Primitives.StringValues right) { throw null; } + public static bool operator !=(string left, Microsoft.Extensions.Primitives.StringValues right) { throw null; } + public static bool operator !=(string[] left, Microsoft.Extensions.Primitives.StringValues right) { throw null; } + void System.Collections.Generic.ICollection.Add(string item) { } + void System.Collections.Generic.ICollection.Clear() { } + bool System.Collections.Generic.ICollection.Contains(string item) { throw null; } + void System.Collections.Generic.ICollection.CopyTo(string[] array, int arrayIndex) { } + bool System.Collections.Generic.ICollection.Remove(string item) { throw null; } + System.Collections.Generic.IEnumerator System.Collections.Generic.IEnumerable.GetEnumerator() { throw null; } + int System.Collections.Generic.IList.IndexOf(string item) { throw null; } + void System.Collections.Generic.IList.Insert(int index, string item) { } + void System.Collections.Generic.IList.RemoveAt(int index) { } + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } + public string[] ToArray() { throw null; } + public override string ToString() { throw null; } + public partial struct Enumerator : System.Collections.Generic.IEnumerator, System.Collections.IEnumerator, System.IDisposable + { + private object _dummy; + private int _dummyPrimitive; + public Enumerator(ref Microsoft.Extensions.Primitives.StringValues values) { throw null; } + public string Current { get { throw null; } } + object System.Collections.IEnumerator.Current { get { throw null; } } + public void Dispose() { } + public bool MoveNext() { throw null; } + void System.Collections.IEnumerator.Reset() { } + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Primitives/ref/Microsoft.Extensions.Primitives.csproj b/src/libraries/Microsoft.Extensions.Primitives/ref/Microsoft.Extensions.Primitives.csproj new file mode 100644 index 00000000000000..99c135f7df251a --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Primitives/ref/Microsoft.Extensions.Primitives.csproj @@ -0,0 +1,12 @@ + + + + netstandard2.0;netstandard2.1 + + + + + + + + diff --git a/src/libraries/Microsoft.Extensions.Primitives/src/CancellationChangeToken.cs b/src/libraries/Microsoft.Extensions.Primitives/src/CancellationChangeToken.cs new file mode 100644 index 00000000000000..1df95d5a66a74a --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Primitives/src/CancellationChangeToken.cs @@ -0,0 +1,84 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading; + +namespace Microsoft.Extensions.Primitives +{ + /// + /// A implementation using . + /// + public class CancellationChangeToken : IChangeToken + { + /// + /// Initializes a new instance of . + /// + /// The . + public CancellationChangeToken(CancellationToken cancellationToken) + { + Token = cancellationToken; + } + + /// + public bool ActiveChangeCallbacks { get; private set; } = true; + + /// + public bool HasChanged => Token.IsCancellationRequested; + + private CancellationToken Token { get; } + + /// + public IDisposable RegisterChangeCallback(Action callback, object state) + { +#if NETCOREAPP || NETSTANDARD2_1 + try + { + return Token.UnsafeRegister(callback, state); + } + catch (ObjectDisposedException) + { + // Reset the flag so that we can indicate to future callers that this wouldn't work. + ActiveChangeCallbacks = false; + } +#else + // Don't capture the current ExecutionContext and its AsyncLocals onto the token registration causing them to live forever + var restoreFlow = false; + if (!ExecutionContext.IsFlowSuppressed()) + { + ExecutionContext.SuppressFlow(); + restoreFlow = true; + } + + try + { + return Token.Register(callback, state); + } + catch (ObjectDisposedException) + { + // Reset the flag so that we can indicate to future callers that this wouldn't work. + ActiveChangeCallbacks = false; + } + finally + { + // Restore the current ExecutionContext + if (restoreFlow) + { + ExecutionContext.RestoreFlow(); + } + } +#endif + return NullDisposable.Instance; + } + + private class NullDisposable : IDisposable + { + public static readonly NullDisposable Instance = new NullDisposable(); + + public void Dispose() + { + } + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Primitives/src/ChangeToken.cs b/src/libraries/Microsoft.Extensions.Primitives/src/ChangeToken.cs new file mode 100644 index 00000000000000..55232803d85efe --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Primitives/src/ChangeToken.cs @@ -0,0 +1,152 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics; +using System.Threading; + +namespace Microsoft.Extensions.Primitives +{ + /// + /// Propagates notifications that a change has occurred. + /// + public static class ChangeToken + { + /// + /// Registers the action to be called whenever the token produced changes. + /// + /// Produces the change token. + /// Action called when the token changes. + /// + public static IDisposable OnChange(Func changeTokenProducer, Action changeTokenConsumer) + { + if (changeTokenProducer == null) + { + throw new ArgumentNullException(nameof(changeTokenProducer)); + } + if (changeTokenConsumer == null) + { + throw new ArgumentNullException(nameof(changeTokenConsumer)); + } + + return new ChangeTokenRegistration(changeTokenProducer, callback => callback(), changeTokenConsumer); + } + + /// + /// Registers the action to be called whenever the token produced changes. + /// + /// Produces the change token. + /// Action called when the token changes. + /// state for the consumer. + /// + public static IDisposable OnChange(Func changeTokenProducer, Action changeTokenConsumer, TState state) + { + if (changeTokenProducer == null) + { + throw new ArgumentNullException(nameof(changeTokenProducer)); + } + if (changeTokenConsumer == null) + { + throw new ArgumentNullException(nameof(changeTokenConsumer)); + } + + return new ChangeTokenRegistration(changeTokenProducer, changeTokenConsumer, state); + } + + private class ChangeTokenRegistration : IDisposable + { + private readonly Func _changeTokenProducer; + private readonly Action _changeTokenConsumer; + private readonly TState _state; + private IDisposable _disposable; + + private static readonly NoopDisposable _disposedSentinel = new NoopDisposable(); + + public ChangeTokenRegistration(Func changeTokenProducer, Action changeTokenConsumer, TState state) + { + _changeTokenProducer = changeTokenProducer; + _changeTokenConsumer = changeTokenConsumer; + _state = state; + + var token = changeTokenProducer(); + + RegisterChangeTokenCallback(token); + } + + private void OnChangeTokenFired() + { + // The order here is important. We need to take the token and then apply our changes BEFORE + // registering. This prevents us from possible having two change updates to process concurrently. + // + // If the token changes after we take the token, then we'll process the update immediately upon + // registering the callback. + var token = _changeTokenProducer(); + + try + { + _changeTokenConsumer(_state); + } + finally + { + // We always want to ensure the callback is registered + RegisterChangeTokenCallback(token); + } + } + + private void RegisterChangeTokenCallback(IChangeToken token) + { + var registraton = token.RegisterChangeCallback(s => ((ChangeTokenRegistration)s).OnChangeTokenFired(), this); + + SetDisposable(registraton); + } + + private void SetDisposable(IDisposable disposable) + { + // We don't want to transition from _disposedSentinel => anything since it's terminal + // but we want to allow going from previously assigned disposable, to another + // disposable. + var current = Volatile.Read(ref _disposable); + + // If Dispose was called, then immediately dispose the disposable + if (current == _disposedSentinel) + { + disposable.Dispose(); + return; + } + + // Otherwise, try to update the disposable + var previous = Interlocked.CompareExchange(ref _disposable, disposable, current); + + if (previous == _disposedSentinel) + { + // The subscription was disposed so we dispose immediately and return + disposable.Dispose(); + } + else if (previous == current) + { + // We successfuly assigned the _disposable field to disposable + } + else + { + // Sets can never overlap with other SetDisposable calls so we should never get into this situation + throw new InvalidOperationException("Somebody else set the _disposable field"); + } + } + + public void Dispose() + { + // If the previous value is disposable then dispose it, otherwise, + // now we've set the disposed sentinel + Interlocked.Exchange(ref _disposable, _disposedSentinel).Dispose(); + } + + private class NoopDisposable : IDisposable + { + public void Dispose() + { + } + } + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Primitives/src/CompositeChangeToken.cs b/src/libraries/Microsoft.Extensions.Primitives/src/CompositeChangeToken.cs new file mode 100644 index 00000000000000..9578bbc0bd5a2f --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Primitives/src/CompositeChangeToken.cs @@ -0,0 +1,134 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; + +namespace Microsoft.Extensions.Primitives +{ + /// + /// An which represents one or more instances. + /// + public class CompositeChangeToken : IChangeToken + { + private static readonly Action _onChangeDelegate = OnChange; + private readonly object _callbackLock = new object(); + private CancellationTokenSource _cancellationTokenSource; + private bool _registeredCallbackProxy; + private List _disposables; + + /// + /// Creates a new instance of . + /// + /// The list of to compose. + public CompositeChangeToken(IReadOnlyList changeTokens) + { + ChangeTokens = changeTokens ?? throw new ArgumentNullException(nameof(changeTokens)); + for (var i = 0; i < ChangeTokens.Count; i++) + { + if (ChangeTokens[i].ActiveChangeCallbacks) + { + ActiveChangeCallbacks = true; + break; + } + } + } + + /// + /// Returns the list of which compose the current . + /// + public IReadOnlyList ChangeTokens { get; } + + /// + public IDisposable RegisterChangeCallback(Action callback, object state) + { + EnsureCallbacksInitialized(); + return _cancellationTokenSource.Token.Register(callback, state); + } + + /// + public bool HasChanged + { + get + { + if (_cancellationTokenSource != null && _cancellationTokenSource.Token.IsCancellationRequested) + { + return true; + } + + for (var i = 0; i < ChangeTokens.Count; i++) + { + if (ChangeTokens[i].HasChanged) + { + OnChange(this); + return true; + } + } + + return false; + } + } + + /// + public bool ActiveChangeCallbacks { get; } + + private void EnsureCallbacksInitialized() + { + if (_registeredCallbackProxy) + { + return; + } + + lock (_callbackLock) + { + if (_registeredCallbackProxy) + { + return; + } + + _cancellationTokenSource = new CancellationTokenSource(); + _disposables = new List(); + for (var i = 0; i < ChangeTokens.Count; i++) + { + if (ChangeTokens[i].ActiveChangeCallbacks) + { + var disposable = ChangeTokens[i].RegisterChangeCallback(_onChangeDelegate, this); + _disposables.Add(disposable); + } + } + _registeredCallbackProxy = true; + } + } + + private static void OnChange(object state) + { + var compositeChangeTokenState = (CompositeChangeToken)state; + if (compositeChangeTokenState._cancellationTokenSource == null) + { + return; + } + + lock (compositeChangeTokenState._callbackLock) + { + try + { + compositeChangeTokenState._cancellationTokenSource.Cancel(); + } + catch + { + } + } + + var disposables = compositeChangeTokenState._disposables; + Debug.Assert(disposables != null); + for (var i = 0; i < disposables.Count; i++) + { + disposables[i].Dispose(); + } + + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Primitives/src/Extensions.cs b/src/libraries/Microsoft.Extensions.Primitives/src/Extensions.cs new file mode 100644 index 00000000000000..5ab0f84ba32818 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Primitives/src/Extensions.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text; + +namespace Microsoft.Extensions.Primitives +{ + public static class Extensions + { + /// + /// Add the given to the . + /// + /// The to add to. + /// The to add. + /// The original . + public static StringBuilder Append(this StringBuilder builder, StringSegment segment) + { + return builder.Append(segment.Buffer, segment.Offset, segment.Length); + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Primitives/src/IChangeToken.cs b/src/libraries/Microsoft.Extensions.Primitives/src/IChangeToken.cs new file mode 100644 index 00000000000000..958a0dbc24eab2 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Primitives/src/IChangeToken.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Microsoft.Extensions.Primitives +{ + /// + /// Propagates notifications that a change has occurred. + /// + public interface IChangeToken + { + /// + /// Gets a value that indicates if a change has occurred. + /// + bool HasChanged { get; } + + /// + /// Indicates if this token will pro-actively raise callbacks. If false, the token consumer must + /// poll to detect changes. + /// + bool ActiveChangeCallbacks { get; } + + /// + /// Registers for a callback that will be invoked when the entry has changed. + /// MUST be set before the callback is invoked. + /// + /// The to invoke. + /// State to be passed into the callback. + /// An that is used to unregister the callback. + IDisposable RegisterChangeCallback(Action callback, object state); + } +} diff --git a/src/libraries/Microsoft.Extensions.Primitives/src/InplaceStringBuilder.cs b/src/libraries/Microsoft.Extensions.Primitives/src/InplaceStringBuilder.cs new file mode 100644 index 00000000000000..2a55fa98bcffb8 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Primitives/src/InplaceStringBuilder.cs @@ -0,0 +1,137 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace Microsoft.Extensions.Primitives +{ + [DebuggerDisplay("Value = {_value}")] + [Obsolete("This type is obsolete and will be removed in a future version.")] + public struct InplaceStringBuilder + { + private int _offset; + private int _capacity; + private string _value; + + public InplaceStringBuilder(int capacity) : this() + { + if (capacity < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity); + } + + _capacity = capacity; + } + + public int Capacity + { + get => _capacity; + set + { + if (value < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value); + } + + // _offset > 0 indicates writing state + if (_offset > 0) + { + ThrowHelper.ThrowInvalidOperationException(ExceptionResource.Capacity_CannotChangeAfterWriteStarted); + } + + _capacity = value; + } + } + + public void Append(string value) + { + if (value == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.value); + } + + Append(value, 0, value.Length); + } + + public void Append(StringSegment segment) + { + Append(segment.Buffer, segment.Offset, segment.Length); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void Append(string value, int offset, int count) + { + EnsureValueIsInitialized(); + + if (value == null + || offset < 0 + || value.Length - offset < count + || Capacity - _offset < count) + { + ThrowValidationError(value, offset, count); + } + + fixed (char* destination = _value) + fixed (char* source = value) + { + Unsafe.CopyBlockUnaligned(destination + _offset, source + offset, (uint)count * 2); + _offset += count; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void Append(char c) + { + EnsureValueIsInitialized(); + + if (_offset >= Capacity) + { + ThrowHelper.ThrowInvalidOperationException(ExceptionResource.Capacity_NotEnough, 1, Capacity - _offset); + } + + fixed (char* destination = _value) + { + destination[_offset++] = c; + } + } + + public override string ToString() + { + if (Capacity != _offset) + { + ThrowHelper.ThrowInvalidOperationException(ExceptionResource.Capacity_NotUsedEntirely, Capacity, _offset); + } + + return _value; + } + + private void EnsureValueIsInitialized() + { + if (_value == null) + { + _value = new string('\0', _capacity); + } + } + + private void ThrowValidationError(string value, int offset, int count) + { + if (value == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.value); + } + + if (offset < 0 || value.Length - offset < count) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.offset); + } + + if (Capacity - _offset < count) + { + ThrowHelper.ThrowInvalidOperationException(ExceptionResource.Capacity_NotEnough, value.Length, Capacity - _offset); + } + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Primitives/src/MatchingRefApiCompatBaseline.txt b/src/libraries/Microsoft.Extensions.Primitives/src/MatchingRefApiCompatBaseline.txt new file mode 100644 index 00000000000000..9633644eea0ff5 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Primitives/src/MatchingRefApiCompatBaseline.txt @@ -0,0 +1 @@ +TypesMustExist : Type 'Microsoft.DotNet.PlatformAbstractions.HashCodeCombiner' does not exist in the reference but it does exist in the implementation. \ No newline at end of file diff --git a/src/libraries/Microsoft.Extensions.Primitives/src/Microsoft.Extensions.Primitives.csproj b/src/libraries/Microsoft.Extensions.Primitives/src/Microsoft.Extensions.Primitives.csproj new file mode 100644 index 00000000000000..8b40039c308c33 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Primitives/src/Microsoft.Extensions.Primitives.csproj @@ -0,0 +1,41 @@ + + + $(NetCoreAppCurrent);netcoreapp3.0;netstandard2.0 + $(NoWarn);CS1591;CA1200;SA1121;SA1129 + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/libraries/Microsoft.Extensions.Primitives/src/Resources/Strings.resx b/src/libraries/Microsoft.Extensions.Primitives/src/Resources/Strings.resx new file mode 100644 index 00000000000000..197852e21f0f9f --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Primitives/src/Resources/Strings.resx @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Offset and length are out of bounds for the string or length is greater than the number of characters from index to the end of the string. + + + Offset and length are out of bounds for this StringSegment or length is greater than the number of characters to the end of this StringSegment. + + + Cannot change capacity after write started. + + + Not enough capacity to write '{0}' characters, only '{1}' left. + + + Entire reserved capacity was not used. Capacity: '{0}', written '{1}'. + + \ No newline at end of file diff --git a/src/libraries/Microsoft.Extensions.Primitives/src/StringSegment.cs b/src/libraries/Microsoft.Extensions.Primitives/src/StringSegment.cs new file mode 100644 index 00000000000000..68150af110c04f --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Primitives/src/StringSegment.cs @@ -0,0 +1,721 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.CompilerServices; + +namespace Microsoft.Extensions.Primitives +{ + /// + /// An optimized representation of a substring. + /// + public readonly struct StringSegment : IEquatable, IEquatable + { + /// + /// A for . + /// + public static readonly StringSegment Empty = string.Empty; + + /// + /// Initializes an instance of the struct. + /// + /// + /// The original . The includes the whole . + /// + public StringSegment(string buffer) + { + Buffer = buffer; + Offset = 0; + Length = buffer?.Length ?? 0; + } + + /// + /// Initializes an instance of the struct. + /// + /// The original used as buffer. + /// The offset of the segment within the . + /// The length of the segment. + /// + /// is . + /// + /// + /// or is less than zero, or + + /// is greater than the number of characters in . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public StringSegment(string buffer, int offset, int length) + { + // Validate arguments, check is minimal instructions with reduced branching for inlinable fast-path + // Negative values discovered though conversion to high values when converted to unsigned + // Failure should be rare and location determination and message is delegated to failure functions + if (buffer == null || (uint)offset > (uint)buffer.Length || (uint)length > (uint)(buffer.Length - offset)) + { + ThrowInvalidArguments(buffer, offset, length); + } + + Buffer = buffer; + Offset = offset; + Length = length; + } + + /// + /// Gets the buffer for this . + /// + public string Buffer { get; } + + /// + /// Gets the offset within the buffer for this . + /// + public int Offset { get; } + + /// + /// Gets the length of this . + /// + public int Length { get; } + + /// + /// Gets the value of this segment as a . + /// + public string Value + { + get + { + if (HasValue) + { + return Buffer.Substring(Offset, Length); + } + else + { + return null; + } + } + } + + /// + /// Gets whether this contains a valid value. + /// + public bool HasValue + { + get { return Buffer != null; } + } + + /// + /// Gets the at a specified position in the current . + /// + /// The offset into the + /// The at a specified position. + /// + /// is greater than or equal to or less than zero. + /// + public char this[int index] + { + get + { + if ((uint)index >= (uint)Length) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index); + } + + return Buffer[Offset + index]; + } + } + + /// + /// Gets a from the current . + /// + /// The from this . + public ReadOnlySpan AsSpan() => Buffer.AsSpan(Offset, Length); + + /// + /// Gets a from the current . + /// + /// The from this . + public ReadOnlyMemory AsMemory() => Buffer.AsMemory(Offset, Length); + + /// + /// Compares substrings of two specified objects using the specified rules, + /// and returns an integer that indicates their relative position in the sort order. + /// + /// The first to compare. + /// The second to compare. + /// One of the enumeration values that specifies the rules for the comparison. + /// + /// A 32-bit signed integer indicating the lexical relationship between the two comparands. + /// The value is negative if is less than , 0 if the two comparands are equal, + /// and positive if is greater than . + /// + public static int Compare(StringSegment a, StringSegment b, StringComparison comparisonType) + { + var minLength = Math.Min(a.Length, b.Length); + var diff = string.Compare(a.Buffer, a.Offset, b.Buffer, b.Offset, minLength, comparisonType); + if (diff == 0) + { + diff = a.Length - b.Length; + } + + return diff; + } + + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + return obj is StringSegment segment && Equals(segment); + } + + /// + /// Indicates whether the current object is equal to another object of the same type. + /// + /// An object to compare with this object. + /// if the current object is equal to the other parameter; otherwise, . + public bool Equals(StringSegment other) => Equals(other, StringComparison.Ordinal); + + /// + /// Indicates whether the current object is equal to another object of the same type. + /// + /// An object to compare with this object. + /// One of the enumeration values that specifies the rules to use in the comparison. + /// if the current object is equal to the other parameter; otherwise, . + public bool Equals(StringSegment other, StringComparison comparisonType) + { + if (Length != other.Length) + { + return false; + } + + return string.Compare(Buffer, Offset, other.Buffer, other.Offset, other.Length, comparisonType) == 0; + } + + // This handles StringSegment.Equals(string, StringSegment, StringComparison) and StringSegment.Equals(StringSegment, string, StringComparison) + // via the implicit type converter + /// + /// Determines whether two specified objects have the same value. A parameter specifies the culture, case, and + /// sort rules used in the comparison. + /// + /// The first to compare. + /// The second to compare. + /// One of the enumeration values that specifies the rules for the comparison. + /// if the objects are equal; otherwise, . + public static bool Equals(StringSegment a, StringSegment b, StringComparison comparisonType) + { + return a.Equals(b, comparisonType); + } + + /// + /// Checks if the specified is equal to the current . + /// + /// The to compare with the current . + /// if the specified is equal to the current ; otherwise, . + public bool Equals(string text) + { + return Equals(text, StringComparison.Ordinal); + } + + /// + /// Checks if the specified is equal to the current . + /// + /// The to compare with the current . + /// One of the enumeration values that specifies the rules to use in the comparison. + /// if the specified is equal to the current ; otherwise, . + /// + /// is . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(string text, StringComparison comparisonType) + { + if (text == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.text); + } + + var textLength = text.Length; + if (!HasValue || Length != textLength) + { + return false; + } + + return string.Compare(Buffer, Offset, text, 0, textLength, comparisonType) == 0; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() + { +#if NETCOREAPP || NETSTANDARD2_1 + return string.GetHashCode(AsSpan()); +#elif NETSTANDARD2_0 + // This GetHashCode is expensive since it allocates on every call. + // However this is required to ensure we retain any behavior (such as hash code randomization) that + // string.GetHashCode has. + return Value?.GetHashCode() ?? 0; +#else +#error Target frameworks need to be updated. +#endif + + } + + /// + /// Checks if two specified have the same value. + /// + /// The first to compare, or . + /// The second to compare, or . + /// if the value of is the same as the value of ; otherwise, . + public static bool operator ==(StringSegment left, StringSegment right) => left.Equals(right); + + /// + /// Checks if two specified have different values. + /// + /// The first to compare, or . + /// The second to compare, or . + /// if the value of is different from the value of ; otherwise, . + public static bool operator !=(StringSegment left, StringSegment right) => !left.Equals(right); + + // PERF: Do NOT add a implicit converter from StringSegment to String. That would negate most of the perf safety. + /// + /// Creates a new from the given . + /// + /// The to convert to a + public static implicit operator StringSegment(string value) => new StringSegment(value); + + /// + /// Creates a see from the given . + /// + /// The to convert to a . + public static implicit operator ReadOnlySpan(StringSegment segment) => segment.AsSpan(); + + /// + /// Creates a see from the given . + /// + /// The to convert to a . + public static implicit operator ReadOnlyMemory(StringSegment segment) => segment.AsMemory(); + + /// + /// Checks if the beginning of this matches the specified when compared using the specified . + /// + /// The to compare. + /// One of the enumeration values that specifies the rules to use in the comparison. + /// if matches the beginning of this ; otherwise, . + /// + /// is . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool StartsWith(string text, StringComparison comparisonType) + { + if (text == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.text); + } + + var result = false; + var textLength = text.Length; + + if (HasValue && Length >= textLength) + { + result = string.Compare(Buffer, Offset, text, 0, textLength, comparisonType) == 0; + } + + return result; + } + + /// + /// Checks if the end of this matches the specified when compared using the specified . + /// + /// The to compare. + /// One of the enumeration values that specifies the rules to use in the comparison. + /// if matches the end of this ; otherwise, . + /// + /// is . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool EndsWith(string text, StringComparison comparisonType) + { + if (text == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.text); + } + + var result = false; + var textLength = text.Length; + var comparisonLength = Offset + Length - textLength; + + if (HasValue && comparisonLength > 0) + { + result = string.Compare(Buffer, comparisonLength, text, 0, textLength, comparisonType) == 0; + } + + return result; + } + + /// + /// Retrieves a substring from this . + /// The substring starts at the position specified by and has the remaining length. + /// + /// The zero-based starting character position of a substring in this . + /// A that is equivalent to the substring of remaining length that begins at + /// in this + /// + /// is greater than or equal to or less than zero. + /// + public string Substring(int offset) => Substring(offset, Length - offset); + + /// + /// Retrieves a substring from this . + /// The substring starts at the position specified by and has the specified . + /// + /// The zero-based starting character position of a substring in this . + /// The number of characters in the substring. + /// A that is equivalent to the substring of length that begins at + /// in this + /// + /// or is less than zero, or + is + /// greater than . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string Substring(int offset, int length) + { + if (!HasValue || offset < 0 || length < 0 || (uint)(offset + length) > (uint)Length) + { + ThrowInvalidArguments(offset, length); + } + + return Buffer.Substring(Offset + offset, length); + } + + /// + /// Retrieves a that represents a substring from this . + /// The starts at the position specified by . + /// + /// The zero-based starting character position of a substring in this . + /// A that begins at in this + /// whose length is the remainder. + /// + /// is greater than or equal to or less than zero. + /// + public StringSegment Subsegment(int offset) => Subsegment(offset, Length - offset); + + /// + /// Retrieves a that represents a substring from this . + /// The starts at the position specified by and has the specified . + /// + /// The zero-based starting character position of a substring in this . + /// The number of characters in the substring. + /// A that is equivalent to the substring of length that begins at in this + /// + /// or is less than zero, or + is + /// greater than . + /// + public StringSegment Subsegment(int offset, int length) + { + if (!HasValue || offset < 0 || length < 0 || (uint)(offset + length) > (uint)Length) + { + ThrowInvalidArguments(offset, length); + } + + return new StringSegment(Buffer, Offset + offset, length); + } + + /// + /// Gets the zero-based index of the first occurrence of the character in this . + /// The search starts at and examines a specified number of character positions. + /// + /// The Unicode character to seek. + /// The zero-based index position at which the search starts. + /// The number of characters to examine. + /// The zero-based index position of from the beginning of the if that character is found, or -1 if it is not. + /// + /// or is less than zero, or + is + /// greater than . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int IndexOf(char c, int start, int count) + { + var offset = Offset + start; + + if (!HasValue || start < 0 || (uint)offset > (uint)Buffer.Length) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.start); + } + + if (count < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count); + } + + var index = Buffer.IndexOf(c, offset, count); + if (index != -1) + { + index -= Offset; + } + + return index; + } + + /// + /// Gets the zero-based index of the first occurrence of the character in this . + /// The search starts at . + /// + /// The Unicode character to seek. + /// The zero-based index position at which the search starts. + /// The zero-based index position of from the beginning of the if that character is found, or -1 if it is not. + /// + /// is greater than or equal to or less than zero. + /// + public int IndexOf(char c, int start) => IndexOf(c, start, Length - start); + + /// + /// Gets the zero-based index of the first occurrence of the character in this . + /// + /// The Unicode character to seek. + /// The zero-based index position of from the beginning of the if that character is found, or -1 if it is not. + public int IndexOf(char c) => IndexOf(c, 0, Length); + + /// + /// Reports the zero-based index of the first occurrence in this instance of any character in a specified array + /// of Unicode characters. The search starts at a specified character position and examines a specified number + /// of character positions. + /// + /// A Unicode character array containing one or more characters to seek. + /// The search starting position. + /// The number of character positions to examine. + /// The zero-based index position of the first occurrence in this instance where any character in + /// was found; -1 if no character in was found. + /// + /// is . + /// + /// + /// or is less than zero, or + is + /// greater than . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int IndexOfAny(char[] anyOf, int startIndex, int count) + { + var index = -1; + + if (HasValue) + { + if (startIndex < 0 || Offset + startIndex > Buffer.Length) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.start); + } + + if (count < 0 || Offset + startIndex + count > Buffer.Length) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count); + } + + index = Buffer.IndexOfAny(anyOf, Offset + startIndex, count); + if (index != -1) + { + index -= Offset; + } + } + + return index; + } + + /// + /// Reports the zero-based index of the first occurrence in this instance of any character in a specified array + /// of Unicode characters. The search starts at a specified character position. + /// + /// A Unicode character array containing one or more characters to seek. + /// The search starting position. + /// The zero-based index position of the first occurrence in this instance where any character in + /// was found; -1 if no character in was found. + /// + /// is greater than or equal to or less than zero. + /// + public int IndexOfAny(char[] anyOf, int startIndex) + { + return IndexOfAny(anyOf, startIndex, Length - startIndex); + } + + /// + /// Reports the zero-based index of the first occurrence in this instance of any character in a specified array + /// of Unicode characters. + /// + /// A Unicode character array containing one or more characters to seek. + /// The zero-based index position of the first occurrence in this instance where any character in + /// was found; -1 if no character in was found. + public int IndexOfAny(char[] anyOf) + { + return IndexOfAny(anyOf, 0, Length); + } + + /// + /// Reports the zero-based index position of the last occurrence of a specified Unicode character within this instance. + /// + /// The Unicode character to seek. + /// The zero-based index position of value if that character is found, or -1 if it is not. + public int LastIndexOf(char value) + { + var index = -1; + + if (HasValue) + { + index = Buffer.LastIndexOf(value, Offset + Length - 1, Length); + if (index != -1) + { + index -= Offset; + } + } + + return index; + } + + /// + /// Removes all leading and trailing whitespaces. + /// + /// The trimmed . + public StringSegment Trim() => TrimStart().TrimEnd(); + + /// + /// Removes all leading whitespaces. + /// + /// The trimmed . + public unsafe StringSegment TrimStart() + { + var trimmedStart = Offset; + var length = Offset + Length; + + fixed (char* p = Buffer) + { + while (trimmedStart < length) + { + var c = p[trimmedStart]; + + if (!char.IsWhiteSpace(c)) + { + break; + } + + trimmedStart++; + } + } + + return new StringSegment(Buffer, trimmedStart, length - trimmedStart); + } + + /// + /// Removes all trailing whitespaces. + /// + /// The trimmed . + public unsafe StringSegment TrimEnd() + { + var offset = Offset; + var trimmedEnd = offset + Length - 1; + + fixed (char* p = Buffer) + { + while (trimmedEnd >= offset) + { + var c = p[trimmedEnd]; + + if (!char.IsWhiteSpace(c)) + { + break; + } + + trimmedEnd--; + } + } + + return new StringSegment(Buffer, offset, trimmedEnd - offset + 1); + } + + /// + /// Splits a string into s that are based on the characters in an array. + /// + /// A character array that delimits the substrings in this string, an empty array that + /// contains no delimiters, or null. + /// An whose elements contain the s from this instance + /// that are delimited by one or more characters in . + public StringTokenizer Split(char[] chars) + { + return new StringTokenizer(this, chars); + } + + /// + /// Indicates whether the specified is null or an Empty string. + /// + /// The to test. + /// + public static bool IsNullOrEmpty(StringSegment value) + { + var res = false; + + if (!value.HasValue || value.Length == 0) + { + res = true; + } + + return res; + } + + /// + /// Returns the represented by this or if the does not contain a value. + /// + /// The represented by this or if the does not contain a value. + public override string ToString() + { + return Value ?? string.Empty; + } + + // Methods that do no return (i.e. throw) are not inlined + // https://github.com/dotnet/coreclr/pull/6103 + private static void ThrowInvalidArguments(string buffer, int offset, int length) + { + // Only have single throw in method so is marked as "does not return" and isn't inlined to caller + throw GetInvalidArgumentsException(); + + Exception GetInvalidArgumentsException() + { + if (buffer == null) + { + return ThrowHelper.GetArgumentNullException(ExceptionArgument.buffer); + } + + if (offset < 0) + { + return ThrowHelper.GetArgumentOutOfRangeException(ExceptionArgument.offset); + } + + if (length < 0) + { + return ThrowHelper.GetArgumentOutOfRangeException(ExceptionArgument.length); + } + + return ThrowHelper.GetArgumentException(ExceptionResource.Argument_InvalidOffsetLength); + } + } + + private void ThrowInvalidArguments(int offset, int length) + { + throw GetInvalidArgumentsException(HasValue); + + Exception GetInvalidArgumentsException(bool hasValue) + { + if (!hasValue) + { + return ThrowHelper.GetArgumentOutOfRangeException(ExceptionArgument.offset); + } + + if (offset < 0) + { + return ThrowHelper.GetArgumentOutOfRangeException(ExceptionArgument.offset); + } + + if (length < 0) + { + return ThrowHelper.GetArgumentOutOfRangeException(ExceptionArgument.length); + } + + return ThrowHelper.GetArgumentException(ExceptionResource.Argument_InvalidOffsetLengthStringSegment); + } + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Primitives/src/StringSegmentComparer.cs b/src/libraries/Microsoft.Extensions.Primitives/src/StringSegmentComparer.cs new file mode 100644 index 00000000000000..eaecc9efb764cf --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Primitives/src/StringSegmentComparer.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.Extensions.Primitives +{ + public class StringSegmentComparer : IComparer, IEqualityComparer + { + public static StringSegmentComparer Ordinal { get; } + = new StringSegmentComparer(StringComparison.Ordinal, StringComparer.Ordinal); + + public static StringSegmentComparer OrdinalIgnoreCase { get; } + = new StringSegmentComparer(StringComparison.OrdinalIgnoreCase, StringComparer.OrdinalIgnoreCase); + + private StringSegmentComparer(StringComparison comparison, StringComparer comparer) + { + Comparison = comparison; + Comparer = comparer; + } + + private StringComparison Comparison { get; } + private StringComparer Comparer { get; } + + public int Compare(StringSegment x, StringSegment y) + { + return StringSegment.Compare(x, y, Comparison); + } + + public bool Equals(StringSegment x, StringSegment y) + { + return StringSegment.Equals(x, y, Comparison); + } + + public int GetHashCode(StringSegment obj) + { +#if NETCOREAPP || NETSTANDARD2_1 + return string.GetHashCode(obj.AsSpan(), Comparison); +#else + if (!obj.HasValue) + { + return 0; + } + + // .NET Core strings use randomized hash codes for security reasons. Consequently we must materialize the StringSegment as a string + return Comparer.GetHashCode(obj.Value); +#endif + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Primitives/src/StringTokenizer.cs b/src/libraries/Microsoft.Extensions.Primitives/src/StringTokenizer.cs new file mode 100644 index 00000000000000..816de70c3317fc --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Primitives/src/StringTokenizer.cs @@ -0,0 +1,125 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Microsoft.Extensions.Primitives +{ + /// + /// Tokenizes a into s. + /// + public readonly struct StringTokenizer : IEnumerable + { + private readonly StringSegment _value; + private readonly char[] _separators; + + /// + /// Initializes a new instance of . + /// + /// The to tokenize. + /// The characters to tokenize by. + public StringTokenizer(string value, char[] separators) + { + if (value == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.value); + } + + if (separators == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.separators); + } + + _value = value; + _separators = separators; + } + + /// + /// Initializes a new instance of . + /// + /// The to tokenize. + /// The characters to tokenize by. + public StringTokenizer(StringSegment value, char[] separators) + { + if (!value.HasValue) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.value); + } + + if (separators == null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.separators); + } + + _value = value; + _separators = separators; + } + + public Enumerator GetEnumerator() => new Enumerator(in _value, _separators); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public struct Enumerator : IEnumerator + { + private readonly StringSegment _value; + private readonly char[] _separators; + private int _index; + + internal Enumerator(in StringSegment value, char[] separators) + { + _value = value; + _separators = separators; + Current = default; + _index = 0; + } + + public Enumerator(ref StringTokenizer tokenizer) + { + _value = tokenizer._value; + _separators = tokenizer._separators; + Current = default(StringSegment); + _index = 0; + } + + public StringSegment Current { get; private set; } + + object IEnumerator.Current => Current; + + public void Dispose() + { + } + + public bool MoveNext() + { + if (!_value.HasValue || _index > _value.Length) + { + Current = default(StringSegment); + return false; + } + + var next = _value.IndexOfAny(_separators, _index); + if (next == -1) + { + // No separator found. Consume the remainder of the string. + next = _value.Length; + } + + Current = _value.Subsegment(_index, next - _index); + _index = next + 1; + + return true; + } + + public void Reset() + { + Current = default(StringSegment); + _index = 0; + } + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Primitives/src/StringValues.cs b/src/libraries/Microsoft.Extensions.Primitives/src/StringValues.cs new file mode 100644 index 00000000000000..3417ac73f720f4 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Primitives/src/StringValues.cs @@ -0,0 +1,824 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using Microsoft.DotNet.PlatformAbstractions; + +namespace Microsoft.Extensions.Primitives +{ + /// + /// Represents zero/null, one, or many strings in an efficient way. + /// + public readonly struct StringValues : IList, IReadOnlyList, IEquatable, IEquatable, IEquatable + { + /// + /// A readonly instance of the struct whose value is an empty string array. + /// + /// + /// In application code, this field is most commonly used to safely represent a that has null string values. + /// + public static readonly StringValues Empty = new StringValues(Array.Empty()); + + private readonly object _values; + + /// + /// Initializes a new instance of the structure using the specified string. + /// + /// A string value. + public StringValues(string value) + { + _values = value; + } + + /// + /// Initializes a new instance of the structure using the specified array of strings. + /// + /// A string array. + public StringValues(string[] values) + { + _values = values; + } + + /// + /// Defines an implicit conversion of a given string to a . + /// + /// A string to implicitly convert. + public static implicit operator StringValues(string value) + { + return new StringValues(value); + } + + /// + /// Defines an implicit conversion of a given string array to a . + /// + /// A string array to implicitly convert. + public static implicit operator StringValues(string[] values) + { + return new StringValues(values); + } + + /// + /// Defines an implicit conversion of a given to a string, with multiple values joined as a comma separated string. + /// + /// + /// Returns null where has been initialized from an empty string array or is . + /// + /// A to implicitly convert. + public static implicit operator string (StringValues values) + { + return values.GetStringValue(); + } + + /// + /// Defines an implicit conversion of a given to a string array. + /// + /// A to implicitly convert. + public static implicit operator string[] (StringValues value) + { + return value.GetArrayValue(); + } + + /// + /// Gets the number of elements contained in this . + /// + public int Count + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + // Take local copy of _values so type checks remain valid even if the StringValues is overwritten in memory + var value = _values; + if (value is string) + { + return 1; + } + if (value is null) + { + return 0; + } + else + { + // Not string, not null, can only be string[] + return Unsafe.As(value).Length; + } + } + } + + bool ICollection.IsReadOnly + { + get { return true; } + } + + /// + /// Gets the at index. + /// + /// The string at the specified index. + /// The zero-based index of the element to get. + /// Set operations are not supported on readonly . + string IList.this[int index] + { + get { return this[index]; } + set { throw new NotSupportedException(); } + } + + /// + /// Gets the at index. + /// + /// The string at the specified index. + /// The zero-based index of the element to get. + public string this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + // Take local copy of _values so type checks remain valid even if the StringValues is overwritten in memory + var value = _values; + if (index == 0 && value is string str) + { + return str; + } + else if (value != null) + { + // Not string, not null, can only be string[] + return Unsafe.As(value)[index]; // may throw + } + else + { + return OutOfBounds(); // throws + } + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static string OutOfBounds() + { + return Array.Empty()[0]; // throws + } + + /// + /// Converts the value of the current object to its equivalent string representation, with multiple values joined as a comma separated string. + /// + /// A string representation of the value of the current object. + public override string ToString() + { + return GetStringValue() ?? string.Empty; + } + + private string GetStringValue() + { + // Take local copy of _values so type checks remain valid even if the StringValues is overwritten in memory + var value = _values; + if (value is string s) + { + return s; + } + else + { + return GetStringValueFromArray(value); + } + + static string GetStringValueFromArray(object value) + { + if (value is null) + { + return null; + } + + Debug.Assert(value is string[]); + // value is not null or string, array, can only be string[] + var values = Unsafe.As(value); + switch (values.Length) + { + case 0: return null; + case 1: return values[0]; + default: return GetJoinedStringValueFromArray(values); + } + } + + static string GetJoinedStringValueFromArray(string[] values) + { + // Calculate final length + var length = 0; + for (var i = 0; i < values.Length; i++) + { + var value = values[i]; + // Skip null and empty values + if (value != null && value.Length > 0) + { + if (length > 0) + { + // Add seperator + length++; + } + + length += value.Length; + } + } +#if NETCOREAPP || NETSTANDARD2_1 + // Create the new string + return string.Create(length, values, (span, strings) => { + var offset = 0; + // Skip null and empty values + for (var i = 0; i < strings.Length; i++) + { + var value = strings[i]; + if (value != null && value.Length > 0) + { + if (offset > 0) + { + // Add seperator + span[offset] = ','; + offset++; + } + + value.AsSpan().CopyTo(span.Slice(offset)); + offset += value.Length; + } + } + }); +#else +#pragma warning disable CS0618 + var sb = new InplaceStringBuilder(length); +#pragma warning restore CS0618 + var hasAdded = false; + // Skip null and empty values + for (var i = 0; i < values.Length; i++) + { + var value = values[i]; + if (value != null && value.Length > 0) + { + if (hasAdded) + { + // Add seperator + sb.Append(','); + } + + sb.Append(value); + hasAdded = true; + } + } + + return sb.ToString(); +#endif + } + } + + /// + /// Creates a string array from the current object. + /// + /// A string array represented by this instance. + /// + /// If the contains a single string internally, it is copied to a new array. + /// If the contains an array internally it returns that array instance. + /// + public string[] ToArray() + { + return GetArrayValue() ?? Array.Empty(); + } + + private string[] GetArrayValue() + { + // Take local copy of _values so type checks remain valid even if the StringValues is overwritten in memory + var value = _values; + if (value is string[] values) + { + return values; + } + else if (value != null) + { + // value not array, can only be string + return new[] { Unsafe.As(value) }; + } + else + { + return null; + } + } + + /// + /// Returns the zero-based index of the first occurrence of an item in the . + /// + /// The string to locate in the . + /// the zero-based index of the first occurrence of within the , if found; otherwise, –1. + int IList.IndexOf(string item) + { + return IndexOf(item); + } + + private int IndexOf(string item) + { + // Take local copy of _values so type checks remain valid even if the StringValues is overwritten in memory + var value = _values; + if (value is string[] values) + { + for (int i = 0; i < values.Length; i++) + { + if (string.Equals(values[i], item, StringComparison.Ordinal)) + { + return i; + } + } + return -1; + } + + if (value != null) + { + // value not array, can only be string + return string.Equals(Unsafe.As(value), item, StringComparison.Ordinal) ? 0 : -1; + } + + return -1; + } + + /// Determines whether a string is in the . + /// The to locate in the . + /// true if item is found in the ; otherwise, false. + bool ICollection.Contains(string item) + { + return IndexOf(item) >= 0; + } + + /// + /// Copies the entire to a string array, starting at the specified index of the target array. + /// + /// The one-dimensional that is the destination of the elements copied from. The must have zero-based indexing. + /// The zero-based index in the destination array at which copying begins. + /// array is null. + /// arrayIndex is less than 0. + /// The number of elements in the source is greater than the available space from arrayIndex to the end of the destination array. + void ICollection.CopyTo(string[] array, int arrayIndex) + { + CopyTo(array, arrayIndex); + } + + private void CopyTo(string[] array, int arrayIndex) + { + // Take local copy of _values so type checks remain valid even if the StringValues is overwritten in memory + var value = _values; + if (value is string[] values) + { + Array.Copy(values, 0, array, arrayIndex, values.Length); + return; + } + + if (value != null) + { + if (array == null) + { + throw new ArgumentNullException(nameof(array)); + } + if (arrayIndex < 0) + { + throw new ArgumentOutOfRangeException(nameof(arrayIndex)); + } + if (array.Length - arrayIndex < 1) + { + throw new ArgumentException( + $"'{nameof(array)}' is not long enough to copy all the items in the collection. Check '{nameof(arrayIndex)}' and '{nameof(array)}' length."); + } + + // value not array, can only be string + array[arrayIndex] = Unsafe.As(value); + } + } + + void ICollection.Add(string item) => throw new NotSupportedException(); + + void IList.Insert(int index, string item) => throw new NotSupportedException(); + + bool ICollection.Remove(string item) => throw new NotSupportedException(); + + void IList.RemoveAt(int index) => throw new NotSupportedException(); + + void ICollection.Clear() => throw new NotSupportedException(); + + /// Retrieves an object that can iterate through the individual strings in this . + /// An enumerator that can be used to iterate through the . + public Enumerator GetEnumerator() + { + return new Enumerator(_values); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Indicates whether the specified contains no string values. + /// + /// The to test. + /// true if value contains a single null string or empty array; otherwise, false. + public static bool IsNullOrEmpty(StringValues value) + { + var data = value._values; + if (data is null) + { + return true; + } + if (data is string[] values) + { + switch (values.Length) + { + case 0: return true; + case 1: return string.IsNullOrEmpty(values[0]); + default: return false; + } + } + else + { + // Not array, can only be string + return string.IsNullOrEmpty(Unsafe.As(data)); + } + } + + /// + /// Concatenates two specified instances of . + /// + /// The first to concatenate. + /// The second to concatenate. + /// The concatenation of and . + public static StringValues Concat(StringValues values1, StringValues values2) + { + var count1 = values1.Count; + var count2 = values2.Count; + + if (count1 == 0) + { + return values2; + } + + if (count2 == 0) + { + return values1; + } + + var combined = new string[count1 + count2]; + values1.CopyTo(combined, 0); + values2.CopyTo(combined, count1); + return new StringValues(combined); + } + + /// + /// Concatenates specified instance of with specified . + /// + /// The to concatenate. + /// The to concatenate. + /// The concatenation of and . + public static StringValues Concat(in StringValues values, string value) + { + if (value == null) + { + return values; + } + + var count = values.Count; + if (count == 0) + { + return new StringValues(value); + } + + var combined = new string[count + 1]; + values.CopyTo(combined, 0); + combined[count] = value; + return new StringValues(combined); + } + + /// + /// Concatenates specified instance of with specified . + /// + /// The to concatenate. + /// The to concatenate. + /// The concatenation of and . + public static StringValues Concat(string value, in StringValues values) + { + if (value == null) + { + return values; + } + + var count = values.Count; + if (count == 0) + { + return new StringValues(value); + } + + var combined = new string[count + 1]; + combined[0] = value; + values.CopyTo(combined, 1); + return new StringValues(combined); + } + + /// + /// Determines whether two specified objects have the same values in the same order. + /// + /// The first to compare. + /// The second to compare. + /// true if the value of is the same as the value of ; otherwise, false. + public static bool Equals(StringValues left, StringValues right) + { + var count = left.Count; + + if (count != right.Count) + { + return false; + } + + for (var i = 0; i < count; i++) + { + if (left[i] != right[i]) + { + return false; + } + } + + return true; + } + + /// + /// Determines whether two specified have the same values. + /// + /// The first to compare. + /// The second to compare. + /// true if the value of is the same as the value of ; otherwise, false. + public static bool operator ==(StringValues left, StringValues right) + { + return Equals(left, right); + } + + /// + /// Determines whether two specified have different values. + /// + /// The first to compare. + /// The second to compare. + /// true if the value of is different to the value of ; otherwise, false. + public static bool operator !=(StringValues left, StringValues right) + { + return !Equals(left, right); + } + + /// + /// Determines whether this instance and another specified object have the same values. + /// + /// The string to compare to this instance. + /// true if the value of is the same as the value of this instance; otherwise, false. + public bool Equals(StringValues other) => Equals(this, other); + + /// + /// Determines whether the specified and objects have the same values. + /// + /// The to compare. + /// The to compare. + /// true if the value of is the same as the value of ; otherwise, false. If is null, the method returns false. + public static bool Equals(string left, StringValues right) => Equals(new StringValues(left), right); + + /// + /// Determines whether the specified and objects have the same values. + /// + /// The to compare. + /// The to compare. + /// true if the value of is the same as the value of ; otherwise, false. If is null, the method returns false. + public static bool Equals(StringValues left, string right) => Equals(left, new StringValues(right)); + + /// + /// Determines whether this instance and a specified , have the same value. + /// + /// The to compare to this instance. + /// true if the value of is the same as this instance; otherwise, false. If is null, returns false. + public bool Equals(string other) => Equals(this, new StringValues(other)); + + /// + /// Determines whether the specified string array and objects have the same values. + /// + /// The string array to compare. + /// The to compare. + /// true if the value of is the same as the value of ; otherwise, false. + public static bool Equals(string[] left, StringValues right) => Equals(new StringValues(left), right); + + /// + /// Determines whether the specified and string array objects have the same values. + /// + /// The to compare. + /// The string array to compare. + /// true if the value of is the same as the value of ; otherwise, false. + public static bool Equals(StringValues left, string[] right) => Equals(left, new StringValues(right)); + + /// + /// Determines whether this instance and a specified string array have the same values. + /// + /// The string array to compare to this instance. + /// true if the value of is the same as this instance; otherwise, false. + public bool Equals(string[] other) => Equals(this, new StringValues(other)); + + /// + public static bool operator ==(StringValues left, string right) => Equals(left, new StringValues(right)); + + /// + /// Determines whether the specified and objects have different values. + /// + /// The to compare. + /// The to compare. + /// true if the value of is different to the value of ; otherwise, false. + public static bool operator !=(StringValues left, string right) => !Equals(left, new StringValues(right)); + + /// + public static bool operator ==(string left, StringValues right) => Equals(new StringValues(left), right); + + /// + /// Determines whether the specified and objects have different values. + /// + /// The to compare. + /// The to compare. + /// true if the value of is different to the value of ; otherwise, false. + public static bool operator !=(string left, StringValues right) => !Equals(new StringValues(left), right); + + /// + public static bool operator ==(StringValues left, string[] right) => Equals(left, new StringValues(right)); + + /// + /// Determines whether the specified and string array have different values. + /// + /// The to compare. + /// The string array to compare. + /// true if the value of is different to the value of ; otherwise, false. + public static bool operator !=(StringValues left, string[] right) => !Equals(left, new StringValues(right)); + + /// + public static bool operator ==(string[] left, StringValues right) => Equals(new StringValues(left), right); + + /// + /// Determines whether the specified string array and have different values. + /// + /// The string array to compare. + /// The to compare. + /// true if the value of is different to the value of ; otherwise, false. + public static bool operator !=(string[] left, StringValues right) => !Equals(new StringValues(left), right); + + /// + /// Determines whether the specified and , which must be a + /// , , or array of , have the same value. + /// + /// The to compare. + /// The to compare. + /// true if the object is equal to the ; otherwise, false. + public static bool operator ==(StringValues left, object right) => left.Equals(right); + + /// + /// Determines whether the specified and , which must be a + /// , , or array of , have different values. + /// + /// The to compare. + /// The to compare. + /// true if the object is equal to the ; otherwise, false. + public static bool operator !=(StringValues left, object right) => !left.Equals(right); + + /// + /// Determines whether the specified , which must be a + /// , , or array of , and specified , have the same value. + /// + /// The to compare. + /// The to compare. + /// true if the object is equal to the ; otherwise, false. + public static bool operator ==(object left, StringValues right) => right.Equals(left); + + /// + /// Determines whether the specified and object have the same values. + /// + /// The to compare. + /// The to compare. + /// true if the object is equal to the ; otherwise, false. + public static bool operator !=(object left, StringValues right) => !right.Equals(left); + + /// + /// Determines whether this instance and a specified object have the same value. + /// + /// An object to compare with this object. + /// true if the current object is equal to ; otherwise, false. + public override bool Equals(object obj) + { + if (obj == null) + { + return Equals(this, StringValues.Empty); + } + + if (obj is string) + { + return Equals(this, (string)obj); + } + + if (obj is string[]) + { + return Equals(this, (string[])obj); + } + + if (obj is StringValues) + { + return Equals(this, (StringValues)obj); + } + + return false; + } + + /// + public override int GetHashCode() + { + var value = _values; + if (value is string[] values) + { + if (Count == 1) + { + return Unsafe.As(this[0])?.GetHashCode() ?? Count.GetHashCode(); + } + var hcc = new HashCodeCombiner(); + for (var i = 0; i < values.Length; i++) + { + hcc.Add(values[i]); + } + return hcc.CombinedHash; + } + else + { + return Unsafe.As(value)?.GetHashCode() ?? Count.GetHashCode(); + } + } + + /// + /// Enumerates the string values of a . + /// + public struct Enumerator : IEnumerator + { + private readonly string[] _values; + private string _current; + private int _index; + + internal Enumerator(object value) + { + if (value is string str) + { + _values = null; + _current = str; + } + else + { + _current = null; + _values = Unsafe.As(value); + } + _index = 0; + } + + public Enumerator(ref StringValues values) : this(values._values) + { } + + public bool MoveNext() + { + var index = _index; + if (index < 0) + { + return false; + } + + var values = _values; + if (values != null) + { + if ((uint)index < (uint)values.Length) + { + _index = index + 1; + _current = values[index]; + return true; + } + + _index = -1; + return false; + } + + _index = -1; // sentinel value + return _current != null; + } + + public string Current => _current; + + object IEnumerator.Current => _current; + + void IEnumerator.Reset() + { + throw new NotSupportedException(); + } + + public void Dispose() + { + } + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Primitives/src/ThrowHelper.cs b/src/libraries/Microsoft.Extensions.Primitives/src/ThrowHelper.cs new file mode 100644 index 00000000000000..a3ea5ac13b59a0 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Primitives/src/ThrowHelper.cs @@ -0,0 +1,98 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics; + +namespace Microsoft.Extensions.Primitives +{ + internal static class ThrowHelper + { + internal static void ThrowArgumentNullException(ExceptionArgument argument) + { + throw new ArgumentNullException(GetArgumentName(argument)); + } + + internal static void ThrowArgumentOutOfRangeException(ExceptionArgument argument) + { + throw new ArgumentOutOfRangeException(GetArgumentName(argument)); + } + + internal static void ThrowArgumentException(ExceptionResource resource) + { + throw new ArgumentException(GetResourceText(resource)); + } + + internal static void ThrowInvalidOperationException(ExceptionResource resource) + { + throw new InvalidOperationException(GetResourceText(resource)); + } + + internal static void ThrowInvalidOperationException(ExceptionResource resource, params object[] args) + { + var message = string.Format(GetResourceText(resource), args); + + throw new InvalidOperationException(message); + } + + internal static ArgumentNullException GetArgumentNullException(ExceptionArgument argument) + { + return new ArgumentNullException(GetArgumentName(argument)); + } + + internal static ArgumentOutOfRangeException GetArgumentOutOfRangeException(ExceptionArgument argument) + { + return new ArgumentOutOfRangeException(GetArgumentName(argument)); + } + + internal static ArgumentException GetArgumentException(ExceptionResource resource) + { + return new ArgumentException(GetResourceText(resource)); + } + + private static string GetResourceText(ExceptionResource resource) + { + return SR.GetResourceString(GetResourceName(resource)); + } + + private static string GetArgumentName(ExceptionArgument argument) + { + Debug.Assert(Enum.IsDefined(typeof(ExceptionArgument), argument), + "The enum value is not defined, please check the ExceptionArgument Enum."); + + return argument.ToString(); + } + + private static string GetResourceName(ExceptionResource resource) + { + Debug.Assert(Enum.IsDefined(typeof(ExceptionResource), resource), + "The enum value is not defined, please check the ExceptionResource Enum."); + + return resource.ToString(); + } + } + + internal enum ExceptionArgument + { + buffer, + offset, + length, + text, + start, + count, + index, + value, + capacity, + separators + } + + internal enum ExceptionResource + { + Argument_InvalidOffsetLength, + Argument_InvalidOffsetLengthStringSegment, + Capacity_CannotChangeAfterWriteStarted, + Capacity_NotEnough, + Capacity_NotUsedEntirely + } +} diff --git a/src/libraries/Microsoft.Extensions.Primitives/tests/ChangeTokenTest.cs b/src/libraries/Microsoft.Extensions.Primitives/tests/ChangeTokenTest.cs new file mode 100644 index 00000000000000..95b805cf74add9 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Primitives/tests/ChangeTokenTest.cs @@ -0,0 +1,316 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Threading; +using Xunit; + +namespace Microsoft.Extensions.Primitives +{ + public class ChangeTokenTests + { + public class TestChangeToken : IChangeToken + { + private Action _callback; + + public bool ActiveChangeCallbacks { get; set; } + public bool HasChanged { get; set; } + + public IDisposable RegisterChangeCallback(Action callback, object state) + { + _callback = () => callback(state); + return null; + } + + public void Changed() + { + HasChanged = true; + _callback(); + } + } + + [Fact] + public void HasChangeFiresChange() + { + var token = new TestChangeToken(); + bool fired = false; + ChangeToken.OnChange(() => token, () => fired = true); + Assert.False(fired); + token.Changed(); + Assert.True(fired); + } + + [Fact] + public void ChangesFireAfterExceptions() + { + TestChangeToken token = null; + var count = 0; + ChangeToken.OnChange(() => token = new TestChangeToken(), () => + { + count++; + throw new Exception(); + }); + Assert.Throws(() => token.Changed()); + Assert.Equal(1, count); + Assert.Throws(() => token.Changed()); + Assert.Equal(2, count); + } + + [Fact] + public void HasChangeFiresChangeWithState() + { + var token = new TestChangeToken(); + object state = new object(); + object callbackState = null; + ChangeToken.OnChange(() => token, s => callbackState = s, state); + Assert.Null(callbackState); + token.Changed(); + Assert.Equal(state, callbackState); + } + + [Fact] + public void ChangesFireAfterExceptionsWithState() + { + TestChangeToken token = null; + var count = 0; + object state = new object(); + object callbackState = null; + ChangeToken.OnChange(() => token = new TestChangeToken(), s => + { + callbackState = s; + count++; + throw new Exception(); + }, state); + Assert.Throws(() => token.Changed()); + Assert.Equal(1, count); + Assert.NotNull(callbackState); + Assert.Throws(() => token.Changed()); + Assert.Equal(2, count); + Assert.NotNull(callbackState); + } + + [Fact] + public void AsyncLocalsNotCapturedAndRestored() + { + // Capture clean context + var executionContext = ExecutionContext.Capture(); + + var cancellationTokenSource = new CancellationTokenSource(); + var cancellationToken = cancellationTokenSource.Token; + var cancellationChangeToken = new CancellationChangeToken(cancellationToken); + var executed = false; + + // Set AsyncLocal + var asyncLocal = new AsyncLocal(); + asyncLocal.Value = 1; + + // Register Callback + cancellationChangeToken.RegisterChangeCallback(al => + { + // AsyncLocal not set, when run on clean context + // A suppressed flow runs in current context, rather than restoring the captured context + Assert.Equal(0, ((AsyncLocal)al).Value); + executed = true; + }, asyncLocal); + + // AsyncLocal should still be set + Assert.Equal(1, asyncLocal.Value); + + // Check AsyncLocal is not restored by running on clean context + ExecutionContext.Run(executionContext, cts => ((CancellationTokenSource)cts).Cancel(), cancellationTokenSource); + + // AsyncLocal should still be set + Assert.Equal(1, asyncLocal.Value); + Assert.True(executed); + } + + [Fact] + public void DisposingChangeTokenRegistrationDoesNotRaiseConsumerCallback() + { + var provider = new ResettableChangeTokenProvider(); + var count = 0; + var reg = ChangeToken.OnChange(provider.GetChangeToken, () => + { + count++; + }); + + for (int i = 0; i < 5; i++) + { + provider.Changed(); + } + + Assert.Equal(5, count); + + reg.Dispose(); + + for (int i = 0; i < 5; i++) + { + provider.Changed(); + } + + Assert.Equal(5, count); + } + + [Fact] + public void DisposingChangeTokenRegistrationDoesNotRaiseConsumerCallbackStateOverload() + { + var provider = new ResettableChangeTokenProvider(); + var count = 0; + var reg = ChangeToken.OnChange(provider.GetChangeToken, state => + { + count++; + }, + null); + + for (int i = 0; i < 5; i++) + { + provider.Changed(); + } + + Assert.Equal(5, count); + + reg.Dispose(); + + for (int i = 0; i < 5; i++) + { + provider.Changed(); + } + + Assert.Equal(5, count); + } + + [Fact] + public void DisposingChangeTokenRegistrationDuringCallbackWorks() + { + var provider = new ResettableChangeTokenProvider(); + var count = 0; + + IDisposable reg = null; + + reg = ChangeToken.OnChange(provider.GetChangeToken, state => + { + count++; + reg.Dispose(); + }, + null); + + provider.Changed(); + + Assert.Equal(1, count); + + provider.Changed(); + + Assert.Equal(1, count); + } + + [Fact] + public void DoubleDisposeDisposesOnce() + { + var provider = new TrackableChangeTokenProvider(); + var count = 0; + + IDisposable reg = null; + + reg = ChangeToken.OnChange(provider.GetChangeToken, state => + { + count++; + reg.Dispose(); + }, + null); + + provider.Changed(); + + Assert.Equal(1, count); + Assert.Equal(1, provider.RegistrationCalls); + Assert.Equal(1, provider.DisposeCalls); + + reg.Dispose(); + + provider.Changed(); + + Assert.Equal(1, count); + Assert.Equal(2, provider.RegistrationCalls); + Assert.Equal(2, provider.DisposeCalls); + } + + public class TrackableChangeTokenProvider + { + private TrackableChangeToken _cts = new TrackableChangeToken(); + + public int RegistrationCalls { get; set; } + public int DisposeCalls { get; set; } + + public IChangeToken GetChangeToken() => _cts; + + public void Changed() + { + var previous = _cts; + _cts = new TrackableChangeToken(); + previous.Execute(); + + RegistrationCalls += previous.RegistrationCalls; + DisposeCalls += previous.DisposeCalls; + } + } + + public class TrackableChangeToken : IChangeToken + { + private CancellationTokenSource _cts = new CancellationTokenSource(); + + public int RegistrationCalls { get; set; } + public int DisposeCalls { get; set; } + + public bool HasChanged => _cts.IsCancellationRequested; + + public bool ActiveChangeCallbacks => true; + + public void Execute() + { + _cts.Cancel(); + } + + public IDisposable RegisterChangeCallback(Action callback, object state) + { + var registration = _cts.Token.Register(callback, state); + RegistrationCalls++; + + return new DisposableAction(() => + { + DisposeCalls++; + registration.Dispose(); + }); + } + + private class DisposableAction : IDisposable + { + private Action _action; + + public DisposableAction(Action action) + { + _action = action; + } + + public void Dispose() + { + _action?.Invoke(); + } + } + } + + public class ResettableChangeTokenProvider + { + private CancellationTokenSource _cts = new CancellationTokenSource(); + + public IChangeToken GetChangeToken() => new CancellationChangeToken(_cts.Token); + + public void Changed() + { + var previous = _cts; + _cts = new CancellationTokenSource(); + previous.Cancel(); + } + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Primitives/tests/CompositeChangeTokenTest.cs b/src/libraries/Microsoft.Extensions.Primitives/tests/CompositeChangeTokenTest.cs new file mode 100644 index 00000000000000..ce933840199c9d --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Primitives/tests/CompositeChangeTokenTest.cs @@ -0,0 +1,149 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Primitives +{ + public class CompositeChangeTokenTest + { + [Fact] + public void RegisteredCallbacks_AreInvokedExactlyOnce() + { + // Arrange + var firstCancellationTokenSource = new CancellationTokenSource(); + var secondCancellationTokenSource = new CancellationTokenSource(); + var thirdCancellationTokenSource = new CancellationTokenSource(); + var firstCancellationToken = firstCancellationTokenSource.Token; + var secondCancellationToken = secondCancellationTokenSource.Token; + var thirdCancellationToken = thirdCancellationTokenSource.Token; + + var firstCancellationChangeToken = new CancellationChangeToken(firstCancellationToken); + var secondCancellationChangeToken = new CancellationChangeToken(secondCancellationToken); + var thirdCancellationChangeToken = new CancellationChangeToken(thirdCancellationToken); + + var compositeChangeToken = new CompositeChangeToken(new List { firstCancellationChangeToken, secondCancellationChangeToken, thirdCancellationChangeToken }); + var count1 = 0; + var count2 = 0; + compositeChangeToken.RegisterChangeCallback(_ => count1++, null); + compositeChangeToken.RegisterChangeCallback(_ => count2++, null); + + // Act + firstCancellationTokenSource.Cancel(); + secondCancellationTokenSource.Cancel(); + + // Assert + Assert.Equal(1, count1); + Assert.Equal(1, count2); + } + + [Fact] + public void HasChanged_IsTrue_IfAnyTokenHasChanged() + { + // Arrange + var firstChangeToken = new Mock(); + var secondChangeToken = new Mock(); + var thirdChangeToken = new Mock(); + + secondChangeToken.Setup(t => t.HasChanged).Returns(true); + + // Act + var compositeChangeToken = new CompositeChangeToken(new List { firstChangeToken.Object, secondChangeToken.Object, thirdChangeToken.Object }); + + // Assert + Assert.True(compositeChangeToken.HasChanged); + } + + [Fact] + public void HasChanged_IsFalse_IfNoTokenHasChanged() + { + // Arrange + var firstChangeToken = new Mock(); + var secondChangeToken = new Mock(); + + // Act + var compositeChangeToken = new CompositeChangeToken(new List { firstChangeToken.Object, secondChangeToken.Object }); + + // Assert + Assert.False(compositeChangeToken.HasChanged); + } + + [Fact] + public void ActiveChangeCallbacks_IsTrue_IfAnyTokenHasActiveChangeCallbacks() + { + // Arrange + var firstChangeToken = new Mock(); + var secondChangeToken = new Mock(); + var thirdChangeToken = new Mock(); + + secondChangeToken.Setup(t => t.ActiveChangeCallbacks).Returns(true); + + var compositeChangeToken = new CompositeChangeToken(new List { firstChangeToken.Object, secondChangeToken.Object, thirdChangeToken.Object }); + + // Act & Assert + Assert.True(compositeChangeToken.ActiveChangeCallbacks); + } + + [Fact] + public void ActiveChangeCallbacks_IsFalse_IfNoTokenHasActiveChangeCallbacks() + { + // Arrange + var firstChangeToken = new Mock(); + var secondChangeToken = new Mock(); + + var compositeChangeToken = new CompositeChangeToken(new List { firstChangeToken.Object, secondChangeToken.Object }); + + // Act & Assert + Assert.False(compositeChangeToken.ActiveChangeCallbacks); + } + + [Fact] + public async Task RegisteredCallbackGetsInvokedExactlyOnce_WhenMultipleConcurrentChangeEventsOccur() + { + // Arrange + var event1 = new ManualResetEvent(false); + var event2 = new ManualResetEvent(false); + var event3 = new ManualResetEvent(false); + + var cancellationTokenSource = new CancellationTokenSource(); + var cancellationToken = cancellationTokenSource.Token; + var cancellationChangeToken = new CancellationChangeToken(cancellationToken); + var count = 0; + Action callback = _ => + { + count++; + event3.Set(); + event1.WaitOne(5000); + }; + + var compositeChangeToken = new CompositeChangeToken(new List { cancellationChangeToken }); + compositeChangeToken.RegisterChangeCallback(callback, null); + + // Act + var firstChange = Task.Run(() => + { + event2.WaitOne(5000); + cancellationTokenSource.Cancel(); + }); + var secondChange = Task.Run(() => + { + event3.WaitOne(5000); + cancellationTokenSource.Cancel(); + event1.Set(); + }); + + event2.Set(); + + await Task.WhenAll(firstChange, secondChange); + + // Assert + Assert.Equal(1, count); + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Primitives/tests/InplaceStringBuilderTest.cs b/src/libraries/Microsoft.Extensions.Primitives/tests/InplaceStringBuilderTest.cs new file mode 100644 index 00000000000000..03773e82b85752 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Primitives/tests/InplaceStringBuilderTest.cs @@ -0,0 +1,144 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.Extensions.Primitives; +using Xunit; + +// InplaceStringBuilder is obsolete +#pragma warning disable CS0618 + +namespace Microsoft.Extensions.Primitives +{ + public class InplaceStringBuilderTest + { + [Fact] + public void Ctor_ThrowsIfCapacityIsNegative() + { + Assert.Throws(() => new InplaceStringBuilder(-1)); + } + + [Fact] + public void ToString_ReturnsStringWithAllAppendedValues() + { + var s1 = "123"; + var c1 = '4'; + var s2 = "56789"; + var seg = new StringSegment("890123", 2, 2); + + var formatter = new InplaceStringBuilder(); + formatter.Capacity += s1.Length + 1 + s2.Length + seg.Length; + formatter.Append(s1); + formatter.Append(c1); + formatter.Append(s2, 0, 2); + formatter.Append(s2, 2, 2); + formatter.Append(s2, 4, 1); + formatter.Append(seg); + Assert.Equal("12345678901", formatter.ToString()); + } + + [Fact] + public void ToString_ThrowsIfCapacityNotUsed() + { + var formatter = new InplaceStringBuilder(10); + + formatter.Append("abc"); + + var exception = Assert.Throws(() => formatter.ToString()); + Assert.Equal("Entire reserved capacity was not used. Capacity: '10', written '3'.", exception.Message); + } + + [Fact] + public void Build_ThrowsIfNotEnoughWritten() + { + var formatter = new InplaceStringBuilder(5); + formatter.Append("123"); + var exception = Assert.Throws(() => formatter.ToString()); + Assert.Equal("Entire reserved capacity was not used. Capacity: '5', written '3'.", exception.Message); + } + + [Fact] + public void Capacity_ThrowsIfAppendWasCalled() + { + var formatter = new InplaceStringBuilder(3); + formatter.Append("123"); + + var exception = Assert.Throws(() => formatter.Capacity = 5); + Assert.Equal("Cannot change capacity after write started.", exception.Message); + } + + [Fact] + public void Capacity_ThrowsIfNegativeValueSet() + { + var formatter = new InplaceStringBuilder(3); + + Assert.Throws(() => formatter.Capacity = -1); + } + + [Fact] + public void Capacity_GetReturnsCorrectValue() + { + var formatter = new InplaceStringBuilder(3); + Assert.Equal(3, formatter.Capacity); + + formatter.Capacity = 10; + Assert.Equal(10, formatter.Capacity); + + formatter.Append("abc"); + Assert.Equal(10, formatter.Capacity); + } + + [Fact] + public void Append_ThrowsIfValueIsNull() + { + var formatter = new InplaceStringBuilder(3); + + Assert.Throws(() => formatter.Append(null as string)); + } + + [Fact] + public void Append_ThrowsIfValueIsNullInOverloadWithIndex() + { + var formatter = new InplaceStringBuilder(3); + + Assert.Throws(() => formatter.Append(null as string, 0, 3)); + } + + [Fact] + public void Append_ThrowsIfOffsetIsNegative() + { + var formatter = new InplaceStringBuilder(3); + + Assert.Throws(() => formatter.Append("abc", -1, 3)); + } + + [Fact] + public void Append_ThrowIfValueLenghtMinusOffsetSmallerThanCount() + { + var formatter = new InplaceStringBuilder(3); + + Assert.Throws(() => formatter.Append("abc", 1, 3)); + } + + [Fact] + public void Append_ThrowsIfNotEnoughCapacity() + { + var formatter = new InplaceStringBuilder(1); + + var exception = Assert.Throws(() => formatter.Append("123")); + Assert.Equal("Not enough capacity to write '3' characters, only '1' left.", exception.Message); + } + + [Fact] + public void Append_ThrowsWhenNoCapacityIsSet() + { + var formatter = new InplaceStringBuilder(); + + var exception = Assert.Throws(() => formatter.Append("123")); + Assert.Equal("Not enough capacity to write '3' characters, only '0' left.", exception.Message); + } + } +} + +#pragma warning restore CS0618 \ No newline at end of file diff --git a/src/libraries/Microsoft.Extensions.Primitives/tests/Microsoft.Extensions.Primitives.Tests.csproj b/src/libraries/Microsoft.Extensions.Primitives/tests/Microsoft.Extensions.Primitives.Tests.csproj new file mode 100644 index 00000000000000..d9b69e7ce29fa5 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Primitives/tests/Microsoft.Extensions.Primitives.Tests.csproj @@ -0,0 +1,25 @@ + + + + $(DefaultNetCoreTargetFramework);net472 + true + + + + + + + + + + + + + + + + + + + + diff --git a/src/libraries/Microsoft.Extensions.Primitives/tests/StringSegmentTest.cs b/src/libraries/Microsoft.Extensions.Primitives/tests/StringSegmentTest.cs new file mode 100644 index 00000000000000..1f072612e2c0cf --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Primitives/tests/StringSegmentTest.cs @@ -0,0 +1,1174 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Xunit; + +namespace Microsoft.Extensions.Primitives +{ + public class StringSegmentTest + { + [Fact] + public void StringSegment_Empty() + { + // Arrange & Act + var segment = StringSegment.Empty; + + // Assert + Assert.True(segment.HasValue); + Assert.Same(string.Empty, segment.Value); + Assert.Equal(0, segment.Offset); + Assert.Equal(0, segment.Length); + } + + [Fact] + public void StringSegment_ImplicitConvertFromString() + { + StringSegment segment = "Hello"; + + Assert.True(segment.HasValue); + Assert.Equal(0, segment.Offset); + Assert.Equal(5, segment.Length); + Assert.Equal("Hello", segment.Value); + } + + [Fact] + public void StringSegment_AsSpan() + { + var segment = new StringSegment("Hello"); + + var span = segment.AsSpan(); + + Assert.Equal(5, span.Length); + } + + [Fact] + public void StringSegment_ImplicitConvertToSpan() + { + ReadOnlySpan span = new StringSegment("Hello"); + + Assert.Equal(5, span.Length); + } + + [Fact] + public void StringSegment_AsMemory() + { + var segment = new StringSegment("Hello"); + + var memory = segment.AsMemory(); + + Assert.Equal(5, memory.Length); + } + + [Fact] + public void StringSegment_ImplicitConvertToMemory() + { + ReadOnlyMemory memory = new StringSegment("Hello"); + + Assert.Equal(5, memory.Length); + } + + [Fact] + public void StringSegment_StringCtor_AllowsNullBuffers() + { + // Arrange & Act + var segment = new StringSegment(null); + + // Assert + Assert.False(segment.HasValue); + Assert.Equal(0, segment.Offset); + Assert.Equal(0, segment.Length); + } + + [Fact] + public void StringSegmentConstructor_NullBuffer_Throws() + { + // Arrange, Act and Assert + var exception = Assert.Throws(() => new StringSegment(null, 0, 0)); + Assert.Contains("buffer", exception.Message); + } + + [Fact] + public void StringSegmentConstructor_NegativeOffset_Throws() + { + // Arrange, Act and Assert + var exception = Assert.Throws(() => new StringSegment("", -1, 0)); + Assert.Contains("offset", exception.Message); + } + + [Fact] + public void StringSegmentConstructor_NegativeLength_Throws() + { + // Arrange, Act and Assert + var exception = Assert.Throws(() => new StringSegment("", 0, -1)); + Assert.Contains("length", exception.Message); + } + + [Theory] + [InlineData(0, 10)] + [InlineData(10, 0)] + [InlineData(5, 5)] + [InlineData(int.MaxValue, int.MaxValue)] + public void StringSegmentConstructor_OffsetOrLengthOutOfBounds_Throws(int offset, int length) + { + // Arrange, Act and Assert + Assert.Throws(() => new StringSegment("lengthof9", offset, length)); + } + + [Theory] + [InlineData("", 0, 0)] + [InlineData("abc", 2, 0)] + public void StringSegmentConstructor_AllowsEmptyBuffers(string text, int offset, int length) + { + // Arrange & Act + var segment = new StringSegment(text, offset, length); + + // Assert + Assert.True(segment.HasValue); + Assert.Equal(offset, segment.Offset); + Assert.Equal(length, segment.Length); + } + + [Fact] + public void StringSegment_StringCtor_InitializesValuesCorrectly() + { + // Arrange + var buffer = "Hello world!"; + + // Act + var segment = new StringSegment(buffer); + + // Assert + Assert.True(segment.HasValue); + Assert.Equal(0, segment.Offset); + Assert.Equal(buffer.Length, segment.Length); + } + + [Fact] + public void StringSegment_Value_Valid() + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 4); + + // Act + var value = segment.Value; + + // Assert + Assert.Equal("ello", value); + } + + [Fact] + public void StringSegment_Value_Invalid() + { + // Arrange + var segment = new StringSegment(); + + // Act + var value = segment.Value; + + // Assert + Assert.Null(value); + } + + [Fact] + public void StringSegment_HasValue_Valid() + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 4); + + // Act + var hasValue = segment.HasValue; + + // Assert + Assert.True(hasValue); + } + + [Fact] + public void StringSegment_HasValue_Invalid() + { + // Arrange + var segment = new StringSegment(); + + // Act + var hasValue = segment.HasValue; + + // Assert + Assert.False(hasValue); + } + + [Theory] + [InlineData("a", 0, 1, 0, 'a')] + [InlineData("abc", 1, 1, 0, 'b')] + [InlineData("abcdef", 1, 4, 0, 'b')] + [InlineData("abcdef", 1, 4, 1, 'c')] + [InlineData("abcdef", 1, 4, 2, 'd')] + [InlineData("abcdef", 1, 4, 3, 'e')] + public void StringSegment_Indexer_InRange(string value, int offset, int length, int index, char expected) + { + var segment = new StringSegment(value, offset, length); + + var result = segment[index]; + + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("", 0, 0, 0)] + [InlineData("a", 0, 1, -1)] + [InlineData("a", 0, 1, 1)] + public void StringSegment_Indexer_OutOfRangeThrows(string value, int offset, int length, int index) + { + var segment = new StringSegment(value, offset, length); + + Assert.Throws(() => segment[index]); + } + + public static TheoryData EndsWithData + { + get + { + // candidate / comparer / expected result + return new TheoryData() + { + { "Hello", StringComparison.Ordinal, false }, + { "ello ", StringComparison.Ordinal, false }, + { "ll", StringComparison.Ordinal, false }, + { "ello", StringComparison.Ordinal, true }, + { "llo", StringComparison.Ordinal, true }, + { "lo", StringComparison.Ordinal, true }, + { "o", StringComparison.Ordinal, true }, + { string.Empty, StringComparison.Ordinal, true }, + { "eLLo", StringComparison.Ordinal, false }, + { "eLLo", StringComparison.OrdinalIgnoreCase, true }, + }; + } + } + + [Theory] + [MemberData(nameof(EndsWithData))] + public void StringSegment_EndsWith_Valid(string candidate, StringComparison comparison, bool expectedResult) + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 4); + + // Act + var result = segment.EndsWith(candidate, comparison); + + // Assert + Assert.Equal(expectedResult, result); + } + + [Fact] + public void StringSegment_EndsWith_Invalid() + { + // Arrange + var segment = new StringSegment(); + + // Act + var result = segment.EndsWith(string.Empty, StringComparison.Ordinal); + + // Assert + Assert.False(result); + } + + public static TheoryData StartsWithData + { + get + { + // candidate / comparer / expected result + return new TheoryData() + { + { "Hello", StringComparison.Ordinal, false }, + { "ello ", StringComparison.Ordinal, false }, + { "ll", StringComparison.Ordinal, false }, + { "ello", StringComparison.Ordinal, true }, + { "ell", StringComparison.Ordinal, true }, + { "el", StringComparison.Ordinal, true }, + { "e", StringComparison.Ordinal, true }, + { string.Empty, StringComparison.Ordinal, true }, + { "eLLo", StringComparison.Ordinal, false }, + { "eLLo", StringComparison.OrdinalIgnoreCase, true }, + }; + } + } + + [Theory] + [MemberData(nameof(StartsWithData))] + public void StringSegment_StartsWith_Valid(string candidate, StringComparison comparison, bool expectedResult) + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 4); + + // Act + var result = segment.StartsWith(candidate, comparison); + + // Assert + Assert.Equal(expectedResult, result); + } + + [Fact] + public void StringSegment_StartsWith_Invalid() + { + // Arrange + var segment = new StringSegment(); + + // Act + var result = segment.StartsWith(string.Empty, StringComparison.Ordinal); + + // Assert + Assert.False(result); + } + + public static TheoryData EqualsStringData + { + get + { + // candidate / comparer / expected result + return new TheoryData() + { + { "eLLo", StringComparison.OrdinalIgnoreCase, true }, + { "eLLo", StringComparison.Ordinal, false }, + }; + } + } + + [Theory] + [MemberData(nameof(EqualsStringData))] + public void StringSegment_Equals_String_Valid(string candidate, StringComparison comparison, bool expectedResult) + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 4); + + // Act + var result = segment.Equals(candidate, comparison); + + // Assert + Assert.Equal(expectedResult, result); + } + + [Fact] + public void StringSegment_EqualsObject_Valid() + { + var segment1 = new StringSegment("My Car Is Cool", 3, 3); + var segment2 = new StringSegment("Your Carport is blue", 5, 3); + + Assert.True(segment1.Equals((object)segment2)); + } + + [Fact] + public void StringSegment_EqualsNull_Invalid() + { + var segment1 = new StringSegment("My Car Is Cool", 3, 3); + + Assert.False(segment1.Equals(null as object)); + } + + [Fact] + public void StringSegment_StaticEquals_Valid() + { + var segment1 = new StringSegment("My Car Is Cool", 3, 3); + var segment2 = new StringSegment("Your Carport is blue", 5, 3); + + Assert.True(StringSegment.Equals(segment1, segment2)); + } + + [Fact] + public void StringSegment_StaticEquals_Invalid() + { + var segment1 = new StringSegment("My Car Is Cool", 3, 4); + var segment2 = new StringSegment("Your Carport is blue", 5, 4); + + Assert.False(StringSegment.Equals(segment1, segment2)); + } + + [Fact] + public void StringSegment_IsNullOrEmpty_Valid() + { + Assert.True(StringSegment.IsNullOrEmpty(null)); + Assert.True(StringSegment.IsNullOrEmpty(string.Empty)); + Assert.True(StringSegment.IsNullOrEmpty(new StringSegment(null))); + Assert.True(StringSegment.IsNullOrEmpty(new StringSegment(string.Empty))); + Assert.True(StringSegment.IsNullOrEmpty(StringSegment.Empty)); + Assert.True(StringSegment.IsNullOrEmpty(new StringSegment(string.Empty, 0, 0))); + Assert.True(StringSegment.IsNullOrEmpty(new StringSegment("Hello", 0, 0))); + Assert.True(StringSegment.IsNullOrEmpty(new StringSegment("Hello", 3, 0))); + } + + [Fact] + public void StringSegment_IsNullOrEmpty_Invalid() + { + Assert.False(StringSegment.IsNullOrEmpty("A")); + Assert.False(StringSegment.IsNullOrEmpty("ABCDefg")); + Assert.False(StringSegment.IsNullOrEmpty(new StringSegment("A", 0 , 1))); + Assert.False(StringSegment.IsNullOrEmpty(new StringSegment("ABCDefg", 3, 2))); + } + + public static TheoryData GetHashCode_ReturnsSameValueForEqualSubstringsData + { + get + { + return new TheoryData + { + { default(StringSegment), default(StringSegment) }, + { default(StringSegment), new StringSegment() }, + { new StringSegment("Test123", 0, 0), new StringSegment(string.Empty) }, + { new StringSegment("C`est si bon", 2, 3), new StringSegment("Yesterday", 1, 3) }, + { new StringSegment("Hello", 1, 4), new StringSegment("Hello world", 1, 4) }, + { new StringSegment("Hello"), new StringSegment("Hello", 0, 5) }, + }; + } + } + + [Theory] + [MemberData(nameof(GetHashCode_ReturnsSameValueForEqualSubstringsData))] + public void GetHashCode_ReturnsSameValueForEqualSubstrings(StringSegment segment1, StringSegment segment2) + { + // Act + var hashCode1 = segment1.GetHashCode(); + var hashCode2 = segment2.GetHashCode(); + + // Assert + Assert.Equal(hashCode1, hashCode2); + } + + public static TheoryData GetHashCode_ReturnsDifferentValuesForInequalSubstringsData + { + get + { + var testString = "Test123"; + return new TheoryData + { + { new StringSegment(testString, 0, 1), new StringSegment(string.Empty) }, + { new StringSegment(testString, 0, 1), new StringSegment(testString, 1, 1) }, + { new StringSegment(testString, 1, 2), new StringSegment(testString, 1, 3) }, + { new StringSegment(testString, 0, 4), new StringSegment("TEST123", 0, 4) }, + }; + } + } + + [Theory] + [MemberData(nameof(GetHashCode_ReturnsDifferentValuesForInequalSubstringsData))] + public void GetHashCode_ReturnsDifferentValuesForInequalSubstrings( + StringSegment segment1, + StringSegment segment2) + { + // Act + var hashCode1 = segment1.GetHashCode(); + var hashCode2 = segment2.GetHashCode(); + + // Assert + Assert.NotEqual(hashCode1, hashCode2); + } + + [Fact] + public void StringSegment_EqualsString_Invalid() + { + // Arrange + var segment = new StringSegment(); + + // Act + var result = segment.Equals(string.Empty, StringComparison.Ordinal); + + // Assert + Assert.False(result); + } + + public static TheoryData DefaultStringSegmentEqualsStringSegmentData + { + get + { + // candidate + return new TheoryData() + { + { default(StringSegment) }, + { new StringSegment() }, + }; + } + } + + [Theory] + [MemberData(nameof(DefaultStringSegmentEqualsStringSegmentData))] + public void DefaultStringSegment_EqualsStringSegment(StringSegment candidate) + { + // Arrange + var segment = default(StringSegment); + + // Act + var result = segment.Equals(candidate, StringComparison.Ordinal); + + // Assert + Assert.True(result); + } + + public static TheoryData DefaultStringSegmentDoesNotEqualStringSegmentData + { + get + { + // candidate + return new TheoryData() + { + { new StringSegment("Hello, World!", 1, 4) }, + { new StringSegment("Hello", 1, 0) }, + { new StringSegment(string.Empty) }, + }; + } + } + + [Theory] + [MemberData(nameof(DefaultStringSegmentDoesNotEqualStringSegmentData))] + public void DefaultStringSegment_DoesNotEqualStringSegment(StringSegment candidate) + { + // Arrange + var segment = default(StringSegment); + + // Act + var result = segment.Equals(candidate, StringComparison.Ordinal); + + // Assert + Assert.False(result); + } + + public static TheoryData DefaultStringSegmentDoesNotEqualStringData + { + get + { + // candidate + return new TheoryData() + { + { string.Empty }, + { "Hello, World!" }, + }; + } + } + + [Theory] + [MemberData(nameof(DefaultStringSegmentDoesNotEqualStringData))] + public void DefaultStringSegment_DoesNotEqualString(string candidate) + { + // Arrange + var segment = default(StringSegment); + + // Act + var result = segment.Equals(candidate, StringComparison.Ordinal); + + // Assert + Assert.False(result); + } + + public static TheoryData EqualsStringSegmentData + { + get + { + // candidate / comparer / expected result + return new TheoryData() + { + { new StringSegment("Hello, World!", 1, 4), StringComparison.Ordinal, true }, + { new StringSegment("HELlo, World!", 1, 4), StringComparison.Ordinal, false }, + { new StringSegment("HELlo, World!", 1, 4), StringComparison.OrdinalIgnoreCase, true }, + { new StringSegment("ello, World!", 0, 4), StringComparison.Ordinal, true }, + { new StringSegment("ello, World!", 0, 3), StringComparison.Ordinal, false }, + { new StringSegment("ello, World!", 1, 3), StringComparison.Ordinal, false }, + }; + } + } + + [Theory] + [MemberData(nameof(EqualsStringSegmentData))] + public void StringSegment_Equals_StringSegment_Valid(StringSegment candidate, StringComparison comparison, bool expectedResult) + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 4); + + // Act + var result = segment.Equals(candidate, comparison); + + // Assert + Assert.Equal(expectedResult, result); + } + + [Fact] + public void StringSegment_EqualsStringSegment_Invalid() + { + // Arrange + var segment = new StringSegment(); + var candidate = new StringSegment("Hello, World!", 3, 2); + + // Act + var result = segment.Equals(candidate, StringComparison.Ordinal); + + // Assert + Assert.False(result); + } + + [Fact] + public void StringSegment_SubstringOffset_Valid() + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 4); + + // Act + var result = segment.Substring(offset: 1); + + // Assert + Assert.Equal("llo", result); + } + + [Fact] + public void StringSegment_Substring_Valid() + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 4); + + // Act + var result = segment.Substring(offset: 1, length: 2); + + // Assert + Assert.Equal("ll", result); + } + + [Fact] + public void StringSegment_Substring_Invalid() + { + // Arrange + var segment = new StringSegment(); + + // Act & Assert + Assert.Throws(() => segment.Substring(0, 0)); + } + + [Fact] + public void StringSegment_Substring_InvalidOffset() + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 3); + + // Act & Assert + var exception = Assert.Throws(() => segment.Substring(-1, 1)); + Assert.Equal("offset", exception.ParamName); + } + + [Fact] + public void StringSegment_Substring_InvalidLength() + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 3); + + // Act & Assert + var exception = Assert.Throws(() => segment.Substring(0, -1)); + Assert.Equal("length", exception.ParamName); + } + + [Fact] + public void StringSegment_Substring_InvalidOffsetAndLength() + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 3); + + // Act & Assert + var exception = Assert.Throws(() => segment.Substring(2, 3)); + Assert.Contains("bounds", exception.Message); + } + + [Fact] + public void StringSegment_Substring_OffsetAndLengthOverflows() + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 3); + + // Act & Assert + var exception = Assert.Throws(() => segment.Substring(1, int.MaxValue)); + Assert.Contains("bounds", exception.Message); + } + + [Fact] + public void StringSegment_SubsegmentOffset_Valid() + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 4); + + // Act + var result = segment.Subsegment(offset: 1); + + // Assert + Assert.Equal(new StringSegment("Hello, World!", 2, 3), result); + Assert.Equal("llo", result.Value); + } + + [Fact] + public void StringSegment_Subsegment_Valid() + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 4); + + // Act + var result = segment.Subsegment(offset: 1, length: 2); + + // Assert + Assert.Equal(new StringSegment("Hello, World!", 2, 2), result); + Assert.Equal("ll", result.Value); + } + + [Fact] + public void StringSegment_Subsegment_Invalid() + { + // Arrange + var segment = new StringSegment(); + + // Act & Assert + Assert.Throws(() => segment.Subsegment(0, 0)); + } + + [Fact] + public void StringSegment_Subsegment_InvalidOffset() + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 3); + + // Act & Assert + var exception = Assert.Throws(() => segment.Subsegment(-1, 1)); + Assert.Equal("offset", exception.ParamName); + } + + [Fact] + public void StringSegment_Subsegment_InvalidLength() + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 3); + + // Act & Assert + var exception = Assert.Throws(() => segment.Subsegment(0, -1)); + Assert.Equal("length", exception.ParamName); + } + + [Fact] + public void StringSegment_Subsegment_InvalidOffsetAndLength() + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 3); + + // Act & Assert + var exception = Assert.Throws(() => segment.Subsegment(2, 3)); + Assert.Contains("bounds", exception.Message); + } + + [Fact] + public void StringSegment_Subsegment_OffsetAndLengthOverflows() + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 3); + + // Act & Assert + var exception = Assert.Throws(() => segment.Subsegment(1, int.MaxValue)); + Assert.Contains("bounds", exception.Message); + } + + public static TheoryData CompareLesserData + { + get + { + // candidate / comparer + return new TheoryData() + { + { new StringSegment("abcdef", 1, 4), StringSegmentComparer.Ordinal }, + { new StringSegment("abcdef", 1, 5), StringSegmentComparer.OrdinalIgnoreCase }, + { new StringSegment("ABCDEF", 2, 2), StringSegmentComparer.OrdinalIgnoreCase }, + }; + } + } + + [Theory] + [MemberData(nameof(CompareLesserData))] + public void StringSegment_Compare_Lesser(StringSegment candidate, StringSegmentComparer comparer) + { + // Arrange + var segment = new StringSegment("ABCDEF", 1, 4); + + // Act + var result = comparer.Compare(segment, candidate); + + // Assert + Assert.True(result < 0, $"{segment} should be less than {candidate}"); + } + + public static TheoryData CompareEqualData + { + get + { + // candidate / comparer + return new TheoryData() + { + { new StringSegment("abcdef", 1, 4), StringSegmentComparer.Ordinal }, + { new StringSegment("ABCDEF", 1, 4), StringSegmentComparer.OrdinalIgnoreCase }, + { new StringSegment("bcde", 0, 4), StringSegmentComparer.Ordinal }, + { new StringSegment("BcDeF", 0, 4), StringSegmentComparer.OrdinalIgnoreCase }, + }; + } + } + + [Theory] + [MemberData(nameof(CompareEqualData))] + public void StringSegment_Compare_Equal(StringSegment candidate, StringSegmentComparer comparer) + { + // Arrange + var segment = new StringSegment("abcdef", 1, 4); + + // Act + var result = comparer.Compare(segment, candidate); + + // Assert + Assert.True(result == 0, $"{segment} should equal {candidate}"); + } + + public static TheoryData CompareGreaterData + { + get + { + // candidate / comparer + return new TheoryData() + { + { new StringSegment("ABCDEF", 1, 4), StringSegmentComparer.Ordinal }, + { new StringSegment("ABCDEF", 0, 6), StringSegmentComparer.OrdinalIgnoreCase }, + { new StringSegment("abcdef", 0, 3), StringSegmentComparer.Ordinal }, + }; + } + } + + [Theory] + [MemberData(nameof(CompareGreaterData))] + public void StringSegment_Compare_Greater(StringSegment candidate, StringSegmentComparer comparer) + { + // Arrange + var segment = new StringSegment("abcdef", 1, 4); + + // Act + var result = comparer.Compare(segment, candidate); + + // Assert + Assert.True(result > 0, $"{segment} should be greater than {candidate}"); + } + + [Theory] + [MemberData(nameof(GetHashCode_ReturnsSameValueForEqualSubstringsData))] + public void StringSegmentComparerOrdinal_GetHashCode_ReturnsSameValueForEqualSubstrings(StringSegment segment1, StringSegment segment2) + { + // Arrange + var comparer = StringSegmentComparer.Ordinal; + + // Act + var hashCode1 = comparer.GetHashCode(segment1); + var hashCode2 = comparer.GetHashCode(segment2); + + // Assert + Assert.Equal(hashCode1, hashCode2); + } + + [Theory] + [MemberData(nameof(GetHashCode_ReturnsSameValueForEqualSubstringsData))] + public void StringSegmentComparerOrdinalIgnoreCase_GetHashCode_ReturnsSameValueForEqualSubstrings(StringSegment segment1, StringSegment segment2) + { + // Arrange + var comparer = StringSegmentComparer.OrdinalIgnoreCase; + + // Act + var hashCode1 = comparer.GetHashCode(segment1); + var hashCode2 = comparer.GetHashCode(segment2); + + // Assert + Assert.Equal(hashCode1, hashCode2); + } + + [Fact] + public void StringSegmentComparerOrdinalIgnoreCase_GetHashCode_ReturnsSameValueForDifferentlyCasedStrings() + { + // Arrange + var segment1 = new StringSegment("abc"); + var segment2 = new StringSegment("Abcd", 0, 3); + var comparer = StringSegmentComparer.OrdinalIgnoreCase; + + // Act + var hashCode1 = comparer.GetHashCode(segment1); + var hashCode2 = comparer.GetHashCode(segment2); + + // Assert + Assert.Equal(hashCode1, hashCode2); + } + + [Theory] + [MemberData(nameof(GetHashCode_ReturnsDifferentValuesForInequalSubstringsData))] + public void StringSegmentComparerOrdinal_GetHashCode_ReturnsDifferentValuesForInequalSubstrings(StringSegment segment1, StringSegment segment2) + { + // Arrange + var comparer = StringSegmentComparer.Ordinal; + + // Act + var hashCode1 = comparer.GetHashCode(segment1); + var hashCode2 = comparer.GetHashCode(segment2); + + // Assert + Assert.NotEqual(hashCode1, hashCode2); + } + + [Fact] + public void IndexOf_ComputesIndex_RelativeToTheCurrentSegment() + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 10); + + // Act + var result = segment.IndexOf(','); + + // Assert + Assert.Equal(4, result); + } + + [Fact] + public void IndexOf_ReturnsMinusOne_IfElementNotInSegment() + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 3); + + // Act + var result = segment.IndexOf(','); + + // Assert + Assert.Equal(-1, result); + } + + [Fact] + public void IndexOf_SkipsANumberOfCaracters_IfStartIsProvided() + { + // Arrange + const string buffer = "Hello, World!, Hello people!"; + var segment = new StringSegment(buffer, 3, buffer.Length - 3); + + // Act + var result = segment.IndexOf('!', 15); + + // Assert + Assert.Equal(buffer.Length - 4, result); + } + + [Fact] + public void IndexOf_SearchOnlyInsideTheRange_IfStartAndCountAreProvided() + { + // Arrange + const string buffer = "Hello, World!, Hello people!"; + var segment = new StringSegment(buffer, 3, buffer.Length - 3); + + // Act + var result = segment.IndexOf('!', 15, 5); + + // Assert + Assert.Equal(-1, result); + } + + [Fact] + public void IndexOf_NegativeStart_OutOfRangeThrows() + { + // Arrange + const string buffer = "Hello, World!, Hello people!"; + var segment = new StringSegment(buffer, 3, buffer.Length - 3); + + // Act & Assert + Assert.Throws(() => segment.IndexOf('!', -1, 3)); + } + + [Fact] + public void IndexOf_StartOverflowsWithOffset_OutOfRangeThrows() + { + // Arrange + const string buffer = "Hello, World!, Hello people!"; + var segment = new StringSegment(buffer, 3, buffer.Length - 3); + + // Act & Assert + var exception = Assert.Throws(() => segment.IndexOf('!', int.MaxValue, 3)); + Assert.Equal("start", exception.ParamName); + } + + [Fact] + public void IndexOfAny_ComputesIndex_RelativeToTheCurrentSegment() + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 10); + + // Act + var result = segment.IndexOfAny(new[] { ',' }); + + // Assert + Assert.Equal(4, result); + } + + [Fact] + public void IndexOfAny_ReturnsMinusOne_IfElementNotInSegment() + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 3); + + // Act + var result = segment.IndexOfAny(new[] { ',' }); + + // Assert + Assert.Equal(-1, result); + } + + [Fact] + public void IndexOfAny_SkipsANumberOfCaracters_IfStartIsProvided() + { + // Arrange + const string buffer = "Hello, World!, Hello people!"; + var segment = new StringSegment(buffer, 3, buffer.Length - 3); + + // Act + var result = segment.IndexOfAny(new[] { '!' }, 15); + + // Assert + Assert.Equal(buffer.Length - 4, result); + } + + [Fact] + public void IndexOfAny_SearchOnlyInsideTheRange_IfStartAndCountAreProvided() + { + // Arrange + const string buffer = "Hello, World!, Hello people!"; + var segment = new StringSegment(buffer, 3, buffer.Length - 3); + + // Act + var result = segment.IndexOfAny(new[] { '!' }, 15, 5); + + // Assert + Assert.Equal(-1, result); + } + + [Fact] + public void LastIndexOf_ComputesIndex_RelativeToTheCurrentSegment() + { + // Arrange + var segment = new StringSegment("Hello, World, how, are, you!", 1, 14); + + // Act + var result = segment.LastIndexOf(','); + + // Assert + Assert.Equal(11, result); + } + + [Fact] + public void LastIndexOf_ReturnsMinusOne_IfElementNotInSegment() + { + // Arrange + var segment = new StringSegment("Hello, World!", 1, 3); + + // Act + var result = segment.LastIndexOf(','); + + // Assert + Assert.Equal(-1, result); + } + + [Fact] + public void Value_DoesNotAllocateANewString_IfTheSegmentContainsTheWholeBuffer() + { + // Arrange + const string buffer = "Hello, World!"; + var segment = new StringSegment(buffer); + + // Act + var result = segment.Value; + + // Assert + Assert.Same(buffer, result); + } + + [Fact] + public void StringSegment_CreateEmptySegment() + { + // Arrange + var segment = new StringSegment("//", 1, 0); + + // Assert + Assert.True(segment.HasValue); + } + + [Theory] + [InlineData(" value", 0, 8, "value")] + [InlineData("value ", 0, 8, "value")] + [InlineData("\t\tvalue", 0, 7, "value")] + [InlineData("value\t\t", 0, 7, "value")] + [InlineData("\t\tvalue \t a", 1, 8, "value")] + [InlineData(" a ", 0, 9, "a")] + [InlineData("value\t value value ", 2, 13, "lue\t value v")] + [InlineData("\x0009value \x0085", 0, 8, "value")] + [InlineData(" \f\t\u000B\u2028Hello \u2029\n\t ", 1, 13, "Hello")] + [InlineData(" ", 1, 2, "")] + [InlineData("\t\t\t", 0, 3, "")] + [InlineData("\n\n\t\t \t", 2, 3, "")] + [InlineData(" ", 1, 0, "")] + [InlineData("", 0, 0, "")] + public void Trim_RemovesLeadingAndTrailingWhitespaces(string value, int start, int length, string expected) + { + // Arrange + var segment = new StringSegment(value, start, length); + + // Act + var actual = segment.Trim(); + + // Assert + Assert.Equal(expected, actual.Value); + } + + [Theory] + [InlineData(" value", 0, 8, "value")] + [InlineData("value ", 0, 8, "value ")] + [InlineData("\t\tvalue", 0, 7, "value")] + [InlineData("value\t\t", 0, 7, "value\t\t")] + [InlineData("\t\tvalue \t a", 1, 8, "value \t")] + [InlineData(" a ", 0, 9, "a ")] + [InlineData("value\t value value ", 2, 13, "lue\t value v")] + [InlineData("\x0009value \x0085", 0, 8, "value \x0085")] + [InlineData(" \f\t\u000B\u2028Hello \u2029\n\t ", 1, 13, "Hello \u2029\n\t")] + [InlineData(" ", 1, 2, "")] + [InlineData("\t\t\t", 0, 3, "")] + [InlineData("\n\n\t\t \t", 2, 3, "")] + [InlineData(" ", 1, 0, "")] + [InlineData("", 0, 0, "")] + public void TrimStart_RemovesLeadingWhitespaces(string value, int start, int length, string expected) + { + // Arrange + var segment = new StringSegment(value, start, length); + + // Act + var actual = segment.TrimStart(); + + // Assert + Assert.Equal(expected, actual.Value); + } + + [Theory] + [InlineData(" value", 0, 8, " value")] + [InlineData("value ", 0, 8, "value")] + [InlineData("\t\tvalue", 0, 7, "\t\tvalue")] + [InlineData("value\t\t", 0, 7, "value")] + [InlineData("\t\tvalue \t a", 1, 8, "\tvalue")] + [InlineData(" a ", 0, 9, " a")] + [InlineData("value\t value value ", 2, 13, "lue\t value v")] + [InlineData("\x0009value \x0085", 0, 8, "\x0009value")] + [InlineData(" \f\t\u000B\u2028Hello \u2029\n\t ", 1, 13, "\f\t\u000B\u2028Hello")] + [InlineData(" ", 1, 2, "")] + [InlineData("\t\t\t", 0, 3, "")] + [InlineData("\n\n\t\t \t", 2, 3, "")] + [InlineData(" ", 1, 0, "")] + [InlineData("", 0, 0, "")] + public void TrimEnd_RemovesTrailingWhitespaces(string value, int start, int length, string expected) + { + // Arrange + var segment = new StringSegment(value, start, length); + + // Act + var actual = segment.TrimEnd(); + + // Assert + Assert.Equal(expected, actual.Value); + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Primitives/tests/StringTokenizerTest.cs b/src/libraries/Microsoft.Extensions.Primitives/tests/StringTokenizerTest.cs new file mode 100644 index 00000000000000..b3b9ebc33c6d67 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Primitives/tests/StringTokenizerTest.cs @@ -0,0 +1,79 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Linq; +using Xunit; + +namespace Microsoft.Extensions.Primitives +{ + public class StringTokenizerTest + { + [Fact] + public void TokenizerReturnsEmptySequenceForNullValues() + { + // Arrange + var stringTokenizer = new StringTokenizer(); + var enumerator = stringTokenizer.GetEnumerator(); + + // Act + var next = enumerator.MoveNext(); + + // Assert + Assert.False(next); + } + + [Theory] + [InlineData("", new[] { "" })] + [InlineData("a", new[] { "a" })] + [InlineData("abc", new[] { "abc" })] + [InlineData("a,b", new[] { "a", "b" })] + [InlineData("a,,b", new[] { "a", "", "b" })] + [InlineData(",a,b", new[] { "", "a", "b" })] + [InlineData(",,a,b", new[] { "", "", "a", "b" })] + [InlineData("a,b,", new[] { "a", "b", "" })] + [InlineData("a,b,,", new[] { "a", "b", "", "" })] + [InlineData("ab,cde,efgh", new[] { "ab", "cde", "efgh" })] + public void Tokenizer_ReturnsSequenceOfValues(string value, string[] expected) + { + // Arrange + var tokenizer = new StringTokenizer(value, new [] { ',' }); + + // Act + var result = tokenizer.Select(t => t.Value).ToArray(); + + // Assert + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("", new[] { "" })] + [InlineData("a", new[] { "a" })] + [InlineData("abc", new[] { "abc" })] + [InlineData("a.b", new[] { "a", "b" })] + [InlineData("a,b", new[] { "a", "b" })] + [InlineData("a.b,c", new[] { "a", "b", "c" })] + [InlineData("a,b.c", new[] { "a", "b", "c" })] + [InlineData("ab.cd,ef", new[] { "ab", "cd", "ef" })] + [InlineData("ab,cd.ef", new[] { "ab", "cd", "ef" })] + [InlineData(",a.b", new[] { "", "a", "b" })] + [InlineData(".a,b", new[] { "", "a", "b" })] + [InlineData(".,a.b", new[] { "", "", "a", "b" })] + [InlineData(",.a,b", new[] { "", "", "a", "b" })] + [InlineData("a.b,", new[] { "a", "b", "" })] + [InlineData("a,b.", new[] { "a", "b", "" })] + [InlineData("a.b,.", new[] { "a", "b", "", "" })] + [InlineData("a,b.,", new[] { "a", "b", "", "" })] + public void Tokenizer_SupportsMultipleSeparators(string value, string[] expected) + { + // Arrange + var tokenizer = new StringTokenizer(value, new[] { '.', ',' }); + + // Act + var result = tokenizer.Select(t => t.Value).ToArray(); + + // Assert + Assert.Equal(expected, result); + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Primitives/tests/StringValuesTests.cs b/src/libraries/Microsoft.Extensions.Primitives/tests/StringValuesTests.cs new file mode 100644 index 00000000000000..505d6259daca4b --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Primitives/tests/StringValuesTests.cs @@ -0,0 +1,564 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Microsoft.Extensions.Primitives +{ + public class StringValuesTests + { + public static TheoryData DefaultOrNullStringValues + { + get + { + return new TheoryData + { + new StringValues(), + new StringValues((string)null), + new StringValues((string[])null), + (string)null, + (string[])null + }; + } + } + + public static TheoryData EmptyStringValues + { + get + { + return new TheoryData + { + StringValues.Empty, + new StringValues(new string[0]), + new string[0] + }; + } + } + + public static TheoryData FilledStringValues + { + get + { + return new TheoryData + { + new StringValues("abc"), + new StringValues(new[] { "abc" }), + new StringValues(new[] { "abc", "bcd" }), + new StringValues(new[] { "abc", "bcd", "foo" }), + "abc", + new[] { "abc" }, + new[] { "abc", "bcd" }, + new[] { "abc", "bcd", "foo" } + }; + } + } + + public static TheoryData FilledStringValuesWithExpectedStrings + { + get + { + return new TheoryData + { + { default(StringValues), (string)null }, + { StringValues.Empty, (string)null }, + { new StringValues(new string[] { }), (string)null }, + { new StringValues(string.Empty), string.Empty }, + { new StringValues(new string[] { string.Empty }), string.Empty }, + { new StringValues("abc"), "abc" } + }; + } + } + + public static TheoryData FilledStringValuesWithExpectedObjects + { + get + { + return new TheoryData + { + { default(StringValues), (object)null }, + { StringValues.Empty, (object)null }, + { new StringValues(new string[] { }), (object)null }, + { new StringValues("abc"), (object)"abc" }, + { new StringValues("abc"), (object)new[] { "abc" } }, + { new StringValues(new[] { "abc" }), (object)new[] { "abc" } }, + { new StringValues(new[] { "abc", "bcd" }), (object)new[] { "abc", "bcd" } } + }; + } + } + + public static TheoryData FilledStringValuesWithExpected + { + get + { + return new TheoryData + { + { default(StringValues), new string[0] }, + { StringValues.Empty, new string[0] }, + { new StringValues(string.Empty), new[] { string.Empty } }, + { new StringValues("abc"), new[] { "abc" } }, + { new StringValues(new[] { "abc" }), new[] { "abc" } }, + { new StringValues(new[] { "abc", "bcd" }), new[] { "abc", "bcd" } }, + { new StringValues(new[] { "abc", "bcd", "foo" }), new[] { "abc", "bcd", "foo" } }, + { string.Empty, new[] { string.Empty } }, + { "abc", new[] { "abc" } }, + { new[] { "abc" }, new[] { "abc" } }, + { new[] { "abc", "bcd" }, new[] { "abc", "bcd" } }, + { new[] { "abc", "bcd", "foo" }, new[] { "abc", "bcd", "foo" } }, + { new[] { null, "abc", "bcd", "foo" }, new[] { null, "abc", "bcd", "foo" } }, + { new[] { "abc", null, "bcd", "foo" }, new[] { "abc", null, "bcd", "foo" } }, + { new[] { "abc", "bcd", "foo", null }, new[] { "abc", "bcd", "foo", null } }, + { new[] { string.Empty, "abc", "bcd", "foo" }, new[] { string.Empty, "abc", "bcd", "foo" } }, + { new[] { "abc", string.Empty, "bcd", "foo" }, new[] { "abc", string.Empty, "bcd", "foo" } }, + { new[] { "abc", "bcd", "foo", string.Empty }, new[] { "abc", "bcd", "foo", string.Empty } } + }; + } + } + + public static TheoryData FilledStringValuesToStringToExpected + { + get + { + return new TheoryData + { + { default(StringValues), string.Empty }, + { StringValues.Empty, string.Empty }, + { new StringValues(string.Empty), string.Empty }, + { new StringValues("abc"), "abc" }, + { new StringValues(new[] { "abc" }), "abc" }, + { new StringValues(new[] { "abc", "bcd" }), "abc,bcd" }, + { new StringValues(new[] { "abc", "bcd", "foo" }), "abc,bcd,foo" }, + { string.Empty, string.Empty }, + { (string)null, string.Empty }, + { "abc","abc" }, + { new[] { "abc" }, "abc" }, + { new[] { "abc", "bcd" }, "abc,bcd" }, + { new[] { "abc", null, "bcd" }, "abc,bcd" }, + { new[] { "abc", string.Empty, "bcd" }, "abc,bcd" }, + { new[] { "abc", "bcd", "foo" }, "abc,bcd,foo" }, + { new[] { null, "abc", "bcd", "foo" }, "abc,bcd,foo" }, + { new[] { "abc", null, "bcd", "foo" }, "abc,bcd,foo" }, + { new[] { "abc", "bcd", "foo", null }, "abc,bcd,foo" }, + { new[] { string.Empty, "abc", "bcd", "foo" }, "abc,bcd,foo" }, + { new[] { "abc", string.Empty, "bcd", "foo" }, "abc,bcd,foo" }, + { new[] { "abc", "bcd", "foo", string.Empty }, "abc,bcd,foo" }, + { new[] { "abc", "bcd", "foo", string.Empty, null }, "abc,bcd,foo" } + }; + } + } + + [Theory] + [MemberData(nameof(DefaultOrNullStringValues))] + [MemberData(nameof(EmptyStringValues))] + [MemberData(nameof(FilledStringValues))] + public void IsReadOnly_True(StringValues stringValues) + { + Assert.True(((IList)stringValues).IsReadOnly); + Assert.Throws(() => ((IList)stringValues)[0] = string.Empty); + Assert.Throws(() => ((ICollection)stringValues).Add(string.Empty)); + Assert.Throws(() => ((IList)stringValues).Insert(0, string.Empty)); + Assert.Throws(() => ((ICollection)stringValues).Remove(string.Empty)); + Assert.Throws(() => ((IList)stringValues).RemoveAt(0)); + Assert.Throws(() => ((ICollection)stringValues).Clear()); + } + + [Theory] + [MemberData(nameof(DefaultOrNullStringValues))] + public void DefaultOrNull_ExpectedValues(StringValues stringValues) + { + Assert.Null((string[])stringValues); + } + + [Theory] + [MemberData(nameof(DefaultOrNullStringValues))] + [MemberData(nameof(EmptyStringValues))] + public void DefaultNullOrEmpty_ExpectedValues(StringValues stringValues) + { + Assert.Empty(stringValues); + Assert.Null((string)stringValues); + Assert.Equal((string)null, stringValues); + Assert.Equal(string.Empty, stringValues.ToString()); + Assert.Equal(new string[0], stringValues.ToArray()); + + Assert.True(StringValues.IsNullOrEmpty(stringValues)); + Assert.Throws(() => stringValues[0]); + Assert.Throws(() => ((IList)stringValues)[0]); + Assert.Equal(string.Empty, stringValues.ToString()); + Assert.Equal(-1, ((IList)stringValues).IndexOf(null)); + Assert.Equal(-1, ((IList)stringValues).IndexOf(string.Empty)); + Assert.Equal(-1, ((IList)stringValues).IndexOf("not there")); + Assert.False(((ICollection)stringValues).Contains(null)); + Assert.False(((ICollection)stringValues).Contains(string.Empty)); + Assert.False(((ICollection)stringValues).Contains("not there")); + Assert.Empty(stringValues); + } + + [Theory] + [MemberData(nameof(FilledStringValuesToStringToExpected))] + public void ToString_ExpectedValues(StringValues stringValues, string expected) + { + Assert.Equal(stringValues.ToString(), expected); + } + + [Fact] + public void ImplicitStringConverter_Works() + { + string nullString = null; + StringValues stringValues = nullString; + Assert.Empty(stringValues); + Assert.Null((string)stringValues); + Assert.Null((string[])stringValues); + + string aString = "abc"; + stringValues = aString; + Assert.Single(stringValues); + Assert.Equal(aString, stringValues); + Assert.Equal(aString, stringValues[0]); + Assert.Equal(aString, ((IList)stringValues)[0]); + Assert.Equal(new string[] { aString }, stringValues); + } + + [Fact] + public void GetHashCode_SingleValueVsArrayWithOneItem_SameHashCode() + { + var sv1 = new StringValues("value"); + var sv2 = new StringValues(new[] { "value" }); + Assert.Equal(sv1, sv2); + Assert.Equal(sv1.GetHashCode(), sv2.GetHashCode()); + } + + [Fact] + public void GetHashCode_NullCases_DifferentHashCodes() + { + var sv1 = new StringValues((string)null); + var sv2 = new StringValues(new[] { (string)null }); + Assert.NotEqual(sv1, sv2); + Assert.NotEqual(sv1.GetHashCode(), sv2.GetHashCode()); + + var sv3 = new StringValues((string[])null); + Assert.Equal(sv1, sv3); + Assert.Equal(sv1.GetHashCode(), sv3.GetHashCode()); + } + + [Fact] + public void GetHashCode_SingleValueVsArrayWithTwoItems_DifferentHashCodes() + { + var sv1 = new StringValues("value"); + var sv2 = new StringValues(new[] { "value", "value" }); + Assert.NotEqual(sv1, sv2); + Assert.NotEqual(sv1.GetHashCode(), sv2.GetHashCode()); + } + + [Fact] + public void ImplicitStringArrayConverter_Works() + { + string[] nullStringArray = null; + StringValues stringValues = nullStringArray; + Assert.Empty(stringValues); + Assert.Null((string)stringValues); + Assert.Null((string[])stringValues); + + string aString = "abc"; + string[] aStringArray = new[] { aString }; + stringValues = aStringArray; + Assert.Single(stringValues); + Assert.Equal(aString, stringValues); + Assert.Equal(aString, stringValues[0]); + Assert.Equal(aString, ((IList)stringValues)[0]); + Assert.Equal(aStringArray, stringValues); + + aString = "abc"; + string bString = "bcd"; + aStringArray = new[] { aString, bString }; + stringValues = aStringArray; + Assert.Equal(2, stringValues.Count); + Assert.Equal("abc,bcd", stringValues); + Assert.Equal(aStringArray, stringValues); + } + + [Theory] + [MemberData(nameof(DefaultOrNullStringValues))] + [MemberData(nameof(EmptyStringValues))] + public void DefaultNullOrEmpty_Enumerator(StringValues stringValues) + { + var e = stringValues.GetEnumerator(); + Assert.Null(e.Current); + Assert.False(e.MoveNext()); + Assert.Null(e.Current); + Assert.False(e.MoveNext()); + Assert.False(e.MoveNext()); + Assert.False(e.MoveNext()); + + var e1 = ((IEnumerable)stringValues).GetEnumerator(); + Assert.Null(e1.Current); + Assert.False(e1.MoveNext()); + Assert.Null(e1.Current); + Assert.False(e1.MoveNext()); + Assert.False(e1.MoveNext()); + Assert.False(e1.MoveNext()); + + var e2 = ((IEnumerable)stringValues).GetEnumerator(); + Assert.Null(e2.Current); + Assert.False(e2.MoveNext()); + Assert.Null(e2.Current); + Assert.False(e2.MoveNext()); + Assert.False(e2.MoveNext()); + Assert.False(e2.MoveNext()); + } + + [Theory] + [MemberData(nameof(FilledStringValuesWithExpected))] + public void Enumerator(StringValues stringValues, string[] expected) + { + var e = stringValues.GetEnumerator(); + for (int i = 0; i < expected.Length; i++) + { + Assert.True(e.MoveNext()); + Assert.Equal(expected[i], e.Current); + } + Assert.False(e.MoveNext()); + Assert.False(e.MoveNext()); + Assert.False(e.MoveNext()); + + var e1 = ((IEnumerable)stringValues).GetEnumerator(); + for (int i = 0; i < expected.Length; i++) + { + Assert.True(e1.MoveNext()); + Assert.Equal(expected[i], e1.Current); + } + Assert.False(e1.MoveNext()); + Assert.False(e1.MoveNext()); + Assert.False(e1.MoveNext()); + + var e2 = ((IEnumerable)stringValues).GetEnumerator(); + for (int i = 0; i < expected.Length; i++) + { + Assert.True(e2.MoveNext()); + Assert.Equal(expected[i], e2.Current); + } + Assert.False(e2.MoveNext()); + Assert.False(e2.MoveNext()); + Assert.False(e2.MoveNext()); + } + + [Theory] + [MemberData(nameof(FilledStringValuesWithExpected))] + public void IndexOf(StringValues stringValues, string[] expected) + { + IList list = stringValues; + Assert.Equal(-1, list.IndexOf("not there")); + for (int i = 0; i < expected.Length; i++) + { + Assert.Equal(i, list.IndexOf(expected[i])); + } + } + + [Theory] + [MemberData(nameof(FilledStringValuesWithExpected))] + public void Contains(StringValues stringValues, string[] expected) + { + ICollection collection = stringValues; + Assert.False(collection.Contains("not there")); + for (int i = 0; i < expected.Length; i++) + { + Assert.True(collection.Contains(expected[i])); + } + } + + [Theory] + [MemberData(nameof(DefaultOrNullStringValues))] + [MemberData(nameof(EmptyStringValues))] + [MemberData(nameof(FilledStringValues))] + public void CopyTo_TooSmall(StringValues stringValues) + { + ICollection collection = stringValues; + string[] tooSmall = new string[0]; + + if (collection.Count > 0) + { + Assert.Throws(() => collection.CopyTo(tooSmall, 0)); + } + } + + [Theory] + [MemberData(nameof(FilledStringValuesWithExpected))] + public void CopyTo_CorrectSize(StringValues stringValues, string[] expected) + { + ICollection collection = stringValues; + string[] actual = new string[expected.Length]; + + if (collection.Count > 0) + { + Assert.Throws(() => collection.CopyTo(actual, -1)); + Assert.Throws(() => collection.CopyTo(actual, actual.Length + 1)); + } + collection.CopyTo(actual, 0); + Assert.Equal(expected, actual); + } + + [Theory] + [MemberData(nameof(DefaultOrNullStringValues))] + [MemberData(nameof(EmptyStringValues))] + public void DefaultNullOrEmpty_Concat(StringValues stringValues) + { + string[] expected = new[] { "abc", "bcd", "foo" }; + StringValues expectedStringValues = new StringValues(expected); + Assert.Equal(expected, StringValues.Concat(stringValues, expectedStringValues)); + Assert.Equal(expected, StringValues.Concat(expectedStringValues, stringValues)); + Assert.Equal(expected, StringValues.Concat((string)null, in expectedStringValues)); + Assert.Equal(expected, StringValues.Concat(in expectedStringValues, (string)null)); + + string[] empty = new string[0]; + StringValues emptyStringValues = new StringValues(empty); + Assert.Equal(empty, StringValues.Concat(stringValues, StringValues.Empty)); + Assert.Equal(empty, StringValues.Concat(StringValues.Empty, stringValues)); + Assert.Equal(empty, StringValues.Concat(stringValues, new StringValues())); + Assert.Equal(empty, StringValues.Concat(new StringValues(), stringValues)); + Assert.Equal(empty, StringValues.Concat((string)null, in emptyStringValues)); + Assert.Equal(empty, StringValues.Concat(in emptyStringValues, (string)null)); + } + + [Theory] + [MemberData(nameof(FilledStringValuesWithExpected))] + public void Concat(StringValues stringValues, string[] array) + { + string[] filled = new[] { "abc", "bcd", "foo" }; + + string[] expectedPrepended = array.Concat(filled).ToArray(); + Assert.Equal(expectedPrepended, StringValues.Concat(stringValues, new StringValues(filled))); + + string[] expectedAppended = filled.Concat(array).ToArray(); + Assert.Equal(expectedAppended, StringValues.Concat(new StringValues(filled), stringValues)); + + StringValues values = stringValues; + foreach (string s in filled) + { + values = StringValues.Concat(in values, s); + } + Assert.Equal(expectedPrepended, values); + + values = stringValues; + foreach (string s in filled.Reverse()) + { + values = StringValues.Concat(s, in values); + } + Assert.Equal(expectedAppended, values); + } + + [Fact] + public void Equals_OperatorEqual() + { + var equalString = "abc"; + + var equalStringArray = new string[] { equalString }; + var equalStringValues = new StringValues(equalString); + var otherStringValues = new StringValues(equalString); + var stringArray = new string[] { equalString, equalString }; + var stringValuesArray = new StringValues(stringArray); + + Assert.True(equalStringValues == otherStringValues); + + Assert.True(equalStringValues == equalString); + Assert.True(equalString == equalStringValues); + + Assert.True(equalStringValues == equalStringArray); + Assert.True(equalStringArray == equalStringValues); + + Assert.True(stringArray == stringValuesArray); + Assert.True(stringValuesArray == stringArray); + + Assert.False(stringValuesArray == equalString); + Assert.False(stringValuesArray == equalStringArray); + Assert.False(stringValuesArray == equalStringValues); + } + + [Fact] + public void Equals_OperatorNotEqual() + { + var equalString = "abc"; + + var equalStringArray = new string[] { equalString }; + var equalStringValues = new StringValues(equalString); + var otherStringValues = new StringValues(equalString); + var stringArray = new string[] { equalString, equalString }; + var stringValuesArray = new StringValues(stringArray); + + Assert.False(equalStringValues != otherStringValues); + + Assert.False(equalStringValues != equalString); + Assert.False(equalString != equalStringValues); + + Assert.False(equalStringValues != equalStringArray); + Assert.False(equalStringArray != equalStringValues); + + Assert.False(stringArray != stringValuesArray); + Assert.False(stringValuesArray != stringArray); + + Assert.True(stringValuesArray != equalString); + Assert.True(stringValuesArray != equalStringArray); + Assert.True(stringValuesArray != equalStringValues); + } + + [Fact] + public void Equals_Instance() + { + var equalString = "abc"; + + var equalStringArray = new string[] { equalString }; + var equalStringValues = new StringValues(equalString); + var stringArray = new string[] { equalString, equalString }; + var stringValuesArray = new StringValues(stringArray); + + Assert.True(equalStringValues.Equals(equalStringValues)); + Assert.True(equalStringValues.Equals(equalString)); + Assert.True(equalStringValues.Equals(equalStringArray)); + Assert.True(stringValuesArray.Equals(stringArray)); + } + + [Theory] + [MemberData(nameof(FilledStringValuesWithExpectedObjects))] + public void Equals_ObjectEquals(StringValues stringValues, object obj) + { + Assert.True(stringValues == obj); + Assert.True(obj == stringValues); + } + + [Theory] + [MemberData(nameof(FilledStringValuesWithExpectedObjects))] + public void Equals_ObjectNotEquals(StringValues stringValues, object obj) + { + Assert.False(stringValues != obj); + Assert.False(obj != stringValues); + } + + [Theory] + [MemberData(nameof(FilledStringValuesWithExpectedStrings))] + public void Equals_String(StringValues stringValues, string expected) + { + var notEqual = new StringValues("bcd"); + + Assert.True(StringValues.Equals(stringValues, expected)); + Assert.False(StringValues.Equals(stringValues, notEqual)); + + Assert.True(StringValues.Equals(stringValues, new StringValues(expected))); + Assert.Equal(stringValues.GetHashCode(), new StringValues(expected).GetHashCode()); + } + + [Theory] + [MemberData(nameof(FilledStringValuesWithExpected))] + public void Equals_StringArray(StringValues stringValues, string[] expected) + { + var notEqual = new StringValues(new[] { "bcd", "abc" }); + + Assert.True(StringValues.Equals(stringValues, expected)); + Assert.False(StringValues.Equals(stringValues, notEqual)); + + Assert.True(StringValues.Equals(stringValues, new StringValues(expected))); + Assert.Equal(stringValues.GetHashCode(), new StringValues(expected).GetHashCode()); + } + } +} diff --git a/src/libraries/pkg/baseline/packageIndex.json b/src/libraries/pkg/baseline/packageIndex.json index dbf70e5c32f096..19d9a7039dc49c 100644 --- a/src/libraries/pkg/baseline/packageIndex.json +++ b/src/libraries/pkg/baseline/packageIndex.json @@ -145,6 +145,28 @@ "5.0.0.0": "5.0.0" } }, + "Microsoft.Extensions.Primitives": { + "StableVersions": [ + "1.0.0", + "1.0.1", + "1.1.0", + "1.1.1", + "2.0.0", + "2.1.0", + "2.1.1", + "2.1.6", + "2.2.0", + "3.0.0", + "3.0.1", + "3.0.2", + "3.1.0", + "3.1.1" + ], + "InboxOn": {}, + "AssemblyVersionInPackageVersion": { + "5.0.0.0": "5.0.0" + } + }, "Microsoft.IO.Redist": { "StableVersions": [ "4.6.0" diff --git a/src/libraries/pkg/descriptions.json b/src/libraries/pkg/descriptions.json index 2fcc2a383465e2..9c10db8a8bcfec 100644 --- a/src/libraries/pkg/descriptions.json +++ b/src/libraries/pkg/descriptions.json @@ -14,6 +14,15 @@ "Description": "Provides a portable version of the Microsoft.Cci library", "CommonTypes": [] }, + { + "Name": "Microsoft.Extensions.Primitives", + "Description": "Primitives shared by framework extensions. Commonly used types include:", + "CommonTypes": [ + "Microsoft.Extensions.Primitives.IChangeToken", + "Microsoft.Extensions.Primitives.StringValues", + "Microsoft.Extensions.Primitives.StringSegment" + ] + }, { "Name": "Microsoft.Bcl.HashCode", "Description": "Provides the HashCode type for .NET Standard 2.0. This package is not required starting with .NET Standard 2.1 and .NET Core 3.0.",