Skip to content

Commit

Permalink
round 1 - model + mig + some tests
Browse files Browse the repository at this point in the history
  • Loading branch information
maumar committed Apr 1, 2021
1 parent 0c3d9f1 commit fa19711
Show file tree
Hide file tree
Showing 13 changed files with 773 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Utilities;
Expand Down Expand Up @@ -116,5 +119,105 @@ public static bool CanSetIsMemoryOptimized(

return entityTypeBuilder.CanSetAnnotation(SqlServerAnnotationNames.MemoryOptimized, memoryOptimized, fromDataAnnotation);
}

/// <summary>
/// Configures the table that the entity maps to when targeting SQL Server as temporal.
/// </summary>
/// <param name="entityTypeBuilder"> The builder for the entity type being configured. </param>
/// <param name="periodStartPropertyName"> A value specifying the property name representing start of the period.</param>
/// <param name="periodEndPropertyName"> A value specifying the property name representing end of the period.</param>
/// <param name="historyTableName"> A value specifying the history table name for this entity. Default name will be used if none is specified. </param>
/// <returns> The same builder instance so that multiple calls can be chained. </returns>
public static EntityTypeBuilder IsTemporal(
this EntityTypeBuilder entityTypeBuilder,
string periodStartPropertyName,
string periodEndPropertyName,
string? historyTableName = null)
{
Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder));

var value = new SqlServerTemporalTableTransientAnnotationValue(periodStartPropertyName, periodEndPropertyName, historyTableName);

entityTypeBuilder.Metadata.SetIsTemporal(value);

// also add Start and End properties in shadow state
entityTypeBuilder.Property<DateTime>(periodStartPropertyName);
entityTypeBuilder.Property<DateTime>(periodEndPropertyName);

return entityTypeBuilder;
}

/// <summary>
/// Configures the table that the entity maps to when targeting SQL Server as temporal.
/// </summary>
/// <typeparam name="TEntity"> The entity type being configured. </typeparam>
/// <param name="entityTypeBuilder"> The builder for the entity type being configured. </param>
/// <param name="periodStartPropertyExpression"> A value specifying the property representing start of the period.</param>
/// <param name="periodEndPropertyExpression"> A value specifying the property representing end of the period.</param>
/// <param name="historyTableName"> A value specifying the history table name for this entity. Default name will be used if none is specified. </param>
/// <returns> The same builder instance so that multiple calls can be chained. </returns>
public static EntityTypeBuilder<TEntity> IsTemporal<TEntity>(
this EntityTypeBuilder<TEntity> entityTypeBuilder,
Expression<Func<TEntity, DateTime>> periodStartPropertyExpression,
Expression<Func<TEntity, DateTime>> periodEndPropertyExpression,
string? historyTableName = null)
where TEntity : class
=> (EntityTypeBuilder<TEntity>)IsTemporal(
entityTypeBuilder,
periodStartPropertyExpression.GetMemberAccess().Name,
periodEndPropertyExpression.GetMemberAccess().Name,
historyTableName);

/// <summary>
/// Configures the table that the entity maps to when targeting SQL Server as temporal.
/// </summary>
/// <param name="entityTypeBuilder"> The builder for the entity type being configured. </param>
/// <param name="periodStartPropertyName"> A value specifying the property name representing start of the period.</param>
/// <param name="periodEndPropertyName"> A value specifying the property name representing end of the period.</param>
/// <param name="historyTableName"> A value specifying the history table name for this entity. Default name will be used if none is specified. </param>
/// <param name="fromDataAnnotation"> Indicates whether the configuration was specified using a data annotation. </param>
/// <returns>
/// The same builder instance if the configuration was applied,
/// <see langword="null" /> otherwise.
/// </returns>
public static IConventionEntityTypeBuilder? IsTemporal(
this IConventionEntityTypeBuilder entityTypeBuilder,
string periodStartPropertyName,
string periodEndPropertyName,
string? historyTableName = null,
bool fromDataAnnotation = false)
{
if (entityTypeBuilder.CanSetIsTemporal(periodStartPropertyName, periodEndPropertyName, historyTableName, fromDataAnnotation))
{
entityTypeBuilder.Metadata.SetIsTemporal(periodStartPropertyName, periodEndPropertyName, historyTableName, fromDataAnnotation);

return entityTypeBuilder;
}

return null;
}

/// <summary>
/// Returns a value indicating whether the mapped table can be configured as memory-optimized.
/// </summary>
/// <param name="entityTypeBuilder"> The builder for the entity type being configured. </param>
/// <param name="periodStartPropertyName"> A value specifying the property name representing start of the period.</param>
/// <param name="periodEndPropertyName"> A value specifying the property name representing end of the period.</param>
/// <param name="historyTableName"> A value specifying the history table name for this entity. Default name will be used if none is specified. </param>
/// <param name="fromDataAnnotation"> Indicates whether the configuration was specified using a data annotation. </param>
/// <returns> <see langword="true" /> if the mapped table can be configured as memory-optimized. </returns>
public static bool CanSetIsTemporal(
this IConventionEntityTypeBuilder entityTypeBuilder,
string periodStartPropertyName,
string periodEndPropertyName,
string? historyTableName = null,
bool fromDataAnnotation = false)
{
Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder));

