Skip to content

Commit 314bdf8

Browse files
committed
Update sample's Projects endpoints to follow conventions
1 parent c4e2f9b commit 314bdf8

19 files changed

+376
-146
lines changed

sample/src/NimblePros.SampleToDo.UseCases/Projects/Create/CreateProjectCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ namespace NimblePros.SampleToDo.UseCases.Projects.Create;
66
/// Create a new Project.
77
/// </summary>
88
/// <param name="Name"></param>
9-
public record CreateProjectCommand(string Name) : ICommand<Result<ProjectId>>;
9+
public record CreateProjectCommand(ProjectName Name) : ICommand<Result<ProjectId>>;

sample/src/NimblePros.SampleToDo.UseCases/Projects/Create/CreateProjectHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ public class CreateProjectHandler(IRepository<Project> repository) : ICommandHan
99
public async ValueTask<Result<ProjectId>> Handle(CreateProjectCommand request,
1010
CancellationToken cancellationToken)
1111
{
12-
var newProject = new Project(ProjectName.From(request.Name));
12+
var newProject = new Project(request.Name);
1313
// NOTE: This implementation issues 3 queries due to Vogen implementation
1414
var createdItem = await _repository.AddAsync(newProject, cancellationToken);
1515

sample/src/NimblePros.SampleToDo.Web/Projects/Create.CreateProjectRequest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@ public class CreateProjectRequest
77
public const string Route = "/Projects";
88

99
[Required]
10-
public string? Name { get; set; }
10+
public string Name { get; set; } = String.Empty;
1111
}
Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,3 @@
11
namespace NimblePros.SampleToDo.Web.Projects;
22

3-
public class CreateProjectResponse
4-
{
5-
public CreateProjectResponse(int id, string name)
6-
{
7-
Id = id;
8-
Name = name;
9-
}
10-
11-
public int Id { get; set; }
12-
public string Name { get; set; }
13-
}
3+
public record CreateProjectResponse(int Id, string Name);
Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
using FastEndpoints;
22
using FluentValidation;
3-
using NimblePros.SampleToDo.Infrastructure.Data.Config;
3+
using NimblePros.SampleToDo.Core.ProjectAggregate;
44

55
namespace NimblePros.SampleToDo.Web.Projects;
66

7-
/// <summary>
8-
/// See: https://fast-endpoints.com/docs/validation
9-
/// </summary>
107
public class CreateProjectValidator : Validator<CreateProjectRequest>
118
{
129
public CreateProjectValidator()
@@ -15,6 +12,6 @@ public CreateProjectValidator()
1512
.NotEmpty()
1613
.WithMessage("Name is required.")
1714
.MinimumLength(2)
18-
.MaximumLength(DataSchemaConstants.DEFAULT_NAME_LENGTH);
15+
.MaximumLength(ProjectName.MaxLength);
1916
}
2017
}
Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
1-
using Ardalis.Result.AspNetCore;
1+
using NimblePros.SampleToDo.Core.ProjectAggregate;
22
using NimblePros.SampleToDo.UseCases.Projects.Create;
3+
using NimblePros.SampleToDo.Web.Extensions;
34

45
namespace NimblePros.SampleToDo.Web.Projects;
56

6-
/// <summary>
7-
/// Creates a new Project
8-
/// </summary>
9-
/// <remarks>
10-
/// Creates a new project given a name.
11-
/// </remarks>
12-
public class Create(IMediator mediator) : Endpoint<CreateProjectRequest, CreateProjectResponse>
7+
public class Create(IMediator mediator)
8+
: Endpoint<CreateProjectRequest, Results<Created<CreateProjectResponse>, ValidationProblem, ProblemHttpResult>>
139
{
1410
private readonly IMediator _mediator = mediator;
1511

@@ -19,21 +15,35 @@ public override void Configure()
1915
AllowAnonymous();
2016
Summary(s =>
2117
{
22-
s.ExampleRequest = new CreateProjectRequest { Name = "Project Name" };
18+
s.Summary = "Create a new project";
19+
s.Description = "Creates a new project with the provided name. The project name must be between 2 and 100 characters long.";
20+
s.ExampleRequest = new CreateProjectRequest { Name = "My New Project" };
21+
s.ResponseExamples[201] = new CreateProjectResponse(1, "My New Project");
22+
23+
// Document possible responses
24+
s.Responses[201] = "Project created successfully";
25+
s.Responses[400] = "Invalid input data - validation errors";
26+
s.Responses[500] = "Internal server error";
2327
});
28+
29+
// Add tags for API grouping
30+
Tags("Projects");
31+
32+
// Add additional metadata
33+
Description(builder => builder
34+
.Accepts<CreateProjectRequest>("application/json")
35+
.Produces<CreateProjectResponse>(201, "application/json")
36+
.ProducesProblem(400)
37+
.ProducesProblem(500));
2438
}
2539

26-
public override async Task HandleAsync(
27-
CreateProjectRequest request,
28-
CancellationToken cancellationToken)
40+
public override async Task<Results<Created<CreateProjectResponse>, ValidationProblem, ProblemHttpResult>>
41+
ExecuteAsync(CreateProjectRequest request, CancellationToken cancellationToken)
2942
{
30-
var result = await _mediator.Send(new CreateProjectCommand(request.Name!));
43+
var result = await _mediator.Send(new CreateProjectCommand(ProjectName.From(request.Name!)));
3144

32-
if (result.IsSuccess)
33-
{
34-
Response = new CreateProjectResponse(result.Value.Value, request.Name!);
35-
return;
36-
}
37-
await Send.ResultAsync(result.ToMinimalApiResult());
45+
return result.ToCreatedResult(
46+
id => $"/Projects/{id}",
47+
id => new CreateProjectResponse(id.Value, request.Name!));
3848
}
3949
}
Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
using NimblePros.SampleToDo.Core.ContributorAggregate;
22
using NimblePros.SampleToDo.Core.ProjectAggregate;
33
using NimblePros.SampleToDo.UseCases.Projects.AddToDoItem;
4+
using NimblePros.SampleToDo.Web.Extensions;
45
using NimblePros.SampleToDo.Web.Projects;
56

67
namespace NimblePros.SampleToDo.Web.ProjectEndpoints;
78

8-
public class Create : Endpoint<CreateToDoItemRequest>
9+
public class Create : Endpoint<CreateToDoItemRequest, Results<Created, NotFound, ProblemHttpResult>>
910
{
1011
private readonly IMediator _mediator;
1112

@@ -20,19 +21,37 @@ public override void Configure()
2021
AllowAnonymous();
2122
Summary(s =>
2223
{
24+
s.Summary = "Add a new todo item to a project";
25+
s.Description = "Creates a new todo item within an existing project. The project must exist and the contributor (if specified) must be valid.";
2326
s.ExampleRequest = new CreateToDoItemRequest
2427
{
2528
ContributorId = 1,
2629
ProjectId = 1,
27-
Title = "Title",
28-
Description = "Description"
30+
Title = "Implement user authentication",
31+
Description = "Add JWT-based authentication to the API"
2932
};
33+
34+
// Document possible responses
35+
s.Responses[201] = "Todo item created successfully";
36+
s.Responses[404] = "Project or contributor not found";
37+
s.Responses[400] = "Invalid input data";
38+
s.Responses[500] = "Internal server error";
3039
});
40+
41+
// Add tags for API grouping
42+
Tags("Projects");
43+
44+
// Add additional metadata
45+
Description(builder => builder
46+
.Accepts<CreateToDoItemRequest>("application/json")
47+
.Produces(201)
48+
.ProducesProblem(404)
49+
.ProducesProblem(400)
50+
.ProducesProblem(500));
3151
}
3252

