Skip to content

Commit e68aced

Browse files
Copilotrenemadsen
andcommitted
Fix: Use MySqlStructuralJsonTypeMapping like SQL Server
Created MySqlStructuralJsonTypeMapping (similar to SqlServerStructuralJsonTypeMapping) with: - ClrType = JsonTypePlaceholder (not MemoryStream or string) - GetDataReaderMethod() returns DbDataReader.GetString - CustomizeDataReaderExpression() converts string→MemoryStream This matches SQL Server's implementation and avoids SQL generation issues. Using different type mappings with different ClrTypes was breaking SQL generation because EF Core uses the type mapping during both SQL generation and data reading. Removed ClrType-based conversion logic from MySqlJsonTypeMapping since it's now only used for regular JSON columns mapped to string properties. Researched dotnet/efcore SqlServer implementation for reference. Co-authored-by: renemadsen <76994+renemadsen@users.noreply.github.com>
1 parent f99b5dd commit e68aced

3 files changed

Lines changed: 86 additions & 38 deletions

File tree

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

Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,6 @@ public abstract class MySqlJsonTypeMapping : MySqlStringTypeMapping, IMySqlCShar
6464
protected static readonly MethodInfo _getString
6565
= typeof(DbDataReader).GetRuntimeMethod(nameof(DbDataReader.GetString), new[] { typeof(int) });
6666

