.NET Project Automation Secrets: Full Guide to One-Click Version Update and Publish Scripts

.NET Project Automation Secrets: Full Guide to One-Click Version Update and Publish Scripts

This article details how to use PowerShell scripts and batch files to automate version updates and one-click publishing in .NET Avalonia UI projects. First, it explains the setup and modification of PowerShell execution policies to ensure scripts can run properly. Then, it introduces methods for adding scripts in Visual Studio pre-build events to automatically update version numbers, as well as using batch files to publish applications on multiple platforms. Finally, a PowerShell script example is provided that can automatically update program version information based on Git tags. These methods can improve the development efficiency and convenience of the publish process for .NET projects.

Last updated 2/21/2025 10:30 PM
沙漠尽头的狼
11 min read
Category
Avalonia UI
Tags
.NET C# Avalonia UI Visual Studio Publish

In the journey of .NET development, efficient version management and a streamlined publishing process are key to improving development efficiency and project quality. Today, we will dive deep into how to use scripts to automatically update program versions and perform one-click publishing for .NET Avalonia UI projects (of course, these methods also apply to other .NET projects), making your development process smoother and more efficient.

PowerShell Script Execution Policy Check

Before running PowerShell scripts, it is crucial to understand and correctly set the execution policy. The execution policy determines whether PowerShell allows scripts to run and what types of scripts can run. You can use the following simple command to check the current execution policy:

Get-ExecutionPolicy

Different execution policy values have different meanings and impose varying levels of restrictions on script execution:

  • Restricted: This is the default policy, which acts like a strict gatekeeper, not allowing any scripts to run. You can only enter commands line by line in the PowerShell console. This provides a certain level of system security, but it is undoubtedly a constraint for developers who need to execute scripts in batches.
  • AllSigned: Only allows scripts signed by a trusted publisher to run. This is like only allowing people with a specific pass to enter, ensuring the reliability of the script source, but it also increases the threshold for script usage because not all scripts are signed.
  • RemoteSigned: Locally created scripts can run directly, while scripts downloaded from the internet must be signed by a trusted publisher to run. This is a balanced setting between security and convenience, allowing the flexibility of local development while ensuring security for scripts downloaded from the network.
  • Unrestricted: Allows all scripts to run, but scripts downloaded from the internet will prompt for confirmation before running. This gives developers great freedom but also comes with certain security risks because scripts that have not been strictly checked may carry malicious code.
  • Bypass: Does not block any script execution and does not show security prompts. This is a very open setting, generally used only in specific, secure, and controlled environments; otherwise, it may expose the system to security threats.

By default, the execution policy is Restricted, which may cause write-back failures and prompt "cannot write to stream". For example, when you try to run a script that needs to modify file contents, you will encounter permission issues.

Modifying the Execution Policy

To run scripts smoothly, you need to choose an appropriate execution policy according to your needs. If you want to ensure a certain level of security while easily running local scripts and network-downloaded scripts that have undergone certain security checks, setting the execution policy to RemoteSigned is a good choice.

However, note that modifying the execution policy may require administrator privileges, so be sure to run PowerShell as an administrator, then use the following command to modify it:

Set-ExecutionPolicy RemoteSigned

In this way, locally created scripts can run directly, while scripts downloaded from the internet need to pass signature verification, effectively reducing security risks.

Verifying the Modification Result

After modifying the execution policy, to ensure the setting has taken effect, you can use the Get-ExecutionPolicy command again to verify whether the modification was successful. When you see the output result is RemoteSigned, it means you have successfully modified the execution policy, and you should be able to run .ps1 scripts normally.

VS Publishing

In Visual Studio, we can achieve automatic updating of project versions by adding scripts in the pre-build events. The specific operation is as follows: find the "Build Events" tab in the project properties, and add the following command in the "Pre-build event command line":

powershell -ExecutionPolicy Bypass -File "UpdateAssemblyVersion.ps1" -AssemblyInfoFile "GlobalAssembly.cs" -Configuration "$(ConfigurationName)" -Platform "$(PlatformName)"

The parameters here have clear meanings:

  • ConfigurationName: Represents the active solution configuration, commonly like Debug and Release. Under different configurations, we may want to generate programs with different version numbers to distinguish between development and official release versions.
  • PlatformName: Represents the active solution platform, such as AnyCPU, x86, x64, etc. Different platforms may require different version identifiers to meet compatibility and performance requirements.

Batch File Publishing

