diff --git a/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/Project.cs b/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/Project.cs index ccf4c6939..1884fdf55 100644 --- a/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/Project.cs +++ b/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/Project.cs @@ -1,4 +1,5 @@ -using NimblePros.SampleToDo.Core.ProjectAggregate.Events; +using NimblePros.SampleToDo.Core.ContributorAggregate.Events; +using NimblePros.SampleToDo.Core.ProjectAggregate.Events; namespace NimblePros.SampleToDo.Core.ProjectAggregate; @@ -27,6 +28,7 @@ public Project AddItem(ToDoItem newItem) public Project UpdateName(ProjectName newName) { + if (Name.Equals(newName)) return this; Name = newName; return this; } diff --git a/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/Specifications/IncompleteItemsSearchSpec.cs b/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/Specifications/IncompleteItemsSearchSpec.cs index 36185587b..3fc78916e 100644 --- a/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/Specifications/IncompleteItemsSearchSpec.cs +++ b/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/Specifications/IncompleteItemsSearchSpec.cs @@ -6,7 +6,7 @@ public IncompleteItemsSearchSpec(string searchString) { Query .Where(item => !item.IsDone && - (item.Title.Contains(searchString) || - item.Description.Contains(searchString))); + (item.Title.Value.Contains(searchString) || + item.Description.Value.Contains(searchString))); } } diff --git a/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/ToDoItem.cs b/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/ToDoItem.cs index 03dc54b87..e21fe7f29 100644 --- a/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/ToDoItem.cs +++ b/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/ToDoItem.cs @@ -1,4 +1,5 @@ -using NimblePros.SampleToDo.Core.ContributorAggregate; +using System.Xml.Linq; +using NimblePros.SampleToDo.Core.ContributorAggregate; using NimblePros.SampleToDo.Core.ProjectAggregate.Events; namespace NimblePros.SampleToDo.Core.ProjectAggregate; @@ -8,14 +9,19 @@ public class ToDoItem : EntityBase public ToDoItem() : this(Priority.Backlog) { } + public ToDoItem(ToDoItemTitle title, ToDoItemDescription description) : this(Priority.Backlog) + { + Title = title; + Description = description; + } public ToDoItem(Priority priority) { Priority = priority; } - public string Title { get; set; } = string.Empty; // TODO: Use Value Object - public string Description { get; set; } = string.Empty; // TODO: Use Value Object + public ToDoItemTitle Title { get; private set; } + public ToDoItemDescription Description { get; private set; } public ContributorId? ContributorId { get; private set; } // tasks don't have anyone assigned when first created public bool IsDone { get; private set; } @@ -48,9 +54,23 @@ public ToDoItem RemoveContributor() return this; } + public ToDoItem UpdateTitle(ToDoItemTitle newTitle) + { + if (Title.Equals(newTitle)) return this; + Title = newTitle; + return this; + } + + public ToDoItem UpdateDescription(ToDoItemDescription newDescription) + { + if (Description.Equals(newDescription)) return this; + Description = newDescription; + return this; + } + public override string ToString() { string status = IsDone ? "Done!" : "Not done."; - return $"{Id}: Status: {status} - {Title} - Priority: {Priority}"; + return $"{Id}: Status: {status} - {Title.Value} - Priority: {Priority}"; } } diff --git a/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/ToDoItemDescription.cs b/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/ToDoItemDescription.cs new file mode 100644 index 000000000..8e6403849 --- /dev/null +++ b/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/ToDoItemDescription.cs @@ -0,0 +1,15 @@ +using Vogen; + +namespace NimblePros.SampleToDo.Core.ProjectAggregate; + +[ValueObject(conversions: Conversions.SystemTextJson)] +public partial struct ToDoItemDescription +{ + public const int MaxLength = 200; + private static Validation Validate(in string description) => + string.IsNullOrEmpty(description) + ? Validation.Invalid("Description cannot be empty") + : description.Length > MaxLength + ? Validation.Invalid($"Description cannot be longer than {MaxLength} characters") + : Validation.Ok; +} diff --git a/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/ToDoItemTitle.cs b/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/ToDoItemTitle.cs new file mode 100644 index 000000000..e38961938 --- /dev/null +++ b/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/ToDoItemTitle.cs @@ -0,0 +1,15 @@ +using Vogen; + +namespace NimblePros.SampleToDo.Core.ProjectAggregate; + +[ValueObject(conversions: Conversions.SystemTextJson)] +public partial struct ToDoItemTitle +{ + public const int MaxLength = 100; + private static Validation Validate(in string title) => + string.IsNullOrEmpty(title) + ? Validation.Invalid("Title cannot be empty") + : title.Length > MaxLength + ? Validation.Invalid($"Title cannot be longer than {MaxLength} characters") + : Validation.Ok; +} diff --git a/sample/src/NimblePros.SampleToDo.Infrastructure/Data/Config/ToDoItemConfiguration.cs b/sample/src/NimblePros.SampleToDo.Infrastructure/Data/Config/ToDoItemConfiguration.cs index dee312b12..07974d63b 100644 --- a/sample/src/NimblePros.SampleToDo.Infrastructure/Data/Config/ToDoItemConfiguration.cs +++ b/sample/src/NimblePros.SampleToDo.Infrastructure/Data/Config/ToDoItemConfiguration.cs @@ -11,11 +11,17 @@ public void Configure(EntityTypeBuilder builder) .HasValueGenerator>() .HasVogenConversion() .IsRequired(); - builder.Property(t => t.Title) - .HasMaxLength(DataSchemaConstants.DEFAULT_NAME_LENGTH) + + builder.Property(p => p.Title) + .HasVogenConversion() + .HasMaxLength(ToDoItemTitle.MaxLength) + .IsRequired(); + + builder.Property(p => p.Description) + .HasVogenConversion() + .HasMaxLength(ToDoItemDescription.MaxLength) .IsRequired(); - builder.Property(t => t.Description) - .HasMaxLength(200); + builder.Property(t => t.ContributorId) .HasConversion( v => v.HasValue ? v.Value.Value : (int?)null, // to db diff --git a/sample/src/NimblePros.SampleToDo.Infrastructure/Data/Config/VogenEfCoreConverters.cs b/sample/src/NimblePros.SampleToDo.Infrastructure/Data/Config/VogenEfCoreConverters.cs index f07a894b2..9643b0ae4 100644 --- a/sample/src/NimblePros.SampleToDo.Infrastructure/Data/Config/VogenEfCoreConverters.cs +++ b/sample/src/NimblePros.SampleToDo.Infrastructure/Data/Config/VogenEfCoreConverters.cs @@ -9,4 +9,6 @@ namespace NimblePros.SampleToDo.Infrastructure.Data.Config; [EfCoreConverter] [EfCoreConverter] [EfCoreConverter] +[EfCoreConverter] +[EfCoreConverter] internal partial class VogenEfCoreConverters; diff --git a/sample/src/NimblePros.SampleToDo.Infrastructure/Data/Queries/ListIncompleteItemsQueryService.cs b/sample/src/NimblePros.SampleToDo.Infrastructure/Data/Queries/ListIncompleteItemsQueryService.cs index 7aa24553b..72c5d4017 100644 --- a/sample/src/NimblePros.SampleToDo.Infrastructure/Data/Queries/ListIncompleteItemsQueryService.cs +++ b/sample/src/NimblePros.SampleToDo.Infrastructure/Data/Queries/ListIncompleteItemsQueryService.cs @@ -17,7 +17,7 @@ public async Task> ListAsync(int projectId) var projectParameter = new SqlParameter("@projectId", System.Data.SqlDbType.Int); var result = await _db.ToDoItems.FromSqlRaw("SELECT Id, Title, Description, IsDone, ContributorId FROM ToDoItems WHERE ProjectId = @ProjectId", projectParameter) // don't fetch other big columns - .Select(x => new ToDoItemDto(x.Id, x.Title, x.Description, x.IsDone, x.ContributorId)) + .Select(x => new ToDoItemDto(x.Id, x.Title.Value, x.Description.Value, x.IsDone, x.ContributorId)) .ToListAsync(); return result; diff --git a/sample/src/NimblePros.SampleToDo.UseCases/Projects/AddToDoItem/AddToDoItemCommand.cs b/sample/src/NimblePros.SampleToDo.UseCases/Projects/AddToDoItem/AddToDoItemCommand.cs index 129c523d4..6f2f1d541 100644 --- a/sample/src/NimblePros.SampleToDo.UseCases/Projects/AddToDoItem/AddToDoItemCommand.cs +++ b/sample/src/NimblePros.SampleToDo.UseCases/Projects/AddToDoItem/AddToDoItemCommand.cs @@ -12,5 +12,5 @@ namespace NimblePros.SampleToDo.UseCases.Projects.AddToDoItem; /// public record AddToDoItemCommand(ProjectId ProjectId, ContributorId? ContributorId, - string Title, - string Description) : ICommand>; + ToDoItemTitle Title, + ToDoItemDescription Description) : ICommand>; diff --git a/sample/src/NimblePros.SampleToDo.UseCases/Projects/AddToDoItem/AddToDoItemHandler.cs b/sample/src/NimblePros.SampleToDo.UseCases/Projects/AddToDoItem/AddToDoItemHandler.cs index 6d14b45a7..5677022c3 100644 --- a/sample/src/NimblePros.SampleToDo.UseCases/Projects/AddToDoItem/AddToDoItemHandler.cs +++ b/sample/src/NimblePros.SampleToDo.UseCases/Projects/AddToDoItem/AddToDoItemHandler.cs @@ -22,13 +22,9 @@ public async ValueTask> Handle(AddToDoItemCommand request, return Result.NotFound(); } - var newItem = new ToDoItem() - { - Title = request.Title!, - Description = request.Description! - }; + var newItem = new ToDoItem(title: request.Title!, description: request.Description!); - if(request.ContributorId.HasValue) + if (request.ContributorId.HasValue) { newItem.AddContributor(request.ContributorId.Value); } diff --git a/sample/src/NimblePros.SampleToDo.UseCases/Projects/GetWithAllItems/GetProjectWithAllItemsHandler.cs b/sample/src/NimblePros.SampleToDo.UseCases/Projects/GetWithAllItems/GetProjectWithAllItemsHandler.cs index 446fb2e05..9f51a1aa4 100644 --- a/sample/src/NimblePros.SampleToDo.UseCases/Projects/GetWithAllItems/GetProjectWithAllItemsHandler.cs +++ b/sample/src/NimblePros.SampleToDo.UseCases/Projects/GetWithAllItems/GetProjectWithAllItemsHandler.cs @@ -22,7 +22,7 @@ public async ValueTask> Handle(GetProjectWithAllI if (project == null) return Result.NotFound(); var items = project.Items - .Select(i => new ToDoItemDto(i.Id, i.Title, i.Description, i.IsDone, i.ContributorId)).ToList(); + .Select(i => new ToDoItemDto(i.Id, i.Title.Value, i.Description.Value, i.IsDone, i.ContributorId)).ToList(); return new ProjectWithAllItemsDto(project.Id, project.Name, items, project.Status.ToString()) ; } diff --git a/sample/src/NimblePros.SampleToDo.Web/Projects/CreateToDoItem.cs b/sample/src/NimblePros.SampleToDo.Web/Projects/CreateToDoItem.cs index 54ab1c0b5..359c70616 100644 --- a/sample/src/NimblePros.SampleToDo.Web/Projects/CreateToDoItem.cs +++ b/sample/src/NimblePros.SampleToDo.Web/Projects/CreateToDoItem.cs @@ -1,7 +1,6 @@ using NimblePros.SampleToDo.Core.ContributorAggregate; using NimblePros.SampleToDo.Core.ProjectAggregate; using NimblePros.SampleToDo.UseCases.Projects.AddToDoItem; -using NimblePros.SampleToDo.Web.Extensions; using NimblePros.SampleToDo.Web.Projects; namespace NimblePros.SampleToDo.Web.ProjectEndpoints; @@ -30,17 +29,17 @@ public override void Configure() Title = "Implement user authentication", Description = "Add JWT-based authentication to the API" }; - + // Document possible responses s.Responses[201] = "Todo item created successfully"; s.Responses[404] = "Project or contributor not found"; s.Responses[400] = "Invalid input data"; s.Responses[500] = "Internal server error"; }); - + // Add tags for API grouping Tags("Projects"); - + // Add additional metadata Description(builder => builder .Accepts("application/json") @@ -57,7 +56,7 @@ public override async Task> ? ContributorId.From(request.ContributorId.Value) : null; var command = new AddToDoItemCommand(ProjectId.From(request.ProjectId), contributorId, - request.Title, request.Description); + ToDoItemTitle.From(request.Title), ToDoItemDescription.From(request.Description)); var result = await _mediator.Send(command); return result.Status switch diff --git a/sample/src/NimblePros.SampleToDo.Web/SeedData.cs b/sample/src/NimblePros.SampleToDo.Web/SeedData.cs index e609c982f..7f74379f3 100644 --- a/sample/src/NimblePros.SampleToDo.Web/SeedData.cs +++ b/sample/src/NimblePros.SampleToDo.Web/SeedData.cs @@ -6,33 +6,31 @@ namespace NimblePros.SampleToDo.Web; public static class SeedData { - public static readonly Contributor Contributor1 = new (ContributorName.From("Ardalis")); - public static readonly Contributor Contributor2 = new (ContributorName.From("Snowfrog")); + public static readonly Contributor Contributor1 = new(ContributorName.From("Ardalis")); + public static readonly Contributor Contributor2 = new(ContributorName.From("Snowfrog")); public static readonly Project TestProject1 = new Project(ProjectName.From("Test Project")); - public static readonly ToDoItem ToDoItem1 = new ToDoItem - { - Title = "Get Sample Working", - Description = "Try to get the sample to build." - }; - public static readonly ToDoItem ToDoItem2 = new ToDoItem - { - Title = "Review Solution", - Description = "Review the different projects in the solution and how they relate to one another." - }; - public static readonly ToDoItem ToDoItem3 = new ToDoItem - { - Title = "Run and Review Tests", - Description = "Make sure all the tests run and review what they are doing." - }; + + public static readonly ToDoItem ToDoItem1 = + new ToDoItem(title: ToDoItemTitle.From("Get Sample Working"), + description: ToDoItemDescription.From("Try to get the sample to build.")); + + public static readonly ToDoItem ToDoItem2 = + new ToDoItem(title: ToDoItemTitle.From("Review Solution"), + description: ToDoItemDescription.From("Review the different projects in the solution and how they relate to one another.")); + + + public static readonly ToDoItem ToDoItem3 = +new ToDoItem(title: ToDoItemTitle.From("Run and Review Tests"), + description: ToDoItemDescription.From("Make sure all the tests run and review what they are doing.")); public static async Task InitializeAsync(AppDbContext dbContext) { - if (await dbContext.ToDoItems.AnyAsync()) - { - return; // DB has been seeded - } + if (await dbContext.ToDoItems.AnyAsync()) + { + return; // DB has been seeded + } - await PopulateTestDataAsync(dbContext); + await PopulateTestDataAsync(dbContext); } public static async Task PopulateTestDataAsync(AppDbContext dbContext) diff --git a/sample/tests/NimblePros.SampleToDo.IntegrationTests/Data/EfRepositoryAdd.cs b/sample/tests/NimblePros.SampleToDo.IntegrationTests/Data/EfRepositoryAdd.cs index e39eddd32..13ca08b4c 100644 --- a/sample/tests/NimblePros.SampleToDo.IntegrationTests/Data/EfRepositoryAdd.cs +++ b/sample/tests/NimblePros.SampleToDo.IntegrationTests/Data/EfRepositoryAdd.cs @@ -12,7 +12,7 @@ public async Task AddsProjectAndSetsId() var project = new Project(testProjectName); var item = new ToDoItem(); - item.Title = "test item title"; + item.UpdateTitle(ToDoItemTitle.From("test item title")); project.AddItem(item); await repository.AddAsync(project); diff --git a/sample/tests/NimblePros.SampleToDo.UnitTests/Core/ProjectAggregate/Project_AddItem.cs b/sample/tests/NimblePros.SampleToDo.UnitTests/Core/ProjectAggregate/Project_AddItem.cs index 8d62c40e3..e623aaf92 100644 --- a/sample/tests/NimblePros.SampleToDo.UnitTests/Core/ProjectAggregate/Project_AddItem.cs +++ b/sample/tests/NimblePros.SampleToDo.UnitTests/Core/ProjectAggregate/Project_AddItem.cs @@ -9,11 +9,8 @@ public class Project_AddItem [Fact] public void AddsItemToItems() { - var _testItem = new ToDoItem - { - Title = "title", - Description = "description" - }; + var _testItem = new ToDoItem(title: ToDoItemTitle.From("title"), + description: ToDoItemDescription.From("description")); _testProject.AddItem(_testItem); diff --git a/sample/tests/NimblePros.SampleToDo.UnitTests/Core/Services/ToDoItemSearchServiceTests.cs b/sample/tests/NimblePros.SampleToDo.UnitTests/Core/Services/ToDoItemSearchServiceTests.cs index 08004d539..59bcc1e74 100644 --- a/sample/tests/NimblePros.SampleToDo.UnitTests/Core/Services/ToDoItemSearchServiceTests.cs +++ b/sample/tests/NimblePros.SampleToDo.UnitTests/Core/Services/ToDoItemSearchServiceTests.cs @@ -1,7 +1,6 @@ using NimblePros.SampleToDo.Core.Interfaces; using NimblePros.SampleToDo.Core.ProjectAggregate; using NimblePros.SampleToDo.Core.Services; -using NimblePros.SharedKernel; namespace NimblePros.SampleToDo.UnitTests.Core.Services; @@ -9,21 +8,21 @@ public class ToDoItemSearchServiceTests { private readonly IToDoItemSearchService _service; private readonly IRepository _repo = Substitute.For>(); - + public ToDoItemSearchServiceTests() { _service = new ToDoItemSearchService(_repo); - + } [Fact] public async Task ReturnsValidationErrors() { var projects = await _service.GetAllIncompleteItemsAsync(ProjectId.From(0), string.Empty); - + Assert.NotEmpty(projects.ValidationErrors); } - + [Fact] public async Task ReturnsProjectNotFound() { @@ -31,18 +30,14 @@ public async Task ReturnsProjectNotFound() Assert.Equal(ResultStatus.NotFound, projects.Status); } - + [Fact] public async Task ReturnsAllIncompleteItems() { var title = "Some Title"; Project project = new Project(ProjectName.From("Cool Project")); - - project.AddItem(new ToDoItem - { - Title = title, - Description = "Some Description" - }); + + project.AddItem(new ToDoItem(title: ToDoItemTitle.From(title), description: ToDoItemDescription.From("Some Description"))); _repo.FirstOrDefaultAsync(Arg.Any>(), Arg.Any()) .Returns(project); @@ -50,7 +45,7 @@ public async Task ReturnsAllIncompleteItems() var projects = await _service.GetAllIncompleteItemsAsync(ProjectId.From(1), title); Assert.Empty(projects.ValidationErrors); - Assert.Equal(projects.Value.First().Title, title); + Assert.Equal(projects.Value.First().Title.Value, title); Assert.Equal(project.Items.Count(), projects.Value.Count); } } diff --git a/sample/tests/NimblePros.SampleToDo.UnitTests/ToDoItemBuilder.cs b/sample/tests/NimblePros.SampleToDo.UnitTests/ToDoItemBuilder.cs index 423d33cdc..08f851e14 100644 --- a/sample/tests/NimblePros.SampleToDo.UnitTests/ToDoItemBuilder.cs +++ b/sample/tests/NimblePros.SampleToDo.UnitTests/ToDoItemBuilder.cs @@ -14,21 +14,21 @@ public ToDoItemBuilder Id(int id) return this; } - public ToDoItemBuilder Title(string title) + public ToDoItemBuilder Title(String title) { - _todo.Title = title; + _todo.UpdateTitle(ToDoItemTitle.From(title)); return this; } - public ToDoItemBuilder Description(string description) + public ToDoItemBuilder Description(String description) { - _todo.Description = description; + _todo.UpdateDescription(ToDoItemDescription.From(description)); return this; } public ToDoItemBuilder WithDefaultValues() { - _todo = new ToDoItem() { Id = ToDoItemId.From(1), Title = "Test Item", Description = "Test Description" }; + _todo = new ToDoItem(title: ToDoItemTitle.From("Test Item"), description: ToDoItemDescription.From("Test Description")) { Id = ToDoItemId.From(1) }; return this; }