├── .gitignore ├── CleanDds.Application.CommandStack ├── CleanDds.Application.CommandStack.csproj ├── Rates │ ├── DeleteAllRates.cs │ ├── DeleteAllRatesHandler.cs │ ├── SaveRates.cs │ └── SaveRatesHandler.cs └── Transactions │ ├── DeleteAllTransactions.cs │ ├── DeleteAllTransactionsHandler.cs │ ├── SaveTransactions.cs │ └── SaveTransactionsHandler.cs ├── CleanDds.Application.QueryStack ├── CleanDds.Application.QueryStack.csproj ├── Rates │ ├── GetRatesList.cs │ └── GetRatesListHandler.cs └── Transactions │ ├── GetTransactionsBySku.cs │ ├── GetTransactionsBySkuHandler.cs │ ├── GetTransactionsList.cs │ └── GetTransactionsListHandler.cs ├── CleanDds.Application ├── CleanDds.Application.csproj ├── Interfaces │ ├── IDatabaseService.cs │ └── ISeedingService.cs ├── MediatorExtensions.cs └── ViewModels │ └── Transactions │ └── TransactionsBySkuModel.cs ├── CleanDds.Common ├── CleanDds.Common.csproj └── Numbers │ └── Util.cs ├── CleanDds.Domain ├── CleanDds.Domain.csproj ├── Common │ ├── CurrencyType.cs │ └── IEntity.cs └── Currencies │ ├── Rate.cs │ └── Transaction.cs ├── CleanDds.Infrastructure ├── CleanDds.Infrastructure.csproj ├── Logging │ ├── FileLoggerFactoryExtensions.cs │ ├── FileLoggerOptions.cs │ ├── FileLoggerProvider.cs │ └── Internal │ │ ├── BatchingLogger.cs │ │ ├── BatchingLoggerOptions.cs │ │ ├── BatchingLoggerProvider.cs │ │ └── LogMessage.cs └── Seeding │ └── SeedingService.cs ├── CleanDds.Persistance ├── CleanDds.Persistance.csproj └── InMemDatabaseService.cs ├── CleanDds.Service.sln ├── CleanDds.Service ├── .gitignore ├── CleanDds.Service.csproj ├── Controllers │ ├── RatesController.cs │ ├── RestoringController.cs │ └── TransactionsController.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── appsettings.Development.json └── appsettings.json └── CleanDds.Testing ├── CleanDds.Testing.csproj ├── Handlers ├── DeleteAllRatesHandler.cs ├── DeleteAllTransactionsHandlerTests.cs ├── SaveRatesHandlerTests.cs └── SaveTransactionsHandlerTests.cs ├── MathTests.cs └── SerializationTests.cs /.gitignore: -------------------------------------------------------------------------------- 1 | # Autosave files 2 | *~ 3 | 4 | # build 5 | [Oo]bj/ 6 | [Bb]in/ 7 | packages/ 8 | TestResults/ 9 | 10 | # globs 11 | Makefile.in 12 | *.DS_Store 13 | *.sln.cache 14 | *.suo 15 | *.cache 16 | *.pidb 17 | *.userprefs 18 | *.usertasks 19 | config.log 20 | config.make 21 | config.status 22 | aclocal.m4 23 | install-sh 24 | autom4te.cache/ 25 | *.user 26 | *.tar.gz 27 | tarballs/ 28 | test-results/ 29 | Thumbs.db 30 | .vs/ 31 | .idea/ 32 | 33 | # Mac bundle stuff 34 | *.dmg 35 | *.app 36 | 37 | # resharper 38 | *_Resharper.* 39 | *.Resharper 40 | 41 | # dotCover 42 | *.dotCover 43 | -------------------------------------------------------------------------------- /CleanDds.Application.CommandStack/CleanDds.Application.CommandStack.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net7.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /CleanDds.Application.CommandStack/Rates/DeleteAllRates.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace CleanDds.Application.CommandStack.Rates; 4 | 5 | public class DeleteAllRates : IRequest 6 | { 7 | } 8 | -------------------------------------------------------------------------------- /CleanDds.Application.CommandStack/Rates/DeleteAllRatesHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using CleanDds.Application.Interfaces; 5 | using MediatR; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace CleanDds.Application.CommandStack.Rates; 10 | 11 | public class DeleteAllRatesHandler : IRequestHandler 12 | { 13 | private readonly IDatabaseService _database; 14 | private readonly ILogger _logger; 15 | 16 | public DeleteAllRatesHandler(IServiceProvider serviceProvider, IDatabaseService database) 17 | { 18 | _database = database; 19 | _logger = serviceProvider.GetService>(); 20 | } 21 | 22 | public Task Handle(DeleteAllRates request, CancellationToken cancellationToken) 23 | { 24 | _logger.LogInformation("Executing 'Delete All Rates' Command"); 25 | 26 | try 27 | { 28 | _database.Rates.RemoveRange(_database.Rates); 29 | _database.Save(); 30 | } 31 | catch (Exception ex) 32 | { 33 | _logger.LogError(ex, "Error executing 'Delete All Rates' command"); 34 | throw; 35 | } 36 | 37 | return Unit.Task; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /CleanDds.Application.CommandStack/Rates/SaveRates.cs: -------------------------------------------------------------------------------- 1 | using CleanDds.Domain.Currencies; 2 | using MediatR; 3 | 4 | namespace CleanDds.Application.CommandStack.Rates; 5 | 6 | public class SaveRates : IRequest 7 | { 8 | public Rate[] Rates { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /CleanDds.Application.CommandStack/Rates/SaveRatesHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using CleanDds.Application.Interfaces; 5 | using MediatR; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace CleanDds.Application.CommandStack.Rates; 10 | 11 | public class SaveRatesHandler : IRequestHandler 12 | { 13 | private readonly IDatabaseService _database; 14 | private readonly ILogger _logger; 15 | 16 | public SaveRatesHandler(IServiceProvider serviceProvider, IDatabaseService database) 17 | { 18 | _database = database; 19 | _logger = serviceProvider.GetService>(); 20 | } 21 | 22 | public Task Handle(SaveRates request, CancellationToken cancellationToken) 23 | { 24 | _logger.LogInformation("Executing 'Save Rates' Command"); 25 | 26 | try 27 | { 28 | foreach (var rate in request.Rates) 29 | _database.Rates.Add(rate); 30 | 31 | _database.Save(); 32 | } 33 | catch (Exception ex) 34 | { 35 | _logger.LogError(ex, "Error executing 'Save Rates' command"); 36 | throw; 37 | } 38 | 39 | return Unit.Task; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /CleanDds.Application.CommandStack/Transactions/DeleteAllTransactions.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace CleanDds.Application.CommandStack.Transactions; 4 | 5 | public class DeleteAllTransactions : IRequest 6 | { 7 | } 8 | -------------------------------------------------------------------------------- /CleanDds.Application.CommandStack/Transactions/DeleteAllTransactionsHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using CleanDds.Application.Interfaces; 5 | using CleanDds.Persistance; 6 | using MediatR; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace CleanDds.Application.CommandStack.Transactions; 11 | 12 | public class DeleteAllTransactionsHandler : IRequestHandler 13 | { 14 | private readonly IDatabaseService _database; 15 | private readonly ILogger _logger; 16 | 17 | public DeleteAllTransactionsHandler(IServiceProvider serviceProvider, IDatabaseService database) 18 | { 19 | _database = database; 20 | _logger = serviceProvider.GetService>(); 21 | } 22 | 23 | public Task Handle(DeleteAllTransactions request, CancellationToken cancellationToken) 24 | { 25 | _logger.LogInformation("Executing 'Delete All Transactions' Command"); 26 | 27 | try 28 | { 29 | _database.Transactions.RemoveRange(_database.Transactions); 30 | _database.Save(); 31 | } 32 | catch (Exception e) 33 | { 34 | _logger.LogError(e, "Error executing 'Delete All Transactions' command"); 35 | throw; 36 | } 37 | 38 | return Unit.Task; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /CleanDds.Application.CommandStack/Transactions/SaveTransactions.cs: -------------------------------------------------------------------------------- 1 | using CleanDds.Domain.Currencies; 2 | using MediatR; 3 | 4 | namespace CleanDds.Application.CommandStack.Transactions; 5 | 6 | public class SaveTransactions : IRequest 7 | { 8 | public Transaction[] Transactions { get; set; } 9 | } -------------------------------------------------------------------------------- /CleanDds.Application.CommandStack/Transactions/SaveTransactionsHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using CleanDds.Application.Interfaces; 5 | using CleanDds.Persistance; 6 | using MediatR; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace CleanDds.Application.CommandStack.Transactions; 11 | 12 | public class SaveTransactionsHandler : IRequestHandler 13 | { 14 | private readonly IDatabaseService _database; 15 | private readonly ILogger _logger; 16 | 17 | public SaveTransactionsHandler(IServiceProvider serviceProvider, IDatabaseService database) 18 | { 19 | _database = database; 20 | _logger = serviceProvider.GetService>(); 21 | } 22 | 23 | public Task Handle(SaveTransactions request, CancellationToken cancellationToken) 24 | { 25 | _logger.LogInformation("Executing 'Save Transactions' Command"); 26 | 27 | try 28 | { 29 | foreach (var transaction in request.Transactions) 30 | _database.Transactions.Add(transaction); 31 | 32 | _database.Save(); 33 | } 34 | catch (Exception ex) 35 | { 36 | _logger.LogError(ex, "Error executing 'Save Transactions' command"); 37 | throw; 38 | } 39 | 40 | return Unit.Task; 41 | } 42 | } -------------------------------------------------------------------------------- /CleanDds.Application.QueryStack/CleanDds.Application.QueryStack.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net7.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /CleanDds.Application.QueryStack/Rates/GetRatesList.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using CleanDds.Domain.Currencies; 3 | using MediatR; 4 | 5 | namespace CleanDds.Application.QueryStack.Rates; 6 | 7 | public class GetRatesList : IRequest> 8 | { 9 | } -------------------------------------------------------------------------------- /CleanDds.Application.QueryStack/Rates/GetRatesListHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using CleanDds.Application.Interfaces; 6 | using CleanDds.Domain.Currencies; 7 | using CleanDds.Persistance; 8 | using MediatR; 9 | using Microsoft.EntityFrameworkCore; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Microsoft.Extensions.Logging; 12 | 13 | namespace CleanDds.Application.QueryStack.Rates; 14 | 15 | public class GetRatesListHandler : IRequestHandler> 16 | { 17 | private readonly IDatabaseService _database; 18 | private readonly ILogger _logger; 19 | 20 | public GetRatesListHandler(IServiceProvider serviceProvider, InMemDatabaseService database) 21 | { 22 | _database = database; 23 | _logger = serviceProvider.GetService>(); 24 | } 25 | 26 | public Task> Handle(GetRatesList request, CancellationToken cancellationToken) 27 | { 28 | _logger.LogInformation("Getting Rates List"); 29 | 30 | try 31 | { 32 | return _database.Rates.ToListAsync(cancellationToken: cancellationToken); 33 | } 34 | catch (Exception e) 35 | { 36 | _logger.LogError(e, "Error Getting Rates List"); 37 | throw; 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /CleanDds.Application.QueryStack/Transactions/GetTransactionsBySku.cs: -------------------------------------------------------------------------------- 1 | using CleanDds.Application.ViewModels.Transactions; 2 | using MediatR; 3 | 4 | namespace CleanDds.Application.QueryStack.Transactions; 5 | 6 | public class GetTransactionsBySku : IRequest 7 | { 8 | public string Sku { get; set; } 9 | } -------------------------------------------------------------------------------- /CleanDds.Application.QueryStack/Transactions/GetTransactionsBySkuHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Specialized; 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using CleanDds.Application.Interfaces; 8 | using CleanDds.Application.ViewModels.Transactions; 9 | using CleanDds.Common.Numbers; 10 | using CleanDds.Domain.Common; 11 | using CleanDds.Domain.Currencies; 12 | using CleanDds.Persistance; 13 | using MediatR; 14 | using Microsoft.Extensions.DependencyInjection; 15 | using Microsoft.Extensions.Logging; 16 | 17 | namespace CleanDds.Application.QueryStack.Transactions; 18 | 19 | public class GetTransactionsBySkuHandler : IRequestHandler 20 | { 21 | private readonly IDatabaseService _database; 22 | private readonly ILogger _logger; 23 | 24 | public GetTransactionsBySkuHandler(IServiceProvider serviceProvider, InMemDatabaseService database) 25 | { 26 | _database = database; 27 | _logger = serviceProvider.GetService>(); 28 | } 29 | 30 | public Task Handle(GetTransactionsBySku request, CancellationToken cancellationToken) 31 | { 32 | _logger.LogInformation($"Getting Transactions for SKU {request.Sku}"); 33 | 34 | try 35 | { 36 | var finalTransactions = new List(); 37 | var transactions = _database.Transactions.Where(x => x.Sku == request.Sku).ToList(); 38 | var rates = _database.Rates.ToList(); 39 | 40 | foreach (var tx in transactions) 41 | { 42 | if (tx.Currency == CurrencyType.EUR) 43 | { 44 | finalTransactions.Add(tx); 45 | continue; 46 | } 47 | 48 | var rate = rates.First(x => x.CurrencyFrom == tx.Currency && x.CurrencyTo == CurrencyType.EUR); 49 | finalTransactions.Add(new Transaction 50 | { 51 | Currency = CurrencyType.EUR, 52 | Sku = request.Sku, 53 | Id = Guid.NewGuid(), 54 | Amount = Math.Round(Util.RoundHalfToEven(tx.Amount * rate.CurrencyRate), 2) 55 | }); 56 | } 57 | 58 | var response = new TransactionsBySkuModel 59 | { 60 | Total = Math.Round(finalTransactions.Sum(x => x.Amount), 2), 61 | Transactions = finalTransactions.ToArray() 62 | }; 63 | 64 | return Task.FromResult(response); 65 | } 66 | catch (Exception e) 67 | { 68 | _logger.LogError(e, $"Error Getting Transactions for SKU {request.Sku}"); 69 | throw; 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /CleanDds.Application.QueryStack/Transactions/GetTransactionsList.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using CleanDds.Domain.Currencies; 3 | using MediatR; 4 | 5 | namespace CleanDds.Application.QueryStack.Transactions; 6 | 7 | public class GetTransactionsList : IRequest> 8 | { 9 | } -------------------------------------------------------------------------------- /CleanDds.Application.QueryStack/Transactions/GetTransactionsListHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using CleanDds.Application.Interfaces; 6 | using CleanDds.Domain.Currencies; 7 | using CleanDds.Persistance; 8 | using MediatR; 9 | using Microsoft.EntityFrameworkCore; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Microsoft.Extensions.Logging; 12 | 13 | namespace CleanDds.Application.QueryStack.Transactions; 14 | 15 | public class GetTransactionsListHandler : IRequestHandler> 16 | { 17 | private readonly IDatabaseService _database; 18 | private readonly ILogger _logger; 19 | 20 | public GetTransactionsListHandler(IServiceProvider serviceProvider, InMemDatabaseService database) 21 | { 22 | _database = database; 23 | _logger = serviceProvider.GetService>(); 24 | } 25 | 26 | public Task> Handle(GetTransactionsList request, CancellationToken cancellationToken) 27 | { 28 | _logger.LogInformation("Getting Transactions List"); 29 | 30 | try 31 | { 32 | return _database.Transactions.ToListAsync(cancellationToken: cancellationToken); 33 | } 34 | catch (Exception e) 35 | { 36 | _logger.LogError(e, "Error Getting Transactions List"); 37 | throw; 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /CleanDds.Application/CleanDds.Application.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /CleanDds.Application/Interfaces/IDatabaseService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using CleanDds.Domain.Currencies; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace CleanDds.Application.Interfaces; 6 | 7 | public interface IDatabaseService 8 | { 9 | DbSet Rates { get; set; } 10 | DbSet Transactions { get; set; } 11 | Task SaveAsync(); 12 | void Save(); 13 | } 14 | -------------------------------------------------------------------------------- /CleanDds.Application/Interfaces/ISeedingService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace CleanDds.Application.Interfaces; 4 | 5 | public interface ISeedingService 6 | { 7 | Task SeedRates(string ratesUrl); 8 | Task SeedTransactions(string transactionsUrl); 9 | } 10 | -------------------------------------------------------------------------------- /CleanDds.Application/MediatorExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Reflection; 3 | using MediatR; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace CleanDds.Application; 7 | 8 | public static class MediatorExtensions 9 | { 10 | public static IServiceCollection AddMediatorHandlers(this IServiceCollection services, Assembly assembly) 11 | { 12 | var neededTypes = new[] { typeof(IRequestHandler<,>), typeof(IRequestHandler<>) }; 13 | var classTypes = assembly.ExportedTypes.Select(x => x.GetTypeInfo()).Where(x => x.IsClass && !x.IsAbstract); 14 | 15 | foreach (var type in classTypes) 16 | { 17 | var interfaces = type.ImplementedInterfaces.Select(x => x.GetTypeInfo()); 18 | 19 | var iFiltered = interfaces.Where(x => x.IsGenericType && neededTypes.Contains(x.GetGenericTypeDefinition())); 20 | 21 | foreach (var handlerType in iFiltered) 22 | services.AddTransient(handlerType.AsType(), type.AsType()); 23 | } 24 | 25 | return services; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /CleanDds.Application/ViewModels/Transactions/TransactionsBySkuModel.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | using CleanDds.Domain.Currencies; 3 | 4 | namespace CleanDds.Application.ViewModels.Transactions; 5 | 6 | [DataContract] 7 | public class TransactionsBySkuModel 8 | { 9 | [DataMember(Name = "total")] 10 | public decimal Total { get; set; } 11 | 12 | [DataMember(Name = "transactions")] 13 | public Transaction[] Transactions { get; set; } 14 | } 15 | -------------------------------------------------------------------------------- /CleanDds.Common/CleanDds.Common.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /CleanDds.Common/Numbers/Util.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CleanDds.Common.Numbers; 4 | 5 | public static class Util 6 | { 7 | public static decimal RoundHalfToEven(decimal value) 8 | { 9 | var decPart = Math.Abs(value % 1); 10 | var intPart = Math.Abs(value) - decPart; 11 | 12 | var nextNumber = Math.Abs(intPart) + 1; 13 | var previousNumber = Math.Abs(intPart); 14 | 15 | if (decPart == 0.5m) 16 | { 17 | if (nextNumber % 2 == 0) 18 | return nextNumber * (value > 0 ? 1 : -1); 19 | 20 | if (previousNumber % 2 == 0) 21 | return previousNumber * (value > 0 ? 1 : -1); 22 | } 23 | 24 | return value; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /CleanDds.Domain/CleanDds.Domain.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /CleanDds.Domain/Common/CurrencyType.cs: -------------------------------------------------------------------------------- 1 | namespace CleanDds.Domain.Common; 2 | 3 | public enum CurrencyType 4 | { 5 | EUR = 0, 6 | USD, 7 | CAD, 8 | AUD 9 | } 10 | -------------------------------------------------------------------------------- /CleanDds.Domain/Common/IEntity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CleanDds.Domain.Common; 4 | 5 | public interface IEntity 6 | { 7 | Guid Id { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /CleanDds.Domain/Currencies/Rate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.Runtime.Serialization; 4 | using CleanDds.Domain.Common; 5 | using Newtonsoft.Json; 6 | using Newtonsoft.Json.Converters; 7 | 8 | namespace CleanDds.Domain.Currencies; 9 | 10 | [DataContract] 11 | public class Rate : IEntity 12 | { 13 | [DataMember(Name = "from")] 14 | [JsonConverter(typeof(StringEnumConverter))] 15 | public CurrencyType CurrencyFrom { get; set; } 16 | 17 | [DataMember(Name = "to")] 18 | [JsonConverter(typeof(StringEnumConverter))] 19 | public CurrencyType CurrencyTo { get; set; } 20 | 21 | [DataMember(Name = "rate")] 22 | [DataType(DataType.Currency)] 23 | public decimal CurrencyRate { get; set; } 24 | 25 | [Key] 26 | public Guid Id { get; set; } 27 | } 28 | -------------------------------------------------------------------------------- /CleanDds.Domain/Currencies/Transaction.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.Runtime.Serialization; 4 | using CleanDds.Domain.Common; 5 | using Newtonsoft.Json; 6 | using Newtonsoft.Json.Converters; 7 | 8 | namespace CleanDds.Domain.Currencies; 9 | 10 | [DataContract] 11 | public class Transaction : IEntity 12 | { 13 | [DataMember(Name = "sku")] 14 | public string Sku { get; set; } 15 | 16 | [DataMember(Name = "amount")] 17 | public decimal Amount { get; set; } 18 | 19 | [DataMember(Name = "currency")] 20 | [JsonConverter(typeof(StringEnumConverter))] 21 | public CurrencyType Currency { get; set; } 22 | 23 | [Key] 24 | public Guid Id { get; set; } 25 | } 26 | -------------------------------------------------------------------------------- /CleanDds.Infrastructure/CleanDds.Infrastructure.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net7.0 4 | 5 | 6 | 7 | ..\..\..\.nuget\packages\microsoft.extensions.logging\2.0.1\lib\netstandard2.0\Microsoft.Extensions.Logging.dll 8 | 9 | 10 | ..\..\..\.nuget\packages\microsoft.extensions.logging.abstractions\2.0.1\lib\netstandard2.0\Microsoft.Extensions.Logging.Abstractions.dll 11 | 12 | 13 | ..\..\..\.nuget\packages\microsoft.extensions.options\2.0.1\lib\netstandard2.0\Microsoft.Extensions.Options.dll 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /CleanDds.Infrastructure/Logging/FileLoggerFactoryExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace CleanDds.Infrastructure.Logging; 6 | 7 | public static class FileLoggerFactoryExtensions 8 | { 9 | public static ILoggingBuilder AddFile(this ILoggingBuilder builder) 10 | { 11 | builder.Services.AddSingleton(); 12 | return builder; 13 | } 14 | 15 | public static ILoggingBuilder AddFile(this ILoggingBuilder builder, Action configure) 16 | { 17 | builder.AddFile(); 18 | builder.Services.Configure(configure); 19 | 20 | return builder; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /CleanDds.Infrastructure/Logging/FileLoggerOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using CleanDds.Infrastructure.Logging.Internal; 3 | 4 | namespace CleanDds.Infrastructure.Logging; 5 | 6 | public class FileLoggerOptions : BatchingLoggerOptions 7 | { 8 | private int? _fileSizeLimit = 10 * 1024 * 1024; 9 | private int? _retainedFileCountLimit = 2; 10 | private string _fileName = "logs-"; 11 | 12 | 13 | /// 14 | /// Gets or sets a strictly positive value representing the maximum log size in bytes or null for no limit. 15 | /// Once the log is full, no more messages will be appended. 16 | /// Defaults to 10MB. 17 | /// 18 | public int? FileSizeLimit 19 | { 20 | get { return _fileSizeLimit; } 21 | set 22 | { 23 | if (value <= 0) 24 | { 25 | throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(FileSizeLimit)} must be positive."); 26 | } 27 | _fileSizeLimit = value; 28 | } 29 | } 30 | 31 | /// 32 | /// Gets or sets a strictly positive value representing the maximum retained file count or null for no limit. 33 | /// Defaults to 2. 34 | /// 35 | public int? RetainedFileCountLimit 36 | { 37 | get { return _retainedFileCountLimit; } 38 | set 39 | { 40 | if (value <= 0) 41 | { 42 | throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(RetainedFileCountLimit)} must be positive."); 43 | } 44 | _retainedFileCountLimit = value; 45 | } 46 | } 47 | 48 | /// 49 | /// Gets or sets the filename prefix to use for log files. 50 | /// Defaults to logs-. 51 | /// 52 | public string FileName 53 | { 54 | get { return _fileName; } 55 | set 56 | { 57 | if (string.IsNullOrEmpty(value)) 58 | { 59 | throw new ArgumentException(nameof(value)); 60 | } 61 | _fileName = value; 62 | } 63 | } 64 | 65 | /// 66 | /// The directory in which log files will be written, relative to the app process. 67 | /// Default to Logs 68 | /// 69 | /// 70 | public string LogDirectory { get; set; } = "Logs"; 71 | } 72 | -------------------------------------------------------------------------------- /CleanDds.Infrastructure/Logging/FileLoggerProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Microsoft.Extensions.Logging; 7 | using Microsoft.Extensions.Options; 8 | using CleanDds.Infrastructure.Logging.Internal; 9 | 10 | namespace CleanDds.Infrastructure.Logging; 11 | 12 | [ProviderAlias("File")] 13 | public class FileLoggerProvider : BatchingLoggerProvider 14 | { 15 | private readonly string _path; 16 | private readonly string _fileName; 17 | private readonly int? _maxFileSize; 18 | private readonly int? _maxRetainedFiles; 19 | 20 | public FileLoggerProvider(IOptions options) : base(options) 21 | { 22 | var loggerOptions = options.Value; 23 | _path = loggerOptions.LogDirectory; 24 | _fileName = loggerOptions.FileName; 25 | _maxFileSize = loggerOptions.FileSizeLimit; 26 | _maxRetainedFiles = loggerOptions.RetainedFileCountLimit; 27 | } 28 | 29 | // Write the provided messages to the file system 30 | protected override async Task WriteMessagesAsync(IEnumerable messages, CancellationToken cancellationToken) 31 | { 32 | Directory.CreateDirectory(_path); 33 | 34 | // Group messages by log date 35 | foreach (var group in messages.GroupBy(GetGrouping)) 36 | { 37 | var fullName = GetFullName(group.Key); 38 | var fileInfo = new FileInfo(fullName); 39 | // If we've exceeded the max file size, don't write any logs 40 | if (_maxFileSize > 0 && fileInfo.Exists && fileInfo.Length > _maxFileSize) 41 | { 42 | return; 43 | } 44 | 45 | // Write the log messages to the file 46 | using (var streamWriter = File.AppendText(fullName)) 47 | { 48 | foreach (var item in group) 49 | { 50 | await streamWriter.WriteAsync(item.Message); 51 | } 52 | } 53 | } 54 | 55 | RollFiles(); 56 | } 57 | 58 | // Get the file name 59 | private string GetFullName((int Year, int Month, int Day) group) 60 | { 61 | return Path.Combine(_path, $"{_fileName}{group.Year:0000}{group.Month:00}{group.Day:00}.txt"); 62 | } 63 | 64 | private (int Year, int Month, int Day) GetGrouping(LogMessage message) 65 | { 66 | return (message.Timestamp.Year, message.Timestamp.Month, message.Timestamp.Day); 67 | } 68 | 69 | // Delete files if we have too many 70 | protected void RollFiles() 71 | { 72 | if (_maxRetainedFiles > 0) 73 | { 74 | var files = new DirectoryInfo(_path) 75 | .GetFiles(_fileName + "*") 76 | .OrderByDescending(f => f.Name) 77 | .Skip(_maxRetainedFiles.Value); 78 | 79 | foreach (var item in files) 80 | { 81 | item.Delete(); 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /CleanDds.Infrastructure/Logging/Internal/BatchingLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace CleanDds.Infrastructure.Logging.Internal; 6 | 7 | public class BatchingLogger : ILogger 8 | { 9 | private readonly BatchingLoggerProvider _provider; 10 | private readonly string _category; 11 | 12 | public BatchingLogger(BatchingLoggerProvider loggerProvider, string categoryName) 13 | { 14 | _provider = loggerProvider; 15 | _category = categoryName; 16 | } 17 | 18 | public IDisposable BeginScope(TState state) 19 | { 20 | return null; 21 | } 22 | 23 | public bool IsEnabled(LogLevel logLevel) 24 | { 25 | return logLevel != LogLevel.None; 26 | } 27 | 28 | // Write a log message 29 | public void Log(DateTimeOffset timestamp, LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) 30 | { 31 | if (!IsEnabled(logLevel)) 32 | { 33 | return; 34 | } 35 | 36 | var builder = new StringBuilder(); 37 | builder.Append(timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff zzz")); 38 | builder.Append(" ["); 39 | builder.Append(logLevel.ToString()); 40 | builder.Append("] "); 41 | builder.Append(_category); 42 | builder.Append(": "); 43 | builder.AppendLine(formatter(state, exception)); 44 | 45 | if (exception != null) 46 | { 47 | builder.AppendLine(exception.ToString()); 48 | } 49 | 50 | _provider.AddMessage(timestamp, builder.ToString()); 51 | } 52 | 53 | public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) 54 | { 55 | Log(DateTimeOffset.Now, logLevel, eventId, state, exception, formatter); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /CleanDds.Infrastructure/Logging/Internal/BatchingLoggerOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CleanDds.Infrastructure.Logging.Internal; 4 | 5 | public class BatchingLoggerOptions 6 | { 7 | private int? _batchSize = 32; 8 | private int? _backgroundQueueSize; 9 | private TimeSpan _flushPeriod = TimeSpan.FromSeconds(1); 10 | 11 | /// 12 | /// Gets or sets the period after which logs will be flushed to the store. 13 | /// 14 | public TimeSpan FlushPeriod 15 | { 16 | get { return _flushPeriod; } 17 | set 18 | { 19 | if (value <= TimeSpan.Zero) 20 | { 21 | throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(FlushPeriod)} must be positive."); 22 | } 23 | _flushPeriod = value; 24 | } 25 | } 26 | 27 | /// 28 | /// Gets or sets the maximum size of the background log message queue or null for no limit. 29 | /// After maximum queue size is reached log event sink would start blocking. 30 | /// Defaults to null. 31 | /// 32 | public int? BackgroundQueueSize 33 | { 34 | get { return _backgroundQueueSize; } 35 | set 36 | { 37 | if (value < 0) 38 | { 39 | throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(BackgroundQueueSize)} must be non-negative."); 40 | } 41 | _backgroundQueueSize = value; 42 | } 43 | } 44 | 45 | /// 46 | /// Gets or sets a maximum number of events to include in a single batch or null for no limit. 47 | /// 48 | public int? BatchSize 49 | { 50 | get { return _batchSize; } 51 | set 52 | { 53 | if (value <= 0) 54 | { 55 | throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(BatchSize)} must be positive."); 56 | } 57 | _batchSize = value; 58 | } 59 | } 60 | 61 | /// 62 | /// Gets or sets value indicating if logger accepts and queues writes. 63 | /// 64 | public bool IsEnabled { get; set; } 65 | } 66 | -------------------------------------------------------------------------------- /CleanDds.Infrastructure/Logging/Internal/BatchingLoggerProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Microsoft.Extensions.Logging; 7 | using Microsoft.Extensions.Options; 8 | 9 | namespace CleanDds.Infrastructure.Logging.Internal; 10 | 11 | public abstract class BatchingLoggerProvider : ILoggerProvider 12 | { 13 | private readonly List _currentBatch = new List(); 14 | private readonly TimeSpan _interval; 15 | private readonly int? _queueSize; 16 | private readonly int? _batchSize; 17 | 18 | private BlockingCollection _messageQueue; 19 | private Task _outputTask; 20 | private CancellationTokenSource _cancellationTokenSource; 21 | 22 | protected BatchingLoggerProvider(IOptions options) 23 | { 24 | // NOTE: Only IsEnabled is monitored 25 | 26 | var loggerOptions = options.Value; 27 | if (loggerOptions.BatchSize <= 0) 28 | { 29 | throw new ArgumentOutOfRangeException(nameof(loggerOptions.BatchSize), $"{nameof(loggerOptions.BatchSize)} must be a positive number."); 30 | } 31 | if (loggerOptions.FlushPeriod <= TimeSpan.Zero) 32 | { 33 | throw new ArgumentOutOfRangeException(nameof(loggerOptions.FlushPeriod), $"{nameof(loggerOptions.FlushPeriod)} must be longer than zero."); 34 | } 35 | 36 | _interval = loggerOptions.FlushPeriod; 37 | _batchSize = loggerOptions.BatchSize; 38 | _queueSize = loggerOptions.BackgroundQueueSize; 39 | 40 | Start(); 41 | } 42 | 43 | protected abstract Task WriteMessagesAsync(IEnumerable messages, CancellationToken token); 44 | 45 | private async Task ProcessLogQueue(object state) 46 | { 47 | while (!_cancellationTokenSource.IsCancellationRequested) 48 | { 49 | var limit = _batchSize ?? int.MaxValue; 50 | 51 | while (limit > 0 && _messageQueue.TryTake(out var message)) 52 | { 53 | _currentBatch.Add(message); 54 | limit--; 55 | } 56 | 57 | if (_currentBatch.Count > 0) 58 | { 59 | try 60 | { 61 | await WriteMessagesAsync(_currentBatch, _cancellationTokenSource.Token); 62 | } 63 | catch 64 | { 65 | // ignored 66 | } 67 | 68 | _currentBatch.Clear(); 69 | } 70 | 71 | await IntervalAsync(_interval, _cancellationTokenSource.Token); 72 | } 73 | } 74 | 75 | protected virtual Task IntervalAsync(TimeSpan interval, CancellationToken cancellationToken) 76 | { 77 | return Task.Delay(interval, cancellationToken); 78 | } 79 | 80 | internal void AddMessage(DateTimeOffset timestamp, string message) 81 | { 82 | if (!_messageQueue.IsAddingCompleted) 83 | { 84 | try 85 | { 86 | _messageQueue.Add(new LogMessage { Message = message, Timestamp = timestamp }, _cancellationTokenSource.Token); 87 | } 88 | catch 89 | { 90 | //cancellation token canceled or CompleteAdding called 91 | } 92 | } 93 | } 94 | 95 | private void Start() 96 | { 97 | _messageQueue = _queueSize == null ? 98 | new BlockingCollection(new ConcurrentQueue()) : 99 | new BlockingCollection(new ConcurrentQueue(), _queueSize.Value); 100 | 101 | _cancellationTokenSource = new CancellationTokenSource(); 102 | _outputTask = Task.Factory.StartNew( 103 | ProcessLogQueue, 104 | null, 105 | TaskCreationOptions.LongRunning); 106 | } 107 | 108 | private void Stop() 109 | { 110 | _cancellationTokenSource.Cancel(); 111 | _messageQueue.CompleteAdding(); 112 | 113 | try 114 | { 115 | _outputTask.Wait(_interval); 116 | } 117 | catch (TaskCanceledException) 118 | { 119 | } 120 | catch (AggregateException ex) when (ex.InnerExceptions.Count == 1 && ex.InnerExceptions[0] is TaskCanceledException) 121 | { 122 | } 123 | } 124 | 125 | public void Dispose() 126 | { 127 | Stop(); 128 | } 129 | 130 | public ILogger CreateLogger(string categoryName) 131 | { 132 | return new BatchingLogger(this, categoryName); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /CleanDds.Infrastructure/Logging/Internal/LogMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CleanDds.Infrastructure.Logging.Internal; 4 | 5 | public struct LogMessage 6 | { 7 | public DateTimeOffset Timestamp { get; set; } 8 | public string Message { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /CleanDds.Infrastructure/Seeding/SeedingService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | using MediatR; 7 | using Microsoft.Extensions.Logging; 8 | using Newtonsoft.Json; 9 | using CleanDds.Application.CommandStack.Rates; 10 | using CleanDds.Application.CommandStack.Transactions; 11 | using CleanDds.Application.Interfaces; 12 | using CleanDds.Domain.Common; 13 | using CleanDds.Domain.Currencies; 14 | 15 | namespace CleanDds.Infrastructure.Seeding; 16 | 17 | public class SeedingService : ISeedingService 18 | { 19 | private readonly ILogger _logger; 20 | private readonly IMediator _mediator; 21 | 22 | public SeedingService(ILogger logger, IMediator mediator) 23 | { 24 | _logger = logger; 25 | _mediator = mediator; 26 | } 27 | 28 | public async Task SeedRates(string ratesUrl) 29 | { 30 | _logger.LogInformation("Starting seeding rates from web service"); 31 | 32 | try 33 | { 34 | List additionalRates = new List(); 35 | // HttpClient client = new HttpClient(); 36 | // var json = await client.GetStringAsync(ratesUrl); 37 | // var rates = JsonConvert.DeserializeObject(json); 38 | var rates = new[] 39 | { 40 | new Rate {CurrencyFrom = CurrencyType.AUD, CurrencyTo = CurrencyType.CAD, CurrencyRate = 1.0079m}, 41 | new Rate {CurrencyFrom = CurrencyType.CAD, CurrencyTo = CurrencyType.USD, CurrencyRate = 1.0090m} 42 | }; 43 | 44 | foreach (var rate in rates) 45 | rate.Id = Guid.NewGuid(); 46 | 47 | var currencies = new[] { CurrencyType.AUD, CurrencyType.CAD, CurrencyType.EUR, CurrencyType.USD }; 48 | 49 | for (int i = 0; i < currencies.Length; i++) 50 | { 51 | var currencyFrom = currencies[i]; 52 | var isInRates = (from x in rates 53 | where x.CurrencyFrom == currencyFrom 54 | select x).ToArray(); 55 | 56 | for (int j = 0; j < currencies.Length; j++) 57 | { 58 | var currencyTo = currencies[j]; 59 | if (j == i) continue; 60 | 61 | if (isInRates.Any(x => x.CurrencyTo == currencyTo)) 62 | continue; 63 | 64 | ExploreNode(1, rates, isInRates, additionalRates, currencyFrom, currencyTo, currencyFrom); 65 | } 66 | } 67 | 68 | additionalRates.AddRange(rates); 69 | await _mediator.Send(new SaveRates 70 | { 71 | Rates = additionalRates.ToArray() 72 | }); 73 | } 74 | catch (Exception e) 75 | { 76 | _logger.LogError(e, "Error seeding rates from web service"); 77 | throw; 78 | } 79 | } 80 | 81 | private void ExploreNode(decimal incrementalRatio, Rate[] allRates, Rate[] isInRates, List additionalRates, CurrencyType currencyFrom, CurrencyType currencyTo, CurrencyType originalFrom) 82 | { 83 | foreach (var rate in isInRates) 84 | { 85 | var currentFrom = rate.CurrencyTo; 86 | var currentInRates = (from x in allRates 87 | where x.CurrencyFrom == currentFrom 88 | select x).ToArray(); 89 | 90 | if (currentInRates.Any(x => x.CurrencyTo == currencyTo)) 91 | { 92 | var w = currentInRates.First(x => x.CurrencyTo == currencyTo); 93 | additionalRates.Add(new Rate 94 | { 95 | CurrencyFrom = originalFrom, 96 | CurrencyTo = w.CurrencyTo, 97 | CurrencyRate = incrementalRatio * rate.CurrencyRate * w.CurrencyRate, 98 | Id = Guid.NewGuid() 99 | }); 100 | 101 | return; 102 | } 103 | 104 | ExploreNode(incrementalRatio * rate.CurrencyRate, allRates, currentInRates, additionalRates, currentFrom, currencyTo, originalFrom); 105 | } 106 | } 107 | 108 | public async Task SeedTransactions(string transactionsUrl) 109 | { 110 | _logger.LogInformation("Starting seeding transactions from web service"); 111 | 112 | try 113 | { 114 | // HttpClient client = new HttpClient(); 115 | // var json = await client.GetStringAsync(transactionsUrl); 116 | // var transactions = JsonConvert.DeserializeObject(json); 117 | var transactions = new[] 118 | { 119 | new Transaction {Sku = "A", Amount = 10, Currency = CurrencyType.USD}, 120 | new Transaction {Sku = "B", Amount = 15, Currency = CurrencyType.CAD} 121 | }; 122 | 123 | foreach (var transaction in transactions) 124 | transaction.Id = Guid.NewGuid(); 125 | 126 | await _mediator.Send(new SaveTransactions 127 | { 128 | Transactions = transactions 129 | }); 130 | } 131 | catch (Exception e) 132 | { 133 | _logger.LogError(e, "Error seeding transactions from web service"); 134 | throw; 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /CleanDds.Persistance/CleanDds.Persistance.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net7.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /CleanDds.Persistance/InMemDatabaseService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using CleanDds.Application.Interfaces; 4 | using CleanDds.Domain.Currencies; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace CleanDds.Persistance; 9 | 10 | public class InMemDatabaseService : DbContext, IDatabaseService 11 | { 12 | private readonly ILogger _logger; 13 | public DbSet Rates { get; set; } 14 | public DbSet Transactions { get; set; } 15 | 16 | public InMemDatabaseService(DbContextOptions options, ILogger logger) : base(options) 17 | { 18 | _logger = logger; 19 | } 20 | 21 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 22 | { 23 | optionsBuilder.UseInMemoryDatabase("in-mem"); 24 | } 25 | 26 | public async Task SaveAsync() 27 | { 28 | _logger.LogInformation("Asynchronously saving data to in-mem database"); 29 | 30 | try 31 | { 32 | await SaveChangesAsync(); 33 | } 34 | catch (Exception e) 35 | { 36 | _logger.LogError(e, "Error saving data"); 37 | throw; 38 | } 39 | } 40 | 41 | public void Save() 42 | { 43 | _logger.LogInformation("Saving data to in-mem database"); 44 | 45 | try 46 | { 47 | SaveChanges(); 48 | } 49 | catch (Exception e) 50 | { 51 | _logger.LogError(e, "Error seesavingding data"); 52 | throw; 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /CleanDds.Service.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2012 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanDds.Service", "CleanDds.Service\CleanDds.Service.csproj", "{5D16A83F-BE4C-4E3B-AD0C-6D2414EE7592}" 5 | EndProject 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanDds.Application", "CleanDds.Application\CleanDds.Application.csproj", "{5C4D8892-272C-4E93-9AC4-4450010A5456}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanDds.Domain", "CleanDds.Domain\CleanDds.Domain.csproj", "{30FD39F1-6FFE-46E0-A79C-9C9375F79525}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanDds.Common", "CleanDds.Common\CleanDds.Common.csproj", "{2DC7D231-0DD4-4534-A313-21081275C504}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanDds.Infrastructure", "CleanDds.Infrastructure\CleanDds.Infrastructure.csproj", "{96B891BB-F105-4D3D-B6A5-09D152A5E1D9}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanDds.Application.CommandStack", "CleanDds.Application.CommandStack\CleanDds.Application.CommandStack.csproj", "{78322F23-716A-4BC6-9F66-B96A47DDED25}" 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanDds.Application.QueryStack", "CleanDds.Application.QueryStack\CleanDds.Application.QueryStack.csproj", "{6924429B-A68B-4ADC-9502-6C10C98BB013}" 17 | EndProject 18 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanDds.Persistance", "CleanDds.Persistance\CleanDds.Persistance.csproj", "{6C77CBC1-E111-4710-B603-A5242EC6D9D3}" 19 | EndProject 20 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanDds.Testing", "CleanDds.Testing\CleanDds.Testing.csproj", "{F1B17989-762A-44C7-B89E-080511702380}" 21 | EndProject 22 | Global 23 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 24 | Debug|Any CPU = Debug|Any CPU 25 | Release|Any CPU = Release|Any CPU 26 | EndGlobalSection 27 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 28 | {5D16A83F-BE4C-4E3B-AD0C-6D2414EE7592}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {5D16A83F-BE4C-4E3B-AD0C-6D2414EE7592}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {5D16A83F-BE4C-4E3B-AD0C-6D2414EE7592}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {5D16A83F-BE4C-4E3B-AD0C-6D2414EE7592}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {5C4D8892-272C-4E93-9AC4-4450010A5456}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {5C4D8892-272C-4E93-9AC4-4450010A5456}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {5C4D8892-272C-4E93-9AC4-4450010A5456}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {5C4D8892-272C-4E93-9AC4-4450010A5456}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {30FD39F1-6FFE-46E0-A79C-9C9375F79525}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {30FD39F1-6FFE-46E0-A79C-9C9375F79525}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {30FD39F1-6FFE-46E0-A79C-9C9375F79525}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {30FD39F1-6FFE-46E0-A79C-9C9375F79525}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {2DC7D231-0DD4-4534-A313-21081275C504}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {2DC7D231-0DD4-4534-A313-21081275C504}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {2DC7D231-0DD4-4534-A313-21081275C504}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {2DC7D231-0DD4-4534-A313-21081275C504}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {96B891BB-F105-4D3D-B6A5-09D152A5E1D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {96B891BB-F105-4D3D-B6A5-09D152A5E1D9}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {96B891BB-F105-4D3D-B6A5-09D152A5E1D9}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {96B891BB-F105-4D3D-B6A5-09D152A5E1D9}.Release|Any CPU.Build.0 = Release|Any CPU 48 | {78322F23-716A-4BC6-9F66-B96A47DDED25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 49 | {78322F23-716A-4BC6-9F66-B96A47DDED25}.Debug|Any CPU.Build.0 = Debug|Any CPU 50 | {78322F23-716A-4BC6-9F66-B96A47DDED25}.Release|Any CPU.ActiveCfg = Release|Any CPU 51 | {78322F23-716A-4BC6-9F66-B96A47DDED25}.Release|Any CPU.Build.0 = Release|Any CPU 52 | {6924429B-A68B-4ADC-9502-6C10C98BB013}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 53 | {6924429B-A68B-4ADC-9502-6C10C98BB013}.Debug|Any CPU.Build.0 = Debug|Any CPU 54 | {6924429B-A68B-4ADC-9502-6C10C98BB013}.Release|Any CPU.ActiveCfg = Release|Any CPU 55 | {6924429B-A68B-4ADC-9502-6C10C98BB013}.Release|Any CPU.Build.0 = Release|Any CPU 56 | {6C77CBC1-E111-4710-B603-A5242EC6D9D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 57 | {6C77CBC1-E111-4710-B603-A5242EC6D9D3}.Debug|Any CPU.Build.0 = Debug|Any CPU 58 | {6C77CBC1-E111-4710-B603-A5242EC6D9D3}.Release|Any CPU.ActiveCfg = Release|Any CPU 59 | {6C77CBC1-E111-4710-B603-A5242EC6D9D3}.Release|Any CPU.Build.0 = Release|Any CPU 60 | {F1B17989-762A-44C7-B89E-080511702380}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 61 | {F1B17989-762A-44C7-B89E-080511702380}.Debug|Any CPU.Build.0 = Debug|Any CPU 62 | {F1B17989-762A-44C7-B89E-080511702380}.Release|Any CPU.ActiveCfg = Release|Any CPU 63 | {F1B17989-762A-44C7-B89E-080511702380}.Release|Any CPU.Build.0 = Release|Any CPU 64 | EndGlobalSection 65 | EndGlobal 66 | -------------------------------------------------------------------------------- /CleanDds.Service/.gitignore: -------------------------------------------------------------------------------- 1 | Logs/ -------------------------------------------------------------------------------- /CleanDds.Service/CleanDds.Service.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net7.0 4 | enable 5 | enable 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /CleanDds.Service/Controllers/RatesController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using CleanDds.Application.QueryStack.Rates; 5 | using CleanDds.Domain.Currencies; 6 | using MediatR; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace CleanDds.Service.Controllers; 11 | 12 | [Route("api/[controller]")] 13 | public class RatesController : Controller 14 | { 15 | private readonly ILogger _logger; 16 | private readonly IMediator _mediator; 17 | 18 | public RatesController(ILogger logger, IMediator mediator) 19 | { 20 | _logger = logger; 21 | _mediator = mediator; 22 | } 23 | 24 | [HttpGet] 25 | public async Task> Index() 26 | { 27 | _logger.LogInformation("Getting all rates"); 28 | 29 | try 30 | { 31 | var rates = await _mediator.Send(new GetRatesList()); 32 | return rates; 33 | } 34 | catch (Exception e) 35 | { 36 | _logger.LogError(e, "Error getting rates from In_Mem database"); 37 | throw; 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /CleanDds.Service/Controllers/RestoringController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using CleanDds.Application.CommandStack.Rates; 3 | using CleanDds.Application.CommandStack.Transactions; 4 | using CleanDds.Application.Interfaces; 5 | using MediatR; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace CleanDds.Service.Controllers; 10 | 11 | [Route("api/[controller]")] 12 | public class RestoringController : Controller 13 | { 14 | private readonly ISeedingService _seedingService; 15 | private readonly ILogger _logger; 16 | private readonly IMediator _mediator; 17 | private readonly IConfiguration _configuration; 18 | 19 | public RestoringController(ISeedingService seedingService, ILogger logger, IMediator mediator, IConfiguration configuration) 20 | { 21 | _seedingService = seedingService; 22 | _logger = logger; 23 | _mediator = mediator; 24 | _configuration = configuration; 25 | } 26 | 27 | [HttpGet] 28 | public bool Get() 29 | { 30 | _logger.LogInformation("Deleting all data and restoring again from Web Services"); 31 | 32 | try 33 | { 34 | _mediator.Send(new DeleteAllTransactions()); 35 | _mediator.Send(new DeleteAllRates()); 36 | _seedingService.SeedRates(_configuration["RatesUrl"]); 37 | _seedingService.SeedTransactions(_configuration["TransactionsUrl"]); 38 | 39 | return true; 40 | } 41 | catch (Exception e) 42 | { 43 | _logger.LogError(e, "Error restoring data in In-Mem database"); 44 | throw; 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /CleanDds.Service/Controllers/TransactionsController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using CleanDds.Application.QueryStack.Transactions; 5 | using CleanDds.Application.ViewModels.Transactions; 6 | using CleanDds.Domain.Currencies; 7 | using MediatR; 8 | using Microsoft.AspNetCore.Mvc; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace CleanDds.Service.Controllers; 12 | 13 | [Route("api/[controller]")] 14 | public class TransactionsController : Controller 15 | { 16 | private readonly ILogger _logger; 17 | private readonly IMediator _mediator; 18 | 19 | public TransactionsController(ILogger logger, IMediator mediator) 20 | { 21 | _logger = logger; 22 | _mediator = mediator; 23 | } 24 | 25 | [HttpGet] 26 | public async Task> Get() 27 | { 28 | _logger.LogInformation("Getting all transactions"); 29 | 30 | try 31 | { 32 | var transactions = await _mediator.Send(new GetTransactionsList()); 33 | return transactions; 34 | } 35 | catch (Exception e) 36 | { 37 | _logger.LogError(e, "Error getting transactions from In_Mem database"); 38 | throw; 39 | } 40 | } 41 | 42 | [HttpGet("{sku}", Name = "Get")] 43 | public async Task Get(string sku) 44 | { 45 | _logger.LogInformation($"Getting transactions for SKU {sku}"); 46 | 47 | try 48 | { 49 | var response = await _mediator.Send(new GetTransactionsBySku 50 | { 51 | Sku = sku 52 | }); 53 | return response; 54 | } 55 | catch (Exception e) 56 | { 57 | _logger.LogError(e, $"Error getting transactions for SKU {sku}"); 58 | throw; 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /CleanDds.Service/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using CleanDds.Application.Interfaces; 3 | using CleanDds.Infrastructure.Seeding; 4 | using CleanDds.Persistance; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | var builder = WebApplication.CreateBuilder(args); 8 | 9 | // Add services to the container. 10 | 11 | builder.Services.AddControllers(); 12 | builder.Services.AddEndpointsApiExplorer(); 13 | builder.Services.AddSwaggerGen(); 14 | builder.Services.AddDbContext(x => x.UseInMemoryDatabase("in-mem")); 15 | builder.Services.AddTransient(); 16 | builder.Services.AddScoped(); 17 | 18 | builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies( 19 | Assembly.Load("CleanDds.Application.CommandStack"), 20 | Assembly.Load("CleanDds.Application.QueryStack") 21 | )); 22 | 23 | var app = builder.Build(); 24 | 25 | if (app.Environment.IsDevelopment()) 26 | { 27 | app.UseSwagger(); 28 | app.UseSwaggerUI(); 29 | } 30 | 31 | var scopeFactory = app.Services.GetRequiredService(); 32 | using (var scope = scopeFactory.CreateScope()) 33 | { 34 | var seeding = scope.ServiceProvider.GetService(); 35 | 36 | if (seeding is not null) 37 | { 38 | await seeding.SeedRates(app.Configuration["RatesUrl"]); 39 | await seeding.SeedTransactions(app.Configuration["TransactionsUrl"]); 40 | } 41 | } 42 | 43 | app.UseHttpsRedirection(); 44 | app.UseAuthorization(); 45 | app.MapControllers(); 46 | 47 | app.Run(); 48 | -------------------------------------------------------------------------------- /CleanDds.Service/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:55556/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "launchUrl": "api/values", 15 | "environmentVariables": { 16 | "ASPNETCORE_ENVIRONMENT": "Development" 17 | } 18 | }, 19 | "CleanDds.Service": { 20 | "commandName": "Project", 21 | "launchBrowser": true, 22 | "launchUrl": "swagger", 23 | "applicationUrl": "http://localhost:5000/", 24 | "environmentVariables": { 25 | "ASPNETCORE_ENVIRONMENT": "Development" 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /CleanDds.Service/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Debug", 6 | "System": "Information", 7 | "Microsoft": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /CleanDds.Service/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "RatesUrl": "http://test.tests/rates.json", 3 | "TransactionsUrl": "http://test.tests/transactions.json", 4 | "Logging": { 5 | "IncludeScopes": false, 6 | "Debug": { 7 | "LogLevel": { 8 | "Default": "Warning" 9 | } 10 | }, 11 | "Console": { 12 | "LogLevel": { 13 | "Default": "Warning" 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /CleanDds.Testing/CleanDds.Testing.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net7.0 4 | false 5 | 6 | 7 | 8 | 9 | 10 | 11 | all 12 | runtime; build; native; contentfiles; analyzers 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /CleanDds.Testing/Handlers/DeleteAllRatesHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using CleanDds.Application.CommandStack.Rates; 7 | using CleanDds.Application.Interfaces; 8 | using CleanDds.Domain.Currencies; 9 | using FluentAssertions; 10 | using Microsoft.EntityFrameworkCore; 11 | using Microsoft.Extensions.Logging; 12 | using Moq; 13 | using Xunit; 14 | 15 | namespace CleanDds.Testing.Handlers; 16 | 17 | public class DeleteAllRatesHandlerTests 18 | { 19 | private readonly Mock _mockDatabaseService; 20 | private readonly Mock> _mockLogger; 21 | private readonly DeleteAllRatesHandler _handler; 22 | 23 | public DeleteAllRatesHandlerTests() 24 | { 25 | _mockDatabaseService = new Mock(); 26 | _mockLogger = new Mock>(); 27 | var serviceProvider = new Mock(); 28 | serviceProvider.Setup(sp => sp.GetService(typeof(ILogger))) 29 | .Returns(_mockLogger.Object); 30 | 31 | _handler = new DeleteAllRatesHandler(serviceProvider.Object, _mockDatabaseService.Object); 32 | } 33 | 34 | [Fact] 35 | public async Task Handle_ShouldDeleteAllRates() 36 | { 37 | // Arrange 38 | SetupMockRates(); 39 | 40 | // Act 41 | await _handler.Handle(new DeleteAllRates(), CancellationToken.None); 42 | 43 | // Assert 44 | _mockDatabaseService.Verify(db => db.Rates.RemoveRange(It.IsAny>()), Times.Once); 45 | _mockDatabaseService.Verify(db => db.Save(), Times.Once); 46 | } 47 | 48 | [Fact] 49 | public async Task Handle_ShouldLogInformation() 50 | { 51 | // Arrange 52 | SetupMockRates(); 53 | 54 | // Act 55 | await _handler.Handle(new DeleteAllRates(), CancellationToken.None); 56 | 57 | // Assert 58 | _mockLogger.Verify(log => log.Log( 59 | LogLevel.Information, 60 | It.IsAny(), 61 | It.Is((v, t) => v.ToString().Contains("Executing 'Delete All Rates' Command")), 62 | null, 63 | It.IsAny>()), Times.Once); 64 | } 65 | 66 | [Fact] 67 | public async Task Handle_WhenExceptionThrown_ShouldLogErrorAndRethrow() 68 | { 69 | // Arrange 70 | _mockDatabaseService.Setup(db => db.Rates).Throws(new Exception("Database Error")); 71 | 72 | // Act 73 | Func act = async () => await _handler.Handle(new DeleteAllRates(), CancellationToken.None); 74 | 75 | // Assert 76 | await act.Should().ThrowAsync().WithMessage("Database Error"); 77 | _mockLogger.Verify(log => log.Log( 78 | LogLevel.Error, 79 | It.IsAny(), 80 | It.Is((v, t) => v.ToString().Contains("Error executing 'Delete All Rates' command")), 81 | It.IsAny(), 82 | It.IsAny>()), Times.Once); 83 | } 84 | 85 | private void SetupMockRates() 86 | { 87 | var rates = new List { new(), new() }.AsQueryable(); 88 | var mockSet = new Mock>(); 89 | mockSet.As>().Setup(m => m.Provider).Returns(rates.Provider); 90 | mockSet.As>().Setup(m => m.Expression).Returns(rates.Expression); 91 | mockSet.As>().Setup(m => m.ElementType).Returns(rates.ElementType); 92 | mockSet.As>().Setup(m => m.GetEnumerator()).Returns(() => rates.GetEnumerator()); 93 | 94 | _mockDatabaseService.Setup(db => db.Rates).Returns(mockSet.Object); 95 | } 96 | } -------------------------------------------------------------------------------- /CleanDds.Testing/Handlers/DeleteAllTransactionsHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using CleanDds.Application.CommandStack.Transactions; 7 | using CleanDds.Application.Interfaces; 8 | using CleanDds.Domain.Currencies; 9 | using FluentAssertions; 10 | using Microsoft.EntityFrameworkCore; 11 | using Microsoft.Extensions.Logging; 12 | using Moq; 13 | using Xunit; 14 | 15 | namespace CleanDds.Testing.Handlers; 16 | 17 | public class DeleteAllTransactionsHandlerTests 18 | { 19 | private readonly Mock _mockDatabaseService; 20 | private readonly Mock> _mockLogger; 21 | private readonly DeleteAllTransactionsHandler _handler; 22 | 23 | public DeleteAllTransactionsHandlerTests() 24 | { 25 | _mockDatabaseService = new Mock(); 26 | _mockLogger = new Mock>(); 27 | var serviceProvider = new Mock(); 28 | serviceProvider.Setup(sp => sp.GetService(typeof(ILogger))) 29 | .Returns(_mockLogger.Object); 30 | 31 | _handler = new DeleteAllTransactionsHandler(serviceProvider.Object, _mockDatabaseService.Object); 32 | } 33 | 34 | [Fact] 35 | public async Task Handle_ShouldDeleteAllTransactions() 36 | { 37 | // Arrange 38 | SetupMockTransactions(new List { new(), new() }); 39 | 40 | // Act 41 | await _handler.Handle(new DeleteAllTransactions(), CancellationToken.None); 42 | 43 | // Assert 44 | _mockDatabaseService.Verify(db => db.Transactions.RemoveRange(It.IsAny>()), Times.Once); 45 | _mockDatabaseService.Verify(db => db.Save(), Times.Once); 46 | } 47 | 48 | [Fact] 49 | public async Task Handle_ShouldLogInformation() 50 | { 51 | SetupMockTransactions(); 52 | 53 | // Act 54 | await _handler.Handle(new DeleteAllTransactions(), CancellationToken.None); 55 | 56 | // Assert 57 | _mockLogger.Verify(log => log.Log( 58 | LogLevel.Information, 59 | It.IsAny(), 60 | It.Is((v, t) => v.ToString().Contains("Executing 'Delete All Transactions' Command")), 61 | null, 62 | It.IsAny>()), Times.Once); 63 | } 64 | 65 | [Fact] 66 | public async Task Handle_WhenExceptionThrown_ShouldLogErrorAndRethrow() 67 | { 68 | // Arrange 69 | _mockDatabaseService.Setup(db => db.Transactions).Throws(new Exception("Database Error")); 70 | 71 | // Act 72 | Func act = async () => await _handler.Handle(new DeleteAllTransactions(), CancellationToken.None); 73 | 74 | // Assert 75 | await act.Should().ThrowAsync().WithMessage("Database Error"); 76 | _mockLogger.Verify(log => log.Log( 77 | LogLevel.Error, 78 | It.IsAny(), 79 | It.Is((v, t) => v.ToString().Contains("Error executing 'Delete All Transactions' command")), 80 | It.IsAny(), 81 | It.IsAny>()), Times.Once); 82 | } 83 | 84 | private void SetupMockTransactions(IEnumerable rates = null) 85 | { 86 | rates ??= new List(); 87 | var queryableRates = rates.AsQueryable(); 88 | var mockSet = new Mock>(); 89 | mockSet.As>().Setup(m => m.Provider).Returns(queryableRates.Provider); 90 | mockSet.As>().Setup(m => m.Expression).Returns(queryableRates.Expression); 91 | mockSet.As>().Setup(m => m.ElementType).Returns(queryableRates.ElementType); 92 | mockSet.As>().Setup(m => m.GetEnumerator()).Returns(() => queryableRates.GetEnumerator()); 93 | 94 | _mockDatabaseService.Setup(db => db.Transactions).Returns(mockSet.Object); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /CleanDds.Testing/Handlers/SaveRatesHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using CleanDds.Application.CommandStack.Rates; 7 | using CleanDds.Application.Interfaces; 8 | using CleanDds.Domain.Currencies; 9 | using FluentAssertions; 10 | using Microsoft.EntityFrameworkCore; 11 | using Microsoft.Extensions.Logging; 12 | using Moq; 13 | using Xunit; 14 | 15 | namespace CleanDds.Testing.Handlers; 16 | 17 | public class SaveRatesHandlerTests 18 | { 19 | private readonly Mock _mockDatabaseService; 20 | private readonly Mock> _mockLogger; 21 | private readonly SaveRatesHandler _handler; 22 | 23 | public SaveRatesHandlerTests() 24 | { 25 | _mockDatabaseService = new Mock(); 26 | _mockLogger = new Mock>(); 27 | var serviceProvider = new Mock(); 28 | serviceProvider.Setup(sp => sp.GetService(typeof(ILogger))) 29 | .Returns(_mockLogger.Object); 30 | 31 | _handler = new SaveRatesHandler(serviceProvider.Object, _mockDatabaseService.Object); 32 | } 33 | 34 | [Fact] 35 | public async Task Handle_ShouldSaveRates() 36 | { 37 | // Arrange 38 | SetupMockRates(); 39 | var rates = new List{ new(), new() }; 40 | var request = new SaveRates { Rates = rates.ToArray() }; 41 | 42 | // Act 43 | await _handler.Handle(request, CancellationToken.None); 44 | 45 | // Assert 46 | _mockDatabaseService.Verify(db => db.Rates.Add(It.IsAny()), Times.Exactly(rates.Count)); 47 | _mockDatabaseService.Verify(db => db.Save(), Times.Once); 48 | } 49 | 50 | [Fact] 51 | public async Task Handle_ShouldLogInformation() 52 | { 53 | // Arrange 54 | SetupMockRates(); 55 | var rates = new List { new(), new() }; 56 | var request = new SaveRates { Rates = rates.ToArray() }; 57 | 58 | // Act 59 | await _handler.Handle(request, CancellationToken.None); 60 | 61 | // Assert 62 | _mockLogger.Verify(log => log.Log( 63 | LogLevel.Information, 64 | It.IsAny(), 65 | It.Is((v, t) => v.ToString().Contains("Executing 'Save Rates' Command")), 66 | null, 67 | It.IsAny>()), Times.Once); 68 | } 69 | 70 | [Fact] 71 | public async Task Handle_WhenExceptionThrown_ShouldLogErrorAndRethrow() 72 | { 73 | // Arrange 74 | var rates = new List { new(), new() }; 75 | var request = new SaveRates { Rates = rates.ToArray() }; 76 | _mockDatabaseService.Setup(db => db.Rates.Add(It.IsAny())).Throws(new Exception("Database Error")); 77 | 78 | // Act 79 | Func act = async () => await _handler.Handle(request, CancellationToken.None); 80 | 81 | // Assert 82 | await act.Should().ThrowAsync().WithMessage("Database Error"); 83 | _mockLogger.Verify(log => log.Log( 84 | LogLevel.Error, 85 | It.IsAny(), 86 | It.Is((v, t) => v.ToString().Contains("Error executing 'Save Rates' command")), 87 | It.IsAny(), 88 | It.IsAny>()), Times.Once); 89 | } 90 | 91 | private void SetupMockRates() 92 | { 93 | var rates = new List().AsQueryable(); 94 | var mockSet = new Mock>(); 95 | mockSet.As>().Setup(m => m.Provider).Returns(rates.Provider); 96 | mockSet.As>().Setup(m => m.Expression).Returns(rates.Expression); 97 | mockSet.As>().Setup(m => m.ElementType).Returns(rates.ElementType); 98 | mockSet.As>().Setup(m => m.GetEnumerator()).Returns(() => rates.GetEnumerator()); 99 | 100 | _mockDatabaseService.Setup(db => db.Rates).Returns(mockSet.Object); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /CleanDds.Testing/Handlers/SaveTransactionsHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using CleanDds.Application.CommandStack.Transactions; 7 | using CleanDds.Application.Interfaces; 8 | using CleanDds.Domain.Currencies; 9 | using FluentAssertions; 10 | using Microsoft.EntityFrameworkCore; 11 | using Microsoft.Extensions.Logging; 12 | using Moq; 13 | using Xunit; 14 | 15 | namespace CleanDds.Testing.Handlers; 16 | 17 | public class SaveTransactionsHandlerTests 18 | { 19 | private readonly Mock _mockDatabaseService; 20 | private readonly Mock> _mockLogger; 21 | private readonly SaveTransactionsHandler _handler; 22 | 23 | public SaveTransactionsHandlerTests() 24 | { 25 | _mockDatabaseService = new Mock(); 26 | _mockLogger = new Mock>(); 27 | var serviceProvider = new Mock(); 28 | serviceProvider.Setup(sp => sp.GetService(typeof(ILogger))) 29 | .Returns(_mockLogger.Object); 30 | 31 | _handler = new SaveTransactionsHandler(serviceProvider.Object, _mockDatabaseService.Object); 32 | } 33 | 34 | [Fact] 35 | public async Task Handle_ShouldSaveTransactions() 36 | { 37 | // Arrange 38 | var transactions = new List { new(), new() }.ToArray(); 39 | SetupMockTransactions(); 40 | 41 | // Act 42 | await _handler.Handle(new SaveTransactions { Transactions = transactions }, CancellationToken.None); 43 | 44 | // Assert 45 | _mockDatabaseService.Verify(db => db.Transactions.Add(It.IsAny()), Times.Exactly(transactions.Length)); 46 | _mockDatabaseService.Verify(db => db.Save(), Times.Once); 47 | } 48 | 49 | [Fact] 50 | public async Task Handle_ShouldLogInformation() 51 | { 52 | // Act 53 | await _handler.Handle(new SaveTransactions { Transactions = new List().ToArray() }, CancellationToken.None); 54 | 55 | // Assert 56 | _mockLogger.Verify(log => log.Log( 57 | LogLevel.Information, 58 | It.IsAny(), 59 | It.Is((v, t) => v.ToString().Contains("Executing 'Save Transactions' Command")), 60 | null, 61 | It.IsAny>()), Times.Once); 62 | } 63 | 64 | [Fact] 65 | public async Task Handle_WhenExceptionThrown_ShouldLogErrorAndRethrow() 66 | { 67 | // Arrange 68 | _mockDatabaseService.Setup(db => db.Transactions.Add(It.IsAny())).Throws(new Exception("Database Error")); 69 | 70 | // Act 71 | Func act = async () => await _handler.Handle(new SaveTransactions { Transactions = new List { new() }.ToArray() }, CancellationToken.None); 72 | 73 | // Assert 74 | await act.Should().ThrowAsync().WithMessage("Database Error"); 75 | _mockLogger.Verify(log => log.Log( 76 | LogLevel.Error, 77 | It.IsAny(), 78 | It.Is((v, t) => v.ToString().Contains("Error executing 'Save Transactions' command")), 79 | It.IsAny(), 80 | It.IsAny>()), Times.Once); 81 | } 82 | 83 | private void SetupMockTransactions() 84 | { 85 | var queryableRates = new List().AsQueryable(); 86 | var mockSet = new Mock>(); 87 | mockSet.As>().Setup(m => m.Provider).Returns(queryableRates.Provider); 88 | mockSet.As>().Setup(m => m.Expression).Returns(queryableRates.Expression); 89 | mockSet.As>().Setup(m => m.ElementType).Returns(queryableRates.ElementType); 90 | mockSet.As>().Setup(m => m.GetEnumerator()).Returns(() => queryableRates.GetEnumerator()); 91 | 92 | _mockDatabaseService.Setup(db => db.Transactions).Returns(mockSet.Object); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /CleanDds.Testing/MathTests.cs: -------------------------------------------------------------------------------- 1 | using CleanDds.Common.Numbers; 2 | using FluentAssertions; 3 | using Xunit; 4 | 5 | namespace CleanDds.Testing 6 | { 7 | public class MathTests 8 | { 9 | [Fact] 10 | public void RoundHalfToEven_ShouldRoundCorrectly() 11 | { 12 | // Act 13 | var result1 = Util.RoundHalfToEven(23.5m); 14 | var result2 = Util.RoundHalfToEven(24.5m); 15 | var result3 = Util.RoundHalfToEven(-23.5m); 16 | var result4 = Util.RoundHalfToEven(-24.5m); 17 | 18 | // Assert 19 | result1.Should().Be(24m, because: "23.5 should round to the nearest even number"); 20 | result2.Should().Be(24m, because: "24.5 should round down to the nearest even number"); 21 | result3.Should().Be(-24m, because: "-23.5 should round to the nearest even number"); 22 | result4.Should().Be(-24m, because: "-24.5 should round down to the nearest even number"); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /CleanDds.Testing/SerializationTests.cs: -------------------------------------------------------------------------------- 1 | using CleanDds.Domain.Common; 2 | using CleanDds.Domain.Currencies; 3 | using FluentAssertions; 4 | using Newtonsoft.Json; 5 | using Xunit; 6 | 7 | namespace CleanDds.Testing 8 | { 9 | public class SerializationTests 10 | { 11 | [Fact] 12 | public void RateSerialization_ShouldConvertRateObjectToJsonCorrectly() 13 | { 14 | // Arrange 15 | var entityTest = new Rate 16 | { 17 | CurrencyFrom = CurrencyType.AUD, 18 | CurrencyTo = CurrencyType.CAD, 19 | CurrencyRate = 0.88m 20 | }; 21 | 22 | // Act 23 | var converted = JsonConvert.SerializeObject(entityTest); 24 | 25 | // Assert 26 | converted.Should().Be(@"{""from"":""AUD"",""to"":""CAD"",""rate"":0.88}", 27 | because: "the serialized object should match the expected JSON string"); 28 | } 29 | 30 | [Fact] 31 | public void TransactionSerialization_ShouldConvertTransactionObjectToJsonCorrectly() 32 | { 33 | // Arrange 34 | var entityTest = new Transaction 35 | { 36 | Amount = 10.23m, 37 | Currency = CurrencyType.CAD, 38 | Sku = "T2006" 39 | }; 40 | 41 | // Act 42 | var converted = JsonConvert.SerializeObject(entityTest); 43 | 44 | // Assert 45 | converted.Should().Be(@"{""sku"":""T2006"",""amount"":10.23,""currency"":""CAD""}", 46 | because: "the serialized object should match the expected JSON string"); 47 | } 48 | } 49 | } --------------------------------------------------------------------------------