Sometimes, we may want to publish the program without opening Visual Studio, and be able to conveniently extend to publishing for multiple platforms such as Windows, Linux, and macOS. In this case, batch files come in handy. Here is an example batch file:

@echo off
setlocal enabledelayedexpansion

rem Traverse files in the GlobalAssemblies directory to execute PowerShell scripts
for %%f in (GlobalAssemblies\*) do (
    powershell -ExecutionPolicy Bypass -File "UpdateAssemblyVersion.ps1" -AssemblyInfoFile "%%f" -Configuration "Release" -Platform "x64"
    rem Check if the PowerShell script executed successfully
    if !errorlevel! neq 0 (
        echo Failed to update assembly version for file %%f!
        endlocal
        exit /b !errorlevel!
    )
)

rem Set the publish path
set PUBLISH_PATH=publish\win-x64

rem Define project information array, format: project name, project path (only project directory), relative publish directory name
set "projects=码坊工具箱,src\CodeWF.Toolbox.Desktop,codewf Avalonia发布测试,tests\AvaloniaAotDemo,AvaloniaAotDemo"

rem Loop through the project array
for %%a in ("%projects: =","%") do (
    set "current_project=%%~a"
    for /f "tokens=1,2,3 delims=," %%b in ("!current_project!") do (
        set "projectName=%%b"
        set "projectPath=%%c"
        set "relativePublishDir=%%d"

        rem Concatenate the default path of the .pubxml file
        set "pubxmlPath=!projectPath!\Properties\PublishProfiles\FolderProfile-win_x64.pubxml"
        rem Concatenate the current project's publish path
        set "CURRENT_PUBLISH_DIR=!PUBLISH_PATH!\!relativePublishDir!"

        echo "projectName after assignment: !projectName!"
        echo "projectPath after assignment: !projectPath!"
        echo "relativePublishDir after assignment: !relativePublishDir!"
        echo "pubxmlPath after assignment: !pubxmlPath!"
        echo "current publish dir after assignment: !CURRENT_PUBLISH_DIR!"


        echo Publishing !projectName! for win-64...
        rem Clean the publish directory
        if exist "!CURRENT_PUBLISH_DIR!" (
            rd /s /q "!CURRENT_PUBLISH_DIR!"
        )
        rem Create the publish directory
        mkdir /p "!CURRENT_PUBLISH_DIR!" 2>nul
        rem Execute dotnet publish command for AOT publishing, specifying the target framework
        dotnet publish "!projectPath!" /p:PublishProfile="!pubxmlPath!" -f net9.0-windows -o "!CURRENT_PUBLISH_DIR!"
        rem Check the exit code of the dotnet publish command
        if !errorlevel! neq 0 (
            echo Publish of !projectName! failed!
            endlocal
            exit /b !errorlevel!
        )
        rem Check if the publish directory exists
        if exist "!CURRENT_PUBLISH_DIR!" (
            rem Delete debug symbol files
            del "!CURRENT_PUBLISH_DIR!\*.pdb"
        )
        echo Publish of !projectName! succeeded!
        echo Published files of !projectName! are located at: "!CURRENT_PUBLISH_DIR!"
    )
)

endlocal

This batch file first traverses the files in the GlobalAssemblies directory and executes the PowerShell script to update the program version. Then, it defines a project information array including project name, project path, and relative publish directory name. Next, it loops through the project array and performs the publishing operation for each project. During the publishing process, it cleans the publish directory, creates a new publish directory, then executes the dotnet publish command for AOT publishing, specifying the target framework as net9.0-windows. Finally, it checks if the publishing was successful; if so, it deletes the debug symbol files and outputs a success message and the publish path.

PowerShell Script for Automatic Version Update

The following PowerShell script is used to automatically update the program version based on Git tags:

param(
	[Parameter(Mandatory=$true)]
	[string]$AssemblyInfoFile,

	[Parameter(Mandatory=$true)]
	[string]$Configuration,

	[Parameter(Mandatory=$true)]
	[string]$Platform
)

# Check if the file exists
if (-not (Test-Path $AssemblyInfoFile)) {
    throw "Error: File $AssemblyInfoFile does not exist"
}

Write-Output "AssemblyInfoFile: $AssemblyInfoFile"
Write-Output "Configuration: $Configuration"
Write-Output "Platform: $Platform"



