Moved using statements to globalusing file in webhooks.api

This commit is contained in:
Sumit Ghosh 2021-10-12 19:28:22 +05:30
parent ac9874e44a
commit 2cdb56fb69
27 changed files with 704 additions and 881 deletions

View File

@ -1,15 +1,11 @@
using Microsoft.AspNetCore.Mvc; namespace Webhooks.API.Controllers;
namespace Webhooks.API.Controllers public class HomeController : Controller
{ {
// GET: /<controller>/
public class HomeController : Controller public IActionResult Index()
{ {
// GET: /<controller>/ return new RedirectResult("~/swagger");
public IActionResult Index()
{
return new RedirectResult("~/swagger");
}
} }
} }

View File

@ -1,35 +1,29 @@
using System; namespace Webhooks.API.Controllers;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Webhooks.API.Model;
namespace Webhooks.API.Controllers public class WebhookSubscriptionRequest : IValidatableObject
{ {
public class WebhookSubscriptionRequest : IValidatableObject public string Url { get; set; }
public string Token { get; set; }
public string Event { get; set; }
public string GrantUrl { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{ {
public string Url { get; set; } if (!Uri.IsWellFormedUriString(GrantUrl, UriKind.Absolute))
public string Token { get; set; }
public string Event { get; set; }
public string GrantUrl { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{ {
if (!Uri.IsWellFormedUriString(GrantUrl, UriKind.Absolute)) yield return new ValidationResult("GrantUrl is not valid", new[] { nameof(GrantUrl) });
{
yield return new ValidationResult("GrantUrl is not valid", new[] { nameof(GrantUrl) });
}
if (!Uri.IsWellFormedUriString(Url, UriKind.Absolute))
{
yield return new ValidationResult("Url is not valid", new[] { nameof(Url) });
}
var isOk = Enum.TryParse<WebhookType>(Event, ignoreCase: true, result: out WebhookType whtype);
if (!isOk)
{
yield return new ValidationResult($"{Event} is invalid event name", new[] { nameof(Event) });
}
} }
if (!Uri.IsWellFormedUriString(Url, UriKind.Absolute))
{
yield return new ValidationResult("Url is not valid", new[] { nameof(Url) });
}
var isOk = Enum.TryParse<WebhookType>(Event, ignoreCase: true, result: out WebhookType whtype);
if (!isOk)
{
yield return new ValidationResult($"{Event} is invalid event name", new[] { nameof(Event) });
}
} }
} }

View File

@ -1,115 +1,100 @@
using Microsoft.AspNetCore.Authorization; namespace Webhooks.API.Controllers;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Webhooks.API.Infrastructure;
using Webhooks.API.Model;
using Webhooks.API.Services;
namespace Webhooks.API.Controllers [Route("api/v1/[controller]")]
[ApiController]
public class WebhooksController : ControllerBase
{ {
[Route("api/v1/[controller]")] private readonly WebhooksContext _dbContext;
[ApiController] private readonly IIdentityService _identityService;
public class WebhooksController : ControllerBase private readonly IGrantUrlTesterService _grantUrlTester;
public WebhooksController(WebhooksContext dbContext, IIdentityService identityService, IGrantUrlTesterService grantUrlTester)
{ {
private readonly WebhooksContext _dbContext; _dbContext = dbContext;
private readonly IIdentityService _identityService; _identityService = identityService;
private readonly IGrantUrlTesterService _grantUrlTester; _grantUrlTester = grantUrlTester;
public WebhooksController(WebhooksContext dbContext, IIdentityService identityService, IGrantUrlTesterService grantUrlTester)
{
_dbContext = dbContext;
_identityService = identityService;
_grantUrlTester = grantUrlTester;
}
[Authorize]
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<WebhookSubscription>), (int)HttpStatusCode.OK)]
public async Task<IActionResult> ListByUser()
{
var userId = _identityService.GetUserIdentity();
var data = await _dbContext.Subscriptions.Where(s => s.UserId == userId).ToListAsync();
return Ok(data);
}
[Authorize]
[HttpGet("{id:int}")]
[ProducesResponseType(typeof(WebhookSubscription), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
public async Task<IActionResult> GetByUserAndId(int id)
{
var userId = _identityService.GetUserIdentity();
var subscription = await _dbContext.Subscriptions.SingleOrDefaultAsync(s => s.Id == id && s.UserId == userId);
if (subscription != null)
{
return Ok(subscription);
}
return NotFound($"Subscriptions {id} not found");
}
[Authorize]
[HttpPost]
[ProducesResponseType((int)HttpStatusCode.Created)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[ProducesResponseType(418)]
public async Task<IActionResult> SubscribeWebhook(WebhookSubscriptionRequest request)
{
if (!ModelState.IsValid)
{
return ValidationProblem(ModelState);
}
var userId = _identityService.GetUserIdentity();
var grantOk = await _grantUrlTester.TestGrantUrl(request.Url, request.GrantUrl, request.Token ?? string.Empty);
if (grantOk)
{
var subscription = new WebhookSubscription()
{
Date = DateTime.UtcNow,
DestUrl = request.Url,
Token = request.Token,
Type = Enum.Parse<WebhookType>(request.Event, ignoreCase: true),
UserId = _identityService.GetUserIdentity()
};
_dbContext.Add(subscription);
await _dbContext.SaveChangesAsync();
return CreatedAtAction("GetByUserAndId", new { id = subscription.Id }, subscription);
}
else
{
return StatusCode(418, "Grant url can't be validated");
}
}
[Authorize]
[HttpDelete("{id:int}")]
[ProducesResponseType((int)HttpStatusCode.Accepted)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
public async Task<IActionResult> UnsubscribeWebhook(int id)
{
var userId = _identityService.GetUserIdentity();
var subscription = await _dbContext.Subscriptions.SingleOrDefaultAsync(s => s.Id == id && s.UserId == userId);
if (subscription != null)
{
_dbContext.Remove(subscription);
await _dbContext.SaveChangesAsync();
return Accepted();
}
return NotFound($"Subscriptions {id} not found");
}
} }
[Authorize]
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<WebhookSubscription>), (int)HttpStatusCode.OK)]
public async Task<IActionResult> ListByUser()
{
var userId = _identityService.GetUserIdentity();
var data = await _dbContext.Subscriptions.Where(s => s.UserId == userId).ToListAsync();
return Ok(data);
}
[Authorize]
[HttpGet("{id:int}")]
[ProducesResponseType(typeof(WebhookSubscription), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
public async Task<IActionResult> GetByUserAndId(int id)
{
var userId = _identityService.GetUserIdentity();
var subscription = await _dbContext.Subscriptions.SingleOrDefaultAsync(s => s.Id == id && s.UserId == userId);
if (subscription != null)
{
return Ok(subscription);
}
return NotFound($"Subscriptions {id} not found");
}
[Authorize]
[HttpPost]
[ProducesResponseType((int)HttpStatusCode.Created)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[ProducesResponseType(418)]
public async Task<IActionResult> SubscribeWebhook(WebhookSubscriptionRequest request)
{
if (!ModelState.IsValid)
{
return ValidationProblem(ModelState);
}
var userId = _identityService.GetUserIdentity();
var grantOk = await _grantUrlTester.TestGrantUrl(request.Url, request.GrantUrl, request.Token ?? string.Empty);
if (grantOk)
{
var subscription = new WebhookSubscription()
{
Date = DateTime.UtcNow,
DestUrl = request.Url,
Token = request.Token,
Type = Enum.Parse<WebhookType>(request.Event, ignoreCase: true),
UserId = _identityService.GetUserIdentity()
};
_dbContext.Add(subscription);
await _dbContext.SaveChangesAsync();
return CreatedAtAction("GetByUserAndId", new { id = subscription.Id }, subscription);
}
else
{
return StatusCode(418, "Grant url can't be validated");
}
}
[Authorize]
[HttpDelete("{id:int}")]
[ProducesResponseType((int)HttpStatusCode.Accepted)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
public async Task<IActionResult> UnsubscribeWebhook(int id)
{
var userId = _identityService.GetUserIdentity();
var subscription = await _dbContext.Subscriptions.SingleOrDefaultAsync(s => s.Id == id && s.UserId == userId);
if (subscription != null)
{
_dbContext.Remove(subscription);
await _dbContext.SaveChangesAsync();
return Accepted();
}
return NotFound($"Subscriptions {id} not found");
}
} }

View File

@ -1,8 +1,5 @@
using System; namespace Webhooks.API.Exceptions;
namespace Webhooks.API.Exceptions public class WebhooksDomainException : Exception
{ {
public class WebhooksDomainException : Exception
{
}
} }

View File

@ -1,13 +1,9 @@
using Microsoft.AspNetCore.Http; namespace Webhooks.API.Infrastructure.ActionResult;
using Microsoft.AspNetCore.Mvc;
namespace Webhooks.API.Infrastructure.ActionResult class InternalServerErrorObjectResult : ObjectResult
{ {
class InternalServerErrorObjectResult : ObjectResult public InternalServerErrorObjectResult(object error) : base(error)
{ {
public InternalServerErrorObjectResult(object error) : base(error) StatusCode = StatusCodes.Status500InternalServerError;
{
StatusCode = StatusCodes.Status500InternalServerError;
}
} }
} }

View File

@ -1,36 +1,29 @@
using Microsoft.AspNetCore.Authorization; namespace Webhooks.API.Infrastructure;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.Collections.Generic;
using System.Linq;
namespace Webhooks.API.Infrastructure 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 [] { "webhooksapi" }
{ }
[ oAuthScheme ] = new [] { "webhooksapi" } };
}
};
}
} }
} }

