Click here to Skip to main content
15,881,882 members
Articles / Programming Languages / C#

Embeddable web server

Rate me:
Please Sign up or sign in to vote.
4.87/5 (32 votes)
21 Jan 2009LGPL35 min read 80.2K   4.2K   107   16
A web server with HTTPS, MVC, and REST support.

Introduction

This project is a flexible HTTP server which can be embedded in any .NET application. The server implements both HTTP and HTTPS. It has a modular design where features are added using modules. The server supports REST and all HTTP verbs.

The modular design allows you to use a small specific part, or get a full blown MVC web server.

  • Just use the (framework included, not System.Net.) HttpListener to take care of everything yourself.
  • Use the HttpServer that also gives you better response handling and sessions.
  • Add the file module, which also allows the server to serve files under a specific directory/subdirectories (you can also use multiple file modules).
  • Add the controller module to be able to use controllers.
  • Use the template manager in your controllers to get views.
  • Use ViewController to integrate views in the controllers (adds some Render methods).

Background

I needed a web server which can be embedded in my own server, which excluded ASP.NET since I didn't want to use Remoting. I started to build a web server using the .NET HttpListener. It worked fine until I tried to use my application with a regular user Windows account =) The problem with the .NET HttpListener is that it needs to have administrator privileges to be able to register the URIs that your project is going to use.

Using the code

The project is quite large, and an article is not enough to demonstrate everything. I will therefore start with a simple example demonstrating how to use the HttpListener, and then move forward to show a complete MVC server example.

Using the HttpListener

Let's look at the most basic example, creating an HTTP Listener and accepting connections.

C#
public class Example1
{
    private HttpListener _listener;

    public void Start()
    {
        _listener = HttpListener.Create(IPAddress.Any, 8081);
        _listener.RequestReceived += OnRequest;
        _listener.Start(5);
    }

    private void OnRequest(object source, RequestEventArgs args)
    {
        IHttpClientContext context = (IHttpClientContext)source;
        IHttpRequest request = args.Request;

        // Respond is a small convenience function that
        // let's you send one string to the browser.
        // you can also use the Send, SendHeader
        // and SendBody methods to have total control.
        if (request.Uri.AbsolutePath == "/hello")
            context.Respond("Hello to you too!"); 

        // Or use the HttpResponse class.
        else if (request.UriParts.Length == 1 && 
                 request.UriParts[0] == "goodbye")
        {
            HttpResponse response = new HttpResponse(context, request);
            StreamWriter writer = new StreamWriter(response.Body);
            writer.WriteLine("Goodbye to you too!");
            writer.Flush();
            response.Send();
        }
    }
}

Using HTTPS is not much harder. We just need to load a certificate: How to generate a certifcate with OpenSSL.

C#
_cert = new X509Certificate2("../../certInProjectFolder.p12", "yourCertPassword");
_listener = HttpListener.Create(IPAddress.Any, 443, _cert);

Creating an HTTP module

Since the HTTP server is module based, you can easily add your own modules to do whatever you want. Let's create a custom module...

C#
class MyModule : HttpModule
{
    public override bool Process(IHttpRequest request, 
           IHttpResponse response, IHttpSession session)
    {
        if (session["times"] == null)
            session["times"] = 1;
        else
            session["times"] = ((int) session["times"]) + 1;

        StreamWriter writer = new StreamWriter(response.Body);
        writer.WriteLine("Hello dude, you have been here " + 
                         session["times"] + " times.");
        writer.Flush();

        // return true to tell webserver that we've handled the url

        return true;
    }
}

... and add it to an HttpServer:

C#
HttpServer.HttpServer server = new HttpServer.HttpServer();
_server.Add(new MyModule());
_server.Start(IPAddress.Any, 8081);

Creating an MVC server

To be able to create an MVC server, we need an HttpServer, a file module (to handle static or embedded files), and the controller module (that controllers are added to). In this example, I'll skip multilingual support; check the last tutorial in the project source code to see how you add multilingual support.

I usually create a folder structure in my project that looks like this:

ProjectRoot
    Controllers
    Models
    public
        images
        javascripts
        stylesheets
    views

The Controllers namespace contains all the controllers in the project. A controller is sort of a bridge between the models (business objects) and the views. Their responsibility is to load business objects and put them into the views, as a kind of abstraction layer between the models (business layer) and the views (design/presentation layer).

Models are, as I said, your business objects, i.e., a user, a message etc. I've seen many examples where models actually have two roles:

  1. To contain business objects.
  2. To load/save business objects from/to a data source (usually a database). I strongly recommend you to not do so, although that's a bit off topic. Ask in the comments section if you are interested in why.

The "public" folder contains all the static files which are loaded and sent directly by the web server (through the file module). Files can be both on disk and in resources. I usually configure the web server to first try to load files from the hard drive, and if unsuccessful, from the resources. In this way, I can customize design and etc., by just placing the custom files on disk (and therefore override files stored in the resources).

The "views" folder contains all the views that business objects are rendered into. The project currently has two different template engines. One for ASP similar tags, and one for HAML. You can add your own template engines by implementing the ITemplateGenerator interface. All templates are compiled into .NET objects at runtime to increase performance.

Creating controllers

Let's start with controllers. It can be a good idea to create a base controller that has common methods that most of your controllers use. I usually call that controller "ApplicationController".

C#
public abstract class ApplicationController : ViewController
{
    protected ApplicationController(TemplateManager mgr) : base(mgr)
    {
    }

    protected ApplicationController(ViewController controller) : base(controller)
    {
    }

    [BeforeFilter]
    protected bool CheckLogin()
    {
        if (UserName == null && Request.Uri.AbsolutePath != "/user/login/")
        {
            Session["ReturnTo"] = Request.Uri.OriginalString;
            Response.Redirect("/user/login/");
            return false;
        }

        return true;
    }

    protected void ReturnOrRedirect(string url)
    {
        if (Session["ReturnTo"] != null)
            Response.Redirect((string) Session["ReturnTo"]);
        else
            Response.Redirect(url);
    }

    public string UserName
    {
        get
        {
            return (string)Session["UserName"];
        }
        set
        {
            Session["UserName"] = value;
        }
    }
}

BeforeFilters are processed before anything else, and can therefore be used for access control, as in this example.

In this example, we'll just create a controller handling users.

C#
public class UserController : ApplicationController
{
    public UserController(TemplateManager mgr, LanguageNode modelLanguage) : 
                          base(mgr, modelLanguage)
    {
        DefaultMethod = "Main";
    }
    public UserController(ViewController controller) : base(controller)
    {
    }

DefaultMethod is called if no action has been specified, as in "http://localhost/user/".

C#
public string Login()
{
    if (Request.Method == Method.Post)
    {
        FormValidator validator = new FormValidator(Request.Form, Errors);
        string userName = validator.Letters("UserName", true);
        string password = validator.LettersOrDigits("Password", true);
        if (validator.ContainsErrors)
            return RenderErrors("Login", "UserName", userName, "Password", password);

        // validate login

        UserName = userName; // save it in the session

        // and redirect 
        ReturnOrRedirect("/user/");
        return null;
    }

    return Render("UserName", string.Empty, "Password", string.Empty);
}

As you can see, I use the HTTP method to determine if a form has been posted or not. Since we do not use a data source in this example, we'll just pretend that the user has been authenticated properly. The ReturnOrRedirect method will go back to the previous page (if any).

FormValidator is a helper class that can be used for validation. There is another alternative further down in this article.

C#
public string Main()
{
    return Render("message", "Welcome " + UserName);
}

public string AjaxDeluxe()
{
    if (Request.IsAjax)
    {
        Dictionary<int, string> items = new Dictionary<int, string>();
        items.Add(1, "Jonas");
        items.Add(2, "David");
        items.Add(3, "Arne");
        return FormHelper.Options(items, HandleList, 2, true);
    }

    return Render();
}

private static void HandleList(object obj, out object id, out string title)
{
    KeyValuePair<int, string> pair = (KeyValuePair<int, string>)obj;
    id = pair.Key.ToString();
    title = pair.Value;
}

The server supports AJAX, and will automatically exclude the page layout (master page in ASP) when rendering the result if the request is made through AJAX. The FormHelper.Options method will generate a set of OPTION HTML tags, and it uses a delegate to get name/value from objects.

C#
public string Info()
{
    // todo: here you should really load user from the DB
    User user = new User();
    user.UserName = UserName;
    return Render("user", user);
}

public string Settings()
{
    // todo: here you should really load user from the DB
    User user = new User();
    user.UserName = UserName;
    if (Request.Method == Method.Post)
    {
        // load everything from the web form to the object.
        foreach (HttpInputItem item in Request.Form["user"])
            Property.Set(user, item.Name, 
               item.Value == string.Empty ? null : item.Value);

        // validate all attributes.
        ObjectValidator validator = ObjectValidator.Create(typeof(User));
        IList<ValidationError> errors = validator.Validate(user);
        if (errors.Count > 0)
            return RenderErrors(errors, "settings", "user", user);

        // and here you should save it to the database.


        // and do something when you are done.
        Response.Redirect("/user/");
        return null;
    }

    // just render the web form.
    return Render("user", user);
}

First of all, a class in Fadd called Property is used to assign all the values from the HTML form to the model. Quite easy, huh? Next, we use something called ObjectValidator to make sure that all fields have been entered properly. ObjectValidator uses attributes specified in the model for the validation.

C#
     public override object Clone()
    {
        return new UserController(this);
    }
}

And finally, we have to setup the server.

C#
// since we do not use files on disk, we'll just add the resource template loader.
ResourceTemplateLoader templateLoader = new ResourceTemplateLoader();
templateLoader.LoadTemplates("/", Assembly.GetExecutingAssembly(), 
                             GetType().Namespace + ".views");
TemplateManager templateManager = new TemplateManager(templateLoader);
templateManager.AddType(typeof (WebHelper));
templateManager.Add("haml", new HttpServer.Rendering.Haml.HamlGenerator());

// we've just one controller. Add it.
ControllerModule controllerModule = new ControllerModule();
controllerModule.Add(new UserController(templateManager));
_server.Add(controllerModule);

// add file module, to be able to handle files
ResourceFileModule fileModule = new ResourceFileModule();
fileModule.AddResources("/", Assembly.GetExecutingAssembly(), 
                        GetType().Namespace + ".public");
_server.Add(fileModule);

// ok. We should be done. Start the server.
_server.Start(IPAddress.Any, 8081);

Console.WriteLine("Server is loaded. Go to http://localhost:8081/.");
Console.ReadLine();

_server.Stop();

Final words

You can find the project, tutorials, and a discussion forum at CodePlex.

History

  • 2009-01-21 - First version.
  • 2009-01-27 - Fixed a lot of spelling errors =)

License

This article, along with any associated source code and files, is licensed under The GNU Lesser General Public License (LGPLv3)


Written By
Founder 1TCompany AB
Sweden Sweden

Comments and Discussions

 
GeneralMy vote of 5 Pin
Greg Sipes29-Feb-12 4:29
Greg Sipes29-Feb-12 4:29 
GeneralHttpListener.RequestReceived with both Form and QueryString Pin
Alex Weslowski10-Dec-09 16:23
Alex Weslowski10-Dec-09 16:23 
Hi I just thought I'd mention that when a request contains both a POST and a querystring, the Param parameters get filled with querystring values only, and both the Form and the QueryString parameter collections are empty. This might be a special case, the request is coming from a Java applet which might be using an old or rare HTTP implementation. But it's still strange and took me a while to figure out, so here's some example code to help with parsing the POST request body into parameters...

<br />
            //IHttpRequest request;<br />
            //HttpInput Form;<br />
            <br />
            string body = string.Empty;<br />
            if (request.Body != null && request.Body.Length > 0)<br />
            {<br />
                byte[] bytes = new byte[request.Body.Length];<br />
                request.Body.Seek(0, System.IO.SeekOrigin.Begin);<br />
                request.Body.Read(bytes, 0, bytes.Length);<br />
                body = System.Text.Encoding.Default.GetString(bytes);<br />
            }<br />
<br />
            if ((request.Form == null || request.Form.Count() == 0) && body != null && body.Trim() != string.Empty)<br />
            {<br />
                NameValueCollection collection = HttpUtility.ParseQueryString(body);<br />
                Form = new HttpInput("form");<br />
                for (int i = 0; i < collection.Count; i++)<br />
                {<br />
                    Form.Add(collection.GetKey(i), collection.Get(i));<br />
                }<br />
            }<br />

QuestionDo not save in Models ? Pin
dannygoh28-Sep-09 4:15
dannygoh28-Sep-09 4:15 
GeneralI need some tips [modified] Pin
Ivan Peshev25-Sep-09 21:52
Ivan Peshev25-Sep-09 21:52 
Generalthis is great!! Pin
Member 18388312-Jul-09 4:57
Member 18388312-Jul-09 4:57 
Generalcannot display chinese character correctly Pin
yajiya3-Jun-09 17:52
yajiya3-Jun-09 17:52 
GeneralRe: cannot display chinese character correctly Pin
jgauffin3-Aug-09 8:38
jgauffin3-Aug-09 8:38 
GeneralGreat! Pin
PavelGnelitsa15-Mar-09 19:50
PavelGnelitsa15-Mar-09 19:50 
GeneralRe: Great! Pin
jgauffin15-Mar-09 21:18
jgauffin15-Mar-09 21:18 
NewsOhh, forgot one thing Pin
jgauffin28-Jan-09 21:01
jgauffin28-Jan-09 21:01 
GeneralLooks like ASP.Net is being rebuilt by everyone Pin
Tom Janssens21-Jan-09 21:20
Tom Janssens21-Jan-09 21:20 
AnswerRe: Looks like ASP.Net is being rebuilt by everyone Pin
jgauffin21-Jan-09 21:55
jgauffin21-Jan-09 21:55 
GeneralCan't find the file at http://localhost:8081/. Pin
sam.hill21-Jan-09 18:42
sam.hill21-Jan-09 18:42 
GeneralRe: Can't find the file at http://localhost:8081/. Pin
jgauffin21-Jan-09 19:31
jgauffin21-Jan-09 19:31 
GeneralRe: Can't find the file at http://localhost:8081/. Pin
Luka8-Dec-09 11:55
Luka8-Dec-09 11:55 
GeneralThat is.. Pin
Tolgahan ALBAYRAK21-Jan-09 3:48
Tolgahan ALBAYRAK21-Jan-09 3:48 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.