# Get the current date and time, generate a formatted timestamp
$currentDateTime = Get-Date
# Get the current branch, check out the tag creation time [base time]
$strBranchCreateTime = git log -1 --format=%ai $latest_tag
$dateBranchCreateTime = [DateTime]::Parse($strBranchCreateTime)
# Total minutes of the time difference
$timeDifMinutes = [math]::Round(($currentDateTime - $dateBranchCreateTime).TotalMinutes)
# Format version number as x.x.65535.65535
$yy = [math]::Floor($timeDifMinutes / 65535)
$thirdInfo = $timeDifMinutes - $yy * 65535
# Read file content
$content = Get-Content -Path $AssemblyInfoFile -Encoding Unicode
# Use regex to get the value of AssemblyProduct
$productPattern = '^\[assembly: AssemblyProduct\("([^"]+)"\)\]'
$product = ""
# Find the line containing AssemblyProduct and match the regex
foreach($line in $content){
	if($line -match $productPattern){
		$product = $matches[1]
		break # Exit loop after finding match
	}
}
$items = $product -split '_'
$product = $items[0]
# Current nearest tag, checkout hash value
$currentCommitHash = git describe --always --tag --long
$items = $currentCommitHash.Split('-')
$hashInfo = $items[$items.Length - 1]
$firstFileVersion = "0.0"
$secondFileVersion = "100"
if($items.Length -eq 3)
{
	$firstFileVersion = $items[0].Replace("v","");
	$firstFileItems = $firstFileVersion.split('.');
	$firstFileVersion = $firstFileItems[0] + "." + $firstFileItems[1]
	$secondFileVersion = $items[1] + "$yy".PadLeft(2,'0')
}

$platformInfo = ""
if ($Configuration -eq "Debug") {
	$platformInfo = "D"
} elseif ($Configuration -eq "Release") {
	$platformInfo = "R"
} else {
	$platformInfo = "A"
}
switch ($Platform) {
	"x86" {
		$platformInfo += "-86"
	}
	"x64" {
		$platformInfo += "-64"
	}
	"ARM" {
		$platformInfo += "-ARM"
	}
	"AnyCPU" {
		$platformInfo += "-AnyCPU"
	}
	default {
		$platformInfo += "-Unknow"
	}
}

# Define new AssemblyProduct and AssemblyFileVersion
$strDayInfo = $currentDateTime.ToString("yyyyMMddHHmm")
$newAssemblyProduct = "$product" + "_" + "$platformInfo" + "_" + "$hashInfo" + "_" + "$strDayInfo"
if($secondFileVersion -eq "000")
{
	$secondFileVersion = "0"
}
$newAssemblyFileVersion = "$firstFileVersion" + "." + "$secondFileVersion" + "." + "$thirdInfo"

# Update AssemblyProduct
$content = $content -replace '^\[assembly: AssemblyProduct\(".*"\)\]', "[assembly: AssemblyProduct(`"$newAssemblyProduct`")]"

# Update AssemblyVersion
$content = $content -replace '^\[assembly: AssemblyVersion\(".*"\)\]', "[assembly: AssemblyVersion(`"$newAssemblyFileVersion`")]"

# Update AssemblyFileVersion
$content = $content -replace '^\[assembly: AssemblyFileVersion\(".*"\)\]', "[assembly: AssemblyFileVersion(`"$newAssemblyFileVersion`")]"

# Write the updated content to the AssemblyInfo.cs file
Set-Content -Path $AssemblyInfoFile -Value $content -Encoding Unicode

This script first receives three parameters: AssemblyInfoFile (assembly information file path), Configuration (configuration name), and Platform (platform name). Then, it checks if the file exists; if not, it throws an error. Next, by obtaining the current date and time, Git tag creation time, and other information, it calculates the various parts of the version number. Based on the configuration and platform information, it generates new AssemblyProduct and AssemblyFileVersion. Finally, it uses regex to replace the old version information in the file and writes the updated content to the AssemblyInfo.cs file, achieving automatic program version updates.

Through these scripts and methods, we can greatly improve the development efficiency of .NET projects and automate version management and publishing processes. I hope this content helps you in your development work, making you more proficient on the journey of .NET development.

The above example scripts can be found in the open-source project: dotnet9/CodeWF.Toolbox: Cross platform toolbox developed using Avalonia

Keep Exploring

Related Reading

More Articles
Same category / Same tag 8/9/2025

Lang.Avalonia: Avalonia multi-language solution, seamlessly supports three formats: Resx/XML/JSON

This is a multi-language management library designed specifically for the Avalonia framework. It reconstructs multi-language support logic through a plugin-based architecture, not only supporting traditional Resx resource files but also adding support for XML and JSON formats, while providing type-safe resource references and dynamic language switching, making multi-language development simpler and more efficient.

Continue Reading