Tutorials

Extensions

15 min read · Prerequisite: MSI Basics · Wraps demos 29-34

What you'll learn

FalkForge ships 8 built-in extensions for common server and infrastructure tasks. No NuGet packages to install — they're part of the framework. This tutorial walks through each one.

Each extension follows the same pattern: create an instance, configure it with a fluent API, validate, then compile alongside your MSI package. The SDK extension pipeline wires everything together at build time.

Firewall Rules

The FirewallExtension adds Windows Firewall rules that are installed and removed with your MSI. Open demo/29-ext-firewall/Program.cs to follow along.

Adding a rule

var firewall = new FirewallExtension();

firewall.AddRule(rule => rule
    .Id("AllowHttp")
    .Name("My App HTTP")
    .Description("Allow inbound HTTP on port 8080")
    .Protocol(FirewallProtocol.Tcp)
    .Port("8080")
    .Direction(FirewallDirection.Inbound)
    .Action(FirewallRuleAction.Allow)
    .Profile(FirewallProfile.All));

Each rule needs an Id, a display Name, and the network settings: protocol, port, direction, and action. The Profile controls which network profiles the rule applies to — Domain, Private, Public, or All.

Validation

var errors = firewall.ValidateRules();

Always validate before compiling. Error codes start with FWL (FWL001–FWL004).

Deep dive: How MSI installs and removes firewall rules

FalkForge emits custom MSI tables that the extension processes during installation. The firewall rules are created via the Windows Firewall COM API (INetFwPolicy2) during the InstallFinalize phase. On uninstall, the same rules are removed by matching their Id.

This means rules are transactional — if the install rolls back, the rules are cleaned up automatically. Rules persist across reboots because they're stored in Windows Firewall's own configuration, not in the MSI database.

IIS Configuration

The IisExtension manages application pools, web sites, bindings, and certificates. Open demo/30-ext-iis/Program.cs.

Defining an app pool

var iis = new IisExtension();

var appPool = iis.DefineAppPool(pool => pool
    .Id("DemoPool")
    .Name("DemoPool")
    .NoManagedCode()
    .PipelineMode(ManagedPipelineMode.Integrated)
    .Identity(AppPoolIdentityType.ApplicationPoolIdentity));

DefineAppPool returns an AppPoolRef — a typed reference handle you pass to web sites. NoManagedCode() sets the CLR version to empty, which is correct for modern .NET apps running via the ASP.NET Core Module.

Adding a web site

iis.AddWebSite(site => site
    .Id("DemoSite")
    .Description("Demo Web Site")
    .Directory("[INSTALLDIR]wwwroot")
    .AppPool(appPool)
    .Binding(8080)
    .AutoStart(true));

The AppPool(appPool) call uses the typed AppPoolRef returned by DefineAppPool. This ensures type safety — you can't accidentally pass a string that doesn't match an existing pool. Binding(8080) sets up an HTTP binding on that port.

Deep dive: IIS installation order and dependencies

IIS configuration happens in a specific order during MSI execution:

  1. App pools are created first. They must exist before any site can reference them.
  2. Web sites are created next, bound to their app pools.
  3. Bindings are applied to sites. If a certificate is referenced, it must already be installed.

On uninstall, the order reverses: bindings and sites are removed before app pools. FalkForge handles this sequencing automatically via the AppPoolRef and CertificateRef typed references. Error codes range from IIS001 to IIS011.

SQL Server

The SqlExtension creates databases and runs SQL scripts during installation. Open demo/31-ext-sql/Program.cs.

Defining a database

var sql = new SqlExtension();

var dbRef = sql.DefineDatabase(db => db
    .Id("AppDb")
    .Server(".")
    .Database("DemoDb")
    .CreateOnInstall()
    .ConfirmOverwrite());

DefineDatabase returns a Result<SqlDatabaseRef>. The Server(".") targets the local instance. CreateOnInstall() creates the database during installation. ConfirmOverwrite() prompts the user if the database already exists.

Running scripts

var script = new SqlScriptBuilder()
    .Id("CreateSchema")
    .Database(dbRef.Value)
    .SourceFile("payload\\schema.sql")
    .ExecuteOnInstall()
    .Sequence(1)
    .Build();

sql.Scripts.Add(script.Value);

Scripts reference a database via the typed SqlDatabaseRef. The Sequence number controls execution order when you have multiple scripts. ExecuteOnInstall() runs the script during installation only.

Deep dive: SQL script execution order and error handling

Scripts execute in Sequence order within each database. If a script fails, the MSI installation rolls back. FalkForge validates scripts at compile time for basic issues (SQL001–SQL013).

The connection string is built from the Server and Database values. You can also pass MSI property references like [SQL_SERVER] to let the user configure the server at install time.

Both DefineDatabase and SqlScriptBuilder.Build() return Result<T>. Always check IsFailure before accessing .Value.

