Правки после ревью

1. Поправил обновление отправленных уведомлений.
2. Поправил ConnectionManager. Переименовал сервис, удалил абстракцию, так как предполагается только одна реализация.
3. При маппинге из dto в сущность для уведомления добавил игнорирование категории и пользователя.
4. Изменил жизенный цикл зависимости для отправителя.
5. Добавил в репозиторий метод для обновления коллекции уведомлений.
6. Поправил Hub, Id пользователя можно получать внутри хаба.
7. Поправил NotificationRequest + метод BuildQuery в NotificationRepository
This commit is contained in:
parent 4ae769adcc
commit 635e4cd7fc
13 changed files with 141 additions and 108 deletions

View File

@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AsbCloudApp.Data; using AsbCloudApp.Data;
@ -11,6 +12,15 @@ namespace AsbCloudApp.Repositories;
/// </summary> /// </summary>
public interface INotificationRepository : ICrudRepository<NotificationDto> public interface INotificationRepository : ICrudRepository<NotificationDto>
{ {
/// <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

@ -1,5 +1,3 @@
using System;
namespace AsbCloudApp.Requests; namespace AsbCloudApp.Requests;
/// <summary> /// <summary>
@ -8,9 +6,9 @@ namespace AsbCloudApp.Requests;
public class NotificationRequest : RequestBase public class NotificationRequest : RequestBase
{ {
/// <summary> /// <summary>
/// Дата отправки уведомления /// Получение отправленных/не отправленных уведомлений
/// </summary> /// </summary>
public DateTime? SentDate { get; set; } public bool IsSent { get; set; } = false;
/// <summary> /// <summary>
/// Id типа доставки уведомления /// Id типа доставки уведомления

View File

@ -30,6 +30,6 @@ public interface INotificationTransportService
/// <param name="notifications"></param> /// <param name="notifications"></param>
/// <param name="cancellationToken"></param> /// <param name="cancellationToken"></param>
/// <returns></returns> /// <returns></returns>
Task SendRangeAsync(IEnumerable<NotificationDto> notifications, Task SendRangeAsync(IEnumerable<NotificationDto> notifications,
CancellationToken cancellationToken); CancellationToken cancellationToken);
} }

View File

@ -84,6 +84,11 @@ namespace AsbCloudInfrastructure
TypeAdapterConfig.GlobalSettings.Default.Config TypeAdapterConfig.GlobalSettings.Default.Config
.ForType<WellFinalDocumentDto, WellFinalDocument>(); .ForType<WellFinalDocumentDto, WellFinalDocument>();
TypeAdapterConfig.GlobalSettings.Default.Config
.ForType<NotificationDto, Notification>()
.Ignore(dst => dst.NotificationCategory,
dst => dst.User);
} }
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration) public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)

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;
@ -15,14 +18,37 @@ namespace AsbCloudInfrastructure.Repository;
public class NotificationRepository : CrudCacheRepositoryBase<NotificationDto, Notification>, INotificationRepository public class NotificationRepository : CrudCacheRepositoryBase<NotificationDto, Notification>, INotificationRepository
{ {
private static IQueryable<Notification> MakeQueryNotification(DbSet<Notification> dbSet) private static IQueryable<Notification> MakeQueryNotification(DbSet<Notification> dbSet)
=> dbSet.AsNoTracking() => dbSet.Include(n => n.NotificationCategory)
.Include(n => n.NotificationCategory); .AsNoTracking();
public NotificationRepository(IAsbCloudDbContext dbContext, IMemoryCache memoryCache) public NotificationRepository(IAsbCloudDbContext dbContext, IMemoryCache memoryCache)
: base(dbContext, memoryCache, MakeQueryNotification) : base(dbContext, memoryCache, MakeQueryNotification)
{ {
} }
public async Task<int> UpdateRangeAsync(IEnumerable<NotificationDto> notifications, CancellationToken cancellationToken)
{
if (!notifications.Any())
return 0;
var ids = notifications.Select(d => d.Id).ToArray();
var existingEntities = await dbSet
.Where(d => ids.Contains(d.Id))
.AsNoTracking()
.Select(d => d.Id)
.ToArrayAsync(cancellationToken);
if (ids.Length > existingEntities.Length)
return ICrudRepository<SetpointsRequestDto>.ErrorIdNotFound;
var entities = notifications.Select(Convert);
dbContext.Notifications.UpdateRange(entities);
return await dbContext.SaveChangesAsync(cancellationToken);
}
public async Task<PaginationContainer<NotificationDto>> GetNotificationsAsync(int idUser, public async Task<PaginationContainer<NotificationDto>> GetNotificationsAsync(int idUser,
NotificationRequest request, NotificationRequest request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
@ -41,28 +67,28 @@ public class NotificationRepository : CrudCacheRepositoryBase<NotificationDto, N
if (result.Count == 0) if (result.Count == 0)
return result; return result;
result.Items = await query result.Items = await query
.SortBy(request.SortFields) .SortBy(request.SortFields)
.Skip(skip) .Skip(skip)
.Take(take) .Take(take)
.Include(x => x.NotificationCategory) .AsNoTracking()
.Select(x => x.Adapt<NotificationDto>()) .Select(x => x.Adapt<NotificationDto>())
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
return result; return result;
} }
private IQueryable<Notification> BuildQuery(int? idUser, private IQueryable<Notification> BuildQuery(int idUser,
NotificationRequest request) NotificationRequest request)
{ {
var query = dbContext.Notifications.AsQueryable(); var query = dbContext.Notifications
.Include(x => x.NotificationCategory)
.Where(n => n.IdUser == idUser)
.AsQueryable();
if (!request.SentDate.HasValue) if (!request.IsSent)
query = query.Where(n => n.SentDate == null); query = query.Where(n => n.SentDate == null);
if (idUser.HasValue)
query = query.Where(n => n.IdUser == idUser);
if (request.IdTransportType.HasValue) if (request.IdTransportType.HasValue)
query = query.Where(n => n.IdTransportType == request.IdTransportType); query = query.Where(n => n.IdTransportType == request.IdTransportType);

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -8,7 +9,6 @@ using AsbCloudApp.Repositories;
using AsbCloudApp.Requests; using AsbCloudApp.Requests;
using AsbCloudApp.Services; using AsbCloudApp.Services;
using AsbCloudApp.Services.Notifications; using AsbCloudApp.Services.Notifications;
using Microsoft.Extensions.DependencyInjection;
namespace AsbCloudInfrastructure.Services.Notifications; namespace AsbCloudInfrastructure.Services.Notifications;
@ -16,15 +16,15 @@ public class NotificationService : INotificationService
{ {
private readonly ICrudRepository<NotificationCategoryDto> notificationCategoryRepository; private readonly ICrudRepository<NotificationCategoryDto> notificationCategoryRepository;
private readonly INotificationRepository notificationRepository; private readonly INotificationRepository notificationRepository;
private readonly IServiceProvider serviceProvider; private readonly IEnumerable<INotificationTransportService> notificationTransportServices;
public NotificationService(ICrudRepository<NotificationCategoryDto> notificationCategoryRepository, public NotificationService(ICrudRepository<NotificationCategoryDto> notificationCategoryRepository,
INotificationRepository notificationRepository, INotificationRepository notificationRepository,
IServiceProvider serviceProvider) IEnumerable<INotificationTransportService> notificationTransportServices)
{ {
this.notificationCategoryRepository = notificationCategoryRepository; this.notificationCategoryRepository = notificationCategoryRepository;
this.notificationRepository = notificationRepository; this.notificationRepository = notificationRepository;
this.serviceProvider = serviceProvider; this.notificationTransportServices = notificationTransportServices;
} }
public async Task NotifyAsync(int idUser, public async Task NotifyAsync(int idUser,
@ -57,6 +57,9 @@ public class NotificationService : INotificationService
var notificationTransportService = GetNotificationTransportService(idTransportType); var notificationTransportService = GetNotificationTransportService(idTransportType);
await notificationTransportService.SendAsync(notification, cancellationToken); await notificationTransportService.SendAsync(notification, cancellationToken);
await notificationRepository.UpdateAsync(notification,
cancellationToken);
} }
public async Task UpdateNotificationAsync(int idNotification, public async Task UpdateNotificationAsync(int idNotification,
@ -64,41 +67,47 @@ public class NotificationService : INotificationService
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var notification = await notificationRepository.GetOrDefaultAsync(idNotification, var notification = await notificationRepository.GetOrDefaultAsync(idNotification,
cancellationToken) ?? throw new ArgumentInvalidException("Уведомление не найдено", cancellationToken)
nameof(idNotification)); ?? throw new ArgumentInvalidException("Уведомление не найдено", nameof(idNotification));
if (isRead) if (isRead)
{ {
if (notification.SentDate == null) if (notification.SentDate == null)
throw new ArgumentInvalidException("Уведомление не может быть прочитано", nameof(isRead)); throw new ArgumentInvalidException("Уведомление не может быть прочитано", nameof(isRead));
notification.SentDate = DateTime.UtcNow; notification.SentDate = DateTime.UtcNow;
} }
await notificationRepository.UpdateAsync(notification, await notificationRepository.UpdateAsync(notification,
cancellationToken); cancellationToken);
} }
public async Task ResendNotificationAsync(int idUser, public async Task ResendNotificationAsync(int idUser,
NotificationRequest request, NotificationRequest request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
if (!request.IdTransportType.HasValue)
throw new ArgumentInvalidException("Id типа доставки уведомления должен иметь значение",
nameof(request.IdTransportType));
var result = await notificationRepository.GetNotificationsAsync(idUser, var result = await notificationRepository.GetNotificationsAsync(idUser,
request, request,
cancellationToken); cancellationToken);
var notificationTransportService = GetNotificationTransportService(request.IdTransportType!.Value); var notificationTransportService = GetNotificationTransportService(request.IdTransportType.Value);
await notificationTransportService.SendRangeAsync(result.Items, cancellationToken); await notificationTransportService.SendRangeAsync(result.Items,
cancellationToken);
await notificationRepository.UpdateRangeAsync(result.Items,
cancellationToken);
} }
private INotificationTransportService GetNotificationTransportService(int idTransportType) private INotificationTransportService GetNotificationTransportService(int idTransportType)
{ {
var notificationTransportService = serviceProvider.GetServices<INotificationTransportService>() var notificationTransportService = notificationTransportServices
.FirstOrDefault(s => s.IdTransportType == idTransportType); .FirstOrDefault(s => s.IdTransportType == idTransportType)
?? throw new ArgumentInvalidException("Доставщик уведомлений не найден", nameof(idTransportType));
if(notificationTransportService is null)
throw new ArgumentInvalidException("Доставщик уведомлений не найден", nameof(idTransportType));
return notificationTransportService; return notificationTransportService;
} }

View File

@ -138,7 +138,7 @@ namespace AsbCloudWebApi
public static void AddNotificationTransportServices(this IServiceCollection services) public static void AddNotificationTransportServices(this IServiceCollection services)
{ {
services.AddSingleton<INotificationTransportService, SignalRNotificationTransportService>(); services.AddTransient<INotificationTransportService, SignalRNotificationTransportService>();
} }
} }
} }

View File

@ -1,25 +0,0 @@
using System.Collections.Concurrent;
namespace AsbCloudWebApi.SignalR.ConnectionManager;
public class ConnectionManager : IConnectionManager
{
private readonly ConcurrentDictionary<int, string> _connections = new();
public void AddConnection(int userId,
string connectionId)
{
_connections.TryAdd(userId, connectionId);
}
public void RemoveConnection(int userId)
{
_connections.TryRemove(userId, out _);
}
public string? GetConnectionIdByUserId(int userId)
{
_connections.TryGetValue(userId, out var connectionId);
return connectionId;
}
}

View File

@ -1,11 +0,0 @@
namespace AsbCloudWebApi.SignalR.ConnectionManager;
public interface IConnectionManager
{
void AddConnection(int userId,
string connectionId);
void RemoveConnection(int userId);
string? GetConnectionIdByUserId(int userId);
}

View File

@ -3,37 +3,43 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AsbCloudApp.Requests; using AsbCloudApp.Requests;
using AsbCloudApp.Services.Notifications; using AsbCloudApp.Services.Notifications;
using AsbCloudWebApi.SignalR.ConnectionManager; using AsbCloudWebApi.SignalR.Services;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace AsbCloudWebApi.SignalR; namespace AsbCloudWebApi.SignalR;
[Authorize] [Authorize]
public class NotificationHub : BaseHub public class NotificationHub : BaseHub
{ {
private readonly IConnectionManager connectionManager; private readonly ConnectionManagerService connectionManagerService;
private readonly INotificationService notificationService; private readonly INotificationService notificationService;
public NotificationHub(IConnectionManager connectionManager, public NotificationHub(ConnectionManagerService connectionManagerService,
INotificationService notificationService) INotificationService notificationService)
{ {
this.connectionManager = connectionManager; this.connectionManagerService = connectionManagerService;
this.notificationService = notificationService; this.notificationService = notificationService;
} }
public async Task OnConnected(int idUser, NotificationRequest request) public async Task OnConnected(NotificationRequest request)
{ {
try try
{ {
await base.OnConnectedAsync();
var idUser = Context.User?.GetUserId();
if (!idUser.HasValue)
return;
string connectionId = Context.ConnectionId; string connectionId = Context.ConnectionId;
connectionManager.AddConnection(idUser, connectionId); connectionManagerService.AddOrUpdateConnection(idUser.Value, connectionId);
await notificationService.ResendNotificationAsync(idUser, await notificationService.ResendNotificationAsync(idUser.Value,
request, request,
CancellationToken.None); CancellationToken.None);
await base.OnConnectedAsync();
} }
catch (Exception e) catch (Exception e)
{ {
@ -41,10 +47,15 @@ public class NotificationHub : BaseHub
} }
} }
public Task OnDisconnected(int idUser) public async Task OnDisconnected()
{ {
connectionManager.RemoveConnection(idUser); await base.OnDisconnectedAsync(null);
var idUser = Context.User?.GetUserId();
return base.OnDisconnectedAsync(null); if (!idUser.HasValue)
return;
connectionManagerService.RemoveConnection(idUser.Value);
} }
} }

View File

@ -0,0 +1,25 @@
using System.Collections.Concurrent;
namespace AsbCloudWebApi.SignalR.Services;
public class ConnectionManagerService
{
private readonly ConcurrentDictionary<int, string> connections = new();
public void AddOrUpdateConnection(int userId, string connectionId)
{
connections.AddOrUpdate(userId, connectionId,
(key, existingConnectionId) => connectionId);
}
public void RemoveConnection(int userId)
{
connections.TryRemove(userId, out _);
}
public string? GetConnectionIdByUserId(int userId)
{
connections.TryGetValue(userId, out var connectionId);
return connectionId;
}
}

View File

@ -1,29 +1,24 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
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 AsbCloudWebApi.SignalR.ConnectionManager;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
namespace AsbCloudWebApi.SignalR.Services; namespace AsbCloudWebApi.SignalR.Services;
public class SignalRNotificationTransportService : INotificationTransportService public class SignalRNotificationTransportService : INotificationTransportService
{ {
private readonly IConnectionManager connectionManager; private readonly ConnectionManagerService connectionManagerService;
private readonly IHubContext<NotificationHub> notificationHubContext; private readonly IHubContext<NotificationHub> notificationHubContext;
private readonly IServiceProvider serviceProvider;
public SignalRNotificationTransportService(IConnectionManager connectionManager, public SignalRNotificationTransportService(ConnectionManagerService connectionManagerService,
IHubContext<NotificationHub> notificationHubContext, IHubContext<NotificationHub> notificationHubContext)
IServiceProvider serviceProvider)
{ {
this.connectionManager = connectionManager; this.connectionManagerService = connectionManagerService;
this.notificationHubContext = notificationHubContext; this.notificationHubContext = notificationHubContext;
this.serviceProvider = serviceProvider;
} }
public int IdTransportType => 0; public int IdTransportType => 0;
@ -33,7 +28,7 @@ public class SignalRNotificationTransportService : INotificationTransportService
{ {
const string method = "notifications"; const string method = "notifications";
var connectionId = connectionManager.GetConnectionIdByUserId(notification.IdUser); var connectionId = connectionManagerService.GetConnectionIdByUserId(notification.IdUser);
if (!string.IsNullOrWhiteSpace(connectionId)) if (!string.IsNullOrWhiteSpace(connectionId))
{ {
@ -43,25 +38,15 @@ public class SignalRNotificationTransportService : INotificationTransportService
.SendAsync(method, .SendAsync(method,
notification, notification,
cancellationToken); cancellationToken);
var scope = serviceProvider.CreateScope();
var notificationRepository = scope.ServiceProvider.GetService<INotificationRepository>();
if (notificationRepository != null)
{
await notificationRepository.UpdateAsync(notification, cancellationToken);
}
} }
} }
public async Task SendRangeAsync(IEnumerable<NotificationDto> notifications, public Task SendRangeAsync(IEnumerable<NotificationDto> notifications,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
foreach (var notification in notifications) var tasks = Array.ConvertAll(notifications.ToArray(), notification =>
{ SendAsync(notification, cancellationToken));
await SendAsync(notification,
cancellationToken); return Task.WhenAll(tasks);
}
} }
} }

View File

@ -2,7 +2,7 @@ using AsbCloudInfrastructure;
using AsbCloudWebApi.Converters; using AsbCloudWebApi.Converters;
using AsbCloudWebApi.Middlewares; using AsbCloudWebApi.Middlewares;
using AsbCloudWebApi.SignalR; using AsbCloudWebApi.SignalR;
using AsbCloudWebApi.SignalR.ConnectionManager; using AsbCloudWebApi.SignalR.Services;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
@ -48,7 +48,7 @@ namespace AsbCloudWebApi
services.AddJWTAuthentication(); services.AddJWTAuthentication();
services.AddSignalR() services.AddSignalR()
.Services.AddSingleton<IConnectionManager, ConnectionManager>(); .Services.AddSingleton<ConnectionManagerService>();
services.AddCors(options => services.AddCors(options =>
{ {