Integrating .NET Obfuscation with MSBuild and NuGet
Most .NET obfuscators integrate with the build pipeline as a post-build step: compile the project, run the obfuscator as a separate tool on the output, then produce the final artifact. This model has a specific failure mode that matters in CI/CD: forgetting to run the obfuscation step is a silent failure. The build succeeds, the release ships, and the protection was never applied. Nothing in the pipeline indicates that anything went wrong.
ArmDot integrates differently. Two NuGet packages embed obfuscation directly into the MSBuild process. dotnet build obfuscates. dotnet publish obfuscates. There is no separate tool invocation and no way to accidentally skip protection while running a normal build. Protection configuration lives in C# source files as attributes, under version control, visible in code review.
If you are setting up GitHub Actions or Azure DevOps integration, start here - the platform-specific tutorials assume this configuration is already in place.
The two NuGet packages
ArmDot's build integration requires two NuGet packages:
ArmDot.Client provides the C# attributes you apply to assemblies, types, and methods to specify what protection to apply. [VirtualizeCode], [ObfuscateNames], [HideStrings], [ObfuscateControlFlow], [ProtectEmbeddedResources] - all come from this package. It has no runtime dependency: ArmDot removes all attributes from the output assembly after processing, so the protected binary does not reference ArmDot.Client.dll at runtime.
ArmDot.Engine.MSBuildTasks provides the MSBuild task that performs the actual obfuscation during the build. Build-time dependency only - it does not appear in the published output.
dotnet add package ArmDot.Client
dotnet add package ArmDot.Engine.MSBuildTasksThe MSBuild target
Installing the packages alone does not enable obfuscation. The .csproj needs an MSBuild target that invokes the obfuscation task at the right point in the build:
<Target Name="Protect" AfterTargets="AfterCompile" BeforeTargets="BeforePublish">
<ItemGroup>
<Assemblies
Include="$(ProjectDir)$(IntermediateOutputPath)$(TargetFileName)"
CreatePDB="true" />
</ItemGroup>
<ArmDot.Engine.MSBuildTasks.ObfuscateTask
Inputs="@(Assemblies)"
ReferencePaths="@(_ResolveAssemblyReferenceResolvedFiles->'%(RootDir)%(Directory)')"
SkipAlreadyObfuscatedAssemblies="true"
LicenseFile="$(ARMDOT_LICENSE_FILE_PATH)"
/>
</Target>Each parameter matters:
AfterTargets="AfterCompile" runs the task after the compiler has produced the assembly in the intermediate output directory. BeforeTargets="BeforePublish" runs it before the publish step packages the output. The obfuscation happens on the intermediate assembly, which is then picked up by subsequent build steps including packaging, publishing, and signing.
SkipAlreadyObfuscatedAssemblies="true" prevents double-obfuscation when the build evaluates the target more than once - common in multi-configuration builds and incremental build scenarios.
CreatePDB="true" on the assembly item tells ArmDot to generate a map file alongside the obfuscated assembly. Without this map file, exception stack traces from production show obfuscated names that cannot be correlated back to original source. Keep the map file with your build artifacts but do not distribute it - it contains the mapping between original and obfuscated names.
LicenseFile="$(ARMDOT_LICENSE_FILE_PATH)" is optional. If no license key is provided, ArmDot works in demo mode - fully functional, but protected assemblies stop working after two weeks. In local development with a purchased license, ArmDot finds the license key from its default installation path automatically - you do not need to set this parameter or the environment variable. It becomes relevant in CI/CD, where the license is not installed on the build agent and needs to be provided through the pipeline. The CI/CD license setup is covered below.
Protection configuration with C# attributes
With the MSBuild target in place, you specify what to protect using attributes from ArmDot.Client. The attributes live in your source code, travel with the code under version control, and appear in pull requests alongside the logic they protect.
Assembly-wide baseline:
// In AssemblyInfo.cs or any source file
[assembly: ArmDot.Client.ObfuscateNames]
[assembly: ArmDot.Client.HideStrings]
[assembly: ArmDot.Client.ProtectEmbeddedResources]This renames all non-public identifiers, encrypts all string literals, and protects embedded resources throughout the assembly. For most commercial applications this is the right starting point - it costs nothing meaningful at runtime and eliminates casual inspection immediately.
Virtualization on high-value methods:
public class LicenseManager
{
[ArmDot.Client.VirtualizeCode]
public bool ValidateSerialKey(string key)
{
// Original CIL replaced by custom bytecode
// No decompiler can reconstruct this logic
}
[ArmDot.Client.VirtualizeCode]
public string GenerateHardwareId()
{
// Hardware fingerprinting logic - virtualized
}
}Virtualization is the strongest available protection but carries a runtime performance cost. Apply it to the methods where the logic itself is the asset: license validation, serial key checking, hardware ID generation, proprietary algorithms. Not to compute-intensive inner loops.
Layered configuration in a real project:
// Assembly-wide baseline - applied everywhere
[assembly: ArmDot.Client.ObfuscateNames]
[assembly: ArmDot.Client.HideStrings]
[assembly: ArmDot.Client.ProtectEmbeddedResources]
public class ActivationService
{
[ArmDot.Client.VirtualizeCode]
public ActivationResult Activate(string key, string hardwareId)
{
// License activation - strongest protection
}
[ArmDot.Client.ObfuscateControlFlow]
public bool IsTrialExpired()
{
// Trial enforcement - control flow scrambled
}
}
public class ReportEngine
{
// No extra attributes - assembly-wide renaming and
// string encryption already apply here
public Report Generate(DataSet input) { ... }
[ArmDot.Client.VirtualizeCode(Enable = false)]
public void RenderPdf(Report report)
{
// Explicitly excluded from any class-level virtualization
// if applied later - performance-critical rendering loop
}
}This layered approach is the practical configuration for commercial ISV applications: assembly-wide renaming and string encryption as the baseline, with virtualization and control flow obfuscation applied precisely to the methods where the logic is the competitive advantage.
Why attributes are the preferred configuration approach
ArmDot supports two configuration models. Attributes in C# source code are the primary approach for standard .NET projects. A separate XML-based project format (.armdotproj) is available for scenarios where attributes are not practical - Unity projects being the main example, since Unity does not use standard MSBuild/NuGet integration.
For standard .NET projects, attributes have a concrete advantage over any external configuration file, whether from ArmDot or a competing tool.
The practical problem with external configuration is refactoring. When a developer renames UserAuthentication to IdentityVerification and the configuration file still references UserAuthentication, the class silently loses its protection. No build error, no warning - the obfuscator simply does not find the class and skips it. The developer who renamed the class may not even know the configuration file exists.
Attribute-based configuration eliminates this class of failure. The [VirtualizeCode] attribute on a method travels with the method when it is moved between classes, renamed, or refactored. It appears in the same diff as the code change. A reviewer looking at a pull request that adds a new license validation method sees immediately whether the appropriate protection attribute was applied - and can ask why if it was not.
For teams that refactor actively, this is the difference between protection that silently degrades over time and protection that stays aligned with the code it covers.
Passing the license key in CI/CD
In local development, ArmDot reads the license key from its default installation path. In CI/CD, the license key is not installed on the build agent - it needs to be provided securely through the pipeline.
The pattern is consistent across all CI/CD platforms:
- Store the license key as a secret in the CI/CD platform's secrets management (encrypted secrets in GitHub Actions, secure files in Azure DevOps)
- In the pipeline, write the secret to a temporary file on the build agent
- Set the
ARMDOT_LICENSE_FILE_PATHenvironment variable to the path of that file - Run
dotnet build- the MSBuild task reads the license from the path via$(ARMDOT_LICENSE_FILE_PATH)in the project file
The implementation details differ between platforms. GitHub Actions uses encrypted secrets and a bash step to write the key to a temp file. Azure DevOps uses the DownloadSecureFile task and passes the path directly. Both approaches are covered in the dedicated platform tutorials:
Multi-project solutions
For solutions with multiple projects, add the MSBuild target to each project that should be obfuscated. If project A references project B and both are obfuscated, SkipAlreadyObfuscatedAssemblies="true" ensures project B's assembly is not processed twice when project A's build picks it up as a reference.
The ReferencePaths parameter tells ArmDot where to find referenced assemblies for name resolution. The expression @(_ResolveAssemblyReferenceResolvedFiles->'%(RootDir)%(Directory)') passes MSBuild's resolved reference paths, which covers most scenarios automatically.
Verifying the integration
After adding the packages and target, rebuild in Release configuration. The build output should include ArmDot log lines:
[ArmDot] Names obfuscation started
[ArmDot] Names obfuscation finished
[ArmDot] Conversion started for method ...
[ArmDot] Writing protected assembly to ...If you see warning ARMDOT0003: No methods to protect, the MSBuild target is running but no protection attributes have been applied. Add [assembly: ArmDot.Client.ObfuscateNames] to any source file to confirm the full pipeline works end to end.
Open the protected assembly in ILSpy to verify the output. Non-public members should show obfuscated names. Virtualized methods should show the VM interpreter loop rather than the original logic. String literals should be absent from the decompiled output.
Back to: .NET Obfuscation in CI/CD →
ArmDot NuGet packages
ArmDot.Client and ArmDot.Engine.MSBuildTasks are available on NuGet.org. The full attribute reference and all MSBuild task parameters are in the ArmDot documentation. Free trial available - protected assemblies stop working after two weeks. A single developer license is $499.
