Added XAML namespace upgrade step (#1349)
* Added XAML namespace upgrade step as well as the following fixes: * Remove duplicate Compile item <Compile Include="MauiProgram.cs" /> * Remove "Xamarin.Forms" from qualified names (Xamarin.Forms.Shell => Shell) * Replace PlatformConfiguration specific namespaces with Maui versions * Updated MauiSample test assets and added ApplicableComponents to our Maui analyzers * Addressed PR check failures by adding spacing and async suggestions * Upgrade MicrosoftCodeAnalysisVersion to 4.0.0 and switch to making changes to XAML files through Roslyn. Introduced MauiWorkloadUpgradeStep to ensure the MAUI workload is installed. * Target net7.0 in MAUI migration and build against latest 7.0.100 Avoid repeated failed workload upgrade steps * Pinning .NET SDK to RC2 release (7.0.100-rc.2.22477.23) * Updated TFMs in tests to net7.0 * WarningLevel is now being persisted during the upgrade * Temporarily disable MAUI E2E tests * Fixed spacing analyzer issues * 3rd party signing for MessagePack.dll and MessagePack.Annotations.dll
This commit is contained in:
Родитель
3c1aa55c0a
Коммит
72cfd8d28d
|
@ -8,11 +8,7 @@
|
|||
<DebugType>embedded</DebugType>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
|
||||
<!--
|
||||
Use version 3.8.0 of CodeAnalysis assemblies to preserve compatibility
|
||||
with Visual Studio 2019 16.8
|
||||
-->
|
||||
<MicrosoftCodeAnalysisVersion>3.8.0</MicrosoftCodeAnalysisVersion>
|
||||
<MicrosoftCodeAnalysisVersion>4.0.0</MicrosoftCodeAnalysisVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(DotNetBuildFromSource)' == 'true'">
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
<FileSignInfo Include="Autofac.Extensions.DependencyInjection.dll" CertificateName="3PartySHA2" />
|
||||
<FileSignInfo Include="AutoMapper.dll" CertificateName="3PartySHA2" />
|
||||
<FileSignInfo Include="DiffPlex.dll" CertificateName="3PartySHA2" />
|
||||
<FileSignInfo Include="MessagePack.dll" CertificateName="3PartySHA2" />
|
||||
<FileSignInfo Include="MessagePack.Annotations.dll" CertificateName="3PartySHA2" />
|
||||
<FileSignInfo Include="Newtonsoft.Json.dll" CertificateName="3PartySHA2" />
|
||||
<FileSignInfo Include="Serilog.dll" CertificateName="3PartySHA2" />
|
||||
<FileSignInfo Include="Serilog.Extensions.Hosting.dll" CertificateName="3PartySHA2" />
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"sdk": {
|
||||
"version": "7.0.100-rc.1.22431.12",
|
||||
"version": "7.0.100-rc.2.22477.23",
|
||||
"rollForward": "feature"
|
||||
},
|
||||
"tools": {
|
||||
"dotnet": "7.0.100-rc.1.22431.12"
|
||||
"dotnet": "7.0.100-rc.2.22477.23"
|
||||
},
|
||||
"msbuild-sdks": {
|
||||
"Microsoft.DotNet.Arcade.Sdk": "8.0.0-beta.22513.2",
|
||||
|
|
|
@ -14,8 +14,8 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.2" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.9.0" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.9.0" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="$(MicrosoftCodeAnalysisVersion)" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="$(MicrosoftCodeAnalysisVersion)" />
|
||||
|
||||
<!-- In a real extension, Upgrade Assistant abstractions would be referenced as a NuGet package -->
|
||||
<!-- To enable building and testing with the latest Upgrade Assistant changes, samples in this repo use -->
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net6.0</TargetFrameworks>
|
||||
<TargetFrameworks>net7.0;net6.0</TargetFrameworks>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<IsPackable>true</IsPackable>
|
||||
<PackAsTool>true</PackAsTool>
|
||||
<ToolCommandName>upgrade-assistant</ToolCommandName>
|
||||
<Description>A tool to assist developers in upgrading .NET Framework applications to .NET 6.</Description>
|
||||
<Description>A tool to assist developers in upgrading .NET Framework applications to .NET.</Description>
|
||||
<PackageId>upgrade-assistant</PackageId>
|
||||
<PackageReleaseNotes>A changelog is available at https://github.com/dotnet/upgrade-assistant/blob/main/CHANGELOG.md.</PackageReleaseNotes>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
|
@ -23,7 +23,8 @@
|
|||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Autofac">
|
||||
<PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonVersion)" />
|
||||
<PackageReference Include="Autofac">
|
||||
<Version>6.1.0</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Autofac.Extensions.DependencyInjection">
|
||||
|
|
|
@ -67,6 +67,9 @@ namespace Microsoft.DotNet.UpgradeAssistant
|
|||
|
||||
await tcs.Task.ConfigureAwait(false);
|
||||
|
||||
// Async output may still be pending (see Process.HasExited)
|
||||
process.WaitForExit();
|
||||
|
||||
if (process.ExitCode != args.SuccessCode)
|
||||
{
|
||||
const string Message = "[{Tool}] Error: Exited with non-success code: {ExitCode}";
|
||||
|
|
|
@ -34,6 +34,10 @@ namespace Microsoft.DotNet.UpgradeAssistant
|
|||
public static readonly TargetFrameworkMoniker Net60_Android_31 = Net60_Android with { PlatformVersion = new Version(31, 0) };
|
||||
public static readonly TargetFrameworkMoniker Net60_iOS = Net60 with { Platform = Platforms.IOS };
|
||||
public static readonly TargetFrameworkMoniker Net60_iOS_13_5 = Net60_iOS with { PlatformVersion = new Version(13, 5) };
|
||||
public static readonly TargetFrameworkMoniker Net70 = Net60 with { FrameworkVersion = new Version(7, 0) };
|
||||
public static readonly TargetFrameworkMoniker Net70_Android = Net70 with { Platform = Platforms.Android };
|
||||
public static readonly TargetFrameworkMoniker Net70_iOS = Net70 with { Platform = Platforms.IOS };
|
||||
public static readonly TargetFrameworkMoniker Net70_Windows = Net70 with { Platform = Platforms.Windows };
|
||||
|
||||
#pragma warning restore CA1707 // Identifiers should not contain underscores
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Automapper">
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<PackExtension>true</PackExtension>
|
||||
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||
<Description>Provides data for loose assembly lookup based on NuGet.org</Description>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<PackExtension>true</PackExtension>
|
||||
<Description>Provides some services for the .NET Upgrade Assistant to identify loose dependencies based off of available NuGet feeds.</Description>
|
||||
<AssemblyName>Microsoft.DotNet.UpgradeAssistant.Extensions.LooseAssembly</AssemblyName>
|
||||
|
|
|
@ -55,7 +55,7 @@ namespace Microsoft.DotNet.UpgradeAssistant.Extensions.Maui
|
|||
{
|
||||
projectproperties.RemoveProjectProperty("TargetFramework");
|
||||
file.SetPropertyValue("UseMaui", "true");
|
||||
file.SetPropertyValue("TargetFrameworks", "net6.0-android;net6.0-ios");
|
||||
file.SetPropertyValue("TargetFrameworks", "net7.0-android;net7.0-ios");
|
||||
await file.SaveAsync(token).ConfigureAwait(false);
|
||||
Logger.LogInformation("Added TFMs to .NET MAUI project");
|
||||
return new UpgradeStepApplyResult(UpgradeStepStatus.Complete, $"Added TFMs to .NET MAUI Head project");
|
||||
|
@ -130,11 +130,11 @@ namespace Microsoft.DotNet.UpgradeAssistant.Extensions.Maui
|
|||
var propertyValue = Enum.Parse(typeof(ProjectComponents), componentFlagProperty);
|
||||
if (ProjectComponents.XamarinAndroid.CompareTo(propertyValue) == 0)
|
||||
{
|
||||
return TargetFrameworkMoniker.Net60_Android_31;
|
||||
return TargetFrameworkMoniker.Net70_Android;
|
||||
}
|
||||
else
|
||||
{
|
||||
return TargetFrameworkMoniker.Net60_iOS_13_5;
|
||||
return TargetFrameworkMoniker.Net70_iOS;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,12 +20,14 @@ namespace Microsoft.DotNet.UpgradeAssistant.Extensions.Maui
|
|||
services.Services.AddTransient<IUpgradeReadyCheck, XamarinFormsVersionCheck>();
|
||||
services.Services.AddTransient<ITargetFrameworkSelectorFilter, MauiTargetFrameworkSelectorFilter>();
|
||||
services.Services.AddTransient<IComponentIdentifier, MauiComponentIdentifier>();
|
||||
services.Services.AddUpgradeStep<MauiWorkloadUpgradeStep>();
|
||||
services.Services.AddUpgradeStep<MauiPlatformTargetFrameworkUpgradeStep>();
|
||||
services.Services.AddUpgradeStep<MauiAddProjectPropertiesStep>();
|
||||
services.Services.AddTransient<DiagnosticAnalyzer, UsingXamarinFormsAnalyzerAnalyzer>();
|
||||
services.Services.AddTransient<DiagnosticAnalyzer, UsingXamarinEssentialsAnalyzer>();
|
||||
services.Services.AddTransient<CodeFixProvider, UsingXamarinFormsAnalyzerCodeFixProvider>();
|
||||
services.Services.AddTransient<CodeFixProvider, UsingXamarinEssentialsAnalyzerCodeFixProvider>();
|
||||
services.Services.AddUpgradeStep<XamlNamespaceUpgradeStep>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,26 +25,26 @@ namespace Microsoft.DotNet.UpgradeAssistant.Extensions.Maui
|
|||
|
||||
if (tfm.Components.HasFlag(ProjectComponents.XamarinAndroid))
|
||||
{
|
||||
_logger.LogInformation("Project {Name} is of type Xamarin.Android, migration to .NET MAUI requires to be least net6.0-android.", tfm.Project);
|
||||
tfm.TryUpdate(TargetFrameworkMoniker.Net60_Android);
|
||||
_logger.LogInformation("Project {Name} is of type Xamarin.Android, migration to .NET MAUI recommends net7.0-android.", tfm.Project);
|
||||
tfm.TryUpdate(TargetFrameworkMoniker.Net70_Android);
|
||||
}
|
||||
|
||||
if (tfm.Components.HasFlag(ProjectComponents.XamariniOS))
|
||||
{
|
||||
_logger.LogInformation("Project {Name} is of type Xamarin.iOS, migration to .NET MAUI requires to be least net6.0-ios.", tfm.Project);
|
||||
tfm.TryUpdate(TargetFrameworkMoniker.Net60_iOS);
|
||||
_logger.LogInformation("Project {Name} is of type Xamarin.iOS, migration to .NET MAUI recommends net7.0-ios.", tfm.Project);
|
||||
tfm.TryUpdate(TargetFrameworkMoniker.Net70_iOS);
|
||||
}
|
||||
|
||||
if (tfm.Components.HasFlag(ProjectComponents.MauiAndroid))
|
||||
{
|
||||
_logger.LogInformation("Project {Name} is of type .NET MAUI Target:Android, migration to .NET MAUI requires to be least net6.0-android.", tfm.Project);
|
||||
tfm.TryUpdate(TargetFrameworkMoniker.Net60_Android);
|
||||
_logger.LogInformation("Project {Name} is of type .NET MAUI Target:Android, migration to .NET MAUI recommends net7.0-android.", tfm.Project);
|
||||
tfm.TryUpdate(TargetFrameworkMoniker.Net70_Android);
|
||||
}
|
||||
|
||||
if (tfm.Components.HasFlag(ProjectComponents.MauiiOS))
|
||||
{
|
||||
_logger.LogInformation("Project {Name} is of type .NET MAUI Target:iOS, migration to .NET MAUI requires to be least net6.0-ios.", tfm.Project);
|
||||
tfm.TryUpdate(TargetFrameworkMoniker.Net60_iOS);
|
||||
_logger.LogInformation("Project {Name} is of type .NET MAUI Target:iOS, migration to .NET MAUI recommends net7.0-ios.", tfm.Project);
|
||||
tfm.TryUpdate(TargetFrameworkMoniker.Net70_iOS);
|
||||
}
|
||||
|
||||
if (tfm.Components.HasFlag(ProjectComponents.Maui))
|
||||
|
@ -56,7 +56,7 @@ namespace Microsoft.DotNet.UpgradeAssistant.Extensions.Maui
|
|||
else
|
||||
{
|
||||
_logger.LogInformation("Project {Name} is of type .NET MAUI Target: MAUI head, migration to .NET MAUI requires to be multiplatform", tfm.Project);
|
||||
tfm.TryUpdate(TargetFrameworkMoniker.Net60_Android);
|
||||
tfm.TryUpdate(TargetFrameworkMoniker.Net70_Android);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static System.FormattableString;
|
||||
|
||||
namespace Microsoft.DotNet.UpgradeAssistant.Extensions.Maui
|
||||
{
|
||||
public class MauiWorkloadUpgradeStep : UpgradeStep
|
||||
{
|
||||
private static readonly IReadOnlyDictionary<string, ProjectComponents> MauiWorkloadMap = new Dictionary<string, ProjectComponents>
|
||||
{
|
||||
{ "maui-android", ProjectComponents.MauiAndroid },
|
||||
{ "maui-ios", ProjectComponents.MauiiOS },
|
||||
{ "maui", ProjectComponents.MauiAndroid | ProjectComponents.MauiiOS },
|
||||
};
|
||||
|
||||
private static bool? _infoSucceeded;
|
||||
private static bool? _installSucceeded;
|
||||
private readonly IProcessRunner _runner;
|
||||
|
||||
public override string Title => "Install .NET MAUI Workload";
|
||||
|
||||
public override string Description => "Check the .NET SDK for the MAUI workload and install it if necessary.";
|
||||
|
||||
public MauiWorkloadUpgradeStep(ILogger<MauiWorkloadUpgradeStep> logger, IProcessRunner runner)
|
||||
: base(logger)
|
||||
{
|
||||
_runner = runner ?? throw new ArgumentNullException(nameof(runner));
|
||||
}
|
||||
|
||||
public override IEnumerable<string> DependsOn { get; } = new[]
|
||||
{
|
||||
WellKnownStepIds.BackupStepId,
|
||||
WellKnownStepIds.TryConvertProjectConverterStepId,
|
||||
};
|
||||
|
||||
public override IEnumerable<string> DependencyOf { get; } = new[]
|
||||
{
|
||||
typeof(XamlNamespaceUpgradeStep).FullName,
|
||||
};
|
||||
|
||||
// Install or update the MAUI workload
|
||||
protected override async Task<UpgradeStepApplyResult> ApplyImplAsync(IUpgradeContext context, CancellationToken token)
|
||||
{
|
||||
if (context is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (_installSucceeded.HasValue)
|
||||
{
|
||||
return new UpgradeStepApplyResult(UpgradeStepStatus.Skipped, ".NET MAUI workload install has already been run");
|
||||
}
|
||||
|
||||
_installSucceeded = await RunDotnetCommandAsync(context, "workload install maui", (_, message) => LogLevel.Information, token).ConfigureAwait(false);
|
||||
|
||||
if (!_installSucceeded.Value)
|
||||
{
|
||||
Logger.LogError("Command 'dotnet workload install maui' failed!");
|
||||
|
||||
return new UpgradeStepApplyResult(UpgradeStepStatus.Failed, ".NET MAUI workload installation failed!");
|
||||
}
|
||||
|
||||
return new UpgradeStepApplyResult(UpgradeStepStatus.Complete, $".NET MAUI workload installation succeeded.");
|
||||
}
|
||||
|
||||
// Check if the right MAUI workload is installed
|
||||
protected async override Task<UpgradeStepInitializeResult> InitializeImplAsync(IUpgradeContext context, CancellationToken token)
|
||||
{
|
||||
if (context is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (_installSucceeded.HasValue)
|
||||
{
|
||||
return new UpgradeStepInitializeResult(UpgradeStepStatus.Skipped, ".NET MAUI workload install has already been run", _installSucceeded.Value ? BuildBreakRisk.None : BuildBreakRisk.High);
|
||||
}
|
||||
|
||||
var project = context.CurrentProject.Required();
|
||||
var components = await project.GetComponentsAsync(token).ConfigureAwait(false);
|
||||
var workloads = ProjectComponents.None;
|
||||
|
||||
if (!_infoSucceeded.HasValue)
|
||||
{
|
||||
// We only need to display the dotnet info debug information once
|
||||
_infoSucceeded = await RunDotnetCommandAsync(context, "--info", (_, message) => LogLevel.Debug, token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var result = await RunDotnetCommandAsync(context, "workload list", (_, message) =>
|
||||
{
|
||||
var workload = message.Split(' ').First();
|
||||
if (MauiWorkloadMap.TryGetValue(workload, out var component))
|
||||
{
|
||||
workloads |= component;
|
||||
}
|
||||
|
||||
return LogLevel.Information;
|
||||
}, token).ConfigureAwait(false);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
Logger.LogError("Failed to run 'dotnet workload install maui' command!");
|
||||
return new UpgradeStepInitializeResult(UpgradeStepStatus.Failed, "Failed to list .NET MAUI workloads", BuildBreakRisk.High);
|
||||
}
|
||||
else if (workloads.HasFlag(components & (ProjectComponents.MauiAndroid | ProjectComponents.MauiiOS)))
|
||||
{
|
||||
Logger.LogInformation($".NET MAUI workloads installed: {workloads}");
|
||||
return new UpgradeStepInitializeResult(UpgradeStepStatus.Complete, ".NET MAUI workload(s) already installed", BuildBreakRisk.None);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInformation(".NET MAUI workload needs to be installed");
|
||||
return new UpgradeStepInitializeResult(UpgradeStepStatus.Incomplete, ".NET MAUI workload needs to be installed", BuildBreakRisk.High);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this is a MAUI conversion
|
||||
protected override async Task<bool> IsApplicableImplAsync(IUpgradeContext context, CancellationToken token)
|
||||
{
|
||||
if (context?.CurrentProject is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var project = context.CurrentProject.Required();
|
||||
var components = await project.GetComponentsAsync(token).ConfigureAwait(false);
|
||||
|
||||
if (components.HasFlag(ProjectComponents.MauiAndroid) || components.HasFlag(ProjectComponents.MauiiOS) || components.HasFlag(ProjectComponents.Maui))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(context.Properties.GetPropertyValue("componentFlag")))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run specified `dotnet workload` command.
|
||||
/// </summary>
|
||||
public Task<bool> RunDotnetCommandAsync(IUpgradeContext context, string command, Func<bool, string, LogLevel> getMessageLogLevel, CancellationToken token)
|
||||
{
|
||||
if (context is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
return _runner.RunProcessAsync(new ProcessInfo
|
||||
{
|
||||
Command = "dotnet",
|
||||
Arguments = command,
|
||||
EnvironmentVariables = context.GlobalProperties,
|
||||
Name = "dotnet",
|
||||
GetMessageLogLevel = getMessageLogLevel,
|
||||
}, token);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,7 +12,7 @@
|
|||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.8.0" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="$(MicrosoftCodeAnalysisVersion)" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ namespace Microsoft.DotNet.UpgradeAssistant.Extensions.Maui {
|
|||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
internal class Resources {
|
||||
|
@ -60,6 +60,15 @@ namespace Microsoft.DotNet.UpgradeAssistant.Extensions.Maui {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Remove Xamarin.Forms namespace.
|
||||
/// </summary>
|
||||
internal static string NamespaceXamarinFormsTitle {
|
||||
get {
|
||||
return ResourceManager.GetString("NamespaceXamarinFormsTitle", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Namespace '{0}' should not be referenced in .NET MAUI projects.
|
||||
/// </summary>
|
||||
|
|
|
@ -117,6 +117,9 @@
|
|||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="NamespaceXamarinFormsTitle" xml:space="preserve">
|
||||
<value>Remove Xamarin.Forms namespace</value>
|
||||
</data>
|
||||
<data name="UsingXamarinEssentialsAnalyzerMessageFormat" xml:space="preserve">
|
||||
<value>Namespace '{0}' should not be referenced in .NET MAUI projects</value>
|
||||
</data>
|
||||
|
|
|
@ -12,6 +12,7 @@ using Microsoft.CodeAnalysis.Editing;
|
|||
|
||||
namespace Microsoft.DotNet.UpgradeAssistant.Extensions.Maui
|
||||
{
|
||||
[ApplicableComponents(ProjectComponents.Maui)]
|
||||
[ExportCodeFixProvider(LanguageNames.CSharp, Name = "Using Microsoft.Maui.Essentials code fixer")]
|
||||
public class UsingXamarinEssentialsAnalyzerCodeFixProvider : CodeFixProvider
|
||||
{
|
||||
|
|
|
@ -34,10 +34,11 @@ namespace Microsoft.DotNet.UpgradeAssistant.Extensions.Maui
|
|||
|
||||
context.EnableConcurrentExecution();
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
|
||||
context.RegisterSyntaxNodeAction(AnalyzeUsingStatements, SyntaxKind.UsingDirective);
|
||||
context.RegisterSyntaxNodeAction(AnalyzeUsingDirectives, SyntaxKind.UsingDirective);
|
||||
context.RegisterSyntaxNodeAction(AnalyzeQualifiedNames, SyntaxKind.QualifiedName);
|
||||
}
|
||||
|
||||
private void AnalyzeUsingStatements(SyntaxNodeAnalysisContext context)
|
||||
private void AnalyzeUsingDirectives(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
var usingDirective = (UsingDirectiveSyntax)context.Node;
|
||||
|
||||
|
@ -54,5 +55,32 @@ namespace Microsoft.DotNet.UpgradeAssistant.Extensions.Maui
|
|||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
private void AnalyzeQualifiedNames(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
var qualifiedNameNode = (QualifiedNameSyntax)context.Node;
|
||||
if (qualifiedNameNode is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var parentNode = qualifiedNameNode.Parent;
|
||||
while (parentNode is not null)
|
||||
{
|
||||
if (parentNode.IsKind(SyntaxKind.UsingDirective) || parentNode.IsKind(SyntaxKind.UsingStatement))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
parentNode = parentNode.Parent;
|
||||
}
|
||||
|
||||
var qualifiedName = qualifiedNameNode.ToString();
|
||||
if (DisallowedNamespaces.Any(name => qualifiedName.Equals(name, StringComparison.Ordinal)))
|
||||
{
|
||||
var diagnostic = Diagnostic.Create(Rule, qualifiedNameNode.GetLocation(), qualifiedName);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,16 +2,19 @@
|
|||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CodeActions;
|
||||
using Microsoft.CodeAnalysis.CodeFixes;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Editing;
|
||||
|
||||
namespace Microsoft.DotNet.UpgradeAssistant.Extensions.Maui
|
||||
{
|
||||
[ApplicableComponents(ProjectComponents.Maui)]
|
||||
[ExportCodeFixProvider(LanguageNames.CSharp)]
|
||||
public class UsingXamarinFormsAnalyzerCodeFixProvider : CodeFixProvider
|
||||
{
|
||||
|
@ -39,16 +42,30 @@ namespace Microsoft.DotNet.UpgradeAssistant.Extensions.Maui
|
|||
return;
|
||||
}
|
||||
|
||||
// Register a code action that will invoke the fix.
|
||||
context.RegisterCodeFix(
|
||||
CodeAction.Create(
|
||||
Resources.UsingXamarinFormsTitle,
|
||||
cancellationToken => ReplaceNodeAsync(context.Document, node, cancellationToken),
|
||||
nameof(Resources.UsingXamarinFormsTitle)),
|
||||
context.Diagnostics);
|
||||
// Register the appropriate code action that will invoke the fix
|
||||
switch (node.RawKind)
|
||||
{
|
||||
case (int)SyntaxKind.UsingDirective:
|
||||
case (int)SyntaxKind.UsingStatement:
|
||||
context.RegisterCodeFix(
|
||||
CodeAction.Create(
|
||||
Resources.UsingXamarinFormsTitle,
|
||||
cancellationToken => ReplaceUsingStatementAsync(context.Document, node, cancellationToken),
|
||||
nameof(Resources.UsingXamarinFormsTitle)),
|
||||
context.Diagnostics);
|
||||
break;
|
||||
case (int)SyntaxKind.QualifiedName:
|
||||
context.RegisterCodeFix(
|
||||
CodeAction.Create(
|
||||
Resources.NamespaceXamarinFormsTitle,
|
||||
cancellationToken => RemoveNamespaceQualifierAsync(context.Document, node, cancellationToken),
|
||||
nameof(Resources.NamespaceXamarinFormsTitle)),
|
||||
context.Diagnostics);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<Document> ReplaceNodeAsync(Document document, SyntaxNode node, CancellationToken cancellationToken)
|
||||
private static async Task<Document> ReplaceUsingStatementAsync(Document document, SyntaxNode node, CancellationToken cancellationToken)
|
||||
{
|
||||
var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);
|
||||
var documentRoot = (CompilationUnitSyntax)editor.OriginalRoot;
|
||||
|
@ -63,5 +80,17 @@ namespace Microsoft.DotNet.UpgradeAssistant.Extensions.Maui
|
|||
|
||||
return editor.GetChangedDocument();
|
||||
}
|
||||
|
||||
private static async Task<Document> RemoveNamespaceQualifierAsync(Document document, SyntaxNode node, CancellationToken cancellationToken)
|
||||
{
|
||||
var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (node.Parent is not null)
|
||||
{
|
||||
editor.ReplaceNode(node.Parent, node.Parent.ChildNodes().Last());
|
||||
}
|
||||
|
||||
return editor.GetChangedDocument();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,151 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static System.Net.WebRequestMethods;
|
||||
|
||||
namespace Microsoft.DotNet.UpgradeAssistant.Extensions.Maui
|
||||
{
|
||||
public class XamlNamespaceUpgradeStep : UpgradeStep
|
||||
{
|
||||
private static readonly IReadOnlyDictionary<string, string> XamarinToMauiReplacementMap = new Dictionary<string, string>
|
||||
{
|
||||
{ "http://xamarin.com/schemas/2014/forms", "http://schemas.microsoft.com/dotnet/2021/maui" },
|
||||
{ "http://xamarin.com/schemas/2020/toolkit", "http://schemas.microsoft.com/dotnet/2022/maui/toolkit" },
|
||||
{ "clr-namespace:Xamarin.Forms.PlatformConfiguration.AndroidSpecific;assembly=Xamarin.Forms.Core", "clr-namespace:Microsoft.Maui.Controls.PlatformConfiguration.AndroidSpecific;assembly=Microsoft.Maui.Controls" },
|
||||
{ "clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core", "clr-namespace:Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;assembly=Microsoft.Maui.Controls" },
|
||||
{ "clr-namespace:Xamarin.Forms.PlatformConfiguration.macOSSpecific;assembly=Xamarin.Forms.Core", "clr-namespace:Microsoft.Maui.Controls.PlatformConfiguration.macOSSpecific;assembly=Microsoft.Maui.Controls" },
|
||||
{ "clr-namespace:Xamarin.Forms.PlatformConfiguration.TizenSpecific;assembly=Xamarin.Forms.Core", "clr-namespace:Microsoft.Maui.Controls.PlatformConfiguration.TizenSpecific;assembly=Microsoft.Maui.Controls" },
|
||||
{ "clr-namespace:Xamarin.Forms.PlatformConfiguration.WindowsSpecific;assembly=Xamarin.Forms.Core", "clr-namespace:Microsoft.Maui.Controls.PlatformConfiguration.WindowsSpecific;assembly=Microsoft.Maui.Controls" },
|
||||
};
|
||||
|
||||
private readonly IPackageRestorer _restorer;
|
||||
|
||||
public override string Title => "Update XAML Namespaces";
|
||||
|
||||
public override string Description => "Updates XAML namespaces to .NET MAUI";
|
||||
|
||||
public XamlNamespaceUpgradeStep(IPackageRestorer restorer, ILogger<XamlNamespaceUpgradeStep> logger)
|
||||
: base(logger)
|
||||
{
|
||||
_restorer = restorer;
|
||||
}
|
||||
|
||||
public override IEnumerable<string> DependsOn { get; } = new[]
|
||||
{
|
||||
WellKnownStepIds.TemplateInserterStepId,
|
||||
};
|
||||
|
||||
public override IEnumerable<string> DependencyOf { get; } = new[]
|
||||
{
|
||||
WellKnownStepIds.NextProjectStepId,
|
||||
};
|
||||
|
||||
protected override async Task<UpgradeStepApplyResult> ApplyImplAsync(IUpgradeContext context, CancellationToken token)
|
||||
{
|
||||
if (context is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
var project = context.CurrentProject.Required();
|
||||
var roslynProject = GetBestRoslynProject(project.GetRoslynProject());
|
||||
var solution = roslynProject.Solution;
|
||||
|
||||
foreach (var file in GetXamlDocuments(roslynProject))
|
||||
{
|
||||
var sourceText = await file.GetTextAsync(token).ConfigureAwait(false);
|
||||
var text = sourceText.ToString();
|
||||
|
||||
// Make replacements...
|
||||
foreach (var key in XamarinToMauiReplacementMap.Keys)
|
||||
{
|
||||
text = text.Replace(key, XamarinToMauiReplacementMap[key]);
|
||||
}
|
||||
|
||||
var newText = SourceText.From(text, encoding: sourceText.Encoding);
|
||||
|
||||
solution = solution.WithAdditionalDocumentText(file.Id, newText);
|
||||
}
|
||||
|
||||
var status = context.UpdateSolution(solution) ? UpgradeStepStatus.Complete : UpgradeStepStatus.Failed;
|
||||
|
||||
// Remove MauiProgram.cs added by MauiHeadTemplates.json from project file manually again if necessary
|
||||
// because WorkAroundRoslynIssue36781 doesn't think it's a duplicate item - but it is.
|
||||
var projectFile = project.GetFile();
|
||||
if (projectFile.RemoveItem(new ProjectItemDescriptor(ProjectItemType.Compile) { Include = "MauiProgram.cs" }))
|
||||
{
|
||||
await projectFile.SaveAsync(token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return new UpgradeStepApplyResult(status, $"Updated XAML namespaces to .NET MAUI");
|
||||
}
|
||||
|
||||
protected override async Task<UpgradeStepInitializeResult> InitializeImplAsync(IUpgradeContext context, CancellationToken token)
|
||||
{
|
||||
if (context is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
// With updated TFMs and UseMaui, we need to restore packages
|
||||
var project = context.CurrentProject.Required();
|
||||
var roslynProject = GetBestRoslynProject(project.GetRoslynProject());
|
||||
var hasXamlFiles = GetXamlDocuments(roslynProject).Any();
|
||||
if (hasXamlFiles)
|
||||
{
|
||||
Logger.LogInformation(".NET MAUI project has XAML files that may need to be updated");
|
||||
return new UpgradeStepInitializeResult(UpgradeStepStatus.Incomplete, ".NET MAUI project has XAML files that may need to be updated", BuildBreakRisk.High);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInformation(".NET MAUI project does not contain any XAML files");
|
||||
return new UpgradeStepInitializeResult(UpgradeStepStatus.Complete, ".NET MAUI project does not contain any XAML files", BuildBreakRisk.None);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override async Task<bool> IsApplicableImplAsync(IUpgradeContext context, CancellationToken token)
|
||||
{
|
||||
if (context is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (context.CurrentProject is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var project = context.CurrentProject.Required();
|
||||
var components = await project.GetComponentsAsync(token).ConfigureAwait(false);
|
||||
if (components.HasFlag(ProjectComponents.MauiAndroid) || components.HasFlag(ProjectComponents.MauiiOS) || components.HasFlag(ProjectComponents.Maui))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static IEnumerable<TextDocument> GetXamlDocuments(Project project)
|
||||
=> project.AdditionalDocuments.Where(d => d.FilePath?.EndsWith(".xaml", StringComparison.OrdinalIgnoreCase) == true);
|
||||
|
||||
private static Project GetBestRoslynProject(Project project)
|
||||
=> project.Solution.Projects
|
||||
.Where(p => p.FilePath == project.FilePath)
|
||||
.OrderByDescending(p => p.AdditionalDocumentIds.Count)
|
||||
.First();
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -37,6 +37,9 @@ namespace Microsoft.DotNet.UpgradeAssistant
|
|||
public const string Net60_Windows_10_0_19041_0 = "net6.0-windows10.0.19041.0";
|
||||
public const string Net60_Android = "net6.0-android";
|
||||
public const string Net60_iOS = "net6.0-ios";
|
||||
public const string Net70 = "net7.0";
|
||||
public const string Net70_Android = "net7.0-android";
|
||||
public const string Net70_iOS = "net7.0-ios";
|
||||
|
||||
public const string STS = Net50;
|
||||
public const string Preview = Net60;
|
||||
|
@ -72,6 +75,9 @@ namespace Microsoft.DotNet.UpgradeAssistant
|
|||
{ Net60_Windows_10_0_19041_0, TargetFrameworkMoniker.Net60_Windows with { PlatformVersion = new Version(10, 0, 19041, 0) } },
|
||||
{ Net60_Android, TargetFrameworkMoniker.Net60_Android },
|
||||
{ Net60_iOS, TargetFrameworkMoniker.Net60_iOS },
|
||||
{ Net70, TargetFrameworkMoniker.Net70 },
|
||||
{ Net70_Android, TargetFrameworkMoniker.Net70_Android },
|
||||
{ Net70_iOS, TargetFrameworkMoniker.Net70_iOS },
|
||||
};
|
||||
|
||||
[return: NotNullIfNotNull("input")]
|
||||
|
|
|
@ -12,11 +12,11 @@ namespace Microsoft.DotNet.UpgradeAssistant.Extensions.Maui.Tests
|
|||
{
|
||||
public class MauiProjectTargetFrameworkSelectorFilterTests
|
||||
{
|
||||
[InlineData(ProjectComponents.XamarinAndroid, Net60_Android, true)]
|
||||
[InlineData(ProjectComponents.XamariniOS, Net60_iOS, true)]
|
||||
[InlineData(ProjectComponents.MauiAndroid, Net60_Android, true)]
|
||||
[InlineData(ProjectComponents.MauiiOS, Net60_iOS, true)]
|
||||
[InlineData(ProjectComponents.Maui, Net60_Android, true)]
|
||||
[InlineData(ProjectComponents.XamarinAndroid, Net70_Android, true)]
|
||||
[InlineData(ProjectComponents.XamariniOS, Net70_iOS, true)]
|
||||
[InlineData(ProjectComponents.MauiAndroid, Net70_Android, true)]
|
||||
[InlineData(ProjectComponents.MauiiOS, Net70_iOS, true)]
|
||||
[InlineData(ProjectComponents.Maui, Net70_Android, true)]
|
||||
[Theory]
|
||||
public void ProcessTests(ProjectComponents components, string expectedTfmString, bool tryUpdate)
|
||||
{
|
||||
|
|
|
@ -8,6 +8,7 @@ using System.Linq;
|
|||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using AutoMapper.Configuration.Annotations;
|
||||
using Microsoft.DotNet.UpgradeAssistant;
|
||||
using Microsoft.DotNet.UpgradeAssistant.Cli;
|
||||
using Xunit;
|
||||
|
@ -44,8 +45,10 @@ namespace Integration.Tests
|
|||
*/
|
||||
[InlineData("WpfSample/vb", "WpfApp1.sln", "")]
|
||||
[InlineData("WCFSample", "ConsoleApp.csproj", "")]
|
||||
[InlineData("MauiSample/droid", "EwDavidForms.sln", "EwDavidForms.Android.csproj")]
|
||||
[InlineData("MauiSample/ios", "EwDavidForms.sln", "EwDavidForms.iOS.csproj")]
|
||||
|
||||
// TODO: [mgoertz] Re-enable after .NET 7 GA
|
||||
// [InlineData("MauiSample/droid", "EwDavidForms.sln", "EwDavidForms.Android.csproj")]
|
||||
// [InlineData("MauiSample/ios", "EwDavidForms.sln", "EwDavidForms.iOS.csproj")]
|
||||
[Theory]
|
||||
public async Task UpgradeTest(string scenarioPath, string inputFileName, string entrypoint)
|
||||
{
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net6.0</TargetFrameworks>
|
||||
<TargetFrameworks>net7.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="IntegrationScenarios\**" />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Application xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="EwDavidForms.App">
|
||||
<Application.Resources>
|
||||
|
|
|
@ -12,7 +12,5 @@
|
|||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="MauiProgram.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup />
|
||||
</Project>
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="EwDavidForms.MainPage">
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Application xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="EwDavidForms.App">
|
||||
<Application.Resources>
|
||||
|
|
|
@ -12,7 +12,5 @@
|
|||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="MauiProgram.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup />
|
||||
</Project>
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="EwDavidForms.MainPage">
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
<TargetFramework>netstandard1.1</TargetFramework>
|
||||
<OutputType>Library</OutputType>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Net.Http" Version="4.3.4" />
|
||||
|
|
Загрузка…
Ссылка в новой задаче