diff --git a/Directory.Packages.props b/Directory.Packages.props
index 1587781..0830b7f 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -17,5 +17,13 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Nocr.Users.sln b/Nocr.Users.sln
index 47434f5..1785430 100644
--- a/Nocr.Users.sln
+++ b/Nocr.Users.sln
@@ -16,6 +16,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nocr.Users.Host", "src\Nocr
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nocr.Users.Api.Contracts", "src\Nocr.Users.Api.Contracts\Nocr.Users.Api.Contracts.csproj", "{4DDFB05F-DFC7-4BD6-B593-AD52CF1B002E}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Contracts", "Contracts", "{CA3AA87B-9217-4A2D-9EBA-BFC415B63818}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nocr.Users.Persistence", "src\Nocr.Users.Persistence\Nocr.Users.Persistence.csproj", "{51C01BA8-E3E1-45F4-9DDF-6E08DAF5BB46}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -38,5 +42,12 @@ Global
{4DDFB05F-DFC7-4BD6-B593-AD52CF1B002E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4DDFB05F-DFC7-4BD6-B593-AD52CF1B002E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4DDFB05F-DFC7-4BD6-B593-AD52CF1B002E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {51C01BA8-E3E1-45F4-9DDF-6E08DAF5BB46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {51C01BA8-E3E1-45F4-9DDF-6E08DAF5BB46}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {51C01BA8-E3E1-45F4-9DDF-6E08DAF5BB46}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {51C01BA8-E3E1-45F4-9DDF-6E08DAF5BB46}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {4DDFB05F-DFC7-4BD6-B593-AD52CF1B002E} = {CA3AA87B-9217-4A2D-9EBA-BFC415B63818}
EndGlobalSection
EndGlobal
diff --git a/src/Nocr.Users.AppServices/ServiceCollectionExtensions.cs b/src/Nocr.Users.AppServices/ServiceCollectionExtensions.cs
index 8a73ad2..f3c5a10 100644
--- a/src/Nocr.Users.AppServices/ServiceCollectionExtensions.cs
+++ b/src/Nocr.Users.AppServices/ServiceCollectionExtensions.cs
@@ -1,5 +1,4 @@
using Microsoft.Extensions.DependencyInjection;
-using Nocr.Users.AppServices.Users.Repositories;
using Nocr.Users.AppServices.Users.Services;
namespace Nocr.Users.AppServices;
@@ -11,8 +10,6 @@ public static class ServiceCollectionExtensions
if (services == null)
throw new ArgumentNullException(nameof(services));
- // Add registrations here
- services.AddSingleton();
services.AddScoped();
return services;
diff --git a/src/Nocr.Users.AppServices/Users/Repositories/InMemoryUsersRepository.cs b/src/Nocr.Users.AppServices/Users/Repositories/InMemoryUsersRepository.cs
deleted file mode 100644
index 6d71113..0000000
--- a/src/Nocr.Users.AppServices/Users/Repositories/InMemoryUsersRepository.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-using Nocr.Users.Api.Contracts.Users;
-
-namespace Nocr.Users.AppServices.Users.Repositories;
-
-public sealed class InMemoryUsersRepository : IUsersRepository
-{
- private long _id = 0;
-
- private long _userIdentityId = 0;
-
- private readonly List _users = new();
-
- public Task Create(User user, CancellationToken cancellationToken = default)
- {
- var id = Interlocked.Increment(ref _id);
- user.Id = id;
- _users.Add(user);
-
- foreach (var identity in user.Identities)
- {
- identity.Id = Interlocked.Increment(ref _userIdentityId);
- }
-
- return Task.FromResult(id);
- }
-
- public Task GetUserById(long id, CancellationToken cancellationToken = default)
- {
- return Task.FromResult(_users.FirstOrDefault(x => x.Id == id));
- }
-
- public Task GetByIdentity(UserIdentityType identityType, string identity,
- CancellationToken cancellationToken = default)
- {
- var user = _users.SingleOrDefault(x => x.Identities.Any(i =>
- i.Type == identityType && i.Identity.Equals(identity, StringComparison.OrdinalIgnoreCase)));
-
- return Task.FromResult(user);
- }
-}
\ No newline at end of file
diff --git a/src/Nocr.Users.AppServices/Users/User.cs b/src/Nocr.Users.AppServices/Users/User.cs
index 7946f43..f9d3265 100644
--- a/src/Nocr.Users.AppServices/Users/User.cs
+++ b/src/Nocr.Users.AppServices/Users/User.cs
@@ -10,6 +10,13 @@ public sealed class User
public IReadOnlyCollection Identities => _identities;
+ ///
+ /// Used by ef.
+ ///
+ private User()
+ {
+ }
+
private User(string username, params UserIdentity[] identities)
{
Username = username;
diff --git a/src/Nocr.Users.AppServices/Users/UserIdentity.cs b/src/Nocr.Users.AppServices/Users/UserIdentity.cs
index a3b081d..5fd6f19 100644
--- a/src/Nocr.Users.AppServices/Users/UserIdentity.cs
+++ b/src/Nocr.Users.AppServices/Users/UserIdentity.cs
@@ -8,6 +8,8 @@ public sealed class UserIdentity
public string Identity { get; private set; }
+ public long UserId { get; private set; }
+
public UserIdentityType Type { get; private set; }
private UserIdentity(string identity, UserIdentityType type)
diff --git a/src/Nocr.Users.Host/Infrastructure/Startup.cs b/src/Nocr.Users.Host/Infrastructure/Startup.cs
index 82567d2..34c4e69 100644
--- a/src/Nocr.Users.Host/Infrastructure/Startup.cs
+++ b/src/Nocr.Users.Host/Infrastructure/Startup.cs
@@ -1,5 +1,6 @@
using Nocr.Users.AppServices;
using Nocr.Users.Core.Dates;
+using Nocr.Users.Persistence;
namespace Nocr.Users.Host.Infrastructure;
@@ -22,6 +23,7 @@ public class Startup
services.AddSwaggerGen();
services.AddAppServices();
+ services.AddEfPersistence(Configuration.GetConnectionString(nameof(UsersContext))!);
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
diff --git a/src/Nocr.Users.Host/Nocr.Users.Host.csproj b/src/Nocr.Users.Host/Nocr.Users.Host.csproj
index d5bc9be..6173de6 100644
--- a/src/Nocr.Users.Host/Nocr.Users.Host.csproj
+++ b/src/Nocr.Users.Host/Nocr.Users.Host.csproj
@@ -6,6 +6,7 @@
+
@@ -14,6 +15,7 @@
+
diff --git a/src/Nocr.Users.Host/appsettings.Development.json b/src/Nocr.Users.Host/appsettings.Development.json
index fdf6b48..31f8835 100644
--- a/src/Nocr.Users.Host/appsettings.Development.json
+++ b/src/Nocr.Users.Host/appsettings.Development.json
@@ -8,5 +8,8 @@
}
}
]
+ },
+ "ConnectionStrings": {
+ "UsersContext": "server=localhost;port=3307;database=nocr_users;uid=root;pwd=toor"
}
}
diff --git a/src/Nocr.Users.Host/appsettings.DockerCompose.json b/src/Nocr.Users.Host/appsettings.DockerCompose.json
index a2e2bb3..5400b9f 100644
--- a/src/Nocr.Users.Host/appsettings.DockerCompose.json
+++ b/src/Nocr.Users.Host/appsettings.DockerCompose.json
@@ -18,5 +18,8 @@
}
}
]
+ },
+ "ConnectionStrings": {
+ "UsersContext": "server=nocr-users-db;port=3306;database=nocr_users;uid=root;pwd=toor"
}
}
diff --git a/src/Nocr.Users.Persistence/DesignTimeTextMatcherContextFactory.cs b/src/Nocr.Users.Persistence/DesignTimeTextMatcherContextFactory.cs
new file mode 100644
index 0000000..4706c3c
--- /dev/null
+++ b/src/Nocr.Users.Persistence/DesignTimeTextMatcherContextFactory.cs
@@ -0,0 +1,21 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Design;
+using Microsoft.Extensions.Configuration;
+
+namespace Nocr.Users.Persistence;
+
+public class DesignTimeTextMatcherContextFactory : IDesignTimeDbContextFactory
+{
+ public UsersContext CreateDbContext(string[] args)
+ {
+ var optionsBuilder = new DbContextOptionsBuilder();
+ var configuration = new ConfigurationBuilder()
+ .AddJsonFile("appsettings.json")
+ .Build();
+
+ var connectionString = configuration.GetConnectionString("MariaLocal");
+ optionsBuilder.UseMySql(connectionString, new MariaDbServerVersion(MariaDbServerVersion.LatestSupportedServerVersion));
+
+ return new UsersContext(optionsBuilder.Options);
+ }
+}
\ No newline at end of file
diff --git a/src/Nocr.Users.Persistence/Migrations/20240330070727_InitialMigration.Designer.cs b/src/Nocr.Users.Persistence/Migrations/20240330070727_InitialMigration.Designer.cs
new file mode 100644
index 0000000..464c975
--- /dev/null
+++ b/src/Nocr.Users.Persistence/Migrations/20240330070727_InitialMigration.Designer.cs
@@ -0,0 +1,93 @@
+//
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Nocr.Users.Persistence;
+
+#nullable disable
+
+namespace Nocr.Users.Persistence.Migrations
+{
+ [DbContext(typeof(UsersContext))]
+ [Migration("20240330070727_InitialMigration")]
+ partial class InitialMigration
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.3")
+ .HasAnnotation("Relational:MaxIdentifierLength", 64);
+
+ MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
+
+ modelBuilder.Entity("Nocr.Users.AppServices.Users.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("Username")
+ .IsRequired()
+ .HasColumnType("varchar(255)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique()
+ .HasDatabaseName("UX_users_username");
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("Nocr.Users.AppServices.Users.UserIdentity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("Identity")
+ .IsRequired()
+ .HasColumnType("varchar(255)");
+
+ b.Property("Type")
+ .HasColumnType("int");
+
+ b.Property("UserId")
+ .HasColumnType("bigint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("Identity", "Type")
+ .IsUnique()
+ .HasDatabaseName("UX_users_identity_identity_type");
+
+ b.ToTable("UserIdentity");
+ });
+
+ modelBuilder.Entity("Nocr.Users.AppServices.Users.UserIdentity", b =>
+ {
+ b.HasOne("Nocr.Users.AppServices.Users.User", null)
+ .WithMany("Identities")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.NoAction)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Nocr.Users.AppServices.Users.User", b =>
+ {
+ b.Navigation("Identities");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/Nocr.Users.Persistence/Migrations/20240330070727_InitialMigration.cs b/src/Nocr.Users.Persistence/Migrations/20240330070727_InitialMigration.cs
new file mode 100644
index 0000000..0ce86a2
--- /dev/null
+++ b/src/Nocr.Users.Persistence/Migrations/20240330070727_InitialMigration.cs
@@ -0,0 +1,82 @@
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Nocr.Users.Persistence.Migrations
+{
+ ///
+ public partial class InitialMigration : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AlterDatabase()
+ .Annotation("MySql:CharSet", "utf8mb4");
+
+ migrationBuilder.CreateTable(
+ name: "Users",
+ columns: table => new
+ {
+ Id = table.Column(type: "bigint", nullable: false)
+ .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
+ Username = table.Column(type: "varchar(255)", nullable: false)
+ .Annotation("MySql:CharSet", "utf8mb4")
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Users", x => x.Id);
+ })
+ .Annotation("MySql:CharSet", "utf8mb4");
+
+ migrationBuilder.CreateTable(
+ name: "UserIdentity",
+ columns: table => new
+ {
+ Id = table.Column(type: "bigint", nullable: false)
+ .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
+ Identity = table.Column(type: "varchar(255)", nullable: false)
+ .Annotation("MySql:CharSet", "utf8mb4"),
+ UserId = table.Column(type: "bigint", nullable: false),
+ Type = table.Column(type: "int", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_UserIdentity", x => x.Id);
+ table.ForeignKey(
+ name: "FK_UserIdentity_Users_UserId",
+ column: x => x.UserId,
+ principalTable: "Users",
+ principalColumn: "Id");
+ })
+ .Annotation("MySql:CharSet", "utf8mb4");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_UserIdentity_UserId",
+ table: "UserIdentity",
+ column: "UserId");
+
+ migrationBuilder.CreateIndex(
+ name: "UX_users_identity_identity_type",
+ table: "UserIdentity",
+ columns: new[] { "Identity", "Type" },
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "UX_users_username",
+ table: "Users",
+ column: "Username",
+ unique: true);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "UserIdentity");
+
+ migrationBuilder.DropTable(
+ name: "Users");
+ }
+ }
+}
diff --git a/src/Nocr.Users.Persistence/Migrations/UsersContextModelSnapshot.cs b/src/Nocr.Users.Persistence/Migrations/UsersContextModelSnapshot.cs
new file mode 100644
index 0000000..353423a
--- /dev/null
+++ b/src/Nocr.Users.Persistence/Migrations/UsersContextModelSnapshot.cs
@@ -0,0 +1,90 @@
+//
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Nocr.Users.Persistence;
+
+#nullable disable
+
+namespace Nocr.Users.Persistence.Migrations
+{
+ [DbContext(typeof(UsersContext))]
+ partial class UsersContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.3")
+ .HasAnnotation("Relational:MaxIdentifierLength", 64);
+
+ MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
+
+ modelBuilder.Entity("Nocr.Users.AppServices.Users.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("Username")
+ .IsRequired()
+ .HasColumnType("varchar(255)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique()
+ .HasDatabaseName("UX_users_username");
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("Nocr.Users.AppServices.Users.UserIdentity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("Identity")
+ .IsRequired()
+ .HasColumnType("varchar(255)");
+
+ b.Property("Type")
+ .HasColumnType("int");
+
+ b.Property("UserId")
+ .HasColumnType("bigint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("Identity", "Type")
+ .IsUnique()
+ .HasDatabaseName("UX_users_identity_identity_type");
+
+ b.ToTable("UserIdentity");
+ });
+
+ modelBuilder.Entity("Nocr.Users.AppServices.Users.UserIdentity", b =>
+ {
+ b.HasOne("Nocr.Users.AppServices.Users.User", null)
+ .WithMany("Identities")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.NoAction)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Nocr.Users.AppServices.Users.User", b =>
+ {
+ b.Navigation("Identities");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/Nocr.Users.Persistence/Nocr.Users.Persistence.csproj b/src/Nocr.Users.Persistence/Nocr.Users.Persistence.csproj
new file mode 100644
index 0000000..021cc7b
--- /dev/null
+++ b/src/Nocr.Users.Persistence/Nocr.Users.Persistence.csproj
@@ -0,0 +1,26 @@
+
+
+
+ false
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+
+
diff --git a/src/Nocr.Users.Persistence/ServiceCollectionExtensions.cs b/src/Nocr.Users.Persistence/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..a04b6f2
--- /dev/null
+++ b/src/Nocr.Users.Persistence/ServiceCollectionExtensions.cs
@@ -0,0 +1,32 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Nocr.Users.AppServices.Users.Repositories;
+using Nocr.Users.Persistence.Users;
+
+namespace Nocr.Users.Persistence;
+
+public static class ServiceCollectionExtensions
+{
+ public static IServiceCollection AddEfPersistence(this IServiceCollection services, string connectionString)
+ {
+ if (services == null)
+ throw new ArgumentNullException(nameof(services));
+
+ if (string.IsNullOrWhiteSpace(connectionString))
+ throw new ArgumentNullException(nameof(connectionString));
+
+ services.AddScoped();
+
+ services.AddDbContext(
+ (ctx, context) =>
+ {
+ context.UseMySql(connectionString, new MariaDbServerVersion(MariaDbServerVersion.LatestSupportedServerVersion),
+ builder => builder.MigrationsAssembly(typeof(UsersContext).Assembly.FullName))
+ .UseLoggerFactory(ctx.GetRequiredService());
+ }
+ );
+
+ return services;
+ }
+}
\ No newline at end of file
diff --git a/src/Nocr.Users.Persistence/Users/UserConfiguration.cs b/src/Nocr.Users.Persistence/Users/UserConfiguration.cs
new file mode 100644
index 0000000..9eb9075
--- /dev/null
+++ b/src/Nocr.Users.Persistence/Users/UserConfiguration.cs
@@ -0,0 +1,39 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+using Nocr.Users.AppServices.Users;
+
+namespace Nocr.Users.Persistence.Users;
+
+public class UserConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.HasKey(x => x.Id);
+ builder.Property(x => x.Username)
+ .IsRequired();
+
+ builder.HasIndex(x => x.Username)
+ .IsUnique()
+ .HasDatabaseName("UX_users_username");
+
+ builder
+ .HasMany(u => u.Identities)
+ .WithOne()
+ .HasForeignKey(x => x.UserId)
+ .OnDelete(DeleteBehavior.NoAction);
+ }
+}
+
+public class UserIdentityConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.HasKey(x => x.Id);
+ builder.Property(x => x.Identity)
+ .IsRequired();
+
+ builder.HasIndex(x => new { x.Identity, x.Type })
+ .IsUnique()
+ .HasDatabaseName("UX_users_identity_identity_type");
+ }
+}
\ No newline at end of file
diff --git a/src/Nocr.Users.Persistence/Users/UsersRepository.cs b/src/Nocr.Users.Persistence/Users/UsersRepository.cs
new file mode 100644
index 0000000..0c4f08a
--- /dev/null
+++ b/src/Nocr.Users.Persistence/Users/UsersRepository.cs
@@ -0,0 +1,38 @@
+using Microsoft.EntityFrameworkCore;
+using Nocr.Users.Api.Contracts.Users;
+using Nocr.Users.AppServices.Users;
+using Nocr.Users.AppServices.Users.Repositories;
+
+namespace Nocr.Users.Persistence.Users;
+
+public class UsersRepository : IUsersRepository
+{
+ private readonly UsersContext _db;
+
+ public UsersRepository(UsersContext db)
+ {
+ _db = db ?? throw new ArgumentNullException(nameof(db));
+ }
+
+ public async Task Create(User user, CancellationToken cancellationToken = default)
+ {
+ var userId = await _db.Users.AddAsync(user, cancellationToken);
+ await _db.SaveChangesAsync(cancellationToken);
+
+ return userId.Entity.Id;
+ }
+
+ public Task GetUserById(long id, CancellationToken cancellationToken = default)
+ {
+ return _db.Users.Include(x => x.Identities)
+ .FirstOrDefaultAsync(x => x.Id == id, cancellationToken: cancellationToken);
+ }
+
+ public Task GetByIdentity(UserIdentityType identityType, string identity,
+ CancellationToken cancellationToken = default)
+ {
+ return _db.Users.Include(x => x.Identities)
+ .FirstOrDefaultAsync(x => x.Identities.Any(i => i.Type == identityType && i.Identity == identity),
+ cancellationToken: cancellationToken);
+ }
+}
\ No newline at end of file
diff --git a/src/Nocr.Users.Persistence/UsersContext.cs b/src/Nocr.Users.Persistence/UsersContext.cs
new file mode 100644
index 0000000..ff2c275
--- /dev/null
+++ b/src/Nocr.Users.Persistence/UsersContext.cs
@@ -0,0 +1,23 @@
+using Microsoft.EntityFrameworkCore;
+using Nocr.Users.AppServices.Users;
+using Nocr.Users.Persistence.Users;
+
+namespace Nocr.Users.Persistence;
+
+public class UsersContext : DbContext
+{
+ public DbSet Users { get; set; }
+
+ public UsersContext(DbContextOptions options)
+ : base(options)
+ {
+ }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.ApplyConfiguration(new UserConfiguration());
+ modelBuilder.ApplyConfiguration(new UserIdentityConfiguration());
+
+ base.OnModelCreating(modelBuilder);
+ }
+}
\ No newline at end of file
diff --git a/src/Nocr.Users.Persistence/appsettings.json b/src/Nocr.Users.Persistence/appsettings.json
new file mode 100644
index 0000000..62bb122
--- /dev/null
+++ b/src/Nocr.Users.Persistence/appsettings.json
@@ -0,0 +1,5 @@
+{
+ "ConnectionStrings": {
+ "MariaLocal": "server=localhost;port=3307;database=nocr_users;uid=root;pwd=toor"
+ }
+}
\ No newline at end of file