Skip to content

Commit 7424945

Browse files
committed
Completed EFCore User Store
1 parent b3123c9 commit 7424945

File tree

23 files changed

+1283
-3
lines changed

23 files changed

+1283
-3
lines changed

UltimateAuth.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
<Project Path="src/tokens/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore/CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore.csproj" Id="6eb14b32-0b56-460f-a2b2-f95d28bad625" />
3535
<Project Path="src/tokens/CodeBeam.UltimateAuth.Tokens.InMemory/CodeBeam.UltimateAuth.Tokens.InMemory.csproj" Id="8220884e-4958-4b49-8c69-56ce9d2b6c6f" />
3636
<Project Path="src/users/CodeBeam.UltimateAuth.Users.Contracts/CodeBeam.UltimateAuth.Users.Contracts.csproj" Id="3a04f065-8f9d-46b3-9726-1febffe6d46f" />
37+
<Project Path="src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/CodeBeam.UltimateAuth.Users.EntityFrameworkCore.csproj" Id="a8febfee-0cfe-4e8c-8dcb-8703c35dd77b" />
3738
<Project Path="src/users/CodeBeam.UltimateAuth.Users.InMemory/CodeBeam.UltimateAuth.Users.InMemory.csproj" Id="7ce3df22-4773-4b9b-afd0-8ba506e0f9de" />
3839
<Project Path="src/users/CodeBeam.UltimateAuth.Users.Reference/CodeBeam.UltimateAuth.Users.Reference.csproj" Id="601176dd-b760-4b6f-9cc7-c618134ae178" />
3940
<Project Path="src/users/CodeBeam.UltimateAuth.Users/CodeBeam.UltimateAuth.Users.csproj" Id="30d5db36-6dc8-46f6-9139-8b6b3d6053d5" />

