Click here to Skip to main content
15,867,308 members
Articles / Web Development / HTML

A Note on Owin Hosted Services

Rate me:
Please Sign up or sign in to vote.
5.00/5 (8 votes)
18 Jun 2017CPOL11 min read 21.1K   135   11   4
This is a note on Owin (Open Web Interface for .NET) hosted services.

Introduction

This is a note on Owin (Open Web Interface for .NET) hosted services.

Background

This is a note on Owin hosted services. This note is not intended to be a complete reference to Owin. It only addresses a few interesting questions when we use Owin to serve the web contents,

Image 1

The attached is a solution created in Visual Studio 2015. It has six small console applications, each addresses a subject when we use Owin. If you want to run the applications, It is recommend to start your Visual Studio as an administrator. If not, you may not be able to start the services in the debug mode.

A-Minimal-Owin-Application

As the first step, it is nice if we can create a minimum Owin application.

Image 2

To create a minimum Owin application, you need to install minimum three Nuget packages. Any Nuget packages that these three packages depend on will be installed to the project automatically by Visual Studio.

Image 3

Because the intention is to keep the application minimum, all the code to start an Owin service is in the "Program.cs" file.

C#
using Microsoft.Owin.Hosting;
using Owin;
using System;
using System.Web.Http;
    
namespace A_Minimal_Owin_Application
{
    /// <summary>
    /// To start an Owin service, the minimal Nuget packages needed are
    /// 1. Microsoft.Owin.Hosting
    /// 2. Microsoft.Owin.Host.HttpListener
    /// 3. Microsoft.AspNet.WebApi.Owin
    /// 4. The rest of the packges are the dependencies of the 3 above
    /// </summary>
    class Program
    {
        static void Main(string[] args)
        {
            WebApp.Start<Startup>("http://*:800/");
            WebApp.Start<Startup>("http://*:900/");
    
            Console.WriteLine("A_Minimal_Owin_Application started");
            Console.Write("Type any key to stop ... ");
            Console.Read();
        }
    }
    
    class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            // Configure the web apis
            HttpConfiguration config = new HttpConfiguration();
    
            config.MapHttpAttributeRoutes();
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "{controller}/{action}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
    
            app.UseWebApi(config);
        }
    }
    
    public class ResponsePayload
    {
        public string Text { get; set; }
    }
    
    public class AConcreteController : ApiController
    {
        private int i = 0;
    
        public ResponsePayload getAString()
        {
            i++;
            return new ResponsePayload() {
                Text = "i = " + i
            };
        }
    }

    [RoutePrefix("A")]
    public class AnotherConcreteController : ApiController
    {
        [HttpGet]
        [Route("B")]
        public ResponsePayload getAString(string A)
        {
            return new ResponsePayload() {
                Text = A
            };
        }
    }
}

The "Startup" class

To start an Owin service, we need an Owin start-up class

  • The "Startup" class tells how Owin handles the web requests. The name of the class is not important, but it needs to implement the "public void Configuration(IAppBuilder app)" method;
  • When the program runs, we can use the "WebApp.Start<Startup>("http://*:800/")" syntax to start the Owin application. We can call the "WebApp.Start()" multiple times to let the Owin application to listen to multiple port numbers.

The controller classes

When a web request hits the Owin service, Owin will respond to the web request. Typically the response is implemented in a controller class.

  • All the controller classes need to be sub-classes of the "ApiController" classes;
  • All the controller classes need to be public classes;
  • All the controller classes need to implement some action methods. All the action methods need to be public methods;
  • By default, a GET request "http://localhost:800/AConcrete/getAString" is mapped to the "getAString()" method in the "AConcreteController" class;
  • We can use "RoutePrefix()", "Route()", and "HttpGet" annotations to change the request mapping. A GET request "http://localhost:800/A/B?A=ABCD" is mapped to the "getAString()" method in the "AnotherConcreteController" class.

Run the Application

