Изменил отправку уведомлений через SignalR

1. Добавил отправку всех неотправленных уведомлений и кол-во непрочитаннах уведомлений при первом подключении
2. При изменении статуса прочтения уведомления, клиенту отправляется информация о том сколько непрочитанных уведомлений ещё есть.
3. Добавил объект NotificationMessage, который отправляется клиенту.
4. Сделал небольшой рефакторинг
This commit is contained in:
parent 60921a2bcf
commit 5f459b79b8
13 changed files with 231 additions and 104 deletions

View File

@ -18,4 +18,8 @@
<EditorConfigFiles Remove="D:\Source\AsbCloudApp\Services\.editorconfig" /> <EditorConfigFiles Remove="D:\Source\AsbCloudApp\Services\.editorconfig" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AsbCloudDb\AsbCloudDb.csproj" />
</ItemGroup>
</Project> </Project>

View File

@ -1,3 +1,4 @@
using System.Collections.Generic;
using AsbCloudApp.Data; using AsbCloudApp.Data;
using AsbCloudApp.Requests; using AsbCloudApp.Requests;
using AsbCloudApp.Services; using AsbCloudApp.Services;
@ -22,6 +23,25 @@ public interface INotificationRepository : ICrudRepository<NotificationDto>
NotificationRequest request, NotificationRequest request,
CancellationToken cancellationToken); CancellationToken cancellationToken);
/// <summary>
/// Получение всех уведомлений
/// </summary>
/// <param name="idUser"></param>
/// <param name="isSent"></param>
/// <param name="idTransportType"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<IEnumerable<NotificationDto>> GetAllAsync(int? idUser, bool? isSent, int? idTransportType,
CancellationToken cancellationToken);
/// <summary>
/// Обновление уведомлений
/// </summary>
/// <param name="notifications"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<int> UpdateRangeAsync(IEnumerable<NotificationDto> notifications, CancellationToken cancellationToken);
/// <summary> /// <summary>
/// Удаление уведомлений по параметрам /// Удаление уведомлений по параметрам
/// </summary> /// </summary>

View File

@ -16,7 +16,7 @@ public interface INotificationTransportService
int IdTransportType { get; } int IdTransportType { get; }
/// <summary> /// <summary>
/// Отправка одного уведомлений /// Отправка одного уведомления
/// </summary> /// </summary>
/// <param name="notification"></param> /// <param name="notification"></param>
/// <param name="cancellationToken"></param> /// <param name="cancellationToken"></param>

View File

