Getting the Weather Forecast Using SharePoint Workflows

Introduction

I already wrote a post on how to use SharePoint 2013 workflows. This time, I’ll get back to this topic, with a slightly more interesting (IMO) use case: a recurring workflow that runs once a day and emails users the weather forecast for the next day!

Let’s get started! This will work on both SharePoint 2013 on premises or SharePoint Online.

Weather Forecast

I needed a web service for retrieving the weather forecast that was:

  • Free (of course!)
  • REST (not SOAP!)
  • Using JSON (not XML!)

I ended up using Yahoo’s excellent weather web service. It does all this and a lot more, and I suggest you have a look at it.

image

I had to find out the WOEID for the city I was interested in – Coimbra, Portugal, of course – which was as simple as running this YQL query:

select woeid from geo.places(1) where text="coimbra, pt"

Then, all I had to do was pass the returned value (739672) to the query passed in the query string of the REST web service:

https://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20weather.forecast%20where%20woeid%20%3D%20739672&format=json&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys

The result is this:

image

Or, in a formatted way, much easier to read:

{

   "query": {

      "count": 1,

      "created": "2016-05-14T10:13:08Z",

      "lang": "en-US",

      "results": {

         "channel": {

            "units": {

               "distance": "mi",

               "pressure": "in",

               "speed": "mph",

               "temperature": "F"

            },

            "title": "Yahoo! Weather - Coimbra, Coimbra, PT",

            "link": "http://us.rd.yahoo.com/dailynews/rss/weather/Country__Country/*https://weather.yahoo.com/country/state/city-739672/",

            "description": "Yahoo! Weather for Coimbra, Coimbra, PT",

            "language": "en-us",

            "lastBuildDate": "Sat, 14 May 2016 11:13 AM WEST",

            "ttl": "60",

            "location": {

               "city": "Coimbra",

               "country": "Portugal",

               "region": " Coimbra"

            },

            "wind": {

               "chill": "57",

               "direction": "325",

               "speed": "7"

            },

            "atmosphere": {

               "humidity": "90",

               "pressure": "1004.0",

               "rising": "0",

               "visibility": "16.0"

            },

            "astronomy": {

               "sunrise": "6:17 am",

               "sunset": "8:43 pm"

            },

            "image": {

               "title": "Yahoo! Weather",

               "width": "142",

               "height": "18",

               "link": "http://weather.yahoo.com",

               "url": "http://l.yimg.com/a/i/brand/purplelogo//uh/us/news-wea.gif"

            },

            "item": {

               "title": "Conditions for Coimbra, Coimbra, PT at 10:00 AM WEST",

               "lat": "40.194672",

               "long": "-8.40737",

               "link": "http://us.rd.yahoo.com/dailynews/rss/weather/Country__Country/*https://weather.yahoo.com/country/state/city-739672/",

               "pubDate": "Sat, 14 May 2016 10:00 AM WEST",

               "condition": {

                  "code": "26",

                  "date": "Sat, 14 May 2016 10:00 AM WEST",

                  "temp": "57",

                  "text": "Cloudy"

               },

               "forecast": [

                  {

                     "code": "39",

                     "date": "14 May 2016",

                     "day": "Sat",

                     "high": "61",

                     "low": "54",

                     "text": "Scattered Showers"

                  },

                  {

                     "code": "28",

                     "date": "15 May 2016",

                     "day": "Sun",

                     "high": "71",

                     "low": "54",

                     "text": "Mostly Cloudy"

                  },

                  {

                     "code": "30",

                     "date": "16 May 2016",

                     "day": "Mon",

                     "high": "76",

                     "low": "52",

                     "text": "Partly Cloudy"

                  },

                  {

                     "code": "28",

                     "date": "17 May 2016",

                     "day": "Tue",

                     "high": "77",

                     "low": "53",

                     "text": "Mostly Cloudy"

                  },

                  {

                     "code": "30",

                     "date": "18 May 2016",

                     "day": "Wed",

                     "high": "78",

                     "low": "52",

                     "text": "Partly Cloudy"

                  },

                  {

                     "code": "30",

                     "date": "19 May 2016",

                     "day": "Thu",

                     "high": "77",

                     "low": "52",

                     "text": "Partly Cloudy"

                  },

                  {

                     "code": "30",

                     "date": "20 May 2016",

                     "day": "Fri",

                     "high": "71",

                     "low": "55",

                     "text": "Partly Cloudy"

                  },

                  {

                     "code": "28",

                     "date": "21 May 2016",

                     "day": "Sat",

                     "high": "72",

                     "low": "55",

                     "text": "Mostly Cloudy"

                  },

                  {

                     "code": "30",

                     "date": "22 May 2016",

                     "day": "Sun",

                     "high": "72",

                     "low": "53",

                     "text": "Partly Cloudy"

                  },

                  {

                     "code": "30",

                     "date": "23 May 2016",

                     "day": "Mon",

                     "high": "75",

                     "low": "52",

                     "text": "Partly Cloudy"

                  }

               ],

               "description": "<![CDATA[<img src=\"http://l.yimg.com/a/i/us/we/52/26.gif\"/>\n<BR />\n<b>Current Conditions:<\/b>\n<BR />Cloudy\n<BR />\n<BR />\n<b>Forecast:<\/b>\n<BR /> Sat - Scattered Showers. High: 61Low: 54\n<BR /> Sun - Mostly Cloudy. High: 71Low: 54\n<BR /> Mon - Partly Cloudy. High: 76Low: 52\n<BR /> Tue - Mostly Cloudy. High: 77Low: 53\n<BR /> Wed - Partly Cloudy. High: 78Low: 52\n<BR />\n<BR />\n<a href=\"http://us.rd.yahoo.com/dailynews/rss/weather/Country__Country/*https://weather.yahoo.com/country/state/city-739672/\">Full Forecast at Yahoo! Weather<\/a>\n<BR />\n<BR />\n(provided by <a href=\"http://www.weather.com\" >The Weather Channel<\/a>)\n<BR />\n]]>",

               "guid": {

                  "isPermaLink": "false"

               }

            }

         }

      }

   }

}