src/CodeBeam.UltimateAuth.Core/AssemblyVisibility.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,7 @@
33
[assembly: InternalsVisibleTo("CodeBeam.UltimateAuth.Server")]
44
[assembly: InternalsVisibleTo("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore")]
55
[assembly: InternalsVisibleTo("CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore")]
6+
[assembly: InternalsVisibleTo("CodeBeam.UltimateAuth.Users.EntityFrameworkCore")]
7+
[assembly: InternalsVisibleTo("CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore")]
8+
[assembly: InternalsVisibleTo("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore")]
69
[assembly: InternalsVisibleTo("CodeBeam.UltimateAuth.Tests.Unit")]
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using System.Text.Json;
2+
3+
namespace CodeBeam.UltimateAuth.EntityFrameworkCore;
4+
5+
internal static class JsonSerializerWrapper
6+
{
7+
private static readonly JsonSerializerOptions Options = new(JsonSerializerDefaults.Web);
8+
9+
public static string Serialize<T>(T value)
10+
{
11+
return JsonSerializer.Serialize(value, Options);
12+
}
13+
14+
public static T Deserialize<T>(string json)
15+
{
16+
return JsonSerializer.Deserialize<T>(json, Options)!;
17+
}
18+
19+
public static string? SerializeNullable<T>(T? value)
20+
{
21+
return value is null ? null : JsonSerializer.Serialize(value, Options);
22+
}
23+
24+
public static T? DeserializeNullable<T>(string? json)
25+
{
26+
return json is null ? default : JsonSerializer.Deserialize<T>(json, Options);
27+
}
28+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using Microsoft.EntityFrameworkCore.ChangeTracking;
2+
3+
namespace CodeBeam.UltimateAuth.EntityFrameworkCore;
4+
5+
internal static class JsonValueComparerHelpers
6+
{
7+
public static bool AreEqual<T>(T left, T right)
8+
{
9+
return string.Equals(
10+
JsonSerializerWrapper.Serialize(left),
11+
JsonSerializerWrapper.Serialize(right),
12+
StringComparison.Ordinal);
13+
}
14+
15+
public static int GetHashCodeSafe<T>(T value)
16+
{
17+
return JsonSerializerWrapper.Serialize(value).GetHashCode();
18+
}
19+
20+
public static T Snapshot<T>(T value)
21+
{
22+
var json = JsonSerializerWrapper.Serialize(value);
23+
return JsonSerializerWrapper.Deserialize<T>(json);
24+
}
25+
26+
public static bool AreEqualNullable<T>(T? left, T? right)
27+
{
28+
return string.Equals(
29+
JsonSerializerWrapper.SerializeNullable(left),
30+
JsonSerializerWrapper.SerializeNullable(right),
31+
StringComparison.Ordinal);
32+
}
33+
34+
public static int GetHashCodeSafeNullable<T>(T? value)
35+
{
36+
var json = JsonSerializerWrapper.SerializeNullable(value);
37+
return json == null ? 0 : json.GetHashCode();
38+
}
39+
40+
public static T? SnapshotNullable<T>(T? value)
41+
{
42+
var json = JsonSerializerWrapper.SerializeNullable(value);
43+
return json == null ? default : JsonSerializerWrapper.Deserialize<T>(json);
44+
}
45+
}
46+
47+
public static class JsonValueComparers
48+
{
49+
public static ValueComparer<T> Create<T>()
50+
{
51+
return new ValueComparer<T>(
52+
(l, r) => JsonValueComparerHelpers.AreEqual(l, r),
53+
v => JsonValueComparerHelpers.GetHashCodeSafe(v),
54+
v => JsonValueComparerHelpers.Snapshot(v));
55+
}
56+
57+
public static ValueComparer<T?> CreateNullable<T>()
58+
{
59+
return new ValueComparer<T?>(
60+
(l, r) => JsonValueComparerHelpers.AreEqualNullable(l, r),
61+
v => JsonValueComparerHelpers.GetHashCodeSafeNullable(v),
62+
v => JsonValueComparerHelpers.SnapshotNullable(v));
63+
}
64+
}

src/persistence/CodeBeam.UltimateAuth.EntityFrameworkCore.Abstractions/Infrastructure/JsonValueConverter.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@ namespace CodeBeam.UltimateAuth.EntityFrameworkCore;
55

66
public sealed class JsonValueConverter<T> : ValueConverter<T, string>
77
{
8+
private static readonly JsonSerializerOptions Options = new(JsonSerializerDefaults.Web);
9+
810
public JsonValueConverter()
9-
: base(
10-
v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null),
11-
v => JsonSerializer.Deserialize<T>(v, (JsonSerializerOptions?)null)!)
11+
: base(v => Serialize(v), v => Deserialize(v))
1212
{
1313
}
14+
15+
private static string Serialize(T value) => JsonSerializer.Serialize(value, Options);
16+
17+
private static T Deserialize(string json) => JsonSerializer.Deserialize<T>(json, Options)!;
1418
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
2+
using System.Text.Json;
3+
4+
namespace CodeBeam.UltimateAuth.EntityFrameworkCore;
5+
6+
public sealed class NullableJsonValueConverter<T> : ValueConverter<T?, string?>
7+
{
8+
private static readonly JsonSerializerOptions Options = new(JsonSerializerDefaults.Web);
9+
10+
public NullableJsonValueConverter()
11+
: base(v => Serialize(v), v => Deserialize(v))
12+
{
13+
}
14+
15+
private static string? Serialize(T? value) => value == null ? null : JsonSerializer.Serialize(value, Options);
16+
17+
private static T? Deserialize(string? json) => json == null ? default : JsonSerializer.Deserialize<T>(json, Options);
18+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
using System.Runtime.CompilerServices;
2+
3+
[assembly: InternalsVisibleTo("CodeBeam.UltimateAuth.Tests.Unit")]
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<GenerateDocumentationFile>true</GenerateDocumentationFile>
8+
<NoWarn>$(NoWarn);1591</NoWarn>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<ProjectReference Include="..\..\CodeBeam.UltimateAuth.Core\CodeBeam.UltimateAuth.Core.csproj" />
13+
<ProjectReference Include="..\..\sessions\CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore\CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.csproj" />
14+
<ProjectReference Include="..\CodeBeam.UltimateAuth.Users.Contracts\CodeBeam.UltimateAuth.Users.Contracts.csproj" />
15+
<ProjectReference Include="..\CodeBeam.UltimateAuth.Users.Reference\CodeBeam.UltimateAuth.Users.Reference.csproj" />
16+
</ItemGroup>
17+
</Project>
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
using CodeBeam.UltimateAuth.Core.Domain;
2+
using CodeBeam.UltimateAuth.Core.MultiTenancy;
3+
using CodeBeam.UltimateAuth.EntityFrameworkCore;
4+
using Microsoft.EntityFrameworkCore;
5+
using Microsoft.EntityFrameworkCore.ChangeTracking;
6+
using System.Text.Json;
7+
8+
namespace CodeBeam.UltimateAuth.Users.EntityFrameworkCore;
9+
10+
internal sealed class UAuthUserDbContext : DbContext
11+
{
12+
public DbSet<UserIdentifierProjection> Identifiers => Set<UserIdentifierProjection>();
13+
public DbSet<UserLifecycleProjection> Lifecycles => Set<UserLifecycleProjection>();
14+
public DbSet<UserProfileProjection> Profiles => Set<UserProfileProjection>();
15+
16+
private readonly TenantContext _tenant;
17+
18+
public UAuthUserDbContext(DbContextOptions<UAuthUserDbContext> options, TenantContext tenant)
19+
: base(options)
20+
{
21+
_tenant = tenant;
22+
}
23+
24+
protected override void OnModelCreating(ModelBuilder b)
25+
{
26+
ConfigureTenantFilters(b);
27+
28+
ConfigureIdentifiers(b);
29+
ConfigureLifecycles(b);
30+
ConfigureProfiles(b);
31+
}
32+
33+
private void ConfigureTenantFilters(ModelBuilder b)
34+
{
35+
b.Entity<UserIdentifierProjection>()
36+
.HasQueryFilter(x => _tenant.IsGlobal || x.Tenant == _tenant.Tenant);
37+
38+
b.Entity<UserLifecycleProjection>()
39+
.HasQueryFilter(x => _tenant.IsGlobal || x.Tenant == _tenant.Tenant);
40+
41+
b.Entity<UserProfileProjection>()
42+
.HasQueryFilter(x => _tenant.IsGlobal || x.Tenant == _tenant.Tenant);
43+
}
44+
45+
private void ConfigureIdentifiers(ModelBuilder b)
46+
{
47+
b.Entity<UserIdentifierProjection>(e =>
48+
{
49+
e.HasKey(x => x.Id);
50+
51+
e.Property(x => x.Version)
52+
.IsConcurrencyToken();
53+
54+
e.Property(x => x.Tenant)
55+
.HasConversion(
56+
v => v.Value,
57+
v => TenantKey.FromInternal(v))
58+
.HasMaxLength(128)
59+
.IsRequired();
60+
61+
e.Property(x => x.UserKey)
62+
.HasConversion(
63+
v => v.Value,
64+
v => UserKey.FromString(v))
65+
.HasMaxLength(128)
66+
.IsRequired();
67+
68+
e.Property(x => x.Value)
69+
.HasMaxLength(256)
70+
.IsRequired();
71+
72+
e.Property(x => x.NormalizedValue)
73+
.HasMaxLength(256)
74+
.IsRequired();
75+
76+
e.HasIndex(x => new { x.Tenant, x.Type, x.NormalizedValue }).IsUnique();
77+
e.HasIndex(x => new { x.Tenant, x.UserKey });
78+
e.HasIndex(x => new { x.Tenant, x.UserKey, x.Type, x.IsPrimary });
79+
e.HasIndex(x => new { x.Tenant, x.UserKey, x.IsPrimary });
80+
e.HasIndex(x => new { x.Tenant, x.NormalizedValue });
81+
82+
e.Property(x => x.CreatedAt)
83+
.IsRequired();
84+
});
85+
}
86+
87+
private void ConfigureLifecycles(ModelBuilder b)
88+
{
89+
b.Entity<UserLifecycleProjection>(e =>
90+
{
91+
e.HasKey(x => x.Id);
92+
93+
e.Property(x => x.Version)
94+
.IsConcurrencyToken();
95+
96+
e.Property(x => x.Tenant)
97+
.HasConversion(
98+
v => v.Value,
99+
v => TenantKey.FromInternal(v))
100+
.HasMaxLength(128)
101+
.IsRequired();
102+
103+
e.Property(x => x.UserKey)
104+
.HasConversion(
105+
v => v.Value,
106+
v => UserKey.FromString(v))
107+
.HasMaxLength(128)
108+
.IsRequired();
109+
110+
e.HasIndex(x => new { x.Tenant, x.UserKey }).IsUnique();
111+
112+
e.Property(x => x.SecurityVersion)
113+
.IsRequired();
114+
115+
e.Property(x => x.CreatedAt)
116+
.IsRequired();
117+
});
118+
}
119+
120+
private void ConfigureProfiles(ModelBuilder b)
121+
{
122+
b.Entity<UserProfileProjection>(e =>
123+
{
124+
e.HasKey(x => x.Id);
125+
126+
e.Property(x => x.Version)
127+
.IsConcurrencyToken();
128+
129+
e.Property(x => x.Tenant)
130+
.HasConversion(
131+
v => v.Value,
132+
v => TenantKey.FromInternal(v))
133+
.HasMaxLength(128)
134+
.IsRequired();
135+
136+
e.Property(x => x.UserKey)
137+
.HasConversion(
138+
v => v.Value,
139+
v => UserKey.FromString(v))
140+
.HasMaxLength(128)
141+
.IsRequired();
142+
143+
e.HasIndex(x => new { x.Tenant, x.UserKey });
144+
145+
e.Property(x => x.Metadata)
146+
.HasConversion(new NullableJsonValueConverter<Dictionary<string, string>>())
147+
.Metadata.SetValueComparer(JsonValueComparers.Create<DeviceContext>());
148+
149+
e.Property(x => x.CreatedAt)
150+
.IsRequired();
151+
});
152+
}
153+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using CodeBeam.UltimateAuth.Users.Reference;
2+
using Microsoft.EntityFrameworkCore;
3+
using Microsoft.Extensions.DependencyInjection;
4+
5+
namespace CodeBeam.UltimateAuth.Users.EntityFrameworkCore.Extensions;
6+
7+
public static class ServiceCollectionExtensions
8+
{
9+
public static IServiceCollection AddUltimateAuthEntityFrameworkCoreUsers(this IServiceCollection services, Action<DbContextOptionsBuilder> configureDb)
10+
{
11+
services.AddDbContextPool<UAuthUserDbContext>(configureDb);
12+
services.AddScoped<IUserLifecycleStore, EfCoreUserLifecycleStore>();
13+
services.AddScoped<IUserIdentifierStore, EfCoreUserIdentifierStore>();
14+
services.AddScoped<IUserProfileStore, EfCoreUserProfileStore>();
15+
return services;
16+
}
17+
}

0 commit comments

Comments
 (0)