Automating IIS application pool and site creation with C# and WMI on Windows Server 2025 and IIS 10

Posted: (EET/GMT+2)

 

I was today working on some Windows Server 2025 and IIS management and took the time to develop a little C# application that would help me automate configuring an ASP.NET Core web application to run on a local Windows Server 2025 installation with IIS 10.x installed. Also, the solution needed to remotely configure IIS, not locally.

IIS remote administration using C# code is a complex topic, so I wanted to briefly share what works and what doesn't. Firstly, if you investigate the topic, you will find there are multiple options you could use:

  • PowerShell cmdlets
  • Remote WMI
  • The NuGet package "Microsoft.Web.Administration"
  • The IIS management API on port 8172
  • The IIS Administration REST API (see here).

I didn't want to start calling PowerShell scripts, so I tested the NuGet package first. It didn't work for remote connections, and the OpenRemote method (docs) didn't work. I found a Stack Overflow post about this and possibly, you could use the method to open a remote "applicationHost.config" file and use that to edit the XML, but that causes many other issues, like reloading the configuration. So, that didn't simply work. Yes, the call to the OpenRemote method didn't cause any exceptions, but the properties ApplicationPools and Sites were always null and thus caused NullReferenceExceptions.

Also, I found that the IIS management web service on port 8172 is for Microsoft's internal use, and is not well documented nor intended for general automation. Finally, I didn't want to install anything extra: the IIS Administration REST API (see link above) would provide a nice API, but with the need to maintain it separately.

Thus, my selection was to use WMI, since I'd be on Windows anyway. This works well, and to quickly test if your connection to the IIS web server works remotely, you can run the following command:

wmic /node:"myiisbox" /namespace:\\root\WebAdministration path Site get Name

Run this as an administrator, and it should list all the sites on the IIS server "myiisbox". If it doesn't work, you need to enable RPC access first.

If this command succeeds, your server is ready for remote IIS automation via WMI.

If it fails with "Invalid namespace" or RPC errors like "System.Runtime.InteropServices.COMException: Creating an instance of the COM component with CLSID {2B72133B-3F5B-4602-8952-803546CE3344} from the IClassFactory failed due to the following error: 800706ba The RPC server is unavailable. (Exception from HRESULT: 0x800706BA)", then IIS WMI is not installed or not accessible yet.

One important discovery was that on a fresh Windows Server 2025 installation, the IIS WMI provider (root\WebAdministration) is not installed by default. If this namespace does not exist, no amount of C#, firewall rules, or permission fiddling will help. Installing the IIS Management Scripts and Tools feature is mandatory.

Now, the C# code. In .NET Core applications, first install the NuGet package "System.Management". Note that you don't need the "Microsoft.Web.Administration" package. It doesn't work for remote management.

Here's the C# code for creating a connection and an application pool:

[SupportedOSPlatform("Windows")]
private static ManagementScope ConnectWebAdminScope(string iisServer)
{
    // use SSO to connect; if you need explicit credentials, set options.Username/Password and possibly the Authority
    ConnectionOptions options = new()
    {
        EnablePrivileges = true,
        Impersonation = ImpersonationLevel.Impersonate,
        Authentication = AuthenticationLevel.PacketPrivacy // this helps with remote WMI hardening
    };

    ManagementScope scope = new($@"\\{iisServer}\root\WebAdministration", options);
    scope.Connect();

    return scope;
}

[SupportedOSPlatform("Windows")]
private static void CreateApplicationPool(ManagementScope scope, string appPoolName)
{
    // first, check if the pool already exists
    if (WmiObjectsExists(scope, $"SELECT * FROM ApplicationPool WHERE Name='{EscapeWql(appPoolName)}'"))
    {
        // the app pool already exists, no action needed
        return;
    }

    // call the WMI Create static method on the ApplicationPool class
    using ManagementClass appPoolClass = new(scope, new ManagementPath("ApplicationPool"), null);
    appPoolClass.InvokeMethod("Create", [appPoolName]);

    // set ManagedRuntimeVersion to "" for "No Managed Code" which is needed for ASP.NET Core (ASP.NET 5+)
    using ManagementObject? created = GetFirstOrDefaultObject(scope,
      $"SELECT * FROM ApplicationPool WHERE Name='{EscapeWql(appPoolName)}'");
    if (created is not null)
    {
        created["managedRuntimeVersion"] = ""; // set to "No Managed Code"
        created.Put();
    }
}

You can call this like this:

using System.Management;
using System.Runtime.Versioning;
...
string server = "myiisbox";
string appPoolName = "MyAppPoolName";

ManagementScope scope = ConnectWebAdminScope(server);
CreateApplicationPool(scope, appPoolName);

Next, here's the code for creating a new site:

[SupportedOSPlatform("Windows")]
private static void CreateWebSite(ManagementScope scope, string siteName, string physicalPath, string hostHeader, int port)
{
    // first, check if the web site already exists
    if (WmiObjectsExists(scope, $"SELECT * FROM Site WHERE Name='{EscapeWql(siteName)}'"))
    {
        // the site already exists, no action needed
        return;
    }

    // build the bindings array for Site.Create, the format is: "IP:Port:HostHeader" (IP can be "*" for all unassigned)
    string bindingInformation = $"*:{port}:{hostHeader}";

    using ManagementClass bindingClass = new(scope, new ManagementPath("BindingElement"), null);
    using ManagementObject binding = bindingClass.CreateInstance();
    binding["Protocol"] = "http"; // or "https"
    binding["BindingInformation"] = bindingInformation;

    // call the Site.Create method and pass the siteName, bindingsArray and physicalPath
    object[] bindingsArray = [binding];
    using ManagementClass siteClass = new(scope, new ManagementPath("Site"), null);
    siteClass.InvokeMethod("Create", [siteName, bindingsArray, physicalPath]);
}

[SupportedOSPlatform("Windows")]
private static bool WmiObjectsExists(ManagementScope scope, string wql)
{
    using ManagementObjectSearcher searcher = new(scope, new ObjectQuery(wql));
    using ManagementObjectCollection results = searcher.Get();

    // are there any results?
    return results.Count > 0;
}

[SupportedOSPlatform("Windows")]
private static ManagementObject? GetFirstOrDefaultObject(ManagementScope scope, string wql)
{
    // execute the query and return the first object found, or null if none found
    using ManagementObjectSearcher searcher = new(scope, new ObjectQuery(wql));
    foreach (ManagementObject obj in searcher.Get().Cast())
    {
        // return the first object found
        return obj;
    }

    // if we got here, no objects were found
    return null;
}

private static string EscapeWql(string s) => s.Replace("'", "''");

And to call this, you would write:

const int DefaultPort = 80; // for HTTP
string siteName = "MyAspNetCoreApp";
string physicalPathOnServer = @"C:\SomePath\MyApp";

CreateWebSite(scope, siteName, physicalPathOnServer, siteName, DefaultPort);

With this code in place, you can create new application pools and web sites on a remote IIS web server easily. This doesn't nee any PowerShell, undocumented APIs or extra installations (on top of WMI and RPC).

Happy administering!