Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/EFCore.Relational/Properties/RelationalStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions src/EFCore.Relational/Properties/RelationalStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,9 @@
<data name="ExecuteOperationOnKeylessEntityTypeWithUnsupportedOperator" xml:space="preserve">
<value>The operation '{operation}' cannot be performed on keyless entity type '{entityType}', since it contains an operator not natively supported by the database provider.</value>
</data>
<data name="ExecuteOperationOnOwnedJsonIsNotSupported" xml:space="preserve">
<value>'{operation}' used over owned type '{entityType}' which is mapped to JSON; '{operation}' on JSON-mapped owned entities is not supported. Consider mapping your type as a complex type instead.</value>
</data>
<data name="ExecuteOperationOnTPC" xml:space="preserve">
<value>The operation '{operation}' is being applied on entity type '{entityType}', which is using the TPC mapping strategy and is not a leaf type. 'ExecuteDelete'/'ExecuteUpdate' operations on entity types participating in TPC hierarchies is only supported for leaf types.</value>
</data>
Expand All @@ -433,6 +436,12 @@
<data name="ExecuteOperationWithUnsupportedOperatorInSqlGeneration" xml:space="preserve">
<value>The operation '{operation}' contains a select expression feature that isn't supported in the query SQL generator, but has been declared as supported by provider during translation phase. This is a bug in your EF Core provider, file an issue at https://aka.ms/efcorefeedback.</value>
</data>
<data name="ExecuteUpdateCannotSetJsonPropertyToNonJsonColumn" xml:space="preserve">
<value>'ExecuteUpdate' cannot currently set a property in a JSON column to a regular, non-JSON column; see https://github.com/dotnet/efcore/issues/36688.</value>
</data>
<data name="ExecuteUpdateCannotSetJsonPropertyToArbitraryExpression" xml:space="preserve">
<value>'ExecuteUpdate' cannot currently set a property in a JSON column to arbitrary expressions; only constants, parameters and other JSON properties are supported; see https://github.com/dotnet/efcore/issues/36688.</value>
</data>
<data name="ExecuteUpdateDeleteOnEntityNotMappedToTable" xml:space="preserve">
<value>'ExecuteUpdate' or 'ExecuteDelete' was called on entity type '{entityType}', but that entity type is not mapped to a table.</value>
</data>
Expand Down Expand Up @@ -979,6 +988,9 @@
<data name="MissingResultSetWhenSaving" xml:space="preserve">
<value>A result set was missing when reading the results of a SaveChanges operation; this may indicate that a stored procedure was configured to return results in the EF model, but did not. Check your stored procedure definitions.</value>
</data>
<data name="MultipleColumnsWithSameJsonContainerName" xml:space="preserve">
<value>Entity type '{entityType}' is mapped to multiple columns with name '{columnName}', and one of them is configured as a JSON column. Assign different names to the columns.</value>
</data>
<data name="ModificationCommandBatchAlreadyComplete" xml:space="preserve">
<value>Commands cannot be added to a completed 'ModificationCommandBatch'.</value>
</data>
Expand Down Expand Up @@ -1072,6 +1084,12 @@
<data name="ParameterNotObjectArray" xml:space="preserve">
<value>The value provided for parameter '{parameter}' cannot be used because it isn't assignable to type 'object[]'.</value>
</data>
<data name="JsonPartialExecuteUpdateNotSupportedByProvider" xml:space="preserve">
<value>The provider in use does not support partial updates with ExecuteUpdate within JSON columns.</value>
</data>
<data name="JsonExecuteUpdateNotSupportedWithOwnedEntities" xml:space="preserve">
<value>ExecuteUpdate over JSON columns is not supported when the column is mapped as an owned entity. Map the column as a complex type instead.</value>
</data>
<data name="PendingAmbientTransaction" xml:space="preserve">
<value>This connection was used with an ambient transaction. The original ambient transaction needs to be completed before this connection can be used outside of it.</value>
</data>
Expand Down
119 changes: 119 additions & 0 deletions src/EFCore.Relational/Query/Internal/RelationalJsonUtilities.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections;
using System.Text;
using System.Text.Json;

namespace Microsoft.EntityFrameworkCore.Query.Internal;

/// <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 class RelationalJsonUtilities
{
/// <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 readonly MethodInfo SerializeComplexTypeToJsonMethod =
typeof(RelationalJsonUtilities).GetTypeInfo().GetDeclaredMethod(nameof(SerializeComplexTypeToJson))!;

/// <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 string? SerializeComplexTypeToJson(IComplexType complexType, object? value, bool collection)
{
// Note that we treat toplevel null differently: we return a relational NULL for that case. For nested nulls,
// we return JSON null string (so you get { "foo": null })
if (value is null)
{
return null;
}

var stream = new MemoryStream();
var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = false });

WriteJson(writer, complexType, value, collection);

writer.Flush();

return Encoding.UTF8.GetString(stream.ToArray());

void WriteJson(Utf8JsonWriter writer, IComplexType complexType, object? value, bool collection)
{
if (collection)
{
if (value is null)
{
writer.WriteNullValue();

return;
}

writer.WriteStartArray();

foreach (var element in (IEnumerable)value)
{
WriteJsonObject(writer, complexType, element);
}

writer.WriteEndArray();
return;
}

WriteJsonObject(writer, complexType, value);
}

void WriteJsonObject(Utf8JsonWriter writer, IComplexType complexType, object? objectValue)
{
if (objectValue is null)
{
writer.WriteNullValue();
return;
}

writer.WriteStartObject();

foreach (var property in complexType.GetProperties())
{
var jsonPropertyName = property.GetJsonPropertyName();
Check.DebugAssert(jsonPropertyName is not null);
writer.WritePropertyName(jsonPropertyName);

var propertyValue = property.GetGetter().GetClrValue(objectValue);
if (propertyValue is null)
{
writer.WriteNullValue();
}
else
{
var jsonValueReaderWriter = property.GetJsonValueReaderWriter() ?? property.GetTypeMapping().JsonValueReaderWriter;
Check.DebugAssert(jsonValueReaderWriter is not null, "Missing JsonValueReaderWriter on JSON property");
jsonValueReaderWriter.ToJson(writer, propertyValue);
}
}

foreach (var complexProperty in complexType.GetComplexProperties())
{
var jsonPropertyName = complexProperty.GetJsonPropertyName();
Check.DebugAssert(jsonPropertyName is not null);
writer.WritePropertyName(jsonPropertyName);

var propertyValue = complexProperty.GetGetter().GetClrValue(objectValue);

WriteJson(writer, complexProperty.ComplexType, propertyValue, complexProperty.IsCollection);
}

writer.WriteEndObject();
}
}
}
27 changes: 20 additions & 7 deletions src/EFCore.Relational/Query/QuerySqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1459,20 +1459,33 @@ protected override Expression VisitUpdate(UpdateExpression updateExpression)
|| selectExpression.Tables[1] is CrossJoinExpression))
{
_relationalCommandBuilder.Append("UPDATE ");

Visit(updateExpression.Table);

_relationalCommandBuilder.AppendLine();
_relationalCommandBuilder.Append("SET ");
_relationalCommandBuilder.Append(
$"{_sqlGenerationHelper.DelimitIdentifier(updateExpression.ColumnValueSetters[0].Column.Name)} = ");
Visit(updateExpression.ColumnValueSetters[0].Value);
using (_relationalCommandBuilder.Indent())

for (var i = 0; i < updateExpression.ColumnValueSetters.Count; i++)
{
foreach (var columnValueSetter in updateExpression.ColumnValueSetters.Skip(1))
if (i == 1)
{
Sql.IncrementIndent();
}

if (i > 0)
{
_relationalCommandBuilder.AppendLine(",");
_relationalCommandBuilder.Append($"{_sqlGenerationHelper.DelimitIdentifier(columnValueSetter.Column.Name)} = ");
Visit(columnValueSetter.Value);
}

var (column, value) = updateExpression.ColumnValueSetters[i];

_relationalCommandBuilder.Append(_sqlGenerationHelper.DelimitIdentifier(column.Name)).Append(" = ");
Visit(value);
}

if (updateExpression.ColumnValueSetters.Count > 1)
{
Sql.DecrementIndent();
}

var predicate = selectExpression.Predicate;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,27 @@ public partial class RelationalQueryableMethodTranslatingExpressionVisitor
return null;
}

var mappingStrategy = entityType.GetMappingStrategy();
if (mappingStrategy == RelationalAnnotationNames.TptMappingStrategy)
if (entityType.IsMappedToJson())
{
AddTranslationErrorDetails(
RelationalStrings.ExecuteOperationOnTPT(
nameof(EntityFrameworkQueryableExtensions.ExecuteDelete), entityType.DisplayName()));
RelationalStrings.ExecuteOperationOnOwnedJsonIsNotSupported("ExecuteDelete", entityType.DisplayName()));
return null;
}

if (mappingStrategy == RelationalAnnotationNames.TpcMappingStrategy
&& entityType.GetDirectlyDerivedTypes().Any())
switch (entityType.GetMappingStrategy())
{
// We allow TPC is it is leaf type
AddTranslationErrorDetails(
RelationalStrings.ExecuteOperationOnTPC(
nameof(EntityFrameworkQueryableExtensions.ExecuteDelete), entityType.DisplayName()));
return null;
case RelationalAnnotationNames.TptMappingStrategy:
AddTranslationErrorDetails(
RelationalStrings.ExecuteOperationOnTPT(
nameof(EntityFrameworkQueryableExtensions.ExecuteDelete), entityType.DisplayName()));
return null;

// Note that we do allow TPC if the target is a leaf type
case RelationalAnnotationNames.TpcMappingStrategy when entityType.GetDirectlyDerivedTypes().Any():
AddTranslationErrorDetails(
RelationalStrings.ExecuteOperationOnTPC(
nameof(EntityFrameworkQueryableExtensions.ExecuteDelete), entityType.DisplayName()));
return null;
}

// Find the table model that maps to the entity type; there must be exactly one (e.g. no entity splitting).
Expand Down
Loading
Loading