feat: work on implementing UoW pattern
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
using Bogus;
|
||||
|
||||
namespace SanctionsSearch.Worker.Tests.Fakes;
|
||||
|
||||
class SdnFaker : Faker<Sdn>
|
||||
{
|
||||
public SdnFaker()
|
||||
{
|
||||
RuleFor(x => x.Id, f => f.Random.Int(1, int.MaxValue));
|
||||
RuleFor(x => x.Name, f => f.Person.FullName);
|
||||
RuleFor(x => x.Type, f => f.Lorem.Word());
|
||||
RuleFor(x => x.Program, f => f.Lorem.Word());
|
||||
RuleFor(x => x.Title, f => f.Lorem.Word());
|
||||
RuleFor(x => x.CallSign, f => f.Lorem.Word());
|
||||
RuleFor(x => x.VesselType, f => f.Lorem.Word());
|
||||
RuleFor(x => x.Tonnage, f => f.Lorem.Word());
|
||||
RuleFor(x => x.GrossRegisteredTonnage, f => f.Lorem.Word());
|
||||
RuleFor(x => x.VesselFlag, f => f.Lorem.Word());
|
||||
RuleFor(x => x.VesselOwner, f => f.Lorem.Word());
|
||||
RuleFor(x => x.Remarks, f => f.Lorem.Word());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
using SanctionsSearch.Worker.Tests.Fakes;
|
||||
|
||||
namespace SanctionsSearch.Worker.Tests.Integration;
|
||||
|
||||
public class EfRepositoryTests : IAsyncLifetime
|
||||
{
|
||||
private readonly AppDbContext _context;
|
||||
private readonly EfRepository<Sdn> _sdnRepository;
|
||||
private readonly SdnFaker _sdnFaker = new();
|
||||
|
||||
public EfRepositoryTests()
|
||||
{
|
||||
var loggerFactory = LoggerFactory.Create(builder => builder.ClearProviders());
|
||||
|
||||
_context = new AppDbContext(new DbOptions { DatabaseName = $"{Guid.NewGuid()}.db" });
|
||||
_sdnRepository = new EfRepository<Sdn>(_context, loggerFactory.CreateLogger<EfRepository<Sdn>>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Upsert_WithNewEntity_ShouldAddEntityToDatabase()
|
||||
{
|
||||
var sdn = _sdnFaker.Generate();
|
||||
|
||||
await _sdnRepository.Upsert(sdn);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var result = await _context.Sdns.FindAsync(sdn.Id);
|
||||
|
||||
result.Should().BeEquivalentTo(sdn);
|
||||
result!.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
|
||||
result.UpdatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Upsert_WithExistingEntity_ShouldUpdateEntityInDatabase()
|
||||
{
|
||||
var sdn = _sdnFaker.Generate();
|
||||
|
||||
await _sdnRepository.Upsert(sdn);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var createdSdn = await _context.Sdns.FindAsync(sdn.Id);
|
||||
|
||||
createdSdn.Should().BeEquivalentTo(sdn);
|
||||
createdSdn!.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
|
||||
createdSdn.UpdatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
|
||||
|
||||
var updatedSdn = _sdnFaker.Generate();
|
||||
updatedSdn.Id = sdn.Id;
|
||||
|
||||
await _sdnRepository.Upsert(updatedSdn);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var result = await _context.Sdns.FindAsync(sdn.Id);
|
||||
|
||||
result.Should().BeEquivalentTo(updatedSdn);
|
||||
result!.CreatedAt.Should().BeSameDateAs(createdSdn.CreatedAt);
|
||||
result.UpdatedAt.Should().BeAfter(createdSdn.UpdatedAt);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
await _context.Database.MigrateAsync();
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await _context.DisposeAsync();
|
||||
|
||||
SqliteConnection.ClearAllPools();
|
||||
|
||||
File.Delete(_context.DatabasePath);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Bogus" Version="35.6.0" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.70" />
|
||||
|
||||
@@ -9,3 +9,5 @@ global using RichardSzalay.MockHttp;
|
||||
|
||||
global using SanctionsSearch.Worker.Services;
|
||||
global using SanctionsSearch.Worker.Options;
|
||||
global using SanctionsSearch.Worker.Models;
|
||||
global using SanctionsSearch.Worker.Persistence;
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace SanctionsSearch.Worker.Interfaces;
|
||||
|
||||
interface IRepository<T> where T : Entity
|
||||
{
|
||||
Task Upsert(T entity);
|
||||
Task<IEnumerable<T>> Find(Expression<Func<T, bool>> predicate);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace SanctionsSearch.Worker.Interfaces;
|
||||
|
||||
interface ISdnRepository : IRepository<Sdn>
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace SanctionsSearch.Worker.Interfaces;
|
||||
|
||||
interface IUnitOfWork
|
||||
{
|
||||
ISdnRepository Sdns { get; }
|
||||
Task SaveChangesAsync();
|
||||
}
|
||||
Generated
+222
@@ -0,0 +1,222 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using SanctionsSearch.Worker.Persistence;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace SanctionsSearch.Worker.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20240821035135_update_models_with_timestamps")]
|
||||
partial class update_models_with_timestamps
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.8");
|
||||
|
||||
modelBuilder.Entity("SanctionsSearch.Worker.Models.Address", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("CityProvincePostal")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Country")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Remarks")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("SdnId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("StreetAddress")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SdnId");
|
||||
|
||||
b.ToTable("Addresses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SanctionsSearch.Worker.Models.Alias", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Remarks")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("SdnId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SdnId");
|
||||
|
||||
b.ToTable("Aliases");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SanctionsSearch.Worker.Models.Comment", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Remarks")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("SdnId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SdnId");
|
||||
|
||||
b.ToTable("Comments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SanctionsSearch.Worker.Models.Sdn", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("CallSign")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("GrossRegisteredTonnage")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Program")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Remarks")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Tonnage")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("VesselFlag")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("VesselOwner")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("VesselType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Sdns");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SanctionsSearch.Worker.Models.Address", b =>
|
||||
{
|
||||
b.HasOne("SanctionsSearch.Worker.Models.Sdn", "Sdn")
|
||||
.WithMany()
|
||||
.HasForeignKey("SdnId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Sdn");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SanctionsSearch.Worker.Models.Alias", b =>
|
||||
{
|
||||
b.HasOne("SanctionsSearch.Worker.Models.Sdn", "Sdn")
|
||||
.WithMany("Aliases")
|
||||
.HasForeignKey("SdnId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Sdn");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SanctionsSearch.Worker.Models.Comment", b =>
|
||||
{
|
||||
b.HasOne("SanctionsSearch.Worker.Models.Sdn", "Sdn")
|
||||
.WithMany()
|
||||
.HasForeignKey("SdnId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Sdn");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SanctionsSearch.Worker.Models.Sdn", b =>
|
||||
{
|
||||
b.Navigation("Aliases");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace SanctionsSearch.Worker.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class update_models_with_timestamps : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "CreatedAt",
|
||||
table: "Sdns",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "UpdatedAt",
|
||||
table: "Sdns",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "CreatedAt",
|
||||
table: "Comments",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "UpdatedAt",
|
||||
table: "Comments",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "CreatedAt",
|
||||
table: "Aliases",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "UpdatedAt",
|
||||
table: "Aliases",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "CreatedAt",
|
||||
table: "Addresses",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "UpdatedAt",
|
||||
table: "Addresses",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CreatedAt",
|
||||
table: "Sdns");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "UpdatedAt",
|
||||
table: "Sdns");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CreatedAt",
|
||||
table: "Comments");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "UpdatedAt",
|
||||
table: "Comments");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CreatedAt",
|
||||
table: "Aliases");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "UpdatedAt",
|
||||
table: "Aliases");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CreatedAt",
|
||||
table: "Addresses");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "UpdatedAt",
|
||||
table: "Addresses");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
@@ -30,6 +31,9 @@ namespace SanctionsSearch.Worker.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Remarks")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
@@ -41,6 +45,9 @@ namespace SanctionsSearch.Worker.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SdnId");
|
||||
@@ -54,6 +61,9 @@ namespace SanctionsSearch.Worker.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
@@ -69,6 +79,9 @@ namespace SanctionsSearch.Worker.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SdnId");
|
||||
@@ -82,6 +95,9 @@ namespace SanctionsSearch.Worker.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Remarks")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
@@ -89,6 +105,9 @@ namespace SanctionsSearch.Worker.Migrations
|
||||
b.Property<int>("SdnId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SdnId");
|
||||
@@ -106,6 +125,9 @@ namespace SanctionsSearch.Worker.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("GrossRegisteredTonnage")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
@@ -134,6 +156,9 @@ namespace SanctionsSearch.Worker.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("VesselFlag")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
namespace SanctionsSearch.Worker.Models;
|
||||
|
||||
class Address
|
||||
class Address : Entity
|
||||
{
|
||||
public int SdnId { get; set; }
|
||||
public int Id { get; set; }
|
||||
public string StreetAddress { get; set; } = string.Empty;
|
||||
public string CityProvincePostal { get; set; } = string.Empty;
|
||||
public string Country { get; set; } = string.Empty;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
namespace SanctionsSearch.Worker.Models;
|
||||
|
||||
class Alias
|
||||
class Alias : Entity
|
||||
{
|
||||
public int SdnId { get; set; }
|
||||
public int Id { get; set; }
|
||||
public string Type { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Remarks { get; set; } = string.Empty;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
namespace SanctionsSearch.Worker.Models;
|
||||
|
||||
class Comment
|
||||
class Comment : Entity
|
||||
{
|
||||
public int SdnId { get; set; }
|
||||
public int Id { get; set; }
|
||||
public string Remarks { get; set; } = string.Empty;
|
||||
|
||||
public virtual Sdn Sdn { get; set; } = default!;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace SanctionsSearch.Worker.Models;
|
||||
|
||||
abstract class Entity
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
namespace SanctionsSearch.Worker.Models;
|
||||
|
||||
class Sdn
|
||||
class Sdn : Entity
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Type { get; set; } = string.Empty;
|
||||
public string Program { get; set; } = string.Empty;
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
using System.Reflection;
|
||||
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace SanctionsSearch.Worker.Persistence;
|
||||
|
||||
class AppDbContext(IOptionsSnapshot<DbOptions> options) : DbContext, IDisposable
|
||||
class AppDbContext(DbOptions options) : DbContext
|
||||
{
|
||||
private readonly IOptionsSnapshot<DbOptions> _options = options;
|
||||
private readonly DbOptions _options = options;
|
||||
private SqliteConnection? _connection;
|
||||
internal string DatabasePath { get; private set; } = string.Empty;
|
||||
public DbSet<Sdn> Sdns { get; set; } = default!;
|
||||
public DbSet<Address> Addresses { get; set; } = default!;
|
||||
public DbSet<Alias> Aliases { get; set; } = default!;
|
||||
@@ -24,7 +21,8 @@ class AppDbContext(IOptionsSnapshot<DbOptions> options) : DbContext, IDisposable
|
||||
Directory.CreateDirectory(dataDir);
|
||||
}
|
||||
|
||||
var dbPath = Path.Combine(dataDir, _options.Value.DatabaseName);
|
||||
var dbPath = Path.Combine(dataDir, _options.DatabaseName);
|
||||
DatabasePath = dbPath;
|
||||
|
||||
_connection = new SqliteConnection($"Data Source={dbPath}");
|
||||
_connection.Open();
|
||||
@@ -32,9 +30,40 @@ class AppDbContext(IOptionsSnapshot<DbOptions> options) : DbContext, IDisposable
|
||||
optionsBuilder.UseSqlite(_connection);
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
public async override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
_connection?.Dispose();
|
||||
base.Dispose();
|
||||
// TODO: Use TimeProvider instead of DateTime.UtcNow
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
foreach (var changedEntity in ChangeTracker.Entries())
|
||||
{
|
||||
if (changedEntity.Entity is Entity entity)
|
||||
{
|
||||
switch (changedEntity.State)
|
||||
{
|
||||
case EntityState.Added:
|
||||
entity.CreatedAt = now;
|
||||
entity.UpdatedAt = now;
|
||||
break;
|
||||
|
||||
case EntityState.Modified:
|
||||
Entry(entity).Property(x => x.CreatedAt).IsModified = false;
|
||||
entity.UpdatedAt = now;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await base.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async override ValueTask DisposeAsync()
|
||||
{
|
||||
if (_connection is not null)
|
||||
{
|
||||
await _connection.DisposeAsync();
|
||||
}
|
||||
|
||||
await base.DisposeAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
namespace SanctionsSearch.Worker.Persistence;
|
||||
|
||||
class EfRepository<T> : IRepository<T> where T : Entity
|
||||
{
|
||||
protected readonly AppDbContext _context;
|
||||
protected readonly ILogger<EfRepository<T>> _logger;
|
||||
|
||||
public EfRepository(AppDbContext context, ILogger<EfRepository<T>> logger)
|
||||
{
|
||||
_context = context;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<T>> Find(Expression<Func<T, bool>> predicate)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _context.Set<T>().Where(predicate).ToListAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error finding entities of type {Type}", typeof(T).Name);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Upsert(T entity)
|
||||
{
|
||||
try
|
||||
{
|
||||
var existing = await _context.Set<T>().FindAsync(entity.Id);
|
||||
|
||||
if (existing is null)
|
||||
{
|
||||
await _context.Set<T>().AddAsync(entity);
|
||||
return;
|
||||
}
|
||||
|
||||
_context.Entry(existing).CurrentValues.SetValues(entity);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error upserting entity of type {Type} with {Id}", typeof(T).Name, entity.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace SanctionsSearch.Worker.Persistence;
|
||||
|
||||
class EfUnitOfWork(AppDbContext context, ILoggerFactory loggerFactory) : IUnitOfWork, IAsyncDisposable
|
||||
{
|
||||
private readonly AppDbContext _context = context;
|
||||
public ISdnRepository Sdns { get; } = new SdnRepository(context, loggerFactory.CreateLogger<SdnRepository>());
|
||||
|
||||
public async Task SaveChangesAsync()
|
||||
{
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await _context.DisposeAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace SanctionsSearch.Worker.Persistence;
|
||||
|
||||
class SdnRepository(
|
||||
AppDbContext dbContext,
|
||||
ILogger<SdnRepository> logger
|
||||
) : EfRepository<Sdn>(dbContext, logger), ISdnRepository
|
||||
{
|
||||
}
|
||||
@@ -31,6 +31,7 @@ try
|
||||
|
||||
builder.Services.ConfigureOptions<OfacFileServiceOptionsSetup>();
|
||||
builder.Services.ConfigureOptions<DbOptionsSetup>();
|
||||
builder.Services.AddScoped(rs => rs.GetRequiredService<IOptionsSnapshot<DbOptions>>().Value);
|
||||
|
||||
builder.Services.AddDbContext<AppDbContext>();
|
||||
builder.Services.AddHostedService<Worker>();
|
||||
|
||||
@@ -10,3 +10,9 @@ global using Serilog.Exceptions;
|
||||
global using Serilog.Formatting.Compact;
|
||||
|
||||
global using Microsoft.EntityFrameworkCore;
|
||||
|
||||
global using System.Linq.Expressions;
|
||||
|
||||
global using System.Reflection;
|
||||
|
||||
global using Microsoft.Data.Sqlite;
|
||||
Reference in New Issue
Block a user