diff --git a/src/Api/Endpoints/Auth/RegisterEndpoint.cs b/src/Api/Endpoints/Auth/RegisterEndpoint.cs index 01e1267..02c6b97 100644 --- a/src/Api/Endpoints/Auth/RegisterEndpoint.cs +++ b/src/Api/Endpoints/Auth/RegisterEndpoint.cs @@ -1,5 +1,6 @@ using Api.Models.Requests.Auth; using Api.Models.Responses; +using Core.Abstractions; using FastEndpoints; using Infrastructure.Auth0.Abstractions; using Infrastructure.Auth0.Models; @@ -8,10 +9,12 @@ namespace Api.Endpoints.Auth; public class RegisterEndpoint( IAuthService authService, + IUserPreferencesService userPreferencesService, ILogger logger) : Endpoint> { private readonly IAuthService _authService = authService; + private readonly IUserPreferencesService _userPreferencesService = userPreferencesService; private readonly ILogger _logger = logger; public override void Configure() @@ -35,6 +38,8 @@ public override async Task HandleAsync( } else { + await _userPreferencesService.CreateUserPreferences(registrationResponse.UserId, ct); + var tokenInfo = new UserTokenResponse( registrationResponse.TokenInfo.Token, registrationResponse.TokenInfo.ExpiresIn); diff --git a/src/Api/Endpoints/Preferences/UpdatePreferencesEndpoint.cs b/src/Api/Endpoints/Preferences/UpdatePreferencesEndpoint.cs new file mode 100644 index 0000000..d25cf6a --- /dev/null +++ b/src/Api/Endpoints/Preferences/UpdatePreferencesEndpoint.cs @@ -0,0 +1,28 @@ +using Api.Mappers.UserPreferences; +using Api.Models.Requests.UserPreferences; +using Api.Models.Responses; +using Core.Abstractions; +using FastEndpoints; + +namespace Api.Endpoints.Preferences; + +public class UpdatePreferencesEndpoint(IUserPreferencesService userPreferencesService) : + Endpoint +{ + private readonly IUserPreferencesService _preferencesService = userPreferencesService; + + public override void Configure() + { + Verbs(Http.PUT); + Routes("api/preferences"); + } + + public override async Task HandleAsync(UpdateUserPreferencesRequest req, CancellationToken ct) + { + var dto = Map.ToEntity(req).Value; + var result = await _preferencesService.UpdateUserPreferences(dto, ct); + + var apiResponse = Map.FromEntity(result); + await SendAsync(apiResponse, cancellation: ct); + } +} \ No newline at end of file diff --git a/src/Api/Extensions/ErrorOrExtensions.cs b/src/Api/Extensions/ErrorOrExtensions.cs index 59b7c45..c10c5a3 100644 --- a/src/Api/Extensions/ErrorOrExtensions.cs +++ b/src/Api/Extensions/ErrorOrExtensions.cs @@ -24,7 +24,7 @@ internal static ApiResponse ToApiResponse( return ApiResponse.Success(templateResponse); } - private static IEnumerable ToProblemDetailsErrors(this ErrorOr errorOr) + internal static IEnumerable ToProblemDetailsErrors(this ErrorOr errorOr) { if (!errorOr.IsError) throw new ArgumentException("DU contains value, not error"); diff --git a/src/Api/Mappers/UserPreferences/UpdateUserPreferencesMapper.cs b/src/Api/Mappers/UserPreferences/UpdateUserPreferencesMapper.cs new file mode 100644 index 0000000..65a33fc --- /dev/null +++ b/src/Api/Mappers/UserPreferences/UpdateUserPreferencesMapper.cs @@ -0,0 +1,43 @@ +using Api.Extensions; +using Api.Models.Requests.UserPreferences; +using Api.Models.Responses; +using Core.Models.UserPreferences; +using ErrorOr; +using FastEndpoints; + +namespace Api.Mappers.UserPreferences; + +public class UpdateUserPreferencesMapper + : Mapper> +{ + public override ErrorOr ToEntity(UpdateUserPreferencesRequest r) + { + return new UserPreferencesDto + { + UserId = r.UserId, + Channels = r.Channels.ToDictionary( + kvp => kvp.Key, + kvp => new ChannelDescriptorBaseDto + { + Enabled = kvp.Value.Enabled, + Description = kvp.Value.Description, + Metadata = kvp.Value.Metadata + }) + }; + } + + public override ApiResponse FromEntity(ErrorOr e) + { + if (e.IsError) + { + var problemDetails = new ProblemDetails + { + Errors = e.ToProblemDetailsErrors() + }; + + return ApiResponse.Fail(problemDetails); + } + + return ApiResponse.Success(); + } +} \ No newline at end of file diff --git a/src/Api/Models/Requests/UserPreferences/ChannelDescriptorBaseRequest.cs b/src/Api/Models/Requests/UserPreferences/ChannelDescriptorBaseRequest.cs new file mode 100644 index 0000000..6862a6b --- /dev/null +++ b/src/Api/Models/Requests/UserPreferences/ChannelDescriptorBaseRequest.cs @@ -0,0 +1,8 @@ +namespace Api.Models.Requests.UserPreferences; + +public record ChannelDescriptorBaseRequest +{ + public bool Enabled { get; init; } + public string? Description { get; init; } + public Dictionary? Metadata { get; init; } +} \ No newline at end of file diff --git a/src/Api/Models/Requests/UserPreferences/UpdateUserPreferencesRequest.cs b/src/Api/Models/Requests/UserPreferences/UpdateUserPreferencesRequest.cs new file mode 100644 index 0000000..df35830 --- /dev/null +++ b/src/Api/Models/Requests/UserPreferences/UpdateUserPreferencesRequest.cs @@ -0,0 +1,7 @@ +namespace Api.Models.Requests.UserPreferences; + +public class UpdateUserPreferencesRequest +{ + public required string UserId { get; set; } + public required Dictionary Channels { get; set; } +} \ No newline at end of file diff --git a/src/Core/Abstractions/IUserPreferencesService.cs b/src/Core/Abstractions/IUserPreferencesService.cs new file mode 100644 index 0000000..ab6b16e --- /dev/null +++ b/src/Core/Abstractions/IUserPreferencesService.cs @@ -0,0 +1,13 @@ +using Core.Models.UserPreferences; +using ErrorOr; + +namespace Core.Abstractions; + +public interface IUserPreferencesService +{ + Task> CreateUserPreferences(string userId, CancellationToken ct); + Task> UpdateUserPreferences( + UserPreferencesDto userPreferences, CancellationToken ct); + Task> GetChannelDeliveryInfo( + string recipientUserId, string channel, CancellationToken ct); +} diff --git a/src/Core/DependencyInjection.cs b/src/Core/DependencyInjection.cs index 0308bf4..8a22ddd 100644 --- a/src/Core/DependencyInjection.cs +++ b/src/Core/DependencyInjection.cs @@ -11,5 +11,6 @@ public static void AddGatewayCore(this IServiceCollection services, IConfigurati { services.AddScoped(); services.AddScoped(); + services.AddScoped(); } } \ No newline at end of file diff --git a/src/Core/Errors/TemplatesErrors.cs b/src/Core/Errors/TemplatesErrors.cs index 3096bed..e82b967 100644 --- a/src/Core/Errors/TemplatesErrors.cs +++ b/src/Core/Errors/TemplatesErrors.cs @@ -6,5 +6,5 @@ namespace Core.Errors; internal static class TemplatesErrors { internal static ErrorOr NameDuplication - => Error.Conflict("Template.NameDuplication", "Template with same name already exists"); + => Error.Conflict("Template.FailedToCreate", "Template with same name already exists"); } \ No newline at end of file diff --git a/src/Core/Errors/UserPreferencesErrors.cs b/src/Core/Errors/UserPreferencesErrors.cs new file mode 100644 index 0000000..d562144 --- /dev/null +++ b/src/Core/Errors/UserPreferencesErrors.cs @@ -0,0 +1,19 @@ +using Core.Models.UserPreferences; +using ErrorOr; + +namespace Core.Errors; + +public static class UserPreferencesErrors +{ + internal static ErrorOr FailedToCreate + => Error.Unexpected("UserPreferences.CreateFailed", "Failed to create user preferences"); + + internal static ErrorOr FailedToUpdate + => Error.Unexpected("UserPreferences.CreateUpdate", "Failed to update user preferences"); + + internal static ErrorOr NotFound + => Error.NotFound("UserPreferences.NotFound", "User preferences not found"); + + internal static ErrorOr ChannelNotFound + => Error.NotFound("UserPreferences.Channel.NotFound", "User preferences for channel not found"); +} diff --git a/src/Core/Extensions/StringExtensions.cs b/src/Core/Extensions/StringExtensions.cs new file mode 100644 index 0000000..21bf6bf --- /dev/null +++ b/src/Core/Extensions/StringExtensions.cs @@ -0,0 +1,27 @@ +using System.Globalization; +using System.Text; + +namespace Core.Extensions; + +public static class StringExtensions +{ + public static string ToPascalCase(this string text) + { + if (text.Length < 1) + return text; + + var words = text.Split(new[] { ' ', '-', '_', '.' }, StringSplitOptions.RemoveEmptyEntries); + + var sb = new StringBuilder(); + + foreach (var word in words) + { + if (word.Length > 0) + { + sb.Append(CultureInfo.InvariantCulture.TextInfo.ToTitleCase(word.ToLowerInvariant())); + } + } + + return sb.ToString(); + } +} \ No newline at end of file diff --git a/src/Core/Mappers/UserPreferencesMapper.cs b/src/Core/Mappers/UserPreferencesMapper.cs new file mode 100644 index 0000000..2b8d006 --- /dev/null +++ b/src/Core/Mappers/UserPreferencesMapper.cs @@ -0,0 +1,95 @@ +using Core.Extensions; +using Core.Models.UserPreferences; +using Infrastructure.Persistence.Mongo.Entities.Preferences; +using MongoDB.Bson; + +namespace Core.Mappers; + +public static class UserPreferencesChannelMapper +{ + public static ChannelDescriptorBaseDto ToDto(ChannelDescriptorBase e) + { + return new ChannelDescriptorBaseDto + { + Enabled = e.Enabled, + Description = e.Description, + Metadata = e.Metadata + }; + } + + public static ChannelDescriptorBase ToEntity(ChannelDescriptorBaseDto dto) + { + return new ChannelDescriptorBase + { + Enabled = dto.Enabled, + Description = dto.Description, + Metadata = dto.Metadata + }; + } +} + +public static class UserPreferencesMapper +{ + internal static UserPreferencesDto ToDto(UserPreferences e) + { + return new UserPreferencesDto + { + Id = e.Id.ToString(), + UserId = e.UserId, + Channels = e.Channels.ToDictionary(x => x.Key, d => UserPreferencesChannelMapper.ToDto(d.Value)) + }; + } + + public static UserPreferences UpdateEntity(UserPreferences e, UserPreferencesDto dto) + { + var channels = e.Channels; + foreach (var ch in dto.Channels) + { + var metadataKeys = ch.Value.Metadata?.Keys.ToArray()!; + foreach (var mk in metadataKeys) + { + var normalized = mk.ToPascalCase(); + + if (!mk.Equals(normalized)) + { + ch.Value.Metadata![normalized] = ch.Value.Metadata[mk]; + ch.Value.Metadata.Remove(mk); + } + } + + var normalizedKey = ch.Key.ToPascalCase(); + if (channels.TryGetValue(normalizedKey, out var channel)) + { + channel.Enabled = ch.Value.Enabled; + channel.Description = ch.Value.Description; + channel.Metadata = ch.Value.Metadata; + } + else + { + channels.Add(normalizedKey, new ChannelDescriptorBase + { + Enabled = ch.Value.Enabled, + Description = ch.Value.Description, + Metadata = ch.Value.Metadata + }); + } + } + + return e with { LastUpdated = DateTimeOffset.UtcNow, Channels = channels }; + } + + public static UserPreferences ToEntity(UserPreferencesDto dto) + { + var id = string.IsNullOrEmpty(dto.Id) + ? ObjectId.GenerateNewId() + : ObjectId.Parse(dto.Id); + + return new UserPreferences + { + Id = id, + UserId = dto.UserId, + Channels = dto.Channels.ToDictionary(x => x.Key, d => UserPreferencesChannelMapper.ToEntity(d.Value)), + LastUpdated = DateTimeOffset.UtcNow + }; + } +} \ No newline at end of file diff --git a/src/Core/Models/UserPreferences/ChannelDescriptorBaseDto.cs b/src/Core/Models/UserPreferences/ChannelDescriptorBaseDto.cs new file mode 100644 index 0000000..db91703 --- /dev/null +++ b/src/Core/Models/UserPreferences/ChannelDescriptorBaseDto.cs @@ -0,0 +1,8 @@ +namespace Core.Models.UserPreferences; + +public record ChannelDescriptorBaseDto +{ + public bool Enabled { get; init; } + public string? Description { get; init; } + public Dictionary? Metadata { get; init; } +} \ No newline at end of file diff --git a/src/Core/Models/UserPreferences/UserPreferencesDto.cs b/src/Core/Models/UserPreferences/UserPreferencesDto.cs new file mode 100644 index 0000000..5e20f4f --- /dev/null +++ b/src/Core/Models/UserPreferences/UserPreferencesDto.cs @@ -0,0 +1,8 @@ +namespace Core.Models.UserPreferences; + +public record UserPreferencesDto +{ + public string? Id { get; init; } + public required string UserId { get; init; } + public required Dictionary Channels { get; init; } +} \ No newline at end of file diff --git a/src/Core/Services/NotificationService.cs b/src/Core/Services/NotificationService.cs index 67e2e13..19480db 100644 --- a/src/Core/Services/NotificationService.cs +++ b/src/Core/Services/NotificationService.cs @@ -11,6 +11,7 @@ namespace Core.Services; internal class NotificationService( ITemplateFillerClient massTransitClient, + IUserPreferencesService userPreferencesService, INotificationsAnalyticsClient notificationsAnalyticsClient, INotificationsRepository notificationsRepository) : INotificationsService @@ -24,7 +25,7 @@ public async Task> CreateNotification( await notificationsRepository.InsertOne(notification); dto = dto with { Id = notification.Id.ToString() }; - var deliveryRequests = CreateDeliveryRequests(dto).ToList(); + var deliveryRequests = await CreateDeliveryRequests(dto, ct); await massTransitClient.SendMessages(deliveryRequests, string.Empty); foreach (var deliveryRequest in deliveryRequests) @@ -36,10 +37,35 @@ await notificationsAnalyticsClient return dto; } - private IEnumerable CreateDeliveryRequests(NotificationDto dto) + private async Task> CreateDeliveryRequests(NotificationDto dto, CancellationToken ct) { - return dto.Recipients! + return await dto.Recipients! + .ToAsyncEnumerable() .Select(recipient => Mappers.External.DeliveryRequestMapper.ToRequest(dto, recipient)) - .ToList(); + .SelectAwait(deliveryRequest => PopulateDeliveryInfo(deliveryRequest, ct)) + .ToListAsync(cancellationToken: ct); + } + + private async ValueTask PopulateDeliveryInfo(Notification notification, CancellationToken ct) + { + ct.ThrowIfCancellationRequested(); + + var channel = notification.Recipient.Channel; + var deliveryInfo = await userPreferencesService + .GetChannelDeliveryInfo(notification.Recipient.UserId, channel, ct); + if (deliveryInfo.IsError) + { + return notification; + } + + notification = notification with + { + Recipient = notification.Recipient with + { + DeliveryInfo = deliveryInfo.Value + } + }; + + return notification; } } \ No newline at end of file diff --git a/src/Core/Services/UserPreferencesService.cs b/src/Core/Services/UserPreferencesService.cs new file mode 100644 index 0000000..b9597e0 --- /dev/null +++ b/src/Core/Services/UserPreferencesService.cs @@ -0,0 +1,96 @@ +using Core.Abstractions; +using Core.Errors; +using Core.Extensions; +using Core.Mappers; +using Core.Models.UserPreferences; +using ErrorOr; +using Infrastructure.Persistence.MassTransit.Analytics; +using Infrastructure.Persistence.Mongo.Abstractions; +using Infrastructure.Persistence.Mongo.Entities.Preferences; +using Infrastructure.Persistence.Mongo.Specifications; +using Microsoft.Extensions.Logging; + +namespace Core.Services; + +public class UserPreferencesService( + IUserAnalyticsClient userAnalyticsClient, + IUserPreferencesRepository userPreferencesRepository, + ILogger logger) : IUserPreferencesService +{ + private readonly IUserAnalyticsClient _userAnalyticsClient = userAnalyticsClient; + private readonly IMongoRepository _userPreferencesRepository = userPreferencesRepository; + private readonly ILogger _logger = logger; + + public async Task> CreateUserPreferences(string userId, CancellationToken ct) + { + try + { + var defaultPreferences = new UserPreferencesDto + { + UserId = userId, + Channels = new Dictionary() + }; + + var entity = UserPreferencesMapper.ToEntity(defaultPreferences); + await _userPreferencesRepository.InsertOne(entity); + + // here user mail should be sent + await _userAnalyticsClient.SendUserAction( + userId, "UserPreferencesCreated", "recipient", "User's Preferences", ct); + + return UserPreferencesMapper.ToDto(entity); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to create user preferences"); + return UserPreferencesErrors.FailedToCreate; + } + } + + public async Task> UpdateUserPreferences( + UserPreferencesDto userPreferences, CancellationToken ct) + { + try + { + var byUserIdSpec = new AdHocSpecification(x => x.UserId == userPreferences.UserId); + var currentPreferences = await _userPreferencesRepository.FirstOrDefault(byUserIdSpec); + if (currentPreferences is null) + { + var result = await CreateUserPreferences(userPreferences.UserId, ct); + return result; + } + + var entity = UserPreferencesMapper.UpdateEntity(currentPreferences, userPreferences); + await _userPreferencesRepository.Replace(entity); + + // here user mail should be sent + await _userAnalyticsClient.SendUserAction( + userPreferences.UserId, "UserPreferencesUpdated", "recipient", "User's Preferences", ct); + + return UserPreferencesMapper.ToDto(entity); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to update user preferences"); + return UserPreferencesErrors.FailedToUpdate; + } + } + + public async Task> GetChannelDeliveryInfo( + string recipientUserId, string channel, CancellationToken ct) + { + var byUserId = new AdHocSpecification( + x => x.UserId == recipientUserId); + var currentUserPreferences = await _userPreferencesRepository.FirstOrDefault(byUserId); + if (currentUserPreferences is null) + { + return UserPreferencesErrors.ChannelNotFound; + } + + var channelKey = channel.ToPascalCase(); + var channelDescriptor = currentUserPreferences.Channels.GetValueOrDefault(channelKey); + return channelDescriptor is null + ? UserPreferencesErrors.ChannelNotFound + : UserPreferencesChannelMapper.ToDto(channelDescriptor); + } +} \ No newline at end of file diff --git a/src/Infrastructure/Auth0/AuthService.cs b/src/Infrastructure/Auth0/AuthService.cs index 9cc3067..c515343 100644 --- a/src/Infrastructure/Auth0/AuthService.cs +++ b/src/Infrastructure/Auth0/AuthService.cs @@ -58,35 +58,11 @@ public class AuthService( { try { - var username = $"{req.FullName[0]}-{Guid.NewGuid().ToString()[..6]}".ToLower(); - await _auth0Client.SignupUserAsync(new SignupUserRequest - { - Username = username, - Nickname = username, - Name = req.FullName , - Password = req.Password, - Email = req.Email, - - UserMetadata = new Dictionary - { - ["trumpee_uid"] = username - }, - - Connection =_auth0Options.Realm, - ClientId = _auth0Options.ClientId - }, ct); + var userId = await CreateAuth0Account(req, ct); + await _userAnalyticsClient.SendUserSignUp(userId, req.Email, req.FullName, "recipient", ct); - // By now, all users registered are recipients. - // Administrators are created manually via Auth0 management console. - await _userAnalyticsClient.SendUserSignUp(username, req.Email, req.FullName, "recipient", ct); - var tokenInfo = await GetUserToken(new LoginRequestDto(req.Email, req.Password), ct); - if (tokenInfo is null) - { - _logger.LogWarning("Failed to register user {Login}", req.Login); - return null; - } - - return new UserInfoDto(username, tokenInfo); + var tokenInfo = await SignInUserInternal(req, ct); + return new UserInfoDto(userId, tokenInfo!); } catch (Exception ex) { @@ -94,4 +70,39 @@ await _auth0Client.SignupUserAsync(new SignupUserRequest return null; } } + + private async Task CreateAuth0Account(RegisterRequestDto req, CancellationToken ct) + { + var username = $"{req.FullName[0]}-{Guid.NewGuid().ToString()[..6]}".ToLower(); + await _auth0Client.SignupUserAsync(new SignupUserRequest + { + Username = username, + Nickname = username, + Name = req.FullName , + Password = req.Password, + Email = req.Email, + + UserMetadata = new Dictionary + { + ["trumpee_uid"] = username + }, + + Connection =_auth0Options.Realm, + ClientId = _auth0Options.ClientId + }, ct); + + return username; + } + + private async Task SignInUserInternal(RegisterRequestDto req, CancellationToken ct) + { + var tokenInfo = await GetUserToken(new LoginRequestDto(req.Email, req.Password), ct); + if (tokenInfo is null) + { + _logger.LogWarning("Failed to login recently registered user {Login}", req.Login); + return tokenInfo; + } + + return tokenInfo; + } } \ No newline at end of file diff --git a/src/Infrastructure/Persistence/DependencyInjection.cs b/src/Infrastructure/Persistence/DependencyInjection.cs index 16e6275..13a1001 100644 --- a/src/Infrastructure/Persistence/DependencyInjection.cs +++ b/src/Infrastructure/Persistence/DependencyInjection.cs @@ -26,6 +26,7 @@ private static void AddMongoDb(this IServiceCollection services, IConfiguration { services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.Configure(config.GetSection(MongoDbOptions.ConfigurationSectionName)); } diff --git a/src/Infrastructure/Persistence/Mongo/Abstractions/IUserPreferencesRepository.cs b/src/Infrastructure/Persistence/Mongo/Abstractions/IUserPreferencesRepository.cs new file mode 100644 index 0000000..401b04d --- /dev/null +++ b/src/Infrastructure/Persistence/Mongo/Abstractions/IUserPreferencesRepository.cs @@ -0,0 +1,5 @@ +using Infrastructure.Persistence.Mongo.Entities.Preferences; + +namespace Infrastructure.Persistence.Mongo.Abstractions; + +public interface IUserPreferencesRepository : IMongoRepository; \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Mongo/Entities/Preferences/ChannelDescriptorBase.cs b/src/Infrastructure/Persistence/Mongo/Entities/Preferences/ChannelDescriptorBase.cs new file mode 100644 index 0000000..0dd813d --- /dev/null +++ b/src/Infrastructure/Persistence/Mongo/Entities/Preferences/ChannelDescriptorBase.cs @@ -0,0 +1,8 @@ +namespace Infrastructure.Persistence.Mongo.Entities.Preferences; + +public record ChannelDescriptorBase +{ + public bool Enabled { get; set; } + public string? Description { get; set; } + public Dictionary? Metadata { get; set; } +} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Mongo/Entities/Preferences/UserPreferences.cs b/src/Infrastructure/Persistence/Mongo/Entities/Preferences/UserPreferences.cs new file mode 100644 index 0000000..5645727 --- /dev/null +++ b/src/Infrastructure/Persistence/Mongo/Entities/Preferences/UserPreferences.cs @@ -0,0 +1,8 @@ +namespace Infrastructure.Persistence.Mongo.Entities.Preferences; + +public record UserPreferences : MongoBaseEntity +{ + public required string UserId { get; init; } + public required Dictionary Channels { get; init; } + public DateTimeOffset LastUpdated { get; init; } +} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Mongo/Repositories/MongoDbRepositoryBase.cs b/src/Infrastructure/Persistence/Mongo/Repositories/MongoDbRepositoryBase.cs index 7aff034..2559228 100644 --- a/src/Infrastructure/Persistence/Mongo/Repositories/MongoDbRepositoryBase.cs +++ b/src/Infrastructure/Persistence/Mongo/Repositories/MongoDbRepositoryBase.cs @@ -8,7 +8,7 @@ namespace Infrastructure.Persistence.Mongo.Repositories; -internal abstract class MongoDbRepositoryBase : IMongoRepository +public abstract class MongoDbRepositoryBase : IMongoRepository where T : Entities.MongoBaseEntity { protected readonly IMongoCollection Collection; diff --git a/src/Infrastructure/Persistence/Mongo/Repositories/UserPreferencesRepository.cs b/src/Infrastructure/Persistence/Mongo/Repositories/UserPreferencesRepository.cs new file mode 100644 index 0000000..045f304 --- /dev/null +++ b/src/Infrastructure/Persistence/Mongo/Repositories/UserPreferencesRepository.cs @@ -0,0 +1,9 @@ +using Infrastructure.Persistence.Mongo.Abstractions; +using Infrastructure.Persistence.Mongo.Configurations; +using Infrastructure.Persistence.Mongo.Entities.Preferences; +using Microsoft.Extensions.Options; + +namespace Infrastructure.Persistence.Mongo.Repositories; + +public class UserPreferencesRepository(IOptions settings) + : MongoDbRepositoryBase(settings), IUserPreferencesRepository;