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>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Bogus" Version="35.6.0" />
|
||||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||||
<PackageReference Include="Moq" Version="4.20.70" />
|
<PackageReference Include="Moq" Version="4.20.70" />
|
||||||
|
|||||||
@@ -8,4 +8,6 @@ global using Moq;
|
|||||||
global using RichardSzalay.MockHttp;
|
global using RichardSzalay.MockHttp;
|
||||||
|
|
||||||
global using SanctionsSearch.Worker.Services;
|
global using SanctionsSearch.Worker.Services;
|
||||||
global using SanctionsSearch.Worker.Options;
|
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 />
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
@@ -30,6 +31,9 @@ namespace SanctionsSearch.Worker.Migrations
|
|||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("Remarks")
|
b.Property<string>("Remarks")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
@@ -41,6 +45,9 @@ namespace SanctionsSearch.Worker.Migrations
|
|||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("SdnId");
|
b.HasIndex("SdnId");
|
||||||
@@ -54,6 +61,9 @@ namespace SanctionsSearch.Worker.Migrations
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
@@ -69,6 +79,9 @@ namespace SanctionsSearch.Worker.Migrations
|
|||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("SdnId");
|
b.HasIndex("SdnId");
|
||||||
@@ -82,6 +95,9 @@ namespace SanctionsSearch.Worker.Migrations
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("Remarks")
|
b.Property<string>("Remarks")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
@@ -89,6 +105,9 @@ namespace SanctionsSearch.Worker.Migrations
|
|||||||
b.Property<int>("SdnId")
|
b.Property<int>("SdnId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("SdnId");
|
b.HasIndex("SdnId");
|
||||||
@@ -106,6 +125,9 @@ namespace SanctionsSearch.Worker.Migrations
|
|||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("GrossRegisteredTonnage")
|
b.Property<string>("GrossRegisteredTonnage")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
@@ -134,6 +156,9 @@ namespace SanctionsSearch.Worker.Migrations
|
|||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("VesselFlag")
|
b.Property<string>("VesselFlag")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
namespace SanctionsSearch.Worker.Models;
|
namespace SanctionsSearch.Worker.Models;
|
||||||
|
|
||||||
class Address
|
class Address : Entity
|
||||||
{
|
{
|
||||||
public int SdnId { get; set; }
|
public int SdnId { get; set; }
|
||||||
public int Id { get; set; }
|
|
||||||
public string StreetAddress { get; set; } = string.Empty;
|
public string StreetAddress { get; set; } = string.Empty;
|
||||||
public string CityProvincePostal { get; set; } = string.Empty;
|
public string CityProvincePostal { get; set; } = string.Empty;
|
||||||
public string Country { get; set; } = string.Empty;
|
public string Country { get; set; } = string.Empty;
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
namespace SanctionsSearch.Worker.Models;
|
namespace SanctionsSearch.Worker.Models;
|
||||||
|
|
||||||
class Alias
|
class Alias : Entity
|
||||||
{
|
{
|
||||||
public int SdnId { get; set; }
|
public int SdnId { get; set; }
|
||||||
public int Id { get; set; }
|
|
||||||
public string Type { get; set; } = string.Empty;
|
public string Type { get; set; } = string.Empty;
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
public string Remarks { get; set; } = string.Empty;
|
public string Remarks { get; set; } = string.Empty;
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
namespace SanctionsSearch.Worker.Models;
|
namespace SanctionsSearch.Worker.Models;
|
||||||
|
|
||||||
class Comment
|
class Comment : Entity
|
||||||
{
|
{
|
||||||
public int SdnId { get; set; }
|
public int SdnId { get; set; }
|
||||||
public int Id { get; set; }
|
|
||||||
public string Remarks { get; set; } = string.Empty;
|
public string Remarks { get; set; } = string.Empty;
|
||||||
|
|
||||||
public virtual Sdn Sdn { get; set; } = default!;
|
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;
|
namespace SanctionsSearch.Worker.Models;
|
||||||
|
|
||||||
class Sdn
|
class Sdn : Entity
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
public string Type { get; set; } = string.Empty;
|
public string Type { get; set; } = string.Empty;
|
||||||
public string Program { 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;
|
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;
|
private SqliteConnection? _connection;
|
||||||
|
internal string DatabasePath { get; private set; } = string.Empty;
|
||||||
public DbSet<Sdn> Sdns { get; set; } = default!;
|
public DbSet<Sdn> Sdns { get; set; } = default!;
|
||||||
public DbSet<Address> Addresses { get; set; } = default!;
|
public DbSet<Address> Addresses { get; set; } = default!;
|
||||||
public DbSet<Alias> Aliases { get; set; } = default!;
|
public DbSet<Alias> Aliases { get; set; } = default!;
|
||||||
@@ -24,7 +21,8 @@ class AppDbContext(IOptionsSnapshot<DbOptions> options) : DbContext, IDisposable
|
|||||||
Directory.CreateDirectory(dataDir);
|
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 = new SqliteConnection($"Data Source={dbPath}");
|
||||||
_connection.Open();
|
_connection.Open();
|
||||||
@@ -32,9 +30,40 @@ class AppDbContext(IOptionsSnapshot<DbOptions> options) : DbContext, IDisposable
|
|||||||
optionsBuilder.UseSqlite(_connection);
|
optionsBuilder.UseSqlite(_connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Dispose()
|
public async override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
_connection?.Dispose();
|
// TODO: Use TimeProvider instead of DateTime.UtcNow
|
||||||
base.Dispose();
|
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<OfacFileServiceOptionsSetup>();
|
||||||
builder.Services.ConfigureOptions<DbOptionsSetup>();
|
builder.Services.ConfigureOptions<DbOptionsSetup>();
|
||||||
|
builder.Services.AddScoped(rs => rs.GetRequiredService<IOptionsSnapshot<DbOptions>>().Value);
|
||||||
|
|
||||||
builder.Services.AddDbContext<AppDbContext>();
|
builder.Services.AddDbContext<AppDbContext>();
|
||||||
builder.Services.AddHostedService<Worker>();
|
builder.Services.AddHostedService<Worker>();
|
||||||
|
|||||||
@@ -9,4 +9,10 @@ global using Serilog;
|
|||||||
global using Serilog.Exceptions;
|
global using Serilog.Exceptions;
|
||||||
global using Serilog.Formatting.Compact;
|
global using Serilog.Formatting.Compact;
|
||||||
|
|
||||||
global using Microsoft.EntityFrameworkCore;
|
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