@ -7,6 +7,7 @@ using AsbCloudApp.Data;
using AsbCloudApp.Exceptions; using AsbCloudApp.Exceptions;
using AsbCloudApp.Repositories; using AsbCloudApp.Repositories;
using AsbCloudApp.Requests; using AsbCloudApp.Requests;
using AsbCloudDb.Model;
namespace AsbCloudApp.Services.Notifications; namespace AsbCloudApp.Services.Notifications;
@ -42,8 +43,7 @@ public class NotificationService
public async Task NotifyAsync(NotifyRequest request, public async Task NotifyAsync(NotifyRequest request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var notificationCategory = await notificationCategoryRepository var notificationCategory = await notificationCategoryRepository.GetOrDefaultAsync(request.IdNotificationCategory, cancellationToken)
.GetOrDefaultAsync(request.IdNotificationCategory, cancellationToken)
?? throw new ArgumentInvalidException("Категория уведомления не найдена", nameof(request.IdNotificationCategory)); ?? throw new ArgumentInvalidException("Категория уведомления не найдена", nameof(request.IdNotificationCategory));
var notification = new NotificationDto var notification = new NotificationDto
@ -59,7 +59,7 @@ public class NotificationService
notification.Id = await notificationRepository.InsertAsync(notification, cancellationToken); notification.Id = await notificationRepository.InsertAsync(notification, cancellationToken);
notification.NotificationCategory = notificationCategory; notification.NotificationCategory = notificationCategory;
var notificationTransportService = GetNotificationTransportService(request.IdTransportType); var notificationTransportService = GetTransportService(request.IdTransportType);
await notificationTransportService.SendAsync(notification, cancellationToken); await notificationTransportService.SendAsync(notification, cancellationToken);
} }
@ -71,12 +71,11 @@ public class NotificationService
/// <param name="isRead"></param> /// <param name="isRead"></param>
/// <param name="cancellationToken"></param> /// <param name="cancellationToken"></param>
/// <returns></returns> /// <returns></returns>
public async Task UpdateNotificationAsync(int idNotification, public async Task UpdateAsync(int idNotification,
bool isRead, bool isRead,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var notification = await notificationRepository.GetOrDefaultAsync(idNotification, var notification = await notificationRepository.GetOrDefaultAsync(idNotification, cancellationToken)
cancellationToken)
?? throw new ArgumentInvalidException("Уведомление не найдено", nameof(idNotification)); ?? throw new ArgumentInvalidException("Уведомление не найдено", nameof(idNotification));
if(isRead && !notification.SentDate.HasValue) if(isRead && !notification.SentDate.HasValue)
@ -95,25 +94,19 @@ public class NotificationService
/// <param name="request"></param> /// <param name="request"></param>
/// <param name="cancellationToken"></param> /// <param name="cancellationToken"></param>
/// <returns></returns> /// <returns></returns>
public async Task ResendNotificationAsync(int idUser, public async Task RenotifyAsync(int idUser, CancellationToken cancellationToken)
NotificationRequest request,
CancellationToken cancellationToken)
{ {
if (!request.IdTransportType.HasValue) var notifications = await notificationRepository.GetAllAsync(idUser, false,
throw new ArgumentInvalidException("Id типа доставки уведомления должен иметь значение", Notification.IdTransportTypeSignalR,
nameof(request.IdTransportType));
var result = await notificationRepository.GetNotificationsAsync(idUser,
request,
cancellationToken); cancellationToken);
var notificationTransportService = GetNotificationTransportService(request.IdTransportType.Value); var notificationTransportService = GetTransportService(Notification.IdTransportTypeSignalR);
await notificationTransportService.SendRangeAsync(result.Items, await notificationTransportService.SendRangeAsync(notifications,
cancellationToken); cancellationToken);
} }
private INotificationTransportService GetNotificationTransportService(int idTransportType) private INotificationTransportService GetTransportService(int idTransportType)
{ {
var notificationTransportService = notificationTransportServices var notificationTransportService = notificationTransportServices
.FirstOrDefault(s => s.IdTransportType == idTransportType) .FirstOrDefault(s => s.IdTransportType == idTransportType)

View File

@ -8,6 +8,9 @@ namespace AsbCloudDb.Model;
[Table("t_notification"), Comment("Уведомления")] [Table("t_notification"), Comment("Уведомления")]
public class Notification : IId public class Notification : IId
{ {
public const int IdTransportTypeSignalR = 0;
public const int IdTransportTypeTypeEmail = 1;
[Key] [Key]
[Column("id")] [Column("id")]
public int Id { get; set; } public int Id { get; set; }

View File

@ -1,9 +1,12 @@
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AsbCloudApp.Data; using AsbCloudApp.Data;
using AsbCloudApp.Data.SAUB;
using AsbCloudApp.Repositories; using AsbCloudApp.Repositories;
using AsbCloudApp.Requests; using AsbCloudApp.Requests;
using AsbCloudApp.Services;
using AsbCloudDb; using AsbCloudDb;
using AsbCloudDb.Model; using AsbCloudDb.Model;
using Mapster; using Mapster;
@ -54,10 +57,55 @@ public class NotificationRepository : CrudRepositoryBase<NotificationDto, Notifi
return result; return result;
} }
public Task<int> DeleteAsync(NotificationDeleteRequest request, CancellationToken cancellationToken) public async Task<IEnumerable<NotificationDto>> GetAllAsync(int? idUser, bool? isSent, int? idTransportType,
CancellationToken cancellationToken)
{ {
var query = dbContext.Notifications.AsQueryable(); var query = dbContext.Notifications.AsQueryable();
if (idUser.HasValue)
query = query.Where(n => n.IdUser == idUser);
if(isSent.HasValue)
query = isSent.Value ?
query.Where(n => n.SentDate != null)
: query.Where(n => n.SentDate == null);
if (idTransportType.HasValue)
query = query.Where(n => n.IdTransportType == idTransportType);
return await query.AsNoTracking().Select(n => n.Adapt<NotificationDto>())
.ToArrayAsync(cancellationToken);
}
public async Task<int> UpdateRangeAsync(IEnumerable<NotificationDto> notifications, CancellationToken cancellationToken)
{
if (!notifications.Any())
return 0;
var ids = notifications.Select(d => d.Id);
var existingEntities = await dbSet
.Where(d => ids.Contains(d.Id))
.AsNoTracking()
.Select(d => d.Id)
.ToArrayAsync(cancellationToken);
if (ids.Count() > existingEntities.Length)
return ICrudRepository<SetpointsRequestDto>.ErrorIdNotFound;
var entities = notifications.Select(Convert);
dbContext.Notifications.UpdateRange(entities);
return await dbContext.SaveChangesAsync(cancellationToken);
}
public Task<int> DeleteAsync(NotificationDeleteRequest request, CancellationToken cancellationToken)
{
var query = dbContext.Notifications
.Include(n => n.NotificationCategory)
.AsQueryable();
if (request.IdCategory.HasValue) if (request.IdCategory.HasValue)
query = query.Where(n => n.IdNotificationCategory == request.IdCategory.Value); query = query.Where(n => n.IdNotificationCategory == request.IdCategory.Value);
@ -72,16 +120,15 @@ public class NotificationRepository : CrudRepositoryBase<NotificationDto, Notifi
return dbContext.SaveChangesAsync(cancellationToken); return dbContext.SaveChangesAsync(cancellationToken);
} }
public async Task<int> GetUnreadCountAsync(int idUser, int idTransportType, CancellationToken cancellationToken) public Task<int> GetUnreadCountAsync(int idUser, int idTransportType, CancellationToken cancellationToken)
{ {
var count = await dbContext.Notifications return dbContext.Notifications
.Where(n => n.ReadDate == null) .Where(n => !n.ReadDate.HasValue &&
.Where(n => n.IdUser == idUser) n.SentDate.HasValue &&
.Where(n => n.IdTransportType == idTransportType) n.IdUser == idUser &&
.CountAsync(cancellationToken); n.IdTransportType == idTransportType)
.CountAsync(cancellationToken);
return count; }
}
private IQueryable<Notification> BuildQuery(int idUser, private IQueryable<Notification> BuildQuery(int idUser,
NotificationRequest request) NotificationRequest request)

View File

@ -9,6 +9,7 @@ using AsbCloudApp.Data;
using AsbCloudApp.Exceptions; using AsbCloudApp.Exceptions;
using AsbCloudApp.Repositories; using AsbCloudApp.Repositories;
using AsbCloudApp.Services.Notifications; using AsbCloudApp.Services.Notifications;
using AsbCloudDb.Model;
using AsbCloudInfrastructure.Background; using AsbCloudInfrastructure.Background;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -40,7 +41,7 @@ namespace AsbCloudInfrastructure.Services.Email
this.backgroundWorker = backgroundWorker; this.backgroundWorker = backgroundWorker;
} }
public int IdTransportType => 1; public int IdTransportType => Notification.IdTransportTypeTypeEmail;
public Task SendAsync(NotificationDto notification, CancellationToken cancellationToken) public Task SendAsync(NotificationDto notification, CancellationToken cancellationToken)
{ {

View File

@ -6,6 +6,7 @@ using AsbCloudApp.Exceptions;
using AsbCloudApp.Repositories; using AsbCloudApp.Repositories;
using AsbCloudApp.Requests; using AsbCloudApp.Requests;
using AsbCloudApp.Services.Notifications; using AsbCloudApp.Services.Notifications;
using AsbCloudWebApi.SignalR.Services;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -21,12 +22,15 @@ public class NotificationController : ControllerBase
{ {
private readonly NotificationService notificationService; private readonly NotificationService notificationService;
private readonly INotificationRepository notificationRepository; private readonly INotificationRepository notificationRepository;
private readonly NotificationPublisher notificationPublisher;
public NotificationController(NotificationService notificationService, public NotificationController(NotificationService notificationService,
INotificationRepository notificationRepository) INotificationRepository notificationRepository,
NotificationPublisher notificationPublisher)
{ {
this.notificationService = notificationService; this.notificationService = notificationService;
this.notificationRepository = notificationRepository; this.notificationRepository = notificationRepository;
this.notificationPublisher = notificationPublisher;
} }
/// <summary> /// <summary>
@ -55,10 +59,17 @@ public class NotificationController : ControllerBase
[Required] bool isRead, [Required] bool isRead,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
await notificationService.UpdateNotificationAsync(idNotification, var idUser = User.GetUserId();
if (!idUser.HasValue)
return Forbid();
await notificationService.UpdateAsync(idNotification,
isRead, isRead,
cancellationToken); cancellationToken);
await notificationPublisher.PublishCountUnreadAsync(idUser.Value, cancellationToken);
return Ok(); return Ok();
} }
@ -125,28 +136,7 @@ public class NotificationController : ControllerBase
return Ok(); return Ok();
} }
/// <summary> /// <summary>
/// Получение количества непрочитанных уведомлений
/// </summary>
/// <param name="idTransportType"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpGet]
[Route("unreadNotificationCount")]
[ProducesResponseType(typeof(int), (int)System.Net.HttpStatusCode.OK)]
public async Task<IActionResult> GetUnreadCountAsync([FromQuery] int idTransportType, CancellationToken cancellationToken)
{
int? idUser = User.GetUserId();
if (!idUser.HasValue)
return Forbid();
var result = await notificationRepository.GetUnreadCountAsync(idUser.Value, idTransportType, cancellationToken);
return Ok(result);
}
/// <summary>
/// Удаление уведомлений /// Удаление уведомлений
/// </summary> /// </summary>
/// <param name="request">Параметры запроса</param> /// <param name="request">Параметры запроса</param>

View File

@ -141,6 +141,8 @@ namespace AsbCloudWebApi
{ {
services.AddTransient<INotificationTransportService, SignalRNotificationTransportService>(); services.AddTransient<INotificationTransportService, SignalRNotificationTransportService>();
services.AddTransient<INotificationTransportService, EmailNotificationTransportService>(); services.AddTransient<INotificationTransportService, EmailNotificationTransportService>();
services.AddTransient<NotificationPublisher>();
} }
} }
} }

