From 723733ec966271545f10393fd7c9e6411a865092 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 19 Apr 2024 13:00:40 +0200 Subject: [PATCH] Forked Feature Collection from ASP.NET Core (#7067) --- src/HotChocolate/Core/HotChocolate.Core.sln | 30 ++++ .../Core/src/Features/FeatureCollection.cs | 161 ++++++++++++++++++ .../Features/FeatureCollectionExtensions.cs | 73 ++++++++ .../Core/src/Features/FeatureReference.cs | 54 ++++++ .../Core/src/Features/FeatureReferences.cs | 154 +++++++++++++++++ .../src/Features/HotChocolate.Features.csproj | 8 + .../Core/src/Features/IFeatureCollection.cs | 40 +++++ .../src/Features/ReadOnlyFeatureCollection.cs | 76 +++++++++ .../FeatureCollectionExtensionsTests.cs | 34 ++++ .../Features.Tests/FeatureCollectionTests.cs | 105 ++++++++++++ .../HotChocolate.Features.Tests.csproj | 13 ++ .../Core/test/Features.Tests/IThing.cs | 8 + .../Core/test/Features.Tests/Thing.cs | 11 ++ 13 files changed, 767 insertions(+) create mode 100644 src/HotChocolate/Core/src/Features/FeatureCollection.cs create mode 100644 src/HotChocolate/Core/src/Features/FeatureCollectionExtensions.cs create mode 100644 src/HotChocolate/Core/src/Features/FeatureReference.cs create mode 100644 src/HotChocolate/Core/src/Features/FeatureReferences.cs create mode 100644 src/HotChocolate/Core/src/Features/HotChocolate.Features.csproj create mode 100644 src/HotChocolate/Core/src/Features/IFeatureCollection.cs create mode 100644 src/HotChocolate/Core/src/Features/ReadOnlyFeatureCollection.cs create mode 100644 src/HotChocolate/Core/test/Features.Tests/FeatureCollectionExtensionsTests.cs create mode 100644 src/HotChocolate/Core/test/Features.Tests/FeatureCollectionTests.cs create mode 100644 src/HotChocolate/Core/test/Features.Tests/HotChocolate.Features.Tests.csproj create mode 100644 src/HotChocolate/Core/test/Features.Tests/IThing.cs create mode 100644 src/HotChocolate/Core/test/Features.Tests/Thing.cs diff --git a/src/HotChocolate/Core/HotChocolate.Core.sln b/src/HotChocolate/Core/HotChocolate.Core.sln index 5c35ddc98f2..f98736b518c 100644 --- a/src/HotChocolate/Core/HotChocolate.Core.sln +++ b/src/HotChocolate/Core/HotChocolate.Core.sln @@ -145,6 +145,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Types.Queries" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Types.Queries.Tests", "test\Types.Queries.Tests\HotChocolate.Types.Queries.Tests.csproj", "{AE9AF1C7-578A-46A5-84FD-9BBA8EB8DE22}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Features", "src\Features\HotChocolate.Features.csproj", "{669FA147-3B41-4841-921A-55B019C3AF26}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Features.Tests", "test\Features.Tests\HotChocolate.Features.Tests.csproj", "{EA77D317-8767-4DDE-8038-820D582C52D6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -971,6 +975,30 @@ Global {AE9AF1C7-578A-46A5-84FD-9BBA8EB8DE22}.Release|x64.Build.0 = Release|Any CPU {AE9AF1C7-578A-46A5-84FD-9BBA8EB8DE22}.Release|x86.ActiveCfg = Release|Any CPU {AE9AF1C7-578A-46A5-84FD-9BBA8EB8DE22}.Release|x86.Build.0 = Release|Any CPU + {669FA147-3B41-4841-921A-55B019C3AF26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {669FA147-3B41-4841-921A-55B019C3AF26}.Debug|Any CPU.Build.0 = Debug|Any CPU + {669FA147-3B41-4841-921A-55B019C3AF26}.Debug|x64.ActiveCfg = Debug|Any CPU + {669FA147-3B41-4841-921A-55B019C3AF26}.Debug|x64.Build.0 = Debug|Any CPU + {669FA147-3B41-4841-921A-55B019C3AF26}.Debug|x86.ActiveCfg = Debug|Any CPU + {669FA147-3B41-4841-921A-55B019C3AF26}.Debug|x86.Build.0 = Debug|Any CPU + {669FA147-3B41-4841-921A-55B019C3AF26}.Release|Any CPU.ActiveCfg = Release|Any CPU + {669FA147-3B41-4841-921A-55B019C3AF26}.Release|Any CPU.Build.0 = Release|Any CPU + {669FA147-3B41-4841-921A-55B019C3AF26}.Release|x64.ActiveCfg = Release|Any CPU + {669FA147-3B41-4841-921A-55B019C3AF26}.Release|x64.Build.0 = Release|Any CPU + {669FA147-3B41-4841-921A-55B019C3AF26}.Release|x86.ActiveCfg = Release|Any CPU + {669FA147-3B41-4841-921A-55B019C3AF26}.Release|x86.Build.0 = Release|Any CPU + {EA77D317-8767-4DDE-8038-820D582C52D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EA77D317-8767-4DDE-8038-820D582C52D6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EA77D317-8767-4DDE-8038-820D582C52D6}.Debug|x64.ActiveCfg = Debug|Any CPU + {EA77D317-8767-4DDE-8038-820D582C52D6}.Debug|x64.Build.0 = Debug|Any CPU + {EA77D317-8767-4DDE-8038-820D582C52D6}.Debug|x86.ActiveCfg = Debug|Any CPU + {EA77D317-8767-4DDE-8038-820D582C52D6}.Debug|x86.Build.0 = Debug|Any CPU + {EA77D317-8767-4DDE-8038-820D582C52D6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EA77D317-8767-4DDE-8038-820D582C52D6}.Release|Any CPU.Build.0 = Release|Any CPU + {EA77D317-8767-4DDE-8038-820D582C52D6}.Release|x64.ActiveCfg = Release|Any CPU + {EA77D317-8767-4DDE-8038-820D582C52D6}.Release|x64.Build.0 = Release|Any CPU + {EA77D317-8767-4DDE-8038-820D582C52D6}.Release|x86.ActiveCfg = Release|Any CPU + {EA77D317-8767-4DDE-8038-820D582C52D6}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1044,6 +1072,8 @@ Global {78979585-F881-4ACD-9E83-CCB866EB971C} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2} {655739D7-87E9-43EF-B522-AC421F7451A4} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2} {AE9AF1C7-578A-46A5-84FD-9BBA8EB8DE22} = {7462D089-D350-44D6-8131-896D949A65B7} + {669FA147-3B41-4841-921A-55B019C3AF26} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2} + {EA77D317-8767-4DDE-8038-820D582C52D6} = {7462D089-D350-44D6-8131-896D949A65B7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E4D94C77-6657-4630-9D42-0A9AC5153A1B} diff --git a/src/HotChocolate/Core/src/Features/FeatureCollection.cs b/src/HotChocolate/Core/src/Features/FeatureCollection.cs new file mode 100644 index 00000000000..48ec4a0e08a --- /dev/null +++ b/src/HotChocolate/Core/src/Features/FeatureCollection.cs @@ -0,0 +1,161 @@ +// This code was originally forked of https://github.com/dotnet/aspnetcore/tree/c7aae8ff34dce81132d0fb3a976349dcc01ff903/src/Extensions/Features/src + +// ReSharper disable NonAtomicCompoundOperator +using System.Collections; + +namespace HotChocolate.Features; + +/// +/// Default implementation for . +/// +public class FeatureCollection : IFeatureCollection +{ + private static readonly KeyComparer _featureKeyComparer = new(); + private readonly IFeatureCollection? _defaults; + private readonly int _initialCapacity; + private Dictionary? _features; + private volatile int _containerRevision; + + /// + /// Initializes a new instance of . + /// + public FeatureCollection() + { + } + + /// + /// Initializes a new instance of with the specified initial capacity. + /// + /// + /// The initial number of elements that the collection can contain. + /// + /// + /// is less than 0 + /// + public FeatureCollection(int initialCapacity) + { + if (initialCapacity < 0) + { + throw new ArgumentOutOfRangeException(nameof(initialCapacity)); + } + + _initialCapacity = initialCapacity; + } + + /// + /// Initializes a new instance of with the specified defaults. + /// + /// + /// The feature defaults. + /// + public FeatureCollection(IFeatureCollection defaults) + { + _defaults = defaults; + } + + /// + public virtual int Revision + { + get { return _containerRevision + (_defaults?.Revision ?? 0); } + } + + /// + public bool IsReadOnly { get { return false; } } + + /// + public object? this[Type key] + { + get + { + if (key is null) + { + throw new ArgumentNullException(nameof(key)); + } + + return _features != null && _features.TryGetValue(key, out var result) ? result : _defaults?[key]; + } + set + { + if (key is null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (value == null) + { + if (_features != null && _features.Remove(key)) + { + _containerRevision++; + } + return; + } + + if (_features == null) + { + _features = new Dictionary(_initialCapacity); + } + _features[key] = value; + _containerRevision++; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + public IEnumerator> GetEnumerator() + { + if (_features != null) + { + foreach (var pair in _features) + { + yield return pair; + } + } + + if (_defaults != null) + { + // Don't return features masked by the wrapper. + foreach (var pair in _features == null ? _defaults : _defaults.Except(_features, _featureKeyComparer)) + { + yield return pair; + } + } + } + + /// + public TFeature? Get() + { + if (typeof(TFeature).IsValueType) + { + var feature = this[typeof(TFeature)]; + if (feature is null && Nullable.GetUnderlyingType(typeof(TFeature)) is null) + { + throw new InvalidOperationException( + $"{typeof(TFeature).FullName} does not exist in the feature collection " + + $"and because it is a struct the method can't return null. " + + $"Use 'featureCollection[typeof({typeof(TFeature).FullName})] is not null' " + + $"to check if the feature exists."); + } + return (TFeature?)feature; + } + return (TFeature?)this[typeof(TFeature)]; + } + + /// + public void Set(TFeature? instance) + { + this[typeof(TFeature)] = instance; + } + + private sealed class KeyComparer : IEqualityComparer> + { + public bool Equals(KeyValuePair x, KeyValuePair y) => + x.Key.Equals(y.Key); + + public int GetHashCode(KeyValuePair obj) => + obj.Key.GetHashCode(); + } +} diff --git a/src/HotChocolate/Core/src/Features/FeatureCollectionExtensions.cs b/src/HotChocolate/Core/src/Features/FeatureCollectionExtensions.cs new file mode 100644 index 00000000000..95990353b4d --- /dev/null +++ b/src/HotChocolate/Core/src/Features/FeatureCollectionExtensions.cs @@ -0,0 +1,73 @@ +// This code was originally forked of https://github.com/dotnet/aspnetcore/tree/c7aae8ff34dce81132d0fb3a976349dcc01ff903/src/Extensions/Features/src + +namespace HotChocolate.Features; + +/// +/// Extension methods for getting feature from +/// +public static class FeatureCollectionExtensions +{ + /// + /// Retrieves the requested feature from the collection. + /// Throws an if the feature is not present. + /// + /// The . + /// The feature key. + /// The requested feature. + public static TFeature GetRequiredFeature(this IFeatureCollection featureCollection) + where TFeature : notnull + { + if(featureCollection is null) + { + throw new ArgumentNullException(nameof(featureCollection)); + } + + return featureCollection.Get() ?? + throw new InvalidOperationException($"Feature '{typeof(TFeature)}' is not present."); + } + + /// + /// Retrieves the requested feature from the collection. + /// Throws an if the feature is not present. + /// + /// feature collection + /// The feature key. + /// The requested feature. + public static object GetRequiredFeature(this IFeatureCollection featureCollection, Type key) + { + if(featureCollection is null) + { + throw new ArgumentNullException(nameof(featureCollection)); + } + + if(key is null) + { + throw new ArgumentNullException(nameof(key)); + } + + return featureCollection[key] ?? + throw new InvalidOperationException($"Feature '{key}' is not present."); + } + + /// + /// Creates a readonly collection of features. + /// + /// + /// The to make readonly. + /// + /// + /// A readonly . + /// + /// + /// is null. + /// + public static IFeatureCollection ToReadOnly(this IFeatureCollection featureCollection) + { + if(featureCollection is null) + { + throw new ArgumentNullException(nameof(featureCollection)); + } + + return new ReadOnlyFeatureCollection(featureCollection); + } +} diff --git a/src/HotChocolate/Core/src/Features/FeatureReference.cs b/src/HotChocolate/Core/src/Features/FeatureReference.cs new file mode 100644 index 00000000000..acd3b89b31c --- /dev/null +++ b/src/HotChocolate/Core/src/Features/FeatureReference.cs @@ -0,0 +1,54 @@ +// This code was originally forked of https://github.com/dotnet/aspnetcore/tree/c7aae8ff34dce81132d0fb3a976349dcc01ff903/src/Extensions/Features/src + +namespace HotChocolate.Features; + +/// +/// A cached reference to a feature. +/// +/// The feature type. +public struct FeatureReference +{ + private T? _feature; + private int _revision; + + private FeatureReference(T? feature, int revision) + { + _feature = feature; + _revision = revision; + } + + /// + /// Gets the default . + /// + public static readonly FeatureReference Default = new(default, -1); + + /// + /// Gets the feature of type from . + /// + /// The . + /// The feature. + public T? Fetch(IFeatureCollection features) + { + if (_revision == features.Revision) + { + return _feature; + } + _feature = (T?)features[typeof(T)]; + _revision = features.Revision; + return _feature; + } + + /// + /// Updates the reference to the feature. + /// + /// The to update. + /// The instance of the feature. + /// A reference to after the operation has completed. + public T Update(IFeatureCollection features, T feature) + { + features[typeof(T)] = feature; + _feature = feature; + _revision = features.Revision; + return feature; + } +} diff --git a/src/HotChocolate/Core/src/Features/FeatureReferences.cs b/src/HotChocolate/Core/src/Features/FeatureReferences.cs new file mode 100644 index 00000000000..4951909ccde --- /dev/null +++ b/src/HotChocolate/Core/src/Features/FeatureReferences.cs @@ -0,0 +1,154 @@ +// This code was originally forked of https://github.com/dotnet/aspnetcore/tree/c7aae8ff34dce81132d0fb3a976349dcc01ff903/src/Extensions/Features/src + +using System.Runtime.CompilerServices; + +namespace HotChocolate.Features; + +/// +/// A reference to a collection of features. +/// +/// The type of the feature. +public struct FeatureReferences +{ + /// + /// Initializes a new instance of . + /// + /// The . + public FeatureReferences(IFeatureCollection collection) + { + Collection = collection; + Cache = default; + Revision = collection.Revision; + } + + /// + /// Initializes the . + /// + /// The to initialize with. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Initalize(IFeatureCollection collection) + { + Revision = collection.Revision; + Collection = collection; + } + + /// + /// Initializes the . + /// + /// The to initialize with. + /// The version of the . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Initalize(IFeatureCollection collection, int revision) + { + Revision = revision; + Collection = collection; + } + + /// + /// Gets the . + /// + public IFeatureCollection Collection { get; private set; } + + /// + /// Gets the revision number. + /// + public int Revision { get; private set; } + + // cache is a public field because the code calling Fetch must + // be able to pass ref values that "dot through" the TCache struct memory, + // if it was a Property then that getter would return a copy of the memory + // preventing the use of "ref" + /// + /// This API is part of ASP.NET Core's infrastructure and should not be referenced by application code. + /// + public TCache? Cache; + + // Careful with modifications to the Fetch method; it is carefully constructed for inlining + // See: https://github.com/aspnet/HttpAbstractions/pull/704 + // This method is 59 IL bytes and at inline call depth 3 from accessing a property. + // This combination is enough for the jit to consider it an "unprofitable inline" + // Aggressively inlining it causes the entire call chain to dissolve: + // + // This means this call graph: + // + // HttpResponse.Headers -> Response.HttpResponseFeature -> Fetch -> Fetch -> Revision + // -> Collection -> Collection + // -> Collection.Revision + // Has 6 calls eliminated and becomes just: -> UpdateCached + // + // HttpResponse.Headers -> Collection.Revision + // -> UpdateCached (not called on fast path) + // + // As this is inlined at the callsite we want to keep the method small, so it only detects + // if a reset or update is required and all the reset and update logic is pushed to UpdateCached. + // + // Generally Fetch is called at a ratio > x4 of UpdateCached so this is a large gain + + /// + /// This API is part of ASP.NET Core's infrastructure and should not be referenced by application code. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TFeature? Fetch( + ref TFeature? cached, + TState state, + Func factory) where TFeature : class? + { + var flush = false; + var revision = Collection?.Revision ?? ContextDisposed(); + if (Revision != revision) + { + // Clear cached value to force call to UpdateCached + cached = null!; + // Collection changed, clear whole feature cache + flush = true; + } + + return cached ?? UpdateCached(ref cached!, state, factory, revision, flush); + } + + // Update and cache clearing logic, when the fast-path in Fetch isn't applicable + private TFeature? UpdateCached(ref TFeature? cached, TState state, Func factory, int revision, bool flush) where TFeature : class? + { + if (flush) + { + // Collection detected as changed, clear cache + Cache = default; + } + + cached = Collection.Get(); + if (cached == null) + { + // Item not in collection, create it with factory + cached = factory(state); + // Add item to IFeatureCollection + Collection.Set(cached); + // Revision changed by .Set, update revision to new value + Revision = Collection.Revision; + } + else if (flush) + { + // Cache was cleared, but item retrieved from current Collection for version + // so use passed in revision rather than making another virtual call + Revision = revision; + } + + return cached; + } + + /// + /// This API is part of ASP.NET Core's infrastructure and should not be referenced by application code. + /// + public TFeature? Fetch(ref TFeature? cached, Func factory) + where TFeature : class? => Fetch(ref cached, Collection, factory); + + private static int ContextDisposed() + { + ThrowContextDisposed(); + return 0; + } + + private static void ThrowContextDisposed() + { + throw new ObjectDisposedException(nameof(Collection), nameof(IFeatureCollection) + " has been disposed."); + } +} diff --git a/src/HotChocolate/Core/src/Features/HotChocolate.Features.csproj b/src/HotChocolate/Core/src/Features/HotChocolate.Features.csproj new file mode 100644 index 00000000000..1337762dad2 --- /dev/null +++ b/src/HotChocolate/Core/src/Features/HotChocolate.Features.csproj @@ -0,0 +1,8 @@ + + + + enable + enable + + + diff --git a/src/HotChocolate/Core/src/Features/IFeatureCollection.cs b/src/HotChocolate/Core/src/Features/IFeatureCollection.cs new file mode 100644 index 00000000000..e6c97873bd3 --- /dev/null +++ b/src/HotChocolate/Core/src/Features/IFeatureCollection.cs @@ -0,0 +1,40 @@ +// This code was originally forked of https://github.com/dotnet/aspnetcore/tree/c7aae8ff34dce81132d0fb3a976349dcc01ff903/src/Extensions/Features/src + +namespace HotChocolate.Features; + +/// +/// Represents a collection of GraphQL features. +/// +public interface IFeatureCollection : IEnumerable> +{ + /// + /// Indicates if the collection can be modified. + /// + bool IsReadOnly { get; } + + /// + /// Incremented for each modification and can be used to verify cached results. + /// + int Revision { get; } + + /// + /// Gets or sets a given feature. Setting a null value removes the feature. + /// + /// + /// The requested feature, or null if it is not present. + object? this[Type key] { get; set; } + + /// + /// Retrieves the requested feature from the collection. + /// + /// The feature key. + /// The requested feature, or null if it is not present. + TFeature? Get(); + + /// + /// Sets the given feature in the collection. + /// + /// The feature key. + /// The feature value. + void Set(TFeature? instance); +} diff --git a/src/HotChocolate/Core/src/Features/ReadOnlyFeatureCollection.cs b/src/HotChocolate/Core/src/Features/ReadOnlyFeatureCollection.cs new file mode 100644 index 00000000000..3437cca6e74 --- /dev/null +++ b/src/HotChocolate/Core/src/Features/ReadOnlyFeatureCollection.cs @@ -0,0 +1,76 @@ +using System.Collections; +#if NET8_0_OR_GREATER +using System.Collections.Frozen; +#endif + +namespace HotChocolate.Features; + +/// +/// Read-only implementation for . +/// +public sealed class ReadOnlyFeatureCollection : IFeatureCollection +{ +#if NET8_0_OR_GREATER + private readonly FrozenDictionary _features; +#else + private readonly Dictionary _features; +#endif + private volatile int _containerRevision; + + /// + /// Initializes a new instance of . + /// + /// + /// The to make readonly. + /// + public ReadOnlyFeatureCollection(IFeatureCollection features) + { +#if NET8_0_OR_GREATER + _features = features.ToFrozenDictionary(); +#else + _features = features.ToDictionary(t => t.Key, t => t.Value); +#endif + _containerRevision = features.Revision; + } + + /// + public bool IsReadOnly => true; + + /// + public int Revision => _containerRevision; + + /// + public object? this[Type key] + { + get => _features[key]; + set => throw new NotSupportedException("The feature collection is read-only."); + } + + /// + public TFeature? Get() + { + if (typeof(TFeature).IsValueType) + { + var feature = this[typeof(TFeature)]; + if (feature is null && Nullable.GetUnderlyingType(typeof(TFeature)) is null) + { + throw new InvalidOperationException( + $"{typeof(TFeature).FullName} does not exist in the feature collection " + + $"and because it is a struct the method can't return null. " + + $"Use 'featureCollection[typeof({typeof(TFeature).FullName})] is not null' " + + $"to check if the feature exists."); + } + return (TFeature?)feature; + } + return (TFeature?)this[typeof(TFeature)]; + } + + /// + public void Set(TFeature? instance) => + throw new NotSupportedException("The feature collection is read-only."); + + /// + public IEnumerator> GetEnumerator() => _features.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/src/HotChocolate/Core/test/Features.Tests/FeatureCollectionExtensionsTests.cs b/src/HotChocolate/Core/test/Features.Tests/FeatureCollectionExtensionsTests.cs new file mode 100644 index 00000000000..9619c7257f8 --- /dev/null +++ b/src/HotChocolate/Core/test/Features.Tests/FeatureCollectionExtensionsTests.cs @@ -0,0 +1,34 @@ +// This code was originally forked of https://github.com/dotnet/aspnetcore/tree/c7aae8ff34dce81132d0fb3a976349dcc01ff903/src/Extensions/Features/src + +namespace HotChocolate.Features; + +public class FeatureCollectionExtensionsTests +{ + [Fact] + public void AddedFeatureGetsReturned() + { + // Arrange + var features = new FeatureCollection(); + var thing = new Thing(); + features.Set(thing); + + // Act + var retrievedThing = features.GetRequiredFeature(); + + // Assert + Assert.NotNull(retrievedThing); + Assert.Equal(retrievedThing, thing); + } + + [Fact] + public void ExceptionThrown_WhenAskedForUnknownFeature() + { + // Arrange + var features = new FeatureCollection(); + var thing = new Thing(); + features.Set(thing); + + // Assert + Assert.Throws(() => features.GetRequiredFeature()); + } +} diff --git a/src/HotChocolate/Core/test/Features.Tests/FeatureCollectionTests.cs b/src/HotChocolate/Core/test/Features.Tests/FeatureCollectionTests.cs new file mode 100644 index 00000000000..8a7f5a44046 --- /dev/null +++ b/src/HotChocolate/Core/test/Features.Tests/FeatureCollectionTests.cs @@ -0,0 +1,105 @@ +// This code was originally forked of https://github.com/dotnet/aspnetcore/tree/c7aae8ff34dce81132d0fb3a976349dcc01ff903/src/Extensions/Features/src + +namespace HotChocolate.Features; + +public class FeatureCollectionTests +{ + [Fact] + public void AddedInterfaceIsReturned() + { + var interfaces = new FeatureCollection(); + var thing = new Thing(); + + interfaces[typeof(IThing)] = thing; + + var thing2 = interfaces[typeof(IThing)]; + Assert.Equal(thing2, thing); + } + + [Fact] + public void IndexerAlsoAddsItems() + { + var interfaces = new FeatureCollection(); + var thing = new Thing(); + + interfaces[typeof(IThing)] = thing; + + Assert.Equal(interfaces[typeof(IThing)], thing); + } + + [Fact] + public void SetNullValueRemoves() + { + var interfaces = new FeatureCollection(); + var thing = new Thing(); + + interfaces[typeof(IThing)] = thing; + Assert.Equal(interfaces[typeof(IThing)], thing); + + interfaces[typeof(IThing)] = null; + + var thing2 = interfaces[typeof(IThing)]; + Assert.Null(thing2); + } + + [Fact] + public void GetMissingStructFeatureThrows() + { + var interfaces = new FeatureCollection(); + + // Regression test: Used to throw NullReferenceException because it tried to unbox a null object to a struct + var ex = Assert.Throws(() => interfaces.Get()); + Assert.Equal( + "System.Int32 does not exist in the feature collection and because it is " + + "a struct the method can't return null. Use 'featureCollection[typeof(System.Int32)] " + + "is not null' to check if the feature exists.", ex.Message); + } + + [Fact] + public void GetMissingFeatureReturnsNull() + { + var interfaces = new FeatureCollection(); + + Assert.Null(interfaces.Get()); + } + + [Fact] + public void GetStructFeature() + { + var interfaces = new FeatureCollection(); + var value = 20; + interfaces.Set(value); + + Assert.Equal(value, interfaces.Get()); + } + + [Fact] + public void GetNullableStructFeatureWhenSetWithNonNullableStruct() + { + var interfaces = new FeatureCollection(); + var value = 20; + interfaces.Set(value); + + Assert.Null(interfaces.Get()); + } + + [Fact] + public void GetNullableStructFeatureWhenSetWithNullableStruct() + { + var interfaces = new FeatureCollection(); + var value = 20; + interfaces.Set(value); + + Assert.Equal(value, interfaces.Get()); + } + + [Fact] + public void GetFeature() + { + var interfaces = new FeatureCollection(); + var thing = new Thing(); + interfaces.Set(thing); + + Assert.Equal(thing, interfaces.Get()); + } +} diff --git a/src/HotChocolate/Core/test/Features.Tests/HotChocolate.Features.Tests.csproj b/src/HotChocolate/Core/test/Features.Tests/HotChocolate.Features.Tests.csproj new file mode 100644 index 00000000000..b7f73f048fd --- /dev/null +++ b/src/HotChocolate/Core/test/Features.Tests/HotChocolate.Features.Tests.csproj @@ -0,0 +1,13 @@ + + + + HotChocolate.Features + enable + enable + + + + + + + diff --git a/src/HotChocolate/Core/test/Features.Tests/IThing.cs b/src/HotChocolate/Core/test/Features.Tests/IThing.cs new file mode 100644 index 00000000000..f13fb5956b5 --- /dev/null +++ b/src/HotChocolate/Core/test/Features.Tests/IThing.cs @@ -0,0 +1,8 @@ +// This code was originally forked of https://github.com/dotnet/aspnetcore/tree/c7aae8ff34dce81132d0fb3a976349dcc01ff903/src/Extensions/Features/src + +namespace HotChocolate.Features; + +public interface IThing +{ + string Hello(); +} diff --git a/src/HotChocolate/Core/test/Features.Tests/Thing.cs b/src/HotChocolate/Core/test/Features.Tests/Thing.cs new file mode 100644 index 00000000000..7148f3e892b --- /dev/null +++ b/src/HotChocolate/Core/test/Features.Tests/Thing.cs @@ -0,0 +1,11 @@ +// This code was originally forked of https://github.com/dotnet/aspnetcore/tree/c7aae8ff34dce81132d0fb3a976349dcc01ff903/src/Extensions/Features/src + +namespace HotChocolate.Features; + +public class Thing : IThing +{ + public string Hello() + { + return "World"; + } +}