var value = new SqlServerTemporalTableTransientAnnotationValue(periodStartPropertyName, periodEndPropertyName, historyTableName);

return entityTypeBuilder.CanSetAnnotation(SqlServerAnnotationNames.Temporal, value, fromDataAnnotation);
}
}
}
45 changes: 45 additions & 0 deletions src/EFCore.SqlServer/Extensions/SqlServerEntityTypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,50 @@ public static void SetIsMemoryOptimized(this IMutableEntityType entityType, bool
/// <returns> The configuration source for the memory-optimized setting. </returns>
public static ConfigurationSource? GetIsMemoryOptimizedConfigurationSource(this IConventionEntityType entityType)
=> entityType.FindAnnotation(SqlServerAnnotationNames.MemoryOptimized)?.GetConfigurationSource();

/// <summary>
/// Returns a value indicating whether the entity type is mapped to a temporal table.
/// </summary>
/// <param name="entityType"> The entity type. </param>
/// <returns> <see langword="true" /> if the entity type is mapped to a temporal table. </returns>
public static bool IsTemporal(this IReadOnlyEntityType entityType)
=> entityType[SqlServerAnnotationNames.Temporal] is SqlServerTemporalTableTransientAnnotationValue
|| entityType[SqlServerAnnotationNames.Temporal] is SqlServerTemporalTableAnnotationValue;

/// <summary>
/// Sets a value indicating that the entity type is mapped to a temporal table and relevant mapping configuration.
/// </summary>
/// <param name="entityType"> The entity type. </param>
/// <param name="temporal"> The value to set. </param>
public static void SetIsTemporal(this IMutableEntityType entityType, SqlServerTemporalTableTransientAnnotationValue temporal)
=> entityType.SetOrRemoveAnnotation(SqlServerAnnotationNames.Temporal, temporal);

/// <summary>
/// Sets a value indicating that the entity type is mapped to a temporal table and relevant mapping configuration.
/// </summary>
/// <param name="entityType"> The entity type. </param>
/// <param name="periodStartPropertyName"> A value specifying the property name representing start of the period.</param>
/// <param name="periodEndPropertyName"> A value specifying the property name representing end of the period.</param>
/// <param name="historyTableName"> A value specifying the history table name for this entity. Default name will be used if none is specified. </param>
/// <param name="fromDataAnnotation"> Indicates whether the configuration was specified using a data annotation. </param>
public static void SetIsTemporal(
this IConventionEntityType entityType,
string periodStartPropertyName,
string periodEndPropertyName,
string? historyTableName = null,
bool fromDataAnnotation = false)
{
// TODO: maumar - do we need to return value here?
var value = new SqlServerTemporalTableTransientAnnotationValue(periodStartPropertyName, periodEndPropertyName, historyTableName);
entityType.SetOrRemoveAnnotation(SqlServerAnnotationNames.Temporal, value, fromDataAnnotation);
}

/// <summary>
/// Gets the configuration source for the temporal table setting.
/// </summary>
/// <param name="entityType"> The entity type. </param>
/// <returns> The configuration source for the temporal table setting. </returns>
public static ConfigurationSource? GetIsTemporalConfigurationSource(this IConventionEntityType entityType)
=> entityType.FindAnnotation(SqlServerAnnotationNames.Temporal)?.GetConfigurationSource();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,12 @@ protected override void ValidateSharedTableCompatibility(
{
var firstMappedType = mappedTypes[0];
var isMemoryOptimized = firstMappedType.IsMemoryOptimized();
var firstTemporalAnnotation = firstMappedType.FindAnnotation(SqlServerAnnotationNames.Temporal);
if (firstTemporalAnnotation != null
&& firstTemporalAnnotation.Value is SqlServerTemporalTableTransientAnnotationValue)
{
throw new InvalidOperationException("Annotation should not be transient at this point.");
}

foreach (var otherMappedType in mappedTypes.Skip(1))
{
Expand All @@ -234,6 +240,19 @@ protected override void ValidateSharedTableCompatibility(
isMemoryOptimized ? firstMappedType.DisplayName() : otherMappedType.DisplayName(),
!isMemoryOptimized ? firstMappedType.DisplayName() : otherMappedType.DisplayName()));
}

var temporalAnnotation = otherMappedType.FindAnnotation(SqlServerAnnotationNames.Temporal);
if (temporalAnnotation is null != firstTemporalAnnotation is null)
{
throw new InvalidOperationException("Temporal annotation missing on root or derived.");
}

if (temporalAnnotation is not null
&& firstTemporalAnnotation is not null
&& temporalAnnotation.Value != firstTemporalAnnotation.Value)
{
throw new InvalidOperationException("Temporal annotation values are different between root and derived.");
}
}

base.ValidateSharedTableCompatibility(mappedTypes, tableName, schema, logger);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ public override ConventionSet CreateConventionSet()
(SharedTableConvention)new SqlServerSharedTableConvention(Dependencies, RelationalDependencies));
conventionSet.ModelFinalizingConventions.Add(new SqlServerDbFunctionConvention(Dependencies, RelationalDependencies));