View File

@ -0,0 +1,11 @@
using System.Collections.Generic;
using AsbCloudApp.Data;
namespace AsbCloudWebApi.SignalR.Messages;
public class NotificationMessage
{
public IEnumerable<NotificationDto>? Notifications { get; set; }
public int CountUnread { get; set; }
}

View File

@ -1,7 +1,6 @@
using System; using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AsbCloudApp.Requests;
using AsbCloudApp.Services.Notifications; using AsbCloudApp.Services.Notifications;
using AsbCloudWebApi.SignalR.Services; using AsbCloudWebApi.SignalR.Services;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -14,12 +13,15 @@ public class NotificationHub : BaseHub
{ {
private readonly ConnectionManagerService connectionManagerService; private readonly ConnectionManagerService connectionManagerService;
private readonly NotificationService notificationService; private readonly NotificationService notificationService;
private readonly NotificationPublisher notificationPublisher;
public NotificationHub(ConnectionManagerService connectionManagerService, public NotificationHub(ConnectionManagerService connectionManagerService,
NotificationService notificationService) NotificationService notificationService,
NotificationPublisher notificationPublisher)
{ {
this.connectionManagerService = connectionManagerService; this.connectionManagerService = connectionManagerService;
this.notificationService = notificationService; this.notificationService = notificationService;
this.notificationPublisher = notificationPublisher;
} }
public override async Task OnConnectedAsync() public override async Task OnConnectedAsync()
@ -35,9 +37,10 @@ public class NotificationHub : BaseHub
await base.OnConnectedAsync(); await base.OnConnectedAsync();
await notificationService.ResendNotificationAsync(idUser.Value, await notificationPublisher.PublishCountUnreadAsync(idUser.Value, CancellationToken.None);
new NotificationRequest { IsSent = false, IdTransportType = 0},
CancellationToken.None); await notificationService.RenotifyAsync(idUser.Value,
CancellationToken.None);
} }
public override async Task OnDisconnectedAsync(Exception? exception) public override async Task OnDisconnectedAsync(Exception? exception)

