Рефакторинг транспорта уведомлений

1. Создал отдельный бекграунд сервис для уведомлений.
2. Сделал отправку уведомлений с помощью SignalR с использованием бекграунд сервиса.
3. Убрал из NotificationDto свойство User. Данное свойство избыточно в данном Dto.
4. В транспорте отправки уведомлений по e-mail добавил получение пользователя.
5. Поправил NotificationRepository, избавился от использования кэша.
This commit is contained in:
parent 4226d6366c
commit ff65869341
10 changed files with 85 additions and 64 deletions

View File

@ -1,5 +1,4 @@
using System; using System;
using AsbCloudApp.Data.User;
namespace AsbCloudApp.Data; namespace AsbCloudApp.Data;
@ -79,9 +78,4 @@ public class NotificationDto : IId
/// DTO категории уведомления /// DTO категории уведомления
/// </summary> /// </summary>
public NotificationCategoryDto NotificationCategory { get; set; } = null!; public NotificationCategoryDto NotificationCategory { get; set; } = null!;
/// <summary>
/// DTO получателя уведомления
/// </summary>
public UserDto User { get; set; } = null!;
} }

View File

@ -16,7 +16,6 @@ namespace AsbCloudApp.Services.Notifications;
public class NotificationService public class NotificationService
{ {
private readonly ICrudRepository<NotificationCategoryDto> notificationCategoryRepository; private readonly ICrudRepository<NotificationCategoryDto> notificationCategoryRepository;
private readonly IUserRepository userRepository;
private readonly INotificationRepository notificationRepository; private readonly INotificationRepository notificationRepository;
private readonly IEnumerable<INotificationTransportService> notificationTransportServices; private readonly IEnumerable<INotificationTransportService> notificationTransportServices;
@ -28,11 +27,9 @@ public class NotificationService
/// <param name="notificationRepository"></param> /// <param name="notificationRepository"></param>
/// <param name="notificationTransportServices"></param> /// <param name="notificationTransportServices"></param>
public NotificationService(ICrudRepository<NotificationCategoryDto> notificationCategoryRepository, public NotificationService(ICrudRepository<NotificationCategoryDto> notificationCategoryRepository,
IUserRepository userRepository,
INotificationRepository notificationRepository, INotificationRepository notificationRepository,
IEnumerable<INotificationTransportService> notificationTransportServices) IEnumerable<INotificationTransportService> notificationTransportServices)
{ {
this.userRepository = userRepository;
this.notificationCategoryRepository = notificationCategoryRepository; this.notificationCategoryRepository = notificationCategoryRepository;
this.notificationRepository = notificationRepository; this.notificationRepository = notificationRepository;
this.notificationTransportServices = notificationTransportServices; this.notificationTransportServices = notificationTransportServices;
@ -50,14 +47,11 @@ public class NotificationService
.GetOrDefaultAsync(request.IdNotificationCategory, cancellationToken) .GetOrDefaultAsync(request.IdNotificationCategory, cancellationToken)
?? throw new ArgumentInvalidException("Категория уведомления не найдена", nameof(request.IdNotificationCategory)); ?? throw new ArgumentInvalidException("Категория уведомления не найдена", nameof(request.IdNotificationCategory));
var user = await userRepository.GetOrDefaultAsync(request.IdUser, cancellationToken)
?? throw new ArgumentInvalidException("Пользователь не найден" , nameof(request.IdUser));
var notification = new NotificationDto var notification = new NotificationDto
{ {
IdUser = request.IdUser, IdUser = request.IdUser,
RegistrationDate = DateTime.UtcNow, RegistrationDate = DateTime.UtcNow,
IdNotificationCategory = request.IdNotificationCategory, IdNotificationCategory = notificationCategory.Id,
Title = request.Title, Title = request.Title,
Message = request.Message, Message = request.Message,
IdTransportType = request.IdTransportType, IdTransportType = request.IdTransportType,
@ -65,8 +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;
notification.User = user;
var notificationTransportService = GetNotificationTransportService(request.IdTransportType); var notificationTransportService = GetNotificationTransportService(request.IdTransportType);
await notificationTransportService.SendAsync(notification, cancellationToken); await notificationTransportService.SendAsync(notification, cancellationToken);

View File

@ -7,7 +7,6 @@ using System.Threading.Tasks;
namespace AsbCloudInfrastructure.Background namespace AsbCloudInfrastructure.Background
{ {
# nullable enable
/// <summary> /// <summary>
/// Сервис для фонового выполнения работы /// Сервис для фонового выполнения работы
/// </summary> /// </summary>
@ -94,5 +93,4 @@ namespace AsbCloudInfrastructure.Background
} }
} }
} }
} }