When an Owin application starts, Owin will search for any public sub-classes of the "ApiController" class in any available assemblies. When an http request comes, if the mapping rule identifies a public action method in one of the controller classes, Owin will create an instance of the controller class and serve the request through the action method. Because we configured Owin to listen to both port number 800 and 900, we can use either of the port numbers to access the service.

  • If we issue a GET request to "http://localhost:800/AConcrete/getAString" through the POSTMAN, the "getAString()" method in the "AConcreteController" class is hit with the response '{"Text": "i = 1"}'. If we issue the same request again, the same response is returned. If you want to use any instance variables in your controller classes, you need to be aware that Owin creates a new instance of the controller class for every request;
  • If we issue a GET request to "http://localhost:800/A/B?A=ABCD", the "getAString()" method in the "AnotherConcreteController" class is used by Owin to respond to the request.

The following shows the the response received by the POSTMAN for the request "http://localhost:800/A/B?A=ABCD".

Image 4

B-Middle-Ware

If you have used "Node.JS", you should be familiar with middlewares. A middleware is a function that intercepts all the web requests. We can use a middleware to alter the default response behavior.

Image 5

There are many ways to add a middleware to Owin. In this note, I used the simplest way (at least syntactically) to add a middleware in the "Configuration()" method in the "Startup" class.

C#
using Microsoft.Owin.Hosting;
using Owin;
using System;
using System.Web.Http;
    
namespace B_Middle_Ware
{
    class Program
    {
        static void Main(string[] args)
        {
            WebApp.Start<Startup>("http://*:800/");
    
            Console.WriteLine("B_Middle_Ware started");
            Console.Write("Type any key to stop ... ");
            Console.Read();
        }
    }
    
    class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            // A middleware Disable browser cache for all the request
            app.Use((ctx, next) =>
            {
                ctx.Response.Headers["Cache-Control"]
                    = "no-cache, no-store, must-revalidate";
                ctx.Response.Headers["Pragma"] = "no-cache";
                ctx.Response.Headers["Expires"] = "-1";
    
                return next();
            });
    
            // Configure the web apis
            HttpConfiguration config = new HttpConfiguration();
    
            config.MapHttpAttributeRoutes();
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "{controller}/{action}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
    
            app.UseWebApi(config);
        }
    }
    
    public class ResponsePayload
    {
        public string Text { get; set; }
    }
    
    public class AConcreteController : ApiController
    {
        [HttpGet]
        public ResponsePayload getAString()
        {
            return new ResponsePayload()
            {
                Text = "Cache is disabled. Check the HTTP headers"
            };
        }
    }
}

In the "public void Configuration(IAppBuilder app)" method, I used the "app.Use()" method to add an lambda function to the Owin configuration. The lambda function is an Owin middleware.

  • The first parameter of the lambda function is an "IOwinContext", we can use this object to alter the response behavior for all the http requests;
  • The second parameter represents another middleware or an action method. If we do not want Owin to stop further processing the request, we typically need to "return next()" to let it to run.

In this example, I added the headers to disable the browser cache for the responses in the middleware. Unless interrupted, the middleware runs for all the http requests, so the headers are added to all the http requests. If you run the application and hit the url "http://localhost:800/AConcrete/getAString", you can see that the headers are added to the response.

Image 6

C-Action-Filter

In the previous example, we used a middleware to disable the browser cache for all the requests. But in some cases, we do not want the headers to be added to all the requests, but only to some of the controllers or action methods. In this case, an action filter can be used.

Image 7

C#
using Microsoft.Owin.Hosting;
using Owin;
using System;
using System.Net.Http.Headers;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
    