View File

@ -0,0 +1,76 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AsbCloudApp.Data;
using AsbCloudApp.Repositories;
using AsbCloudDb.Model;
using AsbCloudWebApi.SignalR.Messages;
using Microsoft.AspNetCore.SignalR;
namespace AsbCloudWebApi.SignalR.Services;
public class NotificationPublisher
{
private readonly ConnectionManagerService connectionManagerService;
private readonly IHubContext<NotificationHub> notificationHubContext;
private readonly INotificationRepository notificationRepository;
public NotificationPublisher(ConnectionManagerService connectionManagerService,
IHubContext<NotificationHub> notificationHubContext,
INotificationRepository notificationRepository)
{
this.connectionManagerService = connectionManagerService;
this.notificationHubContext = notificationHubContext;
this.notificationRepository = notificationRepository;
}
public async Task PublishAsync(IGrouping<int, NotificationDto> groupedNotifications, CancellationToken cancellationToken)
{
var connectionId = connectionManagerService.GetConnectionIdByUserId(groupedNotifications.Key);
if (!string.IsNullOrWhiteSpace(connectionId))
{
foreach (var notification in groupedNotifications)
{
notification.SentDate = DateTime.UtcNow;
}
var notifications = groupedNotifications.Select(n => n);
await notificationRepository.UpdateRangeAsync(notifications,
cancellationToken);
await PublishMessageAsync(connectionId, new NotificationMessage
{
Notifications = notifications,
CountUnread = await notificationRepository.GetUnreadCountAsync(groupedNotifications.Key,
Notification.IdTransportTypeSignalR,
cancellationToken)
}, cancellationToken);
}
}
public async Task PublishCountUnreadAsync(int idUser, CancellationToken cancellationToken)
{
var connectionId = connectionManagerService.GetConnectionIdByUserId(idUser);
if (!string.IsNullOrWhiteSpace(connectionId))
{
await PublishMessageAsync(connectionId, new NotificationMessage
{
CountUnread = await notificationRepository.GetUnreadCountAsync(idUser, 0,
cancellationToken)
}, cancellationToken);
}
}
private async Task PublishMessageAsync(string connectionId, NotificationMessage message,
CancellationToken cancellationToken)
{
await notificationHubContext.Clients.Client(connectionId)
.SendAsync("receiveNotifications",
message,
cancellationToken);
}
}

