Feature/migrate to minimal api (#662)

* migrate from classic controller to minimal api

* fix all PublicApi integration test

* update all nuget package add forget project

* fix pay now

* Adapt readme use in memory database

* undo AuthenticateEndpoint to use EndpointBaseAsync

* Update README.md

Co-authored-by: Steve Smith <steve@kentsmiths.com>

Co-authored-by: Steve Smith <steve@kentsmiths.com>
This commit is contained in:
Cédric Michel
2022-01-21 16:13:31 +01:00
committed by GitHub
parent 02b509711b
commit 1e13733d3d
63 changed files with 842 additions and 630 deletions

View File

@@ -10,7 +10,7 @@
<PackageReference Include="Ardalis.Specification" Version="5.2.0" />
<PackageReference Include="MediatR" Version="9.0.0" />
<PackageReference Include="System.Security.Claims" Version="4.3.0" />
<PackageReference Include="System.Text.Json" Version="6.0.0" />
<PackageReference Include="System.Text.Json" Version="6.0.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -7,11 +7,11 @@
<ItemGroup>
<PackageReference Include="Blazored.LocalStorage" Version="4.1.5" />
<PackageReference Include="BlazorInputFile" Version="0.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="6.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="6.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.1" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="6.0.0" />
<PackageReference Include="System.Net.Http.Json" Version="6.0.0" />
</ItemGroup>

View File

@@ -8,7 +8,7 @@
<ItemGroup>
<PackageReference Include="BlazorInputFile" Version="0.2.0" />
<PackageReference Include="FluentValidation" Version="10.3.5" />
<PackageReference Include="FluentValidation" Version="10.3.6" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.eShopWeb.Infrastructure.Data;
using Microsoft.eShopWeb.Infrastructure.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.eShopWeb.Infrastructure;
public static class Dependencies
{
public static void ConfigureServices(IConfiguration configuration, IServiceCollection services)
{
var useOnlyInMemoryDatabase = false;
if (configuration["UseOnlyInMemoryDatabase"] != null)
{
useOnlyInMemoryDatabase = bool.Parse(configuration["UseOnlyInMemoryDatabase"]);
}
if (useOnlyInMemoryDatabase)
{
services.AddDbContext<CatalogContext>(c =>
c.UseInMemoryDatabase("Catalog"));
services.AddDbContext<AppIdentityDbContext>(options =>
options.UseInMemoryDatabase("Identity"));
}
else
{
// use real database
// Requires LocalDB which can be installed with SQL Server Express 2016
// https://www.microsoft.com/en-us/download/details.aspx?id=54284
services.AddDbContext<CatalogContext>(c =>
c.UseSqlServer(configuration.GetConnectionString("CatalogConnection")));
// Add Identity DbContext
services.AddDbContext<AppIdentityDbContext>(options =>
options.UseSqlServer(configuration.GetConnectionString("IdentityConnection")));
}
}
}

View File

@@ -7,8 +7,9 @@
<ItemGroup>
<PackageReference Include="Ardalis.Specification.EntityFrameworkCore" Version="5.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="6.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.1" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.15.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -9,14 +9,17 @@ using Swashbuckle.AspNetCore.Annotations;
namespace Microsoft.eShopWeb.PublicApi.AuthEndpoints;
public class Authenticate : EndpointBaseAsync
/// <summary>
/// Authenticates a user
/// </summary>
public class AuthenticateEndpoint : EndpointBaseAsync
.WithRequest<AuthenticateRequest>
.WithActionResult<AuthenticateResponse>
{
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly ITokenClaimsService _tokenClaimsService;
public Authenticate(SignInManager<ApplicationUser> signInManager,
public AuthenticateEndpoint(SignInManager<ApplicationUser> signInManager,
ITokenClaimsService tokenClaimsService)
{
_signInManager = signInManager;
@@ -30,7 +33,7 @@ public class Authenticate : EndpointBaseAsync
OperationId = "auth.authenticate",
Tags = new[] { "AuthEndpoints" })
]
public override async Task<ActionResult<AuthenticateResponse>> HandleAsync(AuthenticateRequest request, CancellationToken cancellationToken)
public override async Task<ActionResult<AuthenticateResponse>> HandleAsync(AuthenticateRequest request, CancellationToken cancellationToken = default)
{
var response = new AuthenticateResponse(request.CorrelationId());

View File

@@ -0,0 +1,48 @@
using System.Linq;
using System.Threading.Tasks;
using AutoMapper;
using Microsoft.AspNetCore.Builder;
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>
{
private IRepository<CatalogBrand> _catalogBrandRepository;
private readonly IMapper _mapper;
public CatalogBrandListEndpoint(IMapper mapper)
{
_mapper = mapper;
}
public void AddRoute(IEndpointRouteBuilder app)
{
app.MapGet("api/catalog-brands",
async (IRepository<CatalogBrand> catalogBrandRepository) =>
{
_catalogBrandRepository = catalogBrandRepository;
return await HandleAsync();
})
.Produces<ListCatalogBrandsResponse>()
.WithTags("CatalogBrandEndpoints");
}
public async Task<IResult> HandleAsync()
{
var response = new ListCatalogBrandsResponse();
var items = await _catalogBrandRepository.ListAsync();
response.CatalogBrands.AddRange(items.Select(_mapper.Map<CatalogBrandDto>));
return Results.Ok(response);
}
}

View File

@@ -1,44 +0,0 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Ardalis.ApiEndpoints;
using AutoMapper;
using Microsoft.AspNetCore.Mvc;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using Swashbuckle.AspNetCore.Annotations;
namespace Microsoft.eShopWeb.PublicApi.CatalogBrandEndpoints;
public class List : EndpointBaseAsync
.WithoutRequest
.WithActionResult<ListCatalogBrandsResponse>
{
private readonly IRepository<CatalogBrand> _catalogBrandRepository;
private readonly IMapper _mapper;
public List(IRepository<CatalogBrand> catalogBrandRepository,
IMapper mapper)
{
_catalogBrandRepository = catalogBrandRepository;
_mapper = mapper;
}
[HttpGet("api/catalog-brands")]
[SwaggerOperation(
Summary = "List Catalog Brands",
Description = "List Catalog Brands",
OperationId = "catalog-brands.List",
Tags = new[] { "CatalogBrandEndpoints" })
]
public override async Task<ActionResult<ListCatalogBrandsResponse>> HandleAsync(CancellationToken cancellationToken)
{
var response = new ListCatalogBrandsResponse();
var items = await _catalogBrandRepository.ListAsync(cancellationToken);
response.CatalogBrands.AddRange(items.Select(_mapper.Map<CatalogBrandDto>));
return Ok(response);
}
}

View File

@@ -0,0 +1,11 @@
namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints;
public class GetByIdCatalogItemRequest : BaseRequest
{
public int CatalogItemId { get; init; }
public GetByIdCatalogItemRequest(int catalogItemId)
{
CatalogItemId = catalogItemId;
}
}

View File

@@ -0,0 +1,56 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
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.CatalogItemEndpoints;
/// <summary>
/// Get a Catalog Item by Id
/// </summary>
public class CatalogItemGetByIdEndpoint : IEndpoint<IResult, GetByIdCatalogItemRequest>
{
private IRepository<CatalogItem> _itemRepository;
private readonly IUriComposer _uriComposer;
public CatalogItemGetByIdEndpoint(IUriComposer uriComposer)
{
_uriComposer = uriComposer;
}
public void AddRoute(IEndpointRouteBuilder app)
{
app.MapGet("api/catalog-items/{catalogItemId}",
async (int catalogItemId, IRepository<CatalogItem> itemRepository) =>
{
_itemRepository = itemRepository;
return await HandleAsync(new GetByIdCatalogItemRequest(catalogItemId));
})
.Produces<GetByIdCatalogItemResponse>()
.WithTags("CatalogItemEndpoints");
}
public async Task<IResult> HandleAsync(GetByIdCatalogItemRequest request)
{
var response = new GetByIdCatalogItemResponse(request.CorrelationId());
var item = await _itemRepository.GetByIdAsync(request.CatalogItemId);
if (item is null)
return Results.NotFound();
response.CatalogItem = new CatalogItemDto
{
Id = item.Id,
CatalogBrandId = item.CatalogBrandId,
CatalogTypeId = item.CatalogTypeId,
Description = item.Description,
Name = item.Name,
PictureUri = _uriComposer.ComposePicUri(item.PictureUri),
Price = item.Price
};
return Results.Ok(response);
}
}

View File

@@ -0,0 +1,17 @@
namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints;
public class ListPagedCatalogItemRequest : BaseRequest
{
public int? PageSize { get; init; }
public int? PageIndex { get; init; }
public int? CatalogBrandId { get; init; }
public int? CatalogTypeId { get; init; }
public ListPagedCatalogItemRequest(int? pageSize, int? pageIndex, int? catalogBrandId, int? catalogTypeId)
{
PageSize = pageSize ?? 0;
PageIndex = pageIndex ?? 0;
CatalogBrandId = catalogBrandId;
CatalogTypeId = catalogTypeId;
}
}

View File

@@ -1,55 +1,58 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Ardalis.ApiEndpoints;
using AutoMapper;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using Microsoft.eShopWeb.ApplicationCore.Specifications;
using Swashbuckle.AspNetCore.Annotations;
using MinimalApi.Endpoint;
namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints;
public class ListPaged : EndpointBaseAsync
.WithRequest<ListPagedCatalogItemRequest>
.WithActionResult<ListPagedCatalogItemResponse>
/// <summary>
/// List Catalog Items (paged)
/// </summary>
public class CatalogItemListPagedEndpoint : IEndpoint<IResult, ListPagedCatalogItemRequest>
{
private readonly IRepository<CatalogItem> _itemRepository;
private IRepository<CatalogItem> _itemRepository;
private readonly IUriComposer _uriComposer;
private readonly IMapper _mapper;
public ListPaged(IRepository<CatalogItem> itemRepository,
IUriComposer uriComposer,
IMapper mapper)
public CatalogItemListPagedEndpoint(IUriComposer uriComposer, IMapper mapper)
{
_itemRepository = itemRepository;
_uriComposer = uriComposer;
_mapper = mapper;
}
[HttpGet("api/catalog-items")]
[SwaggerOperation(
Summary = "List Catalog Items (paged)",
Description = "List Catalog Items (paged)",
OperationId = "catalog-items.ListPaged",
Tags = new[] { "CatalogItemEndpoints" })
]
public override async Task<ActionResult<ListPagedCatalogItemResponse>> HandleAsync([FromQuery] ListPagedCatalogItemRequest request, CancellationToken cancellationToken)
public void AddRoute(IEndpointRouteBuilder app)
{
app.MapGet("api/catalog-items",
async (int? pageSize, int? pageIndex, int? catalogBrandId, int? catalogTypeId, IRepository<CatalogItem> itemRepository) =>
{
_itemRepository = itemRepository;
return await HandleAsync(new ListPagedCatalogItemRequest(pageSize, pageIndex, catalogBrandId, catalogTypeId));
})
.Produces<ListPagedCatalogItemResponse>()
.WithTags("CatalogItemEndpoints");
}
public async Task<IResult> HandleAsync(ListPagedCatalogItemRequest request)
{
var response = new ListPagedCatalogItemResponse(request.CorrelationId());
var filterSpec = new CatalogFilterSpecification(request.CatalogBrandId, request.CatalogTypeId);
int totalItems = await _itemRepository.CountAsync(filterSpec, cancellationToken);
int totalItems = await _itemRepository.CountAsync(filterSpec);
var pagedSpec = new CatalogFilterPaginatedSpecification(
skip: request.PageIndex * request.PageSize,
take: request.PageSize,
skip: request.PageIndex.Value * request.PageSize.Value,
take: request.PageSize.Value,
brandId: request.CatalogBrandId,
typeId: request.CatalogTypeId);
var items = await _itemRepository.ListAsync(pagedSpec, cancellationToken);
var items = await _itemRepository.ListAsync(pagedSpec);
response.CatalogItems.AddRange(items.Select(_mapper.Map<CatalogItemDto>));
foreach (CatalogItemDto item in response.CatalogItems)
@@ -59,13 +62,13 @@ public class ListPaged : EndpointBaseAsync
if (request.PageSize > 0)
{
response.PageCount = int.Parse(Math.Ceiling((decimal)totalItems / request.PageSize).ToString());
response.PageCount = int.Parse(Math.Ceiling((decimal)totalItems / request.PageSize.Value).ToString());
}
else
{
response.PageCount = totalItems > 0 ? 1 : 0;
}
return Ok(response);
return Results.Ok(response);
}
}

View File

@@ -1,52 +1,56 @@
using System.Threading;
using System.Threading.Tasks;
using Ardalis.ApiEndpoints;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
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 Swashbuckle.AspNetCore.Annotations;
using MinimalApi.Endpoint;
namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints;
[Authorize(Roles = BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS, AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class Create : EndpointBaseAsync
.WithRequest<CreateCatalogItemRequest>
.WithActionResult<CreateCatalogItemResponse>
/// <summary>
/// Creates a new Catalog Item
/// </summary>
public class CreateCatalogItemEndpoint : IEndpoint<IResult, CreateCatalogItemRequest>
{
private readonly IRepository<CatalogItem> _itemRepository;
private IRepository<CatalogItem> _itemRepository;
private readonly IUriComposer _uriComposer;
public Create(IRepository<CatalogItem> itemRepository,
IUriComposer uriComposer)
public CreateCatalogItemEndpoint(IUriComposer uriComposer)
{
_itemRepository = itemRepository;
_uriComposer = uriComposer;
}
[HttpPost("api/catalog-items")]
[SwaggerOperation(
Summary = "Creates a new Catalog Item",
Description = "Creates a new Catalog Item",
OperationId = "catalog-items.create",
Tags = new[] { "CatalogItemEndpoints" })
]
public override async Task<ActionResult<CreateCatalogItemResponse>> HandleAsync(CreateCatalogItemRequest request, CancellationToken cancellationToken)
public void AddRoute(IEndpointRouteBuilder app)
{
app.MapPost("api/catalog-items",
[Authorize(Roles = BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS, AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] async
(CreateCatalogItemRequest request, IRepository<CatalogItem> itemRepository) =>
{
_itemRepository = itemRepository;
return await HandleAsync(request);
})
.Produces<CreateCatalogItemResponse>()
.WithTags("CatalogItemEndpoints");
}
public async Task<IResult> HandleAsync(CreateCatalogItemRequest request)
{
var response = new CreateCatalogItemResponse(request.CorrelationId());
var catalogItemNameSpecification = new CatalogItemNameSpecification(request.Name);
var existingCataloogItem = await _itemRepository.CountAsync(catalogItemNameSpecification, cancellationToken);
var existingCataloogItem = await _itemRepository.CountAsync(catalogItemNameSpecification);
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, cancellationToken);
newItem = await _itemRepository.AddAsync(newItem);
if (newItem.Id != 0)
{
@@ -55,7 +59,7 @@ public class Create : EndpointBaseAsync
// 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, cancellationToken);
await _itemRepository.UpdateAsync(newItem);
}
var dto = new CatalogItemDto
@@ -69,8 +73,6 @@ public class Create : EndpointBaseAsync
Price = newItem.Price
};
response.CatalogItem = dto;
return response;
return Results.Created($"api/catalog-items/{dto.Id}", response);
}
}

View File

@@ -1,9 +0,0 @@
using Microsoft.AspNetCore.Mvc;
namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints;
public class DeleteCatalogItemRequest : BaseRequest
{
//[FromRoute]
public int CatalogItemId { get; set; }
}

View File

@@ -1,43 +0,0 @@
using System.Threading;
using System.Threading.Tasks;
using Ardalis.ApiEndpoints;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using Swashbuckle.AspNetCore.Annotations;
namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints;
[Authorize(Roles = BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS, AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class Delete : EndpointBaseAsync
.WithRequest<DeleteCatalogItemRequest>
.WithActionResult<DeleteCatalogItemResponse>
{
private readonly IRepository<CatalogItem> _itemRepository;
public Delete(IRepository<CatalogItem> itemRepository)
{
_itemRepository = itemRepository;
}
[HttpDelete("api/catalog-items/{CatalogItemId}")]
[SwaggerOperation(
Summary = "Deletes a Catalog Item",
Description = "Deletes a Catalog Item",
OperationId = "catalog-items.Delete",
Tags = new[] { "CatalogItemEndpoints" })
]
public override async Task<ActionResult<DeleteCatalogItemResponse>> HandleAsync([FromRoute] DeleteCatalogItemRequest request, CancellationToken cancellationToken)
{
var response = new DeleteCatalogItemResponse(request.CorrelationId());
var itemToDelete = await _itemRepository.GetByIdAsync(request.CatalogItemId, cancellationToken);
if (itemToDelete is null) return NotFound();
await _itemRepository.DeleteAsync(itemToDelete, cancellationToken);
return Ok(response);
}
}

View File

@@ -0,0 +1,11 @@
namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints;
public class DeleteCatalogItemRequest : BaseRequest
{
public int CatalogItemId { get; init; }
public DeleteCatalogItemRequest(int catalogItemId)
{
CatalogItemId = catalogItemId;
}
}

View File

@@ -0,0 +1,45 @@
using System.Threading.Tasks;
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.Interfaces;
using MinimalApi.Endpoint;
namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints;
/// <summary>
/// Deletes a Catalog Item
/// </summary>
public class DeleteCatalogItemEndpoint : IEndpoint<IResult, DeleteCatalogItemRequest>
{
private IRepository<CatalogItem> _itemRepository;
public void AddRoute(IEndpointRouteBuilder app)
{
app.MapDelete("api/catalog-items/{catalogItemId}",
[Authorize(Roles = BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS, AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] async
(int catalogItemId, IRepository<CatalogItem> itemRepository) =>
{
_itemRepository = itemRepository;
return await HandleAsync(new DeleteCatalogItemRequest(catalogItemId));
})
.Produces<DeleteCatalogItemResponse>()
.WithTags("CatalogItemEndpoints");
}
public async Task<IResult> HandleAsync(DeleteCatalogItemRequest request)
{
var response = new DeleteCatalogItemResponse(request.CorrelationId());
var itemToDelete = await _itemRepository.GetByIdAsync(request.CatalogItemId);
if (itemToDelete is null)
return Results.NotFound();
await _itemRepository.DeleteAsync(itemToDelete);
return Results.Ok(response);
}
}

View File

@@ -1,6 +0,0 @@
namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints;
public class GetByIdCatalogItemRequest : BaseRequest
{
public int CatalogItemId { get; set; }
}

View File

@@ -1,50 +0,0 @@
using System.Threading;
using System.Threading.Tasks;
using Ardalis.ApiEndpoints;
using Microsoft.AspNetCore.Mvc;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using Swashbuckle.AspNetCore.Annotations;
namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints;
public class GetById : EndpointBaseAsync
.WithRequest<GetByIdCatalogItemRequest>
.WithActionResult<GetByIdCatalogItemResponse>
{
private readonly IRepository<CatalogItem> _itemRepository;
private readonly IUriComposer _uriComposer;
public GetById(IRepository<CatalogItem> itemRepository, IUriComposer uriComposer)
{
_itemRepository = itemRepository;
_uriComposer = uriComposer;
}
[HttpGet("api/catalog-items/{CatalogItemId}")]
[SwaggerOperation(
Summary = "Get a Catalog Item by Id",
Description = "Gets a Catalog Item by Id",
OperationId = "catalog-items.GetById",
Tags = new[] { "CatalogItemEndpoints" })
]
public override async Task<ActionResult<GetByIdCatalogItemResponse>> HandleAsync([FromRoute] GetByIdCatalogItemRequest request, CancellationToken cancellationToken)
{
var response = new GetByIdCatalogItemResponse(request.CorrelationId());
var item = await _itemRepository.GetByIdAsync(request.CatalogItemId, cancellationToken);
if (item is null) return NotFound();
response.CatalogItem = new CatalogItemDto
{
Id = item.Id,
CatalogBrandId = item.CatalogBrandId,
CatalogTypeId = item.CatalogTypeId,
Description = item.Description,
Name = item.Name,
PictureUri = _uriComposer.ComposePicUri(item.PictureUri),
Price = item.Price
};
return Ok(response);
}
}

View File

@@ -1,9 +0,0 @@
namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints;
public class ListPagedCatalogItemRequest : BaseRequest
{
public int PageSize { get; set; }
public int PageIndex { get; set; }
public int? CatalogBrandId { get; set; }
public int? CatalogTypeId { get; set; }
}

View File

@@ -1,60 +0,0 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Ardalis.ApiEndpoints;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using Swashbuckle.AspNetCore.Annotations;
namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints;
[Authorize(Roles = BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS, AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class Update : EndpointBaseAsync
.WithRequest<UpdateCatalogItemRequest>
.WithActionResult<UpdateCatalogItemResponse>
{
private readonly IRepository<CatalogItem> _itemRepository;
private readonly IUriComposer _uriComposer;
public Update(IRepository<CatalogItem> itemRepository, IUriComposer uriComposer)
{
_itemRepository = itemRepository;
_uriComposer = uriComposer;
}
[HttpPut("api/catalog-items")]
[SwaggerOperation(
Summary = "Updates a Catalog Item",
Description = "Updates a Catalog Item",
OperationId = "catalog-items.update",
Tags = new[] { "CatalogItemEndpoints" })
]
public override async Task<ActionResult<UpdateCatalogItemResponse>> HandleAsync(UpdateCatalogItemRequest request, CancellationToken cancellationToken)
{
var response = new UpdateCatalogItemResponse(request.CorrelationId());
var existingItem = await _itemRepository.GetByIdAsync(request.Id, cancellationToken);
existingItem.UpdateDetails(request.Name, request.Description, request.Price);
existingItem.UpdateBrand(request.CatalogBrandId);
existingItem.UpdateType(request.CatalogTypeId);
await _itemRepository.UpdateAsync(existingItem, cancellationToken);
var dto = new CatalogItemDto
{
Id = existingItem.Id,
CatalogBrandId = existingItem.CatalogBrandId,
CatalogTypeId = existingItem.CatalogTypeId,
Description = existingItem.Description,
Name = existingItem.Name,
PictureUri = _uriComposer.ComposePicUri(existingItem.PictureUri),
Price = existingItem.Price
};
response.CatalogItem = dto;
return response;
}
}

View File

@@ -0,0 +1,64 @@
using System.Threading.Tasks;
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.Interfaces;
using MinimalApi.Endpoint;
namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints;
/// <summary>
/// Updates a Catalog Item
/// </summary>
public class UpdateCatalogItemEndpoint : IEndpoint<IResult, UpdateCatalogItemRequest>
{
private IRepository<CatalogItem> _itemRepository;
private readonly IUriComposer _uriComposer;
public UpdateCatalogItemEndpoint(IUriComposer uriComposer)
{
_uriComposer = uriComposer;
}
public void AddRoute(IEndpointRouteBuilder app)
{
app.MapPut("api/catalog-items",
[Authorize(Roles = BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS, AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] async
(UpdateCatalogItemRequest request, IRepository<CatalogItem> itemRepository) =>
{
_itemRepository = itemRepository;
return await HandleAsync(request);
})
.Produces<UpdateCatalogItemResponse>()
.WithTags("CatalogItemEndpoints");
}
public async Task<IResult> HandleAsync(UpdateCatalogItemRequest request)
{
var response = new UpdateCatalogItemResponse(request.CorrelationId());
var existingItem = await _itemRepository.GetByIdAsync(request.Id);
existingItem.UpdateDetails(request.Name, request.Description, request.Price);
existingItem.UpdateBrand(request.CatalogBrandId);
existingItem.UpdateType(request.CatalogTypeId);
await _itemRepository.UpdateAsync(existingItem);
var dto = new CatalogItemDto
{
Id = existingItem.Id,
CatalogBrandId = existingItem.CatalogBrandId,
CatalogTypeId = existingItem.CatalogTypeId,
Description = existingItem.Description,
Name = existingItem.Name,
PictureUri = _uriComposer.ComposePicUri(existingItem.PictureUri),
Price = existingItem.Price
};
response.CatalogItem = dto;
return Results.Ok(response);
}
}

View File

@@ -0,0 +1,48 @@
using System.Linq;
using System.Threading.Tasks;
using AutoMapper;
using Microsoft.AspNetCore.Builder;
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.CatalogTypeEndpoints;
/// <summary>
/// List Catalog Types
/// </summary>
public class CatalogTypeListEndpoint : IEndpoint<IResult>
{
private IRepository<CatalogType> _catalogTypeRepository;
private readonly IMapper _mapper;
public CatalogTypeListEndpoint(IMapper mapper)
{
_mapper = mapper;
}
public void AddRoute(IEndpointRouteBuilder app)
{
app.MapGet("api/catalog-types",
async (IRepository<CatalogType> catalogTypeRepository) =>
{
_catalogTypeRepository = catalogTypeRepository;
return await HandleAsync();
})
.Produces<ListCatalogTypesResponse>()
.WithTags("CatalogTypeEndpoints");
}
public async Task<IResult> HandleAsync()
{
var response = new ListCatalogTypesResponse();
var items = await _catalogTypeRepository.ListAsync();
response.CatalogTypes.AddRange(items.Select(_mapper.Map<CatalogTypeDto>));
return Results.Ok(response);
}
}

View File

@@ -1,44 +0,0 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Ardalis.ApiEndpoints;
using AutoMapper;
using Microsoft.AspNetCore.Mvc;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using Swashbuckle.AspNetCore.Annotations;
namespace Microsoft.eShopWeb.PublicApi.CatalogTypeEndpoints;
public class List : EndpointBaseAsync
.WithoutRequest
.WithActionResult<ListCatalogTypesResponse>
{
private readonly IRepository<CatalogType> _catalogTypeRepository;
private readonly IMapper _mapper;
public List(IRepository<CatalogType> catalogTypeRepository,
IMapper mapper)
{
_catalogTypeRepository = catalogTypeRepository;
_mapper = mapper;
}
[HttpGet("api/catalog-types")]
[SwaggerOperation(
Summary = "List Catalog Types",
Description = "List Catalog Types",
OperationId = "catalog-types.List",
Tags = new[] { "CatalogTypeEndpoints" })
]
public override async Task<ActionResult<ListCatalogTypesResponse>> HandleAsync(CancellationToken cancellationToken)
{
var response = new ListCatalogTypesResponse();
var items = await _catalogTypeRepository.ListAsync(cancellationToken);
response.CatalogTypes.AddRange(items.Select(_mapper.Map<CatalogTypeDto>));
return Ok(response);
}
}

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using BlazorShared;
using BlazorShared.Models;
using MediatR;
@@ -9,7 +8,6 @@ using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.eShopWeb;
using Microsoft.eShopWeb.ApplicationCore.Constants;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
@@ -25,20 +23,18 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using MinimalApi.Endpoint.Configurations.Extensions;
using MinimalApi.Endpoint.Extensions;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpoints();
//Use to force loading of appsettings.json of test project
builder.Configuration.AddConfigurationFile();
builder.Logging.AddConsole();
// use real database
// Requires LocalDB which can be installed with SQL Server Express 2016
// https://www.microsoft.com/en-us/download/details.aspx?id=54284
builder.Services.AddDbContext<CatalogContext>(c =>
c.UseSqlServer(builder.Configuration.GetConnectionString("CatalogConnection")));
// Add Identity DbContext
builder.Services.AddDbContext<AppIdentityDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("IdentityConnection")));
Microsoft.eShopWeb.Infrastructure.Dependencies.ConfigureServices(builder.Configuration, builder.Services);
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<AppIdentityDbContext>()
@@ -92,6 +88,7 @@ builder.Services.AddControllers();
builder.Services.AddMediatR(typeof(CatalogItem).Assembly);
builder.Services.AddAutoMapper(typeof(MappingProfile).Assembly);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" });
@@ -182,5 +179,8 @@ using (var scope = app.Services.CreateScope())
}
}
app.MapEndpoints();
app.Logger.LogInformation("LAUNCHING PublicApi");
app.Run();
public partial class Program { }

View File

@@ -13,21 +13,22 @@
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="8.1.1" />
<PackageReference Include="MediatR" Version="9.0.0" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="9.0.0" />
<PackageReference Include="MinimalApi.Endpoint" Version="1.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="6.2.3" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.2.3" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.0">
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="6.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="6.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="6.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.14.0" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="6.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="6.0.1" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.15.0" />
</ItemGroup>

View File

@@ -6,12 +6,12 @@ public class BasketItemViewModel
{
public int Id { get; set; }
public int CatalogItemId { get; set; }
public string ProductName { get; set; }
public string? ProductName { get; set; }
public decimal UnitPrice { get; set; }
public decimal OldUnitPrice { get; set; }
[Range(0, int.MaxValue, ErrorMessage = "Quantity must be bigger than 0")]
public int Quantity { get; set; }
public string PictureUrl { get; set; }
public string? PictureUrl { get; set; }
}

View File

@@ -8,7 +8,6 @@ using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.EntityFrameworkCore;
using Microsoft.eShopWeb;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using Microsoft.eShopWeb.Infrastructure.Data;
@@ -22,15 +21,7 @@ var builder = WebApplication.CreateBuilder(args);
builder.Logging.AddConsole();
// use real database
// Requires LocalDB which can be installed with SQL Server Express 2016
// https://www.microsoft.com/en-us/download/details.aspx?id=54284
builder.Services.AddDbContext<CatalogContext>(c =>
c.UseSqlServer(builder.Configuration.GetConnectionString("CatalogConnection")));
// Add Identity DbContext
builder.Services.AddDbContext<AppIdentityDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("IdentityConnection")));
Microsoft.eShopWeb.Infrastructure.Dependencies.ConfigureServices(builder.Configuration, builder.Services);
builder.Services.AddCookieSettings();

View File

@@ -1,7 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using Microsoft.eShopWeb.ApplicationCore.Specifications;

View File

@@ -20,16 +20,16 @@
<PackageReference Include="MediatR" Version="9.0.0" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="9.0.0" />
<PackageReference Include="BuildBundlerMinifier" Version="3.2.449" Condition="'$(Configuration)'=='Release'" PrivateAssets="All" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="6.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="6.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="6.0.1" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="6.0.1" />
<PackageReference Include="Microsoft.Web.LibraryManager.Build" Version="2.1.113" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.0">
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="6.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="6.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -17,4 +17,4 @@
},
"AllowedHosts": "*"
}
}
}