namespace C_Action_Filter
{
    class Program
    {
        static void Main(string[] args)
        {
            WebApp.Start<Startup>("http://*:800/"); ;

            Console.WriteLine("C_Action_Filter started");
            Console.Write("Type any key to stop ... ");
            Console.Read();
        }
    }
    
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {

            HttpConfiguration config = new HttpConfiguration();
    
            config.MapHttpAttributeRoutes();
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "{controller}/{action}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
    
            app.UseWebApi(config);
        }
    }
    
    // The action filter to disable the browser caching
    public class NoCache : ActionFilterAttribute
    {
        public override void OnActionExecuting(HttpActionContext ctx)
        {
            base.OnActionExecuting(ctx);
        }
    
        public override void OnActionExecuted(HttpActionExecutedContext ctx)
        {
            // Add the cache control headers
            ctx.Response.Headers
                .Add("Cache-Control", "no-cache, no-store, must-revalidate");
            ctx.Response.Headers.Add("Pragma", "no-cache");
            ctx.Response.Content.Headers.Expires = DateTimeOffset.Now.AddDays(-365);
    
            base.OnActionExecuted(ctx);
        }
    }
    
    public class AConcreteController : ApiController
    {
        public class ResponsePayload
        {
            public string Text { get; set; }
        }
    
        [NoCache]
        [HttpGet]
        public ResponsePayload NoCacheContent()
        {
            return new ResponsePayload { Text = "The content should not be cached" };
        }
    
        [HttpGet]
        public ResponsePayload CacheableContent()
        {
            return new ResponsePayload { Text = "The content is cacheable" };
        }
    
    }
}

An action filter is a sub-class of the "ActionFilterAttribute" class. In this example, I implemented an action filter in the "NoCache" class.

  • We can override the "OnActionExecuting()" and "OnActionExecuted()" methods in an action filter. The "OnActionExecuting()" method is called before the action method is invoked, the "OnActionExecuted()" method is called after the action method is invoked;
  • In the "NoCache" class, the http headers to disable the browser cache is added to the response.

To apply an action filter to an action method, we can annotate the class name on the action method. If you want the action filter to apply to all the action methods in the controller, you can annotate it on the controller level. In this example, if you hit "http://localhost:800/AConcrete/NoCacheContent", you can see the headers are added to the response. But if you hit "http://localhost:800/AConcrete/CacheableContent", the headers to disable the browser cache is not added to the response. The following show the response headers for the "http://localhost:800/AConcrete/CacheableContent" request.

Image 8

D-Static-Content

We have seen the examples that Owin serves http requests from the action methods. But it is ideal that Owin can also serve static contents. To server static contents from Owin, we need to add an additional Nuget package.

Image 9

The goal of this example is to configure Owin to serve the "A-test-page.html" page in the "Static-content" folder.

Image 10

In order that the "A-test-page.html" is available to the build output folder, we need to right click the "A-test-page.html" page -> Properties to make the "Build Action" to "Content" and "Copy to Output Directory" to "Copy if newer".

Image 11

C#
using Microsoft.Owin.FileSystems;
using Microsoft.Owin.Hosting;
using Microsoft.Owin.StaticFiles;
using Owin;
using System;
    
namespace D_Static_Content
{
    /// <summary>
    /// In addition to the minimal Owin Nuget dependencies
    /// The "Microsoft.Owin.StaticFiles" Nuget package is needed
    /// </summary>
    class Program
    {
        static void Main(string[] args)
        {
            WebApp.Start<Startup>("http://*:800/");
    
            Console.WriteLine("D_Static_Content started");
            Console.Write("Type any key to stop ... ");
            Console.Read();
        }
    }
    
    class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            var path = "./Static-content";
    
            #if DEBUG
            path = "../../Static-content";
            #endif
    
            app.UseFileServer(new FileServerOptions
            {
                EnableDefaultFiles = true,
                FileSystem = new PhysicalFileSystem(path)
            });
        }
    }
}

In order that Owin can serve the static contents, we need to map the physical location of the files in the "public void Configuration(IAppBuilder app)" method. The path of the physical files is relative the location of the EXE file of the application.

  • By default, the location is mapped to the build output folder;
  • In debug mode, the location is mapped to the location of the files in the Visual Studio project. Because they are static files, we do not need to re-compile the project to see the changes if we modify the static files while debugging the application.

Run the application and hit the "http://localhost:800/A-test-page.html" from the browser, we can see that the "A-test-page.html" page is successfully loaded.

Image 12

E-Cross-Domain

