├── .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 | }
--------------------------------------------------------------------------------