Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/TALXIS.CLI.Workspace/TALXIS.CLI.Workspace.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,12 @@
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.7" />
</ItemGroup>

<ItemGroup>
<Content Include="Upgrade\Templates\**\*.*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<PackagePath>Upgrade/Templates/%(RecursiveDir)%(Filename)%(Extension)</PackagePath>
</Content>
</ItemGroup>

</Project>
196 changes: 196 additions & 0 deletions src/TALXIS.CLI.Workspace/Upgrade/Conversion/ProjectUpgrader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
using Microsoft.Extensions.Logging;
using TALXIS.CLI.Workspace.Upgrade.Generation;
using TALXIS.CLI.Workspace.Upgrade.Models;
using TALXIS.CLI.Workspace.Upgrade.Parsers;
using TALXIS.CLI.Workspace.Upgrade.Utilities;

namespace TALXIS.CLI.Workspace.Upgrade.Conversion;

public class ProjectUpgrader
{
private readonly CsprojParser _parser;
private readonly SdkProjectGenerator _generator;
private readonly ILogger<ProjectUpgrader> _logger;

public ProjectUpgrader(ILogger<ProjectUpgrader> logger)
{
_parser = new CsprojParser();
_generator = new SdkProjectGenerator();
_logger = logger;
}

public UpgradeResult Upgrade(
string targetFilePath,
string oldTemplateFilePath,
string newTemplateFilePath,
bool createBackup = true)
{
var result = new UpgradeResult();

try
{
_logger.LogInformation("Parsing project and templates...");
var targetProject = _parser.Parse(targetFilePath);
var oldTemplate = _parser.Parse(oldTemplateFilePath);
var newTemplate = _parser.Parse(newTemplateFilePath);

_logger.LogInformation("Filling parameter placeholders from existing project...");
var newProject = FillParameterPlaceholders(newTemplate, targetProject, targetFilePath);

_logger.LogInformation("Extracting additional dependencies not in baseline template...");
ExtractAdditionalDependencies(oldTemplate, targetProject, newProject);

result.CustomPropertiesFound = CountFilledParameters(newTemplate, targetProject);
result.PackageReferencesFound = newProject.PackageReferences.Count - newTemplate.PackageReferences.Count;
result.ProjectReferencesFound = newProject.ProjectReferences.Count - newTemplate.ProjectReferences.Count;
result.AssemblyReferencesFound = newProject.AssemblyReferences.Count - newTemplate.AssemblyReferences.Count;

if (createBackup)
{
var backupPath = targetFilePath + ".backup";
if (File.Exists(targetFilePath))
{
File.Copy(targetFilePath, backupPath, overwrite: true);
result.BackupCreated = true;
result.BackupPath = backupPath;
_logger.LogInformation("Backup created at {BackupPath}", backupPath);
}
}

if (File.Exists(targetFilePath))
{
_logger.LogInformation("Removing original file {TargetFile}", targetFilePath);
File.Delete(targetFilePath);
}

var newFilePath = Path.Combine(
Path.GetDirectoryName(targetFilePath) ?? string.Empty,
Path.GetFileNameWithoutExtension(targetFilePath) + ".csproj");

_logger.LogInformation("Generating new SDK-style project file at {NewFilePath}", newFilePath);
var xml = _generator.Generate(newProject);
_generator.SaveToFile(xml, newFilePath);

result.Success = true;
result.OutputFilePath = newFilePath;
}
catch (Exception ex)
{
result.Success = false;
result.ErrorMessage = ex.Message;
result.Exception = ex;
_logger.LogError(ex, "Upgrade failed for {TargetFile}", targetFilePath);
}

return result;
}

private CsprojProject FillParameterPlaceholders(
CsprojProject newTemplate,
CsprojProject targetProject,
string targetFilePath)
{
var newProject = new CsprojProject
{
FilePath = targetFilePath.Replace(".cdsproj", ".csproj"),
IsSdkStyle = true,
Sdk = newTemplate.Sdk ?? "Microsoft.NET.Sdk"
};

// Copy all properties
foreach (var prop in newTemplate.Properties)
{
// If template property has empty value, it's a parameter placeholder - fill from target
if (string.IsNullOrWhiteSpace(prop.Value))
{
if (targetProject.Properties.TryGetValue(prop.Key, out var targetValue))
{
newProject.Properties[prop.Key] = targetValue;
}
else
{
newProject.Properties[prop.Key] = prop.Value;
}
}
else
{
newProject.Properties[prop.Key] = prop.Value;
}
}

// Copy all other elements
newProject.PackageReferences.AddRange(newTemplate.PackageReferences);
newProject.ProjectReferences.AddRange(newTemplate.ProjectReferences);
newProject.AssemblyReferences.AddRange(newTemplate.AssemblyReferences);
newProject.CustomTargets.AddRange(newTemplate.CustomTargets);
newProject.CustomImports.AddRange(newTemplate.CustomImports);
newProject.CustomItemGroups.AddRange(newTemplate.CustomItemGroups);
newProject.CustomPropertyGroups.AddRange(newTemplate.CustomPropertyGroups);

return newProject;
}

private int CountFilledParameters(CsprojProject newTemplate, CsprojProject targetProject)
{
int count = 0;
foreach (var prop in newTemplate.Properties)
{
if (string.IsNullOrWhiteSpace(prop.Value) && targetProject.Properties.ContainsKey(prop.Key))
{
count++;
}
}
return count;
}

private void ExtractAdditionalDependencies(
CsprojProject oldTemplate,
CsprojProject targetProject,
CsprojProject newProject)
{
// Find PackageReferences that are in target but NOT in old template baseline
var baselinePackages = new HashSet<string>(
oldTemplate.PackageReferences.Select(p => p.Name),
StringComparer.OrdinalIgnoreCase
);

foreach (var package in targetProject.PackageReferences)
{
if (!baselinePackages.Contains(package.Name))
{
// This is a custom package added by user
newProject.PackageReferences.Add(package);
}
}

// Find ProjectReferences that are in target but NOT in old template baseline
var baselineProjects = new HashSet<string>(
oldTemplate.ProjectReferences.Select(p => p.Include),
StringComparer.OrdinalIgnoreCase
);

foreach (var projectRef in targetProject.ProjectReferences)
{
if (!baselineProjects.Contains(projectRef.Include))
{
// This is a custom project reference added by user
newProject.ProjectReferences.Add(projectRef);
}
}

// Find AssemblyReferences that are in target but NOT in old template baseline
var baselineAssemblies = new HashSet<string>(
oldTemplate.AssemblyReferences.Select(a => a.Include),
StringComparer.OrdinalIgnoreCase
);

foreach (var assembly in targetProject.AssemblyReferences)
{
if (!baselineAssemblies.Contains(assembly.Include))
{
// This is a custom assembly reference added by user
newProject.AssemblyReferences.Add(assembly);
}
}
}
}
184 changes: 184 additions & 0 deletions src/TALXIS.CLI.Workspace/Upgrade/Generation/SdkProjectGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
using System.Xml.Linq;
using TALXIS.CLI.Workspace.Upgrade.Models;