View File

@ -1,69 +1,58 @@
using Microsoft.AspNetCore.Hosting; namespace Webhooks.API.Infrastructure;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Net;
using Webhooks.API.Exceptions;
using Webhooks.API.Infrastructure.ActionResult;
namespace Webhooks.API.Infrastructure public class HttpGlobalExceptionFilter : IExceptionFilter
{ {
public 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(WebhooksDomainException))
{ {
this.env = env; var problemDetails = new ValidationProblemDetails()
this.logger = logger;
}
public void OnException(ExceptionContext context)
{
logger.LogError(new EventId(context.Exception.HResult),
context.Exception,
context.Exception.Message);
if (context.Exception.GetType() == typeof(WebhooksDomainException))
{ {
var problemDetails = new ValidationProblemDetails() Instance = context.HttpContext.Request.Path,
{ Status = StatusCodes.Status400BadRequest,
Instance = context.HttpContext.Request.Path, Detail = "Please refer to the errors property for additional details."
Status = StatusCodes.Status400BadRequest, };
Detail = "Please refer to the errors property for additional details."
};
problemDetails.Errors.Add("DomainValidations", new string[] { context.Exception.Message.ToString() }); problemDetails.Errors.Add("DomainValidations", new string[] { context.Exception.Message.ToString() });
context.Result = new BadRequestObjectResult(problemDetails); context.Result = new BadRequestObjectResult(problemDetails);
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
}
else
{
var json = new JsonErrorResponse
{
Messages = new[] { "An error ocurred." }
};
if (env.IsDevelopment())
{
json.DeveloperMeesage = context.Exception;
}
context.Result = new InternalServerErrorObjectResult(json);
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
}
context.ExceptionHandled = true;
} }
else
private class JsonErrorResponse
{ {
public string[] Messages { get; set; } var json = new JsonErrorResponse
{
Messages = new[] { "An error ocurred." }
};
public object DeveloperMeesage { get; set; } if (env.IsDevelopment())
{
json.DeveloperMeesage = context.Exception;
}
context.Result = new InternalServerErrorObjectResult(json);
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
} }
context.ExceptionHandled = true;
}
private class JsonErrorResponse
{
public string[] Messages { get; set; }
public object DeveloperMeesage { get; set; }
} }
} }

