* added all 3 projects, need some cleaning up and unit tests

* minor edits

* not signing dotnet-scaffold anymore.

* signing 3rd party spectre.console assemblies

* fix WindowsEnvironmentVariableProvider and other minor nits.

* more minor edits.

* PR edits 1

* added 'required' modifiers to CommandInfo and Parameter, added some unit tests for their deserialization.
This commit is contained in:
Deep Choudhery 2024-05-08 14:39:56 -07:00 коммит произвёл GitHub
Родитель 9e86dfb3c2
Коммит 34308bb481
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
89 изменённых файлов: 6826 добавлений и 513 удалений

63
All.sln
Просмотреть файл

@ -118,6 +118,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{CA1DAC
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.Scaffolding.Shared.Tests", "test\Shared\Microsoft.DotNet.Scaffolding.Shared.Tests\Microsoft.DotNet.Scaffolding.Shared.Tests.csproj", "{A4C14F41-7EBC-40C2-93DF-B131B713AB8D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.Scaffolding.Helpers", "src\Shared\Microsoft.DotNet.Scaffolding.Helpers\Microsoft.DotNet.Scaffolding.Helpers.csproj", "{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.Scaffolding.ComponentModel", "src\Shared\Microsoft.DotNet.Scaffolding.ComponentModel\Microsoft.DotNet.Scaffolding.ComponentModel.csproj", "{A31A0E2D-F990-413C-91F6-8B5B8E570EC5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.DotNet.Scaffolding.ComponentModel.Tests", "test\Shared\Microsoft.DotNet.Scaffolding.ComponentModel.Tests\Microsoft.DotNet.Scaffolding.ComponentModel.Tests.csproj", "{F85BABAD-4C0B-419C-AE6A-BA95D259AB53}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
debug_x86|Any CPU = debug_x86|Any CPU
@ -689,6 +695,60 @@ Global
{A4C14F41-7EBC-40C2-93DF-B131B713AB8D}.Release|x64.Build.0 = Release|Any CPU
{A4C14F41-7EBC-40C2-93DF-B131B713AB8D}.Release|x86.ActiveCfg = Release|Any CPU
{A4C14F41-7EBC-40C2-93DF-B131B713AB8D}.Release|x86.Build.0 = Release|Any CPU
{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27}.debug_x86|Any CPU.ActiveCfg = Debug|Any CPU
{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27}.debug_x86|Any CPU.Build.0 = Debug|Any CPU
{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27}.debug_x86|x64.ActiveCfg = Debug|Any CPU
{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27}.debug_x86|x64.Build.0 = Debug|Any CPU
{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27}.debug_x86|x86.ActiveCfg = Debug|Any CPU
{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27}.debug_x86|x86.Build.0 = Debug|Any CPU
{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27}.Debug|x64.ActiveCfg = Debug|Any CPU
{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27}.Debug|x64.Build.0 = Debug|Any CPU
{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27}.Debug|x86.ActiveCfg = Debug|Any CPU
{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27}.Debug|x86.Build.0 = Debug|Any CPU
{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27}.Release|Any CPU.Build.0 = Release|Any CPU
{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27}.Release|x64.ActiveCfg = Release|Any CPU
{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27}.Release|x64.Build.0 = Release|Any CPU
{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27}.Release|x86.ActiveCfg = Release|Any CPU
{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27}.Release|x86.Build.0 = Release|Any CPU
{A31A0E2D-F990-413C-91F6-8B5B8E570EC5}.debug_x86|Any CPU.ActiveCfg = Debug|Any CPU
{A31A0E2D-F990-413C-91F6-8B5B8E570EC5}.debug_x86|Any CPU.Build.0 = Debug|Any CPU
{A31A0E2D-F990-413C-91F6-8B5B8E570EC5}.debug_x86|x64.ActiveCfg = Debug|Any CPU
{A31A0E2D-F990-413C-91F6-8B5B8E570EC5}.debug_x86|x64.Build.0 = Debug|Any CPU
{A31A0E2D-F990-413C-91F6-8B5B8E570EC5}.debug_x86|x86.ActiveCfg = Debug|Any CPU
{A31A0E2D-F990-413C-91F6-8B5B8E570EC5}.debug_x86|x86.Build.0 = Debug|Any CPU
{A31A0E2D-F990-413C-91F6-8B5B8E570EC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A31A0E2D-F990-413C-91F6-8B5B8E570EC5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A31A0E2D-F990-413C-91F6-8B5B8E570EC5}.Debug|x64.ActiveCfg = Debug|Any CPU
{A31A0E2D-F990-413C-91F6-8B5B8E570EC5}.Debug|x64.Build.0 = Debug|Any CPU
{A31A0E2D-F990-413C-91F6-8B5B8E570EC5}.Debug|x86.ActiveCfg = Debug|Any CPU
{A31A0E2D-F990-413C-91F6-8B5B8E570EC5}.Debug|x86.Build.0 = Debug|Any CPU
{A31A0E2D-F990-413C-91F6-8B5B8E570EC5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A31A0E2D-F990-413C-91F6-8B5B8E570EC5}.Release|Any CPU.Build.0 = Release|Any CPU
{A31A0E2D-F990-413C-91F6-8B5B8E570EC5}.Release|x64.ActiveCfg = Release|Any CPU
{A31A0E2D-F990-413C-91F6-8B5B8E570EC5}.Release|x64.Build.0 = Release|Any CPU
{A31A0E2D-F990-413C-91F6-8B5B8E570EC5}.Release|x86.ActiveCfg = Release|Any CPU
{A31A0E2D-F990-413C-91F6-8B5B8E570EC5}.Release|x86.Build.0 = Release|Any CPU
{F85BABAD-4C0B-419C-AE6A-BA95D259AB53}.debug_x86|Any CPU.ActiveCfg = Debug|Any CPU
{F85BABAD-4C0B-419C-AE6A-BA95D259AB53}.debug_x86|Any CPU.Build.0 = Debug|Any CPU
{F85BABAD-4C0B-419C-AE6A-BA95D259AB53}.debug_x86|x64.ActiveCfg = Debug|Any CPU
{F85BABAD-4C0B-419C-AE6A-BA95D259AB53}.debug_x86|x64.Build.0 = Debug|Any CPU
{F85BABAD-4C0B-419C-AE6A-BA95D259AB53}.debug_x86|x86.ActiveCfg = Debug|Any CPU
{F85BABAD-4C0B-419C-AE6A-BA95D259AB53}.debug_x86|x86.Build.0 = Debug|Any CPU
{F85BABAD-4C0B-419C-AE6A-BA95D259AB53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F85BABAD-4C0B-419C-AE6A-BA95D259AB53}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F85BABAD-4C0B-419C-AE6A-BA95D259AB53}.Debug|x64.ActiveCfg = Debug|Any CPU
{F85BABAD-4C0B-419C-AE6A-BA95D259AB53}.Debug|x64.Build.0 = Debug|Any CPU
{F85BABAD-4C0B-419C-AE6A-BA95D259AB53}.Debug|x86.ActiveCfg = Debug|Any CPU
{F85BABAD-4C0B-419C-AE6A-BA95D259AB53}.Debug|x86.Build.0 = Debug|Any CPU
{F85BABAD-4C0B-419C-AE6A-BA95D259AB53}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F85BABAD-4C0B-419C-AE6A-BA95D259AB53}.Release|Any CPU.Build.0 = Release|Any CPU
{F85BABAD-4C0B-419C-AE6A-BA95D259AB53}.Release|x64.ActiveCfg = Release|Any CPU
{F85BABAD-4C0B-419C-AE6A-BA95D259AB53}.Release|x64.Build.0 = Release|Any CPU
{F85BABAD-4C0B-419C-AE6A-BA95D259AB53}.Release|x86.ActiveCfg = Release|Any CPU
{F85BABAD-4C0B-419C-AE6A-BA95D259AB53}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -732,6 +792,9 @@ Global
{06E6C642-BB9B-4946-8AAA-8A8A463CBA48} = {A4057ACF-27F0-4724-963B-44548B6BC4E9}
{CA1DAC77-5D18-4986-A4BB-F6171DEE5A52} = {81B64EBF-613D-43AE-82DA-B375FB751921}
{A4C14F41-7EBC-40C2-93DF-B131B713AB8D} = {CA1DAC77-5D18-4986-A4BB-F6171DEE5A52}
{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27} = {06E6C642-BB9B-4946-8AAA-8A8A463CBA48}
{A31A0E2D-F990-413C-91F6-8B5B8E570EC5} = {06E6C642-BB9B-4946-8AAA-8A8A463CBA48}
{F85BABAD-4C0B-419C-AE6A-BA95D259AB53} = {CA1DAC77-5D18-4986-A4BB-F6171DEE5A52}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {26BCDDB3-5505-4903-9D87-C942ED0D03E6}

Просмотреть файл

@ -5,6 +5,7 @@
<add key="dotnet-eng" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/index.json" />
<add key="dotnet-tools" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json" />
<add key="dotnet-public" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json" />
<add key="vs-impl" value="https://pkgs.dev.azure.com/azure-public/vside/_packaging/vs-impl/nuget/v3/index.json" />
<add key="dotnet9" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json" />
<add key="dotnet9-transport" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9-transport/nuget/v3/index.json" />
<add key="dotnet8" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet8/nuget/v3/index.json" />

Просмотреть файл

@ -1,3 +1,3 @@
;; Exclusions for SignCheck. Corresponds to info in Signing.props.
;; Format: https://github.com/dotnet/arcade/blob/397316e195639450b6c76bfeb9823b40bee72d6d/src/SignCheck/Microsoft.SignCheck/Verification/Exclusion.cs#L23-L35
*.js;; We do not sign JavaScript files.
*.js;; We do not sign JavaScript files.

Просмотреть файл

@ -12,6 +12,8 @@
<FileSignInfo Include = "Tavis.UriTemplates.dll" CertificateName="3PartySHA2"/>
<FileSignInfo Include = "Mono.TextTemplating.dll" CertificateName="3PartySHA2"/>
<FileSignInfo Include = "Std.UriTemplate.dll" CertificateName="3PartySHA2"/>
<FileSignInfo Include = "Spectre.Console.Cli.dll" CertificateName="3PartySHA2"/>
<FileSignInfo Include = "Spectre.Console.dll" CertificateName="3PartySHA2"/>
</ItemGroup>
<ItemGroup Label="Code sign exclusions">

Просмотреть файл

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- Opt out of certain Arcade features -->
<PropertyGroup>
<UsingToolXliff>false</UsingToolXliff>
<UsingToolNetFrameworkReferenceAssemblies>true</UsingToolNetFrameworkReferenceAssemblies>
</PropertyGroup>
<PropertyGroup>
<VersionPrefix>0.1.0</VersionPrefix>
<PreReleaseVersionLabel>preview</PreReleaseVersionLabel>
<IsServicingBuild Condition="'$(PreReleaseVersionLabel)' == 'servicing'">true</IsServicingBuild>
<!--
When StabilizePackageVersion is set to 'true', this branch will produce stable outputs for 'Shipping' packages
-->
<StabilizePackageVersion Condition="'$(StabilizePackageVersion)' == ''">false</StabilizePackageVersion>
<DotNetFinalVersionKind Condition="'$(StabilizePackageVersion)' == 'true'">release</DotNetFinalVersionKind>
</PropertyGroup>
<PropertyGroup>
<SpectreConsoleFlowVersion>0.5.638</SpectreConsoleFlowVersion>
<SpectreConsoleVersion>0.47.0</SpectreConsoleVersion>
</PropertyGroup>
</Project>

Просмотреть файл

@ -68,9 +68,9 @@
<!-- Microsoft.AspNetCore.Razor.Language -->
<MicrosoftAspNetCoreRazorLanguagePackageVersion>6.0.24</MicrosoftAspNetCoreRazorLanguagePackageVersion>
<!-- Microsoft.Build-->
<MicrosoftBuildPackageVersion>17.8.3 </MicrosoftBuildPackageVersion>
<MicrosoftBuildPackageVersion>17.9.5 </MicrosoftBuildPackageVersion>
<!-- Microsoft.Build.Utilities-->
<MicrosoftBuildUtilitiesCorePackageVersion>17.8.3 </MicrosoftBuildUtilitiesCorePackageVersion>
<MicrosoftBuildUtilitiesCorePackageVersion>17.9.5 </MicrosoftBuildUtilitiesCorePackageVersion>
<MicrosoftBuildLocatorPackageVersion>1.7.1</MicrosoftBuildLocatorPackageVersion>
<!-- Microsoft.CodeAnalysis.CSharp -->
<MicrosoftCodeAnalysisCSharpPackageVersion>4.8.0</MicrosoftCodeAnalysisCSharpPackageVersion>
@ -83,6 +83,7 @@
<NewtonsoftJsonPackageVersion>13.0.3</NewtonsoftJsonPackageVersion>
<NuGetPackagingVersion>6.9.1</NuGetPackagingVersion>
<HumanizerPackageVersion>2.14.1</HumanizerPackageVersion>
<MicrosoftVisualStudioSetupConfigurationInteropPackageVersion>3.9.2164</MicrosoftVisualStudioSetupConfigurationInteropPackageVersion>
<!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
<!-- Everything below here are Packages only used by test projects -->
<!-- Microsoft.AspNetCore.Server.Kestrel -->
@ -94,7 +95,7 @@
<!-- Microsoft.AspNetCore -->
<!-- this is from aspnetcore-dev (test project only) -->
<MicrosoftAspNetCorePackageVersion>6.0.0</MicrosoftAspNetCorePackageVersion>
<MicrosoftBuildRuntimePackageVersion>17.8.3</MicrosoftBuildRuntimePackageVersion>
<MicrosoftBuildRuntimePackageVersion>17.9.5</MicrosoftBuildRuntimePackageVersion>
<!-- Microsoft.AspNetCore.Mvc -->
<!-- this is from aspnetcore-dev (test only) -->
<MicrosoftAspNetCoreMvcPackageVersion>6.0.0</MicrosoftAspNetCoreMvcPackageVersion>

Просмотреть файл

@ -1,22 +1,16 @@
set VERSION=7.0.0-dev
set VERSION=0.1.0-dev
set DEFAULT_NUPKG_PATH=%userprofile%/.nuget/packages
set SRC_DIR=%cd%
set NUPKG=artifacts/packages/Debug/Shipping/
call taskkill /f /im dotnet.exe
call rd /Q /S artifacts
call dotnet build Scaffolding.slnf
call dotnet pack Scaffolding.slnf
call build
call dotnet tool uninstall -g Microsoft.dotnet-scaffold
call cd %DEFAULT_NUPKG_PATH%
call rd /Q /S microsoft.visualstudio.web.codegeneration
call rd /Q /S Microsoft.DotNet.Scaffolding.Shared
call rd /Q /S microsoft.visualstudio.web.codegeneration.core
call rd /Q /S microsoft.visualstudio.web.codegeneration.design
call rd /Q /S microsoft.visualstudio.web.codegeneration.entityframeworkcore
call rd /Q /S microsoft.visualstudio.web.codegeneration.templating
call rd /Q /S microsoft.visualstudio.web.codegeneration.utils
call rd /Q /S microsoft.visualstudio.web.codegenerators.mvc
call rd /Q /S Microsoft.dotnet-scaffold
call rd /Q /S Microsoft.DotNet.Scaffolding.Helpers
call rd /Q /S Microsoft.DotNet.Scaffolding.ComponentModel
call cd %SRC_DIR%/%NUPKG%
call dotnet tool install -g Microsoft.dotnet-scaffold --add-source %SRC_DIR%\%NUPKG% --version %VERSION%

Просмотреть файл

@ -0,0 +1,16 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Microsoft.DotNet.Scaffolding.ComponentModel;
/// <summary>
/// What we want components to initialize and return (to stdout serialized as json) when 'get-commands' is invoked.
/// Should be serialized as 'List<CommandInfo>'
/// </summary>
public class CommandInfo
{
public required string Name { get; init; }
public required string DisplayName { get; init; }
public string? Description { get; set; }
public required Parameter[] Parameters { get; init; }
}

Просмотреть файл

@ -0,0 +1,20 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Microsoft.DotNet.Scaffolding.ComponentModel
{
/// <summary>
/// Info from 'dotnet tool list -g'
/// </summary>
public class DotNetToolInfo
{
public string PackageName { get; set; } = default!;
public string Version { get; set; } = default!;
public string Command { get; set; } = default!;
public string ToDisplayString()
{
return $"{Command} ({PackageName} v{Version})";
}
}
}

Просмотреть файл

@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<Import Project="$(RepoRoot)eng\Versions.Scaffold.props" />
</Project>

Просмотреть файл

@ -0,0 +1,56 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.ObjectModel;
namespace Microsoft.DotNet.Scaffolding.ComponentModel;
public class Parameter
{
public required string Name { get; init; }
public required string DisplayName { get; init; }
public required bool Required { get; set; } = false;
public string? Description { get; set; }
public required BaseTypes Type { get; set; } = BaseTypes.String;
public InteractivePickerType? PickerType { get; set; }
internal static readonly ReadOnlyDictionary<BaseTypes, Type> TypeDict = new(
new Dictionary<BaseTypes, Type>
{
{ BaseTypes.Bool, typeof(bool) },
{ BaseTypes.Int, typeof(int) },
{ BaseTypes.Long, typeof(long) },
{ BaseTypes.Double, typeof(double) },
{ BaseTypes.Decimal, typeof(decimal) },
{ BaseTypes.Char, typeof(char) },
{ BaseTypes.String, typeof(string) }
});
public static Type GetParameterType(BaseTypes baseType)
{
return TypeDict[baseType];
}
public Type GetParameterType()
{
return TypeDict[Type];
}
}
//will add List types for all these soon!
public enum BaseTypes
{
Bool,
Int,
Long,
Double,
Decimal,
Char,
String
}
public enum InteractivePickerType
{
ClassPicker,
FilePicker,
DbProviderPicker
}

Просмотреть файл

@ -0,0 +1,30 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Microsoft.DotNet.Scaffolding.ComponentModel;
public static class ParameterHelpers
{
public static bool CheckType(BaseTypes baseType, string value)
{
var expectedType = Parameter.TypeDict[baseType];
if (CanConvertToType(value, expectedType))
{
return true;
}
return false;
}
private static bool CanConvertToType(string value, Type type)
{
try
{
var converter = System.ComponentModel.TypeDescriptor.GetConverter(type);
return converter.IsValid(value);
}
catch
{
return false;
}
}
}

Просмотреть файл

@ -0,0 +1,36 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis;
namespace Microsoft.DotNet.Scaffolding.Helpers.Extensions.Roslyn
{
public static class ClassDeclarationSyntaxExtensions
{
/// <summary>
/// Returns true if class syntax node is based on type node with specified simple name
/// (we cannot reliably compare full names for SyntaxNodes, we use symbols for full types.
/// </summary>
/// <param name="classNode"></param>
/// <param name="baseTypeSimpleName"></param>
/// <returns></returns>
public static bool IsBasedOn(this ClassDeclarationSyntax classNode, string baseTypeSimpleName)
{
if (classNode is null)
{
return false;
}
return classNode.BaseList?.Types.Any(
x => string.Equals(x.Type.ToString(), baseTypeSimpleName, StringComparison.Ordinal)) == true;
}
public static bool IsStaticClass(this ClassDeclarationSyntax? classDeclaration)
{
return classDeclaration is not null ?
classDeclaration.Modifiers.Any(x => x.Text.Equals(SyntaxFactory.Token(SyntaxKind.StaticKeyword).Text)) :
false;
}
}
}

Просмотреть файл

@ -0,0 +1,31 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.CodeAnalysis;
namespace Microsoft.DotNet.Scaffolding.Helpers.Extensions.Roslyn
{
public static class RoslynExtensions
{
public static Project? GetProject(this Solution? solution, string? projectPath)
{
return solution?.Projects?.FirstOrDefault(x => string.Equals(projectPath, x.FilePath, StringComparison.OrdinalIgnoreCase));
}
public static Document? GetDocument(this Project project, string? documentName)
{
var fileName = Path.GetFileName(documentName);
if (string.IsNullOrEmpty(fileName))
{
return null;
}
//often Document.Name is the file path of the document and not the name.
//check for all possible cases.
return project.Documents.FirstOrDefault(x =>
x.Name.EndsWith(fileName, StringComparison.OrdinalIgnoreCase) ||
x.Name.Equals(documentName, StringComparison.OrdinalIgnoreCase) ||
(!string.IsNullOrEmpty(x.FilePath) &&
x.FilePath.Equals(documentName, StringComparison.OrdinalIgnoreCase)));
}
}
}

Просмотреть файл

@ -0,0 +1,148 @@
// 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.Diagnostics;
using System.IO;
namespace Microsoft.DotNet.Scaffolding.Helpers.Extensions
{
public static class StringExtensions
{
public static bool IsCSharpProject(this string projectFilePath)
{
return projectFilePath?.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase) == true;
}
public static bool IsBinary(this string filePath)
{
return
filePath?.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) == true ||
filePath?.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) == true;
}
public static bool IsSolutionFile(this string filePath)
{
return
filePath?.EndsWith(".sln", StringComparison.OrdinalIgnoreCase) == true;
}
/// <summary>
/// since netstandard2.0 does not have a Contains that allows StringOrdinal param, using lower invariant comparison.
/// </summary>
/// <returns>true if lower invariants are equal, false otherwise. false for any null scenario.</returns>
public static bool ContainsIgnoreCase(this string input, string value)
{
if (string.IsNullOrEmpty(input) || string.IsNullOrEmpty(value))
{
return false;
}
return (input?.ToLowerInvariant().Contains(value.ToLowerInvariant())).GetValueOrDefault();
}
public static string ToLowerInvariantFirstChar(this string input)
{
if (input == null)
{
throw new ArgumentNullException(nameof(input));
}
if (input != string.Empty)
{
return input.Substring(0, length: 1).ToLowerInvariant() + input.Substring(1);
}
return input;
}
/// <summary>
/// Helper to make relative paths from physical paths. ie. calculates the relative path from
/// basePath to fullPath. Note that if either path is null, fullPath will be returned.
/// Note that two paths that are equal return ".\".
/// </summary>
public static string MakeRelativePath(this string fullPath, string basePath)
{
if (string.IsNullOrEmpty(basePath) || string.IsNullOrEmpty(fullPath))
{
return fullPath;
}
var separator = Path.DirectorySeparatorChar.ToString();
var tempBasePath = basePath.EnsureTrailingBackslash();
var tempFullPath = fullPath.EnsureTrailingBackslash();
string relativePath = string.Empty;
while (!string.IsNullOrEmpty(tempBasePath))
{
if (tempFullPath.StartsWith(tempBasePath, StringComparison.OrdinalIgnoreCase))
{
// Since we may have added the trailing slash we have to account for that here
if (fullPath.Length < tempBasePath.Length)
{
Debug.Assert(
tempBasePath.Length - fullPath.Length == 1,
"We are at the end. Nothing more to do. Add an empty string to handle case where the paths are equal");
}
else
{
relativePath += fullPath.Remove(0, tempBasePath.Length);
}
// Two equal paths are relative by .\
if (string.IsNullOrEmpty(relativePath))
{
relativePath = "." + Path.DirectorySeparatorChar;
}
break;
}
else
{
tempBasePath = tempBasePath.Remove(tempBasePath.Length - 1);
var nLastIndex = tempBasePath.LastIndexOf(separator, StringComparison.OrdinalIgnoreCase);
if (-1 != nLastIndex)
{
tempBasePath = tempBasePath.Remove(nLastIndex + 1);
relativePath += "..";
relativePath += separator;
}
else
{
relativePath = fullPath;
break;
}
}
}
return relativePath;
}
/// <summary>
/// Makes sure the string has a trailing backslash
/// </summary>
public static string EnsureTrailingBackslash(this string s)
{
return s.EnsureTrailingChar(Path.DirectorySeparatorChar);
}
/// <summary>
/// Makes sure the string has the trailing character
/// </summary>
public static string EnsureTrailingChar(this string s, char ch)
{
return s.Length == 0 || s[s.Length - 1] != ch ? s + ch : s;
}
public static string WithOsPathSeparators(this string text)
{
if (string.IsNullOrEmpty(text))
{
return text;
}
return text.Replace(IncorrectPathSeparator, Path.DirectorySeparatorChar);
}
private static readonly char IncorrectPathSeparator = Path.DirectorySeparatorChar == '\\' ? '/' : '\\';
}
}

Просмотреть файл

@ -0,0 +1,126 @@
// 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.Text;
namespace Microsoft.DotNet.Scaffolding.Helpers
{
public static class ArgumentEscaper
{
/// <summary>
/// Undo the processing which took place to create string[] args in Main,
/// so that the next process will receive the same string[] args
///
/// See here for more info:
/// http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
public static string EscapeAndConcatenateArgArrayForProcessStart(IEnumerable<string> args)
{
return string.Join(" ", EscapeArgArray(args));
}
/// <summary>
/// Undo the processing which took place to create string[] args in Main,
/// so that the next process will receive the same string[] args
///
/// See here for more info:
/// http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
private static IEnumerable<string> EscapeArgArray(IEnumerable<string> args)
{
var escapedArgs = new List<string>();
foreach (var arg in args)
{
escapedArgs.Add(EscapeSingleArg(arg));
}
return escapedArgs;
}
public static string EscapeSingleArg(string arg)
{
var sb = new StringBuilder();
var needsQuotes = ShouldSurroundWithQuotes(arg);
var isQuoted = needsQuotes || IsSurroundedWithQuotes(arg);
if (needsQuotes) sb.Append("\"");
for (int i = 0; i < arg.Length; ++i)
{
var backslashCount = 0;
// Consume All Backslashes
while (i < arg.Length && arg[i] == '\\')
{
backslashCount++;
i++;
}
// Escape any backslashes at the end of the arg
// when the argument is also quoted.
// This ensures the outside quote is interpreted as
// an argument delimiter
if (i == arg.Length && isQuoted)
{
sb.Append('\\', 2 * backslashCount);
}
// At then end of the arg, which isn't quoted,
// just add the backslashes, no need to escape
else if (i == arg.Length)
{
sb.Append('\\', backslashCount);
}
// Escape any preceding backslashes and the quote
else if (arg[i] == '"')
{
sb.Append('\\', (2 * backslashCount) + 1);
sb.Append('"');
}
// Output any consumed backslashes and the character
else
{
sb.Append('\\', backslashCount);
sb.Append(arg[i]);
}
}
if (needsQuotes) sb.Append("\"");
return sb.ToString();
}
internal static bool ShouldSurroundWithQuotes(string argument)
{
// Don't quote already quoted strings
if (IsSurroundedWithQuotes(argument))
{
return false;
}
// Only quote if whitespace exists in the string
return ArgumentContainsWhitespace(argument);
}
internal static bool IsSurroundedWithQuotes(string argument)
{
return argument.StartsWith("\"", StringComparison.Ordinal) &&
argument.EndsWith("\"", StringComparison.Ordinal);
}
internal static bool ArgumentContainsWhitespace(string argument)
{
return argument.Contains(" ") || argument.Contains("\t") || argument.Contains("\n");
}
}
}

Просмотреть файл

@ -0,0 +1,28 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Microsoft.DotNet.Scaffolding.Helpers.General;
public static class CliHelpers
{
public static IDictionary<string, List<string>> ParseILookup(ILookup<string, string?> lookup)
{
ArgumentNullException.ThrowIfNull(lookup);
var parsedDict = new Dictionary<string, List<string>>();
foreach (var group in lookup)
{
var valList = new List<string>();
var key = group.Key;
foreach (var element in group)
{
if (element != null)
{
valList.Add(element);
}
}
parsedDict.Add(key, valList);
}
return parsedDict;
}
}

Просмотреть файл

@ -0,0 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Microsoft.DotNet.Scaffolding.Helpers.General;
public static class Constants
{
public static class EnvironmentVariables
{
public const string MSBuildExtensionsPath32 = nameof(MSBuildExtensionsPath32);
public const string MSBuildExtensionsPath = nameof(MSBuildExtensionsPath);
public const string MSBUILD_EXE_PATH = nameof(MSBUILD_EXE_PATH);
public const string MSBuildSDKsPath = nameof(MSBuildSDKsPath);
public const string USERPROFILE = nameof(USERPROFILE);
public const string VSINSTALLDIR = nameof(VSINSTALLDIR);
}
}

Просмотреть файл

@ -0,0 +1,43 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace Microsoft.DotNet.Scaffolding.Helpers.General;
public static class DotNetMuxer
{
private const string MuxerName = "dotnet";
static DotNetMuxer()
{
MuxerPath = TryFindMuxerPath();
}
public static string MuxerPath { get; }
public static string MuxerPathOrDefault()
=> MuxerPath ?? MuxerName;
private static string TryFindMuxerPath()
{
var fileName = MuxerName;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
fileName += ".exe";
}
var mainModule = Process.GetCurrentProcess().MainModule;
if (!string.IsNullOrEmpty(mainModule?.FileName)
&& Path.GetFileName(mainModule.FileName).Equals(fileName, StringComparison.OrdinalIgnoreCase))
{
return mainModule.FileName;
}
//TODO; fix to have a nullable Command and not resure scaffolding's. Tracked https://github.com/dotnet/Scaffolding/issues/1549
#pragma warning disable CS8603 // Possible null reference return.
return null;
#pragma warning restore CS8603 // Possible null reference return.
}
}

Просмотреть файл