namespace TALXIS.CLI.Workspace.Upgrade.Generation;

public class SdkProjectGenerator
{
public XDocument Generate(CsprojProject project)
{
var root = new XElement("Project",
new XAttribute("Sdk", project.Sdk ?? "Microsoft.NET.Sdk")
);

AddPropertyGroup(root, project);

if (project.PackageReferences.Any())
{
AddPackageReferences(root, project);
}

if (project.ProjectReferences.Any())
{
AddProjectReferences(root, project);
}

if (project.AssemblyReferences.Any())
{
AddAssemblyReferences(root, project);
}

foreach (var itemGroup in project.CustomItemGroups)
{
root.Add(itemGroup);
}

foreach (var import in project.CustomImports)
{
root.Add(import);
}

foreach (var target in project.CustomTargets)
{
root.Add(target);
}

return new XDocument(
new XDeclaration("1.0", "utf-8", null),
root
);
}


public void SaveToFile(XDocument document, string filePath)
{
var directory = Path.GetDirectoryName(filePath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}

document.Save(filePath, SaveOptions.None);
}

private void AddPropertyGroup(XElement root, CsprojProject project)
{
if (!project.Properties.Any())
return;

var propertyGroup = new XElement("PropertyGroup");

var orderedProperties = new[]
{
"TargetFramework", "TargetFrameworks",
"OutputType", "AssemblyName", "RootNamespace",
"LangVersion", "Nullable", "ImplicitUsings"
};

foreach (var key in orderedProperties)
{
if (project.Properties.TryGetValue(key, out var value))
{
propertyGroup.Add(new XElement(key, value));
}
}

foreach (var prop in project.Properties)
{
if (!orderedProperties.Contains(prop.Key))
{
propertyGroup.Add(new XElement(prop.Key, prop.Value));
}
}

root.Add(propertyGroup);
}

private void AddPackageReferences(XElement root, CsprojProject project)
{
var itemGroup = new XElement("ItemGroup");

var attributeNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"PrivateAssets", "ExcludeAssets", "IncludeAssets"
};

foreach (var package in project.PackageReferences.OrderBy(p => p.Name))
{
var element = new XElement("PackageReference",
new XAttribute("Include", package.Name)
);

if (!string.IsNullOrEmpty(package.Version))
{
element.Add(new XAttribute("Version", package.Version));
}

// Add metadata
foreach (var metadata in package.Metadata)
{
if (attributeNames.Contains(metadata.Key))
{
element.Add(new XAttribute(metadata.Key, metadata.Value));
}
else
{
element.Add(new XElement(metadata.Key, metadata.Value));
}
}

itemGroup.Add(element);
}

root.Add(itemGroup);
}

private void AddProjectReferences(XElement root, CsprojProject project)
{
var itemGroup = new XElement("ItemGroup");

foreach (var projectRef in project.ProjectReferences.OrderBy(p => p.Include))
{
var element = new XElement("ProjectReference",
new XAttribute("Include", projectRef.Include)
);

// Add metadata
foreach (var metadata in projectRef.Metadata)
{
element.Add(new XElement(metadata.Key, metadata.Value));
}

itemGroup.Add(element);
}

root.Add(itemGroup);
}

private void AddAssemblyReferences(XElement root, CsprojProject project)
{
var itemGroup = new XElement("ItemGroup");

foreach (var assemblyRef in project.AssemblyReferences.OrderBy(a => a.Include))
{
var element = new XElement("Reference",
new XAttribute("Include", assemblyRef.Include)
);

if (!string.IsNullOrEmpty(assemblyRef.HintPath))
{
element.Add(new XElement("HintPath", assemblyRef.HintPath));
}

// Add metadata
foreach (var metadata in assemblyRef.Metadata)
{
element.Add(new XElement(metadata.Key, metadata.Value));
}

itemGroup.Add(element);
}

root.Add(itemGroup);
}
}
Loading