33-
public override async Task HandleAsync(
34-
CreateToDoItemRequest request,
35-
CancellationToken cancellationToken)
53+
public override async Task<Results<Created, NotFound, ProblemHttpResult>>
54+
ExecuteAsync(CreateToDoItemRequest request, CancellationToken cancellationToken)
3655
{
3756
ContributorId? contributorId = request.ContributorId.HasValue
3857
? ContributorId.From(request.ContributorId.Value)
@@ -41,17 +60,14 @@ public override async Task HandleAsync(
4160
request.Title, request.Description);
4261
var result = await _mediator.Send(command);
4362

44-
if (result.Status == ResultStatus.NotFound)
63+
return result.Status switch
4564
{
46-
await Send.NotFoundAsync(cancellationToken);
47-
return;
48-
}
49-
50-
if (result.IsSuccess)
51-
{
52-
// send route to project
53-
await Send.CreatedAtAsync<GetById>(new { projectId = request.ProjectId }, "");
65+
ResultStatus.Ok => TypedResults.Created($"/Projects/{request.ProjectId}"),
66+
ResultStatus.NotFound => TypedResults.NotFound(),
67+
_ => TypedResults.Problem(
68+
title: "Create todo item failed",
69+
detail: string.Join("; ", result.Errors),
70+
statusCode: StatusCodes.Status400BadRequest)
5471
};
55-
// TODO: Handle other cases as necessary
5672
}
5773
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using FastEndpoints;
2+
using FluentValidation;
3+
4+
namespace NimblePros.SampleToDo.Web.Projects;
5+
6+
/// <summary>
7+
/// See: https://fast-endpoints.com/docs/validation
8+
/// </summary>
9+
public class DeleteProjectValidator : Validator<DeleteProjectRequest>
10+
{
11+
public DeleteProjectValidator()
12+
{
13+
RuleFor(x => x.ProjectId)
14+
.GreaterThan(0);
15+
}
16+
}
Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,51 @@
1-
using Ardalis.Result.AspNetCore;
2-
using NimblePros.SampleToDo.Core.ProjectAggregate;
1+
using NimblePros.SampleToDo.Core.ProjectAggregate;
32
using NimblePros.SampleToDo.UseCases.Projects.Delete;
3+
using NimblePros.SampleToDo.Web.Extensions;
44

55
namespace NimblePros.SampleToDo.Web.Projects;
66

7-
/// <summary>
8-
/// Deletes a project
9-
/// </summary>
10-
public class Delete(IMediator mediator) : Endpoint<DeleteProjectRequest>
7+
public class Delete
8+
: Endpoint<DeleteProjectRequest,
9+
Results<NoContent,
10+
NotFound,
11+
ProblemHttpResult>>
1112
{
12-
private readonly IMediator _mediator = mediator;
13+
private readonly IMediator _mediator;
14+
public Delete(IMediator mediator) => _mediator = mediator;
1315

1416
public override void Configure()
1517
{
1618
Delete(DeleteProjectRequest.Route);
1719
AllowAnonymous();
20+
Summary(s =>
21+
{
22+
s.Summary = "Delete a project";
23+
s.Description = "Deletes an existing project by ID. This will also delete all associated todo items. This action cannot be undone.";
24+
s.ExampleRequest = new DeleteProjectRequest { ProjectId = 1 };
25+
26+
// Document possible responses
27+
s.Responses[204] = "Project deleted successfully";
28+
s.Responses[404] = "Project not found";
29+
s.Responses[400] = "Invalid request or deletion failed";
30+
});
31+
32+
// Add tags for API grouping
33+
Tags("Projects");
34+
35+
// Add additional metadata
36+
Description(builder => builder
37+
.Accepts<DeleteProjectRequest>()
38+
.Produces(204)
39+
.ProducesProblem(404)
40+
.ProducesProblem(400));
1841
}
1942

20-
public override async Task HandleAsync(
21-
DeleteProjectRequest request,
22-
CancellationToken cancellationToken)
43+
public override async Task<Results<NoContent, NotFound, ProblemHttpResult>>
44+
ExecuteAsync(DeleteProjectRequest req, CancellationToken ct)
2345
{
24-
var command = new DeleteProjectCommand(ProjectId.From(request.ProjectId));
46+
var cmd = new DeleteProjectCommand(ProjectId.From(req.ProjectId));
47+
var result = await _mediator.Send(cmd, ct);
2548

26-
var result = await _mediator.Send(command);
27-
28-
await Send.ResultAsync(result.ToMinimalApiResult());
49+
return result.ToDeleteResult();
2950
}
3051
}
Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,3 @@
11
namespace NimblePros.SampleToDo.Web.Projects;
22

3-
public class GetProjectByIdResponse
4-
{
5-
public GetProjectByIdResponse(int id, string name, List<ToDoItemRecord> items)
6-
{
7-
Id = id;
8-
Name = name;
9-
Items = items;
10-
}
11-
12-
public int Id { get; set; }
13-
public string Name { get; set; }
14-
public List<ToDoItemRecord> Items { get; set; } = new();
15-
}
3+
public record GetProjectByIdResponse(int Id, string Name, List<ToDoItemRecord> Items);

0 commit comments

Comments
 (0)