The cross domain problem in a web application is a common problem when we make Ajax calls. It is a relatively well documented problem. But it is also a problem that caused a lot of confusions. In order to support cross domain, Owin actually requires us to install two Nuget packages. If I have time, I may write a dedicated note to talk about the cross domain problem. But in this note, I will only show you how the cross domain works without using the Nuget packages.

  • In the context of the cross domain problem, a domain is recognized by the url. For example, when the browser loads an image from "http://domainb.foo:8080/image.jpg", the domain of the image file is identified by "http://domainb.foo:8080", which includes the port number;
  • When the url to load the HTML content and the url to issue an Ajax call is not in the same domain, the Ajax call is considered cross domain by the browser;
  • When a web browser makes a simple cross domain GET request through an Ajax call, it will make the call directly. But the request header will have an entry named "Referer". The "Referer" is the domain of the web page that makes the Ajax call. If the web server allows this request, it needs to add the "Access-Control-Allow-Origin" response header that matches the "Referer". When the browser receives the response, it will check the "Access-Control-Allow-Origin" header. If it finds the match, it considers the Ajax call a success. Otherwise, it will fail the Ajax request;
  • When a web browser makes a cross domain GET request that involves complex data or the request is not GET, but POST, PUT, etc. It will first make a pre-flighted OPTION request to the server to check if the Ajax request is allowed. The server needs to respond with the appropriate headers to this OPTION request to tell the browser what are the allowed methods and options. If the the type Ajax request is not allowed, the browser will stop and fail the Ajax request.

Image 13

In this example, I will use the "A-test-page.html" to make some Ajax calls to experiment the cross domain problem.

C#
using Microsoft.Owin.FileSystems;
using Microsoft.Owin.Hosting;
using Microsoft.Owin.StaticFiles;
using Owin;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Web.Http;
    
namespace E_Cross_Domain
{
    /// <summary>
    /// In addition to the minimal Owin Nuget dependencies
    /// The following Nuget Packages may be useful for cross domain problems
    /// 1. "Microsoft.AspNet.WebApi.Cors"
    /// 2. "Microsoft.Owin.Cors"
    /// But this example does not use the Nuget packages
    /// </summary>
    class Program
    {
        static void Main(string[] args)
        {
            WebApp.Start<Startup>("http://*:700/");
    
            WebApp.Start<Startup>("http://*:800/");
            WebApp.Start<Startup>("http://*:900/");
    
            Console.WriteLine("E_Cross_Domain started");
            Console.Write("Type any key to stop ... ");
            Console.Read();
        }
    }
    
    class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            // Disable browser cache for all the requests for easy testing
            app.Use((ctx, next) =>
            {
                ctx.Response.Headers["Cache-Control"] = "no-cache, no-store, must-revalidate";
                ctx.Response.Headers["Pragma"] = "no-cache";
                ctx.Response.Headers["Expires"] = "-1";

                return next();
            });
    
            // Owin middle ware to allow cross domain Ajax calls
            // Add the permitted domain in the HashSet to allow cross domain Ajax
            HashSet<string> permitted_domains = new HashSet<string>();
            permitted_domains.Add("http://localhost:900");
    
            app.Use((ctx, next) =>
            {
                var request = ctx.Request;
                var response = ctx.Response;
    
                // If the request has a referer, check if we allow the referer to
                // make the call. If we do, send the "Access-Control-Allow-Origin" header.
                var referer = request.Headers["Referer"];
                if (referer != null)
                {
                    Uri refUri = new Uri(referer);
                    string domainString = refUri.Scheme + "://" + refUri.Authority;
    
                    if (permitted_domains.Contains(domainString))
                    {
                        response.Headers["Access-Control-Allow-Origin"] = domainString;
                    }
                }
    
                // Respond to the browser for preflighted requests
                if (request.Method == "OPTIONS")
                {
                    response.Headers["Access-Control-Allow-Methods"] = "GET, POST";
                    response.Headers["Access-Control-Allow-Headers"] = "Content-Type, Accept";
                    response.Headers["Access-Control-Max-Age"] = "86400";
    
                    return Task.Run(() => { });
                }
    
                return next();
    
            });
    
            // 2. Configure static files
            var path = "./Static-content";
    
            #if DEBUG
            path = "../../Static-content";
            #endif
    
            app.UseFileServer(new FileServerOptions
            {
                EnableDefaultFiles = true,
                FileSystem = new PhysicalFileSystem(path)
            });
    
