Use file-scoped namespaces
This commit is contained in:
parent
5157b01e81
commit
76ddee7756
@ -1,33 +1,26 @@
|
|||||||
using Microsoft.AspNetCore.Mvc.Authorization;
|
namespace Microsoft.eShopOnContainers.Services.Basket.API.Auth.Server;
|
||||||
using Microsoft.OpenApi.Models;
|
|
||||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace Microsoft.eShopOnContainers.Services.Basket.API.Auth.Server
|
public class AuthorizationHeaderParameterOperationFilter : IOperationFilter
|
||||||
{
|
{
|
||||||
public class AuthorizationHeaderParameterOperationFilter : IOperationFilter
|
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||||
{
|
{
|
||||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
var filterPipeline = context.ApiDescription.ActionDescriptor.FilterDescriptors;
|
||||||
|
var isAuthorized = filterPipeline.Select(filterInfo => filterInfo.Filter).Any(filter => filter is AuthorizeFilter);
|
||||||
|
var allowAnonymous = filterPipeline.Select(filterInfo => filterInfo.Filter).Any(filter => filter is IAllowAnonymousFilter);
|
||||||
|
|
||||||
|
if (isAuthorized && !allowAnonymous)
|
||||||
{
|
{
|
||||||
var filterPipeline = context.ApiDescription.ActionDescriptor.FilterDescriptors;
|
if (operation.Parameters == null)
|
||||||
var isAuthorized = filterPipeline.Select(filterInfo => filterInfo.Filter).Any(filter => filter is AuthorizeFilter);
|
operation.Parameters = new List<OpenApiParameter>();
|
||||||
var allowAnonymous = filterPipeline.Select(filterInfo => filterInfo.Filter).Any(filter => filter is IAllowAnonymousFilter);
|
|
||||||
|
|
||||||
if (isAuthorized && !allowAnonymous)
|
|
||||||
|
operation.Parameters.Add(new OpenApiParameter
|
||||||
{
|
{
|
||||||
if (operation.Parameters == null)
|
Name = "Authorization",
|
||||||
operation.Parameters = new List<OpenApiParameter>();
|
In = ParameterLocation.Header,
|
||||||
|
Description = "access token",
|
||||||
|
Required = true
|
||||||
operation.Parameters.Add(new OpenApiParameter
|
});
|
||||||
{
|
|
||||||
Name = "Authorization",
|
|
||||||
In = ParameterLocation.Header,
|
|
||||||
Description = "access token",
|
|
||||||
Required = true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
namespace Microsoft.eShopOnContainers.Services.Basket.API
|
namespace Microsoft.eShopOnContainers.Services.Basket.API;
|
||||||
|
|
||||||
|
public class BasketSettings
|
||||||
{
|
{
|
||||||
public class BasketSettings
|
public string ConnectionString { get; set; }
|
||||||
{
|
|
||||||
public string ConnectionString { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,103 +1,89 @@
|
|||||||
using Basket.API.IntegrationEvents.Events;
|
namespace Microsoft.eShopOnContainers.Services.Basket.API.Controllers;
|
||||||
using Basket.API.Model;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
|
|
||||||
using Microsoft.eShopOnContainers.Services.Basket.API.Model;
|
|
||||||
using Microsoft.eShopOnContainers.Services.Basket.API.Services;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System;
|
|
||||||
using System.Net;
|
|
||||||
using System.Security.Claims;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Microsoft.eShopOnContainers.Services.Basket.API.Controllers
|
[Route("api/v1/[controller]")]
|
||||||
|
[Authorize]
|
||||||
|
[ApiController]
|
||||||
|
public class BasketController : ControllerBase
|
||||||
{
|
{
|
||||||
[Route("api/v1/[controller]")]
|
private readonly IBasketRepository _repository;
|
||||||
[Authorize]
|
private readonly IIdentityService _identityService;
|
||||||
[ApiController]
|
private readonly IEventBus _eventBus;
|
||||||
public class BasketController : ControllerBase
|
private readonly ILogger<BasketController> _logger;
|
||||||
|
|
||||||
|
public BasketController(
|
||||||
|
ILogger<BasketController> logger,
|
||||||
|
IBasketRepository repository,
|
||||||
|
IIdentityService identityService,
|
||||||
|
IEventBus eventBus)
|
||||||
{
|
{
|
||||||
private readonly IBasketRepository _repository;
|
_logger = logger;
|
||||||
private readonly IIdentityService _identityService;
|
_repository = repository;
|
||||||
private readonly IEventBus _eventBus;
|
_identityService = identityService;
|
||||||
private readonly ILogger<BasketController> _logger;
|
_eventBus = eventBus;
|
||||||
|
}
|
||||||
|
|
||||||
public BasketController(
|
[HttpGet("{id}")]
|
||||||
ILogger<BasketController> logger,
|
[ProducesResponseType(typeof(CustomerBasket), (int)HttpStatusCode.OK)]
|
||||||
IBasketRepository repository,
|
public async Task<ActionResult<CustomerBasket>> GetBasketByIdAsync(string id)
|
||||||
IIdentityService identityService,
|
{
|
||||||
IEventBus eventBus)
|
var basket = await _repository.GetBasketAsync(id);
|
||||||
|
|
||||||
|
return Ok(basket ?? new CustomerBasket(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[ProducesResponseType(typeof(CustomerBasket), (int)HttpStatusCode.OK)]
|
||||||
|
public async Task<ActionResult<CustomerBasket>> UpdateBasketAsync([FromBody] CustomerBasket value)
|
||||||
|
{
|
||||||
|
return Ok(await _repository.UpdateBasketAsync(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("checkout")]
|
||||||
|
[HttpPost]
|
||||||
|
[ProducesResponseType((int)HttpStatusCode.Accepted)]
|
||||||
|
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
|
||||||
|
public async Task<ActionResult> CheckoutAsync([FromBody] BasketCheckout basketCheckout, [FromHeader(Name = "x-requestid")] string requestId)
|
||||||
|
{
|
||||||
|
var userId = _identityService.GetUserIdentity();
|
||||||
|
|
||||||
|
basketCheckout.RequestId = (Guid.TryParse(requestId, out Guid guid) && guid != Guid.Empty) ?
|
||||||
|
guid : basketCheckout.RequestId;
|
||||||
|
|
||||||
|
var basket = await _repository.GetBasketAsync(userId);
|
||||||
|
|
||||||
|
if (basket == null)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
return BadRequest();
|
||||||
_repository = repository;
|
|
||||||
_identityService = identityService;
|
|
||||||
_eventBus = eventBus;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
var userName = this.HttpContext.User.FindFirst(x => x.Type == ClaimTypes.Name).Value;
|
||||||
[ProducesResponseType(typeof(CustomerBasket), (int)HttpStatusCode.OK)]
|
|
||||||
public async Task<ActionResult<CustomerBasket>> GetBasketByIdAsync(string id)
|
|
||||||
{
|
|
||||||
var basket = await _repository.GetBasketAsync(id);
|
|
||||||
|
|
||||||
return Ok(basket ?? new CustomerBasket(id));
|
var eventMessage = new UserCheckoutAcceptedIntegrationEvent(userId, userName, basketCheckout.City, basketCheckout.Street,
|
||||||
|
basketCheckout.State, basketCheckout.Country, basketCheckout.ZipCode, basketCheckout.CardNumber, basketCheckout.CardHolderName,
|
||||||
|
basketCheckout.CardExpiration, basketCheckout.CardSecurityNumber, basketCheckout.CardTypeId, basketCheckout.Buyer, basketCheckout.RequestId, basket);
|
||||||
|
|
||||||
|
// Once basket is checkout, sends an integration event to
|
||||||
|
// ordering.api to convert basket to order and proceeds with
|
||||||
|
// order creation process
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_eventBus.Publish(eventMessage);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "ERROR Publishing integration event: {IntegrationEventId} from {AppName}", eventMessage.Id, Program.AppName);
|
||||||
|
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
return Accepted();
|
||||||
[ProducesResponseType(typeof(CustomerBasket), (int)HttpStatusCode.OK)]
|
}
|
||||||
public async Task<ActionResult<CustomerBasket>> UpdateBasketAsync([FromBody] CustomerBasket value)
|
|
||||||
{
|
|
||||||
return Ok(await _repository.UpdateBasketAsync(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Route("checkout")]
|
// DELETE api/values/5
|
||||||
[HttpPost]
|
[HttpDelete("{id}")]
|
||||||
[ProducesResponseType((int)HttpStatusCode.Accepted)]
|
[ProducesResponseType(typeof(void), (int)HttpStatusCode.OK)]
|
||||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
|
public async Task DeleteBasketByIdAsync(string id)
|
||||||
public async Task<ActionResult> CheckoutAsync([FromBody] BasketCheckout basketCheckout, [FromHeader(Name = "x-requestid")] string requestId)
|
{
|
||||||
{
|
await _repository.DeleteBasketAsync(id);
|
||||||
var userId = _identityService.GetUserIdentity();
|
|
||||||
|
|
||||||
basketCheckout.RequestId = (Guid.TryParse(requestId, out Guid guid) && guid != Guid.Empty) ?
|
|
||||||
guid : basketCheckout.RequestId;
|
|
||||||
|
|
||||||
var basket = await _repository.GetBasketAsync(userId);
|
|
||||||
|
|
||||||
if (basket == null)
|
|
||||||
{
|
|
||||||
return BadRequest();
|
|
||||||
}
|
|
||||||
|
|
||||||
var userName = this.HttpContext.User.FindFirst(x => x.Type == ClaimTypes.Name).Value;
|
|
||||||
|
|
||||||
var eventMessage = new UserCheckoutAcceptedIntegrationEvent(userId, userName, basketCheckout.City, basketCheckout.Street,
|
|
||||||
basketCheckout.State, basketCheckout.Country, basketCheckout.ZipCode, basketCheckout.CardNumber, basketCheckout.CardHolderName,
|
|
||||||
basketCheckout.CardExpiration, basketCheckout.CardSecurityNumber, basketCheckout.CardTypeId, basketCheckout.Buyer, basketCheckout.RequestId, basket);
|
|
||||||
|
|
||||||
// Once basket is checkout, sends an integration event to
|
|
||||||
// ordering.api to convert basket to order and proceeds with
|
|
||||||
// order creation process
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_eventBus.Publish(eventMessage);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "ERROR Publishing integration event: {IntegrationEventId} from {AppName}", eventMessage.Id, Program.AppName);
|
|
||||||
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Accepted();
|
|
||||||
}
|
|
||||||
|
|
||||||
// DELETE api/values/5
|
|
||||||
[HttpDelete("{id}")]
|
|
||||||
[ProducesResponseType(typeof(void), (int)HttpStatusCode.OK)]
|
|
||||||
public async Task DeleteBasketByIdAsync(string id)
|
|
||||||
{
|
|
||||||
await _repository.DeleteBasketAsync(id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
namespace Microsoft.eShopOnContainers.Services.Basket.API.Controllers;
|
||||||
|
|
||||||
namespace Microsoft.eShopOnContainers.Services.Basket.API.Controllers
|
public class HomeController : Controller
|
||||||
{
|
{
|
||||||
public class HomeController : Controller
|
// GET: /<controller>/
|
||||||
|
public IActionResult Index()
|
||||||
{
|
{
|
||||||
// GET: /<controller>/
|
return new RedirectResult("~/swagger");
|
||||||
public IActionResult Index()
|
|
||||||
{
|
|
||||||
return new RedirectResult("~/swagger");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,102 +1,93 @@
|
|||||||
using Grpc.Core;
|
namespace GrpcBasket;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
public class BasketService : Basket.BasketBase
|
||||||
using Microsoft.eShopOnContainers.Services.Basket.API.Model;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace GrpcBasket
|
|
||||||
{
|
{
|
||||||
public class BasketService : Basket.BasketBase
|
private readonly IBasketRepository _repository;
|
||||||
|
private readonly ILogger<BasketService> _logger;
|
||||||
|
|
||||||
|
public BasketService(IBasketRepository repository, ILogger<BasketService> logger)
|
||||||
{
|
{
|
||||||
private readonly IBasketRepository _repository;
|
_repository = repository;
|
||||||
private readonly ILogger<BasketService> _logger;
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
public BasketService(IBasketRepository repository, ILogger<BasketService> logger)
|
[AllowAnonymous]
|
||||||
|
public override async Task<CustomerBasketResponse> GetBasketById(BasketRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Begin grpc call from method {Method} for basket id {Id}", context.Method, request.Id);
|
||||||
|
|
||||||
|
var data = await _repository.GetBasketAsync(request.Id);
|
||||||
|
|
||||||
|
if (data != null)
|
||||||
{
|
{
|
||||||
_repository = repository;
|
context.Status = new Status(StatusCode.OK, $"Basket with id {request.Id} do exist");
|
||||||
_logger = logger;
|
|
||||||
|
return MapToCustomerBasketResponse(data);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
context.Status = new Status(StatusCode.NotFound, $"Basket with id {request.Id} do not exist");
|
||||||
}
|
}
|
||||||
|
|
||||||
[AllowAnonymous]
|
return new CustomerBasketResponse();
|
||||||
public override async Task<CustomerBasketResponse> GetBasketById(BasketRequest request, ServerCallContext context)
|
}
|
||||||
|
|
||||||
|
public override async Task<CustomerBasketResponse> UpdateBasket(CustomerBasketRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Begin grpc call BasketService.UpdateBasketAsync for buyer id {Buyerid}", request.Buyerid);
|
||||||
|
|
||||||
|
var customerBasket = MapToCustomerBasket(request);
|
||||||
|
|
||||||
|
var response = await _repository.UpdateBasketAsync(customerBasket);
|
||||||
|
|
||||||
|
if (response != null)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Begin grpc call from method {Method} for basket id {Id}", context.Method, request.Id);
|
return MapToCustomerBasketResponse(response);
|
||||||
|
|
||||||
var data = await _repository.GetBasketAsync(request.Id);
|
|
||||||
|
|
||||||
if (data != null)
|
|
||||||
{
|
|
||||||
context.Status = new Status(StatusCode.OK, $"Basket with id {request.Id} do exist");
|
|
||||||
|
|
||||||
return MapToCustomerBasketResponse(data);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
context.Status = new Status(StatusCode.NotFound, $"Basket with id {request.Id} do not exist");
|
|
||||||
}
|
|
||||||
|
|
||||||
return new CustomerBasketResponse();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<CustomerBasketResponse> UpdateBasket(CustomerBasketRequest request, ServerCallContext context)
|
context.Status = new Status(StatusCode.NotFound, $"Basket with buyer id {request.Buyerid} do not exist");
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private CustomerBasketResponse MapToCustomerBasketResponse(CustomerBasket customerBasket)
|
||||||
|
{
|
||||||
|
var response = new CustomerBasketResponse
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Begin grpc call BasketService.UpdateBasketAsync for buyer id {Buyerid}", request.Buyerid);
|
Buyerid = customerBasket.BuyerId
|
||||||
|
};
|
||||||
|
|
||||||
var customerBasket = MapToCustomerBasket(request);
|
customerBasket.Items.ForEach(item => response.Items.Add(new BasketItemResponse
|
||||||
|
|
||||||
var response = await _repository.UpdateBasketAsync(customerBasket);
|
|
||||||
|
|
||||||
if (response != null)
|
|
||||||
{
|
|
||||||
return MapToCustomerBasketResponse(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.Status = new Status(StatusCode.NotFound, $"Basket with buyer id {request.Buyerid} do not exist");
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private CustomerBasketResponse MapToCustomerBasketResponse(CustomerBasket customerBasket)
|
|
||||||
{
|
{
|
||||||
var response = new CustomerBasketResponse
|
Id = item.Id,
|
||||||
{
|
Oldunitprice = (double)item.OldUnitPrice,
|
||||||
Buyerid = customerBasket.BuyerId
|
Pictureurl = item.PictureUrl,
|
||||||
};
|
Productid = item.ProductId,
|
||||||
|
Productname = item.ProductName,
|
||||||
|
Quantity = item.Quantity,
|
||||||
|
Unitprice = (double)item.UnitPrice
|
||||||
|
}));
|
||||||
|
|
||||||
customerBasket.Items.ForEach(item => response.Items.Add(new BasketItemResponse
|
return response;
|
||||||
{
|
}
|
||||||
Id = item.Id,
|
|
||||||
Oldunitprice = (double)item.OldUnitPrice,
|
|
||||||
Pictureurl = item.PictureUrl,
|
|
||||||
Productid = item.ProductId,
|
|
||||||
Productname = item.ProductName,
|
|
||||||
Quantity = item.Quantity,
|
|
||||||
Unitprice = (double)item.UnitPrice
|
|
||||||
}));
|
|
||||||
|
|
||||||
return response;
|
private CustomerBasket MapToCustomerBasket(CustomerBasketRequest customerBasketRequest)
|
||||||
}
|
{
|
||||||
|
var response = new CustomerBasket
|
||||||
private CustomerBasket MapToCustomerBasket(CustomerBasketRequest customerBasketRequest)
|
|
||||||
{
|
{
|
||||||
var response = new CustomerBasket
|
BuyerId = customerBasketRequest.Buyerid
|
||||||
{
|
};
|
||||||
BuyerId = customerBasketRequest.Buyerid
|
|
||||||
};
|
|
||||||
|
|
||||||
customerBasketRequest.Items.ToList().ForEach(item => response.Items.Add(new BasketItem
|
customerBasketRequest.Items.ToList().ForEach(item => response.Items.Add(new BasketItem
|
||||||
{
|
{
|
||||||
Id = item.Id,
|
Id = item.Id,
|
||||||
OldUnitPrice = (decimal)item.Oldunitprice,
|
OldUnitPrice = (decimal)item.Oldunitprice,
|
||||||
PictureUrl = item.Pictureurl,
|
PictureUrl = item.Pictureurl,
|
||||||
ProductId = item.Productid,
|
ProductId = item.Productid,
|
||||||
ProductName = item.Productname,
|
ProductName = item.Productname,
|
||||||
Quantity = item.Quantity,
|
Quantity = item.Quantity,
|
||||||
UnitPrice = (decimal)item.Unitprice
|
UnitPrice = (decimal)item.Unitprice
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,11 @@
|
|||||||
using Microsoft.AspNetCore.Http;
|
namespace Basket.API.Infrastructure.ActionResults;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace Basket.API.Infrastructure.ActionResults
|
public class InternalServerErrorObjectResult : ObjectResult
|
||||||
{
|
{
|
||||||
public class InternalServerErrorObjectResult : ObjectResult
|
public InternalServerErrorObjectResult(object error)
|
||||||
|
: base(error)
|
||||||
{
|
{
|
||||||
public InternalServerErrorObjectResult(object error)
|
StatusCode = StatusCodes.Status500InternalServerError;
|
||||||
: base(error)
|
|
||||||
{
|
|
||||||
StatusCode = StatusCodes.Status500InternalServerError;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,21 +1,16 @@
|
|||||||
using System;
|
namespace Basket.API.Infrastructure.Exceptions;
|
||||||
|
|
||||||
namespace Basket.API.Infrastructure.Exceptions
|
public class BasketDomainException : Exception
|
||||||
{
|
{
|
||||||
/// <summary>
|
public BasketDomainException()
|
||||||
/// Exception type for app exceptions
|
{ }
|
||||||
/// </summary>
|
|
||||||
public class BasketDomainException : Exception
|
|
||||||
{
|
|
||||||
public BasketDomainException()
|
|
||||||
{ }
|
|
||||||
|
|
||||||
public BasketDomainException(string message)
|
public BasketDomainException(string message)
|
||||||
: base(message)
|
: base(message)
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
public BasketDomainException(string message, Exception innerException)
|
public BasketDomainException(string message, Exception innerException)
|
||||||
: base(message, innerException)
|
: base(message, innerException)
|
||||||
{ }
|
{ }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,20 +1,17 @@
|
|||||||
using Microsoft.AspNetCore.Builder;
|
namespace Basket.API.Infrastructure.Middlewares;
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Basket.API.Infrastructure.Middlewares
|
public static class FailingMiddlewareAppBuilderExtensions
|
||||||
{
|
{
|
||||||
public static class FailingMiddlewareAppBuilderExtensions
|
public static IApplicationBuilder UseFailingMiddleware(this IApplicationBuilder builder)
|
||||||
{
|
{
|
||||||
public static IApplicationBuilder UseFailingMiddleware(this IApplicationBuilder builder)
|
return UseFailingMiddleware(builder, null);
|
||||||
{
|
}
|
||||||
return UseFailingMiddleware(builder, null);
|
public static IApplicationBuilder UseFailingMiddleware(this IApplicationBuilder builder, Action<FailingOptions> action)
|
||||||
}
|
{
|
||||||
public static IApplicationBuilder UseFailingMiddleware(this IApplicationBuilder builder, Action<FailingOptions> action)
|
var options = new FailingOptions();
|
||||||
{
|
action?.Invoke(options);
|
||||||
var options = new FailingOptions();
|
builder.UseMiddleware<FailingMiddleware>(options);
|
||||||
action?.Invoke(options);
|
return builder;
|
||||||
builder.UseMiddleware<FailingMiddleware>(options);
|
|
||||||
return builder;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,58 +1,47 @@
|
|||||||
using Basket.API.Infrastructure.ActionResults;
|
namespace Basket.API.Infrastructure.Filters;
|
||||||
using Basket.API.Infrastructure.Exceptions;
|
|
||||||
using Microsoft.AspNetCore.Hosting;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.AspNetCore.Mvc.Filters;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System.Net;
|
|
||||||
|
|
||||||
|
public partial class HttpGlobalExceptionFilter : IExceptionFilter
|
||||||
namespace Basket.API.Infrastructure.Filters
|
|
||||||
{
|
{
|
||||||
public partial class HttpGlobalExceptionFilter : IExceptionFilter
|
private readonly IWebHostEnvironment env;
|
||||||
|
private readonly ILogger<HttpGlobalExceptionFilter> logger;
|
||||||
|
|
||||||
|
public HttpGlobalExceptionFilter(IWebHostEnvironment env, ILogger<HttpGlobalExceptionFilter> logger)
|
||||||
{
|
{
|
||||||
private readonly IWebHostEnvironment env;
|
this.env = env;
|
||||||
private readonly ILogger<HttpGlobalExceptionFilter> logger;
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
public HttpGlobalExceptionFilter(IWebHostEnvironment env, ILogger<HttpGlobalExceptionFilter> logger)
|
public void OnException(ExceptionContext context)
|
||||||
|
{
|
||||||
|
logger.LogError(new EventId(context.Exception.HResult),
|
||||||
|
context.Exception,
|
||||||
|
context.Exception.Message);
|
||||||
|
|
||||||
|
if (context.Exception.GetType() == typeof(BasketDomainException))
|
||||||
{
|
{
|
||||||
this.env = env;
|
var json = new JsonErrorResponse
|
||||||
this.logger = logger;
|
{
|
||||||
}
|
Messages = new[] { context.Exception.Message }
|
||||||
|
};
|
||||||
|
|
||||||
public void OnException(ExceptionContext context)
|
context.Result = new BadRequestObjectResult(json);
|
||||||
|
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
logger.LogError(new EventId(context.Exception.HResult),
|
var json = new JsonErrorResponse
|
||||||
context.Exception,
|
|
||||||
context.Exception.Message);
|
|
||||||
|
|
||||||
if (context.Exception.GetType() == typeof(BasketDomainException))
|
|
||||||
{
|
{
|
||||||
var json = new JsonErrorResponse
|
Messages = new[] { "An error occurred. Try it again." }
|
||||||
{
|
};
|
||||||
Messages = new[] { context.Exception.Message }
|
|
||||||
};
|
|
||||||
|
|
||||||
context.Result = new BadRequestObjectResult(json);
|
if (env.IsDevelopment())
|
||||||
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
var json = new JsonErrorResponse
|
json.DeveloperMessage = context.Exception;
|
||||||
{
|
|
||||||
Messages = new[] { "An error occurred. Try it again." }
|
|
||||||
};
|
|
||||||
|
|
||||||
if (env.IsDevelopment())
|
|
||||||
{
|
|
||||||
json.DeveloperMessage = context.Exception;
|
|
||||||
}
|
|
||||||
|
|
||||||
context.Result = new InternalServerErrorObjectResult(json);
|
|
||||||
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
|
||||||
}
|
}
|
||||||
context.ExceptionHandled = true;
|
|
||||||
|
context.Result = new InternalServerErrorObjectResult(json);
|
||||||
|
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
||||||
}
|
}
|
||||||
|
context.ExceptionHandled = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
namespace Basket.API.Infrastructure.Filters
|
namespace Basket.API.Infrastructure.Filters;
|
||||||
{
|
|
||||||
public class JsonErrorResponse
|
|
||||||
{
|
|
||||||
public string[] Messages { get; set; }
|
|
||||||
|
|
||||||
public object DeveloperMessage { get; set; }
|
public class JsonErrorResponse
|
||||||
}
|
{
|
||||||
|
public string[] Messages { get; set; }
|
||||||
|
|
||||||
|
public object DeveloperMessage { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,30 +1,26 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
namespace Basket.API.Infrastructure.Filters;
|
||||||
using Microsoft.AspNetCore.Mvc.Filters;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace Basket.API.Infrastructure.Filters
|
public class ValidateModelStateFilter : ActionFilterAttribute
|
||||||
{
|
{
|
||||||
public class ValidateModelStateFilter : ActionFilterAttribute
|
public override void OnActionExecuting(ActionExecutingContext context)
|
||||||
{
|
{
|
||||||
public override void OnActionExecuting(ActionExecutingContext context)
|
if (context.ModelState.IsValid)
|
||||||
{
|
{
|
||||||
if (context.ModelState.IsValid)
|
return;
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var validationErrors = context.ModelState
|
|
||||||
.Keys
|
|
||||||
.SelectMany(k => context.ModelState[k].Errors)
|
|
||||||
.Select(e => e.ErrorMessage)
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
var json = new JsonErrorResponse
|
|
||||||
{
|
|
||||||
Messages = validationErrors
|
|
||||||
};
|
|
||||||
|
|
||||||
context.Result = new BadRequestObjectResult(json);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var validationErrors = context.ModelState
|
||||||
|
.Keys
|
||||||
|
.SelectMany(k => context.ModelState[k].Errors)
|
||||||
|
.Select(e => e.ErrorMessage)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
var json = new JsonErrorResponse
|
||||||
|
{
|
||||||
|
Messages = validationErrors
|
||||||
|
};
|
||||||
|
|
||||||
|
context.Result = new BadRequestObjectResult(json);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,36 +1,29 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
namespace Basket.API.Infrastructure.Filters;
|
||||||
using Microsoft.OpenApi.Models;
|
|
||||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace Basket.API.Infrastructure.Filters
|
public class AuthorizeCheckOperationFilter : IOperationFilter
|
||||||
{
|
{
|
||||||
public class AuthorizeCheckOperationFilter : IOperationFilter
|
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||||
{
|
{
|
||||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
// Check for authorize attribute
|
||||||
|
var hasAuthorize = context.MethodInfo.DeclaringType.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any() ||
|
||||||
|
context.MethodInfo.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any();
|
||||||
|
|
||||||
|
if (!hasAuthorize) return;
|
||||||
|
|
||||||
|
operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" });
|
||||||
|
operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" });
|
||||||
|
|
||||||
|
var oAuthScheme = new OpenApiSecurityScheme
|
||||||
{
|
{
|
||||||
// Check for authorize attribute
|
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "oauth2" }
|
||||||
var hasAuthorize = context.MethodInfo.DeclaringType.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any() ||
|
};
|
||||||
context.MethodInfo.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any();
|
|
||||||
|
|
||||||
if (!hasAuthorize) return;
|
operation.Security = new List<OpenApiSecurityRequirement>
|
||||||
|
|
||||||
operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" });
|
|
||||||
operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" });
|
|
||||||
|
|
||||||
var oAuthScheme = new OpenApiSecurityScheme
|
|
||||||
{
|
{
|
||||||
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "oauth2" }
|
new OpenApiSecurityRequirement
|
||||||
};
|
|
||||||
|
|
||||||
operation.Security = new List<OpenApiSecurityRequirement>
|
|
||||||
{
|
{
|
||||||
new OpenApiSecurityRequirement
|
[ oAuthScheme ] = new [] { "basketapi" }
|
||||||
{
|
}
|
||||||
[ oAuthScheme ] = new [] { "basketapi" }
|
};
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,95 +1,88 @@
|
|||||||
using Microsoft.AspNetCore.Http;
|
namespace Basket.API.Infrastructure.Middlewares;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Basket.API.Infrastructure.Middlewares
|
public class FailingMiddleware
|
||||||
{
|
{
|
||||||
public class FailingMiddleware
|
private readonly RequestDelegate _next;
|
||||||
|
private bool _mustFail;
|
||||||
|
private readonly FailingOptions _options;
|
||||||
|
private readonly Microsoft.Extensions.Logging.ILogger _logger;
|
||||||
|
|
||||||
|
public FailingMiddleware(RequestDelegate next, Microsoft.Extensions.Logging.ILogger<FailingMiddleware> logger, FailingOptions options)
|
||||||
{
|
{
|
||||||
private readonly RequestDelegate _next;
|
_next = next;
|
||||||
private bool _mustFail;
|
_options = options;
|
||||||
private readonly FailingOptions _options;
|
_mustFail = false;
|
||||||
private readonly ILogger _logger;
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
public FailingMiddleware(RequestDelegate next, ILogger<FailingMiddleware> logger, FailingOptions options)
|
public async Task Invoke(HttpContext context)
|
||||||
|
{
|
||||||
|
var path = context.Request.Path;
|
||||||
|
if (path.Equals(_options.ConfigPath, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
_next = next;
|
await ProcessConfigRequest(context);
|
||||||
_options = options;
|
|
||||||
_mustFail = false;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Invoke(HttpContext context)
|
|
||||||
{
|
|
||||||
var path = context.Request.Path;
|
|
||||||
if (path.Equals(_options.ConfigPath, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
await ProcessConfigRequest(context);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (MustFail(context))
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Response for path {Path} will fail.", path);
|
|
||||||
context.Response.StatusCode = (int)System.Net.HttpStatusCode.InternalServerError;
|
|
||||||
context.Response.ContentType = "text/plain";
|
|
||||||
await context.Response.WriteAsync("Failed due to FailingMiddleware enabled.");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await _next.Invoke(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ProcessConfigRequest(HttpContext context)
|
|
||||||
{
|
|
||||||
var enable = context.Request.Query.Keys.Any(k => k == "enable");
|
|
||||||
var disable = context.Request.Query.Keys.Any(k => k == "disable");
|
|
||||||
|
|
||||||
if (enable && disable)
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Must use enable or disable querystring values, but not both");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (disable)
|
|
||||||
{
|
|
||||||
_mustFail = false;
|
|
||||||
await SendOkResponse(context, "FailingMiddleware disabled. Further requests will be processed.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (enable)
|
|
||||||
{
|
|
||||||
_mustFail = true;
|
|
||||||
await SendOkResponse(context, "FailingMiddleware enabled. Further requests will return HTTP 500");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If reach here, that means that no valid parameter has been passed. Just output status
|
|
||||||
await SendOkResponse(context, string.Format("FailingMiddleware is {0}", _mustFail ? "enabled" : "disabled"));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SendOkResponse(HttpContext context, string message)
|
if (MustFail(context))
|
||||||
{
|
{
|
||||||
context.Response.StatusCode = (int)System.Net.HttpStatusCode.OK;
|
_logger.LogInformation("Response for path {Path} will fail.", path);
|
||||||
|
context.Response.StatusCode = (int)System.Net.HttpStatusCode.InternalServerError;
|
||||||
context.Response.ContentType = "text/plain";
|
context.Response.ContentType = "text/plain";
|
||||||
await context.Response.WriteAsync(message);
|
await context.Response.WriteAsync("Failed due to FailingMiddleware enabled.");
|
||||||
}
|
}
|
||||||
|
else
|
||||||
private bool MustFail(HttpContext context)
|
|
||||||
{
|
{
|
||||||
var rpath = context.Request.Path.Value;
|
await _next.Invoke(context);
|
||||||
|
|
||||||
if (_options.NotFilteredPaths.Any(p => p.Equals(rpath, StringComparison.InvariantCultureIgnoreCase)))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _mustFail &&
|
|
||||||
(_options.EndpointPaths.Any(x => x == rpath)
|
|
||||||
|| _options.EndpointPaths.Count == 0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ProcessConfigRequest(HttpContext context)
|
||||||
|
{
|
||||||
|
var enable = context.Request.Query.Keys.Any(k => k == "enable");
|
||||||
|
var disable = context.Request.Query.Keys.Any(k => k == "disable");
|
||||||
|
|
||||||
|
if (enable && disable)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Must use enable or disable querystring values, but not both");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (disable)
|
||||||
|
{
|
||||||
|
_mustFail = false;
|
||||||
|
await SendOkResponse(context, "FailingMiddleware disabled. Further requests will be processed.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (enable)
|
||||||
|
{
|
||||||
|
_mustFail = true;
|
||||||
|
await SendOkResponse(context, "FailingMiddleware enabled. Further requests will return HTTP 500");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If reach here, that means that no valid parameter has been passed. Just output status
|
||||||
|
await SendOkResponse(context, string.Format("FailingMiddleware is {0}", _mustFail ? "enabled" : "disabled"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendOkResponse(HttpContext context, string message)
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = (int)System.Net.HttpStatusCode.OK;
|
||||||
|
context.Response.ContentType = "text/plain";
|
||||||
|
await context.Response.WriteAsync(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool MustFail(HttpContext context)
|
||||||
|
{
|
||||||
|
var rpath = context.Request.Path.Value;
|
||||||
|
|
||||||
|
if (_options.NotFilteredPaths.Any(p => p.Equals(rpath, StringComparison.InvariantCultureIgnoreCase)))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _mustFail &&
|
||||||
|
(_options.EndpointPaths.Any(x => x == rpath)
|
||||||
|
|| _options.EndpointPaths.Count == 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
using System.Collections.Generic;
|
namespace Basket.API.Infrastructure.Middlewares;
|
||||||
|
|
||||||
namespace Basket.API.Infrastructure.Middlewares
|
public class FailingOptions
|
||||||
{
|
{
|
||||||
public class FailingOptions
|
public string ConfigPath = "/Failing";
|
||||||
{
|
public List<string> EndpointPaths { get; set; } = new List<string>();
|
||||||
public string ConfigPath = "/Failing";
|
|
||||||
public List<string> EndpointPaths { get; set; } = new List<string>();
|
|
||||||
|
|
||||||
public List<string> NotFilteredPaths { get; set; } = new List<string>();
|
public List<string> NotFilteredPaths { get; set; } = new List<string>();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,24 +1,20 @@
|
|||||||
using Microsoft.AspNetCore.Builder;
|
namespace Basket.API.Infrastructure.Middlewares;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Basket.API.Infrastructure.Middlewares
|
public class FailingStartupFilter : IStartupFilter
|
||||||
{
|
{
|
||||||
public class FailingStartupFilter : IStartupFilter
|
private readonly Action<FailingOptions> _options;
|
||||||
|
public FailingStartupFilter(Action<FailingOptions> optionsAction)
|
||||||
{
|
{
|
||||||
private readonly Action<FailingOptions> _options;
|
_options = optionsAction;
|
||||||
public FailingStartupFilter(Action<FailingOptions> optionsAction)
|
}
|
||||||
{
|
|
||||||
_options = optionsAction;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
|
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
|
||||||
|
{
|
||||||
|
return app =>
|
||||||
{
|
{
|
||||||
return app =>
|
app.UseFailingMiddleware(_options);
|
||||||
{
|
next(app);
|
||||||
app.UseFailingMiddleware(_options);
|
};
|
||||||
next(app);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,18 +1,14 @@
|
|||||||
using Microsoft.AspNetCore.Hosting;
|
namespace Basket.API.Infrastructure.Middlewares;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Basket.API.Infrastructure.Middlewares
|
public static class WebHostBuildertExtensions
|
||||||
{
|
{
|
||||||
public static class WebHostBuildertExtensions
|
public static IWebHostBuilder UseFailing(this IWebHostBuilder builder, Action<FailingOptions> options)
|
||||||
{
|
{
|
||||||
public static IWebHostBuilder UseFailing(this IWebHostBuilder builder, Action<FailingOptions> options)
|
builder.ConfigureServices(services =>
|
||||||
{
|
{
|
||||||
builder.ConfigureServices(services =>
|
services.AddSingleton<IStartupFilter>(new FailingStartupFilter(options));
|
||||||
{
|
});
|
||||||
services.AddSingleton<IStartupFilter>(new FailingStartupFilter(options));
|
return builder;
|
||||||
});
|
|
||||||
return builder;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,73 +1,63 @@
|
|||||||
using Microsoft.eShopOnContainers.Services.Basket.API.Model;
|
namespace Microsoft.eShopOnContainers.Services.Basket.API.Infrastructure.Repositories;
|
||||||
using Microsoft.Extensions.Logging;
|
public class RedisBasketRepository : IBasketRepository
|
||||||
using StackExchange.Redis;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace Microsoft.eShopOnContainers.Services.Basket.API.Infrastructure.Repositories
|
|
||||||
{
|
{
|
||||||
public class RedisBasketRepository : IBasketRepository
|
private readonly ILogger<RedisBasketRepository> _logger;
|
||||||
|
private readonly ConnectionMultiplexer _redis;
|
||||||
|
private readonly IDatabase _database;
|
||||||
|
|
||||||
|
public RedisBasketRepository(ILoggerFactory loggerFactory, ConnectionMultiplexer redis)
|
||||||
{
|
{
|
||||||
private readonly ILogger<RedisBasketRepository> _logger;
|
_logger = loggerFactory.CreateLogger<RedisBasketRepository>();
|
||||||
private readonly ConnectionMultiplexer _redis;
|
_redis = redis;
|
||||||
private readonly IDatabase _database;
|
_database = redis.GetDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
public RedisBasketRepository(ILoggerFactory loggerFactory, ConnectionMultiplexer redis)
|
public async Task<bool> DeleteBasketAsync(string id)
|
||||||
|
{
|
||||||
|
return await _database.KeyDeleteAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<string> GetUsers()
|
||||||
|
{
|
||||||
|
var server = GetServer();
|
||||||
|
var data = server.Keys();
|
||||||
|
|
||||||
|
return data?.Select(k => k.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CustomerBasket> GetBasketAsync(string customerId)
|
||||||
|
{
|
||||||
|
var data = await _database.StringGetAsync(customerId);
|
||||||
|
|
||||||
|
if (data.IsNullOrEmpty)
|
||||||
{
|
{
|
||||||
_logger = loggerFactory.CreateLogger<RedisBasketRepository>();
|
return null;
|
||||||
_redis = redis;
|
|
||||||
_database = redis.GetDatabase();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> DeleteBasketAsync(string id)
|
return JsonSerializer.Deserialize<CustomerBasket>(data, new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
return await _database.KeyDeleteAsync(id);
|
PropertyNameCaseInsensitive = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CustomerBasket> UpdateBasketAsync(CustomerBasket basket)
|
||||||
|
{
|
||||||
|
var created = await _database.StringSetAsync(basket.BuyerId, JsonSerializer.Serialize(basket));
|
||||||
|
|
||||||
|
if (!created)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Problem occur persisting the item.");
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<string> GetUsers()
|
_logger.LogInformation("Basket item persisted succesfully.");
|
||||||
{
|
|
||||||
var server = GetServer();
|
|
||||||
var data = server.Keys();
|
|
||||||
|
|
||||||
return data?.Select(k => k.ToString());
|
return await GetBasketAsync(basket.BuyerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<CustomerBasket> GetBasketAsync(string customerId)
|
private IServer GetServer()
|
||||||
{
|
{
|
||||||
var data = await _database.StringGetAsync(customerId);
|
var endpoint = _redis.GetEndPoints();
|
||||||
|
return _redis.GetServer(endpoint.First());
|
||||||
if (data.IsNullOrEmpty)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return JsonSerializer.Deserialize<CustomerBasket>(data, new JsonSerializerOptions
|
|
||||||
{
|
|
||||||
PropertyNameCaseInsensitive = true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<CustomerBasket> UpdateBasketAsync(CustomerBasket basket)
|
|
||||||
{
|
|
||||||
var created = await _database.StringSetAsync(basket.BuyerId, JsonSerializer.Serialize(basket));
|
|
||||||
|
|
||||||
if (!created)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Problem occur persisting the item.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("Basket item persisted succesfully.");
|
|
||||||
|
|
||||||
return await GetBasketAsync(basket.BuyerId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private IServer GetServer()
|
|
||||||
{
|
|
||||||
var endpoint = _redis.GetEndPoints();
|
|
||||||
return _redis.GetServer(endpoint.First());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,37 +1,29 @@
|
|||||||
using Basket.API.IntegrationEvents.Events;
|
namespace Basket.API.IntegrationEvents.EventHandling;
|
||||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
|
|
||||||
using Microsoft.eShopOnContainers.Services.Basket.API.Model;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Serilog.Context;
|
|
||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Basket.API.IntegrationEvents.EventHandling
|
public class OrderStartedIntegrationEventHandler : IIntegrationEventHandler<OrderStartedIntegrationEvent>
|
||||||
{
|
{
|
||||||
public class OrderStartedIntegrationEventHandler : IIntegrationEventHandler<OrderStartedIntegrationEvent>
|
private readonly IBasketRepository _repository;
|
||||||
|
private readonly ILogger<OrderStartedIntegrationEventHandler> _logger;
|
||||||
|
|
||||||
|
public OrderStartedIntegrationEventHandler(
|
||||||
|
IBasketRepository repository,
|
||||||
|
ILogger<OrderStartedIntegrationEventHandler> logger)
|
||||||
{
|
{
|
||||||
private readonly IBasketRepository _repository;
|
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
|
||||||
private readonly ILogger<OrderStartedIntegrationEventHandler> _logger;
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
}
|
||||||
|
|
||||||
public OrderStartedIntegrationEventHandler(
|
public async Task Handle(OrderStartedIntegrationEvent @event)
|
||||||
IBasketRepository repository,
|
{
|
||||||
ILogger<OrderStartedIntegrationEventHandler> logger)
|
using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}"))
|
||||||
{
|
{
|
||||||
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
|
_logger.LogInformation("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, Program.AppName, @event);
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Handle(OrderStartedIntegrationEvent @event)
|
await _repository.DeleteBasketAsync(@event.UserId.ToString());
|
||||||
{
|
|
||||||
using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}"))
|
|
||||||
{
|
|
||||||
_logger.LogInformation("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, Program.AppName, @event);
|
|
||||||
|
|
||||||
await _repository.DeleteBasketAsync(@event.UserId.ToString());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,64 +1,53 @@
|
|||||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
|
namespace Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.EventHandling;
|
||||||
using Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.Events;
|
|
||||||
using Microsoft.eShopOnContainers.Services.Basket.API.Model;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Serilog.Context;
|
|
||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.EventHandling
|
public class ProductPriceChangedIntegrationEventHandler : IIntegrationEventHandler<ProductPriceChangedIntegrationEvent>
|
||||||
{
|
{
|
||||||
public class ProductPriceChangedIntegrationEventHandler : IIntegrationEventHandler<ProductPriceChangedIntegrationEvent>
|
private readonly ILogger<ProductPriceChangedIntegrationEventHandler> _logger;
|
||||||
|
private readonly IBasketRepository _repository;
|
||||||
|
|
||||||
|
public ProductPriceChangedIntegrationEventHandler(
|
||||||
|
ILogger<ProductPriceChangedIntegrationEventHandler> logger,
|
||||||
|
IBasketRepository repository)
|
||||||
{
|
{
|
||||||
private readonly ILogger<ProductPriceChangedIntegrationEventHandler> _logger;
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
private readonly IBasketRepository _repository;
|
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
|
||||||
|
}
|
||||||
|
|
||||||
public ProductPriceChangedIntegrationEventHandler(
|
public async Task Handle(ProductPriceChangedIntegrationEvent @event)
|
||||||
ILogger<ProductPriceChangedIntegrationEventHandler> logger,
|
{
|
||||||
IBasketRepository repository)
|
using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}"))
|
||||||
{
|
{
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
_logger.LogInformation("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, Program.AppName, @event);
|
||||||
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Handle(ProductPriceChangedIntegrationEvent @event)
|
var userIds = _repository.GetUsers();
|
||||||
{
|
|
||||||
using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}"))
|
foreach (var id in userIds)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, Program.AppName, @event);
|
var basket = await _repository.GetBasketAsync(id);
|
||||||
|
|
||||||
var userIds = _repository.GetUsers();
|
await UpdatePriceInBasketItems(@event.ProductId, @event.NewPrice, @event.OldPrice, basket);
|
||||||
|
|
||||||
foreach (var id in userIds)
|
|
||||||
{
|
|
||||||
var basket = await _repository.GetBasketAsync(id);
|
|
||||||
|
|
||||||
await UpdatePriceInBasketItems(@event.ProductId, @event.NewPrice, @event.OldPrice, basket);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task UpdatePriceInBasketItems(int productId, decimal newPrice, decimal oldPrice, CustomerBasket basket)
|
|
||||||
{
|
|
||||||
var itemsToUpdate = basket?.Items?.Where(x => x.ProductId == productId).ToList();
|
|
||||||
|
|
||||||
if (itemsToUpdate != null)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("----- ProductPriceChangedIntegrationEventHandler - Updating items in basket for user: {BuyerId} ({@Items})", basket.BuyerId, itemsToUpdate);
|
|
||||||
|
|
||||||
foreach (var item in itemsToUpdate)
|
|
||||||
{
|
|
||||||
if (item.UnitPrice == oldPrice)
|
|
||||||
{
|
|
||||||
var originalPrice = item.UnitPrice;
|
|
||||||
item.UnitPrice = newPrice;
|
|
||||||
item.OldUnitPrice = originalPrice;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await _repository.UpdateBasketAsync(basket);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
private async Task UpdatePriceInBasketItems(int productId, decimal newPrice, decimal oldPrice, CustomerBasket basket)
|
||||||
|
{
|
||||||
|
var itemsToUpdate = basket?.Items?.Where(x => x.ProductId == productId).ToList();
|
||||||
|
|
||||||
|
if (itemsToUpdate != null)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("----- ProductPriceChangedIntegrationEventHandler - Updating items in basket for user: {BuyerId} ({@Items})", basket.BuyerId, itemsToUpdate);
|
||||||
|
|
||||||
|
foreach (var item in itemsToUpdate)
|
||||||
|
{
|
||||||
|
if (item.UnitPrice == oldPrice)
|
||||||
|
{
|
||||||
|
var originalPrice = item.UnitPrice;
|
||||||
|
item.UnitPrice = newPrice;
|
||||||
|
item.OldUnitPrice = originalPrice;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await _repository.UpdateBasketAsync(basket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,15 +1,13 @@
|
|||||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
|
namespace Basket.API.IntegrationEvents.Events;
|
||||||
|
|
||||||
namespace Basket.API.IntegrationEvents.Events
|
// Integration Events notes:
|
||||||
|
// An Event is “something that has happened in the past”, therefore its name has to be
|
||||||
|
// An Integration Event is an event that can cause side effects to other microsrvices, Bounded-Contexts or external systems.
|
||||||
|
public record OrderStartedIntegrationEvent : IntegrationEvent
|
||||||
{
|
{
|
||||||
// Integration Events notes:
|
public string UserId { get; init; }
|
||||||
// An Event is “something that has happened in the past”, therefore its name has to be
|
|
||||||
// An Integration Event is an event that can cause side effects to other microsrvices, Bounded-Contexts or external systems.
|
|
||||||
public record OrderStartedIntegrationEvent : IntegrationEvent
|
|
||||||
{
|
|
||||||
public string UserId { get; init; }
|
|
||||||
|
|
||||||
public OrderStartedIntegrationEvent(string userId)
|
public OrderStartedIntegrationEvent(string userId)
|
||||||
=> UserId = userId;
|
=> UserId = userId;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,23 +1,21 @@
|
|||||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
|
namespace Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.Events;
|
||||||
|
|
||||||
namespace Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.Events
|
// Integration Events notes:
|
||||||
|
// An Event is “something that has happened in the past”, therefore its name has to be
|
||||||
|
// An Integration Event is an event that can cause side effects to other microsrvices, Bounded-Contexts or external systems.
|
||||||
|
public record ProductPriceChangedIntegrationEvent : IntegrationEvent
|
||||||
{
|
{
|
||||||
// Integration Events notes:
|
public int ProductId { get; private init; }
|
||||||
// An Event is “something that has happened in the past”, therefore its name has to be
|
|
||||||
// An Integration Event is an event that can cause side effects to other microsrvices, Bounded-Contexts or external systems.
|
public decimal NewPrice { get; private init; }
|
||||||
public record ProductPriceChangedIntegrationEvent : IntegrationEvent
|
|
||||||
|
public decimal OldPrice { get; private init; }
|
||||||
|
|
||||||
|
public ProductPriceChangedIntegrationEvent(int productId, decimal newPrice, decimal oldPrice)
|
||||||
{
|
{
|
||||||
public int ProductId { get; private init; }
|
ProductId = productId;
|
||||||
|
NewPrice = newPrice;
|
||||||
public decimal NewPrice { get; private init; }
|
OldPrice = oldPrice;
|
||||||
|
|
||||||
public decimal OldPrice { get; private init; }
|
|
||||||
|
|
||||||
public ProductPriceChangedIntegrationEvent(int productId, decimal newPrice, decimal oldPrice)
|
|
||||||
{
|
|
||||||
ProductId = productId;
|
|
||||||
NewPrice = newPrice;
|
|
||||||
OldPrice = oldPrice;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,64 +1,59 @@
|
|||||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
|
namespace Basket.API.IntegrationEvents.Events;
|
||||||
using Microsoft.eShopOnContainers.Services.Basket.API.Model;
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Basket.API.IntegrationEvents.Events
|
public record UserCheckoutAcceptedIntegrationEvent : IntegrationEvent
|
||||||
{
|
{
|
||||||
public record UserCheckoutAcceptedIntegrationEvent : IntegrationEvent
|
public string UserId { get; }
|
||||||
|
|
||||||
|
public string UserName { get; }
|
||||||
|
|
||||||
|
public int OrderNumber { get; init; }
|
||||||
|
|
||||||
|
public string City { get; init; }
|
||||||
|
|
||||||
|
public string Street { get; init; }
|
||||||
|
|
||||||
|
public string State { get; init; }
|
||||||
|
|
||||||
|
public string Country { get; init; }
|
||||||
|
|
||||||
|
public string ZipCode { get; init; }
|
||||||
|
|
||||||
|
public string CardNumber { get; init; }
|
||||||
|
|
||||||
|
public string CardHolderName { get; init; }
|
||||||
|
|
||||||
|
public DateTime CardExpiration { get; init; }
|
||||||
|
|
||||||
|
public string CardSecurityNumber { get; init; }
|
||||||
|
|
||||||
|
public int CardTypeId { get; init; }
|
||||||
|
|
||||||
|
public string Buyer { get; init; }
|
||||||
|
|
||||||
|
public Guid RequestId { get; init; }
|
||||||
|
|
||||||
|
public CustomerBasket Basket { get; }
|
||||||
|
|
||||||
|
public UserCheckoutAcceptedIntegrationEvent(string userId, string userName, string city, string street,
|
||||||
|
string state, string country, string zipCode, string cardNumber, string cardHolderName,
|
||||||
|
DateTime cardExpiration, string cardSecurityNumber, int cardTypeId, string buyer, Guid requestId,
|
||||||
|
CustomerBasket basket)
|
||||||
{
|
{
|
||||||
public string UserId { get; }
|
UserId = userId;
|
||||||
|
UserName = userName;
|
||||||
public string UserName { get; }
|
City = city;
|
||||||
|
Street = street;
|
||||||
public int OrderNumber { get; init; }
|
State = state;
|
||||||
|
Country = country;
|
||||||
public string City { get; init; }
|
ZipCode = zipCode;
|
||||||
|
CardNumber = cardNumber;
|
||||||
public string Street { get; init; }
|
CardHolderName = cardHolderName;
|
||||||
|
CardExpiration = cardExpiration;
|
||||||
public string State { get; init; }
|
CardSecurityNumber = cardSecurityNumber;
|
||||||
|
CardTypeId = cardTypeId;
|
||||||
public string Country { get; init; }
|
Buyer = buyer;
|
||||||
|
Basket = basket;
|
||||||
public string ZipCode { get; init; }
|
RequestId = requestId;
|
||||||
|
|
||||||
public string CardNumber { get; init; }
|
|
||||||
|
|
||||||
public string CardHolderName { get; init; }
|
|
||||||
|
|
||||||
public DateTime CardExpiration { get; init; }
|
|
||||||
|
|
||||||
public string CardSecurityNumber { get; init; }
|
|
||||||
|
|
||||||
public int CardTypeId { get; init; }
|
|
||||||
|
|
||||||
public string Buyer { get; init; }
|
|
||||||
|
|
||||||
public Guid RequestId { get; init; }
|
|
||||||
|
|
||||||
public CustomerBasket Basket { get; }
|
|
||||||
|
|
||||||
public UserCheckoutAcceptedIntegrationEvent(string userId, string userName, string city, string street,
|
|
||||||
string state, string country, string zipCode, string cardNumber, string cardHolderName,
|
|
||||||
DateTime cardExpiration, string cardSecurityNumber, int cardTypeId, string buyer, Guid requestId,
|
|
||||||
CustomerBasket basket)
|
|
||||||
{
|
|
||||||
UserId = userId;
|
|
||||||
UserName = userName;
|
|
||||||
City = city;
|
|
||||||
Street = street;
|
|
||||||
State = state;
|
|
||||||
Country = country;
|
|
||||||
ZipCode = zipCode;
|
|
||||||
CardNumber = cardNumber;
|
|
||||||
CardHolderName = cardHolderName;
|
|
||||||
CardExpiration = cardExpiration;
|
|
||||||
CardSecurityNumber = cardSecurityNumber;
|
|
||||||
CardTypeId = cardTypeId;
|
|
||||||
Buyer = buyer;
|
|
||||||
Basket = basket;
|
|
||||||
RequestId = requestId;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,32 +1,27 @@
|
|||||||
using System;
|
namespace Basket.API.Model;
|
||||||
|
public class BasketCheckout
|
||||||
namespace Basket.API.Model
|
|
||||||
{
|
{
|
||||||
public class BasketCheckout
|
public string City { get; set; }
|
||||||
{
|
|
||||||
public string City { get; set; }
|
|
||||||
|
|
||||||
public string Street { get; set; }
|
public string Street { get; set; }
|
||||||
|
|
||||||
public string State { get; set; }
|
public string State { get; set; }
|
||||||
|
|
||||||
public string Country { get; set; }
|
public string Country { get; set; }
|
||||||
|
|
||||||
public string ZipCode { get; set; }
|
public string ZipCode { get; set; }
|
||||||
|
|
||||||
public string CardNumber { get; set; }
|
public string CardNumber { get; set; }
|
||||||
|
|
||||||
public string CardHolderName { get; set; }
|
public string CardHolderName { get; set; }
|
||||||
|
|
||||||
public DateTime CardExpiration { get; set; }
|
public DateTime CardExpiration { get; set; }
|
||||||
|
|
||||||
public string CardSecurityNumber { get; set; }
|
public string CardSecurityNumber { get; set; }
|
||||||
|
|
||||||
public int CardTypeId { get; set; }
|
public int CardTypeId { get; set; }
|
||||||
|
|
||||||
public string Buyer { get; set; }
|
public string Buyer { get; set; }
|
||||||
|
|
||||||
public Guid RequestId { get; set; }
|
public Guid RequestId { get; set; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,27 +1,23 @@
|
|||||||
using System.Collections.Generic;
|
namespace Microsoft.eShopOnContainers.Services.Basket.API.Model;
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace Microsoft.eShopOnContainers.Services.Basket.API.Model
|
public class BasketItem : IValidatableObject
|
||||||
{
|
{
|
||||||
public class BasketItem : IValidatableObject
|
public string Id { get; set; }
|
||||||
|
public int ProductId { get; set; }
|
||||||
|
public string ProductName { get; set; }
|
||||||
|
public decimal UnitPrice { get; set; }
|
||||||
|
public decimal OldUnitPrice { get; set; }
|
||||||
|
public int Quantity { get; set; }
|
||||||
|
public string PictureUrl { get; set; }
|
||||||
|
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||||
{
|
{
|
||||||
public string Id { get; set; }
|
var results = new List<ValidationResult>();
|
||||||
public int ProductId { get; set; }
|
|
||||||
public string ProductName { get; set; }
|
if (Quantity < 1)
|
||||||
public decimal UnitPrice { get; set; }
|
|
||||||
public decimal OldUnitPrice { get; set; }
|
|
||||||
public int Quantity { get; set; }
|
|
||||||
public string PictureUrl { get; set; }
|
|
||||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
|
||||||
{
|
{
|
||||||
var results = new List<ValidationResult>();
|
results.Add(new ValidationResult("Invalid number of units", new[] { "Quantity" }));
|
||||||
|
|
||||||
if (Quantity < 1)
|
|
||||||
{
|
|
||||||
results.Add(new ValidationResult("Invalid number of units", new[] { "Quantity" }));
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,18 @@
|
|||||||
using System.Collections.Generic;
|
namespace Microsoft.eShopOnContainers.Services.Basket.API.Model;
|
||||||
|
public class CustomerBasket
|
||||||
namespace Microsoft.eShopOnContainers.Services.Basket.API.Model
|
|
||||||
{
|
{
|
||||||
public class CustomerBasket
|
public string BuyerId { get; set; }
|
||||||
|
|
||||||
|
public List<BasketItem> Items { get; set; } = new List<BasketItem>();
|
||||||
|
|
||||||
|
public CustomerBasket()
|
||||||
{
|
{
|
||||||
public string BuyerId { get; set; }
|
|
||||||
|
|
||||||
public List<BasketItem> Items { get; set; } = new List<BasketItem>();
|
}
|
||||||
|
|
||||||
public CustomerBasket()
|
public CustomerBasket(string customerId)
|
||||||
{
|
{
|
||||||
|
BuyerId = customerId;
|
||||||
}
|
|
||||||
|
|
||||||
public CustomerBasket(string customerId)
|
|
||||||
{
|
|
||||||
BuyerId = customerId;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
using System.Collections.Generic;
|
namespace Microsoft.eShopOnContainers.Services.Basket.API.Model;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Microsoft.eShopOnContainers.Services.Basket.API.Model
|
public interface IBasketRepository
|
||||||
{
|
{
|
||||||
public interface IBasketRepository
|
Task<CustomerBasket> GetBasketAsync(string customerId);
|
||||||
{
|
IEnumerable<string> GetUsers();
|
||||||
Task<CustomerBasket> GetBasketAsync(string customerId);
|
Task<CustomerBasket> UpdateBasketAsync(CustomerBasket basket);
|
||||||
IEnumerable<string> GetUsers();
|
Task<bool> DeleteBasketAsync(string id);
|
||||||
Task<CustomerBasket> UpdateBasketAsync(CustomerBasket basket);
|
|
||||||
Task<bool> DeleteBasketAsync(string id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,18 +1,4 @@
|
|||||||
using Basket.API.Infrastructure.Middlewares;
|
var configuration = GetConfiguration();
|
||||||
using Microsoft.AspNetCore;
|
|
||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
using Microsoft.AspNetCore.Hosting;
|
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
|
||||||
using Microsoft.eShopOnContainers.Services.Basket.API;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Azure.Identity;
|
|
||||||
using Serilog;
|
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Net;
|
|
||||||
using Azure.Core;
|
|
||||||
|
|
||||||
var configuration = GetConfiguration();
|
|
||||||
|
|
||||||
Log.Logger = CreateSerilogLogger(configuration);
|
Log.Logger = CreateSerilogLogger(configuration);
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
namespace Microsoft.eShopOnContainers.Services.Basket.API.Services
|
namespace Microsoft.eShopOnContainers.Services.Basket.API.Services;
|
||||||
|
|
||||||
|
public interface IIdentityService
|
||||||
{
|
{
|
||||||
public interface IIdentityService
|
string GetUserIdentity();
|
||||||
{
|
|
||||||
string GetUserIdentity();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,21 +1,17 @@
|
|||||||
|
namespace Microsoft.eShopOnContainers.Services.Basket.API.Services;
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Microsoft.eShopOnContainers.Services.Basket.API.Services
|
public class IdentityService : IIdentityService
|
||||||
{
|
{
|
||||||
public class IdentityService : IIdentityService
|
private IHttpContextAccessor _context;
|
||||||
|
|
||||||
|
public IdentityService(IHttpContextAccessor context)
|
||||||
{
|
{
|
||||||
private IHttpContextAccessor _context;
|
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||||
|
}
|
||||||
|
|
||||||
public IdentityService(IHttpContextAccessor context)
|
public string GetUserIdentity()
|
||||||
{
|
{
|
||||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
return _context.HttpContext.User.FindFirst("sub").Value;
|
||||||
}
|
|
||||||
|
|
||||||
public string GetUserIdentity()
|
|
||||||
{
|
|
||||||
return _context.HttpContext.User.FindFirst("sub").Value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,363 +1,325 @@
|
|||||||
using Autofac;
|
namespace Microsoft.eShopOnContainers.Services.Basket.API;
|
||||||
using Autofac.Extensions.DependencyInjection;
|
|
||||||
using Basket.API.Infrastructure.Filters;
|
|
||||||
using Basket.API.IntegrationEvents.EventHandling;
|
|
||||||
using Basket.API.IntegrationEvents.Events;
|
|
||||||
using GrpcBasket;
|
|
||||||
using HealthChecks.UI.Client;
|
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
|
||||||
using Microsoft.AspNetCore.Hosting;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.Azure.ServiceBus;
|
|
||||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus;
|
|
||||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
|
|
||||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ;
|
|
||||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus;
|
|
||||||
using Microsoft.eShopOnContainers.Services.Basket.API.Controllers;
|
|
||||||
using Microsoft.eShopOnContainers.Services.Basket.API.Infrastructure.Repositories;
|
|
||||||
using Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.EventHandling;
|
|
||||||
using Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.Events;
|
|
||||||
using Microsoft.eShopOnContainers.Services.Basket.API.Model;
|
|
||||||
using Microsoft.eShopOnContainers.Services.Basket.API.Services;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using Microsoft.OpenApi.Models;
|
|
||||||
using RabbitMQ.Client;
|
|
||||||
using StackExchange.Redis;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IdentityModel.Tokens.Jwt;
|
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
namespace Microsoft.eShopOnContainers.Services.Basket.API
|
public class Startup
|
||||||
{
|
{
|
||||||
public class Startup
|
public Startup(IConfiguration configuration)
|
||||||
{
|
{
|
||||||
public Startup(IConfiguration configuration)
|
Configuration = configuration;
|
||||||
{
|
|
||||||
Configuration = configuration;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IConfiguration Configuration { get; }
|
|
||||||
|
|
||||||
// This method gets called by the runtime. Use this method to add services to the container.
|
|
||||||
public virtual IServiceProvider ConfigureServices(IServiceCollection services)
|
|
||||||
{
|
|
||||||
services.AddGrpc(options =>
|
|
||||||
{
|
|
||||||
options.EnableDetailedErrors = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
RegisterAppInsights(services);
|
|
||||||
|
|
||||||
services.AddControllers(options =>
|
|
||||||
{
|
|
||||||
options.Filters.Add(typeof(HttpGlobalExceptionFilter));
|
|
||||||
options.Filters.Add(typeof(ValidateModelStateFilter));
|
|
||||||
|
|
||||||
}) // Added for functional tests
|
|
||||||
.AddApplicationPart(typeof(BasketController).Assembly)
|
|
||||||
.AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true);
|
|
||||||
|
|
||||||
services.AddSwaggerGen(options =>
|
|
||||||
{
|
|
||||||
options.DescribeAllEnumsAsStrings();
|
|
||||||
options.SwaggerDoc("v1", new OpenApiInfo
|
|
||||||
{
|
|
||||||
Title = "eShopOnContainers - Basket HTTP API",
|
|
||||||
Version = "v1",
|
|
||||||
Description = "The Basket Service HTTP API"
|
|
||||||
});
|
|
||||||
|
|
||||||
options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
|
|
||||||
{
|
|
||||||
Type = SecuritySchemeType.OAuth2,
|
|
||||||
Flows = new OpenApiOAuthFlows()
|
|
||||||
{
|
|
||||||
Implicit = new OpenApiOAuthFlow()
|
|
||||||
{
|
|
||||||
AuthorizationUrl = new Uri($"{Configuration.GetValue<string>("IdentityUrlExternal")}/connect/authorize"),
|
|
||||||
TokenUrl = new Uri($"{Configuration.GetValue<string>("IdentityUrlExternal")}/connect/token"),
|
|
||||||
Scopes = new Dictionary<string, string>()
|
|
||||||
{
|
|
||||||
{ "basket", "Basket API" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
options.OperationFilter<AuthorizeCheckOperationFilter>();
|
|
||||||
});
|
|
||||||
|
|
||||||
ConfigureAuthService(services);
|
|
||||||
|
|
||||||
services.AddCustomHealthCheck(Configuration);
|
|
||||||
|
|
||||||
services.Configure<BasketSettings>(Configuration);
|
|
||||||
|
|
||||||
//By connecting here we are making sure that our service
|
|
||||||
//cannot start until redis is ready. This might slow down startup,
|
|
||||||
//but given that there is a delay on resolving the ip address
|
|
||||||
//and then creating the connection it seems reasonable to move
|
|
||||||
//that cost to startup instead of having the first request pay the
|
|
||||||
//penalty.
|
|
||||||
services.AddSingleton<ConnectionMultiplexer>(sp =>
|
|
||||||
{
|
|
||||||
var settings = sp.GetRequiredService<IOptions<BasketSettings>>().Value;
|
|
||||||
var configuration = ConfigurationOptions.Parse(settings.ConnectionString, true);
|
|
||||||
|
|
||||||
configuration.ResolveDns = true;
|
|
||||||
|
|
||||||
return ConnectionMultiplexer.Connect(configuration);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
if (Configuration.GetValue<bool>("AzureServiceBusEnabled"))
|
|
||||||
{
|
|
||||||
services.AddSingleton<IServiceBusPersisterConnection>(sp =>
|
|
||||||
{
|
|
||||||
var serviceBusConnectionString = Configuration["EventBusConnection"];
|
|
||||||
var serviceBusConnection = new ServiceBusConnectionStringBuilder(serviceBusConnectionString);
|
|
||||||
|
|
||||||
var subscriptionClientName = Configuration["SubscriptionClientName"];
|
|
||||||
return new DefaultServiceBusPersisterConnection(serviceBusConnection, subscriptionClientName);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
services.AddSingleton<IRabbitMQPersistentConnection>(sp =>
|
|
||||||
{
|
|
||||||
var logger = sp.GetRequiredService<ILogger<DefaultRabbitMQPersistentConnection>>();
|
|
||||||
|
|
||||||
var factory = new ConnectionFactory()
|
|
||||||
{
|
|
||||||
HostName = Configuration["EventBusConnection"],
|
|
||||||
DispatchConsumersAsync = true
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(Configuration["EventBusUserName"]))
|
|
||||||
{
|
|
||||||
factory.UserName = Configuration["EventBusUserName"];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(Configuration["EventBusPassword"]))
|
|
||||||
{
|
|
||||||
factory.Password = Configuration["EventBusPassword"];
|
|
||||||
}
|
|
||||||
|
|
||||||
var retryCount = 5;
|
|
||||||
if (!string.IsNullOrEmpty(Configuration["EventBusRetryCount"]))
|
|
||||||
{
|
|
||||||
retryCount = int.Parse(Configuration["EventBusRetryCount"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new DefaultRabbitMQPersistentConnection(factory, logger, retryCount);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
RegisterEventBus(services);
|
|
||||||
|
|
||||||
|
|
||||||
services.AddCors(options =>
|
|
||||||
{
|
|
||||||
options.AddPolicy("CorsPolicy",
|
|
||||||
builder => builder
|
|
||||||
.SetIsOriginAllowed((host) => true)
|
|
||||||
.AllowAnyMethod()
|
|
||||||
.AllowAnyHeader()
|
|
||||||
.AllowCredentials());
|
|
||||||
});
|
|
||||||
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
|
||||||
services.AddTransient<IBasketRepository, RedisBasketRepository>();
|
|
||||||
services.AddTransient<IIdentityService, IdentityService>();
|
|
||||||
|
|
||||||
services.AddOptions();
|
|
||||||
|
|
||||||
var container = new ContainerBuilder();
|
|
||||||
container.Populate(services);
|
|
||||||
|
|
||||||
return new AutofacServiceProvider(container.Build());
|
|
||||||
}
|
|
||||||
|
|
||||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
|
||||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
|
|
||||||
{
|
|
||||||
//loggerFactory.AddAzureWebAppDiagnostics();
|
|
||||||
//loggerFactory.AddApplicationInsights(app.ApplicationServices, LogLevel.Trace);
|
|
||||||
|
|
||||||
var pathBase = Configuration["PATH_BASE"];
|
|
||||||
if (!string.IsNullOrEmpty(pathBase))
|
|
||||||
{
|
|
||||||
app.UsePathBase(pathBase);
|
|
||||||
}
|
|
||||||
|
|
||||||
app.UseSwagger()
|
|
||||||
.UseSwaggerUI(setup =>
|
|
||||||
{
|
|
||||||
setup.SwaggerEndpoint($"{ (!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty) }/swagger/v1/swagger.json", "Basket.API V1");
|
|
||||||
setup.OAuthClientId("basketswaggerui");
|
|
||||||
setup.OAuthAppName("Basket Swagger UI");
|
|
||||||
});
|
|
||||||
|
|
||||||
app.UseRouting();
|
|
||||||
app.UseCors("CorsPolicy");
|
|
||||||
ConfigureAuth(app);
|
|
||||||
|
|
||||||
app.UseStaticFiles();
|
|
||||||
|
|
||||||
app.UseEndpoints(endpoints =>
|
|
||||||
{
|
|
||||||
endpoints.MapGrpcService<BasketService>();
|
|
||||||
endpoints.MapDefaultControllerRoute();
|
|
||||||
endpoints.MapControllers();
|
|
||||||
endpoints.MapGet("/_proto/", async ctx =>
|
|
||||||
{
|
|
||||||
ctx.Response.ContentType = "text/plain";
|
|
||||||
using var fs = new FileStream(Path.Combine(env.ContentRootPath, "Proto", "basket.proto"), FileMode.Open, FileAccess.Read);
|
|
||||||
using var sr = new StreamReader(fs);
|
|
||||||
while (!sr.EndOfStream)
|
|
||||||
{
|
|
||||||
var line = await sr.ReadLineAsync();
|
|
||||||
if (line != "/* >>" || line != "<< */")
|
|
||||||
{
|
|
||||||
await ctx.Response.WriteAsync(line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
endpoints.MapHealthChecks("/hc", new HealthCheckOptions()
|
|
||||||
{
|
|
||||||
Predicate = _ => true,
|
|
||||||
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
|
|
||||||
});
|
|
||||||
endpoints.MapHealthChecks("/liveness", new HealthCheckOptions
|
|
||||||
{
|
|
||||||
Predicate = r => r.Name.Contains("self")
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
ConfigureEventBus(app);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RegisterAppInsights(IServiceCollection services)
|
|
||||||
{
|
|
||||||
services.AddApplicationInsightsTelemetry(Configuration);
|
|
||||||
services.AddApplicationInsightsKubernetesEnricher();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ConfigureAuthService(IServiceCollection services)
|
|
||||||
{
|
|
||||||
// prevent from mapping "sub" claim to nameidentifier.
|
|
||||||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub");
|
|
||||||
|
|
||||||
var identityUrl = Configuration.GetValue<string>("IdentityUrl");
|
|
||||||
|
|
||||||
services.AddAuthentication(options =>
|
|
||||||
{
|
|
||||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
|
||||||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
|
||||||
|
|
||||||
}).AddJwtBearer(options =>
|
|
||||||
{
|
|
||||||
options.Authority = identityUrl;
|
|
||||||
options.RequireHttpsMetadata = false;
|
|
||||||
options.Audience = "basket";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void ConfigureAuth(IApplicationBuilder app)
|
|
||||||
{
|
|
||||||
app.UseAuthentication();
|
|
||||||
app.UseAuthorization();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RegisterEventBus(IServiceCollection services)
|
|
||||||
{
|
|
||||||
if (Configuration.GetValue<bool>("AzureServiceBusEnabled"))
|
|
||||||
{
|
|
||||||
services.AddSingleton<IEventBus, EventBusServiceBus>(sp =>
|
|
||||||
{
|
|
||||||
var serviceBusPersisterConnection = sp.GetRequiredService<IServiceBusPersisterConnection>();
|
|
||||||
var iLifetimeScope = sp.GetRequiredService<ILifetimeScope>();
|
|
||||||
var logger = sp.GetRequiredService<ILogger<EventBusServiceBus>>();
|
|
||||||
var eventBusSubcriptionsManager = sp.GetRequiredService<IEventBusSubscriptionsManager>();
|
|
||||||
|
|
||||||
return new EventBusServiceBus(serviceBusPersisterConnection, logger,
|
|
||||||
eventBusSubcriptionsManager, iLifetimeScope);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
services.AddSingleton<IEventBus, EventBusRabbitMQ>(sp =>
|
|
||||||
{
|
|
||||||
var subscriptionClientName = Configuration["SubscriptionClientName"];
|
|
||||||
var rabbitMQPersistentConnection = sp.GetRequiredService<IRabbitMQPersistentConnection>();
|
|
||||||
var iLifetimeScope = sp.GetRequiredService<ILifetimeScope>();
|
|
||||||
var logger = sp.GetRequiredService<ILogger<EventBusRabbitMQ>>();
|
|
||||||
var eventBusSubcriptionsManager = sp.GetRequiredService<IEventBusSubscriptionsManager>();
|
|
||||||
|
|
||||||
var retryCount = 5;
|
|
||||||
if (!string.IsNullOrEmpty(Configuration["EventBusRetryCount"]))
|
|
||||||
{
|
|
||||||
retryCount = int.Parse(Configuration["EventBusRetryCount"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, iLifetimeScope, eventBusSubcriptionsManager, subscriptionClientName, retryCount);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
services.AddSingleton<IEventBusSubscriptionsManager, InMemoryEventBusSubscriptionsManager>();
|
|
||||||
|
|
||||||
services.AddTransient<ProductPriceChangedIntegrationEventHandler>();
|
|
||||||
services.AddTransient<OrderStartedIntegrationEventHandler>();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ConfigureEventBus(IApplicationBuilder app)
|
|
||||||
{
|
|
||||||
var eventBus = app.ApplicationServices.GetRequiredService<IEventBus>();
|
|
||||||
|
|
||||||
eventBus.Subscribe<ProductPriceChangedIntegrationEvent, ProductPriceChangedIntegrationEventHandler>();
|
|
||||||
eventBus.Subscribe<OrderStartedIntegrationEvent, OrderStartedIntegrationEventHandler>();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class CustomExtensionMethods
|
public IConfiguration Configuration { get; }
|
||||||
|
|
||||||
|
// This method gets called by the runtime. Use this method to add services to the container.
|
||||||
|
public virtual IServiceProvider ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
public static IServiceCollection AddCustomHealthCheck(this IServiceCollection services, IConfiguration configuration)
|
services.AddGrpc(options =>
|
||||||
{
|
{
|
||||||
var hcBuilder = services.AddHealthChecks();
|
options.EnableDetailedErrors = true;
|
||||||
|
});
|
||||||
|
|
||||||
hcBuilder.AddCheck("self", () => HealthCheckResult.Healthy());
|
RegisterAppInsights(services);
|
||||||
|
|
||||||
hcBuilder
|
services.AddControllers(options =>
|
||||||
.AddRedis(
|
|
||||||
configuration["ConnectionString"],
|
|
||||||
name: "redis-check",
|
|
||||||
tags: new string[] { "redis" });
|
|
||||||
|
|
||||||
if (configuration.GetValue<bool>("AzureServiceBusEnabled"))
|
|
||||||
{
|
{
|
||||||
hcBuilder
|
options.Filters.Add(typeof(HttpGlobalExceptionFilter));
|
||||||
.AddAzureServiceBusTopic(
|
options.Filters.Add(typeof(ValidateModelStateFilter));
|
||||||
configuration["EventBusConnection"],
|
|
||||||
topicName: "eshop_event_bus",
|
|
||||||
name: "basket-servicebus-check",
|
|
||||||
tags: new string[] { "servicebus" });
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
hcBuilder
|
|
||||||
.AddRabbitMQ(
|
|
||||||
$"amqp://{configuration["EventBusConnection"]}",
|
|
||||||
name: "basket-rabbitmqbus-check",
|
|
||||||
tags: new string[] { "rabbitmqbus" });
|
|
||||||
}
|
|
||||||
|
|
||||||
return services;
|
}) // Added for functional tests
|
||||||
|
.AddApplicationPart(typeof(BasketController).Assembly)
|
||||||
|
.AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true);
|
||||||
|
|
||||||
|
services.AddSwaggerGen(options =>
|
||||||
|
{
|
||||||
|
options.SwaggerDoc("v1", new OpenApiInfo
|
||||||
|
{
|
||||||
|
Title = "eShopOnContainers - Basket HTTP API",
|
||||||
|
Version = "v1",
|
||||||
|
Description = "The Basket Service HTTP API"
|
||||||
|
});
|
||||||
|
|
||||||
|
options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
|
||||||
|
{
|
||||||
|
Type = SecuritySchemeType.OAuth2,
|
||||||
|
Flows = new OpenApiOAuthFlows()
|
||||||
|
{
|
||||||
|
Implicit = new OpenApiOAuthFlow()
|
||||||
|
{
|
||||||
|
AuthorizationUrl = new Uri($"{Configuration.GetValue<string>("IdentityUrlExternal")}/connect/authorize"),
|
||||||
|
TokenUrl = new Uri($"{Configuration.GetValue<string>("IdentityUrlExternal")}/connect/token"),
|
||||||
|
Scopes = new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{ "basket", "Basket API" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
options.OperationFilter<AuthorizeCheckOperationFilter>();
|
||||||
|
});
|
||||||
|
|
||||||
|
ConfigureAuthService(services);
|
||||||
|
|
||||||
|
services.AddCustomHealthCheck(Configuration);
|
||||||
|
|
||||||
|
services.Configure<BasketSettings>(Configuration);
|
||||||
|
|
||||||
|
//By connecting here we are making sure that our service
|
||||||
|
//cannot start until redis is ready. This might slow down startup,
|
||||||
|
//but given that there is a delay on resolving the ip address
|
||||||
|
//and then creating the connection it seems reasonable to move
|
||||||
|
//that cost to startup instead of having the first request pay the
|
||||||
|
//penalty.
|
||||||
|
services.AddSingleton<ConnectionMultiplexer>(sp =>
|
||||||
|
{
|
||||||
|
var settings = sp.GetRequiredService<IOptions<BasketSettings>>().Value;
|
||||||
|
var configuration = ConfigurationOptions.Parse(settings.ConnectionString, true);
|
||||||
|
|
||||||
|
configuration.ResolveDns = true;
|
||||||
|
|
||||||
|
return ConnectionMultiplexer.Connect(configuration);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
if (Configuration.GetValue<bool>("AzureServiceBusEnabled"))
|
||||||
|
{
|
||||||
|
services.AddSingleton<IServiceBusPersisterConnection>(sp =>
|
||||||
|
{
|
||||||
|
var serviceBusConnectionString = Configuration["EventBusConnection"];
|
||||||
|
var serviceBusConnection = new ServiceBusConnectionStringBuilder(serviceBusConnectionString);
|
||||||
|
|
||||||
|
var subscriptionClientName = Configuration["SubscriptionClientName"];
|
||||||
|
return new DefaultServiceBusPersisterConnection(serviceBusConnection, subscriptionClientName);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
services.AddSingleton<IRabbitMQPersistentConnection>(sp =>
|
||||||
|
{
|
||||||
|
var logger = sp.GetRequiredService<ILogger<DefaultRabbitMQPersistentConnection>>();
|
||||||
|
|
||||||
|
var factory = new ConnectionFactory()
|
||||||
|
{
|
||||||
|
HostName = Configuration["EventBusConnection"],
|
||||||
|
DispatchConsumersAsync = true
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(Configuration["EventBusUserName"]))
|
||||||
|
{
|
||||||
|
factory.UserName = Configuration["EventBusUserName"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(Configuration["EventBusPassword"]))
|
||||||
|
{
|
||||||
|
factory.Password = Configuration["EventBusPassword"];
|
||||||
|
}
|
||||||
|
|
||||||
|
var retryCount = 5;
|
||||||
|
if (!string.IsNullOrEmpty(Configuration["EventBusRetryCount"]))
|
||||||
|
{
|
||||||
|
retryCount = int.Parse(Configuration["EventBusRetryCount"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DefaultRabbitMQPersistentConnection(factory, logger, retryCount);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
RegisterEventBus(services);
|
||||||
|
|
||||||
|
|
||||||
|
services.AddCors(options =>
|
||||||
|
{
|
||||||
|
options.AddPolicy("CorsPolicy",
|
||||||
|
builder => builder
|
||||||
|
.SetIsOriginAllowed((host) => true)
|
||||||
|
.AllowAnyMethod()
|
||||||
|
.AllowAnyHeader()
|
||||||
|
.AllowCredentials());
|
||||||
|
});
|
||||||
|
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||||
|
services.AddTransient<IBasketRepository, RedisBasketRepository>();
|
||||||
|
services.AddTransient<IIdentityService, IdentityService>();
|
||||||
|
|
||||||
|
services.AddOptions();
|
||||||
|
|
||||||
|
var container = new ContainerBuilder();
|
||||||
|
container.Populate(services);
|
||||||
|
|
||||||
|
return new AutofacServiceProvider(container.Build());
|
||||||
|
}
|
||||||
|
|
||||||
|
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||||
|
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
|
||||||
|
{
|
||||||
|
//loggerFactory.AddAzureWebAppDiagnostics();
|
||||||
|
//loggerFactory.AddApplicationInsights(app.ApplicationServices, LogLevel.Trace);
|
||||||
|
|
||||||
|
var pathBase = Configuration["PATH_BASE"];
|
||||||
|
if (!string.IsNullOrEmpty(pathBase))
|
||||||
|
{
|
||||||
|
app.UsePathBase(pathBase);
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseSwagger()
|
||||||
|
.UseSwaggerUI(setup =>
|
||||||
|
{
|
||||||
|
setup.SwaggerEndpoint($"{ (!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty) }/swagger/v1/swagger.json", "Basket.API V1");
|
||||||
|
setup.OAuthClientId("basketswaggerui");
|
||||||
|
setup.OAuthAppName("Basket Swagger UI");
|
||||||
|
});
|
||||||
|
|
||||||
|
app.UseRouting();
|
||||||
|
app.UseCors("CorsPolicy");
|
||||||
|
ConfigureAuth(app);
|
||||||
|
|
||||||
|
app.UseStaticFiles();
|
||||||
|
|
||||||
|
app.UseEndpoints(endpoints =>
|
||||||
|
{
|
||||||
|
endpoints.MapGrpcService<BasketService>();
|
||||||
|
endpoints.MapDefaultControllerRoute();
|
||||||
|
endpoints.MapControllers();
|
||||||
|
endpoints.MapGet("/_proto/", async ctx =>
|
||||||
|
{
|
||||||
|
ctx.Response.ContentType = "text/plain";
|
||||||
|
using var fs = new FileStream(Path.Combine(env.ContentRootPath, "Proto", "basket.proto"), FileMode.Open, FileAccess.Read);
|
||||||
|
using var sr = new StreamReader(fs);
|
||||||
|
while (!sr.EndOfStream)
|
||||||
|
{
|
||||||
|
var line = await sr.ReadLineAsync();
|
||||||
|
if (line != "/* >>" || line != "<< */")
|
||||||
|
{
|
||||||
|
await ctx.Response.WriteAsync(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
endpoints.MapHealthChecks("/hc", new HealthCheckOptions()
|
||||||
|
{
|
||||||
|
Predicate = _ => true,
|
||||||
|
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
|
||||||
|
});
|
||||||
|
endpoints.MapHealthChecks("/liveness", new HealthCheckOptions
|
||||||
|
{
|
||||||
|
Predicate = r => r.Name.Contains("self")
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ConfigureEventBus(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RegisterAppInsights(IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddApplicationInsightsTelemetry(Configuration);
|
||||||
|
services.AddApplicationInsightsKubernetesEnricher();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConfigureAuthService(IServiceCollection services)
|
||||||
|
{
|
||||||
|
// prevent from mapping "sub" claim to nameidentifier.
|
||||||
|
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub");
|
||||||
|
|
||||||
|
var identityUrl = Configuration.GetValue<string>("IdentityUrl");
|
||||||
|
|
||||||
|
services.AddAuthentication(options =>
|
||||||
|
{
|
||||||
|
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
|
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
|
|
||||||
|
}).AddJwtBearer(options =>
|
||||||
|
{
|
||||||
|
options.Authority = identityUrl;
|
||||||
|
options.RequireHttpsMetadata = false;
|
||||||
|
options.Audience = "basket";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void ConfigureAuth(IApplicationBuilder app)
|
||||||
|
{
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RegisterEventBus(IServiceCollection services)
|
||||||
|
{
|
||||||
|
if (Configuration.GetValue<bool>("AzureServiceBusEnabled"))
|
||||||
|
{
|
||||||
|
services.AddSingleton<IEventBus, EventBusServiceBus>(sp =>
|
||||||
|
{
|
||||||
|
var serviceBusPersisterConnection = sp.GetRequiredService<IServiceBusPersisterConnection>();
|
||||||
|
var iLifetimeScope = sp.GetRequiredService<ILifetimeScope>();
|
||||||
|
var logger = sp.GetRequiredService<ILogger<EventBusServiceBus>>();
|
||||||
|
var eventBusSubcriptionsManager = sp.GetRequiredService<IEventBusSubscriptionsManager>();
|
||||||
|
|
||||||
|
return new EventBusServiceBus(serviceBusPersisterConnection, logger,
|
||||||
|
eventBusSubcriptionsManager, iLifetimeScope);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
services.AddSingleton<IEventBus, EventBusRabbitMQ>(sp =>
|
||||||
|
{
|
||||||
|
var subscriptionClientName = Configuration["SubscriptionClientName"];
|
||||||
|
var rabbitMQPersistentConnection = sp.GetRequiredService<IRabbitMQPersistentConnection>();
|
||||||
|
var iLifetimeScope = sp.GetRequiredService<ILifetimeScope>();
|
||||||
|
var logger = sp.GetRequiredService<ILogger<EventBusRabbitMQ>>();
|
||||||
|
var eventBusSubcriptionsManager = sp.GetRequiredService<IEventBusSubscriptionsManager>();
|
||||||
|
|
||||||
|
var retryCount = 5;
|
||||||
|
if (!string.IsNullOrEmpty(Configuration["EventBusRetryCount"]))
|
||||||
|
{
|
||||||
|
retryCount = int.Parse(Configuration["EventBusRetryCount"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, iLifetimeScope, eventBusSubcriptionsManager, subscriptionClientName, retryCount);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
services.AddSingleton<IEventBusSubscriptionsManager, InMemoryEventBusSubscriptionsManager>();
|
||||||
|
|
||||||
|
services.AddTransient<ProductPriceChangedIntegrationEventHandler>();
|
||||||
|
services.AddTransient<OrderStartedIntegrationEventHandler>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConfigureEventBus(IApplicationBuilder app)
|
||||||
|
{
|
||||||
|
var eventBus = app.ApplicationServices.GetRequiredService<IEventBus>();
|
||||||
|
|
||||||
|
eventBus.Subscribe<ProductPriceChangedIntegrationEvent, ProductPriceChangedIntegrationEventHandler>();
|
||||||
|
eventBus.Subscribe<OrderStartedIntegrationEvent, OrderStartedIntegrationEventHandler>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class CustomExtensionMethods
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddCustomHealthCheck(this IServiceCollection services, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
var hcBuilder = services.AddHealthChecks();
|
||||||
|
|
||||||
|
hcBuilder.AddCheck("self", () => HealthCheckResult.Healthy());
|
||||||
|
|
||||||
|
hcBuilder
|
||||||
|
.AddRedis(
|
||||||
|
configuration["ConnectionString"],
|
||||||
|
name: "redis-check",
|
||||||
|
tags: new string[] { "redis" });
|
||||||
|
|
||||||
|
if (configuration.GetValue<bool>("AzureServiceBusEnabled"))
|
||||||
|
{
|
||||||
|
hcBuilder
|
||||||
|
.AddAzureServiceBusTopic(
|
||||||
|
configuration["EventBusConnection"],
|
||||||
|
topicName: "eshop_event_bus",
|
||||||
|
name: "basket-servicebus-check",
|
||||||
|
tags: new string[] { "servicebus" });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
hcBuilder
|
||||||
|
.AddRabbitMQ(
|
||||||
|
$"amqp://{configuration["EventBusConnection"]}",
|
||||||
|
name: "basket-rabbitmqbus-check",
|
||||||
|
tags: new string[] { "rabbitmqbus" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
using Microsoft.AspNetCore.Http;
|
namespace Microsoft.eShopOnContainers.Services.Basket.API;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
|
||||||
|
|
||||||
namespace Microsoft.eShopOnContainers.Services.Basket.API
|
internal class TestHttpResponseTrailersFeature : IHttpResponseTrailersFeature
|
||||||
{
|
{
|
||||||
internal class TestHttpResponseTrailersFeature : IHttpResponseTrailersFeature
|
public IHeaderDictionary Trailers { get; set; }
|
||||||
{
|
|
||||||
public IHeaderDictionary Trailers { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user