View File

@ -1,26 +1,21 @@
using Microsoft.EntityFrameworkCore; namespace Webhooks.API.Infrastructure;
using Microsoft.EntityFrameworkCore.Design;
using Webhooks.API.Model;
namespace Webhooks.API.Infrastructure public class WebhooksContext : DbContext
{ {
public class WebhooksContext : DbContext
{
public WebhooksContext(DbContextOptions<WebhooksContext> options) : base(options) public WebhooksContext(DbContextOptions<WebhooksContext> options) : base(options)
{ {
}
public DbSet<WebhookSubscription> Subscriptions { get; set; }
} }
public DbSet<WebhookSubscription> Subscriptions { get; set; }
}
public class WebhooksContextDesignFactory : IDesignTimeDbContextFactory<WebhooksContext> public class WebhooksContextDesignFactory : IDesignTimeDbContextFactory<WebhooksContext>
{
public WebhooksContext CreateDbContext(string[] args)
{ {
public WebhooksContext CreateDbContext(string[] args) var optionsBuilder = new DbContextOptionsBuilder<WebhooksContext>()
{ .UseSqlServer("Server=.;Initial Catalog=Microsoft.eShopOnContainers.Services.CatalogDb;Integrated Security=true");
var optionsBuilder = new DbContextOptionsBuilder<WebhooksContext>()
.UseSqlServer("Server=.;Initial Catalog=Microsoft.eShopOnContainers.Services.CatalogDb;Integrated Security=true");
return new WebhooksContext(optionsBuilder.Options); return new WebhooksContext(optionsBuilder.Options);
}
} }
} }

View File

@ -1,30 +1,26 @@
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; namespace Webhooks.API.IntegrationEvents;
using System.Collections.Generic;
namespace Webhooks.API.IntegrationEvents public record OrderStatusChangedToPaidIntegrationEvent : IntegrationEvent
{ {
public record OrderStatusChangedToPaidIntegrationEvent : IntegrationEvent public int OrderId { get; }
public IEnumerable<OrderStockItem> OrderStockItems { get; }
public OrderStatusChangedToPaidIntegrationEvent(int orderId,
IEnumerable<OrderStockItem> orderStockItems)
{ {
public int OrderId { get; } OrderId = orderId;
public IEnumerable<OrderStockItem> OrderStockItems { get; } OrderStockItems = orderStockItems;
}
public OrderStatusChangedToPaidIntegrationEvent(int orderId, }
IEnumerable<OrderStockItem> orderStockItems)
{ public record OrderStockItem
OrderId = orderId; {
OrderStockItems = orderStockItems; public int ProductId { get; }
} public int Units { get; }
}
public OrderStockItem(int productId, int units)
public record OrderStockItem {
{ ProductId = productId;
public int ProductId { get; } Units = units;
public int Units { get; }
public OrderStockItem(int productId, int units)
{
ProductId = productId;
Units = units;
}
} }
} }

View File

@ -1,30 +1,22 @@
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; namespace Webhooks.API.IntegrationEvents;
using Microsoft.Extensions.Logging;
using System.Linq;
using System.Threading.Tasks;
using Webhooks.API.Model;
using Webhooks.API.Services;
namespace Webhooks.API.IntegrationEvents public class OrderStatusChangedToPaidIntegrationEventHandler : IIntegrationEventHandler<OrderStatusChangedToPaidIntegrationEvent>
{ {
public class OrderStatusChangedToPaidIntegrationEventHandler : IIntegrationEventHandler<OrderStatusChangedToPaidIntegrationEvent> private readonly IWebhooksRetriever _retriever;
private readonly IWebhooksSender _sender;
private readonly ILogger _logger;
public OrderStatusChangedToPaidIntegrationEventHandler(IWebhooksRetriever retriever, IWebhooksSender sender, ILogger<OrderStatusChangedToShippedIntegrationEventHandler> logger)
{ {
private readonly IWebhooksRetriever _retriever; _retriever = retriever;
private readonly IWebhooksSender _sender; _sender = sender;
private readonly ILogger _logger; _logger = logger;
public OrderStatusChangedToPaidIntegrationEventHandler(IWebhooksRetriever retriever, IWebhooksSender sender, ILogger<OrderStatusChangedToShippedIntegrationEventHandler> logger) }
{
_retriever = retriever;
_sender = sender;
_logger = logger;
}
public async Task Handle(OrderStatusChangedToPaidIntegrationEvent @event) public async Task Handle(OrderStatusChangedToPaidIntegrationEvent @event)
{ {
var subscriptions = await _retriever.GetSubscriptionsOfType(WebhookType.OrderPaid); var subscriptions = await _retriever.GetSubscriptionsOfType(WebhookType.OrderPaid);
_logger.LogInformation("Received OrderStatusChangedToShippedIntegrationEvent and got {SubscriptionsCount} subscriptions to process", subscriptions.Count()); _logger.LogInformation("Received OrderStatusChangedToShippedIntegrationEvent and got {SubscriptionsCount} subscriptions to process", subscriptions.Count());
var whook = new WebhookData(WebhookType.OrderPaid, @event); var whook = new WebhookData(WebhookType.OrderPaid, @event);
await _sender.SendAll(subscriptions, whook); await _sender.SendAll(subscriptions, whook);
}
} }
} }

View File

