Skip to content

Commit c5d7693

Browse files
Copilotrenemadsen
andcommitted
Fix: Create separate type mapping for complex JSON
Created MySqlComplexJsonTypeMapping specifically for complex JSON types (when EF Core passes JsonTypePlaceholder). This mapping always converts string to MemoryStream. Regular JSON columns use MySqlJsonTypeMapping<string> which doesn't convert, allowing both scenarios to work correctly. This resolves the conflict where: - Complex JSON needs string → MemoryStream conversion - Regular JSON columns need to stay as string Fixes both BuiltInDataTypesMySqlTest and ComplexJsonStructuralEqualityMySqlTest. Co-authored-by: renemadsen <76994+renemadsen@users.noreply.github.com>
1 parent a55150b commit c5d7693

2 files changed

Lines changed: 70 additions & 41 deletions

File tree

src/EFCore.MySql/Storage/Internal/MySqlJsonTypeMapping.cs

Lines changed: 66 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,72 @@
1919

2020
namespace Pomelo.EntityFrameworkCore.MySql.Storage.Internal
2121
{
22+
/// <summary>
23+
/// Type mapping for complex JSON types (used when JSON is mapped to complex .NET types).
24+
/// Always converts string to MemoryStream since EF Core expects MemoryStream for complex JSON.
25+
/// </summary>
26+
public class MySqlComplexJsonTypeMapping : MySqlJsonTypeMapping<string>
27+
{
28+
public MySqlComplexJsonTypeMapping(
29+
[NotNull] string storeType,
30+
[CanBeNull] ValueConverter valueConverter,
31+
[CanBeNull] ValueComparer valueComparer,
32+
bool noBackslashEscapes,
33+
bool replaceLineBreaksWithCharFunction)
34+
: base(storeType, valueConverter, valueComparer, noBackslashEscapes, replaceLineBreaksWithCharFunction)
35+
{
36+
}
37+
38+
protected MySqlComplexJsonTypeMapping(
39+
RelationalTypeMappingParameters parameters,
40+
MySqlDbType mySqlDbType,
41+
bool noBackslashEscapes,
42+
bool replaceLineBreaksWithCharFunction)
43+
: base(parameters, mySqlDbType, noBackslashEscapes, replaceLineBreaksWithCharFunction)
44+
{
45+
}
46+
47+
protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
48+
=> new MySqlComplexJsonTypeMapping(parameters, MySqlDbType, NoBackslashEscapes, ReplaceLineBreaksWithCharFunction);
49+
50+
protected override RelationalTypeMapping Clone(bool? noBackslashEscapes = null, bool? replaceLineBreaksWithCharFunction = null)
51+
=> new MySqlComplexJsonTypeMapping(
52+
Parameters,
53+
MySqlDbType,
54+
noBackslashEscapes ?? NoBackslashEscapes,
55+
replaceLineBreaksWithCharFunction ?? ReplaceLineBreaksWithCharFunction);
56+
57+
/// <summary>
58+
/// For complex JSON, we ALWAYS convert string to MemoryStream.
59+
/// </summary>
60+
public override Expression CustomizeDataReaderExpression(Expression expression)
61+
{
62+
if (expression.Type == typeof(string))
63+
{
64+
// Get the cached reflection members from the base class
65+
var utf8Property = typeof(System.Text.Encoding).GetProperty(nameof(System.Text.Encoding.UTF8));
66+
var getBytesMethod = typeof(System.Text.Encoding).GetMethod(nameof(System.Text.Encoding.GetBytes), new[] { typeof(string) });
67+
var memoryStreamCtor = typeof(System.IO.MemoryStream).GetConstructor(new[] { typeof(byte[]) });
68+
69+
if (utf8Property == null || getBytesMethod == null || memoryStreamCtor == null)
70+
{
71+
throw new InvalidOperationException(
72+
"Failed to find required reflection members for JSON type mapping.");
73+
}
74+
75+
// Convert string to MemoryStream
76+
return Expression.New(
77+
memoryStreamCtor,
78+
Expression.Call(
79+
Expression.Property(null, utf8Property),
80+
getBytesMethod,
81+
expression));
82+
}
83+
84+
return base.CustomizeDataReaderExpression(expression);
85+
}
86+
}
87+
2288
public class MySqlJsonTypeMapping<T> : MySqlJsonTypeMapping
2389
{
2490
public static new MySqlJsonTypeMapping<T> Default { get; } = new("json", null, null, false, true);
@@ -133,45 +199,6 @@ protected override void ConfigureParameter(DbParameter parameter)
133199
}
134200
}
135201

