Skip to content

Commit

Permalink
Support ReadItem for no-tracking queries
Browse files Browse the repository at this point in the history
Part of #20693
Part of #33893

There is a lot left to do here, but I'm making a break here to get reviews before it goes too far.

Major changes here are:
- Discover and record properties used to form the JSON `id` in one place.
- Use this to generate ate `id` values without tracking an instance. (Makes no-tracking work, needed for Reload.)
- Be better at detecting only detecting patterns we can later translate.

Next up: be better at detecting non-Find query patterns that we can translate.
  • Loading branch information
ajcvickers committed Jun 15, 2024
1 parent 3d7800e commit 2e079da
Show file tree
Hide file tree
Showing 20 changed files with 1,705 additions and 172 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public override void Generate(IEntityType entityType, CSharpRuntimeAnnotationCod
annotations.Remove(CosmosAnnotationNames.AnalyticalStoreTimeToLive);
annotations.Remove(CosmosAnnotationNames.DefaultTimeToLive);
annotations.Remove(CosmosAnnotationNames.Throughput);
annotations.Remove(CosmosAnnotationNames.JsonIdDefinition);
}

base.Generate(entityType, parameters);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.EntityFrameworkCore.Cosmos.Diagnostics.Internal;
using Microsoft.EntityFrameworkCore.Cosmos.Infrastructure.Internal;
using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Conventions.Internal;
using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Cosmos.Query.Internal;
using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal;
using Microsoft.EntityFrameworkCore.Cosmos.ValueGeneration.Internal;
Expand Down Expand Up @@ -115,6 +116,7 @@ public static IServiceCollection AddEntityFrameworkCosmos(this IServiceCollectio
.TryAddSingleton<ICosmosSingletonOptions, CosmosSingletonOptions>()
.TryAddSingleton<ISingletonCosmosClientWrapper, SingletonCosmosClientWrapper>()
.TryAddSingleton<IQuerySqlGeneratorFactory, QuerySqlGeneratorFactory>()
.TryAddSingleton<IRuntimeJsonIdDefinitionFactory, RuntimeJsonIdDefinitionFactory>()
.TryAddScoped<ISqlExpressionFactory, SqlExpressionFactory>()
.TryAddScoped<IMemberTranslatorProvider, CosmosMemberTranslatorProvider>()
.TryAddScoped<IMethodCallTranslatorProvider, CosmosMethodCallTranslatorProvider>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,19 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Conventions;
/// </remarks>
public class CosmosRuntimeModelConvention : RuntimeModelConvention
{
private readonly IRuntimeJsonIdDefinitionFactory _runtimeJsonIdDefinitionFactory;

/// <summary>
/// Creates a new instance of <see cref="CosmosRuntimeModelConvention" />.
/// </summary>
/// <param name="dependencies">Parameter object containing dependencies for this convention.</param>
/// <param name="runtimeJsonIdDefinitionFactory">A factory for creating <see cref="RuntimeJsonIdDefinition"/> instance.</param>
public CosmosRuntimeModelConvention(
ProviderConventionSetBuilderDependencies dependencies)
ProviderConventionSetBuilderDependencies dependencies,
IRuntimeJsonIdDefinitionFactory runtimeJsonIdDefinitionFactory)
: base(dependencies)
{
_runtimeJsonIdDefinitionFactory = runtimeJsonIdDefinitionFactory;
}

/// <summary>
Expand Down Expand Up @@ -67,5 +72,12 @@ protected override void ProcessEntityTypeAnnotations(
annotations.Remove(CosmosAnnotationNames.DefaultTimeToLive);
annotations.Remove(CosmosAnnotationNames.Throughput);
}

if (annotations.TryGetAndRemove(CosmosAnnotationNames.JsonIdDefinition, out JsonIdDefinition jsonId))
{
runtimeEntityType.AddRuntimeAnnotation(
CosmosAnnotationNames.JsonIdDefinition,
_runtimeJsonIdDefinitionFactory.Create(runtimeEntityType, jsonId));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// 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.Cosmos.Metadata.Internal;

namespace Microsoft.EntityFrameworkCore.Cosmos.Metadata.Conventions.Internal;

/// <summary>
Expand All @@ -11,16 +13,20 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Metadata.Conventions.Internal;
/// </summary>
public class CosmosConventionSetBuilder : ProviderConventionSetBuilder
{
private readonly IRuntimeJsonIdDefinitionFactory _runtimeJsonIdDefinitionFactory;

/// <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 CosmosConventionSetBuilder(
ProviderConventionSetBuilderDependencies dependencies)
ProviderConventionSetBuilderDependencies dependencies,
IRuntimeJsonIdDefinitionFactory runtimeJsonIdDefinitionFactory)
: base(dependencies)
{
_runtimeJsonIdDefinitionFactory = runtimeJsonIdDefinitionFactory;
}

/// <summary>
Expand All @@ -36,14 +42,15 @@ public override ConventionSet CreateConventionSet()
conventionSet.Add(new ContextContainerConvention(Dependencies));
conventionSet.Add(new ETagPropertyConvention());
conventionSet.Add(new StoreKeyConvention(Dependencies));
conventionSet.Add(new JsonIdConvention(Dependencies));

conventionSet.Replace<ValueGenerationConvention>(new CosmosValueGenerationConvention(Dependencies));
conventionSet.Replace<KeyDiscoveryConvention>(new CosmosKeyDiscoveryConvention(Dependencies));
conventionSet.Replace<InversePropertyAttributeConvention>(new CosmosInversePropertyAttributeConvention(Dependencies));
conventionSet.Replace<RelationshipDiscoveryConvention>(new CosmosRelationshipDiscoveryConvention(Dependencies));
conventionSet.Replace<DiscriminatorConvention>(new CosmosDiscriminatorConvention(Dependencies));
conventionSet.Replace<ManyToManyJoinEntityTypeConvention>(new CosmosManyToManyJoinEntityTypeConvention(Dependencies));
conventionSet.Replace<RuntimeModelConvention>(new CosmosRuntimeModelConvention(Dependencies));
conventionSet.Replace<RuntimeModelConvention>(new CosmosRuntimeModelConvention(Dependencies, _runtimeJsonIdDefinitionFactory));

return conventionSet;
}
Expand Down
99 changes: 99 additions & 0 deletions src/EFCore.Cosmos/Metadata/Conventions/JsonIdConvention.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// 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.Cosmos.Metadata.Internal;