@ -1,18 +1,15 @@
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; namespace Webhooks.API.IntegrationEvents;
namespace Webhooks.API.IntegrationEvents public record OrderStatusChangedToShippedIntegrationEvent : IntegrationEvent
{ {
public record OrderStatusChangedToShippedIntegrationEvent : IntegrationEvent public int OrderId { get; private init; }
{ public string OrderStatus { get; private init; }
public int OrderId { get; private init; } public string BuyerName { get; private init; }
public string OrderStatus { get; private init; }
public string BuyerName { get; private init; }
public OrderStatusChangedToShippedIntegrationEvent(int orderId, string orderStatus, string buyerName) public OrderStatusChangedToShippedIntegrationEvent(int orderId, string orderStatus, string buyerName)
{ {
OrderId = orderId; OrderId = orderId;
OrderStatus = orderStatus; OrderStatus = orderStatus;
BuyerName = buyerName; BuyerName = buyerName;
}
} }
} }

View File

@ -1,30 +1,22 @@
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; namespace Webhooks.API.IntegrationEvents;
using Microsoft.Extensions.Logging;
using System.Linq;
using System.Threading.Tasks;
using Webhooks.API.Model;
using Webhooks.API.Services;
namespace Webhooks.API.IntegrationEvents public class OrderStatusChangedToShippedIntegrationEventHandler : IIntegrationEventHandler<OrderStatusChangedToShippedIntegrationEvent>
{ {
public class OrderStatusChangedToShippedIntegrationEventHandler : IIntegrationEventHandler<OrderStatusChangedToShippedIntegrationEvent> private readonly IWebhooksRetriever _retriever;
private readonly IWebhooksSender _sender;
private readonly ILogger _logger;
public OrderStatusChangedToShippedIntegrationEventHandler(IWebhooksRetriever retriever, IWebhooksSender sender, ILogger<OrderStatusChangedToShippedIntegrationEventHandler> logger)
{ {
private readonly IWebhooksRetriever _retriever; _retriever = retriever;
private readonly IWebhooksSender _sender; _sender = sender;
private readonly ILogger _logger; _logger = logger;
public OrderStatusChangedToShippedIntegrationEventHandler(IWebhooksRetriever retriever, IWebhooksSender sender, ILogger<OrderStatusChangedToShippedIntegrationEventHandler> logger) }
{
_retriever = retriever;
_sender = sender;
_logger = logger;
}
public async Task Handle(OrderStatusChangedToShippedIntegrationEvent @event) public async Task Handle(OrderStatusChangedToShippedIntegrationEvent @event)
{ {
var subscriptions = await _retriever.GetSubscriptionsOfType(WebhookType.OrderShipped); var subscriptions = await _retriever.GetSubscriptionsOfType(WebhookType.OrderShipped);
_logger.LogInformation("Received OrderStatusChangedToShippedIntegrationEvent and got {SubscriptionCount} subscriptions to process", subscriptions.Count()); _logger.LogInformation("Received OrderStatusChangedToShippedIntegrationEvent and got {SubscriptionCount} subscriptions to process", subscriptions.Count());
var whook = new WebhookData(WebhookType.OrderShipped, @event); var whook = new WebhookData(WebhookType.OrderShipped, @event);
await _sender.SendAll(subscriptions, whook); await _sender.SendAll(subscriptions, whook);
}
} }
} }

View File

@ -1,20 +1,17 @@
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; namespace Webhooks.API.IntegrationEvents;
namespace Webhooks.API.IntegrationEvents public record ProductPriceChangedIntegrationEvent : IntegrationEvent
{ {
public record ProductPriceChangedIntegrationEvent : IntegrationEvent public int ProductId { get; private init; }
public decimal NewPrice { get; private init; }
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;
}
} }
} }

View File

@ -1,13 +1,9 @@
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; namespace Webhooks.API.IntegrationEvents;
using System.Threading.Tasks;
namespace Webhooks.API.IntegrationEvents public class ProductPriceChangedIntegrationEventHandler : IIntegrationEventHandler<ProductPriceChangedIntegrationEvent>
{ {
public class ProductPriceChangedIntegrationEventHandler : IIntegrationEventHandler<ProductPriceChangedIntegrationEvent> public async Task Handle(ProductPriceChangedIntegrationEvent @event)
{ {
public async Task Handle(ProductPriceChangedIntegrationEvent @event) int i = 0;
{
int i = 0;
}
} }
} }

View File

@ -1,21 +1,17 @@
using System; namespace Webhooks.API.Model;
using System.Text.Json;
namespace Webhooks.API.Model public class WebhookData
{ {
public class WebhookData public DateTime When { get; }
public string Payload { get; }
public string Type { get; }
public WebhookData(WebhookType hookType, object data)
{ {
public DateTime When { get; } When = DateTime.UtcNow;
Type = hookType.ToString();
public string Payload { get; } Payload = JsonSerializer.Serialize(data);
public string Type { get; }
public WebhookData(WebhookType hookType, object data)
{
When = DateTime.UtcNow;
Type = hookType.ToString();
Payload = JsonSerializer.Serialize(data);
}
} }
} }

View File

@ -1,15 +1,12 @@
using System; namespace Webhooks.API.Model;
namespace Webhooks.API.Model public class WebhookSubscription
{ {
public class WebhookSubscription public int Id { get; set; }
{
public int Id { get; set; }
public WebhookType Type { get; set; } public WebhookType Type { get; set; }
public DateTime Date { get; set; } public DateTime Date { get; set; }
public string DestUrl { get; set; } public string DestUrl { get; set; }
public string Token { get; set; } public string Token { get; set; }
public string UserId { get; set; } public string UserId { get; set; }
}
} }

View File

@ -1,9 +1,8 @@
namespace Webhooks.API.Model namespace Webhooks.API.Model;
public enum WebhookType
{ {
public enum WebhookType CatalogItemPriceChange = 1,
{ OrderShipped = 2,
CatalogItemPriceChange = 1, OrderPaid = 3
OrderShipped = 2,
OrderPaid = 3
}
} }

View File