            // 3. Configure the web apis
            HttpConfiguration config = new HttpConfiguration();
    
            config.MapHttpAttributeRoutes();
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "{controller}/{action}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
    
            app.UseWebApi(config);
    
        }
    }
    
    public class AConcreteController : ApiController
    {
        [HttpGet]
        public object getTimeByGET()
        {
            return new { Time = DateTime.Now };
        }
    
        [HttpPost]
        public object getTimeByPOST()
        {
            return new { Time = DateTime.Now };
        }
    
        [HttpPut]
        public object getTimeByPUT()
        {
            return new { Time = DateTime.Now };
        }
    }
}
  • In this example, we configured Owin to listen to three ports "700", "800", and "900". In the experiments, I will use "700" to make Ajax calls to the action methods implemented in the "AConcreteController" controller. The "800" and "900" are used to load the "A-test-page.html" page;
  • The "AConcreteController" class implemented three action methods. Each method only takes one http method, either GET, POST, or PUT;
  • A Owin middleware is added to control the cross domain Ajax access. It only sends the "Access-Control-Allow-Origin" when the request "Referer" is "http://localhost:900". When a pre-flighted OPTION request is received, it tell the browser that it allows only "GET" and "POST" methods.

The "A-test-page.html" page has three buttons, each makes an Ajax call to the corresponding action method.

<!DOCTYPE html>
    
<html>
<head>
    <meta charset="utf-8" />
    <title>A static HTML page</title>
    
    <style type="text/css">
        body {
            font-family: Verdana;
            font-weight: 600;
        }
        button {
            width: 150px;
        }
    </style>
    
    <script type="text/javascript">
        window.onload = function () {
            let Ajax = function (method, api) {
                // Hardcoded to call the port number 700
                let url = 'http://localhost:700/' + api;
    
                let xhr = new XMLHttpRequest();
                xhr.open(method, url);
                xhr.setRequestHeader('Content-Type', 'application/json');
    
                xhr.onload = function () {
                    if (xhr.status === 200) {
                        alert(xhr.responseText);
                    }
                    else {
                        alert('Request failed status = ' + xhr.status);
                    }
                };
    
                xhr.onerror = function () {
                    alert('Failed!');
                }
    
                return xhr;
            };
    
            document.getElementById('btnGET')
                .onclick = function () {
                    Ajax("GET", "AConcrete/getTimeByGET").send();
            };
    
            document.getElementById('btnPOST')
                .onclick = function () {
                    Ajax("POST", "AConcrete/getTimeByPOST").send();
            };
    
            document.getElementById('btnPUT')
                .onclick = function () {
                    Ajax("PUT", "AConcrete/getTimeByPUT").send();
            };
    
        };
    </script>
</head>
<body>
    <div><button id="btnGET">GET METHOD</button></div>
    <div><button id="btnPOST">POST METHOD</button></div>
    <div><button id="btnPUT">PUT METHOD</button></div>
</body>
</html>

If we start the application and load the "A-test-page.html" by the url "http://localhost:800/A-test-page.html", you will find that all the Ajax calls fail due to that the "Access-Control-Allow-Origin" header is not available. If you access the "A-test-page.html" by the "http://localhost:900/A-test-page.html", you will find that both the GET and POST methods succeed, but the PUT method fail. The following shows that the PUT Ajax call failed when the page is loaded by port number "900".

Image 14

F-Run-As-Windows-Service

We have seen many examples in this note to start Owin applications as console applications. In many cases, it is necessary that we can deploy an Owin application as a Windows service, so it can start by itself when the computer starts. When I prepare this example, I referenced this example. Although I did not follow exactly his method, I found it is very well written and helpful. To create a Windows service, you need to add the "System.ServiceProcess" reference to your project.

Image 15

The "Program.cs" file implemented a sub-class of the "ServiceBase" class. Visual studio notices that it is a sub-class of "ServiceBase", it will try to take control on it. If you want to view the source of this file, you need to right click on it and choose the "View Code" option.

C#
using Microsoft.Owin.Hosting;
using Owin;
using System;
using System.ServiceProcess;
using System.Web.Http;
    
