diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f8ea0a0..da7257e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -53,8 +53,8 @@ jobs: if: startsWith (matrix.os, 'windows') == false # dotnet test doesn't support mono so we have to use the xunit console runner run: | - nuget install xunit.runner.console -Version 2.4.1 -OutputDirectory testrunner - mono ./testrunner/xunit.runner.console.2.4.1/tools/net472/xunit.console.exe ./Mono.TextTemplating.Tests/bin/${{ matrix.config }}/net472/Mono.TextTemplating.Tests.dll -parallel none -noshadow -noappdomain + nuget install xunit.runner.console -Version 2.4.2 -OutputDirectory testrunner + mono ./testrunner/xunit.runner.console.2.4.2/tools/net472/xunit.console.exe ./Mono.TextTemplating.Tests/bin/${{ matrix.config }}/net472/Mono.TextTemplating.Tests.dll -parallel none -noshadow dotnet test -c ${{ matrix.config }} --no-build -f netcoreapp2.1 dotnet test -c ${{ matrix.config }} --no-build -f netcoreapp3.1 dotnet test -c ${{ matrix.config }} --no-build -f net5.0 diff --git a/.gitignore b/.gitignore index 2c9dc65..4c64ccc 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ packages/ TestResults/ .vs/ +testrunner # globs *.DS_Store diff --git a/Mono.TextTemplating.Roslyn/Mono.TextTemplating.Roslyn.csproj b/Mono.TextTemplating.Roslyn/Mono.TextTemplating.Roslyn.csproj index 6f03a3c..8cc2c11 100644 --- a/Mono.TextTemplating.Roslyn/Mono.TextTemplating.Roslyn.csproj +++ b/Mono.TextTemplating.Roslyn/Mono.TextTemplating.Roslyn.csproj @@ -24,11 +24,6 @@ To enable the in-process C# compiler, use the TemplatingEngine.UseInProcessCompi snupkg - - - - - diff --git a/Mono.TextTemplating.Tests/AppDomainTests.cs b/Mono.TextTemplating.Tests/AppDomainTests.cs new file mode 100644 index 0000000..1a55d37 --- /dev/null +++ b/Mono.TextTemplating.Tests/AppDomainTests.cs @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#if FEATURE_APPDOMAINS + +using System; +using System.CodeDom.Compiler; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +using Xunit; + +namespace Mono.TextTemplating.Tests; + +public class AppDomainTests : AssemblyLoadTests> +{ + protected override TemplateGenerator CreateGenerator ([CallerMemberName] string testName = null) => CreateGeneratorWithAppDomain (testName: testName); + + protected override void CleanupGenerator (TemplateGenerator generator) + { + // FIXME: app domain unload doesn't seem to work on Mono + if (FactExceptOnMonoAttribute.IsRunningOnMono) { + return; + } + + // verify that the AppDomain is collected + var weakRef = ((TestTemplateGeneratorWithAppDomain)generator).ReleaseDomain (); + int count = 0; + while (weakRef.IsAlive) { + GC.Collect (); + Assert.InRange (count++, 0, 5); + } + } + + protected override SnapshotSet GetInitialState () => Snapshot.LoadedAssemblies (); + + protected override void VerifyFinalState (SnapshotSet state) + { + (var added, var removed) = state.GetChanges (); + added = added.Where (a => !ShouldIgnoreAssemblyAdd (a)); + Assert.Empty (added); + Assert.Empty (removed); + } + + static bool ShouldIgnoreAssemblyAdd (string name) => + // System.Configuration may cause these to load in the main AppDomain + name.StartsWith ("System.Configuration,", StringComparison.Ordinal) || + name.StartsWith ("System.Xml.Linq,", StringComparison.Ordinal) || + name.StartsWith ("System.Data,", StringComparison.Ordinal) || + name.StartsWith ("System.Reflection,", StringComparison.Ordinal); + + [Fact] + public async Task BadAppDomain () + { + var testDir = TestDataPath.Get (nameof (LoadOpenApiDll)); + var badGen = CreateGeneratorWithAppDomain (testDir, testDir); + + badGen.ReferencePaths.Add (PackagePath.Microsoft_OpenApi_1_2_3.Combine ("lib", "netstandard2.0")); + + var templateText = await testDir["LoadOpenApiDll.tt"].ReadAllTextNormalizedAsync (); + + await Assert.ThrowsAnyAsync (() => badGen.ProcessTemplateAsync ("LoadOpenApiDll.tt", templateText, null)); + CleanupGenerator (badGen); + } + + [Fact] + public async Task RunsInAppDomain () + { + var gen = CreateGeneratorWithAppDomain (); + var templateText = "<#= System.AppDomain.CurrentDomain.FriendlyName #>"; + var expectedOutputText = GetAppDomainNameForCurrentTest (); + + var state = GetInitialState (); + + var result = await gen.ProcessTemplateAsync ("TemplateInAppDomain.tt", templateText, null); + + Assert.Null (gen.Errors.OfType ().FirstOrDefault ()); + Assert.Equal (expectedOutputText, result.content); + + CleanupGenerator (gen); + VerifyFinalState (state); + } + + static TestTemplateGeneratorWithAppDomain CreateGeneratorWithAppDomain ( + string basePath = null, string relativeSearchPath = null, bool shadowCopy = false, + [CallerMemberName] string testName = null + ) + { + if (basePath is null) { + // need to be able to resolve Mono.TextTemplating to load CompiledTemplate, which will resolve other assemblies via the host. + basePath = AppDomain.CurrentDomain.BaseDirectory; + if (relativeSearchPath is not null) { + throw new ArgumentException ($"{nameof (relativeSearchPath)} must be null if {nameof (basePath)} is null", nameof (relativeSearchPath)); + } + relativeSearchPath = AppDomain.CurrentDomain.RelativeSearchPath; + } + return new (AppDomain.CreateDomain ( + GetAppDomainNameForCurrentTest (testName), + null, + basePath, + relativeSearchPath, + shadowCopy) + ); + } + + static string GetAppDomainNameForCurrentTest ([CallerMemberName] string testName = null) => $"Template Test - {testName ?? "(unknown)"}"; + + class TestTemplateGeneratorWithAppDomain : TemplateGenerator + { + AppDomain appDomain; + public TestTemplateGeneratorWithAppDomain (AppDomain appDomain) => this.appDomain = appDomain; + + public override AppDomain ProvideTemplatingAppDomain (string content) => appDomain; + + public WeakReference ReleaseDomain () + { + AppDomain.Unload (appDomain); + var weakRef = new WeakReference (appDomain); + appDomain = null; + return weakRef; + } + } +} + +#endif \ No newline at end of file diff --git a/Mono.TextTemplating.Tests/AssemblyLoadContextTests.cs b/Mono.TextTemplating.Tests/AssemblyLoadContextTests.cs new file mode 100644 index 0000000..9ca2997 --- /dev/null +++ b/Mono.TextTemplating.Tests/AssemblyLoadContextTests.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#if NETCOREAPP3_0_OR_GREATER + +using System; +using System.CodeDom.Compiler; +using System.Linq; +using System.Runtime.Loader; +using System.Threading.Tasks; +using Xunit; + +namespace Mono.TextTemplating.Tests; + +public class AssemblyLoadContextTests : AssemblyLoadTests<(SnapshotSet assembliesInDefaultContext, SnapshotSet allContexts)> +{ + protected override (SnapshotSet assembliesInDefaultContext, SnapshotSet allContexts) GetInitialState () => ( + Snapshot.LoadedAssemblies (), + Snapshot.AssemblyLoadContexts () + ); + + protected override void VerifyFinalState ((SnapshotSet assembliesInDefaultContext, SnapshotSet allContexts) state) + { + state.assembliesInDefaultContext.AssertUnchanged (); + + // ensure unloadable contexts are collected + for (int i = 0; i < 10; i++) { + GC.Collect (); + GC.WaitForPendingFinalizers (); + } + + state.allContexts.AssertUnchanged (); + } +} + +#endif \ No newline at end of file diff --git a/Mono.TextTemplating.Tests/AssemblyLoadTests.cs b/Mono.TextTemplating.Tests/AssemblyLoadTests.cs new file mode 100644 index 0000000..f5f5933 --- /dev/null +++ b/Mono.TextTemplating.Tests/AssemblyLoadTests.cs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.CodeDom.Compiler; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Xunit; + +namespace Mono.TextTemplating.Tests; + +public abstract class AssemblyLoadTests : StatefulTest +{ + protected virtual TemplateGenerator CreateGenerator ([CallerMemberName] string testName = null) => new (); + protected virtual void CleanupGenerator (TemplateGenerator generator) { } + + [Fact] + public async Task LoadOpenApiDll () + { + var testDir = TestDataPath.Get (); + var gen = CreateGenerator (); + gen.ReferencePaths.Add (PackagePath.Microsoft_OpenApi_1_2_3.Combine ("lib", "netstandard2.0").AssertDirectoryExists ()); + + var templatePath = testDir["LoadOpenApiDll.tt"]; + var templateText = await templatePath.ReadAllTextNormalizedAsync (); + + var expectedOutputPath = testDir["LoadOpenApiDll.yaml"]; + var expectedOutputText = await expectedOutputPath.ReadAllTextNormalizedAsync (); + + var state = GetInitialState (); + + var result = await gen.ProcessTemplateAsync (templatePath, templateText, null); + + Assert.Null (gen.Errors.OfType ().FirstOrDefault ()); + Assert.Equal (expectedOutputText, result.content); + Assert.Equal (expectedOutputPath, result.fileName); + + CleanupGenerator (gen); + VerifyFinalState (state); + } + + [Fact] + public async Task LoadOpenApiReadersDll () + { + var testDir = TestDataPath.Get (nameof (LoadOpenApiDll)); + var gen = CreateGenerator (); + gen.ReferencePaths.Add (PackagePath.Microsoft_OpenApi_1_2_3.Combine ("lib", "netstandard2.0").AssertDirectoryExists ()); + gen.ReferencePaths.Add (PackagePath.Microsoft_OpenApi_Readers_1_2_3.Combine ("lib", "netstandard2.0").AssertDirectoryExists ()); + gen.ReferencePaths.Add (PackagePath.SharpYaml_1_6_5.Combine ("lib", "netstandard2.0").AssertDirectoryExists ()); + + var templatePath = testDir["LoadOpenApiReaders.tt"]; + var templateText = await templatePath.ReadAllTextNormalizedAsync (); + + var state = GetInitialState (); + + var result = await gen.ProcessTemplateAsync (templatePath, templateText, null); + + Assert.Null (gen.Errors.OfType ().FirstOrDefault ()); + Assert.Equal ("Example", result.content); + + CleanupGenerator (gen); + VerifyFinalState (state); + } + + [FactExceptOnMono ("Mono incorrectly resolves the assembly if it has been loaded in a different AppDomain")] + public async Task MissingTransitiveReference () + { + var gen = CreateGenerator (); + gen.ReferencePaths.Add (PackagePath.Microsoft_OpenApi_1_2_3.Combine ("lib", "netstandard2.0").AssertDirectoryExists ()); + gen.ReferencePaths.Add (PackagePath.Microsoft_OpenApi_Readers_1_2_3.Combine ("lib", "netstandard2.0").AssertDirectoryExists ()); + + var testDir = TestDataPath.Get (nameof (LoadOpenApiDll)); + var templatePath = testDir["LoadOpenApiReaders.tt"]; + var templateText = await templatePath.ReadAllTextNormalizedAsync (); + + await gen.ProcessTemplateAsync (templatePath, templateText, null); + + var firstError = gen.Errors.OfType ().FirstOrDefault ()?.ErrorText; + Assert.Contains ("FileNotFoundException: Could not load file or assembly 'SharpYaml, Version=1.6.5.0", firstError); + + CleanupGenerator (gen); + } +} + +public class FactExceptOnMonoAttribute : FactAttribute +{ + public FactExceptOnMonoAttribute (string reason) + { + if (IsRunningOnMono) { + Skip = reason; + } + } + + public static bool IsRunningOnMono { get; } = System.Type.GetType ("Mono.Runtime") != null; +} \ No newline at end of file diff --git a/Mono.TextTemplating.Tests/GlobalSuppressions.cs b/Mono.TextTemplating.Tests/GlobalSuppressions.cs new file mode 100644 index 0000000..278ed3c --- /dev/null +++ b/Mono.TextTemplating.Tests/GlobalSuppressions.cs @@ -0,0 +1,7 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage ("Design", "CA1034:Nested types should not be visible", + Justification = "Many nested classes are test specific but need to be accessible from template code")] diff --git a/Mono.TextTemplating.Tests/LoadedAssembliesSnapshot.cs b/Mono.TextTemplating.Tests/LoadedAssembliesSnapshot.cs new file mode 100644 index 0000000..3635843 --- /dev/null +++ b/Mono.TextTemplating.Tests/LoadedAssembliesSnapshot.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Xunit; + +#if NETCOREAPP3_0_OR_GREATER +using System.Runtime.Loader; +#endif + +namespace Mono.TextTemplating.Tests; + +public abstract class Snapshot +{ + public abstract void AssertUnchanged (); + +#if FEATURE_APPDOMAINS + public static SnapshotSet LoadedAssemblies (AppDomain context = null) => new (() => GetNames ((context ?? AppDomain.CurrentDomain).GetAssemblies ())); +#elif NETCOREAPP3_0_OR_GREATER + public static SnapshotSet LoadedAssemblies (AssemblyLoadContext context = null) => new (() => GetNames ((context ?? AssemblyLoadContext.Default).Assemblies)); + public static SnapshotSet AssemblyLoadContexts () => new (() => AssemblyLoadContext.All); +#endif + + static IEnumerable GetNames (IEnumerable assemblies) + { + var names = assemblies.Select (a => a.FullName); + if (!System.Diagnostics.Debugger.IsAttached) { + return names; + } + return names.Where (a => !a.StartsWith ("Microsoft.VisualStudio.Debugger", StringComparison.Ordinal)); + } +} +public class SnapshotSet : Snapshot +{ + readonly Func> getCurrent; + readonly HashSet initial; + + public SnapshotSet (Func> getCurrent) + { + this.getCurrent = getCurrent; + initial = getCurrent ().ToHashSet (); + } + + public override void AssertUnchanged () + { + (var added, var removed) = GetChanges (); + Assert.Empty (added); + Assert.Empty (removed); + } + + public (IEnumerable added, IEnumerable removed) GetChanges () + { + var current = getCurrent ().ToHashSet (); + return ( + current.Except (initial), + initial.Except (current) + ); + } +} + +public class AggregateSnapshot : Snapshot +{ + readonly Snapshot[] snapshots; + public AggregateSnapshot (params Snapshot[] snapshots) => this.snapshots = snapshots; + public override void AssertUnchanged () + { + foreach (var snapshot in snapshots) { + snapshot.AssertUnchanged (); + } + } +} + +// these test process state so cannot be run in parallel with other tests +[CollectionDefinition (nameof (StatefulTests), DisableParallelization = true)] +public class StatefulTests { } + +[Collection (nameof (StatefulTests))] +public abstract class StatefulTest +{ + protected abstract T GetInitialState (); + protected abstract void VerifyFinalState (T state); +} \ No newline at end of file diff --git a/Mono.TextTemplating.Tests/Mono.TextTemplating.Tests.csproj b/Mono.TextTemplating.Tests/Mono.TextTemplating.Tests.csproj index 68b1988..2810314 100644 --- a/Mono.TextTemplating.Tests/Mono.TextTemplating.Tests.csproj +++ b/Mono.TextTemplating.Tests/Mono.TextTemplating.Tests.csproj @@ -1,4 +1,4 @@ - + net472;netcoreapp2.1;netcoreapp3.1;net5.0 true @@ -21,7 +21,47 @@ - + + + + + + + + + + + + <_PackageDownloadConstsPathFile>$(IntermediateOutputPath)PackageDownloadPathConstants.cs + <_EscapedNuGetPackageRoot>$(NuGetPackageRoot.Replace('\','\\')) + + + + <_PackageDownloadWithVersion Include="@(PackageDownload)" Version="$([System.String]::Copy('%(Version)').Replace('[','').Replace(']',''))" /> + <_PackageDownloadWithPath Include="@(_PackageDownloadWithVersion)" Path="$(NuGetPackageRoot)$([System.String]::Copy('%(Identity)').ToLower())\%(Version)" /> + <_PackageDownloadVars + Include="@(_PackageDownloadWithPath)" + VarName="$([System.String]::Copy('%(Identity)_%(Version)').Replace('.','_'))" + EscapedValue="$([System.String]::Copy('%(Path)').Replace('\','\\'))" + /> + <_PackageDownloadPathConstLines Include="namespace $(ProjectName) {" /> + <_PackageDownloadPathConstLines Include="partial class PackagePath {" /> + <_PackageDownloadPathConstLines Include="@(_PackageDownloadVars->' public static TestDataPath %(VarName) => new("%(EscapedValue)");')" /> + <_PackageDownloadPathConstLines Include="}}" /> + + + + + + <_PackageDownloadWithVersion Remove="@(_PackageDownloadWithVersion)" /> + <_PackageDownloadWithPath Remove="@(_PackageDownloadWithPath)" /> + <_PackageDownloadVars Remove="@(_PackageDownloadVars)" /> + <_PackageDownloadPathConstLines Remove="@(_PackageDownloadPathConstLines)" /> + + + + + diff --git a/Mono.TextTemplating.Tests/ParsingTests.cs b/Mono.TextTemplating.Tests/ParsingTests.cs index 9864610..ba26be4 100644 --- a/Mono.TextTemplating.Tests/ParsingTests.cs +++ b/Mono.TextTemplating.Tests/ParsingTests.cs @@ -35,12 +35,6 @@ namespace Mono.TextTemplating.Tests { public class ParsingTests { - public static string GetTestFile (string filename, [CallerMemberName] string testDir = null) - { - var asmDir = Environment.CurrentDirectory; - return Path.Combine (asmDir, "TestCases", testDir, filename); - } - public const string ParseSample1 = @"<#@ template language=""C#v3.5"" #> Line One @@ -240,7 +234,7 @@ public void IncludeOnceTest () [Fact] public void RelativeInclude () { - var testFile = GetTestFile ("RelativeInclude.tt"); + var testFile = TestDataPath.Get ().Combine ("RelativeInclude.tt"); var host = new TemplateGenerator (); var pt = host.ParseTemplate (testFile, File.ReadAllText (testFile)); Assert.Collection (pt.Content, c => Assert.Equal("Hello", c.Text)); diff --git a/Mono.TextTemplating.Tests/TestCases/LoadOpenApiDll/LoadOpenApiDll.tt b/Mono.TextTemplating.Tests/TestCases/LoadOpenApiDll/LoadOpenApiDll.tt new file mode 100644 index 0000000..54d26db --- /dev/null +++ b/Mono.TextTemplating.Tests/TestCases/LoadOpenApiDll/LoadOpenApiDll.tt @@ -0,0 +1,23 @@ +<#@ output extension=".yaml" #> +<#@ assembly name="Microsoft.OpenApi.dll" #> +<#@ import namespace="Microsoft.OpenApi" #> +<#@ import namespace="Microsoft.OpenApi.Models" #> +<#@ import namespace="Microsoft.OpenApi.Extensions" #> +<#@ import namespace="System.Collections.Generic" #> +<# + +var document = new OpenApiDocument +{ + Info = new OpenApiInfo + { + Version = "1.0.0", + Title = "Example", + }, + Servers = new List + { + new OpenApiServer { Url = "https://example.com/api" } + } +}; + +#> +<#= document.Serialize(OpenApiSpecVersion.OpenApi2_0, OpenApiFormat.Yaml) #> \ No newline at end of file diff --git a/Mono.TextTemplating.Tests/TestCases/LoadOpenApiDll/LoadOpenApiDll.yaml b/Mono.TextTemplating.Tests/TestCases/LoadOpenApiDll/LoadOpenApiDll.yaml new file mode 100644 index 0000000..6697114 --- /dev/null +++ b/Mono.TextTemplating.Tests/TestCases/LoadOpenApiDll/LoadOpenApiDll.yaml @@ -0,0 +1,9 @@ +swagger: '2.0' +info: + title: Example + version: 1.0.0 +host: example.com +basePath: /api +schemes: + - https +paths: { } \ No newline at end of file diff --git a/Mono.TextTemplating.Tests/TestCases/LoadOpenApiDll/LoadOpenApiReaders.tt b/Mono.TextTemplating.Tests/TestCases/LoadOpenApiDll/LoadOpenApiReaders.tt new file mode 100644 index 0000000..b44f2e5 --- /dev/null +++ b/Mono.TextTemplating.Tests/TestCases/LoadOpenApiDll/LoadOpenApiReaders.tt @@ -0,0 +1,17 @@ +<#@ template hostspecific="true" #> +<#@ output extension=".txt" #> +<#@ assembly name="Microsoft.OpenApi.dll" #> +<#@ assembly name="Microsoft.OpenApi.Readers.dll" #> +<#@ import namespace="System.IO" #> +<#@ import namespace="Microsoft.OpenApi.Models" #> +<#@ import namespace="Microsoft.OpenApi.Readers" #> +<# +OpenApiDocument openApiDocument; +OpenApiDiagnostic openApiDiagnostic; +var yamlFile = Host.ResolvePath("LoadOpenApiDll.yaml"); +using (var fileStream = new FileStream(yamlFile, FileMode.Open, FileAccess.Read)) { +var reader = new OpenApiStreamReader(); +openApiDocument = reader.Read(fileStream, out openApiDiagnostic); +} +#> +<#= openApiDocument.Info.Title #> \ No newline at end of file diff --git a/Mono.TextTemplating.Tests/TestDataPath.cs b/Mono.TextTemplating.Tests/TestDataPath.cs new file mode 100644 index 0000000..729c40a --- /dev/null +++ b/Mono.TextTemplating.Tests/TestDataPath.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Xunit; + +namespace Mono.TextTemplating.Tests; + +struct TestDataPath +{ + readonly string path; + public TestDataPath (string path) => this.path = TrimEndingDirectorySeparator (path); + + public static TestDataPath Get ([CallerMemberName] string testName = null) + => new (Path.Combine (Environment.CurrentDirectory, "TestCases", testName)); + + public TestDataPath Combine (string path) => new (Path.Combine (this.path, path)); + public TestDataPath Combine (string path1, string path2) => new (Path.Combine (path, path1, path2)); + public TestDataPath Combine (string path1, string path2, string path3) => new (Path.Combine (path, path1, path2, path3)); + public TestDataPath Combine (params string[] paths) => new (Path.Combine (path, Path.Combine (paths))); + + public TestDataPath this[string path] => Combine (path); + public TestDataPath this[string path1, string path2] => Combine (path1, path2); + public TestDataPath this[string path1, string path2, string path3] => Combine (path1, path2, path3); + public TestDataPath this[params string[] paths] => Combine (paths); + + public static implicit operator string (TestDataPath path) => path.path; + +#if NETCOREAPP2_0_OR_GREATER + public Task ReadAllTextAsync () => File.ReadAllTextAsync (path); + public async Task ReadAllTextNormalizedAsync () => (await ReadAllTextAsync ().ConfigureAwait (false)).NormalizeNewlines (); +#else + public Task ReadAllTextAsync () => Task.FromResult (ReadAllText ()); + public Task ReadAllTextNormalizedAsync () => Task.FromResult (ReadAllText ().NormalizeNewlines ()); +#endif + + public string ReadAllText () => File.ReadAllText (path); + public string ReadAllTextNormalized () => File.ReadAllText (path).NormalizeNewlines (); + + public TestDataPath AssertFileExists () + { + Assert.True (File.Exists (path), $"File '{path}' does not exist"); + return this; + } + + public TestDataPath AssertDirectoryExists () + { + Assert.True (Directory.Exists (path), $"Directory '{path}' does not exist"); + return this; + } + + static string TrimEndingDirectorySeparator (string path) +#if NETCOREAPP3_0_OR_GREATER + => Path.TrimEndingDirectorySeparator (path); +#else + { + var trimmed = path.TrimEnd (Path.DirectorySeparatorChar); + if (trimmed.Length == path.Length && Path.AltDirectorySeparatorChar != Path.DirectorySeparatorChar) { + return trimmed.TrimEnd (Path.DirectorySeparatorChar); + } + return trimmed; + } +#endif +} diff --git a/Mono.TextTemplating/Mono.TextTemplating.CodeCompilation/CompiledAssemblyData.cs b/Mono.TextTemplating/Mono.TextTemplating.CodeCompilation/CompiledAssemblyData.cs index 91ab720..98f2445 100644 --- a/Mono.TextTemplating/Mono.TextTemplating.CodeCompilation/CompiledAssemblyData.cs +++ b/Mono.TextTemplating/Mono.TextTemplating.CodeCompilation/CompiledAssemblyData.cs @@ -11,6 +11,9 @@ namespace Mono.TextTemplating.CodeCompilation { +#if FEATURE_APPDOMAINS + [Serializable] +#endif class CompiledAssemblyData { public byte[] Assembly { get; } @@ -18,10 +21,14 @@ class CompiledAssemblyData public CompiledAssemblyData (byte[] assembly, byte[] debugSymbols) { - Assembly = assembly ?? throw new System.ArgumentNullException (nameof (assembly)); + Assembly = assembly ?? throw new ArgumentNullException (nameof (assembly)); DebugSymbols = debugSymbols; } +#if FEATURE_APPDOMAINS + CompiledAssemblyData () { } +#endif + #if FEATURE_ASSEMBLY_LOAD_CONTEXT public Assembly LoadInAssemblyLoadContext (AssemblyLoadContext loadContext) { diff --git a/Mono.TextTemplating/Mono.TextTemplating.csproj b/Mono.TextTemplating/Mono.TextTemplating.csproj index 398247c..205f2f7 100644 --- a/Mono.TextTemplating/Mono.TextTemplating.csproj +++ b/Mono.TextTemplating/Mono.TextTemplating.csproj @@ -1,6 +1,6 @@ - netstandard2.0;netcoreapp2.1;net472 + netstandard2.0;netcoreapp2.1;netcoreapp3.1;net472 true ..\TextTemplating.snk true @@ -24,10 +24,6 @@ This package allows embedding the T4 engine in an application. snupkg - - - - diff --git a/Mono.TextTemplating/Mono.TextTemplating/CompiledTemplate.TemplateAssemblyContext.cs b/Mono.TextTemplating/Mono.TextTemplating/CompiledTemplate.TemplateAssemblyContext.cs new file mode 100644 index 0000000..d2e697b --- /dev/null +++ b/Mono.TextTemplating/Mono.TextTemplating/CompiledTemplate.TemplateAssemblyContext.cs @@ -0,0 +1,29 @@ +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Reflection; + +using Microsoft.VisualStudio.TextTemplating; +using Mono.TextTemplating.CodeCompilation; + +namespace Mono.TextTemplating; + +partial class CompiledTemplate +{ + sealed class TemplateAssemblyContext : IDisposable + { +#if FEATURE_ASSEMBLY_LOAD_CONTEXT + readonly TemplateAssemblyLoadContext templateContext; + public TemplateAssemblyContext (ITextTemplatingEngineHost host, string[] referenceAssemblyFiles) => templateContext = new (referenceAssemblyFiles, host); + public Assembly LoadAssemblyFile (string assemblyPath) => templateContext.LoadFromAssemblyPath (assemblyPath); + public Assembly LoadInMemoryAssembly (CompiledAssemblyData assemblyData) => assemblyData.LoadInAssemblyLoadContext (templateContext); + public void Dispose () { } +#else + readonly CurrentDomainAssemblyResolver assemblyResolver; + public TemplateAssemblyContext (ITextTemplatingEngineHost host, string[] referenceAssemblyFiles) => assemblyResolver = new (referenceAssemblyFiles, host.ResolveAssemblyReference); + public Assembly LoadAssemblyFile (string assemblyPath) => Assembly.LoadFile (assemblyPath); + public Assembly LoadInMemoryAssembly (CompiledAssemblyData assemblyData) => assemblyData.LoadInCurrentAppDomain (); + public void Dispose () => assemblyResolver.Dispose (); +#endif + } +} diff --git a/Mono.TextTemplating/Mono.TextTemplating/CompiledTemplate.TemplateExecutor.cs b/Mono.TextTemplating/Mono.TextTemplating/CompiledTemplate.TemplateExecutor.cs new file mode 100644 index 0000000..d9cd4ee --- /dev/null +++ b/Mono.TextTemplating/Mono.TextTemplating/CompiledTemplate.TemplateExecutor.cs @@ -0,0 +1,88 @@ +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.CodeDom.Compiler; +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; + +using Microsoft.VisualStudio.TextTemplating; +using Mono.TextTemplating.CodeCompilation; + +namespace Mono.TextTemplating; + +partial class CompiledTemplate +{ + class TemplateProcessor : MarshalByRefObject + { + public string CreateAndProcess (ITextTemplatingEngineHost host, CompiledAssemblyData templateAssemblyData, string templateAssemblyFile, string fullName, CultureInfo culture, string[] referencedAssemblyFiles) + { + using var context = new TemplateAssemblyContext (host, referencedAssemblyFiles); + + Assembly assembly = templateAssemblyData is not null + ? context.LoadInMemoryAssembly (templateAssemblyData) + : context.LoadAssemblyFile (templateAssemblyFile); + + // MS Templating Engine does not care about the type itself + // it only requires the expected members to be on the compiled type + // so we don't try to cast it, we invoke via reflection instead + // TODO: could we use additional codegen to collapse all the init work to a single method? + Type transformType = assembly.GetType (fullName); + object textTransformation = Activator.CreateInstance (transformType); + + //set the host property if it exists + Type hostType = null; + if (host is TemplateGenerator gen) { + hostType = gen.SpecificHostType; + } + var hostProp = transformType.GetProperty ("Host", hostType ?? typeof (ITextTemplatingEngineHost)); + if (hostProp != null && hostProp.CanWrite) + hostProp.SetValue (textTransformation, host, null); + + if (host is ITextTemplatingSessionHost sessionHost) { + //FIXME: should we create a session if it's null? + var sessionProp = transformType.GetProperty ("Session", typeof (IDictionary)); + sessionProp.SetValue (textTransformation, sessionHost.Session, null); + } + + var errorProp = transformType.GetProperty ("Errors", BindingFlags.Instance | BindingFlags.NonPublic); + if (errorProp == null) + throw new ArgumentException ("Template must have 'Errors' property"); + var errorMethod = transformType.GetMethod ("Error", new Type[] { typeof (string) }); + if (errorMethod == null) { + throw new ArgumentException ("Template must have 'Error(string message)' method"); + } + + var errors = (CompilerErrorCollection)errorProp.GetValue (textTransformation, null); + errors.Clear (); + + //set the culture + if (culture != null) + ToStringHelper.FormatProvider = culture; + else + ToStringHelper.FormatProvider = CultureInfo.InvariantCulture; + + string output = null; + + var initMethod = transformType.GetMethod ("Initialize"); + var transformMethod = transformType.GetMethod ("TransformText"); + + if (initMethod == null) { + errorMethod.Invoke (textTransformation, new object[] { "Error running transform: no method Initialize()" }); + } else if (transformMethod == null) { + errorMethod.Invoke (textTransformation, new object[] { "Error running transform: no method TransformText()" }); + } else try { + initMethod.Invoke (textTransformation, null); + output = (string)transformMethod.Invoke (textTransformation, null); + } + catch (Exception ex) { + errorMethod.Invoke (textTransformation, new object[] { "Error running transform: " + ex }); + } + + host.LogErrors (errors); + + ToStringHelper.FormatProvider = CultureInfo.InvariantCulture; + return output; + } + } +} diff --git a/Mono.TextTemplating/Mono.TextTemplating/CompiledTemplate.cs b/Mono.TextTemplating/Mono.TextTemplating/CompiledTemplate.cs index 1992993..b0711cf 100644 --- a/Mono.TextTemplating/Mono.TextTemplating/CompiledTemplate.cs +++ b/Mono.TextTemplating/Mono.TextTemplating/CompiledTemplate.cs @@ -26,149 +26,92 @@ using System; using System.CodeDom.Compiler; -using System.Collections.Generic; using System.Globalization; -using System.Reflection; -using System.IO; using Microsoft.VisualStudio.TextTemplating; using Mono.TextTemplating.CodeCompilation; namespace Mono.TextTemplating { - public sealed class CompiledTemplate : + public sealed partial class CompiledTemplate : #if FEATURE_APPDOMAINS MarshalByRefObject, #endif IDisposable { - ITextTemplatingEngineHost host; - object textTransformation; + readonly ITextTemplatingEngineHost host; readonly CultureInfo culture; + readonly string templateClassFullName; -#if FEATURE_ASSEMBLY_LOAD_CONTEXT - readonly TemplateAssemblyLoadContext templateContext; -#else - readonly CurrentDomainAssemblyResolver assemblyResolver; -#endif + readonly CompiledAssemblyData templateAssemblyData; + readonly string templateAssemblyFile; - [Obsolete ("Should not have been public")] - public CompiledTemplate (ITextTemplatingEngineHost host, CompilerResults results, string fullName, CultureInfo culture, string[] assemblyFiles) - : this (host, culture, assemblyFiles) - { - Assembly assembly = results.PathToAssembly != null - ? LoadAssemblyFile (results.PathToAssembly) - : results.CompiledAssembly; - InitializeTemplate (assembly, fullName); - } + internal string[] ReferencedAssemblyFiles { get; } + [Obsolete ("Should not have been public")] + public CompiledTemplate (ITextTemplatingEngineHost host, CompilerResults results, string fullName, CultureInfo culture, string[] assemblyFiles) => throw new NotSupportedException (); + [Obsolete ("Should not have been public")] public CompiledTemplate (ITextTemplatingEngineHost host, string templateAssemblyFile, string fullName, CultureInfo culture, string[] referenceAssemblyFiles) - : this (host, culture, referenceAssemblyFiles) + : this (fullName, host, culture, referenceAssemblyFiles) { - var assembly = LoadAssemblyFile (templateAssemblyFile); - InitializeTemplate (assembly, fullName); + this.templateAssemblyFile = templateAssemblyFile; } - internal CompiledTemplate (ITextTemplatingEngineHost host, CompiledAssemblyData compiledAssembly, string fullName, CultureInfo culture, string[] referenceAssemblyFiles) - : this (host, culture, referenceAssemblyFiles) + internal CompiledTemplate (ITextTemplatingEngineHost host, CompiledAssemblyData templateAssemblyData, string fullName, CultureInfo culture, string[] referencedAssemblyFiles) + : this (fullName, host, culture, referencedAssemblyFiles) { - var assembly = LoadInMemoryAssembly (compiledAssembly); - InitializeTemplate (assembly, fullName); + this.templateAssemblyData = templateAssemblyData; } - CompiledTemplate (ITextTemplatingEngineHost host, CultureInfo culture, string[] referenceAssemblyFiles) + CompiledTemplate (string templateClassFullName, ITextTemplatingEngineHost host, CultureInfo culture, string[] referencedAssemblyFiles) { + this.templateClassFullName = templateClassFullName; this.host = host; this.culture = culture; - -#if FEATURE_ASSEMBLY_LOAD_CONTEXT - templateContext = new TemplateAssemblyLoadContext (referenceAssemblyFiles, host); -#else - assemblyResolver = new (host, referenceAssemblyFiles); -#endif + this.ReferencedAssemblyFiles = referencedAssemblyFiles; } -#if FEATURE_ASSEMBLY_LOAD_CONTEXT - Assembly LoadAssemblyFile (string assemblyPath) => templateContext.LoadFromAssemblyPath (assemblyPath); - Assembly LoadInMemoryAssembly (CompiledAssemblyData assemblyData) => assemblyData.LoadInAssemblyLoadContext (templateContext); -#else - static Assembly LoadAssemblyFile (string assemblyPath) => Assembly.LoadFile (assemblyPath); - static Assembly LoadInMemoryAssembly (CompiledAssemblyData assemblyData) => assemblyData.LoadInCurrentAppDomain (); -#endif - - void InitializeTemplate (Assembly assembly, string fullName) +#if FEATURE_APPDOMAINS + string templateContentForAppDomain; + internal void SetTemplateContentForAppDomain (string content) { - //MS Templating Engine does not care about the type itself - //it only requires the expected members to be on the compiled type - Type transformType = assembly.GetType (fullName); - textTransformation = Activator.CreateInstance (transformType); + templateContentForAppDomain = content; + } - //set the host property if it exists - Type hostType = null; - if (host is TemplateGenerator gen) { - hostType = gen.SpecificHostType; + TemplateProcessor CreateTemplateProcessor () + { + var domain = host.ProvideTemplatingAppDomain (templateContentForAppDomain); + if (domain == null) { + return new TemplateProcessor (); } - var hostProp = transformType.GetProperty ("Host", hostType ?? typeof (ITextTemplatingEngineHost)); - if (hostProp != null && hostProp.CanWrite) - hostProp.SetValue (textTransformation, host, null); - if (host is ITextTemplatingSessionHost sessionHost) { - //FIXME: should we create a session if it's null? - var sessionProp = transformType.GetProperty ("Session", typeof (IDictionary)); - sessionProp.SetValue (textTransformation, sessionHost.Session, null); + var templateProcessorType = typeof (TemplateProcessor); + + try { + var obj = domain.CreateInstanceAndUnwrap (templateProcessorType.Assembly.FullName, templateProcessorType.FullName); + return (TemplateProcessor)obj; + } + catch (Exception ex) when (ex is MissingMethodException || ex is System.IO.FileNotFoundException || ex is System.Runtime.Serialization.SerializationException) { + throw new TemplatingEngineException ( + $"Could not instantiate type {templateProcessorType.FullName} in templating AppDomain '{domain.FriendlyName ?? "(no name)"}'. " + + $"The assembly '{templateProcessorType.Assembly.FullName}' must be resolvable in the domain. " + + $"The AppDomain's base directory may be incorrect: '{domain.BaseDirectory}'", + ex); } } +#else + static TemplateProcessor CreateTemplateProcessor () => new (); +#endif public string Process () { - var ttType = textTransformation.GetType (); - - var errorProp = ttType.GetProperty ("Errors", BindingFlags.Instance | BindingFlags.NonPublic); - if (errorProp == null) - throw new ArgumentException ("Template must have 'Errors' property"); - var errorMethod = ttType.GetMethod ("Error", new Type [] { typeof (string) }); - if (errorMethod == null) { - throw new ArgumentException ("Template must have 'Error(string message)' method"); - } - - var errors = (CompilerErrorCollection)errorProp.GetValue (textTransformation, null); - errors.Clear (); - - //set the culture - if (culture != null) - ToStringHelper.FormatProvider = culture; - else - ToStringHelper.FormatProvider = CultureInfo.InvariantCulture; - - string output = null; - - var initMethod = ttType.GetMethod ("Initialize"); - var transformMethod = ttType.GetMethod ("TransformText"); - - if (initMethod == null) { - errorMethod.Invoke (textTransformation, new object [] { "Error running transform: no method Initialize()" }); - } else if (transformMethod == null) { - errorMethod.Invoke (textTransformation, new object [] { "Error running transform: no method TransformText()" }); - } else try { - initMethod.Invoke (textTransformation, null); - output = (string)transformMethod.Invoke (textTransformation, null); - } catch (Exception ex) { - errorMethod.Invoke (textTransformation, new object [] { "Error running transform: " + ex }); - } - - host.LogErrors (errors); - - ToStringHelper.FormatProvider = CultureInfo.InvariantCulture; - return output; + TemplateProcessor processor = CreateTemplateProcessor (); + return processor.CreateAndProcess (host, templateAssemblyData, templateAssemblyFile, templateClassFullName, culture, ReferencedAssemblyFiles); } public void Dispose () { - host = null; -#if !FEATURE_ASSEMBLY_LOAD_CONTEXT - assemblyResolver.Dispose (); -#endif } } } diff --git a/Mono.TextTemplating/Mono.TextTemplating/CurrentDomainAssemblyResolver.cs b/Mono.TextTemplating/Mono.TextTemplating/CurrentDomainAssemblyResolver.cs index 28964fd..2d79a8a 100644 --- a/Mono.TextTemplating/Mono.TextTemplating/CurrentDomainAssemblyResolver.cs +++ b/Mono.TextTemplating/Mono.TextTemplating/CurrentDomainAssemblyResolver.cs @@ -6,42 +6,40 @@ using System; using System.IO; using System.Reflection; -using Microsoft.VisualStudio.TextTemplating; - namespace Mono.TextTemplating { class CurrentDomainAssemblyResolver : IDisposable { - readonly ITextTemplatingEngineHost host; + readonly Func resolveAssemblyReference; readonly string[] assemblyFiles; bool disposed; - public CurrentDomainAssemblyResolver (ITextTemplatingEngineHost host, string[] assemblyFiles) + public CurrentDomainAssemblyResolver (string[] assemblyFiles, Func resolveAssemblyReference) { - this.host = host; + this.resolveAssemblyReference = resolveAssemblyReference; this.assemblyFiles = assemblyFiles; + + AppDomain.CurrentDomain.AssemblyResolve += ResolveReferencedAssemblies; } Assembly ResolveReferencedAssemblies (object sender, ResolveEventArgs args) { - AssemblyName asmName = new AssemblyName (args.Name); + var asmName = new AssemblyName (args.Name); + foreach (var asmFile in assemblyFiles) { - if (asmName.Name == Path.GetFileNameWithoutExtension (asmFile)) + if (asmName.Name == Path.GetFileNameWithoutExtension (asmFile)) { return Assembly.LoadFrom (asmFile); + } } - var path = host.ResolveAssemblyReference (asmName.Name + ".dll"); - if (File.Exists (path)) + var path = resolveAssemblyReference (asmName.Name + ".dll"); + if (File.Exists (path)) { return Assembly.LoadFrom (path); + } return null; } - public void RegisterForCurrentDomain () - { - AppDomain.CurrentDomain.AssemblyResolve += ResolveReferencedAssemblies; - } - public void Dispose () { if (!disposed) { diff --git a/Mono.TextTemplating/Mono.TextTemplating/TemplateAssemblyLoadContext.cs b/Mono.TextTemplating/Mono.TextTemplating/TemplateAssemblyLoadContext.cs index cf9ee0d..59bb59b 100644 --- a/Mono.TextTemplating/Mono.TextTemplating/TemplateAssemblyLoadContext.cs +++ b/Mono.TextTemplating/Mono.TextTemplating/TemplateAssemblyLoadContext.cs @@ -23,6 +23,9 @@ class TemplateAssemblyLoadContext : AssemblyLoadContext readonly AssemblyName hostAsmName; public TemplateAssemblyLoadContext (string[] templateAssemblyFiles, ITextTemplatingEngineHost host) +#if NETCOREAPP3_0_OR_GREATER + : base (isCollectible: true) +#endif { this.templateAssemblyFiles = templateAssemblyFiles; this.host = host; diff --git a/Mono.TextTemplating/Mono.TextTemplating/TemplateGenerator.cs b/Mono.TextTemplating/Mono.TextTemplating/TemplateGenerator.cs index b0c6819..1b65322 100644 --- a/Mono.TextTemplating/Mono.TextTemplating/TemplateGenerator.cs +++ b/Mono.TextTemplating/Mono.TextTemplating/TemplateGenerator.cs @@ -90,9 +90,7 @@ public Task CompileTemplateAsync (string content, Cancellation if (string.IsNullOrEmpty (content)) throw new ArgumentNullException (nameof (content)); - Errors.Clear (); - encoding = Utf8.BomlessEncoding; - + InitializeForRun (); return Engine.CompileTemplateAsync (content, this, token); } @@ -104,6 +102,19 @@ protected internal TemplatingEngine Engine { } } + void InitializeForRun (string inputFileName = null, string outputFileName = null, Encoding encoding = null) + { + Errors.Clear (); + this.encoding = encoding ?? Utf8.BomlessEncoding; + + if (outputFileName is null && inputFileName is not null) { + outputFileName = Path.ChangeExtension (inputFileName, ".txt"); + } + + TemplateFile = inputFileName; + OutputFile = outputFileName; + } + [Obsolete("Use ProcessTemplateAsync")] public bool ProcessTemplate (string inputFile, string outputFile) => ProcessTemplateAsync (inputFile, outputFile, CancellationToken.None).Result; @@ -115,6 +126,8 @@ public async Task ProcessTemplateAsync (string inputFile, string outputFil if (string.IsNullOrEmpty (outputFile)) throw new ArgumentNullException (nameof (outputFile)); + InitializeForRun (inputFile, outputFile); + string content; try { #if NETCOREAPP2_1_OR_GREATER @@ -124,7 +137,6 @@ public async Task ProcessTemplateAsync (string inputFile, string outputFil #endif } catch (IOException ex) { - Errors.Clear (); AddError ("Could not read input file '" + inputFile + "':\n" + ex); return false; } @@ -155,15 +167,11 @@ public bool ProcessTemplate (string inputFileName, string inputContent, ref stri public async Task<(string fileName, string content, bool success)> ProcessTemplateAsync (string inputFileName, string inputContent, string outputFileName, CancellationToken token = default) { - Errors.Clear (); - encoding = Utf8.BomlessEncoding; + InitializeForRun (inputFileName, outputFileName); - OutputFile = outputFileName; - TemplateFile = inputFileName; var outputContent = await Engine.ProcessTemplateAsync (inputContent, this, token).ConfigureAwait (false); - outputFileName = OutputFile; - return (outputFileName, outputContent, !Errors.HasErrors); + return (OutputFile, outputContent, !Errors.HasErrors); } public bool PreprocessTemplate (string inputFile, string className, string classNamespace, @@ -177,11 +185,12 @@ public bool PreprocessTemplate (string inputFile, string className, string class if (string.IsNullOrEmpty (outputFile)) throw new ArgumentNullException (nameof (outputFile)); + InitializeForRun (inputFile, outputFile, encoding); + string content; try { content = File.ReadAllText (inputFile); } catch (IOException ex) { - Errors.Clear (); AddError ("Could not read input file '" + inputFile + "':\n" + ex); return false; } @@ -201,10 +210,8 @@ public bool PreprocessTemplate (string inputFile, string className, string class public bool PreprocessTemplate (string inputFileName, string className, string classNamespace, string inputContent, out string language, out string[] references, out string outputContent) { - Errors.Clear (); - encoding = Utf8.BomlessEncoding; + InitializeForRun (null, inputFileName); - TemplateFile = inputFileName; outputContent = Engine.PreprocessTemplate (inputContent, this, className, classNamespace, out language, out references); return !Errors.HasErrors; @@ -231,7 +238,7 @@ public string PreprocessTemplate ( out string language, out string[] references) { - TemplateFile = inputFile; + InitializeForRun (inputFileName: inputFile); return Engine.PreprocessTemplate (pt, inputContent, settings, this, out language, out references); } @@ -243,13 +250,9 @@ public string PreprocessTemplate ( TemplateSettings settings, CancellationToken token = default) { - Errors.Clear (); - encoding = Utf8.BomlessEncoding; + InitializeForRun (inputFileName, outputFileName); - OutputFile = outputFileName; - TemplateFile = inputFileName; var outputContent = await Engine.ProcessTemplateAsync (pt, inputContent, settings, this, token).ConfigureAwait (false); - outputFileName = OutputFile; return (outputFileName, outputContent); } @@ -269,13 +272,13 @@ public virtual AppDomain ProvideTemplatingAppDomain (string content) protected virtual string ResolveAssemblyReference (string assemblyReference) { - if (System.IO.Path.IsPathRooted (assemblyReference)) - return assemblyReference; - foreach (string referencePath in ReferencePaths) { - var path = System.IO.Path.Combine (referencePath, assemblyReference); - if (System.IO.File.Exists (path)) - return path; - } + if (Path.IsPathRooted (assemblyReference)) + return assemblyReference; + foreach (string referencePath in ReferencePaths) { + var path = Path.Combine (referencePath, assemblyReference); + if (File.Exists (path)) + return path; + } var assemblyName = new AssemblyName(assemblyReference); if (assemblyName.Version != null)//Load via GAC and return full path @@ -476,11 +479,13 @@ string ITextTemplatingEngineHost.ResolvePath (string path) void ITextTemplatingEngineHost.SetFileExtension (string extension) { - extension = extension.TrimStart ('.'); - if (Path.HasExtension (OutputFile)) { - OutputFile = Path.ChangeExtension (OutputFile, extension); - } else { - OutputFile = OutputFile + "." + extension; + if (OutputFile is not null) { + extension = extension.TrimStart ('.'); + if (Path.HasExtension (OutputFile)) { + OutputFile = Path.ChangeExtension (OutputFile, extension); + } else { + OutputFile = OutputFile + "." + extension; + } } } diff --git a/Mono.TextTemplating/Mono.TextTemplating/TemplatingEngine.cs b/Mono.TextTemplating/Mono.TextTemplating/TemplatingEngine.cs index 3828d38..eb3b33c 100644 --- a/Mono.TextTemplating/Mono.TextTemplating/TemplatingEngine.cs +++ b/Mono.TextTemplating/Mono.TextTemplating/TemplatingEngine.cs @@ -275,20 +275,11 @@ CancellationToken token return null; } + var compiledTemplate = new CompiledTemplate (host, assembly, settings.GetFullName (), settings.Culture, references); #if FEATURE_APPDOMAINS - var domain = host.ProvideTemplatingAppDomain (content); - var templateClassFullName = string.Concat(settings.Namespace, ".", settings.Name); - if (domain != null) { - var type = typeof(CompiledTemplate); - var obj = domain.CreateInstanceFromAndUnwrap (type.Assembly.Location, - type.FullName, - new object[] { host, results, templateClassFullName, settings.Culture, references.ToArray () }); - - return ((CompiledTemplate)obj, references); - } + compiledTemplate.SetTemplateContentForAppDomain (content); #endif - - return (new CompiledTemplate (host, assembly, settings.GetFullName (), settings.Culture, references), references); + return (compiledTemplate, references); } async Task<(CompilerResults, CompiledAssemblyData)> CompileCode (IEnumerable references, TemplateSettings settings, CodeCompileUnit ccu, CancellationToken token) diff --git a/TextTransform/TextTransform.csproj b/TextTransform/TextTransform.csproj index 703fd32..5974756 100644 --- a/TextTransform/TextTransform.csproj +++ b/TextTransform/TextTransform.csproj @@ -15,10 +15,6 @@ snupkg - - - - diff --git a/dotnet-t4-project-tool/dotnet-t4-project-tool.csproj b/dotnet-t4-project-tool/dotnet-t4-project-tool.csproj index fd89441..6a83abb 100644 --- a/dotnet-t4-project-tool/dotnet-t4-project-tool.csproj +++ b/dotnet-t4-project-tool/dotnet-t4-project-tool.csproj @@ -19,11 +19,6 @@ This package can be installed into a project using `DotNetCliToolReference`. true - - - - - diff --git a/dotnet-t4/dotnet-t4.csproj b/dotnet-t4/dotnet-t4.csproj index 9e4de61..6b641b9 100644 --- a/dotnet-t4/dotnet-t4.csproj +++ b/dotnet-t4/dotnet-t4.csproj @@ -22,11 +22,6 @@ This package can be installed as a dotnet global or local tool. true - - - - - Project test.tt