diff --git a/Directory.Packages.props b/Directory.Packages.props index e3202d965..7caec7726 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -22,6 +22,7 @@ + diff --git a/src/Clean.Architecture.Core/ContributorAggregate/Contributor.cs b/src/Clean.Architecture.Core/ContributorAggregate/Contributor.cs index a3b5482a3..72ff42de9 100644 --- a/src/Clean.Architecture.Core/ContributorAggregate/Contributor.cs +++ b/src/Clean.Architecture.Core/ContributorAggregate/Contributor.cs @@ -9,9 +9,37 @@ public class Contributor(string name) : EntityBase, IAggregateRoot // See: https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/tutorials/primary-constructors#initialize-base-class public string Name { get; private set; } = Guard.Against.NullOrEmpty(name, nameof(name)); public ContributorStatus Status { get; private set; } = ContributorStatus.NotSet; + public PhoneNumber? PhoneNumber { get; private set; } + + public void SetPhoneNumber(string phoneNumber) + { + PhoneNumber = new PhoneNumber(string.Empty, phoneNumber, string.Empty); + } public void UpdateName(string newName) { Name = Guard.Against.NullOrEmpty(newName, nameof(newName)); } } + +public class PhoneNumber : ValueObject +{ + public string CountryCode { get; private set; } = string.Empty; + public string Number { get; private set; } = string.Empty; + public string? Extension { get; private set; } = string.Empty; + + public PhoneNumber(string countryCode, + string number, + string? extension) + { + CountryCode = countryCode; + Number = number; + Extension = extension; + } + protected override IEnumerable GetEqualityComponents() + { + yield return CountryCode; + yield return Number; + yield return Extension ?? String.Empty; + } +} diff --git a/src/Clean.Architecture.Infrastructure/Data/Config/ContributorConfiguration.cs b/src/Clean.Architecture.Infrastructure/Data/Config/ContributorConfiguration.cs index e7a18eb4b..2ccf18b6c 100644 --- a/src/Clean.Architecture.Infrastructure/Data/Config/ContributorConfiguration.cs +++ b/src/Clean.Architecture.Infrastructure/Data/Config/ContributorConfiguration.cs @@ -12,6 +12,8 @@ public void Configure(EntityTypeBuilder builder) .HasMaxLength(DataSchemaConstants.DEFAULT_NAME_LENGTH) .IsRequired(); + builder.OwnsOne(builder => builder.PhoneNumber); + builder.Property(x => x.Status) .HasConversion( x => x.Value, diff --git a/src/Clean.Architecture.Infrastructure/Data/Migrations/20231218143922_PhoneNumber.Designer.cs b/src/Clean.Architecture.Infrastructure/Data/Migrations/20231218143922_PhoneNumber.Designer.cs new file mode 100644 index 000000000..118fcf375 --- /dev/null +++ b/src/Clean.Architecture.Infrastructure/Data/Migrations/20231218143922_PhoneNumber.Designer.cs @@ -0,0 +1,72 @@ +// +using Clean.Architecture.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Clean.Architecture.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20231218143922_PhoneNumber")] + partial class PhoneNumber + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.0"); + + modelBuilder.Entity("Clean.Architecture.Core.ContributorAggregate.Contributor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Contributors"); + }); + + modelBuilder.Entity("Clean.Architecture.Core.ContributorAggregate.Contributor", b => + { + b.OwnsOne("Clean.Architecture.Core.ContributorAggregate.PhoneNumber", "PhoneNumber", b1 => + { + b1.Property("ContributorId") + .HasColumnType("INTEGER"); + + b1.Property("CountryCode") + .IsRequired() + .HasColumnType("TEXT"); + + b1.Property("Extension") + .HasColumnType("TEXT"); + + b1.Property("Number") + .IsRequired() + .HasColumnType("TEXT"); + + b1.HasKey("ContributorId"); + + b1.ToTable("Contributors"); + + b1.WithOwner() + .HasForeignKey("ContributorId"); + }); + + b.Navigation("PhoneNumber"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Clean.Architecture.Infrastructure/Data/Migrations/20231218143922_PhoneNumber.cs b/src/Clean.Architecture.Infrastructure/Data/Migrations/20231218143922_PhoneNumber.cs new file mode 100644 index 000000000..8bf938474 --- /dev/null +++ b/src/Clean.Architecture.Infrastructure/Data/Migrations/20231218143922_PhoneNumber.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Clean.Architecture.Infrastructure.Data.Migrations; + + /// + public partial class PhoneNumber : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Contributors", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(type: "TEXT", maxLength: 100, nullable: false), + Status = table.Column(type: "INTEGER", nullable: false), + PhoneNumber_CountryCode = table.Column(type: "TEXT", nullable: true), + PhoneNumber_Number = table.Column(type: "TEXT", nullable: true), + PhoneNumber_Extension = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Contributors", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Contributors"); + } + } diff --git a/src/Clean.Architecture.Infrastructure/Data/Migrations/AppDbContextModelSnapshot.cs b/src/Clean.Architecture.Infrastructure/Data/Migrations/AppDbContextModelSnapshot.cs new file mode 100644 index 000000000..60b081f62 --- /dev/null +++ b/src/Clean.Architecture.Infrastructure/Data/Migrations/AppDbContextModelSnapshot.cs @@ -0,0 +1,69 @@ +// +using Clean.Architecture.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Clean.Architecture.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + partial class AppDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.0"); + + modelBuilder.Entity("Clean.Architecture.Core.ContributorAggregate.Contributor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Contributors"); + }); + + modelBuilder.Entity("Clean.Architecture.Core.ContributorAggregate.Contributor", b => + { + b.OwnsOne("Clean.Architecture.Core.ContributorAggregate.PhoneNumber", "PhoneNumber", b1 => + { + b1.Property("ContributorId") + .HasColumnType("INTEGER"); + + b1.Property("CountryCode") + .IsRequired() + .HasColumnType("TEXT"); + + b1.Property("Extension") + .HasColumnType("TEXT"); + + b1.Property("Number") + .IsRequired() + .HasColumnType("TEXT"); + + b1.HasKey("ContributorId"); + + b1.ToTable("Contributors"); + + b1.WithOwner() + .HasForeignKey("ContributorId"); + }); + + b.Navigation("PhoneNumber"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Clean.Architecture.Infrastructure/Data/Queries/FakeListContributorsQueryService.cs b/src/Clean.Architecture.Infrastructure/Data/Queries/FakeListContributorsQueryService.cs index f1da19cc5..55c447641 100644 --- a/src/Clean.Architecture.Infrastructure/Data/Queries/FakeListContributorsQueryService.cs +++ b/src/Clean.Architecture.Infrastructure/Data/Queries/FakeListContributorsQueryService.cs @@ -8,8 +8,8 @@ public class FakeListContributorsQueryService : IListContributorsQueryService public Task> ListAsync() { List result = - [new ContributorDTO(1, "Fake Contributor 1"), - new ContributorDTO(2, "Fake Contributor 2")]; + [new ContributorDTO(1, "Fake Contributor 1", ""), + new ContributorDTO(2, "Fake Contributor 2", "")]; return Task.FromResult(result.AsEnumerable()); } diff --git a/src/Clean.Architecture.Infrastructure/Data/Queries/ListContributorsQueryService.cs b/src/Clean.Architecture.Infrastructure/Data/Queries/ListContributorsQueryService.cs index 08e312238..721d987d0 100644 --- a/src/Clean.Architecture.Infrastructure/Data/Queries/ListContributorsQueryService.cs +++ b/src/Clean.Architecture.Infrastructure/Data/Queries/ListContributorsQueryService.cs @@ -11,8 +11,9 @@ public class ListContributorsQueryService(AppDbContext _db) : IListContributorsQ public async Task> ListAsync() { // NOTE: This will fail if testing with EF InMemory provider - var result = await _db.Contributors.FromSqlRaw("SELECT Id, Name FROM Contributors") // don't fetch other big columns - .Select(c => new ContributorDTO(c.Id, c.Name)) + var result = await _db.Database.SqlQuery( + $"SELECT Id, Name, PhoneNumber_Number FROM Contributors") // don't fetch other big columns + //.Select(c => new ContributorDTO(c.Id, c.Name, c.PhoneNumber?.Number ?? "")) .ToListAsync(); return result; diff --git a/src/Clean.Architecture.UseCases/Contributors/ContributorDTO.cs b/src/Clean.Architecture.UseCases/Contributors/ContributorDTO.cs index dfa053e28..cfe44ef5d 100644 --- a/src/Clean.Architecture.UseCases/Contributors/ContributorDTO.cs +++ b/src/Clean.Architecture.UseCases/Contributors/ContributorDTO.cs @@ -1,2 +1,2 @@ namespace Clean.Architecture.UseCases.Contributors; -public record ContributorDTO(int Id, string Name); +public record ContributorDTO(int Id, string Name, string PhoneNumber); diff --git a/src/Clean.Architecture.UseCases/Contributors/Create/CreateContributorCommand.cs b/src/Clean.Architecture.UseCases/Contributors/Create/CreateContributorCommand.cs index 2c88bd5b5..97d370f60 100644 --- a/src/Clean.Architecture.UseCases/Contributors/Create/CreateContributorCommand.cs +++ b/src/Clean.Architecture.UseCases/Contributors/Create/CreateContributorCommand.cs @@ -6,4 +6,4 @@ namespace Clean.Architecture.UseCases.Contributors.Create; /// Create a new Contributor. /// /// -public record CreateContributorCommand(string Name) : Ardalis.SharedKernel.ICommand>; +public record CreateContributorCommand(string Name, string? PhoneNumber) : Ardalis.SharedKernel.ICommand>; diff --git a/src/Clean.Architecture.UseCases/Contributors/Create/CreateContributorHandler.cs b/src/Clean.Architecture.UseCases/Contributors/Create/CreateContributorHandler.cs index 9e86b8650..00829b449 100644 --- a/src/Clean.Architecture.UseCases/Contributors/Create/CreateContributorHandler.cs +++ b/src/Clean.Architecture.UseCases/Contributors/Create/CreateContributorHandler.cs @@ -11,6 +11,10 @@ public async Task> Handle(CreateContributorCommand request, CancellationToken cancellationToken) { var newContributor = new Contributor(request.Name); + if (!string.IsNullOrEmpty(request.PhoneNumber)) + { + newContributor.SetPhoneNumber(request.PhoneNumber); + } var createdItem = await _repository.AddAsync(newContributor, cancellationToken); return createdItem.Id; diff --git a/src/Clean.Architecture.UseCases/Contributors/Get/GetContributorHandler.cs b/src/Clean.Architecture.UseCases/Contributors/Get/GetContributorHandler.cs index efd8f9ab0..793bf7da5 100644 --- a/src/Clean.Architecture.UseCases/Contributors/Get/GetContributorHandler.cs +++ b/src/Clean.Architecture.UseCases/Contributors/Get/GetContributorHandler.cs @@ -17,6 +17,6 @@ public async Task> Handle(GetContributorQuery request, Ca var entity = await _repository.FirstOrDefaultAsync(spec, cancellationToken); if (entity == null) return Result.NotFound(); - return new ContributorDTO(entity.Id, entity.Name); + return new ContributorDTO(entity.Id, entity.Name, entity.PhoneNumber?.Number ?? ""); } } diff --git a/src/Clean.Architecture.UseCases/Contributors/Update/UpdateContributorHandler.cs b/src/Clean.Architecture.UseCases/Contributors/Update/UpdateContributorHandler.cs index 2ceb2061d..59d4c0612 100644 --- a/src/Clean.Architecture.UseCases/Contributors/Update/UpdateContributorHandler.cs +++ b/src/Clean.Architecture.UseCases/Contributors/Update/UpdateContributorHandler.cs @@ -19,6 +19,7 @@ public async Task> Handle(UpdateContributorCommand reques await _repository.UpdateAsync(existingContributor, cancellationToken); - return Result.Success(new ContributorDTO(existingContributor.Id, existingContributor.Name)); + return Result.Success(new ContributorDTO(existingContributor.Id, + existingContributor.Name, existingContributor.PhoneNumber?.Number ?? "")); } } diff --git a/src/Clean.Architecture.Web/Clean.Architecture.Web.csproj b/src/Clean.Architecture.Web/Clean.Architecture.Web.csproj index beedf6ca6..9bd2425e3 100644 --- a/src/Clean.Architecture.Web/Clean.Architecture.Web.csproj +++ b/src/Clean.Architecture.Web/Clean.Architecture.Web.csproj @@ -15,6 +15,7 @@ + @@ -24,4 +25,8 @@ + + + + diff --git a/src/Clean.Architecture.Web/Contributors/ContributorRecord.cs b/src/Clean.Architecture.Web/Contributors/ContributorRecord.cs index 635e393e1..d3be9abb4 100644 --- a/src/Clean.Architecture.Web/Contributors/ContributorRecord.cs +++ b/src/Clean.Architecture.Web/Contributors/ContributorRecord.cs @@ -1,3 +1,3 @@ namespace Clean.Architecture.Web.ContributorEndpoints; -public record ContributorRecord(int Id, string Name); +public record ContributorRecord(int Id, string Name, string PhoneNumber); diff --git a/src/Clean.Architecture.Web/Contributors/Create.CreateContributorRequest.cs b/src/Clean.Architecture.Web/Contributors/Create.CreateContributorRequest.cs index efc0e3ea3..c8b6a815b 100644 --- a/src/Clean.Architecture.Web/Contributors/Create.CreateContributorRequest.cs +++ b/src/Clean.Architecture.Web/Contributors/Create.CreateContributorRequest.cs @@ -8,4 +8,5 @@ public class CreateContributorRequest [Required] public string? Name { get; set; } + public string? PhoneNumber { get; set; } } diff --git a/src/Clean.Architecture.Web/Contributors/Create.cs b/src/Clean.Architecture.Web/Contributors/Create.cs index 0761af630..f3a4d388d 100644 --- a/src/Clean.Architecture.Web/Contributors/Create.cs +++ b/src/Clean.Architecture.Web/Contributors/Create.cs @@ -31,7 +31,8 @@ public override async Task HandleAsync( CreateContributorRequest request, CancellationToken cancellationToken) { - var result = await _mediator.Send(new CreateContributorCommand(request.Name!)); + var result = await _mediator.Send(new CreateContributorCommand(request.Name!, + request.PhoneNumber)); if(result.IsSuccess) { diff --git a/src/Clean.Architecture.Web/Contributors/GetById.cs b/src/Clean.Architecture.Web/Contributors/GetById.cs index e46114b3a..2f80e701a 100644 --- a/src/Clean.Architecture.Web/Contributors/GetById.cs +++ b/src/Clean.Architecture.Web/Contributors/GetById.cs @@ -36,7 +36,7 @@ public override async Task HandleAsync(GetContributorByIdRequest request, if (result.IsSuccess) { - Response = new ContributorRecord(result.Value.Id, result.Value.Name); + Response = new ContributorRecord(result.Value.Id, result.Value.Name, result.Value.PhoneNumber); } } } diff --git a/src/Clean.Architecture.Web/Contributors/List.cs b/src/Clean.Architecture.Web/Contributors/List.cs index c5a7fbceb..62c85215c 100644 --- a/src/Clean.Architecture.Web/Contributors/List.cs +++ b/src/Clean.Architecture.Web/Contributors/List.cs @@ -27,7 +27,7 @@ public override async Task HandleAsync(CancellationToken cancellationToken) { Response = new ContributorListResponse { - Contributors = result.Value.Select(c => new ContributorRecord(c.Id, c.Name)).ToList() + Contributors = result.Value.Select(c => new ContributorRecord(c.Id, c.Name, c.PhoneNumber)).ToList() }; } } diff --git a/src/Clean.Architecture.Web/Contributors/Update.cs b/src/Clean.Architecture.Web/Contributors/Update.cs index 53367f43e..8d0112371 100644 --- a/src/Clean.Architecture.Web/Contributors/Update.cs +++ b/src/Clean.Architecture.Web/Contributors/Update.cs @@ -48,7 +48,7 @@ public override async Task HandleAsync( if (queryResult.IsSuccess) { var dto = queryResult.Value; - Response = new UpdateContributorResponse(new ContributorRecord(dto.Id, dto.Name)); + Response = new UpdateContributorResponse(new ContributorRecord(dto.Id, dto.Name, dto.PhoneNumber)); return; } } diff --git a/src/Clean.Architecture.Web/api.http b/src/Clean.Architecture.Web/api.http index 57af82261..eff20b841 100644 --- a/src/Clean.Architecture.Web/api.http +++ b/src/Clean.Architecture.Web/api.http @@ -8,7 +8,7 @@ GET http://{{hostname}}:{{port}}/Contributors ### // Get a specific contributor -@id_to_get=1 +@id_to_get=8 GET http://{{hostname}}:{{port}}/Contributors/{{id_to_get}} ### @@ -18,8 +18,9 @@ POST http://{{hostname}}:{{port}}/Contributors Content-Type: application/json { - "name": "John Doe", - "email": "test@test.com" + "name": "John Doe 2", + "email": "test@test.com", + "phoneNumber": "1234567890" } ### diff --git a/tests/Clean.Architecture.UnitTests/UseCases/Contributors/CreateContributorHandlerHandle.cs b/tests/Clean.Architecture.UnitTests/UseCases/Contributors/CreateContributorHandlerHandle.cs index fb925593a..0fcc34fea 100644 --- a/tests/Clean.Architecture.UnitTests/UseCases/Contributors/CreateContributorHandlerHandle.cs +++ b/tests/Clean.Architecture.UnitTests/UseCases/Contributors/CreateContributorHandlerHandle.cs @@ -28,7 +28,7 @@ public async Task ReturnsSuccessGivenValidName() { _repository.AddAsync(Arg.Any(), Arg.Any()) .Returns(Task.FromResult(CreateContributor())); - var result = await _handler.Handle(new CreateContributorCommand(_testName), CancellationToken.None); + var result = await _handler.Handle(new CreateContributorCommand(_testName, null), CancellationToken.None); result.IsSuccess.Should().BeTrue(); }