From b65bb528d4a946b50a0d8b884b988cbf4c5b7d27 Mon Sep 17 00:00:00 2001 From: "Isaac Wilson (B406)" Date: Mon, 1 Feb 2021 08:38:11 -0600 Subject: [PATCH 1/3] Added column specifications to queries Added ForAll query to get all temporal entities --- .editorconfig | 110 ++++++++++++++++++ .../Extensions/DbSetExtensions.cs | 56 ++++++++- 2 files changed, 162 insertions(+), 4 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..595aa64 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,110 @@ +# Rules in this file were initially inferred by Visual Studio IntelliCode from the C:\Users\B406\source\repos\Sandbox\EntityFrameworkCore.TemporalTables codebase based on best match to current usage at 02/01/2021 +# You can modify the rules from these initially generated values to suit your own policies +# You can learn more about editorconfig here: https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference +[*.cs] + + +#Core editorconfig formatting - indentation + +#use soft tabs (spaces) for indentation +indent_style = space +indent_size = 4 + +#Formatting - new line options + +#place else statements on a new line +csharp_new_line_before_else = true +#require members of anonymous types to be on separate lines +csharp_new_line_before_members_in_anonymous_types = true +#require members of object initializers to be on the same line +csharp_new_line_before_members_in_object_initializers = false +#require braces to be on a new line for methods, lambdas, types, and control_blocks (also known as "Allman" style) +csharp_new_line_before_open_brace = methods, lambdas, types, control_blocks + +#Formatting - organize using options + +#do not place System.* using directives before other using directives +dotnet_sort_system_directives_first = false + +#Formatting - spacing options + +#require a space before the colon for bases or interfaces in a type declaration +csharp_space_after_colon_in_inheritance_clause = true +#require a space after a keyword in a control flow statement such as a for loop +csharp_space_after_keywords_in_control_flow_statements = true +#require a space before the colon for bases or interfaces in a type declaration +csharp_space_before_colon_in_inheritance_clause = true +#remove space within empty argument list parentheses +csharp_space_between_method_call_empty_parameter_list_parentheses = false +#remove space between method call name and opening parenthesis +csharp_space_between_method_call_name_and_opening_parenthesis = false +#do not place space characters after the opening parenthesis and before the closing parenthesis of a method call +csharp_space_between_method_call_parameter_list_parentheses = false +#remove space within empty parameter list parentheses for a method declaration +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +#place a space character after the opening parenthesis and before the closing parenthesis of a method declaration parameter list. +csharp_space_between_method_declaration_parameter_list_parentheses = false + +#Formatting - wrapping options + +#leave code block on single line +csharp_preserve_single_line_blocks = true + +#Style - Code block preferences + +#prefer curly braces even for one line of code +csharp_prefer_braces = true:suggestion + +#Style - expression bodied member options + +#prefer block bodies for constructors +csharp_style_expression_bodied_constructors = false:suggestion +#prefer block bodies for methods +csharp_style_expression_bodied_methods = false:suggestion + +#Style - expression level options + +#prefer out variables to be declared inline in the argument list of a method call when possible +csharp_style_inlined_variable_declaration = true:suggestion +#prefer the language keyword for member access expressions, instead of the type name, for types that have a keyword to represent them +dotnet_style_predefined_type_for_member_access = true:suggestion + +#Style - Expression-level preferences + +#prefer default over default(T) +csharp_prefer_simple_default_expression = true:suggestion +#prefer inferred anonymous type member names +dotnet_style_prefer_inferred_anonymous_type_member_names = false:suggestion + +#Style - implicit and explicit types + +#prefer var over explicit type in all cases, unless overridden by another code style rule +csharp_style_var_elsewhere = true:suggestion +#prefer explicit type over var to declare variables with built-in system types such as int +csharp_style_var_for_built_in_types = false:suggestion +#prefer explicit type over var when the type is already mentioned on the right-hand side of a declaration +csharp_style_var_when_type_is_apparent = false:suggestion + +#Style - language keyword and framework type options + +#prefer the language keyword for local variables, method parameters, and class members, instead of the type name, for types that have a keyword to represent them +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion + +#Style - modifier options + +#prefer accessibility modifiers to be declared except for public interface members. This will currently not differ from always and will act as future proofing for if C# adds default interface methods. +dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion + +#Style - Modifier preferences + +#when this rule is set to a list of modifiers, prefer the specified ordering. +csharp_preferred_modifier_order = public,private,protected,internal,static,readonly,override,abstract,async:suggestion + +#Style - qualification options + +#prefer fields not to be prefaced with this. or Me. in Visual Basic +dotnet_style_qualification_for_field = false:suggestion +#prefer methods not to be prefaced with this. or Me. in Visual Basic +dotnet_style_qualification_for_method = false:suggestion +#prefer properties not to be prefaced with this. or Me. in Visual Basic +dotnet_style_qualification_for_property = false:suggestion diff --git a/EntityFrameworkCore.TemporalTables/Extensions/DbSetExtensions.cs b/EntityFrameworkCore.TemporalTables/Extensions/DbSetExtensions.cs index c4d4dc0..bca06d5 100644 --- a/EntityFrameworkCore.TemporalTables/Extensions/DbSetExtensions.cs +++ b/EntityFrameworkCore.TemporalTables/Extensions/DbSetExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using EntityFrameworkCore.TemporalTables.Cache; using Microsoft.EntityFrameworkCore; @@ -20,8 +21,11 @@ public static IQueryable AsOf(this DbSet dbSet, DateT where TEntity : class { ValidateDbSet(dbSet); + var PropertyNames = dbSet.GetMappedProperties(); + PropertyNames = PropertyNames.IncludePeriodProperties(); + var propertyNamesClause = string.Join(", ", PropertyNames); - var sql = FormattableString.Invariant($"SELECT * FROM [{GetTableName(dbSet)}] FOR SYSTEM_TIME AS OF {{0}}"); + var sql = FormattableString.Invariant($"SELECT {propertyNamesClause} FROM [{GetTableName(dbSet)}] FOR SYSTEM_TIME AS OF {{0}}"); return dbSet.FromSqlRaw(sql, date).AsNoTracking(); } @@ -36,8 +40,11 @@ public static IQueryable AsOf(this DbSet dbSet, DateT where TEntity : class { ValidateDbSet(dbSet); + var PropertyNames = dbSet.GetMappedProperties(); + PropertyNames = PropertyNames.IncludePeriodProperties(); + var propertyNamesClause = string.Join(", ", PropertyNames); - var sql = FormattableString.Invariant($"SELECT * FROM [{GetTableName(dbSet)}] FOR SYSTEM_TIME AS OF {{0}}"); + var sql = FormattableString.Invariant($"SELECT {propertyNamesClause} FROM [{GetTableName(dbSet)}] FOR SYSTEM_TIME AS OF {{0}}"); return dbSet.FromSqlRaw(sql, dateTimeOffset).AsNoTracking(); } @@ -55,8 +62,11 @@ public static IQueryable Between(this DbSet dbSet, Da where TEntity : class { ValidateDbSet(dbSet); + var PropertyNames = dbSet.GetMappedProperties(); + PropertyNames = PropertyNames.IncludePeriodProperties(); + var propertyNamesClause = string.Join(", ", PropertyNames); - var sql = FormattableString.Invariant($"SELECT * FROM [{GetTableName(dbSet)}] FOR SYSTEM_TIME BETWEEN {{0}} AND {{1}}"); + var sql = FormattableString.Invariant($"SELECT {propertyNamesClause} FROM [{GetTableName(dbSet)}] FOR SYSTEM_TIME BETWEEN {{0}} AND {{1}}"); return dbSet.FromSqlRaw(sql, startDate, endDate).AsNoTracking(); } @@ -74,11 +84,32 @@ public static IQueryable Between(this DbSet dbSet, Da where TEntity : class { ValidateDbSet(dbSet); + var PropertyNames = dbSet.GetMappedProperties(); + PropertyNames = PropertyNames.IncludePeriodProperties(); + var propertyNamesClause = string.Join(", ", PropertyNames); - var sql = FormattableString.Invariant($"SELECT * FROM [{GetTableName(dbSet)}] FOR SYSTEM_TIME BETWEEN {{0}} AND {{1}}"); + var sql = FormattableString.Invariant($"SELECT {propertyNamesClause} FROM [{GetTableName(dbSet)}] FOR SYSTEM_TIME BETWEEN {{0}} AND {{1}}"); return dbSet.FromSqlRaw(sql, startDateOffset, endDateOffset).AsNoTracking(); } + /// + /// Selects the entities for all time periods + /// + /// The type of the entity. + /// The database set. + /// All of the for all time-periods + public static IQueryable ForAll(this DbSet dbSet) + where TEntity : class + { + ValidateDbSet(dbSet); + var PropertyNames = dbSet.GetMappedProperties(); + PropertyNames = PropertyNames.IncludePeriodProperties(); + var propertyNamesClause = string.Join(", ", PropertyNames); + + var sql = FormattableString.Invariant($"SELECT {propertyNamesClause} FROM [{GetTableName(dbSet)}] FOR SYSTEM_TIME ALL"); + return dbSet.FromSqlRaw(sql).AsNoTracking(); + } + /// /// Validates that temporal tables are enabled on the provided data set. /// @@ -139,5 +170,22 @@ private static DbContext GetDbContext(DbSet dbSet) var currentDbContext = serviceProvider.GetService(typeof(ICurrentDbContext)) as ICurrentDbContext; return currentDbContext.Context; } + + private static IEnumerable GetMappedProperties(this DbSet dbSet) + where TEntity : class + { + var propertyNames = dbSet + .GetService() + .Context.Model.FindEntityType(typeof(TEntity)) + .GetProperties() + .Where(s => !string.IsNullOrWhiteSpace(s.GetColumnName())) + .Select(p => $"[{p.GetColumnName()}]"); + return propertyNames; + } + + private static IEnumerable IncludePeriodProperties(this IEnumerable properties) + { + return properties.Union(new string[] { "[SysStartTime]", "[SysEndTime]" }).Distinct(); + } } } From b50a85c58929a2aae474bdf8a24ad019d11f5a0e Mon Sep 17 00:00:00 2001 From: "Isaac Wilson (B406)" Date: Mon, 1 Feb 2021 09:01:27 -0600 Subject: [PATCH 2/3] Added ContainedIn queries --- .../Extensions/DbSetExtensions.cs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/EntityFrameworkCore.TemporalTables/Extensions/DbSetExtensions.cs b/EntityFrameworkCore.TemporalTables/Extensions/DbSetExtensions.cs index bca06d5..019fa92 100644 --- a/EntityFrameworkCore.TemporalTables/Extensions/DbSetExtensions.cs +++ b/EntityFrameworkCore.TemporalTables/Extensions/DbSetExtensions.cs @@ -92,6 +92,50 @@ public static IQueryable Between(this DbSet dbSet, Da return dbSet.FromSqlRaw(sql, startDateOffset, endDateOffset).AsNoTracking(); } + /// + /// Filters the DbSet with entities between the provided dates. + /// + /// The type of the entity. + /// The database set. + /// The start date. + /// The end date. + /// The entities found between the provided dates. + /// The same entity might be returned more than once if it was modified + /// during that time frame. + public static IQueryable ContainedIn(this DbSet dbSet, DateTime startDate, DateTime endDate) + where TEntity : class + { + ValidateDbSet(dbSet); + var PropertyNames = dbSet.GetMappedProperties(); + PropertyNames = PropertyNames.IncludePeriodProperties(); + var propertyNamesClause = string.Join(", ", PropertyNames); + + var sql = FormattableString.Invariant($"SELECT {propertyNamesClause} FROM [{GetTableName(dbSet)}] FOR SYSTEM_TIME CONTAINED IN ({{0}}, {{1}})"); + return dbSet.FromSqlRaw(sql, startDate, endDate).AsNoTracking(); + } + + /// + /// Filters the DbSet with entities between the provided dates. + /// + /// The type of the entity. + /// The database set. + /// The start date time offset. + /// The end date time offset. + /// The entities found between the provided dates. + /// The same entity might be returned more than once if it was modified + /// during that time frame. + public static IQueryable ContainedIn(this DbSet dbSet, DateTimeOffset startDateOffset, DateTimeOffset endDateOffset) + where TEntity : class + { + ValidateDbSet(dbSet); + var PropertyNames = dbSet.GetMappedProperties(); + PropertyNames = PropertyNames.IncludePeriodProperties(); + var propertyNamesClause = string.Join(", ", PropertyNames); + + var sql = FormattableString.Invariant($"SELECT {propertyNamesClause} FROM [{GetTableName(dbSet)}] FOR SYSTEM_TIME CONTAINED IN ({{0}}, {{1}})"); + return dbSet.FromSqlRaw(sql, startDateOffset, endDateOffset).AsNoTracking(); + } + /// /// Selects the entities for all time periods /// From 3168fd221878cace55a1d2e6b04bda9eac2b4a3d Mon Sep 17 00:00:00 2001 From: "Isaac Wilson (B406)" Date: Mon, 1 Feb 2021 09:31:31 -0600 Subject: [PATCH 3/3] Fixed Obsolete GetColumnName - I hope --- .../Extensions/DbSetExtensions.cs | 64 +++++++++++-------- 1 file changed, 39 insertions(+), 25 deletions(-) diff --git a/EntityFrameworkCore.TemporalTables/Extensions/DbSetExtensions.cs b/EntityFrameworkCore.TemporalTables/Extensions/DbSetExtensions.cs index 019fa92..f8f4c42 100644 --- a/EntityFrameworkCore.TemporalTables/Extensions/DbSetExtensions.cs +++ b/EntityFrameworkCore.TemporalTables/Extensions/DbSetExtensions.cs @@ -21,8 +21,7 @@ public static IQueryable AsOf(this DbSet dbSet, DateT where TEntity : class { ValidateDbSet(dbSet); - var PropertyNames = dbSet.GetMappedProperties(); - PropertyNames = PropertyNames.IncludePeriodProperties(); + var PropertyNames = dbSet.GetMappedPropertiesWithPeriodProperties(); var propertyNamesClause = string.Join(", ", PropertyNames); var sql = FormattableString.Invariant($"SELECT {propertyNamesClause} FROM [{GetTableName(dbSet)}] FOR SYSTEM_TIME AS OF {{0}}"); @@ -40,8 +39,7 @@ public static IQueryable AsOf(this DbSet dbSet, DateT where TEntity : class { ValidateDbSet(dbSet); - var PropertyNames = dbSet.GetMappedProperties(); - PropertyNames = PropertyNames.IncludePeriodProperties(); + var PropertyNames = dbSet.GetMappedPropertiesWithPeriodProperties(); var propertyNamesClause = string.Join(", ", PropertyNames); var sql = FormattableString.Invariant($"SELECT {propertyNamesClause} FROM [{GetTableName(dbSet)}] FOR SYSTEM_TIME AS OF {{0}}"); @@ -62,8 +60,7 @@ public static IQueryable Between(this DbSet dbSet, Da where TEntity : class { ValidateDbSet(dbSet); - var PropertyNames = dbSet.GetMappedProperties(); - PropertyNames = PropertyNames.IncludePeriodProperties(); + var PropertyNames = dbSet.GetMappedPropertiesWithPeriodProperties(); var propertyNamesClause = string.Join(", ", PropertyNames); var sql = FormattableString.Invariant($"SELECT {propertyNamesClause} FROM [{GetTableName(dbSet)}] FOR SYSTEM_TIME BETWEEN {{0}} AND {{1}}"); @@ -84,8 +81,7 @@ public static IQueryable Between(this DbSet dbSet, Da where TEntity : class { ValidateDbSet(dbSet); - var PropertyNames = dbSet.GetMappedProperties(); - PropertyNames = PropertyNames.IncludePeriodProperties(); + var PropertyNames = dbSet.GetMappedPropertiesWithPeriodProperties(); var propertyNamesClause = string.Join(", ", PropertyNames); var sql = FormattableString.Invariant($"SELECT {propertyNamesClause} FROM [{GetTableName(dbSet)}] FOR SYSTEM_TIME BETWEEN {{0}} AND {{1}}"); @@ -106,8 +102,7 @@ public static IQueryable ContainedIn(this DbSet dbSet where TEntity : class { ValidateDbSet(dbSet); - var PropertyNames = dbSet.GetMappedProperties(); - PropertyNames = PropertyNames.IncludePeriodProperties(); + var PropertyNames = dbSet.GetMappedPropertiesWithPeriodProperties(); var propertyNamesClause = string.Join(", ", PropertyNames); var sql = FormattableString.Invariant($"SELECT {propertyNamesClause} FROM [{GetTableName(dbSet)}] FOR SYSTEM_TIME CONTAINED IN ({{0}}, {{1}})"); @@ -128,8 +123,7 @@ public static IQueryable ContainedIn(this DbSet dbSet where TEntity : class { ValidateDbSet(dbSet); - var PropertyNames = dbSet.GetMappedProperties(); - PropertyNames = PropertyNames.IncludePeriodProperties(); + var PropertyNames = dbSet.GetMappedPropertiesWithPeriodProperties(); var propertyNamesClause = string.Join(", ", PropertyNames); var sql = FormattableString.Invariant($"SELECT {propertyNamesClause} FROM [{GetTableName(dbSet)}] FOR SYSTEM_TIME CONTAINED IN ({{0}}, {{1}})"); @@ -146,8 +140,7 @@ public static IQueryable ForAll(this DbSet dbSet) where TEntity : class { ValidateDbSet(dbSet); - var PropertyNames = dbSet.GetMappedProperties(); - PropertyNames = PropertyNames.IncludePeriodProperties(); + var PropertyNames = dbSet.GetMappedPropertiesWithPeriodProperties(); var propertyNamesClause = string.Join(", ", PropertyNames); var sql = FormattableString.Invariant($"SELECT {propertyNamesClause} FROM [{GetTableName(dbSet)}] FOR SYSTEM_TIME ALL"); @@ -215,21 +208,42 @@ private static DbContext GetDbContext(DbSet dbSet) return currentDbContext.Context; } - private static IEnumerable GetMappedProperties(this DbSet dbSet) + private static IEnumerable GetMappedPropertiesWithPeriodProperties(this DbSet dbSet) where TEntity : class { - var propertyNames = dbSet + //TODO: these columns should be customizable, and should only be added if the period columns are not already mapped from the existing EF configuration + string SysStartTime = "[SysStartTime]"; + string SysEndTime = "[SysEndTime]"; + + IEnumerable propertyNames = null; + + var entityType = dbSet .GetService() - .Context.Model.FindEntityType(typeof(TEntity)) - .GetProperties() - .Where(s => !string.IsNullOrWhiteSpace(s.GetColumnName())) - .Select(p => $"[{p.GetColumnName()}]"); - return propertyNames; - } + .Context.Model.FindEntityType(typeof(TEntity)); - private static IEnumerable IncludePeriodProperties(this IEnumerable properties) - { - return properties.Union(new string[] { "[SysStartTime]", "[SysEndTime]" }).Distinct(); + //TODO: I'm not sure how this will work with Owned entities or TPT/TPH + StoreObjectIdentifier? so = StoreObjectIdentifier.Create(entityType, StoreObjectType.Table); + + if (so.HasValue) + { + propertyNames = entityType + .GetProperties() + .Where(s => !string.IsNullOrWhiteSpace(s.GetColumnName(so.Value))) + .Select(p => $"[{p.GetColumnName(so.Value)}]"); + + var isOwned = dbSet + .GetService() + .Context.Model.FindEntityType(typeof(TEntity)) + .IsOwned(); + + //Don't add columns if + if (!isOwned) + { + propertyNames = propertyNames.Union(new string[] { SysStartTime, SysEndTime }).Distinct(); + } + } + + return propertyNames; } } }