namespace F_Run_As_Windows_Service
{
    public class WindowsService : ServiceBase
    {
        private static void StartOwinService()
        {
            WebApp.Start<Startup>("http://*:800/");
        }
    
        static void Main()
        {
            if (Environment.UserInteractive)
            {
                StartOwinService();
    
                Console.WriteLine("F_Run_As_Windows_Service started");
                Console.Write("Type any key to stop ... ");
                Console.Read();
            }
            else
            {
                ServiceBase.Run(new WindowsService());
            }
        }
    
        public WindowsService()
        {
            this.CanHandlePowerEvent = false;
            this.CanHandleSessionChangeEvent = false;
            this.CanPauseAndContinue = false;
            this.CanShutdown = false;
            this.CanStop = true;
        }
    
        protected override void OnStart(string[] args)
        {
            StartOwinService();
            base.OnStart(args);
        }
    
        // Nothing to do when service stops
        protected override void OnStop() { base.OnStop(); }
        protected override void Dispose(bool disposing) { base.Dispose(disposing); }
    }
    
    class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            HttpConfiguration config = new HttpConfiguration();
    
            config.MapHttpAttributeRoutes();
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "{controller}/{action}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
    
            app.UseWebApi(config);
        }
    }
    
    public class ResponsePayload
    {
        public string Text { get; set; }
    }
    
    public class AConcreteController : ApiController
    {
        public ResponsePayload getAString()
        {
            return new ResponsePayload()
            {
                Text = "Owin service hosted in Wnd service"
            };
        }
    }
}

This is a minimum Windows service and also a console application. The Owin service is started in the static method "StartOwinService".

  • In the "static void Main()" method, if the "Environment.UserInteractive" is true, it means that we are debugging. It will start as a console application, so we can run the Owin service without deploying it as a Windows service;
  • If the "Environment.UserInteractive" is false, it means that we have deployed it as a Windows service. The Owin service will be started in the "OnStart" event of the Windows service.

Deploy the Windows Service

After compile the application, we can deploy it as a Windows service by the "sc.exe". The "sc.exe" is available on all the windows machines. You may need to start your command prompt window as an "Administrator" to work on the Windows services. You can issue the following commands to deploy and start a Windows service.

sc create OService binPath= "Abosolute-path-to-the-exe" displayName= "O Service"
sc start OService
  • The "OService" is the name of the service. You can choose any name for your service, but the name should not have spaces in it;
  • The "binPath" is the path to the exe file for the service. If no special need, I will recommend you to put the absolute path in it;
  • The "displayName" is the name that the service shows in the Windows service manager.

Image 16

If you deploy and start your service correctly, you should see the "O Service" in your Windows service manager. If you now load the url "http://localhost:800/AConcrete/getAString" with GET method in the POSTMAN, you should see your Owin service works nicely as a Windows service.

Image 17

If you want to delete your Windows service, you can issue the following commands.

sc stop OService
sc delete OService

Points of Interest

  • This is a note on Owin hosted services;
  • In this note, I have given examples for the following topics:
    • How to create a minimum Owin service;
    • How to create and use a middleware in Owin;
    • How to create and use an action filter in Owin;
    • How to host static files in Owin;
    • How to address the cross domain Ajax calls in Owin;
    • How to deploy an Owin service as a Windows service.
  • I hope you like my postings and I hope this note can help you one way or the other.

History

First Revision - 6/18/2017.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
I have been working in the IT industry for some time. It is still exciting and I am still learning. I am a happy and honest person, and I want to be your friend.

Comments and Discussions

 
QuestionHow to solve HTTP error 503 Service Unavailable Pin
Member 975448015-Jun-21 22:41
Member 975448015-Jun-21 22:41 
GeneralMy vote of 5 Pin
Igor Ladnik11-Jul-17 19:03
professionalIgor Ladnik11-Jul-17 19:03 
GeneralRe: My vote of 5 Pin
Dr. Song Li26-Aug-17 16:40
Dr. Song Li26-Aug-17 16:40 
QuestionThanks Pin
Mario Cosmi18-Jun-17 19:57
Mario Cosmi18-Jun-17 19:57 

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.