SharePoint Workflow

Next, I wanted to use this information in a SharePoint workflow. This workflow has to run every day automatically, and this was a challenge, because it is not supported out of the box. But, first things first, let’s see how I designed it the Weather Forecast workflow.

Initialization

Open SharePoint Designer and create a new Site workflow, make sure it is using the SharePoint 2013 settings. I first added a stage called Initialization, where I created a string workflow variable named url, to which I assigned the web service URL:

image

Next, I also created a dictionary variable named requestHeaders, to which I added a string entry called Accept, with value application/json (I don’t think this is necessary, but it is always good to tell a REST web service what we’re expecting):

image

Get Weather Forecast

On a new stage named Get Weather Forecast, I add the call to the web service itself, by adding a Call HTTP Web Service action, pointing to the contents of the url variable:

image

After I do that, I access the properties of Call HTTP Web Service action and I set the request headers as coming from the requestHeaders variable I just created:

image

Make sure you set the response to go to a new dictionary variable, responseContent. The rest is really irrelevant, but if you run into problems, it may be useful to look at the responseCode (string) and responseHeaders (dictionary) variables.

Process Response

I then added a new stage named Process Response, where I added an action of type Get an Item from a Dictionary, from which I extracted value query/results/channel/item/forecast(1)/text from the responseContent variable, which contains the result of the web service invocation, into a new string variable called result:

image

This needs some explanation. The value I’m extracting from dictionary is similar to a JSONPath – the JavaScript dotted-path to traverse an object – with two differences:

  • Instead of dots (.), we use slashes (/)
  • Instead of square brackets ([]) for indexed properties, we use parenthesis

The path we are extracting is so that we can jump directly to the textual description of the forecast for the next day, according to the schema that the Yahoo web service returns. If you use another one, you will need to change this path, of course.

Send Email

The next stage I added is called Send Email, and it’s where I send the weather forecast to the interested users. In it, I add a single action of type Send an Email,

image

In the To field, we should use a group instead of explicitly named users, something like “Weather Forecast Users”. In the body of the email, I have a mix of text and the result variable, which, if you remember, contains the portion of the web service response that has the textual description of the forecast.

Sleep for a Day

The last stage I’m adding is called Sleep for a Day, and, again, only has a single activity, Pause for Duration, configured to sleep for 1 day.


Wrap Up

This is what the resulting workflow looks like:

imageIn each stage, we need to add a transition to the next one, except in Sleep for a Day, on which the transition is going to Initialization. So, I have the following stages and transitions:

Stage Transition
Initialization Get Weather Forecast
Get Weather Forecast Process Response
Process Response Send Email
Send Email Sleep for a Day
Sleep for a Day Initialization

As for variables, I have:

image

Starting the Workflow

Now, all we have to do is, after publishing the workflow, is starting it. We do that through Site contentsSite workflows, normally available at URL /_layouts/15/workflow.aspx, and clicking the Weather Forecast link.

image

This workflow, it should be noted, shouldn’t stop, because after it reaches the final stage, it jumps to the first one, don’t forget about it.

Debugging the Workflow

The debugging experience is not great. What you can do is log stuff to the workflow history, using a Log to History List action, but be warned that you cannot log large contents: for example, logging the result of the web service call won’t work. What you can do is see where the workflow stopped and see if any exception was thrown, by clicking at the Status field of the running or completed workflow and inspecting the messages produced:

image

You also have the Workflow Health page (/_layouts/15/WorkflowServiceHealth.aspx), on which you can also see some information and stop running workflows that have gone rogue:

image

It is also worth pointing out that, unless you configured it otherwise, the output history for the workflows is stored in a hidden list called Workflow History (/Lists/Workflow%20History/AllItems.aspx):

image

Conclusion

SharePoint offers some mechanisms for integrating outside data, in the form of REST web services returning JSON, into workflows. While more complex scenarios require more advanced techniques and APIs (Business Connectivity Services, for example, or custom workflow actions), it is still possible to do some interesting stuff with what is available out of the box. Variations to this example might include conditional checks, like, only sending a warning email if the weather forecast is not good, or store the results for the next days, etc. I leave that as an exercise to you, dear reader! Smile In the meantime, do let me know how it goes!

Querying SharePoint

Introduction

SharePoint, being a content management system, of course, offers a couple of ways to query its contents programmatically. Here we will explore some of these options.

Web Parts

First, of course, there are the web parts. These allow us to configure queries visually on a page. The most important web parts are:

ContentByQueryWebPart: use this for simple queries that do not return much contents, on a single site collection. Can be used in SharePoint Online. Can only be customized through XSLT.

ContentBySearchWebPart (introduced in SharePoint 2013): more powerful, but does not exist in SharePoint Online. Can handle more complex queries that can span multiple site collections and multiple levels of sorting. Can only be customized through HTML and JavaScript templates. Results can be refined.

XsltListViewWebPart/DataFormWebPart: can be used to view of a specific list. The display can be totally configured. XsltListViewWebPart is a bit more powerful.

ListViewByQuery: requires that you pass an SPList and a SPQuery instance. Can only display pre-defined views.

APIs

There are several APIs for querying, either directly or using the search index.

SPQuery: can only be applied to a single list:

var query = new SPQuery();

query.Query = "<Where><Eq><FieldRef Name='Title'/><Value Type='Text'>Bla Bla</Value></Eq></Where>";

 

var list = site.RootWeb.Lists["Some List"];

 

var table = list.GetItems(query).GetDataTable();

SPSiteDataQuery: can be applied to several lists across a site collection. Has some issues, for example, won’t return values for multi-value fields:

var query = new SPSiteDataQuery();

query.Lists = "<List Name='Tasks'/>";

query.Query = "<Where><Eq><FieldRef Name='Title'/><Value Type='Text'>Bla Bla</Value></Eq></Where>";

query.Webs = "<Web Scope='SiteCollection'/>";

 

var table = site.RootWeb.GetSiteData(query);

KeywordQuery: uses the search index to search for keywords, so it requires that contents are indexed beforehand, and the search service is functioning:

using (var query = new KeywordQuery(site))

{

    query.QueryText = "Bla Bla";

    query.ResultsProvider = SearchProvider.Default;

 

    var searchExecutor = new SearchExecutor();

 

    var resultTableCollection = searchExecutor.ExecuteQuery(query);

 

    var searchResult = resultTableCollection.Filter("TableType", KnownTableTypes.RelevantResults).Single();

 

    var table = new DataTable();

    table.TableName = "Result";

    table.Load(searchResult, LoadOption.OverwriteChanges);

}