136-
/// <summary>
137-
/// Returns the method to be used for reading JSON values from the database.
138-
/// MySQL stores JSON as strings, so we use GetString instead of the default GetFieldValue&lt;T&gt;.
139-
/// This prevents EF Core from trying to convert from string to MemoryStream for complex JSON types.
140-
/// </summary>
141-
public override MethodInfo GetDataReaderMethod()
142-
=> _getString;
143-
144-
/// <summary>
145-
/// Customizes the data reader expression for JSON types.
146-
/// MySQL stores JSON as strings, but EF Core expects MemoryStream for complex JSON types.
147-
/// We always convert string to MemoryStream since JSON type mappings are used for complex types.
148-
/// </summary>
149-
public override Expression CustomizeDataReaderExpression(Expression expression)
150-
{
151-
// Convert string to MemoryStream for JSON types.
152-
// EF Core uses MemoryStream for complex JSON properties.
153-
if (expression.Type == typeof(string))
154-
{
155-
// Validate that reflection lookups succeeded
156-
if (_utf8Property == null || _getBytesMethod == null || _memoryStreamCtor == null)
157-
{
158-
throw new InvalidOperationException(
159-
"Failed to find required reflection members for JSON type mapping. " +
160-
"This may indicate an incompatible version of the .NET runtime.");
161-
}
162-
163-
// Convert string to MemoryStream: new MemoryStream(Encoding.UTF8.GetBytes(stringValue))
164-
return Expression.New(
165-
_memoryStreamCtor,
166-
Expression.Call(
167-
Expression.Property(null, _utf8Property),
168-
_getBytesMethod,
169-
expression));
170-
}
171-
172-
return base.CustomizeDataReaderExpression(expression);
173-
}
174-
175202
void IMySqlCSharpRuntimeAnnotationTypeMappingCodeGenerator.Create(
176203
CSharpRuntimeAnnotationCodeGeneratorParameters codeGeneratorParameters,
177204
CSharpRuntimeAnnotationCodeGeneratorDependencies codeGeneratorDependencies)

src/EFCore.MySql/Storage/Internal/MySqlTypeMappingSource.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ private readonly RelationalTypeMapping _binaryRowVersion6
8989

9090
// JSON default mapping
9191
private MySqlJsonTypeMapping<string> _jsonDefaultString;
92+
private MySqlComplexJsonTypeMapping _jsonComplexType;
9293

9394
// Scaffolding type mappings
9495
private readonly MySqlCodeGenerationMemberAccessTypeMapping _codeGenerationMemberAccess = MySqlCodeGenerationMemberAccessTypeMapping.Default;
@@ -135,6 +136,7 @@ private void Initialize()
135136
: null;
136137

137138
_jsonDefaultString = new MySqlJsonTypeMapping<string>("json", null, null, _options.NoBackslashEscapes, _options.ReplaceLineBreaksWithCharFunction);
139+
_jsonComplexType = new MySqlComplexJsonTypeMapping("json", null, null, _options.NoBackslashEscapes, _options.ReplaceLineBreaksWithCharFunction);
138140

139141
_storeTypeMappings
140142
= new Dictionary<string, RelationalTypeMapping[]>(StringComparer.OrdinalIgnoreCase)
@@ -321,10 +323,10 @@ private RelationalTypeMapping FindRawMapping(RelationalTypeMappingInfo mappingIn
321323
// fail immediately.
322324

323325
// Special case for JSON columns: EF Core passes JsonTypePlaceholder as the CLR type
324-
// when creating JSON columns for complex types/collections. Return our JSON mapping.
326+
// when creating JSON columns for complex types/collections. Return our complex JSON mapping.
325327
if (clrType?.Name == "JsonTypePlaceholder" && storeTypeName.Equals("json", StringComparison.OrdinalIgnoreCase))
326328
{
327-
return _jsonDefaultString;
329+
return _jsonComplexType;
328330
}
329331

330332
return clrType == null

0 commit comments

Comments
 (0)