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
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