diff --git a/samples/efcpt-config.schema.json b/samples/efcpt-config.schema.json index ecc58640c..8dd077fa0 100644 --- a/samples/efcpt-config.schema.json +++ b/samples/efcpt-config.schema.json @@ -217,6 +217,11 @@ "type": "boolean", "title": "Customize code using T4 templates" }, + "use-t4-split": { + "type": "boolean", + "default": false, + "title": "Customize code using T4 templates including EntityTypeConfiguration.t4. This cannot be used in combination with use-t4 or split-dbcontext-preview" + }, "remove-defaultsql-from-bool-properties": { "type": "boolean", "title": "Remove SQL default from bool columns to avoid them being bool?" diff --git a/samples/efcpt-schema.json b/samples/efcpt-schema.json index 822354497..b4824c2c6 100644 --- a/samples/efcpt-schema.json +++ b/samples/efcpt-schema.json @@ -377,6 +377,14 @@ false ] }, + "use-t4-split": { + "type": "boolean", + "default": false, + "title": "Customize code using T4 templates including EntityTypeConfiguration.t4. This cannot be used in combination with use-t4 or split-dbcontext-preview", + "examples": [ + false + ] + }, "t4-template-path": { "type": [ "string", "null" ] , "default": null, diff --git a/src/Core/RevEng.Core.60/IReverseEngineerScaffolder.cs b/src/Core/RevEng.Core.60/IReverseEngineerScaffolder.cs index 4eccaafe8..e399862ef 100644 --- a/src/Core/RevEng.Core.60/IReverseEngineerScaffolder.cs +++ b/src/Core/RevEng.Core.60/IReverseEngineerScaffolder.cs @@ -6,7 +6,7 @@ namespace RevEng.Core { public interface IReverseEngineerScaffolder { - SavedModelFiles GenerateDbContext(ReverseEngineerCommandOptions options, List schemas, string outputContextDir, string modelNamespace, string contextNamespace, string projectPath, string outputPath); + SavedModelFiles GenerateDbContext(ReverseEngineerCommandOptions options, List schemas, string outputContextDir, string modelNamespace, string contextNamespace, string projectPath, string outputPath, string rootNameSpace); SavedModelFiles GenerateFunctions(ReverseEngineerCommandOptions options, List schemas, ref List errors, string outputContextDir, string modelNamespace, string contextNamespace, bool supportsFunctions); SavedModelFiles GenerateStoredProcedures(ReverseEngineerCommandOptions options, List schemas, ref List errors, string outputContextDir, string modelNamespace, string contextNamespace, bool supportsProcedures); } diff --git a/src/Core/RevEng.Core.60/ReverseEngineerRunner.cs b/src/Core/RevEng.Core.60/ReverseEngineerRunner.cs index 955abcc5c..4fa1e33b7 100644 --- a/src/Core/RevEng.Core.60/ReverseEngineerRunner.cs +++ b/src/Core/RevEng.Core.60/ReverseEngineerRunner.cs @@ -83,14 +83,14 @@ public static ReverseEngineerResult GenerateFiles(ReverseEngineerCommandOptions try { - SavedModelFiles filePaths = scaffolder!.GenerateDbContext(options, schemas, outputContextDir, modelNamespace, contextNamespace, options.ProjectPath, options.OutputPath); + SavedModelFiles filePaths = scaffolder!.GenerateDbContext(options, schemas, outputContextDir, modelNamespace, contextNamespace, options.ProjectPath, options.OutputPath, options.ProjectRootNamespace); #if CORE70 || CORE80 - if (options.UseT4) + if (options.UseT4 || options.UseT4Split) { foreach (var paths in GetAlternateCodeTemplatePaths(options.ProjectPath)) { - scaffolder!.GenerateDbContext(options, schemas, paths.Path, modelNamespace, contextNamespace, paths.Path, paths.OutputPath); + scaffolder!.GenerateDbContext(options, schemas, paths.Path, modelNamespace, contextNamespace, paths.Path, paths.OutputPath, options.ProjectRootNamespace); } } #endif @@ -139,12 +139,17 @@ public static ReverseEngineerResult GenerateFiles(ReverseEngineerCommandOptions } RemoveFragments(filePaths.ContextFile, options.ContextClassName, options.IncludeConnectionString, options.UseNoDefaultConstructor); - if (!options.UseHandleBars && !options.UseT4) + if (!options.UseHandleBars && !options.UseT4 && !options.UseT4Split) { PostProcess(filePaths.ContextFile, options.UseNullableReferences); } entityTypeConfigurationPaths = SplitDbContext(filePaths.ContextFile, options.UseDbContextSplitting, contextNamespace, options.UseNullableReferences, options.ContextClassName); + + if (options.UseT4Split) + { + entityTypeConfigurationPaths.AddRange(MoveConfigurationFiles(filePaths.AdditionalFiles)); + } } else if (options.Tables.Exists(t => t.ObjectType == ObjectType.Procedure) || options.Tables.Exists(t => t.ObjectType == ObjectType.ScalarFunction)) @@ -152,7 +157,7 @@ public static ReverseEngineerResult GenerateFiles(ReverseEngineerCommandOptions warnings.Add("Selected stored procedures/scalar functions will not be generated, as 'Entity Types only' was selected"); } - if (!options.UseHandleBars && !options.UseT4) + if (!options.UseHandleBars && !options.UseT4 && !options.UseT4Split) { foreach (var file in filePaths.AdditionalFiles) { @@ -193,6 +198,18 @@ public static ReverseEngineerResult GenerateFiles(ReverseEngineerCommandOptions warnings.Add($"'use-database-names' / 'UseDatabaseNames' has been set to true, but a '{Constants.RenamingFileName}' file was also found. This prevents '{Constants.RenamingFileName}' from functioning."); } + if (options.UseT4 && options.UseT4Split) + { + warnings.Add("Both UseT4 and UseT4Split are set to true. Only one of thse should be used, UseT4Split will be ignored."); + options.UseT4Split = false; + } + + if (options.UseT4Split && options.UseDbContextSplitting) + { + warnings.Add("Both UseDbContextSplitting and UseT4Split are set to true. Only one of thse should be used, UseT4Split will be ignored."); + options.UseT4Split = false; + } + var result = new ReverseEngineerResult { EntityErrors = errors, @@ -323,6 +340,22 @@ private static List SplitDbContext(string contextFile, bool useDbContext return DbContextSplitter.Split(contextFile, contextNamespace, supportNullable, dbContextName); } + // If we didn't split, we might have used EntityTypeConfiguration.t4. In that case, Configuration.cs files were generated. + private static List MoveConfigurationFiles(IList files) + { + var configurationFiles = files.Where(x => x.EndsWith("Configuration.cs", StringComparison.InvariantCulture)).ToList(); + + var movedFiles = new List(); + foreach (var configurationFile in configurationFiles) + { + var newFileName = Path.Combine(Path.GetDirectoryName(configurationFile) ?? string.Empty, "Configurations", Path.GetFileName(configurationFile)); + File.Move(configurationFile, newFileName, overwrite: true); + movedFiles.Add(newFileName); + } + + return movedFiles; + } + private static void RemoveFragments(string contextFile, string contextName, bool includeConnectionString, bool removeDefaultConstructor) { if (string.IsNullOrEmpty(contextFile)) diff --git a/src/Core/RevEng.Core.60/ReverseEngineerScaffolder.cs b/src/Core/RevEng.Core.60/ReverseEngineerScaffolder.cs index 3e4c3156d..7f26ffa70 100644 --- a/src/Core/RevEng.Core.60/ReverseEngineerScaffolder.cs +++ b/src/Core/RevEng.Core.60/ReverseEngineerScaffolder.cs @@ -59,7 +59,8 @@ public SavedModelFiles GenerateDbContext( string modelNamespace, string contextNamespace, string projectPath, - string outputPath) + string outputPath, + string rootNameSpace) { ArgumentNullException.ThrowIfNull(options); @@ -77,7 +78,7 @@ public SavedModelFiles GenerateDbContext( ContextName = code.Identifier(options.ContextClassName), ContextDir = outputContextDir, - RootNamespace = null, + RootNamespace = rootNameSpace, ContextNamespace = contextNamespace, ModelNamespace = modelNamespace, SuppressConnectionStringWarning = false, @@ -85,7 +86,7 @@ public SavedModelFiles GenerateDbContext( SuppressOnConfiguring = !options.IncludeConnectionString, UseNullableReferenceTypes = options.UseNullableReferences, #if CORE70 || CORE80 - ProjectDir = options.UseT4 ? (options.T4TemplatePath ?? projectPath) : null, + ProjectDir = (options.UseT4 || options.UseT4Split) ? (options.T4TemplatePath ?? projectPath) : null, #endif }; diff --git a/src/Core/efcpt.8/HostedServices/ScaffoldHostedService.cs b/src/Core/efcpt.8/HostedServices/ScaffoldHostedService.cs index d24b9a84f..1ebf8008b 100644 --- a/src/Core/efcpt.8/HostedServices/ScaffoldHostedService.cs +++ b/src/Core/efcpt.8/HostedServices/ScaffoldHostedService.cs @@ -74,9 +74,9 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) #pragma warning disable S2589 // Boolean expressions should not be gratuitous #pragma warning disable S2583 // Conditionally executed code should be reachable - if (commandOptions.UseT4 && Constants.Version > 6) + if ((commandOptions.UseT4 || commandOptions.UseT4Split) && Constants.Version > 6) { - var t4Result = T4Helper.DropT4Templates(commandOptions.T4TemplatePath ?? commandOptions.ProjectPath, Constants.CodeGeneration); + var t4Result = T4Helper.DropT4Templates(commandOptions.T4TemplatePath ?? commandOptions.ProjectPath, Constants.CodeGeneration, commandOptions.UseT4Split); if (!string.IsNullOrEmpty(t4Result)) { DisplayService.MarkupLine(t4Result, Color.Default); diff --git a/src/Core/efcpt.8/efcpt.8.csproj b/src/Core/efcpt.8/efcpt.8.csproj index c0d6a210c..3e9da274d 100644 --- a/src/Core/efcpt.8/efcpt.8.csproj +++ b/src/Core/efcpt.8/efcpt.8.csproj @@ -34,6 +34,9 @@ Always + + Always + Always diff --git a/src/GUI/RevEng.Shared/Cli/CliConfigMapper.cs b/src/GUI/RevEng.Shared/Cli/CliConfigMapper.cs index 1fc43b2c0..a7cab8b42 100644 --- a/src/GUI/RevEng.Shared/Cli/CliConfigMapper.cs +++ b/src/GUI/RevEng.Shared/Cli/CliConfigMapper.cs @@ -72,6 +72,7 @@ public static ReverseEngineerCommandOptions ToCommandOptions( UseInflector = config.CodeGeneration.UseInflector, UseT4 = config.CodeGeneration.UseT4, T4TemplatePath = config.CodeGeneration.T4TemplatePath != null ? PathHelper.GetAbsPath(config.CodeGeneration.T4TemplatePath, projectPath) : null, + UseT4Split = config.CodeGeneration.UseT4Split, IncludeConnectionString = !isDacpac && config.CodeGeneration.EnableOnConfiguring, SelectedToBeGenerated = selectedToBeGenerated, Dacpac = isDacpac ? connectionString : null, diff --git a/src/GUI/RevEng.Shared/Cli/Configuration/CodeGeneration.cs b/src/GUI/RevEng.Shared/Cli/Configuration/CodeGeneration.cs index 5eec0069b..a983c0f7e 100644 --- a/src/GUI/RevEng.Shared/Cli/Configuration/CodeGeneration.cs +++ b/src/GUI/RevEng.Shared/Cli/Configuration/CodeGeneration.cs @@ -85,5 +85,9 @@ public class CodeGeneration [JsonPropertyOrder(200)] [JsonPropertyName("use-t4")] public bool UseT4 { get; set; } + + [JsonPropertyOrder(210)] + [JsonPropertyName("use-t4-split")] + public bool UseT4Split { get; set; } } } diff --git a/src/GUI/RevEng.Shared/Cli/T4Helper.cs b/src/GUI/RevEng.Shared/Cli/T4Helper.cs index 88245d965..505acfc61 100644 --- a/src/GUI/RevEng.Shared/Cli/T4Helper.cs +++ b/src/GUI/RevEng.Shared/Cli/T4Helper.cs @@ -8,7 +8,7 @@ namespace RevEng.Common.Cli { public static class T4Helper { - public static string DropT4Templates(string projectPath, CodeGenerationMode codeGenerationMode) + public static string DropT4Templates(string projectPath, CodeGenerationMode codeGenerationMode, bool useEntityTypeSplitting = false) { string t4Version = "703"; @@ -22,6 +22,11 @@ public static string DropT4Templates(string projectPath, CodeGenerationMode code t4Version = "900"; } + if (useEntityTypeSplitting) + { + t4Version += "_Split"; + } + var zipName = $"T4_{t4Version}.zip"; var toDir = Path.Combine(projectPath, "CodeTemplates"); @@ -57,6 +62,16 @@ public static string DropT4Templates(string projectPath, CodeGenerationMode code return error; } } + + target = Path.Combine(toDir, "EFCore", "EntityTypeConfiguration.t4"); + if (File.Exists(target)) + { + var content = File.ReadAllText(target, Encoding.UTF8); + if (content.IndexOf(check, StringComparison.OrdinalIgnoreCase) == -1) + { + return error; + } + } } return string.Empty; diff --git a/src/GUI/RevEng.Shared/ReverseEngineerCommandOptions.cs b/src/GUI/RevEng.Shared/ReverseEngineerCommandOptions.cs index 3e6e7da3d..58fdc887f 100644 --- a/src/GUI/RevEng.Shared/ReverseEngineerCommandOptions.cs +++ b/src/GUI/RevEng.Shared/ReverseEngineerCommandOptions.cs @@ -31,6 +31,7 @@ public class ReverseEngineerCommandOptions public bool UseHandleBars { get; set; } public bool UseT4 { get; set; } public string T4TemplatePath { get; set; } + public bool UseT4Split { get; set; } public int SelectedHandlebarsLanguage { get; set; } public bool IncludeConnectionString { get; set; } public int SelectedToBeGenerated { get; set; } diff --git a/src/GUI/RevEng.Shared/ReverseEngineerOptions.cs b/src/GUI/RevEng.Shared/ReverseEngineerOptions.cs index 8ae9ace66..d0ef79df2 100644 --- a/src/GUI/RevEng.Shared/ReverseEngineerOptions.cs +++ b/src/GUI/RevEng.Shared/ReverseEngineerOptions.cs @@ -29,6 +29,7 @@ public class ReverseEngineerOptions public List UncountableWords { get; set; } public bool UseHandleBars { get; set; } public bool UseT4 { get; set; } + public bool UseT4Split { get; set; } public int SelectedHandlebarsLanguage { get; set; } = 2; public bool IncludeConnectionString { get; set; } public int SelectedToBeGenerated { get; set; } diff --git a/src/GUI/Shared/Handlers/ReverseEngineer/EfRevEngLauncher.cs b/src/GUI/Shared/Handlers/ReverseEngineer/EfRevEngLauncher.cs index cae58c8fc..478e58a9a 100644 --- a/src/GUI/Shared/Handlers/ReverseEngineer/EfRevEngLauncher.cs +++ b/src/GUI/Shared/Handlers/ReverseEngineer/EfRevEngLauncher.cs @@ -95,6 +95,7 @@ public static async Task LaunchExternalRunnerAsync(Revers UseHandleBars = options.UseHandleBars, UseT4 = options.UseT4, T4TemplatePath = options.T4TemplatePath != null ? PathHelper.GetAbsPath(options.T4TemplatePath, options.ProjectPath) : null, + UseT4Split = options.UseT4Split, UseInflector = options.UseInflector, UseLegacyPluralizer = options.UseLegacyPluralizer, UncountableWords = options.UncountableWords, diff --git a/src/GUI/lib/T4_800_Split.zip b/src/GUI/lib/T4_800_Split.zip new file mode 100644 index 000000000..c3adb4330 Binary files /dev/null and b/src/GUI/lib/T4_800_Split.zip differ diff --git a/src/GUI/lib/T4_800_Split/EFCore/DbContext.t4 b/src/GUI/lib/T4_800_Split/EFCore/DbContext.t4 new file mode 100644 index 000000000..687457319 --- /dev/null +++ b/src/GUI/lib/T4_800_Split/EFCore/DbContext.t4 @@ -0,0 +1,362 @@ +<#@ template hostSpecific="true" #> +<#@ assembly name="Microsoft.EntityFrameworkCore" #> +<#@ assembly name="Microsoft.EntityFrameworkCore.Design" #> +<#@ assembly name="Microsoft.EntityFrameworkCore.Relational" #> +<#@ assembly name="Microsoft.Extensions.DependencyInjection.Abstractions" #> +<#@ parameter name="Model" type="Microsoft.EntityFrameworkCore.Metadata.IModel" #> +<#@ parameter name="Options" type="Microsoft.EntityFrameworkCore.Scaffolding.ModelCodeGenerationOptions" #> +<#@ parameter name="NamespaceHint" type="System.String" #> +<#@ import namespace="System.Collections.Generic" #> +<#@ import namespace="System.Linq" #> +<#@ import namespace="System.Text" #> +<#@ import namespace="Microsoft.EntityFrameworkCore" #> +<#@ import namespace="Microsoft.EntityFrameworkCore.Design" #> +<#@ import namespace="Microsoft.EntityFrameworkCore.Infrastructure" #> +<#@ import namespace="Microsoft.EntityFrameworkCore.Scaffolding" #> +<#@ import namespace="Microsoft.Extensions.DependencyInjection" #> +<# + // Template version: 800 - please do NOT remove this line + if (!ProductInfo.GetVersion().StartsWith("8.0")) + { + Warning("Your templates were created using an older version of Entity Framework. Additional features and bug fixes may be available. See https://aka.ms/efcore-docs-updating-templates for more information."); + } + + var services = (IServiceProvider)Host; + var providerCode = services.GetRequiredService(); + var annotationCodeGenerator = services.GetRequiredService(); + var code = services.GetRequiredService(); + + var usings = new List + { + "System", + "System.Collections.Generic", + "Microsoft.EntityFrameworkCore" + }; + + if (NamespaceHint != Options.ModelNamespace + && !string.IsNullOrEmpty(Options.ModelNamespace)) + { + usings.Add(Options.ModelNamespace); + } + + if (!string.IsNullOrEmpty(NamespaceHint)) + { +#> +namespace <#= NamespaceHint #>; + +<# + } +#> +public partial class <#= Options.ContextName #> : DbContext +{ +<# + if (!Options.SuppressOnConfiguring) + { +#> + public <#= Options.ContextName #>() + { + } + +<# + } +#> + public <#= Options.ContextName #>(DbContextOptions<<#= Options.ContextName #>> options) + : base(options) + { + } + +<# + foreach (var entityType in Model.GetEntityTypes().Where(e => !e.IsSimpleManyToManyJoinEntityType())) + { +#> + public virtual DbSet<<#= entityType.Name #>> <#= entityType.GetDbSetName() #> { get; set; } + +<# + } + + if (!Options.SuppressOnConfiguring) + { +#> + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) +<# + if (!Options.SuppressConnectionStringWarning) + { +#> +#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see http://go.microsoft.com/fwlink/?LinkId=723263. +<# + } +#> + => optionsBuilder<#= code.Fragment(providerCode.GenerateUseProvider(Options.ConnectionString), indent: 3) #>; + +<# + } + +#> + protected override void OnModelCreating(ModelBuilder modelBuilder) + { +<# + var anyConfiguration = false; + + var modelFluentApiCalls = Model.GetFluentApiCalls(annotationCodeGenerator); + if (modelFluentApiCalls != null) + { + usings.AddRange(modelFluentApiCalls.GetRequiredUsings()); +#> + modelBuilder<#= code.Fragment(modelFluentApiCalls, indent: 3) #>; +<# + anyConfiguration = true; + } + + StringBuilder mainEnvironment; + foreach (var entityType in Model.GetEntityTypes().Where(e => !e.IsSimpleManyToManyJoinEntityType())) + { +// Save all previously generated code, and start generating into a new temporary environment + mainEnvironment = GenerationEnvironment; + GenerationEnvironment = new StringBuilder(); + + if (anyConfiguration) + { + WriteLine(""); + } + + #> + modelBuilder.ApplyConfiguration(new Configurations.<#= entityType.Name #>Configuration()); +<# + anyConfiguration = true; + mainEnvironment.Append(GenerationEnvironment); + // Resume generating code into the main environment + GenerationEnvironment = mainEnvironment; + continue; + + var anyEntityTypeConfiguration = false; +#> + modelBuilder.Entity<<#= entityType.Name #>>(entity => + { +<# + var key = entityType.FindPrimaryKey(); + if (key != null) + { + var keyFluentApiCalls = key.GetFluentApiCalls(annotationCodeGenerator); + if (keyFluentApiCalls != null + || (!key.IsHandledByConvention() && !Options.UseDataAnnotations)) + { + if (keyFluentApiCalls != null) + { + usings.AddRange(keyFluentApiCalls.GetRequiredUsings()); + } +#> + entity.HasKey(<#= code.Lambda(key.Properties, "e") #>)<#= code.Fragment(keyFluentApiCalls, indent: 4) #>; +<# + anyEntityTypeConfiguration = true; + } + } + + var entityTypeFluentApiCalls = entityType.GetFluentApiCalls(annotationCodeGenerator) + ?.FilterChain(c => !(Options.UseDataAnnotations && c.IsHandledByDataAnnotations)); + if (entityTypeFluentApiCalls != null) + { + usings.AddRange(entityTypeFluentApiCalls.GetRequiredUsings()); + + if (anyEntityTypeConfiguration) + { + WriteLine(""); + } +#> + entity<#= code.Fragment(entityTypeFluentApiCalls, indent: 4) #>; +<# + anyEntityTypeConfiguration = true; + } + + foreach (var index in entityType.GetIndexes() + .Where(i => !(Options.UseDataAnnotations && i.IsHandledByDataAnnotations(annotationCodeGenerator)))) + { + if (anyEntityTypeConfiguration) + { + WriteLine(""); + } + + var indexFluentApiCalls = index.GetFluentApiCalls(annotationCodeGenerator); + if (indexFluentApiCalls != null) + { + usings.AddRange(indexFluentApiCalls.GetRequiredUsings()); + } +#> + entity.HasIndex(<#= code.Lambda(index.Properties, "e") #>, <#= code.Literal(index.GetDatabaseName()) #>)<#= code.Fragment(indexFluentApiCalls, indent: 4) #>; +<# + anyEntityTypeConfiguration = true; + } + + var firstProperty = true; + foreach (var property in entityType.GetProperties()) + { + var propertyFluentApiCalls = property.GetFluentApiCalls(annotationCodeGenerator) + ?.FilterChain(c => !(Options.UseDataAnnotations && c.IsHandledByDataAnnotations) + && !(c.Method == "IsRequired" && Options.UseNullableReferenceTypes && !property.ClrType.IsValueType)); + if (propertyFluentApiCalls == null) + { + continue; + } + + usings.AddRange(propertyFluentApiCalls.GetRequiredUsings()); + + if (anyEntityTypeConfiguration && firstProperty) + { + WriteLine(""); + } +#> + entity.Property(e => e.<#= property.Name #>)<#= code.Fragment(propertyFluentApiCalls, indent: 4) #>; +<# + anyEntityTypeConfiguration = true; + firstProperty = false; + } + + foreach (var foreignKey in entityType.GetForeignKeys()) + { + var foreignKeyFluentApiCalls = foreignKey.GetFluentApiCalls(annotationCodeGenerator) + ?.FilterChain(c => !(Options.UseDataAnnotations && c.IsHandledByDataAnnotations)); + if (foreignKeyFluentApiCalls == null) + { + continue; + } + + usings.AddRange(foreignKeyFluentApiCalls.GetRequiredUsings()); + + if (anyEntityTypeConfiguration) + { + WriteLine(""); + } +#> + entity.HasOne(d => d.<#= foreignKey.DependentToPrincipal.Name #>).<#= foreignKey.IsUnique ? "WithOne" : "WithMany" #>(<#= foreignKey.PrincipalToDependent != null ? $"p => p.{foreignKey.PrincipalToDependent.Name}" : "" #>)<#= code.Fragment(foreignKeyFluentApiCalls, indent: 4) #>; +<# + anyEntityTypeConfiguration = true; + } + + foreach (var skipNavigation in entityType.GetSkipNavigations().Where(n => n.IsLeftNavigation())) + { + if (anyEntityTypeConfiguration) + { + WriteLine(""); + } + + var left = skipNavigation.ForeignKey; + var leftFluentApiCalls = left.GetFluentApiCalls(annotationCodeGenerator, useStrings: true); + var right = skipNavigation.Inverse.ForeignKey; + var rightFluentApiCalls = right.GetFluentApiCalls(annotationCodeGenerator, useStrings: true); + var joinEntityType = skipNavigation.JoinEntityType; + + if (leftFluentApiCalls != null) + { + usings.AddRange(leftFluentApiCalls.GetRequiredUsings()); + } + + if (rightFluentApiCalls != null) + { + usings.AddRange(rightFluentApiCalls.GetRequiredUsings()); + } +#> + entity.HasMany(d => d.<#= skipNavigation.Name #>).WithMany(p => p.<#= skipNavigation.Inverse.Name #>) + .UsingEntity>( + <#= code.Literal(joinEntityType.Name) #>, + r => r.HasOne<<#= right.PrincipalEntityType.Name #>>().WithMany()<#= code.Fragment(rightFluentApiCalls, indent: 6) #>, + l => l.HasOne<<#= left.PrincipalEntityType.Name #>>().WithMany()<#= code.Fragment(leftFluentApiCalls, indent: 6) #>, + j => + { +<# + var joinKey = joinEntityType.FindPrimaryKey(); + var joinKeyFluentApiCalls = joinKey.GetFluentApiCalls(annotationCodeGenerator); + + if (joinKeyFluentApiCalls != null) + { + usings.AddRange(joinKeyFluentApiCalls.GetRequiredUsings()); + } +#> + j.HasKey(<#= code.Arguments(joinKey.Properties.Select(e => e.Name)) #>)<#= code.Fragment(joinKeyFluentApiCalls, indent: 7) #>; +<# + var joinEntityTypeFluentApiCalls = joinEntityType.GetFluentApiCalls(annotationCodeGenerator); + if (joinEntityTypeFluentApiCalls != null) + { + usings.AddRange(joinEntityTypeFluentApiCalls.GetRequiredUsings()); +#> + j<#= code.Fragment(joinEntityTypeFluentApiCalls, indent: 7) #>; +<# + } + + foreach (var index in joinEntityType.GetIndexes()) + { + var indexFluentApiCalls = index.GetFluentApiCalls(annotationCodeGenerator); + if (indexFluentApiCalls != null) + { + usings.AddRange(indexFluentApiCalls.GetRequiredUsings()); + } +#> + j.HasIndex(<#= code.Literal(index.Properties.Select(e => e.Name).ToArray()) #>, <#= code.Literal(index.GetDatabaseName()) #>)<#= code.Fragment(indexFluentApiCalls, indent: 7) #>; +<# + } + + foreach (var property in joinEntityType.GetProperties()) + { + var propertyFluentApiCalls = property.GetFluentApiCalls(annotationCodeGenerator); + if (propertyFluentApiCalls == null) + { + continue; + } + + usings.AddRange(propertyFluentApiCalls.GetRequiredUsings()); +#> + j.IndexerProperty<<#= code.Reference(property.ClrType) #>>(<#= code.Literal(property.Name) #>)<#= code.Fragment(propertyFluentApiCalls, indent: 7) #>; +<# + } +#> + }); +<# + anyEntityTypeConfiguration = true; + } +#> + }); +<# + // If any signicant code was generated, append it to the main environment + if (anyEntityTypeConfiguration) + { + mainEnvironment.Append(GenerationEnvironment); + anyConfiguration = true; + } + + // Resume generating code into the main environment + GenerationEnvironment = mainEnvironment; + } + + foreach (var sequence in Model.GetSequences()) + { + var needsType = sequence.Type != typeof(long); + var needsSchema = !string.IsNullOrEmpty(sequence.Schema) && sequence.Schema != sequence.Model.GetDefaultSchema(); + var sequenceFluentApiCalls = sequence.GetFluentApiCalls(annotationCodeGenerator); +#> + modelBuilder.HasSequence<#= needsType ? $"<{code.Reference(sequence.Type)}>" : "" #>(<#= code.Literal(sequence.Name) #><#= needsSchema ? $", {code.Literal(sequence.Schema)}" : "" #>)<#= code.Fragment(sequenceFluentApiCalls, indent: 3) #>; +<# + } + + if (anyConfiguration) + { + WriteLine(""); + } +#> + OnModelCreatingPartial(modelBuilder); + } + + partial void OnModelCreatingPartial(ModelBuilder modelBuilder); +} +<# + mainEnvironment = GenerationEnvironment; + GenerationEnvironment = new StringBuilder(); + + foreach (var ns in usings.Distinct().OrderBy(x => x, new NamespaceComparer())) + { +#> +using <#= ns #>; +<# + } + + WriteLine(""); + + GenerationEnvironment.Append(mainEnvironment); +#> diff --git a/src/GUI/lib/T4_800_Split/EFCore/EntityType.t4 b/src/GUI/lib/T4_800_Split/EFCore/EntityType.t4 new file mode 100644 index 000000000..5b17a96ec --- /dev/null +++ b/src/GUI/lib/T4_800_Split/EFCore/EntityType.t4 @@ -0,0 +1,174 @@ +<#@ template hostSpecific="true" #> +<#@ assembly name="Microsoft.EntityFrameworkCore" #> +<#@ assembly name="Microsoft.EntityFrameworkCore.Design" #> +<#@ assembly name="Microsoft.EntityFrameworkCore.Relational" #> +<#@ assembly name="Microsoft.Extensions.DependencyInjection.Abstractions" #> +<#@ parameter name="EntityType" type="Microsoft.EntityFrameworkCore.Metadata.IEntityType" #> +<#@ parameter name="Options" type="Microsoft.EntityFrameworkCore.Scaffolding.ModelCodeGenerationOptions" #> +<#@ parameter name="NamespaceHint" type="System.String" #> +<#@ import namespace="System.Collections.Generic" #> +<#@ import namespace="System.ComponentModel.DataAnnotations" #> +<#@ import namespace="System.Linq" #> +<#@ import namespace="System.Text" #> +<#@ import namespace="Microsoft.EntityFrameworkCore" #> +<#@ import namespace="Microsoft.EntityFrameworkCore.Design" #> +<#@ import namespace="Microsoft.Extensions.DependencyInjection" #> +<# + // Template version: 800 - please do NOT remove this line + if (EntityType.IsSimpleManyToManyJoinEntityType()) + { + // Don't scaffold these + return ""; + } + + var services = (IServiceProvider)Host; + var annotationCodeGenerator = services.GetRequiredService(); + var code = services.GetRequiredService(); + + var usings = new List + { + "System", + "System.Collections.Generic" + }; + + if (Options.UseDataAnnotations) + { + usings.Add("System.ComponentModel.DataAnnotations"); + usings.Add("System.ComponentModel.DataAnnotations.Schema"); + usings.Add("Microsoft.EntityFrameworkCore"); + } + + if (!string.IsNullOrEmpty(NamespaceHint)) + { +#> +namespace <#= NamespaceHint #>; + +<# + } + + if (!string.IsNullOrEmpty(EntityType.GetComment())) + { +#> +/// +/// <#= code.XmlComment(EntityType.GetComment()) #> +/// +<# + } + + if (Options.UseDataAnnotations) + { + foreach (var dataAnnotation in EntityType.GetDataAnnotations(annotationCodeGenerator)) + { +#> +<#= code.Fragment(dataAnnotation) #> +<# + } + } +#> +public partial class <#= EntityType.Name #> +{ +<# + var firstProperty = true; + foreach (var property in EntityType.GetProperties().OrderBy(p => p.GetColumnOrder() ?? -1)) + { + if (!firstProperty) + { + WriteLine(""); + } + + if (!string.IsNullOrEmpty(property.GetComment())) + { +#> + /// + /// <#= code.XmlComment(property.GetComment(), indent: 1) #> + /// +<# + } + + if (Options.UseDataAnnotations) + { + var dataAnnotations = property.GetDataAnnotations(annotationCodeGenerator) + .Where(a => !(a.Type == typeof(RequiredAttribute) && Options.UseNullableReferenceTypes && !property.ClrType.IsValueType)); + foreach (var dataAnnotation in dataAnnotations) + { +#> + <#= code.Fragment(dataAnnotation) #> +<# + } + } + + usings.AddRange(code.GetRequiredUsings(property.ClrType)); + + var needsNullable = Options.UseNullableReferenceTypes && property.IsNullable && !property.ClrType.IsValueType; + var needsInitializer = Options.UseNullableReferenceTypes && !property.IsNullable && !property.ClrType.IsValueType; +#> + public <#= code.Reference(property.ClrType) #><#= needsNullable ? "?" : "" #> <#= property.Name #> { get; set; }<#= needsInitializer ? " = null!;" : "" #> +<# + firstProperty = false; + } + + foreach (var navigation in EntityType.GetNavigations()) + { + WriteLine(""); + + if (Options.UseDataAnnotations) + { + foreach (var dataAnnotation in navigation.GetDataAnnotations(annotationCodeGenerator)) + { +#> + <#= code.Fragment(dataAnnotation) #> +<# + } + } + + var targetType = navigation.TargetEntityType.Name; + if (navigation.IsCollection) + { +#> + public virtual ICollection<<#= targetType #>> <#= navigation.Name #> { get; set; } = new List<<#= targetType #>>(); +<# + } + else + { + var needsNullable = Options.UseNullableReferenceTypes && !(navigation.ForeignKey.IsRequired && navigation.IsOnDependent); + var needsInitializer = Options.UseNullableReferenceTypes && navigation.ForeignKey.IsRequired && navigation.IsOnDependent; +#> + public virtual <#= targetType #><#= needsNullable ? "?" : "" #> <#= navigation.Name #> { get; set; }<#= needsInitializer ? " = null!;" : "" #> +<# + } + } + + foreach (var skipNavigation in EntityType.GetSkipNavigations()) + { + WriteLine(""); + + if (Options.UseDataAnnotations) + { + foreach (var dataAnnotation in skipNavigation.GetDataAnnotations(annotationCodeGenerator)) + { +#> + <#= code.Fragment(dataAnnotation) #> +<# + } + } +#> + public virtual ICollection<<#= skipNavigation.TargetEntityType.Name #>> <#= skipNavigation.Name #> { get; set; } = new List<<#= skipNavigation.TargetEntityType.Name #>>(); +<# + } +#> +} +<# + var previousOutput = GenerationEnvironment; + GenerationEnvironment = new StringBuilder(); + + foreach (var ns in usings.Distinct().OrderBy(x => x, new NamespaceComparer())) + { +#> +using <#= ns #>; +<# + } + + WriteLine(""); + + GenerationEnvironment.Append(previousOutput); +#> diff --git a/src/GUI/lib/T4_800_Split/EFCore/EntityTypeConfiguration.t4 b/src/GUI/lib/T4_800_Split/EFCore/EntityTypeConfiguration.t4 new file mode 100644 index 000000000..228d6a016 --- /dev/null +++ b/src/GUI/lib/T4_800_Split/EFCore/EntityTypeConfiguration.t4 @@ -0,0 +1,270 @@ +<#@ template hostSpecific="true" debug="false" #> +<#@ assembly name="Microsoft.EntityFrameworkCore" #> +<#@ assembly name="Microsoft.EntityFrameworkCore.Design" #> +<#@ assembly name="Microsoft.EntityFrameworkCore.Relational" #> +<#@ assembly name="Microsoft.Extensions.DependencyInjection.Abstractions" #> +<#@ parameter name="EntityType" type="Microsoft.EntityFrameworkCore.Metadata.IEntityType" #> +<#@ parameter name="Options" type="Microsoft.EntityFrameworkCore.Scaffolding.ModelCodeGenerationOptions" #> +<#@ parameter name="NamespaceHint" type="System.String" #> +<#@ parameter name="ProjectDefaultNamespace" type="System.String" #> +<#@ import namespace="System.Collections.Generic" #> +<#@ import namespace="System.Linq" #> +<#@ import namespace="System.Text" #> +<#@ import namespace="Microsoft.EntityFrameworkCore" #> +<#@ import namespace="Microsoft.EntityFrameworkCore.Design" #> +<#@ import namespace="Microsoft.EntityFrameworkCore.Infrastructure" #> +<#@ import namespace="Microsoft.EntityFrameworkCore.Scaffolding" #> +<#@ import namespace="Microsoft.Extensions.DependencyInjection" #> +<#@ import namespace="Microsoft.EntityFrameworkCore.Metadata.Builders" #> +<# + if (!ProductInfo.GetVersion().StartsWith("8.0")) + { + Warning("Your templates were created using an older version of Entity Framework. Additional features and bug fixes may be available. See https://aka.ms/efcore-docs-updating-templates for more information."); + } + + var services = (IServiceProvider)Host; + var providerCode = services.GetRequiredService(); + var annotationCodeGenerator = services.GetRequiredService(); + var code = services.GetRequiredService(); + + var usings = new List + { + "System", + "System.Collections.Generic", + "Microsoft.EntityFrameworkCore" + }; + + if (NamespaceHint != Options.ModelNamespace + && !string.IsNullOrEmpty(Options.ModelNamespace)) + { + usings.Add(Options.ModelNamespace); + } + usings.Add(typeof(EntityTypeBuilder<>).Namespace); + + if (!string.IsNullOrEmpty(NamespaceHint)) + { +#> +namespace <#= NamespaceHint #>.Configurations; + +<# + } +#> +public partial class <#= EntityType.Name #>Configuration : IEntityTypeConfiguration<<#= EntityType.Name #>> +{ + public void Configure(EntityTypeBuilder<<#= EntityType.Name #>> entity) + { +<# + var anyConfiguration = false; + + StringBuilder mainEnvironment; + if (EntityType?.Name!=null) + { + // Save all previously generated code, and start generating into a new temporary environment + mainEnvironment = GenerationEnvironment; + GenerationEnvironment = new StringBuilder(); + + var anyEntityTypeConfiguration = false; + var key = EntityType.FindPrimaryKey(); + if (key != null) + { + var keyFluentApiCalls = key.GetFluentApiCalls(annotationCodeGenerator); + if (keyFluentApiCalls != null + || (!key.IsHandledByConvention() && !Options.UseDataAnnotations)) + { + if (keyFluentApiCalls != null) + { + usings.AddRange(keyFluentApiCalls.GetRequiredUsings()); + } +#> + entity.HasKey(<#= code.Lambda(key.Properties, "e") #>)<#= code.Fragment(keyFluentApiCalls, indent: 3) #>; +<# + anyEntityTypeConfiguration = true; + } + } + + var entityTypeFluentApiCalls = EntityType.GetFluentApiCalls(annotationCodeGenerator) + ?.FilterChain(c => !(Options.UseDataAnnotations && c.IsHandledByDataAnnotations)); + if (entityTypeFluentApiCalls != null) + { + usings.AddRange(entityTypeFluentApiCalls.GetRequiredUsings()); + + if (anyEntityTypeConfiguration) + { + WriteLine(""); + } +#> + entity<#= code.Fragment(entityTypeFluentApiCalls, indent: 3) #>; +<# + anyEntityTypeConfiguration = true; + } + + foreach (var index in EntityType.GetIndexes() + .Where(i => !(Options.UseDataAnnotations && i.IsHandledByDataAnnotations(annotationCodeGenerator)))) + { + if (anyEntityTypeConfiguration) + { + WriteLine(""); + } + + var indexFluentApiCalls = index.GetFluentApiCalls(annotationCodeGenerator); + if (indexFluentApiCalls != null) + { + usings.AddRange(indexFluentApiCalls.GetRequiredUsings()); + } +#> + entity.HasIndex(<#= code.Lambda(index.Properties, "e") #>, <#= code.Literal(index.GetDatabaseName()) #>)<#= code.Fragment(indexFluentApiCalls, indent: 3) #>; +<# + anyEntityTypeConfiguration = true; + } + + var firstProperty = true; + foreach (var property in EntityType.GetProperties()) + { + var propertyFluentApiCalls = property.GetFluentApiCalls(annotationCodeGenerator) + ?.FilterChain(c => !(Options.UseDataAnnotations && c.IsHandledByDataAnnotations) + && !(c.Method == "IsRequired" && Options.UseNullableReferenceTypes && !property.ClrType.IsValueType)); + if (propertyFluentApiCalls == null) + { + continue; + } + + usings.AddRange(propertyFluentApiCalls.GetRequiredUsings()); + + if (anyEntityTypeConfiguration && firstProperty) + { + WriteLine(""); + } +#> + entity.Property(e => e.<#= property.Name #>)<#= code.Fragment(propertyFluentApiCalls, indent: 3) #>; +<# + anyEntityTypeConfiguration = true; + firstProperty = false; + } + + foreach (var foreignKey in EntityType.GetForeignKeys()) + { + var foreignKeyFluentApiCalls = foreignKey.GetFluentApiCalls(annotationCodeGenerator) + ?.FilterChain(c => !(Options.UseDataAnnotations && c.IsHandledByDataAnnotations)); + if (foreignKeyFluentApiCalls == null) + { + continue; + } + + usings.AddRange(foreignKeyFluentApiCalls.GetRequiredUsings()); + + if (anyEntityTypeConfiguration) + { + WriteLine(""); + } + if (foreignKey.DependentToPrincipal?.Name != null && foreignKey.PrincipalToDependent?.Name != null) + { +#> + entity.HasOne(d => d.<#= foreignKey.DependentToPrincipal.Name #>).<#= foreignKey.IsUnique ? "WithOne" : "WithMany" #>(p => p.<#= foreignKey.PrincipalToDependent.Name #>)<#= code.Fragment(foreignKeyFluentApiCalls, indent: 3) #>; +<# + } + anyEntityTypeConfiguration = true; + } + + foreach (var skipNavigation in EntityType.GetSkipNavigations().Where(n => n.IsLeftNavigation())) + { + if (anyEntityTypeConfiguration) + { + WriteLine(""); + } + + var left = skipNavigation.ForeignKey; + var leftFluentApiCalls = left.GetFluentApiCalls(annotationCodeGenerator, useStrings: true); + var right = skipNavigation.Inverse.ForeignKey; + var rightFluentApiCalls = right.GetFluentApiCalls(annotationCodeGenerator, useStrings: true); + var joinEntityType = skipNavigation.JoinEntityType; + + if (leftFluentApiCalls != null) + { + usings.AddRange(leftFluentApiCalls.GetRequiredUsings()); + } + + if (rightFluentApiCalls != null) + { + usings.AddRange(rightFluentApiCalls.GetRequiredUsings()); + } +#> + entity.HasMany(d => d.<#= skipNavigation.Name #>).WithMany(p => p.<#= skipNavigation.Inverse.Name #>) + .UsingEntity>( + <#= code.Literal(joinEntityType.Name) #>, + r => r.HasOne<<#= right.PrincipalEntityType.Name #>>().WithMany()<#= code.Fragment(rightFluentApiCalls, indent: 6) #>, + l => l.HasOne<<#= left.PrincipalEntityType.Name #>>().WithMany()<#= code.Fragment(leftFluentApiCalls, indent: 6) #>, + j => + { +<# + var joinKey = joinEntityType.FindPrimaryKey(); + var joinKeyFluentApiCalls = joinKey.GetFluentApiCalls(annotationCodeGenerator); + + if (joinKeyFluentApiCalls != null) + { + usings.AddRange(joinKeyFluentApiCalls.GetRequiredUsings()); + } +#> + j.HasKey(<#= code.Arguments(joinKey.Properties.Select(e => e.Name)) #>)<#= code.Fragment(joinKeyFluentApiCalls, indent: 7) #>; +<# + var joinEntityTypeFluentApiCalls = joinEntityType.GetFluentApiCalls(annotationCodeGenerator); + if (joinEntityTypeFluentApiCalls != null) + { + usings.AddRange(joinEntityTypeFluentApiCalls.GetRequiredUsings()); +#> + j<#= code.Fragment(joinEntityTypeFluentApiCalls, indent: 7) #>; +<# + } + + foreach (var index in joinEntityType.GetIndexes()) + { + var indexFluentApiCalls = index.GetFluentApiCalls(annotationCodeGenerator); + if (indexFluentApiCalls != null) + { + usings.AddRange(indexFluentApiCalls.GetRequiredUsings()); + } +#> + j.HasIndex(<#= code.Literal(index.Properties.Select(e => e.Name).ToArray()) #>, <#= code.Literal(index.GetDatabaseName()) #>)<#= code.Fragment(indexFluentApiCalls, indent: 7) #>; +<# + } +#> + }); +<# + anyEntityTypeConfiguration = true; + } + + // If any significant code was generated, append it to the main environment + if (anyEntityTypeConfiguration) + { + mainEnvironment.Append(GenerationEnvironment); + anyConfiguration = true; + } + + // Resume generating code into the main environment + GenerationEnvironment = mainEnvironment; + } + + if (anyConfiguration) + { + WriteLine(""); + } +#> + OnConfigurePartial(entity); + } + + partial void OnConfigurePartial(EntityTypeBuilder<<#= EntityType.Name #>> modelBuilder); +} +<# + mainEnvironment = GenerationEnvironment; + GenerationEnvironment = new StringBuilder(); + + foreach (var ns in usings.Distinct().OrderBy(x => x, new NamespaceComparer())) + { +#> +using <#= ns #>; +<# + } + + WriteLine(""); + + GenerationEnvironment.Append(mainEnvironment); +#> \ No newline at end of file