diff --git a/DashboardService.Test/DashboardService.Test.csproj b/DashboardService.Test/DashboardService.Test.csproj new file mode 100644 index 0000000..2cc9615 --- /dev/null +++ b/DashboardService.Test/DashboardService.Test.csproj @@ -0,0 +1,22 @@ + + + + netcoreapp3.1 + + false + + + + + + + + + + + + + + + + diff --git a/DashboardService.Test/UnitTest1.cs b/DashboardService.Test/UnitTest1.cs new file mode 100644 index 0000000..6125ad3 --- /dev/null +++ b/DashboardService.Test/UnitTest1.cs @@ -0,0 +1,73 @@ +using System; +using System.Threading.Tasks; +using DashboardService.DataAccess.Elastic; +using DashboardService.Domain; +using DotNet.Testcontainers.Containers.Builders; +using DotNet.Testcontainers.Containers.Modules; +using DotNet.Testcontainers.Containers.WaitStrategies; +using Nest; +using Xunit; + +namespace DashboardService.Test +{ + public class UnitTest1 + { + [Fact] + public async Task Test1() + { + //docker run -d --name elasticsearch -p 9200:9200 -p 9300:9300 elasticsearch:7.5.1 + + var testContainersBuilder = new TestcontainersBuilder() + .WithImage("elasticsearch:6.4.0") + .WithName("elasticsearch-33333") + .WithEnvironment("discovery.type","single-node") + .WithPortBinding(9200, 9200) + .WithPortBinding(9300, 9300) + //.WithCleanUp(true) + //.WithWaitStrategy(Wait.UntilContainerIsRunning()); + .WithWaitStrategy(Wait.UntilPortsAreAvailable(9200)); + + using var testContainer = testContainersBuilder.Build(); + await testContainer.StartAsync(); + + var policyRepo = new ElasticPolicyRepository(CreateElasticClient()); + + var pol = new PolicyDocument + ( + "POL1201", + new DateTime(2019,1,1), + new DateTime(2019,12,31), + "Jan Ziomalski", + "BDA", + 4500M, + "jim beam" + ); + + policyRepo.Save(pol); + + var saved = policyRepo.FindByNumber("POL1201"); + + + Assert.NotNull(saved); + + /* + * Elasticsearch.Net.UnexpectedElasticsearchClientException: Cannot deserialize the current JSON object (e.g. {"name":"value"}) into type 'System.Int64' because the type requires a JSON primitive value (e.g. string, number, boolean, null) to deserialize correctly. +To fix this error either change the JSON to a JSON primitive value (e.g. string, number, boolean, null) or change the deserialized type so that it is a normal .NET type (e.g. not a primitive type like integer, not a collection type like an array or List) that can be deserialized from a JSON object. JsonObjectAttribute can also be added to the type to force it to deserialize from a JSON object. +Path 'hits.total.value', line 1, position 114. + ---> Nest.Json.JsonSerializationException: Cannot deserialize the current JSON object (e.g. {"name":"value"}) into type 'System.Int64' because the type requires a JSON primitive value (e.g. string, number, boolean, null) to deserialize correctly. +To fix this error either change the JSON to a JSON primitive value (e.g. string, number, boolean, null) or change the deserialized type so that it is a normal .NET type (e.g. not a primitive type like integer, not a collection type like an array or List) that can be deserialized from a JSON object. JsonObjectAttribute can also be added to the type to force it to deserialize from a JSON object. +Path 'hits.total.value', line 1, position 114. + at Nest.Json.Serialization + */ + + } + + private ElasticClient CreateElasticClient() + { + var connectionSettings = new ConnectionSettings() + .DefaultMappingFor(m=> + m.IndexName("policy_lab_stats").IdProperty(d=>d.Number)); + return new ElasticClient(connectionSettings); + } + } +} \ No newline at end of file diff --git a/DashboardService/DashboardService.csproj b/DashboardService/DashboardService.csproj index 20b9fe2..7259b09 100644 --- a/DashboardService/DashboardService.csproj +++ b/DashboardService/DashboardService.csproj @@ -10,10 +10,13 @@ + + + diff --git a/DashboardService/DataAccess/Elastic/ElasticPolicyRepository.cs b/DashboardService/DataAccess/Elastic/ElasticPolicyRepository.cs index 8bd30c4..9155c52 100644 --- a/DashboardService/DataAccess/Elastic/ElasticPolicyRepository.cs +++ b/DashboardService/DataAccess/Elastic/ElasticPolicyRepository.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using DashboardService.Domain; using Nest; @@ -8,13 +9,6 @@ public class ElasticPolicyRepository : IPolicyRepository { private readonly ElasticClient elasticClient; - public ElasticPolicyRepository() - { - var connectionSettings = new ConnectionSettings() - .DefaultMappingFor(m=>m.IndexName("policy_lab_stats").IdProperty(d=>d.Number)); - elasticClient = new ElasticClient(connectionSettings); - } - public ElasticPolicyRepository(ElasticClient elasticClient) { this.elasticClient = elasticClient; @@ -28,13 +22,18 @@ public ElasticPolicyRepository(ElasticClient elasticClient) public void Save(PolicyDocument policy) { - elasticClient.Index + var response = elasticClient.Index ( policy, i => i .Index("policy_lab_stats") .Id(policy.Number) ); + + if (!response.IsValid) + { + throw new ApplicationException("Failed to index a policy document"); + } } public PolicyDocument FindByNumber(string policyNumber) diff --git a/DashboardService/DataAccess/Elastic/NestInstaller.cs b/DashboardService/DataAccess/Elastic/NestInstaller.cs new file mode 100644 index 0000000..9d1ae5e --- /dev/null +++ b/DashboardService/DataAccess/Elastic/NestInstaller.cs @@ -0,0 +1,25 @@ +using System; +using DashboardService.Domain; +using Microsoft.Extensions.DependencyInjection; +using Nest; + +namespace DashboardService.DataAccess.Elastic +{ + public static class NestInstaller + { + public static IServiceCollection AddElasticSearch(this IServiceCollection services, string cnString) + { + services.AddSingleton(typeof(ElasticClient), svc => CreateElasticClient(cnString)); + services.AddScoped(typeof(IPolicyRepository), typeof(ElasticPolicyRepository)); + return services; + } + + private static ElasticClient CreateElasticClient(string cnString) + { + var connectionSettings = new ConnectionSettings(new Uri(cnString)) + .DefaultMappingFor(m=> + m.IndexName("policy_lab_stats").IdProperty(d=>d.Number)); + return new ElasticClient(connectionSettings); + } + } +} diff --git a/DashboardService/Listeners/PolicyCreatedHandler.cs b/DashboardService/Listeners/PolicyCreatedHandler.cs new file mode 100644 index 0000000..8c4e845 --- /dev/null +++ b/DashboardService/Listeners/PolicyCreatedHandler.cs @@ -0,0 +1,36 @@ +using System.Threading; +using System.Threading.Tasks; +using DashboardService.Domain; +using MediatR; +using PolicyService.Api.Events; + +namespace DashboardService.Listeners +{ + public class PolicyCreatedHandler : INotificationHandler + { + private readonly IPolicyRepository policyRepository; + + public PolicyCreatedHandler(IPolicyRepository policyRepository) + { + this.policyRepository = policyRepository; + } + + public Task Handle(PolicyCreated notification, CancellationToken cancellationToken) + { + var policy = new PolicyDocument + ( + notification.PolicyNumber, + notification.PolicyFrom, + notification.PolicyTo, + $"{notification.PolicyHolder.FirstName} {notification.PolicyHolder.LastName}", + notification.ProductCode, + notification.TotalPremium, + notification.AgentLogin + ); + + policyRepository.Save(policy); + + return Task.CompletedTask; + } + } +} diff --git a/DashboardService/Messaging/RabbitMq/RabbitEventListener.cs b/DashboardService/Messaging/RabbitMq/RabbitEventListener.cs new file mode 100644 index 0000000..9d4f9ed --- /dev/null +++ b/DashboardService/Messaging/RabbitMq/RabbitEventListener.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using RawRabbit; + +namespace DashboardService.Messaging.RabbitMq +{ + public class RabbitEventListener + { + private readonly IBusClient busClient; + private readonly IServiceProvider serviceProvider; + + public RabbitEventListener( + IBusClient busClient, + IServiceProvider serviceProvider) + { + this.busClient = busClient; + this.serviceProvider = serviceProvider; + } + + public void ListenTo(List eventsToSubscribe) + { + foreach (var evtType in eventsToSubscribe) + { + //add check if is INotification + this.GetType() + .GetMethod("Subscribe", System.Reflection.BindingFlags.NonPublic| System.Reflection.BindingFlags.Instance) + .MakeGenericMethod(evtType) + .Invoke(this, new object[] { }); + } + } + + private void Subscribe() where T : INotification + { + //TODO: move exchange name and queue prefix to cfg + this.busClient.SubscribeAsync( + async (msg) => + { + //add logging + using (var scope = serviceProvider.CreateScope()) + { + var internalBus = scope.ServiceProvider.GetRequiredService(); + await internalBus.Publish(msg); + } + }, + cfg => cfg.UseSubscribeConfiguration( + c => c + .OnDeclaredExchange(e => e + .WithName("lab-dotnet-micro") + .WithType(RawRabbit.Configuration.Exchange.ExchangeType.Topic) + .WithArgument("key", typeof(T).Name.ToLower())) + .FromDeclaredQueue(q => q.WithName("lab-dashboard-service-" + typeof(T).Name))) + ); + } + } +} diff --git a/DashboardService/Messaging/RabbitMq/RabbitMqOptions.cs b/DashboardService/Messaging/RabbitMq/RabbitMqOptions.cs new file mode 100644 index 0000000..c4b9d9d --- /dev/null +++ b/DashboardService/Messaging/RabbitMq/RabbitMqOptions.cs @@ -0,0 +1,9 @@ +namespace DashboardService.Messaging.RabbitMq +{ + public class RabbitMqOptions + { + public string Host { get; set; } + + public int Port { get; set; } + } +} \ No newline at end of file diff --git a/DashboardService/Messaging/RabbitMq/RawRabbitInstaller.cs b/DashboardService/Messaging/RabbitMq/RawRabbitInstaller.cs new file mode 100644 index 0000000..7baed7a --- /dev/null +++ b/DashboardService/Messaging/RabbitMq/RawRabbitInstaller.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using RawRabbit; +using RawRabbit.DependencyInjection.ServiceCollection; +using RawRabbit.Instantiation; + +namespace DashboardService.Messaging.RabbitMq +{ + public static class RawRabbitInstaller + { + public static IServiceCollection AddRabbitListeners(this IServiceCollection services, RabbitMqOptions options) + { + services.AddRawRabbit(new RawRabbitOptions + { + ClientConfiguration = new RawRabbit.Configuration.RawRabbitConfiguration + { + Username = "guest", + Password = "guest", + VirtualHost = "/", + Port = options.Port, + Hostnames = new List { options.Host }, + RequestTimeout = TimeSpan.FromSeconds(10), + PublishConfirmTimeout = TimeSpan.FromSeconds(1), + RecoveryInterval = TimeSpan.FromSeconds(1), + PersistentDeliveryMode = true, + AutoCloseConnection = true, + AutomaticRecovery = true, + TopologyRecovery = true, + Exchange = new RawRabbit.Configuration.GeneralExchangeConfiguration + { + Durable = true, + AutoDelete = false, + Type = RawRabbit.Configuration.Exchange.ExchangeType.Topic + }, + Queue = new RawRabbit.Configuration.GeneralQueueConfiguration + { + Durable = true, + AutoDelete = false, + Exclusive = false + } + } + }); + + services.AddSingleton(svc => new RabbitEventListener(svc.GetRequiredService(), svc)); + + return services; + } + } + + public static class RabbitListenersInstaller + { + public static void UseRabbitListeners(this IApplicationBuilder app, List eventTypes) + { + app.ApplicationServices.GetRequiredService().ListenTo(eventTypes); + } + } +} diff --git a/DashboardService/Startup.cs b/DashboardService/Startup.cs index 402ab1c..3c98689 100644 --- a/DashboardService/Startup.cs +++ b/DashboardService/Startup.cs @@ -1,5 +1,8 @@ +using System; +using System.Collections.Generic; using DashboardService.DataAccess.Elastic; using DashboardService.Domain; +using DashboardService.Messaging.RabbitMq; using MediatR; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -7,6 +10,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using PolicyService.Api.Events; namespace DashboardService { @@ -26,7 +30,9 @@ public void ConfigureServices(IServiceCollection services) .AddNewtonsoftJson() .SetCompatibilityVersion(CompatibilityVersion.Version_3_0); services.AddMediatR(); + services.AddElasticSearch(Configuration.GetConnectionString("ElasticSearchConnection")); services.AddSingleton(); + services.AddRabbitListeners(Configuration.GetSection("RabbitMqOptions").Get()); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -44,6 +50,8 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); + + app.UseRabbitListeners(new List { typeof(PolicyCreated) }); } } } \ No newline at end of file diff --git a/DashboardService/appsettings.json b/DashboardService/appsettings.json index d9d9a9b..a65d635 100644 --- a/DashboardService/appsettings.json +++ b/DashboardService/appsettings.json @@ -6,5 +6,12 @@ "Microsoft.Hosting.Lifetime": "Information" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "RabbitMqOptions" : { + "Host" : "localhost", + "Port" : 5672 + }, + "ConnectionStrings": { + "ElasticSearchConnection": "http://localhost:9200" + } } diff --git a/DotNetMicroservicesPoc.sln b/DotNetMicroservicesPoc.sln index fe4376c..1979240 100644 --- a/DotNetMicroservicesPoc.sln +++ b/DotNetMicroservicesPoc.sln @@ -45,6 +45,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DashboardService.Api", "Das EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DashboardService", "DashboardService\DashboardService.csproj", "{C8B8AF36-EB9C-4C93-BB9F-0CC5D3A4411C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DashboardService.Test", "DashboardService.Test\DashboardService.Test.csproj", "{4B044FAA-8281-4936-9211-6B236890A199}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -135,6 +137,10 @@ Global {C8B8AF36-EB9C-4C93-BB9F-0CC5D3A4411C}.Debug|Any CPU.Build.0 = Debug|Any CPU {C8B8AF36-EB9C-4C93-BB9F-0CC5D3A4411C}.Release|Any CPU.ActiveCfg = Release|Any CPU {C8B8AF36-EB9C-4C93-BB9F-0CC5D3A4411C}.Release|Any CPU.Build.0 = Release|Any CPU + {4B044FAA-8281-4936-9211-6B236890A199}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4B044FAA-8281-4936-9211-6B236890A199}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4B044FAA-8281-4936-9211-6B236890A199}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4B044FAA-8281-4936-9211-6B236890A199}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE