Remote Administration of IIS

Table of contents

Introduction

I had to find an optimal way to control remote IIS through the code and encapsulate this code into some module (in fact, .NET assembly). This task was very interesting and had many issues—that's why I want to share my experiences here.

Here is the list of requirements. The module has to:

  • Perform main IIS tasks:
    • Create web site
    • Create virtual application
    • Create virtual directory
    • Setup site bindings, including SSL bindings
    • Create application pools
  • Support parallel configuring of IIS on different servers
  • Support IIS 8.0 (without backward compatibility).

Actually, the module has to do everything that IIS Manager does and even a little bit more.

I found and investigated three approaches:

  1. Windows Management Instrumentation (WMI)
  2. ASP.NET Configuration API
  3. Microsoft Web Administration

For each of them a test application was created and it did not take much time to see Microsoft.Web.Administration is the best one.

WMI it is too difficult for understanding. This increases probability of making mistakes. Moreover, working with WMI smells like coding COM-components.

Site creation example using WMI:

DirectoryEntry IIS = new DirectoryEntry("IIS://localhost/W3SVC");          
object[] bindings = new object[] { "127.0.0.1:4000:", ":8000:" };
IIS.Invoke("CreateNewSite", "TestSite", bindings, "C:\\InetPub\\WWWRoot");

The second one (ASP.NET Configuration API) is actually a direct changing of XML configuration files. In other words, it supposes manual changing of the root web.config file on web servers. You can see why this approach is not great.

The last one allows creating web sites and performs all low-level work for the developer. An additional benefit of that approach is that IIS Manager uses the same component for performing its work.

However I could not avoid some issues during the implementation. I googled a lot to get Microsoft.Web.Administration working my way. There is much information over the Internet about this library, however all these articles are spread a lot. That made me create this post about Microsoft.Web.Administration where I can place all my issues in one place. I hope it would be useful for someone.

So, let’s start!

System configuration

Assume we have a web farm. Each web server has Windows Server 2012 with IIS 8.0. There is an Application server with a Windows Service managing IIS on all web servers in the farm. This Windows service is based on our module.

Servers

Development

Microsoft.Web.Administration

Some issues appeared on the very first move. I referenced DLL in my project, wrote a piece of code to create a web site and got the following exception ‘Exception from HRESULT: 0x80070005 (E_ACCESSDENIED)’. As I figured out reading a number of articles (for example, stackoverflow.com/questions/8963641/permissions-required-to-use-microsoft-web-administration), Microsoft.Web.Administration needed full administrator access to change the root web.config file.

Well, I created a user on the remove server with some login and password. Then I created a user on the Application server with the same login and password (it is important that users have the same login and password for impersonation). Execute the code. The same exception!

Therefore, I had to dig deeper into the issue. After reading a few articles I understood that it is not enough just to create an administrator. Starting with Windows Vista, all Windows have UAC. According to UAC even an administrator is not administrator, it is just a user with extended permissions. Considering that, to get our application working, we needed to disable UAC on the remove server. However, you cannot just switch of UAC using Administration Panel. ‘Disable’ in Administration Panel does nothing actually. We needed to completely disable UAC. I did it using system registry as it was described in the article. I agree it is not safe enough. However, it is the only solution. Moreover, if you decide to use different approach you will need to do the same, because you can change web site settings only by changing the root web.config file.

Running our application. Eureka! The site was created. It means, we could move further.

After we deployed our application on a test server, another issue faced out – configuration issue: the module could not find Microsoft.Web.Administration and stopped with an exception. I figured out, that there was a DLL with a different version in the server GAC. To solve the issue we decided to include DLL into bin folder (copy always in the project configuration for that DLL).

The last issue connected to the plugging in of the library also appear due to DLL versions. During the development process I found libraries with following versions: 7.0.0.0 and 7.5.0.0. The second one made it easier to implement some functionalities. For example, it was easy to set up an AlwaysRunning application pool property. That's why I used that library at first. However, after deploying the application to the test server I got an exception. The reason was that Microsoft.Web.Administration 7.5.0.0 worked only with IIS Express. Therefore, if you plan to control normal IIS, use DLL with version 7.0.0.0.

These were the main issues appeared during plugging in the library. Now let's move to implementing of requirements.

Requirements implementation

Microsoft.Web.Administration API contains some trivial methods and there are methods required time to think. I'm not describing the trivial stuff and skip stuff that can be easily googled. Let's concentrate on issues which took much time to be solved.

Multithreading

According to requirements, I had to implement the possibility of creating of a number of web sites on different web servers at the same time. However, two threads cannot control the same IIS, because it would mean changing the same file on the server – the root web.config file. A lock on the server name solves this.

private ServerManager _server;
private static readonly ConcurrentDictionary<string, object> LockersByServerName = new ConcurrentDictionary<string, object>();

private object Locker
{
    get { return LockersByServerName.GetOrAdd(ServerName, new object()); }
}

private void ConnectAndProcess(Action handler, CancellationToken token)
{
    token.ThrowIfCancellationRequested();

    lock (Locker)
    {
        try
        {
             _server = ServerManager.OpenRemote(ServerName);
             token.ThrowIfCancellationRequested();

             try
             {
                 handler();
             }
             catch (FileLoadException)
             {
                 // try again

                  token.ThrowIfCancellationRequested();
                  if (_server != null) _server.Dispose();
                  _server = ServerManager.OpenRemote(ServerName);
                  token.ThrowIfCancellationRequested();

                  handler();
             }
        }
        finally
        {
             if(_server != null)
                 _server.Dispose();
             _server = null;
        }
    }
}

In this example ServerName—it is NetBIOS machine name in the local network.

Each module method is wrapped in that handler. For example, website existence check:

public bool WebSiteExists(string name, CancellationToken token)
{
    return ConnectAndGetValue(() =>
    {
        Site site = _server.Sites[name];
        return site != null;
    }, token);
}

private TValue ConnectAndGetValue<TValue>(Func<TValue> func, CancellationToken token)
{
        TValue result = default(TValue);
        ConnectAndProcess(() => { result = func(); }, token);
        return result;
}

Why to create a new server connection each time?

Firstly, the system, our module is designed for, is easily configured. That's why we do not know, which methods will be called and in which order. We do not know for how long an instance of the connector class (let it be MWAConnector) will exist.

Secondly, another thread can try to create a connector. Therefore, if we have a connection opened by one connector, then the second connector cannot open the connection to the same server. Otherwise, we will get an exception of parallel file access for editing.

Considering both these points, there is one instance of the MWAConnector for a number of simple operations, each of which is executed in the independent context.

The drawback of this approach is the resource consuming for a connection creation. However, we can neglect them, because they are not the bottleneck of our module: operation execution like site creation takes much more time to complete.

Setting up AlwaysRunning

One of the tasks was to create an application pool with AlwaysRunning set to true. There are many application pool properties in the ApplicationPool class of the Microsoft.Web.Administration 7.0.0.0 library like AutoStart, Enable32BitAppOnWin64, ManagedRuntimeVersion, QueueLength. However, there is not RunningMode. We can find this property in the DLL with version 7.5.0.0, but, as I mentioned before, this version works only with IIS Express. Nevertheless, I found a solution:

ApplicationPool pool = _server.ApplicationPools.Add(name);

//some code to set pool properties

if (alwaysRunning)
{
  pool["startMode"] = "AlwaysRunning";
}

To save changes call CommitChanges() method on the ServerManager instance.

_server.CommitChanges();

Setting up PreloadEnabled

Another issue, I faced, was a lack of web site PreloadEnabled property. This flag is responsible for reducing web site first load time. It is useful, when a web site takes much time to ‘warm up’.

A code snippet below shows a solution.

Site site = _server.Sites[siteName];
string path = string.Format("/{0}", applicationName);
Application app = site.Applications.Add(path, physicalPath);
app.ApplicationPoolName = applicationPoolName;
if (preload)
    app.SetAttributeValue("preloadEnabled", true);
_server.CommitChanges();

Please note, the web application name must start with ‘/’. This is required by Microsoft.Web.Administration.

Change website settings

Sometimes you need to change application pool for site. However, there is no such property as ApplicationPool in the Site class. But there is such property in the Application class. How can we change a pool then?

The solution is simple – get the application of the site:

Site site = _server.Sites[siteName];
Application app = site.Applications["/"];

Website deletion

A web site deletion is a simple task you will say – it’s enough to call _server.Sites.Remove(site). But there is an issue with deleting website with https binding. The root of the problem is that when Microsoft.Web.Administration deletes a site, it deletes an information about site bindings. So far so good, but the way it does it not as great as we want it to be. Fact is that Microsoft.Web.Administration deletes a record in the system config file, that binds IP:port:SSL. Thus, if you have a number of sites, which use the same IP and port and, moreover, they share the same SSL certificate (multi-domain certificate), then deleting one of these sites would mean loosing information about IP:port:SSL fot other sites.

A newer version of Microsoft.Web.Administration has a method overload for deleting a binding. This overload expects a flag, that tells, if the method should keep information about IP:port:SSL or not. Therefore, the solution looks like:

 Site site = _server.Sites[name];
if (site == null)
    return;

var bindings = site.Bindings.ToList();
foreach (Binding binding in bindings)
{
    site.Bindings.Remove(binding, true);
}
_server.Sites.Remove(site);

_server.CommitChanges();

Afterwords

The module successfully does its job now. I hope this article helps you to avoid having same trouble!

You Might Also Like

Blog Posts Story of Deprecation and Positive Thinking in URLs Encoding
May 13, 2022
There is the saying, ‘If it works, don’t touch it!’ I like it, but sometimes changes could be requested by someone from the outside, and if it is Apple, we have to listen.
Blog Posts The Laws of Proximity and Common Region in UX Design
April 18, 2022
The Laws of Proximity and Common Region explain how people decide if an element is a part of a group and are especially helpful for interface designers.
Blog Posts Custom Segmented Control with System-like Interface in SwiftUI
March 31, 2022
Our goal today is to create a Segmented Control that accepts segments not as an array, but as views provided by the ViewBuilder. This is the same method that the standard Picker employs.