namespace Microsoft.EntityFrameworkCore.Cosmos.Metadata.Conventions;

/// <summary>
/// A convention that builds an <see cref="JsonIdDefinition"/> for each top-level entity type. This is used
/// to build JSON `id` property values from combinations of other property values.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-conventions">Model building conventions</see>, and
/// <see href="https://aka.ms/efcore-docs-cosmos">Accessing Azure Cosmos DB with EF Core</see> for more information and examples.
/// </remarks>
public class JsonIdConvention : IModelFinalizingConvention
{
/// <summary>
/// Creates a new instance of <see cref="JsonIdConvention" />.
/// </summary>
/// <param name="dependencies">Parameter object containing dependencies for this convention.</param>
public JsonIdConvention(ProviderConventionSetBuilderDependencies dependencies)
{
Dependencies = dependencies;
}

/// <summary>
/// Dependencies for this service.
/// </summary>
protected virtual ProviderConventionSetBuilderDependencies Dependencies { get; }

/// <inheritdoc />
public virtual void ProcessModelFinalizing(
IConventionModelBuilder modelBuilder,
IConventionContext<IConventionModelBuilder> context)
{
foreach (var entityType in modelBuilder.Metadata.GetEntityTypes())
{
var primaryKey = entityType.FindPrimaryKey();
if (entityType.IsOwned() || primaryKey == null)
{
entityType.RemoveAnnotation(CosmosAnnotationNames.JsonIdDefinition);
continue;
}

// Remove properties that are also partition keys, since Cosmos handles those separately, and so they should not be in `id`.
var partitionKeyNames = entityType.GetPartitionKeyPropertyNames();
var primaryKeyProperties = new List<IConventionProperty>(primaryKey.Properties.Count);
foreach (var property in primaryKey.Properties)
{
if (!partitionKeyNames.Contains(property.Name))
{
primaryKeyProperties.Add(property);
}
}

var idProperty = entityType.GetProperties()
.FirstOrDefault(p => p.GetJsonPropertyName() == StoreKeyConvention.IdPropertyJsonName);

var properties = new List<IConventionProperty>();
// If the property mapped to the JSON id is simply the primary key, or is the primary key without partition keys, then use
// it directly.
if ((primaryKeyProperties.Count == 1
&& primaryKeyProperties[0] == idProperty)
|| (primaryKey.Properties.Count == 1
&& primaryKey.Properties[0] == idProperty))
{
properties.Add(idProperty);
}
// Otherwise, if the property mapped to the JSON id doesn't have a generator, then we can't use ReadItem.
else if (idProperty != null && idProperty.GetValueGeneratorFactory() == null)
{
entityType.RemoveAnnotation(CosmosAnnotationNames.JsonIdDefinition);
continue;
}
else
{
var discriminator = entityType.GetDiscriminatorValue();
// If the discriminator is not part of the primary key already, then add it to the Cosmos `id`.
if (discriminator != null)
{
var discriminatorProperty = entityType.FindDiscriminatorProperty();
if (!primaryKey.Properties.Contains(discriminatorProperty))
{
properties.Add(discriminatorProperty!);
}
}

// Next add all primary key properties, except for those that are also partition keys, which were removed above.
foreach (var property in primaryKeyProperties)
{
properties.Add(property);
}
}

entityType.SetAnnotation(CosmosAnnotationNames.JsonIdDefinition, new JsonIdDefinition(properties));
}
}
}
3 changes: 2 additions & 1 deletion src/EFCore.Cosmos/Metadata/Conventions/StoreKeyConvention.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ private static void ProcessIdProperty(IConventionEntityTypeBuilder entityTypeBui

if (idProperty != null)
{
if (idProperty.ClrType == typeof(string))
var converter = idProperty.GetValueConverter();
if ((converter == null ? idProperty.ClrType : converter.ProviderClrType) == typeof(string))
{
if (idProperty.IsPrimaryKey())
{
Expand Down
8 changes: 8 additions & 0 deletions src/EFCore.Cosmos/Metadata/Internal/CosmosAnnotationNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,12 @@ public static class CosmosAnnotationNames
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public const string Throughput = Prefix + "Throughput";

/// <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 const string JsonIdDefinition = Prefix + "JsonIdDefinition";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.EntityFrameworkCore.Cosmos.Metadata.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 interface IRuntimeJsonIdDefinitionFactory
{
/// <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>
RuntimeJsonIdDefinition Create(RuntimeEntityType entityType, JsonIdDefinition jsonIdDefinition);
}
32 changes: 32 additions & 0 deletions src/EFCore.Cosmos/Metadata/Internal/JsonIdDefinition.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.EntityFrameworkCore.Cosmos.Metadata.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 readonly struct JsonIdDefinition
{
/// <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 IReadOnlyList<IConventionProperty> Properties { get; }

/// <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 JsonIdDefinition(IReadOnlyList<IConventionProperty> properties)
{
Properties = properties;
}
}
Loading

0 comments on commit 2e079da

Please sign in to comment.