Add persistence

This commit is contained in:
Sergey Nazarov 2024-03-30 10:24:13 +03:00
parent 2721f57b9c
commit c9da4cc805
20 changed files with 487 additions and 43 deletions

View File

@ -17,5 +17,13 @@
<ItemGroup Label="Microsoft">
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftVersion)" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="$(MicrosoftVersion)" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="$(MicrosoftVersion)" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="$(MicrosoftVersion)" />
</ItemGroup>
<ItemGroup Label="EntityFramework">
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="8.0.3" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.3" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.3" />
<PackageVersion Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
</ItemGroup>
</Project>

View File

@ -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

View File

@ -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<IUsersRepository, InMemoryUsersRepository>();
services.AddScoped<IUsersService, UsersService>();
return services;

View File

@ -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<User> _users = new();
public Task<long> 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<User?> GetUserById(long id, CancellationToken cancellationToken = default)
{
return Task.FromResult(_users.FirstOrDefault(x => x.Id == id));
}
public Task<User?> 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);
}
}

View File

@ -10,6 +10,13 @@ public sealed class User
public IReadOnlyCollection<UserIdentity> Identities => _identities;
/// <summary>
/// Used by ef.
/// </summary>
private User()
{
}
private User(string username, params UserIdentity[] identities)
{
Username = username;

View File

@ -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)

View File

@ -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)

View File

@ -6,6 +6,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
<PackageReference Include="Serilog.AspNetCore" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
<PackageReference Include="Swashbuckle.AspNetCore" />
@ -14,6 +15,7 @@
<ItemGroup>
<ProjectReference Include="..\Nocr.Users.Api.Contracts\Nocr.Users.Api.Contracts.csproj" />
<ProjectReference Include="..\Nocr.Users.AppServices\Nocr.Users.AppServices.csproj" />
<ProjectReference Include="..\Nocr.Users.Persistence\Nocr.Users.Persistence.csproj" />
</ItemGroup>
<ItemGroup>

View File

@ -8,5 +8,8 @@
}
}
]
},
"ConnectionStrings": {
"UsersContext": "server=localhost;port=3307;database=nocr_users;uid=root;pwd=toor"
}
}

View File

@ -18,5 +18,8 @@
}
}
]
},
"ConnectionStrings": {
"UsersContext": "server=nocr-users-db;port=3306;database=nocr_users;uid=root;pwd=toor"
}
}

View File

@ -0,0 +1,21 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Configuration;
namespace Nocr.Users.Persistence;
public class DesignTimeTextMatcherContextFactory : IDesignTimeDbContextFactory<UsersContext>
{
public UsersContext CreateDbContext(string[] args)
{
var optionsBuilder = new DbContextOptionsBuilder<UsersContext>();
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);
}
}

View File

@ -0,0 +1,93 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<string>("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<string>("Identity")
.IsRequired()
.HasColumnType("varchar(255)");
b.Property<int>("Type")
.HasColumnType("int");
b.Property<long>("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
}
}
}

View File

@ -0,0 +1,82 @@
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Nocr.Users.Persistence.Migrations
{
/// <inheritdoc />
public partial class InitialMigration : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterDatabase()
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "Users",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Username = table.Column<string>(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<long>(type: "bigint", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Identity = table.Column<string>(type: "varchar(255)", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
UserId = table.Column<long>(type: "bigint", nullable: false),
Type = table.Column<int>(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);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "UserIdentity");
migrationBuilder.DropTable(
name: "Users");
}
}
}

View File

@ -0,0 +1,90 @@
// <auto-generated />
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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<string>("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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<string>("Identity")
.IsRequired()
.HasColumnType("varchar(255)");
b.Property<int>("Type")
.HasColumnType("int");
b.Property<long>("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
}
}
}

View File

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Nocr.Users.AppServices\Nocr.Users.AppServices.csproj" />
</ItemGroup>
<ItemGroup>
<None Remove="appsettings.json" />
<Content Include="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@ -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<IUsersRepository, UsersRepository>();
services.AddDbContext<UsersContext>(
(ctx, context) =>
{
context.UseMySql(connectionString, new MariaDbServerVersion(MariaDbServerVersion.LatestSupportedServerVersion),
builder => builder.MigrationsAssembly(typeof(UsersContext).Assembly.FullName))
.UseLoggerFactory(ctx.GetRequiredService<ILoggerFactory>());
}
);
return services;
}
}

View File

@ -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<User>
{
public void Configure(EntityTypeBuilder<User> 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<UserIdentity>
{
public void Configure(EntityTypeBuilder<UserIdentity> 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");
}
}

View File

@ -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<long> Create(User user, CancellationToken cancellationToken = default)
{
var userId = await _db.Users.AddAsync(user, cancellationToken);
await _db.SaveChangesAsync(cancellationToken);
return userId.Entity.Id;
}
public Task<User?> GetUserById(long id, CancellationToken cancellationToken = default)
{
return _db.Users.Include(x => x.Identities)
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken: cancellationToken);
}
public Task<User?> 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);
}
}

View File

@ -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<User> 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);
}
}

View File

@ -0,0 +1,5 @@
{
"ConnectionStrings": {
"MariaLocal": "server=localhost;port=3307;database=nocr_users;uid=root;pwd=toor"
}
}