diff --git a/samples/app-with-targetframeworks/MultipleTargetFrameworks/MultipleTargetFrameworks.csproj b/samples/app-with-targetframeworks/MultipleTargetFrameworks/MultipleTargetFrameworks.csproj new file mode 100644 index 00000000..85e12a1d --- /dev/null +++ b/samples/app-with-targetframeworks/MultipleTargetFrameworks/MultipleTargetFrameworks.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp3.1;netcoreapp2.1 + + + + + + + diff --git a/samples/app-with-targetframeworks/MultipleTargetFrameworks/Program.cs b/samples/app-with-targetframeworks/MultipleTargetFrameworks/Program.cs new file mode 100644 index 00000000..bb4cbb51 --- /dev/null +++ b/samples/app-with-targetframeworks/MultipleTargetFrameworks/Program.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace MultipleTargetFrameworks +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + +#if NETCOREAPP3_1 + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); +#else + public static IWebHostBuilder CreateHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup(); +#endif + } +} diff --git a/samples/app-with-targetframeworks/MultipleTargetFrameworks/Properties/launchSettings.json b/samples/app-with-targetframeworks/MultipleTargetFrameworks/Properties/launchSettings.json new file mode 100644 index 00000000..bb776a83 --- /dev/null +++ b/samples/app-with-targetframeworks/MultipleTargetFrameworks/Properties/launchSettings.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:45835", + "sslPort": 44389 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "weatherforecast", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "MultipleTargetFrameworks": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "weatherforecast", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/app-with-targetframeworks/MultipleTargetFrameworks/Startup.cs b/samples/app-with-targetframeworks/MultipleTargetFrameworks/Startup.cs new file mode 100644 index 00000000..596a699b --- /dev/null +++ b/samples/app-with-targetframeworks/MultipleTargetFrameworks/Startup.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.HttpsPolicy; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace MultipleTargetFrameworks +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app) + { + app.UseHttpsRedirection(); +#if NETCOREAPP3_1 + app.Run(async context => await context.Response.WriteAsync("NETCOREAPP3_1")); +#else + app.Run(async context => await context.Response.WriteAsync("NETCOREAPP2_2")); +#endif + } + } +} diff --git a/samples/app-with-targetframeworks/MultipleTargetFrameworks/appsettings.Development.json b/samples/app-with-targetframeworks/MultipleTargetFrameworks/appsettings.Development.json new file mode 100644 index 00000000..8983e0fc --- /dev/null +++ b/samples/app-with-targetframeworks/MultipleTargetFrameworks/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/samples/app-with-targetframeworks/MultipleTargetFrameworks/appsettings.json b/samples/app-with-targetframeworks/MultipleTargetFrameworks/appsettings.json new file mode 100644 index 00000000..d9d9a9bf --- /dev/null +++ b/samples/app-with-targetframeworks/MultipleTargetFrameworks/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/samples/app-with-targetframeworks/app-with-targetframeworks.sln b/samples/app-with-targetframeworks/app-with-targetframeworks.sln new file mode 100644 index 00000000..c949b11a --- /dev/null +++ b/samples/app-with-targetframeworks/app-with-targetframeworks.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MultipleTargetFrameworks", "MultipleTargetFrameworks\MultipleTargetFrameworks.csproj", "{D67C994A-74B0-4A15-BE84-E0518EEF3F74}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D67C994A-74B0-4A15-BE84-E0518EEF3F74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D67C994A-74B0-4A15-BE84-E0518EEF3F74}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D67C994A-74B0-4A15-BE84-E0518EEF3F74}.Debug|x64.ActiveCfg = Debug|Any CPU + {D67C994A-74B0-4A15-BE84-E0518EEF3F74}.Debug|x64.Build.0 = Debug|Any CPU + {D67C994A-74B0-4A15-BE84-E0518EEF3F74}.Debug|x86.ActiveCfg = Debug|Any CPU + {D67C994A-74B0-4A15-BE84-E0518EEF3F74}.Debug|x86.Build.0 = Debug|Any CPU + {D67C994A-74B0-4A15-BE84-E0518EEF3F74}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D67C994A-74B0-4A15-BE84-E0518EEF3F74}.Release|Any CPU.Build.0 = Release|Any CPU + {D67C994A-74B0-4A15-BE84-E0518EEF3F74}.Release|x64.ActiveCfg = Release|Any CPU + {D67C994A-74B0-4A15-BE84-E0518EEF3F74}.Release|x64.Build.0 = Release|Any CPU + {D67C994A-74B0-4A15-BE84-E0518EEF3F74}.Release|x86.ActiveCfg = Release|Any CPU + {D67C994A-74B0-4A15-BE84-E0518EEF3F74}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/samples/app-with-targetframeworks/tye.yaml b/samples/app-with-targetframeworks/tye.yaml new file mode 100644 index 00000000..331dd038 --- /dev/null +++ b/samples/app-with-targetframeworks/tye.yaml @@ -0,0 +1,13 @@ +# tye application configuration file +# read all about it at https://github.com/dotnet/tye +# +# when you've given us a try, we'd love to know what you think: +# https://aka.ms/AA7q20u +# +name: app-with-targetframeworks +services: +- name: multipletargetframeworks + project: MultipleTargetFrameworks/MultipleTargetFrameworks.csproj + buildProperties: + - name: TargetFramework + value: netcoreapp3.1 diff --git a/src/Microsoft.Tye.Core/ApplicationFactory.cs b/src/Microsoft.Tye.Core/ApplicationFactory.cs index 9e34cd87..1e382a42 100644 --- a/src/Microsoft.Tye.Core/ApplicationFactory.cs +++ b/src/Microsoft.Tye.Core/ApplicationFactory.cs @@ -17,7 +17,7 @@ namespace Microsoft.Tye { public static class ApplicationFactory { - public static async Task CreateAsync(OutputContext output, FileInfo source, ApplicationFactoryFilter? filter = null) + public static async Task CreateAsync(OutputContext output, FileInfo source, string? framework = null, ApplicationFactoryFilter? filter = null) { if (source is null) { @@ -104,7 +104,10 @@ namespace Microsoft.Tye sb.AppendLine($" $"{kvp.Name}={kvp.Value}").Aggregate((a, b) => a + ";" + b) : string.Empty)}\" />"); + $"BuildProperties=\"" + + $"{(project.BuildProperties.Any() ? project.BuildProperties.Select(kvp => $"{kvp.Name}={kvp.Value}").Aggregate((a, b) => a + ";" + b) : string.Empty)}" + + $"{(string.IsNullOrEmpty(framework) ? string.Empty : $";TargetFramework={framework}")}" + + $"\" />"); } sb.AppendLine(@" "); @@ -124,7 +127,12 @@ namespace Microsoft.Tye "dotnet", $"build " + $"\"{projectPath}\" " + + // CustomAfterMicrosoftCommonTargets is imported by non-crosstargeting (single TFM) projects $"/p:CustomAfterMicrosoftCommonTargets={Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "ProjectEvaluation.targets")} " + + // CustomAfterMicrosoftCommonCrossTargetingTargets is imported by crosstargeting (multi-TFM) projects + // This ensures projects properties are evaluated correctly. However, multi-TFM projects must specify + // a specific TFM to build/run/publish and will otherwise throw an exception. + $"/p:CustomAfterMicrosoftCommonCrossTargetingTargets={Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "ProjectEvaluation.targets")} " + $"/nologo", throwOnError: false, workingDirectory: directory.DirectoryPath); @@ -181,6 +189,7 @@ namespace Microsoft.Tye { project.BuildProperties.Add(buildProperty.Name, buildProperty.Value); } + project.Replicas = configService.Replicas ?? 1; project.Liveness = configService.Liveness != null ? GetProbeBuilder(configService.Liveness) : null; @@ -198,6 +207,12 @@ namespace Microsoft.Tye ProjectReader.ReadProjectDetails(output, project, projectMetadata[configService.Name]); + if (framework != null && project.TargetFrameworks.Any()) + { + // Only use the TargetFramework for the "--framework" if it's a multi-targeted project and an override is provided + project.BuildProperties["TargetFramework"] = framework; + } + // Do k8s by default. project.ManifestInfo = new KubernetesManifestInfo(); } diff --git a/src/Microsoft.Tye.Core/PublishProjectStep.cs b/src/Microsoft.Tye.Core/PublishProjectStep.cs index 9dbbd7c1..8cfd6da0 100644 --- a/src/Microsoft.Tye.Core/PublishProjectStep.cs +++ b/src/Microsoft.Tye.Core/PublishProjectStep.cs @@ -36,11 +36,15 @@ namespace Microsoft.Tye var outputDirectory = TempDirectory.Create(); output.WriteDebugLine("Running 'dotnet publish'."); - output.WriteCommandLine("dotnet", $"publish \"{project.ProjectFile.FullName}\" -c Release -o \"{outputDirectory.DirectoryPath}\""); + var dotnetPublishArguments = project.BuildProperties.TryGetValue("TargetFramework", out var framework) + ? $"publish \"{project.ProjectFile.FullName}\" -c Release -f {framework} -o \"{outputDirectory.DirectoryPath}\"" + : $"publish \"{project.ProjectFile.FullName}\" -c Release -o \"{outputDirectory.DirectoryPath}\""; + + output.WriteCommandLine("dotnet", dotnetPublishArguments); var publishResult = await ProcessUtil.RunAsync( $"dotnet", - $"publish \"{project.ProjectFile.FullName}\" -c Release -o \"{outputDirectory.DirectoryPath}\"", + dotnetPublishArguments, project.ProjectFile.DirectoryName, throwOnError: false); diff --git a/src/Microsoft.Tye.Core/StandardOptions.cs b/src/Microsoft.Tye.Core/StandardOptions.cs index 6d3ef878..783dafd5 100644 --- a/src/Microsoft.Tye.Core/StandardOptions.cs +++ b/src/Microsoft.Tye.Core/StandardOptions.cs @@ -46,16 +46,18 @@ namespace Microsoft.Tye } } - public static Option Force - { - get - { - return new Option("--force", "Force overwrite of existing files") - { - Argument = new Argument(), - }; - } - } + public static Option Framework => + new Option(new string[] { "-f", "--framework" }) + { + Description = "The target framework override to use for all cross-targeting projects with multiple TFMs. " + + "This value must be a valid target framework for each individual cross-targeting project. " + + "Non-crosstargeting projects will ignore this value. ", + Argument = new Argument("framework") + { + Arity = ArgumentArity.ExactlyOne + }, + Required = false + }; public static Option Interactive { @@ -224,5 +226,13 @@ namespace Microsoft.Tye }; } } + + public static Option CreateForce(string descriptions) => + new Option(new[] { "--force" }) + { + Argument = new Argument(), + Description = descriptions, + Required = false + }; } } diff --git a/src/tye/ApplicationBuilderExtensions.cs b/src/tye/ApplicationBuilderExtensions.cs index a8305f0f..e8840f52 100644 --- a/src/tye/ApplicationBuilderExtensions.cs +++ b/src/tye/ApplicationBuilderExtensions.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; using Microsoft.Tye.Extensions; using Microsoft.Tye.Hosting.Model; @@ -122,7 +123,17 @@ namespace Microsoft.Tye { if (project.TargetFrameworks.Length > 1) { - throw new InvalidOperationException($"Unable to run {project.Name}. Multi-targeted projects are not supported."); + if (project.BuildProperties.TryGetValue("TargetFramework", out var targetFramework)) + { + if (!project.TargetFrameworks.Contains(targetFramework)) + { + throw new InvalidOperationException($"Unable to run {project.Name}. The specified TargetFramework is not one of the existing TargetFrameworks in the project."); + } + } + else + { + throw new InvalidOperationException($"Unable to run {project.Name}. Your project targets multiple frameworks. Specify which framework to run using '--framework'."); + } } if (project.RunCommand == null) diff --git a/src/tye/BuildHost.cs b/src/tye/BuildHost.cs index 3e46ca00..dcfe2076 100644 --- a/src/tye/BuildHost.cs +++ b/src/tye/BuildHost.cs @@ -11,11 +11,9 @@ namespace Microsoft.Tye { public static class BuildHost { - public static async Task BuildAsync(IConsole console, FileInfo path, Verbosity verbosity, bool interactive, string[] tags) + public static async Task BuildAsync(OutputContext output, FileInfo path, bool interactive, string? framework = null, ApplicationFactoryFilter? filter = null) { - var output = new OutputContext(console, verbosity); - var filter = ApplicationFactoryFilter.GetApplicationFactoryFilter(tags); - var application = await ApplicationFactory.CreateAsync(output, path, filter); + var application = await ApplicationFactory.CreateAsync(output, path, framework, filter); if (application.Services.Count == 0) { throw new CommandException($"No services found in \"{application.Source.Name}\""); diff --git a/src/tye/GenerateHost.cs b/src/tye/GenerateHost.cs index 1ff0cec5..978ca359 100644 --- a/src/tye/GenerateHost.cs +++ b/src/tye/GenerateHost.cs @@ -13,18 +13,15 @@ namespace Microsoft.Tye { public static class GenerateHost { - public static async Task GenerateAsync(IConsole console, FileInfo path, Verbosity verbosity, bool interactive, string ns, string[] tags) + public static async Task GenerateAsync(OutputContext output, FileInfo path, bool interactive, string ns, string? framework = null, ApplicationFactoryFilter? filter = null) { - var output = new OutputContext(console, verbosity); + var application = await ApplicationFactory.CreateAsync(output, path, framework, filter); - output.WriteInfoLine("Loading Application Details..."); - var filter = ApplicationFactoryFilter.GetApplicationFactoryFilter(tags); - var application = await ApplicationFactory.CreateAsync(output, path, filter); if (application.Services.Count == 0) { throw new CommandException($"No services found in \"{application.Source.Name}\""); } - if (!String.IsNullOrEmpty(ns)) + if (!string.IsNullOrEmpty(ns)) { application.Namespace = ns; } diff --git a/src/tye/Program.BuildCommand.cs b/src/tye/Program.BuildCommand.cs index eb436aba..d7079b13 100644 --- a/src/tye/Program.BuildCommand.cs +++ b/src/tye/Program.BuildCommand.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.Collections.Generic; using System.CommandLine; using System.CommandLine.Invocation; @@ -20,22 +21,43 @@ namespace Microsoft.Tye { CommonArguments.Path_Required, StandardOptions.Interactive, + StandardOptions.Tags, StandardOptions.Verbosity, - StandardOptions.Tags + StandardOptions.Framework, }; - command.Handler = CommandHandler.Create((console, path, verbosity, interactive, tags) => + command.Handler = CommandHandler.Create(args => { // Workaround for https://github.com/dotnet/command-line-api/issues/723#issuecomment-593062654 - if (path is null) + if (args.Path is null) { throw new CommandException("No project or solution file was found."); } - return BuildHost.BuildAsync(console, path, verbosity, interactive, tags); + var output = new OutputContext(args.Console, args.Verbosity); + output.WriteInfoLine("Loading Application Details..."); + + var filter = ApplicationFactoryFilter.GetApplicationFactoryFilter(args.Tags); + + return BuildHost.BuildAsync(output, args.Path, args.Interactive, args.Framework, filter); }); return command; } + + private class BuildCommandArguments + { + public IConsole Console { get; set; } = default!; + + public FileInfo Path { get; set; } = default!; + + public Verbosity Verbosity { get; set; } + + public string Framework { get; set; } = default!; + + public bool Interactive { get; set; } = false; + + public string[] Tags { get; set; } = Array.Empty(); + } } } diff --git a/src/tye/Program.DeployCommand.cs b/src/tye/Program.DeployCommand.cs index e50f2891..4cef89e6 100644 --- a/src/tye/Program.DeployCommand.cs +++ b/src/tye/Program.DeployCommand.cs @@ -24,39 +24,37 @@ namespace Microsoft.Tye StandardOptions.Interactive, StandardOptions.Verbosity, StandardOptions.Namespace, + StandardOptions.Framework, StandardOptions.Tags, + StandardOptions.CreateForce("Override validation and force deployment.") }; - command.AddOption(new Option(new[] { "-f", "--force" }) - { - Description = "Override validation and force deployment.", - Required = false - }); - - command.Handler = CommandHandler.Create(async (console, path, verbosity, interactive, force, @namespace, tags) => + command.Handler = CommandHandler.Create(async args => { // Workaround for https://github.com/dotnet/command-line-api/issues/723#issuecomment-593062654 - if (path is null) + if (args.Path is null) { throw new CommandException("No project or solution file was found."); } - var output = new OutputContext(console, verbosity); - + var output = new OutputContext(args.Console, args.Verbosity); output.WriteInfoLine("Loading Application Details..."); - var filter = ApplicationFactoryFilter.GetApplicationFactoryFilter(tags); + var filter = ApplicationFactoryFilter.GetApplicationFactoryFilter(args.Tags); - var application = await ApplicationFactory.CreateAsync(output, path, filter); + var application = await ApplicationFactory.CreateAsync(output, args.Path, args.Framework, filter); if (application.Services.Count == 0) { throw new CommandException($"No services found in \"{application.Source.Name}\""); } - if (!string.IsNullOrEmpty(@namespace)) + + if (!string.IsNullOrEmpty(args.Namespace)) { - application.Namespace = @namespace; + application.Namespace = args.Namespace; } - await ExecuteDeployAsync(new OutputContext(console, verbosity), application, environment: "production", interactive, force); + + var executeOutput = new OutputContext(args.Console, args.Verbosity); + await ExecuteDeployAsync(executeOutput, application, environment: "production", args.Interactive, args.Force); }); return command; @@ -132,5 +130,24 @@ namespace Microsoft.Tye // No registry specified, and that's OK! } } + + private class DeployCommandArguments + { + public IConsole Console { get; set; } = default!; + + public FileInfo Path { get; set; } = default!; + + public Verbosity Verbosity { get; set; } + + public string Namespace { get; set; } = default!; + + public bool Interactive { get; set; } = false; + + public string Framework { get; set; } = default!; + + public bool Force { get; set; } = false; + + public string[] Tags { get; set; } = Array.Empty(); + } } } diff --git a/src/tye/Program.GenerateCommand.cs b/src/tye/Program.GenerateCommand.cs index 7f5e1ef2..1fbc550a 100644 --- a/src/tye/Program.GenerateCommand.cs +++ b/src/tye/Program.GenerateCommand.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.Collections.Generic; using System.CommandLine; using System.CommandLine.Invocation; @@ -22,25 +23,48 @@ namespace Microsoft.Tye StandardOptions.Interactive, StandardOptions.Verbosity, StandardOptions.Namespace, - StandardOptions.Tags + StandardOptions.Tags, + StandardOptions.Framework, }; // This is a super-secret VIP-only command! It's useful for testing, but we're // not documenting it right now. command.IsHidden = true; - command.Handler = CommandHandler.Create((console, path, verbosity, interactive, @namespace, tags) => + command.Handler = CommandHandler.Create(args => { // Workaround for https://github.com/dotnet/command-line-api/issues/723#issuecomment-593062654 - if (path is null) + if (args.Path is null) { throw new CommandException("No project or solution file was found."); } - return GenerateHost.GenerateAsync(console, path, verbosity, interactive, @namespace, tags); + var output = new OutputContext(args.Console, args.Verbosity); + output.WriteInfoLine("Loading Application Details..."); + + var filter = ApplicationFactoryFilter.GetApplicationFactoryFilter(args.Tags); + + return GenerateHost.GenerateAsync(output, args.Path, args.Interactive, args.Namespace, args.Framework, filter); }); return command; } + + private class GenerateCommandArguments + { + public IConsole Console { get; set; } = default!; + + public FileInfo Path { get; set; } = default!; + + public Verbosity Verbosity { get; set; } + + public bool Interactive { get; set; } = false; + + public string Namespace { get; set; } = default!; + + public string Framework { get; set; } = default!; + + public string[] Tags { get; set; } = Array.Empty(); + } } } diff --git a/src/tye/Program.InitCommand.cs b/src/tye/Program.InitCommand.cs index 0171efac..e38d6442 100644 --- a/src/tye/Program.InitCommand.cs +++ b/src/tye/Program.InitCommand.cs @@ -20,29 +20,34 @@ namespace Microsoft.Tye var command = new Command("init", "create a yaml manifest") { CommonArguments.Path_Optional, + StandardOptions.CreateForce("Overrides the tye.yaml file if already present for project.") }; - command.AddOption(new Option(new[] { "-f", "--force" }) - { - Description = "Overrides the tye.yaml file if already present for project.", - Required = false - }); - - command.Handler = CommandHandler.Create((console, path, force) => + command.Handler = CommandHandler.Create(args => { var watch = System.Diagnostics.Stopwatch.StartNew(); - var outputFilePath = InitHost.CreateTyeFile(path, force); - console.Out.WriteLine($"Created '{outputFilePath}'."); + var output = new OutputContext(args.Console, args.Verbosity); + var outputFilePath = InitHost.CreateTyeFile(args.Path, args.Force); + output.WriteInfoLine($"Created '{outputFilePath}'."); watch.Stop(); - - TimeSpan elapsedTime = watch.Elapsed; - - console.Out.WriteLine($"Time Elapsed: {elapsedTime.Hours:00}:{elapsedTime.Minutes:00}:{elapsedTime.Seconds:00}:{elapsedTime.Milliseconds / 10:00}"); + var elapsedTime = watch.Elapsed; + output.WriteInfoLine($"Time Elapsed: {elapsedTime.Hours:00}:{elapsedTime.Minutes:00}:{elapsedTime.Seconds:00}:{elapsedTime.Milliseconds / 10:00}"); }); return command; } + + private class InitCommandArguments + { + public IConsole Console { get; set; } = default!; + + public FileInfo Path { get; set; } = default!; + + public Verbosity Verbosity { get; set; } + + public bool Force { get; set; } = false; + } } } diff --git a/src/tye/Program.PushCommand.cs b/src/tye/Program.PushCommand.cs index 0209c93d..bed327d7 100644 --- a/src/tye/Program.PushCommand.cs +++ b/src/tye/Program.PushCommand.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.CommandLine; using System.CommandLine.Invocation; using System.IO; @@ -18,41 +19,38 @@ namespace Microsoft.Tye CommonArguments.Path_Required, StandardOptions.Interactive, StandardOptions.Verbosity, - StandardOptions.Tags + StandardOptions.Tags, + StandardOptions.Framework, + StandardOptions.CreateForce("Override validation and force push.") }; - command.AddOption(new Option(new[] { "-f", "--force" }) - { - Description = "Override validation and force push.", - Required = false - }); - - command.Handler = CommandHandler.Create(async (console, path, verbosity, interactive, force, tags) => + command.Handler = CommandHandler.Create(async args => { // Workaround for https://github.com/dotnet/command-line-api/issues/723#issuecomment-593062654 - if (path is null) + if (args.Path is null) { throw new CommandException("No project or solution file was found."); } - var output = new OutputContext(console, verbosity); - + var output = new OutputContext(args.Console, args.Verbosity); output.WriteInfoLine("Loading Application Details..."); - var filter = ApplicationFactoryFilter.GetApplicationFactoryFilter(tags); - var application = await ApplicationFactory.CreateAsync(output, path, filter); + var filter = ApplicationFactoryFilter.GetApplicationFactoryFilter(args.Tags); + + var application = await ApplicationFactory.CreateAsync(output, args.Path, args.Framework, filter); if (application.Services.Count == 0) { throw new CommandException($"No services found in \"{application.Source.Name}\""); } - await ExecutePushAsync(new OutputContext(console, verbosity), application, environment: "production", interactive, force); + var executeOutput = new OutputContext(args.Console, args.Verbosity); + await ExecutePushAsync(output, application, environment: "production", args.Interactive); }); return command; } - private static async Task ExecutePushAsync(OutputContext output, ApplicationBuilder application, string environment, bool interactive, bool force) + private static async Task ExecutePushAsync(OutputContext output, ApplicationBuilder application, string environment, bool interactive) { await application.ProcessExtensionsAsync(options: null, output, ExtensionContext.OperationKind.Deploy); ApplyRegistry(output, application, interactive, requireRegistry: true); @@ -71,5 +69,20 @@ namespace Microsoft.Tye await executor.ExecuteAsync(application); } + + private class PushCommandArguments + { + public IConsole Console { get; set; } = default!; + + public FileInfo Path { get; set; } = default!; + + public Verbosity Verbosity { get; set; } + + public bool Interactive { get; set; } = false; + + public string Framework { get; set; } = default!; + + public string[] Tags { get; set; } = Array.Empty(); + } } } diff --git a/src/tye/Program.RunCommand.cs b/src/tye/Program.RunCommand.cs index 917f2903..dd59e428 100644 --- a/src/tye/Program.RunCommand.cs +++ b/src/tye/Program.RunCommand.cs @@ -74,6 +74,7 @@ namespace Microsoft.Tye Description = "Watches for code changes for all dotnet projects.", Required = false }, + StandardOptions.Framework, StandardOptions.Tags, StandardOptions.Verbosity, }; @@ -92,7 +93,7 @@ namespace Microsoft.Tye var filter = ApplicationFactoryFilter.GetApplicationFactoryFilter(args.Tags); - var application = await ApplicationFactory.CreateAsync(output, args.Path, filter); + var application = await ApplicationFactory.CreateAsync(output, args.Path, args.Framework, filter); if (application.Services.Count == 0) { throw new CommandException($"No services found in \"{application.Source.Name}\""); @@ -171,6 +172,8 @@ namespace Microsoft.Tye public bool Watch { get; set; } + public string Framework { get; set; } = default!; + public string[] Tags { get; set; } = Array.Empty(); } } diff --git a/src/tye/Program.UndeployCommand.cs b/src/tye/Program.UndeployCommand.cs index 8dc3ccb9..24682f77 100644 --- a/src/tye/Program.UndeployCommand.cs +++ b/src/tye/Program.UndeployCommand.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.Collections.Generic; using System.CommandLine; using System.CommandLine.Invocation; @@ -30,18 +31,40 @@ namespace Microsoft.Tye }, }; - command.Handler = CommandHandler.Create((console, path, verbosity, @namespace, interactive, whatIf, tags) => + command.Handler = CommandHandler.Create(args => { // Workaround for https://github.com/dotnet/command-line-api/issues/723#issuecomment-593062654 - if (path is null) + if (args.Path is null) { throw new CommandException("No project or solution file was found."); } - return UndeployHost.UndeployAsync(console, path, verbosity, @namespace, interactive, whatIf, tags); + var output = new OutputContext(args.Console, args.Verbosity); + output.WriteInfoLine("Loading Application Details..."); + + var filter = ApplicationFactoryFilter.GetApplicationFactoryFilter(args.Tags); + + return UndeployHost.UndeployAsync(output, args.Path, args.Namespace, args.Interactive, args.WhatIf, filter); }); return command; } + + private class UndeployCommandArguments + { + public IConsole Console { get; set; } = default!; + + public FileInfo Path { get; set; } = default!; + + public Verbosity Verbosity { get; set; } + + public string Namespace { get; set; } = default!; + + public bool Interactive { get; set; } = false; + + public bool WhatIf { get; set; } = false; + + public string[] Tags { get; set; } = Array.Empty(); + } } } diff --git a/src/tye/ProjectEvaluation.targets b/src/tye/ProjectEvaluation.targets index fbcdd7f6..03d12b51 100644 --- a/src/tye/ProjectEvaluation.targets +++ b/src/tye/ProjectEvaluation.targets @@ -1,15 +1,23 @@  - + + + Restore;ResolveReferences;ResolvePackageDependenciesDesignTime;PrepareResources;PrepareResources;GetAssemblyAttributes + + Restore + + + <_MicrosoftTye_MetadataFile>$([System.IO.Path]::GetFullPath('$(IntermediateOutputPath)MicrosoftTye.ProjectMetadata.txt')) - <_MicrosoftTye_ProjectFrameworkReference>@(FrameworkReference, '%3B') + <_MicrosoftTye_ProjectFrameworkReference>@(FrameworkReference, '%3B') + <_MicrosoftTye_ProjectFrameworks>$(TargetFrameworks.Replace(';', '%3B')) <_MicrosoftTye_ProjectMetadata Include="AssemblyInformationalVersion: $(AssemblyInformationalVersion)" /> <_MicrosoftTye_ProjectMetadata Include="InformationalVersion: $(InformationalVersion)" /> <_MicrosoftTye_ProjectMetadata Include="Version: $(Version)" /> - <_MicrosoftTye_ProjectMetadata Include="TargetFrameworks: $(TargetFrameworks)" /> + <_MicrosoftTye_ProjectMetadata Include="TargetFrameworks: $(_MicrosoftTye_ProjectFrameworks)" /> <_MicrosoftTye_ProjectMetadata Include="RunCommand: $(RunCommand)" /> <_MicrosoftTye_ProjectMetadata Include="RunArguments: $(RunArguments)" /> <_MicrosoftTye_ProjectMetadata Include="TargetPath: $(TargetPath)" /> diff --git a/src/tye/Properties/launchSettings.json b/src/tye/Properties/launchSettings.json new file mode 100644 index 00000000..91755933 --- /dev/null +++ b/src/tye/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "profiles": { + "tye": { + "commandName": "Project", + "commandLineArgs": "build --framework netcoreapp3.1", + "workingDirectory": "..\\..\\samples\\app-with-targetframeworks\\" + } + } +} \ No newline at end of file diff --git a/src/tye/UndeployHost.cs b/src/tye/UndeployHost.cs index 9dc32c7f..5e8a4b69 100644 --- a/src/tye/UndeployHost.cs +++ b/src/tye/UndeployHost.cs @@ -17,16 +17,12 @@ namespace Microsoft.Tye { public static class UndeployHost { - public static async Task UndeployAsync(IConsole console, FileInfo path, Verbosity verbosity, string @namespace, bool interactive, bool whatIf, string[] tags) + public static async Task UndeployAsync(OutputContext output, FileInfo path, string @namespace, bool interactive, bool whatIf, ApplicationFactoryFilter? filter = null) { var watch = System.Diagnostics.Stopwatch.StartNew(); - var output = new OutputContext(console, verbosity); + var application = await ApplicationFactory.CreateAsync(output, path, null, filter); - output.WriteInfoLine("Loading Application Details..."); - - var filter = ApplicationFactoryFilter.GetApplicationFactoryFilter(tags); - var application = await ApplicationFactory.CreateAsync(output, path, filter); if (!string.IsNullOrEmpty(@namespace)) { application.Namespace = @namespace; diff --git a/test/E2ETest/ApplicationFactoryTests.cs b/test/E2ETest/ApplicationFactoryTests.cs index c9fc7071..b31ae4af 100644 --- a/test/E2ETest/ApplicationFactoryTests.cs +++ b/test/E2ETest/ApplicationFactoryTests.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using System.Linq; using System.Threading.Tasks; using Microsoft.Tye; @@ -27,11 +28,12 @@ services: - name: test-project include: tye.yaml"; var yamlFile = Path.Combine(projectDirectory.DirectoryPath, "tye.yaml"); - await File.WriteAllTextAsync(yamlFile, content); + // Debug targets can be null if not specified, so make sure calling host.Start does not throw. var outputContext = new OutputContext(_sink, Verbosity.Debug); - var application = await ApplicationFactory.CreateAsync(outputContext, new FileInfo(yamlFile)); + var projectFile = new FileInfo(yamlFile); + var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); Assert.Empty(application.Services); } @@ -52,7 +54,8 @@ services: // Debug targets can be null if not specified, so make sure calling host.Start does not throw. var outputContext = new OutputContext(_sink, Verbosity.Debug); - var application = await ApplicationFactory.CreateAsync(outputContext, new FileInfo(yamlFile)); + var projectFile = new FileInfo(yamlFile); + var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); Assert.Equal(5, application.Services.Count); @@ -100,7 +103,8 @@ services: // Debug targets can be null if not specified, so make sure calling host.Start does not throw. var outputContext = new OutputContext(_sink, Verbosity.Debug); - var application = await ApplicationFactory.CreateAsync(outputContext, new FileInfo(yamlFile)); + var projectFile = new FileInfo(yamlFile); + var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); Assert.Equal(4, application.Services.Count); var redisService = application.Services.Single(s => s.Name == "redis"); @@ -121,5 +125,94 @@ services: var wrongProjectPath = Path.Combine(projectDirectory.DirectoryPath, "backend1/backend.csproj"); Assert.Equal($"Failed to locate project: '{wrongProjectPath}'.", exception.Message); } + + [Fact] + public async Task TargetFrameworkFromCliArgs() + { + using var projectDirectory = TestHelpers.CopyTestProjectDirectory(Path.Combine("multi-targetframeworks")); + var yamlFile = Path.Combine(projectDirectory.DirectoryPath, "tye-no-buildproperties.yaml"); + + // Debug targets can be null if not specified, so make sure calling host.Start does not throw. + var outputContext = new OutputContext(_sink, Verbosity.Debug); + var projectFile = new FileInfo(yamlFile); + var applicationBuilder = await ApplicationFactory.CreateAsync(outputContext, projectFile, "netcoreapp3.1"); + + Assert.Single(applicationBuilder.Services); + var service = applicationBuilder.Services.Single(s => s.Name == "multi-targetframeworks"); + + var containsTargetFramework = ((DotnetProjectServiceBuilder)service).BuildProperties.TryGetValue("TargetFramework", out var targetFramework); + Assert.True(containsTargetFramework); + Assert.Equal("netcoreapp3.1", targetFramework); + } + + [Fact] + public async Task TargetFrameworkFromCliArgsOverwriteYaml() + { + using var projectDirectory = TestHelpers.CopyTestProjectDirectory(Path.Combine("multi-targetframeworks")); + var yamlFile = Path.Combine(projectDirectory.DirectoryPath, "tye-with-netcoreapp21.yaml"); + + // Debug targets can be null if not specified, so make sure calling host.Start does not throw. + var outputContext = new OutputContext(_sink, Verbosity.Debug); + var projectFile = new FileInfo(yamlFile); + var applicationBuilder = await ApplicationFactory.CreateAsync(outputContext, projectFile, "netcoreapp3.1"); + + Assert.Single(applicationBuilder.Services); + var service = applicationBuilder.Services.Single(s => s.Name == "multi-targetframeworks"); + + var containsTargetFramework = ((DotnetProjectServiceBuilder)service).BuildProperties.TryGetValue("TargetFramework", out var targetFramework); + Assert.True(containsTargetFramework); + Assert.Equal("netcoreapp3.1", targetFramework); + } + + [Fact] + public async Task TargetFrameworkFromCliArgsDoesNotOverrideSingleTFM() + { + using var projectDirectory = TestHelpers.CopyTestProjectDirectory(Path.Combine("single-project")); + var yamlFile = Path.Combine(projectDirectory.DirectoryPath, "tye.yaml"); + + // Debug targets can be null if not specified, so make sure calling host.Start does not throw. + var outputContext = new OutputContext(_sink, Verbosity.Debug); + var projectFile = new FileInfo(yamlFile); + var applicationBuilder = await ApplicationFactory.CreateAsync(outputContext, projectFile, "net5.0"); + + Assert.Single(applicationBuilder.Services); + var service = applicationBuilder.Services.Single(s => s.Name == "test-project"); + + var containsTargetFramework = ((DotnetProjectServiceBuilder)service).BuildProperties.TryGetValue("TargetFramework", out var targetFramework); + Assert.False(containsTargetFramework); + } + + [Fact] + public async Task ThrowIfNoSpecificTargetFramework() + { + using var projectDirectory = TestHelpers.CopyTestProjectDirectory(Path.Combine("multi-targetframeworks")); + var yamlFile = Path.Combine(projectDirectory.DirectoryPath, "tye-no-buildproperties.yaml"); + + // Debug targets can be null if not specified, so make sure calling host.Start does not throw. + var outputContext = new OutputContext(_sink, Verbosity.Debug); + var projectFile = new FileInfo(yamlFile); + var applicationBuilder = await ApplicationFactory.CreateAsync(outputContext, projectFile); + + Assert.Single(applicationBuilder.Services); + var service = applicationBuilder.Services.Single(s => s.Name == "multi-targetframeworks"); + + var containsTargetFramework = ((DotnetProjectServiceBuilder)service).BuildProperties.TryGetValue("TargetFramework", out var targetFramework); + Assert.False(containsTargetFramework); + + Assert.Throws(() => applicationBuilder.ToHostingApplication()); + } + + [Fact] + public async Task ThrowIfSpecifyTargetFrameworkNotDefinedIsCsproj() + { + using var projectDirectory = TestHelpers.CopyTestProjectDirectory(Path.Combine("multi-targetframeworks")); + var yamlFile = Path.Combine(projectDirectory.DirectoryPath, "tye-with-netcoreapp21.yaml"); + + // Debug targets can be null if not specified, so make sure calling host.Start does not throw. + var outputContext = new OutputContext(_sink, Verbosity.Debug); + var projectFile = new FileInfo(yamlFile); + + await Assert.ThrowsAsync(async () => await ApplicationFactory.CreateAsync(outputContext, projectFile, "foobar")); + } } } diff --git a/test/E2ETest/ApplicationTests.cs b/test/E2ETest/ApplicationTests.cs index b5332279..5f9a8c0f 100644 --- a/test/E2ETest/ApplicationTests.cs +++ b/test/E2ETest/ApplicationTests.cs @@ -25,7 +25,8 @@ namespace E2ETest // Debug targets can be null if not specified, so make sure calling host.Start does not throw. var outputContext = new OutputContext(_sink, Verbosity.Debug); - var application = await ApplicationFactory.CreateAsync(outputContext, new FileInfo(yamlFile)); + var projectFile = new FileInfo(yamlFile); + var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); var app = application.ToHostingApplication(); var dictionary = new Dictionary(); diff --git a/test/E2ETest/TyeBuildTests.Dockerfile.cs b/test/E2ETest/TyeBuildTests.Dockerfile.cs index b31aab23..d81c94c4 100644 --- a/test/E2ETest/TyeBuildTests.Dockerfile.cs +++ b/test/E2ETest/TyeBuildTests.Dockerfile.cs @@ -210,5 +210,74 @@ namespace E2ETest await DockerAssert.DeleteDockerImagesAsync(output, imageName); } } + + [ConditionalFact] + [SkipIfDockerNotRunning] + public async Task TyeBuild_MultipleTargetFrameworks_CliArgs() + { + var projectName = "multi-targetframeworks"; + var environment = "production"; + var imageName = "test/multi-targetframeworks"; + + await DockerAssert.DeleteDockerImagesAsync(output, imageName); + + using var projectDirectory = CopyTestProjectDirectory(projectName); + + var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye-no-buildproperties.yaml")); + + var outputContext = new OutputContext(sink, Verbosity.Debug); + var application = await ApplicationFactory.CreateAsync(outputContext, projectFile, "netcoreapp3.1"); + + application.Registry = new ContainerRegistry("test"); + + try + { + await BuildHost.ExecuteBuildAsync(outputContext, application, environment, interactive: false); + + var publishOutput = Assert.Single(application.Services.Single().Outputs.OfType()); + Assert.False(Directory.Exists(publishOutput.Directory.FullName), $"Directory {publishOutput.Directory.FullName} should be deleted."); + + await DockerAssert.AssertImageExistsAsync(output, imageName); + } + finally + { + await DockerAssert.DeleteDockerImagesAsync(output, imageName); + } + } + + [ConditionalFact] + [SkipIfDockerNotRunning] + public async Task TyeBuild_MultipleTargetFrameworks_YamlBuildProperties() + { + var projectName = "multi-targetframeworks"; + var environment = "production"; + var imageName = "test/multi-targetframeworks"; + + await DockerAssert.DeleteDockerImagesAsync(output, imageName); + + using var projectDirectory = CopyTestProjectDirectory(projectName); + + var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye-with-netcoreapp31.yaml")); + + var outputContext = new OutputContext(sink, Verbosity.Debug); + var application = await ApplicationFactory.CreateAsync(outputContext, projectFile, "netcoreapp3.1"); + + application.Registry = new ContainerRegistry("test"); + + try + { + await BuildHost.ExecuteBuildAsync(outputContext, application, environment, interactive: false); + + var publishOutput = Assert.Single(application.Services.Single().Outputs.OfType()); + Assert.False(Directory.Exists(publishOutput.Directory.FullName), $"Directory {publishOutput.Directory.FullName} should be deleted."); + + await DockerAssert.AssertImageExistsAsync(output, imageName); + } + finally + { + await DockerAssert.DeleteDockerImagesAsync(output, imageName); + } + } + } } diff --git a/test/E2ETest/TyeRunTests.cs b/test/E2ETest/TyeRunTests.cs index 5b202c05..fda69e83 100644 --- a/test/E2ETest/TyeRunTests.cs +++ b/test/E2ETest/TyeRunTests.cs @@ -823,12 +823,14 @@ services: repository: https://github.com/jkotalik/TyeMultiRepoVoting - name: results repository: https://github.com/jkotalik/TyeMultiRepoResults"; + var yamlFile = Path.Combine(projectDirectory.DirectoryPath, "tye.yaml"); + var projectFile = new FileInfo(yamlFile); await File.WriteAllTextAsync(yamlFile, content); // Debug targets can be null if not specified, so make sure calling host.Start does not throw. var outputContext = new OutputContext(_sink, Verbosity.Debug); - var application = await ApplicationFactory.CreateAsync(outputContext, new FileInfo(yamlFile)); + var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); var handler = new HttpClientHandler { @@ -908,10 +910,12 @@ services: - name: frontend project: frontend/frontend.csproj"; - var projectFile = Path.Combine(projectDirectory.DirectoryPath, "tye.yaml"); - await File.WriteAllTextAsync(projectFile, content); + var yamlFile = Path.Combine(projectDirectory.DirectoryPath, "tye.yaml"); + var projectFile = new FileInfo(yamlFile); + await File.WriteAllTextAsync(yamlFile, content); + var outputContext = new OutputContext(_sink, Verbosity.Debug); - var application = await ApplicationFactory.CreateAsync(outputContext, new FileInfo(projectFile)); + var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); var handler = new HttpClientHandler { @@ -937,6 +941,99 @@ services: }); } + [ConditionalFact] + [SkipIfDockerNotRunning] + public async Task RunExplicitYamlMultipleTargetFrameworksTest() + { + using var projectDirectory = CopyTestProjectDirectory("multi-targetframeworks"); + + var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye-with-netcoreapp31.yaml")); + var outputContext = new OutputContext(_sink, Verbosity.Debug); + var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); + + var handler = new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (a, b, c, d) => true, + AllowAutoRedirect = false + }; + + var client = new HttpClient(new RetryHandler(handler)); + + await RunHostingApplication(application, new HostOptions(), async (app, uri) => + { + // make sure it is running + var backendUri = await GetServiceUrl(client, uri, "multi-targetframeworks"); + + var backendResponse = await client.GetAsync(backendUri); + Assert.True(backendResponse.IsSuccessStatusCode); + + var responseContent = await backendResponse.Content.ReadAsStringAsync(); + Assert.Contains(".NET Core 3.1", responseContent); + }); + } + + [ConditionalFact] + [SkipIfDockerNotRunning] + public async Task RunWithArgsMultipleTargetFrameworksTest() + { + using var projectDirectory = CopyTestProjectDirectory("multi-targetframeworks"); + + var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye-no-buildproperties.yaml")); + var outputContext = new OutputContext(_sink, Verbosity.Debug); + var application = await ApplicationFactory.CreateAsync(outputContext, projectFile, "netcoreapp3.1"); + + var handler = new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (a, b, c, d) => true, + AllowAutoRedirect = false + }; + + var client = new HttpClient(new RetryHandler(handler)); + + await RunHostingApplication(application, new HostOptions(), async (app, uri) => + { + // make sure it is running + var backendUri = await GetServiceUrl(client, uri, "multi-targetframeworks"); + + var backendResponse = await client.GetAsync(backendUri); + Assert.True(backendResponse.IsSuccessStatusCode); + + var responseContent = await backendResponse.Content.ReadAsStringAsync(); + Assert.Contains(".NET Core 3.1", responseContent); + }); + } + + [ConditionalFact] + [SkipIfDockerNotRunning] + public async Task RunCliArgOverrideYamlMultipleTargetFrameworksTest() + { + using var projectDirectory = CopyTestProjectDirectory("multi-targetframeworks"); + + var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye-with-netcoreapp21.yaml")); + var outputContext = new OutputContext(_sink, Verbosity.Debug); + var application = await ApplicationFactory.CreateAsync(outputContext, projectFile, "netcoreapp3.1"); + + var handler = new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (a, b, c, d) => true, + AllowAutoRedirect = false + }; + + var client = new HttpClient(new RetryHandler(handler)); + + await RunHostingApplication(application, new HostOptions(), async (app, uri) => + { + // make sure it is running + var backendUri = await GetServiceUrl(client, uri, "multi-targetframeworks"); + + var backendResponse = await client.GetAsync(backendUri); + Assert.True(backendResponse.IsSuccessStatusCode); + + var responseContent = await backendResponse.Content.ReadAsStringAsync(); + Assert.Contains(".NET Core 3.1", responseContent); + }); + } + private async Task GetServiceUrl(HttpClient client, Uri uri, string serviceName) { var serviceResult = await client.GetStringAsync($"{uri}api/v1/services/{serviceName}"); diff --git a/test/E2ETest/testassets/projects/multi-targetframeworks/multi-targetframeworks.sln b/test/E2ETest/testassets/projects/multi-targetframeworks/multi-targetframeworks.sln new file mode 100644 index 00000000..96d0315d --- /dev/null +++ b/test/E2ETest/testassets/projects/multi-targetframeworks/multi-targetframeworks.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "multi-targetframeworks", "multi-targetframeworks\multi-targetframeworks.csproj", "{CA427ACE-098F-4883-8A91-02F766FC2D46}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {CA427ACE-098F-4883-8A91-02F766FC2D46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CA427ACE-098F-4883-8A91-02F766FC2D46}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CA427ACE-098F-4883-8A91-02F766FC2D46}.Debug|x64.ActiveCfg = Debug|Any CPU + {CA427ACE-098F-4883-8A91-02F766FC2D46}.Debug|x64.Build.0 = Debug|Any CPU + {CA427ACE-098F-4883-8A91-02F766FC2D46}.Debug|x86.ActiveCfg = Debug|Any CPU + {CA427ACE-098F-4883-8A91-02F766FC2D46}.Debug|x86.Build.0 = Debug|Any CPU + {CA427ACE-098F-4883-8A91-02F766FC2D46}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CA427ACE-098F-4883-8A91-02F766FC2D46}.Release|Any CPU.Build.0 = Release|Any CPU + {CA427ACE-098F-4883-8A91-02F766FC2D46}.Release|x64.ActiveCfg = Release|Any CPU + {CA427ACE-098F-4883-8A91-02F766FC2D46}.Release|x64.Build.0 = Release|Any CPU + {CA427ACE-098F-4883-8A91-02F766FC2D46}.Release|x86.ActiveCfg = Release|Any CPU + {CA427ACE-098F-4883-8A91-02F766FC2D46}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/test/E2ETest/testassets/projects/multi-targetframeworks/multi-targetframeworks/Program.cs b/test/E2ETest/testassets/projects/multi-targetframeworks/multi-targetframeworks/Program.cs new file mode 100644 index 00000000..27984781 --- /dev/null +++ b/test/E2ETest/testassets/projects/multi-targetframeworks/multi-targetframeworks/Program.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +#if NETCOREAPP3_1 +#else +using Microsoft.AspNetCore; +#endif + +namespace MultiTargetFrameworks +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + +#if NETCOREAPP3_1 + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(web => + { + web.UseStartup(); + }); +#else + public static IWebHostBuilder CreateHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup(); +#endif + } +} diff --git a/test/E2ETest/testassets/projects/multi-targetframeworks/multi-targetframeworks/Properties/launchSettings.json b/test/E2ETest/testassets/projects/multi-targetframeworks/multi-targetframeworks/Properties/launchSettings.json new file mode 100644 index 00000000..dd5da822 --- /dev/null +++ b/test/E2ETest/testassets/projects/multi-targetframeworks/multi-targetframeworks/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:5000", + "sslPort": 5001 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "multi-targetframeworks": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/test/E2ETest/testassets/projects/multi-targetframeworks/multi-targetframeworks/Startup.cs b/test/E2ETest/testassets/projects/multi-targetframeworks/multi-targetframeworks/Startup.cs new file mode 100644 index 00000000..60c84287 --- /dev/null +++ b/test/E2ETest/testassets/projects/multi-targetframeworks/multi-targetframeworks/Startup.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Http; +using System.Reflection; +using System.Runtime.Versioning; + +namespace MultiTargetFrameworks +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + } + + public void Configure(IApplicationBuilder app) + { + app.Use(async (httpContext, next) => + { +#if NETCOREAPP3_1 + await httpContext.Response.WriteAsync(System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription); +#else + var framework = Assembly.GetEntryAssembly()?.GetCustomAttribute()?.FrameworkName; + await httpContext.Response.WriteAsync(framework); +#endif + }); + } + } +} diff --git a/test/E2ETest/testassets/projects/multi-targetframeworks/multi-targetframeworks/appsettings.Development.json b/test/E2ETest/testassets/projects/multi-targetframeworks/multi-targetframeworks/appsettings.Development.json new file mode 100644 index 00000000..8983e0fc --- /dev/null +++ b/test/E2ETest/testassets/projects/multi-targetframeworks/multi-targetframeworks/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/test/E2ETest/testassets/projects/multi-targetframeworks/multi-targetframeworks/appsettings.json b/test/E2ETest/testassets/projects/multi-targetframeworks/multi-targetframeworks/appsettings.json new file mode 100644 index 00000000..d9d9a9bf --- /dev/null +++ b/test/E2ETest/testassets/projects/multi-targetframeworks/multi-targetframeworks/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/test/E2ETest/testassets/projects/multi-targetframeworks/multi-targetframeworks/multi-targetframeworks.csproj b/test/E2ETest/testassets/projects/multi-targetframeworks/multi-targetframeworks/multi-targetframeworks.csproj new file mode 100644 index 00000000..55ff0baf --- /dev/null +++ b/test/E2ETest/testassets/projects/multi-targetframeworks/multi-targetframeworks/multi-targetframeworks.csproj @@ -0,0 +1,12 @@ + + + + netcoreapp3.1;netcoreapp2.1 + MultiTargetFrameworks + + + + + + + diff --git a/test/E2ETest/testassets/projects/multi-targetframeworks/tye-no-buildproperties.yaml b/test/E2ETest/testassets/projects/multi-targetframeworks/tye-no-buildproperties.yaml new file mode 100644 index 00000000..77ce181f --- /dev/null +++ b/test/E2ETest/testassets/projects/multi-targetframeworks/tye-no-buildproperties.yaml @@ -0,0 +1,8 @@ +# tye application configuration file +# read all about it at https://github.com/dotnet/tye +name: multi-targetframeworks +services: +- name: multi-targetframeworks + project: multi-targetframeworks/multi-targetframeworks.csproj + bindings: + - port: 7000 diff --git a/test/E2ETest/testassets/projects/multi-targetframeworks/tye-with-netcoreapp21.yaml b/test/E2ETest/testassets/projects/multi-targetframeworks/tye-with-netcoreapp21.yaml new file mode 100644 index 00000000..246c41a9 --- /dev/null +++ b/test/E2ETest/testassets/projects/multi-targetframeworks/tye-with-netcoreapp21.yaml @@ -0,0 +1,9 @@ +# tye application configuration file +# read all about it at https://github.com/dotnet/tye +name: multi-targetframeworks +services: +- name: multi-targetframeworks + project: multi-targetframeworks/multi-targetframeworks.csproj + buildProperties: + - name: TargetFramework + value: netcoreapp2.1 diff --git a/test/E2ETest/testassets/projects/multi-targetframeworks/tye-with-netcoreapp31.yaml b/test/E2ETest/testassets/projects/multi-targetframeworks/tye-with-netcoreapp31.yaml new file mode 100644 index 00000000..2aedef8d --- /dev/null +++ b/test/E2ETest/testassets/projects/multi-targetframeworks/tye-with-netcoreapp31.yaml @@ -0,0 +1,9 @@ +# tye application configuration file +# read all about it at https://github.com/dotnet/tye +name: multi-targetframeworks +services: +- name: multi-targetframeworks + project: multi-targetframeworks/multi-targetframeworks.csproj + buildProperties: + - name: TargetFramework + value: netcoreapp3.1