Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -35,26 +35,50 @@ public SqliteByteArrayTypeMapping(string storeType, DbType? dbType = System.Data
typeof(byte[]),
jsonValueReaderWriter: SqliteJsonByteArrayReaderWriter.Instance),
storeType,
dbType: dbType))
dbType: dbType),
false)
{
}

private SqliteByteArrayTypeMapping(
RelationalTypeMappingParameters parameters,
bool isJsonColumn)
: base(parameters)
{
_isJsonColumn = isJsonColumn;
}

private readonly bool _isJsonColumn;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected SqliteByteArrayTypeMapping(RelationalTypeMappingParameters parameters)
: base(parameters)
{
}


Comment on lines +59 to 60
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra blank lines should be removed. There should only be one blank line between the XML documentation comment and the next member declaration.

Copilot uses AI. Check for mistakes.
/// <summary>
/// Creates a copy of this mapping.
/// </summary>
/// <param name="parameters">The parameters for this mapping.</param>
/// <returns>The newly created mapping.</returns>
protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
=> new SqliteByteArrayTypeMapping(parameters);
=> new SqliteByteArrayTypeMapping(parameters, _isJsonColumn);

internal SqliteByteArrayTypeMapping WithJsonColumn()
=> new(Parameters, true);

/// <summary>
/// Configures the parameter, setting the <see cref="Microsoft.Data.Sqlite.SqliteParameter.SqliteType" /> to
/// <see cref="Microsoft.Data.Sqlite.SqliteType.Text" /> when the mapping is for a JSON column.
/// </summary>
/// <param name="parameter">The parameter to be configured.</param>
protected override void ConfigureParameter(DbParameter parameter)
{
if (_isJsonColumn && parameter is Data.Sqlite.SqliteParameter sqliteParameter)
{
sqliteParameter.SqliteType = Data.Sqlite.SqliteType.Text;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Data;
using Microsoft.EntityFrameworkCore.Sqlite.Storage.Json.Internal;
using Microsoft.EntityFrameworkCore.Storage.Json;

namespace Microsoft.EntityFrameworkCore.Sqlite.Storage.Internal;
Expand Down Expand Up @@ -33,7 +34,7 @@ public SqliteTimeOnlyTypeMapping(
DbType? dbType = System.Data.DbType.Time)
: base(
new RelationalTypeMappingParameters(
new CoreTypeMappingParameters(typeof(TimeOnly), jsonValueReaderWriter: JsonTimeOnlyReaderWriter.Instance),
new CoreTypeMappingParameters(typeof(TimeOnly), jsonValueReaderWriter: SqliteJsonTimeOnlyReaderWriter.Instance),
storeType,
dbType: dbType))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,28 @@ public static bool IsSpatialiteType(string columnType)
: mapping;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
/// <remarks>
/// Finds the type mapping for a given property. This method is overridden to special-case <c>byte[]</c>
/// properties inside JSON columns, returning a mapping that works with hex-encoded string representation.
/// </remarks>
/// <param name="property">The property for which mapping is to be found.</param>
/// <returns>The type mapping, or <see langword="null" /> if none was found.</returns>
public override RelationalTypeMapping? FindMapping(IProperty property)
{
var mapping = base.FindMapping(property);
if (mapping is SqliteByteArrayTypeMapping byteArrayMapping && property.DeclaringType.IsMappedToJson())
{
return byteArrayMapping.WithJsonColumn();
}
return mapping;
}

private RelationalTypeMapping? FindRawMapping(RelationalTypeMappingInfo mappingInfo)
{
var clrType = mappingInfo.ClrType;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Globalization;
using System.Text.Json;
using Microsoft.EntityFrameworkCore.Storage.Json;

namespace Microsoft.EntityFrameworkCore.Sqlite.Storage.Json.Internal;

/// <summary>
/// The Sqlite-specific <see cref="JsonValueReaderWriter{TValue}" /> for <see cref="TimeOnly" />. Writes and reads the JSON string
/// representation of <see cref="TimeOnly" />, using <c>HH:mm:ss</c> when there are no fractional seconds and the round-trip
/// (<c>"o"</c>) format otherwise, to match the SQLite non-JSON representation.
/// </summary>
/// <remarks>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </remarks>
public sealed class SqliteJsonTimeOnlyReaderWriter : JsonValueReaderWriter<TimeOnly>
{
private static readonly PropertyInfo InstanceProperty = typeof(SqliteJsonTimeOnlyReaderWriter).GetProperty(nameof(Instance))!;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public static SqliteJsonTimeOnlyReaderWriter Instance { get; } = new();

private SqliteJsonTimeOnlyReaderWriter()
{
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public override TimeOnly FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null)
=> TimeOnly.Parse(manager.CurrentReader.GetString()!);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public override void ToJsonTyped(Utf8JsonWriter writer, TimeOnly value)
=> writer.WriteStringValue(value.Ticks % TimeSpan.TicksPerSecond == 0 ? string.Format(CultureInfo.InvariantCulture, @"{0:HH\:mm\:ss}", value)
: value.ToString("o"));

/// <inheritdoc />
public override Expression ConstructorExpression
=> Expression.Property(null, InstanceProperty);
}
30 changes: 29 additions & 1 deletion src/Microsoft.Data.Sqlite.Core/SqliteValueBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,17 @@ public virtual void Bind()
}
else if (type == typeof(byte[]))
{
// In SQLite JSON columns, parameter binding for byte[] results in a blob being bound, whereas the column is a text column. This conversion is needed to align representations.
var value1 = (byte[])value;
BindBlob(value1);
if (sqliteType == SqliteType.Text)
{
var value = ToHexString(value1);
BindText(value);
}
else
{
BindBlob(value1);
}
}
else if (type == typeof(char))
{
Expand Down Expand Up @@ -313,4 +322,23 @@ private static double GetTotalDays(int hour, int minute, int second, int millise

return iJD / 86400000.0;
}

private static string ToHexString(byte[] bytes)
{
char[] hexChars = new char[bytes.Length * 2];

for (int i = 0; i < bytes.Length; i++)
{
byte b = bytes[i];

int highNibble = (b >> 4);
int lowNibble = (b & 0x0F);

hexChars[i * 2] = (char)(highNibble < 10 ? highNibble + '0' : highNibble + 'A' - 10);
hexChars[i * 2 + 1] = (char)(lowNibble < 10 ? lowNibble + '0' : lowNibble + 'A' - 10);
}

return new string(hexChars);
}

}
45 changes: 45 additions & 0 deletions test/EFCore.Sqlite.FunctionalTests/JsonTypesSqliteTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,51 @@ public override Task Can_read_write_collection_of_nullable_GUID_JSON_values(stri
=> base.Can_read_write_collection_of_nullable_GUID_JSON_values(
"""{"Prop":["00000000-0000-0000-0000-000000000000",null,"8C44242F-8E3F-4A20-8BE8-98C7C1AADEBD","FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF"]}""");

public override Task Can_read_write_TimeOnly_JSON_values(string value, string json)
=> base.Can_read_write_TimeOnly_JSON_values(
value, value switch
{
"00:00:00.0000000" => """{"Prop":"00:00:00"}""",
"23:59:59.9999999" => """{"Prop":"23:59:59.9999999"}""",
"11:05:12.3456789" => """{"Prop":"11:05:12.3456789"}""",
_ => throw new ArgumentOutOfRangeException(nameof(value), value, null)
});

public override Task Can_read_write_nullable_TimeOnly_JSON_values(string? value, string json)
=> base.Can_read_write_nullable_TimeOnly_JSON_values(
value, value switch
{
"00:00:00.0000000" => """{"Prop":"00:00:00"}""",
"23:59:59.9999999" => """{"Prop":"23:59:59.9999999"}""",
"11:05:12.3456789" => """{"Prop":"11:05:12.3456789"}""",
null => """{"Prop":null}""",
_ => throw new ArgumentOutOfRangeException(nameof(value), value, null)
});

public override Task Can_read_write_collection_of_TimeOnly_JSON_values()
=> Can_read_and_write_JSON_value<TimeOnlyCollectionType, IReadOnlyCollection<TimeOnly>>(
nameof(TimeOnlyCollectionType.TimeOnly),
[
TimeOnly.MinValue,
new TimeOnly(11, 5, 2, 3, 4),
TimeOnly.MaxValue
],
"""{"Prop":["00:00:00","11:05:02.0030040","23:59:59.9999999"]}""",
mappedCollection: true,
new List<TimeOnly>());

public override Task Can_read_write_collection_of_nullable_TimeOnly_JSON_values()
=> Can_read_and_write_JSON_value<NullableTimeOnlyCollectionType, List<TimeOnly?>>(
nameof(NullableTimeOnlyCollectionType.TimeOnly),
[
null,
TimeOnly.MinValue,
new TimeOnly(11, 5, 2, 3, 4),
TimeOnly.MaxValue
],
"""{"Prop":[null,"00:00:00","11:05:02.0030040","23:59:59.9999999"]}""",
mappedCollection: true);

public override Task Can_read_write_ulong_enum_JSON_values(EnumU64 value, string json)
=> Can_read_and_write_JSON_value<EnumU64Type, EnumU64>(nameof(EnumU64Type.EnumU64), value, json);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12219,9 +12219,9 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas
int (string v) => ((object)v).GetHashCode(),
string (string v) => v),
converter: new CollectionToJsonStringConverter<TimeOnly?>(new JsonCollectionOfNullableStructsReaderWriter<TimeOnly?[], TimeOnly>(
JsonTimeOnlyReaderWriter.Instance)),
SqliteJsonTimeOnlyReaderWriter.Instance)),
jsonValueReaderWriter: new JsonCollectionOfNullableStructsReaderWriter<TimeOnly?[], TimeOnly>(
JsonTimeOnlyReaderWriter.Instance),
SqliteJsonTimeOnlyReaderWriter.Instance),
elementMapping: SqliteTimeOnlyTypeMapping.Default);
var nullableTimeOnlyArrayElementType = nullableTimeOnlyArray.SetElementType(typeof(TimeOnly?),
nullable: true);
Expand Down Expand Up @@ -13956,7 +13956,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas
TimeOnly (string v) => TimeOnly.Parse(v, CultureInfo.InvariantCulture, DateTimeStyles.None),
string (TimeOnly v) => (v.Ticks % 10000000L == 0L ? string.Format(CultureInfo.InvariantCulture, "{0:HH\\:mm\\:ss}", ((object)(v))) : v.ToString("o"))),
jsonValueReaderWriter: new JsonConvertedValueReaderWriter<string, TimeOnly>(
JsonTimeOnlyReaderWriter.Instance,
SqliteJsonTimeOnlyReaderWriter.Instance,
new ValueConverter<string, TimeOnly>(
TimeOnly (string v) => TimeOnly.Parse(v, CultureInfo.InvariantCulture, DateTimeStyles.None),
string (TimeOnly v) => (v.Ticks % 10000000L == 0L ? string.Format(CultureInfo.InvariantCulture, "{0:HH\\:mm\\:ss}", ((object)(v))) : v.ToString("o")))));
Expand Down Expand Up @@ -14151,9 +14151,9 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas
int (string v) => ((object)v).GetHashCode(),
string (string v) => v),
converter: new CollectionToJsonStringConverter<TimeOnly>(new JsonCollectionOfStructsReaderWriter<TimeOnly[], TimeOnly>(
JsonTimeOnlyReaderWriter.Instance)),
SqliteJsonTimeOnlyReaderWriter.Instance)),
jsonValueReaderWriter: new JsonCollectionOfStructsReaderWriter<TimeOnly[], TimeOnly>(
JsonTimeOnlyReaderWriter.Instance),
SqliteJsonTimeOnlyReaderWriter.Instance),
elementMapping: SqliteTimeOnlyTypeMapping.Default);
var timeOnlyArrayElementType = timeOnlyArray.SetElementType(typeof(TimeOnly));
timeOnlyArrayElementType.TypeMapping = timeOnlyArray.TypeMapping.ElementTypeMapping;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12219,9 +12219,9 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas
int (string v) => ((object)v).GetHashCode(),
string (string v) => v),
converter: new CollectionToJsonStringConverter<TimeOnly?>(new JsonCollectionOfNullableStructsReaderWriter<TimeOnly?[], TimeOnly>(
JsonTimeOnlyReaderWriter.Instance)),
SqliteJsonTimeOnlyReaderWriter.Instance)),
jsonValueReaderWriter: new JsonCollectionOfNullableStructsReaderWriter<TimeOnly?[], TimeOnly>(
JsonTimeOnlyReaderWriter.Instance),
SqliteJsonTimeOnlyReaderWriter.Instance),
elementMapping: SqliteTimeOnlyTypeMapping.Default);
var nullableTimeOnlyArrayElementType = nullableTimeOnlyArray.SetElementType(typeof(TimeOnly?),
nullable: true);
Expand Down Expand Up @@ -13956,7 +13956,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas
TimeOnly (string v) => TimeOnly.Parse(v, CultureInfo.InvariantCulture, DateTimeStyles.None),
string (TimeOnly v) => (v.Ticks % 10000000L == 0L ? string.Format(CultureInfo.InvariantCulture, "{0:HH\\:mm\\:ss}", ((object)(v))) : v.ToString("o"))),
jsonValueReaderWriter: new JsonConvertedValueReaderWriter<string, TimeOnly>(
JsonTimeOnlyReaderWriter.Instance,
SqliteJsonTimeOnlyReaderWriter.Instance,
new ValueConverter<string, TimeOnly>(
TimeOnly (string v) => TimeOnly.Parse(v, CultureInfo.InvariantCulture, DateTimeStyles.None),
string (TimeOnly v) => (v.Ticks % 10000000L == 0L ? string.Format(CultureInfo.InvariantCulture, "{0:HH\\:mm\\:ss}", ((object)(v))) : v.ToString("o")))));
Expand Down Expand Up @@ -14151,9 +14151,9 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas
int (string v) => ((object)v).GetHashCode(),
string (string v) => v),
converter: new CollectionToJsonStringConverter<TimeOnly>(new JsonCollectionOfStructsReaderWriter<TimeOnly[], TimeOnly>(
JsonTimeOnlyReaderWriter.Instance)),
SqliteJsonTimeOnlyReaderWriter.Instance)),
jsonValueReaderWriter: new JsonCollectionOfStructsReaderWriter<TimeOnly[], TimeOnly>(
JsonTimeOnlyReaderWriter.Instance),
SqliteJsonTimeOnlyReaderWriter.Instance),
elementMapping: SqliteTimeOnlyTypeMapping.Default);
var timeOnlyArrayElementType = timeOnlyArray.SetElementType(typeof(TimeOnly));
timeOnlyArrayElementType.TypeMapping = timeOnlyArray.TypeMapping.ElementTypeMapping;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,7 @@ public class GuidTypeFixture : SqliteTypeFixture<Guid>
public class SqliteByteArrayTypeTest(SqliteByteArrayTypeTest.ByteArrayTypeFixture fixture, ITestOutputHelper testOutputHelper)
: RelationalTypeTestBase<byte[], SqliteByteArrayTypeTest.ByteArrayTypeFixture>(fixture, testOutputHelper)
{
// TODO: string representation discrepancy between our JSON and M.D.SQLite's string representation, see #36749.
public override Task Query_property_within_json()
=> Assert.ThrowsAsync<InvalidOperationException>(() => base.Query_property_within_json());
public override Task Query_property_within_json() => base.Query_property_within_json();

public override async Task ExecuteUpdate_within_json_to_nonjson_column()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ public class DateTypeFixture : SqliteTypeFixture<DateOnly>
public class SqliteTimeOnlyTypeTest(SqliteTimeOnlyTypeTest.TimeTypeFixture fixture, ITestOutputHelper testOutputHelper)
: RelationalTypeTestBase<TimeOnly, SqliteTimeOnlyTypeTest.TimeTypeFixture>(fixture, testOutputHelper)
{
// TODO: string representation discrepancy between our JSON and M.D.SQLite's string representation, see #36749.

public override Task Query_property_within_json()
=> Assert.ThrowsAsync<InvalidOperationException>(() => base.Query_property_within_json());
=> base.Query_property_within_json();

public override async Task ExecuteUpdate_within_json_to_nonjson_column()
{
Expand Down
Loading
Loading