diff --git a/EFCore.Runtime.slnf b/EFCore.Runtime.slnf index 1e63744a669..504bb19e161 100644 --- a/EFCore.Runtime.slnf +++ b/EFCore.Runtime.slnf @@ -36,7 +36,7 @@ "test\\EFCore.SqlServer.Tests\\EFCore.SqlServer.Tests.csproj", "test\\EFCore.Sqlite.FunctionalTests\\EFCore.Sqlite.FunctionalTests.csproj", "test\\EFCore.Sqlite.Tests\\EFCore.Sqlite.Tests.csproj", - "test\\EFCore.Tests\\EFCore.Tests.csproj", + "test\\EFCore.Tests\\EFCore.Tests.csproj" ] } } \ No newline at end of file diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs index 29ae6d82cc6..38b207a93b8 100644 --- a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs +++ b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.Cosmos.Internal; +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Update; @@ -20,7 +21,7 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal public class CosmosDatabaseCreator : IDatabaseCreator { private readonly ICosmosClientWrapper _cosmosClient; - private readonly IModel _model; + private readonly IModel _designModel; private readonly IUpdateAdapterFactory _updateAdapterFactory; private readonly IDatabase _database; @@ -32,12 +33,12 @@ public class CosmosDatabaseCreator : IDatabaseCreator /// public CosmosDatabaseCreator( ICosmosClientWrapper cosmosClient, - IModel model, + ICurrentDbContext context, IUpdateAdapterFactory updateAdapterFactory, IDatabase database) { _cosmosClient = cosmosClient; - _model = model; + _designModel = context.Context.DesignTimeModel; _updateAdapterFactory = updateAdapterFactory; _database = database; } @@ -51,7 +52,7 @@ public CosmosDatabaseCreator( public virtual bool EnsureCreated() { var created = _cosmosClient.CreateDatabaseIfNotExists(); - foreach (var entityType in _model.GetEntityTypes()) + foreach (var entityType in _designModel.GetEntityTypes()) { var containerName = entityType.GetContainer(); if (containerName != null) @@ -80,7 +81,7 @@ public virtual async Task EnsureCreatedAsync(CancellationToken cancellatio { var created = await _cosmosClient.CreateDatabaseIfNotExistsAsync(cancellationToken) .ConfigureAwait(false); - foreach (var entityType in _model.GetEntityTypes()) + foreach (var entityType in _designModel.GetEntityTypes()) { var containerName = entityType.GetContainer(); if (containerName != null) @@ -130,10 +131,12 @@ public virtual Task SeedAsync(CancellationToken cancellationToken = default) private IUpdateAdapter AddSeedData() { var updateAdapter = _updateAdapterFactory.CreateStandalone(); - foreach (var entityType in _model.GetEntityTypes()) + foreach (var entityType in _designModel.GetEntityTypes()) { + IEntityType? targetEntityType = null; foreach (var targetSeed in entityType.GetSeedData()) { + targetEntityType ??= updateAdapter.Model.FindEntityType(entityType.Name)!; var entry = updateAdapter.CreateEntry(targetSeed, entityType); entry.EntityState = EntityState.Added; } diff --git a/src/EFCore.Design/Design/DesignTimeServiceCollectionExtensions.cs b/src/EFCore.Design/Design/DesignTimeServiceCollectionExtensions.cs index 27a02922155..82e1c4a3cde 100644 --- a/src/EFCore.Design/Design/DesignTimeServiceCollectionExtensions.cs +++ b/src/EFCore.Design/Design/DesignTimeServiceCollectionExtensions.cs @@ -120,7 +120,7 @@ public static IServiceCollection AddDbContextDesignTimeServices( .AddTransient(_ => context.GetService()) .AddTransient(_ => context.GetService()) .AddTransient(_ => context.GetService()) - .AddTransient(_ => context.GetService()) + .AddTransient(_ => context.DesignTimeModel) .AddTransient(_ => context.GetService()); } } diff --git a/src/EFCore.Design/Migrations/Internal/SnapshotModelProcessor.cs b/src/EFCore.Design/Migrations/Internal/SnapshotModelProcessor.cs index 8375cdd7c12..d139f1e464f 100644 --- a/src/EFCore.Design/Migrations/Internal/SnapshotModelProcessor.cs +++ b/src/EFCore.Design/Migrations/Internal/SnapshotModelProcessor.cs @@ -84,7 +84,7 @@ public SnapshotModelProcessor( model = mutableModel.FinalizeModel(); } - return _modelRuntimeInitializer.Initialize((IModel)model, validationLogger: null); + return _modelRuntimeInitializer.Initialize((IModel)model, designTime: true, validationLogger: null); } private void ProcessCollection(IEnumerable metadata, string version) diff --git a/src/EFCore.Design/Scaffolding/Internal/RelationalScaffoldingModelFactory.cs b/src/EFCore.Design/Scaffolding/Internal/RelationalScaffoldingModelFactory.cs index 2314f535973..84134ac622e 100644 --- a/src/EFCore.Design/Scaffolding/Internal/RelationalScaffoldingModelFactory.cs +++ b/src/EFCore.Design/Scaffolding/Internal/RelationalScaffoldingModelFactory.cs @@ -112,7 +112,7 @@ public virtual IModel Create(DatabaseModel databaseModel, ModelReverseEngineerOp VisitDatabaseModel(modelBuilder, databaseModel); - return _modelRuntimeInitializer.Initialize(modelBuilder.FinalizeModel(), null); + return _modelRuntimeInitializer.Initialize(modelBuilder.FinalizeModel(), designTime: true, null); } /// diff --git a/src/EFCore.InMemory/Storage/Internal/IInMemoryStore.cs b/src/EFCore.InMemory/Storage/Internal/IInMemoryStore.cs index 2d3a4a67b36..9e76286e6fe 100644 --- a/src/EFCore.InMemory/Storage/Internal/IInMemoryStore.cs +++ b/src/EFCore.InMemory/Storage/Internal/IInMemoryStore.cs @@ -25,6 +25,7 @@ public interface IInMemoryStore /// bool EnsureCreated( IUpdateAdapterFactory updateAdapterFactory, + IModel designModel, IDiagnosticsLogger updateLogger); /// diff --git a/src/EFCore.InMemory/Storage/Internal/InMemoryDatabase.cs b/src/EFCore.InMemory/Storage/Internal/InMemoryDatabase.cs index 8dab6501a49..1576aefd5ed 100644 --- a/src/EFCore.InMemory/Storage/Internal/InMemoryDatabase.cs +++ b/src/EFCore.InMemory/Storage/Internal/InMemoryDatabase.cs @@ -1,11 +1,13 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Update; using Microsoft.EntityFrameworkCore.Utilities; @@ -32,6 +34,7 @@ public class InMemoryDatabase : Database, IInMemoryDatabase private readonly IInMemoryStore _store; private readonly IUpdateAdapterFactory _updateAdapterFactory; private readonly IDiagnosticsLogger _updateLogger; + private readonly Func _getDesignModel; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -43,6 +46,7 @@ public InMemoryDatabase( DatabaseDependencies dependencies, IInMemoryStoreCache storeCache, IDbContextOptions options, + ICurrentDbContext context, IUpdateAdapterFactory updateAdapterFactory, IDiagnosticsLogger updateLogger) : base(dependencies) @@ -53,6 +57,7 @@ public InMemoryDatabase( Check.NotNull(updateLogger, nameof(updateLogger)); _store = storeCache.GetStore(options); + _getDesignModel = () => context.Context.DesignTimeModel; _updateAdapterFactory = updateAdapterFactory; _updateLogger = updateLogger; } @@ -93,6 +98,6 @@ public override Task SaveChangesAsync( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual bool EnsureDatabaseCreated() - => _store.EnsureCreated(_updateAdapterFactory, _updateLogger); + => _store.EnsureCreated(_updateAdapterFactory, _getDesignModel(), _updateLogger); } } diff --git a/src/EFCore.InMemory/Storage/Internal/InMemoryStore.cs b/src/EFCore.InMemory/Storage/Internal/InMemoryStore.cs index d9ce25747c4..fd026ea8f6f 100644 --- a/src/EFCore.InMemory/Storage/Internal/InMemoryStore.cs +++ b/src/EFCore.InMemory/Storage/Internal/InMemoryStore.cs @@ -68,6 +68,7 @@ public virtual InMemoryIntegerValueGenerator GetIntegerValueGenerator /// public virtual bool EnsureCreated( IUpdateAdapterFactory updateAdapterFactory, + IModel designModel, IDiagnosticsLogger updateLogger) { lock (_lock) @@ -80,11 +81,13 @@ public virtual bool EnsureCreated( var updateAdapter = updateAdapterFactory.CreateStandalone(); var entries = new List(); - foreach (var entityType in updateAdapter.Model.GetEntityTypes()) + foreach (var entityType in designModel.GetEntityTypes()) { + IEntityType? targetEntityType = null; foreach (var targetSeed in entityType.GetSeedData()) { - var entry = updateAdapter.CreateEntry(targetSeed, entityType); + targetEntityType ??= updateAdapter.Model.FindEntityType(entityType.Name)!; + var entry = updateAdapter.CreateEntry(targetSeed, targetEntityType); entry.EntityState = EntityState.Added; entries.Add(entry); } diff --git a/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs b/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs index 834a9122912..80b48b17400 100644 --- a/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs +++ b/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs @@ -109,6 +109,10 @@ public override ConventionSet CreateConventionSet() (QueryFilterRewritingConvention)new RelationalQueryFilterRewritingConvention( Dependencies, RelationalDependencies)); + ReplaceConvention( + conventionSet.ModelFinalizedConventions, + (SlimModelConvention)new RelationalSlimModelConvention(Dependencies, RelationalDependencies)); + return conventionSet; } } diff --git a/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilderDependencies.cs b/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilderDependencies.cs index b243057bc9c..bff4e656635 100644 --- a/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilderDependencies.cs +++ b/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilderDependencies.cs @@ -71,7 +71,6 @@ public RelationalConventionSetBuilderDependencies(IRelationalAnnotationProvider /// /// The relational annotation provider. /// - [Obsolete("This is now part of RelationalModelRuntimeInitializerDependencies")] public IRelationalAnnotationProvider RelationalAnnotationProvider { get; init; } } } diff --git a/src/EFCore.Relational/Metadata/Conventions/RelationalSlimModelConvention.cs b/src/EFCore.Relational/Metadata/Conventions/RelationalSlimModelConvention.cs new file mode 100644 index 00000000000..d4a08b4cfc4 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Conventions/RelationalSlimModelConvention.cs @@ -0,0 +1,388 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Internal; + +#nullable enable + +namespace Microsoft.EntityFrameworkCore.Metadata.Conventions +{ + /// + /// A convention that creates an optimized copy of the mutable model. This convention is typically + /// implemented by database providers to update provider annotations when creating a read-only model. + /// + public class RelationalSlimModelConvention : SlimModelConvention + { + /// + /// Creates a new instance of . + /// + /// Parameter object containing dependencies for this convention. + /// Parameter object containing relational dependencies for this convention. + public RelationalSlimModelConvention( + ProviderConventionSetBuilderDependencies dependencies, + RelationalConventionSetBuilderDependencies relationalDependencies) + : base(dependencies) + { + RelationalDependencies = relationalDependencies; + } + + /// + /// The service dependencies for + /// + protected virtual RelationalConventionSetBuilderDependencies RelationalDependencies { get; } + + /// + /// Updates the model annotations that will be set on the read-only object. + /// + /// The annotations to be processed. + /// The source model. + /// The target model that will contain the annotations. + /// Indicates whether the given annotations are runtime annotations. + protected override void ProcessModelAnnotations( + Dictionary annotations, IModel model, SlimModel slimModel, bool runtime) + { + base.ProcessModelAnnotations(annotations, model, slimModel, runtime); + + if (runtime) + { + annotations[RelationalAnnotationNames.RelationalModel] = + RelationalModel.Create(slimModel, RelationalDependencies.RelationalAnnotationProvider); + } + else + { + if (annotations.TryGetValue(RelationalAnnotationNames.DbFunctions, out var functions)) + { + var slimFunctions = new SortedDictionary(); + foreach (var functionPair in (SortedDictionary)functions!) + { + var slimFunction = Create(functionPair.Value, slimModel); + slimFunctions[functionPair.Key] = slimFunction; + + foreach (var parameter in functionPair.Value.Parameters) + { + var slimParameter = Create(parameter, slimFunction); + + CreateAnnotations(parameter, slimParameter, static (convention, annotations, source, target, runtime) => + convention.ProcessFunctionParameterAnnotations(annotations, source, target, runtime)); + } + + CreateAnnotations(functionPair.Value, slimFunction, static (convention, annotations, source, target, runtime) => + convention.ProcessFunctionAnnotations(annotations, source, target, runtime)); + } + + annotations[RelationalAnnotationNames.DbFunctions] = slimFunctions; + } + + if (annotations.TryGetValue(RelationalAnnotationNames.Sequences, out var sequences)) + { + var slimSequences = new SortedDictionary<(string, string?), ISequence>(); + foreach (var sequencePair in (SortedDictionary<(string, string?), ISequence>)sequences!) + { + var slimSequence = Create(sequencePair.Value, slimModel); + slimSequences[sequencePair.Key] = slimSequence; + + CreateAnnotations(sequencePair.Value, slimSequence, static (convention, annotations, source, target, runtime) => + convention.ProcessSequenceAnnotations(annotations, source, target, runtime)); + } + + annotations[RelationalAnnotationNames.Sequences] = slimSequences; + } + } + } + + /// + /// Updates the entity type annotations that will be set on the read-only object. + /// + /// The annotations to be processed. + /// The source entity type. + /// The target entity type that will contain the annotations. + /// Indicates whether the given annotations are runtime annotations. + protected override void ProcessEntityTypeAnnotations( + IDictionary annotations, IEntityType entityType, SlimEntityType slimEntityType, bool runtime) + { + base.ProcessEntityTypeAnnotations(annotations, entityType, slimEntityType, runtime); + + if (runtime) + { + annotations.Remove(RelationalAnnotationNames.TableMappings); + annotations.Remove(RelationalAnnotationNames.ViewMappings); + annotations.Remove(RelationalAnnotationNames.SqlQueryMappings); + annotations.Remove(RelationalAnnotationNames.FunctionMappings); + annotations.Remove(RelationalAnnotationNames.DefaultMappings); + } + else + { + if (annotations.TryGetValue(RelationalAnnotationNames.CheckConstraints, out var constraints)) + { + var slimCheckConstraints = new Dictionary(); + foreach (var constraintPair in (Dictionary?)constraints!) + { + var slimCheckConstraint = Create(constraintPair.Value, slimEntityType); + slimCheckConstraints[constraintPair.Key] = slimCheckConstraint; + + CreateAnnotations(constraintPair.Value, slimCheckConstraint, static (convention, annotations, source, target, runtime) => + convention.ProcessCheckConstraintAnnotations(annotations, source, target, runtime)); + } + + annotations[RelationalAnnotationNames.CheckConstraints] = slimCheckConstraints; + } + + // These need to be set explicitly to prevent default values from being generated + annotations[RelationalAnnotationNames.TableName] = entityType.GetTableName(); + annotations[RelationalAnnotationNames.Schema] = entityType.GetSchema(); + annotations[RelationalAnnotationNames.ViewName] = entityType.GetViewName(); + annotations[RelationalAnnotationNames.ViewSchema] = entityType.GetViewSchema(); + annotations[RelationalAnnotationNames.SqlQuery] = entityType.GetSqlQuery(); + annotations[RelationalAnnotationNames.FunctionName] = entityType.GetFunctionName(); + } + } + + private void CreateAnnotations( + TSource source, + TTarget target, + Action, TSource, TTarget, bool> process) + where TSource : IAnnotatable + where TTarget : AnnotatableBase + { + var annotations = source.GetAnnotations().ToDictionary(a => a.Name, a => a.Value); + process(this, annotations, source, target, false); + target.AddAnnotations(annotations); + + annotations = source.GetRuntimeAnnotations().ToDictionary(a => a.Name, a => a.Value); + process(this, annotations, source, target, true); + target.AddRuntimeAnnotations(annotations); + } + + private SlimDbFunction Create(IDbFunction function, SlimModel slimModel) + => new SlimDbFunction( + function.ModelName, + slimModel, + function.MethodInfo, + function.ReturnType, + function.IsScalar, + function.IsAggregate, + function.IsNullable, + function.IsBuiltIn, + function.Name, + function.Schema, + function.StoreType, + function.TypeMapping, + function.Translation); + + /// + /// Updates the function annotations that will be set on the read-only object. + /// + /// The annotations to be processed. + /// The source function. + /// The target function that will contain the annotations. + /// Indicates whether the given annotations are runtime annotations. + protected virtual void ProcessFunctionAnnotations( + Dictionary annotations, + IDbFunction function, + SlimDbFunction slimFunction, + bool runtime) + { + } + + private SlimDbFunctionParameter Create(IDbFunctionParameter parameter, SlimDbFunction slimFunction) + => slimFunction.AddParameter( + parameter.Name, + parameter.ClrType, + parameter.PropagatesNullability, + parameter.StoreType, + parameter.TypeMapping); + + /// + /// Updates the function parameter annotations that will be set on the read-only object. + /// + /// The annotations to be processed. + /// The source function parameter. + /// The target function parameter that will contain the annotations. + /// Indicates whether the given annotations are runtime annotations. + protected virtual void ProcessFunctionParameterAnnotations( + Dictionary annotations, + IDbFunctionParameter parameter, + SlimDbFunctionParameter slimParameter, + bool runtime) + { + } + + private SlimSequence Create(ISequence sequence, SlimModel slimModel) + => new SlimSequence( + sequence.Name, + sequence.Schema, + slimModel, + sequence.Type, + sequence.StartValue, + sequence.IncrementBy, + sequence.IsCyclic, + sequence.MinValue, + sequence.MaxValue); + + /// + /// Updates the sequence annotations that will be set on the read-only object. + /// + /// The annotations to be processed. + /// The source sequence. + /// The target sequence that will contain the annotations. + /// Indicates whether the given annotations are runtime annotations. + protected virtual void ProcessSequenceAnnotations( + Dictionary annotations, + ISequence sequence, + SlimSequence slimSequence, + bool runtime) + { + } + + private SlimCheckConstraint Create(ICheckConstraint checkConstraint, SlimEntityType slimEntityType) + => new SlimCheckConstraint( + checkConstraint.Name, + slimEntityType, + checkConstraint.Sql); + + /// + /// Updates the check constraint annotations that will be set on the read-only object. + /// + /// The annotations to be processed. + /// The source check constraint. + /// The target check constraint that will contain the annotations. + /// Indicates whether the given annotations are runtime annotations. + protected virtual void ProcessCheckConstraintAnnotations( + Dictionary annotations, + ICheckConstraint checkConstraint, + SlimCheckConstraint slimCheckConstraint, + bool runtime) + { + } + + /// + /// Updates the property annotations that will be set on the read-only object. + /// + /// The annotations to be processed. + /// The source property. + /// The target property that will contain the annotations. + /// Indicates whether the given annotations are runtime annotations. + protected override void ProcessPropertyAnnotations( + Dictionary annotations, IProperty property, SlimProperty slimProperty, bool runtime) + { + base.ProcessPropertyAnnotations(annotations, property, slimProperty, runtime); + + if (runtime) + { + annotations.Remove(RelationalAnnotationNames.TableColumnMappings); + annotations.Remove(RelationalAnnotationNames.ViewColumnMappings); + annotations.Remove(RelationalAnnotationNames.SqlQueryColumnMappings); + annotations.Remove(RelationalAnnotationNames.FunctionColumnMappings); + annotations.Remove(RelationalAnnotationNames.DefaultColumnMappings); + } + else + { + if (annotations.TryGetValue(RelationalAnnotationNames.RelationalOverrides, out var overrides)) + { + var slimPropertyOverrides = new SortedDictionary(); + foreach (var overridesPair in (SortedDictionary?)overrides!) + { + var slimOverrides = Create(overridesPair.Value, slimProperty); + slimPropertyOverrides[overridesPair.Key] = slimOverrides; + + CreateAnnotations(overridesPair.Value, slimOverrides, static (convention, annotations, source, target, runtime) => + convention.ProcessPropertyOverridesAnnotations(annotations, source, target, runtime)); + } + + annotations[RelationalAnnotationNames.RelationalOverrides] = slimPropertyOverrides; + } + } + } + + private SlimRelationalPropertyOverrides Create( + IRelationalPropertyOverrides propertyOverrides, + SlimProperty slimProperty) + => new SlimRelationalPropertyOverrides( + slimProperty, + propertyOverrides.ColumnName, + propertyOverrides.ColumnNameOverriden); + + /// + /// Updates the relational property overrides annotations that will be set on the read-only object. + /// + /// The annotations to be processed. + /// The source relational property overrides. + /// The target relational property overrides that will contain the annotations. + /// Indicates whether the given annotations are runtime annotations. + protected virtual void ProcessPropertyOverridesAnnotations( + Dictionary annotations, + IRelationalPropertyOverrides propertyOverrides, + SlimRelationalPropertyOverrides slimPropertyOverrides, + bool runtime) + { + } + + /// + /// Updates the key annotations that will be set on the read-only object. + /// + /// The annotations to be processed. + /// The source key. + /// The target key that will contain the annotations. + /// Indicates whether the given annotations are runtime annotations. + protected override void ProcessKeyAnnotations( + IDictionary annotations, + IKey key, + SlimKey slimKey, + bool runtime) + { + base.ProcessKeyAnnotations(annotations, key, slimKey, runtime); + + if (runtime) + { + annotations.Remove(RelationalAnnotationNames.UniqueConstraintMappings); + } + } + + /// + /// Updates the index annotations that will be set on the read-only object. + /// + /// The annotations to be processed. + /// The source index. + /// The target index that will contain the annotations. + /// Indicates whether the given annotations are runtime annotations. + protected override void ProcessIndexAnnotations( + Dictionary annotations, + IIndex index, + SlimIndex slimIndex, + bool runtime) + { + base.ProcessIndexAnnotations(annotations, index, slimIndex, runtime); + + if (runtime) + { + annotations.Remove(RelationalAnnotationNames.TableIndexMappings); + } + } + + /// + /// Updates the foreign key annotations that will be set on the read-only object. + /// + /// The annotations to be processed. + /// The source foreign key. + /// The target foreign key that will contain the annotations. + /// Indicates whether the given annotations are runtime annotations. + protected override void ProcessForeignKeyAnnotations( + Dictionary annotations, + IForeignKey foreignKey, + SlimForeignKey slimForeignKey, + bool runtime) + { + base.ProcessForeignKeyAnnotations(annotations, foreignKey, slimForeignKey, runtime); + + if (runtime) + { + annotations.Remove(RelationalAnnotationNames.ForeignKeyMappings); + } + } + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs index f10e78f74de..f3688fb7b9f 100644 --- a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs +++ b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs @@ -180,9 +180,9 @@ public static IRelationalModel Create( constraint.AddAnnotations(relationalAnnotationProvider.For(constraint)); } - foreach (CheckConstraint checkConstraint in ((ITable)table).CheckConstraints) + foreach (var checkConstraint in ((ITable)table).CheckConstraints) { - checkConstraint.AddAnnotations(relationalAnnotationProvider.For(checkConstraint)); + ((AnnotatableBase)checkConstraint).AddAnnotations(relationalAnnotationProvider.For(checkConstraint)); } table.AddAnnotations(relationalAnnotationProvider.For(table)); @@ -232,9 +232,9 @@ public static IRelationalModel Create( if (relationalAnnotationProvider != null) { - foreach (Sequence sequence in ((IRelationalModel)databaseModel).Sequences) + foreach (var sequence in ((IRelationalModel)databaseModel).Sequences) { - sequence.AddAnnotations(relationalAnnotationProvider.For(sequence)); + ((AnnotatableBase)sequence).AddAnnotations(relationalAnnotationProvider.For(sequence)); } databaseModel.AddAnnotations(relationalAnnotationProvider.For(databaseModel)); diff --git a/src/EFCore.Relational/Migrations/HistoryRepository.cs b/src/EFCore.Relational/Migrations/HistoryRepository.cs index d71be1fb5a6..63eb8681b95 100644 --- a/src/EFCore.Relational/Migrations/HistoryRepository.cs +++ b/src/EFCore.Relational/Migrations/HistoryRepository.cs @@ -108,7 +108,7 @@ private IModel EnsureModel() x.ToTable(TableName, TableSchema); }); - _model = Dependencies.ModelRuntimeInitializer.Initialize(modelBuilder.FinalizeModel(), validationLogger: null); + _model = Dependencies.ModelRuntimeInitializer.Initialize(modelBuilder.FinalizeModel(), designTime: true, validationLogger: null); } return _model; diff --git a/src/EFCore.Relational/Migrations/Internal/Migrator.cs b/src/EFCore.Relational/Migrations/Internal/Migrator.cs index 956e3216efe..a9328dcbb68 100644 --- a/src/EFCore.Relational/Migrations/Internal/Migrator.cs +++ b/src/EFCore.Relational/Migrations/Internal/Migrator.cs @@ -502,7 +502,7 @@ private IModel FinalizeModel(IModel model) model = mutableModel.FinalizeModel(); } - return _modelRuntimeInitializer.Initialize(model, validationLogger: null); + return _modelRuntimeInitializer.Initialize(model, designTime: true, validationLogger: null); } } } diff --git a/src/EFCore.Relational/Storage/RelationalDatabaseCreator.cs b/src/EFCore.Relational/Storage/RelationalDatabaseCreator.cs index f7e7fa0c8a7..aa1b99c7f08 100644 --- a/src/EFCore.Relational/Storage/RelationalDatabaseCreator.cs +++ b/src/EFCore.Relational/Storage/RelationalDatabaseCreator.cs @@ -144,7 +144,9 @@ public virtual Task CreateTablesAsync(CancellationToken cancellationToken = defa protected virtual IReadOnlyList GetCreateTablesCommands( MigrationsSqlGenerationOptions options = MigrationsSqlGenerationOptions.Default) => Dependencies.MigrationsSqlGenerator.Generate( - Dependencies.ModelDiffer.GetDifferences(null, Dependencies.Model.GetRelationalModel()), Dependencies.Model, options); + Dependencies.ModelDiffer.GetDifferences(null, Dependencies.CurrentContext.Context.DesignTimeModel.GetRelationalModel()), + Dependencies.CurrentContext.Context.DesignTimeModel, + options); /// /// Determines whether the database contains any tables. No attempt is made to determine if diff --git a/src/EFCore.Relational/Storage/RelationalDatabaseCreatorDependencies.cs b/src/EFCore.Relational/Storage/RelationalDatabaseCreatorDependencies.cs index 33df52954c2..c77144ac775 100644 --- a/src/EFCore.Relational/Storage/RelationalDatabaseCreatorDependencies.cs +++ b/src/EFCore.Relational/Storage/RelationalDatabaseCreatorDependencies.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; @@ -82,7 +83,9 @@ public RelationalDatabaseCreatorDependencies( Check.NotNull(currentContext, nameof(currentContext)); Check.NotNull(commandLogger, nameof(commandLogger)); +#pragma warning disable CS0618 // Type or member is obsolete Model = model; +#pragma warning restore CS0618 // Type or member is obsolete Connection = connection; ModelDiffer = modelDiffer; MigrationsSqlGenerator = migrationsSqlGenerator; @@ -106,6 +109,7 @@ public RelationalDatabaseCreatorDependencies( /// /// Gets the model for the context this creator is being used with. /// + [Obsolete("Use CurrentContext.Context.DesignTimeModel instead")] public IModel Model { get; init; } /// diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerDatabaseCreator.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerDatabaseCreator.cs index 5dc0fcff785..be5c71c4c6c 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerDatabaseCreator.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerDatabaseCreator.cs @@ -178,7 +178,7 @@ private IReadOnlyList CreateCreateOperations() { Name = builder.InitialCatalog, FileName = builder.AttachDBFilename, - Collation = Dependencies.Model.GetCollation() + Collation = Dependencies.CurrentContext.Context.DesignTimeModel.GetCollation() } }, null); diff --git a/src/EFCore/DbContext.cs b/src/EFCore/DbContext.cs index d1144ded047..acf465ba10f 100644 --- a/src/EFCore/DbContext.cs +++ b/src/EFCore/DbContext.cs @@ -138,11 +138,22 @@ public virtual ChangeTracker ChangeTracker /// /// The metadata about the shape of entities, the relationships between them, and how they map to the database. + /// May not include all the information necessary to initialize the database. /// public virtual IModel Model { [DebuggerStepThrough] - get => DbContextDependencies.Model; + get => ContextServices.Model; + } + + /// + /// The metadata about the shape of entities, the relationships between them, and how they map to the database. + /// Also includes all the information necessary to initialize the database. + /// + public virtual IModel DesignTimeModel + { + [DebuggerStepThrough] + get => ContextServices.DesignTimeModel; } /// @@ -331,7 +342,9 @@ private IEntityFinder Finder(Type type) return DbContextDependencies.EntityFinderFactory.Create(entityType); } - private IServiceProvider InternalServiceProvider + private IServiceProvider InternalServiceProvider => ContextServices.InternalServiceProvider; + + private IDbContextServices ContextServices { get { @@ -339,7 +352,7 @@ private IServiceProvider InternalServiceProvider if (_contextServices != null) { - return _contextServices.InternalServiceProvider; + return _contextServices; } if (_initializing) @@ -382,7 +395,7 @@ private IServiceProvider InternalServiceProvider _initializing = false; } - return _contextServices.InternalServiceProvider; + return _contextServices; } } diff --git a/src/EFCore/Infrastructure/IModelCacheKeyFactory.cs b/src/EFCore/Infrastructure/IModelCacheKeyFactory.cs index 924b0b8d230..0c5ad9e0a53 100644 --- a/src/EFCore/Infrastructure/IModelCacheKeyFactory.cs +++ b/src/EFCore/Infrastructure/IModelCacheKeyFactory.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.EntityFrameworkCore.Infrastructure @@ -29,6 +30,20 @@ public interface IModelCacheKeyFactory /// The context to get the model cache key for. /// /// The created key. + [Obsolete("Use the overload with most parameters")] object Create(DbContext context); + + /// + /// Gets the model cache key for a given context. + /// + /// + /// The context to get the model cache key for. + /// + /// Whether the model should contain design-time configuration. + /// The created key. + object Create(DbContext context, bool designTime) +#pragma warning disable CS0618 // Type or member is obsolete + => Create(context); +#pragma warning restore CS0618 // Type or member is obsolete } } diff --git a/src/EFCore/Infrastructure/IModelRuntimeInitializer.cs b/src/EFCore/Infrastructure/IModelRuntimeInitializer.cs index b50f9ccacc0..27c5c39bd46 100644 --- a/src/EFCore/Infrastructure/IModelRuntimeInitializer.cs +++ b/src/EFCore/Infrastructure/IModelRuntimeInitializer.cs @@ -29,10 +29,14 @@ public interface IModelRuntimeInitializer /// Validates and initializes the given model with runtime dependencies. /// /// The model to initialize. - /// The validation logger. + /// Whether the model should contain design-time configuration. + /// + /// The validation logger. If is provided validation will not be performed. + /// /// The initialized model. IModel Initialize( IModel model, - IDiagnosticsLogger? validationLogger); + bool designTime = true, + IDiagnosticsLogger? validationLogger = null); } } diff --git a/src/EFCore/Infrastructure/IModelSource.cs b/src/EFCore/Infrastructure/IModelSource.cs index d66ba58157b..6033f027b1b 100644 --- a/src/EFCore/Infrastructure/IModelSource.cs +++ b/src/EFCore/Infrastructure/IModelSource.cs @@ -54,9 +54,11 @@ IModel GetModel( /// /// The context the model is being produced for. /// The dependencies object used during the creation of the model. + /// Whether the model should contain design-time configuration. /// The model to be used. IModel GetModel( DbContext context, - ModelCreationDependencies modelCreationDependencies); + ModelCreationDependencies modelCreationDependencies, + bool designTime); } } diff --git a/src/EFCore/Infrastructure/ModelCacheKey.cs b/src/EFCore/Infrastructure/ModelCacheKey.cs index 090e169ed13..ed080eb6d7f 100644 --- a/src/EFCore/Infrastructure/ModelCacheKey.cs +++ b/src/EFCore/Infrastructure/ModelCacheKey.cs @@ -18,6 +18,9 @@ namespace Microsoft.EntityFrameworkCore.Infrastructure /// public class ModelCacheKey { + private readonly Type _dbContextType; + private readonly bool _designTime; + /// /// Initializes a new instance of the class. /// @@ -29,7 +32,18 @@ public ModelCacheKey(DbContext context) _dbContextType = context.GetType(); } - private readonly Type _dbContextType; + /// + /// Initializes a new instance of the class. + /// + /// + /// The context instance that this key is for. + /// + /// Whether the model should contain design-time configuration. + public ModelCacheKey(DbContext context, bool designTime) + { + _dbContextType = context.GetType(); + _designTime = designTime; + } /// /// Determines if this key is equivalent to a given key (i.e. if they are for the same context type). @@ -41,7 +55,8 @@ public ModelCacheKey(DbContext context) /// if the key is for the same context type, otherwise . /// protected virtual bool Equals(ModelCacheKey other) - => _dbContextType == other._dbContextType; + => _dbContextType == other._dbContextType + && _designTime == other._designTime; /// /// Determines if this key is equivalent to a given object (i.e. if they are keys for the same context type). @@ -63,6 +78,11 @@ public override bool Equals(object? obj) /// The hash code for the key. /// public override int GetHashCode() - => _dbContextType.GetHashCode(); + { + var hash = new HashCode(); + hash.Add(_dbContextType); + hash.Add(_designTime); + return hash.ToHashCode(); + } } } diff --git a/src/EFCore/Infrastructure/ModelCacheKeyFactory.cs b/src/EFCore/Infrastructure/ModelCacheKeyFactory.cs index 86dad05754d..ac54a4931b5 100644 --- a/src/EFCore/Infrastructure/ModelCacheKeyFactory.cs +++ b/src/EFCore/Infrastructure/ModelCacheKeyFactory.cs @@ -42,5 +42,16 @@ public ModelCacheKeyFactory(ModelCacheKeyFactoryDependencies dependencies) /// The created key. public virtual object Create(DbContext context) => new ModelCacheKey(context); + + /// + /// Gets the model cache key for a given context. + /// + /// + /// The context to get the model cache key for. + /// + /// Whether the model should contain design-time configuration. + /// The created key. + public virtual object Create(DbContext context, bool designTime) + => new ModelCacheKey(context, designTime); } } diff --git a/src/EFCore/Infrastructure/ModelRuntimeInitializer.cs b/src/EFCore/Infrastructure/ModelRuntimeInitializer.cs index 4822f461c10..9dd5ab8a30f 100644 --- a/src/EFCore/Infrastructure/ModelRuntimeInitializer.cs +++ b/src/EFCore/Infrastructure/ModelRuntimeInitializer.cs @@ -45,11 +45,13 @@ public ModelRuntimeInitializer(ModelRuntimeInitializerDependencies dependencies) /// Validates and initializes the given model with runtime dependencies. /// /// The model to initialize. + /// Whether the model should contain design-time configuration. /// The validation logger. /// The initialized model. public virtual IModel Initialize( IModel model, - IDiagnosticsLogger? validationLogger) + bool designTime = true, + IDiagnosticsLogger? validationLogger = null) { if (model.ModelDependencies == null) { @@ -57,7 +59,8 @@ public virtual IModel Initialize( CoreAnnotationNames.ReadOnlyModel, static args => { - var (initializer, model, validationLogger) = args; + var (initializer, model, designTime, validationLogger) = args; + model.ModelDependencies = initializer.Dependencies.ModelDependencies; initializer.InitializeModel(model, preValidation: true); @@ -70,18 +73,35 @@ public virtual IModel Initialize( initializer.InitializeModel(model, preValidation: false); - if (model is Model mutableModel) + if (!designTime + && model is Model mutableModel) { model = mutableModel.OnModelFinalized(); } return model; }, - (this, model, validationLogger)); + (this, model, designTime, validationLogger)); + + if (designTime) + { + model.RemoveRuntimeAnnotation(CoreAnnotationNames.ReadOnlyModel); + } } - else + else if (!designTime) { - model = (IModel)model.FindRuntimeAnnotationValue(CoreAnnotationNames.ReadOnlyModel)!; + model = model.GetOrAddRuntimeAnnotationValue( + CoreAnnotationNames.ReadOnlyModel, + static model => + { + if (model is Model mutableModel) + { + model = mutableModel.OnModelFinalized(); + } + + return model!; + }, + model); } return model; diff --git a/src/EFCore/Infrastructure/ModelSource.cs b/src/EFCore/Infrastructure/ModelSource.cs index 60db833973b..503fef8d6ad 100644 --- a/src/EFCore/Infrastructure/ModelSource.cs +++ b/src/EFCore/Infrastructure/ModelSource.cs @@ -111,13 +111,15 @@ public virtual IModel GetModel( /// /// The context the model is being produced for. /// The dependencies object used during the creation of the model. + /// Whether the model should contain design-time configuration. /// The model to be used. public virtual IModel GetModel( DbContext context, - ModelCreationDependencies modelCreationDependencies) + ModelCreationDependencies modelCreationDependencies, + bool designTime) { var cache = Dependencies.MemoryCache; - var cacheKey = Dependencies.ModelCacheKeyFactory.Create(context); + var cacheKey = Dependencies.ModelCacheKeyFactory.Create(context, designTime); if (!cache.TryGetValue(cacheKey, out IModel model)) { // Make sure OnModelCreating really only gets called once, since it may not be thread safe. @@ -127,7 +129,8 @@ public virtual IModel GetModel( { model = CreateModel(context, modelCreationDependencies.ConventionSetBuilder, modelCreationDependencies.ModelDependencies); - modelCreationDependencies.ModelRuntimeInitializer.Initialize(model, modelCreationDependencies.ValidationLogger); + model = modelCreationDependencies.ModelRuntimeInitializer.Initialize( + model, designTime, modelCreationDependencies.ValidationLogger); model = cache.Set(cacheKey, model, new MemoryCacheEntryOptions { Size = 100, Priority = CacheItemPriority.High }); } @@ -169,7 +172,7 @@ protected virtual IModel CreateModel( IConventionSetBuilder conventionSetBuilder, ModelDependencies modelDependencies) { - Check.NotNull(context, nameof(context)); + Check.DebugAssert(context != null, "context == null"); var modelBuilder = new ModelBuilder(conventionSetBuilder.CreateConventionSet(), modelDependencies); diff --git a/src/EFCore/Internal/DbContextDependencies.cs b/src/EFCore/Internal/DbContextDependencies.cs index c0c9d9d4c37..c6f559feacf 100644 --- a/src/EFCore/Internal/DbContextDependencies.cs +++ b/src/EFCore/Internal/DbContextDependencies.cs @@ -4,7 +4,6 @@ using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Query; using Microsoft.Extensions.DependencyInjection; @@ -41,7 +40,6 @@ public DbContextDependencies( IDbSetSource setSource, IEntityFinderSource entityFinderSource, IEntityGraphAttacher entityGraphAttacher, - IModel model, IAsyncQueryProvider queryProvider, IStateManager stateManager, IDiagnosticsLogger updateLogger, @@ -50,7 +48,6 @@ public DbContextDependencies( ChangeDetector = changeDetector; SetSource = setSource; EntityGraphAttacher = entityGraphAttacher; - Model = model; QueryProvider = queryProvider; StateManager = stateManager; UpdateLogger = updateLogger; @@ -58,14 +55,6 @@ public DbContextDependencies( EntityFinderFactory = new EntityFinderFactory(entityFinderSource, stateManager, setSource, currentContext.Context); } - /// - /// 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. - /// - public IModel Model { get; init; } - /// /// 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 diff --git a/src/EFCore/Internal/DbContextServices.cs b/src/EFCore/Internal/DbContextServices.cs index b2e70640ccf..b977ff7400b 100644 --- a/src/EFCore/Internal/DbContextServices.cs +++ b/src/EFCore/Internal/DbContextServices.cs @@ -33,6 +33,7 @@ public class DbContextServices : IDbContextServices private IDbContextOptions? _contextOptions; private ICurrentDbContext? _currentContext; private IModel? _model; + private IModel? _designTimeModel; private bool _inOnModelCreating; /// @@ -70,7 +71,7 @@ public virtual IDbContextServices Initialize( private static string BuildDatabaseNamesString(IEnumerable available) => string.Join(", ", available.Select(e => "'" + e.Name + "'")); - private IModel CreateModel(IModel? modelFromOptions) + private IModel CreateModel(bool designTime) { if (_inOnModelCreating) { @@ -82,9 +83,11 @@ private IModel CreateModel(IModel? modelFromOptions) _inOnModelCreating = true; var dependencies = _scopedProvider!.GetRequiredService(); + var modelFromOptions = CoreOptions?.Model; return modelFromOptions == null - ? dependencies.ModelSource.GetModel(_currentContext!.Context, dependencies) - : dependencies.ModelRuntimeInitializer.Initialize(modelFromOptions, dependencies.ValidationLogger); + || (designTime && modelFromOptions is not Metadata.Internal.Model) + ? dependencies.ModelSource.GetModel(_currentContext!.Context, dependencies, designTime) + : dependencies.ModelRuntimeInitializer.Initialize(modelFromOptions, designTime, dependencies.ValidationLogger); } finally { @@ -108,7 +111,16 @@ public virtual ICurrentDbContext CurrentContext /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual IModel Model - => _model ??= CreateModel(CoreOptions?.Model); + => _model ??= CreateModel(designTime: false); + + /// + /// 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. + /// + public virtual IModel DesignTimeModel + => _designTimeModel ??= CreateModel(designTime: true); private CoreOptionsExtension? CoreOptions => _contextOptions?.FindExtension(); diff --git a/src/EFCore/Internal/IDbContextDependencies.cs b/src/EFCore/Internal/IDbContextDependencies.cs index aea5f552706..ac8ebc9c88f 100644 --- a/src/EFCore/Internal/IDbContextDependencies.cs +++ b/src/EFCore/Internal/IDbContextDependencies.cs @@ -25,14 +25,6 @@ namespace Microsoft.EntityFrameworkCore.Internal /// public interface IDbContextDependencies { - /// - /// 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. - /// - IModel Model { get; } - /// /// 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 diff --git a/src/EFCore/Internal/IDbContextServices.cs b/src/EFCore/Internal/IDbContextServices.cs index d5503072efd..83c2289624c 100644 --- a/src/EFCore/Internal/IDbContextServices.cs +++ b/src/EFCore/Internal/IDbContextServices.cs @@ -51,6 +51,14 @@ IDbContextServices Initialize( /// IModel Model { get; } + /// + /// 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. + /// + IModel DesignTimeModel { get; } + /// /// 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 diff --git a/src/EFCore/Metadata/ConstructorBinding.cs b/src/EFCore/Metadata/ConstructorBinding.cs index 99c796cf483..d58384ab93c 100644 --- a/src/EFCore/Metadata/ConstructorBinding.cs +++ b/src/EFCore/Metadata/ConstructorBinding.cs @@ -51,5 +51,13 @@ public override Expression CreateConstructorExpression(ParameterBindingInfo bind /// public override Type RuntimeType => Constructor.DeclaringType!; + + /// + /// Creates a copy that contains the given parameter bindings. + /// + /// The new parameter bindings. + /// A copy with replaced parameter bindings. + public override InstantiationBinding With(IReadOnlyList parameterBindings) + => new ConstructorBinding(Constructor, parameterBindings); } } diff --git a/src/EFCore/Metadata/ContextParameterBinding.cs b/src/EFCore/Metadata/ContextParameterBinding.cs index 592879a9987..0de60443398 100644 --- a/src/EFCore/Metadata/ContextParameterBinding.cs +++ b/src/EFCore/Metadata/ContextParameterBinding.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; +using System.Linq; using System.Linq.Expressions; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Utilities; @@ -49,5 +51,13 @@ var propertyExpression ? (Expression)Expression.TypeAs(propertyExpression, ServiceType) : propertyExpression; } + + /// + /// Creates a copy that contains the given consumed properties. + /// + /// The new consumed properties. + /// A copy with replaced consumed properties. + public override ParameterBinding With(IReadOnlyList consumedProperties) + => new ContextParameterBinding(ParameterType, consumedProperties.SingleOrDefault()); } } diff --git a/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs b/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs index 502498f009d..dbfb7166b17 100644 --- a/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs +++ b/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs @@ -238,6 +238,8 @@ public virtual ConventionSet CreateConventionSet() conventionSet.ModelFinalizingConventions.Add(inversePropertyAttributeConvention); conventionSet.ModelFinalizingConventions.Add(backingFieldConvention); + conventionSet.ModelFinalizedConventions.Add(new SlimModelConvention(Dependencies)); + return conventionSet; } diff --git a/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilderDependencies.cs b/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilderDependencies.cs index 44c54f2ed69..171c5eac187 100644 --- a/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilderDependencies.cs +++ b/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilderDependencies.cs @@ -159,8 +159,15 @@ public Type ContextType public ProviderConventionSetBuilderDependencies With(ICurrentDbContext currentContext) #pragma warning disable CS0618 // Type or member is obsolete => new( - TypeMappingSource, ConstructorBindingFactory, ParameterBindingFactories, MemberClassifier, Logger, ValidationLogger, - SetFinder, currentContext, ModelValidator); + TypeMappingSource, + ConstructorBindingFactory, + ParameterBindingFactories, + MemberClassifier, + Logger, + ValidationLogger, + SetFinder, + currentContext, + ModelValidator); #pragma warning restore CS0618 // Type or member is obsolete } } diff --git a/src/EFCore/Metadata/Conventions/SlimModelConvention.cs b/src/EFCore/Metadata/Conventions/SlimModelConvention.cs new file mode 100644 index 00000000000..19535629e47 --- /dev/null +++ b/src/EFCore/Metadata/Conventions/SlimModelConvention.cs @@ -0,0 +1,538 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Utilities; + +#nullable enable + +namespace Microsoft.EntityFrameworkCore.Metadata.Conventions +{ + /// + /// A convention that creates an optimized copy of the mutable model. This convention is typically + /// implemented by database providers to update provider annotations when creating a read-only model. + /// + public class SlimModelConvention : IModelFinalizedConvention + { + /// + /// Creates a new instance of . + /// + /// Parameter object containing dependencies for this convention. + public SlimModelConvention( + ProviderConventionSetBuilderDependencies dependencies) + { + Dependencies = dependencies; + } + + /// + /// Parameter object containing service dependencies. + /// + protected virtual ProviderConventionSetBuilderDependencies Dependencies { get; } + + /// + /// Called after a model is finalized and can no longer be mutated. + /// + /// The model. + public virtual IModel ProcessModelFinalized(IModel model) + => Create(model); + + /// + /// Creates an optimized model base on the supplied one. + /// + /// The source model. + /// An optimized model. + protected virtual SlimModel Create(IModel model) + { + var slimModel = new SlimModel(model.ModelDependencies!, ((IRuntimeModel)model).SkipDetectChanges); + + var entityTypes = Sort(model.GetEntityTypes()); + var entityTypePairs = new List<(IEntityType Source, SlimEntityType Target)>(entityTypes.Count); + + foreach (var entityType in entityTypes) + { + var slimEntityType = Create(entityType, slimModel); + entityTypePairs.Add((entityType, slimEntityType)); + + foreach (var property in entityType.GetDeclaredProperties()) + { + var slimProperty = Create(property, slimEntityType); + CreateAnnotations(property, slimProperty, static (convention, annotations, source, target, runtime) => + convention.ProcessPropertyAnnotations(annotations, source, target, runtime)); + } + + foreach (var serviceProperty in entityType.GetDeclaredServiceProperties()) + { + var slimServiceProperty = Create(serviceProperty, slimEntityType); + CreateAnnotations(serviceProperty, slimServiceProperty, static (convention, annotations, source, target, runtime) => + convention.ProcessServicePropertyAnnotations(annotations, source, target, runtime)); + slimServiceProperty.ParameterBinding = + (ServiceParameterBinding)Create(serviceProperty.ParameterBinding, slimEntityType); + } + + foreach (var key in entityType.GetDeclaredKeys()) + { + var slimKey = Create(key, slimEntityType); + if (key.IsPrimaryKey()) + { + slimEntityType.SetPrimaryKey(slimKey); + } + + CreateAnnotations(key, slimKey, static (convention, annotations, source, target, runtime) => + convention.ProcessKeyAnnotations(annotations, source, target, runtime)); + } + + foreach (var index in entityType.GetDeclaredIndexes()) + { + var slimIndex = Create(index, slimEntityType); + CreateAnnotations(index, slimIndex, static (convention, annotations, source, target, runtime) => + convention.ProcessIndexAnnotations(annotations, source, target, runtime)); + } + + slimEntityType.ConstructorBinding = Create(entityType.ConstructorBinding, slimEntityType); + slimEntityType.ServiceOnlyConstructorBinding = + Create(((IRuntimeEntityType)entityType).ServiceOnlyConstructorBinding, slimEntityType); + } + + foreach (var (entityType, slimEntityType) in entityTypePairs) + { + foreach (var foreignKey in entityType.GetDeclaredForeignKeys()) + { + var slimForeignKey = Create(foreignKey, slimEntityType); + + var navigation = foreignKey.DependentToPrincipal; + if (navigation != null) + { + var slimNavigation = Create(navigation, slimForeignKey); + CreateAnnotations(navigation, slimNavigation, static (convention, annotations, source, target, runtime) => + convention.ProcessNavigationAnnotations(annotations, source, target, runtime)); + } + + navigation = foreignKey.PrincipalToDependent; + if (navigation != null) + { + var slimNavigation = Create(navigation, slimForeignKey); + CreateAnnotations(navigation, slimNavigation, static (convention, annotations, source, target, runtime) => + convention.ProcessNavigationAnnotations(annotations, source, target, runtime)); + } + + CreateAnnotations(foreignKey, slimForeignKey, static (convention, annotations, source, target, runtime) => + convention.ProcessForeignKeyAnnotations(annotations, source, target, runtime)); + } + } + + foreach (var (entityType, slimEntityType) in entityTypePairs) + { + foreach (var navigation in entityType.GetDeclaredSkipNavigations()) + { + var slimNavigation = Create(navigation, slimEntityType); + + var inverse = slimNavigation.TargetEntityType.FindSkipNavigation(navigation.Inverse.Name); + if (inverse != null) + { + slimNavigation.Inverse = inverse; + inverse.Inverse = slimNavigation; + } + + CreateAnnotations(navigation, slimNavigation, static (convention, annotations, source, target, runtime) => + convention.ProcessSkipNavigationAnnotations(annotations, source, target, runtime)); + } + + CreateAnnotations(entityType, slimEntityType, static (convention, annotations, source, target, runtime) => + convention.ProcessEntityTypeAnnotations(annotations, source, target, runtime)); + } + + CreateAnnotations(model, slimModel, static (convention, annotations, source, target, runtime) => + convention.ProcessModelAnnotations(annotations, source, target, runtime)); + + return slimModel; + } + + private void CreateAnnotations( + TSource source, + TTarget target, + Action, TSource, TTarget, bool> process) + where TSource : IAnnotatable + where TTarget : AnnotatableBase + { + var annotations = source.GetAnnotations().ToDictionary(a => a.Name, a => a.Value); + process(this, annotations, source, target, false); + target.AddAnnotations(annotations); + + annotations = source.GetRuntimeAnnotations().ToDictionary(a => a.Name, a => a.Value); + process(this, annotations, source, target, true); + target.AddRuntimeAnnotations(annotations); + } + + /// + /// Updates the model annotations that will be set on the read-only object. + /// + /// The annotations to be processed. + /// The source model. + /// The target model that will contain the annotations. + /// Indicates whether the given annotations are runtime annotations. + protected virtual void ProcessModelAnnotations( + Dictionary annotations, + IModel model, + SlimModel slimModel, + bool runtime) + { + if (runtime) + { + annotations.Remove(CoreAnnotationNames.ModelDependencies); + annotations[CoreAnnotationNames.ReadOnlyModel] = slimModel; + } + else + { + annotations.Remove(CoreAnnotationNames.OwnedTypes); + annotations.Remove(CoreAnnotationNames.PropertyAccessMode); + } + } + + private static IReadOnlyList Sort(IEnumerable entityTypes) + { + var entityTypeGraph = new Multigraph(); + entityTypeGraph.AddVertices(entityTypes); + foreach (var entityType in entityTypes.Where(et => et.BaseType != null)) + { + entityTypeGraph.AddEdge(entityType.BaseType!, entityType, 0); + } + + return entityTypeGraph.TopologicalSort(); + } + + private SlimEntityType Create(IEntityType entityType, SlimModel model) + => model.AddEntityType(entityType.Name, + entityType.ClrType, + entityType.HasSharedClrType, + entityType.BaseType == null ? null : model.FindEntityType(entityType.BaseType.Name)!, + entityType.GetDiscriminatorPropertyName(), + entityType.GetChangeTrackingStrategy(), + entityType.FindIndexerPropertyInfo(), + entityType.IsPropertyBag); + + private ParameterBinding Create(ParameterBinding parameterBinding, SlimEntityType entityType) + => parameterBinding.With(parameterBinding.ConsumedProperties.Select(property => + (entityType.FindProperty(property.Name) + ?? entityType.FindServiceProperty(property.Name) + ?? entityType.FindNavigation(property.Name) + ?? (IPropertyBase?)entityType.FindSkipNavigation(property.Name))!).ToList()); + + private InstantiationBinding? Create(InstantiationBinding? instantiationBinding, SlimEntityType entityType) + => instantiationBinding?.With(instantiationBinding.ParameterBindings.Select(binding => Create(binding, entityType)).ToList()); + + /// + /// Updates the entity type annotations that will be set on the read-only object. + /// + /// The annotations to be processed. + /// The source entity type. + /// The target entity type that will contain the annotations. + /// Indicates whether the given annotations are runtime annotations. + protected virtual void ProcessEntityTypeAnnotations( + IDictionary annotations, + IEntityType entityType, + SlimEntityType slimEntityType, + bool runtime) + { + if (!runtime) + { + annotations.Remove(CoreAnnotationNames.PropertyAccessMode); + annotations.Remove(CoreAnnotationNames.NavigationAccessMode); + annotations.Remove(CoreAnnotationNames.DiscriminatorProperty); + + if (annotations.TryGetValue(CoreAnnotationNames.QueryFilter, out var queryFilter)) + { + annotations[CoreAnnotationNames.QueryFilter] = + new QueryRootRewritingExpressionVisitor(slimEntityType.Model).Rewrite((Expression)queryFilter!); + } + +#pragma warning disable CS0612 // Type or member is obsolete + if (annotations.TryGetValue(CoreAnnotationNames.DefiningQuery, out var definingQuery)) + { + annotations[CoreAnnotationNames.DefiningQuery] = + new QueryRootRewritingExpressionVisitor(slimEntityType.Model).Rewrite((Expression)definingQuery!); + } +#pragma warning restore CS0612 // Type or member is obsolete + } + } + + private SlimProperty Create(IProperty property, SlimEntityType slimEntityType) + => slimEntityType.AddProperty( + property.Name, + property.ClrType, + property.PropertyInfo, + property.FieldInfo, + property.GetPropertyAccessMode(), + property.IsNullable, + property.IsConcurrencyToken, + property.ValueGenerated, + property.GetBeforeSaveBehavior(), + property.GetAfterSaveBehavior(), + property.GetMaxLength(), + property.IsUnicode(), + property.GetPrecision(), + property.GetScale(), + property.GetProviderClrType(), + property.GetValueGeneratorFactory(), + property.GetValueConverter(), + property.GetValueComparer(), + property.GetKeyValueComparer(), + property.GetTypeMapping()); + + /// + /// Updates the property annotations that will be set on the read-only object. + /// + /// The annotations to be processed. + /// The source property. + /// The target property that will contain the annotations. + /// Indicates whether the given annotations are runtime annotations. + protected virtual void ProcessPropertyAnnotations( + Dictionary annotations, + IProperty property, + SlimProperty slimProperty, + bool runtime) + { + if (!runtime) + { + annotations.Remove(CoreAnnotationNames.PropertyAccessMode); + annotations.Remove(CoreAnnotationNames.BeforeSaveBehavior); + annotations.Remove(CoreAnnotationNames.AfterSaveBehavior); + annotations.Remove(CoreAnnotationNames.MaxLength); + annotations.Remove(CoreAnnotationNames.Unicode); + annotations.Remove(CoreAnnotationNames.Precision); + annotations.Remove(CoreAnnotationNames.Scale); + annotations.Remove(CoreAnnotationNames.ProviderClrType); + annotations.Remove(CoreAnnotationNames.ValueConverter); + annotations.Remove(CoreAnnotationNames.ValueComparer); + } + } + + private SlimServiceProperty Create(IServiceProperty property, SlimEntityType slimEntityType) + => slimEntityType.AddServiceProperty( + property.Name, + property.PropertyInfo, + property.FieldInfo, + property.GetPropertyAccessMode()); + + /// + /// Updates the service property annotations that will be set on the read-only object. + /// + /// The annotations to be processed. + /// The source service property. + /// The target service property that will contain the annotations. + /// Indicates whether the given annotations are runtime annotations. + protected virtual void ProcessServicePropertyAnnotations( + Dictionary annotations, + IServiceProperty property, + SlimServiceProperty slimProperty, + bool runtime) + { + if (!runtime) + { + annotations.Remove(CoreAnnotationNames.PropertyAccessMode); + } + } + + private SlimKey Create(IKey key, SlimEntityType slimEntityType) + => slimEntityType.AddKey(slimEntityType.FindProperties(key.Properties.Select(p => p.Name))!); + + /// + /// Updates the key annotations that will be set on the read-only object. + /// + /// The annotations to be processed. + /// The source key. + /// The target key that will contain the annotations. + /// Indicates whether the given annotations are runtime annotations. + protected virtual void ProcessKeyAnnotations( + IDictionary annotations, + IKey key, + SlimKey slimKey, + bool runtime) + { + } + + private SlimIndex Create(IIndex index, SlimEntityType slimEntityType) + => slimEntityType.AddIndex( + slimEntityType.FindProperties(index.Properties.Select(p => p.Name))!, + index.Name, + index.IsUnique); + + /// + /// Updates the index annotations that will be set on the read-only object. + /// + /// The annotations to be processed. + /// The source index. + /// The target index that will contain the annotations. + /// Indicates whether the given annotations are runtime annotations. + protected virtual void ProcessIndexAnnotations( + Dictionary annotations, + IIndex index, + SlimIndex slimIndex, + bool runtime) + { + } + + private SlimForeignKey Create(IForeignKey foreignKey, SlimEntityType slimEntityType) + { + var principalEntityType = slimEntityType.Model.FindEntityType(foreignKey.PrincipalEntityType.Name)!; + return slimEntityType.AddForeignKey( + slimEntityType.FindProperties(foreignKey.Properties.Select(p => p.Name))!, + GetKey(foreignKey.PrincipalKey, principalEntityType), + principalEntityType, + foreignKey.DeleteBehavior, + foreignKey.IsUnique, + foreignKey.IsRequired, + foreignKey.IsRequiredDependent, + foreignKey.IsOwnership); + } + + /// + /// Updates the foreign key annotations that will be set on the read-only object. + /// + /// The annotations to be processed. + /// The source foreign key. + /// The target foreign key that will contain the annotations. + /// Indicates whether the given annotations are runtime annotations. + protected virtual void ProcessForeignKeyAnnotations( + Dictionary annotations, + IForeignKey foreignKey, + SlimForeignKey slimForeignKey, + bool runtime) + { + } + + private SlimNavigation Create(INavigation navigation, SlimForeignKey slimForeigKey) + => (navigation.IsOnDependent ? slimForeigKey.DeclaringEntityType : slimForeigKey.PrincipalEntityType) + .AddNavigation( + navigation.Name, + navigation.ClrType, + navigation.PropertyInfo, + navigation.FieldInfo, + slimForeigKey, + navigation.IsOnDependent, + navigation.GetPropertyAccessMode(), + navigation.IsEagerLoaded); + + /// + /// Updates the navigation annotations that will be set on the read-only object. + /// + /// The annotations to be processed. + /// The source navigation. + /// The target navigation that will contain the annotations. + /// Indicates whether the given annotations are runtime annotations. + protected virtual void ProcessNavigationAnnotations( + Dictionary annotations, + INavigation navigation, + SlimNavigation slimNavigation, + bool runtime) + { + if (!runtime) + { + annotations.Remove(CoreAnnotationNames.PropertyAccessMode); + annotations.Remove(CoreAnnotationNames.EagerLoaded); + } + } + + private SlimSkipNavigation Create(ISkipNavigation navigation, SlimEntityType slimEntityType) + => slimEntityType.AddSkipNavigation( + navigation.Name, + navigation.ClrType, + navigation.PropertyInfo, + navigation.FieldInfo, + slimEntityType.Model.FindEntityType(navigation.TargetEntityType.Name)!, + GetForeignKey(navigation.ForeignKey, slimEntityType.Model.FindEntityType(navigation.ForeignKey.DeclaringEntityType.Name)!), + navigation.IsCollection, + navigation.IsOnDependent, + navigation.GetPropertyAccessMode(), + navigation.IsEagerLoaded); + + /// + /// Gets the corresponding foreign key in the read-optimized model. + /// + /// The original foreign key. + /// The declaring entity type. + /// The corresponding read-optimized foreign key. + protected virtual SlimForeignKey GetForeignKey(IForeignKey foreignKey, SlimEntityType entityType) + => entityType.FindDeclaredForeignKeys( + entityType.FindProperties(foreignKey.Properties.Select(p => p.Name))!) + .Single(fk => fk.PrincipalEntityType.Name == foreignKey.PrincipalEntityType.Name + && fk.PrincipalKey.Properties.Select(p => p.Name).SequenceEqual( + foreignKey.PrincipalKey.Properties.Select(p => p.Name))); + + /// + /// Gets the corresponding key in the read-optimized model. + /// + /// The original key. + /// The declaring entity type. + /// The corresponding read-optimized key. + protected virtual SlimKey GetKey(IKey key, SlimEntityType entityType) + => entityType.FindKey(entityType.FindProperties(key.Properties.Select(p => p.Name))!)!; + + /// + /// Gets the corresponding index in the read-optimized model. + /// + /// The original index. + /// The declaring entity type. + /// The corresponding read-optimized index. + protected virtual SlimIndex GetIndex(IIndex index, SlimEntityType entityType) + => index.Name == null + ? entityType.FindIndex(entityType.FindProperties(index.Properties.Select(p => p.Name))!)! + : entityType.FindIndex(index.Name)!; + + /// + /// Updates the skip navigation annotations that will be set on the read-only object. + /// + /// The annotations to be processed. + /// The source skip navigation. + /// The target skip navigation that will contain the annotations. + /// Indicates whether the given annotations are runtime annotations. + protected virtual void ProcessSkipNavigationAnnotations( + Dictionary annotations, + ISkipNavigation skipNavigation, + SlimSkipNavigation slimSkipNavigation, + bool runtime) + { + if (!runtime) + { + annotations.Remove(CoreAnnotationNames.PropertyAccessMode); + annotations.Remove(CoreAnnotationNames.EagerLoaded); + } + } + + /// + /// A visitor that rewrites encountered in an expression to use a different entity type. + /// + protected class QueryRootRewritingExpressionVisitor : ExpressionVisitor + { + private readonly IModel _model; + + /// + /// Creates a new instance of . + /// + /// The model to look for entity types. + public QueryRootRewritingExpressionVisitor(IModel model) + { + _model = model; + } + + /// + /// Rewrites encountered in an expression to use a different entity type. + /// + /// The query expression to rewrite. + public Expression Rewrite(Expression expression) + => Visit(expression); + + /// + protected override Expression VisitExtension(Expression extensionExpression) + => extensionExpression is QueryRootExpression queryRootExpression + ? new QueryRootExpression(_model.FindEntityType(queryRootExpression.EntityType.Name)!) + : base.VisitExtension(extensionExpression); + } + } +} diff --git a/src/EFCore/Metadata/DependencyInjectionMethodParameterBinding.cs b/src/EFCore/Metadata/DependencyInjectionMethodParameterBinding.cs index 226325b9285..f1a542d1c90 100644 --- a/src/EFCore/Metadata/DependencyInjectionMethodParameterBinding.cs +++ b/src/EFCore/Metadata/DependencyInjectionMethodParameterBinding.cs @@ -83,5 +83,13 @@ public override Expression BindToParameter( delegateVariable }); } + + /// + /// Creates a copy that contains the given consumed properties. + /// + /// The new consumed properties. + /// A copy with replaced consumed properties. + public override ParameterBinding With(IReadOnlyList consumedProperties) + => new DependencyInjectionMethodParameterBinding(ParameterType, ServiceType, Method, consumedProperties.SingleOrDefault()); } } diff --git a/src/EFCore/Metadata/DependencyInjectionParameterBinding.cs b/src/EFCore/Metadata/DependencyInjectionParameterBinding.cs index d9d9ced94d0..d02c0177618 100644 --- a/src/EFCore/Metadata/DependencyInjectionParameterBinding.cs +++ b/src/EFCore/Metadata/DependencyInjectionParameterBinding.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; +using System.Linq; using System.Linq.Expressions; using System.Reflection; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -57,5 +59,13 @@ public override Expression BindToParameter( MaterializationContext.ContextProperty), typeof(IInfrastructure))); } + + /// + /// Creates a copy that contains the given consumed properties. + /// + /// The new consumed properties. + /// A copy with replaced consumed properties. + public override ParameterBinding With(IReadOnlyList consumedProperties) + => new DependencyInjectionParameterBinding(ParameterType, ServiceType, consumedProperties.SingleOrDefault()); } } diff --git a/src/EFCore/Metadata/EntityTypeParameterBinding.cs b/src/EFCore/Metadata/EntityTypeParameterBinding.cs index fe4b97183a0..f20386cdac1 100644 --- a/src/EFCore/Metadata/EntityTypeParameterBinding.cs +++ b/src/EFCore/Metadata/EntityTypeParameterBinding.cs @@ -1,6 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Collections.Generic; +using System.Linq; using System.Linq.Expressions; using Microsoft.EntityFrameworkCore.Utilities; @@ -32,5 +34,13 @@ public override Expression BindToParameter( Expression materializationExpression, Expression entityTypeExpression) => Check.NotNull(entityTypeExpression, nameof(entityTypeExpression)); + + /// + /// Creates a copy that contains the given consumed properties. + /// + /// The new consumed properties. + /// A copy with replaced consumed properties. + public override ParameterBinding With(IReadOnlyList consumedProperties) + => new EntityTypeParameterBinding(consumedProperties.SingleOrDefault()); } } diff --git a/src/EFCore/Metadata/FactoryMethodBinding.cs b/src/EFCore/Metadata/FactoryMethodBinding.cs index 43dd24b8073..89ef4e439bd 100644 --- a/src/EFCore/Metadata/FactoryMethodBinding.cs +++ b/src/EFCore/Metadata/FactoryMethodBinding.cs @@ -87,5 +87,15 @@ Expression expression /// The type that will be created from the expression tree created for this binding. /// public override Type RuntimeType { get; } + + /// + /// Creates a copy that contains the given parameter bindings. + /// + /// The new parameter bindings. + /// A copy with replaced parameter bindings. + public override InstantiationBinding With(IReadOnlyList parameterBindings) + => _factoryInstance == null + ? new FactoryMethodBinding(_factoryMethod, parameterBindings, RuntimeType) + : new FactoryMethodBinding(_factoryInstance, _factoryMethod, parameterBindings, RuntimeType); } } diff --git a/src/EFCore/Metadata/InstantiationBinding.cs b/src/EFCore/Metadata/InstantiationBinding.cs index b5d6cee3357..c542cd309e2 100644 --- a/src/EFCore/Metadata/InstantiationBinding.cs +++ b/src/EFCore/Metadata/InstantiationBinding.cs @@ -44,5 +44,12 @@ protected InstantiationBinding( /// The type that will be created from the expression tree created for this binding. /// public abstract Type RuntimeType { get; } + + /// + /// Creates a copy that contains the given parameter bindings. + /// + /// The new parameter bindings. + /// A copy with replaced parameter bindings. + public abstract InstantiationBinding With(IReadOnlyList parameterBindings); } } diff --git a/src/EFCore/Metadata/ObjectArrayParameterBinding.cs b/src/EFCore/Metadata/ObjectArrayParameterBinding.cs index 99f1d759649..93a377b9eb2 100644 --- a/src/EFCore/Metadata/ObjectArrayParameterBinding.cs +++ b/src/EFCore/Metadata/ObjectArrayParameterBinding.cs @@ -51,5 +51,24 @@ public override Expression BindToParameter(ParameterBindingInfo bindingInfo) return expression; })); + + /// + /// Creates a copy that contains the given consumed properties. + /// + /// The new consumed properties. + /// A copy with replaced consumed properties. + public override ParameterBinding With(IReadOnlyList consumedProperties) + { + var newBindings = new List(_bindings.Count); + var propertyCount = 0; + foreach (var binding in _bindings) + { + var newBinding = binding.With(consumedProperties.Skip(propertyCount).Take(binding.ConsumedProperties.Count).ToList()); + newBindings.Add(newBinding); + propertyCount += binding.ConsumedProperties.Count; + } + + return new ObjectArrayParameterBinding(newBindings); + } } } diff --git a/src/EFCore/Metadata/ParameterBinding.cs b/src/EFCore/Metadata/ParameterBinding.cs index 087fb0c254a..a343798be7e 100644 --- a/src/EFCore/Metadata/ParameterBinding.cs +++ b/src/EFCore/Metadata/ParameterBinding.cs @@ -47,5 +47,12 @@ protected ParameterBinding( /// The binding information. /// The expression tree. public abstract Expression BindToParameter(ParameterBindingInfo bindingInfo); + + /// + /// Creates a copy that contains the given consumed properties. + /// + /// The new consumed properties. + /// A copy with replaced consumed properties. + public abstract ParameterBinding With(IReadOnlyList consumedProperties); } } diff --git a/src/EFCore/Metadata/PropertyParameterBinding.cs b/src/EFCore/Metadata/PropertyParameterBinding.cs index 80af751f175..9bdf9ad1abe 100644 --- a/src/EFCore/Metadata/PropertyParameterBinding.cs +++ b/src/EFCore/Metadata/PropertyParameterBinding.cs @@ -1,6 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Collections.Generic; +using System.Linq; using System.Linq.Expressions; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage; @@ -35,5 +37,13 @@ public override Expression BindToParameter(ParameterBindingInfo bindingInfo) return Expression.Call(bindingInfo.MaterializationContextExpression, MaterializationContext.GetValueBufferMethod) .CreateValueBufferReadValueExpression(property.ClrType, bindingInfo.GetValueBufferIndex(property), property); } + + /// + /// Creates a copy that contains the given consumed properties. + /// + /// The new consumed properties. + /// A copy with replaced consumed properties. + public override ParameterBinding With(IReadOnlyList consumedProperties) + => new PropertyParameterBinding((IProperty)consumedProperties.Single()); } } diff --git a/src/EFCore/Metadata/SlimEntityType.cs b/src/EFCore/Metadata/SlimEntityType.cs index 30252668afb..bce53a8aa65 100644 --- a/src/EFCore/Metadata/SlimEntityType.cs +++ b/src/EFCore/Metadata/SlimEntityType.cs @@ -436,16 +436,6 @@ public virtual SlimSkipNavigation AddSkipNavigation( _skipNavigations.Add(name, skipNavigation); - if (targetEntityType.DeclaredReferencingSkipNavigations == null) - { - targetEntityType.DeclaredReferencingSkipNavigations = - new SortedSet(SkipNavigationComparer.Instance) { skipNavigation }; - } - else - { - targetEntityType.DeclaredReferencingSkipNavigations.Add(skipNavigation); - } - return skipNavigation; } @@ -480,18 +470,6 @@ private IEnumerable GetSkipNavigations() : _baseType.GetSkipNavigations().Concat(_skipNavigations.Values) : _skipNavigations.Values; - private IEnumerable GetReferencingSkipNavigations() - => _baseType != null - ? (DeclaredReferencingSkipNavigations?.Count ?? 0) == 0 - ? _baseType.GetReferencingSkipNavigations() - : _baseType.GetReferencingSkipNavigations().Concat(GetDeclaredReferencingSkipNavigations()) - : GetDeclaredReferencingSkipNavigations(); - - private IEnumerable GetDeclaredReferencingSkipNavigations() - => DeclaredReferencingSkipNavigations ?? Enumerable.Empty(); - - private SortedSet? DeclaredReferencingSkipNavigations { get; set; } - /// /// Adds an index to this entity type. /// @@ -598,6 +576,7 @@ private IEnumerable GetIndexes() /// The factory that has been set to generate values for this property, if any. /// The custom set for this property. /// The for this property. + /// The to use with keys for this property. /// The for this property. /// The newly created property. public virtual SlimProperty AddProperty( @@ -619,6 +598,7 @@ public virtual SlimProperty AddProperty( Func? valueGeneratorFactory = null, ValueConverter? valueConverter = null, ValueComparer? valueComparer = null, + ValueComparer? keyValueComparer = null, CoreTypeMapping? typeMapping = null) { var property = new SlimProperty( @@ -641,6 +621,7 @@ public virtual SlimProperty AddProperty( valueGeneratorFactory, valueConverter, valueComparer, + keyValueComparer, typeMapping); _properties.Add(property.Name, property); diff --git a/src/EFCore/Metadata/SlimProperty.cs b/src/EFCore/Metadata/SlimProperty.cs index 61fd53b8b53..dad86d3f55c 100644 --- a/src/EFCore/Metadata/SlimProperty.cs +++ b/src/EFCore/Metadata/SlimProperty.cs @@ -28,7 +28,8 @@ public class SlimProperty : SlimPropertyBase, IProperty private readonly PropertySaveBehavior _afterSaveBehavior; private readonly Func? _valueGeneratorFactory; private readonly ValueConverter? _valueConverter; - private readonly ValueComparer? _valueComparer; + private readonly ValueComparer _valueComparer; + private readonly ValueComparer _keyValueComparer; private CoreTypeMapping? _typeMapping; /// @@ -58,6 +59,7 @@ public SlimProperty( Func? valueGeneratorFactory, ValueConverter? valueConverter, ValueComparer? valueComparer, + ValueComparer? keyValueComparer, CoreTypeMapping? typeMapping) : base(name, propertyInfo, fieldInfo, propertyAccessMode) { @@ -70,7 +72,6 @@ public SlimProperty( _afterSaveBehavior = afterSaveBehavior; _valueGeneratorFactory = valueGeneratorFactory; _valueConverter = valueConverter; - _valueComparer = valueComparer; if (maxLength != null) { @@ -94,6 +95,8 @@ public SlimProperty( } _typeMapping = typeMapping; + _valueComparer = valueComparer ?? TypeMapping.Comparer; + _keyValueComparer = keyValueComparer ?? TypeMapping.KeyComparer; } /// @@ -258,21 +261,21 @@ IEntityType IProperty.DeclaringEntityType /// ValueComparer? IReadOnlyProperty.GetValueComparer() - => _valueComparer ?? TypeMapping.Comparer; + => _valueComparer; /// [DebuggerStepThrough] ValueComparer IProperty.GetValueComparer() - => _valueComparer ?? TypeMapping.Comparer; + => _valueComparer; /// ValueComparer? IReadOnlyProperty.GetKeyValueComparer() - => _valueComparer ?? TypeMapping.KeyComparer; + => _keyValueComparer; /// [DebuggerStepThrough] ValueComparer IProperty.GetKeyValueComparer() - => _valueComparer ?? TypeMapping.KeyComparer; + => _keyValueComparer; /// [DebuggerStepThrough] diff --git a/test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs b/test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs index 31bd7334698..3724a47ab2f 100644 --- a/test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs @@ -545,6 +545,9 @@ public TestModelCacheKeyFactory(Func getAdditionalKey) public object Create(DbContext context) => Tuple.Create(context.GetType(), _getAdditionalKey()); + + public object Create(DbContext context, bool designTime) + => Tuple.Create(context.GetType(), _getAdditionalKey(), designTime); } } diff --git a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs index ed06d07b56f..b3361926378 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs @@ -292,7 +292,7 @@ private static void MissingAnnotationCheck( ? validAnnotations[annotationName].Value : null); - SqlServerTestHelpers.Instance.Finalize(modelBuilder); + SqlServerTestHelpers.Instance.Finalize(modelBuilder, designTime: true); var sb = new IndentedStringBuilder(); @@ -387,7 +387,7 @@ public void Snapshot_with_enum_discriminator_uses_converted_values() eb.Property("EnumDiscriminator").HasConversion(); }); - var finalizedModel = SqlServerTestHelpers.Instance.Finalize(modelBuilder); + var finalizedModel = SqlServerTestHelpers.Instance.Finalize(modelBuilder, designTime: true); var modelSnapshotCode = generator.GenerateSnapshot( "MyNamespace", @@ -649,7 +649,7 @@ public void Snapshots_compile() entityType.SetPrimaryKey(property2); - var finalizedModel = SqlServerTestHelpers.Instance.Finalize(modelBuilder); + var finalizedModel = SqlServerTestHelpers.Instance.Finalize(modelBuilder, designTime: true); var modelSnapshotCode = generator.GenerateSnapshot( "MyNamespace", @@ -765,7 +765,7 @@ public void Snapshot_with_default_values_are_round_tripped() eb.HasKey(e => e.Boolean); }); - var finalizedModel = SqlServerTestHelpers.Instance.Finalize(modelBuilder); + var finalizedModel = SqlServerTestHelpers.Instance.Finalize(modelBuilder, designTime: true); var modelSnapshotCode = generator.GenerateSnapshot( "MyNamespace", diff --git a/test/EFCore.Design.Tests/Migrations/Design/SnapshotModelProcessorTest.cs b/test/EFCore.Design.Tests/Migrations/Design/SnapshotModelProcessorTest.cs index 7b143d62d2b..ed51df05121 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/SnapshotModelProcessorTest.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/SnapshotModelProcessorTest.cs @@ -203,7 +203,7 @@ public void Can_diff_against_older_ownership_model(Type snapshotType) var processor = new SnapshotModelProcessor(reporter, modelRuntimeInitializer); var model = processor.Process(snapshot.Model); - var differences = differ.GetDifferences(model.GetRelationalModel(), context.Model.GetRelationalModel()); + var differences = differ.GetDifferences(model.GetRelationalModel(), context.DesignTimeModel.GetRelationalModel()); Assert.Empty(differences); } @@ -285,6 +285,10 @@ private DummyModelRuntimeInitializer() public IModel Initialize(IModel model, IDiagnosticsLogger validationLogger) => model; + public IModel Initialize( + IModel model, bool designTime = true, IDiagnosticsLogger validationLogger = null) + => model; + public static DummyModelRuntimeInitializer Instance { get; } = new(); } diff --git a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs index a6d7cabf82b..5fa63b73646 100644 --- a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs +++ b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs @@ -5386,7 +5386,7 @@ protected void Test(IModel model, string expectedCode, Action as new ServiceCollection() .AddEntityFrameworkSqlServerNetTopologySuite()); - serviceProvider.GetService().Initialize(model, validationLogger: null); + serviceProvider.GetService().Initialize(model, designTime: true, validationLogger: null); var generator = CreateMigrationsGenerator(); var code = generator.GenerateSnapshot("RootNamespace", typeof(DbContext), "Snapshot", model); diff --git a/test/EFCore.Proxies.Tests/ChangeDetectionProxyTests.cs b/test/EFCore.Proxies.Tests/ChangeDetectionProxyTests.cs index 54bb6880aa5..195aa1ecf1a 100644 --- a/test/EFCore.Proxies.Tests/ChangeDetectionProxyTests.cs +++ b/test/EFCore.Proxies.Tests/ChangeDetectionProxyTests.cs @@ -89,7 +89,7 @@ public void Sets_default_change_tracking_strategy() { using var context = new ChangeContext(); - Assert.Equal(ChangeTrackingStrategy.ChangingAndChangedNotifications, context.Model.GetChangeTrackingStrategy()); + Assert.Equal(ChangeTrackingStrategy.ChangingAndChangedNotifications, context.DesignTimeModel.GetChangeTrackingStrategy()); } [ConditionalFact] diff --git a/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsInfrastructureTestBase.cs b/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsInfrastructureTestBase.cs index 05d05f30d84..d74df04c3b4 100644 --- a/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsInfrastructureTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsInfrastructureTestBase.cs @@ -307,12 +307,12 @@ public virtual void Can_get_active_provider() protected virtual void DiffSnapshot(ModelSnapshot snapshot, DbContext context) { var sourceModel = context.GetService().Initialize( - ((IMutableModel)snapshot.Model).FinalizeModel(), validationLogger: null); + ((IMutableModel)snapshot.Model).FinalizeModel(), designTime: true, validationLogger: null); var modelDiffer = context.GetService(); var operations = modelDiffer.GetDifferences( sourceModel.GetRelationalModel(), - context.Model.GetRelationalModel()); + context.DesignTimeModel.GetRelationalModel()); Assert.Equal(0, operations.Count); } diff --git a/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsSqlGeneratorTestBase.cs b/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsSqlGeneratorTestBase.cs index 206ebbd129b..7e1eed8cf9a 100644 --- a/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsSqlGeneratorTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsSqlGeneratorTestBase.cs @@ -769,7 +769,8 @@ protected virtual void Generate( modelBuilder.Model.RemoveAnnotation(CoreAnnotationNames.ProductVersion); buildAction(modelBuilder); - model = services.GetService().Initialize(modelBuilder.FinalizeModel(), validationLogger: null); + model = services.GetService().Initialize( + modelBuilder.FinalizeModel(), designTime: true, validationLogger: null); } var batch = services.GetRequiredService().Generate(operation, model, options); diff --git a/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs b/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs index 081d247b504..608b4448666 100644 --- a/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs @@ -11,9 +11,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Conventions; -using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata.Internal; -using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations.Operations; using Microsoft.EntityFrameworkCore.Scaffolding; using Microsoft.EntityFrameworkCore.Scaffolding.Metadata; @@ -1612,13 +1610,13 @@ protected virtual Task Test( var sourceModelBuilder = CreateConventionlessModelBuilder(); buildCommonAction(sourceModelBuilder); buildSourceAction(sourceModelBuilder); - var sourceModel = modelRuntimeInitializer.Initialize(sourceModelBuilder.FinalizeModel(), validationLogger: null); + var sourceModel = modelRuntimeInitializer.Initialize(sourceModelBuilder.FinalizeModel(), designTime: true, validationLogger: null); var targetModelBuilder = CreateConventionlessModelBuilder(); buildCommonAction(targetModelBuilder); buildTargetAction(targetModelBuilder); - var targetModel = modelRuntimeInitializer.Initialize(targetModelBuilder.FinalizeModel(), validationLogger: null); // CreateValidationLogger() + var targetModel = modelRuntimeInitializer.Initialize(targetModelBuilder.FinalizeModel(), designTime: true, validationLogger: null); var operations = modelDiffer.GetDifferences(sourceModel.GetRelationalModel(), targetModel.GetRelationalModel()); @@ -1657,7 +1655,7 @@ protected virtual Task Test( var context = CreateContext(); var modelRuntimeInitializer = context.GetService(); - var sourceModel = modelRuntimeInitializer.Initialize(sourceModelBuilder.FinalizeModel(), validationLogger: null); + var sourceModel = modelRuntimeInitializer.Initialize(sourceModelBuilder.FinalizeModel(), designTime: true, validationLogger: null); return Test(sourceModel, targetModel: null, operations, asserter); } diff --git a/test/EFCore.Relational.Tests/Metadata/DbFunctionMetadataTests.cs b/test/EFCore.Relational.Tests/Metadata/DbFunctionMetadataTests.cs index 86cc1c7b89b..1fe99629596 100644 --- a/test/EFCore.Relational.Tests/Metadata/DbFunctionMetadataTests.cs +++ b/test/EFCore.Relational.Tests/Metadata/DbFunctionMetadataTests.cs @@ -326,7 +326,7 @@ var dup2methodInfo public virtual void Finds_DbFunctions_on_DbContext() { var model = RelationalTestHelpers.Instance.CreateContextServices().GetRequiredService() - .Initialize(GetModelBuilder(new MyDerivedContext()).FinalizeModel(), validationLogger: null); + .Initialize(GetModelBuilder(new MyDerivedContext()).FinalizeModel(), designTime: false, validationLogger: null); foreach (var function in MyBaseContext.FunctionNames) { @@ -680,7 +680,7 @@ var queryableNoParams var functionName = modelBuilder.HasDbFunction(queryableNoParams).Metadata.ModelName; var model = RelationalTestHelpers.Instance.CreateContextServices().GetRequiredService() - .Initialize(modelBuilder.FinalizeModel(), validationLogger: null); + .Initialize(modelBuilder.FinalizeModel(), designTime: false, validationLogger: null); var function = model.FindDbFunction(functionName); var entityType = model.FindEntityType(typeof(Foo)); diff --git a/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTestBase.cs b/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTestBase.cs index d8c9c83ab8a..8829673a395 100644 --- a/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTestBase.cs +++ b/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTestBase.cs @@ -79,8 +79,8 @@ protected void Execute( } var modelRuntimeInitializer = TestHelpers.CreateContextServices().GetService(); - sourceModel = modelRuntimeInitializer.Initialize(sourceModel, validationLogger: null); - targetModel = modelRuntimeInitializer.Initialize(targetModel, validationLogger: null); + sourceModel = modelRuntimeInitializer.Initialize(sourceModel, designTime: true, validationLogger: null); + targetModel = modelRuntimeInitializer.Initialize(targetModel, designTime: true, validationLogger: null); var modelDiffer = CreateModelDiffer(targetOptionsBuilder.Options); diff --git a/test/EFCore.Relational.Tests/Storage/RelationalParameterBuilderTest.cs b/test/EFCore.Relational.Tests/Storage/RelationalParameterBuilderTest.cs index 9c935eda87c..7401228e9dc 100644 --- a/test/EFCore.Relational.Tests/Storage/RelationalParameterBuilderTest.cs +++ b/test/EFCore.Relational.Tests/Storage/RelationalParameterBuilderTest.cs @@ -84,7 +84,7 @@ public void Can_add_type_mapped_parameter_by_property(bool nullable) property.IsNullable = nullable; RelationalTestHelpers.Instance.CreateContextServices().GetRequiredService() - .Initialize(model.FinalizeModel(), validationLogger: null); + .Initialize(model.FinalizeModel(), designTime: false, validationLogger: null); var parameterBuilder = new RelationalCommandBuilder( new RelationalCommandBuilderDependencies(typeMapper)); diff --git a/test/EFCore.Relational.Tests/Update/ModificationCommandComparerTest.cs b/test/EFCore.Relational.Tests/Update/ModificationCommandComparerTest.cs index 30110d7db22..e19a862c104 100644 --- a/test/EFCore.Relational.Tests/Update/ModificationCommandComparerTest.cs +++ b/test/EFCore.Relational.Tests/Update/ModificationCommandComparerTest.cs @@ -19,13 +19,14 @@ public class ModificationCommandComparerTest [ConditionalFact] public void Compare_returns_0_only_for_commands_that_are_equal() { - IMutableModel model = new Model(TestRelationalConventionSetBuilder.Build()); + var modelBuilder = new ModelBuilder(TestRelationalConventionSetBuilder.Build()); + var model = modelBuilder.Model; var entityType = model.AddEntityType(typeof(object)); var key = entityType.AddProperty("Id", typeof(int)); entityType.SetPrimaryKey(key); var optionsBuilder = new DbContextOptionsBuilder() - .UseModel(model.FinalizeModel()) + .UseModel(RelationalTestHelpers.Instance.Finalize(modelBuilder)) .UseInMemoryDatabase(Guid.NewGuid().ToString()) .UseInternalServiceProvider(InMemoryFixture.DefaultServiceProvider); @@ -159,7 +160,8 @@ public void Compare_returns_0_only_for_entries_that_have_same_key_values() private void Compare_returns_0_only_for_entries_that_have_same_key_values_generic(T value1, T value2) { - IMutableModel model = new Model(TestRelationalConventionSetBuilder.Build()); + var modelBuilder = new ModelBuilder(TestRelationalConventionSetBuilder.Build()); + var model = modelBuilder.Model; var entityType = model.AddEntityType(typeof(object)); var keyProperty = entityType.AddProperty("Id", typeof(T)); @@ -168,7 +170,7 @@ private void Compare_returns_0_only_for_entries_that_have_same_key_values_generi var optionsBuilder = new DbContextOptionsBuilder() .UseInternalServiceProvider(InMemoryFixture.DefaultServiceProvider) - .UseModel(model.FinalizeModel()) + .UseModel(RelationalTestHelpers.Instance.Finalize(modelBuilder)) .UseInMemoryDatabase(Guid.NewGuid().ToString()); var stateManager = new DbContext(optionsBuilder.Options).GetService(); diff --git a/test/EFCore.Specification.Tests/DataAnnotationTestBase.cs b/test/EFCore.Specification.Tests/DataAnnotationTestBase.cs index e3787cb7ab8..0cd4b3594e4 100644 --- a/test/EFCore.Specification.Tests/DataAnnotationTestBase.cs +++ b/test/EFCore.Specification.Tests/DataAnnotationTestBase.cs @@ -59,7 +59,7 @@ protected virtual IModel Validate(ModelBuilder modelBuilder) var context = CreateContext(); var modelRuntimeInitializer = context.GetService(); var logger = context.GetService>(); - return modelRuntimeInitializer.Initialize(modelBuilder.FinalizeModel(), logger); + return modelRuntimeInitializer.Initialize(modelBuilder.FinalizeModel(), designTime: false, logger); } protected class Person diff --git a/test/EFCore.Specification.Tests/FieldMappingTestBase.cs b/test/EFCore.Specification.Tests/FieldMappingTestBase.cs index e3aa91f29c8..ad161b4ad5b 100644 --- a/test/EFCore.Specification.Tests/FieldMappingTestBase.cs +++ b/test/EFCore.Specification.Tests/FieldMappingTestBase.cs @@ -2182,7 +2182,7 @@ protected override void Seed(PoolableDbContext context) context.Add(CreateBlogAndPosts(new List())); context.AddRange(CreatePostsAndBlog()); - if (context.Model.GetPropertyAccessMode() != PropertyAccessMode.Property) + if (context.DesignTimeModel.GetPropertyAccessMode() != PropertyAccessMode.Property) { context.Add(CreateBlogAndPosts(new ObservableCollection())); context.AddRange(CreatePostsAndBlog()); diff --git a/test/EFCore.Specification.Tests/TestUtilities/TestHelpers.cs b/test/EFCore.Specification.Tests/TestUtilities/TestHelpers.cs index 56c7d494bfc..a48cc65f528 100644 --- a/test/EFCore.Specification.Tests/TestUtilities/TestHelpers.cs +++ b/test/EFCore.Specification.Tests/TestUtilities/TestHelpers.cs @@ -127,12 +127,12 @@ public IServiceProvider CreateContextServices(IServiceCollection customServices, public IServiceProvider CreateContextServices(IServiceCollection customServices) => ((IInfrastructure)CreateContext(customServices)).Instance; - public IModel Finalize(ModelBuilder modelBuilder, bool skipValidation = false) + public IModel Finalize(ModelBuilder modelBuilder, bool designTime = false, bool skipValidation = false) { var contextServices = CreateContextServices(); var modelRuntimeInitializer = contextServices.GetRequiredService(); - return modelRuntimeInitializer.Initialize(modelBuilder.FinalizeModel(), skipValidation + return modelRuntimeInitializer.Initialize(modelBuilder.FinalizeModel(), designTime, skipValidation ? null : new TestLogger(LoggingDefinitions)); } diff --git a/test/EFCore.Tests/ChangeTracking/Internal/KeyPropagatorTest.cs b/test/EFCore.Tests/ChangeTracking/Internal/KeyPropagatorTest.cs index 4a5465bc91d..18754cbe9d4 100644 --- a/test/EFCore.Tests/ChangeTracking/Internal/KeyPropagatorTest.cs +++ b/test/EFCore.Tests/ChangeTracking/Internal/KeyPropagatorTest.cs @@ -28,6 +28,7 @@ public void Foreign_key_value_is_obtained_from_reference_to_principal(bool gener var dependent = new Product { Id = 21, Category = principal }; var contextServices = CreateContextServices(model); + model = contextServices.GetRequiredService(); var dependentEntry = contextServices.GetRequiredService().GetOrCreateEntry(dependent); var property = model.FindEntityType(typeof(Product)).FindProperty("CategoryId"); var keyPropagator = contextServices.GetRequiredService(); @@ -47,6 +48,7 @@ public void Foreign_key_value_is_obtained_from_tracked_principal_with_populated_ { var model = BuildModel(generateTemporary); var contextServices = CreateContextServices(model); + model = contextServices.GetRequiredService(); var manager = contextServices.GetRequiredService(); var principal = new Category { Id = 11 }; @@ -100,6 +102,7 @@ public void One_to_one_foreign_key_value_is_obtained_from_reference_to_principal var dependent = new ProductDetail { Product = principal }; var contextServices = CreateContextServices(model); + model = contextServices.GetRequiredService(); var dependentEntry = contextServices.GetRequiredService().GetOrCreateEntry(dependent); var property = model.FindEntityType(typeof(ProductDetail)).FindProperty("Id"); var keyPropagator = contextServices.GetRequiredService(); @@ -119,6 +122,7 @@ public void One_to_one_foreign_key_value_is_obtained_from_tracked_principal(bool { var model = BuildModel(generateTemporary); var contextServices = CreateContextServices(model); + model = contextServices.GetRequiredService(); var manager = contextServices.GetRequiredService(); var dependent = new ProductDetail(); @@ -201,6 +205,7 @@ public void Composite_foreign_key_value_is_obtained_from_reference_to_principal( var dependent = new OrderLineDetail { OrderLine = principal }; var contextServices = CreateContextServices(model); + model = contextServices.GetRequiredService(); var dependentEntry = contextServices.GetRequiredService().GetOrCreateEntry(dependent); var property1 = model.FindEntityType(typeof(OrderLineDetail)).FindProperty("OrderId"); var property2 = model.FindEntityType(typeof(OrderLineDetail)).FindProperty("ProductId"); @@ -224,6 +229,7 @@ public void Composite_foreign_key_value_is_obtained_from_tracked_principal(bool { var model = BuildModel(generateTemporary); var contextServices = CreateContextServices(model); + model = contextServices.GetRequiredService(); var manager = contextServices.GetRequiredService(); var dependent = new OrderLineDetail(); diff --git a/test/EFCore.Tests/ChangeTracking/Internal/NavigationFixerTest.cs b/test/EFCore.Tests/ChangeTracking/Internal/NavigationFixerTest.cs index 3d54ab6aec5..a3f090a4817 100644 --- a/test/EFCore.Tests/ChangeTracking/Internal/NavigationFixerTest.cs +++ b/test/EFCore.Tests/ChangeTracking/Internal/NavigationFixerTest.cs @@ -475,6 +475,7 @@ public void Does_fixup_of_related_principals_when_FK_is_set() { var model = BuildModel(); var contextServices = CreateContextServices(model); + model = contextServices.GetRequiredService(); var manager = contextServices.GetRequiredService(); var principal1 = new Category { Id = 11 }; @@ -516,6 +517,7 @@ public void Does_fixup_of_related_principals_when_FK_is_cleared() { var model = BuildModel(); var contextServices = CreateContextServices(model); + model = contextServices.GetRequiredService(); var manager = contextServices.GetRequiredService(); var principal1 = new Category { Id = 11 }; @@ -557,6 +559,7 @@ public void Does_fixup_of_related_principals_when_FK_is_changed() { var model = BuildModel(); var contextServices = CreateContextServices(model); + model = contextServices.GetRequiredService(); var manager = contextServices.GetRequiredService(); var principal1 = new Category { Id = 11 }; @@ -598,6 +601,7 @@ public void Does_fixup_of_one_to_one_relationship_when_FK_changes() { var model = BuildModel(); var contextServices = CreateContextServices(model); + model = contextServices.GetRequiredService(); var manager = contextServices.GetRequiredService(); var principal1 = new Product { Id = 21 }; @@ -641,6 +645,7 @@ public void Does_fixup_of_one_to_one_relationship_when_FK_cleared() { var model = BuildModel(); var contextServices = CreateContextServices(model); + model = contextServices.GetRequiredService(); var manager = contextServices.GetRequiredService(); var principal = new Product { Id = 21 }; @@ -679,6 +684,7 @@ public void Does_fixup_of_one_to_one_relationship_when_FK_set() { var model = BuildModel(); var contextServices = CreateContextServices(model); + model = contextServices.GetRequiredService(); var manager = contextServices.GetRequiredService(); var principal = new Product { Id = 21 }; @@ -717,6 +723,7 @@ public void Does_fixup_of_one_to_one_self_referencing_relationship_when_FK_chang { var model = BuildModel(); var contextServices = CreateContextServices(model); + model = contextServices.GetRequiredService(); var manager = contextServices.GetRequiredService(); var entity1 = new Product { Id = 21, AlternateProductId = 22 }; @@ -770,6 +777,7 @@ public void Can_steal_reference_of_one_to_one_self_referencing_relationship_when { var model = BuildModel(); var contextServices = CreateContextServices(model); + model = contextServices.GetRequiredService(); var manager = contextServices.GetRequiredService(); var entity1 = new Product { Id = 21, AlternateProductId = 22 }; @@ -825,6 +833,7 @@ public void Does_fixup_of_all_related_principals_when_part_of_overlapping_compos { var model = BuildModel(); var contextServices = CreateContextServices(model); + model = contextServices.GetRequiredService(); var manager = contextServices.GetRequiredService(); var photo1 = new ProductPhoto { ProductId = 1, PhotoId = "Photo1" }; diff --git a/test/EFCore.Tests/ChangeTracking/Internal/StateManagerTest.cs b/test/EFCore.Tests/ChangeTracking/Internal/StateManagerTest.cs index 728d1170ed1..2c4d978bb81 100644 --- a/test/EFCore.Tests/ChangeTracking/Internal/StateManagerTest.cs +++ b/test/EFCore.Tests/ChangeTracking/Internal/StateManagerTest.cs @@ -743,7 +743,9 @@ public void AcceptAllChanges_processes_all_tracked_entities() public void Can_get_all_dependent_entries() { var model = BuildModel(); - var stateManager = CreateStateManager(model); + var contextServices = InMemoryTestHelpers.Instance.CreateContextServices(model); + model = contextServices.GetRequiredService(); + var stateManager = contextServices.GetRequiredService(); var categoryEntry1 = stateManager.StartTracking( stateManager.GetOrCreateEntry( @@ -791,7 +793,9 @@ public void Can_get_all_dependent_entries() public void Does_not_throws_when_instance_of_unmapped_derived_type_is_used() { var model = BuildModel(); - var stateManager = CreateStateManager(model); + var contextServices = InMemoryTestHelpers.Instance.CreateContextServices(model); + model = contextServices.GetRequiredService(); + var stateManager = contextServices.GetRequiredService(); var entry = stateManager.GetOrCreateEntry(new SpecialProduct()); diff --git a/test/EFCore.Tests/DbContextServicesTest.cs b/test/EFCore.Tests/DbContextServicesTest.cs index 72ca7464715..4daff0ed773 100644 --- a/test/EFCore.Tests/DbContextServicesTest.cs +++ b/test/EFCore.Tests/DbContextServicesTest.cs @@ -661,7 +661,8 @@ public IModel GetModel( public IModel GetModel( DbContext context, - ModelCreationDependencies modelCreationDependencies) + ModelCreationDependencies modelCreationDependencies, + bool designTime) => new Model(); } diff --git a/test/EFCore.Tests/DbContextTest.cs b/test/EFCore.Tests/DbContextTest.cs index 0436171e974..ce96f49ebc2 100644 --- a/test/EFCore.Tests/DbContextTest.cs +++ b/test/EFCore.Tests/DbContextTest.cs @@ -763,7 +763,7 @@ public async Task It_throws_object_disposed_exception(bool async) (await Assert.ThrowsAsync(() => context.FindAsync(typeof(Random), 77).AsTask())).Message); var methodCount = typeof(DbContext).GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly).Count(); - var expectedMethodCount = 42 + 8; + var expectedMethodCount = 51; Assert.True( methodCount == expectedMethodCount, userMessage: $"Expected {expectedMethodCount} methods on DbContext but found {methodCount}. " @@ -778,11 +778,16 @@ public async Task It_throws_object_disposed_exception(bool async) CoreStrings.ContextDisposed, Assert.Throws(() => context.Model).Message); + Assert.StartsWith( + CoreStrings.ContextDisposed, + Assert.Throws(() => context.DesignTimeModel).Message); + var expectedProperties = new List { nameof(DbContext.ChangeTracker), nameof(DbContext.ContextId), // By-design, does not throw for disposed context nameof(DbContext.Database), + nameof(DbContext.DesignTimeModel), nameof(DbContext.Model) }; diff --git a/test/EFCore.Tests/Infrastructure/ModelValidatorTestBase.cs b/test/EFCore.Tests/Infrastructure/ModelValidatorTestBase.cs index 87edff2fd2f..21469ce38a9 100644 --- a/test/EFCore.Tests/Infrastructure/ModelValidatorTestBase.cs +++ b/test/EFCore.Tests/Infrastructure/ModelValidatorTestBase.cs @@ -298,7 +298,7 @@ protected virtual IModel Validate(IMutableModel model, bool sensitiveDataLogging var modelRuntimeInitializer = serviceProvider.GetRequiredService(); var validationLogger = CreateValidationLogger(sensitiveDataLoggingEnabled); - return modelRuntimeInitializer.Initialize(model.FinalizeModel(), validationLogger); + return modelRuntimeInitializer.Initialize(model.FinalizeModel(), designTime: false, validationLogger); } protected DiagnosticsLogger CreateValidationLogger(bool sensitiveDataLoggingEnabled = false) diff --git a/test/EFCore.Tests/Metadata/Conventions/PropertyMappingValidationConventionTest.cs b/test/EFCore.Tests/Metadata/Conventions/PropertyMappingValidationConventionTest.cs index 33d20e4bb62..292e2a997f6 100644 --- a/test/EFCore.Tests/Metadata/Conventions/PropertyMappingValidationConventionTest.cs +++ b/test/EFCore.Tests/Metadata/Conventions/PropertyMappingValidationConventionTest.cs @@ -204,7 +204,7 @@ protected virtual Action CreatePropertyMappingValidator() try { m.FinalizeModel(); - modelRuntimeInitializer.Initialize(m, validationLogger: null); + modelRuntimeInitializer.Initialize(m, designTime: false, validationLogger: null); validatePropertyMappingMethod.Invoke( validator, new object[] { m, logger }); } diff --git a/test/EFCore.Tests/Metadata/Internal/PropertyBaseTest.cs b/test/EFCore.Tests/Metadata/Internal/PropertyBaseTest.cs index 3d55d780ffc..025c4167ea7 100644 --- a/test/EFCore.Tests/Metadata/Internal/PropertyBaseTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/PropertyBaseTest.cs @@ -870,7 +870,8 @@ private void MemberInfoTestCommon( var contextServices = InMemoryTestHelpers.Instance.CreateContextServices(); var modelRuntimeInitializer = contextServices.GetRequiredService(); - model = modelRuntimeInitializer.Initialize(model, new TestLogger()); + model = modelRuntimeInitializer.Initialize( + model, designTime: false, new TestLogger()); Assert.Null(failMessage); } diff --git a/test/EFCore.Tests/ModelBuilding/InheritanceTestBase.cs b/test/EFCore.Tests/ModelBuilding/InheritanceTestBase.cs index b5abfeeb696..e6cea5a811e 100644 --- a/test/EFCore.Tests/ModelBuilding/InheritanceTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/InheritanceTestBase.cs @@ -244,15 +244,29 @@ public virtual void Pulling_relationship_to_a_derived_type_creates_relationships .HasMany(e => e.SpecialOrders) .WithOne(e => (SpecialCustomer)e.Customer); - modelBuilder.FinalizeModel(); + var model = modelBuilder.FinalizeModel(); - Assert.Empty(dependentEntityBuilder.Metadata.GetForeignKeys()); - Assert.Empty(dependentEntityBuilder.Metadata.GetNavigations()); - var newFk = derivedDependentEntityBuilder.Metadata.GetDeclaredNavigations().Single().ForeignKey; + var dependentEntityType = model.FindEntityType(dependentEntityBuilder.Metadata.Name); + var derivedDependentEntityType = model.FindEntityType(derivedDependentEntityBuilder.Metadata.Name); + var otherDerivedDependentEntityType = model.FindEntityType(otherDerivedDependentEntityBuilder.Metadata.Name); + var derivedPrincipalEntityType = model.FindEntityType(derivedPrincipalEntityBuilder.Metadata.Name); + + Assert.Empty(dependentEntityType.GetForeignKeys()); + Assert.Empty(dependentEntityType.GetNavigations()); + var newFk = derivedDependentEntityType.GetDeclaredNavigations().Single().ForeignKey; Assert.Equal(nameof(Order.Customer), newFk.DependentToPrincipal.Name); Assert.Equal(nameof(SpecialCustomer.SpecialOrders), newFk.PrincipalToDependent.Name); - Assert.Same(derivedPrincipalEntityBuilder.Metadata, newFk.PrincipalEntityType); - var otherDerivedFk = otherDerivedDependentEntityBuilder.Metadata.GetDeclaredNavigations().Single().ForeignKey; + Assert.Same(derivedPrincipalEntityType, newFk.PrincipalEntityType); + Assert.Same(newFk.DependentToPrincipal, + dependentEntityType.GetDerivedNavigations().Single(fk => fk.DeclaringEntityType == derivedDependentEntityType)); + Assert.Same(newFk, dependentEntityType.GetDerivedForeignKeys().Single(fk => fk.DeclaringEntityType == derivedDependentEntityType)); + Assert.Same(newFk, derivedDependentEntityType.GetDeclaredForeignKeys().Single()); + Assert.Same(newFk, derivedDependentEntityType.FindForeignKey(newFk.Properties, newFk.PrincipalKey, newFk.PrincipalEntityType)); + Assert.Same(newFk, + derivedPrincipalEntityType.GetReferencingForeignKeys().Single(fk => fk.DeclaringEntityType == derivedDependentEntityType)); + Assert.Equal(derivedPrincipalEntityType.GetDeclaredReferencingForeignKeys(), + derivedPrincipalEntityType.GetReferencingForeignKeys().Where(fk => fk.DeclaringEntityType == derivedDependentEntityType)); + var otherDerivedFk = otherDerivedDependentEntityType.GetDeclaredNavigations().Single().ForeignKey; Assert.Equal(nameof(Order.Customer), otherDerivedFk.DependentToPrincipal.Name); Assert.Null(otherDerivedFk.PrincipalToDependent); Assert.Equal(nameof(Order.CustomerId), otherDerivedFk.Properties.Single().Name); diff --git a/test/EFCore.Tests/ModelBuilding/ManyToManyTestBase.cs b/test/EFCore.Tests/ModelBuilding/ManyToManyTestBase.cs index 25f20bed862..e8c0abfc28d 100644 --- a/test/EFCore.Tests/ModelBuilding/ManyToManyTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/ManyToManyTestBase.cs @@ -174,7 +174,7 @@ public virtual void Join_type_is_automatically_configured_by_convention() var manyToManyA = model.FindEntityType(typeof(ImplicitManyToManyA))!; var manyToManyB = model.FindEntityType(typeof(ImplicitManyToManyB))!; var joinEntityType = model.GetEntityTypes() - .Where(et => ((EntityType)et).IsImplicitlyCreatedJoinEntityType) + .Where(et => et.ClrType == Model.DefaultPropertyBagType) .Single(); Assert.Equal("ImplicitManyToManyAImplicitManyToManyB", joinEntityType.Name); diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs index b184f8fde36..88e9273e936 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs @@ -223,6 +223,9 @@ public override TestEntityTypeBuilder Ignore(string propertyName) public override TestIndexBuilder HasIndex(Expression> indexExpression) => new GenericTestIndexBuilder(EntityTypeBuilder.HasIndex(indexExpression)); + public override TestIndexBuilder HasIndex(Expression> indexExpression, string name) + => new GenericTestIndexBuilder(EntityTypeBuilder.HasIndex(indexExpression, name)); + public override TestIndexBuilder HasIndex(params string[] propertyNames) => new GenericTestIndexBuilder(EntityTypeBuilder.HasIndex(propertyNames)); diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs index 1eca7affdaa..8f9f1f5a8a3 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs @@ -105,7 +105,8 @@ public virtual void HasForeignKey_infers_type_for_shadow_property_when_not_speci modelBuilder.Entity().HasKey(c => c.Key); - var model = (IConventionModel)modelBuilder.FinalizeModel(); + var model = (IConventionModel)modelBuilder.FinalizeModel(designTime: true); + var property = model .FindEntityType(typeof(ComplexCaseChild13108))!.GetProperties().Single(p => p.Name == "ParentKey"); Assert.Equal(typeof(int), property.ClrType); @@ -266,6 +267,10 @@ public override TestIndexBuilder HasIndex(Expression new NonGenericTestIndexBuilder( EntityTypeBuilder.HasIndex(indexExpression.GetMemberAccessList().Select(p => p.GetSimpleMemberName()).ToArray())); + public override TestIndexBuilder HasIndex(Expression> indexExpression, string name) + => new NonGenericTestIndexBuilder( + EntityTypeBuilder.HasIndex(indexExpression.GetMemberAccessList().Select(p => p.GetSimpleMemberName()).ToArray(), name)); + public override TestIndexBuilder HasIndex(params string[] propertyNames) => new NonGenericTestIndexBuilder(EntityTypeBuilder.HasIndex(propertyNames)); diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs index b688b3d92ef..0edf2aba76a 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs @@ -172,12 +172,12 @@ public abstract TestModelBuilder SharedTypeEntity(string name, Action() where TEntity : class; - public virtual IModel FinalizeModel() + public virtual IModel FinalizeModel(bool designTime = false) { var serviceProvider = TestHelpers.CreateContextServices(); var modelRuntimeInitializer = serviceProvider.GetRequiredService(); - return modelRuntimeInitializer.Initialize(ModelBuilder.FinalizeModel(), ValidationLogger); + return modelRuntimeInitializer.Initialize(ModelBuilder.FinalizeModel(), designTime, ValidationLogger); } public virtual string GetDisplayName(Type entityType) @@ -227,6 +227,7 @@ public abstract TestEntityTypeBuilder Ignore( public abstract TestEntityTypeBuilder Ignore(string propertyName); public abstract TestIndexBuilder HasIndex(Expression> indexExpression); + public abstract TestIndexBuilder HasIndex(Expression> indexExpression, string name); public abstract TestIndexBuilder HasIndex(params string[] propertyNames); public abstract TestOwnedNavigationBuilder OwnsOne(string navigationName) diff --git a/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs b/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs index 8135cedb9d0..774871fae52 100644 --- a/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs @@ -1265,18 +1265,26 @@ public virtual void Can_add_index_when_no_clr_property() public virtual void Can_add_multiple_indexes() { var modelBuilder = CreateModelBuilder(); - var model = modelBuilder.Model; var entityBuilder = modelBuilder.Entity(); - var firstIndexBuilder = entityBuilder.HasIndex(ix => ix.Id).IsUnique(); - var secondIndexBuilder = entityBuilder.HasIndex(ix => ix.Name).HasAnnotation("A1", "V1"); + entityBuilder.HasIndex(ix => ix.Id).IsUnique(); + entityBuilder.HasIndex(ix => ix.Name).HasAnnotation("A1", "V1"); + entityBuilder.HasIndex(ix => ix.Id, "Named"); - var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(Customer)); + var model = modelBuilder.FinalizeModel(); - Assert.Equal(2, entityType.GetIndexes().Count()); - Assert.True(firstIndexBuilder.Metadata.IsUnique); - Assert.False(((IReadOnlyIndex)secondIndexBuilder.Metadata).IsUnique); - Assert.Equal("V1", secondIndexBuilder.Metadata["A1"]); + var entityType = model.FindEntityType(typeof(Customer)); + var idProperty = entityType.FindProperty(nameof(Customer.Id)); + var nameProperty = entityType.FindProperty(nameof(Customer.Name)); + + Assert.Equal(3, entityType.GetIndexes().Count()); + var firstIndex = entityType.FindIndex(idProperty); + Assert.True(firstIndex.IsUnique); + var secondIndex = entityType.FindIndex(nameProperty); + Assert.False(secondIndex.IsUnique); + Assert.Equal("V1", secondIndex["A1"]); + var namedIndex = entityType.FindIndex("Named"); + Assert.False(namedIndex.IsUnique); } [ConditionalFact] @@ -1630,7 +1638,7 @@ public virtual void Can_add_seed_data_anonymous_objects_indexed_property_diction b.HasData(new { Id = -1, Required = 2 }); }); - var model = modelBuilder.FinalizeModel(); + var model = modelBuilder.FinalizeModel(designTime: true); var entityType = model.FindEntityType(typeof(IndexedClassByDictionary)); var data = Assert.Single(entityType.GetSeedData()); @@ -1670,26 +1678,35 @@ protected class Entityss public virtual void Can_add_shared_type_entity_type() { var modelBuilder = CreateModelBuilder(); - modelBuilder.SharedTypeEntity>("Shared1"); + modelBuilder.SharedTypeEntity>("Shared1", b => + { + b.IndexerProperty("Key"); + b.HasKey("Key"); + }); modelBuilder.SharedTypeEntity>("Shared2", b => b.IndexerProperty("Id")); - var model = modelBuilder.Model; + Assert.Equal( + CoreStrings.ClashingSharedType(typeof(Dictionary).ShortDisplayName()), + Assert.Throws(() => modelBuilder.Entity>()).Message); + + var model = modelBuilder.FinalizeModel(); Assert.Equal(2, model.GetEntityTypes().Count()); - var shared1 = modelBuilder.Model.FindEntityType("Shared1"); + var shared1 = model.FindEntityType("Shared1"); Assert.NotNull(shared1); Assert.True(shared1.HasSharedClrType); Assert.Null(shared1.FindProperty("Id")); - var shared2 = modelBuilder.Model.FindEntityType("Shared2"); + var shared2 = model.FindEntityType("Shared2"); Assert.NotNull(shared2); Assert.True(shared2.HasSharedClrType); Assert.NotNull(shared2.FindProperty("Id")); - Assert.Equal( - CoreStrings.ClashingSharedType(typeof(Dictionary).ShortDisplayName()), - Assert.Throws(() => modelBuilder.Entity>()).Message); + var indexer = shared1.FindIndexerPropertyInfo(); + Assert.True(model.IsIndexerMethod(indexer.GetMethod)); + Assert.True(model.IsIndexerMethod(indexer.SetMethod)); + Assert.Same(indexer, shared2.FindIndexerPropertyInfo()); } [ConditionalFact] diff --git a/test/EFCore.Tests/ModelBuilding/OneToOneTestBase.cs b/test/EFCore.Tests/ModelBuilding/OneToOneTestBase.cs index 1d868f4a668..92dcafe2937 100644 --- a/test/EFCore.Tests/ModelBuilding/OneToOneTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/OneToOneTestBase.cs @@ -1671,6 +1671,7 @@ public virtual void Principal_and_dependent_can_be_flipped_twice_separately() Assert.Same(fk, principalType.GetNavigations().Single().ForeignKey); Assert.Empty(principalType.GetForeignKeys()); Assert.Same(fk.PrincipalKey, principalType.GetKeys().First(k => !k.IsPrimaryKey())); + Assert.Same(fk.PrincipalKey, principalType.GetDeclaredKeys().First(k => !k.IsPrimaryKey())); Assert.Empty(dependentType.GetIndexes()); Assert.Empty(principalType.GetIndexes()); Assert.True(fk.IsUnique); diff --git a/test/EFCore.Tests/ModelBuilding/OwnedTypesTestBase.cs b/test/EFCore.Tests/ModelBuilding/OwnedTypesTestBase.cs index 8fde3059d68..7ec6da2ae58 100644 --- a/test/EFCore.Tests/ModelBuilding/OwnedTypesTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/OwnedTypesTestBase.cs @@ -195,7 +195,7 @@ public virtual void Can_configure_owned_type_properties() .Ignore(d => d.Id) .Property("foo"); - var model = modelBuilder.FinalizeModel(); + var model = modelBuilder.FinalizeModel(designTime: true); var owner = model.FindEntityType(typeof(Customer)); var owned = owner.FindNavigation(nameof(Customer.Details)).ForeignKey.DeclaringEntityType; @@ -400,12 +400,11 @@ public virtual void Can_configure_owned_type_collection() entityBuilder.WithOwner(o => o.Customer) .HasPrincipalKey(c => c.AlternateKey); - var model = modelBuilder.FinalizeModel(); + var model = modelBuilder.FinalizeModel(designTime: true); var owner = model.FindEntityType(typeof(Customer)); var ownership = owner.FindNavigation(nameof(Customer.Orders)).ForeignKey; var owned = ownership.DeclaringEntityType; - Assert.Same(entityBuilder.OwnedEntityType, owned); Assert.True(ownership.IsOwnership); Assert.Equal("CustomerAlternateKey", ownership.Properties.Single().Name); Assert.Equal(nameof(Customer.AlternateKey), ownership.PrincipalKey.Properties.Single().Name); @@ -578,7 +577,7 @@ public virtual void Can_configure_owned_type_from_an_owned_type_collection(HasDa } }); - var model = modelBuilder.FinalizeModel(); + var model = modelBuilder.FinalizeModel(designTime: true); var ownership = model.FindEntityType(typeof(Customer)).FindNavigation(nameof(Customer.Orders)).ForeignKey; var owned = ownership.DeclaringEntityType; @@ -618,7 +617,7 @@ public virtual void Can_chain_owned_type_collection_configurations() }); }); - var model = modelBuilder.FinalizeModel(); + var model = modelBuilder.FinalizeModel(designTime: true); var ownership = model.FindEntityType(typeof(Customer)).FindNavigation(nameof(Customer.Orders)).ForeignKey; var owned = ownership.DeclaringEntityType; @@ -762,28 +761,36 @@ public virtual void Can_configure_owned_type_collection_with_one_call() .OwnsMany(c => c.Orders) .HasKey(o => o.OrderId); - var specialCustomer = modelBuilder.Entity() + modelBuilder.Entity() .OwnsMany( c => c.SpecialOrders, so => { so.HasKey(o => o.SpecialOrderId); so.Ignore(o => o.Customer); so.OwnsOne(o => o.BackOrder); - }).Metadata; + }); var model = modelBuilder.FinalizeModel(); var customer = model.FindEntityType(typeof(Customer)); + var specialCustomer = model.FindEntityType(typeof(SpecialCustomer)); var ownership = customer.FindNavigation(nameof(Customer.Orders)).ForeignKey; Assert.True(ownership.IsOwnership); Assert.False(ownership.IsUnique); Assert.Equal(nameof(Order.OrderId), ownership.DeclaringEntityType.FindPrimaryKey().Properties.Single().Name); + Assert.Same(ownership.DeclaringEntityType, + model.FindEntityType(typeof(Order), nameof(Customer.Orders), customer)); + Assert.Equal(2, model.FindEntityTypes(typeof(Order)).Count()); + Assert.True(model.IsShared(typeof(Order))); + var specialOwnership = specialCustomer.FindNavigation(nameof(SpecialCustomer.SpecialOrders)).ForeignKey; Assert.True(specialOwnership.IsOwnership); Assert.False(specialOwnership.IsUnique); Assert.Equal( nameof(SpecialOrder.SpecialOrderId), specialOwnership.DeclaringEntityType.FindPrimaryKey().Properties.Single().Name); + Assert.Same(specialOwnership.DeclaringEntityType, + model.FindEntityType(typeof(SpecialOrder))); Assert.Equal(9, modelBuilder.Model.GetEntityTypes().Count()); Assert.Equal(2, modelBuilder.Model.FindEntityTypes(typeof(Order)).Count()); diff --git a/test/EFCore.Tests/ModelSourceTest.cs b/test/EFCore.Tests/ModelSourceTest.cs index a827a9993ac..1dcb8f030d5 100644 --- a/test/EFCore.Tests/ModelSourceTest.cs +++ b/test/EFCore.Tests/ModelSourceTest.cs @@ -9,7 +9,6 @@ using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal; @@ -73,7 +72,8 @@ public void Adds_all_entities_based_on_all_distinct_entity_types_found() .GetModel( new Context1(), serviceProvider.GetRequiredService() - with { ConventionSetBuilder = CreateRuntimeConventionSetBuilder(new FakeSetFinder(), serviceProvider) }); + with { ConventionSetBuilder = CreateRuntimeConventionSetBuilder(new FakeSetFinder(), serviceProvider) }, + designTime: false); Assert.Equal( new[] { typeof(SetA).DisplayName(), typeof(SetB).DisplayName() }, @@ -123,12 +123,69 @@ public void Caches_model_by_context_type() var modelSource = serviceProvider.GetRequiredService(); var testModelDependencies = serviceProvider.GetRequiredService(); - var model1 = modelSource.GetModel(new Context1(), testModelDependencies); - var model2 = modelSource.GetModel(new Context2(), testModelDependencies); + var model1 = modelSource.GetModel(new Context1(), testModelDependencies, designTime: false); + var model2 = modelSource.GetModel(new Context2(), testModelDependencies, designTime: false); + + var designModel1 = modelSource.GetModel(new Context1(), testModelDependencies, designTime: true); + var designModel2 = modelSource.GetModel(new Context2(), testModelDependencies, designTime: true); Assert.NotSame(model1, model2); - Assert.Same(model1, modelSource.GetModel(new Context1(), testModelDependencies)); - Assert.Same(model2, modelSource.GetModel(new Context2(), testModelDependencies)); + Assert.Same(model1, modelSource.GetModel(new Context1(), testModelDependencies, designTime: false)); + Assert.Same(model2, modelSource.GetModel(new Context2(), testModelDependencies, designTime: false)); + + Assert.NotSame(designModel1, designModel2); + Assert.Same(designModel1, modelSource.GetModel(new Context1(), testModelDependencies, designTime: true)); + Assert.Same(designModel2, modelSource.GetModel(new Context2(), testModelDependencies, designTime: true)); + + Assert.NotSame(model1, designModel1); + Assert.NotSame(model2, designModel2); + } + + [ConditionalFact] + public void Model_from_options_is_preserved() + { + var serviceProvider = InMemoryTestHelpers.Instance.CreateContextServices(); + var modelSource = serviceProvider.GetRequiredService(); + var testModelDependencies = serviceProvider.GetRequiredService(); + + var model = modelSource.GetModel(new Context1(), testModelDependencies, designTime: false); + var designTimeModel = modelSource.GetModel(new Context1(), testModelDependencies, designTime: true); + + Assert.NotSame(model, designTimeModel); + + var context = new ModelContext(model); + + Assert.NotSame(context.Model, context.DesignTimeModel); + Assert.Same(model, context.Model); + Assert.NotSame(model, context.DesignTimeModel); + + var designTimeContext = new ModelContext(designTimeModel); + + Assert.NotSame(context.Model, designTimeContext.DesignTimeModel); + Assert.NotSame(model, designTimeContext.Model); + Assert.Same(designTimeModel, designTimeContext.DesignTimeModel); + } + + private class ModelContext : DbContext + { + private readonly IModel _model; + + public ModelContext(IModel model) + { + _model = model; + } + + protected internal override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder = optionsBuilder + .UseInternalServiceProvider(InMemoryFixture.DefaultServiceProvider) + .UseInMemoryDatabase(nameof(ModelContext)); + + if (_model != null) + { + optionsBuilder.UseModel(_model); + } + } } [ConditionalFact] @@ -138,7 +195,7 @@ public void Stores_model_version_information_as_annotation_on_model() var modelSource = serviceProvider.GetRequiredService(); var testModelDependencies = serviceProvider.GetRequiredService(); - var model = modelSource.GetModel(new Context1(), testModelDependencies); + var model = modelSource.GetModel(new Context1(), testModelDependencies, designTime: false); var packageVersion = typeof(Context1).Assembly.GetCustomAttributes() .Single(m => m.Key == "PackageVersion").Value;