FullTextSqlQuery: uses SharePoint Search SQL to execute queries, which makes it generally more powerful than KeywordQuery:

using (var query = new FullTextSqlQuery(site))

{

    query.QueryText = "SELECT * FROM scope() WHERE Title = 'Teste'";

    query.ResultsProvider = SearchProvider.Default;

 

    var searchExecutor = new SearchExecutor();

 

    var resultTableCollection = searchExecutor.ExecuteQuery(query);

 

    var searchResult = resultTableCollection.Filter("TableType", KnownTableTypes.RelevantResults).Single();

 

    var table = new DataTable();

    table.TableName = "Result";

    table.Load(searchResult, LoadOption.OverwriteChanges);

}

CrossListQueryInfo and CrossListQueryCache: performs queries in a single site collection but multiple sites, with optional audience targeting. CrossListQueryCache caches the results for a period of time:

var crossListQueryInfo = new CrossListQueryInfo();

crossListQueryInfo.Query = "<Where><Eq><FieldRef Name='Title'/><Value Type='Text'>Bla Bla</Value></Eq></Where>";

crossListQueryInfo.Lists = "<List Name='Some List'/>";

crossListQueryInfo.Webs = "<Webs Scope='SiteCollection' />";

crossListQueryInfo.UseCache = true;

 

var crossListQueryCache = new CrossListQueryCache(crossListQueryInfo);

 

var table = crossListQueryCache.GetSiteData(site.RootWeb);

Client KeywordQuery: this is the client counterpart to KeywordQuery. It uses the SharePoint Client Components, or Client Side Object Model (CSOM), which means, it accesses SharePoint through its web services. It is basically similar to the server KeywordQuery, but can be used from a remote machine:

var keywordQuery = new Microsoft.SharePoint.Client.Search.Query.KeywordQuery(ctx);

keywordQuery.QueryText = "SharePoint";

 

var searchExecutor = new Microsoft.SharePoint.Client.Search.Query.SearchExecutor(ctx);

 

var results = searchExecutor.ExecuteQuery(keywordQuery);

 

ctx.ExecuteQuery();

 

var searchResult = results.Value.Where(x => x.TableType == KnownTableTypes.RelevantResults.ToString()).Single();

Web Services

Search.asmx: SOAP web service that takes a SharePoint SQL query. Part of the SharePoint Foundation 2010 Web Services, now somewhat obsolete.

Lists.asmx: Another SOAP web service that can return and update items in a list.

ListData.svc: WCF Data Service REST/OData that can be used for querying (including OData queries) or modifying contents of lists.

SharePoint 2013 REST Services: new REST/OData web services introduced in SharePoint 2013. Includes _api/web, _api/search, _api/web/lists, for searches, web or list operations.

SharePoint Foundation RPC Protocol

The SharePoint Foundation RPC Protocol, now obsolete, this allowed querying, exporting contents and performing a number of other operations through the OWSSVR.DLL handler. Although almost unused nowadays, still offers the only out of the box way to, for example, export a list in XML format.

JavaScript

In SharePoint 2010 the JavaScript Side Object Model (JSOM) was introduced and in version 2013 it was enhanced. It is now possible to do anything that the SharePoint Client API allows.

Conclusion

You see, lots of ways to get contents from SharePoint, as usual. Make sure you chose the one that best suits your needs.

References

SharePoint 2013 .NET Server, CSOM, JSOM, and REST API index

When to use the Content Query Web Part or the Content Search Web Part in SharePoint

Choose the right API set in SharePoint 2013

Use OData query operations in SharePoint REST requests

SharePoint Server 2013 Client Components SDK

SharePoint Search SQL Syntax Reference

Hosting HTTP Resources

Introduction

How do I host thee? Let me count the ways!

You may not have realized that .NET offers a lot of alternatives when it comes to hosting an HTTP server, that is, without resorting to IIS, IIS Express or the now gone Visual Studio Web Development Server (aka, Cassini, rest in peace); by that, I either mean:

  • Opening up a TCP port and listening for HTTP requests, or a subset of them;
  • Running ASP.NET pages without a server.

In this post I am going through some of them. Some are specific to web services, but since they understand REST, I think they qualify as well as generic HTTP hosting mechanisms.

.NET HttpListener

Let’s start with HttpListener. This is included in .NET since version 2 and offers a decent server for static contents, that is, it cannot run any dynamic contents, like ASP.NET handlers, nor does it know anything about them. You merely point it to a physical folder on your file system, and it will happily serve any contents located inside it. Let’s see an example:

using (var listener = new System.Net.HttpListener())

{

    var url = "http://*:2000/";

    listener.Prefixes.Add(url);

    listener.Start();

 

    var ctx = listener.GetContext();

 

    var message = "Hello, World!";

 

    ctx.Response.StatusCode = (Int32) HttpStatusCode.OK;

    ctx.Response.ContentType = "text/plain";

    ctx.Response.ContentLength64 = message.Length;

 

    using (var writer = new StreamWriter(ctx.Response.OutputStream))

    {

        writer.Write(message);

    }

 

    Console.ReadLine();

}

This is a very basic example that just listens on port 2000, for any host name and request, and just returns Hello, World! when contacted before shutting down.

ASP.NET ApplicationHost

Complementary to HttpListener, we have a way to execute ASP.NET handlers (ASPX pages, ASHX generic handlers and ASMX web services) in a self-hosted application domain. For that, we use the ApplicationHost class to create the ASP.NET application domain, and a regular .NET class for the server implementation. An example:

public class Host : MarshalByRefObject

{

    public void ProcessPage(String page, String query, TextWriter writer)

    {

        var worker = new SimpleWorkerRequest(page, query, writer);

        HttpRuntime.ProcessRequest(worker);

    }

}

 

//strip out bin\debug, so as to find the base path where web files are located

var path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location).Replace(@"\bin\Debug", String.Empty);

 

//we need to copy the assembly to the base path

File.Copy(Assembly.GetExecutingAssembly().Location, Path.Combine(path, "bin", Assembly.GetExecutingAssembly().CodeBase.Split('/').Last()), true);

 

var host = System.Web.Hosting.ApplicationHost.CreateApplicationHost(typeof(Host), "/", path) as Host;

host.ProcessPage("Default.aspx", null);

Notice the File.Copy call; this is necessary because the assembly referenced by the Default.aspx page needs to be located in the same folder as the page. An alternative to this would be to add a post build-event to the Visual Studio project:

image

I leave as an exercise to the interested readers how we can combine this with HttpListener! Winking smile

OWIN WebApp

Moving on to more recent technologies, we now have OWIN. In case you’ve been living in another world and haven’t heard of OWIN, I’ll just say that it is a standard for decoupling .NET from any particular web servers, like IIS or IIS Express. It also happens to have a self-hosting implementation – which, by the way, uses HttpListener underneath.

We need to add a reference to the Microsoft.Owin.SelfHost NuGet package:

image

After that, we just register an instance of WebApp with the default parameters, add an handler, and we’re done:

class Program

{

    public static void Configuration(IAppBuilder app)

    {

        app.Use(new Func<AppFunc, AppFunc>(next => (async ctx =>

        {

            using (var writer = new StreamWriter(ctx["owin.ResponseBody"] as Stream))

            {

                await writer.WriteAsync("Hello, World!");

            }

        })));

    }

 

    static void Main(String[] args)

    {

        using (WebApp.Start<Program>("http://*:2000"))

        {

            Console.ReadLine();

        }

    }

}

Again, no fancy dynamic stuff, just plain and simple HTTP: it waits for a request and just returns Hello, World!. It is possible to run ASP.NET MVC on top of OWIN, that is the goal of project Helios, which is currently in alpha stage. Do check out the Helios NuGet package at https://www.nuget.org/packages/Microsoft.Owin.Host.IIS/1.0.0-alpha1:

image

WCF ServiceHost

Since its release, WCF offers a way for it to be self-hosted in a .NET process. The class responsible for that is ServiceHost, or one of its descendants, like WebServiceHost, more suitable for REST. I will show an example using REST, which can be easily tested using a web browser:

[ServiceContract]

public interface IRest

{

