Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Source/Csla.Blazor.Test/Csla.Blazor.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="AwesomeAssertions" Version="8.2.0" />
<PackageReference Include="AwesomeAssertions.Analyzers" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
Expand Down
21 changes: 13 additions & 8 deletions Source/Csla.Blazor.Test/ViewModelSaveAsyncErrorTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using Csla.Blazor.Test.Fakes;
using Csla.Core;
using Csla.TestHelpers;
using FluentAssertions;
using FluentAssertions.Execution;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Csla.Blazor.Test
Expand Down Expand Up @@ -67,10 +69,12 @@ public async Task SavingSuccess_ErrorEventIsNotInvoked()
// Act
await vm.SaveAsync();

// Assert
Assert.IsFalse(error); // Error event shouldn't have been triggered
Assert.IsNull(vm.Exception);
Assert.IsNull(vm.ViewModelErrorText);
using (new AssertionScope())
{
error.Should().BeFalse(); // Error event shouldn't have been triggered
vm.Exception.Should().BeNull();
vm.ViewModelErrorText.Should().BeEmpty();
}
}


Expand All @@ -89,10 +93,11 @@ public async Task SavingWithCancellationToken_Success()
// Act
await vm.SaveAsync(cancellationToken);

// Assert

Assert.IsNull(vm.Exception);
Assert.IsNull(vm.ViewModelErrorText);
using (new AssertionScope())
{
vm.Exception.Should().BeNull();
vm.ViewModelErrorText.Should().BeEmpty();
}
}

[TestMethod]
Expand Down
51 changes: 33 additions & 18 deletions Source/Csla.Blazor.WebAssembly/ApplicationContextManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
// </copyright>
// <summary>Application context manager that uses HttpContextAccessor</summary>
//-----------------------------------------------------------------------
using System.Diagnostics.CodeAnalysis;
using System.Security.Claims;
using System.Security.Principal;
using Csla.Core;
using Csla.State;
using Microsoft.AspNetCore.Components.Authorization;
using System.Security.Claims;
using System.Security.Principal;

namespace Csla.Blazor.WebAssembly;

