diff --git a/Directory.Build.targets b/Directory.Build.targets index 6d564cde7912..077353216668 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -1,5 +1,7 @@ + + $([MSBuild]::ValueOrDefault($(IsTrimmable),'false')) $(EnableAOTAnalyzer) diff --git a/eng/CodeGen.proj b/eng/CodeGen.proj index 1de7e09de4db..d5d8f0a5f6b6 100644 --- a/eng/CodeGen.proj +++ b/eng/CodeGen.proj @@ -24,10 +24,12 @@ <_RequiresDelayedBuild Include="@(_ProvidesReferenceOrRequiresDelay->WithMetadataValue('RequiresDelayedBuild','true')->Distinct())" /> <_SharedFrameworkAndPackageRef Include="@(_ProjectReferenceProvider->WithMetadataValue('IsAspNetCoreApp','true')->WithMetadataValue('IsPackable', 'true'))" /> <_SharedFrameworkRef Include="@(_ProjectReferenceProvider->WithMetadataValue('IsAspNetCoreApp','true')->WithMetadataValue('IsPackable', 'false'))" /> - <_TrimmableProject Include="@(_ProjectReferenceProvider->WithMetadataValue('IsTrimmable', 'true'))" /> <_ShippingAssemblyWithDupes Include="@(_ProjectReferenceProvider->WithMetadataValue('IsAspNetCoreApp', 'true'))" /> <_ShippingAssemblyWithDupes Include="@(_ProjectReferenceProvider->WithMetadataValue('IsShippingPackage', 'true'))" /> <_ShippingAssembly Include="@(_ShippingAssemblyWithDupes->Distinct())" /> + + + <_TrimmableProject Include="@(_ProvidesReferenceOrRequiresDelay->WithMetadataValue('IsTrimmable', 'true')->WithMetadataValue('IsProjectReferenceProvider','true')->Distinct())" /> diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index bbf3caca0f44..6770f6ccd9f3 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -189,9 +189,9 @@ https://github.com/dotnet/runtime f8c110b8003d68cc635add4ca791d6cf2e645561 - + https://github.com/dotnet/source-build-externals - 8fc77fa8f591051da1120ebb76c3795b7b584495 + 0603839a51f5e18b89c60a3690aff5e81fa666ca diff --git a/eng/Versions.props b/eng/Versions.props index 70c8340b33bf..4311a25a81ed 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -11,7 +11,7 @@ 0 1 true - 7.0.0-preview + 7.0.0-preview3 @@ -162,7 +162,7 @@ 8.0.0-beta.23425.2 8.0.0-beta.23425.2 - 8.0.0-alpha.1.23418.1 + 8.0.0-alpha.1.23429.1 8.0.0-alpha.1.23424.1 @@ -304,7 +304,7 @@ 0.11.2 2.2.1 1.0.2 - 13.0.1 + 13.0.3 13.0.4 1.1.6 1.28.0 diff --git a/eng/scripts/CodeCheck.ps1 b/eng/scripts/CodeCheck.ps1 index 1b6f3c5d55ed..616e3dcc6693 100644 --- a/eng/scripts/CodeCheck.ps1 +++ b/eng/scripts/CodeCheck.ps1 @@ -253,7 +253,7 @@ try { } } # Check for changes in Unshipped in servicing branches - if ($targetBranch -like 'release*' -and $targetBranch -notlike '*preview*' -and $file -like '*PublicAPI.Unshipped.txt') { + if ($targetBranch -like 'release*' -and $targetBranch -notlike '*preview*' -and $targetBranch -notlike '*rc1*' -and $targetBranch -notlike '*rc2*' -and $file -like '*PublicAPI.Unshipped.txt') { $changedAPIBaselines.Add($file) } } @@ -263,7 +263,8 @@ try { if ($changedAPIBaselines.count -gt 0) { LogError ("Detected modification to baseline API files. PublicAPI.Shipped.txt files should only " + - "be updated after a major release. See /docs/APIBaselines.md for more information.") + "be updated after a major release, and PublicAPI.Unshipped.txt files should not " + + "be updated in release branches. See /docs/APIBaselines.md for more information.") LogError "Modified API baseline files:" foreach ($file in $changedAPIBaselines) { LogError $file diff --git a/eng/test-configuration.json b/eng/test-configuration.json index 3f5af7a1b766..2dd203e2ea05 100644 --- a/eng/test-configuration.json +++ b/eng/test-configuration.json @@ -22,6 +22,7 @@ {"testName": {"contains": "HEADERS_Received_SecondRequest_ConnectProtocolReset"}}, {"testName": {"contains": "ClientUsingOldCallWithNewProtocol"}}, {"testName": {"contains": "CertificateChangedOnDisk"}}, + {"testName": {"contains": "CertificateChangedOnDisk_Symlink"}}, {"testAssembly": {"contains": "IIS"}}, {"testAssembly": {"contains": "Template"}}, {"failureMessage": {"contains":"(Site is started but no worker process found)"}}, diff --git a/eng/tools/GenerateFiles/Directory.Build.props.in b/eng/tools/GenerateFiles/Directory.Build.props.in index dd4ea40b86b6..619ec1ded3d9 100644 --- a/eng/tools/GenerateFiles/Directory.Build.props.in +++ b/eng/tools/GenerateFiles/Directory.Build.props.in @@ -8,11 +8,4 @@ true - - - - - - - diff --git a/eng/tools/RepoTasks/RepoTasks.csproj b/eng/tools/RepoTasks/RepoTasks.csproj index 5f9e4f60ae98..aa693e2131f6 100644 --- a/eng/tools/RepoTasks/RepoTasks.csproj +++ b/eng/tools/RepoTasks/RepoTasks.csproj @@ -15,6 +15,9 @@ + + diff --git a/global.json b/global.json index 5f4bd889b7dc..394a42897f46 100644 --- a/global.json +++ b/global.json @@ -1,9 +1,9 @@ { "sdk": { - "version": "8.0.100-rc.1.23381.2" + "version": "8.0.100-rc.2.23422.11" }, "tools": { - "dotnet": "8.0.100-rc.1.23381.2", + "dotnet": "8.0.100-rc.2.23422.11", "runtimes": { "dotnet/x86": [ "$(MicrosoftNETCoreBrowserDebugHostTransportVersion)" diff --git a/src/Components/Components/src/CascadingParameterAttribute.cs b/src/Components/Components/src/CascadingParameterAttribute.cs index bb9be43a5b08..becc2ce1cb57 100644 --- a/src/Components/Components/src/CascadingParameterAttribute.cs +++ b/src/Components/Components/src/CascadingParameterAttribute.cs @@ -20,5 +20,5 @@ public sealed class CascadingParameterAttribute : CascadingParameterAttributeBas /// that supplies a value with a compatible /// type. /// - public override string? Name { get; set; } + public string? Name { get; set; } } diff --git a/src/Components/Components/src/CascadingParameterAttributeBase.cs b/src/Components/Components/src/CascadingParameterAttributeBase.cs index 307743c890cb..47a50edce641 100644 --- a/src/Components/Components/src/CascadingParameterAttributeBase.cs +++ b/src/Components/Components/src/CascadingParameterAttributeBase.cs @@ -8,12 +8,6 @@ namespace Microsoft.AspNetCore.Components; /// public abstract class CascadingParameterAttributeBase : Attribute { - /// - /// Gets or sets the name for the parameter, which correlates to the name - /// of a cascading value. - /// - public abstract string? Name { get; set; } - /// /// Gets a flag indicating whether the cascading parameter should /// be supplied only once per component. diff --git a/src/Components/Components/src/CascadingValueServiceCollectionExtensions.cs b/src/Components/Components/src/CascadingValueServiceCollectionExtensions.cs index 07e0ae985b58..f4dc1ab0b892 100644 --- a/src/Components/Components/src/CascadingValueServiceCollectionExtensions.cs +++ b/src/Components/Components/src/CascadingValueServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.DependencyInjection.Extensions; namespace Microsoft.Extensions.DependencyInjection; @@ -16,11 +17,11 @@ public static class CascadingValueServiceCollectionExtensions /// /// The value type. /// The . - /// A callback that supplies a fixed value within each service provider scope. + /// A callback that supplies a fixed value within each service provider scope. /// The . public static IServiceCollection AddCascadingValue( - this IServiceCollection serviceCollection, Func valueFactory) - => serviceCollection.AddScoped(sp => new CascadingValueSource(() => valueFactory(sp), isFixed: true)); + this IServiceCollection serviceCollection, Func initialValueFactory) + => serviceCollection.AddScoped(sp => new CascadingValueSource(() => initialValueFactory(sp), isFixed: true)); /// /// Adds a cascading value to the . This is equivalent to having @@ -29,11 +30,11 @@ public static IServiceCollection AddCascadingValue( /// The value type. /// The . /// A name for the cascading value. If set, can be configured to match based on this name. - /// A callback that supplies a fixed value within each service provider scope. + /// A callback that supplies a fixed value within each service provider scope. /// The . public static IServiceCollection AddCascadingValue( - this IServiceCollection serviceCollection, string name, Func valueFactory) - => serviceCollection.AddScoped(sp => new CascadingValueSource(name, () => valueFactory(sp), isFixed: true)); + this IServiceCollection serviceCollection, string name, Func initialValueFactory) + => serviceCollection.AddScoped(sp => new CascadingValueSource(name, () => initialValueFactory(sp), isFixed: true)); /// /// Adds a cascading value to the . This is equivalent to having @@ -50,4 +51,59 @@ public static IServiceCollection AddCascadingValue( public static IServiceCollection AddCascadingValue( this IServiceCollection serviceCollection, Func> sourceFactory) => serviceCollection.AddScoped(sourceFactory); + + /// + /// Adds a cascading value to the , if none is already registered + /// with the value type. This is equivalent to having a fixed at + /// the root of the component hierarchy. + /// + /// The value type. + /// The . + /// A callback that supplies a fixed value within each service provider scope. + /// The . + public static void TryAddCascadingValue( + this IServiceCollection serviceCollection, Func valueFactory) + { + serviceCollection.TryAddEnumerable( + ServiceDescriptor.Scoped>( + sp => new CascadingValueSource(() => valueFactory(sp), isFixed: true))); + } + + /// + /// Adds a cascading value to the , if none is already registered + /// with the value type, regardless of the . This is equivalent to having a fixed + /// at the root of the component hierarchy. + /// + /// The value type. + /// The . + /// A name for the cascading value. If set, can be configured to match based on this name. + /// A callback that supplies a fixed value within each service provider scope. + /// The . + public static void TryAddCascadingValue( + this IServiceCollection serviceCollection, string name, Func valueFactory) + { + serviceCollection.TryAddEnumerable( + ServiceDescriptor.Scoped>( + sp => new CascadingValueSource(name, () => valueFactory(sp), isFixed: true))); + } + + /// + /// Adds a cascading value to the , if none is already registered + /// with the value type. This is equivalent to having a fixed at + /// the root of the component hierarchy. + /// + /// With this overload, you can supply a which allows you + /// to notify about updates to the value later, causing recipients to re-render. This overload should + /// only be used if you plan to update the value dynamically. + /// + /// The value type. + /// The . + /// A callback that supplies a within each service provider scope. + /// The . + public static void TryAddCascadingValue( + this IServiceCollection serviceCollection, Func> sourceFactory) + { + serviceCollection.TryAddEnumerable( + ServiceDescriptor.Scoped>(sourceFactory)); + } } diff --git a/src/Components/Components/src/CascadingValueSource.cs b/src/Components/Components/src/CascadingValueSource.cs index 680c022c71ee..dbf9a4662265 100644 --- a/src/Components/Components/src/CascadingValueSource.cs +++ b/src/Components/Components/src/CascadingValueSource.cs @@ -48,20 +48,20 @@ public CascadingValueSource(string name, TValue value, bool isFixed) : this(valu /// /// Constructs an instance of . /// - /// A callback that produces the initial value when first required. + /// A callback that produces the initial value when first required. /// A flag to indicate whether the value is fixed. If false, all receipients will subscribe for update notifications, which you can issue by calling . These subscriptions come at a performance cost, so if the value will not change, set to true. - public CascadingValueSource(Func valueFactory, bool isFixed) : this(isFixed) + public CascadingValueSource(Func initialValueFactory, bool isFixed) : this(isFixed) { - _initialValueFactory = valueFactory; + _initialValueFactory = initialValueFactory; } /// /// Constructs an instance of . /// /// A name for the cascading value. If set, can be configured to match based on this name. - /// A callback that produces the initial value when first required. + /// A callback that produces the initial value when first required. /// A flag to indicate whether the value is fixed. If false, all receipients will subscribe for update notifications, which you can issue by calling . These subscriptions come at a performance cost, so if the value will not change, set to true. - public CascadingValueSource(string name, Func valueFactory, bool isFixed) : this(valueFactory, isFixed) + public CascadingValueSource(string name, Func initialValueFactory, bool isFixed) : this(initialValueFactory, isFixed) { ArgumentNullException.ThrowIfNull(name); _name = name; diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index f892dc2cbd74..df77f9475253 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -1,6 +1,4 @@ #nullable enable -abstract Microsoft.AspNetCore.Components.CascadingParameterAttributeBase.Name.get -> string? -abstract Microsoft.AspNetCore.Components.CascadingParameterAttributeBase.Name.set -> void abstract Microsoft.AspNetCore.Components.RenderModeAttribute.Mode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode! Microsoft.AspNetCore.Components.CascadingParameterAttributeBase Microsoft.AspNetCore.Components.CascadingParameterAttributeBase.CascadingParameterAttributeBase() -> void @@ -10,9 +8,9 @@ Microsoft.AspNetCore.Components.CascadingParameterInfo.CascadingParameterInfo() Microsoft.AspNetCore.Components.CascadingParameterInfo.PropertyName.get -> string! Microsoft.AspNetCore.Components.CascadingParameterInfo.PropertyType.get -> System.Type! Microsoft.AspNetCore.Components.CascadingValueSource -Microsoft.AspNetCore.Components.CascadingValueSource.CascadingValueSource(string! name, System.Func! valueFactory, bool isFixed) -> void +Microsoft.AspNetCore.Components.CascadingValueSource.CascadingValueSource(string! name, System.Func! initialValueFactory, bool isFixed) -> void Microsoft.AspNetCore.Components.CascadingValueSource.CascadingValueSource(string! name, TValue value, bool isFixed) -> void -Microsoft.AspNetCore.Components.CascadingValueSource.CascadingValueSource(System.Func! valueFactory, bool isFixed) -> void +Microsoft.AspNetCore.Components.CascadingValueSource.CascadingValueSource(System.Func! initialValueFactory, bool isFixed) -> void Microsoft.AspNetCore.Components.CascadingValueSource.CascadingValueSource(TValue value, bool isFixed) -> void Microsoft.AspNetCore.Components.CascadingValueSource.NotifyChangedAsync() -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.CascadingValueSource.NotifyChangedAsync(TValue newValue) -> System.Threading.Tasks.Task! @@ -26,10 +24,10 @@ Microsoft.AspNetCore.Components.RenderHandle.DispatchExceptionAsync(System.Excep Microsoft.AspNetCore.Components.NavigationManager.ToAbsoluteUri(string? relativeUri) -> System.Uri! Microsoft.AspNetCore.Components.Rendering.ComponentState.LogicalParentComponentState.get -> Microsoft.AspNetCore.Components.Rendering.ComponentState? Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.AddComponentParameter(int sequence, string! name, Microsoft.AspNetCore.Components.IComponentRenderMode! renderMode) -> void -Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.AddComponentRenderMode(int sequence, Microsoft.AspNetCore.Components.IComponentRenderMode! renderMode) -> void *REMOVED*Microsoft.AspNetCore.Components.RouteData.RouteData(System.Type! pageType, System.Collections.Generic.IReadOnlyDictionary! routeValues) -> void *REMOVED*Microsoft.AspNetCore.Components.RouteData.RouteValues.get -> System.Collections.Generic.IReadOnlyDictionary! -Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.AddNamedEvent(int sequence, string! eventType, string! assignedName) -> void +Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.AddComponentRenderMode(Microsoft.AspNetCore.Components.IComponentRenderMode! renderMode) -> void +Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.AddNamedEvent(string! eventType, string! assignedName) -> void Microsoft.AspNetCore.Components.RenderTree.ComponentFrameFlags Microsoft.AspNetCore.Components.RenderTree.ComponentFrameFlags.HasCallerSpecifiedRenderMode = 1 -> Microsoft.AspNetCore.Components.RenderTree.ComponentFrameFlags Microsoft.AspNetCore.Components.RenderTree.NamedEventChange @@ -82,25 +80,20 @@ Microsoft.AspNetCore.Components.Sections.SectionOutlet.SectionOutlet() -> void Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.AddComponentParameter(int sequence, string! name, object? value) -> void Microsoft.AspNetCore.Components.StreamRenderingAttribute Microsoft.AspNetCore.Components.StreamRenderingAttribute.Enabled.get -> bool -Microsoft.AspNetCore.Components.StreamRenderingAttribute.StreamRenderingAttribute(bool enabled) -> void -*REMOVED*Microsoft.AspNetCore.Components.CascadingParameterAttribute.Name.get -> string? -*REMOVED*Microsoft.AspNetCore.Components.CascadingParameterAttribute.Name.set -> void +Microsoft.AspNetCore.Components.StreamRenderingAttribute.StreamRenderingAttribute(bool enabled = true) -> void Microsoft.AspNetCore.Components.SupplyParameterFromQueryProviderServiceCollectionExtensions Microsoft.Extensions.DependencyInjection.CascadingValueServiceCollectionExtensions -override Microsoft.AspNetCore.Components.CascadingParameterAttribute.Name.get -> string? -override Microsoft.AspNetCore.Components.CascadingParameterAttribute.Name.set -> void override Microsoft.AspNetCore.Components.EventCallback.GetHashCode() -> int override Microsoft.AspNetCore.Components.EventCallback.Equals(object? obj) -> bool override Microsoft.AspNetCore.Components.EventCallback.GetHashCode() -> int override Microsoft.AspNetCore.Components.EventCallback.Equals(object? obj) -> bool -*REMOVED*Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.Name.get -> string? -*REMOVED*Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.Name.set -> void -override Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.Name.get -> string? -override Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.Name.set -> void static Microsoft.AspNetCore.Components.SupplyParameterFromQueryProviderServiceCollectionExtensions.AddSupplyValueFromQueryProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.DependencyInjection.CascadingValueServiceCollectionExtensions.AddCascadingValue(this Microsoft.Extensions.DependencyInjection.IServiceCollection! serviceCollection, string! name, System.Func! valueFactory) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.CascadingValueServiceCollectionExtensions.AddCascadingValue(this Microsoft.Extensions.DependencyInjection.IServiceCollection! serviceCollection, string! name, System.Func! initialValueFactory) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Extensions.DependencyInjection.CascadingValueServiceCollectionExtensions.AddCascadingValue(this Microsoft.Extensions.DependencyInjection.IServiceCollection! serviceCollection, System.Func!>! sourceFactory) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.DependencyInjection.CascadingValueServiceCollectionExtensions.AddCascadingValue(this Microsoft.Extensions.DependencyInjection.IServiceCollection! serviceCollection, System.Func! valueFactory) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.CascadingValueServiceCollectionExtensions.AddCascadingValue(this Microsoft.Extensions.DependencyInjection.IServiceCollection! serviceCollection, System.Func! initialValueFactory) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.CascadingValueServiceCollectionExtensions.TryAddCascadingValue(this Microsoft.Extensions.DependencyInjection.IServiceCollection! serviceCollection, string! name, System.Func! valueFactory) -> void +static Microsoft.Extensions.DependencyInjection.CascadingValueServiceCollectionExtensions.TryAddCascadingValue(this Microsoft.Extensions.DependencyInjection.IServiceCollection! serviceCollection, System.Func!>! sourceFactory) -> void +static Microsoft.Extensions.DependencyInjection.CascadingValueServiceCollectionExtensions.TryAddCascadingValue(this Microsoft.Extensions.DependencyInjection.IServiceCollection! serviceCollection, System.Func! valueFactory) -> void virtual Microsoft.AspNetCore.Components.NavigationManager.Refresh(bool forceReload = false) -> void virtual Microsoft.AspNetCore.Components.Rendering.ComponentState.DisposeAsync() -> System.Threading.Tasks.ValueTask virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.AddPendingTask(Microsoft.AspNetCore.Components.Rendering.ComponentState? componentState, System.Threading.Tasks.Task! task) -> void diff --git a/src/Components/Components/src/Reflection/ComponentProperties.cs b/src/Components/Components/src/Reflection/ComponentProperties.cs index d49cfad7978a..9a0b3cbdfdb4 100644 --- a/src/Components/Components/src/Reflection/ComponentProperties.cs +++ b/src/Components/Components/src/Reflection/ComponentProperties.cs @@ -184,8 +184,7 @@ private static void ThrowForUnknownIncomingParameterName([DynamicallyAccessedMem { throw new InvalidOperationException( $"Object of type '{targetType.FullName}' has a property matching the name '{parameterName}', " + - $"but it does not have [{nameof(ParameterAttribute)}], [{nameof(CascadingParameterAttribute)}] or " + - $"[SupplyParameterFromFormAttribute] applied."); + $"but it does not have [Parameter], [CascadingParameter], or any other parameter-supplying attribute."); } else { diff --git a/src/Components/Components/src/RenderModeAttribute.cs b/src/Components/Components/src/RenderModeAttribute.cs index d9d7ff286270..87fb58c0e9de 100644 --- a/src/Components/Components/src/RenderModeAttribute.cs +++ b/src/Components/Components/src/RenderModeAttribute.cs @@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Components; /// be implemented to work across all render modes. Component authors should only specify /// a fixed rendering mode when the component is incapable of running in other modes. /// -[AttributeUsage(AttributeTargets.Class)] +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] public abstract class RenderModeAttribute : Attribute { /// diff --git a/src/Components/Components/src/RenderTree/RenderTreeFrameArrayBuilder.cs b/src/Components/Components/src/RenderTree/RenderTreeFrameArrayBuilder.cs index 3b946154a254..56cc77e9d966 100644 --- a/src/Components/Components/src/RenderTree/RenderTreeFrameArrayBuilder.cs +++ b/src/Components/Components/src/RenderTree/RenderTreeFrameArrayBuilder.cs @@ -135,7 +135,7 @@ public void AppendRegion(int sequence) }; } - public void AppendComponentRenderMode(int sequence, IComponentRenderMode renderMode) + public void AppendComponentRenderMode(IComponentRenderMode renderMode) { if (_itemsInUse == _items.Length) { @@ -144,13 +144,13 @@ public void AppendComponentRenderMode(int sequence, IComponentRenderMode renderM _items[_itemsInUse++] = new RenderTreeFrame { - SequenceField = sequence, + SequenceField = 0, // We're only interested in one of these, so it's not useful to optimize diffing over multiple FrameTypeField = RenderTreeFrameType.ComponentRenderMode, ComponentRenderModeField = renderMode, }; } - public void AppendNamedEvent(int sequence, string eventType, string assignedName) + public void AppendNamedEvent(string eventType, string assignedName) { if (_itemsInUse == _items.Length) { @@ -159,7 +159,7 @@ public void AppendNamedEvent(int sequence, string eventType, string assignedName _items[_itemsInUse++] = new RenderTreeFrame { - SequenceField = sequence, + SequenceField = 0, // We're only interested in one of these per eventType, so it's not useful to optimize diffing over multiple FrameTypeField = RenderTreeFrameType.NamedEvent, NamedEventTypeField = eventType, NamedEventAssignedNameField = assignedName, diff --git a/src/Components/Components/src/Rendering/RenderTreeBuilder.cs b/src/Components/Components/src/Rendering/RenderTreeBuilder.cs index 78f9cd39d57c..5d2cabc1ce4e 100644 --- a/src/Components/Components/src/Rendering/RenderTreeBuilder.cs +++ b/src/Components/Components/src/Rendering/RenderTreeBuilder.cs @@ -28,7 +28,7 @@ public sealed class RenderTreeBuilder : IDisposable private bool _hasSeenAddMultipleAttributes; private Dictionary? _seenAttributeNames; private IComponentRenderMode? _pendingComponentCallSiteRenderMode; // TODO: Remove when Razor compiler supports call-site @rendermode - private (int Sequence, string AssignedName)? _pendingNamedSubmitEvent; // TODO: Remove when Razor compiler supports @formname + private string? _pendingNamedSubmitEvent; // TODO: Remove when Razor compiler supports @formname /// /// The reserved parameter name used for supplying child content. @@ -83,9 +83,9 @@ public void CloseElement() // TODO: Remove this once Razor supports @formname private void CompletePendingNamedSubmitEvent() { - if (_pendingNamedSubmitEvent is { } pendingNamedSubmitEvent) + if (_pendingNamedSubmitEvent is not null) { - AddNamedEvent(pendingNamedSubmitEvent.Sequence, "onsubmit", pendingNamedSubmitEvent.AssignedName); + AddNamedEvent("onsubmit", _pendingNamedSubmitEvent); _pendingNamedSubmitEvent = default; } } @@ -241,7 +241,7 @@ public void AddAttribute(int sequence, string name, string? value) // That should compile directly as a call to AddNamedEvent. if (string.Equals(name, "@formname", StringComparison.Ordinal) && _lastNonAttributeFrameType == RenderTreeFrameType.Element) { - _pendingNamedSubmitEvent = (sequence, value!); + _pendingNamedSubmitEvent = value!; } else { @@ -623,7 +623,7 @@ public void CloseComponent() { if (_pendingComponentCallSiteRenderMode is not null) { - AddComponentRenderMode(0, _pendingComponentCallSiteRenderMode); + AddComponentRenderMode(_pendingComponentCallSiteRenderMode); _pendingComponentCallSiteRenderMode = null; } @@ -681,9 +681,8 @@ public void AddComponentReferenceCapture(int sequence, Action componentR /// /// Adds a frame indicating the render mode on the enclosing component frame. /// - /// An integer that represents the position of the instruction in the source code. /// The . - public void AddComponentRenderMode(int sequence, IComponentRenderMode renderMode) + public void AddComponentRenderMode(IComponentRenderMode renderMode) { ArgumentNullException.ThrowIfNull(renderMode); @@ -709,17 +708,16 @@ public void AddComponentRenderMode(int sequence, IComponentRenderMode renderMode parentFrame.ComponentFrameFlagsField |= ComponentFrameFlags.HasCallerSpecifiedRenderMode; - _entries.AppendComponentRenderMode(sequence, renderMode); + _entries.AppendComponentRenderMode(renderMode); _lastNonAttributeFrameType = RenderTreeFrameType.ComponentRenderMode; } /// /// Assigns a name to an event in the enclosing element. /// - /// An integer that represents the position of the instruction in the source code. /// The event type, e.g., 'onsubmit'. /// The application-assigned name. - public void AddNamedEvent(int sequence, string eventType, string assignedName) + public void AddNamedEvent(string eventType, string assignedName) { ArgumentNullException.ThrowIfNull(eventType); ArgumentException.ThrowIfNullOrEmpty(assignedName); @@ -733,7 +731,7 @@ public void AddNamedEvent(int sequence, string eventType, string assignedName) throw new InvalidOperationException($"Named events may only be added as children of frames of type {RenderTreeFrameType.Element}"); } - _entries.AppendNamedEvent(sequence, eventType, assignedName); + _entries.AppendNamedEvent(eventType, assignedName); _lastNonAttributeFrameType = RenderTreeFrameType.NamedEvent; } diff --git a/src/Components/Components/src/StreamRenderingAttribute.cs b/src/Components/Components/src/StreamRenderingAttribute.cs index 6abe38053954..3b96783de037 100644 --- a/src/Components/Components/src/StreamRenderingAttribute.cs +++ b/src/Components/Components/src/StreamRenderingAttribute.cs @@ -18,8 +18,8 @@ public class StreamRenderingAttribute : Attribute /// /// Constructs an instance of /// - /// A flag to indicate whether this component and its descendants should stream their rendering. - public StreamRenderingAttribute(bool enabled) + /// A flag to indicate whether this component and its descendants should stream their rendering. The default value is true. + public StreamRenderingAttribute(bool enabled = true) { Enabled = enabled; } diff --git a/src/Components/Components/src/SupplyParameterFromQueryAttribute.cs b/src/Components/Components/src/SupplyParameterFromQueryAttribute.cs index ffae75576ff7..9177a8d652c2 100644 --- a/src/Components/Components/src/SupplyParameterFromQueryAttribute.cs +++ b/src/Components/Components/src/SupplyParameterFromQueryAttribute.cs @@ -14,5 +14,5 @@ public sealed class SupplyParameterFromQueryAttribute : CascadingParameterAttrib /// Gets or sets the name of the querystring parameter. If null, the querystring /// parameter is assumed to have the same name as the associated property. /// - public override string? Name { get; set; } + public string? Name { get; set; } } diff --git a/src/Components/Components/src/SupplyParameterFromQueryProviderServiceCollectionExtensions.cs b/src/Components/Components/src/SupplyParameterFromQueryProviderServiceCollectionExtensions.cs index 3de3ab99ee9a..21cb8c98db19 100644 --- a/src/Components/Components/src/SupplyParameterFromQueryProviderServiceCollectionExtensions.cs +++ b/src/Components/Components/src/SupplyParameterFromQueryProviderServiceCollectionExtensions.cs @@ -50,7 +50,8 @@ public bool CanSupplyValue(in CascadingParameterInfo parameterInfo) UpdateQueryParameters(); } - var queryParameterName = parameterInfo.Attribute.Name ?? parameterInfo.PropertyName; + var attribute = (SupplyParameterFromQueryAttribute)parameterInfo.Attribute; // Must be a valid cast because we check in CanSupplyValue + var queryParameterName = attribute.Name ?? parameterInfo.PropertyName; return _queryParameterValueSupplier.GetQueryParameterValue(parameterInfo.PropertyType, queryParameterName); } diff --git a/src/Components/Components/test/CascadingParameterStateTest.cs b/src/Components/Components/test/CascadingParameterStateTest.cs index ed6420fffb25..e055b9c70801 100644 --- a/src/Components/Components/test/CascadingParameterStateTest.cs +++ b/src/Components/Components/test/CascadingParameterStateTest.cs @@ -476,8 +476,6 @@ class ComponentWithNamedCascadingParam : TestComponentBase class SupplyParameterWithSingleDeliveryAttribute : CascadingParameterAttributeBase { - public override string Name { get; set; } - internal override bool SingleDelivery => true; } @@ -523,19 +521,3 @@ public TestNavigationManager() } } } - -[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] -public sealed class SupplyParameterFromFormAttribute : CascadingParameterAttributeBase -{ - /// - /// Gets or sets the name for the parameter. The name is used to match - /// the form data and decide whether or not the value needs to be bound. - /// - public override string Name { get; set; } - - /// - /// Gets or sets the name for the handler. The name is used to match - /// the form data and decide whether or not the value needs to be bound. - /// - public string Handler { get; set; } -} diff --git a/src/Components/Components/test/CascadingParameterTest.cs b/src/Components/Components/test/CascadingParameterTest.cs index 9ce74e19708b..a99baeb96833 100644 --- a/src/Components/Components/test/CascadingParameterTest.cs +++ b/src/Components/Components/test/CascadingParameterTest.cs @@ -727,6 +727,51 @@ public void OmitsSingleDeliveryCascadingParametersWhenUpdatingDirectParameters() }); } + [Fact] + public void CanUseTryAddPatternForCascadingValuesInServiceCollection_ValueFactory() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.TryAddCascadingValue(_ => new Type1()); + services.TryAddCascadingValue(_ => new Type1()); + services.TryAddCascadingValue(_ => new Type2()); + + // Assert + Assert.Equal(2, services.Count()); + } + + [Fact] + public void CanUseTryAddPatternForCascadingValuesInServiceCollection_NamedValueFactory() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.TryAddCascadingValue("Name1", _ => new Type1()); + services.TryAddCascadingValue("Name2", _ => new Type1()); + services.TryAddCascadingValue("Name3", _ => new Type2()); + + // Assert + Assert.Equal(2, services.Count()); + } + + [Fact] + public void CanUseTryAddPatternForCascadingValuesInServiceCollection_CascadingValueSource() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.TryAddCascadingValue(_ => new CascadingValueSource("Name1", new Type1(), false)); + services.TryAddCascadingValue(_ => new CascadingValueSource("Name2", new Type1(), false)); + services.TryAddCascadingValue(_ => new CascadingValueSource("Name3", new Type2(), false)); + + // Assert + Assert.Equal(2, services.Count()); + } + private class SingleDeliveryValue(string text) { public string Text => text; @@ -734,8 +779,6 @@ private class SingleDeliveryValue(string text) private class SingleDeliveryCascadingParameterAttribute : CascadingParameterAttributeBase { - public override string Name { get; set; } - internal override bool SingleDelivery => true; } @@ -852,13 +895,11 @@ class SecondCascadingParameterConsumerComponent : CascadingParameterCons [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] class CustomCascadingParameter1Attribute : CascadingParameterAttributeBase { - public override string Name { get; set; } } [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] class CustomCascadingParameter2Attribute : CascadingParameterAttributeBase { - public override string Name { get; set; } } class CustomCascadingValueProducer : AutoRenderComponent, ICascadingValueSupplier @@ -904,7 +945,7 @@ void ICascadingValueSupplier.Unsubscribe(ComponentState subscriber, in Cascading class CustomCascadingValueConsumer1 : AutoRenderComponent { - [CustomCascadingParameter1(Name = nameof(Value))] + [CustomCascadingParameter1] public object Value { get; set; } protected override void BuildRenderTree(RenderTreeBuilder builder) @@ -915,7 +956,7 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) class CustomCascadingValueConsumer2 : AutoRenderComponent { - [CustomCascadingParameter2(Name = nameof(Value))] + [CustomCascadingParameter2] public object Value { get; set; } protected override void BuildRenderTree(RenderTreeBuilder builder) @@ -944,4 +985,7 @@ public void ChangeValue(string newValue) StringValue = newValue; } } + + class Type1 { } + class Type2 { } } diff --git a/src/Components/Components/test/ComponentFactoryTest.cs b/src/Components/Components/test/ComponentFactoryTest.cs index 7eca91d4471a..fd18139905bf 100644 --- a/src/Components/Components/test/ComponentFactoryTest.cs +++ b/src/Components/Components/test/ComponentFactoryTest.cs @@ -151,6 +151,29 @@ public void InstantiateComponent_WithRenderModeOnComponent_UsesRenderModeResolve Assert.IsType(renderer.SuppliedRenderMode); } + [Fact] + public void InstantiateComponent_WithDerivedRenderModeOnDerivedComponent_CausesAmbiguousMatchException() + { + // We could allow derived components to override the rendermode, but: + // [1] It's unclear how that would be legitimate. If the base specifies a rendermode, it's saying + // it only works in that mode. It wouldn't be safe for a derived type to change that. + // [2] If we did want to implement this, we'd need to implement our own inheritance chain walking + // to make sure we find the rendermode from the *closest* ancestor type. GetCustomAttributes + // on its own isn't documented to return the results in any specific order. + // Since issue [1] makes it unclear we'd want to support this, for now we don't. + + // Arrange + var resolvedComponent = new ComponentWithInjectProperties(); + var componentType = typeof(DerivedComponentWithRenderMode); + var renderer = new RendererWithResolveComponentForRenderMode(resolvedComponent); + var componentActivator = new DefaultComponentActivator(); + var factory = new ComponentFactory(componentActivator, renderer); + + // Act/Assert + Assert.Throws( + () => factory.InstantiateComponent(GetServiceProvider(), componentType, null, 1234)); + } + [Fact] public void InstantiateComponent_WithRenderModeOnCallSite_UsesRenderModeResolver() { @@ -290,6 +313,16 @@ public IComponent CreateInstance(Type componentType) } private class TestRenderMode : IComponentRenderMode { } + private class DerivedComponentRenderMode : IComponentRenderMode { } + + [DerivedComponentRenderMode] + private class DerivedComponentWithRenderMode : ComponentWithRenderMode + { + class DerivedComponentRenderModeAttribute : RenderModeAttribute + { + public override IComponentRenderMode Mode => new DerivedComponentRenderMode(); + } + } [OwnRenderMode] private class ComponentWithRenderMode : IComponent diff --git a/src/Components/Components/test/ParameterViewTest.Assignment.cs b/src/Components/Components/test/ParameterViewTest.Assignment.cs index 262f9584c4e4..f0308c0182a0 100644 --- a/src/Components/Components/test/ParameterViewTest.Assignment.cs +++ b/src/Components/Components/test/ParameterViewTest.Assignment.cs @@ -183,7 +183,7 @@ public void IncomingParameterMatchesPropertyNotDeclaredAsParameter_Throws() Assert.Equal(default, target.IntProp); Assert.Equal( $"Object of type '{typeof(HasPropertyWithoutParameterAttribute).FullName}' has a property matching the name '{nameof(HasPropertyWithoutParameterAttribute.IntProp)}', " + - $"but it does not have [{nameof(ParameterAttribute)}], [{nameof(CascadingParameterAttribute)}] or [{nameof(SupplyParameterFromFormAttribute)}] applied.", + "but it does not have [Parameter], [CascadingParameter], or any other parameter-supplying attribute.", ex.Message); } diff --git a/src/Components/Components/test/RenderTreeDiffBuilderTest.cs b/src/Components/Components/test/RenderTreeDiffBuilderTest.cs index 4cce00158c87..f1d0766f1841 100644 --- a/src/Components/Components/test/RenderTreeDiffBuilderTest.cs +++ b/src/Components/Components/test/RenderTreeDiffBuilderTest.cs @@ -2220,10 +2220,10 @@ public void RecognizesNamedEventBeingAdded() newTree.OpenElement(0, "existing"); newTree.AddAttribute(1, "attr1", "unrelated val1"); - newTree.AddNamedEvent(2, "someevent1", "added to existing element"); + newTree.AddNamedEvent("someevent1", "added to existing element"); newTree.CloseElement(); - newTree.OpenElement(3, "new element"); - newTree.AddNamedEvent(4, "someevent2", "added with new element"); + newTree.OpenElement(2, "new element"); + newTree.AddNamedEvent("someevent2", "added with new element"); newTree.CloseElement(); // Act @@ -2247,10 +2247,10 @@ public void RecognizesNamedEventBeingRemoved() { oldTree.OpenElement(0, "retaining"); oldTree.AddAttribute(1, "attr1", "unrelated val1"); - oldTree.AddNamedEvent(2, "someevent1", "removing from retained element"); + oldTree.AddNamedEvent("someevent1", "removing from retained element"); oldTree.CloseElement(); - oldTree.OpenElement(3, "removing"); - oldTree.AddNamedEvent(4, "someevent2", "removed because element was removed"); + oldTree.OpenElement(2, "removing"); + oldTree.AddNamedEvent("someevent2", "removed because element was removed"); oldTree.CloseElement(); newTree.OpenElement(0, "retaining"); @@ -2272,12 +2272,12 @@ public void RecognizesNamedEventBeingRemoved() public void RecognizesNamedEventBeingMoved() { oldTree.OpenElement(0, "elem"); - oldTree.AddNamedEvent(2, "eventname", "assigned name"); + oldTree.AddNamedEvent("eventname", "assigned name"); oldTree.CloseElement(); newTree.OpenElement(0, "elem"); newTree.AddAttribute(1, "attr1", "unrelated val1"); - newTree.AddNamedEvent(2, "eventname", "assigned name"); + newTree.AddNamedEvent("eventname", "assigned name"); newTree.CloseElement(); // Act @@ -2300,13 +2300,13 @@ public void RecognizesNamedEventBeingMoved() public void RecognizesNamedEventChangingAssignedName() { oldTree.OpenElement(0, "elem"); - oldTree.AddNamedEvent(1, "eventname1", "original name"); - oldTree.AddNamedEvent(2, "eventname2", "will be left unchanged"); + oldTree.AddNamedEvent("eventname1", "original name"); + oldTree.AddNamedEvent("eventname2", "will be left unchanged"); oldTree.CloseElement(); newTree.OpenElement(0, "elem"); - newTree.AddNamedEvent(1, "eventname1", "changed name"); - newTree.AddNamedEvent(2, "eventname2", "will be left unchanged"); + newTree.AddNamedEvent("eventname1", "changed name"); + newTree.AddNamedEvent("eventname2", "will be left unchanged"); newTree.CloseElement(); // Act diff --git a/src/Components/Components/test/RendererTest.cs b/src/Components/Components/test/RendererTest.cs index 1b7a91792bad..ea383da376ee 100644 --- a/src/Components/Components/test/RendererTest.cs +++ b/src/Components/Components/test/RendererTest.cs @@ -5002,7 +5002,7 @@ public void ThrowsForUnknownRenderMode_AtCallSite() var component = new TestComponent(builder => { builder.OpenComponent(0); - builder.AddComponentRenderMode(1, new ComponentWithUnknownRenderMode.UnknownRenderMode()); + builder.AddComponentRenderMode(new ComponentWithUnknownRenderMode.UnknownRenderMode()); builder.CloseComponent(); }); @@ -5046,7 +5046,7 @@ public void RenderModeResolverCanSupplyComponent_CallSiteRenderMode() { builder.OpenComponent(0); builder.AddComponentParameter(1, nameof(MessageComponent.Message), "Some message"); - builder.AddComponentRenderMode(2, new SubstituteComponentRenderMode()); + builder.AddComponentRenderMode(new SubstituteComponentRenderMode()); builder.CloseComponent(); }); diff --git a/src/Components/Components/test/Rendering/RenderTreeBuilderTest.cs b/src/Components/Components/test/Rendering/RenderTreeBuilderTest.cs index 07f6e5e4554b..8ef9bbea6dac 100644 --- a/src/Components/Components/test/Rendering/RenderTreeBuilderTest.cs +++ b/src/Components/Components/test/Rendering/RenderTreeBuilderTest.cs @@ -2110,7 +2110,7 @@ public void CanAddComponentRenderMode() // Act builder.OpenComponent(0); builder.AddComponentParameter(1, "param", 123); - builder.AddComponentRenderMode(2, renderMode); + builder.AddComponentRenderMode(renderMode); builder.CloseComponent(); // Assert @@ -2122,7 +2122,7 @@ public void CanAddComponentRenderMode() Assert.True(frame.ComponentFrameFlags.HasFlag(ComponentFrameFlags.HasCallerSpecifiedRenderMode)); }, frame => AssertFrame.Attribute(frame, "param", 123, 1), - frame => AssertFrame.ComponentRenderMode(frame, renderMode, 2)); + frame => AssertFrame.ComponentRenderMode(frame, renderMode)); } [Fact] @@ -2135,7 +2135,7 @@ public void CannotAddComponentRenderModeToElement() // Act/Assert var ex = Assert.Throws(() => { - builder.AddComponentRenderMode(1, new TestRenderMode()); + builder.AddComponentRenderMode(new TestRenderMode()); }); Assert.Equal($"The enclosing frame is not of the required type '{nameof(RenderTreeFrameType.Component)}'.", ex.Message); } @@ -2150,7 +2150,7 @@ public void CannotAddNullComponentRenderMode() // Act/Assert var ex = Assert.Throws(() => { - builder.AddComponentRenderMode(1, null); + builder.AddComponentRenderMode(null); }); Assert.Equal("renderMode", ex.ParamName); } @@ -2161,7 +2161,7 @@ public void CannotAddParametersAfterComponentRenderMode() // Arrange var builder = new RenderTreeBuilder(); builder.OpenComponent(0); - builder.AddComponentRenderMode(1, new TestRenderMode()); + builder.AddComponentRenderMode(new TestRenderMode()); // Act/Assert var ex = Assert.Throws(() => @@ -2201,7 +2201,7 @@ public void TemporaryApiForCallSiteComponentRenderModeWorksEvenIfOtherParameterA }, frame => AssertFrame.Attribute(frame, "param", 123, 1), frame => AssertFrame.Attribute(frame, "anotherparam", 456, 3), - frame => AssertFrame.ComponentRenderMode(frame, renderMode, 0)); + frame => AssertFrame.ComponentRenderMode(frame, renderMode)); } [Fact] @@ -2214,7 +2214,7 @@ public void CanAddNamedEvent() // Act builder.OpenElement(0, "elem"); builder.AddAttribute(1, "attr", 123); - builder.AddNamedEvent(2, "myeventtype", "my event name"); + builder.AddNamedEvent("myeventtype", "my event name"); builder.CloseElement(); // Assert @@ -2235,7 +2235,7 @@ public void CannotAddNamedEventToComponent() // Act/Assert var ex = Assert.Throws(() => { - builder.AddNamedEvent(1, "x", "y"); + builder.AddNamedEvent("x", "y"); }); Assert.Equal($"Named events may only be added as children of frames of type {RenderTreeFrameType.Element}", ex.Message); } @@ -2250,7 +2250,7 @@ public void CannotAddNamedEventWithNullEventType() // Act/Assert var ex = Assert.Throws(() => { - builder.AddNamedEvent(1, null, "assigned name"); + builder.AddNamedEvent(null, "assigned name"); }); Assert.Equal("eventType", ex.ParamName); } @@ -2265,7 +2265,7 @@ public void CannotAddNamedEventWithNullAssignedName() // Act/Assert var ex = Assert.Throws(() => { - builder.AddNamedEvent(1, "eventtype", null); + builder.AddNamedEvent("eventtype", null); }); Assert.Equal("assignedName", ex.ParamName); } @@ -2280,7 +2280,7 @@ public void CannotAddNamedEventWithEmptyAssignedName() // Act/Assert var ex = Assert.Throws(() => { - builder.AddNamedEvent(1, "eventtype", ""); + builder.AddNamedEvent("eventtype", ""); }); Assert.Equal("assignedName", ex.ParamName); } @@ -2291,7 +2291,7 @@ public void CannotAddAttributesAfterNamedEvent() // Arrange var builder = new RenderTreeBuilder(); builder.OpenElement(0, "elem"); - builder.AddNamedEvent(1, "someevent", "somename"); + builder.AddNamedEvent("someevent", "somename"); // Act/Assert var ex = Assert.Throws(() => @@ -2325,7 +2325,7 @@ public void TemporaryApiForFormNameEventsWorksEvenIfAttributesAddedAfter() frame => AssertFrame.Element(frame, "div", 5, 0), frame => AssertFrame.Attribute(frame, "attr1", "123", 1), frame => AssertFrame.Attribute(frame, "attr2", "456", 3), - frame => AssertFrame.NamedEvent(frame, "onsubmit", "some custom name", 2), + frame => AssertFrame.NamedEvent(frame, "onsubmit", "some custom name"), frame => AssertFrame.Element(frame, "other", 1)); } diff --git a/src/Components/Endpoints/src/Builder/ConfiguredRenderModesMetadata.cs b/src/Components/Endpoints/src/Builder/ConfiguredRenderModesMetadata.cs new file mode 100644 index 000000000000..65c99a924843 --- /dev/null +++ b/src/Components/Endpoints/src/Builder/ConfiguredRenderModesMetadata.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components.Endpoints; + +internal class ConfiguredRenderModesMetadata(IComponentRenderMode[] configuredRenderModes) +{ + public IComponentRenderMode[] ConfiguredRenderModes => configuredRenderModes; +} diff --git a/src/Components/Endpoints/src/Builder/RazorComponentEndpointDataSource.cs b/src/Components/Endpoints/src/Builder/RazorComponentEndpointDataSource.cs index eac2bd011495..8013ab54794d 100644 --- a/src/Components/Endpoints/src/Builder/RazorComponentEndpointDataSource.cs +++ b/src/Components/Endpoints/src/Builder/RazorComponentEndpointDataSource.cs @@ -24,22 +24,27 @@ internal class RazorComponentEndpointDataSource<[DynamicallyAccessedMembers(Comp private readonly IApplicationBuilder _applicationBuilder; private readonly RenderModeEndpointProvider[] _renderModeEndpointProviders; private readonly RazorComponentEndpointFactory _factory; - + private readonly HotReloadService? _hotReloadService; private List? _endpoints; - // TODO: Implement endpoint data source updates https://github.com/dotnet/aspnetcore/issues/47026 - private readonly CancellationTokenSource _cancellationTokenSource; - private readonly IChangeToken _changeToken; + private CancellationTokenSource _cancellationTokenSource; + private IChangeToken _changeToken; + + // Internal for testing. + internal ComponentApplicationBuilder Builder => _builder; + internal List> Conventions => _conventions; public RazorComponentEndpointDataSource( ComponentApplicationBuilder builder, IEnumerable renderModeEndpointProviders, IApplicationBuilder applicationBuilder, - RazorComponentEndpointFactory factory) + RazorComponentEndpointFactory factory, + HotReloadService? hotReloadService = null) { _builder = builder; _applicationBuilder = applicationBuilder; _renderModeEndpointProviders = renderModeEndpointProviders.ToArray(); _factory = factory; + _hotReloadService = hotReloadService; DefaultBuilder = new RazorComponentsEndpointConventionBuilder( _lock, builder, @@ -62,7 +67,7 @@ public override IReadOnlyList Endpoints // The order is as follows: // * MapRazorComponents gets called and the data source gets created. // * The RazorComponentEndpointConventionBuilder is returned and the user gets a chance to call on it to add conventions. - // * The first request arrives and the DfaMatcherBuilder acesses the data sources to get the endpoints. + // * The first request arrives and the DfaMatcherBuilder accesses the data sources to get the endpoints. // * The endpoints get created and the conventions get applied. Initialize(); Debug.Assert(_changeToken != null); @@ -89,47 +94,63 @@ private void Initialize() private void UpdateEndpoints() { - var endpoints = new List(); - var context = _builder.Build(); - - foreach (var definition in context.Pages) + lock (_lock) { - _factory.AddEndpoints(endpoints, typeof(TRootComponent), definition, _conventions, _finallyConventions); - } + var endpoints = new List(); + var context = _builder.Build(); + var configuredRenderModesMetadata = new ConfiguredRenderModesMetadata( + Options.ConfiguredRenderModes.ToArray()); - ICollection renderModes = Options.ConfiguredRenderModes; + foreach (var definition in context.Pages) + { + _factory.AddEndpoints(endpoints, typeof(TRootComponent), definition, _conventions, _finallyConventions, configuredRenderModesMetadata); + } - foreach (var renderMode in renderModes) - { - var found = false; - foreach (var provider in _renderModeEndpointProviders) + ICollection renderModes = Options.ConfiguredRenderModes; + + foreach (var renderMode in renderModes) { - if (provider.Supports(renderMode)) + var found = false; + foreach (var provider in _renderModeEndpointProviders) + { + if (provider.Supports(renderMode)) + { + found = true; + RenderModeEndpointProvider.AddEndpoints( + endpoints, + typeof(TRootComponent), + provider.GetEndpointBuilders(renderMode, _applicationBuilder.New()), + renderMode, + _conventions, + _finallyConventions); + } + } + + if (!found) { - found = true; - RenderModeEndpointProvider.AddEndpoints( - endpoints, - typeof(TRootComponent), - provider.GetEndpointBuilders(renderMode, _applicationBuilder.New()), - renderMode, - _conventions, - _finallyConventions); + throw new InvalidOperationException($"Unable to find a provider for the render mode: {renderMode.GetType().FullName}. This generally " + + "means that a call to 'AddWebAssemblyComponents' or 'AddServerComponents' is missing. " + + "For example, change builder.Services.AddRazorComponents() to builder.Services.AddRazorComponents().AddServerComponents()."); } } - if (!found) + var oldCancellationTokenSource = _cancellationTokenSource; + _endpoints = endpoints; + _cancellationTokenSource = new CancellationTokenSource(); + _changeToken = new CancellationChangeToken(_cancellationTokenSource.Token); + oldCancellationTokenSource?.Cancel(); + if (_hotReloadService is { MetadataUpdateSupported : true }) { - throw new InvalidOperationException($"Unable to find a provider for the render mode: {renderMode.GetType().FullName}. This generally " + - $"means that a call to 'AddWebAssemblyComponents' or 'AddServerComponents' is missing. " + - $"Alternatively call 'AddWebAssemblyRenderMode', 'AddServerRenderMode' might be missing if you have set UseDeclaredRenderModes = false."); + ChangeToken.OnChange(_hotReloadService.GetChangeToken, UpdateEndpoints); } } - - _endpoints = endpoints; } + public override IChangeToken GetChangeToken() { - // TODO: Handle updates if necessary (for hot reload). + Initialize(); + Debug.Assert(_changeToken != null); + Debug.Assert(_endpoints != null); return _changeToken; } } diff --git a/src/Components/Endpoints/src/Builder/RazorComponentEndpointDataSourceFactory.cs b/src/Components/Endpoints/src/Builder/RazorComponentEndpointDataSourceFactory.cs index 3c66aa7264dc..3dcee668151a 100644 --- a/src/Components/Endpoints/src/Builder/RazorComponentEndpointDataSourceFactory.cs +++ b/src/Components/Endpoints/src/Builder/RazorComponentEndpointDataSourceFactory.cs @@ -14,13 +14,16 @@ internal class RazorComponentEndpointDataSourceFactory { private readonly RazorComponentEndpointFactory _factory; private readonly IEnumerable _providers; + private readonly HotReloadService? _hotReloadService; public RazorComponentEndpointDataSourceFactory( RazorComponentEndpointFactory factory, - IEnumerable providers) + IEnumerable providers, + HotReloadService? hotReloadService = null) { _factory = factory; _providers = providers; + _hotReloadService = hotReloadService; } public RazorComponentEndpointDataSource CreateDataSource<[DynamicallyAccessedMembers(Component)] TRootComponent>(IEndpointRouteBuilder endpoints) @@ -28,6 +31,6 @@ public RazorComponentEndpointDataSourceFactory( var builder = ComponentApplicationBuilder.GetBuilder() ?? DefaultRazorComponentApplication.Instance.GetBuilder(); - return new RazorComponentEndpointDataSource(builder, _providers, endpoints.CreateApplicationBuilder(), _factory); + return new RazorComponentEndpointDataSource(builder, _providers, endpoints.CreateApplicationBuilder(), _factory, _hotReloadService); } } diff --git a/src/Components/Endpoints/src/Builder/RazorComponentEndpointFactory.cs b/src/Components/Endpoints/src/Builder/RazorComponentEndpointFactory.cs index c14ae30a8f2b..117aaea090f3 100644 --- a/src/Components/Endpoints/src/Builder/RazorComponentEndpointFactory.cs +++ b/src/Components/Endpoints/src/Builder/RazorComponentEndpointFactory.cs @@ -24,7 +24,8 @@ internal void AddEndpoints( [DynamicallyAccessedMembers(Component)] Type rootComponent, PageComponentInfo pageDefinition, IReadOnlyList> conventions, - IReadOnlyList> finallyConventions) + IReadOnlyList> finallyConventions, + ConfiguredRenderModesMetadata configuredRenderModesMetadata) { // We do not provide a way to establish the order or the name for the page routes. // Order is not supported in our client router. @@ -48,6 +49,7 @@ internal void AddEndpoints( builder.Metadata.Add(HttpMethodsMetadata); builder.Metadata.Add(new ComponentTypeMetadata(pageDefinition.Type)); builder.Metadata.Add(new RootComponentMetadata(rootComponent)); + builder.Metadata.Add(configuredRenderModesMetadata); foreach (var convention in conventions) { diff --git a/src/Components/Endpoints/src/DependencyInjection/HotReloadService.cs b/src/Components/Endpoints/src/DependencyInjection/HotReloadService.cs new file mode 100644 index 000000000000..51680ca61034 --- /dev/null +++ b/src/Components/Endpoints/src/DependencyInjection/HotReloadService.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection.Metadata; +using Microsoft.Extensions.Primitives; + +[assembly: MetadataUpdateHandler(typeof(Microsoft.AspNetCore.Components.Endpoints.HotReloadService))] + +namespace Microsoft.AspNetCore.Components.Endpoints; + +internal sealed class HotReloadService : IDisposable +{ + public HotReloadService() + { + UpdateApplicationEvent += NotifyUpdateApplication; + MetadataUpdateSupported = MetadataUpdater.IsSupported; + } + + private CancellationTokenSource _tokenSource = new(); + private static event Action? UpdateApplicationEvent; + + public bool MetadataUpdateSupported { get; internal set; } + + public IChangeToken GetChangeToken() => new CancellationChangeToken(_tokenSource.Token); + + public static void UpdateApplication(Type[]? changedTypes) + { + UpdateApplicationEvent?.Invoke(changedTypes); + } + + private void NotifyUpdateApplication(Type[]? changedTypes) + { + var current = Interlocked.Exchange(ref _tokenSource, new CancellationTokenSource()); + current.Cancel(); + } + + public void Dispose() + { + UpdateApplicationEvent -= NotifyUpdateApplication; + _tokenSource.Dispose(); + } +} diff --git a/src/Components/Endpoints/src/DependencyInjection/HttpNavigationManager.cs b/src/Components/Endpoints/src/DependencyInjection/HttpNavigationManager.cs index 0da30332d2c4..8098deb3b6ce 100644 --- a/src/Components/Endpoints/src/DependencyInjection/HttpNavigationManager.cs +++ b/src/Components/Endpoints/src/DependencyInjection/HttpNavigationManager.cs @@ -11,6 +11,7 @@ internal sealed class HttpNavigationManager : NavigationManager, IHostEnvironmen protected override void NavigateToCore(string uri, bool forceLoad) { - throw new NavigationException(uri); + var absoluteUriString = ToAbsoluteUri(uri).ToString(); + throw new NavigationException(absoluteUriString); } } diff --git a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs index 3b143bb6cff2..d361c17687c4 100644 --- a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs +++ b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; +using System.Reflection.Metadata; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Endpoints; using Microsoft.AspNetCore.Components.Endpoints.DependencyInjection; @@ -39,12 +40,13 @@ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection services.TryAddSingleton(); - // Results - services.TryAddSingleton(); - // Endpoints services.TryAddSingleton(); services.TryAddSingleton(); + if (MetadataUpdater.IsSupported) + { + services.TryAddSingleton(); + } services.TryAddScoped(); // Common services required for components server side rendering @@ -63,6 +65,7 @@ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection services.TryAddScoped(); services.TryAddScoped(sp => sp.GetRequiredService()); services.AddSupplyValueFromQueryProvider(); + services.TryAddCascadingValue(sp => sp.GetRequiredService().HttpContext); // Form handling services.AddSupplyValueFromFormProvider(); diff --git a/src/Components/Endpoints/src/PublicAPI.Unshipped.txt b/src/Components/Endpoints/src/PublicAPI.Unshipped.txt index a4f923605d68..a3ca0d2f9115 100644 --- a/src/Components/Endpoints/src/PublicAPI.Unshipped.txt +++ b/src/Components/Endpoints/src/PublicAPI.Unshipped.txt @@ -18,25 +18,6 @@ Microsoft.AspNetCore.Components.Endpoints.Infrastructure.RenderModeEndpointProvi Microsoft.AspNetCore.Components.Endpoints.Infrastructure.RenderModeEndpointProvider.RenderModeEndpointProvider() -> void Microsoft.AspNetCore.Components.Endpoints.IRazorComponentEndpointInvoker Microsoft.AspNetCore.Components.Endpoints.IRazorComponentEndpointInvoker.Render(Microsoft.AspNetCore.Http.HttpContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Components.Endpoints.RazorComponentResult -Microsoft.AspNetCore.Components.Endpoints.RazorComponentResult.ComponentType.get -> System.Type! -Microsoft.AspNetCore.Components.Endpoints.RazorComponentResult.ContentType.get -> string? -Microsoft.AspNetCore.Components.Endpoints.RazorComponentResult.ContentType.set -> void -Microsoft.AspNetCore.Components.Endpoints.RazorComponentResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Components.Endpoints.RazorComponentResult.Parameters.get -> System.Collections.Generic.IReadOnlyDictionary! -Microsoft.AspNetCore.Components.Endpoints.RazorComponentResult.PreventStreamingRendering.get -> bool -Microsoft.AspNetCore.Components.Endpoints.RazorComponentResult.PreventStreamingRendering.set -> void -Microsoft.AspNetCore.Components.Endpoints.RazorComponentResult.RazorComponentResult(System.Type! componentType) -> void -Microsoft.AspNetCore.Components.Endpoints.RazorComponentResult.RazorComponentResult(System.Type! componentType, object? parameters) -> void -Microsoft.AspNetCore.Components.Endpoints.RazorComponentResult.RazorComponentResult(System.Type! componentType, System.Collections.Generic.IReadOnlyDictionary? parameters) -> void -Microsoft.AspNetCore.Components.Endpoints.RazorComponentResult.StatusCode.get -> int? -Microsoft.AspNetCore.Components.Endpoints.RazorComponentResult.StatusCode.set -> void -Microsoft.AspNetCore.Components.Endpoints.RazorComponentResult -Microsoft.AspNetCore.Components.Endpoints.RazorComponentResult.RazorComponentResult() -> void -Microsoft.AspNetCore.Components.Endpoints.RazorComponentResult.RazorComponentResult(object! parameters) -> void -Microsoft.AspNetCore.Components.Endpoints.RazorComponentResult.RazorComponentResult(System.Collections.Generic.IReadOnlyDictionary! parameters) -> void -Microsoft.AspNetCore.Components.Endpoints.RazorComponentResultExecutor -Microsoft.AspNetCore.Components.Endpoints.RazorComponentResultExecutor.RazorComponentResultExecutor() -> void Microsoft.AspNetCore.Components.Endpoints.RazorComponentsOptions Microsoft.AspNetCore.Components.Endpoints.RazorComponentsOptions.MaxFormMappingCollectionSize.get -> int Microsoft.AspNetCore.Components.Endpoints.RazorComponentsOptions.MaxFormMappingCollectionSize.set -> void @@ -54,11 +35,26 @@ Microsoft.AspNetCore.Components.PersistedStateSerializationMode Microsoft.AspNetCore.Components.PersistedStateSerializationMode.Infer = 1 -> Microsoft.AspNetCore.Components.PersistedStateSerializationMode Microsoft.AspNetCore.Components.PersistedStateSerializationMode.Server = 2 -> Microsoft.AspNetCore.Components.PersistedStateSerializationMode Microsoft.AspNetCore.Components.PersistedStateSerializationMode.WebAssembly = 3 -> Microsoft.AspNetCore.Components.PersistedStateSerializationMode +Microsoft.AspNetCore.Http.HttpResults.RazorComponentResult +Microsoft.AspNetCore.Http.HttpResults.RazorComponentResult.ComponentType.get -> System.Type! +Microsoft.AspNetCore.Http.HttpResults.RazorComponentResult.ContentType.get -> string? +Microsoft.AspNetCore.Http.HttpResults.RazorComponentResult.ContentType.set -> void +Microsoft.AspNetCore.Http.HttpResults.RazorComponentResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Http.HttpResults.RazorComponentResult.Parameters.get -> System.Collections.Generic.IReadOnlyDictionary! +Microsoft.AspNetCore.Http.HttpResults.RazorComponentResult.PreventStreamingRendering.get -> bool +Microsoft.AspNetCore.Http.HttpResults.RazorComponentResult.PreventStreamingRendering.set -> void +Microsoft.AspNetCore.Http.HttpResults.RazorComponentResult.RazorComponentResult(System.Type! componentType) -> void +Microsoft.AspNetCore.Http.HttpResults.RazorComponentResult.RazorComponentResult(System.Type! componentType, object! parameters) -> void +Microsoft.AspNetCore.Http.HttpResults.RazorComponentResult.RazorComponentResult(System.Type! componentType, System.Collections.Generic.IReadOnlyDictionary! parameters) -> void +Microsoft.AspNetCore.Http.HttpResults.RazorComponentResult.StatusCode.get -> int? +Microsoft.AspNetCore.Http.HttpResults.RazorComponentResult.StatusCode.set -> void +Microsoft.AspNetCore.Http.HttpResults.RazorComponentResult +Microsoft.AspNetCore.Http.HttpResults.RazorComponentResult.RazorComponentResult() -> void +Microsoft.AspNetCore.Http.HttpResults.RazorComponentResult.RazorComponentResult(object! parameters) -> void +Microsoft.AspNetCore.Http.HttpResults.RazorComponentResult.RazorComponentResult(System.Collections.Generic.IReadOnlyDictionary! parameters) -> void Microsoft.Extensions.DependencyInjection.IRazorComponentsBuilder Microsoft.Extensions.DependencyInjection.IRazorComponentsBuilder.Services.get -> Microsoft.Extensions.DependencyInjection.IServiceCollection! Microsoft.Extensions.DependencyInjection.RazorComponentsServiceCollectionExtensions static Microsoft.AspNetCore.Builder.RazorComponentsEndpointConventionBuilderExtensions.AddAdditionalAssemblies(this Microsoft.AspNetCore.Builder.RazorComponentsEndpointConventionBuilder! builder, params System.Reflection.Assembly![]! assemblies) -> Microsoft.AspNetCore.Builder.RazorComponentsEndpointConventionBuilder! static Microsoft.AspNetCore.Builder.RazorComponentsEndpointRouteBuilderExtensions.MapRazorComponents(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints) -> Microsoft.AspNetCore.Builder.RazorComponentsEndpointConventionBuilder! static Microsoft.Extensions.DependencyInjection.RazorComponentsServiceCollectionExtensions.AddRazorComponents(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action? configure = null) -> Microsoft.Extensions.DependencyInjection.IRazorComponentsBuilder! -static readonly Microsoft.AspNetCore.Components.Endpoints.RazorComponentResultExecutor.DefaultContentType -> string! -virtual Microsoft.AspNetCore.Components.Endpoints.RazorComponentResultExecutor.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext, Microsoft.AspNetCore.Components.Endpoints.RazorComponentResult! result) -> System.Threading.Tasks.Task! diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs index 979cfaa17e16..9a8663ec5098 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs @@ -29,7 +29,7 @@ protected override IComponent ResolveComponentForRenderMode([DynamicallyAccessed else { // This component is the start of a subtree with a rendermode, so introduce a new rendermode boundary here - return new SSRRenderModeBoundary(componentType, renderMode); + return new SSRRenderModeBoundary(_httpContext, componentType, renderMode); } } @@ -84,7 +84,7 @@ public async ValueTask PrerenderComponentAsync( { var rootComponent = prerenderMode is null ? InstantiateComponent(componentType) - : new SSRRenderModeBoundary(componentType, prerenderMode); + : new SSRRenderModeBoundary(_httpContext, componentType, prerenderMode); var htmlRootComponent = await Dispatcher.InvokeAsync(() => BeginRenderingComponent(rootComponent, parameters)); var result = new PrerenderedComponentHtmlContent(Dispatcher, htmlRootComponent); diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs index 1118dfeb5825..0c46325988a1 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs @@ -52,6 +52,8 @@ public EndpointHtmlRenderer(IServiceProvider serviceProvider, ILoggerFactory log _services = serviceProvider; } + internal HttpContext? HttpContext => _httpContext; + private void SetHttpContext(HttpContext httpContext) { if (_httpContext is null) diff --git a/src/Components/Endpoints/src/Rendering/SSRRenderModeBoundary.cs b/src/Components/Endpoints/src/Rendering/SSRRenderModeBoundary.cs index 8f1529e3e0a0..f0ac9aaba6db 100644 --- a/src/Components/Endpoints/src/Rendering/SSRRenderModeBoundary.cs +++ b/src/Components/Endpoints/src/Rendering/SSRRenderModeBoundary.cs @@ -7,6 +7,7 @@ using System.Globalization; using System.Security.Cryptography; using System.Text; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Http; @@ -31,8 +32,13 @@ internal class SSRRenderModeBoundary : IComponent private IReadOnlyDictionary? _latestParameters; private string? _markerKey; - public SSRRenderModeBoundary([DynamicallyAccessedMembers(Component)] Type componentType, IComponentRenderMode renderMode) + public SSRRenderModeBoundary( + HttpContext httpContext, + [DynamicallyAccessedMembers(Component)] Type componentType, + IComponentRenderMode renderMode) { + AssertRenderModeIsConfigured(httpContext, componentType, renderMode); + _componentType = componentType; _renderMode = renderMode; _prerender = renderMode switch @@ -44,6 +50,50 @@ public SSRRenderModeBoundary([DynamicallyAccessedMembers(Component)] Type compon }; } + private static void AssertRenderModeIsConfigured(HttpContext httpContext, Type componentType, IComponentRenderMode renderMode) + { + var configuredRenderModesMetadata = httpContext.GetEndpoint()?.Metadata.GetMetadata(); + if (configuredRenderModesMetadata is null) + { + // This is not a Razor Components endpoint. It might be that the app is using RazorComponentResult, + // or perhaps something else has changed the endpoint dynamically. In this case we don't know how + // the app is configured so we just proceed and allow any errors to happen if the client-side code + // later tries to reach endpoints that aren't mapped. + return; + } + + var configuredModes = configuredRenderModesMetadata.ConfiguredRenderModes; + + // We have to allow for specified rendermodes being subclases of the known types + if (renderMode is ServerRenderMode || renderMode is AutoRenderMode) + { + AssertRenderModeIsConfigured(componentType, renderMode, configuredModes, "AddServerRenderMode"); + } + + if (renderMode is WebAssemblyRenderMode || renderMode is AutoRenderMode) + { + AssertRenderModeIsConfigured(componentType, renderMode, configuredModes, "AddWebAssemblyRenderMode"); + } + } + + private static void AssertRenderModeIsConfigured(Type componentType, IComponentRenderMode specifiedMode, IComponentRenderMode[] configuredModes, string expectedCall) where TRequiredMode: IComponentRenderMode + { + foreach (var configuredMode in configuredModes) + { + // We have to allow for configured rendermodes being subclases of the known types + if (configuredMode is TRequiredMode) + { + return; + } + } + + throw new InvalidOperationException($"A component of type '{componentType}' has render mode '{specifiedMode.GetType().Name}', " + + $"but the required endpoints are not mapped on the server. When calling " + + $"'{nameof(RazorComponentsEndpointRouteBuilderExtensions.MapRazorComponents)}', add a call to " + + $"'{expectedCall}'. For example, " + + $"'builder.{nameof(RazorComponentsEndpointRouteBuilderExtensions.MapRazorComponents)}<...>.{expectedCall}()'"); + } + public void Attach(RenderHandle renderHandle) { _renderHandle = renderHandle; diff --git a/src/Components/Endpoints/src/Results/RazorComponentResult.cs b/src/Components/Endpoints/src/Results/RazorComponentResult.cs index 3049313f9a21..0f4e385ac288 100644 --- a/src/Components/Endpoints/src/Results/RazorComponentResult.cs +++ b/src/Components/Endpoints/src/Results/RazorComponentResult.cs @@ -1,18 +1,19 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Endpoints; using Microsoft.Extensions.Internal; using static Microsoft.AspNetCore.Internal.LinkerFlags; -namespace Microsoft.AspNetCore.Components.Endpoints; +namespace Microsoft.AspNetCore.Http.HttpResults; /// /// An that renders a Razor Component. /// -public class RazorComponentResult : IResult +public class RazorComponentResult : IResult, IStatusCodeHttpResult, IContentTypeHttpResult { private static readonly IReadOnlyDictionary EmptyParameters = new Dictionary().AsReadOnly(); @@ -22,7 +23,7 @@ public class RazorComponentResult : IResult /// /// The type of the component to render. This must implement . public RazorComponentResult([DynamicallyAccessedMembers(Component)] Type componentType) - : this(componentType, null) + : this(componentType, ReadOnlyDictionary.Empty) { } @@ -31,7 +32,9 @@ public RazorComponentResult([DynamicallyAccessedMembers(Component)] Type compone /// /// The type of the component to render. This must implement . /// Parameters for the component. - public RazorComponentResult([DynamicallyAccessedMembers(Component)] Type componentType, object? parameters) + public RazorComponentResult( + [DynamicallyAccessedMembers(Component)] Type componentType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] object parameters) : this(componentType, CoerceParametersObjectToDictionary(parameters)) { } @@ -41,25 +44,27 @@ public RazorComponentResult([DynamicallyAccessedMembers(Component)] Type compone /// /// The type of the component to render. This must implement . /// Parameters for the component. - public RazorComponentResult([DynamicallyAccessedMembers(Component)] Type componentType, IReadOnlyDictionary? parameters) + public RazorComponentResult( + [DynamicallyAccessedMembers(Component)] Type componentType, + IReadOnlyDictionary parameters) { + ArgumentNullException.ThrowIfNull(componentType); + ArgumentNullException.ThrowIfNull(parameters); + // Note that the Blazor renderer will validate that componentType implements IComponent and throws a suitable // exception if not, so we don't need to duplicate that logic here. - - ArgumentNullException.ThrowIfNull(componentType); ComponentType = componentType; Parameters = parameters ?? EmptyParameters; } - private static IReadOnlyDictionary? CoerceParametersObjectToDictionary(object? parameters) + private static IReadOnlyDictionary CoerceParametersObjectToDictionary(object? parameters) => parameters is null - ? null + ? throw new ArgumentNullException(nameof(parameters)) : (IReadOnlyDictionary)PropertyHelper.ObjectToDictionary(parameters); /// /// Gets the component type. /// - [DynamicallyAccessedMembers(Component)] public Type ComponentType { get; } /// @@ -87,15 +92,10 @@ public RazorComponentResult([DynamicallyAccessedMembers(Component)] Type compone public bool PreventStreamingRendering { get; set; } /// - /// Requests the service of - /// - /// to process itself in the given . + /// Processes this result in the given . /// /// An associated with the current request. /// A which will complete when execution is completed. public Task ExecuteAsync(HttpContext httpContext) - { - var executor = httpContext.RequestServices.GetRequiredService(); - return executor.ExecuteAsync(httpContext, this); - } + => RazorComponentResultExecutor.ExecuteAsync(httpContext, this); } diff --git a/src/Components/Endpoints/src/Results/RazorComponentResultExecutor.cs b/src/Components/Endpoints/src/Results/RazorComponentResultExecutor.cs index af14fa146f70..5467336e932d 100644 --- a/src/Components/Endpoints/src/Results/RazorComponentResultExecutor.cs +++ b/src/Components/Endpoints/src/Results/RazorComponentResultExecutor.cs @@ -9,23 +9,15 @@ using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.DependencyInjection; using static Microsoft.AspNetCore.Internal.LinkerFlags; +using Microsoft.AspNetCore.Http.HttpResults; namespace Microsoft.AspNetCore.Components.Endpoints; -/// -/// Executes a . -/// -public class RazorComponentResultExecutor +internal static class RazorComponentResultExecutor { - /// - /// The default content-type header value for Razor Components, text/html; charset=utf-8. - /// - public static readonly string DefaultContentType = "text/html; charset=utf-8"; + public const string DefaultContentType = "text/html; charset=utf-8"; - /// - /// Executes a asynchronously. - /// - public virtual Task ExecuteAsync(HttpContext httpContext, RazorComponentResult result) + public static Task ExecuteAsync(HttpContext httpContext, RazorComponentResult result) { ArgumentNullException.ThrowIfNull(httpContext); @@ -44,7 +36,7 @@ public virtual Task ExecuteAsync(HttpContext httpContext, RazorComponentResult r result.PreventStreamingRendering); } - internal static Task RenderComponentToResponse( + private static Task RenderComponentToResponse( HttpContext httpContext, [DynamicallyAccessedMembers(Component)] Type componentType, IReadOnlyDictionary? componentParameters, diff --git a/src/Components/Endpoints/src/Results/RazorComponentResultOfT.cs b/src/Components/Endpoints/src/Results/RazorComponentResultOfT.cs index 3019550b9100..5ab0f23481f0 100644 --- a/src/Components/Endpoints/src/Results/RazorComponentResultOfT.cs +++ b/src/Components/Endpoints/src/Results/RazorComponentResultOfT.cs @@ -2,15 +2,16 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Components; using static Microsoft.AspNetCore.Internal.LinkerFlags; -namespace Microsoft.AspNetCore.Components.Endpoints; +namespace Microsoft.AspNetCore.Http.HttpResults; /// /// An that renders a Razor Component. /// -public class RazorComponentResult<[DynamicallyAccessedMembers(Component)] TComponent> : RazorComponentResult where TComponent: IComponent +public class RazorComponentResult<[DynamicallyAccessedMembers(Component)] TComponent> + : RazorComponentResult where TComponent: IComponent { /// /// Constructs an instance of . @@ -23,7 +24,8 @@ public RazorComponentResult() : base(typeof(TComponent)) /// Constructs an instance of . /// /// Parameters for the component. - public RazorComponentResult(object parameters) : base(typeof(TComponent), parameters) + public RazorComponentResult( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] object parameters) : base(typeof(TComponent), parameters) { } diff --git a/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs b/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs index 7b3cda4f7ac4..a2c730f1f21c 100644 --- a/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs +++ b/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs @@ -980,7 +980,7 @@ public async Task Dispatching_WhenComponentReRendersNamedEventAtSameLocation() builder.SetKey(firstRender); builder.AddAttribute(1, "onsubmit", () => { eventReceivedCount++; component.TriggerRender(); }); - builder.AddNamedEvent(2, "onsubmit", "my-name"); + builder.AddNamedEvent("onsubmit", "my-name"); builder.CloseElement(); firstRender = false; @@ -1014,7 +1014,7 @@ public async Task Dispatching_WhenNamedEventChangesName() { builder.OpenElement(0, "form"); builder.AddAttribute(1, "onsubmit", () => { eventReceivedCount++; }); - builder.AddNamedEvent(2, "onsubmit", firstRender ? "my-name-1" : "my-name-2"); + builder.AddNamedEvent("onsubmit", firstRender ? "my-name-1" : "my-name-2"); builder.CloseElement(); firstRender = false; }); @@ -1186,7 +1186,7 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) { builder.OpenElement(0, "form"); builder.AddAttribute(1, "onsubmit", Handler ?? (() => { })); - builder.AddNamedEvent(2, "onsubmit", "default"); + builder.AddNamedEvent("onsubmit", "default"); builder.CloseElement(); } } @@ -1201,12 +1201,12 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) if (!hasRendered) { builder.AddAttribute(1, "onsubmit", () => { }); - builder.AddNamedEvent(2, "onsubmit", "default"); + builder.AddNamedEvent("onsubmit", "default"); } else { builder.AddAttribute(1, "onsubmit", () => { GC.KeepAlive(new object()); }); - builder.AddNamedEvent(2, "onsubmit", "default"); + builder.AddNamedEvent("onsubmit", "default"); } builder.CloseElement(); if (!hasRendered) @@ -1231,7 +1231,7 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) builder.AddAttribute(1, "onsubmit", !hasRendered ? () => { Message = "Received call to original handler"; } : () => { Message = "Received call to updated handler"; }); - builder.AddNamedEvent(2, "onsubmit", "default"); + builder.AddNamedEvent("onsubmit", "default"); builder.CloseElement(); } @@ -1248,7 +1248,7 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) { builder.OpenElement(0, "form"); builder.AddAttribute(1, "onsubmit", () => { }); - builder.AddNamedEvent(2, "onsubmit", "default"); + builder.AddNamedEvent("onsubmit", "default"); builder.CloseElement(); } } diff --git a/src/Components/Endpoints/test/HotReloadServiceTests.cs b/src/Components/Endpoints/test/HotReloadServiceTests.cs new file mode 100644 index 000000000000..afa86e09971e --- /dev/null +++ b/src/Components/Endpoints/test/HotReloadServiceTests.cs @@ -0,0 +1,213 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Builder; +using System.Reflection; +using Microsoft.AspNetCore.Components.Discovery; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Primitives; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Routing.Patterns; +using Microsoft.AspNetCore.Components.Endpoints.Infrastructure; + +namespace Microsoft.AspNetCore.Components.Endpoints.Tests; + +public class HotReloadServiceTests +{ + [Fact] + public void UpdatesEndpointsWhenHotReloadChangeTokenTriggered() + { + // Arrange + var builder = CreateBuilder(typeof(ServerComponent)); + var services = CreateServices(typeof(MockEndpointProvider)); + var endpointDataSource = CreateDataSource(builder, services); + var invoked = false; + + // Act + ChangeToken.OnChange(endpointDataSource.GetChangeToken, () => invoked = true); + + // Assert + Assert.False(invoked); + HotReloadService.UpdateApplication(null); + Assert.True(invoked); + } + + [Fact] + public void AddNewEndpointWhenDataSourceChanges() + { + // Arrange + var builder = CreateBuilder(typeof(ServerComponent)); + var services = CreateServices(typeof(MockEndpointProvider)); + var endpointDataSource = CreateDataSource(builder, services); + + // Assert - 1 + var endpoint = Assert.IsType(Assert.Single(endpointDataSource.Endpoints)); + Assert.Equal("/server", endpoint.RoutePattern.RawText); + + // Act - 2 + endpointDataSource.Builder.Pages.AddFromLibraryInfo("TestAssembly2", new[] + { + new PageComponentBuilder + { + AssemblyName = "TestAssembly2", + PageType = typeof(StaticComponent), + RouteTemplates = new List { "/app/test" } + } + }); + HotReloadService.UpdateApplication(null); + + // Assert - 2 + Assert.Equal(2, endpointDataSource.Endpoints.Count); + Assert.Collection( + endpointDataSource.Endpoints, + (ep) => Assert.Equal("/app/test", ((RouteEndpoint)ep).RoutePattern.RawText), + (ep) => Assert.Equal("/server", ((RouteEndpoint)ep).RoutePattern.RawText)); + } + + [Fact] + public void RemovesEndpointWhenDataSourceChanges() + { + // Arrange + var builder = CreateBuilder(typeof(ServerComponent)); + var services = CreateServices(typeof(MockEndpointProvider)); + var endpointDataSource = CreateDataSource(builder, services); + + // Assert - 1 + var endpoint = Assert.IsType(Assert.Single(endpointDataSource.Endpoints)); + Assert.Equal("/server", endpoint.RoutePattern.RawText); + + // Act - 2 + endpointDataSource.Builder.RemoveLibrary("TestAssembly"); + endpointDataSource.Options.ConfiguredRenderModes.Clear(); + HotReloadService.UpdateApplication(null); + + // Assert - 2 + Assert.Empty(endpointDataSource.Endpoints); + } + + [Fact] + public void ModifiesEndpointWhenDataSourceChanges() + { + // Arrange + var builder = CreateBuilder(typeof(ServerComponent)); + var services = CreateServices(typeof(MockEndpointProvider)); + var endpointDataSource = CreateDataSource(builder, services); + + // Assert - 1 + var endpoint = Assert.IsType(Assert.Single(endpointDataSource.Endpoints)); + Assert.Equal("/server", endpoint.RoutePattern.RawText); + Assert.DoesNotContain(endpoint.Metadata, (element) => element is TestMetadata); + + // Act - 2 + endpointDataSource.Conventions.Add(builder => + builder.Metadata.Add(new TestMetadata())); + HotReloadService.UpdateApplication(null); + + // Assert - 2 + var updatedEndpoint = Assert.IsType(Assert.Single(endpointDataSource.Endpoints)); + Assert.Equal("/server", updatedEndpoint.RoutePattern.RawText); + Assert.Contains(updatedEndpoint.Metadata, (element) => element is TestMetadata); + } + + [Fact] + public void NotifiesCompositeEndpointDataSource() + { + // Arrange + var builder = CreateBuilder(typeof(ServerComponent)); + var services = CreateServices(typeof(MockEndpointProvider)); + var endpointDataSource = CreateDataSource(builder, services); + var compositeEndpointDataSource = new CompositeEndpointDataSource( + new[] { endpointDataSource }); + + // Assert - 1 + var endpoint = Assert.IsType(Assert.Single(endpointDataSource.Endpoints)); + Assert.Equal("/server", endpoint.RoutePattern.RawText); + var compositeEndpoint = Assert.IsType(Assert.Single(compositeEndpointDataSource.Endpoints)); + Assert.Equal("/server", compositeEndpoint.RoutePattern.RawText); + + // Act - 2 + endpointDataSource.Builder.Pages.RemoveFromAssembly("TestAssembly"); + endpointDataSource.Options.ConfiguredRenderModes.Clear(); + HotReloadService.UpdateApplication(null); + + // Assert - 2 + Assert.Empty(endpointDataSource.Endpoints); + Assert.Empty(compositeEndpointDataSource.Endpoints); + } + + private class TestMetadata { } + + private ComponentApplicationBuilder CreateBuilder(params Type[] types) + { + var builder = new ComponentApplicationBuilder(); + builder.AddLibrary(new AssemblyComponentLibraryDescriptor( + "TestAssembly", + Array.Empty(), + types.Select(t => new ComponentBuilder + { + AssemblyName = "TestAssembly", + ComponentType = t, + RenderMode = t.GetCustomAttribute() + }).ToArray())); + + return builder; + } + + private IServiceProvider CreateServices(params Type[] types) + { + var services = new ServiceCollection(); + foreach (var type in types) + { + services.TryAddEnumerable(ServiceDescriptor.Singleton(typeof(RenderModeEndpointProvider), type)); + } + + return services.BuildServiceProvider(); + } + + private static RazorComponentEndpointDataSource CreateDataSource( + ComponentApplicationBuilder builder, + IServiceProvider services, + IComponentRenderMode[] renderModes = null) + { + var result = new RazorComponentEndpointDataSource( + builder, + new[] { new MockEndpointProvider() }, + new ApplicationBuilder(services), + new RazorComponentEndpointFactory(), + new HotReloadService() { MetadataUpdateSupported = true }); + + if (renderModes != null) + { + foreach (var mode in renderModes) + { + result.Options.ConfiguredRenderModes.Add(mode); + } + } + else + { + result.Options.ConfiguredRenderModes.Add(new ServerRenderMode()); + } + + return result; + } + + private class StaticComponent : ComponentBase { } + + [RenderModeServer] + private class ServerComponent : ComponentBase { } + + private class MockEndpointProvider : RenderModeEndpointProvider + { + public override IEnumerable GetEndpointBuilders(IComponentRenderMode renderMode, IApplicationBuilder applicationBuilder) + { + yield return new RouteEndpointBuilder( + (context) => Task.CompletedTask, + RoutePatternFactory.Parse("/server"), + 0); + } + + public override bool Supports(IComponentRenderMode renderMode) => true; + } +} diff --git a/src/Components/Endpoints/test/RazorComponentEndpointDataSourceTest.cs b/src/Components/Endpoints/test/RazorComponentEndpointDataSourceTest.cs index 07606588ea10..c758d717c8c2 100644 --- a/src/Components/Endpoints/test/RazorComponentEndpointDataSourceTest.cs +++ b/src/Components/Endpoints/test/RazorComponentEndpointDataSourceTest.cs @@ -228,7 +228,8 @@ private RazorComponentEndpointDataSource CreateDataSource.Instance.GetBuilder(), services?.GetService>() ?? Enumerable.Empty(), new ApplicationBuilder(services ?? new ServiceCollection().BuildServiceProvider()), - new RazorComponentEndpointFactory()); + new RazorComponentEndpointFactory(), + new HotReloadService() { MetadataUpdateSupported = true }); if (renderModes != null) { diff --git a/src/Components/Endpoints/test/RazorComponentEndpointFactoryTest.cs b/src/Components/Endpoints/test/RazorComponentEndpointFactoryTest.cs index 0e35b75e68f4..a7dbd751bf25 100644 --- a/src/Components/Endpoints/test/RazorComponentEndpointFactoryTest.cs +++ b/src/Components/Endpoints/test/RazorComponentEndpointFactoryTest.cs @@ -18,13 +18,16 @@ public void AddEndpoints_CreatesEndpointWithExpectedMetadata() var factory = new RazorComponentEndpointFactory(); var conventions = new List>(); var finallyConventions = new List>(); + var testRenderMode = new TestRenderMode(); + var configuredRenderModes = new ConfiguredRenderModesMetadata(new[] { testRenderMode }); factory.AddEndpoints(endpoints, typeof(App), new PageComponentInfo( "App", typeof(App), "/", new object[] { new AuthorizeAttribute() }), conventions, - finallyConventions); + finallyConventions, + configuredRenderModes); var endpoint = Assert.Single(endpoints); Assert.Equal("/ (App)", endpoint.DisplayName); @@ -35,6 +38,8 @@ public void AddEndpoints_CreatesEndpointWithExpectedMetadata() Assert.Contains(endpoint.Metadata, m => m is ComponentTypeMetadata); Assert.Contains(endpoint.Metadata, m => m is SuppressLinkGenerationMetadata); Assert.Contains(endpoint.Metadata, m => m is AuthorizeAttribute); + Assert.Contains(endpoint.Metadata, m => m is ConfiguredRenderModesMetadata c + && c.ConfiguredRenderModes.Single() == testRenderMode); Assert.NotNull(endpoint.RequestDelegate); var methods = Assert.Single(endpoint.Metadata.GetOrderedMetadata()); @@ -63,7 +68,8 @@ public void AddEndpoints_RunsConventions() "/", Array.Empty()), conventions, - finallyConventions); + finallyConventions, + new ConfiguredRenderModesMetadata(Array.Empty())); var endpoint = Assert.Single(endpoints); Assert.Contains(endpoint.Metadata, m => m is AuthorizeAttribute); @@ -90,7 +96,8 @@ public void AddEndpoints_RunsFinallyConventions() "/", Array.Empty()), conventions, - finallyConventions); + finallyConventions, + new ConfiguredRenderModesMetadata(Array.Empty())); var endpoint = Assert.Single(endpoints); Assert.Contains(endpoint.Metadata, m => m is AuthorizeAttribute); @@ -117,7 +124,8 @@ public void AddEndpoints_RouteOrderCanNotBeChanged() "/", Array.Empty()), conventions, - finallyConventions); + finallyConventions, + new ConfiguredRenderModesMetadata(Array.Empty())); var endpoint = Assert.Single(endpoints); var routeEndpoint = Assert.IsType(endpoint); @@ -148,9 +156,12 @@ public void AddEndpoints_RunsFinallyConventionsAfterRegularConventions() "/", Array.Empty()), conventions, - finallyConventions); + finallyConventions, + new ConfiguredRenderModesMetadata(Array.Empty())); var endpoint = Assert.Single(endpoints); Assert.DoesNotContain(endpoint.Metadata, m => m is AuthorizeAttribute); } + + class TestRenderMode : IComponentRenderMode { } } diff --git a/src/Components/Endpoints/test/RazorComponentResultExecutorTest.cs b/src/Components/Endpoints/test/RazorComponentResultExecutorTest.cs deleted file mode 100644 index 14c0646f9a28..000000000000 --- a/src/Components/Endpoints/test/RazorComponentResultExecutorTest.cs +++ /dev/null @@ -1,454 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.AspNetCore.Components.Infrastructure; -using System.Diagnostics; -using Microsoft.AspNetCore.Components.Routing; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.Extensions.Internal; -using Microsoft.AspNetCore.Http.Features; -using Moq; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Hosting; -using Microsoft.AspNetCore.Components.Endpoints.Tests.TestComponents; -using System.Text.RegularExpressions; -using Microsoft.AspNetCore.Components.Forms; -using Microsoft.AspNetCore.Components.Endpoints.Forms; - -namespace Microsoft.AspNetCore.Components.Endpoints; - -public class RazorComponentResultExecutorTest -{ - [Fact] - public async Task CanRenderComponentStatically() - { - // Arrange - var httpContext = GetTestHttpContext(); - var responseBody = new MemoryStream(); - httpContext.Response.Body = responseBody; - - // Act - await RazorComponentResultExecutor.RenderComponentToResponse( - httpContext, - typeof(SimpleComponent), - componentParameters: null, - preventStreamingRendering: false); - - // Assert - Assert.Equal("