@ -1,11 +1,4 @@
using Microsoft.AspNetCore; CreateWebHostBuilder(args).Build()
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Webhooks.API;
using Webhooks.API.Infrastructure;
CreateWebHostBuilder(args).Build()
.MigrateDbContext<WebhooksContext>((_, __) => { }) .MigrateDbContext<WebhooksContext>((_, __) => { })
.Run(); .Run();

View File

@ -1,57 +1,50 @@
using Microsoft.Extensions.Logging; namespace Webhooks.API.Services;
using System;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
namespace Webhooks.API.Services class GrantUrlTesterService : IGrantUrlTesterService
{ {
class GrantUrlTesterService : IGrantUrlTesterService private readonly IHttpClientFactory _clientFactory;
private readonly ILogger _logger;
public GrantUrlTesterService(IHttpClientFactory factory, ILogger<IGrantUrlTesterService> logger)
{ {
private readonly IHttpClientFactory _clientFactory; _clientFactory = factory;
private readonly ILogger _logger; _logger = logger;
public GrantUrlTesterService(IHttpClientFactory factory, ILogger<IGrantUrlTesterService> logger) }
public async Task<bool> TestGrantUrl(string urlHook, string url, string token)
{
if (!CheckSameOrigin(urlHook, url))
{ {
_clientFactory = factory; _logger.LogWarning("Url of the hook ({UrlHook} and the grant url ({Url} do not belong to same origin)", urlHook, url);
_logger = logger; return false;
} }
public async Task<bool> TestGrantUrl(string urlHook, string url, string token)
var client = _clientFactory.CreateClient("GrantClient");
var msg = new HttpRequestMessage(HttpMethod.Options, url);
msg.Headers.Add("X-eshop-whtoken", token);
_logger.LogInformation("Sending the OPTIONS message to {Url} with token \"{Token}\"", url, token ?? string.Empty);
try
{ {
if (!CheckSameOrigin(urlHook, url)) var response = await client.SendAsync(msg);
{ var tokenReceived = response.Headers.TryGetValues("X-eshop-whtoken", out var tokenValues) ? tokenValues.FirstOrDefault() : null;
_logger.LogWarning("Url of the hook ({UrlHook} and the grant url ({Url} do not belong to same origin)", urlHook, url); var tokenExpected = string.IsNullOrWhiteSpace(token) ? null : token;
return false; _logger.LogInformation("Response code is {StatusCode} for url {Url} and token in header was {TokenReceived} (expected token was {TokenExpected})", response.StatusCode, url, tokenReceived, tokenExpected);
} return response.IsSuccessStatusCode && tokenReceived == tokenExpected;
var client = _clientFactory.CreateClient("GrantClient");
var msg = new HttpRequestMessage(HttpMethod.Options, url);
msg.Headers.Add("X-eshop-whtoken", token);
_logger.LogInformation("Sending the OPTIONS message to {Url} with token \"{Token}\"", url, token ?? string.Empty);
try
{
var response = await client.SendAsync(msg);
var tokenReceived = response.Headers.TryGetValues("X-eshop-whtoken", out var tokenValues) ? tokenValues.FirstOrDefault() : null;
var tokenExpected = string.IsNullOrWhiteSpace(token) ? null : token;
_logger.LogInformation("Response code is {StatusCode} for url {Url} and token in header was {TokenReceived} (expected token was {TokenExpected})", response.StatusCode, url, tokenReceived, tokenExpected);
return response.IsSuccessStatusCode && tokenReceived == tokenExpected;
}
catch (Exception ex)
{
_logger.LogWarning("Exception {TypeName} when sending OPTIONS request. Url can't be granted.", ex.GetType().Name);
return false;
}
} }
catch (Exception ex)
private bool CheckSameOrigin(string urlHook, string url)
{ {
var firstUrl = new Uri(urlHook, UriKind.Absolute); _logger.LogWarning("Exception {TypeName} when sending OPTIONS request. Url can't be granted.", ex.GetType().Name);
var secondUrl = new Uri(url, UriKind.Absolute); return false;
return firstUrl.Scheme == secondUrl.Scheme &&
firstUrl.Port == secondUrl.Port &&
firstUrl.Host == firstUrl.Host;
} }
} }
private bool CheckSameOrigin(string urlHook, string url)
{
var firstUrl = new Uri(urlHook, UriKind.Absolute);
var secondUrl = new Uri(url, UriKind.Absolute);
return firstUrl.Scheme == secondUrl.Scheme &&
firstUrl.Port == secondUrl.Port &&
firstUrl.Host == firstUrl.Host;
}
} }

View File

@ -1,9 +1,6 @@
using System.Threading.Tasks; namespace Webhooks.API.Services;
namespace Webhooks.API.Services public interface IGrantUrlTesterService
{ {
public interface IGrantUrlTesterService Task<bool> TestGrantUrl(string urlHook, string url, string token);
{
Task<bool> TestGrantUrl(string urlHook, string url, string token);
}
} }

View File

@ -1,7 +1,6 @@
namespace Webhooks.API.Services namespace Webhooks.API.Services;
public interface IIdentityService
{ {
public interface IIdentityService string GetUserIdentity();
{
string GetUserIdentity();
}
} }

View File

@ -1,12 +1,7 @@
using System.Collections.Generic; namespace Webhooks.API.Services;
using System.Threading.Tasks;
using Webhooks.API.Model;
namespace Webhooks.API.Services public interface IWebhooksRetriever
{ {
public interface IWebhooksRetriever
{
Task<IEnumerable<WebhookSubscription>> GetSubscriptionsOfType(WebhookType type); Task<IEnumerable<WebhookSubscription>> GetSubscriptionsOfType(WebhookType type);
}
} }

View File

@ -1,11 +1,6 @@
using System.Collections.Generic; namespace Webhooks.API.Services;
using System.Threading.Tasks;
using Webhooks.API.Model;
namespace Webhooks.API.Services public interface IWebhooksSender
{ {
public interface IWebhooksSender Task SendAll(IEnumerable<WebhookSubscription> receivers, WebhookData data);
{
Task SendAll(IEnumerable<WebhookSubscription> receivers, WebhookData data);
}
} }

