diff --git a/src/Components/Endpoints/src/DependencyInjection/CookieTempDataProvider.cs b/src/Components/Endpoints/src/DependencyInjection/CookieTempDataProvider.cs new file mode 100644 index 000000000000..3be0db26065f --- /dev/null +++ b/src/Components/Endpoints/src/DependencyInjection/CookieTempDataProvider.cs @@ -0,0 +1,128 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Components.Endpoints; + +internal sealed partial class CookieTempDataProvider : ITempDataProvider +{ + private const string CookieName = ".AspNetCore.Components.TempData"; + private const string Purpose = "Microsoft.AspNetCore.Components.CookieTempDataProviderToken.v1"; + private const int MaxEncodedLength = 4050; + private readonly IDataProtector _dataProtector; + private readonly ITempDataSerializer _tempDataSerializer; + + public CookieTempDataProvider( + IDataProtectionProvider dataProtectionProvider, + ITempDataSerializer tempDataSerializer) + { + _dataProtector = dataProtectionProvider.CreateProtector(Purpose); + _tempDataSerializer = tempDataSerializer; + } + + public IDictionary LoadTempData(HttpContext context) + { + try + { + var serializedDataFromCookie = context.Request.Cookies[CookieName]; + if (serializedDataFromCookie is null) + { + return new Dictionary(); + } + + var protectedBytes = WebEncoders.Base64UrlDecode(serializedDataFromCookie); + var unprotectedBytes = _dataProtector.Unprotect(protectedBytes); + + var dataFromCookie = JsonSerializer.Deserialize>(unprotectedBytes); + + if (dataFromCookie is null) + { + return new Dictionary(); + } + + var convertedData = new Dictionary(); + foreach (var kvp in dataFromCookie) + { + convertedData[kvp.Key] = _tempDataSerializer.Deserialize(kvp.Value); + } + return convertedData; + } + catch (Exception ex) + { + // If any error occurs during loading (e.g. data protection key changed, malformed cookie), + // return an empty TempData dictionary. + if (context.RequestServices.GetService>() is { } logger) + { + Log.TempDataCookieLoadFailure(logger, CookieName, ex); + } + + context.Response.Cookies.Delete(CookieName, new CookieOptions + { + Path = context.Request.PathBase.HasValue ? context.Request.PathBase.Value : "/", + }); + return new Dictionary(); + } + } + + public void SaveTempData(HttpContext context, IDictionary values) + { + foreach (var kvp in values) + { + if (!_tempDataSerializer.EnsureObjectCanBeSerialized(kvp.Value?.GetType() ?? typeof(object))) + { + throw new InvalidOperationException($"TempData cannot store values of type '{kvp.Value?.GetType()}'."); + } + } + + if (values.Count == 0) + { + context.Response.Cookies.Delete(CookieName, new CookieOptions + { + Path = context.Request.PathBase.HasValue ? context.Request.PathBase.Value : "/", + }); + return; + } + + var bytes = JsonSerializer.SerializeToUtf8Bytes(values); + var protectedBytes = _dataProtector.Protect(bytes); + var encodedValue = WebEncoders.Base64UrlEncode(protectedBytes); + + if (encodedValue.Length > MaxEncodedLength) + { + if (context.RequestServices.GetService>() is { } logger) + { + Log.TempDataCookieSaveFailure(logger, CookieName); + } + + context.Response.Cookies.Delete(CookieName, new CookieOptions + { + Path = context.Request.PathBase.HasValue ? context.Request.PathBase.Value : "/", + }); + return; + } + + context.Response.Cookies.Append(CookieName, encodedValue, new CookieOptions + { + HttpOnly = true, + IsEssential = true, + SameSite = SameSiteMode.Lax, + Secure = context.Request.IsHttps, + Path = context.Request.PathBase.HasValue ? context.Request.PathBase.Value : "/", + }); + } + + private static partial class Log + { + [LoggerMessage(3, LogLevel.Warning, "The temp data cookie {CookieName} could not be loaded.", EventName = "TempDataCookieLoadFailure")] + public static partial void TempDataCookieLoadFailure(ILogger logger, string cookieName, Exception exception); + + [LoggerMessage(3, LogLevel.Warning, "The temp data cookie {CookieName} could not be saved, because it is too large to fit in a single cookie.", EventName = "TempDataCookieSaveFailure")] + public static partial void TempDataCookieSaveFailure(ILogger logger, string cookieName); + } +} diff --git a/src/Components/Endpoints/src/DependencyInjection/ITempDataProvider.cs b/src/Components/Endpoints/src/DependencyInjection/ITempDataProvider.cs new file mode 100644 index 000000000000..fb7d3d3608ed --- /dev/null +++ b/src/Components/Endpoints/src/DependencyInjection/ITempDataProvider.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Components.Endpoints; + +/// +/// Provides an abstraction for a provider that stores and retrieves temporary data. +/// +public interface ITempDataProvider +{ + /// + /// Loads temporary data from the given . + /// + IDictionary LoadTempData(HttpContext context); + + /// + /// Saves temporary data to the given . + /// + void SaveTempData(HttpContext context, IDictionary values); +} diff --git a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs index dc365194fcbe..a69a51d573d0 100644 --- a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs +++ b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs @@ -74,6 +74,7 @@ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection services.TryAddCascadingValue(sp => sp.GetRequiredService().HttpContext); services.TryAddScoped(); services.TryAddScoped(); + services.AddDefaultTempDataValueProvider(); services.TryAddScoped(); RegisterPersistentComponentStateServiceCollectionExtensions.AddPersistentServiceRegistration(services, RenderMode.InteractiveWebAssembly); diff --git a/src/Components/Endpoints/src/DependencyInjection/TempDataService.cs b/src/Components/Endpoints/src/DependencyInjection/TempDataService.cs new file mode 100644 index 000000000000..ae7af2cf2291 --- /dev/null +++ b/src/Components/Endpoints/src/DependencyInjection/TempDataService.cs @@ -0,0 +1,31 @@ +// 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.Http; + +namespace Microsoft.AspNetCore.Components.Endpoints; + +internal sealed partial class TempDataService +{ + private readonly ITempDataProvider _tempDataProvider; + + public TempDataService(ITempDataProvider tempDataProvider) + { + _tempDataProvider = tempDataProvider; + } + + public TempData CreateEmpty(HttpContext httpContext) + { + return new TempData(() => Load(httpContext)); + } + + public IDictionary Load(HttpContext httpContext) + { + return _tempDataProvider.LoadTempData(httpContext); + } + + public void Save(HttpContext httpContext, TempData tempData) + { + _tempDataProvider.SaveTempData(httpContext, tempData.Save()); + } +} diff --git a/src/Components/Endpoints/src/PublicAPI.Unshipped.txt b/src/Components/Endpoints/src/PublicAPI.Unshipped.txt index ac1780ba883e..0ef662edabbf 100644 --- a/src/Components/Endpoints/src/PublicAPI.Unshipped.txt +++ b/src/Components/Endpoints/src/PublicAPI.Unshipped.txt @@ -1,3 +1,14 @@ #nullable enable Microsoft.AspNetCore.Components.Endpoints.BasePath Microsoft.AspNetCore.Components.Endpoints.BasePath.BasePath() -> void +Microsoft.AspNetCore.Components.Endpoints.ITempDataProvider +Microsoft.AspNetCore.Components.Endpoints.ITempDataProvider.LoadTempData(Microsoft.AspNetCore.Http.HttpContext! context) -> System.Collections.Generic.IDictionary! +Microsoft.AspNetCore.Components.Endpoints.ITempDataProvider.SaveTempData(Microsoft.AspNetCore.Http.HttpContext! context, System.Collections.Generic.IDictionary! values) -> void +Microsoft.AspNetCore.Components.Endpoints.TempDataProviderServiceCollectionExtensions +Microsoft.AspNetCore.Components.ITempData +Microsoft.AspNetCore.Components.ITempData.ContainsValue(object! value) -> bool +Microsoft.AspNetCore.Components.ITempData.Get(string! key) -> object? +Microsoft.AspNetCore.Components.ITempData.Keep() -> void +Microsoft.AspNetCore.Components.ITempData.Keep(string! key) -> void +Microsoft.AspNetCore.Components.ITempData.Peek(string! key) -> object? +static Microsoft.AspNetCore.Components.Endpoints.TempDataProviderServiceCollectionExtensions.AddTempDataValueProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Components/Endpoints/src/TempData/ITempData.cs b/src/Components/Endpoints/src/TempData/ITempData.cs new file mode 100644 index 000000000000..eaa805dadc57 --- /dev/null +++ b/src/Components/Endpoints/src/TempData/ITempData.cs @@ -0,0 +1,37 @@ +// 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; + +/// +/// Provides a dictionary for storing data that is needed for subsequent requests. +/// Data stored in TempData is automatically removed after it is read unless +/// or is called, or it is accessed via . +/// +public interface ITempData : IDictionary +{ + /// + /// Gets the value associated with the specified key and then schedules it for deletion. + /// + object? Get(string key); + + /// + /// Gets the value associated with the specified key without scheduling it for deletion. + /// + object? Peek(string key); + + /// + /// Makes all of the keys currently in TempData persist for another request. + /// + void Keep(); + + /// + /// Makes the element with the persist for another request. + /// + void Keep(string key); + + /// + /// Returns true if the TempData dictionary contains the specified . + /// + bool ContainsValue(object value); +} diff --git a/src/Components/Endpoints/src/TempData/JsonTempDataSerializer.cs b/src/Components/Endpoints/src/TempData/JsonTempDataSerializer.cs new file mode 100644 index 000000000000..504c6e8f8bc1 --- /dev/null +++ b/src/Components/Endpoints/src/TempData/JsonTempDataSerializer.cs @@ -0,0 +1,139 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNetCore.Components.Endpoints; + +internal class JsonTempDataSerializer : ITempDataSerializer +{ + private static readonly Dictionary> _elementConverters = new(4) + { + { JsonValueKind.Number, static e => e.GetInt32() }, + { JsonValueKind.True, static e => e.GetBoolean() }, + { JsonValueKind.False, static e => e.GetBoolean() }, + { JsonValueKind.Null, static _ => null }, + }; + + public object? Deserialize(JsonElement element) + { + try + { + return DeserializeSimpleType(element); + } + catch (InvalidOperationException) + { + return element.ValueKind switch + { + JsonValueKind.Array => DeserializeArray(element), + JsonValueKind.Object => DeserializeDictionaryEntry(element), + _ => throw new InvalidOperationException($"TempData cannot deserialize value of type '{element.ValueKind}'.") + }; + } + } + + private static object? DeserializeSimpleType(JsonElement element) + { + if (_elementConverters.TryGetValue(element.ValueKind, out var converter)) + { + return converter(element); + } + + return element.ValueKind switch + { + JsonValueKind.String => DeserializeString(element), + _ => throw new InvalidOperationException($"TempData cannot deserialize value of type '{element.ValueKind}'.") + }; + } + + private static object? DeserializeString(JsonElement element) + { + if (element.TryGetGuid(out var guid)) + { + return guid; + } + if (element.TryGetDateTime(out var dateTime)) + { + return dateTime; + } + return element.GetString(); + } + + private static object? DeserializeArray(JsonElement arrayElement) + { + var arrayLength = arrayElement.GetArrayLength(); + if (arrayLength == 0) + { + return Array.Empty(); + } + var array = new object?[arrayLength]; + for (var i = 0; i < arrayLength; i++) + { + array[i] = DeserializeSimpleType(arrayElement[i]); + } + return array; + } + + private static Dictionary DeserializeDictionaryEntry(JsonElement objectElement) + { + var dictionary = new Dictionary(StringComparer.Ordinal); + foreach (var item in objectElement.EnumerateObject()) + { + // JSON object keys are always strings by specification + dictionary[item.Name] = DeserializeSimpleType(item.Value); + } + + return dictionary; + } + + public bool EnsureObjectCanBeSerialized(Type type) + { + var actualType = type; + if (type.IsArray) + { + actualType = type.GetElementType(); + } + else if (type.IsGenericType) + { + var genericTypeArguments = type.GenericTypeArguments; + if (ClosedGenericMatcher.ExtractGenericInterface(type, typeof(IList<>)) != null && genericTypeArguments.Length == 1) + { + actualType = genericTypeArguments[0]; + } + else if (ClosedGenericMatcher.ExtractGenericInterface(type, typeof(IDictionary<,>)) != null && genericTypeArguments.Length == 2 && genericTypeArguments[0] == typeof(string)) + { + actualType = genericTypeArguments[1]; + } + else + { + return false; + } + } + if (actualType is null) + { + return false; + } + + actualType = Nullable.GetUnderlyingType(actualType) ?? actualType; + + if (!IsSimpleType(actualType)) + { + return false; + } + return true; + } + + private static bool IsSimpleType(Type type) + { + return type.IsPrimitive || + type.IsEnum || + type.Equals(typeof(decimal)) || + type.Equals(typeof(string)) || + type.Equals(typeof(DateTime)) || + type.Equals(typeof(Guid)) || + type.Equals(typeof(DateTimeOffset)) || + type.Equals(typeof(TimeSpan)) || + type.Equals(typeof(Uri)); + } +} diff --git a/src/Components/Endpoints/src/TempData/TempData.cs b/src/Components/Endpoints/src/TempData/TempData.cs new file mode 100644 index 000000000000..c61c53032157 --- /dev/null +++ b/src/Components/Endpoints/src/TempData/TempData.cs @@ -0,0 +1,224 @@ +// 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; + +namespace Microsoft.AspNetCore.Components; + +/// +internal sealed class TempData : ITempData +{ + private readonly Dictionary _data = new(StringComparer.OrdinalIgnoreCase); + private readonly HashSet _retainedKeys = new(StringComparer.OrdinalIgnoreCase); + private Func>? _loadFunc; + private bool _loaded; + + internal TempData(Func>? loadFunc = null) + { + _loadFunc = loadFunc; + } + + private void EnsureLoaded() + { + if (!_loaded && _loadFunc is not null) + { + var dataToLoad = _loadFunc(); + Load(dataToLoad); + _loadFunc = null!; + _loaded = true; + } + } + + public object? this[string key] + { + get + { + EnsureLoaded(); + return Get(key); + } + set + { + EnsureLoaded(); + _data[key] = value; + _retainedKeys.Add(key); + } + } + + public object? Get(string key) + { + EnsureLoaded(); + _retainedKeys.Remove(key); + return _data.GetValueOrDefault(key); + } + + public object? Peek(string key) + { + EnsureLoaded(); + return _data.GetValueOrDefault(key); + } + + public void Keep() + { + EnsureLoaded(); + _retainedKeys.Clear(); + _retainedKeys.UnionWith(_data.Keys); + } + + public void Keep(string key) + { + EnsureLoaded(); + if (_data.ContainsKey(key)) + { + _retainedKeys.Add(key); + } + } + + public bool ContainsValue(object value) + { + EnsureLoaded(); + return _data.ContainsValue(value); + } + + public bool ContainsKey(string key) + { + EnsureLoaded(); + return _data.ContainsKey(key); + } + + public bool Remove(string key) + { + EnsureLoaded(); + _retainedKeys.Remove(key); + return _data.Remove(key); + } + + public IDictionary Save() + { + EnsureLoaded(); + var dataToSave = new Dictionary(); + foreach (var key in _retainedKeys) + { + dataToSave[key] = _data[key]; + } + return dataToSave; + } + + public void Load(IDictionary data) + { + _data.Clear(); + _retainedKeys.Clear(); + foreach (var kvp in data) + { + _data[kvp.Key] = kvp.Value; + _retainedKeys.Add(kvp.Key); + } + } + + public void Clear() + { + EnsureLoaded(); + _data.Clear(); + _retainedKeys.Clear(); + } + + ICollection IDictionary.Keys => _data.Keys; + + ICollection IDictionary.Values => _data.Values; + + int ICollection>.Count => _data.Count; + + bool ICollection>.IsReadOnly => ((ICollection>)_data).IsReadOnly; + + void IDictionary.Add(string key, object? value) + { + this[key] = value; + } + + bool IDictionary.TryGetValue(string key, out object? value) + { + value = Get(key); + return ContainsKey(key); + } + + void ICollection>.Add(KeyValuePair item) + { + ((IDictionary)this).Add(item.Key, item.Value); + } + + bool ICollection>.Contains(KeyValuePair item) + { + return ContainsKey(item.Key) && Equals(Peek(item.Key), item.Value); + } + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + EnsureLoaded(); + ((ICollection>)_data).CopyTo(array, arrayIndex); + } + + bool ICollection>.Remove(KeyValuePair item) + { + if (ContainsKey(item.Key) && Equals(Peek(item.Key), item.Value)) + { + return Remove(item.Key); + } + return false; + } + + IEnumerator> IEnumerable>.GetEnumerator() + { + EnsureLoaded(); + return new TempDataEnumerator(this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + EnsureLoaded(); + return new TempDataEnumerator(this); + } + + class TempDataEnumerator : IEnumerator> + { + private readonly TempData _tempData; + private readonly IEnumerator> _innerEnumerator; + private readonly List _keysToRemove = new(); + + public TempDataEnumerator(TempData tempData) + { + _tempData = tempData; + _innerEnumerator = tempData._data.GetEnumerator(); + } + + public KeyValuePair Current + { + get + { + var kvp = _innerEnumerator.Current; + _keysToRemove.Add(kvp.Key); + return kvp; + } + } + + object IEnumerator.Current => _innerEnumerator.Current; + + public void Dispose() + { + _innerEnumerator.Dispose(); + foreach (var key in _keysToRemove) + { + _tempData._retainedKeys.Remove(key); + } + } + + public bool MoveNext() + { + return _innerEnumerator.MoveNext(); + } + + public void Reset() + { + _innerEnumerator.Reset(); + _keysToRemove.Clear(); + } + } +} diff --git a/src/Components/Endpoints/src/TempData/TempDataSerializer.cs b/src/Components/Endpoints/src/TempData/TempDataSerializer.cs new file mode 100644 index 000000000000..568a43011b6f --- /dev/null +++ b/src/Components/Endpoints/src/TempData/TempDataSerializer.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; + +namespace Microsoft.AspNetCore.Components.Endpoints; + +internal interface ITempDataSerializer +{ + public object? Deserialize(JsonElement element); + public bool EnsureObjectCanBeSerialized(Type type); +} diff --git a/src/Components/Endpoints/src/TempDataProviderServiceCollectionExtensions.cs b/src/Components/Endpoints/src/TempDataProviderServiceCollectionExtensions.cs new file mode 100644 index 000000000000..50f097b7d042 --- /dev/null +++ b/src/Components/Endpoints/src/TempDataProviderServiceCollectionExtensions.cs @@ -0,0 +1,69 @@ +// 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.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Components.Endpoints; + +/// +/// Enables component parameters to be supplied from the . +/// +public static class TempDataProviderServiceCollectionExtensions +{ + + internal static IServiceCollection AddDefaultTempDataValueProvider(this IServiceCollection services) + { + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + + services.TryAddCascadingValue(sp => + { + var httpContext = sp.GetRequiredService().HttpContext; + if (httpContext is null) + { + return null!; + } + return GetOrCreateTempData(httpContext); + }); + return services; + } + + /// + /// Enables component parameters to be supplied from the . + /// + public static IServiceCollection AddTempDataValueProvider(this IServiceCollection services) + { + // add services based on options + + services.TryAddCascadingValue(sp => + { + var httpContext = sp.GetRequiredService().HttpContext; + if (httpContext is null) + { + return null!; + } + return GetOrCreateTempData(httpContext); + }); + return services; + } + + private static ITempData GetOrCreateTempData(HttpContext httpContext) + { + var key = typeof(ITempData); + if (!httpContext.Items.TryGetValue(key, out var tempData)) + { + var tempDataService = httpContext.RequestServices.GetRequiredService(); + var tempDataInstance = tempDataService.CreateEmpty(httpContext); + httpContext.Items[key] = tempDataInstance; + httpContext.Response.OnStarting(() => + { + tempDataService.Save(httpContext, tempDataInstance); + return Task.CompletedTask; + }); + } + return (ITempData)httpContext.Items[key]!; + } +} diff --git a/src/Components/Endpoints/test/TempData/CookieTempDataProviderTest.cs b/src/Components/Endpoints/test/TempData/CookieTempDataProviderTest.cs new file mode 100644 index 000000000000..88736561c8ee --- /dev/null +++ b/src/Components/Endpoints/test/TempData/CookieTempDataProviderTest.cs @@ -0,0 +1,295 @@ +// 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; +using Microsoft.AspNetCore.Components.Endpoints; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.Extensions.DependencyInjection; + +public class CookieTempDataProviderTest +{ + private readonly CookieTempDataProvider cookieTempDataProvider = new CookieTempDataProvider(new EphemeralDataProtectionProvider(), new JsonTempDataSerializer()); + + [Fact] + public void Load_ReturnsEmptyTempData_WhenNoCookieExists() + { + var httpContext = CreateHttpContext(); + var tempData = cookieTempDataProvider.LoadTempData(httpContext); + + Assert.NotNull(tempData); + Assert.Empty(tempData); + } + + [Fact] + public void Save_DeletesCookie_WhenNoDataToSave() + { + var httpContext = CreateHttpContext(); + var tempData = new TempData(); + cookieTempDataProvider.SaveTempData(httpContext, tempData.Save()); + + var cookieFeature = httpContext.Features.Get(); + Assert.NotNull(cookieFeature); + Assert.Contains(".AspNetCore.Components.TempData", cookieFeature.DeletedCookies); + } + + [Fact] + public void Save_SetsCookie_WhenDataExists() + { + var httpContext = CreateHttpContext(); + var tempData = new TempData(); + tempData["Key1"] = "Value1"; + + cookieTempDataProvider.SaveTempData(httpContext, tempData.Save()); + + var cookieFeature = httpContext.Features.Get(); + Assert.NotNull(cookieFeature); + Assert.True(cookieFeature.SetCookies.ContainsKey(".AspNetCore.Components.TempData")); + } + + [Fact] + public void RoundTrip_PreservesStringValue() + { + var httpContext = CreateHttpContext(); + var tempData = new TempData(); + tempData["StringKey"] = "StringValue"; + + cookieTempDataProvider.SaveTempData(httpContext, tempData.Save()); + SimulateCookieRoundTrip(httpContext); + var loadedTempData = cookieTempDataProvider.LoadTempData(httpContext); + + Assert.Equal("StringValue", loadedTempData["StringKey"]); + } + + [Fact] + public void RoundTrip_PreservesIntValue() + { + var httpContext = CreateHttpContext(); + var tempData = new TempData(); + tempData["IntKey"] = 42; + + cookieTempDataProvider.SaveTempData(httpContext, tempData.Save()); + SimulateCookieRoundTrip(httpContext); + var loadedTempData = cookieTempDataProvider.LoadTempData(httpContext); + + Assert.Equal(42, loadedTempData["IntKey"]); + } + + [Fact] + public void RoundTrip_PreservesBoolValue() + { + var httpContext = CreateHttpContext(); + var tempData = new TempData(); + tempData["BoolKey"] = true; + + cookieTempDataProvider.SaveTempData(httpContext, tempData.Save()); + SimulateCookieRoundTrip(httpContext); + var loadedTempData = cookieTempDataProvider.LoadTempData(httpContext); + + Assert.Equal(true, loadedTempData["BoolKey"]); + } + + [Fact] + public void RoundTrip_PreservesGuidValue() + { + var httpContext = CreateHttpContext(); + var tempData = new TempData(); + var guid = Guid.NewGuid(); + tempData["GuidKey"] = guid; + + cookieTempDataProvider.SaveTempData(httpContext, tempData.Save()); + SimulateCookieRoundTrip(httpContext); + var loadedTempData = cookieTempDataProvider.LoadTempData(httpContext); + + Assert.Equal(guid, loadedTempData["GuidKey"]); + } + + [Fact] + public void RoundTrip_PreservesDateTimeValue() + { + var httpContext = CreateHttpContext(); + var tempData = new TempData(); + var dateTime = new DateTime(2025, 12, 15, 10, 30, 0, DateTimeKind.Utc); + tempData["DateTimeKey"] = dateTime; + + cookieTempDataProvider.SaveTempData(httpContext, tempData.Save()); + SimulateCookieRoundTrip(httpContext); + var loadedTempData = cookieTempDataProvider.LoadTempData(httpContext); + + Assert.Equal(dateTime, loadedTempData["DateTimeKey"]); + } + + [Fact] + public void RoundTrip_PreservesStringArray() + { + var httpContext = CreateHttpContext(); + var tempData = new TempData(); + var array = new[] { "one", "two", "three" }; + tempData["ArrayKey"] = array; + + cookieTempDataProvider.SaveTempData(httpContext, tempData.Save()); + SimulateCookieRoundTrip(httpContext); + var loadedTempData = cookieTempDataProvider.LoadTempData(httpContext); + + Assert.Equal(array, loadedTempData["ArrayKey"]); + } + + [Fact] + public void RoundTrip_PreservesIntArray() + { + var httpContext = CreateHttpContext(); + var tempData = new TempData(); + var array = new[] { 1, 2, 3 }; + tempData["ArrayKey"] = array; + + cookieTempDataProvider.SaveTempData(httpContext, tempData.Save()); + SimulateCookieRoundTrip(httpContext); + var loadedTempData = cookieTempDataProvider.LoadTempData(httpContext); + + Assert.Equal(array, loadedTempData["ArrayKey"]); + } + + [Fact] + public void RoundTrip_PreservesDictionary() + { + var httpContext = CreateHttpContext(); + var tempData = new TempData(); + var dict = new Dictionary { ["a"] = "1", ["b"] = "2" }; + tempData["DictKey"] = dict; + + cookieTempDataProvider.SaveTempData(httpContext, tempData.Save()); + SimulateCookieRoundTrip(httpContext); + var loadedTempData = cookieTempDataProvider.LoadTempData(httpContext); + + var loadedDict = Assert.IsType>(loadedTempData["DictKey"]); + Assert.Equal("1", loadedDict["a"]); + Assert.Equal("2", loadedDict["b"]); + } + + [Fact] + public void RoundTrip_PreservesMultipleDifferentValues() + { + var httpContext = CreateHttpContext(); + var tempData = new TempData(); + tempData["Key1"] = "Value1"; + tempData["Key2"] = 123; + tempData["Key3"] = true; + + cookieTempDataProvider.SaveTempData(httpContext, tempData.Save()); + SimulateCookieRoundTrip(httpContext); + var loadedTempData = cookieTempDataProvider.LoadTempData(httpContext); + + Assert.Equal("Value1", loadedTempData["Key1"]); + Assert.Equal(123, loadedTempData["Key2"]); + Assert.Equal(true, loadedTempData["Key3"]); + } + + [Fact] + public void Save_ThrowsForUnsupportedType() + { + var httpContext = CreateHttpContext(); + var tempData = new TempData(); + tempData["Key"] = new object(); + + Assert.Throws(() => cookieTempDataProvider.SaveTempData(httpContext, tempData.Save())); + } + + [Fact] + public void Load_ReturnsEmptyTempData_ForInvalidBase64Cookie() + { + var httpContext = CreateHttpContext(); + httpContext.Request.Headers["Cookie"] = ".AspNetCore.Components.TempData=not-valid-base64!!!"; + var tempData = cookieTempDataProvider.LoadTempData(httpContext); + + Assert.NotNull(tempData); + Assert.Empty(tempData); + } + + [Fact] + public void Load_ReturnsEmptyTempData_ForUnsupportedType() + { + var httpContext = CreateHttpContext(); + var json = "{\"Key\":[true, false, true]}"; + var encoded = Microsoft.AspNetCore.WebUtilities.WebEncoders.Base64UrlEncode(System.Text.Encoding.UTF8.GetBytes(json)); + httpContext.Request.Headers["Cookie"] = $".AspNetCore.Components.TempData={encoded}"; + var tempData = cookieTempDataProvider.LoadTempData(httpContext); + + Assert.NotNull(tempData); + Assert.Empty(tempData); + } + + private static DefaultHttpContext CreateHttpContext() + { + var services = new ServiceCollection() + .AddSingleton() + .BuildServiceProvider(); + + var httpContext = new DefaultHttpContext + { + RequestServices = services + }; + httpContext.Request.Scheme = "https"; + httpContext.Request.Host = new HostString("localhost"); + + var cookieFeature = new TestResponseCookiesFeature(); + httpContext.Features.Set(cookieFeature); + httpContext.Features.Set(cookieFeature); + + return httpContext; + } + + private static void SimulateCookieRoundTrip(HttpContext httpContext) + { + var cookieFeature = httpContext.Features.Get(); + if (cookieFeature != null && cookieFeature.SetCookies.TryGetValue(".AspNetCore.Components.TempData", out var cookieValue)) + { + httpContext.Request.Headers["Cookie"] = $".AspNetCore.Components.TempData={cookieValue}"; + } + } + + private class PassThroughDataProtectionProvider : IDataProtectionProvider + { + public IDataProtector CreateProtector(string purpose) => new PassThroughDataProtector(); + + private class PassThroughDataProtector : IDataProtector + { + public IDataProtector CreateProtector(string purpose) => this; + public byte[] Protect(byte[] plaintext) => plaintext; + public byte[] Unprotect(byte[] protectedData) => protectedData; + } + } + + private class TestResponseCookiesFeature : IResponseCookiesFeature + { + public Dictionary SetCookies { get; } = new(); + public HashSet DeletedCookies { get; } = new(); + + public IResponseCookies Cookies => new TestResponseCookies(this); + + private class TestResponseCookies : IResponseCookies + { + private readonly TestResponseCookiesFeature _feature; + + public TestResponseCookies(TestResponseCookiesFeature feature) + { + _feature = feature; + } + + public void Append(string key, string value) => Append(key, value, new CookieOptions()); + + public void Append(string key, string value, CookieOptions options) + { + _feature.SetCookies[key] = value; + } + + public void Delete(string key) => Delete(key, new CookieOptions()); + + public void Delete(string key, CookieOptions options) + { + _feature.DeletedCookies.Add(key); + } + } + } +} diff --git a/src/Components/Endpoints/test/TempData/JsonTempDataSerializerTest.cs b/src/Components/Endpoints/test/TempData/JsonTempDataSerializerTest.cs new file mode 100644 index 000000000000..f0919234c976 --- /dev/null +++ b/src/Components/Endpoints/test/TempData/JsonTempDataSerializerTest.cs @@ -0,0 +1,283 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; + +namespace Microsoft.AspNetCore.Components.Endpoints; + +public class JsonTempDataSerializerTest +{ + private static JsonTempDataSerializer CreateSerializer() => new JsonTempDataSerializer(); + + public static TheoryData InvalidTypes + { + get + { + return new TheoryData + { + { typeof(object) }, + { typeof(object[]) }, + { typeof(TestItem) }, + { typeof(List) }, + { typeof(Dictionary) }, + }; + } + } + + public static TheoryData InvalidDictionaryKeyTypes + { + get + { + return new TheoryData + { + { typeof(Dictionary) }, + { typeof(Dictionary) }, + { typeof(Dictionary) }, + { typeof(Dictionary) } + }; + } + } + + public static TheoryData ValidTypes + { + get + { + return new TheoryData + { + { typeof(int) }, + { typeof(int[]) }, + { typeof(string) }, + { typeof(Uri) }, + { typeof(Guid) }, + { typeof(List) }, + { typeof(DateTimeOffset) }, + { typeof(decimal) }, + { typeof(Dictionary) }, + { typeof(Uri[]) }, + { typeof(DayOfWeek) }, + { typeof(DateTime) }, + { typeof(bool) }, + { typeof(TimeSpan) }, + }; + } + } + + [Theory] + [MemberData(nameof(InvalidTypes))] + public void EnsureObjectCanBeSerialized_ReturnsFalse_OnInvalidType(Type type) + { + // Arrange + var serializer = CreateSerializer(); + + // Act + var result = serializer.EnsureObjectCanBeSerialized(type); + + // Assert + Assert.False(result); + } + + [Theory] + [MemberData(nameof(InvalidDictionaryKeyTypes))] + public void EnsureObjectCanBeSerialized_ReturnsFalse_OnInvalidDictionaryKeyType(Type type) + { + // Arrange + var serializer = CreateSerializer(); + + // Act + var result = serializer.EnsureObjectCanBeSerialized(type); + + // Assert + Assert.False(result); + } + + [Theory] + [MemberData(nameof(ValidTypes))] + public void EnsureObjectCanBeSerialized_ReturnsTrue_OnValidType(Type type) + { + // Arrange + var serializer = CreateSerializer(); + + // Act + var result = serializer.EnsureObjectCanBeSerialized(type); + + // Assert + Assert.True(result); + } + + [Fact] + public void Deserialize_Int() + { + // Arrange + var serializer = CreateSerializer(); + var json = "42"; + var element = JsonDocument.Parse(json).RootElement; + + // Act + var result = serializer.Deserialize(element); + + // Assert + Assert.Equal(42, result); + } + + [Fact] + public void Deserialize_Bool() + { + // Arrange + var serializer = CreateSerializer(); + var element = JsonDocument.Parse("true").RootElement; + + // Act + var result = serializer.Deserialize(element); + + // Assert + Assert.Equal(true, result); + } + + [Fact] + public void Deserialize_String() + { + // Arrange + var serializer = CreateSerializer(); + var element = JsonDocument.Parse("\"hello\"").RootElement; + + // Act + var result = serializer.Deserialize(element); + + // Assert + Assert.Equal("hello", result); + } + + [Fact] + public void Deserialize_Guid() + { + // Arrange + var serializer = CreateSerializer(); + var guid = Guid.NewGuid(); + var element = JsonDocument.Parse($"\"{guid}\"").RootElement; + + // Act + var result = serializer.Deserialize(element); + + // Assert + Assert.Equal(guid, result); + } + + [Fact] + public void Deserialize_DateTime() + { + // Arrange + var serializer = CreateSerializer(); + var dateTime = new DateTime(2007, 1, 1, 0, 0, 0, DateTimeKind.Utc); + var element = JsonDocument.Parse($"\"{dateTime:O}\"").RootElement; + + // Act + var result = serializer.Deserialize(element); + + // Assert + Assert.IsType(result); + Assert.Equal(dateTime, result); + } + + [Fact] + public void Deserialize_Null() + { + // Arrange + var serializer = CreateSerializer(); + var element = JsonDocument.Parse("null").RootElement; + + // Act + var result = serializer.Deserialize(element); + + // Assert + Assert.Null(result); + } + + [Fact] + public void Deserialize_Array() + { + // Arrange + var serializer = CreateSerializer(); + var element = JsonDocument.Parse("[1, 2, 3]").RootElement; + + // Act + var result = serializer.Deserialize(element); + + // Assert + var array = Assert.IsType(result); + Assert.Equal(3, array.Length); + Assert.Equal(1, array[0]); + Assert.Equal(2, array[1]); + Assert.Equal(3, array[2]); + } + + [Fact] + public void Deserialize_EmptyArray() + { + // Arrange + var serializer = CreateSerializer(); + var element = JsonDocument.Parse("[]").RootElement; + + // Act + var result = serializer.Deserialize(element); + + // Assert + var array = Assert.IsType(result); + Assert.Empty(array); + } + + [Fact] + public void Deserialize_Dictionary() + { + // Arrange + var serializer = CreateSerializer(); + var element = JsonDocument.Parse("{\"key1\": 1, \"key2\": 2}").RootElement; + + // Act + var result = serializer.Deserialize(element); + + // Assert + var dictionary = Assert.IsType>(result); + Assert.Equal(2, dictionary.Count); + Assert.Equal(1, dictionary["key1"]); + Assert.Equal(2, dictionary["key2"]); + } + + [Fact] + public void Deserialize_StringArray() + { + // Arrange + var serializer = CreateSerializer(); + var element = JsonDocument.Parse("[\"foo\", \"bar\"]").RootElement; + + // Act + var result = serializer.Deserialize(element); + + // Assert + var array = Assert.IsType(result); + Assert.Equal(2, array.Length); + Assert.Equal("foo", array[0]); + Assert.Equal("bar", array[1]); + } + + [Fact] + public void Deserialize_DictionaryWithStringValues() + { + // Arrange + var serializer = CreateSerializer(); + var element = JsonDocument.Parse("{\"key1\": \"value1\", \"key2\": \"value2\"}").RootElement; + + // Act + var result = serializer.Deserialize(element); + + // Assert + var dictionary = Assert.IsType>(result); + Assert.Equal(2, dictionary.Count); + Assert.Equal("value1", dictionary["key1"]); + Assert.Equal("value2", dictionary["key2"]); + } + + private class TestItem + { + public int DummyInt { get; set; } + } +} diff --git a/src/Components/Endpoints/test/TempData/TempDataTest.cs b/src/Components/Endpoints/test/TempData/TempDataTest.cs new file mode 100644 index 000000000000..e02c062ffaad --- /dev/null +++ b/src/Components/Endpoints/test/TempData/TempDataTest.cs @@ -0,0 +1,210 @@ +// 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; + +public class TempDataTest +{ + [Fact] + public void Indexer_CanSetAndGetValues() + { + var tempData = new TempData(); + tempData["Key1"] = "Value1"; + var value = tempData["Key1"]; + Assert.Equal("Value1", value); + } + + [Fact] + public void Get_ReturnsValueAndRemovesFromRetainedKeys() + { + var tempData = new TempData(); + tempData["Key1"] = "Value1"; + + var value = tempData.Get("Key1"); + + Assert.Equal("Value1", value); + var saved = tempData.Save(); + Assert.Empty(saved); + } + + [Fact] + public void Get_ReturnsNullForNonExistentKey() + { + var tempData = new TempData(); + var value = tempData.Get("NonExistent"); + Assert.Null(value); + } + + [Fact] + public void Peek_ReturnsValueWithoutRemovingFromRetainedKeys() + { + var tempData = new TempData(); + tempData["Key1"] = "Value1"; + var value = tempData.Peek("Key1"); + Assert.Equal("Value1", value); + value = tempData.Get("Key1"); + Assert.Equal("Value1", value); + } + + [Fact] + public void Peek_ReturnsNullForNonExistentKey() + { + var tempData = new TempData(); + var value = tempData.Peek("NonExistent"); + Assert.Null(value); + } + + [Fact] + public void Keep_RetainsAllKeys() + { + var tempData = new TempData(); + tempData["Key1"] = "Value1"; + tempData["Key2"] = "Value2"; + _ = tempData.Get("Key1"); + _ = tempData.Get("Key2"); + + tempData.Keep(); + + var value1 = tempData.Get("Key1"); + var value2 = tempData.Get("Key2"); + Assert.Equal("Value1", value1); + Assert.Equal("Value2", value2); + } + + [Fact] + public void KeepWithKey_RetainsSpecificKey() + { + var tempData = new TempData(); + tempData["Key1"] = "Value1"; + tempData["Key2"] = "Value2"; + _ = tempData.Get("Key1"); + _ = tempData.Get("Key2"); + + tempData.Keep("Key1"); + + var saved = tempData.Save(); + Assert.Single(saved); + Assert.Equal("Value1", saved["Key1"]); + } + + [Fact] + public void KeepWithKey_DoesNothingForNonExistentKey() + { + var tempData = new TempData(); + tempData["Key1"] = "Value1"; + _ = tempData.Get("Key1"); + + tempData.Keep("NonExistent"); + + var value = tempData.Get("NonExistent"); + Assert.Null(value); + } + + [Fact] + public void ContainsKey_ReturnsTrueForExistingKey() + { + var tempData = new TempData(); + tempData["Key1"] = "Value1"; + var result = tempData.ContainsKey("Key1"); + Assert.True(result); + } + + [Fact] + public void ContainsKey_ReturnsFalseForNonExistentKey() + { + var tempData = new TempData(); + var result = tempData.ContainsKey("NonExistent"); + Assert.False(result); + } + + [Fact] + public void Remove_RemovesKeyAndReturnsTrue() + { + var tempData = new TempData(); + tempData["Key1"] = "Value1"; + + var result = tempData.Remove("Key1"); + + Assert.True(result); + var value = tempData.Get("Key1"); + Assert.Null(value); + } + + [Fact] + public void Remove_ReturnsFalseForNonExistentKey() + { + var tempData = new TempData(); + var result = tempData.Remove("NonExistent"); + Assert.False(result); + } + + [Fact] + public void Save_ReturnsOnlyRetainedKeys() + { + var tempData = new TempData(); + tempData["Key1"] = "Value1"; + tempData["Key2"] = "Value2"; + tempData["Key3"] = "Value3"; + _ = tempData.Get("Key1"); + _ = tempData.Get("Key2"); + + var saved = tempData.Save(); + + Assert.Single(saved); + Assert.Equal("Value3", saved["Key3"]); + } + + [Fact] + public void Load_PopulatesDataFromDictionary() + { + var tempData = new TempData(); + var dataToLoad = new Dictionary + { + ["Key1"] = "Value1", + ["Key2"] = "Value2" + }; + + tempData.Load(dataToLoad); + + Assert.Equal("Value1", tempData.Get("Key1")); + Assert.Equal("Value2", tempData.Get("Key2")); + } + + [Fact] + public void Load_ClearsExistingDataBeforeLoading() + { + var tempData = new TempData(); + tempData["ExistingKey"] = "ExistingValue"; + var dataToLoad = new Dictionary + { + ["NewKey"] = "NewValue" + }; + + tempData.Load(dataToLoad); + + Assert.False(tempData.ContainsKey("ExistingKey")); + Assert.True(tempData.ContainsKey("NewKey")); + } + + [Fact] + public void Clear_RemovesAllData() + { + var tempData = new TempData(); + tempData["Key1"] = "Value1"; + tempData["Key2"] = "Value2"; + + tempData.Clear(); + + Assert.Null(tempData.Get("Key1")); + Assert.Null(tempData.Get("Key2")); + } + + [Fact] + public void Indexer_IsCaseInsensitive() + { + var tempData = new TempData(); + tempData["Key1"] = "Value1"; + var value = tempData["KEY1"]; + Assert.Equal("Value1", value); + } +} diff --git a/src/Components/test/E2ETest/Tests/TempDataTest.cs b/src/Components/test/E2ETest/Tests/TempDataTest.cs new file mode 100644 index 000000000000..75ed38cdda2c --- /dev/null +++ b/src/Components/test/E2ETest/Tests/TempDataTest.cs @@ -0,0 +1,126 @@ +// 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.E2ETest.Infrastructure; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; +using Components.TestServer.RazorComponents; +using Microsoft.AspNetCore.E2ETesting; +using Xunit.Abstractions; +using OpenQA.Selenium; +using TestServer; + +namespace Microsoft.AspNetCore.Components.E2ETest.Tests; + +public class TempDataTest : ServerTestBase>> +{ + private const string TempDataCookieName = ".AspNetCore.Components.TempData"; + + public TempDataTest( + BrowserFixture browserFixture, + BasicTestAppServerSiteFixture> serverFixture, + ITestOutputHelper output) + : base(browserFixture, serverFixture, output) + { + } + + public override Task InitializeAsync() => InitializeAsync(BrowserFixture.StreamingContext); + + protected override void InitializeAsyncCore() + { + base.InitializeAsyncCore(); + Browser.Manage().Cookies.DeleteCookieNamed(TempDataCookieName); + } + + [Fact] + public void TempDataCanPersistThroughNavigation() + { + Navigate($"{ServerPathBase}/tempdata"); + + Browser.Equal("No message", () => Browser.FindElement(By.Id("message")).Text); + Browser.FindElement(By.Id("set-values-button")).Click(); + Browser.Equal("Message", () => Browser.FindElement(By.Id("message")).Text); + } + + [Fact] + public void TempDataCanPersistThroughDifferentPages() + { + Navigate($"{ServerPathBase}/tempdata"); + + Browser.Equal("No message", () => Browser.FindElement(By.Id("message")).Text); + Browser.FindElement(By.Id("set-values-button-diff-page")).Click(); + Browser.Equal("Message", () => Browser.FindElement(By.Id("message")).Text); + } + + [Fact] + public void TempDataPeekDoesntDelete() + { + Navigate($"{ServerPathBase}/tempdata"); + + Browser.Equal("No message", () => Browser.FindElement(By.Id("message")).Text); + Browser.FindElement(By.Id("set-values-button")).Click(); + Browser.Equal("Message", () => Browser.FindElement(By.Id("message")).Text); + Browser.FindElement(By.Id("redirect-button")).Click(); + Browser.Equal("No message", () => Browser.FindElement(By.Id("message")).Text); + Browser.Equal("Peeked value", () => Browser.FindElement(By.Id("peeked-value")).Text); + } + + [Fact] + public void TempDataKeepAllElements() + { + Navigate($"{ServerPathBase}/tempdata?ValueToKeep=all"); + + Browser.Equal("No message", () => Browser.FindElement(By.Id("message")).Text); + Browser.FindElement(By.Id("set-values-button")).Click(); + Browser.Equal("Kept value", () => Browser.FindElement(By.Id("kept-value")).Text); + Browser.Equal("Message", () => Browser.FindElement(By.Id("message")).Text); + Browser.FindElement(By.Id("redirect-button")).Click(); + Browser.Equal("Kept value", () => Browser.FindElement(By.Id("kept-value")).Text); + Browser.Equal("Message", () => Browser.FindElement(By.Id("message")).Text); + } + + [Fact] + public void TempDataKeepOneElement() + { + Navigate($"{ServerPathBase}/tempdata?ValueToKeep=KeptValue"); + + Browser.Equal("No message", () => Browser.FindElement(By.Id("message")).Text); + Browser.FindElement(By.Id("set-values-button")).Click(); + Browser.Equal("Kept value", () => Browser.FindElement(By.Id("kept-value")).Text); + Browser.Equal("Message", () => Browser.FindElement(By.Id("message")).Text); + Browser.FindElement(By.Id("redirect-button")).Click(); + Browser.Equal("No message", () => Browser.FindElement(By.Id("message")).Text); + Browser.Equal("Kept value", () => Browser.FindElement(By.Id("kept-value")).Text); + } + + [Fact] + public void CanRemoveTheElementWithRemove() + { + Navigate($"{ServerPathBase}/tempdata"); + + Browser.Equal("No peeked value", () => Browser.FindElement(By.Id("peeked-value")).Text); + Browser.FindElement(By.Id("set-values-button")).Click(); + Browser.Equal("Peeked value", () => Browser.FindElement(By.Id("peeked-value")).Text); + Browser.Equal("Message", () => Browser.FindElement(By.Id("message")).Text); + Browser.FindElement(By.Id("redirect-button")).Click(); + Browser.Equal("No message", () => Browser.FindElement(By.Id("message")).Text); + Browser.Equal("Peeked value", () => Browser.FindElement(By.Id("peeked-value")).Text); + Browser.FindElement(By.Id("delete-button")).Click(); + Browser.Equal("No message", () => Browser.FindElement(By.Id("message")).Text); + Browser.Equal("No peeked value", () => Browser.FindElement(By.Id("peeked-value")).Text); + } + + [Fact] + public void CanCheckIfTempDataContainsKey() + { + Navigate($"{ServerPathBase}/tempdata"); + + Browser.Equal("False", () => Browser.FindElement(By.Id("contains-peeked-value")).Text); + Browser.Equal("False", () => Browser.FindElement(By.Id("contains-message")).Text); + Browser.FindElement(By.Id("set-values-button")).Click(); + Browser.Equal("True", () => Browser.FindElement(By.Id("contains-peeked-value")).Text); + Browser.Equal("True", () => Browser.FindElement(By.Id("contains-message")).Text); + Browser.FindElement(By.Id("redirect-button")).Click(); + Browser.Equal("True", () => Browser.FindElement(By.Id("contains-peeked-value")).Text); + Browser.Equal("False", () => Browser.FindElement(By.Id("contains-message")).Text); + } +} diff --git a/src/Components/test/testassets/Components.TestServer/Program.cs b/src/Components/test/testassets/Components.TestServer/Program.cs index 2f06b00b72ac..f42fbdc26f2e 100644 --- a/src/Components/test/testassets/Components.TestServer/Program.cs +++ b/src/Components/test/testassets/Components.TestServer/Program.cs @@ -38,6 +38,7 @@ public static async Task Main(string[] args) ["Hot Reload"] = (BuildWebHost(CreateAdditionalArgs(args)), "/subdir"), ["Dev server client-side blazor"] = CreateDevServerHost(CreateAdditionalArgs(args)), ["Global Interactivity"] = (BuildWebHost>(CreateAdditionalArgs(args)), "/subdir"), + ["SSR (No Interactivity)"] = (BuildWebHost>(CreateAdditionalArgs(args)), "/subdir"), }; var mainHost = BuildWebHost(args); diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/TempData/TempDataComponent.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/TempData/TempDataComponent.razor new file mode 100644 index 000000000000..ed4ce0680f49 --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/TempData/TempDataComponent.razor @@ -0,0 +1,101 @@ +@page "/tempdata" +@using Microsoft.AspNetCore.Components.Forms +@inject NavigationManager NavigationManager + +

TempData Basic Test

+ +
+ + + + +
+ + + + +
+ + + + +
+ + + + +
+ +

@_message

+

@_peekedValue

+

@_keptValue

+ +

@_containsMessageKey

+

@_containsPeekedValueKey

+ +@code { + [SupplyParameterFromForm(Name = "_handler")] + public string? Handler { get; set; } + + [CascadingParameter] + public ITempData? TempData { get; set; } + + [SupplyParameterFromQuery] + public string ValueToKeep { get; set; } = string.Empty; + + [SupplyParameterFromQuery] + public string ContainsKey { get; set; } = string.Empty; + + private string? _message; + private string? _peekedValue; + private string? _keptValue; + + private bool _containsMessageKey; + private bool _containsPeekedValueKey; + + protected override void OnInitialized() + { + _containsMessageKey = TempData?.ContainsKey("Message") ?? false; + _containsPeekedValueKey = TempData?.ContainsKey("PeekedValue") ?? false; + + if (Handler is not null) + { + return; + } + _message = TempData!.Get("Message") as string ?? "No message"; + _peekedValue = TempData!.Peek("PeekedValue") as string ?? "No peeked value"; + _keptValue = TempData!.Get("KeptValue") as string ?? "No kept value"; + + if (ValueToKeep == "all") + { + TempData!.Keep(); + } + else if (!string.IsNullOrEmpty(ValueToKeep)) + { + TempData!.Keep(ValueToKeep); + } + } + + private void SetValues(bool differentPage = false) + { + TempData!["Message"] = "Message"; + TempData!["PeekedValue"] = "Peeked value"; + TempData!["KeptValue"] = "Kept value"; + if (differentPage) + { + NavigateToDifferentPage(); + return; + } + NavigateToSamePageKeep(ValueToKeep); + } + + private void DeletePeekedValue() + { + TempData!.Remove("PeekedValue"); + NavigateToSamePage(); + } + + private void NavigateToSamePage() => NavigationManager.NavigateTo("/subdir/tempdata", forceLoad: true); + private void NavigateToSamePageKeep(string valueToKeep) => NavigationManager.NavigateTo($"/subdir/tempdata?ValueToKeep={valueToKeep}", forceLoad: true); + private void NavigateToDifferentPage() => NavigationManager.NavigateTo("/subdir/tempdata/read", forceLoad: true); +} diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/TempData/TempDataReadComponent.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/TempData/TempDataReadComponent.razor new file mode 100644 index 000000000000..950e58e1c24b --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/TempData/TempDataReadComponent.razor @@ -0,0 +1,33 @@ +@page "/tempdata/read" +@using Microsoft.AspNetCore.Components.Forms +@inject NavigationManager NavigationManager + +

TempData Read Test

+ +

@_message

+

@_peekedValue

+

@_keptValue

+ + +@code { + [CascadingParameter] + public ITempData? TempData { get; set; } + + private string? _message; + private string? _peekedValue; + private string? _keptValue; + + protected override void OnInitialized() + { + if (TempData is null) + { + return; + } + _message = TempData.Get("Message") as string; + _message ??= "No message"; + _peekedValue = TempData.Get("PeekedValue") as string; + _peekedValue ??= "No peeked value"; + _keptValue = TempData.Get("KeptValue") as string; + _keptValue ??= "No kept value"; + } +}