Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use FastEndpoints instead of Ardalis.ApiEndpoints and MinimalApi.Endpoint #56

Merged
merged 5 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<PackageVersion Include="BlazorInputFile" Version="0.2.0" />
<PackageVersion Include="Blazored.LocalStorage" Version="4.5.0" />
<PackageVersion Include="BuildBundlerMinifier" Version="3.2.449" PrivateAssets="All" />
<PackageVersion Include="FastEndpoints" Version="5.27.0" />
<PackageVersion Include="FluentValidation" Version="11.9.2" />
<PackageVersion Include="MediatR" Version="12.3.0" />
<PackageVersion Include="Microsoft.AspNetCore.Components.Authorization" Version="$(AspNetVersion)" />
Expand Down
30 changes: 16 additions & 14 deletions src/PublicApi/AuthEndpoints/AuthenticateEndpoint.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
using System.Threading;
using System.Threading.Tasks;
using Ardalis.ApiEndpoints;
using FastEndpoints;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using Microsoft.eShopWeb.Infrastructure.Identity;
using Swashbuckle.AspNetCore.Annotations;

namespace Microsoft.eShopWeb.PublicApi.AuthEndpoints;

/// <summary>
/// Authenticates a user
/// </summary>
public class AuthenticateEndpoint : EndpointBaseAsync
.WithRequest<AuthenticateRequest>
.WithActionResult<AuthenticateResponse>
public class AuthenticateEndpoint : Endpoint<AuthenticateRequest, AuthenticateResponse>
{
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly ITokenClaimsService _tokenClaimsService;
Expand All @@ -26,14 +24,18 @@ public AuthenticateEndpoint(SignInManager<ApplicationUser> signInManager,
_tokenClaimsService = tokenClaimsService;
}

[HttpPost("api/authenticate")]
[SwaggerOperation(
Summary = "Authenticates a user",
Description = "Authenticates a user",
OperationId = "auth.authenticate",
Tags = new[] { "AuthEndpoints" })
]
public override async Task<ActionResult<AuthenticateResponse>> HandleAsync(AuthenticateRequest request,
public override void Configure()
{
Post("api/authenticate");
AllowAnonymous();
Description(d =>
d.WithSummary("Authenticates a user")
.WithDescription("Authenticates a user")
.WithName("auth.authenticate")
.WithTags("AuthEndpoints"));
}

public override async Task<AuthenticateResponse> ExecuteAsync(AuthenticateRequest request,
CancellationToken cancellationToken = default)
{
var response = new AuthenticateResponse(request.CorrelationId());
Expand Down
34 changes: 16 additions & 18 deletions src/PublicApi/CatalogBrandEndpoints/CatalogBrandListEndpoint.cs
Original file line number Diff line number Diff line change
@@ -1,46 +1,44 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AutoMapper;
using Microsoft.AspNetCore.Builder;
using FastEndpoints;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using MinimalApi.Endpoint;

namespace Microsoft.eShopWeb.PublicApi.CatalogBrandEndpoints;

/// <summary>
/// List Catalog Brands
/// </summary>
public class CatalogBrandListEndpoint : IEndpoint<IResult, IRepository<CatalogBrand>>
public class CatalogBrandListEndpoint : EndpointWithoutRequest<ListCatalogBrandsResponse>
{
private readonly IMapper _mapper;
private readonly IRepository<CatalogBrand> _catalogBrandRepository;
private readonly AutoMapper.IMapper _mapper;

public CatalogBrandListEndpoint(IMapper mapper)
public CatalogBrandListEndpoint(IRepository<CatalogBrand> catalogBrandRepository, AutoMapper.IMapper mapper)
{
_catalogBrandRepository = catalogBrandRepository;
_mapper = mapper;
}

public void AddRoute(IEndpointRouteBuilder app)
public override void Configure()
{
app.MapGet("api/catalog-brands",
async (IRepository<CatalogBrand> catalogBrandRepository) =>
{
return await HandleAsync(catalogBrandRepository);
})
.Produces<ListCatalogBrandsResponse>()
.WithTags("CatalogBrandEndpoints");
Get("api/catalog-brands");
AllowAnonymous();
Description(d =>
d.Produces<ListCatalogBrandsResponse>()
.WithTags("CatalogBrandEndpoints"));
}

public async Task<IResult> HandleAsync(IRepository<CatalogBrand> catalogBrandRepository)
public override async Task<ListCatalogBrandsResponse> ExecuteAsync(CancellationToken ct)
{
var response = new ListCatalogBrandsResponse();

var items = await catalogBrandRepository.ListAsync();
var items = await _catalogBrandRepository.ListAsync(ct);

response.CatalogBrands.AddRange(items.Select(_mapper.Map<CatalogBrandDto>));

return Results.Ok(response);
return response;
}
}
37 changes: 19 additions & 18 deletions src/PublicApi/CatalogItemEndpoints/CatalogItemGetByIdEndpoint.cs
Original file line number Diff line number Diff line change
@@ -1,43 +1,44 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using System.Threading;
using System.Threading.Tasks;
using FastEndpoints;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using MinimalApi.Endpoint;

namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints;

/// <summary>
/// Get a Catalog Item by Id
/// </summary>
public class CatalogItemGetByIdEndpoint : IEndpoint<IResult, GetByIdCatalogItemRequest, IRepository<CatalogItem>>
public class CatalogItemGetByIdEndpoint
: Endpoint<GetByIdCatalogItemRequest, Results<Ok<GetByIdCatalogItemResponse>, NotFound>>
{
private readonly IRepository<CatalogItem> _itemRepository;
private readonly IUriComposer _uriComposer;

public CatalogItemGetByIdEndpoint(IUriComposer uriComposer)
public CatalogItemGetByIdEndpoint(IRepository<CatalogItem> itemRepository, IUriComposer uriComposer)
{
_itemRepository = itemRepository;
_uriComposer = uriComposer;
}

public void AddRoute(IEndpointRouteBuilder app)
public override void Configure()
{
app.MapGet("api/catalog-items/{catalogItemId}",
async (int catalogItemId, IRepository<CatalogItem> itemRepository) =>
{
return await HandleAsync(new GetByIdCatalogItemRequest(catalogItemId), itemRepository);
})
.Produces<GetByIdCatalogItemResponse>()
.WithTags("CatalogItemEndpoints");
Get("api/catalog-items/{catalogItemId}");
AllowAnonymous();
Description(d =>
d.Produces<GetByIdCatalogItemResponse>()
.WithTags("CatalogItemEndpoints"));
}

public async Task<IResult> HandleAsync(GetByIdCatalogItemRequest request, IRepository<CatalogItem> itemRepository)
public override async Task<Results<Ok<GetByIdCatalogItemResponse>, NotFound>> ExecuteAsync(GetByIdCatalogItemRequest request, CancellationToken ct)
{
var response = new GetByIdCatalogItemResponse(request.CorrelationId());

var item = await itemRepository.GetByIdAsync(request.CatalogItemId);
var item = await _itemRepository.GetByIdAsync(request.CatalogItemId, ct);
if (item is null)
return Results.NotFound();
return TypedResults.NotFound();

response.CatalogItem = new CatalogItemDto
{
Expand All @@ -49,6 +50,6 @@ public async Task<IResult> HandleAsync(GetByIdCatalogItemRequest request, IRepos
PictureUri = _uriComposer.ComposePicUri(item.PictureUri),
Price = item.Price
};
return Results.Ok(response);
return TypedResults.Ok(response);
}
}
39 changes: 19 additions & 20 deletions src/PublicApi/CatalogItemEndpoints/CatalogItemListPagedEndpoint.cs
Original file line number Diff line number Diff line change
@@ -1,57 +1,56 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AutoMapper;
using Microsoft.AspNetCore.Builder;
using FastEndpoints;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using Microsoft.eShopWeb.ApplicationCore.Specifications;
using MinimalApi.Endpoint;

namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints;

/// <summary>
/// List Catalog Items (paged)
/// </summary>
public class CatalogItemListPagedEndpoint : IEndpoint<IResult, ListPagedCatalogItemRequest, IRepository<CatalogItem>>
public class CatalogItemListPagedEndpoint : Endpoint<ListPagedCatalogItemRequest, ListPagedCatalogItemResponse>
{
private readonly IRepository<CatalogItem> _itemRepository;
private readonly IUriComposer _uriComposer;
private readonly IMapper _mapper;
private readonly AutoMapper.IMapper _mapper;

public CatalogItemListPagedEndpoint(IUriComposer uriComposer, IMapper mapper)
public CatalogItemListPagedEndpoint(IRepository<CatalogItem> itemRepository, IUriComposer uriComposer, AutoMapper.IMapper mapper)
{
_itemRepository = itemRepository;
_uriComposer = uriComposer;
_mapper = mapper;
}

public void AddRoute(IEndpointRouteBuilder app)
public override void Configure()
{
app.MapGet("api/catalog-items",
async (int? pageSize, int? pageIndex, int? catalogBrandId, int? catalogTypeId, IRepository<CatalogItem> itemRepository) =>
{
return await HandleAsync(new ListPagedCatalogItemRequest(pageSize, pageIndex, catalogBrandId, catalogTypeId), itemRepository);
})
.Produces<ListPagedCatalogItemResponse>()
.WithTags("CatalogItemEndpoints");
Get("api/catalog-items");
AllowAnonymous();
Description(d =>
d.Produces<ListPagedCatalogItemResponse>()
.WithTags("CatalogItemEndpoints"));
}

public async Task<IResult> HandleAsync(ListPagedCatalogItemRequest request, IRepository<CatalogItem> itemRepository)
public override async Task<ListPagedCatalogItemResponse> ExecuteAsync(ListPagedCatalogItemRequest request, CancellationToken ct)
{
await Task.Delay(1000);
await Task.Delay(1000, ct);

var response = new ListPagedCatalogItemResponse(request.CorrelationId());

var filterSpec = new CatalogFilterSpecification(request.CatalogBrandId, request.CatalogTypeId);
int totalItems = await itemRepository.CountAsync(filterSpec);
int totalItems = await _itemRepository.CountAsync(filterSpec, ct);

var pagedSpec = new CatalogFilterPaginatedSpecification(
skip: request.PageIndex * request.PageSize,
take: request.PageSize,
brandId: request.CatalogBrandId,
typeId: request.CatalogTypeId);

var items = await itemRepository.ListAsync(pagedSpec);
var items = await _itemRepository.ListAsync(pagedSpec, ct);

response.CatalogItems.AddRange(items.Select(_mapper.Map<CatalogItemDto>));
foreach (CatalogItemDto item in response.CatalogItems)
Expand All @@ -68,6 +67,6 @@ public async Task<IResult> HandleAsync(ListPagedCatalogItemRequest request, IRep
response.PageCount = totalItems > 0 ? 1 : 0;
}

return Results.Ok(response);
return response;
}
}
41 changes: 20 additions & 21 deletions src/PublicApi/CatalogItemEndpoints/CreateCatalogItemEndpoint.cs
Original file line number Diff line number Diff line change
@@ -1,54 +1,52 @@
using System.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
using FastEndpoints;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using Microsoft.eShopWeb.ApplicationCore.Exceptions;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using Microsoft.eShopWeb.ApplicationCore.Specifications;
using MinimalApi.Endpoint;

namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints;

/// <summary>
/// Creates a new Catalog Item
/// </summary>
public class CreateCatalogItemEndpoint : IEndpoint<IResult, CreateCatalogItemRequest, IRepository<CatalogItem>>
public class CreateCatalogItemEndpoint : Endpoint<CreateCatalogItemRequest, CreateCatalogItemResponse>
{
private readonly IRepository<CatalogItem> _itemRepository;
private readonly IUriComposer _uriComposer;

public CreateCatalogItemEndpoint(IUriComposer uriComposer)
public CreateCatalogItemEndpoint(IRepository<CatalogItem> itemRepository, IUriComposer uriComposer)
{
_itemRepository = itemRepository;
_uriComposer = uriComposer;
}

public void AddRoute(IEndpointRouteBuilder app)
public override void Configure()
{
app.MapPost("api/catalog-items",
[Authorize(Roles = BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS, AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] async
(CreateCatalogItemRequest request, IRepository<CatalogItem> itemRepository) =>
{
return await HandleAsync(request, itemRepository);
})
.Produces<CreateCatalogItemResponse>()
.WithTags("CatalogItemEndpoints");
Post("api/catalog-items");
Roles(BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS);
AuthSchemes(JwtBearerDefaults.AuthenticationScheme);
Description(d =>
d.Produces<CreateCatalogItemResponse>()
.WithTags("CatalogItemEndpoints"));
}

public async Task<IResult> HandleAsync(CreateCatalogItemRequest request, IRepository<CatalogItem> itemRepository)
public override async Task HandleAsync(CreateCatalogItemRequest request, CancellationToken ct)
{
var response = new CreateCatalogItemResponse(request.CorrelationId());

var catalogItemNameSpecification = new CatalogItemNameSpecification(request.Name);
var existingCataloogItem = await itemRepository.CountAsync(catalogItemNameSpecification);
var existingCataloogItem = await _itemRepository.CountAsync(catalogItemNameSpecification, ct);
if (existingCataloogItem > 0)
{
throw new DuplicateException($"A catalogItem with name {request.Name} already exists");
}

var newItem = new CatalogItem(request.CatalogTypeId, request.CatalogBrandId, request.Description, request.Name, request.Price, request.PictureUri);
newItem = await itemRepository.AddAsync(newItem);
newItem = await _itemRepository.AddAsync(newItem, ct);

if (newItem.Id != 0)
{
Expand All @@ -57,7 +55,7 @@ public async Task<IResult> HandleAsync(CreateCatalogItemRequest request, IReposi
// In production, we recommend uploading to a blob storage and deliver the image via CDN after a verification process.

newItem.UpdatePictureUri("eCatalog-item-default.png");
await itemRepository.UpdateAsync(newItem);
await _itemRepository.UpdateAsync(newItem, ct);
}

var dto = new CatalogItemDto
Expand All @@ -71,6 +69,7 @@ public async Task<IResult> HandleAsync(CreateCatalogItemRequest request, IReposi
Price = newItem.Price
};
response.CatalogItem = dto;
return Results.Created($"api/catalog-items/{dto.Id}", response);

await SendCreatedAtAsync<CatalogItemGetByIdEndpoint>(new{ CatalogItemId = dto.Id }, response, cancellation: ct);
}
}
Loading
Loading