diff --git a/CSharpBase/src/DMHexagonal/Catalog/DMHexagonal.Catalog.Core/BrandAggregate/Events/BrandCreated.cs b/CSharpBase/src/DMHexagonal/Catalog/DMHexagonal.Catalog.Core/BrandAggregate/Events/BrandCreated.cs index 4ef2d40..45e64b6 100644 --- a/CSharpBase/src/DMHexagonal/Catalog/DMHexagonal.Catalog.Core/BrandAggregate/Events/BrandCreated.cs +++ b/CSharpBase/src/DMHexagonal/Catalog/DMHexagonal.Catalog.Core/BrandAggregate/Events/BrandCreated.cs @@ -3,4 +3,4 @@ namespace DMHexagonal.Catalog.Core.BrandAggregate.Events; -public record BrandCreated(BrandId Id, BrandNameValue Name) : DomainEvent; \ No newline at end of file +public record BrandCreated(BrandId Id, BrandNameValue Name) : DomainEvent; diff --git a/CSharpBase/src/DMHexagonal/Catalog/DMHexagonal.Catalog.Core/BrandAggregate/Events/BrandDeleted.cs b/CSharpBase/src/DMHexagonal/Catalog/DMHexagonal.Catalog.Core/BrandAggregate/Events/BrandDeleted.cs index 00c01a1..896b046 100644 --- a/CSharpBase/src/DMHexagonal/Catalog/DMHexagonal.Catalog.Core/BrandAggregate/Events/BrandDeleted.cs +++ b/CSharpBase/src/DMHexagonal/Catalog/DMHexagonal.Catalog.Core/BrandAggregate/Events/BrandDeleted.cs @@ -3,4 +3,4 @@ namespace DMHexagonal.Catalog.Core.BrandAggregate.Events; -public record BrandDeleted(BrandId Id) : DomainEvent; \ No newline at end of file +public record BrandDeleted(BrandId Id) : DomainEvent; diff --git a/CSharpBase/src/DMHexagonal/Catalog/DMHexagonal.Catalog.Core/BrandAggregate/Events/BrandRenamed.cs b/CSharpBase/src/DMHexagonal/Catalog/DMHexagonal.Catalog.Core/BrandAggregate/Events/BrandRenamed.cs index 68ef0b3..dc8b4c8 100644 --- a/CSharpBase/src/DMHexagonal/Catalog/DMHexagonal.Catalog.Core/BrandAggregate/Events/BrandRenamed.cs +++ b/CSharpBase/src/DMHexagonal/Catalog/DMHexagonal.Catalog.Core/BrandAggregate/Events/BrandRenamed.cs @@ -3,4 +3,4 @@ namespace DMHexagonal.Catalog.Core.BrandAggregate.Events; -public record BrandRenamed(BrandId Id, BrandNameValue Name) : DomainEvent; \ No newline at end of file +public record BrandRenamed(BrandId Id, BrandNameValue Name) : DomainEvent; diff --git a/CSharpBase/src/DMHexagonal/Catalog/README.md b/CSharpBase/src/DMHexagonal/Catalog/README.md index 356c955..1854c61 100644 --- a/CSharpBase/src/DMHexagonal/Catalog/README.md +++ b/CSharpBase/src/DMHexagonal/Catalog/README.md @@ -1,10 +1,14 @@ # Inventory. -Пример основного поддомена инвентаризационной базы +Пример основного поддомена каталога товаров + +## Описание + +Образец основого поддомена, размещающий в себе простую бизнес-логику. ## Назначение -Инвентаризация предметов на складе, хранение названий и количество элементов. +Хранение в каталоге информации о всех товаров магазина, которые доступны для покупки. ## Сценарии diff --git a/CSharpBase/src/DMHexagonal/DMHexagonalConsoleApp/Scripts/DeveloperScript.cs b/CSharpBase/src/DMHexagonal/DMHexagonalConsoleApp/Scripts/DeveloperScript.cs index 3d3fe79..6a86e0b 100644 --- a/CSharpBase/src/DMHexagonal/DMHexagonalConsoleApp/Scripts/DeveloperScript.cs +++ b/CSharpBase/src/DMHexagonal/DMHexagonalConsoleApp/Scripts/DeveloperScript.cs @@ -16,5 +16,10 @@ public static void Run(IRegisterDispatcher dispatcher) { Console.WriteLine($"## Событие переименования товара c Id: {ev.Id} Новое имя: {ev.Name}"); }); + + dispatcher.RegisterHandler(ev => + { + Console.WriteLine($"## Событие изменения цены товара c Id: {ev.Id} Новая цена: {ev.Price} руб."); + }); } } \ No newline at end of file diff --git a/CSharpBase/src/DMHexagonal/README.md b/CSharpBase/src/DMHexagonal/README.md index 5e2e11b..4868c83 100644 --- a/CSharpBase/src/DMHexagonal/README.md +++ b/CSharpBase/src/DMHexagonal/README.md @@ -8,7 +8,7 @@ ## Содержимое -Пример основного поддомена инвентаризационной базы +Пример основного поддомена каталога товаров Образцовое консольное приложение diff --git a/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Application/ESCQRS.Catalog.Application.csproj b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Application/ESCQRS.Catalog.Application.csproj new file mode 100644 index 0000000..de08eec --- /dev/null +++ b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Application/ESCQRS.Catalog.Application.csproj @@ -0,0 +1,13 @@ + + + + net9.0 + enable + enable + + + + + + + diff --git a/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/Base/Abstractions/IDispatcher.cs b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/Base/Abstractions/IDispatcher.cs new file mode 100644 index 0000000..d696bac --- /dev/null +++ b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/Base/Abstractions/IDispatcher.cs @@ -0,0 +1,12 @@ +namespace ESCQRS.Catalog.Core.Base.Abstractions; + +public interface IDispatcher : IDispatchDispatcher +{ + void RegisterHandler(Action handler) + where T : IMessage; +} + +public interface IDispatchDispatcher +{ + void Dispatch(IEnumerable events); +} diff --git a/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/Base/Abstractions/IEventStore.cs b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/Base/Abstractions/IEventStore.cs new file mode 100644 index 0000000..61b5ee4 --- /dev/null +++ b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/Base/Abstractions/IEventStore.cs @@ -0,0 +1,8 @@ +namespace ESCQRS.Catalog.Core.Base.Abstractions; + +public interface IEventStore +{ + EventStream LoadEventStream(IId id); + + void AppendToStream(IId id, ICollection events, int expectedVersion); +} diff --git a/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/Base/Abstractions/IId.cs b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/Base/Abstractions/IId.cs new file mode 100644 index 0000000..4338dcc --- /dev/null +++ b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/Base/Abstractions/IId.cs @@ -0,0 +1,3 @@ +namespace ESCQRS.Catalog.Core.Base.Abstractions; + +public interface IId; diff --git a/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/Base/Abstractions/IMessage.cs b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/Base/Abstractions/IMessage.cs new file mode 100644 index 0000000..42f1f6b --- /dev/null +++ b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/Base/Abstractions/IMessage.cs @@ -0,0 +1,3 @@ +namespace ESCQRS.Catalog.Core.Base.Abstractions; + +public interface IMessage; diff --git a/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/Base/Abstractions/IStorage.cs b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/Base/Abstractions/IStorage.cs new file mode 100644 index 0000000..6f3261c --- /dev/null +++ b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/Base/Abstractions/IStorage.cs @@ -0,0 +1,16 @@ +namespace ESCQRS.Catalog.Core.Base.Abstractions; + +public interface IStorage : ITransactionalStorage + where T : EventAggregateRoot, new() +{ + T Load(IId id); + + void Save(T aggregate); +} + +public interface ITransactionalStorage +{ + public void BeginTransaction(); + public void Commit(); + public void Rollback(); +} diff --git a/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/Base/DomainEvent.cs b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/Base/DomainEvent.cs new file mode 100644 index 0000000..6bb81cf --- /dev/null +++ b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/Base/DomainEvent.cs @@ -0,0 +1,5 @@ +using ESCQRS.Catalog.Core.Base.Abstractions; + +namespace ESCQRS.Catalog.Core.Base; + +public record DomainEvent : IMessage; diff --git a/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/Base/EventAggregateRoot.cs b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/Base/EventAggregateRoot.cs new file mode 100644 index 0000000..109d777 --- /dev/null +++ b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/Base/EventAggregateRoot.cs @@ -0,0 +1,30 @@ +using ESCQRS.Catalog.Core.Base.Abstractions; + +namespace ESCQRS.Catalog.Core.Base; + +public abstract class EventAggregateRoot +{ + private readonly List _changes = new(); + + public abstract IId Id { get; } + + public int Version { get; private set; } = -1; + + public void Apply(EventStream stream) + { + foreach (var @event in stream.Events) Mutate(@event); + Version = stream.Version; + } + + public ICollection Changes() => _changes; + + public void Reset() => _changes.Clear(); + + protected void ApplyChange(IMessage @event) + { + Mutate(@event); + _changes.Add(@event); + } + + protected abstract void Mutate(IMessage @event); +} \ No newline at end of file diff --git a/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/Base/EventStream.cs b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/Base/EventStream.cs new file mode 100644 index 0000000..5f8cc0e --- /dev/null +++ b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/Base/EventStream.cs @@ -0,0 +1,5 @@ +using ESCQRS.Catalog.Core.Base.Abstractions; + +namespace ESCQRS.Catalog.Core.Base; + +public record EventStream(ICollection Events, int Version = -1); diff --git a/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/BrandAggregate/BrandItem.cs b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/BrandAggregate/BrandItem.cs new file mode 100644 index 0000000..062956a --- /dev/null +++ b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/BrandAggregate/BrandItem.cs @@ -0,0 +1,41 @@ +using ESCQRS.Catalog.Core.Base; +using ESCQRS.Catalog.Core.Base.Abstractions; +using ESCQRS.Catalog.Core.BrandAggregate.Events; +using ESCQRS.Catalog.Core.BrandAggregate.Values; + +namespace ESCQRS.Catalog.Core.BrandAggregate; + +public class BrandItem : EventAggregateRoot +{ + private BrandId _id = null!; + public override BrandId Id => _id; + + public static BrandItem Create(BrandId id, BrandNameValue name) + { + var result = new BrandItem(); + result.ApplyChange(new BrandCreated(id, name)); + return result; + } + + public void Rename(BrandNameValue newName) + { + ApplyChange(new BrandRenamed(Id, newName)); + } + + public void Delete() + { + ApplyChange(new BrandDeleted(Id)); + } + + protected override void Mutate(IMessage @event) => + ((dynamic)this).when((dynamic)@event); + + private void when(BrandCreated ev) + { + _id = ev.Id; + } + + private void when(BrandRenamed ev) { } + + private void when(BrandDeleted ev) { } +} \ No newline at end of file diff --git a/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/BrandAggregate/Events/BrandCreated.cs b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/BrandAggregate/Events/BrandCreated.cs new file mode 100644 index 0000000..c13891d --- /dev/null +++ b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/BrandAggregate/Events/BrandCreated.cs @@ -0,0 +1,6 @@ +using ESCQRS.Catalog.Core.Base; +using ESCQRS.Catalog.Core.BrandAggregate.Values; + +namespace ESCQRS.Catalog.Core.BrandAggregate.Events; + +public record BrandCreated(BrandId Id, BrandNameValue Name) : DomainEvent; diff --git a/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/BrandAggregate/Events/BrandDeleted.cs b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/BrandAggregate/Events/BrandDeleted.cs new file mode 100644 index 0000000..055aa8a --- /dev/null +++ b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/BrandAggregate/Events/BrandDeleted.cs @@ -0,0 +1,6 @@ +using ESCQRS.Catalog.Core.Base; +using ESCQRS.Catalog.Core.BrandAggregate.Values; + +namespace ESCQRS.Catalog.Core.BrandAggregate.Events; + +public record BrandDeleted(BrandId Id) : DomainEvent; diff --git a/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/BrandAggregate/Events/BrandRenamed.cs b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/BrandAggregate/Events/BrandRenamed.cs new file mode 100644 index 0000000..c16ccc2 --- /dev/null +++ b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/BrandAggregate/Events/BrandRenamed.cs @@ -0,0 +1,6 @@ +using ESCQRS.Catalog.Core.Base; +using ESCQRS.Catalog.Core.BrandAggregate.Values; + +namespace ESCQRS.Catalog.Core.BrandAggregate.Events; + +public record BrandRenamed(BrandId Id, BrandNameValue Name) : DomainEvent; diff --git a/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/BrandAggregate/Values/BrandId.cs b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/BrandAggregate/Values/BrandId.cs new file mode 100644 index 0000000..d3c8730 --- /dev/null +++ b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/BrandAggregate/Values/BrandId.cs @@ -0,0 +1,14 @@ +using ESCQRS.Catalog.Core.Base.Abstractions; +using Kanadeiar.Common.Functionals; + +namespace ESCQRS.Catalog.Core.BrandAggregate.Values; + +public record BrandId(Guid Value) : IId +{ + public static BrandId New() => new(Guid.NewGuid()); + + public Guid Value { get; } = Value.Require(Value != Guid.Empty, () => + throw new ApplicationException("Номер идентификатора должен быть назначен")); + + public override string ToString() => Value.ToString(); +} \ No newline at end of file diff --git a/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/BrandAggregate/Values/BrandNameValue.cs b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/BrandAggregate/Values/BrandNameValue.cs new file mode 100644 index 0000000..15cce6f --- /dev/null +++ b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/BrandAggregate/Values/BrandNameValue.cs @@ -0,0 +1,11 @@ +using Kanadeiar.Common.Functionals; + +namespace ESCQRS.Catalog.Core.BrandAggregate.Values; + +public record BrandNameValue(string Value) +{ + public string Value { get; } = Value.Require(Value.Length is >= 3 and <= 300, () => + throw new ApplicationException("Название бренда товаров должно быть длинной от 3 до 300 символов")); + + public override string ToString() => $"{Value}"; +} \ No newline at end of file diff --git a/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/CategoryAggregate/CategoryItem.cs b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/CategoryAggregate/CategoryItem.cs new file mode 100644 index 0000000..36deea4 --- /dev/null +++ b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/CategoryAggregate/CategoryItem.cs @@ -0,0 +1,5 @@ +namespace ESCQRS.Catalog.Core.CategoryAggregate; + +public class CategoryItem +{ +} \ No newline at end of file diff --git a/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/CategoryAggregate/Events/CategoryCreated.cs b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/CategoryAggregate/Events/CategoryCreated.cs new file mode 100644 index 0000000..3a72b91 --- /dev/null +++ b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/CategoryAggregate/Events/CategoryCreated.cs @@ -0,0 +1,6 @@ +using ESCQRS.Catalog.Core.Base; +using ESCQRS.Catalog.Core.CategoryAggregate.Values; + +namespace ESCQRS.Catalog.Core.CategoryAggregate.Events; + +public record CategoryCreated(CategoryId Id, CategoryNameValue Name) : DomainEvent; diff --git a/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/CategoryAggregate/Values/CategoryId.cs b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/CategoryAggregate/Values/CategoryId.cs new file mode 100644 index 0000000..dd476a4 --- /dev/null +++ b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/CategoryAggregate/Values/CategoryId.cs @@ -0,0 +1,14 @@ +using ESCQRS.Catalog.Core.Base.Abstractions; +using Kanadeiar.Common.Functionals; + +namespace ESCQRS.Catalog.Core.CategoryAggregate.Values; + +public record CategoryId(Guid Value) : IId +{ + public static CategoryId New() => new(Guid.NewGuid()); + + public Guid Value { get; } = Value.Require(Value != Guid.Empty, () => + throw new ApplicationException("Номер идентификатора должен быть назначен")); + + public override string ToString() => Value.ToString(); +} \ No newline at end of file diff --git a/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/CategoryAggregate/Values/CategoryNameValue.cs b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/CategoryAggregate/Values/CategoryNameValue.cs new file mode 100644 index 0000000..5eaac94 --- /dev/null +++ b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/CategoryAggregate/Values/CategoryNameValue.cs @@ -0,0 +1,11 @@ +using Kanadeiar.Common.Functionals; + +namespace ESCQRS.Catalog.Core.CategoryAggregate.Values; + +public record CategoryNameValue(string Value) +{ + public string Value { get; } = Value.Require(Value.Length is >= 3 and <= 300, () => + throw new ApplicationException("Название категории товаров должно быть длинной от 3 до 300 символов")); + + public override string ToString() => $"{Value}"; +} \ No newline at end of file diff --git a/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/ESCQRS.Catalog.Core.csproj b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/ESCQRS.Catalog.Core.csproj new file mode 100644 index 0000000..00ce45d --- /dev/null +++ b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/ESCQRS.Catalog.Core.csproj @@ -0,0 +1,19 @@ + + + + net9.0 + enable + enable + + + + + Common\%(RecursiveDir)%(FileName)%(Extension) + + + + + + + + diff --git a/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/ProductAggregate/ProductItem.cs b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/ProductAggregate/ProductItem.cs new file mode 100644 index 0000000..59ba570 --- /dev/null +++ b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/ProductAggregate/ProductItem.cs @@ -0,0 +1,5 @@ +namespace ESCQRS.Catalog.Core.ProductAggregate; + +public class ProductItem +{ +} \ No newline at end of file diff --git a/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/ProductAggregate/Values/PriveValue.cs b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/ProductAggregate/Values/PriveValue.cs new file mode 100644 index 0000000..0ef2c7e --- /dev/null +++ b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/ProductAggregate/Values/PriveValue.cs @@ -0,0 +1,11 @@ +using Kanadeiar.Common.Functionals; + +namespace ESCQRS.Catalog.Core.ProductAggregate.Values; + +public record PriceValue(decimal Value) +{ + public decimal Value { get; } = Value.Require(Value is >= 0M and <= 100000M, () => + throw new ApplicationException("Цена товара должна быть от 0 до 100000 руб.")); + + public override string ToString() => $"{Value}"; +} \ No newline at end of file diff --git a/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/ProductAggregate/Values/ProductId.cs b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/ProductAggregate/Values/ProductId.cs new file mode 100644 index 0000000..f8c9716 --- /dev/null +++ b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/ProductAggregate/Values/ProductId.cs @@ -0,0 +1,14 @@ +using ESCQRS.Catalog.Core.Base.Abstractions; +using Kanadeiar.Common.Functionals; + +namespace ESCQRS.Catalog.Core.ProductAggregate.Values; + +public record ProductId(Guid Value) : IId +{ + public static ProductId New() => new(Guid.NewGuid()); + + public Guid Value { get; } = Value.Require(Value != Guid.Empty, () => + throw new ApplicationException("Номер идентификатора должен быть назначен")); + + public override string ToString() => Value.ToString(); +} \ No newline at end of file diff --git a/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/ProductAggregate/Values/ProductNameValue.cs b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/ProductAggregate/Values/ProductNameValue.cs new file mode 100644 index 0000000..5b0787b --- /dev/null +++ b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/ProductAggregate/Values/ProductNameValue.cs @@ -0,0 +1,11 @@ +using Kanadeiar.Common.Functionals; + +namespace ESCQRS.Catalog.Core.ProductAggregate.Values; + +public record ProductNameValue(string Value) +{ + public string Value { get; } = Value.Require(Value.Length is >= 3 and <= 300, () => + throw new ApplicationException("Название категории товаров должно быть длинной от 3 до 300 символов")); + + public override string ToString() => $"{Value}"; +} \ No newline at end of file diff --git a/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/ReadModel/BrandProjection.cs b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/ReadModel/BrandProjection.cs new file mode 100644 index 0000000..515649e --- /dev/null +++ b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/ReadModel/BrandProjection.cs @@ -0,0 +1,17 @@ +using ESCQRS.Catalog.Core.BrandAggregate.Events; +using ESCQRS.Catalog.Core.BrandAggregate.Values; + +namespace ESCQRS.Catalog.Core.ReadModel; + +public record BrandProjection(BrandId Id, BrandNameValue Name, bool IsDeleted = false) +{ + public BrandProjection(BrandCreated ev) : this(ev.Id, ev.Name) { } + + public BrandProjection Apply(BrandRenamed ev) => + this with { Name = ev.Name }; + + public BrandProjection Apply(BrandDeleted ev) => + this with { IsDeleted = true }; + + public override string ToString() => $"{Name}"; +} \ No newline at end of file diff --git a/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/ReadModel/CategoryProjection.cs b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/ReadModel/CategoryProjection.cs new file mode 100644 index 0000000..dc9f0a5 --- /dev/null +++ b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/ReadModel/CategoryProjection.cs @@ -0,0 +1,5 @@ +namespace ESCQRS.Catalog.Core.ReadModel; + +public record CategoryProjection +{ +} \ No newline at end of file diff --git a/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/ReadModel/ProductProjection.cs b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/ReadModel/ProductProjection.cs new file mode 100644 index 0000000..b983070 --- /dev/null +++ b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Core/ReadModel/ProductProjection.cs @@ -0,0 +1,5 @@ +namespace ESCQRS.Catalog.Core.ReadModel; + +public record ProductProjection +{ +} \ No newline at end of file diff --git a/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Infra/ESCQRS.Catalog.Infra.csproj b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Infra/ESCQRS.Catalog.Infra.csproj new file mode 100644 index 0000000..6a5495b --- /dev/null +++ b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Infra/ESCQRS.Catalog.Infra.csproj @@ -0,0 +1,13 @@ + + + + net9.0 + enable + enable + + + + + + + diff --git a/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Presentation/ESCQRS.Catalog.Presentation.csproj b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Presentation/ESCQRS.Catalog.Presentation.csproj new file mode 100644 index 0000000..d380514 --- /dev/null +++ b/CSharpBase/src/ESCQRS/Catalog/ESCQRS.Catalog.Presentation/ESCQRS.Catalog.Presentation.csproj @@ -0,0 +1,13 @@ + + + + net9.0 + enable + enable + + + + + + + diff --git a/CSharpBase/src/ESCQRS/Catalog/README.md b/CSharpBase/src/ESCQRS/Catalog/README.md new file mode 100644 index 0000000..b9b2f27 --- /dev/null +++ b/CSharpBase/src/ESCQRS/Catalog/README.md @@ -0,0 +1,38 @@ +# Inventory. + +Пример важного основного поддомена каталога товаров + +## Описание + +Образец основого поддомена, содержащий в себе важную бизнес-логику. + +## Назначение + +Хранение в каталоге информации о всех товаров магазина, которые доступны для покупки. + +## Сценарии + +- Просмотр всего каталога товаров + +- Фильтрация товаров + +- Редактирование названия любого товара + +- Редактирование цены любого товара + +- Удаление любого товара из каталога товаров + +## Инструменты + +1) Предметное проектирование. + +2) Модель предметной области, основанная на событиях. + +3) CQRS. + +4) Пирамида тестирования. + +5) Простейший диспетчер событий. + +### Использовать как образец. + diff --git a/CSharpBase/src/ESCQRS/ESCQRSConsoleApp/ESCQRSConsoleApp.csproj b/CSharpBase/src/ESCQRS/ESCQRSConsoleApp/ESCQRSConsoleApp.csproj new file mode 100644 index 0000000..d4bbe2e --- /dev/null +++ b/CSharpBase/src/ESCQRS/ESCQRSConsoleApp/ESCQRSConsoleApp.csproj @@ -0,0 +1,21 @@ + + + + Exe + net9.0 + enable + enable + + + + + Common\%(RecursiveDir)%(FileName)%(Extension) + + + + + + + + + diff --git a/CSharpBase/src/ESCQRS/ESCQRSConsoleApp/Program.cs b/CSharpBase/src/ESCQRS/ESCQRSConsoleApp/Program.cs new file mode 100644 index 0000000..65a33c7 --- /dev/null +++ b/CSharpBase/src/ESCQRS/ESCQRSConsoleApp/Program.cs @@ -0,0 +1,8 @@ +using Kanadeiar.Common; + +ConsoleHelper.PrintHeader("Образец важного основного поддомена на языке C#.", "Предметно-ориентированное проектирование на платформе .NET. Примеры приложений."); +ConsoleHelper.PrintLine("Образец: модель предметной области, основанная на событиях, CQRS и пирамида тестирования."); + + + +ConsoleHelper.PrintFooter(); \ No newline at end of file diff --git a/CSharpBase/src/ESCQRS/README.md b/CSharpBase/src/ESCQRS/README.md new file mode 100644 index 0000000..b4e876e --- /dev/null +++ b/CSharpBase/src/ESCQRS/README.md @@ -0,0 +1,41 @@ +# Event Source CQRS + +Пример ограниченного контекста + +## Описание + +Пример основного ограниченного контекта предметного проектирования. + +## Содержимое + +Пример важного основного поддомена каталога товаров + +Образцовое консольное приложение + +## Глоссарий + +Бренд - Brand - бренд товаров + +Категория - Category - категория товаров + +Товар - Product - товар каталога товаров, доступный для покупки + +Фильтр - Filter - фильтрация товаров с целью более быстрого поиска нужного + +Переименовать - Rename - изменить название товара + +Цена - Price - цена товара в каталоге товаров + +Консоль - Console - консольный интерфейс взаимодействия с пользователем. + +## Инструменты + +1) Предметное проектирование. + +2) Модель предметной области, основанная на событиях. + +3) CQRS. + +4) Пирамида тестирования. + +### Использовать как образец. diff --git a/CSharpBase/tests/ESCQRS/Catalog/ESCQRS.Catalog.Core.Tests.Unit/ESCQRS.Catalog.Core.Tests.Unit.csproj b/CSharpBase/tests/ESCQRS/Catalog/ESCQRS.Catalog.Core.Tests.Unit/ESCQRS.Catalog.Core.Tests.Unit.csproj new file mode 100644 index 0000000..b1a837b --- /dev/null +++ b/CSharpBase/tests/ESCQRS/Catalog/ESCQRS.Catalog.Core.Tests.Unit/ESCQRS.Catalog.Core.Tests.Unit.csproj @@ -0,0 +1,39 @@ + + + + net9.0 + enable + enable + false + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + Common\%(RecursiveDir)%(FileName)%(Extension) + + + + + + + + diff --git a/CSharpBase/tests/ESCQRS/Catalog/ESCQRS.Catalog.Presentation.Tests.Story/ESCQRS.Catalog.Presentation.Tests.Story.csproj b/CSharpBase/tests/ESCQRS/Catalog/ESCQRS.Catalog.Presentation.Tests.Story/ESCQRS.Catalog.Presentation.Tests.Story.csproj new file mode 100644 index 0000000..b1a837b --- /dev/null +++ b/CSharpBase/tests/ESCQRS/Catalog/ESCQRS.Catalog.Presentation.Tests.Story/ESCQRS.Catalog.Presentation.Tests.Story.csproj @@ -0,0 +1,39 @@ + + + + net9.0 + enable + enable + false + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + Common\%(RecursiveDir)%(FileName)%(Extension) + + + + + + + + diff --git a/CSharpBase/tests/ESCQRS/Catalog/ESCQRS.Catalog.Presentation.Tests.Story/UnitTest1.cs b/CSharpBase/tests/ESCQRS/Catalog/ESCQRS.Catalog.Presentation.Tests.Story/UnitTest1.cs new file mode 100644 index 0000000..dda14cc --- /dev/null +++ b/CSharpBase/tests/ESCQRS/Catalog/ESCQRS.Catalog.Presentation.Tests.Story/UnitTest1.cs @@ -0,0 +1,11 @@ +namespace ESCQRS.Catalog.Presentation.Tests.Story +{ + public class UnitTest1 + { + [Fact] + public void Test1() + { + + } + } +} diff --git a/DotNetDDDExamples.sln b/DotNetDDDExamples.sln index ebb697a..b552ec0 100644 --- a/DotNetDDDExamples.sln +++ b/DotNetDDDExamples.sln @@ -114,6 +114,34 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DMHexagonal.Catalog.Present EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DMHexagonal.Catalog.Presentation.Tests.Story", "CSharpBase\tests\DMHexagonal\Catalog\DMHexagonal.Catalog.Presentation.Tests.Story\DMHexagonal.Catalog.Presentation.Tests.Story.csproj", "{250FFA75-2E70-4AEC-BCD9-E8247A1B49BB}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ESCQRS", "ESCQRS", "{96195664-61C5-4ACD-8BD0-F23DD59C05EF}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ESCQRS", "ESCQRS", "{BACA981E-15DD-4ED7-92D4-C3C1F2430388}" + ProjectSection(SolutionItems) = preProject + CSharpBase\src\ESCQRS\README.md = CSharpBase\src\ESCQRS\README.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Catalog", "Catalog", "{867DD46B-9383-486E-AF3F-C7C5F949B7FD}" + ProjectSection(SolutionItems) = preProject + CSharpBase\src\ESCQRS\Catalog\README.md = CSharpBase\src\ESCQRS\Catalog\README.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Catalog", "Catalog", "{F2BBD92E-22CC-4B48-8526-5BD028B8CDC0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ESCQRSConsoleApp", "CSharpBase\src\ESCQRS\ESCQRSConsoleApp\ESCQRSConsoleApp.csproj", "{BACE99E5-B2B7-4552-A5AC-4D808212311E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ESCQRS.Catalog.Core", "CSharpBase\src\ESCQRS\Catalog\ESCQRS.Catalog.Core\ESCQRS.Catalog.Core.csproj", "{50AE1D84-378A-4E24-B7CB-F77092773591}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ESCQRS.Catalog.Application", "CSharpBase\src\ESCQRS\Catalog\ESCQRS.Catalog.Application\ESCQRS.Catalog.Application.csproj", "{368DD323-5A44-42B6-B6E4-8DEFB234E658}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ESCQRS.Catalog.Infra", "CSharpBase\src\ESCQRS\Catalog\ESCQRS.Catalog.Infra\ESCQRS.Catalog.Infra.csproj", "{7F5B2382-8FA0-4484-878F-500DADF153A8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ESCQRS.Catalog.Presentation", "CSharpBase\src\ESCQRS\Catalog\ESCQRS.Catalog.Presentation\ESCQRS.Catalog.Presentation.csproj", "{CC156A3D-229E-4DA1-B682-8BD6DD2545A7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ESCQRS.Catalog.Core.Tests.Unit", "CSharpBase\tests\ESCQRS\Catalog\ESCQRS.Catalog.Core.Tests.Unit\ESCQRS.Catalog.Core.Tests.Unit.csproj", "{74B3D207-4CAB-409C-B41D-8CAE42582356}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ESCQRS.Catalog.Presentation.Tests.Story", "CSharpBase\tests\ESCQRS\Catalog\ESCQRS.Catalog.Presentation.Tests.Story\ESCQRS.Catalog.Presentation.Tests.Story.csproj", "{7F16AE4A-CBD1-4D4A-A0FA-EF8A03E85D6B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -208,6 +236,34 @@ Global {250FFA75-2E70-4AEC-BCD9-E8247A1B49BB}.Debug|Any CPU.Build.0 = Debug|Any CPU {250FFA75-2E70-4AEC-BCD9-E8247A1B49BB}.Release|Any CPU.ActiveCfg = Release|Any CPU {250FFA75-2E70-4AEC-BCD9-E8247A1B49BB}.Release|Any CPU.Build.0 = Release|Any CPU + {BACE99E5-B2B7-4552-A5AC-4D808212311E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BACE99E5-B2B7-4552-A5AC-4D808212311E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BACE99E5-B2B7-4552-A5AC-4D808212311E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BACE99E5-B2B7-4552-A5AC-4D808212311E}.Release|Any CPU.Build.0 = Release|Any CPU + {50AE1D84-378A-4E24-B7CB-F77092773591}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {50AE1D84-378A-4E24-B7CB-F77092773591}.Debug|Any CPU.Build.0 = Debug|Any CPU + {50AE1D84-378A-4E24-B7CB-F77092773591}.Release|Any CPU.ActiveCfg = Release|Any CPU + {50AE1D84-378A-4E24-B7CB-F77092773591}.Release|Any CPU.Build.0 = Release|Any CPU + {368DD323-5A44-42B6-B6E4-8DEFB234E658}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {368DD323-5A44-42B6-B6E4-8DEFB234E658}.Debug|Any CPU.Build.0 = Debug|Any CPU + {368DD323-5A44-42B6-B6E4-8DEFB234E658}.Release|Any CPU.ActiveCfg = Release|Any CPU + {368DD323-5A44-42B6-B6E4-8DEFB234E658}.Release|Any CPU.Build.0 = Release|Any CPU + {7F5B2382-8FA0-4484-878F-500DADF153A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F5B2382-8FA0-4484-878F-500DADF153A8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F5B2382-8FA0-4484-878F-500DADF153A8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F5B2382-8FA0-4484-878F-500DADF153A8}.Release|Any CPU.Build.0 = Release|Any CPU + {CC156A3D-229E-4DA1-B682-8BD6DD2545A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CC156A3D-229E-4DA1-B682-8BD6DD2545A7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CC156A3D-229E-4DA1-B682-8BD6DD2545A7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CC156A3D-229E-4DA1-B682-8BD6DD2545A7}.Release|Any CPU.Build.0 = Release|Any CPU + {74B3D207-4CAB-409C-B41D-8CAE42582356}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {74B3D207-4CAB-409C-B41D-8CAE42582356}.Debug|Any CPU.Build.0 = Debug|Any CPU + {74B3D207-4CAB-409C-B41D-8CAE42582356}.Release|Any CPU.ActiveCfg = Release|Any CPU + {74B3D207-4CAB-409C-B41D-8CAE42582356}.Release|Any CPU.Build.0 = Release|Any CPU + {7F16AE4A-CBD1-4D4A-A0FA-EF8A03E85D6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F16AE4A-CBD1-4D4A-A0FA-EF8A03E85D6B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F16AE4A-CBD1-4D4A-A0FA-EF8A03E85D6B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F16AE4A-CBD1-4D4A-A0FA-EF8A03E85D6B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -253,6 +309,17 @@ Global {F98E9A53-17D1-4BAA-B59F-8F5BCFD794E3} = {88C7C734-BB46-494B-8C76-58A5C4DD8C44} {CADA4B18-C5FC-4090-91CF-B3355C98FB28} = {001BD9F7-A22F-4ED9-9BCE-234BC7A65C53} {250FFA75-2E70-4AEC-BCD9-E8247A1B49BB} = {88C7C734-BB46-494B-8C76-58A5C4DD8C44} + {96195664-61C5-4ACD-8BD0-F23DD59C05EF} = {4D0905FF-7A9E-40D2-9531-49540C1346EF} + {BACA981E-15DD-4ED7-92D4-C3C1F2430388} = {CF6817F9-39C7-4419-B95B-7ACF9F75573B} + {867DD46B-9383-486E-AF3F-C7C5F949B7FD} = {BACA981E-15DD-4ED7-92D4-C3C1F2430388} + {F2BBD92E-22CC-4B48-8526-5BD028B8CDC0} = {96195664-61C5-4ACD-8BD0-F23DD59C05EF} + {BACE99E5-B2B7-4552-A5AC-4D808212311E} = {BACA981E-15DD-4ED7-92D4-C3C1F2430388} + {50AE1D84-378A-4E24-B7CB-F77092773591} = {867DD46B-9383-486E-AF3F-C7C5F949B7FD} + {368DD323-5A44-42B6-B6E4-8DEFB234E658} = {867DD46B-9383-486E-AF3F-C7C5F949B7FD} + {7F5B2382-8FA0-4484-878F-500DADF153A8} = {867DD46B-9383-486E-AF3F-C7C5F949B7FD} + {CC156A3D-229E-4DA1-B682-8BD6DD2545A7} = {867DD46B-9383-486E-AF3F-C7C5F949B7FD} + {74B3D207-4CAB-409C-B41D-8CAE42582356} = {F2BBD92E-22CC-4B48-8526-5BD028B8CDC0} + {7F16AE4A-CBD1-4D4A-A0FA-EF8A03E85D6B} = {F2BBD92E-22CC-4B48-8526-5BD028B8CDC0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {9CF52F65-07D0-4D06-B813-6177B3867551}