Expand All @@ -24,9 +25,10 @@ public class ApplicationContextManager : IContextManager, IDisposable
/// with the required IServiceProvider.
/// </summary>
/// <param name="authenticationStateProvider">AuthenticationStateProvider service</param>
/// <exception cref="ArgumentNullException"><paramref name="authenticationStateProvider"/> is <see langword="null"/>.</exception>
public ApplicationContextManager(AuthenticationStateProvider authenticationStateProvider)
{
AuthenticationStateProvider = authenticationStateProvider;
AuthenticationStateProvider = authenticationStateProvider ?? throw new ArgumentNullException(nameof(authenticationStateProvider));
AuthenticationStateProvider.AuthenticationStateChanged += AuthenticationStateProvider_AuthenticationStateChanged;
InitializeUser();
}
Expand All @@ -42,13 +44,15 @@ public ApplicationContextManager(AuthenticationStateProvider authenticationState
/// <summary>
/// Gets or sets a reference to the current ApplicationContext.
/// </summary>
public ApplicationContext ApplicationContext { get; set; }
public ApplicationContext? ApplicationContext { get; set; }

[MemberNotNull(nameof(AuthenticationState))]
private void InitializeUser()
{
AuthenticationStateProvider_AuthenticationStateChanged(AuthenticationStateProvider.GetAuthenticationStateAsync());
}

[MemberNotNull(nameof(AuthenticationState))]
private void AuthenticationStateProvider_AuthenticationStateChanged(Task<AuthenticationState> task)
{
AuthenticationState = task;
Expand Down Expand Up @@ -98,11 +102,11 @@ public virtual void SetUser(IPrincipal principal)
/// </summary>
public IContextDictionary GetLocalContext()
{
ThrowIoeIfApplicationContextIsNull();

var session = GetSession();
IContextDictionary localContext;
var sessionManager = ApplicationContext.GetRequiredService<ISessionManager>();
var session = sessionManager.GetCachedSession();
session.TryGetValue("localContext", out var result);
if (result is IContextDictionary context)
if (session.TryGetValue("localContext", out var result) && result is IContextDictionary context)
{
localContext = context;
}
Expand All @@ -118,10 +122,11 @@ public IContextDictionary GetLocalContext()
/// Sets the local context.
/// </summary>
/// <param name="localContext">Local context.</param>
public void SetLocalContext(IContextDictionary localContext)
public void SetLocalContext(IContextDictionary? localContext)
{
var sessionManager = ApplicationContext.GetRequiredService<ISessionManager>();
var session = sessionManager.GetCachedSession();
ThrowIoeIfApplicationContextIsNull();

var session = GetSession();
session["localContext"] = localContext;
}

Expand All @@ -131,11 +136,11 @@ public void SetLocalContext(IContextDictionary localContext)
/// <param name="executionLocation"></param>
public IContextDictionary GetClientContext(ApplicationContext.ExecutionLocations executionLocation)
{
ThrowIoeIfApplicationContextIsNull();

IContextDictionary clientContext;
var sessionManager = ApplicationContext.GetRequiredService<ISessionManager>();
var session = sessionManager.GetCachedSession();
session.TryGetValue("clientContext", out var result);
if (result is IContextDictionary context)
var session = GetSession();
if (session.TryGetValue("clientContext", out var result) && result is IContextDictionary context)
{
clientContext = context;
}
Expand All @@ -152,13 +157,23 @@ public IContextDictionary GetClientContext(ApplicationContext.ExecutionLocations
/// </summary>
/// <param name="clientContext">Client context.</param>
/// <param name="executionLocation"></param>
public void SetClientContext(IContextDictionary clientContext, ApplicationContext.ExecutionLocations executionLocation)
public void SetClientContext(IContextDictionary? clientContext, ApplicationContext.ExecutionLocations executionLocation)
{
var sessionManager = ApplicationContext.GetRequiredService<ISessionManager>();
var session = sessionManager.GetCachedSession();
var session = GetSession();
session["clientContext"] = clientContext;
}

private Session GetSession()
{
ThrowIoeIfApplicationContextIsNull();
var session = ApplicationContext.GetRequiredService<ISessionManager>().GetCachedSession();
ExceptionLocalizer.ThrowIfNullSessionNotRetrieved(session);
return session;
}

[MemberNotNull(nameof(ApplicationContext))]
private void ThrowIoeIfApplicationContextIsNull() => _ = ApplicationContext ?? throw new InvalidOperationException($"{nameof(ApplicationContext)} == null");

/// <summary>
/// Dispose this object's resources.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,9 @@ public class CslaAuthenticationStateProvider : AuthenticationStateProvider
/// </summary>
public CslaAuthenticationStateProvider()
{
SetPrincipal(new ClaimsPrincipal());
}

private AuthenticationState AuthenticationState { get; set; }
private AuthenticationState AuthenticationState { get; set; } = new AuthenticationState(new ClaimsPrincipal());

/// <summary>
/// Gets the authentication state.
Expand All @@ -37,8 +36,11 @@ public override Task<AuthenticationState> GetAuthenticationStateAsync()
/// Sets the principal representing the current user identity.
/// </summary>
/// <param name="principal">ClaimsPrincipal instance</param>
/// <exception cref="ArgumentNullException"><paramref name="principal"/> is <see langword="null"/>.</exception>
public void SetPrincipal(ClaimsPrincipal principal)
{
ArgumentNullException.ThrowIfNull(principal);

AuthenticationState = new AuthenticationState(principal);
NotifyAuthenticationStateChanged(Task.FromResult(AuthenticationState));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
// </copyright>
// <summary>Implement extension methods for .NET Core configuration</summary>
//-----------------------------------------------------------------------
using Csla.State;
using Csla.Blazor;
using Csla.Blazor.WebAssembly.Configuration;
using Csla.State;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Csla.Blazor.WebAssembly.Configuration;

namespace Csla.Configuration
{
Expand All @@ -24,6 +24,7 @@ public static class BlazorWasmConfigurationExtensions
/// Registers services necessary for Blazor WebAssembly.
/// </summary>
/// <param name="config">CslaConfiguration object</param>
/// <exception cref="ArgumentNullException"><paramref name="config"/> is <see langword="null"/>.</exception>
public static CslaOptions AddBlazorWebAssembly(this CslaOptions config)
{
return AddBlazorWebAssembly(config, null);
Expand All @@ -34,8 +35,11 @@ public static CslaOptions AddBlazorWebAssembly(this CslaOptions config)
/// </summary>
/// <param name="config">CslaConfiguration object</param>
/// <param name="options">Options object</param>
public static CslaOptions AddBlazorWebAssembly(this CslaOptions config, Action<BlazorWebAssemblyConfigurationOptions> options)
/// <exception cref="ArgumentNullException"><paramref name="config"/> is <see langword="null"/>.</exception>
public static CslaOptions AddBlazorWebAssembly(this CslaOptions config, Action<BlazorWebAssemblyConfigurationOptions>? options)
{
ArgumentNullException.ThrowIfNull(config);

var blazorOptions = new BlazorWebAssemblyConfigurationOptions();
options?.Invoke(blazorOptions);

Expand Down
2 changes: 2 additions & 0 deletions Source/Csla.Blazor.WebAssembly/Csla.Blazor.WebAssembly.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
<BaseOutputPath>..\..\Bin</BaseOutputPath>
<Title>CSLA .NET Blazor WebAssembly</Title>
<PackageTags>CSLA;Blazor;aspnetcore;WebAssembly;wasm</PackageTags>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable</WarningsAsErrors>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
Expand Down
29 changes: 29 additions & 0 deletions Source/Csla.Blazor.WebAssembly/ExceptionLocalizer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Csla.Blazor.State;
using Csla.Properties;
using Csla.State;

namespace Csla.Blazor.WebAssembly;

internal static class ExceptionLocalizer
{
[StackTraceHidden]
public static void ThrowIfNullSessionNotRetrieved([NotNull] Session? session)
{
if (session is not null)
{
return;
}

Throw();

[DoesNotReturn, StackTraceHidden]
static void Throw()
{
const string SessionRetrievalHint = $"await {nameof(StateManager)}.{nameof(StateManager.InitializeAsync)}()";
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resources.WasmApplicationContextManagerSessionNotRetrieved, SessionRetrievalHint));
}
}
}
32 changes: 15 additions & 17 deletions Source/Csla.Blazor.WebAssembly/State/SessionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// <summary>Manages all user session data</summary>
//-----------------------------------------------------------------------

using System.Globalization;
using System.Net.Http.Json;
using Csla.Blazor.Authentication;
using Csla.Blazor.State.Messages;
Expand All @@ -23,18 +24,18 @@ namespace Csla.Blazor.WebAssembly.State
/// <param name="applicationContext"></param>
/// <param name="httpClient"></param>
/// <param name="options"></param>
public class SessionManager(
ApplicationContext applicationContext, HttpClient httpClient, BlazorWebAssemblyConfigurationOptions options) : ISessionManager
/// <exception cref="ArgumentNullException"><paramref name="applicationContext"/>, <paramref name="httpClient"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
public class SessionManager(ApplicationContext applicationContext, HttpClient httpClient, BlazorWebAssemblyConfigurationOptions options) : ISessionManager
{
private readonly ApplicationContext ApplicationContext = applicationContext;
private readonly HttpClient client = httpClient;
private readonly BlazorWebAssemblyConfigurationOptions _options = options;
private Session _session;
private readonly ApplicationContext ApplicationContext = applicationContext ?? throw new ArgumentNullException(nameof(applicationContext));
private readonly HttpClient client = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
private readonly BlazorWebAssemblyConfigurationOptions _options = options ?? throw new ArgumentNullException(nameof(options));
private Session? _session;

/// <summary>
/// Gets the current user's session from the cache.
/// </summary>
public Session GetCachedSession()
public Session? GetCachedSession()
{
if (!_options.SyncContextWithServer && _session == null)
{
Expand Down Expand Up @@ -78,18 +79,13 @@ public async Task<Session> RetrieveSession(CancellationToken ct)
if (_session != null)
lastTouched = _session.LastTouched;
var url = $"{_options.StateControllerName}?lastTouched={lastTouched}";
var stateResult = await client.GetFromJsonAsync<StateResult>(url, ct).ConfigureAwait(false);
if (stateResult.ResultStatus == ResultStatuses.Success)
var stateResult = (await client.GetFromJsonAsync<StateResult>(url, ct).ConfigureAwait(false)) ?? throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Csla.Properties.Resources.SessionManagerSessionStateCouldNotBeRetrieved, _options.StateControllerName, nameof(StateResult)));
if (stateResult.IsSuccess)
{
var formatter = ApplicationContext.GetRequiredService<ISerializationFormatter>();
var buffer = new MemoryStream(stateResult.SessionData)
{
Position = 0
};
var message = (SessionMessage)formatter.Deserialize(buffer);
var message = (SessionMessage)formatter.Deserialize(stateResult.SessionData)!;
_session = message.Session;
if (message.Principal is not null &&
ApplicationContext.GetRequiredService<AuthenticationStateProvider>() is CslaAuthenticationStateProvider provider)
if (message.Principal is not null && ApplicationContext.GetRequiredService<AuthenticationStateProvider>() is CslaAuthenticationStateProvider provider)
{
provider.SetPrincipal(message.Principal);
}
Expand Down Expand Up @@ -141,9 +137,11 @@ private static CancellationToken GetCancellationToken(TimeSpan timeout)
/// <returns>A task representing the asynchronous operation.</returns>
public async Task SendSession(CancellationToken ct)
{
_session.Touch();
_session?.Touch();
if (_options.SyncContextWithServer)
{
ExceptionLocalizer.ThrowIfNullSessionNotRetrieved(_session);

var formatter = ApplicationContext.GetRequiredService<ISerializationFormatter>();
var buffer = new MemoryStream();
formatter.Serialize(buffer, _session);
Expand Down
40 changes: 40 additions & 0 deletions Source/Csla.Blazor/BlazorServerConfigurationOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//-----------------------------------------------------------------------
// <copyright file="BlazorBlazorServerConfigurationOptions.cs" company="Marimer LLC">
// Copyright (c) Marimer LLC. All rights reserved.
// Website: https://cslanet.com
// </copyright>
// <summary>Implement extension methods for .NET Core configuration</summary>
//-----------------------------------------------------------------------

namespace Csla.Configuration
{
/// <summary>
/// Options for Blazor server-rendered and server-interactive.
/// </summary>
public class BlazorServerConfigurationOptions
{
/// <summary>
/// Gets or sets a value indicating whether the app
/// should be configured to use CSLA permissions
/// policies (default = true).
/// </summary>
public bool UseCslaPermissionsPolicy { get; set; } = true;

/// <summary>
/// Gets or sets a value indicating whether to use a
/// scoped DI container to manage the ApplicationContext;
/// false to use the Blazor 8 state management subsystem.
/// </summary>
public bool UseInMemoryApplicationContextManager { get; set; } = true;

/// <summary>
/// Gets or sets the type of the ISessionManager service.
/// </summary>
public Type SessionManagerType { get; set; } = Type.GetType("Csla.Blazor.State.SessionManager, Csla.AspNetCore", true)!;

/// <summary>
/// Gets or sets the type of the ISessionIdManager service.
/// </summary>
public Type SessionIdManagerType { get; set; } = Type.GetType("Csla.Blazor.State.SessionIdManager, Csla.AspNetCore", true)!;
}
}
Loading
Loading