67-
// Cache reflection lookups for performance
68-
protected static readonly PropertyInfo _utf8Property
69-
= typeof(System.Text.Encoding).GetProperty(nameof(System.Text.Encoding.UTF8));
70-
protected static readonly MethodInfo _getBytesMethod
71-
= typeof(System.Text.Encoding).GetMethod(nameof(System.Text.Encoding.GetBytes), new[] { typeof(string) });
72-
protected static readonly ConstructorInfo _memoryStreamCtor
73-
= typeof(System.IO.MemoryStream).GetConstructor(new[] { typeof(byte[]) });
74-
7567
public MySqlJsonTypeMapping(
7668
[NotNull] string storeType,
7769
[NotNull] Type clrType,
@@ -142,32 +134,12 @@ public override MethodInfo GetDataReaderMethod()
142134

143135
/// <summary>
144136
/// Customizes the data reader expression for JSON types.
145-
/// MySQL stores JSON as strings, but EF Core expects MemoryStream for complex JSON types.
146-
/// We only convert when ClrType is MemoryStream (complex JSON types), not for regular JSON columns mapped to string.
137+
/// This is only used for regular JSON columns mapped to string properties.
138+
/// Complex JSON types use MySqlStructuralJsonTypeMapping instead.
147139
/// </summary>
148140
public override Expression CustomizeDataReaderExpression(Expression expression)
149141
{
150-
// Only convert for complex JSON types (where ClrType is MemoryStream)
151-
// For regular JSON columns mapped to string, don't convert
152-
if (expression.Type == typeof(string) && ClrType == typeof(System.IO.MemoryStream))
153-
{
154-
// Validate that reflection lookups succeeded
155-
if (_utf8Property == null || _getBytesMethod == null || _memoryStreamCtor == null)
156-
{
157-
throw new InvalidOperationException(
158-
"Failed to find required reflection members for JSON type mapping. " +
159-
"This may indicate an incompatible version of the .NET runtime.");
160-
}
161-
162-
// Convert string to MemoryStream: new MemoryStream(Encoding.UTF8.GetBytes(stringValue))
163-
return Expression.New(
164-
_memoryStreamCtor,
165-
Expression.Call(
166-
Expression.Property(null, _utf8Property),
167-
_getBytesMethod,
168-
expression));
169-
}
170-
142+
// For regular JSON columns, no conversion needed - just return the string
171143
return base.CustomizeDataReaderExpression(expression);
172144
}
173145

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright (c) Pomelo Foundation. All rights reserved.
2+
// Licensed under the MIT. See LICENSE in the project root for license information.
3+
4+
using System;
5+
using System.Data.Common;
6+
using System.IO;
7+
using System.Linq.Expressions;
8+
using System.Reflection;
9+
using System.Text;
10+
using Microsoft.EntityFrameworkCore.Storage;
11+
using MySqlConnector;
12+
13+
namespace Pomelo.EntityFrameworkCore.MySql.Storage.Internal
14+
{
15+
/// <summary>
16+
/// Type mapping for complex JSON types (types mapped with .ToJson()).
17+
/// This mapping handles the conversion from MySQL's string-based JSON to MemoryStream expected by EF Core.
18+
/// </summary>
19+
public class MySqlStructuralJsonTypeMapping : JsonTypeMapping
20+
{
21+
private static readonly MethodInfo _createUtf8StreamMethod
22+
= typeof(MySqlStructuralJsonTypeMapping).GetMethod(nameof(CreateUtf8Stream), new[] { typeof(string) });
23+
24+
private static readonly MethodInfo _getStringMethod
25+
= typeof(DbDataReader).GetRuntimeMethod(nameof(DbDataReader.GetString), new[] { typeof(int) });
26+
27+
public static MySqlStructuralJsonTypeMapping Default { get; } = new("json");
28+
29+
public MySqlStructuralJsonTypeMapping(string storeType)
30+
: base(storeType, typeof(JsonTypePlaceholder), System.Data.DbType.String)
31+
{
32+
}
33+
34+
protected MySqlStructuralJsonTypeMapping(RelationalTypeMappingParameters parameters)
35+
: base(parameters)
36+
{
37+
}
38+
39+
/// <summary>
40+
/// MySQL stores JSON as strings, so we read using GetString.
41+
/// </summary>
42+
public override MethodInfo GetDataReaderMethod()
43+
=> _getStringMethod;
44+
45+
/// <summary>
46+
/// Converts the string read from MySQL to a MemoryStream for EF Core's JSON processing.
47+
/// </summary>
48+
public static MemoryStream CreateUtf8Stream(string json)
49+
=> new MemoryStream(Encoding.UTF8.GetBytes(json ?? string.Empty));
50+
51+
/// <summary>
52+
/// Customizes the data reader expression to convert string to MemoryStream.
53+
/// </summary>
54+
public override Expression CustomizeDataReaderExpression(Expression expression)
55+
=> Expression.Call(_createUtf8StreamMethod, expression);
56+
57+
protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
58+
=> new MySqlStructuralJsonTypeMapping(parameters);
59+
60+
protected string EscapeSqlLiteral(string literal)
61+
=> literal.Replace("'", "''");
62+
63+
protected override string GenerateNonNullSqlLiteral(object value)
64+
=> $"'{EscapeSqlLiteral((string)value)}'";
65+
66+
protected override void ConfigureParameter(DbParameter parameter)
67+
{
68+
// MySQL uses JSON db type for JSON columns
69+
if (parameter is MySqlParameter mySqlParameter)
70+
{
71+
mySqlParameter.MySqlDbType = MySqlDbType.JSON;
72+
}
73+
74+
base.ConfigureParameter(parameter);
75+
}
76+
}
77+
}

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

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,10 @@ private readonly RelationalTypeMapping _binaryRowVersion6
8787
// guid
8888
private GuidTypeMapping _guid;
8989

90-
// JSON default mapping
90+
// JSON default mapping for regular JSON columns mapped to string
9191
private MySqlJsonTypeMapping<string> _jsonDefaultString;
92-
// JSON mapping for complex types (JsonTypePlaceholder)
93-
private MySqlJsonTypeMapping<System.IO.MemoryStream> _jsonComplex;
92+
// JSON mapping for complex types (types mapped with .ToJson())
93+
private MySqlStructuralJsonTypeMapping _jsonStructural;
9494

9595
// Scaffolding type mappings
9696
private readonly MySqlCodeGenerationMemberAccessTypeMapping _codeGenerationMemberAccess = MySqlCodeGenerationMemberAccessTypeMapping.Default;
@@ -137,7 +137,7 @@ private void Initialize()
137137
: null;
138138

139139
_jsonDefaultString = new MySqlJsonTypeMapping<string>("json", null, null, _options.NoBackslashEscapes, _options.ReplaceLineBreaksWithCharFunction);
140-
_jsonComplex = new MySqlJsonTypeMapping<System.IO.MemoryStream>("json", null, null, _options.NoBackslashEscapes, _options.ReplaceLineBreaksWithCharFunction);
140+
_jsonStructural = new MySqlStructuralJsonTypeMapping("json");
141141

142142
_storeTypeMappings
143143
= new Dictionary<string, RelationalTypeMapping[]>(StringComparer.OrdinalIgnoreCase)
@@ -324,11 +324,10 @@ private RelationalTypeMapping FindRawMapping(RelationalTypeMappingInfo mappingIn
324324
// fail immediately.
325325

326326
// Special case for JSON columns: EF Core passes JsonTypePlaceholder as the CLR type
327-
// when creating JSON columns for complex types/collections. Return our JSON mapping
328-
// with byte[] as ClrType so the CustomizeDataReaderExpression logic triggers conversion.
327+
// when creating JSON columns for complex types/collections. Return our structural JSON mapping.
329328
if (clrType?.Name == "JsonTypePlaceholder" && storeTypeName.Equals("json", StringComparison.OrdinalIgnoreCase))
330329
{
331-
return _jsonComplex;
330+
return _jsonStructural;
332331
}
333332

334333
return clrType == null

0 commit comments

Comments
 (0)