Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Boilerplate SignalR PubSub integration (#9526) #9527

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
4 changes: 4 additions & 0 deletions .github/workflows/bit.full.ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ jobs:

- name: Simple tests (no --advancedTests)
id: simple-test
continue-on-error: true
run: |
dotnet new bit-bp --name SimpleTest --database Sqlite --framework net8.0
cd SimpleTest/src/Server/SimpleTest.Server.Api/
Expand All @@ -79,6 +80,7 @@ jobs:

- name: Test Sqlite database option
id: sqlite-test
continue-on-error: true
run: |
dotnet new bit-bp --name TestSqlite --database Sqlite --advancedTests --framework net9.0
cd TestSqlite/src/Server/TestSqlite.Server.Api/
Expand All @@ -101,6 +103,7 @@ jobs:

- name: Test SqlServer database option
id: sqlserver-test
continue-on-error: true
run: |
dotnet new bit-bp --name TestSqlServer --database SqlServer --advancedTests --framework net8.0
cd TestSqlServer/src/Server/TestSqlServer.Server.Api/
Expand All @@ -121,6 +124,7 @@ jobs:

- name: Test Multilingual disabled option
id: multilingual-disabled-test
continue-on-error: true
run: |
dotnet new bit-bp --name MultilingualDisabled --database Sqlite --advancedTests --framework net8.0
cd MultilingualDisabled/src/Server/MultilingualDisabled.Server.Api/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,10 +180,10 @@ private void SubscribeToSignalREventsMessages()
// You can also leverage IPubSubService to notify other components in the application.
}));