View File

@ -1,21 +1,16 @@
 namespace Webhooks.API.Services;
using Microsoft.AspNetCore.Http;
using System;
namespace Webhooks.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;
}
} }
} }

View File

@ -1,23 +1,15 @@
using Microsoft.EntityFrameworkCore; namespace Webhooks.API.Services;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Webhooks.API.Infrastructure;
using Webhooks.API.Model;
namespace Webhooks.API.Services public class WebhooksRetriever : IWebhooksRetriever
{ {
public class WebhooksRetriever : IWebhooksRetriever private readonly WebhooksContext _db;
public WebhooksRetriever(WebhooksContext db)
{ {
private readonly WebhooksContext _db; _db = db;
public WebhooksRetriever(WebhooksContext db) }
{ public async Task<IEnumerable<WebhookSubscription>> GetSubscriptionsOfType(WebhookType type)
_db = db; {
} var data = await _db.Subscriptions.Where(s => s.Type == type).ToListAsync();
public async Task<IEnumerable<WebhookSubscription>> GetSubscriptionsOfType(WebhookType type) return data;
{
var data = await _db.Subscriptions.Where(s => s.Type == type).ToListAsync();
return data;
}
} }
} }

View File

@ -1,49 +1,38 @@
using Microsoft.Extensions.Logging; namespace Webhooks.API.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Webhooks.API.Model;
namespace Webhooks.API.Services public class WebhooksSender : IWebhooksSender
{ {
public class WebhooksSender : IWebhooksSender private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger _logger;
public WebhooksSender(IHttpClientFactory httpClientFactory, ILogger<WebhooksSender> logger)
{ {
private readonly IHttpClientFactory _httpClientFactory; _httpClientFactory = httpClientFactory;
private readonly ILogger _logger; _logger = logger;
public WebhooksSender(IHttpClientFactory httpClientFactory, ILogger<WebhooksSender> logger)
{
_httpClientFactory = httpClientFactory;
_logger = logger;
}
public async Task SendAll(IEnumerable<WebhookSubscription> receivers, WebhookData data)
{
var client = _httpClientFactory.CreateClient();
var json = JsonSerializer.Serialize(data);
var tasks = receivers.Select(r => OnSendData(r, json, client));
await Task.WhenAll(tasks.ToArray());
}
private Task OnSendData(WebhookSubscription subs, string jsonData, HttpClient client)
{
var request = new HttpRequestMessage()
{
RequestUri = new Uri(subs.DestUrl, UriKind.Absolute),
Method = HttpMethod.Post,
Content = new StringContent(jsonData, Encoding.UTF8, "application/json")
};
if (!string.IsNullOrWhiteSpace(subs.Token))
{
request.Headers.Add("X-eshop-whtoken", subs.Token);
}
_logger.LogDebug("Sending hook to {DestUrl} of type {Type}", subs.Type.ToString(), subs.Type.ToString());
return client.SendAsync(request);
}
} }
public async Task SendAll(IEnumerable<WebhookSubscription> receivers, WebhookData data)
{
var client = _httpClientFactory.CreateClient();
var json = JsonSerializer.Serialize(data);
var tasks = receivers.Select(r => OnSendData(r, json, client));
await Task.WhenAll(tasks.ToArray());
}
private Task OnSendData(WebhookSubscription subs, string jsonData, HttpClient client)
{
var request = new HttpRequestMessage()
{
RequestUri = new Uri(subs.DestUrl, UriKind.Absolute),
Method = HttpMethod.Post,
Content = new StringContent(jsonData, Encoding.UTF8, "application/json")
};
if (!string.IsNullOrWhiteSpace(subs.Token))
{
request.Headers.Add("X-eshop-whtoken", subs.Token);
}
_logger.LogDebug("Sending hook to {DestUrl} of type {Type}", subs.Type.ToString(), subs.Type.ToString());
return client.SendAsync(request);
}
} }

View File

