Skip to content

Commit 9ab945c

Browse files
authored
#105 ✨ Added Heroes & Teams endpoints (#115)
Resolves #105 - Migrates endpoints from CA to VSA - Refactors endpoints into VSA format - Migrates tests for endpoints
1 parent 9e54125 commit 9ab945c

File tree

17 files changed

+950
-2
lines changed

17 files changed

+950
-2
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using Microsoft.AspNetCore.Http.HttpResults;
2+
3+
namespace VerticalSliceArchitectureTemplate.Common.Extensions;
4+
5+
public static class CustomResult
6+
{
7+
public static IResult Problem(List<Error> errors)
8+
{
9+
if (errors.Count is 0)
10+
{
11+
return TypedResults.Problem();
12+
}
13+
14+
if (errors.All(error => error.Type == ErrorType.Validation))
15+
{
16+
return ValidationProblem(errors);
17+
}
18+
19+
return Problem(errors[0]);
20+
}
21+
22+
private static ProblemHttpResult Problem(Error error)
23+
{
24+
var statusCode = error.Type switch
25+
{
26+
ErrorType.Conflict => StatusCodes.Status409Conflict,
27+
ErrorType.Validation => StatusCodes.Status400BadRequest,
28+
ErrorType.NotFound => StatusCodes.Status404NotFound,
29+
_ => StatusCodes.Status500InternalServerError
30+
};
31+
32+
return TypedResults.Problem(statusCode: statusCode, title: error.Description);
33+
}
34+
35+
private static ValidationProblem ValidationProblem(List<Error> errors)
36+
{
37+
var validationErrors = new Dictionary<string, string[]>();
38+
foreach (var e in errors)
39+
{
40+
if (validationErrors.Remove(e.Code, out var value))
41+
validationErrors.Add(e.Code, [.. value, e.Description]);
42+
else
43+
validationErrors.Add(e.Code, [e.Description]);
44+
}
45+
46+
return TypedResults.ValidationProblem(validationErrors, title: "One or more validation errors occurred.");
47+
}
48+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using MediatR;
2+
using VerticalSliceArchitectureTemplate.Common.Domain.Heroes;
3+
using VerticalSliceArchitectureTemplate.Common.Extensions;
4+
5+
namespace VerticalSliceArchitectureTemplate.Features.Heroes.Commands;
6+
7+
public static class CreateHeroCommand
8+
{
9+
public record CreateHeroPowerDto(string Name, int PowerLevel);
10+
11+
public record Request(
12+
string Name,
13+
string Alias,
14+
IEnumerable<CreateHeroPowerDto> Powers) : IRequest<ErrorOr<Guid>>;
15+
16+
public class Endpoint : IEndpoint
17+
{
18+
public static void MapEndpoint(IEndpointRouteBuilder endpoints)
19+
{
20+
endpoints
21+
.MapApiGroup(HeroesFeature.FeatureName)
22+
.MapPost("/",
23+
async (ISender sender, Request request, CancellationToken ct) =>
24+
{
25+
var result = await sender.Send(request, ct);
26+
return result.Match(_ => TypedResults.Created(), CustomResult.Problem);
27+
})
28+
.WithName("CreateHero")
29+
.ProducesPost();
30+
}
31+
}
32+
33+
internal sealed class Validator : AbstractValidator<Request>
34+
{
35+
public Validator()
36+
{
37+
RuleFor(v => v.Name)
38+
.NotEmpty();
39+
40+
RuleFor(v => v.Alias)
41+
.NotEmpty();
42+
}
43+
}
44+
45+
internal sealed class Handler(AppDbContext dbContext)
46+
: IRequestHandler<Request, ErrorOr<Guid>>
47+
{
48+
public async Task<ErrorOr<Guid>> Handle(Request request, CancellationToken cancellationToken)
49+
{
50+
var hero = Hero.Create(request.Name, request.Alias);
51+
var powers = request.Powers.Select(p => new Power(p.Name, p.PowerLevel));
52+
hero.UpdatePowers(powers);
53+
54+
await dbContext.Heroes.AddAsync(hero, cancellationToken);
55+
await dbContext.SaveChangesAsync(cancellationToken);
56+
57+
return hero.Id.Value;
58+
}
59+
}
60+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
using MediatR;
2+
using System.Text.Json.Serialization;
3+
using VerticalSliceArchitectureTemplate.Common.Domain.Heroes;
4+
using VerticalSliceArchitectureTemplate.Common.Extensions;
5+
6+
namespace VerticalSliceArchitectureTemplate.Features.Heroes.Commands;
7+
8+
public static class UpdateHeroCommand
9+
{
10+
public record UpdateHeroPowerDto(string Name, int PowerLevel);
11+
12+
public record Request(
13+
string Name,
14+
string Alias,
15+
IEnumerable<UpdateHeroPowerDto> Powers) : IRequest<ErrorOr<Guid>>
16+
{
17+
[JsonIgnore]
18+
public Guid HeroId { get; set; }
19+
}
20+
21+
public class Endpoint : IEndpoint
22+
{
23+
public static void MapEndpoint(IEndpointRouteBuilder endpoints)
24+
{
25+
endpoints
26+
.MapApiGroup(HeroesFeature.FeatureName)
27+
.MapPut("/{heroId:guid}",
28+
async (ISender sender, Guid heroId, Request request, CancellationToken cancellationToken) =>
29+
{
30+
request.HeroId = heroId;
31+
var result = await sender.Send(request, cancellationToken);
32+
return result.Match(_ => TypedResults.NoContent(), CustomResult.Problem);
33+
})
34+
.WithName("UpdateHero")
35+
.ProducesPut();
36+
}
37+
}
38+
39+
internal sealed class Validator : AbstractValidator<Request>
40+
{
41+
public Validator()
42+
{
43+
RuleFor(v => v.HeroId)
44+
.NotEmpty();
45+
46+
RuleFor(v => v.Name)
47+
.NotEmpty();
48+
49+
RuleFor(v => v.Alias)
50+
.NotEmpty();
51+
}
52+
}
53+
54+
internal sealed class Handler(AppDbContext dbContext)
55+
: IRequestHandler<Request, ErrorOr<Guid>>
56+
{
57+
public async Task<ErrorOr<Guid>> Handle(Request request, CancellationToken cancellationToken)
58+
{
59+
var heroId = HeroId.From(request.HeroId);
60+
var hero = await dbContext.Heroes
61+
.Include(h => h.Powers)
62+
.FirstOrDefaultAsync(h => h.Id == heroId, cancellationToken);
63+
64+
if (hero is null)
65+
return HeroErrors.NotFound;
66+
67+
hero.Name = request.Name;
68+
hero.Alias = request.Alias;
69+
var powers = request.Powers.Select(p => new Power(p.Name, p.PowerLevel));
70+
hero.UpdatePowers(powers);
71+
72+
await dbContext.SaveChangesAsync(cancellationToken);
73+
74+
return hero.Id.Value;
75+
}
76+
}
77+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace VerticalSliceArchitectureTemplate.Features.Heroes;
2+
3+
public sealed class HeroesFeature : IFeature
4+
{
5+
public static string FeatureName => "Heroes";
6+
7+
public static void ConfigureServices(IServiceCollection services, IConfiguration config)
8+
{
9+
10+
}
11+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
using MediatR;
2+
using VerticalSliceArchitectureTemplate.Common.Extensions;
3+
4+
namespace VerticalSliceArchitectureTemplate.Features.Heroes.Queries;
5+
6+
public static class GetAllHeroesQuery
7+
{
8+
public record HeroDto(Guid Id, string Name, string Alias, int PowerLevel, IEnumerable<HeroPowerDto> Powers);
9+
public record HeroPowerDto(string Name, int PowerLevel);
10+
11+
public record Request : IRequest<ErrorOr<IReadOnlyList<HeroDto>>>;
12+
13+
public class Endpoint : IEndpoint
14+
{
15+
public static void MapEndpoint(IEndpointRouteBuilder endpoints)
16+
{
17+
endpoints
18+
.MapApiGroup(HeroesFeature.FeatureName)
19+
.MapGet("/",
20+
async (ISender sender, CancellationToken cancellationToken) =>
21+
{
22+
var request = new Request();
23+
var result = await sender.Send(request, cancellationToken);
24+
return TypedResults.Ok(result);
25+
})
26+
.WithName("GetAllHeroes")
27+
.ProducesGet<IReadOnlyList<HeroDto>>();
28+
}
29+
}
30+
31+
public class Validator : AbstractValidator<Request>
32+
{
33+
public Validator() {}
34+
}
35+
36+
internal sealed class Handler : IRequestHandler<Request, ErrorOr<IReadOnlyList<HeroDto>>>
37+
{
38+
private readonly AppDbContext _dbContext;
39+
40+
public Handler(AppDbContext dbContext)
41+
{
42+
_dbContext = dbContext;
43+
}
44+
45+
public async Task<ErrorOr<IReadOnlyList<HeroDto>>> Handle(
46+
Request request,
47+
CancellationToken cancellationToken)
48+
{
49+
return await _dbContext.Heroes
50+
.Select(h => new HeroDto(
51+
h.Id.Value,
52+
h.Name,
53+
h.Alias,
54+
h.PowerLevel,
55+
h.Powers.Select(p => new HeroPowerDto(p.Name, p.PowerLevel))))
56+
.ToListAsync(cancellationToken);
57+
}
58+
}
59+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
using Ardalis.Specification.EntityFrameworkCore;
2+
using MediatR;
3+
using System.Text.Json.Serialization;
4+
using VerticalSliceArchitectureTemplate.Common.Domain.Heroes;
5+
using VerticalSliceArchitectureTemplate.Common.Domain.Teams;
6+
using VerticalSliceArchitectureTemplate.Common.Extensions;
7+
8+
namespace VerticalSliceArchitectureTemplate.Features.Teams.Commands;
9+
10+
public static class AddHeroToTeamCommand
11+
{
12+
public record Request : IRequest<ErrorOr<Success>>
13+
{
14+
[JsonIgnore]
15+
public Guid TeamId { get; set; }
16+
[JsonIgnore]
17+
public Guid HeroId { get; set; }
18+
}
19+
20+
public class Endpoint : IEndpoint
21+
{
22+
public static void MapEndpoint(IEndpointRouteBuilder endpoints)
23+
{
24+
endpoints
25+
.MapApiGroup(TeamsFeature.FeatureName)
26+
.MapPost("/{teamId:guid}/heroes/{heroId:guid}",
27+
async (
28+
ISender sender,
29+
Guid teamId,
30+
Guid heroId,
31+
Request request,
32+
CancellationToken ct) =>
33+
{
34+
request.TeamId = teamId;
35+
request.HeroId = heroId;
36+
var result = await sender.Send(request, ct);
37+
return result.Match(_ => TypedResults.Created(), CustomResult.Problem);
38+
})
39+
.WithName("AddHeroToTeam")
40+
.ProducesPost();
41+
}
42+
}
43+
44+
internal sealed class Validator : AbstractValidator<Request>
45+
{
46+
public Validator()
47+
{
48+
RuleFor(v => v.HeroId)
49+
.NotEmpty();
50+
51+
RuleFor(v => v.TeamId)
52+
.NotEmpty();
53+
}
54+
}
55+
56+
internal sealed class Handler(AppDbContext dbContext)
57+
: IRequestHandler<Request, ErrorOr<Success>>
58+
{
59+
public async Task<ErrorOr<Success>> Handle(Request request, CancellationToken cancellationToken)
60+
{
61+
var teamId = TeamId.From(request.TeamId);
62+
var heroId = HeroId.From(request.HeroId);
63+
64+
var team = dbContext.Teams
65+
.WithSpecification(new TeamByIdSpec(teamId))
66+
.FirstOrDefault();
67+
68+
if (team is null)
69+
return TeamErrors.NotFound;
70+
71+
var hero = dbContext.Heroes
72+
.WithSpecification(new HeroByIdSpec(heroId))
73+
.FirstOrDefault();
74+
75+
if (hero is null)
76+
return HeroErrors.NotFound;
77+
78+
team.AddHero(hero);
79+
await dbContext.SaveChangesAsync(cancellationToken);
80+
81+
return new Success();
82+
}
83+
}
84+
}

0 commit comments

Comments
 (0)