View File

@ -0,0 +1,10 @@
using System;
namespace AsbCloudInfrastructure.Background;
public class NotificationBackgroundWorker : BackgroundWorker
{
public NotificationBackgroundWorker(IServiceProvider serviceProvider) : base(serviceProvider)
{
}
}

View File

@ -111,6 +111,7 @@ namespace AsbCloudInfrastructure
services.AddSingleton(provider => TelemetryDataCache<TelemetryDataSpinDto>.GetInstance<TelemetryDataSpin>(provider)); services.AddSingleton(provider => TelemetryDataCache<TelemetryDataSpinDto>.GetInstance<TelemetryDataSpin>(provider));
services.AddSingleton<IRequerstTrackerService, RequestTrackerService>(); services.AddSingleton<IRequerstTrackerService, RequestTrackerService>();
services.AddSingleton<BackgroundWorker>(); services.AddSingleton<BackgroundWorker>();
services.AddSingleton<NotificationBackgroundWorker>();
services.AddSingleton<IReduceSamplingService>(provider => ReduceSamplingService.GetInstance(configuration)); services.AddSingleton<IReduceSamplingService>(provider => ReduceSamplingService.GetInstance(configuration));
services.AddTransient<IAuthService, AuthService>(); services.AddTransient<IAuthService, AuthService>();

View File