    [WebGet(ResponseFormat = WebMessageFormat.Json)]

    [OperationContract]

    String Index();

}

 

public class Rest : IRest

{

    public String Index()

    {

        return "Hello, World!";

    }

}

 

using (var host = new WebServiceHost(typeof(Rest)))

{

    var url = new Uri(@"http://localhost:2000");

    var binding = new WebHttpBinding();

 

    host.AddServiceEndpoint(typeof(IRest), binding, url);

    host.Open();

 

    Console.ReadLine();

}

This example listens for a request of /Index on port 2000 and upon receiving it, returns Hello, World! in JSON format – because we are only sending a string, it will be wrapped in . WCF REST out of the box only supports returning data in XML or JSON format, no Text or HTML, but, to be fair, that’s not what it was meant to. Should be possible to return HTML, but, honestly, it would probably mean more work than it’s worth.

Web API HttpServer

Another web services technology in the .NET stack is Web API. Web API uses a concept similar to MVC, with controllers, models and action methods, but no views. It can be self-hosted as well, using the HttpServer class. In order to use it, install the Microsoft.AspNet.WebApi.SelfHost NuGet package. You will notice that its description claims that it is legacy, and has been replaced for another based on OWIN, yet, it is fully functional, if you don’t required it to be OWIN-compliant:

image

Because of the Web API architecture, we need to implement a controller for handling requests, :

public class DummyController : ApiController

{

    [HttpGet]

    public IHttpActionResult Index()

    {

        return this.Content(HttpStatusCode.OK, "Hello, World!");

    }

}

In this example, we do not take any parameters and just return the usual response.

Here’s the infrastructure code:

var url = "http://localhost:2000";

var config = new HttpSelfHostConfiguration(url);

config.Routes.MapHttpRoute("DefaultApi", "api/{controller}/{action}");

 

using (var server = new HttpSelfHostServer(config))

{

    server.OpenAsync().Wait();

}

The DummyController is found by reflecting the current executing assembly and applying conventions; any HTTP requests for /api/Dummy/Index will land there and the outcome will be plain text.

IIS Hostable Web Core

Now, this one is tricky. IIS, from version 7, allows hosting its core engine in-process, that is, from inside another application; this is called IIS Hostable Web Core (HWC). We can supply our own Web.config and ApplicationHost.config files and specify a root folder from which IIS will serve our web resources, including any dynamic contents that IIS can serve (ASPX pages, ASHX handlers, ASMX and WCF web services, etc). Yes, I know, this contradicts my introduction, where I claimed that this post would be about hosting web resources without IIS… still, I think this is important to know, because it can be fully controlled through code.

You need to make sure HWC is installed… one option is using PowerShell’s Install-WindowsFeature cmdlet:

Or the Server Manager application:

 

Features page

 

Because HWC is controlled through an unmanaged DLL, we have to import its public API control functions and call it with .NET code. Here’s an example:

public class Host : IDisposable

{

    private static readonly String FrameworkDirectory = RuntimeEnvironment.GetRuntimeDirectory();

    private static readonly String RootWebConfigPath = Environment.ExpandEnvironmentVariables(Path.Combine(FrameworkDirectory, @"Config\Web.config"));

 

    public Host(String physicalPath, Int32 port)

    {

        this.ApplicationHostConfigurationPath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName() + ".config");

        this.PhysicalPath = physicalPath;

        this.Port = port;

 

        var applicationHostConfigurationContent = File.ReadAllText("ApplicationHost.config");

        var text = String.Format(applicationHostConfigurationContent, this.PhysicalPath, this.Port);

 

        File.WriteAllText(this.ApplicationHostConfigurationPath, text);

    }

 

    ~Host()

    {

        this.Dispose(false);

    }

 

    public String ApplicationHostConfigurationPath

    {

        get;

        private set;

    }

 

    public Int32 Port

    {

        get;

        private set;

    }

 

    public String PhysicalPath

    {

        get;

        private set;

    }

 

    public void Dispose()

    {

        this.Dispose(true);

        GC.SuppressFinalize(this);

    }

 

    protected virtual void Dispose(Boolean disposing)

    {

        this.Stop();

    }

 

    public void Start()

    {

        if (IisHostableWebCoreEngine.IsActivated == false)

        {

            IisHostableWebCoreEngine.Activate(this.ApplicationHostConfigurationPath, RootWebConfigPath, Guid.NewGuid().ToString());

        }

    }

 

    public void Stop()

    {

        if (IisHostableWebCoreEngine.IsActivated == true)

        {

            IisHostableWebCoreEngine.Shutdown(false);

 

            this.PhysicalPath = String.Empty;

            this.Port = 0;

 

            File.Delete(this.ApplicationHostConfigurationPath);

 

            this.ApplicationHostConfigurationPath = String.Empty;

        }

    }

 

    private static class IisHostableWebCoreEngine

    {

        private delegate Int32 FnWebCoreActivate([In, MarshalAs(UnmanagedType.LPWStr)] String appHostConfig, [In, MarshalAs(UnmanagedType.LPWStr)] String rootWebConfig, [In, MarshalAs(UnmanagedType.LPWStr)] String instanceName);

        private delegate Int32 FnWebCoreShutdown(Boolean immediate);

 

        private const String HostableWebCorePath = @"%WinDir%\System32\InetSrv\HWebCore.dll";

        private static readonly IntPtr HostableWebCoreLibrary = LoadLibrary(Environment.ExpandEnvironmentVariables(HostableWebCorePath));

 

        private static readonly IntPtr WebCoreActivateAddress = GetProcAddress(HostableWebCoreLibrary, "WebCoreActivate");

        private static readonly FnWebCoreActivate WebCoreActivate = Marshal.GetDelegateForFunctionPointer(WebCoreActivateAddress, typeof(FnWebCoreActivate)) as FnWebCoreActivate;

 

        private static readonly IntPtr WebCoreShutdownAddress = GetProcAddress(HostableWebCoreLibrary, "WebCoreShutdown");

        private static readonly FnWebCoreShutdown WebCoreShutdown = Marshal.GetDelegateForFunctionPointer(WebCoreShutdownAddress, typeof(FnWebCoreShutdown)) as FnWebCoreShutdown;

 

        internal static Boolean IsActivated

        {

            get;

            private set;

        }

 

        internal static void Activate(String appHostConfig, String rootWebConfig, String instanceName)

        {

            var result = WebCoreActivate(appHostConfig, rootWebConfig, instanceName);

 

            if (result != 0)

            {

                Marshal.ThrowExceptionForHR(result);

            }

 

            IsActivated = true;

        }

 

        internal static void Shutdown(Boolean immediate)

        {

            if (IsActivated == true)

            {

                WebCoreShutdown(immediate);

                IsActivated = false;

            }

        }

 

        [DllImport("Kernel32.dll")]

        private static extern IntPtr LoadLibrary(String dllname);

 

        [DllImport("Kernel32.dll")]

        private static extern IntPtr GetProcAddress(IntPtr hModule, String procname);

    }

}

In order for this to work, we need to have an ApplicationHost.config file, a minimum working example being:

<?xml version="1.0" encoding="UTF-8" ?>

