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
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,6 @@ static void ValidateType(ITypeBase typeBase, IDiagnosticsLogger<DbLoggerCategory

foreach (var complexProperty in typeBase.GetDeclaredComplexProperties())
{
if (complexProperty.IsCollection)
{
throw new InvalidOperationException(
CosmosStrings.ComplexTypeCollectionsNotSupported(
complexProperty.ComplexType.ShortName(),
complexProperty.Name));
}

ValidateType(complexProperty.ComplexType, logger);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ private SaveGroups CreateSaveGroups(IList<IUpdateEntry> entries)
SingleUpdateEntries = []
};
}

var batches = CreateBatches(batchableEntries);

// For bulk it is important that single writes are always classified as singleUpdateEntries so that they will be executed in parallel
Expand Down
214 changes: 141 additions & 73 deletions src/EFCore.Cosmos/Update/Internal/DocumentSource.cs

Large diffs are not rendered by default.

38 changes: 38 additions & 0 deletions src/EFCore/Update/Internal/InternalUpdateEntryExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;

namespace Microsoft.EntityFrameworkCore.Update.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 InternalUpdateEntryExtensions
{
/// <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 object? GetCurrentProviderValue(this IInternalEntry updateEntry, IProperty property)
{
var value = updateEntry.GetCurrentValue(property);
var typeMapping = property.GetTypeMapping();
value = value?.GetType().IsInteger() == true && typeMapping.ClrType.UnwrapNullableType().IsEnum
? Enum.ToObject(typeMapping.ClrType.UnwrapNullableType(), value)
: value;

var converter = typeMapping.Converter;
if (converter != null)
{
value = converter.ConvertToProvider(value);
}

return value;
}
}
17 changes: 2 additions & 15 deletions src/EFCore/Update/UpdateEntryExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Text;
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Update.Internal;

namespace Microsoft.EntityFrameworkCore.Update;

Expand All @@ -25,21 +26,7 @@ public static class UpdateEntryExtensions
/// <param name="property">The property to get the value for.</param>
/// <returns>The value for the property.</returns>
public static object? GetCurrentProviderValue(this IUpdateEntry updateEntry, IProperty property)
{
var value = updateEntry.GetCurrentValue(property);
var typeMapping = property.GetTypeMapping();
value = value?.GetType().IsInteger() == true && typeMapping.ClrType.UnwrapNullableType().IsEnum
? Enum.ToObject(typeMapping.ClrType.UnwrapNullableType(), value)
: value;

var converter = typeMapping.Converter;
if (converter != null)
{
value = converter.ConvertToProvider(value);
}

return value;
}
=> ((IInternalEntry)updateEntry).GetCurrentProviderValue(property);

/// <summary>
/// Gets the original value that was assigned to the property and converts it to the provider-expected value.
Expand Down
178 changes: 178 additions & 0 deletions test/EFCore.Cosmos.FunctionalTests/CosmosComplexTypesTrackingTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Newtonsoft.Json.Linq;
using Xunit.Sdk;

namespace Microsoft.EntityFrameworkCore;

