Skip to content

Commit

Permalink
Add example value object for phone number
Browse files Browse the repository at this point in the history
  • Loading branch information
ardalis committed Dec 18, 2023
1 parent 4ee1967 commit 5286712
Show file tree
Hide file tree
Showing 22 changed files with 240 additions and 17 deletions.
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" />
Expand Down
28 changes: 28 additions & 0 deletions src/Clean.Architecture.Core/ContributorAggregate/Contributor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<object> GetEqualityComponents()
{
yield return CountryCode;
yield return Number;
yield return Extension ?? String.Empty;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ public void Configure(EntityTypeBuilder<Contributor> builder)
.HasMaxLength(DataSchemaConstants.DEFAULT_NAME_LENGTH)
.IsRequired();

builder.OwnsOne(builder => builder.PhoneNumber);

builder.Property(x => x.Status)
.HasConversion(
x => x.Value,
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace Clean.Architecture.Infrastructure.Data.Migrations;

/// <inheritdoc />
public partial class PhoneNumber : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Contributors",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
Status = table.Column<int>(type: "INTEGER", nullable: false),
PhoneNumber_CountryCode = table.Column<string>(type: "TEXT", nullable: true),
PhoneNumber_Number = table.Column<string>(type: "TEXT", nullable: true),
PhoneNumber_Extension = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Contributors", x => x.Id);
});
}

/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Contributors");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// <auto-generated />
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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");

b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");

b.Property<int>("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<int>("ContributorId")
.HasColumnType("INTEGER");

b1.Property<string>("CountryCode")
.IsRequired()
.HasColumnType("TEXT");

b1.Property<string>("Extension")
.HasColumnType("TEXT");

b1.Property<string>("Number")
.IsRequired()
.HasColumnType("TEXT");

b1.HasKey("ContributorId");

b1.ToTable("Contributors");

b1.WithOwner()
.HasForeignKey("ContributorId");
});

b.Navigation("PhoneNumber");
});
#pragma warning restore 612, 618
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ public class FakeListContributorsQueryService : IListContributorsQueryService
public Task<IEnumerable<ContributorDTO>> ListAsync()
{
List<ContributorDTO> 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());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ public class ListContributorsQueryService(AppDbContext _db) : IListContributorsQ
public async Task<IEnumerable<ContributorDTO>> 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<ContributorDTO>(
$"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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ namespace Clean.Architecture.UseCases.Contributors.Create;
/// Create a new Contributor.
/// </summary>
/// <param name="Name"></param>
public record CreateContributorCommand(string Name) : Ardalis.SharedKernel.ICommand<Result<int>>;
public record CreateContributorCommand(string Name, string? PhoneNumber) : Ardalis.SharedKernel.ICommand<Result<int>>;
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ public async Task<Result<int>> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ public async Task<Result<ContributorDTO>> 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 ?? "");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public async Task<Result<ContributorDTO>> 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 ?? ""));
}
}
5 changes: 5 additions & 0 deletions src/Clean.Architecture.Web/Clean.Architecture.Web.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<PackageReference Include="FastEndpoints" />
<PackageReference Include="FastEndpoints.Swagger" />
<PackageReference Include="MediatR" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" PrivateAssets="All" />
<PackageReference Include="Serilog.AspNetCore" />
</ItemGroup>
Expand All @@ -24,4 +25,8 @@
<ProjectReference Include="..\Clean.Architecture.UseCases\Clean.Architecture.UseCases.csproj" />
</ItemGroup>

<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ public class CreateContributorRequest

[Required]
public string? Name { get; set; }
public string? PhoneNumber { get; set; }
}
3 changes: 2 additions & 1 deletion src/Clean.Architecture.Web/Contributors/Create.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
2 changes: 1 addition & 1 deletion src/Clean.Architecture.Web/Contributors/GetById.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
2 changes: 1 addition & 1 deletion src/Clean.Architecture.Web/Contributors/List.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
};
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/Clean.Architecture.Web/Contributors/Update.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down
7 changes: 4 additions & 3 deletions src/Clean.Architecture.Web/api.http
Original file line number Diff line number Diff line change
Expand Up @@ -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}}

###
Expand All @@ -18,8 +18,9 @@ POST http://{{hostname}}:{{port}}/Contributors
Content-Type: application/json

{
"name": "John Doe",
"email": "[email protected]"
"name": "John Doe 2",
"email": "[email protected]",
"phoneNumber": "1234567890"
}

###
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public async Task ReturnsSuccessGivenValidName()
{
_repository.AddAsync(Arg.Any<Contributor>(), Arg.Any<CancellationToken>())
.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();
}
Expand Down

0 comments on commit 5286712

Please sign in to comment.