<configuration>

    <configSections>

        <sectionGroup name="system.applicationHost">

            <section name="applicationPools" />

            <section name="sites" />

        </sectionGroup>

 

        <sectionGroup name="system.webServer">

            <section name="globalModules" />

            <section name="modules" />

            <section name="handlers" />

            <section name="staticContent" />

            <section name="serverRuntime" />

            <sectionGroup name="security">

                <section name="access"/>

                <sectionGroup name="authentication">

                    <section name="anonymousAuthentication" />

                    <section name="windowsAuthentication" />

                    <section name="basicAuthentication" />

                </sectionGroup>

                <section name="authorization" />

                <section name="requestFiltering" />

                <section name="applicationDependencies" />

                <section name="ipSecurity" />

            </sectionGroup>

            <section name="asp" />

            <section name="caching" />

            <section name="cgi" />

            <section name="defaultDocument" />

            <section name="directoryBrowse" />

            <section name="httpErrors" />

            <section name="httpLogging" />

            <section name="httpProtocol" />

            <section name="httpRedirect" />

            <section name="httpTracing" />

            <section name="isapiFilters" allowDefinition="MachineToApplication" />

            <section name="odbcLogging" />

        </sectionGroup>

    </configSections>

 

    <system.applicationHost>

        <applicationPools>

            <add name="AppPool" managedPipelineMode="Integrated" managedRuntimeVersion="v4.0" autoStart="true" />

        </applicationPools>

 

        <sites>

            <site name="MySite" id="1">

                <bindings>

                    <binding protocol="http" bindingInformation="*:{1}:localhost" />

                </bindings>

                <application path="/" applicationPool="AppPool" >

                    <virtualDirectory path="/" physicalPath="{0}" />

                </application>

            </site>

        </sites>

    </system.applicationHost>

 

    <system.webServer>

        <globalModules>

            <add name="StaticFileModule" image="%windir%\System32\inetsrv\static.dll" />

            <add name="AnonymousAuthenticationModule" image="%windir%\System32\inetsrv\authanon.dll" />

            <add name="ManagedEngine" image="%windir%\Microsoft.NET\Framework\v4.0.30319\webengine4.dll" />

        </globalModules>

 

        <modules>

            <add name="StaticFileModule" />

            <add name="AnonymousAuthenticationModule" />

            <add name="DefaultAuthentication" type="System.Web.Security.DefaultAuthenticationModule" preCondition="managedHandler" />

            <add name="UrlAuthorization" type="System.Web.Security.UrlAuthorizationModule" preCondition="managedHandler" />

            <add name="FileAuthorization" type="System.Web.Security.FileAuthorizationModule" preCondition="managedHandler" />

            <add name="AnonymousIdentification" type="System.Web.Security.AnonymousIdentificationModule" preCondition="managedHandler" />

        </modules>

 

        <handlers accessPolicy="Read, Script">

            <add name="PageHandlerFactory-Integrated" path="*.aspx" verb="GET,HEAD,POST,DEBUG" type="System.Web.UI.PageHandlerFactory" preCondition="integratedMode" />

            <add name="StaticFile" path="*" verb="*" modules="StaticFileModule" resourceType="Either" requireAccess="Read" />

        </handlers>

 

        <staticContent>

            <mimeMap fileExtension=".html" mimeType="text/html" />

            <mimeMap fileExtension=".jpg" mimeType="image/jpeg" />

            <mimeMap fileExtension=".gif" mimeType="image/gif" />

            <mimeMap fileExtension=".png" mimeType="image/png" />

        </staticContent>

    </system.webServer>

</configuration>

And all we need to start hosting pages on the port and physical path specified by ApplicationHost.config is:

using (var host = new Host(path, port))

{

    host.Start();

 

    Console.ReadLine();

}

A couple of notes:

  • Because it calls unmanaged functions, can be terrible to debug;
  • The ApplicationHost.config needs to be in the application’s binary build directory and must have two placeholders, {0} and {1}, for the physical path and HTTP port, respectively;
  • It refers to .NET 4.0, if you want to change it, you will to change a number of modules and paths;
  • Only very few modules are loaded, if you want, get a full file from %HOMEPATH%\Documents\IISExpress\config\ApplicationHost.config and adapt it to your likings.

.NET TcpListener

And finally, one for the low-level guys. The TcpListener class allows the opening of TCP/IP ports and the handling of requests coming through them. It doesn’t know anything about the HTTP protocol, of course, so, if we want to leverage it, we need to implement it ourselves. Here’s a very, very, basic example:

var listener = System.Net.Sockets.TcpListener.Create(2000);

listener.Start();

 

using (var client = listener.AcceptTcpClient())

{

    using (var reader = new StreamReader(client.GetStream()))

    using (var writer = new StreamWriter(client.GetStream()))

    {

        var request = reader.ReadLine();

 

        writer.WriteLine("HTTP/1.1 200 OK");

        writer.WriteLine("Content-type: text/plain");

        writer.WriteLine();

        writer.WriteLine("Hello, World!");

        writer.Flush();

    }

}

 

listener.Stop();

Here we’re just reading any string content and responding with some HTTP headers plus the usual response. Of course, HTTP is quite complex, so I wouldn’t recommend you try to implement it yourself.

Conclusion

I presented a couple of solutions for hosting web resources, servicing HTTP requests or running ASP.NET handlers. Hopefully you will find one that matches your needs.