@ -0,0 +1,66 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
namespace Microsoft.DotNet.Scaffolding.Helpers.General;
/// <summary>
/// To run 'dotnet' or any processes and capture consequent stdout and stderr (using 'ExecuteAndCaptureOutput'
/// </summary>
public class DotnetCliRunner
{
public static DotnetCliRunner CreateDotNet(string commandName, IEnumerable<string> args)
{
return Create(DotNetMuxer.MuxerPathOrDefault(), new[] { commandName }.Concat(args));
}
public static DotnetCliRunner Create(string commandName, IEnumerable<string> args)
{
return new DotnetCliRunner(commandName, args);
}
public int ExecuteAndCaptureOutput(out string? stdOut, out string? stdErr)
{
using var outStream = new ProcessOutputStreamReader();
using var errStream = new ProcessOutputStreamReader();
outStream.Capture();
errStream.Capture();
_psi.RedirectStandardOutput = true;
_psi.RedirectStandardError = true;
using var process = new Process
{
StartInfo = _psi
};
process.EnableRaisingEvents = true;
process.Start();
var taskOut = outStream.BeginRead(process.StandardOutput);
var taskErr = errStream.BeginRead(process.StandardError);
process.WaitForExit();
taskOut.Wait();
taskErr.Wait();
stdOut = outStream.CapturedOutput;
stdErr = errStream.CapturedOutput;
return process.ExitCode;
}
internal ProcessStartInfo _psi;
private DotnetCliRunner(string commandName, IEnumerable<string> args)
{
_psi = new ProcessStartInfo
{
FileName = commandName,
Arguments = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(args),
UseShellExecute = false,
};
}
}

Просмотреть файл

@ -0,0 +1,46 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Reflection;
namespace Microsoft.DotNet.Scaffolding.Helpers.General;
internal static class ProjectModelHelper
{
internal static Dictionary<string, string> ShortTfmDictionary = new Dictionary<string, string>()
{
{ ".NETCoreApp,Version=v3.1", "netcoreapp3.1" },
{ ".NETCoreApp,Version=v5.0", "net5.0" },
{ ".NETCoreApp,Version=v6.0", "net6.0" },
{ ".NETCoreApp,Version=v2.1", "netcoreapp2.1" },
{ ".NETCoreApp,Version=v7.0", "net7.0" },
{ ".NETCoreApp,Version=v8.0", "net8.0" },
{ ".NETCoreApp,Version=v9.0", "net9.0" },
};
internal static bool IsTfmPreRelease(string tfm)
{
return tfm.Equals("net9.0", StringComparison.OrdinalIgnoreCase);
}
internal static string GetManifestResource(Assembly assembly, string shortResourceName)
{
string jsonText = string.Empty;
var resourceNames = assembly.GetManifestResourceNames();
var resourceName = resourceNames?.FirstOrDefault(x => x.EndsWith(shortResourceName));
if (assembly != null && !string.IsNullOrEmpty(resourceName))
{
Stream? stream = assembly.GetManifestResourceStream(resourceName);
if (stream is null)
{
return string.Empty;
}
using (StreamReader reader = new StreamReader(stream))
{
jsonText = reader.ReadToEnd();
}
}
return jsonText;
}
}

Просмотреть файл

@ -0,0 +1,176 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Microsoft.DotNet.Scaffolding.Helpers.General;
public static class StringUtil
{
//converts Project.Namespace.SubNamespace to Project//Namespace//SubNamespace or Project\\Namespace\\SubNamespace (based on OS)
public static string ToPath(string namespaceName, string basePath, string projectRootNamespace)
{
string path = string.Empty;
if (string.IsNullOrEmpty(namespaceName) || string.IsNullOrEmpty(basePath))
{
return string.Empty;
}
namespaceName = RemovePrefix(namespaceName, basePath, projectRootNamespace);
namespaceName = namespaceName.Replace(".", Path.DirectorySeparatorChar.ToString());
try
{
basePath = Path.HasExtension(basePath) ? Path.GetDirectoryName(basePath) ?? basePath : basePath;
var combinedPath = Path.Combine(basePath, namespaceName);
path = Path.GetFullPath(combinedPath);
}
//invalid path
catch (Exception ex) when (ex is ArgumentException || ex is PathTooLongException || ex is NotSupportedException)
{ }
return path;
}
//remove prefix from namespace, used to remove the project name from namespace when creating the path
public static string RemovePrefix(string projectNamespace, string basePath, string prefix)
{
string[] namespaceParts = projectNamespace.Split('.');
string[] basePathParts = basePath.Split(new char[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries);
if (namespaceParts.Length > 0 && namespaceParts[0] == prefix && basePathParts[basePathParts.Length - 1] == prefix)
{
projectNamespace = string.Join(".", namespaceParts, 1, namespaceParts.Length - 1);
}
return projectNamespace;
}
/// <summary>
/// expecting unrooted paths (no drive letter)
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public static string ToNamespace(string path)
{
string ns = path ?? "";
if (!string.IsNullOrEmpty(ns))
{
ns = Path.HasExtension(ns) ? Path.GetDirectoryName(ns) ?? ns : ns;
if (ns.Contains(Path.DirectorySeparatorChar))
{
ns = ns.Replace(Path.DirectorySeparatorChar.ToString(), ".");
}
if (ns.Last() == '.')
{
ns = ns.Remove(ns.Length - 1);
}
}
return ns;
}
/// <summary>
/// get the full file path without extension, not just the file name
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static string GetFilePathWithoutExtension(string input)
{
string path = string.Empty;
if (!string.IsNullOrEmpty(input))
{
path = Path.ChangeExtension(input, null);
if (path.Contains(Path.DirectorySeparatorChar))
{
path = path.Replace(Path.DirectorySeparatorChar.ToString(), ".").TrimEnd();
}
if (path.EndsWith("."))
{
path = path.Remove(path.Length - 1);
}
}
return path;
}
public static string GetTypeNameFromNamespace(string templateName)
{
string[] parts = templateName.Split('.');
return parts[parts.Length - 1];
}
public static string NormalizeLineEndings(string text)
{
//change all line endings to "\n" and then replace them with the appropriate ending
return text.Replace("\r\n", "\n").Replace("\r", "\n").Replace("\n", System.Environment.NewLine);
}
public static List<string> ToStringList(this List<string?> listWithNullableStrings)
{
return listWithNullableStrings.ConvertAll(s => s ?? string.Empty).Where(s => s.Equals(string.Empty)).ToList();
}
public static Dictionary<string, string> ParseArguments(List<string> args)
{
var arguments = new Dictionary<string, string>();
if (args != null && args.Count > 0)
{
string currentKey = string.Empty;
foreach (var arg in args)
{
if (arg.StartsWith("--") || arg.StartsWith("-"))
{
currentKey = arg;
arguments[currentKey] = string.Empty;
}
else if (!string.IsNullOrEmpty(currentKey))
{
arguments[currentKey] = arg;
currentKey = string.Empty;
}
}
}
return arguments;
}
/// <summary>
/// Use enumeration to get file name if they already exist on disk.
/// For example, if File.cs exists, try File1.cs and so forth.
/// </summary>
/// <param name="filePath"></param>
/// <returns></returns>
public static string GetUniqueFilePath(string filePath)
{
if (!File.Exists(filePath))
{
return filePath;
}
var directory = Path.GetDirectoryName(filePath);
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(filePath);
var fileExtension = Path.GetExtension(filePath);
if (string.IsNullOrEmpty(directory) || string.IsNullOrEmpty(fileNameWithoutExtension))
{
return filePath;
}
int count = 1;
string newFilePath;
do
{
newFilePath = Path.Combine(directory, $"{fileNameWithoutExtension}{count}{fileExtension}");
count++;
} while (File.Exists(newFilePath));
return newFilePath;
}
public static string EnsureCsExtension(string filePath)
{
if (!filePath.EndsWith(".cs", StringComparison.OrdinalIgnoreCase))
{
filePath += ".cs";
}
return filePath;
}
}

Просмотреть файл

@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<Import Project="$(RepoRoot)eng\Versions.Scaffold.props" />
<ItemGroup>
<PackageReference Include="Microsoft.Build.Locator" Version="$(MicrosoftBuildLocatorPackageVersion)" />
<PackageReference Include="Microsoft.Build" Version="$(MicrosoftBuildPackageVersion)" ExcludeAssets="runtime" />
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="$(CodeAnalysisVersion)" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="$(CodeAnalysisVersion)" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Features" Version="$(CodeAnalysisVersion)" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="$(CodeAnalysisVersion)" />
<PackageReference Include="Microsoft.CodeAnalysis.Features" Version="$(CodeAnalysisVersion)" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="$(CodeAnalysisVersion)" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="$(CodeAnalysisVersion)" />
<PackageReference Include="Mono.TextTemplating" Version="$(MonoTextTemplatingVersion)" />
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="$(MicrosoftExtensionsDependencyModelPackageVersion)" />
<PackageReference Include="Microsoft.VisualStudio.Setup.Configuration.Interop" PrivateAssets="runtime" Version ="$(MicrosoftVisualStudioSetupConfigurationInteropPackageVersion)">
<!-- This package is mostly COM definitions which can be loaded in .NET 6 runtime just fine -->
<NoWarn>NU1701</NoWarn>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(RepoRoot)src\Shared\Microsoft.DotNet.Scaffolding.ComponentModel\Microsoft.DotNet.Scaffolding.ComponentModel.csproj" />
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,93 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System.Text;
namespace Microsoft.DotNet.Scaffolding.Helpers.General;
internal sealed class ProcessOutputStreamReader : IDisposable
{
private const char FlushBuilderCharacter = '\n';
private static readonly char[] IgnoreCharacters = [ '\r' ];
private StringBuilder? _builder;
private StringWriter? _capture;
public string? CapturedOutput => _capture?.GetStringBuilder()?.ToString();
public ProcessOutputStreamReader Capture()
{
ThrowIfCaptureSet();
_capture = new StringWriter();
return this;
}
public Task BeginRead(TextReader reader)
{
return Task.Run(() => Read(reader));
}
public void Read(TextReader reader)
{
var bufferSize = 1;
char currentCharacter;
var buffer = new char[bufferSize];
_builder = new StringBuilder();
if (reader is not null)
{
// Using Read with buffer size 1 to prevent looping endlessly
// like we would when using Read() with no buffer
while (reader.Read(buffer, 0, bufferSize) > 0)
{
currentCharacter = buffer[0];
if (currentCharacter == FlushBuilderCharacter)
{
WriteBuilder();
}
else if (!IgnoreCharacters.Contains(currentCharacter))
{
_builder.Append(currentCharacter);
}
}
}
// Flush anything else when the stream is closed
// Which should only happen if someone used console.Write
WriteBuilder();
void WriteBuilder()
{
if (_builder.Length == 0)
{
return;
}
WriteLine(_builder.ToString());
_builder.Clear();
}
}
private void WriteLine(string str)
{
_capture?.WriteLine(str);
}
private void ThrowIfCaptureSet()
{
if (_capture != null)
{
throw new InvalidOperationException("Already capturing stream!"); // TODO: Localize this?
}
}
public void Dispose()
{
_capture?.Dispose();
_capture = null;
}
}

Просмотреть файл

@ -0,0 +1,126 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Xml;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Formatting;
namespace Microsoft.DotNet.Scaffolding.Helpers.Roslyn;
public static class ClassModifier
{
public static CompilationUnitSyntax AddUsings(CompilationUnitSyntax docRoot, string[] usings)
{
var usingNodes = CreateUsings(usings);
if (usingNodes.Length != 0 && docRoot.Usings.Count == 0)
{
return docRoot.WithUsings(SyntaxFactory.List(usingNodes));
}
else
{
var uniqueUsings = GetUniqueUsings(docRoot.Usings.ToArray(), usingNodes);
return uniqueUsings.Any() ? docRoot.WithUsings(docRoot.Usings.AddRange(uniqueUsings)) : docRoot;
}
}
public static UsingDirectiveSyntax[] CreateUsings(string[] usings)
{
var usingDirectiveList = new List<UsingDirectiveSyntax>();
if (usings is null)
{
return usingDirectiveList.ToArray();
}
var nameLeadingTrivia = SyntaxFactory.TriviaList(SyntaxFactory.Space);
var usingTrailingTrivia = SyntaxFactory.TriviaList(SyntaxFactory.CarriageReturnLineFeed);
foreach (var usingDirectiveString in usings)
{
if (!string.IsNullOrEmpty(usingDirectiveString) && !usingDirectiveString.Equals("<global namespace>", StringComparison.OrdinalIgnoreCase))
{
//leading space on the value of the using eg. (using' 'Microsoft.Yadada)
var usingDirective = SyntaxFactory.UsingDirective(SyntaxFactory.IdentifierName(usingDirectiveString)
.WithLeadingTrivia(nameLeadingTrivia))
.WithAdditionalAnnotations(Formatter.Annotation)
.WithTrailingTrivia(usingTrailingTrivia);
usingDirectiveList.Add(usingDirective);
}
}
return usingDirectiveList.ToArray();
}
public static SyntaxList<UsingDirectiveSyntax> GetUniqueUsings(UsingDirectiveSyntax[] existingUsings, UsingDirectiveSyntax[] newUsings)
{
return SyntaxFactory.List(
newUsings.Where(u => u.Name != null && !existingUsings.Any(oldUsing => oldUsing.Name != null && oldUsing.Name.ToString().Equals(u.Name.ToString())))
.OrderBy(us => us.Name?.ToString()));
}
public static ClassDeclarationSyntax GetNewStaticClassDeclaration(string className)
{
return SyntaxFactory.ClassDeclaration(className)
.WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PublicKeyword), SyntaxFactory.Token(SyntaxKind.StaticKeyword)))
.NormalizeWhitespace()
.WithLeadingTrivia(SyntaxFactory.CarriageReturnLineFeed, SyntaxFactory.CarriageReturnLineFeed);
}
public static CompilationUnitSyntax GetNewStaticCompilationUnit(string className)
{
var classDeclaration = GetNewStaticClassDeclaration(className);
return SyntaxFactory.CompilationUnit()
.AddMembers(classDeclaration);
}
public static void RemoveCompileIncludes(string? projectCsprojPath, List<string> excludeList)
{
if (string.IsNullOrEmpty(projectCsprojPath))
{
return;
}
// Load the project file
var doc = new XmlDocument();
doc.Load(projectCsprojPath);
if (doc.DocumentElement is null)
{
return;
}
// Get the default namespace for the project file
var nsMgr = new XmlNamespaceManager(doc.NameTable);
nsMgr.AddNamespace("ns", doc.DocumentElement.NamespaceURI);
// Find all <Compile> elements to remove
foreach (var include in excludeList)
{
var compileNode = doc.SelectSingleNode(
$"/ns:Project/ns:ItemGroup/ns:Compile[@Include='{include}']",
nsMgr);
// If the <Compile> element exists, remove it
if (compileNode != null && compileNode.ParentNode != null)
{
compileNode.ParentNode.RemoveChild(compileNode);
}
}
// Remove empty <ItemGroup> elements
var itemGroups = doc.SelectNodes("/ns:Project/ns:ItemGroup", nsMgr);
if (itemGroups != null && itemGroups.Count != 0)
{
foreach (XmlNode itemGroup in itemGroups)
{
if (itemGroup.ChildNodes.Count == 0 && itemGroup.ParentNode != null)
{
itemGroup.ParentNode.RemoveChild(itemGroup);
}
}
}
// Save the modified project file
doc.Save(projectCsprojPath);
}
}

Просмотреть файл

@ -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.
using System.Text.Json.Serialization;
namespace Microsoft.DotNet.Scaffolding.Helpers.Roslyn
{
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum CodeChangeType
{
Default,
MemberAccess,
Lambda
}
public class CodeChangeOptionStrings
{
public const string IfStatement = nameof(IfStatement);
public const string ElseStatement = nameof(ElseStatement);
public const string MicrosoftGraph = nameof(MicrosoftGraph);
public const string DownstreamApi = nameof(DownstreamApi);
public const string Skip = nameof(Skip);
public const string NonMinimalApp = nameof(NonMinimalApp);
public const string MinimalApp = nameof(MinimalApp);
public const string OpenApi = nameof(OpenApi);
}
public class CodeChangeOptions
{
public bool MicrosoftGraph { get; set; } = false;
public bool DownstreamApi { get; set; } = false;
public bool IsMinimalApp { get; set; } = false;
public bool UsingTopLevelsStatements { get; set; } = true;
}
}

Просмотреть файл

@ -0,0 +1,31 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Microsoft.DotNet.Scaffolding.Helpers.Roslyn;
public class CodeFile
{
public Dictionary<string, Method>? Methods { get; set; }
public CodeSnippet[]? Replacements { get; set; }
public string? AddFilePath { get; set; }
public string[]? Usings { get; set; }
public CodeBlock[]? UsingsWithOptions { get; set; }
public string? FileName { get; set; }
public string? Extension => FileName?.Substring(FileName.LastIndexOf('.') + 1);
public CodeBlock[]? ClassProperties { get; set; }
public CodeBlock[]? ClassAttributes { get; set; }
public string[]? GlobalVariables { get; set; }
public string[]? Options { get; set; }
}
public class CodeBlock
{
public string? Block { get; set; }
public string[]? Options { get; set; }
}
public class Formatting
{
public bool Newline { get; set; }
public int NumberOfSpaces { get; set; }
public bool Semicolon { get; set; }
}

Просмотреть файл