signalROnDisposables.Add(hubConnection.On<string>(SignalREvents.PUBLISH_MESSAGE, async (message) =>
signalROnDisposables.Add(hubConnection.On<string, object?>(SignalREvents.PUBLISH_MESSAGE, async (message, payload) =>
{
logger.LogInformation("SignalR Message {Message} received from server to publish.", message);
PubSubService.Publish(message);
PubSubService.Publish(message, payload);
}));

hubConnection.Closed += HubConnectionStateChange;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ protected override async Task OnInitAsync()
{
unsubscribePageTitleChanged = PubSubService.Subscribe(ClientPubSubMessages.PAGE_TITLE_CHANGED, async payload =>
{
(pageTitle, pageSubtitle) = (ValueTuple<string?, string?>)payload!;
(pageTitle, pageSubtitle) = ((string, string))payload!;

StateHasChanged();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ protected override Task OnInitAsync()
{
unsubscribe = PubSubService.Subscribe(ClientPubSubMessages.SHOW_SNACK, async args =>
{
var (title, body, color) = (ValueTuple<string, string, BitColor>)args!;
var (title, body, color) = ((string, string, BitColor))args!;

await snackbarRef.Show(title, body, color);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ protected override async Task OnInitAsync()
{
if (payload is null) return;

user = (UserDto)payload;
user = payload is JsonElement jsonDocument
? jsonDocument.Deserialize(JsonSerializerOptions.GetTypeInfo<UserDto>())! // PROFILE_UPDATED can be invoked from server through SignalR
: (UserDto)payload;

await InvokeAsync(StateHasChanged);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public partial class DashboardPage
protected async override Task OnInitAsync()
{
//#if (signalR == true)
unsubscribe = PubSubService.Subscribe(SharedPubSubMessages.DASHBOARD_DATA_CHANGED, async _ =>
unsubscribe = PubSubService.Subscribe(ClientPubSubMessages.DASHBOARD_DATA_CHANGED, async _ =>
{
NavigationManager.NavigateTo(Urls.DashboardPage, replace: true);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace Boilerplate.Client.Core.Services;

public static partial class ClientPubSubMessages
public partial class ClientPubSubMessages : SharedPubSubMessages
{
public const string SHOW_SNACK = nameof(SHOW_SNACK);
public const string SHOW_MODAL = nameof(SHOW_MODAL);
Expand All @@ -12,7 +12,6 @@ public static partial class ClientPubSubMessages
public const string THEME_CHANGED = nameof(THEME_CHANGED);
public const string OPEN_NAV_PANEL = nameof(OPEN_NAV_PANEL);
public const string CULTURE_CHANGED = nameof(CULTURE_CHANGED);
public const string PROFILE_UPDATED = nameof(PROFILE_UPDATED);
/// <summary>
/// <inheritdoc cref="Parameters.IsOnline"/>
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
using Boilerplate.Server.Api.Models.Identity;
using FluentStorage.Blobs;
//+:cnd:noEmit
using ImageMagick;
using FluentStorage.Blobs;
//#if (signalR == true)
using Microsoft.AspNetCore.SignalR;
using Boilerplate.Server.Api.SignalR;
//#endif
using Boilerplate.Shared.Dtos.Identity;
using Boilerplate.Server.Api.Models.Identity;

namespace Boilerplate.Server.Api.Controllers;

[Route("api/[controller]/[action]")]
[ApiController]
public partial class AttachmentController : AppControllerBase
{
[AutoInject] private UserManager<User> userManager = default!;

[AutoInject] private IBlobStorage blobStorage = default!;
[AutoInject] private UserManager<User> userManager = default!;
//#if (signalR == true)
[AutoInject] private IHubContext<AppHub> appHubContext = default!;
//#endif

[HttpPost]
[RequestSizeLimit(11 * 1024 * 1024 /*11MB*/)]
Expand Down Expand Up @@ -63,6 +71,10 @@ public async Task UploadProfileImage(IFormFile? file, CancellationToken cancella

throw;
}

//#if (signalR == true)
await PublishUserProfileUpdated(user.Map(), cancellationToken);
//#endif
}

[HttpDelete]
Expand All @@ -87,6 +99,10 @@ public async Task RemoveProfileImage(CancellationToken cancellationToken)
throw new ResourceValidationException(result.Errors.Select(err => new LocalizedString(err.Code, err.Description)).ToArray());

await blobStorage.DeleteAsync(filePath, cancellationToken);

//#if (signalR == true)
await PublishUserProfileUpdated(user.Map(), cancellationToken);
//#endif
}

[AllowAnonymous]
Expand All @@ -106,4 +122,17 @@ public async Task<IActionResult> GetProfileImage(Guid userId, CancellationToken

return File(await blobStorage.OpenReadAsync(filePath, cancellationToken), "image/webp", enableRangeProcessing: true);
}

//#if (signalR == true)
private async Task PublishUserProfileUpdated(UserDto user, CancellationToken cancellationToken)
{
// Notify other sessions of the user that user's info has been updated, so they'll update their UI.
var currentUserSessionId = User.GetSessionId();
var userSessionIdsExceptCurrentUserSessionId = await DbContext.UserSessions
.Where(us => us.UserId == user.Id && us.Id != currentUserSessionId && us.SignalRConnectionId != null)
.Select(us => us.SignalRConnectionId!)
.ToArrayAsync(cancellationToken);
await appHubContext.Clients.Clients(userSessionIdsExceptCurrentUserSessionId).SendAsync(SignalREvents.PUBLISH_MESSAGE, SharedPubSubMessages.PROFILE_UPDATED, user, cancellationToken);
}
//#endif
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ private async Task PublishDashboardDataChanged(CancellationToken cancellationTok
{
// Checkout AppHub's comments for more info.
// In order to exclude current user session, gets its signalR connection id from database and use GroupExcept instead.
await appHubContext.Clients.Group("AuthenticatedClients").SendAsync(SignalREvents.PUBLISH_MESSAGE, SharedPubSubMessages.DASHBOARD_DATA_CHANGED, cancellationToken);
await appHubContext.Clients.Group("AuthenticatedClients").SendAsync(SignalREvents.PUBLISH_MESSAGE, SharedPubSubMessages.DASHBOARD_DATA_CHANGED, null, cancellationToken);
ysmoradi marked this conversation as resolved.
Show resolved Hide resolved
}
//#endif
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public async Task RevokeSession(Guid id, CancellationToken cancellationToken)
// Checkout AppHub's comments for more info.
if (userSession.SignalRConnectionId is not null)
{
await appHubContext.Clients.Client(userSession.SignalRConnectionId).SendAsync(SignalREvents.PUBLISH_MESSAGE, SharedPubSubMessages.SESSION_REVOKED, cancellationToken);
await appHubContext.Clients.Client(userSession.SignalRConnectionId).SendAsync(SignalREvents.PUBLISH_MESSAGE, SharedPubSubMessages.SESSION_REVOKED, null, cancellationToken);
}
//#endif
}
Expand All @@ -107,7 +107,19 @@ public async Task<UserDto> Update(EditUserDto userDto, CancellationToken cancell
if (result.Succeeded is false)
throw new ResourceValidationException(result.Errors.Select(err => new LocalizedString(err.Code, err.Description)).ToArray());

return await GetCurrentUser(cancellationToken);
var updatedUser = await GetCurrentUser(cancellationToken);

//#if (signalR == true)
// Notify other sessions of the user that user's info has been updated, so they'll update their UI.
var currentUserSessionId = User.GetSessionId();
var userSessionIdsExceptCurrentUserSessionId = await DbContext.UserSessions
.Where(us => us.UserId == user.Id && us.Id != currentUserSessionId && us.SignalRConnectionId != null)
.Select(us => us.SignalRConnectionId!)
.ToArrayAsync(cancellationToken);
await appHubContext.Clients.Clients(userSessionIdsExceptCurrentUserSessionId).SendAsync(SignalREvents.PUBLISH_MESSAGE, SharedPubSubMessages.PROFILE_UPDATED, updatedUser, cancellationToken);
//#endif

return updatedUser;
}

[HttpPost]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ private async Task PublishDashboardDataChanged(CancellationToken cancellationTok
{
// Checkout AppHub's comments for more info.
// In order to exclude current user session, gets its signalR connection id from database and use GroupExcept instead.
await appHubContext.Clients.Group("AuthenticatedClients").SendAsync(SignalREvents.PUBLISH_MESSAGE, SharedPubSubMessages.DASHBOARD_DATA_CHANGED, cancellationToken);
await appHubContext.Clients.Group("AuthenticatedClients").SendAsync(SignalREvents.PUBLISH_MESSAGE, SharedPubSubMessages.DASHBOARD_DATA_CHANGED, null, cancellationToken);
}
//#endif
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ namespace Boilerplate.Shared.Services;
/// message keys used for pub/sub messaging between server and client through SignalR.
/// For client-only pub/sub messages, refer to the ClientPubSubMessages class in the Client/Core project.
/// </summary>
public static partial class SharedPubSubMessages
public partial class SharedPubSubMessages
{
//#if (sample == "Admin")
public const string DASHBOARD_DATA_CHANGED = nameof(DASHBOARD_DATA_CHANGED);
//#endif

public const string SESSION_REVOKED = nameof(SESSION_REVOKED);

public const string PROFILE_UPDATED = nameof(PROFILE_UPDATED);
}

public static partial class SignalREvents
Expand Down
Loading