var temporalConvention = new SqlServerTemporalConvention();
conventionSet.ModelFinalizingConventions.Add(temporalConvention);

return conventionSet;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Linq;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal;

namespace Microsoft.EntityFrameworkCore.Metadata.Conventions
{
/// <summary>
/// TODO: add comments
/// </summary>
public class SqlServerTemporalConvention : IModelFinalizingConvention
{
/// <summary>
/// TODO: add comments
/// </summary>
public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context)
{
// TODO: is this the right place to do all the tweaks?
foreach (var rootEntityType in modelBuilder.Metadata.GetEntityTypes().Select(et => et.GetRootType()).Distinct())
{
if (rootEntityType[SqlServerAnnotationNames.Temporal] is SqlServerTemporalTableTransientAnnotationValue transientValue)
//if (rootEntityType.FindAnnotation(SqlServerAnnotationNames.Temporal) is IConventionAnnotation annotation
// && annotation.Value is SqlServerTemporalTableTransientAnnotationValue transientValue)
{
var startProperty = rootEntityType.FindProperty(transientValue.PeriodStartPropertyName);
var startColumn = startProperty!.GetColumnBaseName();
startProperty!.SetValueGenerated(ValueGenerated.OnAddOrUpdate);
var endProperty = rootEntityType.FindProperty(transientValue.PeriodEndPropertyName);
endProperty!.SetValueGenerated(ValueGenerated.OnAddOrUpdate);
var endColumn = endProperty!.GetColumnBaseName();

var finalValue = new SqlServerTemporalTableAnnotationValue(startColumn, endColumn, transientValue.HistoryTableName);
rootEntityType.SetAnnotation(SqlServerAnnotationNames.Temporal, finalValue);
}
}

foreach (var entityType in modelBuilder.Metadata.GetEntityTypes())
{
if (entityType != entityType.GetRootType()
&& entityType.GetRootType()[SqlServerAnnotationNames.Temporal] is SqlServerTemporalTableAnnotationValue rootAnnotationValue
//&& entityType.GetRootType().FindAnnotation(SqlServerAnnotationNames.Temporal) is IConventionAnnotation rootAnnotation
//&& rootAnnotation.Value is SqlServerTemporalTableAnnotationValue rootAnnotationValue
//&& entityType.FindAnnotation(SqlServerAnnotationNames.Temporal) == null)
&& entityType[SqlServerAnnotationNames.Temporal] != null)
{
entityType.SetAnnotation(SqlServerAnnotationNames.Temporal, rootAnnotationValue);
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,14 @@ public static class SqlServerAnnotationNames
/// </summary>
public const string Sparse = Prefix + "Sparse";

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public const string Temporal = Prefix + "Temporal";

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,13 @@ public override IEnumerable<IAnnotation> For(ITable table)
{
yield return new Annotation(SqlServerAnnotationNames.MemoryOptimized, true);
}

if (table.EntityTypeMappings.First().EntityType.IsTemporal())
{
var value = table.EntityTypeMappings.First().EntityType[SqlServerAnnotationNames.Temporal];

yield return new Annotation(SqlServerAnnotationNames.Temporal, value);
}
}

/// <summary>
Expand Down Expand Up @@ -192,6 +199,17 @@ public override IEnumerable<IAnnotation> For(IColumn column)
{
yield return new Annotation(SqlServerAnnotationNames.Sparse, isSparse);
}

var entityType = column.Table.EntityTypeMappings.First().EntityType;
if (entityType.IsTemporal()
&& entityType[SqlServerAnnotationNames.Temporal] is SqlServerTemporalTableAnnotationValue annotationValue
&& (column.Name == annotationValue.PeriodStartColumnName || column.Name == annotationValue.PeriodEndColumnName))
{
// TODO: should we use dedicated annotations for these?
yield return new Annotation(
SqlServerAnnotationNames.Temporal,
new SqlServerTemporalTableAnnotationValue(annotationValue.PeriodStartColumnName, annotationValue.PeriodEndColumnName, annotationValue.HistoryTableName));
}
}
}
}
Loading

0 comments on commit fa19711

Please sign in to comment.