View File

@ -4,10 +4,9 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AsbCloudApp.Data; using AsbCloudApp.Data;
using AsbCloudApp.Repositories;
using AsbCloudApp.Services.Notifications; using AsbCloudApp.Services.Notifications;
using AsbCloudDb.Model;
using AsbCloudInfrastructure.Background; using AsbCloudInfrastructure.Background;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace AsbCloudWebApi.SignalR.Services; namespace AsbCloudWebApi.SignalR.Services;
@ -15,66 +14,44 @@ namespace AsbCloudWebApi.SignalR.Services;
public class SignalRNotificationTransportService : INotificationTransportService public class SignalRNotificationTransportService : INotificationTransportService
{ {
private readonly NotificationBackgroundWorker backgroundWorker; private readonly NotificationBackgroundWorker backgroundWorker;
private readonly ConnectionManagerService connectionManagerService;
private readonly IHubContext<NotificationHub> notificationHubContext;
public SignalRNotificationTransportService(NotificationBackgroundWorker backgroundWorker, public SignalRNotificationTransportService(NotificationBackgroundWorker backgroundWorker)
ConnectionManagerService connectionManagerService,
IHubContext<NotificationHub> notificationHubContext)
{ {
this.backgroundWorker = backgroundWorker; this.backgroundWorker = backgroundWorker;
this.connectionManagerService = connectionManagerService;
this.notificationHubContext = notificationHubContext;
} }
public int IdTransportType => 0; public int IdTransportType => Notification.IdTransportTypeSignalR;
public Task SendAsync(NotificationDto notification, public Task SendAsync(NotificationDto notification,
CancellationToken cancellationToken) CancellationToken cancellationToken) => SendRangeAsync(new[] { notification }, cancellationToken);
{
var workId = notification.Id.ToString();
if (!backgroundWorker.Contains(workId))
{
var connectionId = connectionManagerService.GetConnectionIdByUserId(notification.IdUser);
if (!string.IsNullOrWhiteSpace(connectionId))
{
var workAction = MakeSignalRSendWorkAction(notification, connectionId);
var work = new WorkBase(workId, workAction);
backgroundWorker.Push(work);
}
}
return Task.CompletedTask;
}
public Task SendRangeAsync(IEnumerable<NotificationDto> notifications, public Task SendRangeAsync(IEnumerable<NotificationDto> notifications,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var tasks = notifications var workId = HashCode.Combine(notifications.Select(n => n.Id)).ToString("x");
.Select(notification => SendAsync(notification, cancellationToken));
return Task.WhenAll(tasks); if (backgroundWorker.Contains(workId))
return Task.CompletedTask;
var workAction = MakeSignalRSendWorkAction(notifications);
var work = new WorkBase(workId, workAction);
backgroundWorker.Push(work);
return Task.CompletedTask;
} }
private Func<string, IServiceProvider, CancellationToken, Task> MakeSignalRSendWorkAction(NotificationDto notification, private Func<string, IServiceProvider, CancellationToken, Task> MakeSignalRSendWorkAction(IEnumerable<NotificationDto> notifications)
string connectionId)
{ {
const string method = "receiveNotifications";
return async (_, serviceProvider, cancellationToken) => return async (_, serviceProvider, cancellationToken) =>
{ {
notification.SentDate = DateTime.UtcNow; var notificationPublisher = serviceProvider.GetRequiredService<NotificationPublisher>();
await notificationHubContext.Clients.Client(connectionId)
.SendAsync(method,
notification,
cancellationToken);
var notificationRepository = serviceProvider.GetRequiredService<INotificationRepository>();
await notificationRepository.UpdateAsync(notification, cancellationToken); var groupedNotificationsByUsers = notifications.GroupBy(n => n.IdUser);
foreach (var groupedNotificationByUser in groupedNotificationsByUsers)
{
await notificationPublisher.PublishAsync(groupedNotificationByUser, cancellationToken);
}
}; };
} }
} }