From 1a57cb6c91ccec86f147f87047ea568e8bcc8369 Mon Sep 17 00:00:00 2001 From: Andrew Petrukhin Date: Wed, 29 Oct 2025 16:58:05 +0300 Subject: [PATCH] feat: replace concurent dict with in-memory database --- AuthService.Test/AuthService.Test.csproj | 1 + AuthService.Test/AuthServiceTests.cs | 29 ++++++++++++--- AuthService/AuthService.csproj | 2 + AuthService/DataAccess/EF/AuthDbContext.cs | 37 +++++++++++++++++++ AuthService/DataAccess/EF/EFInstaller.cs | 20 ++++++++++ .../DataAccess/EF/InsuranceAgentRepository.cs | 27 ++++++++++++++ .../DataAccess/InsuranceAgentsInMemoryDb.cs | 30 --------------- AuthService/Domain/InsuranceAgent.cs | 16 +++++--- .../Init/ApplicationBuilderExtensions.cs | 16 ++++++++ AuthService/Init/DataLoader.cs | 31 ++++++++++++++++ AuthService/Init/DataLoaderInstaller.cs | 12 ++++++ AuthService/Startup.cs | 9 ++++- 12 files changed, 188 insertions(+), 42 deletions(-) create mode 100644 AuthService/DataAccess/EF/AuthDbContext.cs create mode 100644 AuthService/DataAccess/EF/EFInstaller.cs create mode 100644 AuthService/DataAccess/EF/InsuranceAgentRepository.cs delete mode 100644 AuthService/DataAccess/InsuranceAgentsInMemoryDb.cs create mode 100644 AuthService/Init/ApplicationBuilderExtensions.cs create mode 100644 AuthService/Init/DataLoader.cs create mode 100644 AuthService/Init/DataLoaderInstaller.cs diff --git a/AuthService.Test/AuthService.Test.csproj b/AuthService.Test/AuthService.Test.csproj index e64cc4f9..58b81b27 100644 --- a/AuthService.Test/AuthService.Test.csproj +++ b/AuthService.Test/AuthService.Test.csproj @@ -20,6 +20,7 @@ + diff --git a/AuthService.Test/AuthServiceTests.cs b/AuthService.Test/AuthServiceTests.cs index 1cb310ee..c81577ed 100644 --- a/AuthService.Test/AuthServiceTests.cs +++ b/AuthService.Test/AuthServiceTests.cs @@ -1,6 +1,9 @@ using System; -using AuthService.DataAccess; +using System.Collections.Generic; +using AuthService.DataAccess.EF; +using AuthService.Domain; using FluentAssertions; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using Moq; using Xunit; @@ -11,21 +14,37 @@ namespace AuthService.Test; public class AuthServiceTests { private readonly Domain.AuthService authService; - private readonly InsuranceAgentsInMemoryDb agentsDb; + private readonly IInsuranceAgents agentsRepository; private readonly AppSettings appSettings; private readonly ITestOutputHelper output; public AuthServiceTests(ITestOutputHelper output) { this.output = output; - agentsDb = new InsuranceAgentsInMemoryDb(); + + // Setup EF Core in-memory database + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .Options; + + var dbContext = new AuthDbContext(options); + agentsRepository = new InsuranceAgentRepository(dbContext); + + // Seed test data + agentsRepository.Add(new InsuranceAgent("jimmy.solid", "secret", "static/avatars/jimmy_solid.png", + new List { "TRI", "HSI", "FAI", "CAR" })); + agentsRepository.Add(new InsuranceAgent("danny.solid", "secret", "static/avatars/danny.solid.png", + new List { "TRI", "HSI", "FAI", "CAR" })); + agentsRepository.Add(new InsuranceAgent("admin", "admin", "static/avatars/admin.png", + new List { "TRI", "HSI", "FAI", "CAR" })); + appSettings = new AppSettings { Secret = "ThisIsASecretKeyForJWTTokenGeneration123456789" }; - var options = Options.Create(appSettings); + var appSettingsOptions = Options.Create(appSettings); - authService = new Domain.AuthService(agentsDb, options); + authService = new Domain.AuthService(agentsRepository, appSettingsOptions); } [Theory] diff --git a/AuthService/AuthService.csproj b/AuthService/AuthService.csproj index 18a0c100..4a3d3617 100644 --- a/AuthService/AuthService.csproj +++ b/AuthService/AuthService.csproj @@ -8,6 +8,8 @@ + + diff --git a/AuthService/DataAccess/EF/AuthDbContext.cs b/AuthService/DataAccess/EF/AuthDbContext.cs new file mode 100644 index 00000000..b21ef9a4 --- /dev/null +++ b/AuthService/DataAccess/EF/AuthDbContext.cs @@ -0,0 +1,37 @@ +using System; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using AuthService.Domain; + +namespace AuthService.DataAccess.EF; + +public class AuthDbContext : DbContext +{ + public AuthDbContext(DbContextOptions options) : base(options) + { + } + + public DbSet InsuranceAgents { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.EnableSensitiveDataLogging(); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.HasIndex(e => e.Login).IsUnique(); + entity.Property(e => e.Login).IsRequired(); + entity.Property(e => e.Password).IsRequired(); + entity.Property(e => e.Avatar).IsRequired(); + entity.Property(e => e.AvailableProducts) + .HasConversion( + v => string.Join(',', v), + v => v.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList() + ); + }); + } +} diff --git a/AuthService/DataAccess/EF/EFInstaller.cs b/AuthService/DataAccess/EF/EFInstaller.cs new file mode 100644 index 00000000..07239ed8 --- /dev/null +++ b/AuthService/DataAccess/EF/EFInstaller.cs @@ -0,0 +1,20 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using AuthService.Domain; + +namespace AuthService.DataAccess.EF; + +public static class EFInstaller +{ + public static IServiceCollection AddEFConfiguration(this IServiceCollection services, IConfiguration configuration) + { + services.AddDbContext(options => + { + options.UseInMemoryDatabase("InsuranceAgents"); + }); + + services.AddScoped(); + return services; + } +} diff --git a/AuthService/DataAccess/EF/InsuranceAgentRepository.cs b/AuthService/DataAccess/EF/InsuranceAgentRepository.cs new file mode 100644 index 00000000..04906ab5 --- /dev/null +++ b/AuthService/DataAccess/EF/InsuranceAgentRepository.cs @@ -0,0 +1,27 @@ +using System; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using AuthService.Domain; + +namespace AuthService.DataAccess.EF; + +public class InsuranceAgentRepository : IInsuranceAgents +{ + private readonly AuthDbContext dbContext; + + public InsuranceAgentRepository(AuthDbContext dbContext) + { + this.dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); + } + + public void Add(InsuranceAgent agent) + { + dbContext.InsuranceAgents.Add(agent); + } + + public InsuranceAgent FindByLogin(string login) + { + return dbContext.InsuranceAgents + .FirstOrDefault(a => a.Login == login); + } +} diff --git a/AuthService/DataAccess/InsuranceAgentsInMemoryDb.cs b/AuthService/DataAccess/InsuranceAgentsInMemoryDb.cs deleted file mode 100644 index 26c8c88c..00000000 --- a/AuthService/DataAccess/InsuranceAgentsInMemoryDb.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Collections.Concurrent; -using System.Collections.Generic; -using AuthService.Domain; - -namespace AuthService.DataAccess; - -public class InsuranceAgentsInMemoryDb : IInsuranceAgents -{ - private readonly IDictionary db = new ConcurrentDictionary(); - - public InsuranceAgentsInMemoryDb() - { - Add(new InsuranceAgent("jimmy.solid", "secret", "static/avatars/jimmy_solid.png", - new List { "TRI", "HSI", "FAI", "CAR" })); - Add(new InsuranceAgent("danny.solid", "secret", "static/avatars/danny.solid.png", - new List { "TRI", "HSI", "FAI", "CAR" })); - Add(new InsuranceAgent("admin", "admin", "static/avatars/admin.png", - new List { "TRI", "HSI", "FAI", "CAR" })); - } - - public void Add(InsuranceAgent agent) - { - db[agent.Login] = agent; - } - - public InsuranceAgent FindByLogin(string login) - { - return db.TryGetValue(login, out var agent) ? agent : null; - } -} \ No newline at end of file diff --git a/AuthService/Domain/InsuranceAgent.cs b/AuthService/Domain/InsuranceAgent.cs index cc2543cf..0bd6206d 100644 --- a/AuthService/Domain/InsuranceAgent.cs +++ b/AuthService/Domain/InsuranceAgent.cs @@ -4,6 +4,11 @@ namespace AuthService.Domain; public class InsuranceAgent { + // EF Core requires a parameterless constructor + private InsuranceAgent() + { + } + public InsuranceAgent(string login, string password, string avatar, List availableProducts) { Login = login; @@ -12,13 +17,14 @@ public InsuranceAgent(string login, string password, string avatar, List AvailableProducts = availableProducts; } - public string Login { get; } - public string Password { get; } - public string Avatar { get; } - public List AvailableProducts { get; } + public int Id { get; private set; } + public string Login { get; private set; } + public string Password { get; private set; } + public string Avatar { get; private set; } + public List AvailableProducts { get; private set; } public bool PasswordMatches(string passwordToTest) { return Password == passwordToTest; } -} \ No newline at end of file +} diff --git a/AuthService/Init/ApplicationBuilderExtensions.cs b/AuthService/Init/ApplicationBuilderExtensions.cs new file mode 100644 index 00000000..307f4333 --- /dev/null +++ b/AuthService/Init/ApplicationBuilderExtensions.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace AuthService.Init; + +public static class ApplicationBuilderExtensions +{ + public static void UseInitializer(this IApplicationBuilder app) + { + using (var scope = app.ApplicationServices.CreateScope()) + { + var initializer = scope.ServiceProvider.GetService(); + initializer.Seed(); + } + } +} diff --git a/AuthService/Init/DataLoader.cs b/AuthService/Init/DataLoader.cs new file mode 100644 index 00000000..a126f9a4 --- /dev/null +++ b/AuthService/Init/DataLoader.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Linq; +using AuthService.DataAccess.EF; +using AuthService.Domain; + +namespace AuthService.Init; + +public class DataLoader +{ + private readonly AuthDbContext dbContext; + + public DataLoader(AuthDbContext context) + { + dbContext = context; + } + + public void Seed() + { + dbContext.Database.EnsureCreated(); + if (dbContext.InsuranceAgents.Any()) return; + + dbContext.InsuranceAgents.Add(new InsuranceAgent("jimmy.solid", "secret", "static/avatars/jimmy_solid.png", + new List { "TRI", "HSI", "FAI", "CAR" })); + dbContext.InsuranceAgents.Add(new InsuranceAgent("danny.solid", "secret", "static/avatars/danny.solid.png", + new List { "TRI", "HSI", "FAI", "CAR" })); + dbContext.InsuranceAgents.Add(new InsuranceAgent("admin", "admin", "static/avatars/admin.png", + new List { "TRI", "HSI", "FAI", "CAR" })); + + dbContext.SaveChanges(); + } +} diff --git a/AuthService/Init/DataLoaderInstaller.cs b/AuthService/Init/DataLoaderInstaller.cs new file mode 100644 index 00000000..6f00c364 --- /dev/null +++ b/AuthService/Init/DataLoaderInstaller.cs @@ -0,0 +1,12 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace AuthService.Init; + +public static class DataLoaderInstaller +{ + public static IServiceCollection AddAuthDemoInitializer(this IServiceCollection services) + { + services.AddScoped(); + return services; + } +} diff --git a/AuthService/Startup.cs b/AuthService/Startup.cs index 725a2f47..9dfef9d1 100644 --- a/AuthService/Startup.cs +++ b/AuthService/Startup.cs @@ -1,6 +1,7 @@ using System.Text; -using AuthService.DataAccess; +using AuthService.DataAccess.EF; using AuthService.Domain; +using AuthService.Init; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -59,7 +60,8 @@ public void ConfigureServices(IServiceCollection services) }); services.AddSingleton(); - services.AddSingleton(); + services.AddEFConfiguration(Configuration); + services.AddAuthDemoInitializer(); services.AddSwaggerGen(); } @@ -84,6 +86,9 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseAuthorization(); app.UseHttpsRedirection(); + + // Ensure initializer is awaited so seeding completes before the app starts handling requests + app.UseInitializer(); app.UseEndpoints(endpoints => endpoints.MapControllers()); } } \ No newline at end of file