Skip to content

Commit d6853ea

Browse files
committed
#105 Added Heroes endpoints
1 parent 9e54125 commit d6853ea

File tree

13 files changed

+654
-1
lines changed

13 files changed

+654
-1
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+
.MapGet("/{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: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using MediatR;
2+
using VerticalSliceArchitectureTemplate.Common.Extensions;
3+
4+
namespace VerticalSliceArchitectureTemplate.Features.Heroes.Queries;
5+
6+
public static class GetAllHeroesQuery
7+
{
8+
// SM: Duplication of DTO's?
9+
public record HeroDto(Guid Id, string Name, string Alias, int PowerLevel, IEnumerable<HeroPowerDto> Powers);
10+
public record HeroPowerDto(string Name, int PowerLevel);
11+
12+
public record Request : IRequest<ErrorOr<IReadOnlyList<HeroDto>>>;
13+
14+
public class Endpoint : IEndpoint
15+
{
16+
public static void MapEndpoint(IEndpointRouteBuilder endpoints)
17+
{
18+
endpoints
19+
.MapApiGroup(HeroesFeature.FeatureName)
20+
.MapGet("/",
21+
async (ISender sender, CancellationToken cancellationToken) =>
22+
{
23+
var request = new Request();
24+
var results = await sender.Send(request, cancellationToken);
25+
return TypedResults.Ok(results);
26+
})
27+
.WithName("GetAllHeroes")
28+
.ProducesGet<IReadOnlyList<HeroDto>>();
29+
}
30+
}
31+
32+
public class Validator : AbstractValidator<Request>
33+
{
34+
public Validator() {}
35+
}
36+
37+
internal sealed class Handler : IRequestHandler<Request, ErrorOr<IReadOnlyList<HeroDto>>>
38+
{
39+
private readonly AppDbContext _dbContext;
40+
41+
public Handler(AppDbContext dbContext)
42+
{
43+
_dbContext = dbContext;
44+
}
45+
46+
public async Task<ErrorOr<IReadOnlyList<HeroDto>>> Handle(
47+
Request request,
48+
CancellationToken cancellationToken)
49+
{
50+
return await _dbContext.Heroes
51+
.Select(h => new HeroDto(
52+
h.Id.Value,
53+
h.Name,
54+
h.Alias,
55+
h.PowerLevel,
56+
h.Powers.Select(p => new HeroPowerDto(p.Name, p.PowerLevel))))
57+
.ToListAsync(cancellationToken);
58+
}
59+
}
60+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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] public Guid TeamId { get; set; }
15+
[JsonIgnore] public Guid HeroId { get; set; }
16+
}
17+
18+
public class Endpoint : IEndpoint
19+
{
20+
public static void MapEndpoint(IEndpointRouteBuilder endpoints)
21+
{
22+
endpoints
23+
.MapApiGroup(TeamsFeature.FeatureName)
24+
.MapPost("/{teamId:guid}/heroes/{heroId:guid}",
25+
async (
26+
ISender sender,
27+
Guid teamId,
28+
Guid heroId,
29+
Request request,
30+
CancellationToken ct) =>
31+
{
32+
request.TeamId = teamId;
33+
request.HeroId = heroId;
34+
var result = await sender.Send(request, ct);
35+
return result.Match(_ => TypedResults.Created(), CustomResult.Problem);
36+
})
37+
.WithName("AddHeroToTeam")
38+
.ProducesPost();
39+
}
40+
}
41+
42+
internal sealed class Validator : AbstractValidator<Request>
43+
{
44+
public Validator()
45+
{
46+
RuleFor(v => v.HeroId)
47+
.NotEmpty();
48+
49+
RuleFor(v => v.TeamId)
50+
.NotEmpty();
51+
}
52+
}
53+
54+
internal sealed class Handler(AppDbContext dbContext)
55+
: IRequestHandler<Request, ErrorOr<Success>>
56+
{
57+
public async Task<ErrorOr<Success>> Handle(Request request, CancellationToken cancellationToken)
58+
{
59+
var teamId = TeamId.From(request.TeamId);
60+
var heroId = HeroId.From(request.HeroId);
61+
62+
var team = dbContext.Teams
63+
.WithSpecification(new TeamByIdSpec(teamId))
64+
.FirstOrDefault();
65+
66+
if (team is null)
67+
return TeamErrors.NotFound;
68+
69+
var hero = dbContext.Heroes
70+
.WithSpecification(new HeroByIdSpec(heroId))
71+
.FirstOrDefault();
72+
73+
if (hero is null)
74+
return HeroErrors.NotFound;
75+
76+
team.AddHero(hero);
77+
await dbContext.SaveChangesAsync(cancellationToken);
78+
79+
return new Success();
80+
}
81+
}
82+
}

0 commit comments

Comments
 (0)