diff --git a/.DS_Store b/.DS_Store index fd0484d..f601b59 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c53cd3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +################################################################################ +# This .gitignore file was automatically created by Microsoft(R) Visual Studio. +################################################################################ + +/src/.vs/GOC.ApiGateway/v15/sqlite3/storage.ide diff --git a/src/GOC.ApiGateway/AppSettings.cs b/src/GOC.ApiGateway/AppSettings.cs new file mode 100644 index 0000000..f6c6570 --- /dev/null +++ b/src/GOC.ApiGateway/AppSettings.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using Microphone.Consul; + +namespace GOC.ApiGateway +{ + public class AppSettings + { + public CircuitBreakerSettings CircuitBreaker {get;set;} + public WaitAndRetrySettings WaitAndRetry { get; set; } + public ConsulOptions Consul { get; set; } + public IdentitySettings Identity { get; set; } + public RabbitMQSettings Rabbit { get; set; } + } + public class CircuitBreakerSettings + { + public double FailureThreshold { get; set; } + public int SamplingDurationInSeconds { get; set; } + public int MinimumThroughput { get; set; } + public int DurationOfBreakInSeconds { get; set; } + } + public class WaitAndRetrySettings + { + public int RetryAttempts { get; set; } + } + public class IdentitySettings + { + public string Authority { get; set; } + public string ApiName { get; set; } + public string ApiSecret { get; set; } + public string TokenEndpoint { get; set; } + public string ApiClientId { get; set; } + public string ApiClientSecret { get; set; } + public string TokenEndpointUrl + { + get => $"{Authority}/{TokenEndpoint}"; + } + public IEnumerable Resources { get; set; } + } + //TODO do this in json config file + public class ApiResource + { + public string ResourceFriendlyName { get; set; } + public string ResourceName { get; set; } + } + public class RabbitMQSettings + { + public string Host { get; set; } + } + +} diff --git a/src/GOC.ApiGateway/Controllers/BaseController.cs b/src/GOC.ApiGateway/Controllers/BaseController.cs new file mode 100644 index 0000000..9d23b0a --- /dev/null +++ b/src/GOC.ApiGateway/Controllers/BaseController.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace GOC.ApiGateway.Controllers +{ + public class BaseController : Controller where T : class + { + protected ILogger Logger; + + public BaseController(ILoggerFactory loggerFactory) + { + Logger = loggerFactory.CreateLogger(); + } + } +} diff --git a/src/GOC.ApiGateway/Controllers/CompaniesController.cs b/src/GOC.ApiGateway/Controllers/CompaniesController.cs new file mode 100644 index 0000000..1662bb7 --- /dev/null +++ b/src/GOC.ApiGateway/Controllers/CompaniesController.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; +using GOC.ApiGateway.Dtos; +using GOC.ApiGateway.Interfaces; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +// For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 + +namespace GOC.ApiGateway.Controllers +{ + [Authorize] + [Route("api/[controller]")] + public class CompaniesController : BaseController + { + private readonly ICompanyService _companyService; + public CompaniesController(ICompanyService company, ILoggerFactory loggerFactory) : base(loggerFactory) + { + _companyService = company; + } + + [HttpPost] + public async Task Post([FromBody] CompanyPostDto company) + { + var result = await _companyService.CreateCompanyAsync(company); + return Ok(result.Value); + } + } +} diff --git a/src/GOC.ApiGateway/Controllers/InventoriesController.cs b/src/GOC.ApiGateway/Controllers/InventoriesController.cs new file mode 100644 index 0000000..4bcf79d --- /dev/null +++ b/src/GOC.ApiGateway/Controllers/InventoriesController.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using GOC.ApiGateway.Dtos; +using GOC.ApiGateway.Interfaces; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +// For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 + +namespace GOC.ApiGateway.Controllers +{ + [Authorize] + [Route("api/companies/{companyId}/[controller]")] + public class InventoriesController : BaseController + { + private IInventoryService _inventoryService; + public InventoriesController(IInventoryService inventoryService, ILoggerFactory loggerFactory) : base(loggerFactory) + { + _inventoryService = inventoryService; + } + + [HttpGet] + public async Task Get() + { + return View(); + } + + [HttpGet("{id}")] + public async Task Get(Guid id) + { + return View(); + } + + [HttpPost] + public async Task Post(Guid companyId, InventoryPostDto inventory) + { + inventory.CompanyId = companyId; + var result = await _inventoryService.CreateInventoryAsync(inventory); + return Ok(result.Value); + } + } +} diff --git a/src/GOC.ApiGateway/Controllers/ValuesController.cs b/src/GOC.ApiGateway/Controllers/ValuesController.cs deleted file mode 100644 index efac1ef..0000000 --- a/src/GOC.ApiGateway/Controllers/ValuesController.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; - -namespace GOC.ApiGateway.Controllers -{ - [Route("api/[controller]")] - public class ValuesController : Controller - { - // GET api/values - [HttpGet] - public IEnumerable Get() - { - return new string[] { "value1", "value2" }; - } - - // GET api/values/5 - [HttpGet("{id}")] - public string Get(int id) - { - return "value"; - } - - // POST api/values - [HttpPost] - public void Post([FromBody]string value) - { - } - - // PUT api/values/5 - [HttpPut("{id}")] - public void Put(int id, [FromBody]string value) - { - } - - // DELETE api/values/5 - [HttpDelete("{id}")] - public void Delete(int id) - { - } - } -} diff --git a/src/GOC.ApiGateway/Controllers/VendorsController.cs b/src/GOC.ApiGateway/Controllers/VendorsController.cs new file mode 100644 index 0000000..a4408fa --- /dev/null +++ b/src/GOC.ApiGateway/Controllers/VendorsController.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + +// For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 + +namespace GOC.ApiGateway.Controllers +{ + public class VendorsController : Controller + { + // GET: // + public IActionResult Index() + { + return View(); + } + } +} diff --git a/src/GOC.ApiGateway/Dtos/AddressDto.cs b/src/GOC.ApiGateway/Dtos/AddressDto.cs new file mode 100644 index 0000000..322c6a7 --- /dev/null +++ b/src/GOC.ApiGateway/Dtos/AddressDto.cs @@ -0,0 +1,17 @@ +using System; +namespace GOC.ApiGateway.Dtos +{ + public class AddressDto + { + public string AddressLine1 { get; set; } + + public string AddressLine2 { get; set; } + + public string City { get; set; } + + public string State { get; set; } + + public string ZipCode { get; set; } + + } +} diff --git a/src/GOC.ApiGateway/Dtos/CompanyDto.cs b/src/GOC.ApiGateway/Dtos/CompanyDto.cs new file mode 100644 index 0000000..ca6b906 --- /dev/null +++ b/src/GOC.ApiGateway/Dtos/CompanyDto.cs @@ -0,0 +1,16 @@ +using System; +namespace GOC.ApiGateway.Dtos +{ + public class CompanyDto + { + public Guid Id { get; set; } + + public AddressDto Address { get; set; } + + public string Name { get; set; } + + public int UserId { get; set; } + + public string PhoneNumber { get; set; } + } +} diff --git a/src/GOC.ApiGateway/Dtos/CompanyPostDto.cs b/src/GOC.ApiGateway/Dtos/CompanyPostDto.cs new file mode 100644 index 0000000..a1b1a87 --- /dev/null +++ b/src/GOC.ApiGateway/Dtos/CompanyPostDto.cs @@ -0,0 +1,13 @@ +namespace GOC.ApiGateway.Dtos +{ + public class CompanyPostDto + { + public AddressDto Address { get; set; } + + public string Name { get; set; } + + public int UserId { get; set; } + + public string PhoneNumber { get; set; } + } +} diff --git a/src/GOC.ApiGateway/Dtos/InventoryDto.cs b/src/GOC.ApiGateway/Dtos/InventoryDto.cs new file mode 100644 index 0000000..5a6e18f --- /dev/null +++ b/src/GOC.ApiGateway/Dtos/InventoryDto.cs @@ -0,0 +1,14 @@ +using System; +namespace GOC.ApiGateway.Dtos +{ + public class InventoryDto + { + public Guid Id { get; set; } + + public string Name { get; set; } + + public Guid CompanyId { get; set; } + + public int UserId { get; set; } + } +} diff --git a/src/GOC.ApiGateway/Dtos/InventoryPostDto.cs b/src/GOC.ApiGateway/Dtos/InventoryPostDto.cs new file mode 100644 index 0000000..2f85daf --- /dev/null +++ b/src/GOC.ApiGateway/Dtos/InventoryPostDto.cs @@ -0,0 +1,12 @@ +using System; +namespace GOC.ApiGateway.Dtos +{ + public class InventoryPostDto + { + public string Name { get; set; } + + public Guid CompanyId { get; set; } + + public int UserId { get; set; } + } +} diff --git a/src/GOC.ApiGateway/Enums/ServiceNameTypes.cs b/src/GOC.ApiGateway/Enums/ServiceNameTypes.cs new file mode 100644 index 0000000..48b7acc --- /dev/null +++ b/src/GOC.ApiGateway/Enums/ServiceNameTypes.cs @@ -0,0 +1,8 @@ +namespace GOC.ApiGateway.Enums +{ + public enum ServiceNameTypes + { + InventoryService, + CrmService + } +} diff --git a/src/GOC.ApiGateway/Extensions/ConsulExtension.cs b/src/GOC.ApiGateway/Extensions/ConsulExtension.cs new file mode 100644 index 0000000..6fe08c9 --- /dev/null +++ b/src/GOC.ApiGateway/Extensions/ConsulExtension.cs @@ -0,0 +1,56 @@ +using System; +using System.Linq; +using Consul; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace GOC.ApiGateway.Extensions +{ + public static class ConsulExtension + { + //public static IApplicationBuilder RegisterWithConsul(this IApplicationBuilder app, + //IApplicationLifetime lifetime) + //{ + // // Retrieve Consul client from DI + // var consulClient = app.ApplicationServices + // .GetRequiredService(); + // var consulConfig = app.ApplicationServices + // .GetRequiredService>(); + // // Setup logger + // var loggingFactory = app.ApplicationServices + // .GetRequiredService(); + // var logger = loggingFactory.CreateLogger(); + + // // Get server IP address + // var features = app.Properties["server.Features"] as FeatureCollection; + // var addresses = features.Get(); + // var address = addresses.Addresses.First(); + + // // Register service with consul + // var uri = new Uri(address); + // var registration = new AgentServiceRegistration() + // { + // ID = $"{consulConfig.Value.ServiceID}-{uri.Port}", + // Name = consulConfig.Value.ServiceName, + // Address = $"{uri.Scheme}://{uri.Host}", + // Port = uri.Port, + // Tags = new[] { "Students", "Courses", "School" } + // }; + + // logger.LogInformation("Registering with Consul"); + // consulClient.Agent.ServiceDeregister(registration.ID).Wait(); + // consulClient.Agent.ServiceRegister(registration).Wait(); + + // lifetime.ApplicationStopping.Register(() => { + // logger.LogInformation("Deregistering from Consul"); + // consulClient.Agent.ServiceDeregister(registration.ID).Wait(); + // }); + // return app; + //} + } +} diff --git a/src/GOC.ApiGateway/Extensions/HttpExtensions.cs b/src/GOC.ApiGateway/Extensions/HttpExtensions.cs new file mode 100644 index 0000000..2c880be --- /dev/null +++ b/src/GOC.ApiGateway/Extensions/HttpExtensions.cs @@ -0,0 +1,23 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; + +namespace GOC.ApiGateway.Extensions +{ + public static class HttpExtensions + { + public static bool IsAjaxRequest(this HttpRequest request) + { + if (request == null) throw new ArgumentNullException("request"); + if (request.Headers != null) return request.Headers["X-Requested-With"] == "XMLHttpRequest"; + return false; + } + + public static async Task ReadAsAsync(this HttpResponseMessage response) + { + return JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + } + } +} diff --git a/src/GOC.ApiGateway/GOC.ApiGateway.csproj b/src/GOC.ApiGateway/GOC.ApiGateway.csproj index 60ee45b..8134091 100644 --- a/src/GOC.ApiGateway/GOC.ApiGateway.csproj +++ b/src/GOC.ApiGateway/GOC.ApiGateway.csproj @@ -4,12 +4,38 @@ netcoreapp2.0 + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/GOC.ApiGateway/Handlers/TokenDelegateHandler.cs b/src/GOC.ApiGateway/Handlers/TokenDelegateHandler.cs new file mode 100644 index 0000000..8179724 --- /dev/null +++ b/src/GOC.ApiGateway/Handlers/TokenDelegateHandler.cs @@ -0,0 +1,23 @@ +using System; +using System.Threading.Tasks; +using IdentityModel.Client; + +namespace GOC.ApiGateway.Handlers +{ + public class TokenDelegateHandler + { + //public async Task DelegateAsync(string userToken, string authority, DownstreamClient downstreamClient) + //{ + // var payload = new + // { + // token = userToken + // }; + + // // create token client + // var client = new TokenClient($"{authority}/connect/token", downstreamClient.ApiName, downstreamClient.ApiSecret); + + + // return await client.RequestCustomGrantAsync("delegation", "api2", payload); + //} + } +} diff --git a/src/GOC.ApiGateway/Helpers/JsonContent.cs b/src/GOC.ApiGateway/Helpers/JsonContent.cs new file mode 100644 index 0000000..2001080 --- /dev/null +++ b/src/GOC.ApiGateway/Helpers/JsonContent.cs @@ -0,0 +1,13 @@ +using System.Net.Http; +using System.Text; +using Newtonsoft.Json; + +namespace GOC.ApiGateway.Helpers +{ + public class JsonContent : StringContent + { + public JsonContent(object obj): base (JsonConvert.SerializeObject(obj), Encoding.UTF8, "application/json") + { + } + } +} diff --git a/src/GOC.ApiGateway/HttpClientHelper/HttpTokenAuthorizationContext.cs b/src/GOC.ApiGateway/HttpClientHelper/HttpTokenAuthorizationContext.cs new file mode 100644 index 0000000..504fb64 --- /dev/null +++ b/src/GOC.ApiGateway/HttpClientHelper/HttpTokenAuthorizationContext.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace GOC.ApiGateway.HttpClientHelper +{ + public class HttpTokenAuthorizationContext : IHttpTokenAuthorizationContext + { + private readonly Func _httpContextAccessor; + private readonly Func>> _bearerTokenAccessor; + private readonly Func> _accessTokenAccessor; + + public HttpTokenAuthorizationContext(Func httpContextAccessor, + Func>> bearerTokenAccessor, + Func> accessTokenAccessor) + { + _httpContextAccessor = httpContextAccessor; + _bearerTokenAccessor = bearerTokenAccessor; + _accessTokenAccessor = accessTokenAccessor; + } + + public string AccessToken => _accessTokenAccessor(_httpContextAccessor()).Result; + + public IEnumerable BearerTokens => _bearerTokenAccessor(_httpContextAccessor()).Result; + } +} diff --git a/src/GOC.ApiGateway/Interfaces/ICompanyRepository.cs b/src/GOC.ApiGateway/Interfaces/ICompanyRepository.cs new file mode 100644 index 0000000..ceb07ee --- /dev/null +++ b/src/GOC.ApiGateway/Interfaces/ICompanyRepository.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using CSharpFunctionalExtensions; +using GOC.ApiGateway.Dtos; + +namespace GOC.ApiGateway.Interfaces +{ + public interface ICompanyRepository + { + Task> CreateCompanyAsync(CompanyPostDto company); + } +} diff --git a/src/GOC.ApiGateway/Interfaces/ICompanyService.cs b/src/GOC.ApiGateway/Interfaces/ICompanyService.cs new file mode 100644 index 0000000..0a332fd --- /dev/null +++ b/src/GOC.ApiGateway/Interfaces/ICompanyService.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using CSharpFunctionalExtensions; +using GOC.ApiGateway.Dtos; + +namespace GOC.ApiGateway.Interfaces +{ + public interface ICompanyService + { + Task> CreateCompanyAsync(CompanyPostDto company); + } +} diff --git a/src/GOC.ApiGateway/Interfaces/IGocHttpClient.cs b/src/GOC.ApiGateway/Interfaces/IGocHttpClient.cs new file mode 100644 index 0000000..cc7f048 --- /dev/null +++ b/src/GOC.ApiGateway/Interfaces/IGocHttpClient.cs @@ -0,0 +1,14 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; + +namespace GOC.ApiGateway +{ + public interface IGocHttpClient + { + Task GetAsync(string serviceName, ApiResource resource, string relativeUri); + Task PostAsync(string serviceName, ApiResource resource, string relativeUri, HttpContent content); + Task PutAsync(string serviceName, ApiResource resource, string relativeUri, HttpContent content); + Task DeleteAsync(string serviceName, ApiResource resource, Uri relativeUri); + } +} diff --git a/src/GOC.ApiGateway/Interfaces/IHttpTokenAuthorizationContext.cs b/src/GOC.ApiGateway/Interfaces/IHttpTokenAuthorizationContext.cs new file mode 100644 index 0000000..29753f0 --- /dev/null +++ b/src/GOC.ApiGateway/Interfaces/IHttpTokenAuthorizationContext.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace GOC.ApiGateway +{ + public interface IHttpTokenAuthorizationContext + { + string AccessToken { get; } + + IEnumerable BearerTokens { get; } + } +} diff --git a/src/GOC.ApiGateway/Interfaces/IInventoryRepository.cs b/src/GOC.ApiGateway/Interfaces/IInventoryRepository.cs new file mode 100644 index 0000000..f417188 --- /dev/null +++ b/src/GOC.ApiGateway/Interfaces/IInventoryRepository.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using CSharpFunctionalExtensions; +using GOC.ApiGateway.Dtos; + +namespace GOC.ApiGateway.Interfaces +{ + public interface IInventoryRepository + { + Task> CreateInventoryAsync(InventoryPostDto inventory); + } +} diff --git a/src/GOC.ApiGateway/Interfaces/IInventoryService.cs b/src/GOC.ApiGateway/Interfaces/IInventoryService.cs new file mode 100644 index 0000000..fc45c99 --- /dev/null +++ b/src/GOC.ApiGateway/Interfaces/IInventoryService.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using CSharpFunctionalExtensions; +using GOC.ApiGateway.Dtos; + +namespace GOC.ApiGateway.Interfaces +{ + public interface IInventoryService + { + Task> CreateInventoryAsync(InventoryPostDto inventory); + } +} diff --git a/src/GOC.ApiGateway/Program.cs b/src/GOC.ApiGateway/Program.cs index 8902185..b1272ad 100644 --- a/src/GOC.ApiGateway/Program.cs +++ b/src/GOC.ApiGateway/Program.cs @@ -13,7 +13,7 @@ public static void Main(string[] args) public static IWebHost BuildWebHost(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup() - .UseUrls("http://*:5001") + .UseUrls("http://*:5010") .Build(); } } diff --git a/src/GOC.ApiGateway/Repositories/CrmService/CompanyRepository.cs b/src/GOC.ApiGateway/Repositories/CrmService/CompanyRepository.cs new file mode 100644 index 0000000..d8ea5b7 --- /dev/null +++ b/src/GOC.ApiGateway/Repositories/CrmService/CompanyRepository.cs @@ -0,0 +1,47 @@ +using System.Linq; +using System.Threading.Tasks; +using CSharpFunctionalExtensions; +using GOC.ApiGateway.Dtos; +using GOC.ApiGateway.Enums; +using GOC.ApiGateway.Extensions; +using GOC.ApiGateway.Helpers; +using GOC.ApiGateway.Interfaces; +using Microsoft.Extensions.Logging; +using Polly; + +namespace GOC.ApiGateway.Repositories +{ + public class CompanyRepository : RepositoryBase, ICompanyRepository + { + private ServiceNameTypes _serviceName = ServiceNameTypes.CrmService; + + private readonly ApiResource _apiTargetResource = Startup.AppSettings.Identity.Resources.Single(x => x.ResourceFriendlyName == "Crm.API"); + + private readonly Policy _crmRetryPolicy; + + private const string BaseUrl = "/api/companies"; + + public CompanyRepository(RetryPolicies retryPolicies, IGocHttpClient httpClient, ILoggerFactory loggerFactory) : base(httpClient, loggerFactory) + { + _crmRetryPolicy = retryPolicies.CrmServiceCircuitBreaker; + } + + public async Task> CreateCompanyAsync(CompanyPostDto company) + { + var response = await _crmRetryPolicy.ExecuteAsync(() => + HttpClient.PostAsync(ServiceNameTypes.CrmService.ToString(), _apiTargetResource, BaseUrl, new JsonContent(company))); + + if (response.IsSuccessStatusCode) + { + var projectsServiceResponse = await response.ReadAsAsync(); + return Result.Ok(projectsServiceResponse); + } + + //await HttpResponseExtensionMethods.ThrowIfClientErrorAsync(response); + //await HttpResponseExtensionMethods.LogHttpResponseAsync(response, Logger); + + var errorMessage = await response.Content.ReadAsStringAsync(); + return Result.Fail(errorMessage); + } + } +} diff --git a/src/GOC.ApiGateway/Repositories/HttpClientWrapper.cs b/src/GOC.ApiGateway/Repositories/HttpClientWrapper.cs new file mode 100644 index 0000000..6bbb166 --- /dev/null +++ b/src/GOC.ApiGateway/Repositories/HttpClientWrapper.cs @@ -0,0 +1,144 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using IdentityModel.Client; + +namespace GOC.ApiGateway +{ + public class HttpClientWrapper : IGocHttpClient + { + private readonly HttpClient _httpClient; + private readonly Func _relativeUriResolver; + private IHttpTokenAuthorizationContext _authContext { get; set; } + + public HttpClientWrapper(HttpClient client, IHttpTokenAuthorizationContext authContext,Func serviceUriResolver = null) + { + _authContext = authContext; + _httpClient = client; + _relativeUriResolver = serviceUriResolver ?? ((sn, s) => new Uri(s)); + } + + /// Send a GET request to the specified Uri as an asynchronous operation. + /// + /// Returns .The task object representing the asynchronous + /// operation. + /// + /// Name of consul service which is used to get address of service. + /// The relative Uri the request is sent to. + /// The was null. + public async Task GetAsync(string serviceName, ApiResource resource, string relativeUri) + { + return await GetAsync(serviceName, resource ,relativeUri, CancellationToken.None); + } + + /// Send a POST request to the specified Uri as an asynchronous operation. + /// + /// Returns .The task object representing the asynchronous + /// operation. + /// + /// Name of consul service which is used to get address of service. + /// The relative Uri the request is sent to. + /// The HTTP request content sent to the server. + /// The was null. + public async Task PostAsync(string serviceName, ApiResource resource, string relativeUri, HttpContent content) + { + return await PostAsync(serviceName, resource, relativeUri, content, CancellationToken.None); + } + + /// Send a PUT request to the specified Uri as an asynchronous operation. + /// + /// Returns .The task object representing the asynchronous + /// operation. + /// + /// Name of consul service which is used to get address of service. + /// The relative Uri the request is sent to. + /// The HTTP request content sent to the server. + /// The was null. + public async Task PutAsync(string serviceName, ApiResource resource, string relativeUri, HttpContent content) + { + return await PutAsync(serviceName, resource, relativeUri, content, CancellationToken.None); + } + + /// Send a DELETE request to the specified Uri as an asynchronous operation. + /// + /// Returns .The task object representing the asynchronous + /// operation. + /// + /// Name of consul service which is used to get address of service. + /// The relative Uri the request is sent to. + /// The was null. + /// + /// The request message was already sent by the + /// instance. + /// + public async Task DeleteAsync(string serviceName, ApiResource resource, Uri relativeUri) + { + return await DeleteAsync(serviceName, resource, relativeUri.ToString(), CancellationToken.None); + } + + async Task PostAsync(string serviceName, ApiResource resource, string relativeUri, HttpContent content, CancellationToken cancellationToken) + { + await SetAccessToken(resource); + var request = CreateRequestMessage(HttpMethod.Post, serviceName, relativeUri); + request.Content = content; + return await _httpClient.SendAsync(request, cancellationToken); + } + + async Task PutAsync(string serviceName, ApiResource resource, string relativeUri, HttpContent content, CancellationToken cancellationToken) + { + await SetAccessToken(resource); + var request = CreateRequestMessage(HttpMethod.Put, serviceName, relativeUri); + request.Content = content; + return await _httpClient.SendAsync(request, cancellationToken); + } + + async Task DeleteAsync(string serviceName, ApiResource resource, string relativeUri, CancellationToken cancellationToken) + { + await SetAccessToken(resource); + var request = CreateRequestMessage(HttpMethod.Delete, serviceName, relativeUri); + return await _httpClient.SendAsync(request, cancellationToken); + } + + async Task GetAsync(string serviceName, ApiResource resource, string relativeUri, CancellationToken cancellationToken) + { + await SetAccessToken(resource); + var request = CreateRequestMessage(HttpMethod.Get, serviceName, relativeUri); + return await _httpClient.SendAsync(request, cancellationToken); + } + + Uri ResolveRelativeUri(string serviceName, string relativeUri) + { + return _relativeUriResolver(serviceName, relativeUri); + } + + async Task DelegateAsync(string userToken, string clientId, string clientSecret, string resourceName) + { + var payload = new + { + token = userToken + }; + + // create token client + var client = new TokenClient(Startup.AppSettings.Identity.TokenEndpointUrl, clientId, clientSecret); + + // send custom grant to token endpoint, return response + var result = await client.RequestCustomGrantAsync("delegation", resourceName, payload); + return result; + } + + async Task SetAccessToken(ApiResource resource) + { + var delegateToken = await DelegateAsync(_authContext.AccessToken, Startup.AppSettings.Identity.ApiClientId, Startup.AppSettings.Identity.ApiClientSecret, resource.ResourceName); + _httpClient.SetBearerToken(delegateToken.AccessToken); + } + + HttpRequestMessage CreateRequestMessage(HttpMethod method, string serviceName, string relativeUri) + { + var uri = _relativeUriResolver.Invoke(serviceName, relativeUri); + var request = new HttpRequestMessage(method, uri); + return request; + } + + } +} diff --git a/src/GOC.ApiGateway/Repositories/InventoryService/InventoryRepository.cs b/src/GOC.ApiGateway/Repositories/InventoryService/InventoryRepository.cs new file mode 100644 index 0000000..f1c3033 --- /dev/null +++ b/src/GOC.ApiGateway/Repositories/InventoryService/InventoryRepository.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Polly; +using CSharpFunctionalExtensions; +using GOC.ApiGateway.Extensions; +using GOC.ApiGateway.Enums; +using System.Linq; +using GOC.ApiGateway.Interfaces; +using GOC.ApiGateway.Dtos; +using GOC.ApiGateway.Helpers; + +namespace GOC.ApiGateway.Repositories +{ + public class InventoryRepository : RepositoryBase, IInventoryRepository + { + private ServiceNameTypes _serviceName = ServiceNameTypes.InventoryService; + + private readonly ApiResource _apiTargetResource = Startup.AppSettings.Identity.Resources.Single(x => x.ResourceFriendlyName == "Inventory.API"); + + private readonly Policy _inventoryRetryPolicy; + + public InventoryRepository(RetryPolicies retryPolicies, IGocHttpClient client, ILoggerFactory loggerFactory) : base(client, loggerFactory) + { + _inventoryRetryPolicy = retryPolicies.InventoryServiceCircuitBreaker; + + } + + private const string BaseUrl = "/api/inventories"; + + + public async Task>> GetAllInventoriesAsync() + { + var response = await _inventoryRetryPolicy.ExecuteAsync(() => HttpClient.GetAsync(ServiceNameTypes.InventoryService.ToString(), _apiTargetResource, BaseUrl)); + + if (response.IsSuccessStatusCode) + { + var projectsServiceResponse = await response.ReadAsAsync>(); + return Result.Ok(projectsServiceResponse); + } + + //await HttpResponseExtensionMethods.ThrowIfClientErrorAsync(response); + //await HttpResponseExtensionMethods.LogHttpResponseAsync(response, Logger); + + var errorMessage = await response.Content.ReadAsStringAsync(); + return Result.Fail>(errorMessage); + } + + public async Task> CreateInventoryAsync(InventoryPostDto inventory) + { + var response = await _inventoryRetryPolicy.ExecuteAsync(() => + HttpClient.PostAsync(ServiceNameTypes.InventoryService.ToString(), _apiTargetResource, BaseUrl, new JsonContent(inventory))); + + if (response.IsSuccessStatusCode) + { + var projectsServiceResponse = await response.ReadAsAsync(); + return Result.Ok(projectsServiceResponse); + } + + //await HttpResponseExtensionMethods.ThrowIfClientErrorAsync(response); + //await HttpResponseExtensionMethods.LogHttpResponseAsync(response, Logger); + + var errorMessage = await response.Content.ReadAsStringAsync(); + return Result.Fail(errorMessage); + } + } +} diff --git a/src/GOC.ApiGateway/Repositories/RepositoryBase.cs b/src/GOC.ApiGateway/Repositories/RepositoryBase.cs new file mode 100644 index 0000000..14641ac --- /dev/null +++ b/src/GOC.ApiGateway/Repositories/RepositoryBase.cs @@ -0,0 +1,29 @@ +using GOC.ApiGateway; +using Microsoft.Extensions.Logging; + +public abstract class RepositoryBase where T : class +{ + + protected readonly IGocHttpClient HttpClient; + + + private ILoggerFactory _loggerFactory; + + protected RepositoryBase(IGocHttpClient httpClient, ILoggerFactory loggerFactory) + { + HttpClient = httpClient; + LoggerFactory = loggerFactory; + } + + protected ILogger Logger { get; set; } + + protected ILoggerFactory LoggerFactory + { + get => _loggerFactory; + set + { + Logger = value.CreateLogger(); + _loggerFactory = value; + } + } +} \ No newline at end of file diff --git a/src/GOC.ApiGateway/RetryPolicies.cs b/src/GOC.ApiGateway/RetryPolicies.cs new file mode 100644 index 0000000..ddfc148 --- /dev/null +++ b/src/GOC.ApiGateway/RetryPolicies.cs @@ -0,0 +1,63 @@ +using System; +using Microsoft.Extensions.Logging; +using Polly; +using Polly.CircuitBreaker; +using Polly.Retry; + +namespace GOC.ApiGateway +{ + public class RetryPolicies + { + private readonly CircuitBreakerSettings _circuitBreakerSettings; + private readonly WaitAndRetrySettings _waitAndRetrySettings; + private readonly ILogger _logger; + public readonly Policy InventoryServiceCircuitBreaker; + public readonly Policy CrmServiceCircuitBreaker; + + + public RetryPolicies(CircuitBreakerSettings circuitBreakerSettings, WaitAndRetrySettings waitAndRetrySettings, ILoggerFactory loggerFactory) + { + _circuitBreakerSettings = circuitBreakerSettings; + _waitAndRetrySettings = waitAndRetrySettings; + _logger = loggerFactory.CreateLogger(); + + var inventoryCircuitBreaker = CircuitBreakerPolicy(); + var crmCircuitBreaker = CircuitBreakerPolicy(); + + var waitAndRetry = WaitAndRetryPolicy(); + + CrmServiceCircuitBreaker = Policy.WrapAsync(waitAndRetry, crmCircuitBreaker); + InventoryServiceCircuitBreaker = Policy.WrapAsync(waitAndRetry, inventoryCircuitBreaker); + } + + private CircuitBreakerPolicy CircuitBreakerPolicy() + { + return Policy + .Handle() + .AdvancedCircuitBreakerAsync( + failureThreshold: _circuitBreakerSettings.FailureThreshold, + samplingDuration: TimeSpan.FromSeconds(_circuitBreakerSettings.SamplingDurationInSeconds), + minimumThroughput: _circuitBreakerSettings.MinimumThroughput, + durationOfBreak: TimeSpan.FromSeconds(_circuitBreakerSettings.DurationOfBreakInSeconds), + onBreak: (ex, breakDelay) => + { + _logger.LogError("Circuit Breaker: Breaking the circuit for " + breakDelay.TotalMilliseconds + "ms! Exception: {@ex}", ex); + }, + onReset: () => _logger.LogInformation("Circuit Breaker: : Call ok! Closed the circuit again!"), + onHalfOpen: () => _logger.LogWarning("Circuit Breaker: Half-open: Next call is a trial!") + ); + } + + private RetryPolicy WaitAndRetryPolicy() + { + return Policy + .Handle(e => !(e is BrokenCircuitException)) // Exception filtering! We don't retry if the inner circuit-breaker judges the underlying system is out of commission! + .WaitAndRetryAsync(_waitAndRetrySettings.RetryAttempts, + attempt => TimeSpan.FromSeconds(0.1 * Math.Pow(2, attempt)), // Back off! 200ms, 400ms, 800ms, 1600ms + (ex, calculatedWaitDuration) => + { + _logger.LogError("WaitAndRetry: Delaying for " + calculatedWaitDuration.TotalMilliseconds + "ms. Exception Message: {ExceptionMessage}", ex.Message); + }); + } + } +} diff --git a/src/GOC.ApiGateway/Services/CompanyService.cs b/src/GOC.ApiGateway/Services/CompanyService.cs new file mode 100644 index 0000000..743acf8 --- /dev/null +++ b/src/GOC.ApiGateway/Services/CompanyService.cs @@ -0,0 +1,32 @@ +using System.Threading.Tasks; +using CSharpFunctionalExtensions; +using GOC.ApiGateway.Dtos; +using GOC.ApiGateway.Interfaces; + +namespace GOC.ApiGateway.Services +{ + public class CompanyService : ICompanyService + { + private readonly ICompanyRepository _companyRepository; + + public CompanyService(ICompanyRepository companyRepository) + { + _companyRepository = companyRepository; + } + + public async Task> CreateCompanyAsync(CompanyPostDto company) + { + var result = await _companyRepository.CreateCompanyAsync(company); + + var mappedResult = new CompanyDto + { + Id = result.Value.Id, + Name = result.Value.Name, + PhoneNumber = result.Value.PhoneNumber, + Address = result.Value.Address + }; + return Result.Ok(mappedResult); + } + + } +} diff --git a/src/GOC.ApiGateway/Services/InventoryService.cs b/src/GOC.ApiGateway/Services/InventoryService.cs new file mode 100644 index 0000000..5798761 --- /dev/null +++ b/src/GOC.ApiGateway/Services/InventoryService.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using CSharpFunctionalExtensions; +using GOC.ApiGateway.Dtos; +using GOC.ApiGateway.Interfaces; + +namespace GOC.ApiGateway.Services +{ + public class InventoryService : IInventoryService + { + private readonly IInventoryRepository _inventoryRepository; + + public InventoryService(IInventoryRepository inventoryRepository) + { + _inventoryRepository = inventoryRepository; + } + + public async Task> CreateInventoryAsync(InventoryPostDto inventory) + { + var result = await _inventoryRepository.CreateInventoryAsync(inventory); + + var mappedResult = new InventoryDto + { + Id = result.Value.Id, + CompanyId = result.Value.CompanyId, + Name = result.Value.Name + }; + return Result.Ok(mappedResult); + } + } +} diff --git a/src/GOC.ApiGateway/Startup.cs b/src/GOC.ApiGateway/Startup.cs index 0171378..1b2af64 100644 --- a/src/GOC.ApiGateway/Startup.cs +++ b/src/GOC.ApiGateway/Startup.cs @@ -1,31 +1,105 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using Microphone.AspNet; +using Microphone.Consul; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - +using SimpleInjector; +using Serilog; +using System.Net.Http; +using System.Reflection; +using System.Linq; +using System.Collections.Generic; +using System.Threading.Tasks; +using EasyNetQ; +using GOC.ApiGateway.HttpClientHelper; +using Microphone; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.ViewComponents; +using SimpleInjector.Lifestyles; +using SimpleInjector.Integration.AspNetCore.Mvc; +using Consul; +using Microsoft.Extensions.Options; +using Microphone.Core; + namespace GOC.ApiGateway { public class Startup { - public Startup(IConfiguration configuration) + public Startup(IHostingEnvironment env) { - Configuration = configuration; - } + var builder = new ConfigurationBuilder() + .SetBasePath(env.ContentRootPath) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true); + + Configuration = builder.Build(); + AppSettings = Configuration.GetSection("ApiGateWay").Get(); + } + public static AppSettings AppSettings { get; private set; } public IConfiguration Configuration { get; } + private Container Container { get; } = new Container(); + private ILoggerFactory LoggerFactory { get; set; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { - services.AddMvc(); + LoggerFactory = new LoggerFactory(); + + services.AddSingleton(LoggerFactory); + LoggerFactory.AddSerilog(); + LoggerFactory.AddDebug(); + var serilogConfiguration = new LoggerConfiguration().ReadFrom.Configuration(Configuration); + Log.Logger = serilogConfiguration.CreateLogger().ForContext("Application", "Api Gateway"); + + services.AddMvcCore() + .AddAuthorization() + .AddJsonFormatters(); + + services.AddMicrophone(); + + services.Configure(options => + { + options.Heartbeat = AppSettings.Consul.Heartbeat; + options.Host = AppSettings.Consul.Host; + options.Port = AppSettings.Consul.Port; + }); + + services.AddAuthentication("Bearer") + .AddIdentityServerAuthentication(options => + { + options.Authority = AppSettings.Identity.Authority; + options.RequireHttpsMetadata = false; + options.ApiSecret = AppSettings.Identity.ApiSecret; + options.ApiName = AppSettings.Identity.ApiName; + }); + + IntegrateSimpleInjector(services); + + } + + private void IntegrateSimpleInjector(IServiceCollection services) + { + Container.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle(); + + services.AddSingleton(); + + services.AddSingleton( + new SimpleInjectorControllerActivator(Container)); + services.AddSingleton( + new SimpleInjectorViewComponentActivator(Container)); + + + services.EnableSimpleInjectorCrossWiring(Container); + services.UseSimpleInjectorAspNetRequestScoping(Container); } + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { @@ -34,7 +108,86 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env) app.UseDeveloperExceptionPage(); } - app.UseMvc(); + app.UseAuthentication(); + app.UseMvc() + .UseMicrophone("ApiGateway", "1.0", new Uri($"http://vagrant:5001")); + InitializeContainer(app); + } + + protected void InitializeContainer(IApplicationBuilder app) + { + RegisterCustomHttpClient(app); + + Container.RegisterSingleton(new RetryPolicies(AppSettings.CircuitBreaker, AppSettings.WaitAndRetry, LoggerFactory)); + + // auto register all other dependencies + var repositoryAssembly = Assembly.GetEntryAssembly(); + var registrations = repositoryAssembly.GetExportedTypes() + .Where(type => + type.Namespace == "GOC.ApiGateway.Repositories" || type.Namespace == "GOC.ApiGateway.Services" || + type.Namespace == "GOC.ApiGateway.Interfaces") + .Where(type => type.GetInterfaces().Any()) + .Select(type => new { Service = type.GetInterfaces().Single(), Implementation = type }); + foreach(var reg in registrations) + { + Container.Register(reg.Service, reg.Implementation, Lifestyle.Scoped); + } + + // message bus registration + Container.Register(() => RabbitHutch.CreateBus($"host={AppSettings.Rabbit.Host}"), Lifestyle.Singleton); + Container.Register(Lifestyle.Scoped); + Container.RegisterSingleton(new EmptyHealthCheck()); + Container.CrossWire(app); + Container.Verify(); + + } + + private void RegisterCustomHttpClient(IApplicationBuilder app) + { + Container.Register(() => + { + return new HttpTokenAuthorizationContext( + httpContextAccessor: () => + { + var httpContextAccessor = (HttpContextAccessor)app.ApplicationServices.GetService(typeof(IHttpContextAccessor)); + return httpContextAccessor.HttpContext; + }, + bearerTokenAccessor: BearerTokenAccessor, + // if request comes from javascript app + accessTokenAccessor: async (hc) => await hc.GetTokenAsync("access_token") + ); + }, Lifestyle.Scoped); + + var consulUriResolverRegistration = Lifestyle.Singleton.CreateRegistration>( + () => (serviceName, relativeUri) => Cluster.Client.ResolveUri(serviceName, relativeUri), Container); + + + var gocHttpClientRegistration = Lifestyle.Singleton.CreateRegistration(() => new HttpClient(), Container); + + + Container.RegisterConditional(serviceType: typeof(HttpClient), + registration: gocHttpClientRegistration, + predicate: c => c.Consumer.ImplementationType.GetInterface(nameof(IGocHttpClient)) != null); + + void RegisterHttpClient(Type type, Registration registration) => Container.RegisterConditional(serviceType: type, + registration: registration, + predicate: context => + context.Consumer.ImplementationType == typeof(HttpClientWrapper)); + + RegisterHttpClient(typeof(Func), consulUriResolverRegistration); + + Container.Register(Lifestyle.Scoped); + } + + + + private async Task> BearerTokenAccessor(HttpContext context) + { + return context.Request.Headers["Authorization"] + .Where(x => x.StartsWith("Bearer ")) + .Select(x => x.Substring(7)); } } + + } diff --git a/src/GOC.ApiGateway/appsettings.Production.json b/src/GOC.ApiGateway/appsettings.Production.json new file mode 100644 index 0000000..d177980 --- /dev/null +++ b/src/GOC.ApiGateway/appsettings.Production.json @@ -0,0 +1,3 @@ +{ + +} diff --git a/src/GOC.ApiGateway/appsettings.json b/src/GOC.ApiGateway/appsettings.json index 647f1a9..19d33f3 100644 --- a/src/GOC.ApiGateway/appsettings.json +++ b/src/GOC.ApiGateway/appsettings.json @@ -1,4 +1,57 @@ { + "ApiGateway" : { + "CircuitBreaker" : { + "failureThreshold" : 0.6, + "samplingDurationInSeconds" : 5, + "minimumThroughput": 20, + "durationOfBreakInSeconds" : 10 + }, + "WaitAndRetry" : { + "retryAttempts" : 4 + }, + "Consul" : { + "Host" : "consul", + "Posrt" : 8500, + "Heartbeat" : 1 + }, + "Identity" : { + "Authority": "http://vagrant:5000", + "Resources" : [ + { + "ResourceFriendlyName": "Inventory.API", + "ResourceName": "api2" + }, + { + "ResourceFriendlyName": "Crm.API", + "ResourceName": "api3" + } + ], + "ApiName": "api1", + "ApiSecret": "api1-secret", + "TokenEndpoint": "connect/token", + "ApiClientId": "api1.client", + "ApiClientSecret": "api1.client-secret" + }, + "Rabbit": { + "Host": "vagrant" + } + }, + "Serilog" : { + "Filter" : [ + { + "Name" : "ByExcluding", + "Args" : { + "expression" : "RequestPath = '/status'" + } + } + ], + "Using" : ["Serilog.Sinks.Literate"], + "WriteTo" : [ + { + "Name" : "LiterateConsole" + } + ] + }, "Logging": { "IncludeScopes": false, "Debug": {