@ -0,0 +1,39 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
namespace Microsoft.DotNet.Scaffolding.Helpers.Roslyn
{
public class CodeSnippet
{
public string? InsertAfter { get; set; }
private string _block = default!;
public string Block
{
get
{
if (string.IsNullOrEmpty(_block) && MultiLineBlock is not null)
{
_block = string.Join(System.Environment.NewLine, MultiLineBlock);
}
return _block;
}
set { _block = value; }
}
public string? CheckBlock { get; set; }
public string? Parent { get; set; }
public bool Prepend { get; set; } = false;
public bool Replace { get; set; } = false;
public string[]? InsertBefore { get; set; }
public string[]? Options { get; set; }
public Formatting LeadingTrivia { get; set; } = new Formatting();
public Formatting TrailingTrivia { get; set; } = new Formatting { Semicolon = true, Newline = true };
public string? Parameter { get; set; }
public CodeChangeType? CodeChangeType { get; set; } = Roslyn.CodeChangeType.Default;
public string[]? MultiLineBlock { get; set; }
public string[]? ReplaceSnippet { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,14 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Microsoft.DotNet.Scaffolding.Helpers.Roslyn
{
public class Method
{
public string[]? Parameters { get; set; }
public CodeBlock[]? AddParameters { get; set; }
public CodeBlock? EditType { get; set; }
public CodeSnippet[]? CodeChanges { get; set; }
public CodeBlock[]? Attributes { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Microsoft.DotNet.Scaffolding.Helpers.Roslyn;
public class CodeModifierConfig
{
public string? Identifier { get; set; }
public CodeFile[]? Files { get; set; }
}

Просмотреть файл

@ -0,0 +1,995 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.DotNet.Scaffolding.Helpers.General;
using Microsoft.DotNet.Scaffolding.Helpers.Services;
namespace Microsoft.DotNet.Scaffolding.Helpers.Roslyn;
public class DocumentBuilder
{
private readonly CodeFile _codeFile;
private readonly ILogger _consoleLogger;
private readonly Document _document;
public DocumentBuilder(
Document document,
CodeFile codeFile,
ILogger consoleLogger)
{
_document = document ?? throw new ArgumentNullException(nameof(document));
_codeFile = codeFile ?? throw new ArgumentNullException(nameof(codeFile));
_consoleLogger = consoleLogger ?? throw new ArgumentNullException(nameof(consoleLogger));
}
public async Task<Document> RunAsync()
{
var document = _document;
var syntaxRoot = await document.GetSyntaxRootAsync() as CompilationUnitSyntax;
var modifiedRoot = ModifyRoot(syntaxRoot, new CodeChangeOptions());
if (modifiedRoot != null)
{
return document.WithSyntaxRoot(modifiedRoot);
}
//return the same document if syntax root came back null.
return document;
}
internal static BaseMethodDeclarationSyntax GetModifiedMethod(string fileName, BaseMethodDeclarationSyntax method, Method methodChanges, CodeChangeOptions options, StringBuilder? output)
{
method = ModifyMethodAttributes(method, methodChanges, options);
method = AddCodeSnippetsToMethod(fileName, method, methodChanges, options, output);
method = EditMethodReturnType(method, methodChanges, options);
method = AddMethodParameters(method, methodChanges, options);
return method;
}
private static BaseMethodDeclarationSyntax ModifyMethodAttributes(BaseMethodDeclarationSyntax method, Method methodChanges, CodeChangeOptions options)
{
if (methodChanges.Attributes != null && methodChanges.Attributes.Any())
{
var attributes = ProjectModifierHelper.FilterCodeBlocks(methodChanges.Attributes, options);
var methodAttributes = CreateAttributeList(attributes, method.AttributeLists, method.GetLeadingTrivia());
method = method.WithAttributeLists(methodAttributes);
}
return method;
}
public CompilationUnitSyntax? ModifyRoot(CompilationUnitSyntax? root, CodeChangeOptions options)
{
if (root is null)
{
return null;
}
root = AddUsings(root, options);
if (!string.IsNullOrEmpty(_codeFile.FileName) && _codeFile.FileName.Equals("Program.cs") &&
_codeFile.Methods != null && _codeFile.Methods.TryGetValue("Global", out var globalChanges))
{
var filteredChanges = ProjectModifierHelper.FilterCodeSnippets(globalChanges.CodeChanges, options);
var updatedIdentifer = ProjectModifierHelper.GetBuilderVariableIdentifierTransformation(root.Members);
if (updatedIdentifer.HasValue)
{
(string oldValue, string newValue) = updatedIdentifer.Value;
filteredChanges = ProjectModifierHelper.UpdateVariables(filteredChanges, oldValue, newValue);
}
if (!options.UsingTopLevelsStatements)
{
var mainMethod = root?.DescendantNodes().OfType<MethodDeclarationSyntax>()
.FirstOrDefault(n => ProjectModifierHelper.Main.Equals(n.Identifier.ToString(), StringComparison.OrdinalIgnoreCase));
if (mainMethod != null
&& ApplyChangesToMethod(mainMethod.Body, filteredChanges, _codeFile.FileName) is BlockSyntax updatedBody)
{
var updatedMethod = mainMethod.WithBody(updatedBody);
return root?.ReplaceNode(mainMethod, updatedMethod);
}
}
else if (root.Members.Any(node => node.IsKind(SyntaxKind.GlobalStatement)))
{
return ApplyChangesToMethod(root, filteredChanges, _codeFile.FileName) as CompilationUnitSyntax;
}
}
else if (!string.IsNullOrEmpty(_codeFile.FileName))
{
var namespaceNode = root?.Members.OfType<BaseNamespaceDeclarationSyntax>()?.FirstOrDefault();
string className = ProjectModifierHelper.GetClassName(_codeFile.FileName);
// get classNode. All class changes are done on the ClassDeclarationSyntax and then that node is replaced using documentEditor.
var classDeclarationSyntax = namespaceNode?.
DescendantNodes().
OfType<ClassDeclarationSyntax>().
FirstOrDefault(node =>
node.Identifier.ValueText.Contains(className));
if (classDeclarationSyntax != null)
{
var modifiedClassDeclarationSyntax = classDeclarationSyntax;
//add class properties
modifiedClassDeclarationSyntax = AddProperties(modifiedClassDeclarationSyntax, options);
//add class attributes
modifiedClassDeclarationSyntax = AddClassAttributes(modifiedClassDeclarationSyntax, options);
//add code snippets/changes.
if (_codeFile.Methods != null)
{
modifiedClassDeclarationSyntax = ModifyMethods(_codeFile.FileName, modifiedClassDeclarationSyntax, _codeFile.Methods, options);
}
if (root is SyntaxNode syntaxRoot)
{
root = root.ReplaceNode(classDeclarationSyntax, modifiedClassDeclarationSyntax);
}
}
}
return root;
}
public CompilationUnitSyntax AddUsings(CompilationUnitSyntax docRoot, CodeChangeOptions options)
{
// adding usings
if (_codeFile.UsingsWithOptions != null && _codeFile.UsingsWithOptions.Any())
{
var usingsWithOptions = FilterUsingsWithOptions(_codeFile, options);
_codeFile.Usings = _codeFile.Usings?.Concat(usingsWithOptions).ToArray() ?? usingsWithOptions.ToArray();
}
var usingNodes = CreateUsings(_codeFile.Usings);
if (usingNodes.Any() && docRoot.Usings.Count == 0)
{
return docRoot.WithUsings(SyntaxFactory.List(usingNodes));
}
else
{
var uniqueUsings = GetUniqueUsings(docRoot.Usings.ToArray(), usingNodes);
return uniqueUsings.Any() ? docRoot.WithUsings(docRoot.Usings.AddRange(uniqueUsings)) : docRoot;
}
}
internal static SyntaxList<UsingDirectiveSyntax> GetUniqueUsings(UsingDirectiveSyntax[] existingUsings, UsingDirectiveSyntax[] newUsings)
{
return SyntaxFactory.List(
newUsings.Where(u => !existingUsings.Any(oldUsing => oldUsing.Name != null && oldUsing.Name.ToString().Equals(u.Name?.ToString())))
.OrderBy(us => us.Name?.ToString()));
}
internal static IList<string> FilterUsingsWithOptions(CodeFile codeFile, CodeChangeOptions options)
{
List<string> usingsWithOptions = [];
if (codeFile != null)
{
var filteredCodeBlocks = codeFile.UsingsWithOptions?.Where(us => ProjectModifierHelper.FilterOptions(us.Options, options)).ToList();
if (filteredCodeBlocks != null && filteredCodeBlocks.Count != 0)
{
usingsWithOptions = filteredCodeBlocks.Select(cb => cb.Block).ToList().ToStringList();
}
}
return usingsWithOptions;
}
//Add class members to the top of the class.
public ClassDeclarationSyntax AddProperties(ClassDeclarationSyntax classDeclarationSyntax, CodeChangeOptions options)
{
var modifiedClassDeclarationSyntax = classDeclarationSyntax;
if (_codeFile.ClassProperties != null && _codeFile.ClassProperties.Any() && classDeclarationSyntax != null)
{
_codeFile.ClassProperties = ProjectModifierHelper.FilterCodeBlocks(_codeFile.ClassProperties, options);
//get a sample member for leading trivia. Trailing trivia will still be semi colon and new line.
var sampleMember = modifiedClassDeclarationSyntax.Members.FirstOrDefault();
var memberLeadingTrivia = sampleMember?.GetLeadingTrivia() ?? SyntaxFactory.TriviaList(SyntaxFactory.Whitespace(" "));
var memberTrailingTrivia = SyntaxFactory.TriviaList(SemiColonTrivia, SyntaxFactory.CarriageReturnLineFeed);
//create MemberDeclarationSyntax[] with all the Property strings.
var classProperties = CreateClassProperties(modifiedClassDeclarationSyntax.Members, memberLeadingTrivia, memberTrailingTrivia);
if (classProperties.Length > 0)
{
//add to the top of the class.
var members = modifiedClassDeclarationSyntax.Members;
foreach (var property in classProperties)
{
members = members.Insert(0, property);
}
modifiedClassDeclarationSyntax = modifiedClassDeclarationSyntax
.WithMembers(members);
}
}
return modifiedClassDeclarationSyntax;
}
//Add class attributes '[Attribute]' to a class.
public ClassDeclarationSyntax AddClassAttributes(ClassDeclarationSyntax classDeclarationSyntax, CodeChangeOptions options)
{
var modifiedClassDeclarationSyntax = classDeclarationSyntax;
if (_codeFile.ClassAttributes != null && _codeFile.ClassAttributes.Any() && classDeclarationSyntax != null)
{
_codeFile.ClassAttributes = ProjectModifierHelper.FilterCodeBlocks(_codeFile.ClassAttributes, options);
var classAttributes = CreateAttributeList(_codeFile.ClassAttributes, modifiedClassDeclarationSyntax.AttributeLists, classDeclarationSyntax.GetLeadingTrivia());
modifiedClassDeclarationSyntax = modifiedClassDeclarationSyntax.WithAttributeLists(classAttributes);
}
return modifiedClassDeclarationSyntax;
}
internal static ClassDeclarationSyntax ModifyMethods(string fileName, ClassDeclarationSyntax classNode, Dictionary<string, Method> methods, CodeChangeOptions options, StringBuilder? output = null)
{
foreach ((string methodName, Method methodChanges) in methods)
{
if (methodChanges is null || methodChanges.Parameters is null)
{
continue;
}
var methodNode = ProjectModifierHelper.GetOriginalMethod(classNode, methodName, methodChanges);
if (methodNode is null)
{
continue;
}
var parameters = ProjectModifierHelper.VerifyParameters(methodChanges.Parameters, methodNode.ParameterList.Parameters.ToList());
foreach ((string oldValue, string newValue) in parameters)
{
methodChanges.CodeChanges = ProjectModifierHelper.UpdateVariables(methodChanges.CodeChanges, oldValue, newValue);
}
var updatedMethodNode = GetModifiedMethod(fileName, methodNode, methodChanges, options, output);
if (updatedMethodNode != null)
{
classNode = classNode.ReplaceNode(methodNode, updatedMethodNode);
}
}
return classNode;
}
internal static BaseMethodDeclarationSyntax AddMethodParameters(BaseMethodDeclarationSyntax originalMethod, Method methodChanges, CodeChangeOptions options)
{
if (methodChanges is null || methodChanges.AddParameters is null || !methodChanges.AddParameters.Any())
{
return originalMethod;
}
// Filter for CodeChangeOptions
methodChanges.AddParameters = ProjectModifierHelper.FilterCodeBlocks(methodChanges.AddParameters, options);
return AddParameters(originalMethod, methodChanges.AddParameters, options);
}
// Add all the different code snippet.
internal static BaseMethodDeclarationSyntax AddCodeSnippetsToMethod(string fileName, BaseMethodDeclarationSyntax originalMethod, Method methodChanges, CodeChangeOptions options, StringBuilder? output)
{
if (methodChanges.CodeChanges == null || !methodChanges.CodeChanges.Any())
{
return originalMethod;
}
var filteredChanges = ProjectModifierHelper.FilterCodeSnippets(methodChanges.CodeChanges, options);
if (filteredChanges is null || !filteredChanges.Any())
{
return originalMethod;
}
var blockSyntax = originalMethod.Body;
var modifiedMethod = ApplyChangesToMethod(blockSyntax, filteredChanges, fileName, output);
if (blockSyntax != null && modifiedMethod != null)
{
return originalMethod.ReplaceNode(blockSyntax, modifiedMethod);
}
return originalMethod;
}
internal static SyntaxNode? ApplyChangesToMethod(SyntaxNode? root, CodeSnippet[]? filteredChanges, string? fileName = null, StringBuilder? output = null)
{
if (root is null)
{
return null;
}
bool changesMade = false;
if (filteredChanges is null)
{
return root;
}
foreach (var change in filteredChanges)
{
var update = ModifyMethod(root, change, output);
if (update != null)
{
changesMade = true;
root = root.ReplaceNode(root, update);
}
}
if (!changesMade)
{
output?.AppendLine(value: $"No modifications made for file: {fileName}");
}
else
{
output?.AppendLine($"Modified {fileName}");
}
return root;
}
internal static MethodDeclarationSyntax? GetMethodFromSyntaxRoot(CompilationUnitSyntax root, string methodIdentifier)
{
BaseNamespaceDeclarationSyntax? namespaceNode = root.Members.OfType<NamespaceDeclarationSyntax>()?.FirstOrDefault();
if (namespaceNode == null)
{
namespaceNode = root.Members.OfType<FileScopedNamespaceDeclarationSyntax>()?.FirstOrDefault();
}
var classNode = namespaceNode?.Members.OfType<ClassDeclarationSyntax>()?.FirstOrDefault() ??
root?.Members.OfType<ClassDeclarationSyntax>()?.FirstOrDefault();
if (classNode?.ChildNodes().FirstOrDefault(
n => n is MethodDeclarationSyntax syntax &&
syntax.Identifier.ToString().Equals(methodIdentifier, StringComparison.OrdinalIgnoreCase)) is MethodDeclarationSyntax method)
{
return method;
}
return null;
}
public static CodeSnippet AddLeadingTriviaSpaces(CodeSnippet snippet, int spaces)
{
snippet.LeadingTrivia = snippet.LeadingTrivia ?? new Formatting();
snippet.LeadingTrivia.NumberOfSpaces += spaces;
return snippet;
}
internal static CodeSnippet[] AddLeadingTriviaSpaces(CodeSnippet[] snippets, int spaces)
{
for (int i = 0; i < snippets.Length; i++)
{
var snippet = snippets[i];
snippet = AddLeadingTriviaSpaces(snippet, spaces);
snippets[i] = snippet;
}
return snippets;
}
internal static SyntaxNode ModifyMethod(SyntaxNode originalMethod, CodeSnippet codeChange, StringBuilder? output = null)
{
SyntaxNode? modifiedMethod = null;
try
{
switch (codeChange.CodeChangeType)
{
case CodeChangeType.Lambda:
{
modifiedMethod = AddOrUpdateLambda(originalMethod, codeChange);
break;
}
case CodeChangeType.MemberAccess:
{
modifiedMethod = AddExpressionToParent(originalMethod, codeChange);
break;
}
default:
{
modifiedMethod = UpdateMethod(originalMethod, codeChange);
break;
}
}
}
catch
{
//output?.Append(value: $"Error modifying method {originalMethod}\nCodeChange:{codeChange.ToJson()}");
}
return modifiedMethod != null ? originalMethod.ReplaceNode(originalMethod, modifiedMethod) : originalMethod;
}
internal static SyntaxNode UpdateMethod(SyntaxNode originalMethod, CodeSnippet codeChange)
{
var children = GetDescendantNodes(originalMethod);
if (children is null)
{
return originalMethod;
}
//check for CodeChange.Block and CodeChange.CheckBlock for block's are easy to check.
if (ProjectModifierHelper.StatementExists(children, codeChange.Block) || (!string.IsNullOrEmpty(codeChange.CheckBlock) && ProjectModifierHelper.StatementExists(children, codeChange.CheckBlock)))
{
return originalMethod;
}
SyntaxNode updatedMethod;
if (codeChange.InsertBefore?.Any() is true)
{
updatedMethod = InsertBefore(codeChange, children, originalMethod);
}
else if (!string.IsNullOrEmpty(codeChange.InsertAfter))
{
updatedMethod = InsertAfter(codeChange, children, originalMethod);
}
else
{
updatedMethod = GetBlockStatement(originalMethod, codeChange);
}
return updatedMethod ?? originalMethod;
}
private static SyntaxNode InsertBefore(CodeSnippet codeChange, IEnumerable<SyntaxNode> children, SyntaxNode originalMethod)
{
var followingNode = GetFollowingNode(codeChange.InsertBefore, children);
if (followingNode is null)
{
return originalMethod;
}
var leadingWhitespaceTrivia = followingNode.GetLeadingTrivia().LastOrDefault();
if (leadingWhitespaceTrivia.IsKind(SyntaxKind.WhitespaceTrivia))
{
codeChange.LeadingTrivia.NumberOfSpaces += leadingWhitespaceTrivia.Span.Length;
}
var newNodes = GetNodeInsertionList(codeChange, originalMethod.Kind());
if (newNodes is null)
{
return originalMethod;
}
return originalMethod.InsertNodesBefore(followingNode, newNodes);
}
private static SyntaxNode InsertAfter(CodeSnippet codeChange, IEnumerable<SyntaxNode> children, SyntaxNode originalMethod)
{
var precedingNode = GetSpecifiedNode(codeChange.InsertAfter, children);
if (precedingNode is null)
{
return originalMethod;
}
var leadingWhitespaceTrivia = precedingNode.GetLeadingTrivia().LastOrDefault();
if (leadingWhitespaceTrivia.IsKind(SyntaxKind.WhitespaceTrivia))
{
codeChange.LeadingTrivia.NumberOfSpaces += leadingWhitespaceTrivia.Span.Length;
}
var newNodes = GetNodeInsertionList(codeChange, originalMethod.Kind());
if (newNodes is null)
{
return originalMethod;
}
return originalMethod.InsertNodesAfter(precedingNode, newNodes);
}
private static SyntaxNode? GetFollowingNode(string[]? insertBefore, IEnumerable<SyntaxNode> descendantNodes)
{
if (insertBefore != null)
{
foreach (var specifier in insertBefore)
{
if (GetSpecifiedNode(specifier, descendantNodes) is SyntaxNode insertBeforeNode)
{
return insertBeforeNode;
}
}
}
return null;
}
/// <summary>
/// Creates a List of SyntaxNodes with the code change added, useful as an input for methods such as SyntaxNode.InsertNodesBefore that require a list
/// Additionally, with this method we can use the same code for StatementSyntax and GlobalStatementSyntax nodes
/// </summary>
/// <param name="codeChange"></param>
/// <param name="syntaxKind"></param>
/// <returns></returns>
private static List<SyntaxNode>? GetNodeInsertionList(CodeSnippet codeChange, SyntaxKind syntaxKind)
{
var statement = GetStatementWithTrivia(codeChange);
if (statement is null)
{
return null;
}
return syntaxKind == SyntaxKind.CompilationUnit
? new List<SyntaxNode> { SyntaxFactory.GlobalStatement(statement) }
: new List<SyntaxNode> { statement };
}
private static SyntaxNode GetBlockStatement(SyntaxNode node, CodeSnippet codeChange)
{
var syntaxKind = node.Kind();
var statement = GetStatementWithTrivia(codeChange);
if (syntaxKind == SyntaxKind.Block && node is BlockSyntax block)
{
block = codeChange.Prepend
? block.WithStatements(block.Statements.Insert(0, statement))
: block.AddStatements(statement);
return block;
}
if (syntaxKind == SyntaxKind.CompilationUnit && node is CompilationUnitSyntax compilationUnit)
{
var globalStatement = SyntaxFactory.GlobalStatement(statement);
return codeChange.Prepend
? compilationUnit.WithMembers(compilationUnit.Members.Insert(0, globalStatement))
: compilationUnit.AddMembers(globalStatement);
}
return node;
}
private static StatementSyntax GetStatementWithTrivia(CodeSnippet change)
{
var leadingTrivia = GetLeadingTrivia(change.LeadingTrivia);
var trailingTrivia = GetTrailingTrivia(change.TrailingTrivia);
return SyntaxFactory.ParseStatement(change.Block)
.WithAdditionalAnnotations(Formatter.Annotation)
.WithLeadingTrivia(leadingTrivia)
.WithTrailingTrivia(trailingTrivia);
}
private static SyntaxTriviaList GetLeadingTrivia(Formatting codeFormatting)
{
var statementLeadingTrivia = SyntaxFactory.TriviaList();
if (codeFormatting != null)
{
if (codeFormatting.Newline)
{
statementLeadingTrivia = statementLeadingTrivia.Add(SyntaxFactory.ElasticCarriageReturnLineFeed);
}
if (codeFormatting.NumberOfSpaces > 0)
{
statementLeadingTrivia = statementLeadingTrivia.Add(SyntaxFactory.Whitespace(new string(' ', codeFormatting.NumberOfSpaces)));
}
}
return statementLeadingTrivia;
}
private static SyntaxTriviaList GetTrailingTrivia(Formatting codeFormatting)
{
var statementLeadingTrivia = SyntaxFactory.TriviaList();
if (codeFormatting != null)
{
if (codeFormatting.Semicolon)
{
statementLeadingTrivia = statementLeadingTrivia.Add(SemiColonTrivia);
}
if (codeFormatting.Newline)
{
statementLeadingTrivia = statementLeadingTrivia.Add(SyntaxFactory.ElasticCarriageReturnLineFeed);
}
}
return statementLeadingTrivia;
}
/// <summary>
/// Searches through list of nodes and returns the first node that contains the specifierStatement
/// Note: can be null
/// </summary>
/// <param name="specifierStatement"></param>
/// <param name="descendantNodes"></param>
/// <returns></returns>
internal static SyntaxNode? GetSpecifiedNode(string? specifierStatement, IEnumerable<SyntaxNode> descendantNodes)
{
if (string.IsNullOrEmpty(specifierStatement))
{
return null;
}
// TODO descendantNodes could be null
var specifiedDescendant =
descendantNodes?.FirstOrDefault(d => d != null && d.ToString().Contains(specifierStatement)) ??
descendantNodes?.FirstOrDefault(d => d != null && d.ToString().Contains(ProjectModifierHelper.TrimStatement(specifierStatement)));
return specifiedDescendant;
}
internal static BaseMethodDeclarationSyntax EditMethodReturnType(BaseMethodDeclarationSyntax originalMethod, Method methodChanges, CodeChangeOptions options)
{
if (methodChanges is null || methodChanges.EditType is null || !(originalMethod is MethodDeclarationSyntax modifiedMethod))
{
return originalMethod;
}
methodChanges.EditType = ProjectModifierHelper.FilterCodeBlocks(new CodeBlock[] { methodChanges.EditType }, options).FirstOrDefault();
// After filtering, the method type might not need editing
if (methodChanges.EditType is null || string.IsNullOrEmpty(methodChanges.EditType.Block))
{
return originalMethod;
}
var returnTypeString = modifiedMethod.ReturnType.ToFullString();
if (modifiedMethod.Modifiers.Any(m => m.ToFullString().Contains("async")))
{
returnTypeString = $"async {returnTypeString}";
}
if (!ProjectModifierHelper.TrimStatement(returnTypeString).Equals(ProjectModifierHelper.TrimStatement(methodChanges.EditType.Block)))
{
var typeIdentifier = SyntaxFactory.IdentifierName(SyntaxFactory.Identifier(methodChanges.EditType.Block));
modifiedMethod = modifiedMethod.WithReturnType(typeIdentifier.WithTrailingTrivia(SyntaxFactory.Whitespace(" ")));
return modifiedMethod;
}
return originalMethod;
}
internal static BaseMethodDeclarationSyntax AddParameters(BaseMethodDeclarationSyntax methodNode, CodeBlock[] addParameters, CodeChangeOptions toolOptions)
{
var newMethod = methodNode;
List<ParameterSyntax> newParameters = new List<ParameterSyntax>();
foreach (var parameter in addParameters)
{
if (string.IsNullOrEmpty(parameter.Block))
{
continue;
}
var identifier = SyntaxFactory.Identifier(parameter.Block).WithLeadingTrivia(SyntaxFactory.Whitespace(" "));
var parameterSyntax = SyntaxFactory.Parameter(identifier);
if (!newMethod.ParameterList.Parameters.Any(p => p.ToFullString().Equals(parameter.Block)))
{
newParameters.Add(parameterSyntax);
}
}
if (newParameters.Any())
{
newMethod = newMethod.AddParameterListParameters(newParameters.ToArray());
}
return newMethod;
}
private static SyntaxNode AddOrUpdateLambda(SyntaxNode originalMethod, CodeSnippet change)
{
var descendants = GetDescendantNodes(originalMethod);
if (descendants is null)
{
return originalMethod;
}
var parent = GetSpecifiedNode(change.Parent, descendants);
if (parent is null)
{
return originalMethod;
}
var children = GetDescendantNodes(parent);
if (children is null)
{
return originalMethod;
}
var updatedParent = parent;
// Check for existing lambda
if (children.FirstOrDefault(
d => d.IsKind(SyntaxKind.ParenthesizedLambdaExpression)
|| d.IsKind(SyntaxKind.SimpleLambdaExpression)) is LambdaExpressionSyntax existingLambda)
{
updatedParent = GetNodeWithUpdatedLambda(existingLambda, change, parent);
}
else // Add a new lambda
{
updatedParent = AddLambdaToParent(parent, children, change);
}
return originalMethod.ReplaceNode(parent, updatedParent);
}
/// <summary>
/// Given an existing lamba expression, updates the parameters and block as necessary
/// </summary>
/// <param name="existingLambda"></param>
/// <param name="change"></param>
/// <param name="parent"></param>
/// <returns>parent with updated lambda expression</returns>
internal static SyntaxNode GetNodeWithUpdatedLambda(LambdaExpressionSyntax existingLambda, CodeSnippet change, SyntaxNode parent)
{
var lambdaWithUpdatedParameters = UpdateLambdaParameters(existingLambda, change);
var lambdaWithUpdatedBlock = UpdateLambdaBlock(lambdaWithUpdatedParameters, change);
return parent.ReplaceNode(existingLambda, lambdaWithUpdatedBlock);
}
private static LambdaExpressionSyntax UpdateLambdaParameters(LambdaExpressionSyntax existingLambda, CodeSnippet change)
{
var existingParameters = GetDescendantNodes(existingLambda)?.Where(n => n.IsKind(SyntaxKind.Parameter));
if (existingParameters is null || string.IsNullOrEmpty(change.Parameter))
{
return existingLambda;
}
if (ProjectModifierHelper.StatementExists(existingParameters, change.Parameter))
{
return existingLambda;
}
var lambdaParam = SyntaxFactory.Parameter(SyntaxFactory.Identifier(change.Parameter));
// Lambda might be ParenthesizedLambda or SimpleLambda
if (existingLambda is ParenthesizedLambdaExpressionSyntax updatedLambda)
{
updatedLambda = updatedLambda.AddParameterListParameters(lambdaParam);
}
else // if SimpleLambda we are adding a parameter and making it a ParenthesizedLambda
{
updatedLambda = SyntaxFactory.ParenthesizedLambdaExpression(existingLambda).AddParameterListParameters(lambdaParam);
}
if (updatedLambda == null)
{
return existingLambda;
}
return updatedLambda;
}
private static LambdaExpressionSyntax UpdateLambdaBlock(LambdaExpressionSyntax existingLambda, CodeSnippet change)
{
if (ProjectModifierHelper.StatementExists(existingLambda.Body, change.Block))
{
return existingLambda;
}
var leadingWhitespaceTrivia = existingLambda.GetLeadingTrivia().LastOrDefault();
if (leadingWhitespaceTrivia.IsKind(SyntaxKind.WhitespaceTrivia))
{
change.LeadingTrivia.NumberOfSpaces += leadingWhitespaceTrivia.Span.Length;
}
if (change.Replace)
{
return existingLambda.WithBody(GetStatementWithTrivia(change));
}
// Get new lambda block
var updatedBlock = GetBlockStatement(existingLambda.Body, change);
// Try to replace existing block with updated block
if (existingLambda.WithBody(updatedBlock as CSharpSyntaxNode) is LambdaExpressionSyntax updatedLambda)
{
return updatedLambda;
}
return existingLambda;
}
internal static SyntaxNode AddLambdaToParent(SyntaxNode parent, IEnumerable<SyntaxNode> children, CodeSnippet change)
{
if (ProjectModifierHelper.StatementExists(children, change.Block) ||
string.IsNullOrEmpty(change.Parameter) ||
// Determine if there is an existing argument list to add the lambda
!(children.FirstOrDefault(n => n.IsKind(SyntaxKind.ArgumentList)) is ArgumentListSyntax argList))
{
return parent;
}
// Create a lambda parameter
var parameter = SyntaxFactory.Parameter(
SyntaxFactory.Identifier(change.Parameter))
.WithTrailingTrivia(SyntaxFactory.Space);
var parentLeadingWhiteSpace = parent.GetLeadingTrivia().LastOrDefault();
if (parentLeadingWhiteSpace.IsKind(SyntaxKind.WhitespaceTrivia))
{
change.LeadingTrivia.NumberOfSpaces += parentLeadingWhiteSpace.Span.Length;
}
// Ensure that block statement is valid
if (!(GetBlockStatement(SyntaxFactory.Block(), change) is BlockSyntax block))
{
return parent;
}
// Update white space for non-top-level statements
if (parentLeadingWhiteSpace.IsKind(SyntaxKind.WhitespaceTrivia))
{
block = block
.WithOpenBraceToken(block.OpenBraceToken.WithLeadingTrivia(parentLeadingWhiteSpace))
.WithCloseBraceToken(block.CloseBraceToken.WithLeadingTrivia(parentLeadingWhiteSpace));
}
// Create lambda expression with parameter and block (add leading newline to block for formatting)
var newLambdaExpression = SyntaxFactory.SimpleLambdaExpression(
parameter,
block.WithLeadingTrivia(parentLeadingWhiteSpace));
// Add lambda to parent block's argument list
var argument = SyntaxFactory.Argument(newLambdaExpression);
var updatedParent = parent.ReplaceNode(argList, argList.AddArguments(argument));
return updatedParent;
}
// return modified parent node with code snippet added
internal static SyntaxNode AddExpressionToParent(SyntaxNode originalMethod, CodeSnippet change)
{
// Determine the parent node onto which we are adding
var descendantNodes = GetDescendantNodes(originalMethod);
if (descendantNodes is null)
{
return originalMethod;
}
var parent = GetSpecifiedNode(change.Parent, descendantNodes);
if (parent is null)
{
return originalMethod;
}
var children = GetDescendantNodes(parent);
if (children is null)
{
return originalMethod;
}
if (ProjectModifierHelper.StatementExists(children, change.Block))
{
return originalMethod;
}
// Find parent's expression statement
if (!(children.FirstOrDefault(n => n.IsKind(SyntaxKind.ExpressionStatement)) is ExpressionStatementSyntax exprNode))
{
return originalMethod;
}
// Create new expression to update old expression
var leadingTrivia = GetLeadingTrivia(change.LeadingTrivia);
var identifier = SyntaxFactory.IdentifierName(change.Block);
var newExpression = SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
exprNode.Expression.WithTrailingTrivia(leadingTrivia),
identifier);
var modifiedExprNode = exprNode.WithExpression(newExpression);
if (modifiedExprNode is null)
{
return originalMethod;
}
// Replace existing expression with updated expression
var updatedParent = parent.ReplaceNode(exprNode, modifiedExprNode);
return updatedParent != null ? originalMethod.ReplaceNode(parent, updatedParent) : originalMethod;
}
internal static IEnumerable<SyntaxNode>? GetDescendantNodes(SyntaxNode root)
{
if (root is BlockSyntax block)
{
return block.Statements;
}
else if (root is CompilationUnitSyntax compilationUnit)
{
return compilationUnit.Members;
}
return root?.DescendantNodes();
}
// create UsingDirectiveSyntax[] using a string[] to add to the root of the class (root.Usings).
internal static UsingDirectiveSyntax[] CreateUsings(string[]? usings)
{
var usingDirectiveList = new List<UsingDirectiveSyntax>();
if (usings == null)
{
return usingDirectiveList.ToArray();
}
var nameLeadingTrivia = SyntaxFactory.TriviaList(SyntaxFactory.Space);
var usingTrailingTrivia = SyntaxFactory.TriviaList(SyntaxFactory.CarriageReturnLineFeed);
foreach (var usingDirectiveString in usings)
{
if (!string.IsNullOrEmpty(usingDirectiveString))
{
//leading space on the value of the using eg. (using' 'Microsoft.Yadada)
var usingDirective = SyntaxFactory.UsingDirective(SyntaxFactory.IdentifierName(usingDirectiveString)
.WithLeadingTrivia(nameLeadingTrivia))
.WithAdditionalAnnotations(Formatter.Annotation)
.WithTrailingTrivia(usingTrailingTrivia);
usingDirectiveList.Add(usingDirective);
}
}
return usingDirectiveList.ToArray();
}
// create AttributeListSyntax using string[] to add on top of a ClassDeclrationSyntax
internal static SyntaxList<AttributeListSyntax> CreateAttributeList(CodeBlock[] attributes, SyntaxList<AttributeListSyntax> attributeLists, SyntaxTriviaList leadingTrivia)
{
if (attributes == null)
{
return attributeLists;
}
foreach (var attribute in attributes)
{
var attributeList = new List<AttributeSyntax>();
// filter by apps
if (!string.IsNullOrEmpty(attribute.Block) && !ProjectModifierHelper.AttributeExists(attribute.Block, attributeLists))
{
attributeList.Add(SyntaxFactory.Attribute(SyntaxFactory.ParseName(attribute.Block)));
}
if (attributeList.Any())
{
var attributeListSyntax = SyntaxFactory.AttributeList(SyntaxFactory.SeparatedList(attributeList)).WithLeadingTrivia(leadingTrivia);
if (!leadingTrivia.ToString().Contains("\n"))
{
attributeListSyntax = attributeListSyntax.WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed);
}
attributeLists = attributeLists.Insert(0, attributeListSyntax);
}
}
return attributeLists;
}
//create MemberDeclarationSyntax[] to add at the top of ClassDeclarationSynax. Created from the property strings.
internal MemberDeclarationSyntax[] CreateClassProperties(
SyntaxList<MemberDeclarationSyntax> members,
SyntaxTriviaList leadingTrivia,
SyntaxTriviaList trailingTrivia)
{
var propertyDeclarationList = new List<MemberDeclarationSyntax>();
if (_codeFile.ClassProperties != null && _codeFile.ClassProperties.Any())
{
foreach (var classProperty in _codeFile.ClassProperties)
{
if (!string.IsNullOrEmpty(classProperty.Block) && !PropertyExists(classProperty.Block, members))
{
var additionalAnnotation = Formatter.Annotation;
var classPropertyDeclaration = SyntaxFactory.ParseMemberDeclaration(classProperty.Block)
?.WithAdditionalAnnotations(additionalAnnotation)
?.WithTrailingTrivia(trailingTrivia)
?.WithLeadingTrivia(leadingTrivia);
if (classPropertyDeclaration != null)
{
propertyDeclarationList.Add(classPropertyDeclaration);
}
}
}
}
return propertyDeclarationList.ToArray();
}
internal static bool PropertyExists(string property, SyntaxList<MemberDeclarationSyntax> members)
{
if (string.IsNullOrEmpty(property))
{
return false;
}
var trimmedProperty = property.Trim(ProjectModifierHelper.CodeSnippetTrimChars);
return members.Where(m => m.ToString().Trim(ProjectModifierHelper.CodeSnippetTrimChars).Equals(trimmedProperty)).Any();
}
private static SyntaxTrivia SemiColonTrivia => SyntaxFactory.Trivia(SyntaxFactory.SkippedTokensTrivia()
.WithTokens(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.SemicolonToken))));
}

Просмотреть файл

@ -0,0 +1,64 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.CodeAnalysis;
namespace Microsoft.DotNet.Scaffolding.Helpers.Roslyn;
public static class EfDbContextHelpers
{
public static EfDbContextProperties GetEfDbContextProperties(ISymbol dbContextSymbol, ISymbol modelSymbol)
{
EfDbContextProperties efDbContextProperties = new();
if (dbContextSymbol is INamedTypeSymbol dbContextTypeSymbol)
{
// Assuming there's only one DbSet property in the DbContext for simplicity
var dbSetProperties = dbContextTypeSymbol.GetMembers().OfType<IPropertySymbol>()
.Where(p => p.Type is INamedTypeSymbol namedTypeSymbol &&
namedTypeSymbol.Interfaces.Any(i => i.ToDisplayString() == "Microsoft.EntityFrameworkCore.DbSet"));
if (dbSetProperties.Any())
{
var dbSetProperty = (INamedTypeSymbol)dbSetProperties.First();
var entityType = (INamedTypeSymbol)dbSetProperties.First().Type;
var primaryKey = entityType.GetMembers().OfType<IPropertySymbol>().FirstOrDefault(IsKeyProperty);
if (primaryKey != null)
{
efDbContextProperties.PrimaryKeyName = primaryKey.Name;
efDbContextProperties.PrimaryKeyShortTypeName = primaryKey.Type.Name;
efDbContextProperties.PrimaryKeyTypeName = primaryKey.Type.ToDisplayString();
efDbContextProperties.EntitySetName = $"{dbSetProperty.Name}";
}
}
efDbContextProperties.ModelProperties = GetModelProperties(modelSymbol);
}
return efDbContextProperties;
}
private static List<IPropertySymbol> GetModelProperties(ISymbol modelSymbol)
{
List<IPropertySymbol> properties = [];
if (modelSymbol is INamedTypeSymbol namedTypeSymbol)
{
properties.AddRange(namedTypeSymbol.GetMembers().OfType<IPropertySymbol>());
}
return properties;
}
private static bool IsKeyProperty(IPropertySymbol propertySymbol)
{
return propertySymbol.GetAttributes().Any(a => a.AttributeClass?.Name == "KeyAttribute");
}
}
public class EfDbContextProperties
{
public List<IPropertySymbol> ModelProperties { get; set; } = default!;
public string PrimaryKeyName { get; set; } = default!;
public string PrimaryKeyShortTypeName { get; set; } = default!;
public string PrimaryKeyTypeName { get; set; } = default!;
public string EntitySetName { get; set; } = default!;
}

Просмотреть файл

@ -0,0 +1,166 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.DotNet.Scaffolding.Helpers.Extensions.Roslyn;
using Microsoft.DotNet.Scaffolding.Helpers.Services;
namespace Microsoft.DotNet.Scaffolding.Helpers.Roslyn;
public class ProjectModifier
{
private readonly ILogger _consoleLogger;
private readonly ICodeService _codeService;
private readonly IAppSettings _appSettings;
private const string Main = nameof(Main);
private readonly StringBuilder _output;
private readonly CodeChangeOptions _codeChangeOptions;
private readonly CodeModifierConfig? _codeModifierConfig;
public ProjectModifier(
IAppSettings appSettings,
ICodeService codeService,
ILogger consoleLogger,
CodeChangeOptions codeChangeOptions,
CodeModifierConfig? codeModifierConfig = null)
{
_appSettings = appSettings;
_codeService = codeService;
_consoleLogger = consoleLogger ?? throw new ArgumentNullException(nameof(consoleLogger));
_output = new StringBuilder();
_codeChangeOptions = codeChangeOptions;
_codeModifierConfig = codeModifierConfig;
}
public async Task<bool> RunAsync()
{
if (_codeModifierConfig is null || _codeModifierConfig.Files is null || !_codeModifierConfig.Files.Any())
{
return false;
}
var solution = (await _codeService.GetWorkspaceAsync())?.CurrentSolution;
var roslynProject = solution?.GetProject(_appSettings.Workspace().InputPath);
var filteredFiles = _codeModifierConfig.Files.Where(f => ProjectModifierHelper.FilterOptions(f.Options, _codeChangeOptions));
foreach (var file in filteredFiles)
{
Document? originalDocument = roslynProject?.GetDocument(file.FileName);
var modifiedDocument = await HandleCodeFileAsync(originalDocument, file, _codeChangeOptions);
roslynProject = modifiedDocument?.Project;
}
return _codeService.TryApplyChanges(roslynProject?.Solution);
}
private async Task<Document?> HandleCodeFileAsync(Document? document, CodeFile file, CodeChangeOptions options)
{
try
{
if (!string.IsNullOrEmpty(file.AddFilePath))
{
return AddFile(file);
}
else
{
switch (file.Extension)
{
case "cs":
return await ModifyCsFile(file, document, options);
case "cshtml":
return await ModifyCshtmlFile(file, document, options);
case "razor":
case "html":
return await ApplyTextReplacements(file, document, options);
}
//_output.AppendLine(string.Format(Resources.ModifiedCodeFile, file.FileName));
}
}
catch (Exception)
{
//_output.Append(string.Format(Resources.FailedToModifyCodeFile, file.FileName, e.Message));
}
return document;
}
internal static async Task<Document?> ModifyCshtmlFile(CodeFile file, Document? fileDoc, CodeChangeOptions options)
{
if (fileDoc is null || file.Methods is null || !file.Methods.TryGetValue("Global", out var globalMethod))
{
return fileDoc;
}
var filteredCodeChanges = globalMethod?.CodeChanges?.Where(cc => ProjectModifierHelper.FilterOptions(cc.Options, options));
if (filteredCodeChanges != null && !filteredCodeChanges.Any())
{
return fileDoc;
}
// add code snippets/changes.
return await ProjectModifierHelper.ModifyDocumentTextAsync(fileDoc, filteredCodeChanges);
/* if (editedDocument != null)
{
await ProjectModifierHelper.UpdateDocument(editedDocument);
}*/
}
/// <summary>
/// Updates .razor and .html files via string replacement
/// </summary>
/// <param name="file"></param>
/// <param name="project"></param>
/// <param name="toolOptions"></param>
/// <returns></returns>
internal static async Task<Document?> ApplyTextReplacements(CodeFile file, Document? document, CodeChangeOptions toolOptions)
{
if (document is null)
{
return null;
}
var replacements = file.Replacements?.Where(cc => ProjectModifierHelper.FilterOptions(cc.Options, toolOptions));
if (replacements != null && !replacements.Any())
{
return document;
}
return await ProjectModifierHelper.ModifyDocumentTextAsync(document, replacements);
/* if (editedDocument != null)
{
await ProjectModifierHelper.UpdateDocument(editedDocument);
}*/
}
internal async Task<Document?> ModifyCsFile(CodeFile file, Document? fileDoc, CodeChangeOptions options)
{
if (fileDoc is null || string.IsNullOrEmpty(fileDoc.Name))
{
return fileDoc;
}
DocumentBuilder documentBuilder = new(fileDoc, file, _consoleLogger);
return await documentBuilder.RunAsync();
}
private Document? AddFile(CodeFile file)
{
/* var filePath = Path.Combine("TODOprojectpath", file.AddFilePath);
if (File.Exists(filePath))
{
return; // File exists, don't need to create
}
var codeFileString = string.Empty;// GetCodeFileString(file);
var fileDir = Path.GetDirectoryName(filePath);
if (!string.IsNullOrEmpty(fileDir))
{
Directory.CreateDirectory(fileDir);
File.WriteAllText(filePath, codeFileString);
}*/
return null;
}
}

Просмотреть файл

@ -0,0 +1,615 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using Microsoft.DotNet.Scaffolding.Helpers.Extensions;
using Microsoft.DotNet.Scaffolding.Helpers.General;
using Microsoft.DotNet.Scaffolding.Helpers.Services;
namespace Microsoft.DotNet.Scaffolding.Helpers.Roslyn;
public static class ProjectModifierHelper
{
internal static char[] CodeSnippetTrimChars = new char[] { ' ', '\r', '\n', ';' };
internal static IEnumerable<string> CodeSnippetTrimStrings = CodeSnippetTrimChars.Select(c => c.ToString());
internal static char[] Parentheses = new char[] { '(', ')' };
internal const string VarIdentifier = "var";
internal const string WebApplicationBuilderIdentifier = "WebApplicationBuilder";
public const string Main = nameof(Main);
public static CodeModifierConfig? GetCodeModifierConfig(string configName, Assembly? assembly = null)
{
assembly = assembly ?? Assembly.GetExecutingAssembly();
string jsonText = ProjectModelHelper.GetManifestResource(assembly, shortResourceName: configName);
return System.Text.Json.JsonSerializer.Deserialize<CodeModifierConfig>(jsonText);
}
/// <summary>
/// Check if Startup.cs or similar file exists.
/// </summary>
/// <returns>true if Startup.cs does not exist, false if it does exist.</returns>
public static async Task<bool> IsMinimalApp(ICodeService codeService)
{
//find Startup if named Startup.
var allClassSymbols = await codeService.GetAllClassSymbolsAsync();
var startupType = allClassSymbols.FirstOrDefault(x => x.Name.Equals("Startup", StringComparison.OrdinalIgnoreCase));
if (startupType == null)
{
//if changed the name in Program.cs, get the class name and check.
var programDocument = (await codeService.GetAllDocumentsAsync()).FirstOrDefault(d => d.Name.EndsWith("Program.cs"));
var startupClassName = await GetStartupClassName(programDocument);
startupType = allClassSymbols.FirstOrDefault(x => x.Name.Equals(startupClassName, StringComparison.OrdinalIgnoreCase));
}
return startupType == null;
}
public static async Task<bool> IsUsingTopLevelStatements(ICodeService codeService)
{
var allDocuments = await codeService.GetAllDocumentsAsync();
var programDocument = allDocuments?.FirstOrDefault(d => d.Name.EndsWith("Program.cs"));
if (programDocument != null && await programDocument.GetSyntaxRootAsync() is CompilationUnitSyntax root)
{
var fileScopedNamespaceNode = root.Members.OfType<FileScopedNamespaceDeclarationSyntax>()?.FirstOrDefault();
if (fileScopedNamespaceNode == null)
{
var mainMethod = DocumentBuilder.GetMethodFromSyntaxRoot(root, Main);
return mainMethod == null;
}
}
return true;
}
/// <summary>
/// Parses the csproj xml text and gets one or more TargetFrameworks for the project.
/// </summary>
/// <param name="csprojText">.csproj file as string</param>
/// <returns>string[] containing target frameworks of the project</returns>
internal static string[] ProcessCsprojTfms(string csprojText)
{
List<string> processedTfms = new List<string>();
if (!string.IsNullOrEmpty(csprojText))
{
//use XDocument to get all csproj elements.
XDocument document = XDocument.Parse(csprojText);
var docNodes = document.Root?.Elements();
var allElements = docNodes?.SelectMany(x => x.Elements());
//add them to a dictionary for easy comparisons.
var csprojVariables = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (allElements != null && allElements.Any())
{
foreach (var elem in allElements)
{
//dont' add PackageReference(s) since they are useless for getting tfm properties.
if (!elem.Name.LocalName.Equals("PackageReference", StringComparison.OrdinalIgnoreCase))
{
//change the keys from TargetFramework to $(TargetFramework) and so forth for nested variable analysis.
//eg. analysing <TargetFramework>$(X)</TargetFramework> and getting the value for $(X).
//makes for a easy string comparison without using regex and splitting.
string tfmKey = string.Format("$({0})", elem.Name.LocalName);
if (!csprojVariables.ContainsKey(tfmKey))
{
csprojVariables.Add(tfmKey, elem.Value);
}
}
}
}
//if only one TargetFramework
if (csprojVariables.TryGetValue("$(TargetFramework)", out string? tfmValue))
{
string processedTfm = ProcessTfm(tfmValue.Trim(), csprojVariables);
if (!string.IsNullOrEmpty(processedTfm) && ProjectModelHelper.ShortTfmDictionary.Values.ToList().Contains(processedTfm, StringComparer.OrdinalIgnoreCase))
{
processedTfms.Add(processedTfm);
}
}
//if multiple, split by ';' and add them all.
else if (csprojVariables.TryGetValue("$(TargetFrameworks)", out string? tfms))
{
string processedTfm = ProcessTfm(tfms.Trim(), csprojVariables);
//tfms should be separated by ;
var splitTfms = processedTfm.Split(';');
foreach (var tfm in splitTfms)
{
if (!string.IsNullOrEmpty(tfm) && ProjectModelHelper.ShortTfmDictionary.Values.ToList().Contains(tfm, StringComparer.OrdinalIgnoreCase))
{
processedTfms.Add(tfm);
}
}
}
}
return processedTfms.ToArray();
}
// Returns true when there is no Startup.cs or equivalent
internal static async Task<bool> IsMinimalApp(List<Document> documents)
{
if (documents.Where(d => d.Name.EndsWith("Startup.cs")).Any())
{
return false;
}
// if changed the name in Program.cs, get the class name and check.
var programDocument = documents.FirstOrDefault(d => d.Name.EndsWith("Program.cs"));
var startupClassName = await GetStartupClassName(programDocument);
return string.IsNullOrEmpty(startupClassName); // If project has UseStartup in Program.cs, it is not a minimal app
}
// Get Startup class name from CreateHostBuilder in Program.cs. If Program.cs is not being used, method
// will return null.
internal static async Task<string> GetStartupClass(List<Document> documents)
{
string startupClassName = string.Empty;
if (documents != null && documents.Any())
{
var programCsDocument = documents.FirstOrDefault(d => d.Name.Equals("Program.cs"));
startupClassName = await GetStartupClassName(programCsDocument);
}
return string.IsNullOrEmpty(startupClassName) ? string.Empty : string.Concat(startupClassName, ".cs");
}
internal static async Task<string> GetStartupClassName(Document? programDoc)
{
if (programDoc != null && await programDoc.GetSyntaxRootAsync() is CompilationUnitSyntax root)
{
var namespaceNode = root.Members.OfType<NamespaceDeclarationSyntax>()?.FirstOrDefault();
var programClassNode =
namespaceNode?.DescendantNodes()
.FirstOrDefault(node =>
node is ClassDeclarationSyntax cds &&
cds.Identifier
.ValueText.Contains("Program")) ??
root?.DescendantNodes()
.FirstOrDefault(node =>
node is ClassDeclarationSyntax cds &&
cds.Identifier
.ValueText.Contains("Program"));
var useStartupNode = programClassNode?.DescendantNodes()?
.FirstOrDefault(node =>
node is MemberAccessExpressionSyntax maes &&
maes.ToString()
.Contains("webBuilder.UseStartup"));
var useStartupTxt = useStartupNode?.ToString();
if (!string.IsNullOrEmpty(useStartupTxt))
{
int startIndex = useStartupTxt.IndexOf("<");
int endIndex = useStartupTxt.IndexOf(">");
if (startIndex > -1 && endIndex > startIndex)
{
return useStartupTxt.Substring(startIndex + 1, endIndex - startIndex - 1);
}
}
}
return string.Empty;
}
internal static string GetClassName(string className)
{
string formattedClassName = string.Empty;
if (!string.IsNullOrEmpty(className))
{
//switched to using string[] for netstandard2.0 compatibility.
string[] blocks = className.Split(new string[] { ".cs" }, StringSplitOptions.None);
if (blocks.Length > 1)
{
return blocks[0];
}
}
return formattedClassName;
}
internal static BaseMethodDeclarationSyntax? GetOriginalMethod(ClassDeclarationSyntax classNode, string methodName, Method methodChanges)
{
if (classNode?.Members.FirstOrDefault(node
=> node is MethodDeclarationSyntax mds && mds.Identifier.ValueText.Equals(methodName)
|| node is ConstructorDeclarationSyntax cds && cds.Identifier.ValueText.Equals(methodName))
is BaseMethodDeclarationSyntax foundMethod)
{
return foundMethod;
}
return null;
}
// check if the parameters match for the given method, and populate a Dictionary with parameter.Type keys and Parameter.Identifier values.
internal static IDictionary<string, string> VerifyParameters(string[] parametersToCheck, List<ParameterSyntax> foundParameters)
{
IDictionary<string, string> parametersWithNames = new Dictionary<string, string>();
if (foundParameters.Any() && parametersToCheck != null && parametersToCheck.Any())
{
var pars = foundParameters.ToList();
foreach (var parameter in parametersToCheck)
{
//Trim(' ') for the additional whitespace at the end of the parameter.Type string. Parameter.Type should be a singular word.
var verifiedParams = pars.Where(p => (p.Type?.ToFullString()?.Trim(' ')?.Equals(parameter)).GetValueOrDefault(false));
if (verifiedParams.Any())
{
parametersWithNames.Add(parameter, verifiedParams.First().Identifier.ValueText);
}
}
//Dictionary should have the same number of parameters we are trying to verify.
if (parametersWithNames.Count == parametersToCheck.Length)
{
return parametersWithNames;
}
else
{
return new Dictionary<string, string>();
}
}
return parametersWithNames;
}
/// <summary>
/// Format a string of a SimpleMemberAccessExpression(eg., Type.Value)
/// Replace Type with its value from the parameterDict.
/// </summary>
/// <param name="codeBlock">SimpleMemberAccessExpression string</param>
/// <param name="parameterDict">IDictionary with parameter type keys and values</param>
/// <param name="trim">Whether to trim the resulting string</param>
/// <returns></returns>
internal static string FormatCodeBlock(string codeBlock, IDictionary<string, string> parameterDict, bool trim = false)
{
string formattedCodeBlock = codeBlock;
if (!string.IsNullOrEmpty(codeBlock) && parameterDict != null)
{
string value = Regex.Replace(codeBlock, "^([^.]*).", "");
string param = Regex.Replace(codeBlock, "[*^.].*", "");
if (parameterDict.TryGetValue(param, out string? parameter))
{
formattedCodeBlock = $"{parameter}.{value}";
}
}
return trim ? formattedCodeBlock.Trim(CodeSnippetTrimChars) : formattedCodeBlock;
}
/// <summary>
/// Looks for oldValue in codeBlock and replaces with newValue
/// </summary>
/// <param name="codeBlock"></param>
/// <param name="oldValue"></param>
/// <param name="newValue"></param>
/// <returns>codeBlock where any instance of 'oldValue' is replaced with 'newValue'</returns>
internal static string ReplaceValue(string codeBlock, string oldValue, string newValue)
{
string formattedStatement = codeBlock;
if (!string.IsNullOrEmpty(formattedStatement) && !string.IsNullOrEmpty(oldValue) && !string.IsNullOrEmpty(newValue))
{
return formattedStatement.Replace(oldValue, newValue);
}
return formattedStatement;
}
/// <summary>
/// Replaces all instances of the old value with the new value
/// </summary>
/// <param name="changes"></param>
/// <param name="oldValue"></param>
/// <param name="newValue"></param>
/// <returns>updated CodeSnippet array</returns>
internal static CodeSnippet[]? UpdateVariables(CodeSnippet[]? changes, string oldValue, string newValue) => changes?.Select(c => UpdateVariables(c, oldValue, newValue)).ToArray();
/// <summary>
/// Replaces all instances of the old value with the new value
/// </summary>
/// <param name="change"></param>
/// <param name="oldValue"></param>
/// <param name="newValue"></param>
/// <returns>updated CodeSnippet</returns>
internal static CodeSnippet UpdateVariables(CodeSnippet change, string oldValue, string newValue) //TODO
{
// format CodeSnippet fields for any variables or parameters.
if (!string.IsNullOrEmpty(change.Block))
{
change.Block = ReplaceValue(change.Block, oldValue, newValue);
}
if (!string.IsNullOrEmpty(change.Parent))
{
change.Parent = ReplaceValue(change.Parent, oldValue, newValue);
}
if (!string.IsNullOrEmpty(change.CheckBlock))
{
change.CheckBlock = ReplaceValue(change.CheckBlock, oldValue, newValue);
}
if (!string.IsNullOrEmpty(change.InsertAfter))
{
change.InsertAfter = ReplaceValue(change.InsertAfter, oldValue, newValue);
}
if (change.InsertBefore != null && change.InsertBefore.Any())
{
for (int i = 0; i < change.InsertBefore.Count(); i++)
{
change.InsertBefore[i] = ReplaceValue(change.InsertBefore[i], oldValue, newValue);
}
}
return change;
}
/// <summary>
/// Trim ' ', '\r', '\n' and replace any whitespace with no spaces.
/// </summary>
/// <param name="statement"></param>
/// <returns></returns>
internal static string TrimStatement(string statement)
{
StringBuilder sb = new StringBuilder(statement);
if (!string.IsNullOrEmpty(statement))
{
foreach (string replacement in CodeSnippetTrimStrings)
{
sb.Replace(replacement, string.Empty);
}
}
return sb.ToString();
}
/// <summary>
/// Searches through the list of nodes to find replacement variable identifier names
/// </summary>
/// <param name="members"></param>
/// <returns></returns>
internal static (string, string)? GetBuilderVariableIdentifierTransformation(SyntaxList<MemberDeclarationSyntax> members)
{
if (!(members.FirstOrDefault(
m => TrimStatement(m.ToString())
.Contains("=WebApplication.CreateBuilder")) is SyntaxNode memberNode))
{
return null;
}
var memberString = TrimStatement(memberNode.ToString());
var start = 0;
if (memberString.Contains(VarIdentifier))
{
start = memberString.IndexOf(VarIdentifier) + VarIdentifier.Length;
}
else if (memberString.Contains(WebApplicationBuilderIdentifier))
{
start = memberString.IndexOf(WebApplicationBuilderIdentifier) + WebApplicationBuilderIdentifier.Length;
}
if (start > 0)
{
var end = memberString.IndexOf("=");
return ("WebApplication.CreateBuilder", memberString.Substring(start, end - start));
}
return null;
}
internal static bool GlobalStatementExists(CompilationUnitSyntax root, GlobalStatementSyntax statement)
{
if (root != null && statement != null)
{
var formattedStatementString = TrimStatement(statement.ToString());
bool foundStatement = root.Members.Where(st => TrimStatement(st.ToString()).Contains(formattedStatementString)).Any();
if (foundStatement)
{
return true;
}
}
return false;
}
internal static bool AttributeExists(string attribute, SyntaxList<AttributeListSyntax> attributeList)
{
if (attributeList.Any() && !string.IsNullOrEmpty(attribute))
{
return attributeList.Where(al => al.Attributes.Where(attr => attr.ToString().Equals(attribute, StringComparison.OrdinalIgnoreCase)).Any()).Any();
}
return false;
}
internal static bool StatementExists(IEnumerable<SyntaxNode> nodes, string statement)
{
statement = statement.TrimEnd(Parentheses);
return nodes.Any(n => StatementExists(n, statement));
}
internal static bool StatementExists(SyntaxNode node, string statement)
{
var existingBlock = TrimStatement(node.ToString());
var statementToCheck = TrimStatement(statement);
return existingBlock.Contains(statementToCheck);
}
// Filter out CodeSnippets that are invalid using FilterOptions
internal static CodeSnippet[]? FilterCodeSnippets(CodeSnippet[]? codeSnippets, CodeChangeOptions options) => codeSnippets?.Where(cs => FilterOptions(cs.Options, options)).ToArray();
/// <summary>
/// Filter Options string array to matching CodeChangeOptions.
/// Primary use to filter out CodeBlocks and Files that apply in Microsoft Graph and Downstream API scenarios
/// </summary>
/// <param name="options">string [] in cm_*.json files for code modifications</param>
/// <param name="codeChangeOptions">based on cli parameters</param>
/// <returns>true if the CodeChangeOptions apply, false otherwise. </returns>
internal static bool FilterOptions(string[]? options, CodeChangeOptions codeChangeOptions)
{
//if no options are passed, CodeBlock is valid
if (options == null)
{
return true;
}
//if options have a "Skip", every CodeBlock is invalid
if (options.Contains(CodeChangeOptionStrings.Skip))
{
return false;
}
// for example, program.cs is only modified when codeChangeOptions.IsMinimalApp is true
if (options.Contains(CodeChangeOptionStrings.MinimalApp) && !codeChangeOptions.IsMinimalApp)
{
return false;
}
//if its a minimal app and options have a "NonMinimalApp", don't add the CodeBlock
if (options.Contains(CodeChangeOptionStrings.NonMinimalApp) && codeChangeOptions.IsMinimalApp)
{
return false;
}
//an app will either support DownstreamApi, MicrosoftGraph, both, or neither.
if (codeChangeOptions.DownstreamApi)
{
if (options.Contains(CodeChangeOptionStrings.DownstreamApi) ||
!options.Contains(CodeChangeOptionStrings.MicrosoftGraph))
{
return true;
}
}
if (codeChangeOptions.MicrosoftGraph)
{
if (options.Contains(CodeChangeOptionStrings.MicrosoftGraph) ||
!options.Contains(CodeChangeOptionStrings.DownstreamApi))
{
return true;
}
}
if (!codeChangeOptions.DownstreamApi && !codeChangeOptions.MicrosoftGraph)
{
if (options == null ||
(!options.Contains(CodeChangeOptionStrings.MicrosoftGraph) &&
!options.Contains(CodeChangeOptionStrings.DownstreamApi)))
{
return true;
}
}
return false;
}
/// <summary>
/// Replaces text within document or appends text to the end of the document
/// depending on whether change.ReplaceSnippet is set
/// </summary>
/// <param name="fileDoc"></param>
/// <param name="codeChanges"></param>
/// <returns>updated document, or null if no changes made</returns>
internal static async Task<Document?> ModifyDocumentTextAsync(Document? fileDoc, IEnumerable<CodeSnippet>? codeChanges)
{
if (fileDoc is null || codeChanges is null || !codeChanges.Any())
{
return null;
}
var sourceText = await fileDoc.GetTextAsync();
var sourceFileString = sourceText?.ToString() ?? null;
if (sourceFileString is null)
{
return null;
}
var trimmedSourceFile = TrimStatement(sourceFileString);
var applicableCodeChanges = codeChanges.Where(c => !trimmedSourceFile.Contains(TrimStatement(c.Block)));
if (!applicableCodeChanges.Any())
{
return null;
}
foreach (var change in applicableCodeChanges)
{
// If doing a code replacement, replace ReplaceSnippet in source with Block
if (change.ReplaceSnippet != null)
{
var replaceSnippet = string.Join(System.Environment.NewLine, change.ReplaceSnippet);
if (sourceFileString.Contains(replaceSnippet))
{
sourceFileString = sourceFileString.Replace(replaceSnippet, change.Block);
}
else
{
// TODO: Generate readme file when replace snippets not found in file
}
}
else
{
sourceFileString += change.Block; // Otherwise appending block to end of file
}
}
var updatedSourceText = SourceText.From(sourceFileString);
return fileDoc.WithText(updatedSourceText);
}
internal static async Task UpdateDocument(Document document)
{
var classFileTxt = await document.GetTextAsync();
File.WriteAllText(document.Name, classFileTxt.ToString(), new UTF8Encoding(false));
}
// Filter out CodeBlocks that are invalid using FilterOptions
internal static CodeBlock[] FilterCodeBlocks(CodeBlock[] codeBlocks, CodeChangeOptions options)
{
var filteredCodeBlocks = new HashSet<CodeBlock>();
if (codeBlocks != null && codeBlocks.Any() && options != null)
{
foreach (var codeBlock in codeBlocks)
{
if (FilterOptions(codeBlock.Options, options))
{
filteredCodeBlocks.Add(codeBlock);
}
}
}
return filteredCodeBlocks.ToArray();
}
/// <summary>
/// Take the tfm value in the csproj and use the Dictionary of variables to find its true value.
/// </summary>
/// <param name="tfm">value for <TargetFramework/> or '<TargetFrameworks/> in the csproj file</param>
/// <param name="csprojVariables">dictionary with all csproj properties and values</param>
/// <returns></returns>
internal static string ProcessTfm(string tfm, Dictionary<string, string> csprojVariables)
{
if (string.IsNullOrEmpty(tfm))
{
return string.Empty;
}
bool tfmHasVars = true;
while (tfmHasVars)
{
//if the value is in the tfm dictionary (valid values), return it.
if (ProjectModelHelper.ShortTfmDictionary.Values.ToList().Contains(tfm, StringComparer.OrdinalIgnoreCase))
{
return tfm;
}
//if the value has a variable (key) in it, replace it with its value.
else if (tfm.Contains('$'))
{
foreach (var key in csprojVariables.Keys)
{
if (tfm.ContainsIgnoreCase(key) && csprojVariables.TryGetValue(key, out string? val))
{
tfm = tfm.Replace(key, val);
}
}
}
else
{
return tfm;
}
}
return tfm;
}
}

Просмотреть файл

@ -0,0 +1,38 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
namespace Microsoft.DotNet.Scaffolding.Helpers.Services;
public class AppSettings : IAppSettings
{
private readonly Dictionary<string, object> _settings;
public AppSettings()
{
_settings = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
GlobalProperties = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
/// <inheritdoc />
public IDictionary<string, string> GlobalProperties { get; }
/// <inheritdoc />
public object? GetSettings(string sectionName)
{
if (string.IsNullOrEmpty(sectionName))
{
return null;
}
return _settings.TryGetValue(sectionName, out var settings) ? settings : null;
}
/// <inheritdoc />
public void AddSettings(string sectionName, object settings)
{
if (string.IsNullOrEmpty(sectionName))
{
return;
}
_settings[sectionName] = settings;
}
}

Просмотреть файл

@ -0,0 +1,18 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Microsoft.DotNet.Scaffolding.Helpers.Services;
public static class AppSettingsExtensions
{
public static WorkspaceSettings Workspace(this IAppSettings settings)
{
return settings.GetSettings<WorkspaceSettings>("workspace") ?? new WorkspaceSettings();
}
public static T? GetSettings<T>(this IAppSettings settings, string sectionName) where T : class
{
var sectionObject = settings.GetSettings(sectionName);
return sectionObject as T;
}
}

Просмотреть файл

@ -0,0 +1,192 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.MSBuild;
using Microsoft.DotNet.Scaffolding.Helpers.Extensions.Roslyn;
namespace Microsoft.DotNet.Scaffolding.Helpers.Services;
/// <summary>
/// Service that manages Roslyn workspace. It ensures that all projects of interest are loaded in the workspace and are up to date.
/// - If user's input path is project we also always open it since we expect some contracts might
/// want to get access to that project.
/// - all other projects loaded to the workspace only per-request to avoid running DT builds on all
/// full solution. When some caller need to ensure project is loaded (for example ProjectService
/// when it creates a new instance of <see cref="IProject"/>) it should call OpenProjectAsync
/// here and that would ensure project is loaded in the workspace.
/// </summary>
public class CodeService : ICodeService, IDisposable
{
private readonly ILogger _logger;
private MSBuildWorkspace? _workspace;
private Compilation? _compilation;
private readonly IAppSettings _settings;
public CodeService(IAppSettings settings, ILogger logger)
{
_logger = logger;
_settings = settings;
}
/// <inheritdoc />
public async Task<Workspace?> GetWorkspaceAsync()
{
return await GetMsBuildWorkspaceAsync(_settings.Workspace().InputPath);
}
/// <inheritdoc />
public bool TryApplyChanges(Solution? solution)
{
if (solution is null || _workspace is null)
{
return false;
}
var success = _workspace?.TryApplyChanges(solution) == true;
return success;
}
private async Task<MSBuildWorkspace?> GetMsBuildWorkspaceAsync(string? path)
{
if (string.IsNullOrEmpty(path))
{
return _workspace;
}
if (_workspace is not null)
{
return _workspace;
}
var workspace = MSBuildWorkspace.Create(_settings.GlobalProperties);
workspace.WorkspaceFailed += OnWorkspaceFailed;
workspace.LoadMetadataForReferencedProjects = true;
await workspace.OpenProjectAsync(path).ConfigureAwait(false);
_workspace = workspace;
return _workspace;
}
public async Task OpenProjectAsync(string projectPath)
{
if (string.IsNullOrEmpty(projectPath))
{
return;
}
var workspace = await GetWorkspaceAsync();
if (workspace is not MSBuildWorkspace msbuildWorkspace)
{
return;
}
Project? project = msbuildWorkspace.CurrentSolution.GetProject(projectPath);
if (project is not null)
{
return;
}
try
{
await msbuildWorkspace.OpenProjectAsync(projectPath).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogMessage(ex.ToString(), LogMessageType.Error);
}
}
private void UnloadWorkspace()
{
var workspace = _workspace;
_workspace = null;
_compilation = null;
if (workspace is not null)
{
workspace.WorkspaceFailed -= OnWorkspaceFailed;
workspace.CloseSolution();
workspace.Dispose();
}
}
public async Task ReloadWorkspaceAsync(string? projectPath)
{
UnloadWorkspace();
await GetMsBuildWorkspaceAsync(_settings.Workspace().InputPath);
await OpenProjectAsync(projectPath!);
}
private void OnWorkspaceFailed(object? sender, WorkspaceDiagnosticEventArgs e)
{
var diagnostic = e.Diagnostic!;
_logger.LogMessage($"[{diagnostic.Kind}] Problem loading file in MSBuild workspace {diagnostic.Message}");
}
public void Dispose()
{
UnloadWorkspace();
}
public async Task<IList<ISymbol>> GetAllClassSymbolsAsync()
{
List<ISymbol> classSymbols = [];
if (_compilation is null)
{
var workspace = await GetWorkspaceAsync();
var project = workspace?.CurrentSolution?.GetProject(_settings.Workspace().InputPath);
if (project is not null)
{
_compilation = await project.GetCompilationAsync();
}
}
var compilationClassSymbols = _compilation?.SyntaxTrees.SelectMany(tree =>
{
var model = _compilation.GetSemanticModel(tree);
var allNodes = tree.GetRoot().DescendantNodes();
return tree.GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>().Select(classSyntax =>
{
var classSymbol = model.GetDeclaredSymbol(classSyntax);
return classSymbol;
});
}).Append(_compilation.GetEntryPoint(CancellationToken.None)?.ContainingType).ToList();
compilationClassSymbols?.ForEach(x =>
{
if (x is not null)
{
classSymbols.Add(x);
}
});
return classSymbols;
}
public async Task<IList<Document>> GetAllDocumentsAsync()
{
var workspace = await GetWorkspaceAsync();
var project = workspace?.CurrentSolution?.GetProject(_settings.Workspace().InputPath);
if (project is not null)
{
return project.Documents.ToList();
}
return [];
}
public async Task<Document?> GetDocumentAsync(string? documentName)
{
if (string.IsNullOrWhiteSpace(documentName))
{
return null;
}
var workspace = await GetWorkspaceAsync();
var project = workspace?.CurrentSolution?.GetProject(_settings.Workspace().InputPath);
if (project is not null)
{
return project.Documents.FirstOrDefault(x => x.Name.EndsWith(documentName));
}
return null;
}
}

Просмотреть файл

@ -0,0 +1,142 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Concurrent;
using System.Text.Json;
using System.Text.RegularExpressions;
using Microsoft.DotNet.Scaffolding.ComponentModel;
using Microsoft.DotNet.Scaffolding.Helpers.General;
using Microsoft.DotNet.Scaffolding.Helpers.Services.Environment;
namespace Microsoft.DotNet.Scaffolding.Helpers.Services
{
public class DotNetToolService : IDotNetToolService
{
private readonly IEnvironmentService _environmentService;
private readonly IFileSystem _fileSystem;
public DotNetToolService(IEnvironmentService environmentService, IFileSystem fileSystem)
{
_environmentService = environmentService;
_fileSystem = fileSystem;
}
private IList<DotNetToolInfo>? _dotNetTools;
public IList<DotNetToolInfo> GlobalDotNetTools
{
get
{
_dotNetTools ??= GetDotNetTools();
return _dotNetTools;
}
}
public List<CommandInfo> GetCommands(string dotnetToolName)
{
List<CommandInfo>? commands = null;
if (GlobalDotNetTools.FirstOrDefault(x => x.Command.Equals(dotnetToolName, StringComparison.OrdinalIgnoreCase)) != null)
{
var runner = DotnetCliRunner.Create(dotnetToolName, ["get-commands"]);
var exitCode = runner.ExecuteAndCaptureOutput(out var stdOut, out var stdErr);
if (exitCode == 0 && !string.IsNullOrEmpty(stdOut))
{
try
{
string escapedJsonString = stdOut.Replace("\r", "").Replace("\n", "");
commands = JsonSerializer.Deserialize<List<CommandInfo>>(escapedJsonString);
}
catch (Exception)
{
}
}
}
return commands ?? [];
}
public DotNetToolInfo? GetDotNetTool(string? componentName, string? version = null)
{
if (string.IsNullOrEmpty(componentName))
{
return null;
}
var matchingTools = GlobalDotNetTools.Where(x => x.Command.Equals(componentName, StringComparison.OrdinalIgnoreCase));
if (string.IsNullOrEmpty(version))
{
return matchingTools.FirstOrDefault();
}
else
{
return matchingTools.FirstOrDefault(x => x.Version.Equals(version));
}
}
public IList<KeyValuePair<string, CommandInfo>> GetAllCommandsParallel(IList<DotNetToolInfo>? components = null)
{
if (components is null || components.Count == 0)
{
components = GlobalDotNetTools;
}
var options = new ParallelOptions
{
MaxDegreeOfParallelism = System.Environment.ProcessorCount
};
var commands = new ConcurrentBag<KeyValuePair<string, CommandInfo>>();
Parallel.ForEach(components, options, dotnetTool =>
{
var commandInfo = GetCommands(dotnetTool.Command);
if (commandInfo != null)
{
foreach (var cmd in commandInfo)
{
commands.Add(KeyValuePair.Create(dotnetTool.Command, cmd));
}
}
});
return commands.ToList();
}
private static IList<DotNetToolInfo> GetDotNetTools()
{
var dotnetToolList = new List<DotNetToolInfo>();
var runner = DotnetCliRunner.CreateDotNet("tool", ["list", "-g"]);
var exitCode = runner.ExecuteAndCaptureOutput(out var stdOut, out var stdErr);
if (exitCode == 0 && !string.IsNullOrEmpty(stdOut))
{
var stdOutByLine = stdOut.Split(System.Environment.NewLine);
foreach (var line in stdOutByLine)
{
var parsedDotNetTool = ParseToolInfo(line);
if (parsedDotNetTool != null &&
!parsedDotNetTool.Command.Equals("dotnet-scaffold", StringComparison.OrdinalIgnoreCase) &&
!parsedDotNetTool.PackageName.Equals("package", StringComparison.OrdinalIgnoreCase))
{
dotnetToolList.Add(parsedDotNetTool);
}
}
}
return dotnetToolList;
}
private static DotNetToolInfo? ParseToolInfo(string line)
{
var match = Regex.Match(line, @"^(\S+)\s+(\S+)\s+(\S+)");
if (match.Success)
{
return new DotNetToolInfo
{
PackageName = match.Groups[1].Value,
Version = match.Groups[2].Value,
Command = match.Groups[3].Value
};
}
return null;
}
}
}

Просмотреть файл

@ -0,0 +1,144 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using System.Runtime.InteropServices;
namespace Microsoft.DotNet.Scaffolding.Helpers.Services.Environment;
/// <summary>
/// Wrapper over System.Environment abstraction for unit testing.
/// </summary>
public class EnvironmentService : IEnvironmentService
{
private readonly IFileSystem _fileSystem;
public EnvironmentService(IFileSystem fileSystem)
{
_fileSystem = fileSystem;
}
private const string DOTNET_RUNNING_IN_CONTAINER = nameof(DOTNET_RUNNING_IN_CONTAINER);
private static string? _nugetCache;
public static string LocalNugetCachePath
{
get
{
if (string.IsNullOrEmpty(_nugetCache))
{
var nugetPackagesEnvironmentVariable = System.Environment.GetEnvironmentVariable("NUGET_PACKAGES");
_nugetCache = string.IsNullOrWhiteSpace(nugetPackagesEnvironmentVariable)
? Path.Combine(LocalUserProfilePath, ".nuget", "packages")
: nugetPackagesEnvironmentVariable;
}
return _nugetCache!;
}
}
public static string LocalUserProfilePath
{
get
{
return System.Environment.GetEnvironmentVariable(
RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? "USERPROFILE"
: "HOME") ?? "USERPROFILE";
}
}
public const string DotnetProfileDirectoryName = ".dotnet";
/// <inheritdoc />
public string CurrentDirectory => System.Environment.CurrentDirectory;
/// <inheritdoc />
public OperatingSystem OS => System.Environment.OSVersion;
/// <inheritdoc />
public bool Is64BitOperatingSystem => System.Environment.Is64BitOperatingSystem;
/// <inheritdoc />
public bool Is64BitProcess => System.Environment.Is64BitProcess;
/// <inheritdoc />
public string UserProfilePath => LocalNugetCachePath;
/// <inheritdoc />
public string? DomainName => System.Net.NetworkInformation.IPGlobalProperties.GetIPGlobalProperties().DomainName;
/// <inheritdoc />
public string NugetCachePath => LocalNugetCachePath;
private string? _dotnetHomePath;
/// <inheritdoc />
public string DotnetUserProfilePath
{
get
{
if (string.IsNullOrEmpty(_dotnetHomePath))
{
var homePath = System.Environment.GetEnvironmentVariable("DOTNET_CLI_HOME");
if (string.IsNullOrEmpty(homePath))
{
homePath = UserProfilePath;
}
if (!string.IsNullOrEmpty(homePath))
{
homePath = Path.Combine(LocalUserProfilePath, DotnetProfileDirectoryName);
}
_dotnetHomePath = homePath ?? string.Empty;
}
return _dotnetHomePath!;
}
}
private string? _localUserFolderPath;
public string LocalUserFolderPath
{
get
{
if (string.IsNullOrEmpty(_localUserFolderPath))
{
_localUserFolderPath = LocalUserProfilePath;
}
return _localUserFolderPath!;
}
}
/// <inheritdoc />
public string GetMachineName()
{
return System.Environment.MachineName;
}
/// <inheritdoc />
public string? GetEnvironmentVariable(string name)
{
return System.Environment.GetEnvironmentVariable(name);
}
/// <inheritdoc />
public void SetEnvironmentVariable(string name, string value, EnvironmentVariableTarget envTarget)
{
System.Environment.SetEnvironmentVariable(name, value, envTarget);
}
/// <inheritdoc />
public string GetFolderPath(System.Environment.SpecialFolder specifalFolder)
{
return System.Environment.GetFolderPath(specifalFolder);
}
/// <inheritdoc />
public string ExpandEnvironmentVariables(string name)
{
return System.Environment.ExpandEnvironmentVariables(name);
}
}

Просмотреть файл

@ -0,0 +1,48 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System.Composition;
namespace Microsoft.DotNet.Scaffolding.Helpers.Services.Environment;
/// <summary>
/// Sets environment variables for this process.
/// </summary>
public class EnvironmentVariablesStartup
{
private readonly IHostService _hostService;
private readonly IEnvironmentService _environment;
private readonly IAppSettings _settings;
public EnvironmentVariablesStartup(
IHostService hostService,
IEnvironmentService environment,
IAppSettings settings)
{
_hostService = hostService;
_environment = environment;
_settings = settings;
}
/// <inheritdoc />
public async ValueTask<bool> StartupAsync()
{
var environmentVariables = await _hostService.GetEnvironmentVariablesAsync();
SetEnvironmentVariables(environmentVariables);
return true;
}
private void SetEnvironmentVariables(IDictionary<string, string> environmentVariables)
{
foreach (var kvp in environmentVariables)
{
if (string.IsNullOrEmpty(kvp.Key))
{
continue;
}
_environment.SetEnvironmentVariable(kvp.Key, kvp.Value);
_settings.GlobalProperties[kvp.Key] = kvp.Value;
}
}
}

Просмотреть файл

@ -0,0 +1,55 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System.Reflection;
namespace Microsoft.DotNet.Scaffolding.Helpers.Services.Environment;
public class HostService : IHostService
{
private readonly ILogger _logger;
private readonly IEnumerable<IEnvironmentVariableProvider> _providers;
private Dictionary<string, string>? _variables;
public HostService(ILogger logger, IEnumerable<IEnvironmentVariableProvider> providers)
{
_logger = logger;
_providers = providers;
}
/// <inheritdoc />
public string GetInstallationPath()
{
return Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? string.Empty;
}
/// <inheritdoc />
public async ValueTask<IDictionary<string, string>> GetEnvironmentVariablesAsync()
{
if (_variables is null)
{
var variables = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var provider in _providers)
{
try
{
var providerVariables = await provider.GetEnvironmentVariablesAsync();
if (providerVariables is not null)
{
foreach (var kvp in providerVariables)
{
variables[kvp.Key] = kvp.Value;
}
}
}
catch (Exception ex)
{
_logger.LogMessage(ex.Message, LogMessageType.Error);
}
}
_variables = variables;
}
return _variables;
}
}

Просмотреть файл

@ -0,0 +1,19 @@
namespace Microsoft.DotNet.Scaffolding.Helpers.Services.Environment;
public interface IEnvironmentService
{
string UserProfilePath { get; }
string NugetCachePath { get; }
string LocalUserFolderPath { get; }
string DotnetUserProfilePath { get; }
string CurrentDirectory { get; }
bool Is64BitOperatingSystem { get; }
bool Is64BitProcess { get; }
string? DomainName { get; }
OperatingSystem OS { get; }
string GetMachineName();
string? GetEnvironmentVariable(string name);
void SetEnvironmentVariable(string name, string value, EnvironmentVariableTarget envTarget = EnvironmentVariableTarget.Process);
string GetFolderPath(System.Environment.SpecialFolder specifalFolder);
string ExpandEnvironmentVariables(string name);
}

Просмотреть файл

@ -0,0 +1,19 @@
// 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.Text;
using System.Threading.Tasks;
namespace Microsoft.DotNet.Scaffolding.Helpers.Services.Environment;
public interface IEnvironmentVariableProvider
{
/// <summary>
/// Returns environment variables to be set in current process.
/// </summary>
/// <returns></returns>
ValueTask<IEnumerable<KeyValuePair<string, string>>?> GetEnvironmentVariablesAsync();
}

Просмотреть файл

@ -0,0 +1,150 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Runtime.InteropServices;
using Microsoft.DotNet.Scaffolding.Helpers.General;
namespace Microsoft.DotNet.Scaffolding.Helpers.Services.Environment;
public class MacMsbuildEnvironmentVariableProvider : IEnvironmentVariableProvider
{
private const string MacOSMonoFrameworkMSBuildExtensionsDir = "/Library/Frameworks/Mono.framework/External/xbuild";
private readonly IAppSettings _settings;
private readonly IFileSystem _fileSystem;
private readonly ILogger _logger;
public MacMsbuildEnvironmentVariableProvider(IAppSettings settings, IFileSystem fileSystem, ILogger logger)
{
_settings = settings;
_fileSystem = fileSystem;
_logger = logger;
}
/// <inheritdoc />
public ValueTask<IEnumerable<KeyValuePair<string, string>>?> GetEnvironmentVariablesAsync()
{
// Note: try to do it for Mac and Linux.
return !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? GetMacEnvironmentVariablesAsync()
: new ValueTask<IEnumerable<KeyValuePair<string, string>>?>((IEnumerable<KeyValuePair<string, string>>?)null);
}
private ValueTask<IEnumerable<KeyValuePair<string, string>>?> GetMacEnvironmentVariablesAsync()
{
var variables = new Dictionary<string, string>();
var msbuildPath = _settings.Workspace().MSBuildPath;
// If the user-settable MSBuildPath is null/empty at this point, then use the
// "MSBuildExtensionsPath" environment variable which *should* have been set
// by the MSBuildLocator.
if (string.IsNullOrEmpty(msbuildPath))
{
msbuildPath = System.Environment.GetEnvironmentVariable("MSBuildExtensionsPath");
}
var msbuildExtensionsPath = GetMacOSMSBuildExtensionsPath(msbuildPath);
if (!string.IsNullOrEmpty(msbuildExtensionsPath))
{
_settings.Workspace().MSBuildPath = msbuildExtensionsPath;
variables.Add(Constants.EnvironmentVariables.MSBuildExtensionsPath32, msbuildExtensionsPath);
variables.Add(Constants.EnvironmentVariables.MSBuildExtensionsPath, msbuildExtensionsPath);
var msbuildExePath = Path.Combine(msbuildExtensionsPath, "MSBuild.dll");
variables.Add(Constants.EnvironmentVariables.MSBUILD_EXE_PATH, msbuildExePath);
var msbuildSdksPath = Path.Combine(msbuildExtensionsPath, "Sdks");
variables.Add(Constants.EnvironmentVariables.MSBuildSDKsPath, msbuildSdksPath);
}
variables.Add(Constants.EnvironmentVariables.USERPROFILE, System.Environment.GetEnvironmentVariable("HOME") ?? string.Empty);
return new ValueTask<IEnumerable<KeyValuePair<string, string>>?>(variables);
}
private string? GetMacOSMSBuildExtensionsPath(string? suppliedPath)
{
const string DefaultDotnetSdkLocation = "/usr/local/share/dotnet/sdk/";
if (string.IsNullOrEmpty(suppliedPath) || !suppliedPath.StartsWith(DefaultDotnetSdkLocation, StringComparison.Ordinal))
{
return suppliedPath;
}
string? msbuildExtensionsPath = null;
if (_fileSystem.DirectoryExists(MacOSMonoFrameworkMSBuildExtensionsDir))
{
// Check to see if the specified MSBuildPath contains the Mono.framework build extensions.
var monoExtensionDirectories = _fileSystem.EnumerateDirectories(MacOSMonoFrameworkMSBuildExtensionsDir, "*.*", SearchOption.TopDirectoryOnly);
var createTempExtensionsDir = false;
foreach (var monoExtensionDir in monoExtensionDirectories)
{
var dotnetExtensionDir = Path.Combine(suppliedPath, Path.GetFileName(monoExtensionDir));
if (!_fileSystem.DirectoryExists(dotnetExtensionDir))
{
createTempExtensionsDir = true;
break;
}
}
// If the specified MSBuildPath does not contain the Mono.framework build extensions, create a temp
// directory that we'll use to symlink everything.
if (createTempExtensionsDir)
{
var homeDir = System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile);
//var versionDir = Path.GetFileName(suppliedPath.TrimEndPathSeparators());
var versionDir = suppliedPath;
msbuildExtensionsPath = Path.Combine(homeDir, ".dotnet-upgrade-assistant", "dotnet-sdk", versionDir);
if (!_fileSystem.DirectoryExists(msbuildExtensionsPath))
{
_fileSystem.CreateDirectory(msbuildExtensionsPath);
}
// First, create symbolic links to all of the dotnet MSBuild file system entries.
CreateSymbolicLinks(msbuildExtensionsPath, suppliedPath);
// Then create the symbolic links to the Mono.framework/External/xbuild system entries.
CreateSymbolicLinks(msbuildExtensionsPath, MacOSMonoFrameworkMSBuildExtensionsDir);
}
}
return msbuildExtensionsPath ?? suppliedPath;
}
private static void CreateSymbolicLinks(string targetDir, string sourceDir)
{
foreach (var entry in Directory.EnumerateFileSystemEntries(sourceDir))
{
var target = Path.Combine(targetDir, Path.GetFileName(entry));
var fileInfo = new FileInfo(target);
if (fileInfo.Exists)
{
if (fileInfo.LinkTarget is not null && fileInfo.LinkTarget.Equals(entry, StringComparison.Ordinal))
{
continue;
}
File.Delete(target);
}
else
{
var dirInfo = new DirectoryInfo(target);
if (dirInfo.Exists)
{
if (dirInfo.LinkTarget is not null && dirInfo.LinkTarget.Equals(entry, StringComparison.Ordinal))
{
continue;
}
Directory.Delete(target);
}
}
File.CreateSymbolicLink(target, entry);
}
}
}

Просмотреть файл

@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System.Runtime.InteropServices;
using Microsoft.VisualStudio.Setup.Configuration;
namespace Microsoft.DotNet.Scaffolding.Helpers.Environment;
#pragma warning disable CA5392 // Use DefaultDllImportSearchPaths attribute for P/Invokes
#pragma warning disable SA1114 // Parameter list should follow declaration
internal static class NativeMethods
{
[DllImport("Microsoft.VisualStudio.Setup.Configuration.Native.dll", ExactSpelling = true, PreserveSig = true)]
public static extern int GetSetupConfiguration(
[MarshalAs(UnmanagedType.Interface)][Out] out ISetupConfiguration configuration,
IntPtr reserved);
}

Просмотреть файл

@ -0,0 +1,177 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System.Runtime.InteropServices;
using Microsoft.DotNet.Scaffolding.Helpers.Services.Environment;
using Microsoft.DotNet.Scaffolding.Helpers.Services;
using Microsoft.DotNet.Scaffolding.Helpers.General;
using Microsoft.DotNet.Scaffolding.Helpers.Extensions;
using Microsoft.VisualStudio.Setup.Configuration;
namespace Microsoft.DotNet.Scaffolding.Helpers.Environment;
public class WindowsEnvironmentVariableProvider : IEnvironmentVariableProvider
{
private const int REGDB_E_CLASSNOTREG = unchecked((int)0x80040154);
private readonly IAppSettings _settings;
private readonly IFileSystem _fileSystem;
private readonly ILogger _logger;
public WindowsEnvironmentVariableProvider(IAppSettings settings, IFileSystem fileSystem, ILogger logger)
{
_settings = settings;
_fileSystem = fileSystem;
_logger = logger;
}
/// <inheritdoc />
public ValueTask<IEnumerable<KeyValuePair<string, string>>?> GetEnvironmentVariablesAsync()
{
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? GetWindowsEnvironmentVariablesAsync()
: new ValueTask<IEnumerable<KeyValuePair<string, string>>?>((IEnumerable<KeyValuePair<string, string>>?)null);
}
private ValueTask<IEnumerable<KeyValuePair<string, string>>?> GetWindowsEnvironmentVariablesAsync()
{
var variables = new Dictionary<string, string>();
var workspace = _settings.Workspace();
var (visualStudioPath, visualStudioVersion) = GetLatestVisualStudioPath(workspace.VisualStudioPath);
if (!string.IsNullOrEmpty(visualStudioPath))
{
// TODO mark this class as IStartup explicitly so it does initialize all settings it can.
workspace.VisualStudioPath ??= visualStudioPath.WithOsPathSeparators();
workspace.MSBuildPath ??= Path.Combine(visualStudioPath, "MSBuild");
variables.Add(Constants.EnvironmentVariables.VSINSTALLDIR, workspace.VisualStudioPath);
variables.Add(Constants.EnvironmentVariables.MSBuildExtensionsPath32, workspace.MSBuildPath);
variables.Add(Constants.EnvironmentVariables.MSBuildExtensionsPath, workspace.MSBuildPath);
}
if (visualStudioVersion is int version)
{
workspace.VisualStudioVersion ??= $"{version}.0";
variables.Add("VisualStudioVersion", workspace.VisualStudioVersion);
}
variables.Add(Constants.EnvironmentVariables.USERPROFILE, System.Environment.GetEnvironmentVariable(Constants.EnvironmentVariables.USERPROFILE) ?? string.Empty);
return new ValueTask<IEnumerable<KeyValuePair<string, string>>?>(variables);
}
private (string? Path, int? Version) GetLatestVisualStudioPath(string? suppliedPath)
{
var latest = GetLatestVisualStudio(suppliedPath);
if (latest.InstallPath is null)
{
_logger.LogMessage("Did not find a Visual Studio instance", LogMessageType.Information);
return default;
}
if (_fileSystem.DirectoryExists(latest.InstallPath))
{
return (latest.InstallPath, latest.Version.Major);
}
else
{
return default;
}
}
private (string? InstallPath, Version Version) GetLatestVisualStudio(string? suppliedPath)
{
var resultVersion = new Version(0, 0);
string? resultPath = null;
try
{
// This code is not obvious. See the sample (link above) for reference.
var query = (ISetupConfiguration2)GetQuery();
var e = query.EnumAllInstances();
int fetched;
var instances = new ISetupInstance[1];
do
{
// Call e.Next to query for the next instance (single item or nothing returned).
e.Next(1, instances, out fetched);
if (fetched <= 0)
{
continue;
}
var instance = (ISetupInstance2)instances[0];
var state = instance.GetState();
if (!Version.TryParse(instance.GetInstallationVersion(), out var version))
{
continue;
}
// If the install was complete and a valid version, consider it.
if (state == InstanceState.Complete ||
(state.HasFlag(InstanceState.Registered) && state.HasFlag(InstanceState.NoRebootRequired)))
{
var instanceHasMSBuild = false;
foreach (var package in instance.GetPackages())
{
if (string.Equals(package.GetId(), "Microsoft.Component.MSBuild", StringComparison.OrdinalIgnoreCase))
{
instanceHasMSBuild = true;
break;
}
}
if (instanceHasMSBuild && instance is not null)
{
var installPath = instance.GetInstallationPath();
if (suppliedPath is not null && string.Equals(suppliedPath, installPath, StringComparison.OrdinalIgnoreCase))
{
return (installPath, version);
}
else if (version > resultVersion)
{
resultPath = installPath;
resultVersion = version;
}
}
}
}
while (fetched > 0);
}
catch (COMException)
{
}
catch (DllNotFoundException)
{
// This is OK, VS "15" or greater likely not installed.
}
return (resultPath, resultVersion);
}
private static ISetupConfiguration GetQuery()
{
try
{
// Try to CoCreate the class object.
return new SetupConfiguration();
}
catch (COMException ex) when (ex.ErrorCode == REGDB_E_CLASSNOTREG)
{
// Try to get the class object using app-local call.
var result = NativeMethods.GetSetupConfiguration(out var query, IntPtr.Zero);
if (result < 0)
{
throw;
}
return query;
}
}
}

Просмотреть файл

@ -0,0 +1,132 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System.Diagnostics;
using System.Reflection;
namespace Microsoft.DotNet.Scaffolding.Helpers.Services
{
/// <summary>
/// The default implementation of <see cref="IFileSystem"/>
/// used by product code. This just makes calls to methods in System.IO
/// </summary>
public class FileSystem : IFileSystem
{
private static IFileSystem? _fileSystem;
public static IFileSystem Instance => _fileSystem ??= new FileSystem();
/// <inheritdoc />
public bool FileExists(string path)
{
return File.Exists(path);
}
/// <inheritdoc />
public bool DirectoryExists(string dirPath)
{
return Directory.Exists(dirPath);
}
/// <inheritdoc />
public void CreateDirectory(string dirPath)
{
Directory.CreateDirectory(dirPath);
}
/// <inheritdoc />
public string ReadAllText(string filePath)
{
return File.ReadAllText(filePath);
}
/// <inheritdoc />
public void WriteAllText(string filePath, string content)
{
File.WriteAllText(filePath, content);
}
/// <inheritdoc />
public string[] ReadAllLines(string filePath)
{
return File.ReadAllLines(filePath);
}
/// <inheritdoc />
public void WriteAllLines(string filePath, string[] content)
{
File.WriteAllLines(filePath, content);
}
/// <inheritdoc />
public Stream OpenFileStream(string path, FileMode mode, FileAccess access, FileShare share)
{
return new FileStream(path, mode, access, share);
}
/// <inheritdoc />
public IEnumerable<string> EnumerateDirectories(string path, string searchPattern, SearchOption searchOption)
{
return Directory.EnumerateDirectories(path, searchPattern, searchOption);
}
/// <inheritdoc />
public IEnumerable<string> EnumerateFiles(string path, string searchPattern, SearchOption searchOption)
{
return Directory.EnumerateFiles(path, searchPattern, searchOption);
}
/// <inheritdoc />
public void DeleteFile(string filePath)
{
File.Delete(filePath);
}
/// <inheritdoc />
public void CopyFile(string sourcePath, string destinationPath, bool overwrite)
{
File.Copy(sourcePath, destinationPath, overwrite);
}
/// <inheritdoc />
public string GetTempPath()
{
return Path.GetTempPath();
}
/// <inheritdoc />
public DateTime GetLastWriteTime(string filePath)
{
return File.GetLastWriteTime(filePath);
}
/// <inheritdoc />
public string? GetFileVersion(string filePath)
{
return FileVersionInfo.GetVersionInfo(filePath)?.FileVersion;
}
/// <inheritdoc />
public Version? GetAssemblyVersion(string filePath)
{
return AssemblyName.GetAssemblyName(filePath)?.Version;
}
public void MakeFileWritable(string path)
{
Debug.Assert(File.Exists(path));
FileAttributes attributes = File.GetAttributes(path);
if (attributes.HasFlag(FileAttributes.ReadOnly))
{
File.SetAttributes(path, attributes & ~FileAttributes.ReadOnly);
}
}
public async Task AddFileAsync(string outputPath, Stream sourceStream)
{
using (var writeStream = new FileStream(outputPath, FileMode.Create, FileAccess.Write))
{
await sourceStream.CopyToAsync(writeStream);
}
}
}
}

Просмотреть файл

@ -0,0 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Microsoft.DotNet.Scaffolding.Helpers.Services;
public interface IAppSettings
{
IDictionary<string, string> GlobalProperties { get; }
object? GetSettings(string sectionName);
void AddSettings(string sectionName, object settings);
}

Просмотреть файл

@ -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.
using Microsoft.CodeAnalysis;
namespace Microsoft.DotNet.Scaffolding.Helpers.Services;
public interface ICodeService
{
/// <summary>
/// Returns current Roslyn workspace object.
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<Workspace?> GetWorkspaceAsync();
/// <summary>
/// Attempts to apply changes to current Roslyn workspace and returns true if it was successful.
/// </summary>
/// <param name="solution"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
bool TryApplyChanges(Solution? solution);
/// <summary>
/// Reloads Roslyn workspace.
/// </summary>
/// <param name="projectPath"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task ReloadWorkspaceAsync(string? projectPath);
Task <IList<ISymbol>> GetAllClassSymbolsAsync();
Task<IList<Document>> GetAllDocumentsAsync();
Task<Document?> GetDocumentAsync(string? documentName);
}

Просмотреть файл

@ -0,0 +1,13 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.DotNet.Scaffolding.ComponentModel;
namespace Microsoft.DotNet.Scaffolding.Helpers.Services;
public interface IDotNetToolService
{
IList<DotNetToolInfo> GlobalDotNetTools { get; }
IList<KeyValuePair<string, CommandInfo>> GetAllCommandsParallel(IList<DotNetToolInfo>? components = null);
DotNetToolInfo? GetDotNetTool(string? componentName, string? version = null);
List<CommandInfo> GetCommands(string dotnetToolName);
}

Просмотреть файл

@ -0,0 +1,47 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
namespace Microsoft.DotNet.Scaffolding.Helpers.Services;
///<summary>
/// A wrapper interface to be used for all file system related operations for easy unit testing.
/// Any component that does some file system operations should only talk to this interface but not directly
/// to System.IO implementations. Unit tests can then provide a mock implementation of
/// this interface for testing that component.
///</summary>
public interface IFileSystem
{
bool FileExists(string path);
bool DirectoryExists(string dirPath);
void CreateDirectory(string dirPath);
string ReadAllText(string filePath);
string[] ReadAllLines(string filePath);
void WriteAllText(string filePath, string content);
void WriteAllLines(string filePath, string[] content);
Stream OpenFileStream(string path, FileMode mode, FileAccess access, FileShare share);
IEnumerable<string> EnumerateDirectories(string path, string searchPattern, SearchOption searchOption);
IEnumerable<string> EnumerateFiles(string path, string searchPattern, SearchOption searchOption);
void DeleteFile(string filePath);
void CopyFile(string sourcePath, string destinationPath, bool overwrite);
string GetTempPath();
DateTime GetLastWriteTime(string filePath);
string? GetFileVersion(string filePath);
Version? GetAssemblyVersion(string filePath);
void MakeFileWritable(string filePath);
Task AddFileAsync(string filePath, Stream fileStream);
}

Просмотреть файл

@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
namespace Microsoft.DotNet.Scaffolding.Helpers.Services;
/// <summary>
/// Abstracts out host properties. Host represents a tool running a the moment: VS, CLI etc.
/// </summary>
public interface IHostService
{
/// <summary>
/// Returns host installation directory.
/// </summary>
/// <returns></returns>
string GetInstallationPath();
/// <summary>
/// Returns host specific environment variables to be set in current process.
/// </summary>
/// <returns></returns>
ValueTask<IDictionary<string, string>> GetEnvironmentVariablesAsync();
}

Просмотреть файл

@ -0,0 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Build.Evaluation;
namespace Microsoft.DotNet.Scaffolding.Helpers.Services
{
public interface IProjectService
{
Project Project { get; }
}
}

Просмотреть файл

@ -0,0 +1,102 @@
// 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.Text;
namespace Microsoft.DotNet.Scaffolding.Helpers.Services
{
public class ConsoleLogger : ILogger
{
private readonly bool _jsonOutput;
private readonly bool _silent;
private static bool isTrace = !string.IsNullOrEmpty(System.Environment.GetEnvironmentVariable("codegen_trace"));
public bool IsTracing => isTrace;
private string CommandName { get; }
public ConsoleLogger(string? commandName = null, bool jsonOutput = false, bool silent = false)
{
CommandName = commandName ?? string.Empty;
_jsonOutput = jsonOutput;
_silent = silent;
Console.OutputEncoding = Encoding.UTF8;
}
public void LogMessage(string? message, LogMessageType level, bool removeNewLine = false)
{
//if json output is enabled, don't write to console at all.
if (!_silent && !_jsonOutput)
{
switch (level)
{
case LogMessageType.Error:
if (removeNewLine)
{
Console.Error.Write(message);
}
else
{
Console.Error.WriteLine(message);
}
break;
case LogMessageType.Information:
if (removeNewLine)
{
Console.Write(message);
}
else
{
Console.WriteLine(message);
}
break;
}
}
}
public void LogJsonMessage(string? state = null, object? content = null, string? output = null)
{
if (!_silent)
{
if (_jsonOutput)
{
var jsonMessage = new JsonResponse(CommandName, state, content, output);
Console.WriteLine(jsonMessage.ToJsonString());
}
else
{
if (state == State.Fail)
{
LogMessage(output, LogMessageType.Error);
}
else
{
LogMessage(output);
}
}
}
}
public void LogMessage(string? message, bool removeNewLine = false)
{
if (!_silent && !_jsonOutput)
{
LogMessage(message, LogMessageType.Information, removeNewLine);
}
}
public void LogFailureAndExit(string failureMessage)
{
if (_jsonOutput)
{
LogJsonMessage(State.Fail, output: failureMessage);
}
else
{
LogMessage(failureMessage, LogMessageType.Error);
}
System.Environment.Exit(1);
}
}
}

Просмотреть файл

@ -0,0 +1,20 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Microsoft.DotNet.Scaffolding.Helpers.Services
{
public interface ILogger
{
void LogMessage(string message, LogMessageType level, bool removeNewLine = false);
void LogMessage(string message, bool removeNewLine = false);
void LogJsonMessage(string? state = null, object? content = null, string? output = null);
void LogFailureAndExit(string failureMessage);
}
public enum LogMessageType
{
Error,
Information,
Trace
}
}

Просмотреть файл

@ -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.
using System.Text.Json;
namespace Microsoft.DotNet.Scaffolding.Helpers.Services
{
public class JsonResponse
{
public string Command { get; }
public string? State { get; set; }
public object? Content { get; set; }
public string? Output { get; set; }
public JsonResponse(string command, string? state = null, object? content = null, string? output = null)
{
Command = command;
State = state;
Content = content;
Output = output;
}
public string ToJsonString()
{
return JsonSerializer.Serialize(this);
}
}
public class State
{
public const string Success = nameof(Success);
public const string Processing = nameof(Processing);
public const string Fail = nameof(Fail);
}
}

Просмотреть файл

@ -0,0 +1,100 @@
using System.Reflection;
using System.Runtime.Loader;
using Microsoft.Build.Locator;
namespace Microsoft.DotNet.Scaffolding.Helpers.Services;
public class MsBuildInitializer
{
private readonly ILogger _logger;
public MsBuildInitializer(ILogger logger)
{
_logger = logger;
}
public void Initialize()
{
RegisterMsbuild();
}
/// <summary>
/// use MSBuildLocator to find MSBuild instances, return the first (highest version found).
/// </summary>
/// <returns></returns>
private string RegisterMsbuild()
{
var msbuildPath = FindMSBuildPath();
if (string.IsNullOrEmpty(msbuildPath))
{
_logger.LogMessage($"No msbuild path found!", LogMessageType.Error);
return string.Empty;
}
var instance = MSBuildLocator.QueryVisualStudioInstances()
.FirstOrDefault(i => string.Equals(msbuildPath, i.MSBuildPath, StringComparison.OrdinalIgnoreCase));
if (instance is null)
{
_logger.LogMessage($"No msbuild find out at path '{msbuildPath}'!", LogMessageType.Error);
return string.Empty;
}
if (!MSBuildLocator.IsRegistered)
{
// Must register instance rather than just path so everything gets set correctly for .NET SDK instances
MSBuildLocator.RegisterInstance(instance);
}
var resolver = new AssemblyDependencyResolver(msbuildPath);
AssemblyLoadContext.Default.Resolving += ResolveAssembly;
var version = instance.Version.ToString();
return version;
Assembly? ResolveAssembly(AssemblyLoadContext context, AssemblyName assemblyName)
{
if (context is null || assemblyName is null)
{
return null;
}
if (resolver.ResolveAssemblyToPath(assemblyName) is string path)
{
return context.LoadFromAssemblyPath(path);
}
return null;
}
}
private string? FindMSBuildPath()
{
var msBuildInstances = FilterForBitness(MSBuildLocator.QueryVisualStudioInstances())
.OrderByDescending(m => m.Version)
.ToList();
if (msBuildInstances.Count == 0)
{
return string.Empty;
}
else
{
return msBuildInstances.FirstOrDefault()?.MSBuildPath;
}
}
private IEnumerable<VisualStudioInstance> FilterForBitness(IEnumerable<VisualStudioInstance> instances)
{
foreach (var instance in instances)
{
var is32bit = instance.MSBuildPath.Contains("x86", StringComparison.OrdinalIgnoreCase);
if (System.Environment.Is64BitProcess == !is32bit)
{
yield return instance;
}
}
}
}

Просмотреть файл

@ -0,0 +1,32 @@
using Microsoft.Build.Evaluation;
namespace Microsoft.DotNet.Scaffolding.Helpers.Services;
public class ProjectService : IProjectService, IDisposable
{
private readonly string _projectPath;
private readonly ILogger _logger;
private readonly bool _shouldUnload;
public ProjectService(string projectPath, ILogger logger)
{
_projectPath = projectPath ?? string.Empty;
_logger = logger;
var projects = ProjectCollection.GlobalProjectCollection.GetLoadedProjects(_projectPath);
_shouldUnload = projects.Count == 0;
Project = projects.SingleOrDefault() ?? ProjectCollection.GlobalProjectCollection.LoadProject(_projectPath);
Project.Build();
}
public Project Project { get; }
public void Dispose()
{
if (_shouldUnload && Project is not null)
{
// Unload it again if we were the ones who loaded it.
// Unload both, project and project root element, to remove it from strong and weak MSBuild caches.
ProjectCollection.GlobalProjectCollection.UnloadProject(Project);
ProjectCollection.GlobalProjectCollection.UnloadProject(Project.Xml);
}
}
}

Просмотреть файл

@ -0,0 +1,49 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using Microsoft.DotNet.Scaffolding.Helpers.Extensions;
namespace Microsoft.DotNet.Scaffolding.Helpers.Services;
/// <summary>
/// Workspace settings are initialized in following order:
///
/// - by default we get values from the environment variables
/// - then when command line arguments are passed these settings could be overridden
/// - then when we discover any environment variables to be set/modified during IStartup executions,
/// we could set those settings if they were not provided before by earlier steps.
///
/// This way we let shell environment variables override what we could potentially pick for msbuild or VS paths,
/// then user could explicitly specify those paths, and only then if nothing happened we would set them ourselves
/// to the value we think is the best on current machine.
/// </summary>
public class WorkspaceSettings
{
public WorkspaceSettings()
{
MSBuildPath = System.Environment.GetEnvironmentVariable("MSBuildExtensionsPath");
VisualStudioPath = System.Environment.GetEnvironmentVariable("VSINSTALLDIR");
VisualStudioVersion = System.Environment.GetEnvironmentVariable("VisualStudioVersion");
}
private string? _inputPath;
public string? InputPath
{
get => _inputPath;
set => _inputPath = value?.WithOsPathSeparators();
}
private string? _msbuildPath;
public string? MSBuildPath
{
get => _msbuildPath;
set => _msbuildPath = value?.WithOsPathSeparators();
}
private string? _visualStudioPath;
public string? VisualStudioPath
{
get => _visualStudioPath;
set => _visualStudioPath = value?.WithOsPathSeparators();
}
public string? VisualStudioVersion { get; set; }
}

Просмотреть файл

@ -0,0 +1,12 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Microsoft.DotNet.Scaffolding.Helpers.T4Templating;
/// <summary>
/// switch out the CallContext.LogicalGetData call in the generated .cs template with this
/// </summary>
public sealed class CallContext
{
public static object? LogicalGetData(string name)
=> null;
}

Просмотреть файл

@ -0,0 +1,22 @@
namespace Microsoft.DotNet.Scaffolding.Helpers.T4Templating;
/// <summary>
/// Services for invoking T4 text templates for code generation.
/// </summary>
public interface ITemplateInvoker
{
/// <summary>
/// Invokes a T4 text template and returns the result.
/// </summary>
/// <param name="template">ITextTransformation template object.</param>
/// <param name="templateParameters">Parameters for template execution.
/// These parameters can be accessed in text template using a parameter directive.
/// The values passed in must be either serializable or
/// extend <see cref="System.MarshalByRefObject"/> type.</param>
/// <returns>Generated code if there were no processing errors. Throws
/// <see cref="System.InvalidOperationException" /> otherwise.
/// </returns>
string InvokeTemplate(
ITextTransformation template,
IDictionary<string, object> templateParameters);
}

Просмотреть файл

@ -0,0 +1,25 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.CodeDom.Compiler;
namespace Microsoft.DotNet.Scaffolding.Helpers.T4Templating;
/// <summary>
/// This is currently an internal API that supports scaffolding. Use with caution.
/// </summary>
public interface ITextTransformation
{
/// <summary>
/// Session object that can be used to transmit information into a template.
/// </summary>
IDictionary<string, object> Session { get; set; }
/// <summary>
/// Errors received after performing the text transformation.
/// </summary>
CompilerErrorCollection Errors { get; }
void Initialize();
string TransformText();
}

Просмотреть файл

@ -0,0 +1,67 @@
using System.CodeDom.Compiler;
using Microsoft.DotNet.Scaffolding.Helpers.Services;
namespace Microsoft.DotNet.Scaffolding.Helpers.T4Templating;
/// <summary>
/// Contains useful helper functions for running visual studio text transformation.
/// For internal microsoft use only. Use <see cref="ITemplateInvoker"/>
/// in custom code generators.
/// </summary>
public class TemplateInvoker : ITemplateInvoker
{
private readonly ConsoleLogger _consoleLogger;
/// <summary>
/// Constructor.
/// </summary>
public TemplateInvoker(ConsoleLogger? consoleLogger = null)
{
_consoleLogger = consoleLogger ?? new ConsoleLogger();
}
/// <summary>
/// Executes a code generator template to generate the code.
/// </summary>
/// <param name="template">ITextTransformation template object</param>
/// <param name="templateParameters">Parameters for the template.
/// These parameters can be accessed in text template using a parameter directive.
/// The values passed in must be either serializable or
/// extend <see cref="MarshalByRefObject"/> type.</param>
/// <returns>Generated code if there were no processing errors. Throws
/// <see cref="InvalidOperationException" /> otherwise.
/// </returns>
public string InvokeTemplate(ITextTransformation template, IDictionary<string, object> templateParameters)
{
if (template is null)
{
return string.Empty;
}
if (templateParameters is not null && templateParameters.Any())
{
foreach (var param in templateParameters)
{
template.Session.Add(param.Key, param.Value);
}
}
template.Initialize();
return ProcessTemplate(template);
}
private string ProcessTemplate(ITextTransformation transformation)
{
var output = transformation.TransformText();
if (transformation.Errors.HasErrors)
{
foreach (CompilerError error in transformation.Errors)
{
_consoleLogger.LogMessage(error.ErrorText, LogMessageType.Error);
}
throw new InvalidOperationException($"Processing '{transformation.GetType().Name}' failed");
}
return output;
}
}

Просмотреть файл

@ -0,0 +1,148 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.CodeDom.Compiler;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text;
using Microsoft.Extensions.DependencyModel;
using Microsoft.VisualStudio.TextTemplating;
namespace Microsoft.EntityFrameworkCore.Scaffolding.Internal;
/// <summary>
/// This is an internal API that supports the T4 Templating infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new scaffolding release.
/// Referencing from dotnet/efcore/src/EFCore.Design/Scaffolding/Internal/TextTemplatingEngineHost.cs
/// </summary>
public class TextTemplatingEngineHost : ITextTemplatingSessionHost, ITextTemplatingEngineHost, IServiceProvider
{
private static readonly List<string> _noWarn = ["CS1701", "CS1702"];
private readonly IServiceProvider? _serviceProvider;
private ITextTemplatingSession? _session;
private CompilerErrorCollection? _errors;
private string? _extension;
private Encoding? _outputEncoding;
private bool _fromOutputDirective;
public TextTemplatingEngineHost(IServiceProvider? serviceProvider = null)
{
_serviceProvider = serviceProvider;
}
[AllowNull]
public virtual ITextTemplatingSession Session
{
get => _session ??= CreateSession();
set => _session = value;
}
public virtual IList<string> StandardAssemblyReferences { get; } = new[]
{
typeof(ITextTemplatingEngineHost).Assembly.Location, typeof(CompilerErrorCollection).Assembly.Location
};
public virtual IList<string> StandardImports { get; } = new[] { "System" };
public virtual string? TemplateFile { get; set; }
public virtual string Extension
=> _extension ?? ".cs";
public virtual CompilerErrorCollection Errors
=> _errors ??= [];
public virtual Encoding OutputEncoding
=> _outputEncoding ?? Encoding.UTF8;
public virtual void Initialize()
{
_session?.Clear();
_errors = null;
_extension = null;
_outputEncoding = null;
_fromOutputDirective = false;
}
public virtual ITextTemplatingSession CreateSession()
=> new TextTemplatingSession();
public virtual object? GetHostOption(string optionName)
=> null;
public virtual bool LoadIncludeText(string requestFileName, out string content, out string location)
{
location = ResolvePath(requestFileName);
var exists = File.Exists(location);
content = exists
? File.ReadAllText(location)
: string.Empty;
return exists;
}
public virtual void LogErrors(CompilerErrorCollection errors)
=> Errors.AddRange(errors.Cast<CompilerError>().Where(e => !_noWarn.Contains(e.ErrorNumber)).ToArray());
public virtual AppDomain ProvideTemplatingAppDomain(string content)
=> AppDomain.CurrentDomain;
public virtual string ResolveAssemblyReference(string assemblyReference)
{
if (Path.IsPathRooted(assemblyReference))
{
return assemblyReference;
}
var path = DependencyContext.Default?.CompileLibraries
.FirstOrDefault(l => l.Assemblies.Any(a => Path.GetFileNameWithoutExtension(a) == assemblyReference))
?.ResolveReferencePaths()
.First(p => Path.GetFileNameWithoutExtension(p) == assemblyReference);
if (path is not null)
{
return path;
}
try
{
return Assembly.Load(assemblyReference).Location;
}
catch
{
}
// TODO: Expand variables?
return assemblyReference;
}
public virtual Type ResolveDirectiveProcessor(string processorName)
=> throw new FileNotFoundException($"Failed to resolve type for directive processor {processorName}");
public virtual string ResolveParameterValue(string directiveId, string processorName, string parameterName)
=> string.Empty;
public virtual string ResolvePath(string path)
=> !Path.IsPathRooted(path) && Path.IsPathRooted(TemplateFile)
? Path.Combine(Path.GetDirectoryName(TemplateFile)!, path)
: path;
public virtual void SetFileExtension(string extension)
=> _extension = extension;
public virtual void SetOutputEncoding(Encoding encoding, bool fromOutputDirective)
{
if (_fromOutputDirective)
{
return;
}
_outputEncoding = encoding;
_fromOutputDirective = fromOutputDirective;
}
public virtual object? GetService(Type serviceType)
=> _serviceProvider?.GetService(serviceType);
}

Просмотреть файл

@ -0,0 +1,108 @@
using System.Collections.Generic;
using System.Linq;
using Xunit;
namespace Microsoft.DotNet.Scaffolding.ComponentModel.Tests
{
public class CommandInfoTests
{
private static List<CommandInfo> CommandInfos;
private static Parameter ValidParameter;
private static Parameter PartialParameter;
private static Parameter PartialParameter2;
private static CommandInfo ValidCommandInfo;
private static CommandInfo PartialCommandInfo;
private static CommandInfo PartialCommandInfo2;
public CommandInfoTests()
{
ValidParameter = new()
{
Name = "parameter",
DisplayName = "Param Display Name",
Required = true,
Description = "Parameter description",
Type = BaseTypes.String,
PickerType = InteractivePickerType.DbProviderPicker
};
PartialParameter = new()
{
Name = string.Empty,
DisplayName = "Param Display Name 2",
Required = true,
Type = BaseTypes.Int
};
PartialParameter2 = new()
{
Name = "parameter2",
DisplayName = "Param Display Name 3",
Required = true,
Type = BaseTypes.Int,
Description = null,
PickerType = null
};
ValidCommandInfo = new()
{
Name = "command1",
DisplayName = "Command 1",
Description = "Description 1",
Parameters = [ValidParameter]
};
PartialCommandInfo = new()
{
Name = "command2",
DisplayName = "Command 2",
Parameters =
[
ValidParameter, PartialParameter
]
};
PartialCommandInfo2 = new()
{
Name = "command3",
DisplayName = "Command 3",
Description = null,
Parameters =
[
ValidParameter, PartialParameter, PartialParameter2
]
};
CommandInfos =
[
ValidCommandInfo, PartialCommandInfo, PartialCommandInfo2
];
}
[Fact]
public void SerialzationAndDeserializationTests()
{
var serializedJson = System.Text.Json.JsonSerializer.Serialize(CommandInfos);
Assert.False(string.IsNullOrEmpty(serializedJson));
Assert.True(serializedJson.Contains("\"Name\":\"command1\"", System.StringComparison.OrdinalIgnoreCase));
Assert.True(serializedJson.Contains("\"Name\":\"command2\"", System.StringComparison.OrdinalIgnoreCase));
Assert.True(serializedJson.Contains("\"Name\":\"command3\"", System.StringComparison.OrdinalIgnoreCase));
var deserializedJson = System.Text.Json.JsonSerializer.Deserialize<List<CommandInfo>>(serializedJson);
Assert.NotNull(deserializedJson);
Assert.True(deserializedJson.Count == 3);
var validCommandInfoDeserialized = deserializedJson[0];
var partialCommandInfoDeserialized = deserializedJson[1];
var partialCommandInfo2Deserialized = deserializedJson[2];
Assert.NotNull(validCommandInfoDeserialized);
Assert.NotNull(partialCommandInfoDeserialized);
Assert.NotNull(partialCommandInfo2Deserialized);
Assert.Contains(validCommandInfoDeserialized.Parameters, x => x.Name.Equals("parameter"));
Assert.Contains(partialCommandInfoDeserialized.Parameters, x => x.Name.Equals(PartialParameter.Name));
Assert.Contains(partialCommandInfo2Deserialized.Parameters, x => x.Description is null);
Assert.Contains(partialCommandInfo2Deserialized.Parameters, x => x.PickerType is null);
}
}
}

Просмотреть файл

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(StandardTestTfms)</TargetFramework>
<IsPackable>false</IsPackable>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="$(RepoRoot)\src\Shared\Microsoft.DotNet.Scaffolding.ComponentModel\Microsoft.DotNet.Scaffolding.ComponentModel.csproj" />
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,22 @@
using System;
using System.Threading.Tasks;
namespace Microsoft.DotNet.Tools.Scaffold;
public class ScaffoldCommandApp
{
private readonly string[] _args;
private readonly Spectre.Console.Cli.CommandApp<ScaffoldCommand> _commandApp;
public ScaffoldCommandApp(Spectre.Console.Cli.CommandApp<ScaffoldCommand> commandApp, string[] args)
{
_commandApp = commandApp;
_args = args;
}
public Task<int> RunAsync()
{
return _commandApp.RunAsync(_args);
}
}

Просмотреть файл

@ -0,0 +1,54 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System.Reflection;
using Microsoft.DotNet.Scaffolding.Helpers.Environment;
using Microsoft.DotNet.Scaffolding.Helpers.Services;
using Microsoft.DotNet.Scaffolding.Helpers.Services.Environment;
using Microsoft.DotNet.Tools.Scaffold.AppBuilder;
using Microsoft.DotNet.Tools.Scaffold.Services;
using Spectre.Console.Cli;
namespace Microsoft.DotNet.Tools.Scaffold;
public class ScaffoldCommandAppBuilder(string[] args)
{
private readonly string[] _args = args;
//try to update this every release
private readonly string _backupDotNetScaffoldVersion = "0.1.0-dev";
public ScaffoldCommandApp Build()
{
var serviceRegistrations = GetDefaultServices();
var commandApp = new CommandApp<ScaffoldCommand>(serviceRegistrations);
commandApp.Configure(config =>
{
config
.SetApplicationName("dotnet-scaffold")
.SetApplicationVersion(GetToolVersion());
});
return new ScaffoldCommandApp(commandApp, _args);
}
private ITypeRegistrar? GetDefaultServices()
{
var registrar = new TypeRegistrar();
registrar.Register(typeof(IFileSystem), typeof(FileSystem));
registrar.Register(typeof(IEnvironmentService), typeof(EnvironmentService));
registrar.Register(typeof(IFlowProvider), typeof(FlowProvider));
registrar.Register(typeof(IDotNetToolService), typeof(DotNetToolService));
registrar.Register(typeof(ILogger), typeof(AnsiConsoleLogger));
registrar.Register(typeof(IAppSettings), typeof(AppSettings));
registrar.Register(typeof(IEnvironmentVariableProvider), typeof(MacMsbuildEnvironmentVariableProvider));
registrar.Register(typeof(IEnvironmentVariableProvider), typeof(WindowsEnvironmentVariableProvider));
registrar.Register(typeof(IHostService), typeof(HostService));
return registrar;
}
private string GetToolVersion()
{
var assembly = Assembly.GetExecutingAssembly();
var assemblyAttr = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>();
return assemblyAttr?.InformationalVersion ?? _backupDotNetScaffoldVersion;
}
}

Просмотреть файл

@ -0,0 +1,43 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using Microsoft.DotNet.Tools.Scaffold.AspNet.AppBuilder;
using Microsoft.Extensions.DependencyInjection;
using Spectre.Console.Cli;
namespace Microsoft.DotNet.Tools.Scaffold.AppBuilder;
internal sealed class TypeRegistrar : ITypeRegistrar
{
private readonly IServiceCollection _services;
public TypeRegistrar()
{
_services = new ServiceCollection();
}
public ITypeResolver Build()
{
return new TypeResolver(_services.BuildServiceProvider());
}
public void Register(Type service, Type implementation)
{
_services.AddSingleton(service, implementation);
}
public void RegisterInstance(Type service, object implementation)
{
_services.AddSingleton(service, implementation);
}
public void RegisterLazy(Type service, Func<object> func)
{
if (func is null)
{
throw new ArgumentNullException(nameof(func));
}
_services.AddSingleton(service, (provider) => func());
}
}

Просмотреть файл

@ -0,0 +1,34 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using Spectre.Console.Cli;
namespace Microsoft.DotNet.Tools.Scaffold.AspNet.AppBuilder;
internal sealed class TypeResolver : ITypeResolver, IDisposable
{
private readonly IServiceProvider _provider;
public TypeResolver(IServiceProvider serviceProvider)
{
_provider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
}
public object? Resolve(Type? type)
{
if (type is null)
{
return null;
}
return _provider.GetService(type);
}
public void Dispose()
{
if (_provider is IDisposable disposable)
{
disposable.Dispose();
}
}
}

Просмотреть файл

@ -0,0 +1,46 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.Tools.Scaffold.Flow;
using Microsoft.DotNet.Tools.Scaffold.Services;
using Spectre.Console.Cli;
using Spectre.Console.Flow;
namespace Microsoft.UpgradeAssistant.Cli.Commands;
public abstract class BaseCommand<TSettings> : AsyncCommand<TSettings>
where TSettings : CommandSettings
{
protected BaseCommand(IFlowProvider flowProvider)
{
FlowProvider = flowProvider;
}
protected IFlowProvider FlowProvider { get; }
protected async ValueTask<int> RunFlowAsync(IEnumerable<IFlowStep> flowSteps, TSettings settings, IRemainingArguments remainingArgs, bool nonInteractive = false, bool showSelectedOptions = true)
{
var properties = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
{
{ FlowContextProperties.RemainingArgs, remainingArgs },
{ FlowContextProperties.CommandSettings, settings }
};
IFlow? flow = null;
Exception? exception = null;
try
{
flow = FlowProvider.GetFlow(flowSteps, properties, nonInteractive, showSelectedOptions);
return await flow.RunAsync(CancellationToken.None);
}
catch (Exception) {}
return exception is not null
? throw exception
: int.MinValue;
}
}

Просмотреть файл

@ -0,0 +1,68 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.DotNet.Scaffolding.Helpers.Services;
using Microsoft.DotNet.Scaffolding.Helpers.Services.Environment;
using Microsoft.DotNet.Tools.Scaffold.Flow.Steps;
using Microsoft.DotNet.Tools.Scaffold.Flow.Steps.Project;
using Microsoft.DotNet.Tools.Scaffold.Services;
using Microsoft.UpgradeAssistant.Cli.Commands;
using Spectre.Console.Cli;
using Spectre.Console.Flow;
public class ScaffoldCommand : BaseCommand<ScaffoldCommand.Settings>
{
private readonly IAppSettings _appSettings;
private readonly IDotNetToolService _dotnetToolService;
private readonly IFileSystem _fileSystem;
private readonly ILogger _logger;
private readonly IEnvironmentService _environmentService;
private readonly IHostService _hostService;
public ScaffoldCommand(
IAppSettings appSettings,
IDotNetToolService dotnetToolService,
IEnvironmentService environmentService,
IFileSystem fileSystem,
IFlowProvider flowProvider,
IHostService hostService,
ILogger logger)
: base(flowProvider)
{
_appSettings = appSettings;
_dotnetToolService = dotnetToolService;
_environmentService = environmentService;
_fileSystem = fileSystem;
_hostService = hostService;
_logger = logger;
}
public class Settings : CommandSettings
{
[CommandArgument(0, "[COMPONENT]")]
public string? ComponentName { get; set; }
[CommandArgument(1, "[COMMAND NAME]")]
public string? CommandName { get; set; }
[CommandOption("--project")]
public string? Project { get; init; }
[CommandOption("--non-interactive")]
public bool NonInteractive { get; init; }
}
public override async Task<int> ExecuteAsync(CommandContext context, Settings settings)
{
IEnumerable<IFlowStep> flowSteps =
[
new StartupFlowStep(_appSettings, _dotnetToolService, _environmentService, _fileSystem, _hostService, _logger),
new SourceProjectFlowStep(_appSettings, _environmentService, _fileSystem, _logger),
new CommandPickerFlowStep(_logger, _dotnetToolService),
new CommandExecuteFlowStep()
];
return await RunFlowAsync(flowSteps, settings, context.Remaining, settings.NonInteractive);
}
}

Просмотреть файл

@ -0,0 +1,33 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
namespace Microsoft.DotNet.Tools.Scaffold.Flow;
internal static class FlowContextProperties
{
public const string CommandSettings = nameof(CommandSettings);
public const string CommandInfos = nameof(CommandInfos);
public const string Controller = nameof(Controller);
public const string ExcludedProjects = nameof(ExcludedProjects);
public const string OriginalTraits = nameof(OriginalTraits);
public const string ProjectTemplateName = nameof(ProjectTemplateName);
public const string Slice = nameof(Slice);
public const string SourceProjectPath = nameof(SourceProjectPath);
public const string SourceProjectDisplay = "Source project";
public const string CommandArgs = nameof(CommandArgs);
public const string CommandArgValues = nameof(CommandArgValues);
public const string ComponentName = nameof(ComponentName);
public const string ComponentObj = nameof(ComponentObj);
public const string CommandName = nameof(CommandName);
public const string CommandObj = nameof(CommandObj);
public const string ComponentNameDisplay = "Component";
//of type Microsoft.DotNet.Scaffolding.Helpers.Services.IProjectService
public const string SourceProject = nameof(SourceProject);
//of type Microsoft.DotNet.Scaffolding.Helpers.Services.ICodeService
public const string CodeService = nameof(CodeService);
public const string TargetFrameworkName = nameof(TargetFrameworkName);
public const string RemainingArgs = nameof(RemainingArgs);
public const string DotnetToolComponents = nameof(DotnetToolComponents);
}

Просмотреть файл

@ -0,0 +1,80 @@
// 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 Microsoft.DotNet.Scaffolding.ComponentModel;
using Microsoft.DotNet.Scaffolding.Helpers.Services;
using Spectre.Console;
using Spectre.Console.Cli;
using Spectre.Console.Flow;
namespace Microsoft.DotNet.Tools.Scaffold.Flow
{
internal static class FlowContextExtensions
{
public static DotNetToolInfo? GetComponentObj(this IFlowContext context, bool throwIfEmpty = false)
{
return context.GetValueOrThrow<DotNetToolInfo>(FlowContextProperties.ComponentObj, throwIfEmpty);
}
public static CommandInfo? GetCommandObj(this IFlowContext context, bool throwIfEmpty = false)
{
return context.GetValueOrThrow<CommandInfo>(FlowContextProperties.CommandObj, throwIfEmpty);
}
public static List<string>? GetCommandArgValues(this IFlowContext context, bool throwIfEmpty = false)
{
return context.GetValueOrThrow<List<string>>(FlowContextProperties.CommandArgValues, throwIfEmpty);
}
public static IList<KeyValuePair<string, CommandInfo>>? GetCommandInfos(this IFlowContext context, bool throwIfEmpty = false)
{
return context.GetValueOrThrow<IList<KeyValuePair<string, CommandInfo>>>(FlowContextProperties.CommandInfos, throwIfEmpty);
}
public static string? GetSourceProjectPath(this IFlowContext context, bool throwIfEmpty = false)
{
return context.GetValueOrThrow<string>(FlowContextProperties.SourceProjectPath, throwIfEmpty);
}
public static ICodeService? GetCodeService(this IFlowContext context, bool throwIfEmpty = false)
{
return context.GetValueOrThrow<ICodeService>(FlowContextProperties.CodeService, throwIfEmpty);
}
public static ScaffoldCommand.Settings? GetCommandSettings(this IFlowContext context, bool throwIfEmpty = false)
{
return context.GetValueOrThrow<ScaffoldCommand.Settings>(FlowContextProperties.CommandSettings, throwIfEmpty);
}
public static IRemainingArguments? GetRemainingArgs(this IFlowContext context, bool throwIfEmpty = false)
{
return context.GetValueOrThrow<IRemainingArguments>(FlowContextProperties.RemainingArgs, throwIfEmpty);
}
public static IDictionary<string, List<string>>? GetArgsDict(this IFlowContext context, bool throwIfEmpty = false)
{
return context.GetValueOrThrow<IDictionary<string, List<string>>>(FlowContextProperties.CommandArgs, throwIfEmpty);
}
public static Status WithSpinner(this Status status)
{
return status
.AutoRefresh(true)
.Spinner(Spinner.Known.Aesthetic)
.SpinnerStyle(Styles.Highlight);
}
private static T? GetValueOrThrow<T>(this IFlowContext context, string propertyName, bool throwIfEmpty = false)
{
var value = context.GetValue<T>(propertyName);
if (throwIfEmpty && value is null)
{
throw new ArgumentNullException(propertyName);
}
return value;
}
}
}

Просмотреть файл

@ -0,0 +1,70 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Generic;
using System.Linq;
using Microsoft.DotNet.Scaffolding.ComponentModel;
using Microsoft.DotNet.Scaffolding.Helpers.Services;
using Spectre.Console;
using Spectre.Console.Flow;
namespace Microsoft.DotNet.Tools.Scaffold.Flow.Steps
{
internal class CommandDiscovery
{
private readonly IDotNetToolService _dotnetToolService;
private readonly DotNetToolInfo? _componentPicked;
public CommandDiscovery(IDotNetToolService dotnetToolService, DotNetToolInfo? componentPicked)
{
_dotnetToolService = dotnetToolService;
_componentPicked = componentPicked;
}
public FlowStepState State { get; private set; }
public KeyValuePair<string, CommandInfo>? Discover(IFlowContext context)
{
var allCommands = context.GetCommandInfos();
if (allCommands is null || allCommands.Count == 0)
{
allCommands = AnsiConsole
.Status()
.WithSpinner()
.Start("Gathering scaffolding commands!", statusContext =>
{
if (_componentPicked != null)
{
return _dotnetToolService.GetCommands(_componentPicked.Command)?.Select(x => KeyValuePair.Create(_componentPicked.Command, x))?.ToList();
}
return _dotnetToolService.GetAllCommandsParallel();
});
if (allCommands is not null)
{
context.Set(FlowContextProperties.CommandInfos, allCommands);
}
}
return Prompt(context);
}
private KeyValuePair<string, CommandInfo>? Prompt(IFlowContext context)
{
var allCommands = context.GetCommandInfos();
var prompt = new FlowSelectionPrompt<KeyValuePair<string, CommandInfo>>()
.Title("[lightseagreen]Pick a scaffolding command: [/]")
.Converter(GetCommandInfoDisplayName)
.AddChoices(allCommands, navigation: context.Navigation);
var result = prompt.Show();
State = result.State;
return result.Value;
}
private string GetCommandInfoDisplayName(KeyValuePair<string, CommandInfo> commandInfo)
{
return $"{commandInfo.Value.DisplayName} ({commandInfo.Key})";
}
}
}

Просмотреть файл

@ -0,0 +1,99 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.Scaffolding.ComponentModel;
using Microsoft.DotNet.Scaffolding.Helpers.General;
using Spectre.Console;
using Spectre.Console.Flow;
namespace Microsoft.DotNet.Tools.Scaffold.Flow.Steps
{
/// <summary>
/// IFlowStep where we gather all the parameter values, and then execute the chosen/given component (dotnet tool).
/// Print out stdout/stderr
/// </summary>
internal class CommandExecuteFlowStep : IFlowStep
{
public string Id => nameof(CommandExecuteFlowStep);
public string DisplayName => "Command Execute";
public ValueTask ResetAsync(IFlowContext context, CancellationToken cancellationToken)
{
return ValueTask.CompletedTask;
}
public ValueTask<FlowStepResult> RunAsync(IFlowContext context, CancellationToken cancellationToken)
{
return new ValueTask<FlowStepResult>(FlowStepResult.Success);
}
public ValueTask<FlowStepResult> ValidateUserInputAsync(IFlowContext context, CancellationToken cancellationToken)
{
//need all 3 things, throw if not found
var componentName = context.GetComponentObj()?.Command;
var commandObj = context.GetCommandObj();
if (commandObj is null || string.IsNullOrEmpty(componentName))
{
return new ValueTask<FlowStepResult>(FlowStepResult.Failure("Missing value for name of the component and/or command"));
}
var parameterValues = GetAllParameterValues(context, commandObj);
if (!string.IsNullOrEmpty(componentName) && parameterValues.Count != 0 && !string.IsNullOrEmpty(commandObj.Name))
{
var componentExecutionString = $"{componentName} {commandObj.Name} {string.Join(" ", parameterValues)}";
string? stdOut = null, stdErr = null;
int? exitCode = null;
AnsiConsole.Status().WithSpinner()
.Start($"Executing '{componentExecutionString}'", statusContext =>
{
var cliRunner = DotnetCliRunner.Create(componentName, parameterValues);
exitCode = cliRunner.ExecuteAndCaptureOutput(out stdOut, out stdErr);
});
if (exitCode != null && (!string.IsNullOrEmpty(stdOut) || string.IsNullOrEmpty(stdErr)))
{
AnsiConsole.Console.WriteLine($"\nCommand executed : '{componentExecutionString}'");
AnsiConsole.Console.WriteLine($"\nCommand exit code - {exitCode}");
if (exitCode == 0)
{
AnsiConsole.Console.MarkupLine($"\nCommand stdout :\n[lightgreen]{stdOut}[/]");
}
else
{
AnsiConsole.Console.MarkupLine($"\nCommand stderr :\n[lightred]{stdErr}[/]");
}
return new ValueTask<FlowStepResult>(FlowStepResult.Success);
}
}
return new ValueTask<FlowStepResult>(FlowStepResult.Failure());
}
private List<string> GetAllParameterValues(IFlowContext context, CommandInfo commandInfo)
{
var sourceProjectPath = context.GetSourceProjectPath();
var parameterValues = new List<string> { commandInfo.Name };
if (!string.IsNullOrEmpty(sourceProjectPath))
{
parameterValues.Add("--project");
parameterValues.Add(sourceProjectPath);
}
foreach (var parameter in commandInfo.Parameters)
{
var parameterValue = context.GetValue<string>(parameter.Name);
if (!string.IsNullOrEmpty(parameterValue))
{
parameterValues.Add(parameter.Name);
parameterValues.Add(parameterValue);
}
}
return parameterValues;
}
}
}

Просмотреть файл

@ -0,0 +1,192 @@
// 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.DotNet.Scaffolding.ComponentModel;
using Microsoft.DotNet.Scaffolding.Helpers.Services;
using Spectre.Console.Flow;
namespace Microsoft.DotNet.Tools.Scaffold.Flow.Steps
{
/// <summary>
/// IFlowStep that deals with the selection of the component(DotnetToolInfo) and the associated command(CommandInfo).
/// if provided by the user, verify if the component is installed and the command is supported.
/// </summary>
internal class CommandPickerFlowStep : IFlowStep
{
private readonly ILogger _logger;
private readonly IDotNetToolService _dotnetToolService;
public CommandPickerFlowStep(ILogger logger, IDotNetToolService dotnetToolService)
{
_logger = logger;
_dotnetToolService = dotnetToolService;
}
public string Id => nameof(CommandPickerFlowStep);
public string DisplayName => "Command Name";
public ValueTask ResetAsync(IFlowContext context, CancellationToken cancellationToken)
{
context.Unset(FlowContextProperties.ComponentName);
context.Unset(FlowContextProperties.ComponentObj);
context.Unset(FlowContextProperties.CommandName);
context.Unset(FlowContextProperties.CommandObj);
return new ValueTask();
}
public ValueTask<FlowStepResult> RunAsync(IFlowContext context, CancellationToken cancellationToken)
{
var settings = context.GetCommandSettings();
var componentName = settings?.ComponentName;
var commandName = settings?.CommandName;
//KeyValuePair with key being name of the DotnetToolInfo (component) and value being the CommandInfo supported by that component.
KeyValuePair<string, CommandInfo>? commandInfoKvp = null;
CommandInfo? commandInfo = null;
var dotnetToolComponent = _dotnetToolService.GlobalDotNetTools.FirstOrDefault(x => x.Command.Equals(componentName, StringComparison.OrdinalIgnoreCase));
CommandDiscovery commandDiscovery = new(_dotnetToolService, dotnetToolComponent);
commandInfoKvp = commandDiscovery.Discover(context);
if (commandDiscovery.State.IsNavigation())
{
return new ValueTask<FlowStepResult>(new FlowStepResult { State = commandDiscovery.State });
}
if (commandInfoKvp is null || !commandInfoKvp.HasValue || commandInfoKvp.Value.Value is null || string.IsNullOrEmpty(commandInfoKvp.Value.Key))
{
return new ValueTask<FlowStepResult>(FlowStepResult.Failure("Unable to find any commands!"));
}
else
{
commandInfo = commandInfoKvp.Value.Value;
componentName = commandInfoKvp.Value.Key;
dotnetToolComponent ??= _dotnetToolService.GetDotNetTool(componentName);
if (dotnetToolComponent != null)
{
SelectComponent(context, dotnetToolComponent);
}
SelectCommand(context, commandInfo);
}
var commandFirstStep = GetFirstParameterBasedStep(commandInfo);
if (commandFirstStep is null)
{
return new ValueTask<FlowStepResult>(FlowStepResult.Failure($"Failed to get/parse parameters for command '{commandInfo.Name}'"));
}
return new ValueTask<FlowStepResult>(new FlowStepResult { State = FlowStepState.Success, Steps = new List<ParameterBasedFlowStep> { commandFirstStep } });
}
public ValueTask<FlowStepResult> ValidateUserInputAsync(IFlowContext context, CancellationToken cancellationToken)
{
var settings = context.GetCommandSettings();
var componentName = settings?.ComponentName;
var commandName = settings?.CommandName;
CommandInfo? commandInfo = null;
//check if user input included a component name.
//if included, check for a command name, and get the CommandInfo object.
var dotnetToolComponent = _dotnetToolService.GlobalDotNetTools.FirstOrDefault(x => x.Command.Equals(componentName, StringComparison.OrdinalIgnoreCase));
if (dotnetToolComponent != null)
{
var allCommands = _dotnetToolService.GetCommands(dotnetToolComponent.Command);
commandInfo = allCommands.FirstOrDefault(x => x.Name.Equals(commandName, StringComparison.OrdinalIgnoreCase));
}
else
{
return new ValueTask<FlowStepResult>(FlowStepResult.Failure("No component (dotnet tool) provided!"));
}
if (commandInfo is null)
{
return new ValueTask<FlowStepResult>(FlowStepResult.Failure($"Invalid or empty command provided for component '{componentName}'"));
}
var commandFirstStep = GetFirstParameterBasedStep(commandInfo);
if (commandFirstStep is null)
{
return new ValueTask<FlowStepResult>(FlowStepResult.Failure($"Failed to get/parse parameters for command '{commandInfo.Name}'"));
}
SelectComponent(context, dotnetToolComponent);
SelectCommand(context, commandInfo);
return new ValueTask<FlowStepResult>(new FlowStepResult { State = FlowStepState.Success, Steps = new List<ParameterBasedFlowStep> { commandFirstStep } });
}
//Wrapper to get the first ParameterBasedFlowStep. Use 'BuildParameterFlowSteps'
internal ParameterBasedFlowStep? GetFirstParameterBasedStep(CommandInfo commandInfo)
{
ParameterBasedFlowStep? firstParameterStep = null;
if (commandInfo.Parameters != null && commandInfo.Parameters.Length != 0)
{
firstParameterStep = BuildParameterFlowSteps([.. commandInfo.Parameters]);
}
return firstParameterStep;
}
/// <summary>
/// Take all the 'Parameter's, create ParameterBasedFlowSteps with connecting them using 'NextStep'.
/// </summary>
/// <returns>first step from the connected ParameterBasedFlowSteps</returns>
internal ParameterBasedFlowStep? BuildParameterFlowSteps(List<Parameter> parameters)
{
ParameterBasedFlowStep? firstStep = null;
ParameterBasedFlowStep? previousStep = null;
foreach (var parameter in parameters)
{
var step = new ParameterBasedFlowStep(parameter, null);
if (firstStep == null)
{
// This is the first step
firstStep = step;
}
else
{
if (previousStep != null)
{
// Connect the previous step to this step
previousStep.NextStep = step;
}
}
previousStep = step;
}
return firstStep;
}
private void SelectCommand(IFlowContext context, CommandInfo command)
{
context.Set(new FlowProperty(
FlowContextProperties.CommandName,
command.Name,
"Command Name",
isVisible: true));
context.Set(new FlowProperty(
FlowContextProperties.CommandObj,
command,
isVisible: false));
}
private void SelectComponent(IFlowContext context, DotNetToolInfo dotnetToolInfo)
{
context.Set(new FlowProperty(
FlowContextProperties.ComponentName,
dotnetToolInfo.Command,
"Component Name",
isVisible: true));
context.Set(new FlowProperty(
FlowContextProperties.ComponentObj,
dotnetToolInfo,
isVisible: false));
}
}
}

Просмотреть файл

@ -0,0 +1,54 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Generic;
using System.Linq;
using Microsoft.DotNet.Scaffolding.ComponentModel;
using Microsoft.DotNet.Scaffolding.Helpers.Services;
using Spectre.Console.Flow;
namespace Microsoft.DotNet.Tools.Scaffold.Flow.Steps
{
internal class ComponentDiscovery
{
private readonly IDotNetToolService _dotnetToolService;
public ComponentDiscovery(IDotNetToolService dotNetToolService)
{
_dotnetToolService = dotNetToolService;
}
public FlowStepState State { get; private set; }
public DotNetToolInfo? Discover(IFlowContext context)
{
return Prompt(context, "Pick a scaffolding component ('dotnet tool')", _dotnetToolService.GlobalDotNetTools);
}
private DotNetToolInfo? Prompt(IFlowContext context, string title, IList<DotNetToolInfo> components)
{
if (components.Count == 0)
{
return null;
}
if (components.Count == 1)
{
return components[0];
}
var prompt = new FlowSelectionPrompt<DotNetToolInfo>()
.Title(title)
.Converter(GetDotNetToolInfoDisplayString)
.AddChoices(components, navigation: context.Navigation);
var result = prompt.Show();
State = result.State;
return result.Value;
}
internal string GetDotNetToolInfoDisplayString(DotNetToolInfo dotnetToolInfo)
{
return dotnetToolInfo.ToDisplayString();
}
}
}

Просмотреть файл

@ -0,0 +1,111 @@
// 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.Configuration;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.Scaffolding.ComponentModel;
using Spectre.Console.Flow;
namespace Microsoft.DotNet.Tools.Scaffold.Flow.Steps
{
internal class ParameterBasedFlowStep : IFlowStep
{
public Parameter Parameter { get; set; }
public ParameterBasedFlowStep? NextStep { get; set; }
public ParameterBasedFlowStep(Parameter parameter, ParameterBasedFlowStep? nextStep)
{
Parameter = parameter;
NextStep = nextStep;
}
public string Id => nameof(ParameterBasedFlowStep);
public string DisplayName => Parameter.DisplayName;
public ValueTask ResetAsync(IFlowContext context, CancellationToken cancellationToken)
{
context.Unset(Parameter.Name);
return new ValueTask();
}
public async ValueTask<FlowStepResult> RunAsync(IFlowContext context, CancellationToken cancellationToken)
{
ParameterDiscovery paraDiscovery = new ParameterDiscovery(Parameter);
var parameterValue = await paraDiscovery.DiscoverAsync(context);
if (string.Equals(parameterValue, FlowNavigation.BackInputToken, StringComparison.OrdinalIgnoreCase))
{
return FlowStepResult.Back;
}
else if (paraDiscovery.State.IsNavigation())
{
return new FlowStepResult { State = paraDiscovery.State };
}
else
{
SelectParameter(context, parameterValue ?? string.Empty);
}
if (NextStep != null)
{
return new FlowStepResult { State = FlowStepState.Success, Steps = new List<ParameterBasedFlowStep> { NextStep } };
}
else
{
return FlowStepResult.Success;
}
}
public ValueTask<FlowStepResult> ValidateUserInputAsync(IFlowContext context, CancellationToken cancellationToken)
{
//check for parameter in the context using --name
var cmdlineArgs = context.GetArgsDict();
var commandSettings = context.GetCommandSettings();
if (cmdlineArgs != null && cmdlineArgs.Count != 0)
{
var strippedParameterName = Parameter.Name.Replace("--", string.Empty);
cmdlineArgs.TryGetValue(strippedParameterName, out var cmdlineValues);
if (cmdlineValues != null && cmdlineValues.Count != 0 && ParameterHelpers.CheckType(Parameter.Type, cmdlineValues.First()))
{
SelectParameter(context, cmdlineValues.First());
if (NextStep != null)
{
return new ValueTask<FlowStepResult>(new FlowStepResult { State = FlowStepState.Success, Steps = new List<ParameterBasedFlowStep> { NextStep } });
}
else
{
return new ValueTask<FlowStepResult>(FlowStepResult.Success);
}
}
}
if (Parameter.Required || (commandSettings != null && !commandSettings.NonInteractive))
{
return new ValueTask<FlowStepResult>(FlowStepResult.Failure($"No value found for option '{Parameter.Name}'"));
}
else if (NextStep != null)
{
return new ValueTask<FlowStepResult>(new FlowStepResult { State = FlowStepState.Success, Steps = new List<ParameterBasedFlowStep> { NextStep } });
}
else
{
return new ValueTask<FlowStepResult>(FlowStepResult.Success);
}
}
private void SelectParameter(IFlowContext context, string parameterValue)
{
if (!string.IsNullOrEmpty(parameterValue))
{
context.Set(new FlowProperty(
Parameter.Name,
parameterValue,
Parameter.DisplayName,
isVisible: true));
}
}
}
}

Просмотреть файл

@ -0,0 +1,182 @@
// 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.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.DotNet.Scaffolding.ComponentModel;
using Microsoft.DotNet.Scaffolding.Helpers.Services;
using Spectre.Console;
using Spectre.Console.Flow;
namespace Microsoft.DotNet.Tools.Scaffold.Flow.Steps
{
internal class ParameterDiscovery
{
private readonly Parameter _parameter;
public ParameterDiscovery(Parameter parameter)
{
_parameter = parameter;
}
public FlowStepState State { get; private set; }
public async Task<string> DiscoverAsync(IFlowContext context)
{
var optionParameterAddition = _parameter.Required ? "(" : "(empty to skip, ";
return await PromptAsync(context, $"Enter new value for '{_parameter.DisplayName}' {optionParameterAddition}[sandybrown]<[/] to go back) : ");
}
private async Task<string> PromptAsync(IFlowContext context, string title)
{
//check if Parameter has a InteractivePickerType
if (_parameter.PickerType is null)
{
var prompt = new TextPrompt<string>($"[lightseagreen]{title}[/]")
.ValidationErrorMessage("bad value fix it please")
.Validate(x =>
{
if (x.Trim() == FlowNavigation.BackInputToken)
{
return ValidationResult.Success();
}
return Validate(context, x);
})
.AllowEmpty();
await Task.Delay(1);
return AnsiConsole.Prompt(prompt).Trim();
}
else
{
var codeService = context.GetCodeService();
if (codeService != null)
{
return await PromptInteractivePicker(context, _parameter.PickerType, codeService) ?? string.Empty;
}
}
return string.Empty;
}
private async Task<string?> PromptInteractivePicker(IFlowContext context, InteractivePickerType? pickerType, ICodeService codeService)
{
IList<Tuple<string, string>>? displayTuples = [];
string interactiveTitle = string.Empty;
switch (pickerType)
{
case InteractivePickerType.ClassPicker:
var allClassSymbols = await AnsiConsole
.Status()
.WithSpinner()
.Start("Gathering project classes!", async statusContext =>
{
return (await codeService.GetAllClassSymbolsAsync()).ToList();
});
displayTuples = GetClassDisplayNames(allClassSymbols);
interactiveTitle = "Class";
break;
case InteractivePickerType.FilePicker:
var allDocuments = (await codeService.GetAllDocumentsAsync()).ToList();
displayTuples = GetDocumentNames(allDocuments);
interactiveTitle = "File";
break;
case InteractivePickerType.DbProviderPicker:
displayTuples = DbProviders;
interactiveTitle = "DbProvider";
break;
}
if (!_parameter.Required)
{
displayTuples.Insert(0, Tuple.Create("None", string.Empty));
}
var prompt = new FlowSelectionPrompt<Tuple<string, string>>()
.Title($"[lightseagreen]Pick a {interactiveTitle}: [/]")
.Converter(GetDisplayNameFromTuple)
.AddChoices(displayTuples, navigation: context.Navigation);
var result = prompt.Show();
State = result.State;
return result.Value?.Item2;
}
private string GetDisplayNameFromTuple(Tuple<string, string> tuple)
{
bool displayNone = tuple.Item1.Equals("None", StringComparison.OrdinalIgnoreCase);
return displayNone ? $"[sandybrown]{tuple.Item1} (empty to skip parameter)[/]" : $"{tuple.Item1} ({tuple.Item2})";
}
private ValidationResult Validate(IFlowContext context, string promptVal)
{
if (!_parameter.Required && string.IsNullOrEmpty(promptVal))
{
return ValidationResult.Success();
}
if (!string.IsNullOrEmpty(promptVal) && !ParameterHelpers.CheckType(_parameter.Type, promptVal))
{
return ValidationResult.Error("Invalid input, please try again!");
}
return ValidationResult.Success();
}
private IList<Tuple<string, string>> GetClassDisplayNames(List<ISymbol> compilationClassSymbols)
{
List<Tuple<string, string>> classNames = [];
if (compilationClassSymbols != null && compilationClassSymbols.Count != 0)
{
compilationClassSymbols.ForEach(
x =>
{
if (x != null)
{
classNames.Add(Tuple.Create(x.MetadataName, x.Name));
}
});
}
return classNames;
}
private List<Tuple<string, string>> GetDocumentNames(List<Document> documents)
{
List<Tuple<string, string>> classNames = [];
if (documents != null && documents.Count != 0)
{
documents.ForEach(
x =>
{
if (x != null)
{
string fileName = System.IO.Path.GetFileName(x.Name);
classNames.Add(Tuple.Create(fileName, x.Name));
}
});
}
return classNames;
}
private static List<Tuple<string, string>>? _dbProviders;
private static List<Tuple<string, string>> DbProviders
{
get
{
_dbProviders ??=
[
Tuple.Create("SQL Server", "sqlserver"),
Tuple.Create("SQLite", "sqlite"),
Tuple.Create("PostgreSQL", "postgres"),
Tuple.Create("Cosmos DB", "cosmos")
];
return _dbProviders;
}
}
}
}

Просмотреть файл

@ -0,0 +1,120 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.DotNet.Scaffolding.Helpers.Extensions;
using Microsoft.DotNet.Scaffolding.Helpers.Services;
using Spectre.Console;
using Spectre.Console.Flow;
namespace Microsoft.DotNet.Tools.Scaffold.Flow.Steps.Project
{
internal class ProjectDiscovery
{
public ProjectDiscovery(IFileSystem fileSystem, string workingDir)
{
FileSystem = fileSystem;
WorkingDir = workingDir;
}
protected IFileSystem FileSystem { get; }
protected string WorkingDir { get; set; }
public FlowStepState State { get; private set; }
public string? Discover(IFlowContext context, string path)
{
List<string> projects = [];
projects = AnsiConsole
.Status()
.WithSpinner()
.Start("Discovering project files!", statusContext =>
{
if (!FileSystem.DirectoryExists(path))
{
return [];
}
var projects = FileSystem.EnumerateFiles(path, "*.csproj", SearchOption.AllDirectories).ToList();
var slnFiles = FileSystem.EnumerateFiles(path, "*.sln", SearchOption.TopDirectoryOnly).ToList();
var projectsFromSlnFiles = GetProjectsFromSolutionFiles(slnFiles, WorkingDir);
projects = projects.Union(projectsFromSlnFiles).ToList();
//should we search in all directories if nothing in top level? (yes, for now)
if (projects.Count == 0)
{
projects = FileSystem.EnumerateFiles(path, "*.csproj", SearchOption.AllDirectories).ToList();
}
return projects;
});
return Prompt(context, $"[lightseagreen]Pick project ({projects.Count}): [/]", projects);
}
internal IList<string> GetProjectsFromSolutionFiles(List<string> solutionFiles, string workingDir)
{
List<string> projectPaths = new();
foreach(var solutionFile in solutionFiles)
{
projectPaths.AddRange(GetProjectsFromSolutionFile(solutionFile, workingDir));
}
return projectPaths;
}
internal IList<string> GetProjectsFromSolutionFile(string solutionFilePath, string workingDir)
{
List<string> projectPaths = new();
if (string.IsNullOrEmpty(solutionFilePath))
{
return projectPaths;
}
string slnText = FileSystem.ReadAllText(solutionFilePath);
string projectPattern = @"Project\(""\{[A-F0-9\-]*\}""\)\s*=\s*""([^""]*)"",\s*""([^""]*)"",\s*""\{[A-F0-9\-]*\}""";
var matches = Regex.Matches(slnText, projectPattern);
foreach (Match match in matches)
{
string projectRelativePath = match.Groups[2].Value;
string? solutionDirPath = Path.GetDirectoryName(solutionFilePath) ?? workingDir;
string projectPath = Path.GetFullPath(projectRelativePath, solutionDirPath);
projectPaths.Add(projectPath);
}
return projectPaths;
}
internal string GetProjectDisplayName(string projectPath)
{
var name = Path.GetFileNameWithoutExtension(projectPath);
var relativePath = projectPath.MakeRelativePath(WorkingDir).ToSuggestion();
return $"{name} {relativePath.ToSuggestion(withBrackets: true)}";
}
private string? Prompt(IFlowContext context, string title, IList<string> projectFiles)
{
if (projectFiles.Count == 0)
{
return null;
}
if (projectFiles.Count == 1)
{
return projectFiles[0];
}
var prompt = new FlowSelectionPrompt<string>()
.Title(title)
.Converter(GetProjectDisplayName)
.AddChoices(projectFiles.OrderBy(x => Path.GetFileNameWithoutExtension(x)), navigation: context.Navigation);
var result = prompt.Show();
State = result.State;
return result.Value;
}
}
}

Просмотреть файл

@ -0,0 +1,139 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.Scaffolding.Helpers.Extensions;
using Microsoft.DotNet.Scaffolding.Helpers.Services;
using Microsoft.DotNet.Scaffolding.Helpers.Services.Environment;
using Spectre.Console.Flow;
namespace Microsoft.DotNet.Tools.Scaffold.Flow.Steps.Project;
internal class SourceProjectFlowStep : IFlowStep
{
private readonly IAppSettings _appSettings;
private readonly IFileSystem _fileSystem;
private readonly IEnvironmentService _environmentService;
private readonly ILogger _logger;
public SourceProjectFlowStep(
IAppSettings appSettings,
IEnvironmentService environment,
IFileSystem fileSystem,
ILogger logger)
{
_appSettings = appSettings;
_fileSystem = fileSystem;
_environmentService = environment;
_logger = logger;
}
/// <inheritdoc />
public string Id => nameof(SourceProjectFlowStep);
/// <inheritdoc />
public string DisplayName => "Source Project";
/// <inheritdoc />
public ValueTask<FlowStepResult> ValidateUserInputAsync(IFlowContext context, CancellationToken cancellationToken)
{
var projectPath = context.GetSourceProjectPath();
if (string.IsNullOrEmpty(projectPath))
{
var settings = context.GetCommandSettings();
projectPath = settings?.Project;
}
if (string.IsNullOrEmpty(projectPath))
{
return new ValueTask<FlowStepResult>(FlowStepResult.Failure("Source project is needed!"));
}
if (!projectPath.IsCSharpProject())
{
return new ValueTask<FlowStepResult>(FlowStepResult.Failure($"Project path is invalid '{projectPath}'"));
}
if (!Path.IsPathRooted(projectPath))
{
projectPath = Path.GetFullPath(Path.Combine(_environmentService.CurrentDirectory, projectPath.Trim(Path.DirectorySeparatorChar)));
}
if (!_fileSystem.FileExists(projectPath))
{
return new ValueTask<FlowStepResult>(FlowStepResult.Failure(string.Format("Project file '{0}' does not exist", projectPath)));
}
SelectSourceProject(context, projectPath);
return new ValueTask<FlowStepResult>(FlowStepResult.Success);
}
/// <inheritdoc />
public ValueTask<FlowStepResult> RunAsync(IFlowContext context, CancellationToken cancellationToken)
{
var settings = context.GetCommandSettings();
var path = settings?.Project;
if (string.IsNullOrEmpty(path))
{
path = _environmentService.CurrentDirectory;
}
if (!Path.IsPathRooted(path))
{
path = Path.GetFullPath(Path.Combine(_environmentService.CurrentDirectory, path.Trim(Path.DirectorySeparatorChar)));
}
var workingDir = _environmentService.CurrentDirectory;
if (path.EndsWith(".sln"))
{
workingDir = Path.GetDirectoryName(path)!;
}
else if (_fileSystem.DirectoryExists(path))
{
workingDir = path;
}
ProjectDiscovery projectDiscovery = new(_fileSystem, workingDir);
var projectPath = projectDiscovery.Discover(context, path);
if (projectDiscovery.State.IsNavigation())
{
return new ValueTask<FlowStepResult>(new FlowStepResult { State = projectDiscovery.State });
}
if (projectPath is not null)
{
SelectSourceProject(context, projectPath);
return new ValueTask<FlowStepResult>(FlowStepResult.Success);
}
_logger.LogMessage("No projects found in current directory", LogMessageType.Error);
return new ValueTask<FlowStepResult>(FlowStepResult.Failure());
}
/// <inheritdoc />
public ValueTask ResetAsync(IFlowContext context, CancellationToken cancellationToken)
{
context.Unset(FlowContextProperties.SourceProjectPath);
context.Unset(FlowContextProperties.SourceProject);
return new ValueTask();
}
private void SelectSourceProject(IFlowContext context, string projectPath)
{
if (!string.IsNullOrEmpty(projectPath))
{
context.Set(new FlowProperty(
FlowContextProperties.SourceProjectPath,
projectPath,
FlowContextProperties.SourceProjectDisplay,
isVisible: true));
_appSettings.Workspace().InputPath = projectPath;
ICodeService codeService = new CodeService(_appSettings, _logger);
context.Set(new FlowProperty(
FlowContextProperties.CodeService,
codeService,
isVisible: false));
}
}
}

Просмотреть файл

@ -0,0 +1,111 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.Scaffolding.ComponentModel;
using Microsoft.DotNet.Scaffolding.Helpers.General;
using Microsoft.DotNet.Scaffolding.Helpers.Services;
using Microsoft.DotNet.Scaffolding.Helpers.Services.Environment;
using Spectre.Console;
using Spectre.Console.Flow;
namespace Microsoft.DotNet.Tools.Scaffold.Flow.Steps;
/// <summary>
/// do first time initialization in ValidateUserInputAsync
/// - initialize MSBuild instance
/// - gather some specific environment variables
/// -
/// -
/// </summary>
public class StartupFlowStep : IFlowStep
{
private readonly IAppSettings _appSettings;
private readonly IEnvironmentService _environmentService;
private readonly IDotNetToolService _dotnetToolService;
private readonly IFileSystem _fileSystem;
private readonly IHostService _hostService;
private readonly ILogger _logger;
private readonly bool _initializeMsbuild;
public StartupFlowStep(
IAppSettings appSettings,
IDotNetToolService dotnetToolService,
IEnvironmentService environmentService,
IFileSystem fileSystem,
IHostService hostService,
ILogger logger,
bool initializeMsbuild = true)
{
_appSettings = appSettings;
_dotnetToolService = dotnetToolService;
_environmentService = environmentService;
_fileSystem = fileSystem;
_hostService = hostService;
_logger = logger;
_initializeMsbuild = initializeMsbuild;
}
public string Id => nameof(StartupFlowStep);
public string DisplayName => "Startup step";
public ValueTask ResetAsync(IFlowContext context, CancellationToken cancellationToken)
{
return new ValueTask();
}
public ValueTask<FlowStepResult> RunAsync(IFlowContext context, CancellationToken cancellationToken)
{
return new ValueTask<FlowStepResult>(FlowStepResult.Success);
}
public ValueTask<FlowStepResult> ValidateUserInputAsync(IFlowContext context, CancellationToken cancellationToken)
{
AnsiConsole.Status()
.WithSpinner()
.Start("Initializing dotnet-scaffold", async statusContext =>
{
statusContext.Refresh();
//add 'workspace' settings
var workspaceSettings = new WorkspaceSettings();
_appSettings.AddSettings("workspace", workspaceSettings);
if (_initializeMsbuild)
{
statusContext.Status = "Initializing msbuild!";
new MsBuildInitializer(_logger).Initialize();
statusContext.Status = "DONE\n";
}
//initialize environment variables
statusContext.Status = "Gathering environment variables!";
var environmentVariableProvider = new EnvironmentVariablesStartup(_hostService, _environmentService, _appSettings);
await environmentVariableProvider.StartupAsync();
statusContext.Status = "DONE\n";
//parse args passed
statusContext.Status = "Parsing args!";
var remainingArgs = context.GetRemainingArgs();
if (remainingArgs != null)
{
var argDict = CliHelpers.ParseILookup(remainingArgs.Parsed);
if (argDict != null)
{
SelectCommandArgs(context, argDict);
}
}
statusContext.Status = "DONE\n";
});
return new ValueTask<FlowStepResult>(FlowStepResult.Success);
}
private void SelectCommandArgs(IFlowContext context, IDictionary<string, List<string>> args)
{
if (args != null)
{
context.Set(new FlowProperty(
FlowContextProperties.CommandArgs,
args,
isVisible: false));
}
}
}

Просмотреть файл

@ -1,489 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.DotNet.Tools.Scaffold;
using System;
using System.CommandLine;
using System.CommandLine.Parsing;
using Microsoft.DotNet.MSIdentity.Tool;
using MsIdentity = Microsoft.DotNet.MSIdentity.Tool.Program;
namespace Microsoft.DotNet.Tools.Scaffold
{
public class Program
{
private const string SCAFFOLD_COMMAND = "scaffold";
private const string AREA_COMMAND = "--area";
private const string CONTROLLER_COMMAND = "--controller";
private const string IDENTITY_COMMAND = "--identity";
private const string RAZORPAGE_COMMAND = "--razorpage";
private const string VIEW_COMMAND = "--view";
/*
dotnet scaffold [generator] [-p|--project] [-n|--nuget-package-dir] [-c|--configuration] [-tfm|--target-framework] [-b|--build-base-path] [--no-build]
This commands supports the following generators :
Area
Controller
Identity
Razorpage
View
e.g: dotnet scaffold area <AreaNameToGenerate>
dotnet scaffold identity
dotnet scaffold razorpage
*/
public static int Main(string[] args)
{
var rootCommand = ScaffoldCommand();
rootCommand.AddCommand(ScaffoldAreaCommand());
rootCommand.AddCommand(ScaffoldControllerCommand());
rootCommand.AddCommand(ScaffoldRazorPageCommand());
rootCommand.AddCommand(ScaffoldViewCommand());
rootCommand.AddCommand(ScaffoldIdentityCommand());
//msidentity commands
//new BinderBase for System.Commandline update, new way to bind handlers to commands.
var provisioningToolBinder = new ProvisioningToolOptionsBinder(
MsIdentity.JsonOption,
MsIdentity.EnableIdTokenOption,
MsIdentity.EnableAccessToken,
MsIdentity.CallsGraphOption,
MsIdentity.CallsDownstreamApiOption,
MsIdentity.UpdateUserSecretsOption,
MsIdentity.ConfigUpdateOption,
MsIdentity.CodeUpdateOption,
MsIdentity.PackagesUpdateOption,
MsIdentity.ClientIdOption,
MsIdentity.AppDisplayName,
MsIdentity.ProjectType,
MsIdentity.ClientSecretOption,
MsIdentity.RedirectUriOption,
MsIdentity.ProjectFilePathOption,
MsIdentity.ClientProjectOption,
MsIdentity.ApiScopesOption,
MsIdentity.HostedAppIdUriOption,
MsIdentity.ApiClientIdOption,
MsIdentity.SusiPolicyIdOption,
MsIdentity.TenantOption,
MsIdentity.UsernameOption,
MsIdentity.InstanceOption,
MsIdentity.CalledApiUrlOption);
//internal commands
var listAadAppsCommand = MsIdentity.ListAADAppsCommand();
var listServicePrincipalsCommand = MsIdentity.ListServicePrincipalsCommand();
var listTenantsCommand = MsIdentity.ListTenantsCommand();
var createClientSecretCommand = MsIdentity.CreateClientSecretCommand();
//exposed commands
var registerApplicationCommand = MsIdentity.RegisterApplicationCommand();
var unregisterApplicationCommand = MsIdentity.UnregisterApplicationCommand();
var updateAppRegistrationCommand = MsIdentity.UpdateAppRegistrationCommand();
var updateProjectCommand = MsIdentity.UpdateProjectCommand();
var createAppRegistration = MsIdentity.CreateAppRegistrationCommand();
//hide internal commands.
listAadAppsCommand.IsHidden = true;
listServicePrincipalsCommand.IsHidden = true;
listTenantsCommand.IsHidden = true;
updateProjectCommand.IsHidden = true;
createClientSecretCommand.IsHidden = true;
listAadAppsCommand.SetHandler(MsIdentity.HandleListApps, provisioningToolBinder);
listServicePrincipalsCommand.SetHandler(MsIdentity.HandleListServicePrincipals, provisioningToolBinder);
listTenantsCommand.SetHandler(MsIdentity.HandleListTenants, provisioningToolBinder);
registerApplicationCommand.SetHandler(MsIdentity.HandleRegisterApplication, provisioningToolBinder);
unregisterApplicationCommand.SetHandler(MsIdentity.HandleUnregisterApplication, provisioningToolBinder);
updateAppRegistrationCommand.SetHandler(MsIdentity.HandleUpdateApplication, provisioningToolBinder);
updateProjectCommand.SetHandler(MsIdentity.HandleUpdateProject, provisioningToolBinder);
createClientSecretCommand.SetHandler(MsIdentity.HandleClientSecrets, provisioningToolBinder);
createAppRegistration.SetHandler(MsIdentity.HandleCreateAppRegistration, provisioningToolBinder);
rootCommand.AddCommand(listAadAppsCommand);
rootCommand.AddCommand(listServicePrincipalsCommand);
rootCommand.AddCommand(listTenantsCommand);
rootCommand.AddCommand(registerApplicationCommand);
rootCommand.AddCommand(unregisterApplicationCommand);
rootCommand.AddCommand(createAppRegistration);
rootCommand.AddCommand(updateAppRegistrationCommand);
rootCommand.AddCommand(updateProjectCommand);
rootCommand.AddCommand(createClientSecretCommand);
rootCommand.Description = "dotnet scaffold [command] [-p|--project] [-n|--nuget-package-dir] [-c|--configuration] [-tfm|--target-framework] [-b|--build-base-path] [--no-build] ";
if (args.Length == 0)
{
args = new string[] { "-h" };
}
var commandLineBuilder = new CommandLineBuilder(rootCommand);
commandLineBuilder.UseDefaults();
var parser = commandLineBuilder.Build();
int parseExitCode = parser.Invoke(args);
if (parseExitCode != 0)
{
return parseExitCode;
}
ParseResult parseResult = parser.Parse(args);
string parsedCommandName = parseResult.CommandResult.Command.Name;
switch (parsedCommandName)
{
case AREA_COMMAND:
case CONTROLLER_COMMAND:
case IDENTITY_COMMAND:
case RAZORPAGE_COMMAND:
case VIEW_COMMAND:
if (parseResult.CommandResult.Children.Count == 1 &&
string.Equals(parseResult.CommandResult.Children[0].Symbol?.Name, "help", StringComparison.OrdinalIgnoreCase))
{
// The help option for the commands are handled by System.Commandline.
return 0;
}
args[0] = args[0].Replace("--", "");
return VisualStudio.Web.CodeGeneration.Tools.Program.Main(args);
default:
// The command is not handled by 'dotnet scaffold'.
return -1;
}
}
private static Option ProjectOption() =>
new Option<string>(
aliases: new[] { "-p", "--project" },
description: "Specifies the path of the project file to run (folder name or full path). If not specified, it defaults to the current directory.")
{
IsRequired = false
};
private static Option NuGetPackageOption() =>
new Option<string>(
aliases: new[] { "-n", "--nuget-package-dir" },
description: "Specifies the NuGet package directory.")
{
IsRequired = false
};
private static Option ConfigurationOption() =>
new Option<string>(
aliases: new[] { "-c", "--configuration" },
defaultValueFactory: () => "Debug",
description: "Defines the build configuration. The default value is Debug.")
{
IsRequired = false
};
private static Option TargetFrameworkOption() =>
new Option<string>(
aliases: new[] { "-tfm", "--target-framework" },
description: "Target Framework to use. For example, net46.")
{
IsRequired = false
};
private static Option BuildBasePathOption() =>
new Option<string>(
aliases: new[] { "-b", "--build-base-path" },
description: "The build base path.")
{
IsRequired = false
};
private static Option NoBuildOption() =>
new Option<bool>(
aliases: new[] { "--no-build" },
description: "Doesn't build the project before running. It also implicitly sets the --no-restore flag.")
{
IsRequired = false
};
private static Command ScaffoldCommand() =>
new Command(
name: SCAFFOLD_COMMAND,
description: "Scaffold files to a .NET Application")
{
// Arguments & Options
ProjectOption(), NuGetPackageOption(), ConfigurationOption(), TargetFrameworkOption(), BuildBasePathOption(), NoBuildOption()
};
// AREA SCAFFOLDERS
private static Argument AreaNameArgument() =>
new Argument<string>(
name: "AreaNameToGenerate",
description: "This tool is intended for ASP.NET Core web projects with controllers and views. It's not intended for Razor Pages apps.")
{
Arity = ArgumentArity.ExactlyOne
};
private static Command ScaffoldAreaCommand() =>
new Command(
name: AREA_COMMAND,
description: "Scaffolds an Area")
{
// Arguments & Options
AreaNameArgument()
};
private static Option ControllerNameOption() =>
new Option<string>(
aliases: new[] { "-name", "--controllerName" },
description: "Name of the controller.")
{
IsRequired = true
};
private static Option AsyncActionsOption() =>
new Option<bool>(
aliases: new[] { "-async", "--useAsyncActions" },
description: "Generate async controller actions.")
{
IsRequired = false
};
private static Option GenerateNoViewOption() =>
new Option<bool>(
aliases: new[] { "-nv", "--noViews" },
description: "Generate no views.")
{
IsRequired = false
};
private static Option RestWithNoViewOption() =>
new Option<bool>(
aliases: new[] { "-api", "--restWithNoViews" },
description: "Generate a Controller with REST style API. noViews is assumed and any view related options are ignored.")
{
IsRequired = false
};
private static Option ReadWriteActionOption() =>
new Option<bool>(
aliases: new[] { "-actions", "--readWriteActions" },
description: "Generate controller with read/write actions without a model.")
{
IsRequired = false
};
private static Option ModelClassOption() =>
new Option<string>(
aliases: new[] { "-m", "--model" },
description: "Model class to use.")
{
IsRequired = false
};
private static Option DataContextOption() =>
new Option<string>(
aliases: new[] { "-dc", "--dataContext" },
description: "The DbContext class to use.")
{
IsRequired = false
};
private static Option BootStrapVersionOption() =>
new Option<string>(
aliases: new[] { "-b", "--bootstrapVersion" },
description: "Specifies the bootstrap version. Valid values are 3 or 4. Default is 4. If needed and not present, a wwwroot directory is created that includes the bootstrap files of the specified version.")
{
IsRequired = false
};
private static Option ReferenceScriptLibrariesOption() =>
new Option<bool>(
aliases: new[] { "-scripts", "--referenceScriptLibraries" },
description: "Reference script libraries in the generated views. Adds _ValidationScriptsPartial to Edit and Create pages.")
{
IsRequired = false
};
private static Option CustomLayoutOption() =>
new Option<string>(
aliases: new[] { "-l", "--layout" },
description: "Custom Layout page to use.")
{
IsRequired = false
};
private static Option UseDefaultLayoutOption() =>
new Option<bool>(
aliases: new[] { "-udl", "--useDefaultLayout" },
description: "Use the default layout for the views.")
{
IsRequired = false
};
private static Option OverwriteFilesOption() =>
new Option<bool>(
aliases: new[] { "-f", "--force" },
description: "Overwrite existing files.")
{
IsRequired = false
};
private static Option RelativeFolderPathOption() =>
new Option<string>(
aliases: new[] { "-outDir", "--relativeFolderPath" },
description: "The relative output folder path from project where the file are generated. If not specified, files are generated in the project folder.")
{
IsRequired = false
};
private static Option ControllerNamespaceOption() =>
new Option<string>(
aliases: new[] { "-namespace", "--controllerNamespace" },
description: "Specify the name of the namespace to use for the generated controller.")
{
IsRequired = false
};
private static Option UseSQLliteOption() =>
new Option<bool>(
aliases: new[] { "-sqlite", "--useSqlite" },
description: "Flag to specify if DbContext should use SQLite instead of SQL Server.")
{
IsRequired = false
};
private static Command ScaffoldControllerCommand() =>
new Command(
name: CONTROLLER_COMMAND,
description: "Scaffolds a Controller")
{
// Arguments & Options
ControllerNameOption(), AsyncActionsOption(), GenerateNoViewOption(), RestWithNoViewOption(), ReadWriteActionOption(),
ModelClassOption(), DataContextOption(), BootStrapVersionOption(), ReferenceScriptLibrariesOption(), CustomLayoutOption(), UseDefaultLayoutOption(), OverwriteFilesOption(), RelativeFolderPathOption(),
ControllerNamespaceOption(), UseSQLliteOption()
};
private static Option RazorpageNamespaceNameOption() =>
new Option<string>(
aliases: new[] { "-namespace", "--namespaceName" },
description: "The name of the namespace to use for the generated PageModel.")
{
IsRequired = false
};
private static Option PartialViewOption() =>
new Option<bool>(
aliases: new[] { "-partial", "--partialView" },
description: "Generate a partial view. Layout options -l and -udl are ignored if this is specified.")
{
IsRequired = false
};
private static Option NoPageModelOption() =>
new Option<string>(
aliases: new[] { "-npm", "--noPageModel" },
description: "Switch to not generate a PageModel class for Empty template.")
{
IsRequired = false
};
private static Argument RazorPageNameArgument() =>
new Argument<string>(
name: "RazorpageNameToGenerate",
description: "The name of the razor page to generate.")
{
Arity = ArgumentArity.ZeroOrOne
};
private static Argument TemplateNameArgument() =>
new Argument<string>(
name: "TemplateName",
description: "Razor Pages or Views can be individually scaffolded by specifying the name of the new page/view and the template to use. The supported templates are: Empty|Create|Edit|Delete|Details|List")
{
Arity = ArgumentArity.ZeroOrOne
};
private static Command ScaffoldRazorPageCommand() =>
new Command(
name: RAZORPAGE_COMMAND,
description: "Scaffolds Razor pages")
{
// Arguments
RazorPageNameArgument(), TemplateNameArgument(),
// Options
RazorpageNamespaceNameOption(), PartialViewOption(), NoPageModelOption(),
ModelClassOption(), DataContextOption(), BootStrapVersionOption(), ReferenceScriptLibrariesOption(), CustomLayoutOption(), UseDefaultLayoutOption(), OverwriteFilesOption(), RelativeFolderPathOption(),
UseSQLliteOption()
};
private static Argument ViewNameArgument() =>
new Argument<string>(
name: "ViewNameToGenerate",
description: "The name of the View to generate.")
{
Arity = ArgumentArity.ZeroOrOne
};
private static Command ScaffoldViewCommand() =>
new Command(
name: VIEW_COMMAND,
description: "Scaffolds a View")
{
// Arguments
ViewNameArgument(),TemplateNameArgument(),
// Options
ModelClassOption(), DataContextOption(), BootStrapVersionOption(), ReferenceScriptLibrariesOption(), CustomLayoutOption(), UseDefaultLayoutOption(), OverwriteFilesOption(), RelativeFolderPathOption(),
ControllerNamespaceOption(), UseSQLliteOption(), PartialViewOption()
};
private static Option DBContextOption() =>
new Option<string>(
aliases: new[] { "-dc", "--dbContext" },
description: "Name of the DbContext to use, or generate (if it does not exist).")
{
IsRequired = false
};
private static Option FilesListOption() =>
new Option<string>(
aliases: new[] { "-fi", "--files" },
description: "List of semicolon separated files to scaffold. Use the --listFiles option to see the available options.")
{
IsRequired = false
};
private static Option ListFilesOption() =>
new Option<string>(
aliases: new[] { "-lf", "--listFiles" },
description: "Lists the files that can be scaffolded by using the '--files' option.")
{
IsRequired = false
};
private static Option UserClassOption() =>
new Option<string>(
aliases: new[] { "-u", "--userClass" },
description: "Name of the User class to generate.")
{
IsRequired = false
};
private static Option UseDefaultUIOption() =>
new Option<string>(
aliases: new[] { "-udui", "--useDefaultUI" },
description: "Use this option to setup identity and to use Default UI.")
{
IsRequired = false
};
private static Option GenerateLayoutOption() =>
new Option<string>(
aliases: new[] { "-gl", "--generateLayout" },
description: "Use this option to generate a new _Layout.cshtml")
{
IsRequired = false
};
private static Command ScaffoldIdentityCommand() =>
new Command(
name: IDENTITY_COMMAND,
description: "Scaffolds Identity")
{
// Options
DBContextOption(), FilesListOption(), ListFilesOption(), UserClassOption(), UseSQLliteOption(), OverwriteFilesOption(), UseDefaultUIOption(), CustomLayoutOption(),
GenerateLayoutOption(), BootStrapVersionOption()
};
}
}
var builder = new ScaffoldCommandAppBuilder(args);
var app = builder.Build();
await app.RunAsync();

Просмотреть файл

@ -1,7 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Web.CodeGeneration.Tools.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("E2E_Test.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]

Просмотреть файл

@ -0,0 +1,104 @@
// 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.Text;
using Microsoft.DotNet.Scaffolding.Helpers.Services;
using Spectre.Console;
namespace Microsoft.DotNet.Tools.Scaffold.Services
{
internal class AnsiConsoleLogger : ILogger
{
private readonly bool _jsonOutput;
private readonly bool _silent;
private static bool isTrace = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("codegen_trace"));
public bool IsTracing => isTrace;
private string CommandName { get; }
public AnsiConsoleLogger(string? commandName = null, bool jsonOutput = false, bool silent = false)
{
CommandName = commandName ?? string.Empty;
_jsonOutput = jsonOutput;
_silent = silent;
AnsiConsole.Console.Profile.Encoding = Encoding.UTF8;
}
public void LogMessage(string? message, LogMessageType level, bool removeNewLine = false)
{
//if json output is enabled, don't write to console at all.
if (!_silent && !_jsonOutput && !string.IsNullOrEmpty(message))
{
switch (level)
{
case LogMessageType.Error:
if (removeNewLine)
{
AnsiConsole.Console.Markup($"[red]{message}");
}
else
{
AnsiConsole.Console.MarkupLine($"[red]{message}");
}
break;
case LogMessageType.Information:
if (removeNewLine)
{
AnsiConsole.Console.Write(message);
}
else
{
AnsiConsole.Console.WriteLine(message);
}
break;
}
}
}
public void LogJsonMessage(string? state = null, object? content = null, string? output = null)
{
if (!_silent)
{
if (_jsonOutput)
{
var jsonMessage = new JsonResponse(CommandName, state, content, output);
AnsiConsole.WriteLine(jsonMessage.ToJsonString());
}
else
{
if (state == State.Fail)
{
LogMessage(output, LogMessageType.Error);
}
else
{
LogMessage(output);
}
}
}
}
public void LogMessage(string? message, bool removeNewLine = false)
{
if (!_silent && !_jsonOutput)
{
LogMessage(message, LogMessageType.Information, removeNewLine);
}
}
public void LogFailureAndExit(string failureMessage)
{
if (_jsonOutput)
{
LogJsonMessage(State.Fail, output: failureMessage);
}
else
{
LogMessage(failureMessage, LogMessageType.Error);
}
Environment.Exit(1);
}
}
}

Просмотреть файл

@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System.Collections.Generic;
using Spectre.Console.Flow;
namespace Microsoft.DotNet.Tools.Scaffold.Services;
internal class FlowProvider : IFlowProvider
{
public FlowProvider()
{
}
/// <inheritdoc />
public IFlow? CurrentFlow { get; private set; }
/// <inheritdoc />
public IFlow GetFlow(IEnumerable<IFlowStep> steps, Dictionary<string, object> properties, bool nonInteractive, bool showSelectedOptions = true)
{
var flow = new FlowRunner(steps, properties, nonInteractive)
.Breadcrumbs()
.SelectedOptions(showSelectedOptions);
CurrentFlow = flow;
return flow;
}
}

Просмотреть файл

@ -0,0 +1,22 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Spectre.Console.Flow;
using System.Collections.Generic;
namespace Microsoft.DotNet.Tools.Scaffold.Services;
/// <summary>
/// A factory that creates a new flow.
/// </summary>
public interface IFlowProvider
{
/// <summary>
/// Since we are in CLI, only one flow is possible at the given moment. This property contains current flow object.
/// </summary>
IFlow? CurrentFlow { get; }
/// <summary>
/// Initialize the FlowRunner with given steps and properties.
/// </summary>
IFlow GetFlow(IEnumerable<IFlowStep> steps, Dictionary<string, object> properties, bool nonInteractive, bool showSelectedOptions = true);
}

Просмотреть файл

@ -1,17 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Scaffolding tool for .NET projects. Contains the dotnet-scaffold command used for generating areas, views, controllers, identity pages etc. </Description>
<Description>Scaffolding tool for .NET projects. </Description>
<TargetFramework>net8.0</TargetFramework>
<OutputType>exe</OutputType>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackAsTool>true</PackAsTool>
<PackageTags>dotnet;scaffold</PackageTags>
<PackageId>Microsoft.dotnet-scaffold</PackageId>
<RootNamespace>Microsoft.DotNet.Tools.Scaffold</RootNamespace>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<SignAssembly>false</SignAssembly>
</PropertyGroup>
<Import Project="$(RepoRoot)eng\Versions.Scaffold.props" />
<ItemGroup>
<ProjectReference Include="$(RepoRoot)\tools\dotnet-aspnet-codegenerator\dotnet-aspnet-codegenerator.csproj" />
<ProjectReference Include="$(RepoRoot)\tools\dotnet-msidentity\dotnet-msidentity.csproj" />
<PackageReference Include="System.Commandline" Version="$(SystemCommandLineVersion)" />
<PackageReference Include="Spectre.Console.Flow" Version="$(SpectreConsoleFlowVersion)" />
<PackageReference Include="Spectre.Console" Version="$(SpectreConsoleVersion)" />
<PackageReference Include="Spectre.Console.Cli" Version="$(SpectreConsoleVersion)" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(MicrosoftExtensionsDependencyInjectionPackageVersion)" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(RepoRoot)src\Shared\Microsoft.DotNet.Scaffolding.Helpers\Microsoft.DotNet.Scaffolding.Helpers.csproj" />
<ProjectReference Include="$(RepoRoot)src\Shared\Microsoft.DotNet.Scaffolding.ComponentModel\Microsoft.DotNet.Scaffolding.ComponentModel.csproj" />
</ItemGroup>
</Project>