diff --git a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs index 1bb73c75382..98f1c51809f 100644 --- a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs +++ b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs @@ -316,80 +316,103 @@ protected virtual void GenerateSequence( ISequence sequence, IndentedStringBuilder stringBuilder) { - stringBuilder - .AppendLine() + var sequenceBuilderNameBuilder = new StringBuilder(); + sequenceBuilderNameBuilder .Append(modelBuilderName) .Append(".HasSequence"); if (sequence.Type != Sequence.DefaultClrType) { - stringBuilder + sequenceBuilderNameBuilder .Append("<") .Append(Code.Reference(sequence.Type)) .Append(">"); } - stringBuilder + sequenceBuilderNameBuilder .Append("(") .Append(Code.Literal(sequence.Name)); if (!string.IsNullOrEmpty(sequence.Schema) && sequence.Model.GetDefaultSchema() != sequence.Schema) { - stringBuilder + sequenceBuilderNameBuilder .Append(", ") .Append(Code.Literal(sequence.Schema)); } - stringBuilder.Append(")"); + sequenceBuilderNameBuilder.Append(")"); + var sequenceBuilderName = sequenceBuilderNameBuilder.ToString(); - using (stringBuilder.Indent()) + stringBuilder + .AppendLine() + .Append(sequenceBuilderName); + + // Note that GenerateAnnotations below does the corresponding decrement + stringBuilder.IncrementIndent(); + + if (sequence.StartValue != Sequence.DefaultStartValue) { - if (sequence.StartValue != Sequence.DefaultStartValue) - { - stringBuilder - .AppendLine() - .Append(".StartsAt(") - .Append(Code.Literal(sequence.StartValue)) - .Append(")"); - } + stringBuilder + .AppendLine() + .Append(".StartsAt(") + .Append(Code.Literal(sequence.StartValue)) + .Append(")"); + } - if (sequence.IncrementBy != Sequence.DefaultIncrementBy) - { - stringBuilder - .AppendLine() - .Append(".IncrementsBy(") - .Append(Code.Literal(sequence.IncrementBy)) - .Append(")"); - } + if (sequence.IncrementBy != Sequence.DefaultIncrementBy) + { + stringBuilder + .AppendLine() + .Append(".IncrementsBy(") + .Append(Code.Literal(sequence.IncrementBy)) + .Append(")"); + } - if (sequence.MinValue != Sequence.DefaultMinValue) - { - stringBuilder - .AppendLine() - .Append(".HasMin(") - .Append(Code.Literal(sequence.MinValue)) - .Append(")"); - } + if (sequence.MinValue != Sequence.DefaultMinValue) + { + stringBuilder + .AppendLine() + .Append(".HasMin(") + .Append(Code.Literal(sequence.MinValue)) + .Append(")"); + } - if (sequence.MaxValue != Sequence.DefaultMaxValue) - { - stringBuilder - .AppendLine() - .Append(".HasMax(") - .Append(Code.Literal(sequence.MaxValue)) - .Append(")"); - } + if (sequence.MaxValue != Sequence.DefaultMaxValue) + { + stringBuilder + .AppendLine() + .Append(".HasMax(") + .Append(Code.Literal(sequence.MaxValue)) + .Append(")"); + } - if (sequence.IsCyclic != Sequence.DefaultIsCyclic) - { - stringBuilder - .AppendLine() - .Append(".IsCyclic()"); - } + if (sequence.IsCyclic != Sequence.DefaultIsCyclic) + { + stringBuilder + .AppendLine() + .Append(".IsCyclic()"); } - stringBuilder.AppendLine(";"); + GenerateSequenceAnnotations(sequenceBuilderName, sequence, stringBuilder); + } + + /// + /// Generates code for sequence annotations. + /// + /// The name of the sequence builder variable. + /// The sequence. + /// The builder code is added to. + protected virtual void GenerateSequenceAnnotations( + string sequenceBuilderName, + ISequence sequence, + IndentedStringBuilder stringBuilder) + { + var annotations = Dependencies.AnnotationCodeGenerator + .FilterIgnoredAnnotations(sequence.GetAnnotations()) + .ToDictionary(a => a.Name, a => a); + + GenerateAnnotations(sequenceBuilderName, sequence, stringBuilder, annotations, inChainedCall: true); } /// @@ -849,7 +872,7 @@ private void GenerateTableMapping( annotations.Remove(isExcludedAnnotation.Name); } - var hasTriggers = entityType.GetTriggers().Any(); + var hasTriggers = entityType.GetTriggers().Any(t => t.TableName == tableName! && t.TableSchema == schema); var hasOverrides = table != null && entityType.GetProperties().Select(p => p.FindOverrides(table.Value)).Any(o => o != null); var requiresTableBuilder = isExcludedFromMigrations @@ -889,8 +912,7 @@ private void GenerateTableMapping( if (hasTriggers) { - stringBuilder.AppendLine(); - GenerateTriggers("t", entityType, stringBuilder); + GenerateTriggers("t", entityType, tableName!, schema, stringBuilder); } if (hasOverrides) @@ -914,13 +936,14 @@ private void GenerateSplitTableMapping( { foreach (var fragment in entityType.GetMappingFragments(StoreObjectType.Table)) { + var table = fragment.StoreObject; stringBuilder .AppendLine() .Append(entityTypeBuilderName) .Append(".SplitToTable(") - .Append(Code.UnknownLiteral(fragment.StoreObject.Name)) + .Append(Code.UnknownLiteral(table.Name)) .Append(", ") - .Append(Code.UnknownLiteral(fragment.StoreObject.Schema)) + .Append(Code.UnknownLiteral(table.Schema)) .AppendLine(", t =>"); using (stringBuilder.Indent()) @@ -929,7 +952,9 @@ private void GenerateSplitTableMapping( using (stringBuilder.Indent()) { - GenerateOverrides("t", entityType, fragment.StoreObject, stringBuilder); + GenerateTriggers("t", entityType, table.Name, table.Schema, stringBuilder); + GenerateOverrides("t", entityType, table, stringBuilder); + GenerateEntityTypeMappingFragmentAnnotations("t", fragment, stringBuilder); } stringBuilder @@ -1025,6 +1050,7 @@ private void GenerateSplitViewMapping( using (stringBuilder.Indent()) { GenerateOverrides("v", entityType, fragment.StoreObject, stringBuilder); + GenerateEntityTypeMappingFragmentAnnotations("v", fragment, stringBuilder); } stringBuilder @@ -1034,6 +1060,26 @@ private void GenerateSplitViewMapping( } } + /// + /// Generates code for mapping fragment annotations. + /// + /// The name of the table builder variable. + /// The mapping fragment. + /// The builder code is added to. + protected virtual void GenerateEntityTypeMappingFragmentAnnotations( + string tableBuilderName, + IEntityTypeMappingFragment fragment, + IndentedStringBuilder stringBuilder) + { + var annotations = Dependencies.AnnotationCodeGenerator + .FilterIgnoredAnnotations(fragment.GetAnnotations()) + .ToDictionary(a => a.Name, a => a); + if (annotations.Count > 0) + { + GenerateAnnotations(tableBuilderName, fragment, stringBuilder, annotations, inChainedCall: false); + } + } + /// /// Generates code for objects. /// @@ -1137,14 +1183,23 @@ protected virtual void GenerateCheckConstraintAnnotations( /// /// The name of the table builder variable. /// The entity type. + /// The table name. + /// The table schema. /// The builder code is added to. protected virtual void GenerateTriggers( string tableBuilderName, IEntityType entityType, + string table, + string? schema, IndentedStringBuilder stringBuilder) { foreach (var trigger in entityType.GetTriggers()) { + if (trigger.TableName != table || trigger.TableSchema != schema) + { + continue; + } + GenerateTrigger(tableBuilderName, trigger, stringBuilder); } } @@ -1161,13 +1216,14 @@ protected virtual void GenerateTrigger( IndentedStringBuilder stringBuilder) { var triggerBuilderName = $"{tableBuilderName}.HasTrigger({Code.Literal(trigger.ModelName)})"; - stringBuilder.Append(triggerBuilderName); + stringBuilder + .AppendLine() + .Append(triggerBuilderName); // Note that GenerateAnnotations below does the corresponding decrement stringBuilder.IncrementIndent(); - if (trigger.Name != null - && trigger.Name != (trigger.GetDefaultName() ?? trigger.ModelName)) + if (trigger.Name != trigger.GetDefaultName()!) { stringBuilder .AppendLine() diff --git a/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs b/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs index 41cbee2abbb..45f5999c6b0 100644 --- a/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs +++ b/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs @@ -433,57 +433,63 @@ private void GenerateEntityType(IEntityType entityType) { _builder.AppendLine(); - _builder.Append($"{EntityLambdaIdentifier}.{nameof(RelationalEntityTypeBuilderExtensions.ToTable)}(tb => "); + _builder.AppendLine($"{EntityLambdaIdentifier}.{nameof(RelationalEntityTypeBuilderExtensions.ToTable)}(tb =>"); - // Note: no trigger annotation support as of yet - - if (triggers.Length == 1) - { - var trigger = triggers[0]; - if (trigger.Name is not null) - { - _builder.AppendLine($"tb.HasTrigger({_code.Literal(trigger.Name)}));"); - } - } - else + using (_builder.Indent()) { - _builder.AppendLine("{"); - - using (_builder.Indent()) + _builder.Append("{"); + foreach (var trigger in entityType.GetTriggers().Where(t => t.Name is not null)) { - foreach (var trigger in entityType.GetTriggers().Where(t => t.Name is not null)) - { - _builder.AppendLine($"tb.HasTrigger({_code.Literal(trigger.Name!)});"); - } + GenerateTrigger("tb", trigger); } - _builder.AppendLine("});"); } + } } } - private void AppendMultiLineFluentApi(IEntityType entityType, IList lines) + private void GenerateTrigger(string tableBuilderName, ITrigger trigger) + { + var lines = new List { $".HasTrigger({_code.Literal(trigger.Name!)})" }; + + var annotations = _annotationCodeGenerator + .FilterIgnoredAnnotations(trigger.GetAnnotations()) + .ToDictionary(a => a.Name, a => a); + + _annotationCodeGenerator.RemoveAnnotationsHandledByConventions(trigger, annotations); + + GenerateAnnotations(trigger, annotations, lines); + + AppendMultiLineFluentApi(null, lines, tableBuilderName); + } + + private void AppendMultiLineFluentApi(IEntityType? entityType, IList lines, string? builderName = null) { if (lines.Count <= 0) { return; } - InitializeEntityTypeBuilder(entityType); + if (entityType != null) + { + InitializeEntityTypeBuilder(entityType); + } using (_builder.Indent()) { - _builder.AppendLine(); - - _builder.Append(EntityLambdaIdentifier + lines[0]); + _builder + .AppendLine() + .Append(builderName ?? EntityLambdaIdentifier) + .Append(lines[0]); using (_builder.Indent()) { foreach (var line in lines.Skip(1)) { - _builder.AppendLine(); - _builder.Append(line); + _builder + .AppendLine() + .Append(line); } } diff --git a/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs b/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs index 3e80ae0dc5d..44ce8440296 100644 --- a/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs +++ b/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs @@ -157,6 +157,12 @@ public virtual void RemoveAnnotationsHandledByConventions( RemoveConventionalAnnotationsHelper(entityType, annotations, IsHandledByConvention); } + /// + public virtual void RemoveAnnotationsHandledByConventions( + IEntityTypeMappingFragment fragment, + IDictionary annotations) + => RemoveConventionalAnnotationsHelper(fragment, annotations, IsHandledByConvention); + /// public virtual void RemoveAnnotationsHandledByConventions( IProperty property, @@ -185,6 +191,25 @@ public virtual void RemoveAnnotationsHandledByConventions( public virtual void RemoveAnnotationsHandledByConventions(IIndex index, IDictionary annotations) => RemoveConventionalAnnotationsHelper(index, annotations, IsHandledByConvention); + /// + public virtual void RemoveAnnotationsHandledByConventions( + ICheckConstraint checkConstraint, IDictionary annotations) + => RemoveConventionalAnnotationsHelper(checkConstraint, annotations, IsHandledByConvention); + + /// + public virtual void RemoveAnnotationsHandledByConventions(ITrigger trigger, IDictionary annotations) + => RemoveConventionalAnnotationsHelper(trigger, annotations, IsHandledByConvention); + + /// + public virtual void RemoveAnnotationsHandledByConventions( + IRelationalPropertyOverrides overrides, IDictionary annotations) + => RemoveConventionalAnnotationsHelper(overrides, annotations, IsHandledByConvention); + + /// + public virtual void RemoveAnnotationsHandledByConventions( + ISequence sequence, IDictionary annotations) + => RemoveConventionalAnnotationsHelper(sequence, annotations, IsHandledByConvention); + /// public virtual IReadOnlyList GenerateFluentApiCalls( IModel model, @@ -244,6 +269,18 @@ public virtual IReadOnlyList GenerateFluentApiCalls( return methodCallCodeFragments; } + /// + public virtual IReadOnlyList GenerateFluentApiCalls( + IEntityTypeMappingFragment fragment, + IDictionary annotations) + { + var methodCallCodeFragments = new List(); + + methodCallCodeFragments.AddRange(GenerateFluentApiCallsHelper(fragment, annotations, GenerateFluentApi)); + + return methodCallCodeFragments; + } + /// public virtual IReadOnlyList GenerateFluentApiCalls( IProperty property, @@ -371,6 +408,54 @@ public virtual IReadOnlyList GenerateFluentApiCalls( return methodCallCodeFragments; } + /// + public virtual IReadOnlyList GenerateFluentApiCalls( + ICheckConstraint checkConstraint, + IDictionary annotations) + { + var methodCallCodeFragments = new List(); + + methodCallCodeFragments.AddRange(GenerateFluentApiCallsHelper(checkConstraint, annotations, GenerateFluentApi)); + + return methodCallCodeFragments; + } + + /// + public virtual IReadOnlyList GenerateFluentApiCalls( + ITrigger trigger, + IDictionary annotations) + { + var methodCallCodeFragments = new List(); + + methodCallCodeFragments.AddRange(GenerateFluentApiCallsHelper(trigger, annotations, GenerateFluentApi)); + + return methodCallCodeFragments; + } + + /// + public virtual IReadOnlyList GenerateFluentApiCalls( + IRelationalPropertyOverrides overrides, + IDictionary annotations) + { + var methodCallCodeFragments = new List(); + + methodCallCodeFragments.AddRange(GenerateFluentApiCallsHelper(overrides, annotations, GenerateFluentApi)); + + return methodCallCodeFragments; + } + + /// + public virtual IReadOnlyList GenerateFluentApiCalls( + ISequence sequence, + IDictionary annotations) + { + var methodCallCodeFragments = new List(); + + methodCallCodeFragments.AddRange(GenerateFluentApiCallsHelper(sequence, annotations, GenerateFluentApi)); + + return methodCallCodeFragments; + } + /// public virtual IReadOnlyList GenerateDataAnnotationAttributes( IEntityType entityType, @@ -434,6 +519,19 @@ protected virtual bool IsHandledByConvention(IModel model, IAnnotation annotatio protected virtual bool IsHandledByConvention(IEntityType entityType, IAnnotation annotation) => false; + /// + /// Checks if the given is handled by convention when + /// applied to the given . + /// + /// + /// The default implementation always returns . + /// + /// The . + /// The . + /// . + protected virtual bool IsHandledByConvention(IEntityTypeMappingFragment fragment, IAnnotation annotation) + => false; + /// /// Checks if the given is handled by convention when /// applied to the given . @@ -486,6 +584,58 @@ protected virtual bool IsHandledByConvention(IForeignKey foreignKey, IAnnotation protected virtual bool IsHandledByConvention(IIndex index, IAnnotation annotation) => false; + /// + /// Checks if the given is handled by convention when + /// applied to the given . + /// + /// + /// The default implementation always returns . + /// + /// The . + /// The . + /// . + protected virtual bool IsHandledByConvention(ICheckConstraint checkConstraint, IAnnotation annotation) + => false; + + /// + /// Checks if the given is handled by convention when + /// applied to the given . + /// + /// + /// The default implementation always returns . + /// + /// The . + /// The . + /// . + protected virtual bool IsHandledByConvention(ITrigger trigger, IAnnotation annotation) + => false; + + /// + /// Checks if the given is handled by convention when + /// applied to the given . + /// + /// + /// The default implementation always returns . + /// + /// The . + /// The . + /// . + protected virtual bool IsHandledByConvention(IRelationalPropertyOverrides overrides, IAnnotation annotation) + => false; + + /// + /// Checks if the given is handled by convention when + /// applied to the given . + /// + /// + /// The default implementation always returns . + /// + /// The . + /// The . + /// . + protected virtual bool IsHandledByConvention(ISequence sequence, IAnnotation annotation) + => false; + /// /// Returns a fluent API call for the given , or /// if no fluent API call exists for it. @@ -512,6 +662,19 @@ protected virtual bool IsHandledByConvention(IIndex index, IAnnotation annotatio protected virtual MethodCallCodeFragment? GenerateFluentApi(IEntityType entityType, IAnnotation annotation) => null; + /// + /// Returns a fluent API call for the given , or + /// if no fluent API call exists for it. + /// + /// + /// The default implementation always returns . + /// + /// The . + /// The . + /// . + protected virtual MethodCallCodeFragment? GenerateFluentApi(IEntityTypeMappingFragment fragment, IAnnotation annotation) + => null; + /// /// Returns a fluent API call for the given , or /// if no fluent API call exists for it. @@ -590,6 +753,58 @@ protected virtual bool IsHandledByConvention(IIndex index, IAnnotation annotatio protected virtual MethodCallCodeFragment? GenerateFluentApi(IIndex index, IAnnotation annotation) => null; + /// + /// Returns a fluent API call for the given , or + /// if no fluent API call exists for it. + /// + /// + /// The default implementation always returns . + /// + /// The . + /// The . + /// . + protected virtual MethodCallCodeFragment? GenerateFluentApi(ICheckConstraint checkConstraint, IAnnotation annotation) + => null; + + /// + /// Returns a fluent API call for the given , or + /// if no fluent API call exists for it. + /// + /// + /// The default implementation always returns . + /// + /// The . + /// The . + /// . + protected virtual MethodCallCodeFragment? GenerateFluentApi(ITrigger trigger, IAnnotation annotation) + => null; + + /// + /// Returns a fluent API call for the given , or + /// if no fluent API call exists for it. + /// + /// + /// The default implementation always returns . + /// + /// The . + /// The . + /// . + protected virtual MethodCallCodeFragment? GenerateFluentApi(IRelationalPropertyOverrides overrides, IAnnotation annotation) + => null; + + /// + /// Returns a fluent API call for the given , or + /// if no fluent API call exists for it. + /// + /// + /// The default implementation always returns . + /// + /// The . + /// The . + /// . + protected virtual MethodCallCodeFragment? GenerateFluentApi(ISequence sequence, IAnnotation annotation) + => null; + /// /// Returns a data annotation attribute code fragment for the given , /// or if no data annotation exists for it. diff --git a/src/EFCore.Relational/Design/IAnnotationCodeGenerator.cs b/src/EFCore.Relational/Design/IAnnotationCodeGenerator.cs index 68b9fd17393..87fdb3aa92f 100644 --- a/src/EFCore.Relational/Design/IAnnotationCodeGenerator.cs +++ b/src/EFCore.Relational/Design/IAnnotationCodeGenerator.cs @@ -48,6 +48,16 @@ void RemoveAnnotationsHandledByConventions(IEntityType entity, IDictionary + /// Removes annotation whose configuration is already applied by convention, and do not need to be + /// specified explicitly. + /// + /// The entity mapping fragment to which the annotations are applied. + /// The set of annotations from which to remove the conventional ones. + void RemoveAnnotationsHandledByConventions(IEntityTypeMappingFragment fragment, IDictionary annotations) + { + } + /// /// Removes annotation whose configuration is already applied by convention, and do not need to be /// specified explicitly. @@ -118,6 +128,16 @@ void RemoveAnnotationsHandledByConventions(IRelationalPropertyOverrides override { } + /// + /// Removes annotation whose configuration is already applied by convention, and do not need to be + /// specified explicitly. + /// + /// The sequence to which the annotations are applied. + /// The set of annotations from which to remove the conventional ones. + void RemoveAnnotationsHandledByConventions(ISequence sequence, IDictionary annotations) + { + } + /// /// For the given annotations which have corresponding fluent API calls, returns those fluent API calls /// and removes the annotations. @@ -136,6 +156,10 @@ void RemoveAnnotationsHandledByConventions(IAnnotatable annotatable, IDictionary RemoveAnnotationsHandledByConventions(entityType, annotations); return; + case IEntityTypeMappingFragment fragment: + RemoveAnnotationsHandledByConventions(fragment, annotations); + return; + case IProperty property: RemoveAnnotationsHandledByConventions(property, annotations); return; @@ -172,6 +196,10 @@ void RemoveAnnotationsHandledByConventions(IAnnotatable annotatable, IDictionary RemoveAnnotationsHandledByConventions(overrides, annotations); return; + case ISequence sequence: + RemoveAnnotationsHandledByConventions(sequence, annotations); + return; + default: throw new ArgumentException(RelationalStrings.UnhandledAnnotatableType(annotatable.GetType())); } @@ -199,6 +227,17 @@ IReadOnlyList GenerateFluentApiCalls( IDictionary annotations) => Array.Empty(); + /// + /// For the given annotations which have corresponding fluent API calls, returns those fluent API calls + /// and removes the annotations. + /// + /// The entity mapping fragment to which the annotations are applied. + /// The set of annotations from which to generate fluent API calls. + IReadOnlyList GenerateFluentApiCalls( + IEntityTypeMappingFragment fragment, + IDictionary annotations) + => Array.Empty(); + /// /// For the given annotations which have corresponding fluent API calls, returns those fluent API calls /// and removes the annotations. @@ -298,6 +337,17 @@ IReadOnlyList GenerateFluentApiCalls( IDictionary annotations) => Array.Empty(); + /// + /// For the given annotations which have corresponding fluent API calls, returns those fluent API calls + /// and removes the annotations. + /// + /// The sequence to which the annotations are applied. + /// The set of annotations from which to generate fluent API calls. + IReadOnlyList GenerateFluentApiCalls( + ISequence sequence, + IDictionary annotations) + => Array.Empty(); + /// /// For the given annotations which have corresponding fluent API calls, returns those fluent API calls /// and removes the annotations. @@ -309,7 +359,9 @@ IReadOnlyList GenerateFluentApiCalls(IAnnotatable annota { IModel model => GenerateFluentApiCalls(model, annotations), IEntityType entityType => GenerateFluentApiCalls(entityType, annotations), + IEntityTypeMappingFragment fragment => GenerateFluentApiCalls(fragment, annotations), IProperty property => GenerateFluentApiCalls(property, annotations), + IRelationalPropertyOverrides overrides => GenerateFluentApiCalls(overrides, annotations), IKey key => GenerateFluentApiCalls(key, annotations), IForeignKey foreignKey => GenerateFluentApiCalls(foreignKey, annotations), INavigation navigation => GenerateFluentApiCalls(navigation, annotations), @@ -317,7 +369,7 @@ IReadOnlyList GenerateFluentApiCalls(IAnnotatable annota IIndex index => GenerateFluentApiCalls(index, annotations), ICheckConstraint checkConstraint => GenerateFluentApiCalls(checkConstraint, annotations), ITrigger trigger => GenerateFluentApiCalls(trigger, annotations), - IRelationalPropertyOverrides overrides => GenerateFluentApiCalls(overrides, annotations), + ISequence sequence => GenerateFluentApiCalls(sequence, annotations), _ => throw new ArgumentException(RelationalStrings.UnhandledAnnotatableType(annotatable.GetType())) }; diff --git a/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs b/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs index 99233db8e21..867ba686d71 100644 --- a/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs +++ b/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs @@ -385,7 +385,7 @@ private void Create( CreateAnnotations( fragment, - GenerateOverrides, + Generate, parameters with { TargetName = overrideVariable }); mainBuilder.Append(fragmentsVariable).Append(".Add("); @@ -396,6 +396,14 @@ private void Create( .Append(overrideVariable).AppendLine(");"); } + /// + /// Generates code to create the given annotations. + /// + /// The fragment to which the annotations are applied. + /// Additional parameters used during code generation. + public virtual void Generate(IEntityTypeMappingFragment fragment, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) + => GenerateSimpleAnnotations(parameters); + private void Create(ITrigger trigger, string triggersVariable, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) { var code = Dependencies.CSharpHelper; @@ -500,7 +508,7 @@ private void Create( CreateAnnotations( overrides, - GenerateOverrides, + Generate, parameters with { TargetName = overrideVariable }); mainBuilder.Append(overridesVariable).Append(".Add("); @@ -516,7 +524,7 @@ private void Create( /// /// The property overrides to which the annotations are applied. /// Additional parameters used during code generation. - public virtual void GenerateOverrides(IAnnotatable overrides, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) + public virtual void Generate(IRelationalPropertyOverrides overrides, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) => GenerateSimpleAnnotations(parameters); /// diff --git a/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs b/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs index bef4f64ea75..c17b3127d68 100644 --- a/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs @@ -1075,7 +1075,7 @@ public static IConventionEntityTypeMappingFragment GetOrCreateMappingFragment( public static IMutableEntityTypeMappingFragment? RemoveMappingFragment( this IMutableEntityType entityType, in StoreObjectIdentifier storeObject) - => EntityTypeMappingFragment.Remove(entityType, storeObject, ConfigurationSource.Explicit); + => EntityTypeMappingFragment.Remove(entityType, storeObject); /// /// @@ -1088,17 +1088,14 @@ public static IConventionEntityTypeMappingFragment GetOrCreateMappingFragment( /// /// The entity type. /// The identifier of a table-like store object. - /// Indicates whether the configuration was specified using a data annotation. /// /// The removed or /// if no overrides for the given store object were found or the existing overrides were configured from a higher source. /// public static IConventionEntityTypeMappingFragment? RemoveMappingFragment( this IConventionEntityType entityType, - in StoreObjectIdentifier storeObject, - bool fromDataAnnotation = false) - => EntityTypeMappingFragment.Remove((IMutableEntityType)entityType, storeObject, - fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + in StoreObjectIdentifier storeObject) + => EntityTypeMappingFragment.Remove((IMutableEntityType)entityType, storeObject); /// /// Gets the foreign keys for the given entity type that point to other entity types diff --git a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs index 7dff017dcce..e14f264d4ad 100644 --- a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs @@ -51,7 +51,7 @@ public static string GetColumnName(this IReadOnlyProperty property) /// The name of the column to which the property is mapped. public static string? GetColumnName(this IReadOnlyProperty property, in StoreObjectIdentifier storeObject) { - var overrides = RelationalPropertyOverrides.Find(property, storeObject); + var overrides = property.FindOverrides(storeObject); if (overrides?.ColumnNameOverridden == true) { return overrides.ColumnName; @@ -294,8 +294,7 @@ public static void SetColumnName( public static ConfigurationSource? GetColumnNameConfigurationSource( this IConventionProperty property, in StoreObjectIdentifier storeObject) - => ((IConventionRelationalPropertyOverrides?)RelationalPropertyOverrides.Find(property, storeObject)) - ?.GetColumnNameConfigurationSource(); + => property.FindOverrides(storeObject)?.GetColumnNameConfigurationSource(); /// /// Returns the order of the column this property is mapped to. @@ -1440,6 +1439,36 @@ public static RelationalTypeMapping GetRelationalTypeMapping(this IReadOnlyPrope public static IEnumerable GetOverrides(this IReadOnlyProperty property) => RelationalPropertyOverrides.Get(property) ?? Enumerable.Empty(); + /// + /// + /// Returns all the property facet overrides. + /// + /// + /// This method is typically used by database providers (and other extensions). It is generally + /// not used in application code. + /// + /// + /// The property. + /// The property facet overrides. + public static IEnumerable GetOverrides(this IMutableProperty property) + => ((IEnumerable?)RelationalPropertyOverrides.Get(property)) + ?? Enumerable.Empty(); + + /// + /// + /// Returns all the property facet overrides. + /// + /// + /// This method is typically used by database providers (and other extensions). It is generally + /// not used in application code. + /// + /// + /// The property. + /// The property facet overrides. + public static IEnumerable GetOverrides(this IConventionProperty property) + => ((IEnumerable?)RelationalPropertyOverrides.Get(property)) + ?? Enumerable.Empty(); + /// /// /// Returns all the property facet overrides. @@ -1452,7 +1481,7 @@ public static IEnumerable GetOverrides(thi /// The property. /// The property facet overrides. public static IEnumerable GetOverrides(this IProperty property) - => RelationalPropertyOverrides.Get(property)?.Cast() + => ((IEnumerable?)RelationalPropertyOverrides.Get(property)) ?? Enumerable.Empty(); /// @@ -1472,6 +1501,40 @@ public static IEnumerable GetOverrides(this IPrope in StoreObjectIdentifier storeObject) => RelationalPropertyOverrides.Find(property, storeObject); + /// + /// + /// Returns the property facet overrides for a particular table-like store object. + /// + /// + /// This method is typically used by database providers (and other extensions). It is generally + /// not used in application code. + /// + /// + /// The property. + /// The identifier of the table-like store object containing the column. + /// An object that stores property facet overrides. + public static IMutableRelationalPropertyOverrides? FindOverrides( + this IMutableProperty property, + in StoreObjectIdentifier storeObject) + => (IMutableRelationalPropertyOverrides?)RelationalPropertyOverrides.Find(property, storeObject); + + /// + /// + /// Returns the property facet overrides for a particular table-like store object. + /// + /// + /// This method is typically used by database providers (and other extensions). It is generally + /// not used in application code. + /// + /// + /// The property. + /// The identifier of the table-like store object containing the column. + /// An object that stores property facet overrides. + public static IConventionRelationalPropertyOverrides? FindOverrides( + this IConventionProperty property, + in StoreObjectIdentifier storeObject) + => (IConventionRelationalPropertyOverrides?)RelationalPropertyOverrides.Find(property, storeObject); + /// /// /// Returns the property facet overrides for a particular table-like store object. @@ -1544,7 +1607,7 @@ public static IConventionRelationalPropertyOverrides GetOrCreateOverrides( public static IMutableRelationalPropertyOverrides? RemoveOverrides( this IMutableProperty property, in StoreObjectIdentifier storeObject) - => RelationalPropertyOverrides.Remove(property, storeObject, ConfigurationSource.Explicit); + => RelationalPropertyOverrides.Remove(property, storeObject); /// /// @@ -1557,17 +1620,14 @@ public static IConventionRelationalPropertyOverrides GetOrCreateOverrides( /// /// The property. /// The identifier of a table-like store object. - /// Indicates whether the configuration was specified using a data annotation. /// /// The removed or /// if no overrides for the given store object were found or the existing overrides were configured from a higher source. /// public static IConventionRelationalPropertyOverrides? RemoveOverrides( this IConventionProperty property, - in StoreObjectIdentifier storeObject, - bool fromDataAnnotation = false) - => RelationalPropertyOverrides.Remove((IMutableProperty)property, storeObject, - fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + in StoreObjectIdentifier storeObject) + => RelationalPropertyOverrides.Remove((IMutableProperty)property, storeObject); /// /// diff --git a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs index 722d9e77ebb..d8bb74f0d5a 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs @@ -1864,8 +1864,10 @@ protected virtual void ValidateTriggers( RelationalStrings.TriggerOnUnmappedEntityType(trigger.ModelName, entityType.DisplayName())); } - if ((trigger.TableName != tableName) - || (trigger.TableSchema is not null && trigger.TableSchema != tableSchema)) + if ((trigger.TableName != tableName + || trigger.TableSchema != tableSchema) + && entityType.GetMappingFragments(StoreObjectType.Table) + .All(f => trigger.TableName != f.StoreObject.Name || trigger.TableSchema != f.StoreObject.Schema)) { throw new InvalidOperationException( RelationalStrings.TriggerWithMismatchedTable( diff --git a/src/EFCore.Relational/Metadata/Builders/ColumnBuilder.cs b/src/EFCore.Relational/Metadata/Builders/ColumnBuilder.cs index 484e6fd5e7f..a8979453ae5 100644 --- a/src/EFCore.Relational/Metadata/Builders/ColumnBuilder.cs +++ b/src/EFCore.Relational/Metadata/Builders/ColumnBuilder.cs @@ -62,6 +62,23 @@ public virtual ColumnBuilder HasColumnName(string? name) return this; } + /// + /// Adds or updates an annotation on the property for a specific table. + /// If an annotation with the key specified in + /// already exists, its value will be updated. + /// + /// The key of the annotation to be added or updated. + /// The value to be stored in the annotation. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ColumnBuilder HasAnnotation(string annotation, object? value) + { + Check.NotEmpty(annotation, nameof(annotation)); + + InternalOverrides.Builder.HasAnnotation(annotation, value, ConfigurationSource.Explicit); + + return this; + } + PropertyBuilder IInfrastructure.Instance => PropertyBuilder; #region Hidden System.Object members diff --git a/src/EFCore.Relational/Metadata/Builders/ColumnBuilder`.cs b/src/EFCore.Relational/Metadata/Builders/ColumnBuilder`.cs index 51e666742a8..d51269971e3 100644 --- a/src/EFCore.Relational/Metadata/Builders/ColumnBuilder`.cs +++ b/src/EFCore.Relational/Metadata/Builders/ColumnBuilder`.cs @@ -34,6 +34,17 @@ private PropertyBuilder PropertyBuilder /// The same builder instance so that multiple calls can be chained. public new virtual ColumnBuilder HasColumnName(string? name) => (ColumnBuilder)base.HasColumnName(name); + + /// + /// Adds or updates an annotation on the property for a specific table. + /// If an annotation with the key specified in + /// already exists, its value will be updated. + /// + /// The key of the annotation to be added or updated. + /// The value to be stored in the annotation. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual ColumnBuilder HasAnnotation(string annotation, object? value) + => (ColumnBuilder)base.HasAnnotation(annotation, value); PropertyBuilder IInfrastructure>.Instance => PropertyBuilder; } diff --git a/src/EFCore.Relational/Metadata/Builders/IConventionEntityTypeMappingFragmentBuilder.cs b/src/EFCore.Relational/Metadata/Builders/IConventionEntityTypeMappingFragmentBuilder.cs new file mode 100644 index 00000000000..40e7453da64 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Builders/IConventionEntityTypeMappingFragmentBuilder.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Metadata.Builders; + +/// +/// Provides a simple API for configuring a . +/// +/// +/// See Model building conventions for more information and examples. +/// +public interface IConventionEntityTypeMappingFragmentBuilder : IConventionAnnotatableBuilder +{ + /// + /// The fragment being configured. + /// + new IConventionEntityTypeMappingFragment Metadata { get; } +} diff --git a/src/EFCore.Relational/Metadata/Builders/IConventionRelationalPropertyOverridesBuilder.cs b/src/EFCore.Relational/Metadata/Builders/IConventionRelationalPropertyOverridesBuilder.cs new file mode 100644 index 00000000000..8f445de4d1f --- /dev/null +++ b/src/EFCore.Relational/Metadata/Builders/IConventionRelationalPropertyOverridesBuilder.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Metadata.Builders; + +/// +/// Provides a simple API for configuring a . +/// +/// +/// See Model building conventions for more information and examples. +/// +public interface IConventionRelationalPropertyOverridesBuilder : IConventionAnnotatableBuilder +{ + /// + /// The overrides being configured. + /// + new IConventionRelationalPropertyOverrides Metadata { get; } +} diff --git a/src/EFCore.Relational/Metadata/Builders/OwnedNavigationSplitTableBuilder.cs b/src/EFCore.Relational/Metadata/Builders/OwnedNavigationSplitTableBuilder.cs index 7e7f7596b1d..04cbba7bcd4 100644 --- a/src/EFCore.Relational/Metadata/Builders/OwnedNavigationSplitTableBuilder.cs +++ b/src/EFCore.Relational/Metadata/Builders/OwnedNavigationSplitTableBuilder.cs @@ -24,7 +24,7 @@ public OwnedNavigationSplitTableBuilder(in StoreObjectIdentifier storeObject, Ow Check.DebugAssert(storeObject.StoreObjectType == StoreObjectType.Table, "StoreObjectType should be Table, not " + storeObject.StoreObjectType); - MappingFragment = EntityTypeMappingFragment.GetOrCreate( + InternalMappingFragment = EntityTypeMappingFragment.GetOrCreate( ownedNavigationBuilder.OwnedEntityType, storeObject, ConfigurationSource.Explicit); OwnedNavigationBuilder = ownedNavigationBuilder; } @@ -39,10 +39,19 @@ public OwnedNavigationSplitTableBuilder(in StoreObjectIdentifier storeObject, Ow /// public virtual string? Schema => MappingFragment.StoreObject.Schema; + /// + /// 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. + /// + [EntityFrameworkInternal] + protected virtual EntityTypeMappingFragment InternalMappingFragment { get; } + /// /// The mapping fragment being configured. /// - public virtual IMutableEntityTypeMappingFragment MappingFragment { get; } + public virtual IMutableEntityTypeMappingFragment MappingFragment => InternalMappingFragment; private OwnedNavigationBuilder OwnedNavigationBuilder { get; } @@ -96,6 +105,22 @@ public virtual ColumnBuilder Property(string propertyName) public virtual ColumnBuilder Property(string propertyName) => new(MappingFragment.StoreObject, OwnedNavigationBuilder.Property(propertyName)); + /// + /// Adds or updates an annotation on the table. If an annotation with the key specified in + /// already exists, its value will be updated. + /// + /// The key of the annotation to be added or updated. + /// The value to be stored in the annotation. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual OwnedNavigationSplitTableBuilder HasAnnotation(string annotation, object? value) + { + Check.NotEmpty(annotation, nameof(annotation)); + + InternalMappingFragment.Builder.HasAnnotation(annotation, value, ConfigurationSource.Explicit); + + return this; + } + OwnedNavigationBuilder IInfrastructure.Instance => OwnedNavigationBuilder; #region Hidden System.Object members diff --git a/src/EFCore.Relational/Metadata/Builders/OwnedNavigationSplitTableBuilder``.cs b/src/EFCore.Relational/Metadata/Builders/OwnedNavigationSplitTableBuilder``.cs index ee02ff954c5..a747d19bc10 100644 --- a/src/EFCore.Relational/Metadata/Builders/OwnedNavigationSplitTableBuilder``.cs +++ b/src/EFCore.Relational/Metadata/Builders/OwnedNavigationSplitTableBuilder``.cs @@ -55,6 +55,17 @@ private OwnedNavigationBuilder OwnedNavigationBu public virtual ColumnBuilder Property(Expression> propertyExpression) => new(MappingFragment.StoreObject, OwnedNavigationBuilder.Property(propertyExpression)); + /// + /// Adds or updates an annotation on the table. If an annotation with the key specified in + /// already exists, its value will be updated. + /// + /// The key of the annotation to be added or updated. + /// The value to be stored in the annotation. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual OwnedNavigationSplitTableBuilder HasAnnotation( + string annotation, object? value) + => (OwnedNavigationSplitTableBuilder)base.HasAnnotation(annotation, value); + OwnedNavigationBuilder IInfrastructure>.Instance => OwnedNavigationBuilder; } diff --git a/src/EFCore.Relational/Metadata/Builders/SequenceBuilder.cs b/src/EFCore.Relational/Metadata/Builders/SequenceBuilder.cs index 6ad456007ac..2a0d2e0646c 100644 --- a/src/EFCore.Relational/Metadata/Builders/SequenceBuilder.cs +++ b/src/EFCore.Relational/Metadata/Builders/SequenceBuilder.cs @@ -114,6 +114,22 @@ public virtual SequenceBuilder IsCyclic(bool cyclic = true) return this; } + /// + /// Adds or updates an annotation on the sequence. If an annotation with the key specified in + /// already exists, its value will be updated. + /// + /// The key of the annotation to be added or updated. + /// The value to be stored in the annotation. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual SequenceBuilder HasAnnotation(string annotation, object? value) + { + Check.NotEmpty(annotation, nameof(annotation)); + + Builder.HasAnnotation(annotation, value, ConfigurationSource.Explicit); + + return this; + } + #region Hidden System.Object members /// diff --git a/src/EFCore.Relational/Metadata/Builders/SplitTableBuilder.cs b/src/EFCore.Relational/Metadata/Builders/SplitTableBuilder.cs index b9f1cf14db7..b5dc8eb4d25 100644 --- a/src/EFCore.Relational/Metadata/Builders/SplitTableBuilder.cs +++ b/src/EFCore.Relational/Metadata/Builders/SplitTableBuilder.cs @@ -24,7 +24,7 @@ public SplitTableBuilder(in StoreObjectIdentifier storeObject, EntityTypeBuilder Check.DebugAssert(storeObject.StoreObjectType == StoreObjectType.Table, "StoreObjectType should be Table, not " + storeObject.StoreObjectType); - MappingFragment = EntityTypeMappingFragment.GetOrCreate( + InternalMappingFragment = EntityTypeMappingFragment.GetOrCreate( entityTypeBuilder.Metadata, storeObject, ConfigurationSource.Explicit); EntityTypeBuilder = entityTypeBuilder; } @@ -38,11 +38,20 @@ public SplitTableBuilder(in StoreObjectIdentifier storeObject, EntityTypeBuilder /// The specified table schema. /// public virtual string? Schema => MappingFragment.StoreObject.Schema; - + + /// + /// 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. + /// + [EntityFrameworkInternal] + protected virtual EntityTypeMappingFragment InternalMappingFragment { get; } + /// /// The mapping fragment being configured. /// - public virtual IMutableEntityTypeMappingFragment MappingFragment { get; } + public virtual IMutableEntityTypeMappingFragment MappingFragment => InternalMappingFragment; private EntityTypeBuilder EntityTypeBuilder { get; } @@ -96,6 +105,22 @@ public virtual ColumnBuilder Property(string propertyName) public virtual ColumnBuilder Property(string propertyName) => new(MappingFragment.StoreObject, EntityTypeBuilder.Property(propertyName)); + /// + /// Adds or updates an annotation on the table. If an annotation with the key specified in + /// already exists, its value will be updated. + /// + /// The key of the annotation to be added or updated. + /// The value to be stored in the annotation. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual SplitTableBuilder HasAnnotation(string annotation, object? value) + { + Check.NotEmpty(annotation, nameof(annotation)); + + InternalMappingFragment.Builder.HasAnnotation(annotation, value, ConfigurationSource.Explicit); + + return this; + } + EntityTypeBuilder IInfrastructure.Instance => EntityTypeBuilder; #region Hidden System.Object members diff --git a/src/EFCore.Relational/Metadata/Builders/SplitTableBuilder`.cs b/src/EFCore.Relational/Metadata/Builders/SplitTableBuilder`.cs index 6d02e378b27..6a244425e45 100644 --- a/src/EFCore.Relational/Metadata/Builders/SplitTableBuilder`.cs +++ b/src/EFCore.Relational/Metadata/Builders/SplitTableBuilder`.cs @@ -47,5 +47,15 @@ private EntityTypeBuilder EntityTypeBuilder public virtual ColumnBuilder Property(Expression> propertyExpression) => new(MappingFragment.StoreObject, EntityTypeBuilder.Property(propertyExpression)); + /// + /// Adds or updates an annotation on the table. If an annotation with the key specified in + /// already exists, its value will be updated. + /// + /// The key of the annotation to be added or updated. + /// The value to be stored in the annotation. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual SplitTableBuilder HasAnnotation(string annotation, object? value) + => (SplitTableBuilder)base.HasAnnotation(annotation, value); + EntityTypeBuilder IInfrastructure>.Instance => EntityTypeBuilder; } diff --git a/src/EFCore.Relational/Metadata/Conventions/CheckConstraintConvention.cs b/src/EFCore.Relational/Metadata/Conventions/CheckConstraintConvention.cs index 6c05c901dcc..f99b58c2609 100644 --- a/src/EFCore.Relational/Metadata/Conventions/CheckConstraintConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/CheckConstraintConvention.cs @@ -144,7 +144,7 @@ public virtual void ProcessEntityTypeBaseTypeChanged( foreach (var checkConstraintToBeDetached in checkConstraintsToBeDetached) { var baseCheckConstraint = baseCheckConstraints[checkConstraintToBeDetached.ModelName]; - CheckConstraint.Attach(checkConstraintToBeDetached, baseCheckConstraint); + CheckConstraint.MergeInto(checkConstraintToBeDetached, baseCheckConstraint); checkConstraintToBeDetached.EntityType.RemoveCheckConstraint(checkConstraintToBeDetached.ModelName); } diff --git a/src/EFCore.Relational/Metadata/Conventions/EntitySplittingConvention.cs b/src/EFCore.Relational/Metadata/Conventions/EntitySplittingConvention.cs index ee8e4fc30d6..78ac818063b 100644 --- a/src/EFCore.Relational/Metadata/Conventions/EntitySplittingConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/EntitySplittingConvention.cs @@ -1,16 +1,18 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.EntityFrameworkCore.Metadata.Internal; + namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; /// -/// A convention that creates linking relationships for entity splitting. +/// A convention that creates linking relationships for entity splitting and manages the mapping fragments. /// /// /// See Model building conventions and /// Entity type hierarchy mapping for more information and examples. /// -public class EntitySplittingConvention : IModelFinalizingConvention +public class EntitySplittingConvention : IModelFinalizingConvention, IEntityTypeAddedConvention { /// /// Creates a new instance of . @@ -35,6 +37,45 @@ public EntitySplittingConvention( /// protected virtual RelationalConventionSetBuilderDependencies RelationalDependencies { get; } + /// + public virtual void ProcessEntityTypeAdded( + IConventionEntityTypeBuilder entityTypeBuilder, + IConventionContext context) + { + var entityType = entityTypeBuilder.Metadata; + if (!entityType.HasSharedClrType) + { + return; + } + + List? fragmentsToReattach = null; + foreach (var fragment in entityType.GetMappingFragments()) + { + if (fragment.EntityType == entityType) + { + continue; + } + + fragmentsToReattach ??= new(); + + fragmentsToReattach.Add(fragment); + } + + if (fragmentsToReattach == null) + { + return; + } + + foreach (var fragment in fragmentsToReattach) + { + var removedFragment = entityType.RemoveMappingFragment(fragment.StoreObject); + if (removedFragment != null) + { + EntityTypeMappingFragment.Attach(entityType, removedFragment); + } + } + } + /// public virtual void ProcessModelFinalizing( IConventionModelBuilder modelBuilder, diff --git a/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs b/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs index a14ff223ec2..33d2dfcb622 100644 --- a/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs +++ b/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs @@ -64,13 +64,15 @@ public override ConventionSet CreateConventionSet() conventionSet.PropertyAddedConventions.Add(relationalColumnAttributeConvention); conventionSet.PropertyAddedConventions.Add(relationalCommentAttributeConvention); + var tableNameFromDbSetConvention = new TableNameFromDbSetConvention(Dependencies, RelationalDependencies); + var entitySplittingConvention = new EntitySplittingConvention(Dependencies, RelationalDependencies); var checkConstraintConvention = new CheckConstraintConvention(Dependencies, RelationalDependencies); var triggerConvention = new TriggerConvention(Dependencies, RelationalDependencies); - var tableNameFromDbSetConvention = new TableNameFromDbSetConvention(Dependencies, RelationalDependencies); conventionSet.EntityTypeAddedConventions.Add(new RelationalTableAttributeConvention(Dependencies, RelationalDependencies)); conventionSet.EntityTypeAddedConventions.Add( new RelationalTableCommentAttributeConvention(Dependencies, RelationalDependencies)); conventionSet.EntityTypeAddedConventions.Add(tableNameFromDbSetConvention); + conventionSet.EntityTypeAddedConventions.Add(entitySplittingConvention); conventionSet.EntityTypeAddedConventions.Add(checkConstraintConvention); conventionSet.EntityTypeAddedConventions.Add(triggerConvention); @@ -93,6 +95,8 @@ public override ConventionSet CreateConventionSet() ReplaceConvention(conventionSet.ForeignKeyRemovedConventions, valueGenerationConvention); + conventionSet.PropertyAddedConventions.Add(new PropertyOverridesConvention(Dependencies, RelationalDependencies)); + conventionSet.PropertyFieldChangedConventions.Add(relationalColumnAttributeConvention); conventionSet.PropertyFieldChangedConventions.Add(relationalCommentAttributeConvention); @@ -112,7 +116,7 @@ public override ConventionSet CreateConventionSet() conventionSet.ModelFinalizingConventions.Add(dbFunctionAttributeConvention); conventionSet.ModelFinalizingConventions.Add(tableNameFromDbSetConvention); conventionSet.ModelFinalizingConventions.Add(storeGenerationConvention); - conventionSet.ModelFinalizingConventions.Add(new EntitySplittingConvention(Dependencies, RelationalDependencies)); + conventionSet.ModelFinalizingConventions.Add(entitySplittingConvention); conventionSet.ModelFinalizingConventions.Add(new EntityTypeHierarchyMappingConvention(Dependencies, RelationalDependencies)); conventionSet.ModelFinalizingConventions.Add(new SequenceUniquificationConvention(Dependencies, RelationalDependencies)); conventionSet.ModelFinalizingConventions.Add(new SharedTableConvention(Dependencies, RelationalDependencies)); diff --git a/src/EFCore.Relational/Metadata/Conventions/PropertyOverridesConvention.cs b/src/EFCore.Relational/Metadata/Conventions/PropertyOverridesConvention.cs new file mode 100644 index 00000000000..706897b4ce0 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Conventions/PropertyOverridesConvention.cs @@ -0,0 +1,78 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Metadata.Internal; + +namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; + +/// +/// A convention that ensures that the declaring property is current for the property overrides. +/// +/// +/// See Model building conventions and +/// Entity type hierarchy mapping for more information and examples. +/// +public class PropertyOverridesConvention : IPropertyAddedConvention +{ + /// + /// Creates a new instance of . + /// + /// Parameter object containing dependencies for this convention. + /// Parameter object containing relational dependencies for this convention. + public PropertyOverridesConvention( + ProviderConventionSetBuilderDependencies dependencies, + RelationalConventionSetBuilderDependencies relationalDependencies) + { + Dependencies = dependencies; + RelationalDependencies = relationalDependencies; + } + + /// + /// Dependencies for this service. + /// + protected virtual ProviderConventionSetBuilderDependencies Dependencies { get; } + + /// + /// Relational provider-specific dependencies for this service. + /// + protected virtual RelationalConventionSetBuilderDependencies RelationalDependencies { get; } + + /// + public virtual void ProcessPropertyAdded( + IConventionPropertyBuilder propertyBuilder, + IConventionContext context) + { + var property = propertyBuilder.Metadata; + if (!property.DeclaringEntityType.HasSharedClrType) + { + return; + } + + List? overridesToReattach = null; + foreach (var overrides in property.GetOverrides()) + { + if (overrides.Property == property) + { + continue; + } + + overridesToReattach ??= new(); + + overridesToReattach.Add(overrides); + } + + if (overridesToReattach == null) + { + return; + } + + foreach (var overrides in overridesToReattach) + { + var removedOverrides = property.RemoveOverrides(overrides.StoreObject); + if (removedOverrides != null) + { + RelationalPropertyOverrides.Attach(property, removedOverrides); + } + } + } +} diff --git a/src/EFCore.Relational/Metadata/Conventions/TriggerConvention.cs b/src/EFCore.Relational/Metadata/Conventions/TriggerConvention.cs index 01cd41333e7..fd2ed5f9371 100644 --- a/src/EFCore.Relational/Metadata/Conventions/TriggerConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/TriggerConvention.cs @@ -49,14 +49,14 @@ public virtual void ProcessEntityTypeAdded( } List? triggersToReattach = null; - foreach (var trigger in entityType.GetTriggers()) + foreach (var trigger in entityType.GetDeclaredTriggers()) { if (trigger.EntityType == entityType) { continue; } - triggersToReattach ??= new List(); + triggersToReattach ??= new(); triggersToReattach.Add(trigger); } @@ -71,7 +71,7 @@ public virtual void ProcessEntityTypeAdded( var removedTrigger = entityType.RemoveTrigger(trigger.ModelName); if (removedTrigger != null) { - Trigger.MergeInto(entityType, removedTrigger); + Trigger.Attach(entityType, removedTrigger); } } } diff --git a/src/EFCore.Relational/Metadata/ICheckConstraint.cs b/src/EFCore.Relational/Metadata/ICheckConstraint.cs index d353cfe9c88..3814c656be2 100644 --- a/src/EFCore.Relational/Metadata/ICheckConstraint.cs +++ b/src/EFCore.Relational/Metadata/ICheckConstraint.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Text; - namespace Microsoft.EntityFrameworkCore.Metadata; /// @@ -17,41 +15,4 @@ public interface ICheckConstraint : IReadOnlyCheckConstraint, IAnnotatable /// Gets the entity type on which this check constraint is defined. /// new IEntityType EntityType { get; } - - /// - /// - /// Creates a human-readable representation of the given metadata. - /// - /// - /// Warning: Do not rely on the format of the returned string. - /// It is designed for debugging only and may change arbitrarily between releases. - /// - /// - /// Options for generating the string. - /// The number of indent spaces to use before each new line. - /// A human-readable representation. - string ToDebugString(MetadataDebugStringOptions options = MetadataDebugStringOptions.ShortDefault, int indent = 0) - { - var builder = new StringBuilder(); - var indentString = new string(' ', indent); - - builder - .Append(indentString) - .Append("Check: "); - - builder.Append(ModelName) - .Append(" \"") - .Append(Sql) - .Append('"'); - - if ((options & MetadataDebugStringOptions.SingleLine) == 0) - { - if ((options & MetadataDebugStringOptions.IncludeAnnotations) != 0) - { - builder.Append(AnnotationsToDebugString(indent: indent + 2)); - } - } - - return builder.ToString(); - } } diff --git a/src/EFCore.Relational/Metadata/IConventionEntityTypeMappingFragment.cs b/src/EFCore.Relational/Metadata/IConventionEntityTypeMappingFragment.cs index fb7553795f4..ff3f453c233 100644 --- a/src/EFCore.Relational/Metadata/IConventionEntityTypeMappingFragment.cs +++ b/src/EFCore.Relational/Metadata/IConventionEntityTypeMappingFragment.cs @@ -16,6 +16,12 @@ public interface IConventionEntityTypeMappingFragment : IReadOnlyEntityTypeMappi /// new IConventionEntityType EntityType { get; } + /// + /// Gets the builder that can be used to configure this fragment. + /// + /// If the fragment has been removed from the model. + new IConventionEntityTypeMappingFragmentBuilder Builder { get; } + /// /// Returns the configuration source for this fragment. /// diff --git a/src/EFCore.Relational/Metadata/IConventionRelationalPropertyOverrides.cs b/src/EFCore.Relational/Metadata/IConventionRelationalPropertyOverrides.cs index 4122aa05172..fff6a25c68a 100644 --- a/src/EFCore.Relational/Metadata/IConventionRelationalPropertyOverrides.cs +++ b/src/EFCore.Relational/Metadata/IConventionRelationalPropertyOverrides.cs @@ -16,6 +16,12 @@ public interface IConventionRelationalPropertyOverrides : IReadOnlyRelationalPro /// new IConventionProperty Property { get; } + /// + /// Gets the builder that can be used to configure this function. + /// + /// If the function has been removed from the model. + new IConventionRelationalPropertyOverridesBuilder Builder { get; } + /// /// Returns the configuration source for these overrides. /// diff --git a/src/EFCore.Relational/Metadata/IReadOnlyCheckConstraint.cs b/src/EFCore.Relational/Metadata/IReadOnlyCheckConstraint.cs index a9f2f811206..02036b2fe63 100644 --- a/src/EFCore.Relational/Metadata/IReadOnlyCheckConstraint.cs +++ b/src/EFCore.Relational/Metadata/IReadOnlyCheckConstraint.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Text; + namespace Microsoft.EntityFrameworkCore.Metadata; /// @@ -57,4 +59,47 @@ public interface IReadOnlyCheckConstraint : IReadOnlyAnnotatable /// Gets the constraint sql used in a check constraint in the database. /// string Sql { get; } + + /// + /// + /// Creates a human-readable representation of the given metadata. + /// + /// + /// Warning: Do not rely on the format of the returned string. + /// It is designed for debugging only and may change arbitrarily between releases. + /// + /// + /// Options for generating the string. + /// The number of indent spaces to use before each new line. + /// A human-readable representation. + string ToDebugString(MetadataDebugStringOptions options = MetadataDebugStringOptions.ShortDefault, int indent = 0) + { + var builder = new StringBuilder(); + var indentString = new string(' ', indent); + + builder + .Append(indentString) + .Append("Check: "); + + builder.Append(ModelName); + if (Name != ModelName) + { + builder.Append('*'); + } + + builder + .Append(" \"") + .Append(Sql) + .Append('"'); + + if ((options & MetadataDebugStringOptions.SingleLine) == 0) + { + if ((options & MetadataDebugStringOptions.IncludeAnnotations) != 0) + { + builder.Append(AnnotationsToDebugString(indent: indent + 2)); + } + } + + return builder.ToString(); + } } diff --git a/src/EFCore.Relational/Metadata/IReadOnlyDbFunction.cs b/src/EFCore.Relational/Metadata/IReadOnlyDbFunction.cs index 100532fc9c6..f7c13715218 100644 --- a/src/EFCore.Relational/Metadata/IReadOnlyDbFunction.cs +++ b/src/EFCore.Relational/Metadata/IReadOnlyDbFunction.cs @@ -119,6 +119,11 @@ string ToDebugString(MetadataDebugStringOptions options = MetadataDebugStringOpt builder.Append(Name); + if (Name != ModelName) + { + builder.Append('*'); + } + if ((options & MetadataDebugStringOptions.SingleLine) == 0) { var parameters = Parameters.ToList(); diff --git a/src/EFCore.Relational/Metadata/IReadOnlyEntityTypeMappingFragment.cs b/src/EFCore.Relational/Metadata/IReadOnlyEntityTypeMappingFragment.cs index ae79f90cfac..591f6e55291 100644 --- a/src/EFCore.Relational/Metadata/IReadOnlyEntityTypeMappingFragment.cs +++ b/src/EFCore.Relational/Metadata/IReadOnlyEntityTypeMappingFragment.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Text; + namespace Microsoft.EntityFrameworkCore.Metadata; /// @@ -26,4 +28,42 @@ public interface IReadOnlyEntityTypeMappingFragment : IReadOnlyAnnotatable /// /// A value indicating whether the associated table is ignored by Migrations. bool? IsTableExcludedFromMigrations { get; } + + /// + /// + /// Creates a human-readable representation of the given metadata. + /// + /// + /// Warning: Do not rely on the format of the returned string. + /// It is designed for debugging only and may change arbitrarily between releases. + /// + /// + /// Options for generating the string. + /// The number of indent spaces to use before each new line. + /// A human-readable representation. + string ToDebugString(MetadataDebugStringOptions options = MetadataDebugStringOptions.ShortDefault, int indent = 0) + { + var builder = new StringBuilder(); + var indentString = new string(' ', indent); + + builder + .Append(indentString) + .Append("Fragment: ") + .Append(StoreObject.DisplayName()); + + if (IsTableExcludedFromMigrations == true) + { + builder.Append("ExcludedFromMigrations"); + } + + if ((options & MetadataDebugStringOptions.SingleLine) == 0) + { + if ((options & MetadataDebugStringOptions.IncludeAnnotations) != 0) + { + builder.Append(AnnotationsToDebugString(indent: indent + 2)); + } + } + + return builder.ToString(); + } } diff --git a/src/EFCore.Relational/Metadata/IReadOnlyRelationalPropertyOverrides.cs b/src/EFCore.Relational/Metadata/IReadOnlyRelationalPropertyOverrides.cs index 76f806bcf44..5fe8987bf95 100644 --- a/src/EFCore.Relational/Metadata/IReadOnlyRelationalPropertyOverrides.cs +++ b/src/EFCore.Relational/Metadata/IReadOnlyRelationalPropertyOverrides.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Text; + namespace Microsoft.EntityFrameworkCore.Metadata; /// @@ -30,4 +32,43 @@ public interface IReadOnlyRelationalPropertyOverrides : IReadOnlyAnnotatable /// Gets a value indicating whether the column name is overriden. /// bool ColumnNameOverridden { get; } + + /// + /// + /// Creates a human-readable representation of the given metadata. + /// + /// + /// Warning: Do not rely on the format of the returned string. + /// It is designed for debugging only and may change arbitrarily between releases. + /// + /// + /// Options for generating the string. + /// The number of indent spaces to use before each new line. + /// A human-readable representation. + string ToDebugString(MetadataDebugStringOptions options = MetadataDebugStringOptions.ShortDefault, int indent = 0) + { + var builder = new StringBuilder(); + var indentString = new string(' ', indent); + + builder + .Append(indentString) + .Append("Override: ") + .Append(StoreObject.DisplayName()); + + if (ColumnNameOverridden) + { + builder.Append(" ColumnName: ") + .Append(ColumnName); + } + + if ((options & MetadataDebugStringOptions.SingleLine) == 0) + { + if ((options & MetadataDebugStringOptions.IncludeAnnotations) != 0) + { + builder.Append(AnnotationsToDebugString(indent: indent + 2)); + } + } + + return builder.ToString(); + } } diff --git a/src/EFCore.Relational/Metadata/IReadOnlyTrigger.cs b/src/EFCore.Relational/Metadata/IReadOnlyTrigger.cs index 9b27689154f..a03fdff0e6e 100644 --- a/src/EFCore.Relational/Metadata/IReadOnlyTrigger.cs +++ b/src/EFCore.Relational/Metadata/IReadOnlyTrigger.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Text; + namespace Microsoft.EntityFrameworkCore.Metadata; /// @@ -67,4 +69,53 @@ public interface IReadOnlyTrigger : IReadOnlyAnnotatable /// Gets the entity type on which this trigger is defined. /// IReadOnlyEntityType EntityType { get; } + + /// + /// + /// Creates a human-readable representation of the given metadata. + /// + /// + /// Warning: Do not rely on the format of the returned string. + /// It is designed for debugging only and may change arbitrarily between releases. + /// + /// + /// Options for generating the string. + /// The number of indent spaces to use before each new line. + /// A human-readable representation. + string ToDebugString(MetadataDebugStringOptions options = MetadataDebugStringOptions.ShortDefault, int indent = 0) + { + var builder = new StringBuilder(); + var indentString = new string(' ', indent); + + builder + .Append(indentString) + .Append("Trigger: ") + .Append(ModelName); + + if (Name != ModelName) + { + builder.Append('*'); + } + + builder.Append(" "); + + if (TableSchema != null) + { + builder + .Append('.') + .Append(TableSchema); + } + + builder.Append(TableName); + + if ((options & MetadataDebugStringOptions.SingleLine) == 0) + { + if ((options & MetadataDebugStringOptions.IncludeAnnotations) != 0) + { + builder.Append(AnnotationsToDebugString(indent: indent + 2)); + } + } + + return builder.ToString(); + } } diff --git a/src/EFCore.Relational/Metadata/ITableMapping.cs b/src/EFCore.Relational/Metadata/ITableMapping.cs index bc9c6fa382c..71bceeb10a0 100644 --- a/src/EFCore.Relational/Metadata/ITableMapping.cs +++ b/src/EFCore.Relational/Metadata/ITableMapping.cs @@ -48,13 +48,36 @@ string ToDebugString(MetadataDebugStringOptions options = MetadataDebugStringOpt builder.Append("TableMapping: "); } - builder.Append(EntityType.Name).Append(" - "); + builder + .Append(EntityType.Name) + .Append(" - ") + .Append(Table.Name); - builder.Append(Table.Name); + builder.Append(" "); + if (!IncludesDerivedTypes) + { + builder.Append("!"); + } + builder.Append("IncludesDerivedTypes"); + + if (IsSharedTablePrincipal != null) + { + builder.Append(" "); + if (!IsSharedTablePrincipal.Value) + { + builder.Append("!"); + } + builder.Append("IsSharedTablePrincipal"); + } - if (IncludesDerivedTypes) + if (IsSplitEntityTypePrincipal != null) { - builder.Append(" IncludesDerivedTypes"); + builder.Append(" "); + if (!IsSplitEntityTypePrincipal.Value) + { + builder.Append("!"); + } + builder.Append("IsSplitEntityTypePrincipal"); } if (!singleLine && (options & MetadataDebugStringOptions.IncludeAnnotations) != 0) diff --git a/src/EFCore.Relational/Metadata/ITrigger.cs b/src/EFCore.Relational/Metadata/ITrigger.cs index 75434b0c9de..ab5eee6ec59 100644 --- a/src/EFCore.Relational/Metadata/ITrigger.cs +++ b/src/EFCore.Relational/Metadata/ITrigger.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Text; - namespace Microsoft.EntityFrameworkCore.Metadata; /// @@ -24,35 +22,7 @@ public interface ITrigger : IReadOnlyTrigger, IAnnotatable new IEntityType EntityType { get; } /// - /// - /// Creates a human-readable representation of the given metadata. - /// - /// - /// Warning: Do not rely on the format of the returned string. - /// It is designed for debugging only and may change arbitrarily between releases. - /// + /// Gets the database name of the trigger. /// - /// Options for generating the string. - /// The number of indent spaces to use before each new line. - /// A human-readable representation. - string ToDebugString(MetadataDebugStringOptions options = MetadataDebugStringOptions.ShortDefault, int indent = 0) - { - var builder = new StringBuilder(); - var indentString = new string(' ', indent); - - builder - .Append(indentString) - .Append("Trigger: ") - .Append(ModelName); - - if ((options & MetadataDebugStringOptions.SingleLine) == 0) - { - if ((options & MetadataDebugStringOptions.IncludeAnnotations) != 0) - { - builder.Append(AnnotationsToDebugString(indent: indent + 2)); - } - } - - return builder.ToString(); - } + new string Name { get; } } diff --git a/src/EFCore.Relational/Metadata/Internal/CheckConstraint.cs b/src/EFCore.Relational/Metadata/Internal/CheckConstraint.cs index e21dc834c69..91cdf62e3f9 100644 --- a/src/EFCore.Relational/Metadata/Internal/CheckConstraint.cs +++ b/src/EFCore.Relational/Metadata/Internal/CheckConstraint.cs @@ -169,7 +169,7 @@ public static void Attach(IConventionEntityType entityType, IConventionCheckCons detachedCheckConstraint.Sql, detachedCheckConstraint.GetConfigurationSource()); - Attach(detachedCheckConstraint, newCheckConstraint); + MergeInto(detachedCheckConstraint, newCheckConstraint); } /// @@ -178,7 +178,7 @@ public static void Attach(IConventionEntityType entityType, IConventionCheckCons /// 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 static void Attach(IConventionCheckConstraint detachedCheckConstraint, IConventionCheckConstraint existingCheckConstraint) + public static void MergeInto(IConventionCheckConstraint detachedCheckConstraint, IConventionCheckConstraint existingCheckConstraint) { var nameConfigurationSource = detachedCheckConstraint.GetNameConfigurationSource(); if (nameConfigurationSource != null) @@ -416,6 +416,18 @@ public virtual void UpdateConfigurationSource(ConfigurationSource configurationS public override string ToString() => ((ICheckConstraint)this).ToDebugString(MetadataDebugStringOptions.SingleLineDefault); + /// + /// 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. + /// + [EntityFrameworkInternal] + public virtual DebugView DebugView + => new( + () => ((ICheckConstraint)this).ToDebugString(), + () => ((ICheckConstraint)this).ToDebugString(MetadataDebugStringOptions.LongDefault)); + /// /// 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.Relational/Metadata/Internal/DbFunction.cs b/src/EFCore.Relational/Metadata/Internal/DbFunction.cs index 6ddcf2fea6b..2c76de5e509 100644 --- a/src/EFCore.Relational/Metadata/Internal/DbFunction.cs +++ b/src/EFCore.Relational/Metadata/Internal/DbFunction.cs @@ -651,6 +651,18 @@ public virtual IReadOnlyList Parameters public override string ToString() => ((IDbFunction)this).ToDebugString(MetadataDebugStringOptions.SingleLineDefault); + /// + /// 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. + /// + [EntityFrameworkInternal] + public virtual DebugView DebugView + => new( + () => ((IDbFunction)this).ToDebugString(), + () => ((IDbFunction)this).ToDebugString(MetadataDebugStringOptions.LongDefault)); + /// IConventionDbFunctionBuilder IConventionDbFunction.Builder { diff --git a/src/EFCore.Relational/Metadata/Internal/EntityTypeMappingFragment.cs b/src/EFCore.Relational/Metadata/Internal/EntityTypeMappingFragment.cs index fbe0f4e2141..8bd23181316 100644 --- a/src/EFCore.Relational/Metadata/Internal/EntityTypeMappingFragment.cs +++ b/src/EFCore.Relational/Metadata/Internal/EntityTypeMappingFragment.cs @@ -16,10 +16,11 @@ public class EntityTypeMappingFragment : IConventionEntityTypeMappingFragment { private bool? _isTableExcludedFromMigrations; + private InternalEntityTypeMappingFragmentBuilder? _builder; private ConfigurationSource _configurationSource; private ConfigurationSource? _isTableExcludedFromMigrationsConfigurationSource; - + /// /// 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 @@ -34,8 +35,39 @@ public EntityTypeMappingFragment( EntityType = entityType; StoreObject = storeObject; _configurationSource = configurationSource; + _builder = new InternalEntityTypeMappingFragmentBuilder(this, ((IConventionModel)entityType.Model).Builder); + } + + /// + /// 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 InternalEntityTypeMappingFragmentBuilder Builder + { + [DebuggerStepThrough] + get => _builder ?? throw new InvalidOperationException(CoreStrings.ObjectRemovedFromModel); } + /// + /// 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 bool IsInModel + => _builder is not null; + + /// + /// 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 void SetRemovedFromModel() + => _builder = null; + /// /// 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 @@ -79,6 +111,44 @@ public virtual ConfigurationSource GetConfigurationSource() public virtual void UpdateConfigurationSource(ConfigurationSource configurationSource) => _configurationSource = configurationSource.Max(_configurationSource); + /// + /// 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 static void Attach(IConventionEntityType entityType, IConventionEntityTypeMappingFragment detachedFragment) + { + var newFragment = GetOrCreate( + (IMutableEntityType)entityType, + detachedFragment.StoreObject, + detachedFragment.GetConfigurationSource()); + + MergeInto(detachedFragment, newFragment); + } + + /// + /// 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 static EntityTypeMappingFragment MergeInto( + IConventionEntityTypeMappingFragment detachedFragment, IConventionEntityTypeMappingFragment existingFragment) + { + var isTableExcludedFromMigrationsConfigurationSource = detachedFragment.GetIsTableExcludedFromMigrationsConfigurationSource(); + if (isTableExcludedFromMigrationsConfigurationSource != null) + { + existingFragment = ((InternalEntityTypeMappingFragmentBuilder)existingFragment.Builder).ExcludeTableFromMigrations( + detachedFragment.IsTableExcludedFromMigrations, isTableExcludedFromMigrationsConfigurationSource.Value) + !.Metadata; + } + + return ((InternalEntityTypeMappingFragmentBuilder)existingFragment.Builder) + .MergeAnnotationsFrom((EntityTypeMappingFragment)detachedFragment) + .Metadata; + } + /// public virtual bool? IsTableExcludedFromMigrations { @@ -98,7 +168,7 @@ public virtual bool? IsTableExcludedFromMigrations { return null; } - + _isTableExcludedFromMigrations = excluded; _isTableExcludedFromMigrationsConfigurationSource = excluded == null @@ -160,7 +230,7 @@ public static EntityTypeMappingFragment GetOrCreate( var fragment = fragments.Find(storeObject); if (fragment == null) { - fragment = new (entityType, storeObject, configurationSource); + fragment = new(entityType, storeObject, configurationSource); fragments.Add(storeObject, fragment); } else @@ -170,7 +240,7 @@ public static EntityTypeMappingFragment GetOrCreate( return fragment; } - + /// /// 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 @@ -179,8 +249,7 @@ public static EntityTypeMappingFragment GetOrCreate( /// public static EntityTypeMappingFragment? Remove( IMutableEntityType entityType, - in StoreObjectIdentifier storeObject, - ConfigurationSource configurationSource) + in StoreObjectIdentifier storeObject) { var fragments = (StoreObjectDictionary?) entityType[RelationalAnnotationNames.MappingFragments]; @@ -195,23 +264,40 @@ public static EntityTypeMappingFragment GetOrCreate( return null; } - if (configurationSource.Overrides(fragment.GetConfigurationSource())) - { - fragments.Remove(storeObject); - - return fragment; - } + fragments.Remove(storeObject); + fragment.SetRemovedFromModel(); - return null; + return fragment; } + /// + /// 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 override string ToString() + => ((IEntityTypeMappingFragment)this).ToDebugString(MetadataDebugStringOptions.SingleLineDefault); + + /// + /// 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. + /// + [EntityFrameworkInternal] + public virtual DebugView DebugView + => new( + () => ((IEntityTypeMappingFragment)this).ToDebugString(), + () => ((IEntityTypeMappingFragment)this).ToDebugString(MetadataDebugStringOptions.LongDefault)); + /// IEntityType IEntityTypeMappingFragment.EntityType { [DebuggerStepThrough] get => (IEntityType)EntityType; } - + /// IMutableEntityType IMutableEntityTypeMappingFragment.EntityType { @@ -229,5 +315,15 @@ IConventionEntityType IConventionEntityTypeMappingFragment.EntityType bool? IConventionEntityTypeMappingFragment.SetIsTableExcludedFromMigrations(bool? excluded, bool fromDataAnnotation) => SetIsTableExcludedFromMigrations(excluded, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - bool? IReadOnlyEntityTypeMappingFragment.IsTableExcludedFromMigrations => IsTableExcludedFromMigrations; + bool? IReadOnlyEntityTypeMappingFragment.IsTableExcludedFromMigrations + { + [DebuggerStepThrough] + get => IsTableExcludedFromMigrations; + } + + IConventionEntityTypeMappingFragmentBuilder IConventionEntityTypeMappingFragment.Builder + { + [DebuggerStepThrough] + get => Builder; + } } diff --git a/src/EFCore.Relational/Metadata/Internal/InternalCheckConstraintBuilder.cs b/src/EFCore.Relational/Metadata/Internal/InternalCheckConstraintBuilder.cs index 727c1147f29..5ef202fa24f 100644 --- a/src/EFCore.Relational/Metadata/Internal/InternalCheckConstraintBuilder.cs +++ b/src/EFCore.Relational/Metadata/Internal/InternalCheckConstraintBuilder.cs @@ -123,7 +123,7 @@ public virtual bool CanSetName(string? name, ConfigurationSource configurationSo { foreach (var detachedCheckConstraint in detachedCheckConstraints) { - CheckConstraint.Attach(detachedCheckConstraint, constraint); + CheckConstraint.MergeInto(detachedCheckConstraint, constraint); } } } diff --git a/src/EFCore.Relational/Metadata/Internal/InternalEntityTypeMappingFragmentBuilder.cs b/src/EFCore.Relational/Metadata/Internal/InternalEntityTypeMappingFragmentBuilder.cs new file mode 100644 index 00000000000..ce48ddd90ff --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/InternalEntityTypeMappingFragmentBuilder.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Metadata.Internal; + +/// +/// 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 class InternalEntityTypeMappingFragmentBuilder : + AnnotatableBuilder, + IConventionEntityTypeMappingFragmentBuilder +{ + /// + /// 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 InternalEntityTypeMappingFragmentBuilder( + EntityTypeMappingFragment fragment, IConventionModelBuilder modelBuilder) + : base(fragment, modelBuilder) + { + } + + /// + /// 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 InternalEntityTypeMappingFragmentBuilder? ExcludeTableFromMigrations( + bool? excludedFromMigrations, + ConfigurationSource configurationSource) + { + if (!CanExcludeTableFromMigrations(excludedFromMigrations, configurationSource)) + { + return null; + } + + Metadata.SetIsTableExcludedFromMigrations(excludedFromMigrations, configurationSource); + return this; + } + + /// + /// 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 bool CanExcludeTableFromMigrations( + bool? excludedFromMigrations, + ConfigurationSource configurationSource) + => configurationSource.Overrides(Metadata.GetIsTableExcludedFromMigrationsConfigurationSource()) + || Metadata.IsTableExcludedFromMigrations == excludedFromMigrations; + + /// + IConventionEntityTypeMappingFragment IConventionEntityTypeMappingFragmentBuilder.Metadata + { + [DebuggerStepThrough] + get => Metadata; + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/InternalRelationalPropertyOverridesBuilder.cs b/src/EFCore.Relational/Metadata/Internal/InternalRelationalPropertyOverridesBuilder.cs new file mode 100644 index 00000000000..3b5e1caaf3f --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/InternalRelationalPropertyOverridesBuilder.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Metadata.Internal; + +/// +/// 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 class InternalRelationalPropertyOverridesBuilder : + AnnotatableBuilder, + IConventionRelationalPropertyOverridesBuilder +{ + /// + /// 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 InternalRelationalPropertyOverridesBuilder( + RelationalPropertyOverrides overrides, IConventionModelBuilder modelBuilder) + : base(overrides, modelBuilder) + { + } + + /// + /// 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 InternalRelationalPropertyOverridesBuilder? HasColumnName( + string? name, + ConfigurationSource configurationSource) + { + if (!CanSetColumnName(name, configurationSource)) + { + return null; + } + + Metadata.SetColumnName(name, configurationSource); + + return this; + } + + /// + /// 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 bool CanSetColumnName( + string? name, + ConfigurationSource configurationSource) + => configurationSource.Overrides(Metadata.GetColumnNameConfigurationSource()) + || Metadata.ColumnName == name; + + /// + IConventionRelationalPropertyOverrides IConventionRelationalPropertyOverridesBuilder.Metadata + { + [DebuggerStepThrough] + get => Metadata; + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/InternalTriggerBuilder.cs b/src/EFCore.Relational/Metadata/Internal/InternalTriggerBuilder.cs index 2a1850f0602..7c38a619788 100644 --- a/src/EFCore.Relational/Metadata/Internal/InternalTriggerBuilder.cs +++ b/src/EFCore.Relational/Metadata/Internal/InternalTriggerBuilder.cs @@ -66,7 +66,8 @@ public virtual bool CanSetName(string? name, ConfigurationSource configurationSo var trigger = entityType.FindTrigger(name); if (trigger != null) { - if (trigger.TableName == tableName && trigger.TableSchema == tableSchema) + if ((tableName == null && tableSchema == null) + || (trigger.TableName == tableName && trigger.TableSchema == tableSchema)) { ((Trigger)trigger).UpdateConfigurationSource(configurationSource); return trigger; @@ -83,14 +84,14 @@ public virtual bool CanSetName(string? name, ConfigurationSource configurationSo { foreach (var derivedType in entityType.GetDerivedTypes()) { - var derivedTrigger = - (IConventionTrigger?)Trigger.FindDeclaredTrigger(derivedType, name); + var derivedTrigger = (IConventionTrigger?)Trigger.FindDeclaredTrigger(derivedType, name); if (derivedTrigger == null) { continue; } - if ((derivedTrigger.TableName != tableName || derivedTrigger.TableSchema != tableSchema) + if ((tableName != null || tableSchema != null) + && (derivedTrigger.TableName != tableName || derivedTrigger.TableSchema != tableSchema) && !configurationSource.Overrides(derivedTrigger.GetConfigurationSource())) { return null; @@ -139,6 +140,12 @@ public static bool CanHaveTrigger( string? tableSchema, ConfigurationSource configurationSource) { + if (tableName == null + && tableSchema == null) + { + return true; + } + if (entityType.FindTrigger(name) is IConventionTrigger trigger) { return (trigger.TableName == tableName diff --git a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs index 6bce5b2c861..d01f84c0462 100644 --- a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs +++ b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs @@ -970,7 +970,8 @@ private static void PopulateTableConfiguration(Table table, bool designTime) } } - foreach (var trigger in entityType.GetTriggers()) + // Triggers cannot be inherited + foreach (var trigger in entityType.GetDeclaredTriggers()) { var name = trigger.GetName(storeObject); if (name == null) @@ -979,7 +980,7 @@ private static void PopulateTableConfiguration(Table table, bool designTime) } Check.DebugAssert(trigger.TableName == table.Name, "Mismatch in trigger table name"); - Check.DebugAssert(trigger.TableSchema is null || trigger.TableSchema == table.Schema, "Mismatch in trigger table schema"); + Check.DebugAssert(trigger.TableSchema == table.Schema, "Mismatch in trigger table schema"); if (!table.Triggers.ContainsKey(name)) { diff --git a/src/EFCore.Relational/Metadata/Internal/RelationalPropertyOverrides.cs b/src/EFCore.Relational/Metadata/Internal/RelationalPropertyOverrides.cs index 8058c2ab173..aeec0461e23 100644 --- a/src/EFCore.Relational/Metadata/Internal/RelationalPropertyOverrides.cs +++ b/src/EFCore.Relational/Metadata/Internal/RelationalPropertyOverrides.cs @@ -16,6 +16,7 @@ public class RelationalPropertyOverrides : IRelationalPropertyOverrides { private string? _columnName; + private InternalRelationalPropertyOverridesBuilder? _builder; private ConfigurationSource _configurationSource; private ConfigurationSource? _columnNameConfigurationSource; @@ -34,6 +35,8 @@ public RelationalPropertyOverrides( Property = property; StoreObject = storeObject; _configurationSource = configurationSource; + _builder = new InternalRelationalPropertyOverridesBuilder( + this, ((IConventionModel)property.DeclaringEntityType.Model).Builder); } /// @@ -60,6 +63,74 @@ public RelationalPropertyOverrides( /// public override bool IsReadOnly => ((Annotatable)Property).IsReadOnly; + + /// + /// 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 InternalRelationalPropertyOverridesBuilder Builder + { + [DebuggerStepThrough] + get => _builder ?? throw new InvalidOperationException(CoreStrings.ObjectRemovedFromModel); + } + + /// + /// 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 bool IsInModel + => _builder is not null; + + /// + /// 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 void SetRemovedFromModel() + => _builder = null; + + /// + /// 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 static void Attach(IConventionProperty property, IConventionRelationalPropertyOverrides detachedOverrides) + { + var newOverrides = GetOrCreate( + (IMutableProperty)property, + detachedOverrides.StoreObject, + detachedOverrides.GetConfigurationSource()); + + MergeInto(detachedOverrides, newOverrides); + } + + /// + /// 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 static RelationalPropertyOverrides MergeInto( + IConventionRelationalPropertyOverrides detachedOverrides, IConventionRelationalPropertyOverrides existingOverrides) + { + var columnNameConfigurationSource = detachedOverrides.GetColumnNameConfigurationSource(); + if (columnNameConfigurationSource != null) + { + existingOverrides = ((InternalRelationalPropertyOverridesBuilder)existingOverrides.Builder) + .HasColumnName(detachedOverrides.ColumnName, columnNameConfigurationSource.Value) + !.Metadata; + } + + return ((InternalRelationalPropertyOverridesBuilder)existingOverrides.Builder) + .MergeAnnotationsFrom((RelationalPropertyOverrides)detachedOverrides) + .Metadata; + } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -206,8 +277,7 @@ public static RelationalPropertyOverrides GetOrCreate( /// public static RelationalPropertyOverrides? Remove( IMutableProperty property, - in StoreObjectIdentifier storeObject, - ConfigurationSource configurationSource) + in StoreObjectIdentifier storeObject) { var tableOverrides = (StoreObjectDictionary?) property[RelationalAnnotationNames.RelationalOverrides]; @@ -222,16 +292,33 @@ public static RelationalPropertyOverrides GetOrCreate( return null; } - if (configurationSource.Overrides(overrides.GetConfigurationSource())) - { - tableOverrides.Remove(storeObject); + tableOverrides.Remove(storeObject); + overrides.SetRemovedFromModel(); - return overrides; - } - - return null; + return overrides; } + /// + /// 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 override string ToString() + => ((IRelationalPropertyOverrides)this).ToDebugString(MetadataDebugStringOptions.SingleLineDefault); + + /// + /// 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. + /// + [EntityFrameworkInternal] + public virtual DebugView DebugView + => new( + () => ((IRelationalPropertyOverrides)this).ToDebugString(), + () => ((IRelationalPropertyOverrides)this).ToDebugString(MetadataDebugStringOptions.LongDefault)); + /// IProperty IRelationalPropertyOverrides.Property { @@ -253,6 +340,13 @@ IConventionProperty IConventionRelationalPropertyOverrides.Property get => (IConventionProperty)Property; } + /// + IConventionRelationalPropertyOverridesBuilder IConventionRelationalPropertyOverrides.Builder + { + [DebuggerStepThrough] + get => Builder; + } + string? IConventionRelationalPropertyOverrides.SetColumnName(string? name, bool fromDataAnnotation) => SetColumnName(name, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); diff --git a/src/EFCore.Relational/Metadata/Internal/Trigger.cs b/src/EFCore.Relational/Metadata/Internal/Trigger.cs index 58600cb3db9..40807b0871a 100644 --- a/src/EFCore.Relational/Metadata/Internal/Trigger.cs +++ b/src/EFCore.Relational/Metadata/Internal/Trigger.cs @@ -159,7 +159,7 @@ public static IEnumerable GetTriggers(IReadOnlyEntityType enti /// 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 static void MergeInto(IConventionEntityType entityType, IConventionTrigger detachedTrigger) + public static void Attach(IConventionEntityType entityType, IConventionTrigger detachedTrigger) { var newTrigger = new Trigger( (IMutableEntityType)entityType, @@ -257,22 +257,11 @@ public virtual string? Name /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual string? GetName(in StoreObjectIdentifier storeObject) - { - if (storeObject.StoreObjectType != StoreObjectType.Table) - { - return null; - } - - foreach (var containingType in EntityType.GetDerivedTypesInclusive()) - { - if (StoreObjectIdentifier.Create(containingType, storeObject.StoreObjectType) == storeObject) - { - return _name ?? ((IReadOnlyTrigger)this).GetDefaultName(storeObject); - } - } - - return null; - } + => storeObject.StoreObjectType == StoreObjectType.Table + && TableName == storeObject.Name + && TableSchema == storeObject.Schema + ? _name ?? ((IReadOnlyTrigger)this).GetDefaultName(storeObject) + : null; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -346,7 +335,7 @@ public virtual string TableName /// public virtual string? TableSchema { - get => _tableSchema; + get => _tableSchema ?? EntityType.GetSchema(); set => SetTableSchema(value, ConfigurationSource.Explicit); } @@ -406,6 +395,18 @@ public virtual void UpdateConfigurationSource(ConfigurationSource configurationS public override string ToString() => ((ITrigger)this).ToDebugString(MetadataDebugStringOptions.SingleLineDefault); + /// + /// 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. + /// + [EntityFrameworkInternal] + public virtual DebugView DebugView + => new( + () => ((ITrigger)this).ToDebugString(), + () => ((ITrigger)this).ToDebugString(MetadataDebugStringOptions.LongDefault)); + /// /// 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 @@ -442,6 +443,18 @@ IEntityType ITrigger.EntityType get => (IEntityType)EntityType; } + /// + /// 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. + /// + string ITrigger.Name + { + [DebuggerStepThrough] + get => Name!; + } + /// /// 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.Relational/Metadata/RuntimeEntityTypeMappingFragment.cs b/src/EFCore.Relational/Metadata/RuntimeEntityTypeMappingFragment.cs index ccd5d231505..15e0ae4c661 100644 --- a/src/EFCore.Relational/Metadata/RuntimeEntityTypeMappingFragment.cs +++ b/src/EFCore.Relational/Metadata/RuntimeEntityTypeMappingFragment.cs @@ -43,6 +43,22 @@ public RuntimeEntityTypeMappingFragment( /// public virtual bool? IsTableExcludedFromMigrations => (bool?)this[RelationalAnnotationNames.IsTableExcludedFromMigrations]; + /// + public override string ToString() + => ((IEntityTypeMappingFragment)this).ToDebugString(MetadataDebugStringOptions.SingleLineDefault); + + /// + /// 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. + /// + [EntityFrameworkInternal] + public virtual DebugView DebugView + => new( + () => ((IEntityTypeMappingFragment)this).ToDebugString(), + () => ((IEntityTypeMappingFragment)this).ToDebugString(MetadataDebugStringOptions.LongDefault)); + /// IEntityType IEntityTypeMappingFragment.EntityType { diff --git a/src/EFCore.Relational/Metadata/RuntimeRelationalPropertyOverrides.cs b/src/EFCore.Relational/Metadata/RuntimeRelationalPropertyOverrides.cs index 224554159eb..43f8fb11100 100644 --- a/src/EFCore.Relational/Metadata/RuntimeRelationalPropertyOverrides.cs +++ b/src/EFCore.Relational/Metadata/RuntimeRelationalPropertyOverrides.cs @@ -40,6 +40,22 @@ public RuntimeRelationalPropertyOverrides( /// public virtual StoreObjectIdentifier StoreObject { get; } + /// + public override string ToString() + => ((IRelationalPropertyOverrides)this).ToDebugString(MetadataDebugStringOptions.SingleLineDefault); + + /// + /// 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. + /// + [EntityFrameworkInternal] + public virtual DebugView DebugView + => new( + () => ((IRelationalPropertyOverrides)this).ToDebugString(), + () => ((IRelationalPropertyOverrides)this).ToDebugString(MetadataDebugStringOptions.LongDefault)); + /// IProperty IRelationalPropertyOverrides.Property { diff --git a/src/EFCore.Relational/Metadata/RuntimeTrigger.cs b/src/EFCore.Relational/Metadata/RuntimeTrigger.cs index ed95ba9e6e3..e5ad7097b4b 100644 --- a/src/EFCore.Relational/Metadata/RuntimeTrigger.cs +++ b/src/EFCore.Relational/Metadata/RuntimeTrigger.cs @@ -20,7 +20,7 @@ public class RuntimeTrigger : AnnotatableBase, ITrigger public RuntimeTrigger( RuntimeEntityType entityType, string modelName, - string? name, + string name, string tableName, string? tableSchema) { @@ -37,11 +37,15 @@ public RuntimeTrigger( /// /// Gets the database name of the trigger. /// - public virtual string? Name { get; } + public virtual string Name { get; } /// public virtual string? GetName(in StoreObjectIdentifier storeObject) - => Name; + => storeObject.StoreObjectType == StoreObjectType.Table + && TableName == storeObject.Name + && TableSchema == storeObject.Schema + ? Name + : null; /// public virtual string TableName { get; } @@ -52,6 +56,22 @@ public RuntimeTrigger( /// public virtual IEntityType EntityType { get; } + /// + public override string ToString() + => ((ITrigger)this).ToDebugString(MetadataDebugStringOptions.SingleLineDefault); + + /// + /// 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. + /// + [EntityFrameworkInternal] + public virtual DebugView DebugView + => new( + () => ((ITrigger)this).ToDebugString(), + () => ((ITrigger)this).ToDebugString(MetadataDebugStringOptions.LongDefault)); + /// IReadOnlyEntityType IReadOnlyTrigger.EntityType => EntityType; diff --git a/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs b/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs index c26efe1b249..2f6cf549ec4 100644 --- a/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs +++ b/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs @@ -176,6 +176,13 @@ public override IReadOnlyList GenerateFluentApiCalls( return fragments; } + /// + public override IReadOnlyList GenerateFluentApiCalls( + IRelationalPropertyOverrides overrides, IDictionary annotations) + { + return base.GenerateFluentApiCalls(overrides, annotations); + } + /// /// 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.SqlServer/Design/Internal/SqlServerCSharpRuntimeAnnotationCodeGenerator.cs b/src/EFCore.SqlServer/Design/Internal/SqlServerCSharpRuntimeAnnotationCodeGenerator.cs index c1552ade2f0..67a61fdf330 100644 --- a/src/EFCore.SqlServer/Design/Internal/SqlServerCSharpRuntimeAnnotationCodeGenerator.cs +++ b/src/EFCore.SqlServer/Design/Internal/SqlServerCSharpRuntimeAnnotationCodeGenerator.cs @@ -106,4 +106,17 @@ public override void Generate(IEntityType entityType, CSharpRuntimeAnnotationCod base.Generate(entityType, parameters); } + + /// + public override void Generate(IRelationalPropertyOverrides overrides, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) + { + if (!parameters.IsRuntime) + { + var annotations = parameters.Annotations; + annotations.Remove(SqlServerAnnotationNames.IdentityIncrement); + annotations.Remove(SqlServerAnnotationNames.IdentitySeed); + } + + base.Generate(overrides, parameters); + } } diff --git a/src/EFCore.SqlServer/Extensions/SqlServerPropertyBuilderExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerPropertyBuilderExtensions.cs index 5bf965a51d1..ee6a0003c04 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerPropertyBuilderExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerPropertyBuilderExtensions.cs @@ -183,6 +183,32 @@ public static PropertyBuilder UseIdentityColumn( int increment = 1) => propertyBuilder.UseIdentityColumn((long)seed, increment); + /// + /// Configures the key column to use the SQL Server IDENTITY feature to generate values for new entities, + /// when targeting SQL Server. This method sets the property to be . + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing SQL Server and SQL Azure databases with EF Core + /// for more information and examples. + /// + /// The builder for the column being configured. + /// The value that is used for the very first row loaded into the table. + /// The incremental value that is added to the identity value of the previous row that was loaded. + /// The same builder instance so that multiple calls can be chained. + public static ColumnBuilder UseIdentityColumn( + this ColumnBuilder columnBuilder, + long seed = 1, + int increment = 1) + { + var overrides = columnBuilder.Overrides; + overrides.SetValueGenerationStrategy(SqlServerValueGenerationStrategy.IdentityColumn); + overrides.SetIdentitySeed(seed); + overrides.SetIdentityIncrement(increment); + + return columnBuilder; + } + /// /// Configures the key property to use the SQL Server IDENTITY feature to generate values for new entities, /// when targeting SQL Server. This method sets the property to be . @@ -223,6 +249,26 @@ public static PropertyBuilder UseIdentityColumn( int increment = 1) => (PropertyBuilder)UseIdentityColumn((PropertyBuilder)propertyBuilder, (long)seed, increment); + /// + /// Configures the key column to use the SQL Server IDENTITY feature to generate values for new entities, + /// when targeting SQL Server. This method sets the property to be . + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing SQL Server and SQL Azure databases with EF Core + /// for more information and examples. + /// + /// The type of the property being configured. + /// The builder for the column being configured. + /// The value that is used for the very first row loaded into the table. + /// The incremental value that is added to the identity value of the previous row that was loaded. + /// The same builder instance so that multiple calls can be chained. + public static ColumnBuilder UseIdentityColumn( + this ColumnBuilder columnBuilder, + long seed = 1, + int increment = 1) + => (ColumnBuilder)UseIdentityColumn((ColumnBuilder)columnBuilder, seed, increment); + /// /// Configures the seed for SQL Server IDENTITY. /// @@ -252,6 +298,37 @@ public static PropertyBuilder UseIdentityColumn( return null; } + /// + /// Configures the seed for SQL Server IDENTITY for a particular table. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing SQL Server and SQL Azure databases with EF Core + /// for more information and examples. + /// + /// The builder for the property being configured. + /// The value that is used for the very first row loaded into the table. + /// The table identifier. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + public static IConventionPropertyBuilder? HasIdentityColumnSeed( + this IConventionPropertyBuilder propertyBuilder, + long? seed, + in StoreObjectIdentifier storeObject, + bool fromDataAnnotation = false) + { + if (propertyBuilder.CanSetIdentityColumnSeed(seed, storeObject, fromDataAnnotation)) + { + propertyBuilder.Metadata.SetIdentitySeed(seed, storeObject, fromDataAnnotation); + return propertyBuilder; + } + + return null; + } + /// /// Returns a value indicating whether the given value can be set as the seed for SQL Server IDENTITY. /// @@ -270,6 +347,32 @@ public static bool CanSetIdentityColumnSeed( bool fromDataAnnotation = false) => propertyBuilder.CanSetAnnotation(SqlServerAnnotationNames.IdentitySeed, seed, fromDataAnnotation); + /// + /// Returns a value indicating whether the given value can be set as the seed for SQL Server IDENTITY + /// for a particular table. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing SQL Server and SQL Azure databases with EF Core + /// for more information and examples. + /// + /// The builder for the property being configured. + /// The value that is used for the very first row loaded into the table. + /// The table identifier. + /// Indicates whether the configuration was specified using a data annotation. + /// if the given value can be set as the seed for SQL Server IDENTITY. + public static bool CanSetIdentityColumnSeed( + this IConventionPropertyBuilder propertyBuilder, + long? seed, + in StoreObjectIdentifier storeObject, + bool fromDataAnnotation = false) + => propertyBuilder.Metadata.FindOverrides(storeObject)?.Builder + .CanSetAnnotation( + SqlServerAnnotationNames.IdentitySeed, + seed, + fromDataAnnotation) + ?? true; + /// /// Configures the increment for SQL Server IDENTITY. /// @@ -299,6 +402,37 @@ public static bool CanSetIdentityColumnSeed( return null; } + /// + /// Configures the increment for SQL Server IDENTITY for a particular table. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing SQL Server and SQL Azure databases with EF Core + /// for more information and examples. + /// + /// The builder for the property being configured. + /// The incremental value that is added to the identity value of the previous row that was loaded. + /// The table identifier. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + public static IConventionPropertyBuilder? HasIdentityColumnIncrement( + this IConventionPropertyBuilder propertyBuilder, + int? increment, + in StoreObjectIdentifier storeObject, + bool fromDataAnnotation = false) + { + if (propertyBuilder.CanSetIdentityColumnIncrement(increment, storeObject, fromDataAnnotation)) + { + propertyBuilder.Metadata.SetIdentityIncrement(increment, storeObject, fromDataAnnotation); + return propertyBuilder; + } + + return null; + } + /// /// Returns a value indicating whether the given value can be set as the increment for SQL Server IDENTITY. /// @@ -317,6 +451,32 @@ public static bool CanSetIdentityColumnIncrement( bool fromDataAnnotation = false) => propertyBuilder.CanSetAnnotation(SqlServerAnnotationNames.IdentityIncrement, increment, fromDataAnnotation); + /// + /// Returns a value indicating whether the given value can be set as the increment for SQL Server IDENTITY + /// for a particular table. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing SQL Server and SQL Azure databases with EF Core + /// for more information and examples. + /// + /// The builder for the property being configured. + /// The incremental value that is added to the identity value of the previous row that was loaded. + /// The table identifier. + /// Indicates whether the configuration was specified using a data annotation. + /// if the given value can be set as the default increment for SQL Server IDENTITY. + public static bool CanSetIdentityColumnIncrement( + this IConventionPropertyBuilder propertyBuilder, + int? increment, + in StoreObjectIdentifier storeObject, + bool fromDataAnnotation = false) + => propertyBuilder.Metadata.FindOverrides(storeObject)?.Builder + .CanSetAnnotation( + SqlServerAnnotationNames.IdentityIncrement, + increment, + fromDataAnnotation) + ?? true; + /// /// Configures the value generation strategy for the key property, when targeting SQL Server. /// @@ -358,6 +518,43 @@ public static bool CanSetIdentityColumnIncrement( return null; } + /// + /// Configures the value generation strategy for the key property, when targeting SQL Server for a particular table. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing SQL Server and SQL Azure databases with EF Core + /// for more information and examples. + /// + /// The builder for the property being configured. + /// The value generation strategy. + /// The table identifier. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + public static IConventionPropertyBuilder? HasValueGenerationStrategy( + this IConventionPropertyBuilder propertyBuilder, + SqlServerValueGenerationStrategy? valueGenerationStrategy, + in StoreObjectIdentifier storeObject, + bool fromDataAnnotation = false) + { + if (propertyBuilder.CanSetValueGenerationStrategy(valueGenerationStrategy, storeObject, fromDataAnnotation)) + { + propertyBuilder.Metadata.SetValueGenerationStrategy(valueGenerationStrategy, storeObject, fromDataAnnotation); + if (valueGenerationStrategy != SqlServerValueGenerationStrategy.IdentityColumn) + { + propertyBuilder.HasIdentityColumnSeed(null, storeObject, fromDataAnnotation); + propertyBuilder.HasIdentityColumnIncrement(null, storeObject, fromDataAnnotation); + } + + return propertyBuilder; + } + + return null; + } + /// /// Returns a value indicating whether the given value can be set as the value generation strategy. /// @@ -379,6 +576,33 @@ public static bool CanSetValueGenerationStrategy( && propertyBuilder.CanSetAnnotation( SqlServerAnnotationNames.ValueGenerationStrategy, valueGenerationStrategy, fromDataAnnotation); + /// + /// Returns a value indicating whether the given value can be set as the value generation strategy for a particular table. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing SQL Server and SQL Azure databases with EF Core + /// for more information and examples. + /// + /// The builder for the property being configured. + /// The value generation strategy. + /// The table identifier. + /// Indicates whether the configuration was specified using a data annotation. + /// if the given value can be set as the default value generation strategy. + public static bool CanSetValueGenerationStrategy( + this IConventionPropertyBuilder propertyBuilder, + SqlServerValueGenerationStrategy? valueGenerationStrategy, + in StoreObjectIdentifier storeObject, + bool fromDataAnnotation = false) + => (valueGenerationStrategy == null + || SqlServerPropertyExtensions.IsCompatibleWithValueGeneration(propertyBuilder.Metadata)) + && (propertyBuilder.Metadata.FindOverrides(storeObject)?.Builder + .CanSetAnnotation( + SqlServerAnnotationNames.ValueGenerationStrategy, + valueGenerationStrategy, + fromDataAnnotation) + ?? true); + /// /// Configures whether the property's column is created as sparse when targeting SQL Server. /// diff --git a/src/EFCore.SqlServer/Extensions/SqlServerPropertyExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerPropertyExtensions.cs index 2cfa3f3d202..ec60455e0e6 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerPropertyExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerPropertyExtensions.cs @@ -231,6 +231,12 @@ public static void SetHiLoSequenceSchema(this IMutableProperty property, string? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData); } + var @override = property.FindOverrides(storeObject)?.FindAnnotation(SqlServerAnnotationNames.IdentitySeed); + if (@override != null) + { + return (long?)@override.Value; + } + var annotation = property.FindAnnotation(SqlServerAnnotationNames.IdentitySeed); if (annotation is not null) { @@ -246,6 +252,16 @@ public static void SetHiLoSequenceSchema(this IMutableProperty property, string? : sharedProperty.GetIdentitySeed(storeObject); } + /// + /// Returns the identity seed. + /// + /// The property overrides. + /// The identity seed. + public static long? GetIdentitySeed(this IReadOnlyRelationalPropertyOverrides overrides) + => overrides is RuntimeRelationalPropertyOverrides + ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData) + : (long?)overrides.FindAnnotation(SqlServerAnnotationNames.IdentitySeed)?.Value; + /// /// Sets the identity seed. /// @@ -276,6 +292,56 @@ public static void SetIdentitySeed(this IMutableProperty property, long? seed) return seed; } + /// + /// Sets the identity seed for a particular table. + /// + /// The property. + /// The value to set. + /// The identifier of the table containing the column. + public static void SetIdentitySeed( + this IMutableProperty property, + long? seed, + in StoreObjectIdentifier storeObject) + => property.GetOrCreateOverrides(storeObject) + .SetIdentitySeed(seed); + + /// + /// Sets the identity seed for a particular table. + /// + /// The property. + /// The value to set. + /// The identifier of the table containing the column. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + public static long? SetIdentitySeed( + this IConventionProperty property, + long? seed, + in StoreObjectIdentifier storeObject, + bool fromDataAnnotation = false) + => property.GetOrCreateOverrides(storeObject, fromDataAnnotation) + .SetIdentitySeed(seed, fromDataAnnotation); + + /// + /// Sets the identity seed for a particular table. + /// + /// The property overrides. + /// The value to set. + public static void SetIdentitySeed(this IMutableRelationalPropertyOverrides overrides, long? seed) + => overrides.SetOrRemoveAnnotation(SqlServerAnnotationNames.IdentitySeed, seed); + + /// + /// Sets the identity seed for a particular table. + /// + /// The property overrides. + /// The value to set. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + public static long? SetIdentitySeed( + this IConventionRelationalPropertyOverrides overrides, + long? seed, + bool fromDataAnnotation = false) + => (long?)overrides.SetOrRemoveAnnotation(SqlServerAnnotationNames.IdentitySeed, seed, fromDataAnnotation)?.Value; + /// /// Returns the for the identity seed. /// @@ -284,6 +350,26 @@ public static void SetIdentitySeed(this IMutableProperty property, long? seed) public static ConfigurationSource? GetIdentitySeedConfigurationSource(this IConventionProperty property) => property.FindAnnotation(SqlServerAnnotationNames.IdentitySeed)?.GetConfigurationSource(); + /// + /// Returns the for the identity seed for a particular table. + /// + /// The property. + /// The identifier of the table containing the column. + /// The for the identity seed. + public static ConfigurationSource? GetIdentitySeedConfigurationSource( + this IConventionProperty property, + in StoreObjectIdentifier storeObject) + => property.FindOverrides(storeObject)?.GetIdentitySeedConfigurationSource(); + + /// + /// Returns the for the identity seed for a particular table. + /// + /// The property overrides. + /// The for the identity seed. + public static ConfigurationSource? GetIdentitySeedConfigurationSource( + this IConventionRelationalPropertyOverrides overrides) + => overrides.FindAnnotation(SqlServerAnnotationNames.IdentitySeed)?.GetConfigurationSource(); + /// /// Returns the identity increment. /// @@ -308,6 +394,12 @@ public static void SetIdentitySeed(this IMutableProperty property, long? seed) throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData); } + var @override = property.FindOverrides(storeObject)?.FindAnnotation(SqlServerAnnotationNames.IdentityIncrement); + if (@override != null) + { + return (int?)@override.Value; + } + var annotation = property.FindAnnotation(SqlServerAnnotationNames.IdentityIncrement); if (annotation != null) { @@ -320,6 +412,16 @@ public static void SetIdentitySeed(this IMutableProperty property, long? seed) : sharedProperty.GetIdentityIncrement(storeObject); } + /// + /// Returns the identity increment. + /// + /// The property overrides. + /// The identity increment. + public static int? GetIdentityIncrement(this IReadOnlyRelationalPropertyOverrides overrides) + => overrides is RuntimeRelationalPropertyOverrides + ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData) + : (int?)overrides.FindAnnotation(SqlServerAnnotationNames.IdentityIncrement)?.Value; + /// /// Sets the identity increment. /// @@ -341,14 +443,61 @@ public static void SetIdentityIncrement(this IMutableProperty property, int? inc this IConventionProperty property, int? increment, bool fromDataAnnotation = false) - { - property.SetOrRemoveAnnotation( + => (int?)property.SetOrRemoveAnnotation( SqlServerAnnotationNames.IdentityIncrement, increment, - fromDataAnnotation); + fromDataAnnotation)?.Value; - return increment; - } + /// + /// Sets the identity increment for a particular table. + /// + /// The property. + /// The value to set. + /// The identifier of the table containing the column. + public static void SetIdentityIncrement( + this IMutableProperty property, + int? increment, + in StoreObjectIdentifier storeObject) + => property.GetOrCreateOverrides(storeObject) + .SetIdentityIncrement(increment); + + /// + /// Sets the identity increment for a particular table. + /// + /// The property. + /// The value to set. + /// The identifier of the table containing the column. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + public static int? SetIdentityIncrement( + this IConventionProperty property, + int? increment, + in StoreObjectIdentifier storeObject, + bool fromDataAnnotation = false) + => property.GetOrCreateOverrides(storeObject, fromDataAnnotation) + .SetIdentityIncrement(increment, fromDataAnnotation); + + /// + /// Sets the identity increment for a particular table. + /// + /// The property overrides. + /// The value to set. + public static void SetIdentityIncrement(this IMutableRelationalPropertyOverrides overrides, int? increment) + => overrides.SetOrRemoveAnnotation(SqlServerAnnotationNames.IdentityIncrement, increment); + + /// + /// Sets the identity increment for a particular table. + /// + /// The property overrides. + /// The value to set. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + public static int? SetIdentityIncrement( + this IConventionRelationalPropertyOverrides overrides, + int? increment, + bool fromDataAnnotation = false) + => (int?)overrides.SetOrRemoveAnnotation(SqlServerAnnotationNames.IdentityIncrement, increment, fromDataAnnotation) + ?.Value; /// /// Returns the for the identity increment. @@ -358,6 +507,26 @@ public static void SetIdentityIncrement(this IMutableProperty property, int? inc public static ConfigurationSource? GetIdentityIncrementConfigurationSource(this IConventionProperty property) => property.FindAnnotation(SqlServerAnnotationNames.IdentityIncrement)?.GetConfigurationSource(); + /// + /// Returns the for the identity increment for a particular table. + /// + /// The property. + /// The identifier of the table containing the column. + /// The for the identity increment. + public static ConfigurationSource? GetIdentityIncrementConfigurationSource( + this IConventionProperty property, + in StoreObjectIdentifier storeObject) + => property.FindOverrides(storeObject)?.GetIdentityIncrementConfigurationSource(); + + /// + /// Returns the for the identity increment for a particular table. + /// + /// The property overrides. + /// The for the identity increment. + public static ConfigurationSource? GetIdentityIncrementConfigurationSource( + this IConventionRelationalPropertyOverrides overrides) + => overrides.FindAnnotation(SqlServerAnnotationNames.IdentityIncrement)?.GetConfigurationSource(); + /// /// Returns the to use for the property. /// @@ -405,6 +574,12 @@ internal static SqlServerValueGenerationStrategy GetValueGenerationStrategy( in StoreObjectIdentifier storeObject, ITypeMappingSource? typeMappingSource) { + var @override = property.FindOverrides(storeObject)?.FindAnnotation(SqlServerAnnotationNames.ValueGenerationStrategy); + if (@override != null) + { + return (SqlServerValueGenerationStrategy?)@override.Value ?? SqlServerValueGenerationStrategy.None; + } + var annotation = property.FindAnnotation(SqlServerAnnotationNames.ValueGenerationStrategy); if (annotation?.Value != null && StoreObjectIdentifier.Create(property.DeclaringEntityType, storeObject.StoreObjectType) == storeObject) @@ -455,6 +630,19 @@ is StoreObjectIdentifier principal return defaultStategy; } + /// + /// Returns the to use for the property. + /// + /// + /// If no strategy is set for the property, then the strategy to use will be taken from the . + /// + /// The property overrides. + /// The strategy, or if none was set. + public static SqlServerValueGenerationStrategy? GetValueGenerationStrategy( + this IReadOnlyRelationalPropertyOverrides overrides) + => (SqlServerValueGenerationStrategy?)overrides.FindAnnotation(SqlServerAnnotationNames.ValueGenerationStrategy) + ?.Value; + private static SqlServerValueGenerationStrategy GetDefaultValueGenerationStrategy(IReadOnlyProperty property) { var modelStrategy = property.DeclaringEntityType.Model.GetValueGenerationStrategy(); @@ -517,9 +705,71 @@ public static void SetValueGenerationStrategy( { CheckValueGenerationStrategy(property, value); - property.SetOrRemoveAnnotation(SqlServerAnnotationNames.ValueGenerationStrategy, value, fromDataAnnotation); + return (SqlServerValueGenerationStrategy?)property.SetOrRemoveAnnotation( + SqlServerAnnotationNames.ValueGenerationStrategy, value, fromDataAnnotation) + ?.Value; + } + + /// + /// Sets the to use for the property for a particular table. + /// + /// The property. + /// The strategy to use. + /// The identifier of the table containing the column. + public static void SetValueGenerationStrategy( + this IMutableProperty property, + SqlServerValueGenerationStrategy? value, + in StoreObjectIdentifier storeObject) + => property.GetOrCreateOverrides(storeObject) + .SetValueGenerationStrategy(value); + + /// + /// Sets the to use for the property for a particular table. + /// + /// The property. + /// The strategy to use. + /// The identifier of the table containing the column. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + public static SqlServerValueGenerationStrategy? SetValueGenerationStrategy( + this IConventionProperty property, + SqlServerValueGenerationStrategy? value, + in StoreObjectIdentifier storeObject, + bool fromDataAnnotation = false) + => property.GetOrCreateOverrides(storeObject, fromDataAnnotation) + .SetValueGenerationStrategy(value, fromDataAnnotation); + + /// + /// Sets the to use for the property for a particular table. + /// + /// The property overrides. + /// The strategy to use. + public static void SetValueGenerationStrategy( + this IMutableRelationalPropertyOverrides overrides, + SqlServerValueGenerationStrategy? value) + { + CheckValueGenerationStrategy(overrides.Property, value); + + overrides.SetOrRemoveAnnotation(SqlServerAnnotationNames.ValueGenerationStrategy, value); + } + + /// + /// Sets the to use for the property for a particular table. + /// + /// The property overrides. + /// The strategy to use. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + public static SqlServerValueGenerationStrategy? SetValueGenerationStrategy( + this IConventionRelationalPropertyOverrides overrides, + SqlServerValueGenerationStrategy? value, + bool fromDataAnnotation = false) + { + CheckValueGenerationStrategy(overrides.Property, value); - return value; + return (SqlServerValueGenerationStrategy?)overrides.SetOrRemoveAnnotation( + SqlServerAnnotationNames.ValueGenerationStrategy, value, fromDataAnnotation) + ?.Value; } private static void CheckValueGenerationStrategy(IReadOnlyProperty property, SqlServerValueGenerationStrategy? value) @@ -555,6 +805,26 @@ private static void CheckValueGenerationStrategy(IReadOnlyProperty property, Sql this IConventionProperty property) => property.FindAnnotation(SqlServerAnnotationNames.ValueGenerationStrategy)?.GetConfigurationSource(); + /// + /// Returns the for the for a particular table. + /// + /// The property. + /// The identifier of the table containing the column. + /// The for the . + public static ConfigurationSource? GetValueGenerationStrategyConfigurationSource( + this IConventionProperty property, + in StoreObjectIdentifier storeObject) + => property.FindOverrides(storeObject)?.GetValueGenerationStrategyConfigurationSource(); + + /// + /// Returns the for the for a particular table. + /// + /// The property overrides. + /// The for the . + public static ConfigurationSource? GetValueGenerationStrategyConfigurationSource( + this IConventionRelationalPropertyOverrides overrides) + => overrides.FindAnnotation(SqlServerAnnotationNames.ValueGenerationStrategy)?.GetConfigurationSource(); + /// /// Returns a value indicating whether the property is compatible with any . /// @@ -566,10 +836,9 @@ public static bool IsCompatibleWithValueGeneration(IReadOnlyProperty property) ?? property.FindTypeMapping()?.Converter; var type = (valueConverter?.ProviderClrType ?? property.ClrType).UnwrapNullableType(); - - return (type.IsInteger() + return type.IsInteger() || type.IsEnum - || type == typeof(decimal)); + || type == typeof(decimal); } private static bool IsCompatibleWithValueGeneration( diff --git a/src/EFCore.SqlServer/Metadata/Conventions/SqlServerRuntimeModelConvention.cs b/src/EFCore.SqlServer/Metadata/Conventions/SqlServerRuntimeModelConvention.cs index 2f0deede3c9..76fb8b3bc92 100644 --- a/src/EFCore.SqlServer/Metadata/Conventions/SqlServerRuntimeModelConvention.cs +++ b/src/EFCore.SqlServer/Metadata/Conventions/SqlServerRuntimeModelConvention.cs @@ -29,13 +29,7 @@ public SqlServerRuntimeModelConvention( { } - /// - /// 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, @@ -54,13 +48,7 @@ protected override void ProcessModelAnnotations( } } - /// - /// 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, @@ -82,13 +70,23 @@ protected override void ProcessPropertyAnnotations( } } - /// - /// 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 ProcessPropertyOverridesAnnotations( + Dictionary annotations, + IRelationalPropertyOverrides propertyOverrides, + RuntimeRelationalPropertyOverrides runtimePropertyOverrides, + bool runtime) + { + base.ProcessPropertyOverridesAnnotations(annotations, propertyOverrides, runtimePropertyOverrides, runtime); + + if (!runtime) + { + annotations.Remove(SqlServerAnnotationNames.IdentityIncrement); + annotations.Remove(SqlServerAnnotationNames.IdentitySeed); + } + } + + /// protected override void ProcessIndexAnnotations( Dictionary annotations, IIndex index, @@ -106,13 +104,7 @@ protected override void ProcessIndexAnnotations( } } - /// - /// 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, @@ -127,13 +119,7 @@ protected override void ProcessKeyAnnotations( } } - /// - /// 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, diff --git a/src/EFCore.SqlServer/Update/Internal/SqlServerUpdateSqlGenerator.cs b/src/EFCore.SqlServer/Update/Internal/SqlServerUpdateSqlGenerator.cs index c5c3624ee25..ed30a2eb3dd 100644 --- a/src/EFCore.SqlServer/Update/Internal/SqlServerUpdateSqlGenerator.cs +++ b/src/EFCore.SqlServer/Update/Internal/SqlServerUpdateSqlGenerator.cs @@ -846,5 +846,5 @@ private static bool HasAnyTriggers(IReadOnlyModificationCommand command) // Data seeding doesn't provide any entries, so we we don't know if the table has triggers; assume it does to generate SQL // that works everywhere. => command.Entries.Count == 0 - || command.Entries[0].EntityType.Model.GetRelationalModel().FindTable(command.TableName, command.Schema)!.Triggers.Any(); + || command.Table!.Triggers.Any(); } diff --git a/src/EFCore/Infrastructure/AnnotatableBuilder.cs b/src/EFCore/Infrastructure/AnnotatableBuilder.cs index 5b857ca90f7..0889d2eadf3 100644 --- a/src/EFCore/Infrastructure/AnnotatableBuilder.cs +++ b/src/EFCore/Infrastructure/AnnotatableBuilder.cs @@ -71,7 +71,10 @@ protected AnnotatableBuilder(TMetadata metadata, TModelBuilder modelBuilder) return this; } - if (!CanSetAnnotationValue(existingAnnotation, value, configurationSource, canOverrideSameSource)) + var existingConfigurationSource = existingAnnotation.GetConfigurationSource(); + if (!configurationSource.Overrides(existingConfigurationSource) + || (configurationSource == existingConfigurationSource + && !canOverrideSameSource)) { return null; } @@ -170,7 +173,7 @@ public virtual bool CanRemoveAnnotation(string name, ConfigurationSource configu /// Copies all the explicitly configured annotations from the given object overwriting any existing ones. /// /// The object to copy annotations from. - public virtual void MergeAnnotationsFrom(TMetadata annotatable) + public virtual AnnotatableBuilder MergeAnnotationsFrom(TMetadata annotatable) => MergeAnnotationsFrom(annotatable, ConfigurationSource.Explicit); /// @@ -178,20 +181,24 @@ public virtual void MergeAnnotationsFrom(TMetadata annotatable) /// /// The object to copy annotations from. /// The minimum configuration source for an annotation to be copied. - public virtual void MergeAnnotationsFrom(TMetadata annotatable, ConfigurationSource minimalConfigurationSource) + public virtual AnnotatableBuilder MergeAnnotationsFrom( + TMetadata annotatable, ConfigurationSource minimalConfigurationSource) { + var builder = this; foreach (var annotation in annotatable.GetAnnotations()) { var configurationSource = annotation.GetConfigurationSource(); if (configurationSource.Overrides(minimalConfigurationSource)) { - HasAnnotation( + builder = builder.HasAnnotation( annotation.Name, annotation.Value, configurationSource, - canOverrideSameSource: false); + canOverrideSameSource: false) ?? builder; } } + + return builder; } /// diff --git a/src/EFCore/Infrastructure/ConventionAnnotatable.cs b/src/EFCore/Infrastructure/ConventionAnnotatable.cs index f4fd65eeeb6..188128ad3c7 100644 --- a/src/EFCore/Infrastructure/ConventionAnnotatable.cs +++ b/src/EFCore/Infrastructure/ConventionAnnotatable.cs @@ -56,6 +56,7 @@ public override void SetAnnotation(string name, object? value) /// The key of the annotation to be added. /// The value to be stored in the annotation. /// The configuration source of the annotation to be set. + /// The new annotation. public virtual ConventionAnnotation? SetAnnotation( string name, object? value, @@ -77,11 +78,14 @@ public override void SetAnnotation(string name, object? value) } /// - /// 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. + /// Sets the annotation with given key and value on this object using given configuration source. + /// Removes the existing annotation if an annotation with the specified name already exists and + /// is . /// + /// The key of the annotation to be added. + /// The value to be stored in the annotation. + /// The configuration source of the annotation to be set. + /// The new annotation. public virtual ConventionAnnotation? SetOrRemoveAnnotation( string name, object? value, diff --git a/src/EFCore/Metadata/Conventions/ForeignKeyPropertyDiscoveryConvention.cs b/src/EFCore/Metadata/Conventions/ForeignKeyPropertyDiscoveryConvention.cs index 85155be2193..795e1a2adf2 100644 --- a/src/EFCore/Metadata/Conventions/ForeignKeyPropertyDiscoveryConvention.cs +++ b/src/EFCore/Metadata/Conventions/ForeignKeyPropertyDiscoveryConvention.cs @@ -409,7 +409,9 @@ private bool TryFindMatchingProperties( var referencedProperty = propertiesToReference[i]; var property = TryGetProperty( dependentEntityType, - baseName, referencedProperty.Name); + baseName, + referencedProperty.Name, + matchImplicitProperties: propertiesToReference.Count != 1); if (property == null) { @@ -420,13 +422,21 @@ private bool TryFindMatchingProperties( foreignKeyProperties[i] = property; } + if (matchFound + && foreignKeyProperties.Length != 1 + && foreignKeyProperties.All(p => p.IsImplicitlyCreated() + && ConfigurationSource.Convention.Overrides(p.GetConfigurationSource()))) + { + return false; + } + if (!matchFound && propertiesToReference.Count == 1 && baseName.Length > 0) { var property = TryGetProperty( dependentEntityType, - baseName, "Id"); + baseName, "Id", matchImplicitProperties: false); if (property != null) { @@ -504,11 +514,13 @@ private bool TryFindMatchingProperties( return true; } - private static IConventionProperty? TryGetProperty(IConventionEntityType entityType, string prefix, string suffix) + private static IConventionProperty? TryGetProperty( + IConventionEntityType entityType, string prefix, string suffix, bool matchImplicitProperties) { foreach (var property in entityType.GetProperties()) { if ((!property.IsImplicitlyCreated() + || matchImplicitProperties || !ConfigurationSource.Convention.Overrides(property.GetConfigurationSource())) && property.Name.Length == prefix.Length + suffix.Length && property.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) diff --git a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs index 1385e3b7c32..3645ca93e4f 100644 --- a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs +++ b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs @@ -668,11 +668,14 @@ public virtual void Entity_splitting_is_stored_in_snapshot_with_tables() b.Property("Shadow").HasColumnName("Shadow"); b.ToTable("Order", tb => { + tb.Property(e => e.Id).UseIdentityColumn(2, 3).HasAnnotation("fii", "arr"); tb.Property("Shadow"); }); b.SplitToTable("SplitOrder", sb => { sb.Property("Shadow"); + sb.HasTrigger("splitTrigger").HasAnnotation("oof", "rab"); + sb.HasAnnotation("foo", "bar"); }); b.OwnsOne(p => p.OrderBillingDetails, od => @@ -725,12 +728,23 @@ public virtual void Entity_splitting_is_stored_in_snapshot_with_tables() b.ToTable(""Order"", null, t => { + t.Property(""Id"") + .HasAnnotation(""fii"", ""arr"") + .HasAnnotation(""SqlServer:IdentityIncrement"", 3) + .HasAnnotation(""SqlServer:IdentitySeed"", 2L) + .HasAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.IdentityColumn); + t.Property(""Shadow""); }); b.SplitToTable(""SplitOrder"", null, t => { + t.HasTrigger(""splitTrigger"") + .HasAnnotation(""oof"", ""rab""); + t.Property(""Shadow""); + + t.HasAnnotation(""foo"", ""bar""); }); }); @@ -852,6 +866,17 @@ public virtual void Entity_splitting_is_stored_in_snapshot_with_tables() var orderEntityType = model.FindEntityType(typeof(Order)); Assert.Equal(nameof(Order), orderEntityType.GetTableName()); + var id = orderEntityType.FindProperty("Id"); + Assert.Equal(SqlServerValueGenerationStrategy.IdentityColumn, id.GetValueGenerationStrategy()); + Assert.Equal(1, id.GetIdentitySeed()); + Assert.Equal(1, id.GetIdentityIncrement()); + + var overrides = id.FindOverrides(StoreObjectIdentifier.Create(orderEntityType, StoreObjectType.Table).Value)!; + Assert.Equal(SqlServerValueGenerationStrategy.IdentityColumn, overrides.GetValueGenerationStrategy()); + Assert.Equal(2, overrides.GetIdentitySeed()); + Assert.Equal(3, overrides.GetIdentityIncrement()); + Assert.Equal("arr", overrides["fii"]); + var billingOwnership = orderEntityType.FindNavigation(nameof(Order.OrderBillingDetails)) .ForeignKey; var billingEntityType = billingOwnership.DeclaringEntityType; @@ -884,6 +909,12 @@ public virtual void Entity_splitting_is_stored_in_snapshot_with_tables() var splitTable = relationalModel.FindTable(fragment.StoreObject.Name, fragment.StoreObject.Schema); Assert.Equal(new[] { billingEntityType, orderEntityType }, splitTable.FindColumn("Shadow").PropertyMappings.Select(m => m.TableMapping.EntityType)); + Assert.Equal("bar", fragment["foo"]); + + var trigger = orderEntityType.GetTriggers().Single(); + Assert.Equal(splitTable.Name, trigger.TableName); + Assert.Equal(splitTable.Schema, trigger.TableSchema); + Assert.Equal("rab", trigger["oof"]); var billingFragment = billingEntityType.GetMappingFragments().Single(); var billingTable = relationalModel.FindTable(billingFragment.StoreObject.Name, billingFragment.StoreObject.Schema); @@ -1133,7 +1164,8 @@ public virtual void Sequence_is_stored_in_snapshot_as_fluent_api() .HasMin(1) .HasMax(3) .IncrementsBy(2) - .IsCyclic(); + .IsCyclic() + .HasAnnotation("foo", "bar"); }, AddBoilerPlate( GetHeading() @@ -1143,10 +1175,19 @@ public virtual void Sequence_is_stored_in_snapshot_as_fluent_api() .IncrementsBy(2) .HasMin(1L) .HasMax(3L) - .IsCyclic();"), - o => + .IsCyclic() + .HasAnnotation(""foo"", ""bar"");"), + model => { - Assert.Equal(5, o.GetAnnotations().Count()); + Assert.Equal(5, model.GetAnnotations().Count()); + + var sequence = model.GetSequences().Single(); + Assert.Equal(2, sequence.StartValue); + Assert.Equal(1, sequence.MinValue); + Assert.Equal(3, sequence.MaxValue); + Assert.Equal(2, sequence.IncrementBy); + Assert.True(sequence.IsCyclic); + Assert.Equal("bar", sequence["foo"]); }); [ConditionalFact] @@ -1308,6 +1349,7 @@ public virtual void Triggers_and_ExcludeFromMigrations_are_stored_in_snapshot() t.ExcludeFromMigrations(); t.HasTrigger(""SomeTrigger1""); + t.HasTrigger(""SomeTrigger2""); }); });"), diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs index 9f88365b5a0..0265be6861f 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs @@ -1252,10 +1252,12 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { entity.Property(e => e.Id).UseIdentityColumn(); - entity.ToTable(tb => { - tb.HasTrigger(""Trigger1""); - tb.HasTrigger(""Trigger2""); - }); + entity.ToTable(tb => + { + tb.HasTrigger(""Trigger1""); + + tb.HasTrigger(""Trigger2""); + }); }); OnModelCreatingPartial(modelBuilder); diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs index ef23e7ee37d..e7bb3a7ec0a 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs @@ -1208,8 +1208,18 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba ""PrincipalBaseId"", typeof(long), propertyAccessMode: PropertyAccessMode.Field, + valueGenerated: ValueGenerated.OnAdd, afterSaveBehavior: PropertySaveBehavior.Throw); - principalBaseId.AddAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.None); + var overrides = new StoreObjectDictionary(); + var principalBaseIdPrincipalBase = new RuntimeRelationalPropertyOverrides( + principalBaseId, + StoreObjectIdentifier.Table(""PrincipalBase"", ""mySchema""), + false, + null); + principalBaseIdPrincipalBase.AddAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.IdentityColumn); + overrides.Add(StoreObjectIdentifier.Table(""PrincipalBase"", ""mySchema""), principalBaseIdPrincipalBase); + principalBaseId.AddAnnotation(""Relational:RelationalOverrides"", overrides); + principalBaseId.AddAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.IdentityColumn); var principalBaseAlternateId = runtimeEntityType.AddProperty( ""PrincipalBaseAlternateId"", @@ -1226,14 +1236,14 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba fieldInfo: typeof(CSharpRuntimeModelCodeGeneratorTest.OwnedType).GetField(""
k__BackingField"", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), propertyAccessMode: PropertyAccessMode.Field, nullable: true); - var overrides = new StoreObjectDictionary(); + var overrides0 = new StoreObjectDictionary(); var detailsDetails = new RuntimeRelationalPropertyOverrides( details, StoreObjectIdentifier.Table(""Details"", null), false, null); - overrides.Add(StoreObjectIdentifier.Table(""Details"", null), detailsDetails); - details.AddAnnotation(""Relational:RelationalOverrides"", overrides); + overrides0.Add(StoreObjectIdentifier.Table(""Details"", null), detailsDetails); + details.AddAnnotation(""Relational:RelationalOverrides"", overrides0); details.AddAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.None); var number = runtimeEntityType.AddProperty( @@ -1883,11 +1893,25 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) CoreStrings.RuntimeModelMissingData, Assert.Throws(() => referenceOwnedType.GetNavigationAccessMode()).Message); + var principalTable = StoreObjectIdentifier.Create(referenceOwnedType, StoreObjectType.Table).Value; + + var ownedId = referenceOwnedType.FindProperty("PrincipalBaseId"); + Assert.True(ownedId.IsPrimaryKey()); + Assert.Equal( + SqlServerValueGenerationStrategy.IdentityColumn, + principalId.GetValueGenerationStrategy(principalTable)); + Assert.Equal( + CoreStrings.RuntimeModelMissingData, + Assert.Throws(() => principalId.GetIdentityIncrement(principalTable)).Message); + Assert.Equal( + CoreStrings.RuntimeModelMissingData, + Assert.Throws(() => principalId.GetIdentitySeed(principalTable)).Message); + var ownedFragment = referenceOwnedType.GetMappingFragments().Single(); Assert.Equal(nameof(OwnedType.Details), referenceOwnedType.FindProperty(nameof(OwnedType.Details)).GetColumnName(ownedFragment.StoreObject)); Assert.Null(referenceOwnedType.FindProperty(nameof(OwnedType.Details)) - .GetColumnName(StoreObjectIdentifier.Create(referenceOwnedType, StoreObjectType.Table).Value)); + .GetColumnName(principalTable)); var referenceOwnership = referenceOwnedNavigation.ForeignKey; Assert.Empty(referenceOwnership.GetAnnotations()); @@ -2195,6 +2219,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) ob.HasChangeTrackingStrategy(ChangeTrackingStrategy.ChangingAndChangedNotificationsWithOriginalValues); ob.UsePropertyAccessMode(PropertyAccessMode.Field); + ob.ToTable("PrincipalBase", "mySchema", + t => t.Property("PrincipalBaseId").UseIdentityColumn(2, 3)); + ob.SplitToTable("Details", s => s.Property(e => e.Details)); }); diff --git a/test/EFCore.Relational.Tests/Metadata/RelationalBuilderExtensionsTest.cs b/test/EFCore.Relational.Tests/Metadata/RelationalBuilderExtensionsTest.cs index 30d27f5130d..82a36d3e519 100644 --- a/test/EFCore.Relational.Tests/Metadata/RelationalBuilderExtensionsTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/RelationalBuilderExtensionsTest.cs @@ -568,6 +568,7 @@ public void Base_check_constraint_overrides_derived_one() var derivedBuilder = modelBuilder.Entity(typeof(Splow), ConfigurationSource.Convention); IReadOnlyEntityType derivedEntityType = derivedBuilder.Metadata; + derivedBuilder.HasBaseType((EntityType)null, ConfigurationSource.DataAnnotation); Assert.NotNull( derivedBuilder.HasCheckConstraint("Splew", "s < p", fromDataAnnotation: true) @@ -581,17 +582,13 @@ public void Base_check_constraint_overrides_derived_one() Assert.False(derivedBuilder.CanHaveCheckConstraint("Splew", "s > p")); Assert.True(derivedBuilder.CanHaveCheckConstraint("Splot", "s > p")); + Assert.Null(derivedBuilder.HasCheckConstraint("Splew", "s > p")); + Assert.Equal("s < p", derivedEntityType.GetCheckConstraints().Single().Sql); + var baseBuilder = modelBuilder.Entity(typeof(Splot), ConfigurationSource.DataAnnotation); IReadOnlyEntityType baseEntityType = baseBuilder.Metadata; - - Assert.True(baseBuilder.CanHaveCheckConstraint("Splew", "s < p")); - Assert.True(baseBuilder.CanHaveCheckConstraint("Splew", "s > p", fromDataAnnotation: true)); - Assert.False(baseBuilder.CanHaveCheckConstraint("Splew", "s > p")); - Assert.True(baseBuilder.CanHaveCheckConstraint("Splot", "s > p")); - - Assert.Null(baseBuilder.HasCheckConstraint("Splew", "s > p")); + Assert.Null(derivedEntityType.BaseType); Assert.Empty(baseEntityType.GetCheckConstraints()); - Assert.Equal("s < p", derivedEntityType.GetCheckConstraints().Single().Sql); Assert.NotNull( baseBuilder.HasCheckConstraint("Splew", "s < p", fromDataAnnotation: true) @@ -600,7 +597,7 @@ public void Base_check_constraint_overrides_derived_one() Assert.Equal("s < p", baseEntityType.GetCheckConstraints().Single().Sql); Assert.Equal("CK_Splot", baseEntityType.GetCheckConstraints().Single().Name); - derivedBuilder.HasBaseType((EntityType)baseEntityType, ConfigurationSource.Convention); + Assert.NotNull(derivedBuilder.HasBaseType((EntityType)baseEntityType, ConfigurationSource.DataAnnotation)); Assert.Null( baseBuilder.HasCheckConstraint("Splew", "s < p", fromDataAnnotation: true) @@ -776,31 +773,36 @@ public void Base_trigger_overrides_derived_one_after_base_is_set() IReadOnlyEntityType derivedEntityType = derivedBuilder.Metadata; Assert.NotNull( - derivedBuilder.HasTrigger("Splew", "Table1", "dbo", fromDataAnnotation: true) + derivedBuilder.HasTrigger("Splew", nameof(Splow), null, fromDataAnnotation: true) .HasName("Splow_Trigger", fromDataAnnotation: true)); Assert.Equal("Splew", derivedEntityType.GetTriggers().Single().ModelName); - Assert.Equal("Table1", derivedEntityType.GetTriggers().Single().TableName); + Assert.Equal(nameof(Splow), derivedEntityType.GetTriggers().Single().TableName); Assert.Equal("Splow_Trigger", derivedEntityType.GetTriggers().Single().Name); + Assert.Equal("Splow_Trigger", derivedEntityType.GetTriggers().Single() + .GetName(StoreObjectIdentifier.Create(derivedEntityType, StoreObjectType.Table).Value)); var baseBuilder = modelBuilder.Entity(typeof(Splot), ConfigurationSource.Convention); IReadOnlyEntityType baseEntityType = baseBuilder.Metadata; Assert.Null(derivedEntityType.BaseType); Assert.NotNull( - baseBuilder.HasTrigger("Splew", "Table1", "dbo", fromDataAnnotation: true) + baseBuilder.HasTrigger("Splew", nameof(Splot), null, fromDataAnnotation: true) .HasName("Splot_Trigger", fromDataAnnotation: true)); Assert.Equal("Splew", baseEntityType.GetTriggers().Single().ModelName); - Assert.Equal("Table1", baseEntityType.GetTriggers().Single().TableName); + Assert.Equal(nameof(Splot), baseEntityType.GetTriggers().Single().TableName); Assert.Equal("Splot_Trigger", baseEntityType.GetTriggers().Single().Name); Assert.NotNull(derivedBuilder.HasBaseType((EntityType)baseEntityType, ConfigurationSource.DataAnnotation)); + Assert.Null(baseBuilder.HasTrigger("Splew", "Table1", "dbo")); Assert.Null( - baseBuilder.HasTrigger("Splew", "Table1", "dbo", fromDataAnnotation: true) + baseBuilder.HasTrigger("Splew", nameof(Splot), null, fromDataAnnotation: true) .HasName("Splew_Trigger")); Assert.Equal("Splew", baseEntityType.GetTriggers().Single().ModelName); - Assert.Equal("Table1", baseEntityType.GetTriggers().Single().TableName); + Assert.Equal(nameof(Splot), baseEntityType.GetTriggers().Single().TableName); Assert.Equal("Splot_Trigger", baseEntityType.GetTriggers().Single().Name); + Assert.Equal("Splot_Trigger", baseEntityType.GetTriggers().Single() + .GetName(StoreObjectIdentifier.Create(baseEntityType, StoreObjectType.Table).Value)); Assert.Empty(derivedEntityType.GetDeclaredTriggers()); Assert.Same(baseEntityType.GetTriggers().Single(), derivedEntityType.GetTriggers().Single()); } diff --git a/test/EFCore.Relational.Tests/ModelBuilding/RelationalModelBuilderTest.cs b/test/EFCore.Relational.Tests/ModelBuilding/RelationalModelBuilderTest.cs index 908599ab78a..b9c4b25fcc1 100644 --- a/test/EFCore.Relational.Tests/ModelBuilding/RelationalModelBuilderTest.cs +++ b/test/EFCore.Relational.Tests/ModelBuilding/RelationalModelBuilderTest.cs @@ -166,6 +166,132 @@ public abstract class RelationalManyToManyTestBase : ManyToManyTestBase public abstract class RelationalOwnedTypesTestBase : OwnedTypesTestBase { + [ConditionalFact] + public virtual void Can_use_table_splitting_with_owned_reference() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Ignore(); + modelBuilder.Ignore(); + modelBuilder.Ignore(); + + modelBuilder.Entity().OwnsOne( + b => b.Label, lb => + { + lb.Ignore(l => l.Book); + lb.Property("ShadowProp"); + + lb.SplitToTable("BookLabelDetails", s => + { + var propertyBuilder = s.Property(o => o.Id); + var columnBuilder = propertyBuilder.HasColumnName("bid"); + if (columnBuilder is IInfrastructure> genericBuilder) + { + Assert.IsType>(genericBuilder.Instance.GetInfrastructure>()); + Assert.IsAssignableFrom(genericBuilder.GetInfrastructure().Overrides); + } + else + { + var nonGenericBuilder = (IInfrastructure)columnBuilder; + Assert.IsAssignableFrom(nonGenericBuilder.Instance.GetInfrastructure()); + Assert.IsAssignableFrom(nonGenericBuilder.Instance.Overrides); + } + }); + }); + modelBuilder.Entity() + .OwnsOne(b => b.AlternateLabel); + + var model = modelBuilder.Model; + + Assert.Equal(2, model.GetEntityTypes().Count(e => e.ClrType == typeof(BookLabel))); + Assert.Equal(3, model.GetEntityTypes().Count()); + + var book = model.FindEntityType(typeof(Book))!; + var bookOwnership1 = book.FindNavigation(nameof(Book.Label))!.ForeignKey; + var bookOwnership2 = book.FindNavigation(nameof(Book.AlternateLabel))!.ForeignKey; + Assert.NotSame(bookOwnership1.DeclaringEntityType, bookOwnership2.DeclaringEntityType); + + var splitTable = StoreObjectIdentifier.Table("BookLabelDetails"); + var fragment = bookOwnership1.DeclaringEntityType.GetMappingFragments().Single(); + Assert.Same(fragment, bookOwnership1.DeclaringEntityType.FindMappingFragment(splitTable)); + Assert.Same(fragment, bookOwnership1.DeclaringEntityType.GetMappingFragments(StoreObjectType.Table).Single()); + + Assert.True(((IConventionEntityTypeMappingFragment)fragment).IsInModel); + Assert.Same(bookOwnership1.DeclaringEntityType, fragment.EntityType); + + var bookId = bookOwnership1.DeclaringEntityType.FindProperty(nameof(BookLabel.Id))!; + Assert.Equal("Id", bookId.GetColumnName()); + Assert.Null(bookId.GetColumnName(StoreObjectIdentifier.Table("Book"))); + Assert.Equal("bid", bookId.GetColumnName(splitTable)); + + var overrides = bookId.GetOverrides().Single(); + Assert.Same(overrides, bookId.FindOverrides(splitTable)); + Assert.True(((IConventionRelationalPropertyOverrides)overrides).IsInModel); + Assert.Same(bookId, overrides.Property); + + var readOnlyModel = modelBuilder.FinalizeModel(); + + Assert.Equal(2, readOnlyModel.GetEntityTypes().Count(e => e.ClrType == typeof(BookLabel))); + Assert.Equal(3, readOnlyModel.GetEntityTypes().Count()); + } + + [ConditionalFact] + public virtual void Can_use_view_splitting_with_owned_collection() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Ignore(); + modelBuilder.Entity().OwnsMany( + c => c.Orders, + r => + { + r.Ignore(o => o.OrderCombination); + r.Ignore(o => o.Details); + r.Property("ShadowProp"); + + r.ToView("Order"); + r.SplitToView("OrderDetails", s => + { + var propertyBuilder = s.Property(o => o.AnotherCustomerId); + var columnBuilder = propertyBuilder.HasColumnName("cid"); + if (columnBuilder is IInfrastructure> genericBuilder) + { + Assert.IsType>(genericBuilder.Instance.GetInfrastructure>()); + Assert.IsAssignableFrom(genericBuilder.GetInfrastructure().Overrides); + } + else + { + var nonGenericBuilder = (IInfrastructure)columnBuilder; + Assert.IsAssignableFrom(nonGenericBuilder.Instance.GetInfrastructure()); + Assert.IsAssignableFrom(nonGenericBuilder.Instance.Overrides); + } + }); + }); + + var model = modelBuilder.FinalizeModel(); + + var ownership = model.FindEntityType(typeof(Customer))!.FindNavigation(nameof(Customer.Orders))!.ForeignKey; + var owned = ownership.DeclaringEntityType; + Assert.True(ownership.IsOwnership); + + var splitView = StoreObjectIdentifier.View("OrderDetails"); + var fragment = owned.GetMappingFragments().Single(); + Assert.Same(fragment, owned.FindMappingFragment(splitView)); + Assert.Same(fragment, owned.GetMappingFragments(StoreObjectType.View).Single()); + + Assert.True(((IConventionEntityTypeMappingFragment)fragment).IsInModel); + Assert.Same(owned, fragment.EntityType); + + var anotherCustomerId = owned.FindProperty(nameof(Order.AnotherCustomerId))!; + Assert.Equal("AnotherCustomerId", anotherCustomerId.GetColumnName()); + Assert.Null(anotherCustomerId.GetColumnName(StoreObjectIdentifier.View("Order"))); + Assert.Equal("cid", anotherCustomerId.GetColumnName(splitView)); + + var overrides = anotherCustomerId.GetOverrides().Single(); + Assert.Same(overrides, anotherCustomerId.FindOverrides(splitView)); + Assert.True(((IConventionRelationalPropertyOverrides)overrides).IsInModel); + Assert.Same(anotherCustomerId, overrides.Property); + } } public abstract class TestTableBuilder