diff --git a/Eshop.sln b/Eshop.sln index 825e572..b036406 100644 --- a/Eshop.sln +++ b/Eshop.sln @@ -83,6 +83,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Email.MessageBus", "crs\Ser EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Idenitty.Grpc", "crs\Services\Identity\Idenitty.Grpc\Idenitty.Grpc.csproj", "{1DEB0D00-7A0F-4F76-85CA-75E2DDA390B5}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Catalog.MessageBus", "crs\Services\Catalog\Catalog.MessageBus\Catalog.MessageBus.csproj", "{CE14C952-F456-4632-A73B-AA5F63BB646A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Basket.Domain", "crs\Services\Basket\Basket.Domain\Basket.Domain.csproj", "{6A9BF2B3-9B6D-43AC-996E-4CCCA5269922}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Basket.Persistence", "crs\Services\Basket\Basket.Persistence\Basket.Persistence.csproj", "{DD696AF3-2BD3-43A8-B7B8-A3CD1BE9A46C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -205,6 +211,18 @@ Global {1DEB0D00-7A0F-4F76-85CA-75E2DDA390B5}.Debug|Any CPU.Build.0 = Debug|Any CPU {1DEB0D00-7A0F-4F76-85CA-75E2DDA390B5}.Release|Any CPU.ActiveCfg = Release|Any CPU {1DEB0D00-7A0F-4F76-85CA-75E2DDA390B5}.Release|Any CPU.Build.0 = Release|Any CPU + {CE14C952-F456-4632-A73B-AA5F63BB646A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE14C952-F456-4632-A73B-AA5F63BB646A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE14C952-F456-4632-A73B-AA5F63BB646A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE14C952-F456-4632-A73B-AA5F63BB646A}.Release|Any CPU.Build.0 = Release|Any CPU + {6A9BF2B3-9B6D-43AC-996E-4CCCA5269922}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6A9BF2B3-9B6D-43AC-996E-4CCCA5269922}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6A9BF2B3-9B6D-43AC-996E-4CCCA5269922}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6A9BF2B3-9B6D-43AC-996E-4CCCA5269922}.Release|Any CPU.Build.0 = Release|Any CPU + {DD696AF3-2BD3-43A8-B7B8-A3CD1BE9A46C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DD696AF3-2BD3-43A8-B7B8-A3CD1BE9A46C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DD696AF3-2BD3-43A8-B7B8-A3CD1BE9A46C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DD696AF3-2BD3-43A8-B7B8-A3CD1BE9A46C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -248,6 +266,9 @@ Global {9767E75F-8186-4BBE-A81A-E18B1F4687D8} = {1B13E7AE-01B6-4092-9DD0-1D53F6906791} {492C4BC4-B89A-4E5C-B9F0-FC7A03BFE1FB} = {7CFB86E0-426D-4822-A84E-D2BC5987D187} {1DEB0D00-7A0F-4F76-85CA-75E2DDA390B5} = {1B13E7AE-01B6-4092-9DD0-1D53F6906791} + {CE14C952-F456-4632-A73B-AA5F63BB646A} = {708EB78A-1D36-4F33-8171-3BC0ADF1C669} + {6A9BF2B3-9B6D-43AC-996E-4CCCA5269922} = {9DF0F008-D19B-4AB9-848F-9C2CB7B759CC} + {DD696AF3-2BD3-43A8-B7B8-A3CD1BE9A46C} = {9DF0F008-D19B-4AB9-848F-9C2CB7B759CC} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {383EFC19-58A6-4418-98E0-23BD0341BA42} diff --git a/crs/CommonComponents/Common/Domain/Primitives/AggregateRoot.cs b/crs/CommonComponents/Common/Domain/Primitives/AggregateRoot.cs index 7841da5..4b5bbbe 100644 --- a/crs/CommonComponents/Common/Domain/Primitives/AggregateRoot.cs +++ b/crs/CommonComponents/Common/Domain/Primitives/AggregateRoot.cs @@ -3,9 +3,9 @@ /// /// Abstract class for aggregate root. /// -/// The strongest id type. -public class AggregateRoot : Entity - where StrongestId : IStrongestId +/// The strongest id type. +public abstract class AggregateRoot : Entity + where TStrongestId : IStrongestId { /// /// Initializes a new instance of the class. @@ -16,5 +16,5 @@ protected AggregateRoot() { } /// Initializes a new instance of the class. /// /// The id. - protected AggregateRoot(StrongestId id) : base(id) { } + protected AggregateRoot(TStrongestId id) : base(id) { } } diff --git a/crs/CommonComponents/Common/Domain/Primitives/Entity.cs b/crs/CommonComponents/Common/Domain/Primitives/Entity.cs index c77b260..9aa6e20 100644 --- a/crs/CommonComponents/Common/Domain/Primitives/Entity.cs +++ b/crs/CommonComponents/Common/Domain/Primitives/Entity.cs @@ -35,7 +35,7 @@ protected Entity() { } /// /// Gets the domain events. /// - public IReadOnlyCollection DomainEvents => _domainEvents.ToList(); + public IReadOnlyCollection DomainEvents => _domainEvents.AsReadOnly(); /// /// Add a domain event. @@ -57,11 +57,6 @@ public override bool Equals(object? obj) return false; } - if(obj.GetType() != GetType()) - { - return false; - } - if(obj is not Entity entity) { return false; diff --git a/crs/CommonComponents/Common/Domain/Primitives/Enumeration.cs b/crs/CommonComponents/Common/Domain/Primitives/Enumeration.cs index 458da46..ac55bed 100644 --- a/crs/CommonComponents/Common/Domain/Primitives/Enumeration.cs +++ b/crs/CommonComponents/Common/Domain/Primitives/Enumeration.cs @@ -8,13 +8,16 @@ public abstract class Enumeration(int value, string name) private static readonly Dictionary _enumerations = GetEnumerations(); - public static TEnum? FromValue(int value) => + public static TEnum? FromValueOrDefault(int value) => _enumerations.TryGetValue(value, out var enumeration) ? enumeration : null; public static TEnum FromName(string name) => _enumerations.Values.Single(x => x.Name == name); + public static TEnum? FromNameOrDefault(string name) => + _enumerations.Values.SingleOrDefault(x => x.Name == name); + public static IEnumerable GetNames() => _enumerations.Values.Select(x => x.Name); diff --git a/crs/CommonComponents/Common/Domain/Primitives/Events/IDomainEvent.cs b/crs/CommonComponents/Common/Domain/Primitives/Events/IDomainEvent.cs index a415383..88788dd 100644 --- a/crs/CommonComponents/Common/Domain/Primitives/Events/IDomainEvent.cs +++ b/crs/CommonComponents/Common/Domain/Primitives/Events/IDomainEvent.cs @@ -10,4 +10,4 @@ public interface IDomainEvent : INotification /// Gets the id of the event. /// Guid Id { get; } -} +} diff --git a/crs/CommonComponents/Common/Domain/Primitives/IUnitOfWork .cs b/crs/CommonComponents/Common/Domain/Primitives/IUnitOfWork .cs index c1d90f6..4f7886a 100644 --- a/crs/CommonComponents/Common/Domain/Primitives/IUnitOfWork .cs +++ b/crs/CommonComponents/Common/Domain/Primitives/IUnitOfWork .cs @@ -10,6 +10,11 @@ public interface IUnitOfWork /// /// The . /// A representing the asynchronous operation. - Task SaveChangesAsync(CancellationToken cancellationToken = default); - int SaveChanges(); + Task CommitAsync(CancellationToken cancellationToken = default); + + /// + /// Gets the database context. + /// + /// The . + int Commit(); } diff --git a/crs/CommonComponents/Common/Infrastrutrure/Middleware/ExceptionHandlingMiddleware.cs b/crs/CommonComponents/Common/Infrastrutrure/Middleware/ExceptionHandlingMiddleware.cs index 7e83a33..d1f7ece 100644 --- a/crs/CommonComponents/Common/Infrastrutrure/Middleware/ExceptionHandlingMiddleware.cs +++ b/crs/CommonComponents/Common/Infrastrutrure/Middleware/ExceptionHandlingMiddleware.cs @@ -32,8 +32,8 @@ public async Task InvokeAsync(HttpContext context) } catch (Exception ex) { - _logger.LogError(ex, ex.Message); - + _logger.LogError(ex, "the error: {0}", ex.Message); + var problemDetails = new ProblemDetails { Status = StatusCodes.Status500InternalServerError, diff --git a/crs/CommonComponents/Contracts/Contracts.csproj b/crs/CommonComponents/Contracts/Contracts.csproj index 7b5f0d4..3de5c17 100644 --- a/crs/CommonComponents/Contracts/Contracts.csproj +++ b/crs/CommonComponents/Contracts/Contracts.csproj @@ -13,7 +13,7 @@ - + diff --git a/crs/CommonComponents/Contracts/Services/Identity/identity.v1.proto b/crs/CommonComponents/Contracts/Services/Identity/identity.proto similarity index 93% rename from crs/CommonComponents/Contracts/Services/Identity/identity.v1.proto rename to crs/CommonComponents/Contracts/Services/Identity/identity.proto index 8d6cb82..f26643f 100644 --- a/crs/CommonComponents/Contracts/Services/Identity/identity.v1.proto +++ b/crs/CommonComponents/Contracts/Services/Identity/identity.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -package identity.v1; +package identity.protobuf; service IdentityService { rpc GetUserInfo(GetUserRequest) returns (UserInfo); diff --git a/crs/CommonComponents/EventBus/EventBus.Common/Abstractions/IMessageBus.cs b/crs/CommonComponents/EventBus/EventBus.Common/Abstractions/IMessageBus.cs index 63a2220..2c0c04f 100644 --- a/crs/CommonComponents/EventBus/EventBus.Common/Abstractions/IMessageBus.cs +++ b/crs/CommonComponents/EventBus/EventBus.Common/Abstractions/IMessageBus.cs @@ -3,7 +3,7 @@ /// /// Base interfase for event bus. /// -public interface IMessageBus +public interface IMessageBusBase { /// /// Publish event to the bus. diff --git a/crs/CommonComponents/EventBus/EventBus.MassTransit.RabbitMQ/EventBus.MassTransit.RabbitMQ.csproj b/crs/CommonComponents/EventBus/EventBus.MassTransit.RabbitMQ/EventBus.MassTransit.RabbitMQ.csproj index 1f3a9c0..ca0dd4d 100644 --- a/crs/CommonComponents/EventBus/EventBus.MassTransit.RabbitMQ/EventBus.MassTransit.RabbitMQ.csproj +++ b/crs/CommonComponents/EventBus/EventBus.MassTransit.RabbitMQ/EventBus.MassTransit.RabbitMQ.csproj @@ -13,6 +13,7 @@ + diff --git a/crs/CommonComponents/EventBus/EventBus.MassTransit.RabbitMQ/GlobalUsings.cs b/crs/CommonComponents/EventBus/EventBus.MassTransit.RabbitMQ/GlobalUsings.cs index b363e81..521ac50 100644 --- a/crs/CommonComponents/EventBus/EventBus.MassTransit.RabbitMQ/GlobalUsings.cs +++ b/crs/CommonComponents/EventBus/EventBus.MassTransit.RabbitMQ/GlobalUsings.cs @@ -1,2 +1,3 @@ global using MassTransit; global using EventBus.Common.Abstractions; +global using EventBus.MassTransit.Abstractions; diff --git a/crs/CommonComponents/EventBus/EventBus.MassTransit.RabbitMQ/Services/EventBusRabitMQ.cs b/crs/CommonComponents/EventBus/EventBus.MassTransit.RabbitMQ/Services/EventBusRabitMQ.cs index 3f4b0c9..1d6371f 100644 --- a/crs/CommonComponents/EventBus/EventBus.MassTransit.RabbitMQ/Services/EventBusRabitMQ.cs +++ b/crs/CommonComponents/EventBus/EventBus.MassTransit.RabbitMQ/Services/EventBusRabitMQ.cs @@ -17,4 +17,9 @@ public async Task Send { await _busControl.Send(command, cancellationToken); } + + public async Task GetSendEndpoint(Uri address) + { + return await _busControl.GetSendEndpoint(address); + } } diff --git a/crs/CommonComponents/EventBus/EventBus.MassTransit/Abstractions/IMessageBus.cs b/crs/CommonComponents/EventBus/EventBus.MassTransit/Abstractions/IMessageBus.cs new file mode 100644 index 0000000..5a2014f --- /dev/null +++ b/crs/CommonComponents/EventBus/EventBus.MassTransit/Abstractions/IMessageBus.cs @@ -0,0 +1,14 @@ +namespace EventBus.MassTransit.Abstractions; + +/// +/// The message bus interface. +/// +public interface IMessageBus : IMessageBusBase +{ + /// + /// Get send endpoint by address. + /// + /// The address. + /// The . + Task GetSendEndpoint(Uri address); +} diff --git a/crs/Docker/.env b/crs/Docker/.env index a9a4364..0db0e97 100644 --- a/crs/Docker/.env +++ b/crs/Docker/.env @@ -1,5 +1,6 @@ -AUTH_ISSUER=https://localhost:8081 -WEB_AUDIENCE=https://localhost:7061 +AUTH_ISSUER=http://identity.app +IDENTITY_GRPC_URL=http://identity.app:81 +WEB_AUDIENCE=http://catalog.app JWT_SECURITY_KEY=86A7B43F17CA48BEE7519EB8D6BDBA2SNSD INFLUXDB_USER=admin diff --git a/crs/Docker/docker-compose.override.yml b/crs/Docker/docker-compose.override.yml index 4316733..cb19547 100644 --- a/crs/Docker/docker-compose.override.yml +++ b/crs/Docker/docker-compose.override.yml @@ -3,8 +3,7 @@ version: "3.4" services: catalog.app: environment: - - ASPNETCORE_URLS=http://+:80 - - ASPNETCORE_HTTP_PORTS=7060 + - ASPNETCORE_HTTP_PORTS=80 - ASPNETCORE_ENVIRONMENT=Development # Custom environment variables - REDIS_PASSWORD=${REDIS_PASSWORD} @@ -14,6 +13,9 @@ services: - AUTH_ISSUER=${AUTH_ISSUER} - WEB_AUDIENCE=${WEB_AUDIENCE} - JWT_SECURITY_KEY=${JWT_SECURITY_KEY} + - RABBITMQ_DEFAULT_USER=${RABBITMQ_DEFAULT_USER} + - RABBITMQ_DEFAULT_PASS=${RABBITMQ_DEFAULT_PASS} + - IDENTITY_GRPC_URL=${IDENTITY_GRPC_URL} volumes: - ${APPDATA}\microsoft\UserSecrets\:/root/.microsoft/usersecrets - ${USERPROFILE}\.aspnet\https:/root/.aspnet/https/ @@ -22,9 +24,9 @@ services: identity.app: environment: - - ASPNETCORE_URLS=http://+:80 - - ASPNETCORE_HTTP_PORTS=8080 + - ASPNETCORE_HTTP_PORTS=80 - ASPNETCORE_ENVIRONMENT=Development + # - Kestrel__EndpointDefaults__Protocols=Http2 # Custom environment variables - POSTGRES_USER=${POSTGRES_USER} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} @@ -34,6 +36,8 @@ services: - JWT_SECURITY_KEY=${JWT_SECURITY_KEY} - RABBITMQ_DEFAULT_USER=${RABBITMQ_DEFAULT_USER} - RABBITMQ_DEFAULT_PASS=${RABBITMQ_DEFAULT_PASS} + - HTTP_PORT=80 + - GRPC_PORT=81 ports: - 8080:80 volumes: @@ -42,8 +46,7 @@ services: monitoring.app: environment: - - ASPNETCORE_URLS=http://+:80 - - ASPNETCORE_HTTP_PORTS=5065 + - ASPNETCORE_HTTP_PORTS=80 - ASPNETCORE_ENVIRONMENT=Development # Custom environment variables - HealthChecksUI__HealthChecks__0__Name=Catalog API @@ -59,9 +62,8 @@ services: gateways.web: environment: - - ASPNETCORE_URL=http://+:80;https://+:443 - - ASPNETCORE_HTTP_PORTS=5039 - - ASPNETCORE_HTTPS_PORTS=5040 + - ASPNETCORE_HTTP_PORTS=80 + - ASPNETCORE_HTTPS_PORTS=443 - ASPNETCORE_ENVIRONMENT=Development # Custom environment variables - LetuceEncrypt__EmailAdress=akber.sharifov2004@gmail.com @@ -73,11 +75,9 @@ services: - ${APPDATA}\microsoft\UserSecrets\:/root/.microsoft/usersecrets - ${USERPROFILE}\.aspnet\https:/root/.aspnet/https/ - email.app: environment: - - ASPNETCORE_URLS=http://+:80 - - ASPNETCORE_HTTP_PORTS=5110 + - ASPNETCORE_HTTP_PORTS=80 - ASPNETCORE_ENVIRONMENT=Development # Custom environment variables - Email__From=${EMAIL_FROM}} @@ -85,17 +85,17 @@ services: - Email__Port=${EMAIL_PORT} - Email__Username=${EMAIL_USERNAME} - Email__Password=${EMAIL_PASSWORD} - - Retry__Message__Send__Count=3 + - Email__RetryMessageSendCount=3 + - IdentityEndpoint__BaseUrl=${AUTH_ISSUER}} - RABBITMQ_DEFAULT_USER=${RABBITMQ_DEFAULT_USER} - RABBITMQ_DEFAULT_PASS=${RABBITMQ_DEFAULT_PASS} - AUTH_ISSUER=${AUTH_ISSUER} - WEB_AUDIENCE=${WEB_AUDIENCE} - JWT_SECURITY_KEY=${JWT_SECURITY_KEY} + - IDENTITY_GRPC_URL=${IDENTITY_GRPC_URL} ports: - "5110:80" - - mssql: environment: - ACCEPT_EULA=Y diff --git a/crs/Docker/docker-compose.yml b/crs/Docker/docker-compose.yml index 844c926..add6faa 100644 --- a/crs/Docker/docker-compose.yml +++ b/crs/Docker/docker-compose.yml @@ -95,3 +95,23 @@ services: networks: web_services_network: driver: bridge + + + # eventstore: + # container_name: eventstore + # image: eventstore/eventstore:21.2.0-buster-slim + # restart: unless-stopped + # environment: + # - EVENTSTORE_CLUSTER_SIZE=1 + # - EVENTSTORE_RUN_PROJECTIONS=All + # - EVENTSTORE_START_STANDARD_PROJECTIONS=true + # - EVENTSTORE_EXT_TCP_PORT=1113 + # - EVENTSTORE_EXT_HTTP_PORT=2113 + # - EVENTSTORE_INSECURE=true + # - EVENTSTORE_ENABLE_EXTERNAL_TCP=true + # - EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP=true + # ports: + # - '1113:1113' + # - '2113:2113' + # networks: + # - booking \ No newline at end of file diff --git a/crs/Services/Basket/Basket.Domain/Basket.Domain.csproj b/crs/Services/Basket/Basket.Domain/Basket.Domain.csproj new file mode 100644 index 0000000..c02be1c --- /dev/null +++ b/crs/Services/Basket/Basket.Domain/Basket.Domain.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/CatalogBasket.cs b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/CatalogBasket.cs new file mode 100644 index 0000000..bb0fc9f --- /dev/null +++ b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/CatalogBasket.cs @@ -0,0 +1,106 @@ +namespace Basket.Domain.CatalogBasketAggregate; + +public sealed class CatalogBasket : AggregateRoot +{ + private readonly UserId UserId; + private readonly List _basketItems = []; + public IReadOnlyCollection BasketItems => _basketItems.AsReadOnly(); + + private CatalogBasket(CatalogBasketId id, UserId userId) + { + Id = id; + UserId = userId; + _basketItems = []; + } + + public static Result Create(CatalogBasketId id, UserId userId, bool isUserBasketExist) + { + if (isUserBasketExist) + { + return Result.Failure( + CatalogBasketErrors.UserBasketExist); + } + + CatalogBasket basket = new(id, userId); + basket.AddDomainEvent( + new CatalogBasketCreatedDomainEvent(Guid.NewGuid(), id)); + + return basket; + } + + public void AddItem(CatalogBasketItem basketItem, int quantity) + { + var existingItem = _basketItems + .SingleOrDefault(basketItem => basketItem.Id == basketItem.Id); + + if (existingItem != null) + { + existingItem.AddQuantity(quantity); + return; + } + + _basketItems.Add(basketItem); + } + + public void RemoveEmptyItems() => + _basketItems.RemoveAll(x => x.Quantity == 0); + + public Result SetQuantity(CatalogBasketItemId basketItemId, int quantity) + { + var basketItemResult = GetBasketItemById(basketItemId); + + if (basketItemResult.IsFailure) + { + return Result.Failure( + basketItemResult.Error); + } + + basketItemResult.Value.SetQuantity(quantity); + return Result.Success(); + } + + public Result AddQuantity(CatalogBasketItemId basketItemId, int quantity) + { + var basketItemResult = GetBasketItemById(basketItemId); + + if (basketItemResult.IsFailure) + { + return Result.Failure( + basketItemResult.Error); + } + + basketItemResult.Value.AddQuantity(quantity); + + return Result.Success(); + } + + public Result RemoveItem(CatalogBasketItemId basketItemId) + { + var basketItemResult = GetBasketItemById(basketItemId); + + if (basketItemResult.IsFailure) + { + return Result.Failure( + basketItemResult.Error); + } + + _basketItems.Remove(basketItemResult.Value); + + return Result.Success(); + } + + public Result GetBasketItemById(CatalogBasketItemId basketItemId) + { + var existingItem = _basketItems.SingleOrDefault(basketItem => basketItem.Id == basketItemId); + + if (existingItem == null) + { + return Result.Failure( + CatalogBasketErrors.BasketItemDoesNotExist); + } + + return existingItem; + } + + public void ClearItems() => _basketItems.Clear(); +} diff --git a/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/DomainEvents/CatalogBasketCreatedDomainEvent.cs b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/DomainEvents/CatalogBasketCreatedDomainEvent.cs new file mode 100644 index 0000000..99b61aa --- /dev/null +++ b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/DomainEvents/CatalogBasketCreatedDomainEvent.cs @@ -0,0 +1,3 @@ +namespace Basket.Domain.CatalogBasketAggregate.DomainEvents; + +public sealed record CatalogBasketCreatedDomainEvent(Guid Id, CatalogBasketId BasketId) : IDomainEvent; diff --git a/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Entities/CatalogBasketItem.cs b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Entities/CatalogBasketItem.cs new file mode 100644 index 0000000..5845f9e --- /dev/null +++ b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Entities/CatalogBasketItem.cs @@ -0,0 +1,38 @@ +namespace Basket.Domain.CatalogBasketAggregate.Entities; + +public class CatalogBasketItem : Entity +{ + public CatalogProduct Product { get; private set; } + public Quantity Quantity { get; private set; } + + private CatalogBasketItem(CatalogBasketItemId id, CatalogProduct product, Quantity quantity) => + (Id, Product, Quantity) = (id, product, quantity); + + public static Result Create(CatalogBasketItemId id, CatalogProduct product, Quantity quantity) + { + var catalogBasketItem = new CatalogBasketItem(id, product, quantity); + + return catalogBasketItem; + } + + public Result SetQuantity(int quantity) + { + if (quantity > Product.Quantity) + { + return Result.Failure( + CatalogBasketItemErrors.QuantityExceedsProductCount); + } + + var quantityResult = Quantity.Create(quantity); + + if (quantityResult.IsFailure) + { + return Result.Failure(quantityResult.Error); + } + + return Result.Success(); + } + + public Result AddQuantity(int quantity) => + SetQuantity(Quantity.Value + quantity); +} diff --git a/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Entities/CatalogProduct.cs b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Entities/CatalogProduct.cs new file mode 100644 index 0000000..7876657 --- /dev/null +++ b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Entities/CatalogProduct.cs @@ -0,0 +1,45 @@ +namespace Basket.Domain.CatalogBasketAggregate.Entities; + +public class CatalogProduct : Entity +{ + public ProductId ProductId { get; private set; } + public CatalogProductName CatalogProductName { get; private set; } + public Money Price { get; private set; } + public ImageUrl ProductImage { get; private set; } + public Quantity Quantity { get; private set; } + + private CatalogProduct( + CatalogProductId catalogProductId, + ProductId productId, + CatalogProductName catalogProductName, + Money price, + ImageUrl productImage, + Quantity quantity) + { + Id = catalogProductId; + ProductId = productId; + CatalogProductName = catalogProductName; + Price = price; + ProductImage = productImage; + Quantity = quantity; + } + + public static Result Create( + CatalogProductId catalogProductId, + ProductId productId, + CatalogProductName catalogProductName, + Money price, + ImageUrl productImage, + Quantity quantity) + { + var catalogProduct = new CatalogProduct( + catalogProductId, + productId, + catalogProductName, + price, + productImage, + quantity); + + return catalogProduct; + } +} diff --git a/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Errors/CatalogBasketErrors.cs b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Errors/CatalogBasketErrors.cs new file mode 100644 index 0000000..c10602d --- /dev/null +++ b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Errors/CatalogBasketErrors.cs @@ -0,0 +1,10 @@ +namespace Basket.Domain.CatalogBasketAggregate.Errors; + +public static class CatalogBasketErrors +{ + public static Error BasketItemDoesNotExist => + new("Basket.BasketItemDoesNotExist", "Basket item does not exist."); + + public static Error UserBasketExist => + new("Basket.UserBasketExist", "User basket already exists."); +} diff --git a/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Errors/CatalogBasketItemErrors.cs b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Errors/CatalogBasketItemErrors.cs new file mode 100644 index 0000000..bbd5dc0 --- /dev/null +++ b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Errors/CatalogBasketItemErrors.cs @@ -0,0 +1,11 @@ +namespace Basket.Domain.CatalogBasketAggregate.Errors; + +public static class CatalogBasketItemErrors +{ + public static Error QuantityMustBeGreaterThanZero => + new("BasketItem.QuantityMustBeGreaterThanZero", "Quantity must be greater than zero"); + + public static Error QuantityExceedsProductCount => + new("BasketItem.QuantityExceedsProductCount", "Quantity exceeds product count"); + +} diff --git a/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Errors/CatalogProductNameErrors.cs b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Errors/CatalogProductNameErrors.cs new file mode 100644 index 0000000..c44b4d6 --- /dev/null +++ b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Errors/CatalogProductNameErrors.cs @@ -0,0 +1,10 @@ +namespace Basket.Domain.CatalogBasketAggregate.ValueObjects; + +public static class CatalogProductNameErrors +{ + public static Error CannotBeEmpty => + new("CatalogProductName.CannotBeEmpty", "name cannot be empty"); + + public static Error CannotBeLongerThan(int maxLength) => + new("CatalogProductName.CannotBeLongerThan", $"name cannot be longer than {maxLength}"); +} \ No newline at end of file diff --git a/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Errors/ImageUrlErrors.cs b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Errors/ImageUrlErrors.cs new file mode 100644 index 0000000..771129f --- /dev/null +++ b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Errors/ImageUrlErrors.cs @@ -0,0 +1,10 @@ +namespace Basket.Domain.CatalogBasketAggregate.ValueObjects; + +public static class ImageUrlErrors +{ + public static Error CannotByEmpty => + new("ImageUrl.CannotByEmpty", "Image url cannot be empty"); + + public static Error IsInvalid => + new("ImageUrl.IsInvalid", "Image url is invalid"); +} \ No newline at end of file diff --git a/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Errors/MoneyErrors.cs b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Errors/MoneyErrors.cs new file mode 100644 index 0000000..cd0e96b --- /dev/null +++ b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Errors/MoneyErrors.cs @@ -0,0 +1,10 @@ +namespace Basket.Domain.CatalogBasketAggregate.ValueObjects; + +public static class MoneyErrors +{ + public static Error CannotBeNegative => + new("Money.CannotBeNegative", "Money cannot be negative"); + + public static Error CannotBeEmpty => + new("Money.CannotBeEmpty", "Money cannot be empty"); +} \ No newline at end of file diff --git a/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Errors/QuantityErrors.cs b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Errors/QuantityErrors.cs new file mode 100644 index 0000000..001e990 --- /dev/null +++ b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Errors/QuantityErrors.cs @@ -0,0 +1,7 @@ +namespace Basket.Domain.CatalogBasketAggregate.ValueObjects; + +public static class QuantityErrors +{ + public static Error QuantityMustBeGreaterThanZero => + new("Quantity.QuantityMustBeGreaterThanZero", "Quantity must be greater than zero"); +} diff --git a/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Ids/CatalogBasketId.cs b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Ids/CatalogBasketId.cs new file mode 100644 index 0000000..117901c --- /dev/null +++ b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Ids/CatalogBasketId.cs @@ -0,0 +1,3 @@ +namespace Basket.Domain.CatalogBasketAggregate.Ids; + +public sealed record CatalogBasketId(Guid Value) : IStrongestId; diff --git a/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Ids/CatalogBasketItemId.cs b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Ids/CatalogBasketItemId.cs new file mode 100644 index 0000000..d492c46 --- /dev/null +++ b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Ids/CatalogBasketItemId.cs @@ -0,0 +1,3 @@ +namespace Basket.Domain.CatalogBasketAggregate.Ids; + +public sealed record CatalogBasketItemId(Guid Value) : IStrongestId; \ No newline at end of file diff --git a/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Ids/CatalogProductId.cs b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Ids/CatalogProductId.cs new file mode 100644 index 0000000..6d80183 --- /dev/null +++ b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Ids/CatalogProductId.cs @@ -0,0 +1,3 @@ +namespace Basket.Domain.CatalogBasketAggregate.Ids; + +public sealed record CatalogProductId(Guid Value) : IStrongestId; diff --git a/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Ids/ProductId.cs b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Ids/ProductId.cs new file mode 100644 index 0000000..04aff18 --- /dev/null +++ b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Ids/ProductId.cs @@ -0,0 +1,3 @@ +namespace Basket.Domain.CatalogBasketAggregate.Ids; + +public sealed record ProductId(Guid Value) : IStrongestId; diff --git a/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Ids/UserId.cs b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Ids/UserId.cs new file mode 100644 index 0000000..4b324d6 --- /dev/null +++ b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Ids/UserId.cs @@ -0,0 +1,3 @@ +namespace Basket.Domain.CatalogBasketAggregate.Ids; + +public sealed record UserId(Guid Value) : IStrongestId; diff --git a/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Regexes/ImageUrlRegex.cs b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Regexes/ImageUrlRegex.cs new file mode 100644 index 0000000..00e7845 --- /dev/null +++ b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Regexes/ImageUrlRegex.cs @@ -0,0 +1,9 @@ +namespace Basket.Domain.CatalogBasketAggregate.Regexes; + +public partial class ImageUrlRegex +{ + private const string ImageUrlPattern = @"^(http(s?):)([/|.|\w|\s|-])*\.(?:jpg|gif|png)$"; + + [GeneratedRegex(ImageUrlPattern)] + public static partial Regex Regex(); +} diff --git a/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Repositories/ICatalogBasketRepository.cs b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Repositories/ICatalogBasketRepository.cs new file mode 100644 index 0000000..76ef4a2 --- /dev/null +++ b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/Repositories/ICatalogBasketRepository.cs @@ -0,0 +1,11 @@ +namespace Basket.Domain.CatalogBasketAggregate.Repositories; + +public interface ICatalogBasketRepository : IRepository +{ + //public void AddProduct(UserId userId, CatalogProduct CatalogBasketItem); + //public void RemoveProduct(UserId userId, CatalogBasketItemId catalogBasketItemId); + //public void AddQuantityToCatalogBasketItem(UserId userId, Quantity quantity); + //public void UpdateCatalogProduct(CatalogProduct catalogProduct); + //public Task GetProducts(UserId userId); + +} diff --git a/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/ValueObjects/CatalogProductName.cs b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/ValueObjects/CatalogProductName.cs new file mode 100644 index 0000000..49d86b5 --- /dev/null +++ b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/ValueObjects/CatalogProductName.cs @@ -0,0 +1,51 @@ +namespace Basket.Domain.CatalogBasketAggregate.ValueObjects; + +/// +/// Catalog product name value object. +/// +public class CatalogProductName : ValueObject +{ + /// + /// Gets value of the catalog product name. + /// + public string Value { get; } + + /// + /// Catalog product name max length. + /// + public const int CatalogProductNameMaxLength = 100; + + private CatalogProductName(string value) => Value = value; + + /// + /// Creates new instance of the class. + /// + /// value of the product name. + /// + /// if is valid returns with value. + /// else returns with error. + /// + public static Result Create(string value) + { + if (value.IsNullOrWhiteSpace()) + { + return Result.Failure( + CatalogProductNameErrors.CannotBeEmpty); + } + + if (value.Length > CatalogProductNameMaxLength) + { + return Result.Failure( + CatalogProductNameErrors.CannotBeLongerThan(CatalogProductNameMaxLength)); + } + + return new CatalogProductName(value); + } + + /// Gets equality components. + /// + public override IEnumerable GetEqualityComponents() + { + yield return Value; + } +} \ No newline at end of file diff --git a/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/ValueObjects/ImageUrl.cs b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/ValueObjects/ImageUrl.cs new file mode 100644 index 0000000..c1ee0b1 --- /dev/null +++ b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/ValueObjects/ImageUrl.cs @@ -0,0 +1,37 @@ +namespace Basket.Domain.CatalogBasketAggregate.ValueObjects; + +public sealed partial class ImageUrl : ValueObject +{ + public string Value { get; private set; } + + private ImageUrl(string value) => Value = value; + + public static Result Create(string imageUrl) + { + if (imageUrl.IsNullOrWhiteSpace()) + { + return Result.Failure( + ImageUrlErrors.CannotByEmpty); + } + + imageUrl = imageUrl.Trim(); + + if (!IsImageUrl(imageUrl)) + { + return Result.Failure( + ImageUrlErrors.IsInvalid); + } + + return new ImageUrl(imageUrl); + } + + public static bool IsImageUrl(string imageUrl) => + ImageUrlRegex.Regex().IsMatch(imageUrl); + + public override IEnumerable GetEqualityComponents() + { + yield return Value; + } + + public static implicit operator string(ImageUrl imageUrl) => imageUrl.Value; +} \ No newline at end of file diff --git a/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/ValueObjects/Money.cs b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/ValueObjects/Money.cs new file mode 100644 index 0000000..23d451a --- /dev/null +++ b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/ValueObjects/Money.cs @@ -0,0 +1,33 @@ +namespace Basket.Domain.CatalogBasketAggregate.ValueObjects; + +public sealed class Money : ValueObject +{ + public string Currency { get; private set; } + public decimal Amount { get; private set; } + + public bool PriceIsNegative => Amount >= 0; + public bool PriceIsNotNegative => !PriceIsNotNegative; + + private Money(string currency, decimal amount) => + (Currency, Amount) = (currency, amount); + + public static Result Create(string currency, decimal amount) + { + if (currency.IsNullOrWhiteSpace()) + { + return Result.Failure( + MoneyErrors.CannotBeEmpty); + } + + return new Money(currency, amount); + } + + + public override IEnumerable GetEqualityComponents() + { + yield return Currency; + yield return Amount; + } + + public static implicit operator decimal(Money money) => money.Amount; +} \ No newline at end of file diff --git a/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/ValueObjects/Quantity.cs b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/ValueObjects/Quantity.cs new file mode 100644 index 0000000..e3a955b --- /dev/null +++ b/crs/Services/Basket/Basket.Domain/CatalogBasketAggregate/ValueObjects/Quantity.cs @@ -0,0 +1,42 @@ +namespace Basket.Domain.CatalogBasketAggregate.ValueObjects; + +public sealed class Quantity : ValueObject +{ + public int Value { get; private set; } + + private Quantity(int value) => Value = value; + + public static Result Create(int value) + { + if (value < 0) + { + return Result.Failure( + QuantityErrors.QuantityMustBeGreaterThanZero); + } + + return new Quantity(value); + } + + public static Quantity Empty => new(0); + + + public override IEnumerable GetEqualityComponents() + { + yield return Value; + } + + public static bool operator ==(Quantity left, int right) => left.Value == right; + public static bool operator !=(Quantity left, int right) => left.Value != right; + public static bool operator >(Quantity left, int right) => left.Value > right; + public static bool operator <(Quantity left, int right) => left.Value < right; + public static bool operator >=(Quantity left, int right) => left.Value >= right; + public static bool operator <=(Quantity left, int right) => left.Value <= right; + + public static bool operator ==(int left, Quantity right) => left == right.Value; + public static bool operator !=(int left, Quantity right) => left != right.Value; + public static bool operator >(int left, Quantity right) => left > right.Value; + public static bool operator <(int left, Quantity right) => left < right.Value; + public static bool operator >=(int left, Quantity right) => left >= right.Value; + public static bool operator <=(int left, Quantity right) => left <= right.Value; + +} \ No newline at end of file diff --git a/crs/Services/Basket/Basket.Domain/GlobalUsings.cs b/crs/Services/Basket/Basket.Domain/GlobalUsings.cs new file mode 100644 index 0000000..f28fb0e --- /dev/null +++ b/crs/Services/Basket/Basket.Domain/GlobalUsings.cs @@ -0,0 +1,11 @@ +global using Common.Domain.Primitives; +global using Basket.Domain.CatalogBasketAggregate.Ids; +global using Common.Domain.Primitives.Results; +global using Basket.Domain.CatalogBasketAggregate.Errors; +global using Basket.Domain.CatalogBasketAggregate.Entities; +global using Common.Extensions; +global using Basket.Domain.CatalogBasketAggregate.ValueObjects; +global using Common.Domain.Primitives.Events; +global using Basket.Domain.CatalogBasketAggregate.DomainEvents; +global using System.Text.RegularExpressions; +global using Basket.Domain.CatalogBasketAggregate.Regexes; diff --git a/crs/Services/Basket/Basket.Persistence/AssemblyReference.cs b/crs/Services/Basket/Basket.Persistence/AssemblyReference.cs new file mode 100644 index 0000000..9b47f07 --- /dev/null +++ b/crs/Services/Basket/Basket.Persistence/AssemblyReference.cs @@ -0,0 +1,6 @@ +namespace Basket.Persistence; + +public static class AssemblyReference +{ + public static Assembly Assembly => typeof(AssemblyReference).Assembly; +} diff --git a/crs/Services/Basket/Basket.Persistence/Basket.Persistence.csproj b/crs/Services/Basket/Basket.Persistence/Basket.Persistence.csproj new file mode 100644 index 0000000..c9bc026 --- /dev/null +++ b/crs/Services/Basket/Basket.Persistence/Basket.Persistence.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/crs/Services/Basket/Basket.Persistence/DbContexts/Abstractions/IMongoDbContext.cs b/crs/Services/Basket/Basket.Persistence/DbContexts/Abstractions/IMongoDbContext.cs new file mode 100644 index 0000000..00cc641 --- /dev/null +++ b/crs/Services/Basket/Basket.Persistence/DbContexts/Abstractions/IMongoDbContext.cs @@ -0,0 +1,8 @@ +namespace Basket.Persistence.DbContexts.Abstractions; + +public interface IMongoDbContext +{ + void AddCommand(Func func); + Task CommitAsync(CancellationToken cancellationToken = default); + IMongoCollection GetCollection(string name); +} \ No newline at end of file diff --git a/crs/Services/Basket/Basket.Persistence/DbContexts/MongoDbContext.cs b/crs/Services/Basket/Basket.Persistence/DbContexts/MongoDbContext.cs new file mode 100644 index 0000000..a44f5c4 --- /dev/null +++ b/crs/Services/Basket/Basket.Persistence/DbContexts/MongoDbContext.cs @@ -0,0 +1,60 @@ +using Basket.Persistence.DbContexts.Abstractions; +using Common.Extensions; +using MongoDB.Bson.Serialization; + +namespace Basket.Persistence.DbContexts; + +public class MongoDbContext : IMongoDbContext +{ + private readonly IMongoDatabase _database; + private readonly MongoClient _mongoClient; + private readonly List> _commands; + + public MongoDbContext(IOptions options) + { + var mongoDbContextOptions = options.Value; + + _mongoClient = new MongoClient(mongoDbContextOptions.ConnectionString); + _database = _mongoClient.GetDatabase(mongoDbContextOptions.ConnectionString); + _commands = []; + } + + public async Task CommitAsync(CancellationToken cancellationToken = default) + { + using var session = await _mongoClient.StartSessionAsync(cancellationToken: cancellationToken); + session.StartTransaction(); + + var commandTasks = _commands.Select(c => c()); + + await Task.WhenAll(commandTasks); + await session.CommitTransactionAsync(cancellationToken); + + return _commands.Count; + } + + public IMongoCollection GetCollection(string name) => _database.GetCollection(name); + + public void AddCommand(Func func) => _commands.Add(func); + + + public static void ConfigureMapFromAssembly(Assembly assembly) => + assembly.DefinedTypes.Where(IsMapConfiguration) + .Foreach(mapConfigurationType => mapConfigurationType.GetInterfaces() + .Where(IsMapConfigurationGeneric) + .Foreach(interfaceMapConfigurationType => + { + var interfaceMapConfigurationTypeGenericArgument = interfaceMapConfigurationType.GetGenericArguments()[0]; + var bsonClassMapType = typeof(BsonClassMap<>).MakeGenericType(interfaceMapConfigurationTypeGenericArgument); + var bsonClassMapObject = Activator.CreateInstance(bsonClassMapType); + var configureMethod = interfaceMapConfigurationType.GetMethod("Configure"); + configureMethod!.Invoke(Activator.CreateInstance(mapConfigurationType), [bsonClassMapObject]); + })); + + private static bool IsMapConfiguration(Type type) => + !type.IsInterface && + !type.IsAbstract && + type.GetInterfaces().Any(i => i.GetGenericTypeDefinition() == typeof(IMapConfiguration<>)); + + private static bool IsMapConfigurationGeneric(Type type) => + type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IMapConfiguration<>); +} diff --git a/crs/Services/Basket/Basket.Persistence/DbContexts/MongoDbContextOptions.cs b/crs/Services/Basket/Basket.Persistence/DbContexts/MongoDbContextOptions.cs new file mode 100644 index 0000000..fd4f90d --- /dev/null +++ b/crs/Services/Basket/Basket.Persistence/DbContexts/MongoDbContextOptions.cs @@ -0,0 +1,7 @@ +namespace Basket.Persistence.DbContexts; + +public sealed class MongoDbContextOptions +{ + public string ConnectionString { get; set; } = null!; + public string DatabaseName { get; set; } = null!; +} \ No newline at end of file diff --git a/crs/Services/Basket/Basket.Persistence/GlobalUsings.cs b/crs/Services/Basket/Basket.Persistence/GlobalUsings.cs new file mode 100644 index 0000000..3da2731 --- /dev/null +++ b/crs/Services/Basket/Basket.Persistence/GlobalUsings.cs @@ -0,0 +1,8 @@ +global using System.Reflection; +global using Basket.Domain.CatalogBasketAggregate.Repositories; +global using Basket.Domain.CatalogBasketAggregate.Ids; +global using MongoDB.Driver; +global using Microsoft.Extensions.Options; +global using Common.Domain.Primitives; +global using Basket.Persistence.MapConfigurations.Abstractions; +global using Basket.Domain.CatalogBasketAggregate; diff --git a/crs/Services/Basket/Basket.Persistence/MapConfigurations/Abstractions/IMapConfiguration.cs b/crs/Services/Basket/Basket.Persistence/MapConfigurations/Abstractions/IMapConfiguration.cs new file mode 100644 index 0000000..2fb2c83 --- /dev/null +++ b/crs/Services/Basket/Basket.Persistence/MapConfigurations/Abstractions/IMapConfiguration.cs @@ -0,0 +1,8 @@ +using MongoDB.Bson.Serialization; + +namespace Basket.Persistence.MapConfigurations.Abstractions; + +public interface IMapConfiguration +{ + public void Configure(BsonClassMap bsonClassMap); +} diff --git a/crs/Services/Basket/Basket.Persistence/MapConfigurations/CatalogBasketMapConfiguration.cs b/crs/Services/Basket/Basket.Persistence/MapConfigurations/CatalogBasketMapConfiguration.cs new file mode 100644 index 0000000..673c3d9 --- /dev/null +++ b/crs/Services/Basket/Basket.Persistence/MapConfigurations/CatalogBasketMapConfiguration.cs @@ -0,0 +1,29 @@ +using MongoDB.Bson.Serialization; + +namespace Basket.Persistence.MapConfigurations; + +internal sealed class CatalogBasketMapConfiguration : IMapConfiguration +{ + //public void Configure() + //{ + // BsonClassMap.RegisterClassMap(map => + // { + // map.AutoMap(); + // map.SetIgnoreExtraElements(true); + // map.MapIdProperty(x => x.Id); + // map.MapProperty(x => x.BasketId).SetElementName("BasketId"); + // map.MapProperty(x => x.ProductId).SetElementName("ProductId"); + // map.MapProperty(x => x.Quantity).SetElementName("Quantity"); + // }); + //} + + public void Configure(BsonClassMap map) + { + map.AutoMap(); + map.SetIgnoreExtraElements(true); + map.MapIdProperty(x => x.Id); + map.MapProperty(x => x.BasketId).SetElementName("BasketId"); + map.MapProperty(x => x.ProductId).SetElementName("ProductId"); + map.MapProperty(x => x.Quantity).SetElementName("Quantity"); + } +} diff --git a/crs/Services/Basket/Basket.Persistence/Repositories/BasketRepository.cs b/crs/Services/Basket/Basket.Persistence/Repositories/BasketRepository.cs new file mode 100644 index 0000000..7c1b7de --- /dev/null +++ b/crs/Services/Basket/Basket.Persistence/Repositories/BasketRepository.cs @@ -0,0 +1,14 @@ +namespace Basket.Persistence.Repositories; + +internal sealed class BasketRepository : ICatalogBasketRepository +{ + + //public BasketRepository( dbContext) + //{ + //} + + //public async Task GetBasketById(CatalogBasketId basketId, CancellationToken cancellationToken = default) + //{ + + //} +} diff --git a/crs/Services/Basket/Basket.Persistence/UnitOfWork.cs b/crs/Services/Basket/Basket.Persistence/UnitOfWork.cs new file mode 100644 index 0000000..67baf66 --- /dev/null +++ b/crs/Services/Basket/Basket.Persistence/UnitOfWork.cs @@ -0,0 +1,14 @@ +using Basket.Persistence.DbContexts.Abstractions; + +namespace Basket.Persistence; + +public sealed class UnitOfWork(IMongoDbContext mongoDbContext) : IUnitOfWork +{ + private readonly IMongoDbContext _mongoDbContext = mongoDbContext; + + public int Commit() => + _mongoDbContext.CommitAsync().Result; + + public async Task CommitAsync(CancellationToken cancellationToken = default) => + await _mongoDbContext.CommitAsync(cancellationToken); +} diff --git a/crs/Services/Catalog/Catalog.App/Catalog.App.csproj b/crs/Services/Catalog/Catalog.App/Catalog.App.csproj index acacddd..1659b61 100644 --- a/crs/Services/Catalog/Catalog.App/Catalog.App.csproj +++ b/crs/Services/Catalog/Catalog.App/Catalog.App.csproj @@ -16,6 +16,8 @@ + + @@ -44,6 +46,7 @@ + diff --git a/crs/Services/Catalog/Catalog.App/Configurations/CachingServiceInstaller.cs b/crs/Services/Catalog/Catalog.App/Configurations/CachingServiceInstaller.cs index 185b176..1e20250 100644 --- a/crs/Services/Catalog/Catalog.App/Configurations/CachingServiceInstaller.cs +++ b/crs/Services/Catalog/Catalog.App/Configurations/CachingServiceInstaller.cs @@ -4,5 +4,5 @@ internal sealed class CachingServiceInstaller : IServiceInstaller { public void Install(IServiceCollection services, IConfiguration configuration) => services.AddStackExchangeRedisCache(redisOptions => - redisOptions.Configuration = $"redis:6379,password={Env.REDIS_PASSWORD}"); + redisOptions.Configuration = Env.ConnectionStrings.REDIS); } diff --git a/crs/Services/Catalog/Catalog.App/Configurations/GrpcServiceInstaller.cs b/crs/Services/Catalog/Catalog.App/Configurations/GrpcServiceInstaller.cs new file mode 100644 index 0000000..11dc421 --- /dev/null +++ b/crs/Services/Catalog/Catalog.App/Configurations/GrpcServiceInstaller.cs @@ -0,0 +1,17 @@ +using Identity.Protobuf; + +namespace Catalog.App.Configurations; + +internal sealed class GrpcServiceInstaller : IServiceInstaller +{ + public void Install(IServiceCollection services, IConfiguration configuration) + { + services.AddGrpcClient(options => + { + options.Address = new Uri(Env.IDENTITY_GRPC_URL); + }); + + services.AddGrpc(); + + } +} diff --git a/crs/Services/Catalog/Catalog.App/Configurations/InfrastructureServiceInstaller.cs b/crs/Services/Catalog/Catalog.App/Configurations/InfrastructureServiceInstaller.cs index d29b10e..1ff3d13 100644 --- a/crs/Services/Catalog/Catalog.App/Configurations/InfrastructureServiceInstaller.cs +++ b/crs/Services/Catalog/Catalog.App/Configurations/InfrastructureServiceInstaller.cs @@ -17,7 +17,9 @@ public void Install(IServiceCollection services, IConfiguration configuration) selector.FromAssemblies( Infrastructure.AssemblyReference.Assembly, Persistence.AssemblyReference.Assembly) - .AddClasses(false) + .AddClasses(classes => classes + .Where(type => !type.Namespace!.Contains("Models"))) + .UsingRegistrationStrategy(RegistrationStrategy.Skip) .AsImplementedInterfaces() .WithScopedLifetime()); } diff --git a/crs/Services/Catalog/Catalog.App/Configurations/MessageBusServiceInstaller.cs b/crs/Services/Catalog/Catalog.App/Configurations/MessageBusServiceInstaller.cs new file mode 100644 index 0000000..2d22495 --- /dev/null +++ b/crs/Services/Catalog/Catalog.App/Configurations/MessageBusServiceInstaller.cs @@ -0,0 +1,22 @@ +namespace Catalog.App.Configurations; + +internal sealed class MessageBusServiceInstaller : IServiceInstaller +{ + public void Install(IServiceCollection services, IConfiguration configuration) => + services.AddMassTransit(configure => + { + configure.SetKebabCaseEndpointNameFormatter(); + configure.UsingRabbitMq((context, configurator) => + { + configurator.Host("rabbitmq", "/", hostConfigurator => + { + hostConfigurator.Username(Env.RABBITMQ_DEFAULT_USER); + hostConfigurator.Password(Env.RABBITMQ_DEFAULT_PASS); + }); + + configurator.ConfigureEndpoints(context); + }); + + configure.AddConsumers(MessageBus.AssemblyReference.Assembly); + }); +} diff --git a/crs/Services/Catalog/Catalog.App/Env.cs b/crs/Services/Catalog/Catalog.App/Env.cs index 7be95ab..b86ae2e 100644 --- a/crs/Services/Catalog/Catalog.App/Env.cs +++ b/crs/Services/Catalog/Catalog.App/Env.cs @@ -13,19 +13,23 @@ public static class Env public static string AUTH_ISSUER => GetEnvironmentVariable("AUTH_ISSUER"); public static string WEB_AUDIENCE => GetEnvironmentVariable("WEB_AUDIENCE"); public static string JWT_SECURITY_KEY => GetEnvironmentVariable("JWT_SECURITY_KEY"); + public static string RABBITMQ_DEFAULT_USER => GetEnvironmentVariable("RABBITMQ_DEFAULT_USER"); + public static string RABBITMQ_DEFAULT_PASS => GetEnvironmentVariable("RABBITMQ_DEFAULT_PASS"); + public static string IDENTITY_GRPC_URL => GetEnvironmentVariable("IDENTITY_GRPC_URL"); private static string GetEnvironmentVariable(string key) => - Environment.GetEnvironmentVariable(key) ?? + Environment.GetEnvironmentVariable(key) ?? throw new Exception($"Environment variable {key} not found"); public static class ConnectionStrings { - public static string MSSQL => - $"Server=mssql,1433;" + - $"Initial Catalog={MSSQL_INITIAL_CATALOG};" + - $"User ID={MSSQL_USER_ID};" + - $"Password={MSSQL_SA_PASSWORD};" + - $"TrustServerCertificate=true"; + public static string MSSQL => $""" + Server=mssql,1433; + Initial Catalog={MSSQL_INITIAL_CATALOG}; + User ID={MSSQL_USER_ID}; + Password={MSSQL_SA_PASSWORD}; + TrustServerCertificate=true; + """; public static string REDIS => $"redis:6379,password={REDIS_PASSWORD}"; } diff --git a/crs/Services/Catalog/Catalog.App/GlobalUsings.cs b/crs/Services/Catalog/Catalog.App/GlobalUsings.cs index e44d44e..f895f43 100644 --- a/crs/Services/Catalog/Catalog.App/GlobalUsings.cs +++ b/crs/Services/Catalog/Catalog.App/GlobalUsings.cs @@ -21,4 +21,7 @@ global using OpenTelemetry.Metrics; global using Prometheus; global using Common.App.HealthChecks; -global using Microsoft.AspNetCore.Authentication.JwtBearer; \ No newline at end of file +global using Microsoft.AspNetCore.Authentication.JwtBearer; +global using Scrutor; +global using MassTransit; + diff --git a/crs/Services/Catalog/Catalog.Application/Brands/Commands/CreateBrand/CreateBrandCommandHandler.cs b/crs/Services/Catalog/Catalog.Application/Brands/Commands/CreateBrand/CreateBrandCommandHandler.cs index 4a260cb..f062dd4 100644 --- a/crs/Services/Catalog/Catalog.Application/Brands/Commands/CreateBrand/CreateBrandCommandHandler.cs +++ b/crs/Services/Catalog/Catalog.Application/Brands/Commands/CreateBrand/CreateBrandCommandHandler.cs @@ -30,7 +30,7 @@ public async Task Handle(CreateBrandCommand request, CancellationToken c } await _brandRepository.AddAsync(brand.Value, cancellationToken); - await _unitOfWork.SaveChangesAsync(cancellationToken); + await _unitOfWork.CommitAsync(cancellationToken); return Result.Success(); } diff --git a/crs/Services/Catalog/Catalog.Application/Brands/Commands/DeleteBrandById/DeleteBrandByIdCommandHandler.cs b/crs/Services/Catalog/Catalog.Application/Brands/Commands/DeleteBrandById/DeleteBrandByIdCommandHandler.cs index a0ef1a5..fee9952 100644 --- a/crs/Services/Catalog/Catalog.Application/Brands/Commands/DeleteBrandById/DeleteBrandByIdCommandHandler.cs +++ b/crs/Services/Catalog/Catalog.Application/Brands/Commands/DeleteBrandById/DeleteBrandByIdCommandHandler.cs @@ -13,7 +13,7 @@ public async Task Handle(DeleteBrandByIdCommand request, CancellationTok var brandId = new BrandId(request.Id); await _brandRepository.DeleteByIdAsync(brandId, cancellationToken); - await _unitOfWork.SaveChangesAsync(cancellationToken); + await _unitOfWork.CommitAsync(cancellationToken); return Result.Success(); } diff --git a/crs/Services/Catalog/Catalog.Application/Brands/Queries/GetBrandById/GetBrandByIdQueryHandler.cs b/crs/Services/Catalog/Catalog.Application/Brands/Queries/GetBrandById/GetBrandByIdQueryHandler.cs index 766c712..08fa68a 100644 --- a/crs/Services/Catalog/Catalog.Application/Brands/Queries/GetBrandById/GetBrandByIdQueryHandler.cs +++ b/crs/Services/Catalog/Catalog.Application/Brands/Queries/GetBrandById/GetBrandByIdQueryHandler.cs @@ -8,7 +8,7 @@ public async Task> Handle(GetBrandByIdQuery request, CancellationT { var brandId = new BrandId(request.Id); - var brand = await _brandRepository.GetByIdAsync(brandId); + var brand = await _brandRepository.GetByIdAsync(brandId, cancellationToken); return brand ?? Result.Failure( BrandErrors.BrandNotFound); diff --git a/crs/Services/Catalog/Catalog.Application/Catalog.Application.csproj b/crs/Services/Catalog/Catalog.Application/Catalog.Application.csproj index f253bc7..52eceaa 100644 --- a/crs/Services/Catalog/Catalog.Application/Catalog.Application.csproj +++ b/crs/Services/Catalog/Catalog.Application/Catalog.Application.csproj @@ -11,7 +11,6 @@ - @@ -21,7 +20,9 @@ + + diff --git a/crs/Services/Catalog/Catalog.Application/GlobalUsings.cs b/crs/Services/Catalog/Catalog.Application/GlobalUsings.cs index 50d7bbc..ce3c278 100644 --- a/crs/Services/Catalog/Catalog.Application/GlobalUsings.cs +++ b/crs/Services/Catalog/Catalog.Application/GlobalUsings.cs @@ -13,3 +13,8 @@ global using Common.Domain.Primitives; global using FluentValidation; global using System.Reflection; +global using Catalog.Domain.SellerAggregate.Repositories; +global using Catalog.Domain.SellerAggregate; +global using MediatR; +global using Idenitty.Grpc; +global using Catalog.Infrastructure.Grpc.Identity; \ No newline at end of file diff --git a/crs/Services/Catalog/Catalog.Application/Products/Queries/GetProductsByFilter/GetProductsByFilterQuerieHandler.cs b/crs/Services/Catalog/Catalog.Application/Products/Queries/GetProductsByFilter/GetProductsByFilterQuerieHandler.cs index d322b1e..9ec2eb5 100644 --- a/crs/Services/Catalog/Catalog.Application/Products/Queries/GetProductsByFilter/GetProductsByFilterQuerieHandler.cs +++ b/crs/Services/Catalog/Catalog.Application/Products/Queries/GetProductsByFilter/GetProductsByFilterQuerieHandler.cs @@ -1,7 +1,4 @@ -using Catalog.Domain.ProductAggregate; -using Catalog.Domain.ProductAggregate.Repositories; - -namespace Catalog.Application.Products.Queries.GetProductsByFilter; +namespace Catalog.Application.Products.Queries.GetProductsByFilter; public sealed class GetProductsByFilterQuerieHandler(IProductRepository productRepository) : IQueryHandler> { diff --git a/crs/Services/Catalog/Catalog.Application/Sellers/Commands/AddSeller/AddSellerCommand.cs b/crs/Services/Catalog/Catalog.Application/Sellers/Commands/AddSeller/AddSellerCommand.cs new file mode 100644 index 0000000..944025e --- /dev/null +++ b/crs/Services/Catalog/Catalog.Application/Sellers/Commands/AddSeller/AddSellerCommand.cs @@ -0,0 +1,3 @@ +namespace Catalog.Application.Sellers.Commands.AddSeller; + +public sealed record AddSellerCommand(Guid UserId) : ICommand; diff --git a/crs/Services/Catalog/Catalog.Application/Sellers/Commands/AddSeller/AddSellerCommandHandler.cs b/crs/Services/Catalog/Catalog.Application/Sellers/Commands/AddSeller/AddSellerCommandHandler.cs new file mode 100644 index 0000000..8615e41 --- /dev/null +++ b/crs/Services/Catalog/Catalog.Application/Sellers/Commands/AddSeller/AddSellerCommandHandler.cs @@ -0,0 +1,20 @@ +using Catalog.Application.Sellers.Commands.CreateSeller; + +namespace Catalog.Application.Sellers.Commands.AddSeller; + +internal sealed class AddSellerCommandHandler( + ISender sender, + IIdentityGrpcService identityGrpcService) + : ICommandHandler +{ + private readonly ISender _sender = sender; + private readonly IIdentityGrpcService _identityGrpcService = identityGrpcService; + + public async Task Handle(AddSellerCommand request, CancellationToken cancellationToken) + { + var userInfo = await _identityGrpcService.GetUserInfoAsync(request.UserId, cancellationToken); + + var command = new CreateSellerCommand(userInfo.Email, userInfo.Email); + return await _sender.Send(command, cancellationToken); + } +} diff --git a/crs/Services/Catalog/Catalog.Application/Sellers/Commands/CreateSeller/CreateSellerCommand.cs b/crs/Services/Catalog/Catalog.Application/Sellers/Commands/CreateSeller/CreateSellerCommand.cs new file mode 100644 index 0000000..40e291b --- /dev/null +++ b/crs/Services/Catalog/Catalog.Application/Sellers/Commands/CreateSeller/CreateSellerCommand.cs @@ -0,0 +1,6 @@ +namespace Catalog.Application.Sellers.Commands.CreateSeller; + +public sealed record CreateSellerCommand( + string SellerName, + string Email + ) : ICommand; diff --git a/crs/Services/Catalog/Catalog.Application/Sellers/Commands/CreateSeller/CreateSellerCommandHandler.cs b/crs/Services/Catalog/Catalog.Application/Sellers/Commands/CreateSeller/CreateSellerCommandHandler.cs new file mode 100644 index 0000000..c2ef436 --- /dev/null +++ b/crs/Services/Catalog/Catalog.Application/Sellers/Commands/CreateSeller/CreateSellerCommandHandler.cs @@ -0,0 +1,39 @@ +using Catalog.Domain.Common.ValueObjects; +using Catalog.Domain.SellerAggregate.Ids; +using Catalog.Domain.SellerAggregate.ValueObjects; + +namespace Catalog.Application.Sellers.Commands.CreateSeller; + +internal sealed class CreateSellerCommandHandler( + ISellerRepository sellerRepository, + IUnitOfWork unitOfWork) + : ICommandHandler +{ + private readonly ISellerRepository _sellerRepository = sellerRepository; + private readonly IUnitOfWork _unitOfWork = unitOfWork; + + public async Task Handle(CreateSellerCommand request, CancellationToken cancellationToken) + { + var sellerId = new SellerId(Guid.NewGuid()); + var sellerNameResult = SellerName.Create(request.SellerName); + var emailResult = Email.Create(request.Email); + var isSellerNameExist = await _sellerRepository.IsSellerNameExist(sellerNameResult.Value, cancellationToken); + + var sellerResult = Seller.Create( + sellerId, + sellerNameResult.Value, + emailResult.Value, + isSellerNameExist + ); + + if (sellerResult.IsFailure) + { + return Result.Failure(sellerResult.Error); + } + + await _sellerRepository.AddAsync(sellerResult.Value, cancellationToken); + await _unitOfWork.CommitAsync(cancellationToken); + + return Result.Success(); + } +} diff --git a/crs/Services/Catalog/Catalog.Application/Sellers/Commands/CreateSeller/CreateSellerValidator.cs b/crs/Services/Catalog/Catalog.Application/Sellers/Commands/CreateSeller/CreateSellerValidator.cs new file mode 100644 index 0000000..719c241 --- /dev/null +++ b/crs/Services/Catalog/Catalog.Application/Sellers/Commands/CreateSeller/CreateSellerValidator.cs @@ -0,0 +1,9 @@ +namespace Catalog.Application.Sellers.Commands.CreateSeller; + +internal sealed class CreateSellerValidator : AbstractValidator +{ + public CreateSellerValidator() + { + + } +} diff --git a/crs/Services/Catalog/Catalog.Domain/Common/Errors/ImageUrlErrors.cs b/crs/Services/Catalog/Catalog.Domain/Common/Errors/ImageUrlErrors.cs index 6257272..bc46f45 100644 --- a/crs/Services/Catalog/Catalog.Domain/Common/Errors/ImageUrlErrors.cs +++ b/crs/Services/Catalog/Catalog.Domain/Common/Errors/ImageUrlErrors.cs @@ -3,8 +3,8 @@ public static class ImageUrlErrors { public static Error CannotByEmpty => - new Error("ImageUrl.CannotByEmpty", "Image url cannot be empty"); + new("ImageUrl.CannotByEmpty", "Image url cannot be empty"); public static Error IsInvalid => - new Error("ImageUrl.IsInvalid", "Image url is invalid"); + new("ImageUrl.IsInvalid", "Image url is invalid"); } \ No newline at end of file diff --git a/crs/Services/Catalog/Catalog.Domain/Common/Errors/MoneyErrors.cs b/crs/Services/Catalog/Catalog.Domain/Common/Errors/MoneyErrors.cs index 5375b21..5a92673 100644 --- a/crs/Services/Catalog/Catalog.Domain/Common/Errors/MoneyErrors.cs +++ b/crs/Services/Catalog/Catalog.Domain/Common/Errors/MoneyErrors.cs @@ -3,8 +3,8 @@ public static class MoneyErrors { public static Error CannotBeNegative => - new Error("Money.CannotBeNegative", "Money cannot be negative"); + new("Money.CannotBeNegative", "Money cannot be negative"); public static Error CannotBeEmpty => - new Error("Money.CannotBeEmpty", "Money cannot be empty"); + new("Money.CannotBeEmpty", "Money cannot be empty"); } \ No newline at end of file diff --git a/crs/Services/Catalog/Catalog.Domain/Common/Regexes/EmailRegex.cs b/crs/Services/Catalog/Catalog.Domain/Common/Regexes/EmailRegex.cs new file mode 100644 index 0000000..a0d518e --- /dev/null +++ b/crs/Services/Catalog/Catalog.Domain/Common/Regexes/EmailRegex.cs @@ -0,0 +1,9 @@ +namespace Catalog.Domain.Common.Regexes; + +public partial class EmailRegex +{ + private const string EmailPattern = @"^(.+)@(.+)$"; + + [GeneratedRegex(EmailPattern)] + public static partial Regex Regex(); +} diff --git a/crs/Services/Catalog/Catalog.Domain/Common/Regexes/ImageUrlRegex.cs b/crs/Services/Catalog/Catalog.Domain/Common/Regexes/ImageUrlRegex.cs new file mode 100644 index 0000000..29f0eae --- /dev/null +++ b/crs/Services/Catalog/Catalog.Domain/Common/Regexes/ImageUrlRegex.cs @@ -0,0 +1,9 @@ +namespace Catalog.Domain.Common.Regexes; + +public partial class ImageUrlRegex +{ + private const string ImageUrlPattern = @"^(http(s?):)([/|.|\w|\s|-])*\.(?:jpg|gif|png)$"; + + [GeneratedRegex(ImageUrlPattern)] + public static partial Regex Regex(); +} diff --git a/crs/Services/Catalog/Catalog.Domain/Common/Repositories/ICatalogRepository.cs b/crs/Services/Catalog/Catalog.Domain/Common/Repositories/ICatalogRepository.cs index 2769514..ccb1f7b 100644 --- a/crs/Services/Catalog/Catalog.Domain/Common/Repositories/ICatalogRepository.cs +++ b/crs/Services/Catalog/Catalog.Domain/Common/Repositories/ICatalogRepository.cs @@ -11,7 +11,7 @@ public interface ICatalogRepository Task> GetAllAsync(CancellationToken cancellationToken = default); Task> GetPagedAsync(int skip, int take, CancellationToken cancellationToken = default); Task CountAsync(CancellationToken cancellationToken = default); - Task GetByIdAsync(TStrongestId id, CancellationToken cancellationToken = default); + Task GetByIdAsync(TStrongestId id, CancellationToken cancellationToken = default); Task ExistsAsync(TStrongestId id, CancellationToken cancellationToken = default); Task DeleteByIdAsync(TStrongestId id, CancellationToken cancellationToken = default); } diff --git a/crs/Services/Catalog/Catalog.Domain/Common/ValueObjects/Email.cs b/crs/Services/Catalog/Catalog.Domain/Common/ValueObjects/Email.cs index dde06e7..6249480 100644 --- a/crs/Services/Catalog/Catalog.Domain/Common/ValueObjects/Email.cs +++ b/crs/Services/Catalog/Catalog.Domain/Common/ValueObjects/Email.cs @@ -1,8 +1,9 @@ -namespace Catalog.Domain.Common.ValueObjects; +using Catalog.Domain.Common.Regexes; -public sealed class Email : ValueObject +namespace Catalog.Domain.Common.ValueObjects; + +public sealed partial class Email : ValueObject { - private const string EmailPattern = @"^(.+)@(.+)$"; public const int EmailMaxLength = 100; public string Value { get; private set; } @@ -39,8 +40,8 @@ public override IEnumerable GetEqualityComponents() yield return Value; } - public static bool IsEmail(string email) => Regex.IsMatch(email, EmailPattern); - + public static bool IsEmail(string email) => + EmailRegex.Regex().IsMatch(email); public static implicit operator string(Email email) => email.Value; } \ No newline at end of file diff --git a/crs/Services/Catalog/Catalog.Domain/Common/ValueObjects/ImageUrl.cs b/crs/Services/Catalog/Catalog.Domain/Common/ValueObjects/ImageUrl.cs index 7524e0d..714aa0b 100644 --- a/crs/Services/Catalog/Catalog.Domain/Common/ValueObjects/ImageUrl.cs +++ b/crs/Services/Catalog/Catalog.Domain/Common/ValueObjects/ImageUrl.cs @@ -1,10 +1,11 @@ -namespace Catalog.Domain.Common.ValueObjects; +using Catalog.Domain.Common.Regexes; -public sealed class ImageUrl : ValueObject +namespace Catalog.Domain.Common.ValueObjects; + +public sealed partial class ImageUrl : ValueObject { public string Value { get; private set; } - private const string ImageUrlPattern = @"^(http(s?):)([/|.|\w|\s|-])*\.(?:jpg|gif|png)$"; private ImageUrl(string value) => Value = value; @@ -28,7 +29,8 @@ public static Result Create(string imageUrl) return new ImageUrl(imageUrl); } - public static bool IsImageUrl(string imageUrl) => Regex.IsMatch(imageUrl, ImageUrlPattern); + public static bool IsImageUrl(string imageUrl) => + ImageUrlRegex.Regex().IsMatch(imageUrl); public override IEnumerable GetEqualityComponents() { @@ -36,4 +38,5 @@ public override IEnumerable GetEqualityComponents() } public static implicit operator string(ImageUrl imageUrl) => imageUrl.Value; + } \ No newline at end of file diff --git a/crs/Services/Catalog/Catalog.Domain/ProductAggregate/Errors/QuantityErrors.cs b/crs/Services/Catalog/Catalog.Domain/ProductAggregate/Errors/QuantityErrors.cs new file mode 100644 index 0000000..59a9c93 --- /dev/null +++ b/crs/Services/Catalog/Catalog.Domain/ProductAggregate/Errors/QuantityErrors.cs @@ -0,0 +1,7 @@ +namespace Catalog.Domain.ProductAggregate.Errors; + +public static class QuantityErrors +{ + public static Error QuantityMustBeGreaterThanZero => + new("Product.QuantityMustBeGreaterThanZero", "Quantity must be greater than zero"); +} diff --git a/crs/Services/Catalog/Catalog.Domain/ProductAggregate/Product.cs b/crs/Services/Catalog/Catalog.Domain/ProductAggregate/Product.cs index 65c0f39..989e2fc 100644 --- a/crs/Services/Catalog/Catalog.Domain/ProductAggregate/Product.cs +++ b/crs/Services/Catalog/Catalog.Domain/ProductAggregate/Product.cs @@ -10,6 +10,7 @@ public class Product : AggregateRoot public Brand Brand { get; private set; } public ImageUrl ProductImage { get; private set; } public ProductDescription Description { get; private set; } + public Quantity Quantity { get; private set; } #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. private Product() { } @@ -24,7 +25,8 @@ private Product( Seller seller, Brand brand, ImageUrl productImage, - ProductDescription description) + ProductDescription description, + Quantity quantity) : base(id) { Sku = sku; @@ -35,6 +37,7 @@ private Product( Brand = brand; ProductImage = productImage; Description = description; + Quantity = quantity; } public static Result Create( @@ -46,7 +49,8 @@ public static Result Create( Seller seller, Brand brand, ImageUrl productImage, - ProductDescription description) + ProductDescription description, + Quantity quantity) { var product = new Product( id, @@ -57,7 +61,8 @@ public static Result Create( seller, brand, productImage, - description); + description, + quantity); product.AddDomainEvent( new ProductCreatedDomainEvent(Guid.NewGuid(), id)); diff --git a/crs/Services/Catalog/Catalog.Domain/ProductAggregate/ValueObjects/Quantity.cs b/crs/Services/Catalog/Catalog.Domain/ProductAggregate/ValueObjects/Quantity.cs new file mode 100644 index 0000000..b3812f7 --- /dev/null +++ b/crs/Services/Catalog/Catalog.Domain/ProductAggregate/ValueObjects/Quantity.cs @@ -0,0 +1,27 @@ + +namespace Catalog.Domain.ProductAggregate.ValueObjects; + +public sealed class Quantity : ValueObject +{ + public int Value { get; private set; } + + private Quantity(int value) => Value = value; + + public static Result Create(int value) + { + if (value < 0) + { + return Result.Failure( + QuantityErrors.QuantityMustBeGreaterThanZero); + } + + return new Quantity(value); + } + + public static Quantity Empty => new(0); + + public override IEnumerable GetEqualityComponents() + { + yield return Value; + } +} \ No newline at end of file diff --git a/crs/Services/Catalog/Catalog.Domain/SellerAggregate/Repositories/ISellerRepository.cs b/crs/Services/Catalog/Catalog.Domain/SellerAggregate/Repositories/ISellerRepository.cs index 5f0e99c..0caa44d 100644 --- a/crs/Services/Catalog/Catalog.Domain/SellerAggregate/Repositories/ISellerRepository.cs +++ b/crs/Services/Catalog/Catalog.Domain/SellerAggregate/Repositories/ISellerRepository.cs @@ -3,4 +3,5 @@ public interface ISellerRepository : ICatalogRepository { Task GetSellerByNameAsync(SellerName name, CancellationToken cancellationToken = default); + Task IsSellerNameExist(SellerName name, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/crs/Services/Catalog/Catalog.Domain/SellerAggregate/Seller.cs b/crs/Services/Catalog/Catalog.Domain/SellerAggregate/Seller.cs index 877c2b1..80ab460 100644 --- a/crs/Services/Catalog/Catalog.Domain/SellerAggregate/Seller.cs +++ b/crs/Services/Catalog/Catalog.Domain/SellerAggregate/Seller.cs @@ -26,16 +26,15 @@ public static Result Create( SellerId id, SellerName sellerName, Email email, - List products, - bool isSellerNameUnique) + bool isSellerNameExist) { - if (!isSellerNameUnique) + if (isSellerNameExist) { return Result.Failure( SellerErrors.SellerNameIsNotUnique(sellerName.Value)); } - var seller = new Seller(id, sellerName, email, products); + var seller = new Seller(id, sellerName, email, products: []); seller.AddDomainEvent( new SellerCreatedDomainEvent(Guid.NewGuid(), id)); diff --git a/crs/Services/Catalog/Catalog.Infrastructure/Catalog.Infrastructure.csproj b/crs/Services/Catalog/Catalog.Infrastructure/Catalog.Infrastructure.csproj index 61ea681..bb788ae 100644 --- a/crs/Services/Catalog/Catalog.Infrastructure/Catalog.Infrastructure.csproj +++ b/crs/Services/Catalog/Catalog.Infrastructure/Catalog.Infrastructure.csproj @@ -14,6 +14,7 @@ + diff --git a/crs/Services/Catalog/Catalog.Infrastructure/GlobalUsings.cs b/crs/Services/Catalog/Catalog.Infrastructure/GlobalUsings.cs index a8922c8..939cdd2 100644 --- a/crs/Services/Catalog/Catalog.Infrastructure/GlobalUsings.cs +++ b/crs/Services/Catalog/Catalog.Infrastructure/GlobalUsings.cs @@ -7,3 +7,4 @@ global using Common.Domain.Primitives; global using Common.Domain.Primitives.Events; global using System.Reflection; +global using Identity.Protobuf; \ No newline at end of file diff --git a/crs/Services/Catalog/Catalog.Infrastructure/Grpc/Identity/IIdentityGrpcService.cs b/crs/Services/Catalog/Catalog.Infrastructure/Grpc/Identity/IIdentityGrpcService.cs new file mode 100644 index 0000000..3ea0b7a --- /dev/null +++ b/crs/Services/Catalog/Catalog.Infrastructure/Grpc/Identity/IIdentityGrpcService.cs @@ -0,0 +1,6 @@ +namespace Catalog.Infrastructure.Grpc.Identity; + +public interface IIdentityGrpcService +{ + public Task GetUserInfoAsync(Guid id, CancellationToken cancellationToken = default); +} diff --git a/crs/Services/Catalog/Catalog.Infrastructure/Grpc/Identity/IdentityGrpcService.cs b/crs/Services/Catalog/Catalog.Infrastructure/Grpc/Identity/IdentityGrpcService.cs new file mode 100644 index 0000000..9c2e938 --- /dev/null +++ b/crs/Services/Catalog/Catalog.Infrastructure/Grpc/Identity/IdentityGrpcService.cs @@ -0,0 +1,14 @@ +using static Identity.Protobuf.IdentityService; + +namespace Catalog.Infrastructure.Grpc.Identity; + +internal sealed class IdentityGrpcService(IdentityServiceClient client) : IIdentityGrpcService +{ + private readonly IdentityServiceClient _client = client; + + public async Task GetUserInfoAsync(Guid id, CancellationToken cancellationToken = default) + { + var request = new GetUserRequest() { Id = id.ToString() }; + return await _client.GetUserInfoAsync(request, cancellationToken: cancellationToken); + } +} diff --git a/crs/Services/Catalog/Catalog.MessageBus/AssemblyReference.cs b/crs/Services/Catalog/Catalog.MessageBus/AssemblyReference.cs new file mode 100644 index 0000000..d90aec2 --- /dev/null +++ b/crs/Services/Catalog/Catalog.MessageBus/AssemblyReference.cs @@ -0,0 +1,6 @@ +namespace Catalog.MessageBus; + +public static class AssemblyReference +{ + public static Assembly Assembly => typeof(AssemblyReference).Assembly; +} diff --git a/crs/Services/Catalog/Catalog.MessageBus/Catalog.MessageBus.csproj b/crs/Services/Catalog/Catalog.MessageBus/Catalog.MessageBus.csproj new file mode 100644 index 0000000..36760dc --- /dev/null +++ b/crs/Services/Catalog/Catalog.MessageBus/Catalog.MessageBus.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + diff --git a/crs/Services/Catalog/Catalog.MessageBus/GlobalUsings.cs b/crs/Services/Catalog/Catalog.MessageBus/GlobalUsings.cs new file mode 100644 index 0000000..8b0c916 --- /dev/null +++ b/crs/Services/Catalog/Catalog.MessageBus/GlobalUsings.cs @@ -0,0 +1,5 @@ +global using Contracts.Services.Identity.Events; +global using EventBus.MassTransit.Handlers; +global using MassTransit; +global using MediatR; +global using System.Reflection; diff --git a/crs/Services/Catalog/Catalog.MessageBus/Handlers/Events/IdentityVerificationConfirmedEventHandler.cs b/crs/Services/Catalog/Catalog.MessageBus/Handlers/Events/IdentityVerificationConfirmedEventHandler.cs new file mode 100644 index 0000000..61c6d5f --- /dev/null +++ b/crs/Services/Catalog/Catalog.MessageBus/Handlers/Events/IdentityVerificationConfirmedEventHandler.cs @@ -0,0 +1,16 @@ +using Catalog.Application.Sellers.Commands.AddSeller; + +namespace Catalog.MessageBus.Handlers.Events; + +internal sealed class IdentityVerificationConfirmedEventHandler(ISender sender) + : IntegrationEventHandler +{ + private readonly ISender _sender = sender; + + public override async Task Handle(ConsumeContext context) + { + var command = new AddSellerCommand(context.Message.UserId); + + await _sender.Send(command); + } +} diff --git a/crs/Services/Catalog/Catalog.Persistence/Configurations/ProductConfiguration.cs b/crs/Services/Catalog/Catalog.Persistence/Configurations/ProductConfiguration.cs index e17d074..e40ba12 100644 --- a/crs/Services/Catalog/Catalog.Persistence/Configurations/ProductConfiguration.cs +++ b/crs/Services/Catalog/Catalog.Persistence/Configurations/ProductConfiguration.cs @@ -7,6 +7,8 @@ public void Configure(EntityTypeBuilder builder) builder.ToTable(nameof(Product)); builder.HasKey(p => p.Id); + builder.HasAlternateKey(p => p.Sku); + builder.Property(p => p.Id).HasConversion( productId => productId.Value, value => new ProductId(value)).IsRequired(); @@ -44,6 +46,13 @@ public void Configure(EntityTypeBuilder builder) .HasMaxLength(ProductName.ProductNameMaxLength) .IsRequired(); + builder.Property(p => p.Quantity) + .HasConversion( + productQuantity => productQuantity.Value, + value => Quantity.Create(value).Value + ) + .IsRequired(); + builder.HasOne(p => p.Category) .WithMany(c => c.Products) .OnDelete(DeleteBehavior.Cascade) diff --git a/crs/Services/Catalog/Catalog.Persistence/Factories/Interfaces/ISqlConnectionFactory.cs b/crs/Services/Catalog/Catalog.Persistence/Factories/Abstractions/ISqlConnectionFactory.cs similarity index 61% rename from crs/Services/Catalog/Catalog.Persistence/Factories/Interfaces/ISqlConnectionFactory.cs rename to crs/Services/Catalog/Catalog.Persistence/Factories/Abstractions/ISqlConnectionFactory.cs index 91700ec..0fd7d19 100644 --- a/crs/Services/Catalog/Catalog.Persistence/Factories/Interfaces/ISqlConnectionFactory.cs +++ b/crs/Services/Catalog/Catalog.Persistence/Factories/Abstractions/ISqlConnectionFactory.cs @@ -1,4 +1,4 @@ -namespace Catalog.Persistence.Factories.Interfaces; +namespace Catalog.Persistence.Factories.Abstractions; internal interface ISqlConnectionFactory { diff --git a/crs/Services/Catalog/Catalog.Persistence/GlobalUsings.cs b/crs/Services/Catalog/Catalog.Persistence/GlobalUsings.cs index 8cdda99..65402aa 100644 --- a/crs/Services/Catalog/Catalog.Persistence/GlobalUsings.cs +++ b/crs/Services/Catalog/Catalog.Persistence/GlobalUsings.cs @@ -17,8 +17,8 @@ global using Catalog.Domain.CategoryAggregate.Repositories; global using Catalog.Domain.CategoryAggregate.ValueObjects; global using Catalog.Domain.SellerAggregate.ValueObjects; -global using Catalog.Persistence.Factories.Interfaces; -global using Catalog.Persistence.Services.Interfaces; +global using Catalog.Persistence.Factories.Abstractions; +global using Catalog.Persistence.Services.Abstractions; global using Dapper; global using Microsoft.EntityFrameworkCore; global using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/crs/Services/Catalog/Catalog.Persistence/Repositories/BrandRepository.cs b/crs/Services/Catalog/Catalog.Persistence/Repositories/BrandRepository.cs index 9bf683b..a77795f 100644 --- a/crs/Services/Catalog/Catalog.Persistence/Repositories/BrandRepository.cs +++ b/crs/Services/Catalog/Catalog.Persistence/Repositories/BrandRepository.cs @@ -3,7 +3,7 @@ internal sealed class BrandRepository( CatalogDbContext dbContext, ISqlConnectionFactory sqlConnectionFactory, - ICachedCatalogService cached) + ICachedEntityService cached) : CatalogBaseRepository( dbContext, sqlConnectionFactory, diff --git a/crs/Services/Catalog/Catalog.Persistence/Repositories/CatalogBaseRepository.cs b/crs/Services/Catalog/Catalog.Persistence/Repositories/CatalogBaseRepository.cs index d1e1474..cdd3dfa 100644 --- a/crs/Services/Catalog/Catalog.Persistence/Repositories/CatalogBaseRepository.cs +++ b/crs/Services/Catalog/Catalog.Persistence/Repositories/CatalogBaseRepository.cs @@ -8,13 +8,13 @@ internal abstract class CatalogBaseRepository protected readonly string _entityName; protected readonly CatalogDbContext _dbContext; protected readonly ISqlConnectionFactory _sqlConnectionFactory; - protected readonly ICachedCatalogService _cached; + protected readonly ICachedEntityService _cached; protected readonly TimeSpan _expirationTime; protected CatalogBaseRepository( CatalogDbContext dbContext, ISqlConnectionFactory sqlConnectionFactory, - ICachedCatalogService cached, + ICachedEntityService cached, TimeSpan expirationTime) { _entityName = typeof(TEntity).Name; @@ -42,10 +42,7 @@ public async Task DeleteAsync(TEntity entity, CancellationToken cancellationToke public async Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default) { - _dbContext - .Set() - .Update(entity); - + _dbContext.Update(entity); await _cached.RefreshAsync(entity.Id, cancellationToken); } @@ -94,7 +91,7 @@ public async Task CountAsync(CancellationToken cancellationToken = default) return await sqlConnection.ExecuteScalarAsync(query, cancellationToken); } - public async Task GetByIdAsync(TStrongestId id, CancellationToken cancellationToken = default) + public async Task GetByIdAsync(TStrongestId id, CancellationToken cancellationToken = default) { var entity = await _cached.GetAsync(id, cancellationToken); @@ -128,6 +125,8 @@ public async Task ExistsAsync(TStrongestId id, CancellationToken cancellat public async Task DeleteByIdAsync(TStrongestId id, CancellationToken cancellationToken = default) { + await _cached.DeleteAsync(id, cancellationToken); + await _dbContext .Set() .Where(entity => entity.Id == id) diff --git a/crs/Services/Catalog/Catalog.Persistence/Repositories/CategoryRepository.cs b/crs/Services/Catalog/Catalog.Persistence/Repositories/CategoryRepository.cs index 21f0080..f2368cc 100644 --- a/crs/Services/Catalog/Catalog.Persistence/Repositories/CategoryRepository.cs +++ b/crs/Services/Catalog/Catalog.Persistence/Repositories/CategoryRepository.cs @@ -3,7 +3,7 @@ internal sealed class CategoryRepository( CatalogDbContext dbContext, ISqlConnectionFactory sqlConnectionFactory, - ICachedCatalogService cached) + ICachedEntityService cached) : CatalogBaseRepository( dbContext, sqlConnectionFactory, diff --git a/crs/Services/Catalog/Catalog.Persistence/Repositories/ProductRepository.cs b/crs/Services/Catalog/Catalog.Persistence/Repositories/ProductRepository.cs index 7bd2c69..c583bab 100644 --- a/crs/Services/Catalog/Catalog.Persistence/Repositories/ProductRepository.cs +++ b/crs/Services/Catalog/Catalog.Persistence/Repositories/ProductRepository.cs @@ -3,7 +3,7 @@ internal sealed class ProductRepository( CatalogDbContext dbContext, ISqlConnectionFactory sqlConnectionFactory, - ICachedCatalogService cached) + ICachedEntityService cached) : CatalogBaseRepository( dbContext, sqlConnectionFactory, diff --git a/crs/Services/Catalog/Catalog.Persistence/Repositories/SellerRepository.cs b/crs/Services/Catalog/Catalog.Persistence/Repositories/SellerRepository.cs index 088cc3b..5df9741 100644 --- a/crs/Services/Catalog/Catalog.Persistence/Repositories/SellerRepository.cs +++ b/crs/Services/Catalog/Catalog.Persistence/Repositories/SellerRepository.cs @@ -3,7 +3,7 @@ internal sealed class SellerRepository( CatalogDbContext dbContext, ISqlConnectionFactory sqlConnectionFactory, - ICachedCatalogService cached) + ICachedEntityService cached) : CatalogBaseRepository( dbContext, sqlConnectionFactory, @@ -37,4 +37,22 @@ await _cached return entity; } + + public async Task IsSellerNameExist(SellerName name, CancellationToken cancellationToken = default) + { + using var sqlConnection = _sqlConnectionFactory.GetOpenConnection(); + + string query = + $""" + SELECT 1 FROM {_entityName} + WHERE [Name] = @SellerName + """; + + var parameters = new { SellerName = name.Value }; + + var result = await sqlConnection + .QueryFirstOrDefaultAsync(query, cancellationToken); + + return result; + } } diff --git a/crs/Services/Catalog/Catalog.Persistence/Services/Interfaces/ICachedCatalogService.cs b/crs/Services/Catalog/Catalog.Persistence/Services/Abstractions/ICachedEntityService.cs similarity index 83% rename from crs/Services/Catalog/Catalog.Persistence/Services/Interfaces/ICachedCatalogService.cs rename to crs/Services/Catalog/Catalog.Persistence/Services/Abstractions/ICachedEntityService.cs index ddc75fd..b3fa650 100644 --- a/crs/Services/Catalog/Catalog.Persistence/Services/Interfaces/ICachedCatalogService.cs +++ b/crs/Services/Catalog/Catalog.Persistence/Services/Abstractions/ICachedEntityService.cs @@ -1,6 +1,6 @@ -namespace Catalog.Persistence.Services.Interfaces; +namespace Catalog.Persistence.Services.Abstractions; -internal interface ICachedCatalogService +internal interface ICachedEntityService where TEntity : Entity where TStrongestId : class, IStrongestId { diff --git a/crs/Services/Catalog/Catalog.Persistence/Services/Interfaces/ICachedService.cs b/crs/Services/Catalog/Catalog.Persistence/Services/Abstractions/ICachedService.cs similarity index 80% rename from crs/Services/Catalog/Catalog.Persistence/Services/Interfaces/ICachedService.cs rename to crs/Services/Catalog/Catalog.Persistence/Services/Abstractions/ICachedService.cs index 42750ac..b8682f2 100644 --- a/crs/Services/Catalog/Catalog.Persistence/Services/Interfaces/ICachedService.cs +++ b/crs/Services/Catalog/Catalog.Persistence/Services/Abstractions/ICachedService.cs @@ -1,6 +1,6 @@ -namespace Catalog.Persistence.Services.Interfaces; +namespace Catalog.Persistence.Services.Abstractions; -public interface ICachedBaseService +public interface ICachedService { Task DeleteAsync(string key, CancellationToken cancellationToken = default); Task GetAsync(string key, CancellationToken cancellationToken = default); diff --git a/crs/Services/Catalog/Catalog.Persistence/Services/CachedCatalogService.cs b/crs/Services/Catalog/Catalog.Persistence/Services/CachedCatalogService.cs deleted file mode 100644 index 9a21b91..0000000 --- a/crs/Services/Catalog/Catalog.Persistence/Services/CachedCatalogService.cs +++ /dev/null @@ -1,63 +0,0 @@ -namespace Catalog.Persistence.Services; - -internal sealed class CachedCatalogService(ICachedBaseService cachedBase) - : ICachedCatalogService - where TEntity : Entity - where TStrongestId : class, IStrongestId -{ - private readonly string _entityName = typeof(TEntity).Name; - private readonly ICachedBaseService _cachedBase = cachedBase; - - private string GetKey(TStrongestId id) => - $"{_entityName} - {id.Value}"; - - private string GetKey(TEntity entity) => - GetKey(entity.Id); - - public async Task GetAsync( - TStrongestId id, - CancellationToken cancellationToken = default) - { - return await _cachedBase - .GetAsync(GetKey(id)); - } - - public async Task SetAsync( - TEntity entity, - TimeSpan expirationDate = default, - CancellationToken cancellationToken = default) - { - await _cachedBase.SetAsync( - GetKey(entity), - entity, - expirationDate, - cancellationToken); - } - - public async Task RefreshAsync( - TStrongestId id, - CancellationToken cancellationToken = default) - { - await _cachedBase - .RefreshAsync(GetKey(id), cancellationToken); - } - - public async Task DeleteAsync( - TStrongestId id, - CancellationToken cancellationToken = default) - { - await _cachedBase - .DeleteAsync(GetKey(id), cancellationToken); - } - - public async Task SetAsync( - IEnumerable entities, - TimeSpan expirationDate = default, - CancellationToken cancellationToken = default) - { - foreach (var entity in entities) - { - await SetAsync(entity, expirationDate, cancellationToken); - } - } -} diff --git a/crs/Services/Catalog/Catalog.Persistence/Services/CachedEntityService.cs b/crs/Services/Catalog/Catalog.Persistence/Services/CachedEntityService.cs new file mode 100644 index 0000000..bd9a945 --- /dev/null +++ b/crs/Services/Catalog/Catalog.Persistence/Services/CachedEntityService.cs @@ -0,0 +1,46 @@ +namespace Catalog.Persistence.Services; + +internal sealed class CachedEntityService(ICachedService cachedBase) + : ICachedEntityService + where TEntity : Entity + where TStrongestId : class, IStrongestId +{ + private readonly string _entityName = typeof(TEntity).Name; + private readonly ICachedService _cachedBase = cachedBase; + + private string GetKey(TStrongestId id) => + $"{_entityName}-{id.Value}"; + + private string GetKey(TEntity entity) => + GetKey(entity.Id); + + public async Task GetAsync(TStrongestId id, CancellationToken cancellationToken = default) => + await _cachedBase.GetAsync(GetKey(id)); + + public async Task SetAsync( + TEntity entity, + TimeSpan expirationDate = default, + CancellationToken cancellationToken = default) => + await _cachedBase.SetAsync( + GetKey(entity), + entity, + expirationDate, + cancellationToken); + + public async Task RefreshAsync(TStrongestId id, CancellationToken cancellationToken = default) => + await _cachedBase.RefreshAsync(GetKey(id), cancellationToken); + + public async Task DeleteAsync(TStrongestId id, CancellationToken cancellationToken = default) => + await _cachedBase.DeleteAsync(GetKey(id), cancellationToken); + + public async Task SetAsync( + IEnumerable entities, + TimeSpan expirationDate = default, + CancellationToken cancellationToken = default) + { + foreach (var entity in entities) + { + await SetAsync(entity, expirationDate, cancellationToken); + } + } +} diff --git a/crs/Services/Catalog/Catalog.Persistence/Services/CachedBaseService.cs b/crs/Services/Catalog/Catalog.Persistence/Services/CachedService.cs similarity index 64% rename from crs/Services/Catalog/Catalog.Persistence/Services/CachedBaseService.cs rename to crs/Services/Catalog/Catalog.Persistence/Services/CachedService.cs index 90362a6..dcea1dc 100644 --- a/crs/Services/Catalog/Catalog.Persistence/Services/CachedBaseService.cs +++ b/crs/Services/Catalog/Catalog.Persistence/Services/CachedService.cs @@ -1,12 +1,15 @@ namespace Catalog.Persistence.Services; -internal class CachedBaseService(IDistributedCache cache) : ICachedBaseService +internal class CachedService(IDistributedCache cache) : ICachedService { protected readonly IDistributedCache _cache = cache; - protected DistributedCacheEntryOptions GetOptions(TimeSpan expirationTime = default) => - new DistributedCacheEntryOptions { AbsoluteExpiration = expirationTime == default ? - null : DateTime.Now.Add(expirationTime) }; + protected static DistributedCacheEntryOptions GetOptions(TimeSpan expirationTime = default) => + new() + { + AbsoluteExpiration = expirationTime == default ? + null : DateTime.Now.Add(expirationTime) + }; public async Task GetAsync( string key, @@ -34,17 +37,9 @@ public async Task SetAsync( await _cache.SetStringAsync(key, serializeObject, options, cancellationToken); } - public async Task RefreshAsync( - string key, - CancellationToken cancellationToken = default) - { + public async Task RefreshAsync(string key, CancellationToken cancellationToken = default) => await _cache.RefreshAsync(key, cancellationToken); - } - public async Task DeleteAsync( - string key, - CancellationToken cancellationToken = default) - { + public async Task DeleteAsync(string key, CancellationToken cancellationToken = default) => await _cache.RemoveAsync(key, cancellationToken); - } } diff --git a/crs/Services/Catalog/Catalog.Persistence/UnitOfWork.cs b/crs/Services/Catalog/Catalog.Persistence/UnitOfWork.cs index 10ca3a8..b9e1462 100644 --- a/crs/Services/Catalog/Catalog.Persistence/UnitOfWork.cs +++ b/crs/Services/Catalog/Catalog.Persistence/UnitOfWork.cs @@ -4,13 +4,13 @@ internal sealed class UnitOfWork(CatalogDbContext dbContext) : IUnitOfWork { private readonly CatalogDbContext _dbContext = dbContext; - public int SaveChanges() + public int Commit() { SendDomainEventsToOutboxMessagesAsync().GetResult(); return _dbContext.SaveChanges(); } - public async Task SaveChangesAsync(CancellationToken cancellationToken = default) + public async Task CommitAsync(CancellationToken cancellationToken = default) { await SendDomainEventsToOutboxMessagesAsync(cancellationToken); return await _dbContext.SaveChangesAsync(cancellationToken); diff --git a/crs/Services/Email/Email.App/Configurations/DocumentationServiceInstaller.cs b/crs/Services/Email/Email.App/Configurations/DocumentationServiceInstaller.cs deleted file mode 100644 index 7269395..0000000 --- a/crs/Services/Email/Email.App/Configurations/DocumentationServiceInstaller.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Email.App.Configurations; - -internal sealed class DocumentationServiceInstaller : IServiceInstaller -{ - public void Install(IServiceCollection services, IConfiguration configuration) - { - services.AddMediatR(configuration => configuration - .RegisterServicesFromAssembly(Application.AssemblyReference.Assembly) - .AddOpenBehavior(typeof(LoggingPipelineBehavior<,>)) - .AddOpenBehavior(typeof(ValidationPipelineBehavior<,>)) - ); - - services.AddValidatorsFromAssembly( - Application.AssemblyReference.Assembly, - includeInternalTypes: true); - } -} diff --git a/crs/Services/Email/Email.App/Configurations/GrpcServiceInstaller.cs b/crs/Services/Email/Email.App/Configurations/GrpcServiceInstaller.cs index d989682..5859474 100644 --- a/crs/Services/Email/Email.App/Configurations/GrpcServiceInstaller.cs +++ b/crs/Services/Email/Email.App/Configurations/GrpcServiceInstaller.cs @@ -1,12 +1,14 @@ -namespace Email.App.Configurations; +using Identity.Protobuf; + +namespace Email.App.Configurations; internal sealed class GrpcServiceInstaller : IServiceInstaller { public void Install(IServiceCollection services, IConfiguration configuration) { - services.AddGrpcClient(options => + services.AddGrpcClient(options => { - options.Address = new Uri(Env.IDENTITY_URL); + options.Address = new Uri(Env.IDENTITY_GRPC_URL); }); services.AddGrpc(); diff --git a/crs/Services/Email/Email.App/Configurations/InfrastructureServiceInstaller.cs b/crs/Services/Email/Email.App/Configurations/InfrastructureServiceInstaller.cs index 6ef9fae..ccbbf63 100644 --- a/crs/Services/Email/Email.App/Configurations/InfrastructureServiceInstaller.cs +++ b/crs/Services/Email/Email.App/Configurations/InfrastructureServiceInstaller.cs @@ -6,10 +6,14 @@ public void Install(IServiceCollection services, IConfiguration configuration) { services.Scan(services => services .FromAssemblies(Infrastructure.AssemblyReference.Assembly) - .AddClasses(false) + .AddClasses(classes => classes + .Where(type => !type.Namespace!.Contains("Models"))) + .UsingRegistrationStrategy(RegistrationStrategy.Skip) .AsImplementedInterfaces() .WithScopedLifetime()); - services.ConfigureOptions(); + services.AddOptions() + .Bind(configuration.GetSection(SD.EmailSectionKey)) + .ValidateDataAnnotations(); } } diff --git a/crs/Services/Email/Email.App/Configurations/MessageBusServiceInstaller.cs b/crs/Services/Email/Email.App/Configurations/MessageBusServiceInstaller.cs index 4df60c6..62559fc 100644 --- a/crs/Services/Email/Email.App/Configurations/MessageBusServiceInstaller.cs +++ b/crs/Services/Email/Email.App/Configurations/MessageBusServiceInstaller.cs @@ -2,8 +2,21 @@ internal sealed class MessageBusServiceInstaller : IServiceInstaller { - public void Install(IServiceCollection services, IConfiguration configuration) - { + public void Install(IServiceCollection services, IConfiguration configuration) => + services.AddMassTransit(configure => + { + configure.SetKebabCaseEndpointNameFormatter(); + configure.UsingRabbitMq((context, configurator) => + { + configurator.Host("rabbitmq", "/", hostConfigurator => + { + hostConfigurator.Username(Env.RABBITMQ_DEFAULT_USER); + hostConfigurator.Password(Env.RABBITMQ_DEFAULT_PASS); + }); - } + configurator.ConfigureEndpoints(context); + }); + + configure.AddConsumers(MessageBus.AssemblyReference.Assembly); + }); } diff --git a/crs/Services/Email/Email.App/Configurations/PresentationServiceInstaller.cs b/crs/Services/Email/Email.App/Configurations/PresentationServiceInstaller.cs index 738e2cb..7ee34fa 100644 --- a/crs/Services/Email/Email.App/Configurations/PresentationServiceInstaller.cs +++ b/crs/Services/Email/Email.App/Configurations/PresentationServiceInstaller.cs @@ -12,5 +12,9 @@ public void Install(IServiceCollection services, IConfiguration configuration) .AllowAnyMethod() .AllowAnyHeader()); }); + + services.AddOptions() + .Bind(configuration.GetSection(SD.IdentityEndpointSectionKey)) + .ValidateDataAnnotations(); } } diff --git a/crs/Services/Email/Email.App/Email.App.csproj b/crs/Services/Email/Email.App/Email.App.csproj index 3ce5b1b..7f3f254 100644 --- a/crs/Services/Email/Email.App/Email.App.csproj +++ b/crs/Services/Email/Email.App/Email.App.csproj @@ -14,6 +14,7 @@ + @@ -29,6 +30,7 @@ + diff --git a/crs/Services/Email/Email.App/Env.cs b/crs/Services/Email/Email.App/Env.cs index 00c25ab..a6e3c8c 100644 --- a/crs/Services/Email/Email.App/Env.cs +++ b/crs/Services/Email/Email.App/Env.cs @@ -5,7 +5,7 @@ /// internal static class Env { - public static string IDENTITY_URL => GetEnvironmentVariable("IDENTITY_URL"); + public static string IDENTITY_GRPC_URL => GetEnvironmentVariable("IDENTITY_GRPC_URL"); public static string RABBITMQ_DEFAULT_USER => GetEnvironmentVariable("RABBITMQ_DEFAULT_USER"); public static string RABBITMQ_DEFAULT_PASS => GetEnvironmentVariable("RABBITMQ_DEFAULT_PASS"); public static string AUTH_ISSUER => GetEnvironmentVariable("AUTH_ISSUER"); diff --git a/crs/Services/Email/Email.App/GlobalUsings.cs b/crs/Services/Email/Email.App/GlobalUsings.cs index 823bd97..26a07d4 100644 --- a/crs/Services/Email/Email.App/GlobalUsings.cs +++ b/crs/Services/Email/Email.App/GlobalUsings.cs @@ -1,15 +1,15 @@ global using OpenTelemetry.Metrics; -global using Microsoft.Extensions.Options; global using System.Reflection; global using Email.App; global using Email.Infrastructure.Email; global using Common.App.Extensions; global using Common.Application.Behaviors; global using FluentValidation; -global using Email.App.OptionsSetup; global using Common.Infrastructure.Middleware; -global using Contracts.Services.Identity; global using Microsoft.AspNetCore.Authentication.JwtBearer; global using Microsoft.IdentityModel.Tokens; global using System.Security.Claims; global using System.Text; +global using MassTransit; +global using Scrutor; +global using Email.Infrastructure.EndpointOptions; diff --git a/crs/Services/Email/Email.App/OptionsSetup/EmailOptionsSetup.cs b/crs/Services/Email/Email.App/OptionsSetup/EmailOptionsSetup.cs deleted file mode 100644 index ee4a79b..0000000 --- a/crs/Services/Email/Email.App/OptionsSetup/EmailOptionsSetup.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Email.App.OptionsSetup; - -internal sealed class EmailOptionsSetup(IConfiguration configuration) : IConfigureOptions -{ - private readonly IConfiguration _configuration = configuration; - - public void Configure(EmailOptions options) => - _configuration.GetSection(SD.EmailSectionKey).Bind(options); -} diff --git a/crs/Services/Email/Email.App/SD.cs b/crs/Services/Email/Email.App/SD.cs index 3e8ab09..7da45c0 100644 --- a/crs/Services/Email/Email.App/SD.cs +++ b/crs/Services/Email/Email.App/SD.cs @@ -7,6 +7,6 @@ public class SD { // Is default values public const string EmailSectionKey = "Email"; - public const string DefaultCorsPolicyName = "CorsPolicy"; + public const string IdentityEndpointSectionKey = "IdentityEndpoint"; } diff --git a/crs/Services/Email/Email.App/Startup.cs b/crs/Services/Email/Email.App/Startup.cs index 37c6da9..d7f56f4 100644 --- a/crs/Services/Email/Email.App/Startup.cs +++ b/crs/Services/Email/Email.App/Startup.cs @@ -13,20 +13,16 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseDeveloperExceptionPage(); app.UseSwagger(); - app.UseSwaggerUI(); } app.UseMiddleware(); - app.UseHttpsRedirection(); - app.UseRouting(); - + app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { - endpoints.MapControllers(); endpoints.MapPrometheusScrapingEndpoint(); }); } diff --git a/crs/Services/Email/Email.Application/Emails/Commands/SendConfirmationUserMessage/SendConfirmationUserMessageCommand.cs b/crs/Services/Email/Email.Application/Emails/Commands/SendConfirmationUserMessage/SendConfirmationUserMessageCommand.cs index df4c90c..a1fdb51 100644 --- a/crs/Services/Email/Email.Application/Emails/Commands/SendConfirmationUserMessage/SendConfirmationUserMessageCommand.cs +++ b/crs/Services/Email/Email.Application/Emails/Commands/SendConfirmationUserMessage/SendConfirmationUserMessageCommand.cs @@ -1,3 +1,6 @@ namespace Email.Application.Emails.Commands.SendConfirmationUserMessage; -public sealed record SendConfirmationUserMessageCommand(Guid UserId, string ReturnUrl) : ICommand; +public sealed record SendConfirmationUserMessageCommand( + Guid UserId, + string ReturnUrl, + string ConfirmationEmailToken) : ICommand; diff --git a/crs/Services/Email/Email.Application/Emails/Commands/SendConfirmationUserMessage/SendConfirmationUserMessageCommandHandler.cs b/crs/Services/Email/Email.Application/Emails/Commands/SendConfirmationUserMessage/SendConfirmationUserMessageCommandHandler.cs index 907b2e2..a990a5d 100644 --- a/crs/Services/Email/Email.Application/Emails/Commands/SendConfirmationUserMessage/SendConfirmationUserMessageCommandHandler.cs +++ b/crs/Services/Email/Email.Application/Emails/Commands/SendConfirmationUserMessage/SendConfirmationUserMessageCommandHandler.cs @@ -1,25 +1,30 @@ -namespace Email.Application.Emails.Commands.SendConfirmationUserMessage; +using Email.Infrastructure.Email.Models; +using Email.Infrastructure.Grpc.Identity; -internal sealed class SendConfirmationUserMessageCommandHandler(IIdentityEmailService emailService) +namespace Email.Application.Emails.Commands.SendConfirmationUserMessage; + +internal sealed class SendConfirmationUserMessageCommandHandler( + IIdentityEmailService emailService, + IIdentityGrpcService identityGrpcService) : ICommandHandler { private readonly IIdentityEmailService _emailService = emailService; + private readonly IIdentityGrpcService _identityGrpcService = identityGrpcService; - - public Task Handle(SendConfirmationUserMessageCommand request, CancellationToken cancellationToken) + public async Task Handle(SendConfirmationUserMessageCommand request, CancellationToken cancellationToken) { + var userInfo = await _identityGrpcService.GetUserInfoAsync(request.UserId, cancellationToken); + var emailRequest = new SendConfirmationEmailRequest( + userInfo.FirstName, + userInfo.LastName, + userInfo.UserId, + userInfo.Email, + request.ConfirmationEmailToken, + request.ReturnUrl); + await _emailService.SendConfirmationEmailAsync(emailRequest, cancellationToken); - _emailService.SendConfirmationEmailAsync( - new SendConfirmationEmailRequest - { - UserId = request.UserId, - EmailConfirmationToken = request.EmailConfirmationToken, - ReturnUrl = request.ReturnUrl, - FirstName = request.FirstName, - LastName = request.LastName, - Email = request.Email - }, cancellationToken); + return Result.Success(); } } diff --git a/crs/Services/Email/Email.Application/GlobalUsings.cs b/crs/Services/Email/Email.Application/GlobalUsings.cs index 1fb9d34..a731ec7 100644 --- a/crs/Services/Email/Email.Application/GlobalUsings.cs +++ b/crs/Services/Email/Email.Application/GlobalUsings.cs @@ -2,3 +2,4 @@ global using Common.Application.Abstractions.Messaging.Command; global using Common.Domain.Primitives.Results; global using Email.Infrastructure.Email.Abstractions; +global using Email.Infrastructure.Grpc; \ No newline at end of file diff --git a/crs/Services/Email/Email.Infrastructure/Email.Infrastructure.csproj b/crs/Services/Email/Email.Infrastructure/Email.Infrastructure.csproj index 02e8703..4e48956 100644 --- a/crs/Services/Email/Email.Infrastructure/Email.Infrastructure.csproj +++ b/crs/Services/Email/Email.Infrastructure/Email.Infrastructure.csproj @@ -7,6 +7,7 @@ + @@ -15,7 +16,12 @@ + + + + + diff --git a/crs/Services/Email/Email.Infrastructure/Email/Models/SendConfirmationEmailRequest.cs b/crs/Services/Email/Email.Infrastructure/Email/Models/SendConfirmationEmailRequest.cs index d582280..9b522cd 100644 --- a/crs/Services/Email/Email.Infrastructure/Email/Models/SendConfirmationEmailRequest.cs +++ b/crs/Services/Email/Email.Infrastructure/Email/Models/SendConfirmationEmailRequest.cs @@ -6,7 +6,5 @@ public record SendConfirmationEmailRequest( string UserId, string Email, string EmailConfirmationToken, - string ReturnUrl, - string Subject, - string EmailConfirmPagePath + string ReturnUrl ); diff --git a/crs/Services/Email/Email.Infrastructure/Email/Services/IdentityEmailService.cs b/crs/Services/Email/Email.Infrastructure/Email/Services/IdentityEmailService.cs index 3f4798f..8955855 100644 --- a/crs/Services/Email/Email.Infrastructure/Email/Services/IdentityEmailService.cs +++ b/crs/Services/Email/Email.Infrastructure/Email/Services/IdentityEmailService.cs @@ -1,10 +1,13 @@ namespace Email.Infrastructure.Email.Services; internal sealed class IdentityEmailService - (IOptions options) : + (IOptions options, + IOptions identityEndpointOptions) : EmailBaseService(options), IIdentityEmailService { + private readonly IdentityEndpointOptions _identityEndpointOptions = identityEndpointOptions.Value; + public async Task SendConfirmationEmailAsync(SendConfirmationEmailRequest request, CancellationToken cancellationToken = default) { var confirmEmailTemplatePath = EmailTemplatePath.ConfirmEmailTemplate; @@ -13,7 +16,7 @@ public async Task SendConfirmationEmailAsync(SendConfirmationEmailRequest reques await File.ReadAllTextAsync(confirmEmailTemplatePath, cancellationToken); var confirmUrl = - $@"{thi}?UserId={request.UserId}&EmailConfirmationToken={request.EmailConfirmationToken}&ReturnUrl={request.ReturnUrl}"; + $@"{_identityEndpointOptions.BaseUrl}/confirm-email?UserId={request.UserId}&EmailConfirmationToken={request.EmailConfirmationToken}&ReturnUrl={request.ReturnUrl}"; var confirmUrlEncode = HtmlEncoder.Default.Encode(confirmUrl); @@ -25,7 +28,7 @@ public async Task SendConfirmationEmailAsync(SendConfirmationEmailRequest reques var sendMessageRequest = new SendMessageRequest( To: request.Email, - Subject: request.Subject, + Subject: $"Eshop - confirm email", Body: confirmEmailTemplate ); diff --git a/crs/Services/Email/Email.Infrastructure/EndpointOptions/IdentityEndpointOptions.cs b/crs/Services/Email/Email.Infrastructure/EndpointOptions/IdentityEndpointOptions.cs new file mode 100644 index 0000000..c1b57f6 --- /dev/null +++ b/crs/Services/Email/Email.Infrastructure/EndpointOptions/IdentityEndpointOptions.cs @@ -0,0 +1,6 @@ +namespace Email.Infrastructure.EndpointOptions; + +public class IdentityEndpointOptions +{ + public string BaseUrl { get; set; } = null!; +} diff --git a/crs/Services/Email/Email.Infrastructure/GlobalUsings.cs b/crs/Services/Email/Email.Infrastructure/GlobalUsings.cs index cf8002c..faf0f05 100644 --- a/crs/Services/Email/Email.Infrastructure/GlobalUsings.cs +++ b/crs/Services/Email/Email.Infrastructure/GlobalUsings.cs @@ -6,4 +6,6 @@ global using MailKit.Net.Smtp; global using System.Reflection; global using System.Text.Encodings.Web; -global using Polly; \ No newline at end of file +global using Polly; +global using Identity.Protobuf; +global using Email.Infrastructure.EndpointOptions; \ No newline at end of file diff --git a/crs/Services/Email/Email.Infrastructure/Grpc/Identity/IIdentityGrpcService.cs b/crs/Services/Email/Email.Infrastructure/Grpc/Identity/IIdentityGrpcService.cs new file mode 100644 index 0000000..546c658 --- /dev/null +++ b/crs/Services/Email/Email.Infrastructure/Grpc/Identity/IIdentityGrpcService.cs @@ -0,0 +1,6 @@ +namespace Email.Infrastructure.Grpc.Identity; + +public interface IIdentityGrpcService +{ + public Task GetUserInfoAsync(Guid id, CancellationToken cancellationToken = default); +} diff --git a/crs/Services/Email/Email.Infrastructure/Grpc/Identity/IdentityGrpcService.cs b/crs/Services/Email/Email.Infrastructure/Grpc/Identity/IdentityGrpcService.cs new file mode 100644 index 0000000..cd6e820 --- /dev/null +++ b/crs/Services/Email/Email.Infrastructure/Grpc/Identity/IdentityGrpcService.cs @@ -0,0 +1,14 @@ +using static Identity.Protobuf.IdentityService; + +namespace Email.Infrastructure.Grpc.Identity; + +internal sealed class IdentityGrpcService(IdentityServiceClient client) : IIdentityGrpcService +{ + private readonly IdentityServiceClient _client = client; + + public async Task GetUserInfoAsync(Guid id, CancellationToken cancellationToken = default) + { + var request = new GetUserRequest() { Id = id.ToString() }; + return await _client.GetUserInfoAsync(request, cancellationToken: cancellationToken); + } +} diff --git a/crs/Services/Email/Email.MessageBus/AssemblyReference.cs b/crs/Services/Email/Email.MessageBus/AssemblyReference.cs new file mode 100644 index 0000000..1b1cf8e --- /dev/null +++ b/crs/Services/Email/Email.MessageBus/AssemblyReference.cs @@ -0,0 +1,7 @@ + +namespace Email.MessageBus; + +public static class AssemblyReference +{ + public static readonly Assembly Assembly = typeof(AssemblyReference).Assembly; +} diff --git a/crs/Services/Email/Email.MessageBus/GlobalUsings.cs b/crs/Services/Email/Email.MessageBus/GlobalUsings.cs index 23ec1f6..4d6ba91 100644 --- a/crs/Services/Email/Email.MessageBus/GlobalUsings.cs +++ b/crs/Services/Email/Email.MessageBus/GlobalUsings.cs @@ -2,3 +2,4 @@ global using Contracts.Services.Identity.Commands; global using MassTransit; global using MediatR; +global using System.Reflection; \ No newline at end of file diff --git a/crs/Services/Email/Email.MessageBus/Handlers/Commands/UserCreatedConfirmationEmailSendCommandHandler.cs b/crs/Services/Email/Email.MessageBus/Handlers/Commands/UserCreatedConfirmationEmailSendCommandHandler.cs index 1279647..0401a2d 100644 --- a/crs/Services/Email/Email.MessageBus/Handlers/Commands/UserCreatedConfirmationEmailSendCommandHandler.cs +++ b/crs/Services/Email/Email.MessageBus/Handlers/Commands/UserCreatedConfirmationEmailSendCommandHandler.cs @@ -11,13 +11,9 @@ public override async Task Handle(ConsumeContext GetUserInfo(GetUserRequest request, ServerCallContext context) + { + var query = new GetUserInfoByIdStringQuery(request.Id); + var result = await _sender.Send(query, context.CancellationToken); + + if (result.IsFailure) + { + throw new RpcException(new Status(StatusCode.NotFound, result.Error)); + } + + var user = result.Value; + + var response = new UserInfo() + { + UserId = user.UserId.ToString(), + Email = user.Email, + FirstName = user.FirstName, + LastName = user.LastName, + IsEmailConfirmed = user.IsEmailConfirmed, + Role = Enum.Parse(user.Role), + Gender = Enum.Parse(user.Gender) + }; + + return response; + } +} diff --git a/crs/Services/Identity/Idenitty.Grpc/IdentityGrpcServiceV1.cs b/crs/Services/Identity/Idenitty.Grpc/IdentityGrpcServiceV1.cs deleted file mode 100644 index 775ff50..0000000 --- a/crs/Services/Identity/Idenitty.Grpc/IdentityGrpcServiceV1.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Identity.Application.Users.Queries.GetUserInfoById; -using Identity.V1; - -namespace Idenitty.Grpc; - -public sealed class IdentityGrpcServiceV1(ISender sender) : IdentityService.IdentityServiceBase -{ - private readonly ISender _sender = sender; - - public override async Task GetUserInfo(GetUserRequest request, ServerCallContext context) - { - Guid.TryParse(request.Id, out Guid userId); - - //if(userId == Guid.Empty) - //{ - // context.Status = new Status(StatusCode.InvalidArgument, "Invalid user id"); - //} - - var query = new GetUserInfoByIdQuery(userId); - var result = await _sender.Send(query); - var response = new UserInfo() { s}; - return base.GetUser(request, context); - } -} diff --git a/crs/Services/Identity/Identity.App/Configurations/InfrastructureServiceInstaller.cs b/crs/Services/Identity/Identity.App/Configurations/InfrastructureServiceInstaller.cs index 9e8d37d..5ef0b76 100644 --- a/crs/Services/Identity/Identity.App/Configurations/InfrastructureServiceInstaller.cs +++ b/crs/Services/Identity/Identity.App/Configurations/InfrastructureServiceInstaller.cs @@ -1,6 +1,4 @@ -using Identity.Application.Abstractions; - -namespace Identity.App.Configurations; +namespace Identity.App.Configurations; internal sealed class InfrastructureServiceInstaller : IServiceInstaller { @@ -13,10 +11,10 @@ public void Install(IServiceCollection services, IConfiguration configuration) selector.FromAssemblies( Infrastructure.AssemblyReference.Assembly, Persistence.AssemblyReference.Assembly) - .AddClasses(false) + .AddClasses(classes => classes + .Where(type => !type.Namespace!.Contains("Models"))) + .UsingRegistrationStrategy(RegistrationStrategy.Skip) .AsImplementedInterfaces() .WithScopedLifetime()); - - services.ConfigureOptions(); } } diff --git a/crs/Services/Identity/Identity.App/Configurations/MessageBusServiceInstaller.cs b/crs/Services/Identity/Identity.App/Configurations/MessageBusServiceInstaller.cs index 453e3d2..5637165 100644 --- a/crs/Services/Identity/Identity.App/Configurations/MessageBusServiceInstaller.cs +++ b/crs/Services/Identity/Identity.App/Configurations/MessageBusServiceInstaller.cs @@ -18,8 +18,7 @@ public void Install(IServiceCollection services, IConfiguration configuration) configurator.ConfigureEndpoints(context); }); - - configure.AddConsumers(App.AssemblyReference.Assembly); + configure.AddConsumers(MessageBus.AssemblyReference.Assembly); }); services.AddTransient(); diff --git a/crs/Services/Identity/Identity.App/Env.cs b/crs/Services/Identity/Identity.App/Env.cs index 502f7e8..1847427 100644 --- a/crs/Services/Identity/Identity.App/Env.cs +++ b/crs/Services/Identity/Identity.App/Env.cs @@ -14,16 +14,19 @@ public static class Env public static string JWT_SECURITY_KEY => GetEnvironmentVariable("JWT_SECURITY_KEY"); public static string RABBITMQ_DEFAULT_USER => GetEnvironmentVariable("RABBITMQ_DEFAULT_USER"); public static string RABBITMQ_DEFAULT_PASS => GetEnvironmentVariable("RABBITMQ_DEFAULT_PASS"); + public static int GRPC_PORT = int.Parse(GetEnvironmentVariable("GRPC_PORT")); + public static int HTTP_PORT = int.Parse(GetEnvironmentVariable("HTTP_PORT")); public static class ConnectionStrings { - public static string POSTGRES => - $"Server=postgres;" + - $"Port=5432;" + - $"Database={POSTGRES_DB};" + - $"Username={POSTGRES_USER};" + - $"Password={POSTGRES_PASSWORD}" + - $";TimeZone=UTC;"; + public static string POSTGRES => $""" + Server=postgres; + Port=5432; + Database={POSTGRES_DB}; + Username={POSTGRES_USER}; + Password={POSTGRES_PASSWORD}; + TimeZone=UTC; + """; public static string RABBITMQ => $"amqp://{RABBITMQ_DEFAULT_USER}:{RABBITMQ_DEFAULT_PASS}@rabbitmq:5672"; @@ -31,5 +34,5 @@ public static class ConnectionStrings private static string GetEnvironmentVariable(string key) => Environment.GetEnvironmentVariable(key) ?? - throw new Exception($"Environment variable {key} not found"); + throw new Exception($"environment variable {key} not found"); } diff --git a/crs/Services/Identity/Identity.App/GlobalUsings.cs b/crs/Services/Identity/Identity.App/GlobalUsings.cs index ba5abeb..316110b 100644 --- a/crs/Services/Identity/Identity.App/GlobalUsings.cs +++ b/crs/Services/Identity/Identity.App/GlobalUsings.cs @@ -26,6 +26,8 @@ global using Prometheus; global using Common.App.HealthChecks; global using MassTransit; -global using EventBus.Common.Abstractions; global using EventBus.MassTransit.RabbitMQ.Services; global using Idenitty.Grpc; +global using Scrutor; +global using EventBus.MassTransit.Abstractions; +global using System.Net; \ No newline at end of file diff --git a/crs/Services/Identity/Identity.App/Identity.App.csproj b/crs/Services/Identity/Identity.App/Identity.App.csproj index 5252d13..fd903f0 100644 --- a/crs/Services/Identity/Identity.App/Identity.App.csproj +++ b/crs/Services/Identity/Identity.App/Identity.App.csproj @@ -51,6 +51,7 @@ + diff --git a/crs/Services/Identity/Identity.App/OptionsSetup/EmailOptionsSetup.cs b/crs/Services/Identity/Identity.App/OptionsSetup/EmailOptionsSetup.cs deleted file mode 100644 index c348c3a..0000000 --- a/crs/Services/Identity/Identity.App/OptionsSetup/EmailOptionsSetup.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Identity.App.OptionsSetup; - -internal sealed class EmailOptionsSetup(IConfiguration configuration) : IConfigureOptions -{ - private readonly IConfiguration _configuration = configuration; - - public void Configure(EmailOptions options) - { - _configuration.GetSection(SD.EmailSectionKey).Bind(options); - } -} diff --git a/crs/Services/Identity/Identity.App/Program.cs b/crs/Services/Identity/Identity.App/Program.cs index 0927e47..df61f5c 100644 --- a/crs/Services/Identity/Identity.App/Program.cs +++ b/crs/Services/Identity/Identity.App/Program.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Server.Kestrel.Core; + CreateHostBuilder(args).Build().Run(); static IHostBuilder CreateHostBuilder(string[] args) => @@ -8,6 +10,20 @@ static IHostBuilder CreateHostBuilder(string[] args) => { config.AddYamlFile( "appsettings.yml", optional: true, reloadOnChange: true); + + }); + + webBuilder.ConfigureKestrel(options => + { + options.ListenAnyIP(Env.HTTP_PORT, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http1AndHttp2; + }); + + options.ListenAnyIP(Env.GRPC_PORT, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http2; + }); }); webBuilder.UseStartup(); diff --git a/crs/Services/Identity/Identity.App/Startup.cs b/crs/Services/Identity/Identity.App/Startup.cs index 1ae6587..88775bb 100644 --- a/crs/Services/Identity/Identity.App/Startup.cs +++ b/crs/Services/Identity/Identity.App/Startup.cs @@ -31,7 +31,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { configure.MapControllers(); configure.MapPrometheusScrapingEndpoint(); - configure.MapGrpcService(); + configure.MapGrpcService(); configure.MapHealthChecks("/health", new HealthCheckOptions { ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse diff --git a/crs/Services/Identity/Identity.Application/GlobalUsings.cs b/crs/Services/Identity/Identity.Application/GlobalUsings.cs index fa9aac6..ef9d2c8 100644 --- a/crs/Services/Identity/Identity.Application/GlobalUsings.cs +++ b/crs/Services/Identity/Identity.Application/GlobalUsings.cs @@ -17,3 +17,6 @@ global using Identity.Infrastructure.Hashing; global using Identity.Infrastructure.Authentication; global using Identity.Application.Users.Common; +global using MediatR; +global using EventBus.MassTransit.Abstractions; +global using Contracts.Services.Identity.Events; diff --git a/crs/Services/Identity/Identity.Application/Identity.Application.csproj b/crs/Services/Identity/Identity.Application/Identity.Application.csproj index d7ce476..dc7678c 100644 --- a/crs/Services/Identity/Identity.Application/Identity.Application.csproj +++ b/crs/Services/Identity/Identity.Application/Identity.Application.csproj @@ -9,6 +9,7 @@ + @@ -17,4 +18,8 @@ + + + + diff --git a/crs/Services/Identity/Identity.Application/Users/Commands/ConfirmEmail/ConfirmEmailCommandHandler.cs b/crs/Services/Identity/Identity.Application/Users/Commands/ConfirmEmail/ConfirmEmailCommandHandler.cs index f0d026d..1beca86 100644 --- a/crs/Services/Identity/Identity.Application/Users/Commands/ConfirmEmail/ConfirmEmailCommandHandler.cs +++ b/crs/Services/Identity/Identity.Application/Users/Commands/ConfirmEmail/ConfirmEmailCommandHandler.cs @@ -2,11 +2,13 @@ internal sealed class ConfirmEmailCommandHandler( IUnitOfWork unitOfWork, - IUserRepository userRepository) + IUserRepository userRepository, + IMessageBus messageBus) : ICommandHandler { private readonly IUserRepository _userRepository = userRepository; private readonly IUnitOfWork _unitOfWork = unitOfWork; + private readonly IMessageBus _messageBus = messageBus; public async Task Handle(ConfirmEmailCommand request, CancellationToken cancellationToken) { @@ -30,7 +32,10 @@ public async Task Handle(ConfirmEmailCommand request, CancellationToken confirmEmailResult.Error); } - await _unitOfWork.SaveChangesAsync(cancellationToken); + await _unitOfWork.CommitAsync(cancellationToken); + + await _messageBus.Publish( + new IdentityVerificationConfirmedEvent(Guid.NewGuid(), user.Id.Value), cancellationToken); return Result.Success(); } diff --git a/crs/Services/Identity/Identity.Application/Users/Commands/Login/LoginCommandHandler.cs b/crs/Services/Identity/Identity.Application/Users/Commands/Login/LoginCommandHandler.cs index 019c055..8b986c4 100644 --- a/crs/Services/Identity/Identity.Application/Users/Commands/Login/LoginCommandHandler.cs +++ b/crs/Services/Identity/Identity.Application/Users/Commands/Login/LoginCommandHandler.cs @@ -35,7 +35,7 @@ public async Task> Handle(LoginCommand request, Canc user.UpdateRefreshToken(refreshToken); var userToken = _jwtProvider.CreateTokenString(user, request.Audience); - await _unitOfWork.SaveChangesAsync(cancellationToken); + await _unitOfWork.CommitAsync(cancellationToken); return new LoginCommanResponse(userToken, refreshToken.Token); } diff --git a/crs/Services/Identity/Identity.Application/Users/Commands/Register/RegisterCommandHandler.cs b/crs/Services/Identity/Identity.Application/Users/Commands/Register/RegisterCommandHandler.cs index 5940757..c71aacb 100644 --- a/crs/Services/Identity/Identity.Application/Users/Commands/Register/RegisterCommandHandler.cs +++ b/crs/Services/Identity/Identity.Application/Users/Commands/Register/RegisterCommandHandler.cs @@ -25,9 +25,12 @@ public async Task Handle(RegisterCommand request, CancellationToken canc var user = userResult.Value; await _userRepository.AddUserAsync(user, cancellationToken); - await _unitOfWork.SaveChangesAsync(cancellationToken); + await _unitOfWork.CommitAsync(cancellationToken); - await _messageBus.Send( + var endpoint = await _messageBus.GetSendEndpoint( + new Uri("queue:user-created-confirmation-email-send-command-handler")); + + await endpoint.Send( new UserCreatedConfirmationEmailSendCommand(Guid.NewGuid(), user.Id.Value, user.EmailConfirmationToken.Value, request.ReturnUrl), cancellationToken); diff --git a/crs/Services/Identity/Identity.Application/Users/Commands/Register/RegisterCommandValidator.cs b/crs/Services/Identity/Identity.Application/Users/Commands/Register/RegisterCommandValidator.cs index 6701cb6..b6b7c1c 100644 --- a/crs/Services/Identity/Identity.Application/Users/Commands/Register/RegisterCommandValidator.cs +++ b/crs/Services/Identity/Identity.Application/Users/Commands/Register/RegisterCommandValidator.cs @@ -26,12 +26,12 @@ public RegisterCommandValidator() RuleFor(x => x.Role) .NotEmpty() - .Must(x => Role.FromName("") is not null) + .Must(x => Role.FromNameOrDefault(x) is not null) .WithMessage("Role is not exist"); RuleFor(x => x.Gender) .NotEmpty() - .Must(x => Gender.FromName("") is not null) + .Must(x => Gender.FromNameOrDefault(x) is not null) .WithMessage("Gender is not exist"); RuleFor(x => x.ReturnUrl); diff --git a/crs/Services/Identity/Identity.Application/Users/Commands/UpdateRefreshToken/UpdateRefreshTokenCommandHandler.cs b/crs/Services/Identity/Identity.Application/Users/Commands/UpdateRefreshToken/UpdateRefreshTokenCommandHandler.cs index 5912a3c..8061b54 100644 --- a/crs/Services/Identity/Identity.Application/Users/Commands/UpdateRefreshToken/UpdateRefreshTokenCommandHandler.cs +++ b/crs/Services/Identity/Identity.Application/Users/Commands/UpdateRefreshToken/UpdateRefreshTokenCommandHandler.cs @@ -36,7 +36,7 @@ public async Task> Handle(UpdateRefres user.UpdateRefreshToken(refreshToken); _userRepository.UpdateUser(user); - await _unitOfWork.SaveChangesAsync(cancellationToken); + await _unitOfWork.CommitAsync(cancellationToken); return new UpdateRefreshTokenCommandResponse(userToken, refreshToken.Token); } diff --git a/crs/Services/Identity/Identity.Application/Users/Common/UserInfo.cs b/crs/Services/Identity/Identity.Application/Users/Common/UserDto.cs similarity index 55% rename from crs/Services/Identity/Identity.Application/Users/Common/UserInfo.cs rename to crs/Services/Identity/Identity.Application/Users/Common/UserDto.cs index 5eeec00..9889d0c 100644 --- a/crs/Services/Identity/Identity.Application/Users/Common/UserInfo.cs +++ b/crs/Services/Identity/Identity.Application/Users/Common/UserDto.cs @@ -1,10 +1,10 @@ -namespace Identity.Application.Users.Common; +namespace Identity.Application.Users.Common; -public sealed record UserInfo( +public sealed record UserDto( Guid UserId, string Email, string FirstName, string LastName, bool IsEmailConfirmed, string Role, - string Gender); + string Gender); \ No newline at end of file diff --git a/crs/Services/Identity/Identity.Application/Users/Queries/GetGenders/GetGendersQuery.cs b/crs/Services/Identity/Identity.Application/Users/Queries/GetGenders/GetGendersQuery.cs new file mode 100644 index 0000000..5af6299 --- /dev/null +++ b/crs/Services/Identity/Identity.Application/Users/Queries/GetGenders/GetGendersQuery.cs @@ -0,0 +1,3 @@ +namespace Identity.Application.Users.Queries.GetGenders; + +public sealed record GetGendersQuery() : IQuery; diff --git a/crs/Services/Identity/Identity.Application/Users/Queries/GetGenders/GetGendersQueryHandler.cs b/crs/Services/Identity/Identity.Application/Users/Queries/GetGenders/GetGendersQueryHandler.cs new file mode 100644 index 0000000..8fd8673 --- /dev/null +++ b/crs/Services/Identity/Identity.Application/Users/Queries/GetGenders/GetGendersQueryHandler.cs @@ -0,0 +1,12 @@ + +namespace Identity.Application.Users.Queries.GetGenders; + +internal sealed class GetRolesQueryHandler : IQueryHandler +{ + public Task> Handle(GetGendersQuery request, CancellationToken cancellationToken) + { + var roles = Gender.GetNames(); + var response = Result.Success(new GetGendersQueryResponse(roles)); + return Task.FromResult(response); + } +} diff --git a/crs/Services/Identity/Identity.Application/Users/Queries/GetGenders/GetGendersQueryResponse.cs b/crs/Services/Identity/Identity.Application/Users/Queries/GetGenders/GetGendersQueryResponse.cs new file mode 100644 index 0000000..7602003 --- /dev/null +++ b/crs/Services/Identity/Identity.Application/Users/Queries/GetGenders/GetGendersQueryResponse.cs @@ -0,0 +1,3 @@ +namespace Identity.Application.Users.Queries.GetGenders; + +public sealed record GetGendersQueryResponse(IEnumerable Genders); \ No newline at end of file diff --git a/crs/Services/Identity/Identity.Application/Users/Queries/GetRoles/GetRolesQuery.cs b/crs/Services/Identity/Identity.Application/Users/Queries/GetRoles/GetRolesQuery.cs index c952b45..1cd2949 100644 --- a/crs/Services/Identity/Identity.Application/Users/Queries/GetRoles/GetRolesQuery.cs +++ b/crs/Services/Identity/Identity.Application/Users/Queries/GetRoles/GetRolesQuery.cs @@ -1,3 +1,3 @@ namespace Identity.Application.Users.Queries.GetRoles; -public sealed class GetRolesQuery() : IQuery; +public sealed record GetRolesQuery() : IQuery; diff --git a/crs/Services/Identity/Identity.Application/Users/Queries/GetRoles/GetRolesQueryHandler.cs b/crs/Services/Identity/Identity.Application/Users/Queries/GetRoles/GetRolesQueryHandler.cs index 83c9c9a..572eaa4 100644 --- a/crs/Services/Identity/Identity.Application/Users/Queries/GetRoles/GetRolesQueryHandler.cs +++ b/crs/Services/Identity/Identity.Application/Users/Queries/GetRoles/GetRolesQueryHandler.cs @@ -6,7 +6,6 @@ public Task> Handle(GetRolesQuery request, Cancell { var roles = Role.GetNames(); var response = Result.Success(new GetRolesQueryResponse(roles)); - return Task.FromResult(response); } } diff --git a/crs/Services/Identity/Identity.Application/Users/Queries/GetUserInfoById/GetUserByIdQueryHandler.cs b/crs/Services/Identity/Identity.Application/Users/Queries/GetUserInfoById/GetUserByIdQueryHandler.cs new file mode 100644 index 0000000..dbf2d69 --- /dev/null +++ b/crs/Services/Identity/Identity.Application/Users/Queries/GetUserInfoById/GetUserByIdQueryHandler.cs @@ -0,0 +1,31 @@ +namespace Identity.Application.Users.Queries.GetUserInfoById; + +internal sealed class GetUserByIdQueryHandler(IUserRepository userRepository) + : IQueryHandler +{ + private readonly IUserRepository _userRepository = userRepository; + + public async Task> Handle(GetUserInfoByIdQuery request, CancellationToken cancellationToken) + { + var userId = new UserId(request.Id); + + var user = await _userRepository.GetUserByIdAsync(userId, cancellationToken); + + if (user is null) + { + return Result.Failure( + UserErrors.UserDoesNotExist); + } + + var userInfo = new UserDto( + user.Id.Value, + user.Email.Value, + user.FirstName.Value, + user.LastName.Value, + user.IsEmailConfirmed, + user.Role.Name, + user.Gender.Name); + + return Result.Success(userInfo); + } +} diff --git a/crs/Services/Identity/Identity.Application/Users/Queries/GetUserInfoById/GetUserInfoByIdQuery.cs b/crs/Services/Identity/Identity.Application/Users/Queries/GetUserInfoById/GetUserInfoByIdQuery.cs index ea9809f..c6ceb26 100644 --- a/crs/Services/Identity/Identity.Application/Users/Queries/GetUserInfoById/GetUserInfoByIdQuery.cs +++ b/crs/Services/Identity/Identity.Application/Users/Queries/GetUserInfoById/GetUserInfoByIdQuery.cs @@ -1,4 +1,4 @@ namespace Identity.Application.Users.Queries.GetUserInfoById; -public sealed record GetUserInfoByIdQuery(Guid Id) : IQuery; +public sealed record GetUserInfoByIdQuery(Guid Id) : IQuery; diff --git a/crs/Services/Identity/Identity.Application/Users/Queries/GetUserInfoById/GetUserInfoByIdQueryHandler.cs b/crs/Services/Identity/Identity.Application/Users/Queries/GetUserInfoById/GetUserInfoByIdQueryHandler.cs deleted file mode 100644 index 450bcbb..0000000 --- a/crs/Services/Identity/Identity.Application/Users/Queries/GetUserInfoById/GetUserInfoByIdQueryHandler.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace Identity.Application.Users.Queries.GetUserInfoById; - -internal sealed class GetUserInfoByIdQueryHandler(IUserRepository userRepository) : IQueryHandler -{ - private readonly IUserRepository _userRepository = userRepository; - - public async Task> Handle(GetUserInfoByIdQuery request, CancellationToken cancellationToken) - { - var userId = new UserId(request.Id); - - var user = await _userRepository.GetUserByIdAsync(userId, cancellationToken); - - if (user is null) - { - return Result.Failure( - UserErrors.UserDoesNotExist); - } - - var userInfo = new UserInfo( - user.Id, - user.Email.Value, - user.(r => r.Name).ToList()); - - return Result.Success(userInfo); - } -} diff --git a/crs/Services/Identity/Identity.Application/Users/Queries/GetUserInfoByIdString/GetUserByIdStringQueryHandler.cs b/crs/Services/Identity/Identity.Application/Users/Queries/GetUserInfoByIdString/GetUserByIdStringQueryHandler.cs new file mode 100644 index 0000000..7e0ca5a --- /dev/null +++ b/crs/Services/Identity/Identity.Application/Users/Queries/GetUserInfoByIdString/GetUserByIdStringQueryHandler.cs @@ -0,0 +1,16 @@ +using Identity.Application.Users.Queries.GetUserInfoById; + +namespace Identity.Application.Users.Queries.GetUserInfoByIdString; + +internal sealed class GetUserByIdStringQueryHandler(ISender sender) : + IQueryHandler +{ + private readonly ISender _sender = sender; + + public async Task> Handle(GetUserInfoByIdStringQuery request, CancellationToken cancellationToken) + { + var userId = Guid.Parse(request.Id); + var query = new GetUserInfoByIdQuery(userId); + return await _sender.Send(query, cancellationToken); + } +} diff --git a/crs/Services/Identity/Identity.Application/Users/Queries/GetUserInfoByIdString/GetUserInfoByIdStringQuery.cs b/crs/Services/Identity/Identity.Application/Users/Queries/GetUserInfoByIdString/GetUserInfoByIdStringQuery.cs new file mode 100644 index 0000000..cf3f197 --- /dev/null +++ b/crs/Services/Identity/Identity.Application/Users/Queries/GetUserInfoByIdString/GetUserInfoByIdStringQuery.cs @@ -0,0 +1,3 @@ +namespace Identity.Application.Users.Queries.GetUserInfoByIdString; + +public sealed record GetUserInfoByIdStringQuery(string Id) : IQuery; diff --git a/crs/Services/Identity/Identity.Application/Users/Queries/GetUserInfoByIdString/GetUserInfoByIdStringQueryValidator.cs b/crs/Services/Identity/Identity.Application/Users/Queries/GetUserInfoByIdString/GetUserInfoByIdStringQueryValidator.cs new file mode 100644 index 0000000..ab1e44e --- /dev/null +++ b/crs/Services/Identity/Identity.Application/Users/Queries/GetUserInfoByIdString/GetUserInfoByIdStringQueryValidator.cs @@ -0,0 +1,14 @@ +namespace Identity.Application.Users.Queries.GetUserInfoByIdString; + +internal sealed class GetUserInfoByIdStringQueryValidator : AbstractValidator +{ + public GetUserInfoByIdStringQueryValidator() + { + RuleFor(x => x.Id) + .NotEmpty(); + + RuleFor(x => x.Id) + .Must(x => Guid.TryParse(x, out _)) + .WithMessage("Invalid Id"); + } +} diff --git a/crs/Services/Identity/Identity.Domain/UserAggregate/Regexes/EmailRegex.cs b/crs/Services/Identity/Identity.Domain/UserAggregate/Regexes/EmailRegex.cs new file mode 100644 index 0000000..32ca82d --- /dev/null +++ b/crs/Services/Identity/Identity.Domain/UserAggregate/Regexes/EmailRegex.cs @@ -0,0 +1,9 @@ +namespace Identity.Domain.UserAggregate.Regexes; + +public partial class EmailRegex +{ + private const string EmailPattern = @"^(.+)@(.+)$"; + + [GeneratedRegex(EmailPattern)] + public static partial Regex Regex(); +} diff --git a/crs/Services/Identity/Identity.Domain/UserAggregate/Repositories/IUserRepository.cs b/crs/Services/Identity/Identity.Domain/UserAggregate/Repositories/IUserRepository.cs index 588d393..70d0233 100644 --- a/crs/Services/Identity/Identity.Domain/UserAggregate/Repositories/IUserRepository.cs +++ b/crs/Services/Identity/Identity.Domain/UserAggregate/Repositories/IUserRepository.cs @@ -4,7 +4,7 @@ public interface IUserRepository { Task AddUserAsync(User user, CancellationToken cancellationToken = default); Task GetUserByEmailAsync(Email email, CancellationToken cancellationToken = default); - Task GetUserByIdAsync(UserId userId, CancellationToken cancellationToken = default); + Task GetUserByIdAsync(UserId userId, CancellationToken cancellationToken = default); Task CheckPasswordAsync(User user, string password, CancellationToken cancellationToken = default); Task ChangePasswordAsync(UserId userId, string currentPassowrd, string newPassword); Task ChangeEmailAsync(UserId userId, Email newEmail, CancellationToken cancellationToken = default); diff --git a/crs/Services/Identity/Identity.Domain/UserAggregate/ValueObjects/Email.cs b/crs/Services/Identity/Identity.Domain/UserAggregate/ValueObjects/Email.cs index a75dafd..c3c307b 100644 --- a/crs/Services/Identity/Identity.Domain/UserAggregate/ValueObjects/Email.cs +++ b/crs/Services/Identity/Identity.Domain/UserAggregate/ValueObjects/Email.cs @@ -1,8 +1,9 @@ -namespace Identity.Domain.UserAggregate.ValueObjects; +using Identity.Domain.UserAggregate.Regexes; -public sealed class Email : ValueObject +namespace Identity.Domain.UserAggregate.ValueObjects; + +public sealed partial class Email : ValueObject { - private const string EmailPattern = @"^(.+)@(.+)$"; public const int MaxLength = 100; public string Value { get; private set; } @@ -39,8 +40,8 @@ public override IEnumerable GetEqualityComponents() yield return Value; } - public static bool IsEmail(string email) => Regex.IsMatch(email, EmailPattern); - + public static bool IsEmail(string email) => + EmailRegex.Regex().IsMatch(email); public static implicit operator string(Email email) => email.Value; } diff --git a/crs/Services/Identity/Identity.Infrastructure/Identity.Infrastructure.csproj b/crs/Services/Identity/Identity.Infrastructure/Identity.Infrastructure.csproj index b09fc69..2a06078 100644 --- a/crs/Services/Identity/Identity.Infrastructure/Identity.Infrastructure.csproj +++ b/crs/Services/Identity/Identity.Infrastructure/Identity.Infrastructure.csproj @@ -9,6 +9,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/crs/Services/Identity/Identity.MessageBus/AssemblyReference.cs b/crs/Services/Identity/Identity.MessageBus/AssemblyReference.cs new file mode 100644 index 0000000..823e5b6 --- /dev/null +++ b/crs/Services/Identity/Identity.MessageBus/AssemblyReference.cs @@ -0,0 +1,6 @@ +namespace Identity.MessageBus; + +public static class AssemblyReference +{ + public static readonly Assembly Assembly = typeof(AssemblyReference).Assembly; +} diff --git a/crs/Services/Identity/Identity.MessageBus/GlobalUsings.cs b/crs/Services/Identity/Identity.MessageBus/GlobalUsings.cs new file mode 100644 index 0000000..fc41a7d --- /dev/null +++ b/crs/Services/Identity/Identity.MessageBus/GlobalUsings.cs @@ -0,0 +1 @@ +global using System.Reflection; \ No newline at end of file diff --git a/crs/Services/Identity/Identity.Persistence/Configurations/UserConfiguration.cs b/crs/Services/Identity/Identity.Persistence/Configurations/UserConfiguration.cs index e3623a3..30c2ada 100644 --- a/crs/Services/Identity/Identity.Persistence/Configurations/UserConfiguration.cs +++ b/crs/Services/Identity/Identity.Persistence/Configurations/UserConfiguration.cs @@ -43,7 +43,7 @@ public void Configure(EntityTypeBuilder builder) builder.OwnsOne(x => x.RefreshToken, refreshTokenBuilder => { refreshTokenBuilder.Property(x => x.Token) - .IsRequired(); + .IsRequired(); refreshTokenBuilder.Property(x => x.Expired) .IsRequired(); @@ -58,6 +58,14 @@ public void Configure(EntityTypeBuilder builder) builder.Property(x => x.Role).IsRequired(); - builder.Property(x => x.Gender).IsRequired(); + builder.Property(x => x.Role).HasConversion( + gender => gender.Name, + name => Role.FromName(name) + ).IsRequired(); + + builder.Property(x => x.Gender).HasConversion( + gender => gender.Name, + name => Gender.FromName(name) + ).IsRequired(); } } diff --git a/crs/Services/Identity/Identity.Persistence/GlobalUsings.cs b/crs/Services/Identity/Identity.Persistence/GlobalUsings.cs index 2527aec..4fb43da 100644 --- a/crs/Services/Identity/Identity.Persistence/GlobalUsings.cs +++ b/crs/Services/Identity/Identity.Persistence/GlobalUsings.cs @@ -9,3 +9,4 @@ global using Common.Extensions; global using Common.Serializers; global using System.Reflection; +global using Contracts.Enumurations; diff --git a/crs/Services/Identity/Identity.Persistence/Identity.Persistence.csproj b/crs/Services/Identity/Identity.Persistence/Identity.Persistence.csproj index 52ae820..3401511 100644 --- a/crs/Services/Identity/Identity.Persistence/Identity.Persistence.csproj +++ b/crs/Services/Identity/Identity.Persistence/Identity.Persistence.csproj @@ -8,6 +8,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/crs/Services/Identity/Identity.Persistence/Migrations/20240206153752_Add_Gender.Designer.cs b/crs/Services/Identity/Identity.Persistence/Migrations/20240206153752_Add_Gender.Designer.cs new file mode 100644 index 0000000..05aa545 --- /dev/null +++ b/crs/Services/Identity/Identity.Persistence/Migrations/20240206153752_Add_Gender.Designer.cs @@ -0,0 +1,147 @@ +// +using System; +using Identity.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Identity.Persistence.Migrations +{ + [DbContext(typeof(UserDbContext))] + [Migration("20240206153752_Add_Gender")] + partial class Add_Gender + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Common.Domain.Primitives.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Error") + .HasColumnType("text"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProcessedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("OutboxMessages"); + }); + + modelBuilder.Entity("Common.Domain.Primitives.OutboxMessageConsumer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("OutboxMessageConsumers"); + }); + + modelBuilder.Entity("Identity.Domain.UserAggregate.User", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("EmailConfirmationToken") + .IsRequired() + .HasColumnType("text"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Gender") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsEmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Identity.Domain.UserAggregate.User", b => + { + b.OwnsOne("Identity.Domain.UserAggregate.ValueObjects.RefreshToken", "RefreshToken", b1 => + { + b1.Property("UserId") + .HasColumnType("uuid"); + + b1.Property("Expired") + .HasColumnType("timestamp with time zone"); + + b1.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("UserId"); + + b1.ToTable("User"); + + b1.WithOwner() + .HasForeignKey("UserId"); + }); + + b.Navigation("RefreshToken"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/crs/Services/Identity/Identity.Persistence/Migrations/20240206153752_Add_Gender.cs b/crs/Services/Identity/Identity.Persistence/Migrations/20240206153752_Add_Gender.cs new file mode 100644 index 0000000..0a62a7d --- /dev/null +++ b/crs/Services/Identity/Identity.Persistence/Migrations/20240206153752_Add_Gender.cs @@ -0,0 +1,70 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Identity.Persistence.Migrations; + +/// +public partial class Add_Gender : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropPrimaryKey( + name: "PK_outboxMessageConsumers", + table: "outboxMessageConsumers"); + + migrationBuilder.RenameTable( + name: "outboxMessageConsumers", + newName: "OutboxMessageConsumers"); + + migrationBuilder.AlterColumn( + name: "Role", + table: "User", + type: "text", + nullable: false, + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AddColumn( + name: "Gender", + table: "User", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddPrimaryKey( + name: "PK_OutboxMessageConsumers", + table: "OutboxMessageConsumers", + column: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropPrimaryKey( + name: "PK_OutboxMessageConsumers", + table: "OutboxMessageConsumers"); + + migrationBuilder.DropColumn( + name: "Gender", + table: "User"); + + migrationBuilder.RenameTable( + name: "OutboxMessageConsumers", + newName: "outboxMessageConsumers"); + + migrationBuilder.AlterColumn( + name: "Role", + table: "User", + type: "integer", + nullable: false, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AddPrimaryKey( + name: "PK_outboxMessageConsumers", + table: "outboxMessageConsumers", + column: "Id"); + } +} diff --git a/crs/Services/Identity/Identity.Persistence/Migrations/UserDbContextModelSnapshot.cs b/crs/Services/Identity/Identity.Persistence/Migrations/UserDbContextModelSnapshot.cs index 2339377..5ed196e 100644 --- a/crs/Services/Identity/Identity.Persistence/Migrations/UserDbContextModelSnapshot.cs +++ b/crs/Services/Identity/Identity.Persistence/Migrations/UserDbContextModelSnapshot.cs @@ -17,11 +17,54 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("ProductVersion", "8.0.1") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("Common.Domain.Primitives.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Error") + .HasColumnType("text"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProcessedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("OutboxMessages"); + }); + + modelBuilder.Entity("Common.Domain.Primitives.OutboxMessageConsumer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("OutboxMessageConsumers"); + }); + modelBuilder.Entity("Identity.Domain.UserAggregate.User", b => { b.Property("Id") @@ -41,6 +84,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(50) .HasColumnType("character varying(50)"); + b.Property("Gender") + .IsRequired() + .HasColumnType("text"); + b.Property("IsEmailConfirmed") .HasColumnType("boolean"); @@ -58,55 +105,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b.Property("Role") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.ToTable("User", (string)null); - }); - - modelBuilder.Entity("Services.Common.Domain.Primitives.OutboxMessage", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Error") - .HasColumnType("text"); - - b.Property("Message") - .IsRequired() - .HasColumnType("text"); - - b.Property("ProcessedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("OutboxMessages"); - }); - - modelBuilder.Entity("Services.Common.Domain.Primitives.OutboxMessageConsumer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Name") + b.Property("Role") .IsRequired() .HasColumnType("text"); b.HasKey("Id"); - b.ToTable("outboxMessageConsumers"); + b.ToTable("User", (string)null); }); modelBuilder.Entity("Identity.Domain.UserAggregate.User", b => diff --git a/crs/Services/Identity/Identity.Persistence/Repositories/UserRepository.cs b/crs/Services/Identity/Identity.Persistence/Repositories/UserRepository.cs index 961f5ec..7589f53 100644 --- a/crs/Services/Identity/Identity.Persistence/Repositories/UserRepository.cs +++ b/crs/Services/Identity/Identity.Persistence/Repositories/UserRepository.cs @@ -40,10 +40,10 @@ await _dbContext .Set() .SingleOrDefaultAsync(u => u.Email == email, cancellationToken); - public async Task GetUserByIdAsync(UserId userId, CancellationToken cancellationToken = default) => + public async Task GetUserByIdAsync(UserId userId, CancellationToken cancellationToken = default) => await _dbContext .Set() - .SingleOrDefaultAsync(u => u.Id == userId, cancellationToken); + .SingleAsync(u => u.Id == userId, cancellationToken); public Task IsEmailConfirmedAsync(UserId userId, CancellationToken cancellationToken = default) { diff --git a/crs/Services/Identity/Identity.Persistence/UnitOfWork.cs b/crs/Services/Identity/Identity.Persistence/UnitOfWork.cs index aec3f52..88f6a8a 100644 --- a/crs/Services/Identity/Identity.Persistence/UnitOfWork.cs +++ b/crs/Services/Identity/Identity.Persistence/UnitOfWork.cs @@ -4,13 +4,13 @@ public sealed class UnitOfWork(UserDbContext dbContext) : IUnitOfWork { private readonly UserDbContext _dbContext = dbContext; - public int SaveChanges() + public int Commit() { SendDomainEventsToOutboxMessagesAsync().GetResult(); return _dbContext.SaveChanges(); } - public async Task SaveChangesAsync(CancellationToken cancellationToken = default) + public async Task CommitAsync(CancellationToken cancellationToken = default) { await SendDomainEventsToOutboxMessagesAsync(cancellationToken); return await _dbContext.SaveChangesAsync(cancellationToken); diff --git a/crs/Services/Identity/Identity.Persistence/UserDbContext.cs b/crs/Services/Identity/Identity.Persistence/UserDbContext.cs index 6c5a236..f15753d 100644 --- a/crs/Services/Identity/Identity.Persistence/UserDbContext.cs +++ b/crs/Services/Identity/Identity.Persistence/UserDbContext.cs @@ -1,14 +1,13 @@ namespace Identity.Persistence; -public sealed class UserDbContext(DbContextOptions options) - : DbContext(options) +public class UserDbContext(DbContextOptions options) : DbContext(options) { //IdentityDbContext public DbSet Users { get; set; } //Outbox pattern public DbSet OutboxMessages { get; set; } - public DbSet outboxMessageConsumers { get; set; } + public DbSet OutboxMessageConsumers { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/crs/Services/Identity/Identity.Presentation/V1/Controllers/UserController.cs b/crs/Services/Identity/Identity.Presentation/V1/Controllers/UserController.cs index 501c1d7..77a8ad5 100644 --- a/crs/Services/Identity/Identity.Presentation/V1/Controllers/UserController.cs +++ b/crs/Services/Identity/Identity.Presentation/V1/Controllers/UserController.cs @@ -3,6 +3,7 @@ using Identity.Application.Users.Commands.Register; using Identity.Application.Users.Commands.RetryConfirmEmailSend; using Identity.Application.Users.Commands.UpdateRefreshToken; +using Identity.Application.Users.Queries.GetGenders; using Identity.Application.Users.Queries.GetRoles; using Identity.Presentation.V1.Models; @@ -89,4 +90,14 @@ public async Task GetRoles() return result.IsSuccess ? Ok(result.Value) : HandleFailure(result); } + + [HttpGet("genders")] + public async Task GetGenders() + { + var query = new GetGendersQuery(); + + var result = await _sender.Send(query); + return result.IsSuccess ? Ok(result.Value) + : HandleFailure(result); + } } diff --git a/crs/tests/Catalog.UnitTests/Services/Brands/Commands/CreateBrandCommandHandlerTests.cs b/crs/tests/Catalog.UnitTests/Services/Brands/Commands/CreateBrandCommandHandlerTests.cs index 121446c..a1d519a 100644 --- a/crs/tests/Catalog.UnitTests/Services/Brands/Commands/CreateBrandCommandHandlerTests.cs +++ b/crs/tests/Catalog.UnitTests/Services/Brands/Commands/CreateBrandCommandHandlerTests.cs @@ -23,7 +23,7 @@ public sealed class CreateBrandCommandHandlerTests public void Test1() { int a = 4; - a.Should().Be(2); + a.Should().Be(4); } } diff --git a/img/Eshop_logo.png b/img/Eshop_logo.png deleted file mode 100644 index b5be050..0000000 Binary files a/img/Eshop_logo.png and /dev/null differ