Hello from SimpleComponent

", GetStringContent(responseBody)); - } - - [Fact] - public async Task PerformsStreamingRendering() - { - // Arrange - var tcs = new TaskCompletionSource(); - var httpContext = GetTestHttpContext(); - var responseBody = new MemoryStream(); - httpContext.Response.Body = responseBody; - - // Act/Assert 1: Emits the initial pre-quiescent output to the response - var completionTask = RazorComponentResultExecutor.RenderComponentToResponse( - httpContext, - typeof(StreamingAsyncLoadingComponent), - PropertyHelper.ObjectToDictionary(new { LoadingTask = tcs.Task }).AsReadOnly(), - preventStreamingRendering: false); - Assert.Equal( - "Loading task status: WaitingForActivation", - MaskComponentIds(GetStringContent(responseBody))); - - // Assert 2: Result task remains incomplete for as long as the component's loading operation remains in flight - // This keeps the HTTP response open - await Task.Yield(); - Assert.False(completionTask.IsCompleted); - - // Act/Assert 3: When loading completes, it emits a streaming batch update and completes the response - tcs.SetResult(); - await completionTask; - Assert.Equal( - "Loading task status: WaitingForActivation", - MaskComponentIds(GetStringContent(responseBody))); - } - - [Fact] - public async Task EmitsEachComponentOnlyOncePerStreamingUpdate_WhenAComponentRendersTwice() - { - // Arrange - var tcs = new TaskCompletionSource(); - var httpContext = GetTestHttpContext(); - var responseBody = new MemoryStream(); - httpContext.Response.Body = responseBody; - - // Act/Assert 1: Emits the initial pre-quiescent output to the response - var completionTask = RazorComponentResultExecutor.RenderComponentToResponse( - httpContext, - typeof(DoubleRenderingStreamingAsyncComponent), - PropertyHelper.ObjectToDictionary(new { WaitFor = tcs.Task }).AsReadOnly(), - preventStreamingRendering: false); - Assert.Equal( - "Loading...", - MaskComponentIds(GetStringContent(responseBody))); - - // Act/Assert 2: When loading completes, it emits a streaming batch update with only one copy of the final output, - // despite the RenderBatch containing two diffs from the component - tcs.SetResult(); - await completionTask; - Assert.Equal( - "Loading...", - MaskComponentIds(GetStringContent(responseBody))); - } - - [Fact] - public async Task EmitsEachComponentOnlyOncePerStreamingUpdate_WhenAnAncestorAlsoUpdated() - { - // Since the HTML rendered for each component also includes all its descendants, we don't - // want to render output for any component that also has an ancestor in the set of updates - // (as it would then be output twice) - - // Arrange - var tcs = new TaskCompletionSource(); - var httpContext = GetTestHttpContext(); - var responseBody = new MemoryStream(); - httpContext.Response.Body = responseBody; - - // Act/Assert 1: Emits the initial pre-quiescent output to the response - var completionTask = RazorComponentResultExecutor.RenderComponentToResponse( - httpContext, - typeof(StreamingComponentWithChild), - PropertyHelper.ObjectToDictionary(new { LoadingTask = tcs.Task }).AsReadOnly(), - preventStreamingRendering: false); - var expectedInitialHtml = "[LoadingTask: WaitingForActivation]\n[Child render: 1]\n"; - Assert.Equal( - expectedInitialHtml, - MaskComponentIds(GetStringContent(responseBody))); - - // Act/Assert 2: When loading completes, it emits a streaming batch update in which the - // child is present only within the parent markup, not as a separate entry - tcs.SetResult(); - await completionTask; - Assert.Equal( - $"{expectedInitialHtml}", - MaskComponentIds(GetStringContent(responseBody))); - } - - [Fact] - public async Task WaitsForQuiescenceIfPreventStreamingRenderingIsTrue() - { - // Arrange - var tcs = new TaskCompletionSource(); - var httpContext = GetTestHttpContext(); - var responseBody = new MemoryStream(); - httpContext.Response.Body = responseBody; - - // Act/Assert: Doesn't complete until loading finishes - var completionTask = RazorComponentResultExecutor.RenderComponentToResponse( - httpContext, - typeof(StreamingAsyncLoadingComponent), - PropertyHelper.ObjectToDictionary(new { LoadingTask = tcs.Task }).AsReadOnly(), - preventStreamingRendering: true); - await Task.Yield(); - Assert.False(completionTask.IsCompleted); - - // Act/Assert: Does complete when loading finishes - tcs.SetResult(); - await completionTask; - Assert.Equal( - "Loading task status: RanToCompletion", - MaskComponentIds(GetStringContent(responseBody))); - } - - [Fact] - public async Task SupportsLayouts() - { - // Arrange - var httpContext = GetTestHttpContext(); - var responseBody = new MemoryStream(); - httpContext.Response.Body = responseBody; - - // Act - await RazorComponentResultExecutor.RenderComponentToResponse( - httpContext, typeof(ComponentWithLayout), - null, false); - - // Assert - Assert.Equal($"[TestParentLayout with content: [TestLayout with content: Page\n]\n]\n", GetStringContent(responseBody)); - } - - [Fact] - public async Task OnNavigationBeforeResponseStarted_Redirects() - { - // Arrange - var httpContext = GetTestHttpContext(); - - // Act - await RazorComponentResultExecutor.RenderComponentToResponse( - httpContext, typeof(ComponentThatRedirectsSynchronously), - null, false); - - // Assert - Assert.Equal("https://test/somewhere/else", httpContext.Response.Headers.Location); - } - - [Fact] - public async Task OnNavigationAfterResponseStarted_WithStreamingOff_Throws() - { - // Arrange - var httpContext = GetTestHttpContext(); - var responseMock = new Mock(); - responseMock.Setup(r => r.HasStarted).Returns(true); - httpContext.Features.Set(responseMock.Object); - - // Act - var ex = await Assert.ThrowsAsync( - () => RazorComponentResultExecutor.RenderComponentToResponse( - httpContext, typeof(StreamingComponentThatRedirectsAsynchronously), - null, preventStreamingRendering: true)); - - // Assert - Assert.Contains("A navigation command was attempted during prerendering after the server already started sending the response", ex.Message); - } - - [Fact] - public async Task OnNavigationAfterResponseStarted_WithStreamingOn_EmitsCommand() - { - // Arrange - var httpContext = GetTestHttpContext(); - var responseBody = new MemoryStream(); - httpContext.Response.Body = responseBody; - - // Act - await RazorComponentResultExecutor.RenderComponentToResponse( - httpContext, typeof(StreamingComponentThatRedirectsAsynchronously), - null, preventStreamingRendering: false); - - // Assert - Assert.Equal( - $"Some output\n", - MaskComponentIds(GetStringContent(responseBody))); - } - - [Fact] - public async Task OnUnhandledExceptionBeforeResponseStarted_Throws() - { - // Arrange - var httpContext = GetTestHttpContext(); - - // Act - var ex = await Assert.ThrowsAsync(() => RazorComponentResultExecutor.RenderComponentToResponse( - httpContext, typeof(ComponentThatThrowsSynchronously), - null, false)); - - // Assert - Assert.Contains("Test message", ex.Message); - } - - [Fact] - public async Task OnUnhandledExceptionAfterResponseStarted_WithStreamingOff_Throws() - { - // Arrange - var httpContext = GetTestHttpContext(); - - // Act - var ex = await Assert.ThrowsAsync(() => RazorComponentResultExecutor.RenderComponentToResponse( - httpContext, typeof(StreamingComponentThatThrowsAsynchronously), - null, preventStreamingRendering: true)); - - // Assert - Assert.Contains("Test message", ex.Message); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task OnUnhandledExceptionAfterResponseStarted_WithStreamingOn_EmitsCommand(bool isDevelopmentEnvironment) - { - // Arrange - var httpContext = GetTestHttpContext(isDevelopmentEnvironment ? Environments.Development : Environments.Production); - var responseBody = new MemoryStream(); - httpContext.Response.Body = responseBody; - - var expectedResponseExceptionInfo = isDevelopmentEnvironment - ? "System.InvalidTimeZoneException: Test message with <b>markup</b>" - : "There was an unhandled exception on the current request. For more details turn on detailed exceptions by setting 'DetailedErrors: true' in 'appSettings.Development.json'"; - - // Act - var ex = await Assert.ThrowsAsync(() => RazorComponentResultExecutor.RenderComponentToResponse( - httpContext, typeof(StreamingComponentThatThrowsAsynchronously), - null, preventStreamingRendering: false)); - - // Assert - Assert.Contains("Test message with markup", ex.Message); - Assert.Contains( - $"Some output\n