@ -1,350 +1,318 @@
using Autofac; namespace Webhooks.API;
using Autofac.Extensions.DependencyInjection;
using Devspaces.Support;
using HealthChecks.UI.Client;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.ServiceBus;
using Microsoft.EntityFrameworkCore;
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus;
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ;
using Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus;
using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models;
using RabbitMQ.Client;
using System;
using System.Collections.Generic;
using System.Data.Common;
using System.IdentityModel.Tokens.Jwt;
using System.Reflection;
using System.Threading;
using Webhooks.API.Infrastructure;
using Webhooks.API.IntegrationEvents;
using Webhooks.API.Services;
namespace Webhooks.API public class Startup
{ {
public class Startup public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{ {
public IConfiguration Configuration { get; } Configuration = configuration;
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services
.AddAppInsight(Configuration)
.AddCustomRouting(Configuration)
.AddCustomDbContext(Configuration)
.AddSwagger(Configuration)
.AddCustomHealthCheck(Configuration)
.AddDevspaces()
.AddHttpClientServices(Configuration)
.AddIntegrationServices(Configuration)
.AddEventBus(Configuration)
.AddCustomAuthentication(Configuration)
.AddSingleton<IHttpContextAccessor, HttpContextAccessor>()
.AddTransient<IIdentityService, IdentityService>()
.AddTransient<IGrantUrlTesterService, GrantUrlTesterService>()
.AddTransient<IWebhooksRetriever, WebhooksRetriever>()
.AddTransient<IWebhooksSender, WebhooksSender>();
var container = new ContainerBuilder();
container.Populate(services);
return new AutofacServiceProvider(container.Build());
}
public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
{
loggerFactory.AddApplicationInsights(app.ApplicationServices, LogLevel.Trace);
var pathBase = Configuration["PATH_BASE"];
if (!string.IsNullOrEmpty(pathBase))
{
loggerFactory.CreateLogger("init").LogDebug("Using PATH BASE '{PathBase}'", pathBase);
app.UsePathBase(pathBase);
}
app.UseRouting();
app.UseCors("CorsPolicy");
ConfigureAuth(app);
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
endpoints.MapControllers();
endpoints.MapHealthChecks("/hc", new HealthCheckOptions()
{
Predicate = _ => true,
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
endpoints.MapHealthChecks("/liveness", new HealthCheckOptions
{
Predicate = r => r.Name.Contains("self")
});
});
app.UseSwagger()
.UseSwaggerUI(c =>
{
c.SwaggerEndpoint($"{ (!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty) }/swagger/v1/swagger.json", "Webhooks.API V1");
c.OAuthClientId("webhooksswaggerui");
c.OAuthAppName("WebHooks Service Swagger UI");
});
ConfigureEventBus(app);
}
protected virtual void ConfigureAuth(IApplicationBuilder app)
{
app.UseAuthentication();
app.UseAuthorization();
}
protected virtual void ConfigureEventBus(IApplicationBuilder app)
{
var eventBus = app.ApplicationServices.GetRequiredService<IEventBus>();
eventBus.Subscribe<ProductPriceChangedIntegrationEvent, ProductPriceChangedIntegrationEventHandler>();
eventBus.Subscribe<OrderStatusChangedToShippedIntegrationEvent, OrderStatusChangedToShippedIntegrationEventHandler>();
eventBus.Subscribe<OrderStatusChangedToPaidIntegrationEvent, OrderStatusChangedToPaidIntegrationEventHandler>();
}
} }
static class CustomExtensionMethods
public IServiceProvider ConfigureServices(IServiceCollection services)
{ {
public static IServiceCollection AddAppInsight(this IServiceCollection services, IConfiguration configuration) services
{ .AddAppInsight(Configuration)
services.AddApplicationInsightsTelemetry(configuration); .AddCustomRouting(Configuration)
services.AddApplicationInsightsKubernetesEnricher(); .AddCustomDbContext(Configuration)
.AddSwagger(Configuration)
.AddCustomHealthCheck(Configuration)
.AddDevspaces()
.AddHttpClientServices(Configuration)
.AddIntegrationServices(Configuration)
.AddEventBus(Configuration)
.AddCustomAuthentication(Configuration)
.AddSingleton<IHttpContextAccessor, HttpContextAccessor>()
.AddTransient<IIdentityService, IdentityService>()
.AddTransient<IGrantUrlTesterService, GrantUrlTesterService>()
.AddTransient<IWebhooksRetriever, WebhooksRetriever>()
.AddTransient<IWebhooksSender, WebhooksSender>();
return services; var container = new ContainerBuilder();
container.Populate(services);
return new AutofacServiceProvider(container.Build());
}
public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
{
loggerFactory.AddApplicationInsights(app.ApplicationServices, LogLevel.Trace);
var pathBase = Configuration["PATH_BASE"];
if (!string.IsNullOrEmpty(pathBase))
{
loggerFactory.CreateLogger("init").LogDebug("Using PATH BASE '{PathBase}'", pathBase);
app.UsePathBase(pathBase);
} }
public static IServiceCollection AddCustomRouting(this IServiceCollection services, IConfiguration configuration)
app.UseRouting();
app.UseCors("CorsPolicy");
ConfigureAuth(app);
app.UseEndpoints(endpoints =>
{ {
services.AddControllers(options => endpoints.MapDefaultControllerRoute();
endpoints.MapControllers();
endpoints.MapHealthChecks("/hc", new HealthCheckOptions()
{ {
options.Filters.Add(typeof(HttpGlobalExceptionFilter)); Predicate = _ => true,
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
endpoints.MapHealthChecks("/liveness", new HealthCheckOptions
{
Predicate = r => r.Name.Contains("self")
});
});
app.UseSwagger()
.UseSwaggerUI(c =>
{
c.SwaggerEndpoint($"{ (!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty) }/swagger/v1/swagger.json", "Webhooks.API V1");
c.OAuthClientId("webhooksswaggerui");
c.OAuthAppName("WebHooks Service Swagger UI");
}); });
services.AddCors(options => ConfigureEventBus(app);
}
protected virtual void ConfigureAuth(IApplicationBuilder app)
{
app.UseAuthentication();
app.UseAuthorization();
}
protected virtual void ConfigureEventBus(IApplicationBuilder app)
{
var eventBus = app.ApplicationServices.GetRequiredService<IEventBus>();
eventBus.Subscribe<ProductPriceChangedIntegrationEvent, ProductPriceChangedIntegrationEventHandler>();
eventBus.Subscribe<OrderStatusChangedToShippedIntegrationEvent, OrderStatusChangedToShippedIntegrationEventHandler>();
eventBus.Subscribe<OrderStatusChangedToPaidIntegrationEvent, OrderStatusChangedToPaidIntegrationEventHandler>();
}
}
static class CustomExtensionMethods
{
public static IServiceCollection AddAppInsight(this IServiceCollection services, IConfiguration configuration)
{
services.AddApplicationInsightsTelemetry(configuration);
services.AddApplicationInsightsKubernetesEnricher();
return services;
}
public static IServiceCollection AddCustomRouting(this IServiceCollection services, IConfiguration configuration)
{
services.AddControllers(options =>
{
options.Filters.Add(typeof(HttpGlobalExceptionFilter));
});
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy",
builder => builder
.SetIsOriginAllowed((host) => true)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
});
return services;
}
public static IServiceCollection AddCustomDbContext(this IServiceCollection services, IConfiguration configuration)
{
services.AddEntityFrameworkSqlServer()
.AddDbContext<WebhooksContext>(options =>
{
options.UseSqlServer(configuration["ConnectionString"],
sqlServerOptionsAction: sqlOptions =>
{
sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name);
//Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency
sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null);
});
});
return services;
}
public static IServiceCollection AddSwagger(this IServiceCollection services, IConfiguration configuration)
{
services.AddSwaggerGen(options =>
{
options.DescribeAllEnumsAsStrings();
options.SwaggerDoc("v1", new OpenApiInfo
{ {
options.AddPolicy("CorsPolicy", Title = "eShopOnContainers - Webhooks HTTP API",
builder => builder Version = "v1",
.SetIsOriginAllowed((host) => true) Description = "The Webhooks Microservice HTTP API. This is a simple webhooks CRUD registration entrypoint"
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
}); });
return services; options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
}
public static IServiceCollection AddCustomDbContext(this IServiceCollection services, IConfiguration configuration)
{
services.AddEntityFrameworkSqlServer()
.AddDbContext<WebhooksContext>(options =>
{ {
options.UseSqlServer(configuration["ConnectionString"], Type = SecuritySchemeType.OAuth2,
sqlServerOptionsAction: sqlOptions => Flows = new OpenApiOAuthFlows()
{
sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name);
//Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency
sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null);
});
});
return services;
}
public static IServiceCollection AddSwagger(this IServiceCollection services, IConfiguration configuration)
{
services.AddSwaggerGen(options =>
{
options.DescribeAllEnumsAsStrings();
options.SwaggerDoc("v1", new OpenApiInfo
{ {
Title = "eShopOnContainers - Webhooks HTTP API", Implicit = new OpenApiOAuthFlow()
Version = "v1",
Description = "The Webhooks Microservice HTTP API. This is a simple webhooks CRUD registration entrypoint"
});
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>()
{ {
AuthorizationUrl = new Uri($"{configuration.GetValue<string>("IdentityUrlExternal")}/connect/authorize"), { "webhooks", "Webhooks API" }
TokenUrl = new Uri($"{configuration.GetValue<string>("IdentityUrlExternal")}/connect/token"),
Scopes = new Dictionary<string, string>()
{
{ "webhooks", "Webhooks API" }
}
} }
} }
}); }
options.OperationFilter<AuthorizeCheckOperationFilter>();
}); });
return services; options.OperationFilter<AuthorizeCheckOperationFilter>();
} });
public static IServiceCollection AddEventBus(this IServiceCollection services, IConfiguration configuration)
{
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, return services;
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<OrderStatusChangedToShippedIntegrationEventHandler>();
services.AddTransient<OrderStatusChangedToPaidIntegrationEventHandler>();
return services;
}
public static IServiceCollection AddCustomHealthCheck(this IServiceCollection services, IConfiguration configuration)
{
var accountName = configuration.GetValue<string>("AzureStorageAccountName");
var accountKey = configuration.GetValue<string>("AzureStorageAccountKey");
var hcBuilder = services.AddHealthChecks();
hcBuilder
.AddCheck("self", () => HealthCheckResult.Healthy())
.AddSqlServer(
configuration["ConnectionString"],
name: "WebhooksApiDb-check",
tags: new string[] { "webhooksdb" });
return services;
}
public static IServiceCollection AddHttpClientServices(this IServiceCollection services, IConfiguration configuration)
{
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddHttpClient("extendedhandlerlifetime").SetHandlerLifetime(Timeout.InfiniteTimeSpan);
//add http client services
services.AddHttpClient("GrantClient")
.SetHandlerLifetime(TimeSpan.FromMinutes(5))
.AddDevspacesSupport();
return services;
}
public static IServiceCollection AddIntegrationServices(this IServiceCollection services, IConfiguration configuration)
{
services.AddTransient<Func<DbConnection, IIntegrationEventLogService>>(
sp => (DbConnection c) => new IntegrationEventLogService(c));
if (configuration.GetValue<bool>("AzureServiceBusEnabled"))
{
services.AddSingleton<IServiceBusPersisterConnection>(sp =>
{
var serviceBusConnection = new ServiceBusConnectionStringBuilder(configuration["EventBusConnection"]);
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);
});
}
return services;
}
public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration)
{
// 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 = "webhooks";
});
return services;
}
} }
} public static IServiceCollection AddEventBus(this IServiceCollection services, IConfiguration configuration)
{
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<OrderStatusChangedToShippedIntegrationEventHandler>();
services.AddTransient<OrderStatusChangedToPaidIntegrationEventHandler>();
return services;
}
public static IServiceCollection AddCustomHealthCheck(this IServiceCollection services, IConfiguration configuration)
{
var accountName = configuration.GetValue<string>("AzureStorageAccountName");
var accountKey = configuration.GetValue<string>("AzureStorageAccountKey");
var hcBuilder = services.AddHealthChecks();
hcBuilder
.AddCheck("self", () => HealthCheckResult.Healthy())
.AddSqlServer(
configuration["ConnectionString"],
name: "WebhooksApiDb-check",
tags: new string[] { "webhooksdb" });
return services;
}
public static IServiceCollection AddHttpClientServices(this IServiceCollection services, IConfiguration configuration)
{
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddHttpClient("extendedhandlerlifetime").SetHandlerLifetime(Timeout.InfiniteTimeSpan);
//add http client services
services.AddHttpClient("GrantClient")
.SetHandlerLifetime(TimeSpan.FromMinutes(5))
.AddDevspacesSupport();
return services;
}
public static IServiceCollection AddIntegrationServices(this IServiceCollection services, IConfiguration configuration)
{
services.AddTransient<Func<DbConnection, IIntegrationEventLogService>>(
sp => (DbConnection c) => new IntegrationEventLogService(c));
if (configuration.GetValue<bool>("AzureServiceBusEnabled"))
{
services.AddSingleton<IServiceBusPersisterConnection>(sp =>
{
var serviceBusConnection = new ServiceBusConnectionStringBuilder(configuration["EventBusConnection"]);
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);
});
}
return services;
}
public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration)
{
// 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 = "webhooks";
});
return services;
}
}