public class CosmosComplexTypesTrackingTest(CosmosComplexTypesTrackingTest.CosmosFixture fixture) : ComplexTypesTrackingTestBase<CosmosComplexTypesTrackingTest.CosmosFixture>(fixture)
{
[ConditionalFact]
public async Task Can_reorder_complex_collection_elements()
{
await using var context = CreateContext();
var pub = CreatePubWithCollections(context);
await context.AddAsync(pub);
await context.SaveChangesAsync();

pub.Activities.Reverse();
var first = pub.Activities[0];
var last = pub.Activities.Last();
await context.SaveChangesAsync();

// TODO: Can be asserted after binding has been implemented.
//await using var assertContext = CreateContext();
//var dbPub = await assertContext.Set<PubWithCollections>().FirstAsync(x => x.Id == pub.Id);
//Assert.Equivalent(first, dbPub.Activities[0]);
//Assert.Equivalent(last, dbPub.Activities.Last());
}

[ConditionalFact]
public async Task Can_change_complex_collection_element()
{
await using var context = CreateContext();
var pub = CreatePubWithCollections(context);
await context.AddAsync(pub);
await context.SaveChangesAsync();

pub.Activities[0].Name = "Changed123";
await context.SaveChangesAsync();

// TODO: Can be asserted after binding has been implemented.
//await using var assertContext = CreateContext();
//var dbPub = await assertContext.Set<PubWithCollections>().FirstAsync(x => x.Id == pub.Id);
//Assert.Equivalent("Changed123", dbPub.Activities[0].Name);
}

[ConditionalFact]
public async Task Can_add_complex_collection_element()
{
await using var context = CreateContext();
var pub = CreatePubWithCollections(context);
await context.AddAsync(pub);
await context.SaveChangesAsync();

pub.Activities.Add(new ActivityWithCollection { Name = "NewActivity" });
await context.SaveChangesAsync();

// TODO: Can be asserted after binding has been implemented.
//await using var assertContext = CreateContext();
//var dbPub = await assertContext.Set<PubWithCollections>().FirstAsync(x => x.Id == pub.Id);
//Assert.Equivalent("NewActivity", dbPub.Activities.Last().Name);
//Assert.Equivalent(pub.Activities.Count, dbPub.Activities.Count);
}

[ConditionalFact]
public async Task Can_add_and_dynamically_update_complex_collection_element()
{
await using var context = CreateContext();
var pub = CreatePubWithCollections(context);
await context.AddAsync(pub);
await context.SaveChangesAsync();

var pubJObject = context.Entry(pub).Property<JObject>("__jObject").CurrentValue;
pubJObject["Activities"]![0]!["test"] = "test";
pub.Activities.Insert(0, new ActivityWithCollection { Name = "NewActivity" });

await context.SaveChangesAsync();

await using var assertContext = CreateContext();
var dbPub = await assertContext.Set<PubWithCollections>().FirstAsync(x => x.Id == pub.Id);
var dbPubJObject = assertContext.Entry(dbPub).Property<JObject>("__jObject").CurrentValue;
Assert.Equal("test", dbPubJObject["Activities"]![1]!["test"]);
Assert.Equal("NewActivity", dbPubJObject["Activities"]![0]!["Name"]);
}

public override Task Can_save_null_second_level_complex_property_with_required_properties(bool async)
{
if (!async)
{
throw SkipException.ForSkip("Cosmos does not support synchronous operations.");
}

return base.Can_save_null_second_level_complex_property_with_required_properties(async);
}

public override Task Can_save_null_third_level_complex_property_with_all_optional_properties(bool async)
{
if (!async)
{
throw SkipException.ForSkip("Cosmos does not support synchronous operations.");
}

return base.Can_save_null_third_level_complex_property_with_all_optional_properties(async);
}

protected override Task TrackAndSaveTest<TEntity>(EntityState state, bool async, Func<DbContext, TEntity> createPub)
{
if (!async)
{
throw SkipException.ForSkip("Cosmos does not support synchronous operations.");
}

return base.TrackAndSaveTest(state, async, createPub);
}

protected override async Task ExecuteWithStrategyInTransactionAsync(Func<DbContext, Task> testOperation, Func<DbContext, Task>? nestedTestOperation1 = null, Func<DbContext, Task>? nestedTestOperation2 = null)
{
using var c = CreateContext();
await c.Database.CreateExecutionStrategy().ExecuteAsync(
c, async context =>
{
using (var innerContext = CreateContext())
{
await testOperation(innerContext);
}

if (nestedTestOperation1 == null)
{
return;
}

using (var innerContext1 = CreateContext())
{
await nestedTestOperation1(innerContext1);
}

if (nestedTestOperation2 == null)
{
return;
}

using (var innerContext2 = CreateContext())
{
await nestedTestOperation2(innerContext2);
}
});
}

public class CosmosFixture : FixtureBase
{
protected override ITestStoreFactory TestStoreFactory
=> CosmosTestStoreFactory.Instance;

protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context)
{
base.OnModelCreating(modelBuilder, context);
modelBuilder.Entity<Pub>().HasPartitionKey(x => x.Id);
modelBuilder.Entity<PubWithStructs>().HasPartitionKey(x => x.Id);
modelBuilder.Entity<PubWithReadonlyStructs>().HasPartitionKey(x => x.Id);
modelBuilder.Entity<PubWithRecords>().HasPartitionKey(x => x.Id);
modelBuilder.Entity<PubWithCollections>().HasPartitionKey(x => x.Id);
modelBuilder.Entity<PubWithRecordCollections>().HasPartitionKey(x => x.Id);
modelBuilder.Entity<PubWithArrayCollections>().HasPartitionKey(x => x.Id);
modelBuilder.Entity<PubWithRecordArrayCollections>().HasPartitionKey(x => x.Id);
modelBuilder.Entity<PubWithPropertyBagCollections>().HasPartitionKey(x => x.Id);
if (!UseProxies)
{
modelBuilder.Entity<FieldPub>().HasPartitionKey(x => x.Id);
modelBuilder.Entity<FieldPubWithStructs>().HasPartitionKey(x => x.Id);
modelBuilder.Entity<FieldPubWithRecords>().HasPartitionKey(x => x.Id);
modelBuilder.Entity<FieldPubWithCollections>().HasPartitionKey(x => x.Id);
modelBuilder.Entity<FieldPubWithRecordCollections>().HasPartitionKey(x => x.Id);
modelBuilder.Entity<Yogurt>().HasPartitionKey(x => x.Id);
}
}
}
}
Loading
Loading