Skip to content

Clean up todo #37

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 5, 2024
Merged
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
134 changes: 71 additions & 63 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,99 +3,107 @@
[![codecov](https://codecov.io/gh/kysect/DotnetProjectSystem/graph/badge.svg?token=eRI09WyDsH)](https://codecov.io/gh/kysect/DotnetProjectSystem)
[![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fkysect%2FDotnetProjectSystem%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/kysect/DotnetProjectSystem/master)

DotnetProjectSystem is a nuget package for working with .sln, .csproj and .props files: parsing, modifying and generating.
DotnetProjectSystem is a nuget package for working with .sln, .csproj and .props files.

## Generating
### Main features

This code samples:
- Creating solution and project files structure from code, fluent API
- Parsing solution and project files
- Typed wrappers for reading and modifying solution and project files
- API for modifying solution and project files. Implementation of strategy for migration solution to Central Package Management
- Support of [TestableIO.System.IO.Abstractions](https://github.com/TestableIO/System.IO.Abstractions) for testing

### Creating solution and project files structure

- Task: create solution file, project files and Directory.Build.props file.
- Use case: need to create sample solution for test proposes (on real file system or MockFileSystem).

Code sample for creating solution and project files structure:

```csharp
var factory = new SolutionFileStructureBuilderFactory(_fileSystem, _syntaxFormatter);

factory
.Create(solutionName)
.AddProject(
new ProjectFileStructureBuilder("Project")
.SetContent("<Project></Project>"))
.AddFile(new SolutionFileInfo(["Directory.Build.props"], "<Project></Project>"))
.Save("C:\\Repositories\\");
var factory = new SolutionFileStructureBuilderFactory(fileSystem, syntaxFormatter);

var project =
new ProjectFileStructureBuilder("FirstProject")
.SetContent("""
<Project>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
</Project>
""")
.AddFile(["Models", "MyModel.cs"], "using System;");

var directoryBuildPropsFile = DirectoryBuildPropsFile.CreateEmpty();
directoryBuildPropsFile.File.Properties.SetProperty("Company", "Kysect");

factory.Create("NewSolution")
.AddProject(project)
.AddDirectoryBuildProps(directoryBuildPropsFile)
.Save(@"D:\Repositories\NewSolution");
```

will create this file structure:

```
- ./
- Project/
- Project.csproj
- MySolution.sln
- Directory.Build.props
D:\Repositories\NewSolution\
|- FirstProject/
|- FirstProject.csproj
|- Models
|- MyModel.cs
|- Directory.Build.props
|- NewSolution.sln
```

## Parsing
### Parsing solution and project files

Nuget provide API for parsing .sln and .csproj files:

```csharp
string solutionFilePath = ...;
DotnetSolutionParser parser = new DotnetSolutionParser(_fileSystem, logger);
DotnetSolutionDescriptor result = parser.ParseContent(solutionFilePath);

// result.FilePath == C:\Solution\Solution.sln
// result.Projects ==
// {
// { "C:\Solution\Project.csproj", <DotnetProjectFile> }
// }
var parser = new DotnetSolutionParser(_fileSystem, logger);
var result = parser.Parse(solutionFilePath);
```

## Modification
Parse method return model DotnetSolutionDescriptor that contains information about projects. Project represented by DotnetProjectFile model that provide access to project properties and items.

Nuget provide API for modification of parsed solution. Sample of modification;
### Typed wrappers for reading and modifying solution and project files

Some samples for working with DotnetProjectFile:

```csharp
syntaxFormatter = new XmlDocumentSyntaxFormatter();
DotnetSolutionModifier solutionModifier = solutionModifierFactory.Create("Solution.sln");
DotnetProjectFile project = ...;
var packageReferences = project.PackageReferences.GetPackageReferences();
// packageReferences = { { "System.Text.Json", "8.0.0" }, { "Microsoft.Extensions.Logging", "8.0.0" } }

project.PackageReferences.SetPackageReference("System.Text.Json", "9.0.0");
// packageReferences = { { "System.Text.Json", "9.0.0" }, { "Microsoft.Extensions.Logging", "8.0.0" }

solutionModifier
.GetOrCreateDirectoryPackagePropsModifier()
.SetCentralPackageManagement(true);
project.PackageReferences.AddPackageReference("Kysect.Editorconfig", "1.0.0");

solutionModifier.Save(syntaxFormatter)
var implicitUsings = project.Properties.GetProperty("ImplicitUsings");
// implicitUsings = true
project.Properties.SetProperty("ImplicitUsings", "false");
// implicitUsings = false
```

This code will add `<ManagePackageVersionsCentrally>` to `Directory.Package.props` file.
### API for modifying solution and project files

For this modification was introduces strategy that describe modification:
Nuget provide API for modification of parsed solution. NuGet package shipped with implementation of strategy for migration solution to Central Package Management as sample.

```csharp
public class SetTargetFrameworkModifyStrategy(string value) : IXmlProjectFileModifyStrategy<XmlElementSyntax>
{
public IReadOnlyCollection<XmlElementSyntax> SelectNodeForModify(XmlDocumentSyntax document)
{
document.ThrowIfNull();

return document
.GetNodesByName("TargetFramework")
.OfType<XmlElementSyntax>()
.ToList();
}

public SyntaxNode ApplyChanges(XmlElementSyntax syntax)
{
syntax.ThrowIfNull();

XmlTextSyntax content = SyntaxFactory.XmlText(SyntaxFactory.XmlTextLiteralToken(value, null, null));
return syntax.ReplaceNode(syntax.Content.Single(), content);
}
}
string solutionFilePath = ...;
var factory = new DotnetSolutionModifierFactory(_fileSystem, _solutionFileContentParser, _formatter);
var solutionModifier = _solutionModifierFactory.Create(solutionFilePath);
var migrator = new CentralPackageManagementMigrator(logger);

migrator.Migrate(solutionModifier);
```

And this strategy applied to solutions:
### Support of TestableIO.System.IO.Abstractions

```csharp
DotnetSolutionModifier solutionModifier = solutionModifierFactory.Create("Solution.sln");
TestableIO.System.IO.Abstractions is NuGet that provide abstraction for file system. This abstraction can be used for testing. Nuget provide implementation of IFileSystem for TestableIO.System.IO.Abstractions. Kysect.DotnetProjectSystem fully support this abstraction.

foreach (DotnetProjectModifier solutionModifierProject in solutionModifier.Projects)
solutionModifierProject.File.UpdateDocument(new SetTargetFrameworkModifyStrategy("net9.0"));
## Used by

solutionModifier.Save(syntaxFormatter);
```
Currently main use case for this library is https://github.com/kysect/Zeya - tool for managing .NET projects and modification them. You can check it for more examples of using this library.
2 changes: 1 addition & 1 deletion Sources/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<PropertyGroup>
<RepositoryUrl>https://github.com/kysect/DotnetProjectSystem</RepositoryUrl>
<PackageProjectUrl>https://github.com/kysect/DotnetProjectSystem</PackageProjectUrl>
<Version>0.1.16</Version>
<Version>1.0.0</Version>
</PropertyGroup>

<!-- Code configuration -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public void Save_AfterAddingFile_FileShouldExists()
{
string solutionName = "MySolution";
string content = "<Project></Project>";
var directoryBuildPropsFile = new DirectoryBuildPropsFile(DotnetProjectFile.Create(content));
var directoryBuildPropsFile = new DirectoryBuildPropsFile(content);

_solutionFileStructureBuilderFactory.Create(solutionName)
.AddDirectoryBuildProps(directoryBuildPropsFile)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public void ArtifactsOutputEnabled_ForEmptyFile_ReturnFalse()
</Project>
""";

var directoryPackagesPropsFile = new DirectoryBuildPropsFile(DotnetProjectFile.Create(input));
var directoryPackagesPropsFile = new DirectoryBuildPropsFile(input);

bool actual = directoryPackagesPropsFile.ArtifactsOutputEnabled();

Expand All @@ -38,7 +38,7 @@ public void ArtifactsOutputEnabled_ForEnabled_ReturnTrue()
</Project>
""";

var directoryPackagesPropsFile = new DirectoryBuildPropsFile(DotnetProjectFile.Create(input));
var directoryPackagesPropsFile = new DirectoryBuildPropsFile(input);

bool actual = directoryPackagesPropsFile.ArtifactsOutputEnabled();

Expand All @@ -56,7 +56,7 @@ public void ArtifactsOutputEnabled_ForDisabled_ReturnTrue()
</Project>
""";

var directoryPackagesPropsFile = new DirectoryBuildPropsFile(DotnetProjectFile.Create(input));
var directoryPackagesPropsFile = new DirectoryBuildPropsFile(input);

bool actual = directoryPackagesPropsFile.ArtifactsOutputEnabled();

Expand All @@ -74,7 +74,7 @@ public void SetArtifactsOutput_ForEmptyFile_CreateExpectedString()
</Project>
""";

var directoryPackagesPropsFile = new DirectoryBuildPropsFile(DotnetProjectFile.CreateEmpty());
var directoryPackagesPropsFile = DirectoryBuildPropsFile.CreateEmpty();

directoryPackagesPropsFile.SetArtifactsOutput(true);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,56 @@ namespace Kysect.DotnetProjectSystem.Tests.Projects;

public class DotnetProjectFilePropertiesTests
{
private readonly XmlDocumentSyntaxFormatter _formatter;
private readonly XmlDocumentSyntaxFormatter _formatter = new();

public DotnetProjectFilePropertiesTests()
[Fact]
public void GetProperties_EmptyFile_ReturnEmptyCollection()
{
var sut = DotnetProjectFile.CreateEmpty();

IReadOnlyCollection<DotnetProjectProperty> values = sut.Properties.GetProperties("Property");

values.Should().BeEmpty();
}

[Fact]
public void GetProperties_OneProperty_ReturnCollectionWithOneElement()
{
const string content = """
<Project>
<PropertyGroup>
<Property>Value</Property>
</PropertyGroup>
</Project>
""";
var sut = DotnetProjectFile.Create(content);

IReadOnlyCollection<DotnetProjectProperty> values = sut.Properties.GetProperties("Property");

values.Should().HaveCount(1);
}

[Fact]
public void GetProperties_TwoProperty_ReturnExpectedCollection()
{
_formatter = new XmlDocumentSyntaxFormatter();
var expected = new DotnetProjectProperty[]
{
new DotnetProjectProperty("Property", "Value1"),
new DotnetProjectProperty("Property", "Value2"),
};
const string content = """
<Project>
<PropertyGroup>
<Property>Value1</Property>
<Property>Value2</Property>
</PropertyGroup>
</Project>
""";
var sut = DotnetProjectFile.Create(content);

IReadOnlyCollection<DotnetProjectProperty> values = sut.Properties.GetProperties("Property");

values.Should().BeEquivalentTo(expected);
}

[Fact]
Expand Down Expand Up @@ -195,4 +240,36 @@ public void RemoveProperty_ProjectWithProperty_PropertyMustBeRemoved()

projectFile.ToXmlString(_formatter).Should().Be(expected);
}

[Fact]
public void GetEnableDefaultItemsOrDefault_ProjectFileSetTrue_ReturnTrue()
{
const string input = """
<Project>
<PropertyGroup>
<EnableDefaultItems>true</EnableDefaultItems>
</PropertyGroup>
</Project>
""";

DotnetProjectFile projectFile = DotnetProjectFile.Create(input);

projectFile.Properties.GetEnableDefaultItemsOrDefault().Should().BeTrue();
}

[Fact]
public void GetEnableDefaultItemsOrDefault_ProjectFileSetFalse_ReturnFalse()
{
const string input = """
<Project>
<PropertyGroup>
<EnableDefaultItems>false</EnableDefaultItems>
</PropertyGroup>
</Project>
""";

DotnetProjectFile projectFile = DotnetProjectFile.Create(input);

projectFile.Properties.GetEnableDefaultItemsOrDefault().Should().BeFalse();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,18 @@ public void Create_FromStringWithXmlTag_ThrowExceptionAboutInvalidTag()
argumentException.Should().NotBeNull();
}

[Fact]
public void Create_NotProjectNode_ThrowException()
{
const string input = "<SomeNode />";

FluentActions
.Invoking(() => DotnetProjectFile.Create(input))
.Should()
.Throw<ArgumentException>()
.WithMessage("XML root must be Project");
}

[Fact]
public void GetProjectNode_ForEmptyNode_ReturnProjectNode()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,17 @@ public void Format_FileWithEmptyLine_ExpectNoChanges()
Validate(input, input);
}

[Fact]
public void Format_OneEmptyXmlElement_NoChanges()
{
var input = """
<Project />
""";

Validate(input, input);
}


private void Validate(string input, string expected)
{
XmlDocumentSyntax document = Parser.ParseText(input);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,20 @@ public class ProjectFileStructureBuilder
public string ProjectName { get; }

public ProjectFileStructureBuilder(string projectName)
: this(projectName, DotnetProjectFile.CreateEmpty())
{
ProjectName = projectName;
_files = new List<SolutionStructureElement>();
_projectFile = DotnetProjectFile.CreateEmpty();
}

public ProjectFileStructureBuilder(string projectName, string projectFileContent)
: this(projectName, DotnetProjectFile.Create(projectFileContent))
{
}

public ProjectFileStructureBuilder(string projectName, DotnetProjectFile projectFileContent) : this(projectName)
public ProjectFileStructureBuilder(string projectName, DotnetProjectFile projectFileContent)
{
ProjectName = projectName;
_projectFile = projectFileContent;
_files = new List<SolutionStructureElement>();
}

public ProjectFileStructureBuilder SetContent(string projectFileContent)
Expand Down
Loading
Loading