diff --git a/.github/dependabot.yml b/.github/dependabot.yml index fecd3c2648f..2d15c4e1f22 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -18,6 +18,7 @@ updates: - dependency-name: Microsoft.Data.SqlClient - dependency-name: Microsoft.DotNet.PlatformAbstractions - dependency-name: mod_spatialite + - dependency-name: Mono.TextTemplating - dependency-name: NetTopologySuite* - dependency-name: Newtonsoft.Json - dependency-name: SQLitePCLRaw* diff --git a/src/EFCore.Design/Design/DesignTimeServiceCollectionExtensions.cs b/src/EFCore.Design/Design/DesignTimeServiceCollectionExtensions.cs index 15a2dfe99cc..e198619bc55 100644 --- a/src/EFCore.Design/Design/DesignTimeServiceCollectionExtensions.cs +++ b/src/EFCore.Design/Design/DesignTimeServiceCollectionExtensions.cs @@ -7,6 +7,8 @@ using Microsoft.EntityFrameworkCore.Migrations.Internal; using Microsoft.EntityFrameworkCore.Scaffolding; using Microsoft.EntityFrameworkCore.Scaffolding.Internal; +using Microsoft.EntityFrameworkCore.TextTemplating; +using Microsoft.EntityFrameworkCore.TextTemplating.Internal; namespace Microsoft.EntityFrameworkCore.Design; @@ -52,7 +54,9 @@ public static IServiceCollection AddEntityFrameworkDesignTimeServices( .TryAddSingleton(reporter) .TryAddSingleton() .TryAddSingleton() - .TryAddSingleton() + .TryAddSingleton() + .TryAddSingletonEnumerable() + .TryAddSingletonEnumerable() .TryAddSingleton() .TryAddSingleton() .TryAddSingleton() diff --git a/src/EFCore.Design/Design/Internal/DatabaseOperations.cs b/src/EFCore.Design/Design/Internal/DatabaseOperations.cs index 0aded8b9714..e6db7ce4479 100644 --- a/src/EFCore.Design/Design/Internal/DatabaseOperations.cs +++ b/src/EFCore.Design/Design/Internal/DatabaseOperations.cs @@ -96,7 +96,8 @@ public virtual SavedModelFiles ScaffoldContext( UseNullableReferenceTypes = _nullable, ContextDir = MakeDirRelative(outputDir, outputContextDir), ContextName = dbContextClassName, - SuppressOnConfiguring = suppressOnConfiguring + SuppressOnConfiguring = suppressOnConfiguring, + ProjectDir = _projectDir }); return scaffolder.Save( diff --git a/src/EFCore.Design/EFCore.Design.csproj b/src/EFCore.Design/EFCore.Design.csproj index f18008e2f39..55c208b9953 100644 --- a/src/EFCore.Design/EFCore.Design.csproj +++ b/src/EFCore.Design/EFCore.Design.csproj @@ -60,6 +60,7 @@ + diff --git a/src/EFCore.Design/Properties/DesignStrings.Designer.cs b/src/EFCore.Design/Properties/DesignStrings.Designer.cs index 1f43f910c25..84c4d344d98 100644 --- a/src/EFCore.Design/Properties/DesignStrings.Designer.cs +++ b/src/EFCore.Design/Properties/DesignStrings.Designer.cs @@ -1,5 +1,7 @@ // +using System; +using System.Reflection; using System.Resources; #nullable enable @@ -118,7 +120,7 @@ public static string CompiledModelTypeMapping(object? entityType, object? proper entityType, property, customize, className); /// - /// The property '{entityType}.{property}' has a value comparer configured using a ValueComparer instance. Instead, create types that inherit from ValueConverter and ValueComparer and use '{method}HasConversion=<ConverterType, ComparerType=>()' or '{method}(Type converterType, Type comparerType)' to configure the value converter and comparer. + /// The property '{entityType}.{property}' has a value comparer configured using a ValueComparer instance. Instead, create types that inherit from ValueConverter and ValueComparer and use '{method}=<ConverterType, ComparerType=>()' or '{method}(Type converterType, Type comparerType)' to configure the value converter and comparer. /// public static string CompiledModelValueComparer(object? entityType, object? property, object? method) => string.Format( @@ -126,7 +128,7 @@ public static string CompiledModelValueComparer(object? entityType, object? prop entityType, property, method); /// - /// The property '{entityType}.{property}' has a value converter configured using a ValueConverter instance or inline expressions. Instead, create a type that inherits from ValueConverter and use '{method}HasConversion=<ConverterType=>()' or '{method}(Type converterType)' to configure the value converter. + /// The property '{entityType}.{property}' has a value converter configured using a ValueConverter instance or inline expressions. Instead, create a type that inherits from ValueConverter and use '{method}=<ConverterType=>()' or '{method}(Type converterType)' to configure the value converter. /// public static string CompiledModelValueConverter(object? entityType, object? property, object? method) => string.Format( @@ -199,6 +201,14 @@ public static string DuplicateMigrationName(object? migrationName) GetString("DuplicateMigrationName", nameof(migrationName)), migrationName); + /// + /// The encoding '{encoding}' specified in the output directive will be ignored. EF Core always scaffolds files using the encoding 'utf-8'. + /// + public static string EncodingIgnored(object? encoding) + => string.Format( + GetString("EncodingIgnored", nameof(encoding)), + encoding); + /// /// An error occurred while accessing the database. Continuing without the information provided by the database. Error: {message} /// @@ -657,6 +667,14 @@ public static string UnhandledEnumValue(object? enumValue) GetString("UnhandledEnumValue", nameof(enumValue)), enumValue); + /// + /// Failed to resolve type for directive processor {name}. + /// + public static string UnknownDirectiveProcessor(object? name) + => string.Format( + GetString("UnknownDirectiveProcessor", nameof(name)), + name); + /// /// Cannot scaffold C# literals of type '{literalType}'. The provider should implement CoreTypeMapping.GenerateCodeLiteral to support using it at design time. /// @@ -777,3 +795,4 @@ private static string GetString(string name, params string[] formatterNames) } } } + diff --git a/src/EFCore.Design/Properties/DesignStrings.resx b/src/EFCore.Design/Properties/DesignStrings.resx index 0a7e6577b7f..c8e89f0c6b7 100644 --- a/src/EFCore.Design/Properties/DesignStrings.resx +++ b/src/EFCore.Design/Properties/DesignStrings.resx @@ -1,17 +1,17 @@  - @@ -189,6 +189,9 @@ The name '{migrationName}' is used by an existing migration. + + The encoding '{encoding}' specified in the output directive will be ignored. EF Core always scaffolds files using the encoding 'utf-8'. + An error occurred while accessing the database. Continuing without the information provided by the database. Error: {message} @@ -378,6 +381,9 @@ Change your target project to the migrations project by using the Package Manage Unhandled enum value '{enumValue}'. + + Failed to resolve type for directive processor {name}. + Cannot scaffold C# literals of type '{literalType}'. The provider should implement CoreTypeMapping.GenerateCodeLiteral to support using it at design time. @@ -420,4 +426,4 @@ Change your target project to the migrations project by using the Package Manage Writing model snapshot to '{file}'. - + \ No newline at end of file diff --git a/src/EFCore.Design/Scaffolding/IModelCodeGeneratorSelector.cs b/src/EFCore.Design/Scaffolding/IModelCodeGeneratorSelector.cs index b2d40980104..1e8d73b84a1 100644 --- a/src/EFCore.Design/Scaffolding/IModelCodeGeneratorSelector.cs +++ b/src/EFCore.Design/Scaffolding/IModelCodeGeneratorSelector.cs @@ -16,5 +16,16 @@ public interface IModelCodeGeneratorSelector /// /// The programming language. /// The . + [Obsolete("Use the overload that takes ModelCodeGenerationOptions instead.")] IModelCodeGenerator Select(string? language); + + /// + /// Selects an service for a given set of options. + /// + /// The options. + /// The . + IModelCodeGenerator Select(ModelCodeGenerationOptions options) +#pragma warning disable CS0618 // Type or member is obsolete + => Select(options.Language); +#pragma warning restore CS0618 } diff --git a/src/EFCore.Design/Scaffolding/Internal/ModelCodeGeneratorSelector.cs b/src/EFCore.Design/Scaffolding/Internal/ModelCodeGeneratorSelector.cs index 517182fe6ac..5eaff106a3c 100644 --- a/src/EFCore.Design/Scaffolding/Internal/ModelCodeGeneratorSelector.cs +++ b/src/EFCore.Design/Scaffolding/Internal/ModelCodeGeneratorSelector.cs @@ -13,6 +13,8 @@ namespace Microsoft.EntityFrameworkCore.Scaffolding.Internal; /// public class ModelCodeGeneratorSelector : LanguageBasedSelector, IModelCodeGeneratorSelector { + private readonly IEnumerable _templatedModelGenerators; + /// /// 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 @@ -20,7 +22,13 @@ public class ModelCodeGeneratorSelector : LanguageBasedSelector public ModelCodeGeneratorSelector(IEnumerable services) - : base(services) - { - } + : base(services.Except(services.OfType()).ToList()) + => _templatedModelGenerators = services.OfType().ToList(); + + /// + public virtual IModelCodeGenerator Select(ModelCodeGenerationOptions options) + => _templatedModelGenerators + .Where(g => options.ProjectDir != null && g.HasTemplates(options.ProjectDir)) + .LastOrDefault() + ?? Select(options.Language); } diff --git a/src/EFCore.Design/Scaffolding/Internal/ReverseEngineerScaffolder.cs b/src/EFCore.Design/Scaffolding/Internal/ReverseEngineerScaffolder.cs index 9d984db84dd..894569031a4 100644 --- a/src/EFCore.Design/Scaffolding/Internal/ReverseEngineerScaffolder.cs +++ b/src/EFCore.Design/Scaffolding/Internal/ReverseEngineerScaffolder.cs @@ -114,7 +114,7 @@ public virtual ScaffoldedModel ScaffoldModel( : DefaultDbContextName; } - var codeGenerator = ModelCodeGeneratorSelector.Select(codeOptions.Language); + var codeGenerator = ModelCodeGeneratorSelector.Select(codeOptions); return codeGenerator.GenerateModel(model, codeOptions); } diff --git a/src/EFCore.Design/Scaffolding/Internal/TextTemplatingModelGenerator.cs b/src/EFCore.Design/Scaffolding/Internal/TextTemplatingModelGenerator.cs new file mode 100644 index 00000000000..4b524c61332 --- /dev/null +++ b/src/EFCore.Design/Scaffolding/Internal/TextTemplatingModelGenerator.cs @@ -0,0 +1,172 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.CodeDom.Compiler; +using System.Text; +using Microsoft.EntityFrameworkCore.Design.Internal; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.TextTemplating; +using Microsoft.EntityFrameworkCore.TextTemplating.Internal; + +namespace Microsoft.EntityFrameworkCore.Scaffolding.Internal; + +internal class TextTemplatingModelGenerator : TemplatedModelGenerator +{ + private readonly ITextTemplating _host; + private readonly IOperationReporter _reporter; + + public TextTemplatingModelGenerator( + ModelCodeGeneratorDependencies dependencies, + ITextTemplating textTemplatingService, + IOperationReporter reporter) + : base(dependencies) + { + _host = textTemplatingService; + _reporter = reporter; + } + + public override bool HasTemplates(string projectDir) + => File.Exists(Path.Combine(projectDir, TemplatesDirectory, "DbContext.t4")); + + public override ScaffoldedModel GenerateModel(IModel model, ModelCodeGenerationOptions options) + { + if (options.ContextName == null) + { + throw new ArgumentException( + CoreStrings.ArgumentPropertyNull(nameof(options.ContextName), nameof(options)), nameof(options)); + } + + if (options.ConnectionString == null) + { + throw new ArgumentException( + CoreStrings.ArgumentPropertyNull(nameof(options.ConnectionString), nameof(options)), nameof(options)); + } + + var resultingFiles = new ScaffoldedModel(); + + var contextTemplate = Path.Combine(options.ProjectDir!, TemplatesDirectory, "DbContext.t4"); + + Check.DebugAssert(_host.Session == null, "Session is not null."); + _host.Session = _host.CreateSession(); + try + { + _host.Session.Add("Model", model); + _host.Session.Add("Options", options); + _host.Session.Add("NamespaceHint", options.ContextNamespace ?? options.ModelNamespace); + _host.Session.Add("ProjectDefaultNamespace", options.RootNamespace); + + var handler = new TextTemplatingCallback(); + var generatedCode = ProcessTemplate(contextTemplate, handler); + + var dbContextFileName = options.ContextName + handler.Extension; + resultingFiles.ContextFile = new ScaffoldedFile + { + Path = options.ContextDir != null + ? Path.Combine(options.ContextDir, dbContextFileName) + : dbContextFileName, + Code = generatedCode + }; + } + finally + { + _host.Session = null; + } + + var entityTypeTemplate = Path.Combine(options.ProjectDir!, TemplatesDirectory, "EntityType.t4"); + if (File.Exists(entityTypeTemplate)) + { + foreach (var entityType in model.GetEntityTypes()) + { + // TODO: Should this be handled inside the template? + if (CSharpDbContextGenerator.IsManyToManyJoinEntityType(entityType)) + { + continue; + } + + _host.Session = _host.CreateSession(); + try + { + _host.Session.Add("EntityType", entityType); + _host.Session.Add("Options", options); + _host.Session.Add("NamespaceHint", options.ModelNamespace); + _host.Session.Add("ProjectDefaultNamespace", options.RootNamespace); + + var handler = new TextTemplatingCallback(); + var generatedCode = ProcessTemplate(entityTypeTemplate, handler); + if (string.IsNullOrWhiteSpace(generatedCode)) + { + continue; + } + + var entityTypeFileName = entityType.Name + handler.Extension; + resultingFiles.AdditionalFiles.Add( + new ScaffoldedFile { Path = entityTypeFileName, Code = generatedCode }); + } + finally + { + _host.Session = null; + } + } + } + + return resultingFiles; + } + + private string ProcessTemplate(string inputFile, TextTemplatingCallback handler) + { + var output = _host.ProcessTemplate( + inputFile, + File.ReadAllText(inputFile), + handler); + + foreach (CompilerError error in handler.Errors) + { + var builder = new StringBuilder(); + + if (!string.IsNullOrEmpty(error.FileName)) + { + builder.Append(error.FileName); + + if (error.Line > 0) + { + builder + .Append("(") + .Append(error.Line); + + if (error.Column > 0) + { + builder + .Append(",") + .Append(error.Line); + } + builder.Append(")"); + } + + builder.Append(" : "); + } + + builder + .Append(error.IsWarning ? "warning" : "error") + .Append(" ") + .Append(error.ErrorNumber) + .Append(": ") + .AppendLine(error.ErrorText); + + if (error.IsWarning) + { + _reporter.WriteWarning(builder.ToString()); + } + else + { + _reporter.WriteError(builder.ToString()); + } + } + + if (handler.OutputEncoding != Encoding.UTF8) + { + _reporter.WriteWarning(DesignStrings.EncodingIgnored(handler.OutputEncoding.WebName)); + } + + return output; + } +} diff --git a/src/EFCore.Design/Scaffolding/ModelCodeGenerationOptions.cs b/src/EFCore.Design/Scaffolding/ModelCodeGenerationOptions.cs index 430a7254dc1..84e6e295ae9 100644 --- a/src/EFCore.Design/Scaffolding/ModelCodeGenerationOptions.cs +++ b/src/EFCore.Design/Scaffolding/ModelCodeGenerationOptions.cs @@ -73,4 +73,10 @@ public class ModelCodeGenerationOptions /// /// The connection string. public virtual string? ConnectionString { get; set; } + + /// + /// Gets or sets the root project directory. + /// + /// The directory. + public virtual string? ProjectDir { get; set; } } diff --git a/src/EFCore.Design/Scaffolding/ModelCodeGenerator.cs b/src/EFCore.Design/Scaffolding/ModelCodeGenerator.cs index 21dcc65c7bc..9d1eb72129b 100644 --- a/src/EFCore.Design/Scaffolding/ModelCodeGenerator.cs +++ b/src/EFCore.Design/Scaffolding/ModelCodeGenerator.cs @@ -24,7 +24,7 @@ protected ModelCodeGenerator(ModelCodeGeneratorDependencies dependencies) /// Gets the programming language supported by this service. /// /// The language. - public abstract string Language { get; } + public abstract string? Language { get; } /// /// Dependencies for this service. diff --git a/src/EFCore.Design/Scaffolding/TemplatedModelGenerator.cs b/src/EFCore.Design/Scaffolding/TemplatedModelGenerator.cs new file mode 100644 index 00000000000..688aabd9790 --- /dev/null +++ b/src/EFCore.Design/Scaffolding/TemplatedModelGenerator.cs @@ -0,0 +1,36 @@ +// 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.Scaffolding; + +/// +/// Base type for model code generators that use templates. +/// +internal abstract class TemplatedModelGenerator : ModelCodeGenerator +{ + /// + /// Initializes a new instance of the class. + /// + /// The dependencies. + protected TemplatedModelGenerator(ModelCodeGeneratorDependencies dependencies) + : base(dependencies) + { + } + + /// + /// Gets the subdirectory under the project to look for templates in. + /// + /// The subdirectory. + protected static string TemplatesDirectory { get; } = Path.Combine("Templates", "EFCore"); + + /// + public override string? Language + => null; + + /// + /// Checks whether the templates required for this generator are present. + /// + /// The root project directory. + /// if the templates are present; otherwise, . + public abstract bool HasTemplates(string projectDir); +} diff --git a/src/EFCore.Design/TextTemplating/ITextTemplating.cs b/src/EFCore.Design/TextTemplating/ITextTemplating.cs new file mode 100644 index 00000000000..c0e7a822cd2 --- /dev/null +++ b/src/EFCore.Design/TextTemplating/ITextTemplating.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.VisualStudio.TextTemplating; + +namespace Microsoft.EntityFrameworkCore.TextTemplating; + +/// +/// The text template transformation service. +/// +public interface ITextTemplating : ITextTemplatingSessionHost +{ + /// + /// Transforms the contents of a text template file to produce the generated text output. + /// + /// The path of the template file. + /// The contents of the template file. + /// The callback used to process errors and information. + /// The output. + string ProcessTemplate(string inputFile, string content, ITextTemplatingCallback? callback = null); +} diff --git a/src/EFCore.Design/TextTemplating/ITextTemplatingCallback.cs b/src/EFCore.Design/TextTemplating/ITextTemplatingCallback.cs new file mode 100644 index 00000000000..b29afc78fb4 --- /dev/null +++ b/src/EFCore.Design/TextTemplating/ITextTemplatingCallback.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.CodeDom.Compiler; +using System.Text; + +namespace Microsoft.EntityFrameworkCore.TextTemplating; + +/// +/// Callback interface to be implemented by clients of that wish to process errors and information. +/// +public interface ITextTemplatingCallback +{ + /// + /// Receives errors and warnings. + /// + /// An error or warning. + void ErrorCallback(CompilerError error); + + /// + /// Receives the file name extension that is expected for the generated text output. + /// + /// The extension. + void SetFileExtension(string extension); + + /// + /// Receives the encoding that is expected for the generated text output. + /// + /// The encoding. + /// A value indicating whether the encoding was specified in the encoding parameter of the output directive. + void SetOutputEncoding(Encoding encoding, bool fromOutputDirective); +} diff --git a/src/EFCore.Design/TextTemplating/Internal/TextTemplatingCallback.cs b/src/EFCore.Design/TextTemplating/Internal/TextTemplatingCallback.cs new file mode 100644 index 00000000000..6899abcbe26 --- /dev/null +++ b/src/EFCore.Design/TextTemplating/Internal/TextTemplatingCallback.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. + +using System.CodeDom.Compiler; +using System.Text; + +namespace Microsoft.EntityFrameworkCore.TextTemplating.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 TextTemplatingCallback : ITextTemplatingCallback +{ + private CompilerErrorCollection? _errors; + private string _extension = ".cs"; + private Encoding _outputEncoding = Encoding.UTF8; + private bool _fromOutputDirective; + + /// + /// 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 string Extension + => _extension; + + /// + /// 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 CompilerErrorCollection Errors + => _errors ??= new CompilerErrorCollection(); + + /// + /// 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 Encoding OutputEncoding + => _outputEncoding; + + void ITextTemplatingCallback.ErrorCallback(CompilerError error) + => Errors.Add(error); + + void ITextTemplatingCallback.SetFileExtension(string extension) + => _extension = extension; + + void ITextTemplatingCallback.SetOutputEncoding(Encoding? encoding, bool fromOutputDirective) + { + if (_fromOutputDirective) + { + return; + } + + _outputEncoding = encoding ?? Encoding.UTF8; + _fromOutputDirective = fromOutputDirective; + } +} diff --git a/src/EFCore.Design/TextTemplating/Internal/TextTemplatingService.cs b/src/EFCore.Design/TextTemplating/Internal/TextTemplatingService.cs new file mode 100644 index 00000000000..b52984b3ab4 --- /dev/null +++ b/src/EFCore.Design/TextTemplating/Internal/TextTemplatingService.cs @@ -0,0 +1,241 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.CodeDom.Compiler; +using System.Text; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.VisualStudio.TextTemplating; +using Engine = Mono.TextTemplating.TemplatingEngine; + +namespace Microsoft.EntityFrameworkCore.TextTemplating.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 TextTemplatingService : ITextTemplating, ITextTemplatingEngineHost, IServiceProvider +{ + private readonly IServiceProvider _serviceProvider; + private ITextTemplatingCallback? _callback; + private string? _templateFile; + + /// + /// 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 TextTemplatingService(IServiceProvider serviceProvider) + => _serviceProvider = serviceProvider; + + /// + /// 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 ITextTemplatingSession? Session { get; set; } + + /// + /// 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 IList StandardAssemblyReferences { get; } = new string[] + { + typeof(ITextTemplatingEngineHost).Assembly.Location, + typeof(CompilerErrorCollection).Assembly.Location + }; + + /// + /// 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 IList StandardImports { get; } = new[] + { + "System" + }; + + /// + /// 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 string? TemplateFile + => _templateFile; + + /// + /// 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 string ProcessTemplate(string inputFile, string content, ITextTemplatingCallback? callback = null) + { + _templateFile = inputFile; + _callback = callback; + + var sessionCreated = false; + if (Session == null) + { + Session = CreateSession(); + sessionCreated = true; + } + + try + { + return new Engine().ProcessTemplate(content, this); + } + finally + { + _templateFile = null; + _callback = null; + + if (sessionCreated) + { + Session = 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 ITextTemplatingSession CreateSession() + => new TextTemplatingSession(); + + /// + /// 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 object? GetHostOption(string optionName) + => 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 bool LoadIncludeText(string requestFileName, out string content, out string location) + { + // TODO: Expand variables? + location = ResolvePath(requestFileName); + var exists = File.Exists(location); + content = exists + ? File.ReadAllText(location) + : string.Empty; + + return exists; + } + + /// + /// 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 LogErrors(CompilerErrorCollection errors) + { + foreach (CompilerError error in errors) + { + _callback?.ErrorCallback(error); + } + } + + /// + /// 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 AppDomain ProvideTemplatingAppDomain(string content) + => AppDomain.CurrentDomain; + + /// + /// 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 string ResolveAssemblyReference(string assemblyReference) + { + try + { + return Assembly.Load(assemblyReference).Location; + } + catch + { + } + + // TODO: Expand variables? + return assemblyReference; + } + + /// + /// 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 Type ResolveDirectiveProcessor(string processorName) + => throw new FileNotFoundException(DesignStrings.UnknownDirectiveProcessor(processorName)); + + /// + /// 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 string ResolveParameterValue(string directiveId, string processorName, string parameterName) + => string.Empty; + + /// + /// 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 string ResolvePath(string path) + => !Path.IsPathRooted(path) + ? Path.Combine(Path.GetDirectoryName(TemplateFile)!, path) + : path; + + /// + /// 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 SetFileExtension(string extension) + => _callback?.SetFileExtension(extension); + + /// + /// 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 SetOutputEncoding(Encoding encoding, bool fromOutputDirective) + => _callback?.SetOutputEncoding(encoding, fromOutputDirective); + + /// + /// 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 object? GetService(Type serviceType) + => _serviceProvider.GetService(serviceType); +} diff --git a/test/EFCore.Design.Tests/Design/DesignTimeServicesTest.cs b/test/EFCore.Design.Tests/Design/DesignTimeServicesTest.cs index ac5b86ad43a..7c871ad77d0 100644 --- a/test/EFCore.Design.Tests/Design/DesignTimeServicesTest.cs +++ b/test/EFCore.Design.Tests/Design/DesignTimeServicesTest.cs @@ -8,6 +8,8 @@ using Microsoft.EntityFrameworkCore.Scaffolding.Metadata; using Microsoft.EntityFrameworkCore.SqlServer.Design.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Scaffolding.Internal; +using Microsoft.EntityFrameworkCore.TextTemplating; +using Microsoft.EntityFrameworkCore.TextTemplating.Internal; using Microsoft.Extensions.DependencyInjection.Extensions; namespace Microsoft.EntityFrameworkCore.Design; @@ -94,7 +96,11 @@ public class UserMigrationsIdGenerator : IMigrationsIdGenerator Assert.Equal(typeof(CSharpMigrationsGenerator), serviceProvider.GetRequiredService().GetType()); Assert.Equal( typeof(MigrationsCodeGeneratorSelector), serviceProvider.GetRequiredService().GetType()); - Assert.Equal(typeof(CSharpModelGenerator), serviceProvider.GetRequiredService().GetType()); + Assert.Equal(typeof(TextTemplatingService), serviceProvider.GetRequiredService().GetType()); + Assert.Collection( + serviceProvider.GetServices(), + s => Assert.Equal(typeof(TextTemplatingModelGenerator), s.GetType()), + s => Assert.Equal(typeof(CSharpModelGenerator), s.GetType())); Assert.Equal(typeof(ModelCodeGeneratorSelector), serviceProvider.GetRequiredService().GetType()); Assert.Equal( typeof(CSharpRuntimeModelCodeGenerator), serviceProvider.GetRequiredService().GetType()); diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs index a785ec29210..3b8eaf0a1ba 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs @@ -191,7 +191,8 @@ public void Required_options_to_GenerateModel_are_not_null() var generator = CreateServices() .AddSingleton() .BuildServiceProvider(validateScopes: true) - .GetRequiredService(); + .GetServices() + .Last(g => g is CSharpModelGenerator); Assert.StartsWith( CoreStrings.ArgumentPropertyNull(nameof(ModelCodeGenerationOptions.ContextName), "options"), @@ -217,7 +218,8 @@ public void Plugins_work() var generator = CreateServices() .AddSingleton() .BuildServiceProvider(validateScopes: true) - .GetRequiredService(); + .GetServices() + .Last(g => g is CSharpModelGenerator); var scaffoldedModel = generator.GenerateModel( new Model(), diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpModelGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpModelGeneratorTest.cs index 7da73280ac2..9023d6205a3 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpModelGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpModelGeneratorTest.cs @@ -53,6 +53,7 @@ private static IModelCodeGenerator CreateGenerator() .AddSingleton() .AddSingleton() .BuildServiceProvider(validateScopes: true) - .GetRequiredService(); + .GetServices() + .Last(g => g is CSharpModelGenerator); } } diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/ModelCodeGeneratorSelectorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/ModelCodeGeneratorSelectorTest.cs new file mode 100644 index 00000000000..e05d4609c8d --- /dev/null +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/ModelCodeGeneratorSelectorTest.cs @@ -0,0 +1,119 @@ +// 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.Internal; + +namespace Microsoft.EntityFrameworkCore.Scaffolding.Internal; + +public class ModelCodeGeneratorSelectorTest +{ + [ConditionalFact] + public void Select_returns_last_service_for_language() + { + var expected = new TestModelCodeGenerator("C#"); + var selector = new ModelCodeGeneratorSelector( + new[] + { + new TestModelCodeGenerator("C#"), + expected + }); + + var result = selector.Select( + new ModelCodeGenerationOptions + { + Language = "C#" + }); + + Assert.Same(expected, result); + } + + [ConditionalFact] + public void Select_throws_when_no_service_for_language() + { + var selector = new ModelCodeGeneratorSelector( + new[] + { + new TestModelCodeGenerator("C#") + }); + var options = new ModelCodeGenerationOptions + { + Language = "VB" + }; + + var ex = Assert.Throws( + () => selector.Select(options)); + + Assert.Equal(DesignStrings.NoLanguageService("VB", nameof(IModelCodeGenerator)), ex.Message); + } + + [ConditionalFact] + public void Select_returns_last_templated_service_with_templates() + { + var expected = new TestTemplatedModelGenerator(hasTemplates: true); + var selector = new ModelCodeGeneratorSelector( + new IModelCodeGenerator[] + { + new TestTemplatedModelGenerator(hasTemplates: true), + expected, + new TestTemplatedModelGenerator(hasTemplates: false), + new TestModelCodeGenerator("C#") + }); + + var result = selector.Select( + new ModelCodeGenerationOptions + { + Language = "C#", + ProjectDir = Directory.GetCurrentDirectory() + }); + + Assert.Same(expected, result); + } + + [ConditionalFact] + public void Select_returns_last_service_for_language_when_no_templates() + { + var expected = new TestModelCodeGenerator("C#"); + var selector = new ModelCodeGeneratorSelector( + new IModelCodeGenerator[] + { + new TestTemplatedModelGenerator(hasTemplates: false), + new TestModelCodeGenerator("C#"), + expected + }); + + var result = selector.Select( + new ModelCodeGenerationOptions + { + Language = "C#" + }); + + Assert.Same(expected, result); + } + + private class TestModelCodeGenerator : ModelCodeGenerator + { + public TestModelCodeGenerator(string language) + : base(new()) + => Language = language; + + public override string Language { get; } + + public override ScaffoldedModel GenerateModel(IModel model, ModelCodeGenerationOptions options) + => throw new NotImplementedException(); + } + + private class TestTemplatedModelGenerator : TemplatedModelGenerator + { + private readonly bool _hasTemplates; + + public TestTemplatedModelGenerator(bool hasTemplates) + : base(new()) + => _hasTemplates = hasTemplates; + + public override ScaffoldedModel GenerateModel(IModel model, ModelCodeGenerationOptions options) + => throw new NotImplementedException(); + + public override bool HasTemplates(string projectDir) + => _hasTemplates; + } +} diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/ModelCodeGeneratorTestBase.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/ModelCodeGeneratorTestBase.cs index a4f7385ad32..00fec7696b0 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/ModelCodeGeneratorTestBase.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/ModelCodeGeneratorTestBase.cs @@ -28,7 +28,8 @@ protected void Test( AddScaffoldingServices(services); var generator = services.BuildServiceProvider(validateScopes: true) - .GetRequiredService(); + .GetServices() + .Last(g => g is CSharpModelGenerator); options.ModelNamespace ??= "TestNamespace"; options.ContextName = "TestDbContext"; diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/TextTemplatingModelGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/TextTemplatingModelGeneratorTest.cs new file mode 100644 index 00000000000..c359d40c655 --- /dev/null +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/TextTemplatingModelGeneratorTest.cs @@ -0,0 +1,316 @@ +// 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.Design.Internal; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.SqlServer.Design.Internal; +using Microsoft.EntityFrameworkCore.TestUtilities.Xunit; + +namespace Microsoft.EntityFrameworkCore.Scaffolding.Internal; + +[PlatformSkipCondition(TestPlatform.Linux, SkipReason = "CI time out")] +public class TextTemplatingModelGeneratorTest +{ + [ConditionalFact] + public void HasTemplates_works_when_templates() + { + using var projectDir = new TempDirectory(); + + var template = Path.Combine(projectDir, "Templates", "EFCore", "DbContext.t4"); + Directory.CreateDirectory(Path.GetDirectoryName(template)); + File.Create(template).Close(); + + var generator = CreateGenerator(); + + var result = generator.HasTemplates(projectDir); + + Assert.True(result); + } + + [ConditionalFact] + public void HasTemplates_works_when_no_templates() + { + using var projectDir = new TempDirectory(); + + var generator = CreateGenerator(); + + var result = generator.HasTemplates(projectDir); + + Assert.False(result); + } + + [ConditionalFact] + public void GenerateModel_uses_templates() + { + using var projectDir = new TempDirectory(); + + var contextTemplate = Path.Combine(projectDir, "Templates", "EFCore", "DbContext.t4"); + Directory.CreateDirectory(Path.GetDirectoryName(contextTemplate)); + File.WriteAllText( + contextTemplate, + "My DbContext template"); + + File.WriteAllText( + Path.Combine(projectDir, "Templates", "EFCore", "EntityType.t4"), + "My entity type template"); + + var generator = CreateGenerator(); + var model = new ModelBuilder() + .Entity("Entity1", b => { }) + .FinalizeModel(); + + var result = generator.GenerateModel( + model, + new() + { + ContextName = "Context", + ConnectionString = @"Name=DefaultConnection", + ProjectDir = projectDir + }); + + Assert.Equal("Context.cs", result.ContextFile.Path); + Assert.Equal("My DbContext template", result.ContextFile.Code); + + var entityType = Assert.Single(result.AdditionalFiles); + Assert.Equal("Entity1.cs", entityType.Path); + Assert.Equal("My entity type template", entityType.Code); + } + + [ConditionalFact] + public void GenerateModel_works_when_no_entity_type_template() + { + using var projectDir = new TempDirectory(); + + var contextTemplate = Path.Combine(projectDir, "Templates", "EFCore", "DbContext.t4"); + Directory.CreateDirectory(Path.GetDirectoryName(contextTemplate)); + File.WriteAllText( + contextTemplate, + "My DbContext template"); + + var generator = CreateGenerator(); + var model = new ModelBuilder() + .Entity("Entity1", b => { }) + .FinalizeModel(); + + var result = generator.GenerateModel( + model, + new() + { + ContextName = "Context", + ConnectionString = @"Name=DefaultConnection", + ProjectDir = projectDir + }); + + Assert.Equal("Context.cs", result.ContextFile.Path); + Assert.Equal("My DbContext template", result.ContextFile.Code); + + Assert.Empty(result.AdditionalFiles); + } + + [ConditionalFact] + public void GenerateModel_sets_session_variables() + { + using var projectDir = new TempDirectory(); + + var contextTemplate = Path.Combine(projectDir, "Templates", "EFCore", "DbContext.t4"); + Directory.CreateDirectory(Path.GetDirectoryName(contextTemplate)); + File.WriteAllText( + contextTemplate, + @"Model not null: <#= Session[""Model""] != null #> +Options not null: <#= Session[""Options""] != null #> +NamespaceHint: <#= Session[""NamespaceHint""] #> +ProjectDefaultNamespace: <#= Session[""ProjectDefaultNamespace""] #>"); + + File.WriteAllText( + Path.Combine(projectDir, "Templates", "EFCore", "EntityType.t4"), + @"EntityType not null: <#= Session[""EntityType""] != null #> +Options not null: <#= Session[""Options""] != null #> +NamespaceHint: <#= Session[""NamespaceHint""] #> +ProjectDefaultNamespace: <#= Session[""ProjectDefaultNamespace""] #>"); + + var generator = CreateGenerator(); + var model = new ModelBuilder() + .Entity("Entity1", b => { }) + .FinalizeModel(); + + var result = generator.GenerateModel( + model, + new() + { + ContextName = "Context", + ConnectionString = @"Name=DefaultConnection", + ContextNamespace = "ContextNamespace", + ModelNamespace = "ModelNamespace", + RootNamespace = "RootNamespace", + ProjectDir = projectDir + }); + + Assert.Equal( + @"Model not null: True +Options not null: True +NamespaceHint: ContextNamespace +ProjectDefaultNamespace: RootNamespace", + result.ContextFile.Code); + + var entityType = Assert.Single(result.AdditionalFiles); + Assert.Equal( + @"EntityType not null: True +Options not null: True +NamespaceHint: ModelNamespace +ProjectDefaultNamespace: RootNamespace", + entityType.Code); + } + + [ConditionalFact] + public void GenerateModel_defaults_to_model_namespace_when_no_context_namespace() + { + using var projectDir = new TempDirectory(); + + var contextTemplate = Path.Combine(projectDir, "Templates", "EFCore", "DbContext.t4"); + Directory.CreateDirectory(Path.GetDirectoryName(contextTemplate)); + File.WriteAllText( + contextTemplate, + @"<#= Session[""NamespaceHint""] #>"); + + var generator = CreateGenerator(); + var model = new ModelBuilder() + .FinalizeModel(); + + var result = generator.GenerateModel( + model, + new() + { + ContextName = "Context", + ConnectionString = @"Name=DefaultConnection", + ModelNamespace = "ModelNamespace", + ProjectDir = projectDir + }); + + Assert.Equal( + "ModelNamespace", + result.ContextFile.Code); + } + + [ConditionalFact] + public void GenerateModel_uses_output_extension() + { + using var projectDir = new TempDirectory(); + + var contextTemplate = Path.Combine(projectDir, "Templates", "EFCore", "DbContext.t4"); + Directory.CreateDirectory(Path.GetDirectoryName(contextTemplate)); + File.WriteAllText( + contextTemplate, + @"<#@ output extension="".vb"" #>"); + + File.WriteAllText( + Path.Combine(projectDir, "Templates", "EFCore", "EntityType.t4"), + @"<#@ output extension="".fs"" #> +My entity type template"); + + var generator = CreateGenerator(); + var model = new ModelBuilder() + .Entity("Entity1", b => { }) + .FinalizeModel(); + + var result = generator.GenerateModel( + model, + new() + { + ContextName = "Context", + ConnectionString = @"Name=DefaultConnection", + ProjectDir = projectDir + }); + + Assert.Equal("Context.vb", result.ContextFile.Path); + + var entityType = Assert.Single(result.AdditionalFiles); + Assert.Equal("Entity1.fs", entityType.Path); + } + + [ConditionalFact] + public void GenerateModel_warns_when_output_encoding() + { + using var projectDir = new TempDirectory(); + + var contextTemplate = Path.Combine(projectDir, "Templates", "EFCore", "DbContext.t4"); + Directory.CreateDirectory(Path.GetDirectoryName(contextTemplate)); + File.WriteAllText( + contextTemplate, + @"<#@ output encoding=""us-ascii"" #>"); + + var reporter = new TestOperationReporter(); + var generator = CreateGenerator(reporter); + var model = new ModelBuilder() + .FinalizeModel(); + + var result = generator.GenerateModel( + model, + new() + { + ContextName = "Context", + ConnectionString = @"Name=DefaultConnection", + ProjectDir = projectDir + }); + + Assert.Collection( + reporter.Messages, + x => + { + Assert.Equal(LogLevel.Warning, x.Level); + Assert.Equal(DesignStrings.EncodingIgnored("us-ascii"), x.Message); + }); + } + + [ConditionalFact] + public void GenerateModel_reports_errors() + { + using var projectDir = new TempDirectory(); + + var contextTemplate = Path.Combine(projectDir, "Templates", "EFCore", "DbContext.t4"); + Directory.CreateDirectory(Path.GetDirectoryName(contextTemplate)); + File.WriteAllText( + contextTemplate, + @"<# Warning(""This is a warning""); +Error(""This is an error""); #>"); + + var reporter = new TestOperationReporter(); + var generator = CreateGenerator(reporter); + var model = new ModelBuilder() + .FinalizeModel(); + + var result = generator.GenerateModel( + model, + new() + { + ContextName = "Context", + ConnectionString = @"Name=DefaultConnection", + ProjectDir = projectDir + }); + + Assert.Collection( + reporter.Messages, + x => + { + Assert.Equal(LogLevel.Warning, x.Level); + Assert.Contains("This is a warning", x.Message); + }, + x => + { + Assert.Equal(LogLevel.Error, x.Level); + Assert.Contains("This is an error", x.Message); + }); + } + + private static TemplatedModelGenerator CreateGenerator(IOperationReporter reporter = null) + { + var serviceCollection = new ServiceCollection() + .AddEntityFrameworkDesignTimeServices(reporter); + new SqlServerDesignTimeServices().ConfigureDesignTimeServices(serviceCollection); + + return serviceCollection + .BuildServiceProvider() + .GetServices() + .OfType() + .Last(); + } +} diff --git a/test/EFCore.Design.Tests/TestUtilities/TempDirectory.cs b/test/EFCore.Design.Tests/TestUtilities/TempDirectory.cs index 06cf980780e..c38c5b2c951 100644 --- a/test/EFCore.Design.Tests/TestUtilities/TempDirectory.cs +++ b/test/EFCore.Design.Tests/TestUtilities/TempDirectory.cs @@ -15,6 +15,9 @@ public TempDirectory() public string Path { get; } + public static implicit operator string(TempDirectory dir) + => dir.Path; + public void Dispose() => Directory.Delete(Path, recursive: true); } diff --git a/test/EFCore.Design.Tests/TextTemplating/Internal/TextTemplatingServiceTest.cs b/test/EFCore.Design.Tests/TextTemplating/Internal/TextTemplatingServiceTest.cs new file mode 100644 index 00000000000..64159cb9898 --- /dev/null +++ b/test/EFCore.Design.Tests/TextTemplating/Internal/TextTemplatingServiceTest.cs @@ -0,0 +1,183 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.CodeDom.Compiler; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.TestUtilities.Xunit; +using Microsoft.VisualStudio.TextTemplating; + +namespace Microsoft.EntityFrameworkCore.TextTemplating.Internal; + +[PlatformSkipCondition(TestPlatform.Linux, SkipReason = "CI time out")] +public class TextTemplatingServiceTest +{ + [ConditionalFact] + public void Service_works() + { + var host = new TextTemplatingService( + new ServiceCollection() + .AddSingleton("Hello, Services!") + .BuildServiceProvider()); + var callback = new TextTemplatingCallback(); + + var result = host.ProcessTemplate( + @"T:\test.tt", + @"<#@ template hostSpecific=""true"" #><#= ((IServiceProvider)Host).GetService(typeof(string)) #>", + callback); + + Assert.Empty(callback.Errors); + Assert.Equal("Hello, Services!", result); + } + + [ConditionalFact] + public void Session_works() + { + var host = new TextTemplatingService( + new ServiceCollection() + .BuildServiceProvider()); + host.Session = new TextTemplatingSession + { + ["Value"] = "Hello, Session!" + }; + var callback = new TextTemplatingCallback(); + + var result = host.ProcessTemplate( + @"T:\test.tt", + @"<#= Session[""Value""] #>", + callback); + + Assert.Empty(callback.Errors); + Assert.Equal("Hello, Session!", result); + } + + [ConditionalFact] + public void Session_works_with_parameter() + { + var host = new TextTemplatingService( + new ServiceCollection() + .BuildServiceProvider()); + host.Session = new TextTemplatingSession + { + ["Value"] = "Hello, Session!" + }; + var callback = new TextTemplatingCallback(); + + var result = host.ProcessTemplate( + @"T:\test.tt", + @"<#@ parameter name=""Value"" type=""System.String"" #><#= Value #>", + callback); + + Assert.Empty(callback.Errors); + Assert.Equal("Hello, Session!", result); + } + + [ConditionalFact] + public void Include_works() + { + using var dir = new TempDirectory(); + File.WriteAllText( + Path.Combine(dir, "test.ttinclude"), + "Hello, Include!"); + + var host = new TextTemplatingService( + new ServiceCollection() + .BuildServiceProvider()); + var callback = new TextTemplatingCallback(); + + var result = host.ProcessTemplate( + Path.Combine(dir, "test.tt"), + @"<#@ include file=""test.ttinclude"" #>", + callback); + + Assert.Empty(callback.Errors); + Assert.Equal("Hello, Include!", result); + } + + [ConditionalFact] + public void Error_works() + { + var host = new TextTemplatingService( + new ServiceCollection() + .BuildServiceProvider()); + var callback = new TextTemplatingCallback(); + + host.ProcessTemplate( + @"T:\test.tt", + @"<# Error(""Hello, Error!""); #>", + callback); + + var error = Assert.Single(callback.Errors.Cast()); + Assert.Equal("Hello, Error!", error.ErrorText); + } + + [ConditionalFact] + public void Directive_throws_when_processor_unknown() + { + var host = new TextTemplatingService( + new ServiceCollection() + .BuildServiceProvider()); + var callback = new TextTemplatingCallback(); + + var ex = Assert.Throws( + () => host.ProcessTemplate( + @"T:\test.tt", + @"<#@ test processor=""TestDirectiveProcessor"" #>", + callback)); + + Assert.Equal(DesignStrings.UnknownDirectiveProcessor("TestDirectiveProcessor"), ex.Message); + } + + [ConditionalFact] + public void ResolvePath_work() + { + using var dir = new TempDirectory(); + + var host = new TextTemplatingService( + new ServiceCollection() + .BuildServiceProvider()); + var callback = new TextTemplatingCallback(); + + var result = host.ProcessTemplate( + Path.Combine(dir, "test.tt"), + @"<#@ template hostSpecific=""true"" #><#= Host.ResolvePath(""data.json"") #>", + callback); + + Assert.Empty(callback.Errors); + Assert.Equal(Path.Combine(dir, "data.json"), result); + } + + [ConditionalFact] + public void Output_works() + { + var host = new TextTemplatingService( + new ServiceCollection() + .BuildServiceProvider()); + var callback = new TextTemplatingCallback(); + + host.ProcessTemplate( + @"T:\test.tt", + @"<#@ output extension="".txt"" encoding=""us-ascii"" #>", + callback); + + Assert.Empty(callback.Errors); + Assert.Equal(".txt", callback.Extension); + Assert.Equal(Encoding.ASCII, callback.OutputEncoding); + } + + [ConditionalFact] + public void Assembly_works() + { + var host = new TextTemplatingService( + new ServiceCollection() + .BuildServiceProvider()); + var callback = new TextTemplatingCallback(); + + var result = host.ProcessTemplate( + @"T:\test.tt", + @"<#@ assembly name=""Microsoft.EntityFrameworkCore"" #><#= nameof(Microsoft.EntityFrameworkCore.DbContext) #>", + callback); + + Assert.Empty(callback.Errors); + Assert.Equal("DbContext", result); + } +} diff --git a/test/EFCore.Relational.Specification.Tests/TestUtilities/BuildReference.cs b/test/EFCore.Relational.Specification.Tests/TestUtilities/BuildReference.cs index 41437f641f3..e56c51a4b99 100644 --- a/test/EFCore.Relational.Specification.Tests/TestUtilities/BuildReference.cs +++ b/test/EFCore.Relational.Specification.Tests/TestUtilities/BuildReference.cs @@ -24,6 +24,7 @@ private BuildReference(IEnumerable references, bool copyLocal public static BuildReference ByName(string name, bool copyLocal = false) { var references = (from l in DependencyContext.Default.CompileLibraries + where l.Assemblies.Any(a => IOPath.GetFileNameWithoutExtension(a) == name) from r in l.ResolveReferencePaths() where IOPath.GetFileNameWithoutExtension(r) == name select MetadataReference.CreateFromFile(r)).ToList();