@ -8,21 +8,21 @@ using AsbCloudDb;
using AsbCloudDb.Model; using AsbCloudDb.Model;
using Mapster; using Mapster;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
namespace AsbCloudInfrastructure.Repository; namespace AsbCloudInfrastructure.Repository;
public class NotificationRepository : CrudCacheRepositoryBase<NotificationDto, Notification>, INotificationRepository public class NotificationRepository : CrudRepositoryBase<NotificationDto, Notification>, INotificationRepository
{ {
private static IQueryable<Notification> MakeQueryNotification(DbSet<Notification> dbSet) private static IQueryable<Notification> MakeQueryNotification(DbSet<Notification> dbSet)
=> dbSet.Include(n => n.NotificationCategory) => dbSet.Include(n => n.NotificationCategory)
.Include(n => n.User) .Include(n => n.User)
.AsNoTracking(); .AsNoTracking();
public NotificationRepository(IAsbCloudDbContext dbContext, IMemoryCache memoryCache) public NotificationRepository(IAsbCloudDbContext context)
: base(dbContext, memoryCache, MakeQueryNotification) : base(context, MakeQueryNotification)
{ {
} }
public async Task<PaginationContainer<NotificationDto>> GetNotificationsAsync(int idUser, public async Task<PaginationContainer<NotificationDto>> GetNotificationsAsync(int idUser,
NotificationRequest request, NotificationRequest request,
@ -33,7 +33,7 @@ public class NotificationRepository : CrudCacheRepositoryBase<NotificationDto, N
var query = BuildQuery(idUser, request); var query = BuildQuery(idUser, request);
var result = new PaginationContainer<NotificationDto>() var result = new PaginationContainer<NotificationDto>
{ {
Skip = skip, Skip = skip,
Take = take, Take = take,
@ -61,13 +61,12 @@ public class NotificationRepository : CrudCacheRepositoryBase<NotificationDto, N
.Include(x => x.NotificationCategory) .Include(x => x.NotificationCategory)
.Where(n => n.IdUser == idUser); .Where(n => n.IdUser == idUser);
if (request.IsSent.HasValue) if (request.IsSent.HasValue)
{ {
if(request.IsSent.Value) query = request.IsSent.Value ?
query = query.Where(n => n.SentDate != null); query.Where(n => n.SentDate != null)
else : query.Where(n => n.SentDate == null);
query = query.Where(n => n.SentDate == null); }
}
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

@ -50,7 +50,7 @@ namespace AsbCloudInfrastructure.Services.Email
return Task.CompletedTask; return Task.CompletedTask;
} }
var workId = MakeWorkId(notification.User.Email, notification.Title, notification.Message); var workId = MakeWorkId(notification.IdUser, notification.Title, notification.Message);
if (!backgroundWorker.Contains(workId)) if (!backgroundWorker.Contains(workId))
{ {
var workAction = MakeEmailSendWorkAction(notification); var workAction = MakeEmailSendWorkAction(notification);
@ -73,17 +73,23 @@ namespace AsbCloudInfrastructure.Services.Email
private Func<string, IServiceProvider, CancellationToken, Task> MakeEmailSendWorkAction(NotificationDto notification) private Func<string, IServiceProvider, CancellationToken, Task> MakeEmailSendWorkAction(NotificationDto notification)
{ {
if(!MailAddress.TryCreate(notification.User.Email, out var mailAddress))
Trace.TraceWarning($"Mail {notification.User.Email} is not correct.");
if (mailAddress is null)
throw new ArgumentInvalidException($"Mail {notification.User.Email} is not null.", nameof(notification.User.Email));
if (string.IsNullOrWhiteSpace(notification.Title)) if (string.IsNullOrWhiteSpace(notification.Title))
throw new ArgumentInvalidException($"{nameof(notification.Title)} should be set", nameof(notification.Title)); throw new ArgumentInvalidException($"{nameof(notification.Title)} should be set", nameof(notification.Title));
return async (_, serviceProvider, token) => return async (_, serviceProvider, token) =>
{ {
var notificationRepository = serviceProvider.GetRequiredService<INotificationRepository>();
var userRepository = serviceProvider.GetRequiredService<IUserRepository>();
var user = await userRepository.GetOrDefaultAsync(notification.IdUser, token)
?? throw new ArgumentInvalidException("Пользователь не найден" , nameof(notification.IdUser));
if(!MailAddress.TryCreate(user.Email, out var mailAddress))
Trace.TraceWarning($"Mail {user.Email} is not correct.");
if (mailAddress is null)
throw new ArgumentInvalidException($"Mail {user.Email} is not null.", nameof(user.Email));
var from = new MailAddress(sender); var from = new MailAddress(sender);
var message = new MailMessage var message = new MailMessage
{ {
@ -105,17 +111,15 @@ namespace AsbCloudInfrastructure.Services.Email
notification.SentDate = DateTime.UtcNow; notification.SentDate = DateTime.UtcNow;
var notificationRepository = serviceProvider.GetRequiredService<INotificationRepository>();
await notificationRepository.UpdateAsync(notification, token); await notificationRepository.UpdateAsync(notification, token);
Trace.TraceInformation($"Send email to {notification.User.Email} subj:{notification.Title} html body count {notification.Message.Length}"); Trace.TraceInformation($"Send email to {user.Email} subj:{notification.Title} html body count {notification.Message.Length}");
}; };
} }
private static string MakeWorkId(string address, string subject, string content) private static string MakeWorkId(int idUser, string subject, string content)
{ {
var hash = address.GetHashCode(); var hash = idUser.GetHashCode();
hash ^= subject.GetHashCode(); hash ^= subject.GetHashCode();
hash ^= content.GetHashCode(); hash ^= content.GetHashCode();
return hash.ToString("x"); return hash.ToString("x");

View File

@ -40,8 +40,14 @@ namespace AsbCloudInfrastructure
backgroundWorker.Push(LimitingParameterCalcWorkFactory.MakeWork()); backgroundWorker.Push(LimitingParameterCalcWorkFactory.MakeWork());
backgroundWorker.Push(MakeMemoryMonitoringWork()); backgroundWorker.Push(MakeMemoryMonitoringWork());
var notificationBackgroundWorker = provider.GetRequiredService<NotificationBackgroundWorker>();
Task.Delay(1_000) Task.Delay(1_000)
.ContinueWith(async (_) => await backgroundWorker.StartAsync(CancellationToken.None)); .ContinueWith(async (_) =>
{
await backgroundWorker.StartAsync(CancellationToken.None);
await notificationBackgroundWorker.StartAsync(CancellationToken.None);
});
} }
static WorkPeriodic MakeMemoryMonitoringWork() static WorkPeriodic MakeMemoryMonitoringWork()

View File

@ -145,7 +145,6 @@ namespace AsbCloudWebApi.Tests.ServicesTests
.Returns(1); .Returns(1);
notificationService = new NotificationService(notificationCategoryRepositoryMock.Object, notificationService = new NotificationService(notificationCategoryRepositoryMock.Object,
userRepositoryMock.Object,
new Mock<INotificationRepository>().Object, new Mock<INotificationRepository>().Object,
new [] { notificationTransportServiceMock.Object }); new [] { notificationTransportServiceMock.Object });

View File

@ -6,50 +6,47 @@ using System.Threading.Tasks;
using AsbCloudApp.Data; using AsbCloudApp.Data;
using AsbCloudApp.Repositories; using AsbCloudApp.Repositories;
using AsbCloudApp.Services.Notifications; using AsbCloudApp.Services.Notifications;
using AsbCloudInfrastructure.Background;
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 NotificationBackgroundWorker backgroundWorker;
private readonly ConnectionManagerService connectionManagerService; private readonly ConnectionManagerService connectionManagerService;
private readonly IHubContext<NotificationHub> notificationHubContext; private readonly IHubContext<NotificationHub> notificationHubContext;
private readonly INotificationRepository notificationRepository;
private readonly SemaphoreSlim semaphoreSlim = new (1, 1);
public SignalRNotificationTransportService(ConnectionManagerService connectionManagerService, public SignalRNotificationTransportService(NotificationBackgroundWorker backgroundWorker,
IHubContext<NotificationHub> notificationHubContext, ConnectionManagerService connectionManagerService,
INotificationRepository notificationRepository) IHubContext<NotificationHub> notificationHubContext)
{ {
this.backgroundWorker = backgroundWorker;
this.connectionManagerService = connectionManagerService; this.connectionManagerService = connectionManagerService;
this.notificationHubContext = notificationHubContext; this.notificationHubContext = notificationHubContext;
this.notificationRepository = notificationRepository;
} }
public int IdTransportType => 0; public int IdTransportType => 0;
public async Task SendAsync(NotificationDto notification, public Task SendAsync(NotificationDto notification,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
const string method = "receiveNotifications"; var workId = notification.Id.ToString();
var connectionId = connectionManagerService.GetConnectionIdByUserId(notification.IdUser); if (!backgroundWorker.Contains(workId))
if (!string.IsNullOrWhiteSpace(connectionId))
{ {
notification.SentDate = DateTime.UtcNow; var connectionId = connectionManagerService.GetConnectionIdByUserId(notification.IdUser);
await notificationHubContext.Clients.Client(connectionId)
.SendAsync(method,
notification,
cancellationToken);
await semaphoreSlim.WaitAsync(cancellationToken); if (!string.IsNullOrWhiteSpace(connectionId))
{
await notificationRepository.UpdateAsync(notification, cancellationToken); var workAction = MakeSignalRSendWorkAction(notification, connectionId);
var work = new WorkBase(workId, workAction);
semaphoreSlim.Release(); backgroundWorker.Push(work);
}
} }
return Task.CompletedTask;
} }
public Task SendRangeAsync(IEnumerable<NotificationDto> notifications, public Task SendRangeAsync(IEnumerable<NotificationDto> notifications,
@ -60,4 +57,24 @@ public class SignalRNotificationTransportService : INotificationTransportService
return Task.WhenAll(tasks); return Task.WhenAll(tasks);
} }
private Func<string, IServiceProvider, CancellationToken, Task> MakeSignalRSendWorkAction(NotificationDto notification,
string connectionId)
{
const string method = "receiveNotifications";
return async (_, serviceProvider, cancellationToken) =>
{
notification.SentDate = DateTime.UtcNow;
await notificationHubContext.Clients.Client(connectionId)
.SendAsync(method,
notification,
cancellationToken);
var notificationRepository = serviceProvider.GetRequiredService<INotificationRepository>();
await notificationRepository.UpdateAsync(notification, cancellationToken);
};
}
} }