.NET Runtime Detection

The DotNetExtension detects installed .NET runtimes. Combine it with launch conditions to block installation when a required runtime is missing. Open demo/32-ext-dotnet/Program.cs.

Searching for a runtime

var dotnet = new DotNetExtension();

var search = dotnet.SearchForRuntime()
    .RuntimeType(DotNetRuntimeType.Runtime)
    .Platform(DotNetPlatform.X64)
    .MinVersion(new Version(8, 0, 0))
    .Variable("DOTNET8_FOUND")
    .Build();

This searches for the .NET 8+ runtime on x64. The result is stored in the MSI property DOTNET8_FOUND.

Using as a launch condition

package.Require("DOTNET8_FOUND",
    ".NET 8.0 Runtime (x64) or later is required.");

Require adds a launch condition. If DOTNET8_FOUND is empty at install time, the MSI shows the error message and aborts. The user must install .NET 8 first.

Deep dive: .NET version detection strategies

FalkForge uses two detection strategies:

  1. Registry probing — checks HKLM\SOFTWARE\dotnet\Setup\InstalledVersions for registered runtimes. Fast and reliable for officially installed runtimes.
  2. Filesystem probing — scans %ProgramFiles%\dotnet\shared for runtime directories. Catches standalone deployments that may not register in the registry.

You can target different runtime types: Runtime (base), AspNetCore, or WindowsDesktop. Platform options are X64, X86, and Arm64. Error codes are NET001–NET003.

Utility Operations

The UtilExtension bundles six common operations into one extension. Open demo/33-ext-util/Program.cs for the XML configuration example.

XmlConfig — modify XML files during install

var util = new UtilExtension();

var config = new XmlConfigBuilder()
    .Id("SetMode")
    .File("[INSTALLDIR]app.config")
    .XPath("//appSettings/add[@key='Mode']")
    .SetAttribute("value", "production")
    .Sequence(1)
    .Build();

util.XmlConfig.Add(config.Value);

Target any XML file with an XPath expression. SetAttribute modifies an existing attribute. The change is applied during install and reverted on uninstall.

Other utilities

Validation error codes are XCF001–XCF009.

Deep dive: When to use QuietExec vs custom actions

QuietExec is best for simple, one-shot commands that don't need rollback logic. It runs a command line silently and captures the exit code. Examples: registering a COM server, running netsh, or calling a configuration tool.

Custom actions give you full control. Use them when you need:

  • Rollback behavior — undo the action if the install fails.
  • Conditional execution — run only when a specific MSI property is set.
  • Complex logic — multiple steps, error handling, or data from MSI properties.

Rule of thumb: if a single command with no rollback is enough, use QuietExec. If you need transactional behavior, write a custom action.

Dependency Provider/Consumer

The DependencyExtension implements WiX-compatible reference counting between MSI packages. Open demo/34-ext-dependency/Program.cs.

Registering as a provider

var dependency = new DependencyExtension();

dependency.Provides("Demo.SharedRuntime", provider => provider
    .Version("1.0.0")
    .DisplayName("Demo Shared Runtime"));

This registers your package as a provider of Demo.SharedRuntime version 1.0.0. Other packages can declare a dependency on this key.

Declaring a requirement

dependency.Requires("Demo.Framework", consumer => consumer
    .ConsumerKey("Demo.App")
    .MinVersion("2.0.0")
    .MinInclusive());

Requires declares that this package needs Demo.Framework at version 2.0.0 or higher. ConsumerKey identifies this package in the dependency graph. MinInclusive() means version 2.0.0 itself satisfies the requirement.

Deep dive: How dependency ref-counting works across MSI packages

The dependency extension writes to the Windows registry under HKLM. Each provider registers a key with its version. Each consumer writes a reference to the provider key.

When a user tries to uninstall a provider package, the extension checks the registry for active consumers. If any exist, the uninstall is blocked with a message listing the dependent packages. This prevents the "DLL hell" scenario where removing a shared runtime breaks other applications.

This model is fully compatible with WiX's dependency extension. Packages built with WiX and FalkForge can participate in the same dependency graph. Error codes are DEP001–DEP007.

Recap

Extension Demo Key Types
Firewall demo/29-ext-firewall FirewallExtension, FirewallProtocol, FirewallDirection, FirewallProfile
IIS demo/30-ext-iis IisExtension, AppPoolRef, CertificateRef, ManagedPipelineMode
SQL demo/31-ext-sql SqlExtension, SqlDatabaseRef, SqlScriptBuilder
.NET demo/32-ext-dotnet DotNetExtension, DotNetRuntimeType, DotNetPlatform
Util demo/33-ext-util UtilExtension, XmlConfigBuilder
Dependency demo/34-ext-dependency DependencyExtension, DependencyProvider, DependencyConsumer