Browse Source

Add HasAbpJsonConversion property builder extension

pull/25904/head
maliming 2 months ago
parent
commit
4681a852e3
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 26
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Modeling/AbpPropertyBuilderExtensions.cs
  2. 26
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueComparers/AbpJsonValueComparer.cs
  3. 74
      framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/ValueComparers/AbpJsonValueComparer_Tests.cs
  4. 13
      modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs

26
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Modeling/AbpPropertyBuilderExtensions.cs

@ -0,0 +1,26 @@
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Volo.Abp.EntityFrameworkCore.ValueComparers;
using Volo.Abp.EntityFrameworkCore.ValueConverters;
namespace Volo.Abp.EntityFrameworkCore.Modeling;
public static class AbpPropertyBuilderExtensions
{
/// <summary>
/// Stores the property as a JSON serialized string with snapshot-based
/// change tracking.
/// </summary>
/// <remarks>
/// A fallback for EF Core providers without owned-JSON (ToJson) or primitive
/// collection mapping support. The value is converted as a whole: JSON path
/// querying and partial updates are not available, and the property type must
/// fully round-trip with System.Text.Json.
/// </remarks>
public static PropertyBuilder<TProperty> HasAbpJsonConversion<TProperty>(
this PropertyBuilder<TProperty> propertyBuilder)
{
return propertyBuilder.HasConversion(
new AbpJsonValueConverter<TProperty>(),
new AbpJsonValueComparer<TProperty>());
}
}

26
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueComparers/AbpJsonValueComparer.cs

@ -0,0 +1,26 @@
using System.Text.Json;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Volo.Abp.EntityFrameworkCore.ValueConverters;
namespace Volo.Abp.EntityFrameworkCore.ValueComparers;
public class AbpJsonValueComparer<TPropertyType> : ValueComparer<TPropertyType>
{
public AbpJsonValueComparer()
: base(
(left, right) => Serialize(left) == Serialize(right),
v => Serialize(v).GetHashCode(),
v => Deserialize(Serialize(v)))
{
}
private static string Serialize(TPropertyType? value)
{
return JsonSerializer.Serialize(value, AbpJsonValueConverter<TPropertyType>.SerializeOptions);
}
private static TPropertyType Deserialize(string value)
{
return JsonSerializer.Deserialize<TPropertyType>(value, AbpJsonValueConverter<TPropertyType>.DeserializeOptions)!;
}
}

74
framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/ValueComparers/AbpJsonValueComparer_Tests.cs

@ -0,0 +1,74 @@
using System.Collections.Generic;
using Shouldly;
using Volo.Abp.EntityFrameworkCore.ValueComparers;
using Xunit;
namespace Volo.Abp.EntityFrameworkCore.ValueComparers;
public class AbpJsonValueComparer_Tests
{
[Fact]
public void Should_Compare_Collections_By_Content()
{
var comparer = new AbpJsonValueComparer<List<string>>();
comparer.Equals(new List<string> { "a", "b" }, new List<string> { "a", "b" }).ShouldBeTrue();
comparer.Equals(new List<string> { "a", "b" }, new List<string> { "b", "a" }).ShouldBeFalse();
comparer.GetHashCode(new List<string> { "a", "b" })
.ShouldBe(comparer.GetHashCode(new List<string> { "a", "b" }));
}
[Fact]
public void Should_Handle_Null_Values()
{
var comparer = new AbpJsonValueComparer<List<string>>();
comparer.Equals(null, null).ShouldBeTrue();
comparer.Equals(new List<string>(), null).ShouldBeFalse();
}
[Fact]
public void Snapshot_Should_Be_A_Deep_Copy()
{
var comparer = new AbpJsonValueComparer<List<string>>();
var original = new List<string> { "a" };
var snapshot = (List<string>)comparer.Snapshot(original)!;
original.Add("b");
snapshot.ShouldBe(new[] { "a" });
comparer.Equals(original, snapshot).ShouldBeFalse();
}
[Fact]
public void Snapshot_Should_Copy_Nested_Arrays_Of_Typed_Objects()
{
var comparer = new AbpJsonValueComparer<SampleData>();
var original = new SampleData { Count = 1, Tags = new[] { "x" } };
var snapshot = (SampleData)comparer.Snapshot(original)!;
original.Tags[0] = "changed";
original.Count = 2;
snapshot.Tags.ShouldBe(new[] { "x" });
snapshot.Count.ShouldBe(1);
comparer.Equals(original, snapshot).ShouldBeFalse();
}
[Fact]
public void Should_Support_Nullable_Value_Types()
{
var comparer = new AbpJsonValueComparer<int?>();
comparer.Equals(42, 42).ShouldBeTrue();
comparer.Equals(42, 43).ShouldBeFalse();
comparer.Equals(null, null).ShouldBeTrue();
}
private class SampleData
{
public int Count { get; set; }
public string[] Tags { get; set; } = default!;
}
}

13
modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs

@ -1,8 +1,6 @@
using System; using System;
using System.Text.Json;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Volo.Abp.EntityFrameworkCore.Modeling; using Volo.Abp.EntityFrameworkCore.Modeling;
using Volo.Abp.Users.EntityFrameworkCore; using Volo.Abp.Users.EntityFrameworkCore;
@ -194,18 +192,11 @@ public static class IdentityDbContextModelBuilderExtensions
{ {
/* MySQL providers do not support EF Core JSON columns (ToJson), /* MySQL providers do not support EF Core JSON columns (ToJson),
* so store Data as a serialized json column with the same column * so store Data as a serialized json column with the same column
* name and content. The comparer detects in-place mutations * name and content. */
* (e.g. sign count updates on login). */
b.Property(p => p.Data) b.Property(p => p.Data)
.HasColumnName(nameof(IdentityUserPasskey.Data)) .HasColumnName(nameof(IdentityUserPasskey.Data))
.HasColumnType("json") .HasColumnType("json")
.HasConversion( .HasAbpJsonConversion();
d => JsonSerializer.Serialize(d, (JsonSerializerOptions?)null),
s => JsonSerializer.Deserialize<IdentityPasskeyData>(s, (JsonSerializerOptions?)null)!,
new ValueComparer<IdentityPasskeyData>(
(l, r) => JsonSerializer.Serialize(l, (JsonSerializerOptions?)null) == JsonSerializer.Serialize(r, (JsonSerializerOptions?)null),
v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null).GetHashCode(),
v => JsonSerializer.Deserialize<IdentityPasskeyData>(JsonSerializer.Serialize(v, (JsonSerializerOptions?)null), (JsonSerializerOptions?)null)!));
} }
else else
{ {

Loading…
Cancel
Save