ADotNet is a.NET library that enables software engineers on the .NET platform to develop AzureDevOps pipelines and Git Actions in C#.
There's an issue today with developing Azure DevOps pipelines and Git Actions with YAML. The technology/language can be challenging to learn and predict the available options for orchestrating build steps. ADotNet presents a solution to pipeline tasks as C# models, predefined, with all the options available to orchestrate a pipeline without searching for the available options on the documentation websites.
Here's how this library works. Let's assume you want to write a task in your pipeline that restores packages for your ASP.NET Core project. Today, engineers write the following command in YAML:
- task: DotNetCoreCLI@2
displayName: 'Restore'
inputs:
command: 'restore'
feedsToUse: 'select'
The problem with the above YAML code is that it's not that easy to remember. Even while staring at it, I just can't seem to remember DotNetCoreCLI@2
and what does this mean to someone who is a full-stack engineer trying to get off the ground as fast as possible? Here's how the very same code above would look like in ADotNet:
new DotNetExecutionTask
{
DisplayName = "Restore",
Inputs = new DotNetExecutionTasksInputs
{
Command = Command.restore,
FeedsToUse = Feeds.select
}
}
The options here are available with the power of strongly typed options and Enums. You don't have to think about what needs to go there. The syntax is already directing you towards the options you need to get going with building your pipeline.
Here's how this library works. Let's assume you want to write a task in your pipeline that uses a particular version for your ASP.NET Core project. Today, engineers write the following command in YAML:
- name: Setup .Net
uses: actions/setup-dotnet@v1
with:
dotnet-version: 6.0.100-rc.1.21463.6
include-prerelease: true
The problem with the above YAML code is that it's not that easy to remember. Even while I'm staring at it, I just can't seem to remember actions/setup-dotnet@v1
and what does this mean to someone who is a full-stack engineer trying to get off the ground as soon as possible? Here's how the very same code above would look like in ADotNet:
new SetupDotNetTaskV1
{
Name = "Setup .Net",
TargetDotNetVersion = new TargetDotNetVersion
{
DotNetVersion = "6.0.100-rc.1.21463.6",
IncludePrerelease = true
}
}
The options here are available with the power of strongly typed options and Enums. You don't have to think about what needs to go there. It's already directing you towards the options you need to get going with building your pipeline.
This library relies heavily on YamlDotNet which is an amazing .NET library developed by Antoine Aubry along with so many other amazing contributors who made C# to YAML possible.
The library also leverages native .NET System.IO.File
functionality to write files to a destination of the consumer's choosing.
The library's architecture follows The Standard. Breaking it's capabilities into brokers, services and clients. Here's a low-level architecture view of how it works:
The abstraction of the dependencies should allow a future expansion and pluggability for any other C# to YAML components easily.
Here's something I'm using in my open source OtripleS project which is built in ASP.NET Core 6.0:
var adoClient = new ADotNetClient();
var aspNetPipeline = new AspNetPipeline
{
TriggeringBranches = new List<string>
{
"master"
},
VirtualMachinesPool = new VirtualMachinesPool
{
VirtualMachineImage = VirtualMachineImages.Windows2019
},
ConfigurationVariables = new ConfigurationVariables
{
BuildConfiguration = BuildConfiguration.Release
},
Tasks = new List<BuildTask>
{
new UseDotNetTask
{
DisplayName = "Use DotNet 6.0",
Inputs = new UseDotNetTasksInputs
{
Version = "6.0.100-preview.3.21202.5",
IncludePreviewVersions = true,
PackageType = PackageType.sdk
}
},
new DotNetExecutionTask
{
DisplayName = "Restore",
Inputs = new DotNetExecutionTasksInputs
{
Command = Command.restore,
FeedsToUse = Feeds.select
}
},
new DotNetExecutionTask
{
DisplayName = "Build",
Inputs = new DotNetExecutionTasksInputs
{
Command = Command.build,
}
},
new DotNetExecutionTask
{
DisplayName = "Test",
Inputs = new DotNetExecutionTasksInputs
{
Command = Command.test,
Projects = "**/*Unit*.csproj"
}
},
new DotNetExecutionTask
{
DisplayName = "Publish",
Inputs = new DotNetExecutionTasksInputs
{
Command = Command.publish,
PublishWebProjects = true
}
}
}
};
adoClient.SerializeAndWriteToFile(aspNetPipeline, "../../azure-pipelines.yaml");
And here's the YAML output of this code:
trigger:
- master
pool:
vmImage: 'ubuntu-latest'
variables:
buildConfiguration: 'Release'
steps:
- task: UseDotNet@2
displayName: 'Use DotNet 6.0'
inputs:
packageType: 'sdk'
version: '6.0.100-preview.3.21202.5'
includePreviewVersions: true
- task: DotNetCoreCLI@2
displayName: 'Restore'
inputs:
command: 'restore'
feedsToUse: 'select'
- task: DotNetCoreCLI@2
displayName: 'Build'
inputs:
command: 'build'
- task: DotNetCoreCLI@2
displayName: 'Test'
inputs:
command: 'test'
projects: '**/*Unit*.csproj'
- task: DotNetCoreCLI@2
displayName: 'Publish'
inputs:
command: 'publish'
publishWebProjects: true
And finally, here's the result:
Here's something I'm using in my open source OtripleS project which is built in ASP.NET Core 6.0:
var aDotNetClient = new ADotNetClient();
var githubPipeline = new GithubPipeline
{
Name = "Github",
OnEvents = new Events
{
Push = new PushEvent
{
Branches = new string[] { "master" }
},
PullRequest = new PullRequestEvent
{
Branches = new string[] { "master" }
}
},
Jobs = new Dictionary<string, Job>
{
{
"build",
new Job
{
RunsOn = BuildMachines.Windows2019,
Steps = new List<GithubTask>
{
new CheckoutTaskV2
{
Name = "Check out"
},
new SetupDotNetTaskV1
{
Name = "Setup .Net",
TargetDotNetVersion = new TargetDotNetVersion
{
DotNetVersion = "6.0.100-rc.1.21463.6",
IncludePrerelease = true
}
},
new RestoreTask
{
Name = "Restore"
},
new DotNetBuildTask
{
Name = "Build"
},
new TestTask
{
Name = "Test"
}
}
}
}
}
};
string buildScriptPath = "../../../../.github/workflows/dotnet.yml";
string directoryPath = Path.GetDirectoryName(buildScriptPath);
if (!Directory.Exists(directoryPath))
{
Directory.CreateDirectory(directoryPath);
}
aDotNetClient.SerializeAndWriteToFile(githubPipeline, path: buildScriptPath);
And here's the YAML output of this code:
name: Github
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
build:
runs-on: windows-2019
steps:
- name: Check out
uses: actions/checkout@v2
- name: Setup .Net
uses: actions/setup-dotnet@v1
with:
dotnet-version: 6.0.100-rc.1.21463.6
include-prerelease: true
- name: Restore
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Test
run: dotnet test --no-build --verbosity normal
And finally, here's the result:
I have intentionally limited some of the capabilities in this library to ensure any contributions go to this repository so everyone can benefit from the updates. For instance, I could've easily made selecting a virtual machine as a string input to allow for anyone to pass in whatever vm they need. But the problem with that is for those who will need the same thing and have to do the same research to find the right VM for their build.
I'm intentionally making my library less usable to ensure this level of hive mindset is reflected in our changes in here.
If you have any suggestions, comments or questions, please feel free to contact me on: