Click here to Skip to main content
Click here to Skip to main content

ApiFrame : A simple library for Web API security, exception and versioning

, 3 May 2014 CPOL
Rate this:
Please Sign up or sign in to vote.
A simple C#.NET library that implements the core components required for ASP.NET WEB API development

ApiFrame

Related article

How to integrate ApiFrame in ASP.NET Web API Application[^]

Contents

Introduction

ApiFrame is a simple .NET library that implements the core components required for ASP.NET WEB API development. It's a C# class library with logical separation of Server (Provider), Client (Consumer) and the supporting Framework (An Infrastructure). The Server components provide an interface to implement security (Authentication & Authorization), exception handling and versioning of Web API methods. The security mechanism implemented in this library is HMAC (Hash Message Authentication Code) authentication. One example of HMAC usage is Amazon web service (AWS) and the authentication process in this library uses a HMAC-SHA signature which is the same approach as AWS. The Client part provides a Gateway component that can be referenced by the .NET clients to consume the WEB API.

This article will serve as a simple documentation for ApiFrame that focus on providing a basic description about the class components within the library. The diagram below shows the outline of component architecture.

Component Architecture

HMAC Authentication – An Overview

HMAC authentication provides a simple way to authenticate a HTTP request using a secret key that is known to client and server. Both client and server have access to secret key. Generally, this secret key is a Unique Id which is created at the time of registration and stored in the database. Using the secret key and a message based on request content client generates a signature (MAC) using HMAC algorithm and this signature is attached to the authorization header of HTTP request. When the server receives the request, it extracts the hashed signature (MAC) from the request header and calculates its own version of signature to verify if the received signature matches the calculated signature. If the two signature matches, then the system concludes that the request is valid and should be served. If the two signatures don’t match, then the request is dropped and the system responds with an error message.

Framework

The Framework provides token models, configuration class, constant values, helpers, extensions to support the implementation of server and client components. Also, it offers an Interface to integrate with the other applications through dependency injection.

Tokens

HMAC authentication depends on token based communication. A Token is a small piece of data that is attached to the authorization header of every API request. In general, Tokens are keys that are used for authentication and authorization of the HTTP request. In the below class diagram, you will notice that Base class "ApiBaseToken" has the following properties

  • AccessToken: Access token is a Public key
  • SecretToken: Secret Token is a private key used for generating the signature using a hash algorithm
  • AuthScheme: AuthScheme is a simple abbreviated text that indicates the consumer (application / customer) name

There are three classes derived from this Base class, each represents a token model used for a distinct purpose. ApiApplicationToken is required for Authentication. ApiUserToken is required for Authorization. ApiRequestToken is required for creating a client request.

Interface

There are few areas in the library where dependencies need to be injected. To achieve this, the required interfaces are defined. The using application should supply the required tokens to authenticate and authorize an HTTP request. The interface IApiInception expose the necessary methods that the calling assembly need to implement which returns the tokens required to process the HTTP Request. The interface IApiException is for exception logging. Every application has its own exception logging mechanism. When an unhandled program exception is thrown in the using application, It can be caught by implementing the IApiException interface. The interface IApiSignature is to calculate HMAC signature. The library itself contains an implementation of this interface that uses HMACSHA256 algorithm to calculate the HMAC signature. The same implementation can be used or a different implementation can be injected using this interface if required.

    public interface IApiInception
    {
        ApiApplicationToken GetApplicationToken(string accessToken);
        ApiUserToken GetUserToken(string username, string password);
        ApiUserToken GetUserToken(string accessToken);
    }

    public interface IApiException
    {
        void LogMessage(Exception exception);
    }

    public interface IApiSignature
    {
        string CalculateHmac(string secretKey, string stringToSign);
    }

Dependency Injection

A simple Dependency Injection class is defined with two generic static methods that allows to inject objects into a class. RegisterType<TInterface, TClass> method is called by the using application to map an interface type with corresponding concrete class type. GetInstance<TInterface> method is called by the internal class components to get the object instance of the registered interface type.

    public class ApiObjectFactory
    {
        private static readonly Dictionary<string, System.Type> ObjectTypes 
             = new Dictionary<string, System.Type>();

        public static void RegisterType<TInterface, TClass>() where TClass : TInterface
        {
            var typeInterface = typeof(TInterface);
            var typeClass = typeof(TClass);

            if (!typeInterface.IsInterface || typeClass.IsInterface || typeClass.IsAbstract)
            {
                throw new ApiRequestException(ApiErrorCode.InvalidOperation);
            }

            ObjectTypes.Add(typeInterface.Name, typeClass);
        }

        public static TInterface GetInstance<TInterface>()
        {
            System.Type type = null;

            if (ObjectTypes.TryGetValue(typeof(TInterface).Name, out type))
            {
                return (TInterface)System.Activator.CreateInstance(type);
            }
           
            throw new ApiRequestException(ApiErrorCode.MissingRequiredType);
        }
    }

Signature Calculation

When implementing HMAC authentication, Every API request requires signing with HMAC signature. The signature is computed with the token “SecretKey” and a message “StringToSign”. The “StringToSign” is constructed using the URI, request timestamp and other HTTP header values. The computed signature is converted to base64 string. This encoded base64 string is the calculated signature used for signing the HTTP request.

    public string CalculateHmac(string secretKey, string stringToSign)
        {
            byte[] secretBytes = Encoding.UTF8.GetBytes(secretKey);
            byte[] stringBytes = Encoding.UTF8.GetBytes(stringToSign);

            string signature;

            using (var hmac = new HMACSHA256(secretBytes))
            {
                byte[] hash = hmac.ComputeHash(stringBytes);
                signature = Convert.ToBase64String(hash);
            }

            return signature;
        }

Configuration

A Configuration class "ApiConfiguration" implements a singleton instance with properties shown in the below class diagram. "RequestValidityInMinutes" property is used to configure the request validity in minutes, that no HTTP Request can be older than x minutes. The x value is configured through the instance of ApiConfiguration class. Its initial value defaults to 10 minutes. This can be modified by the using application. "VersionNamespaceKey" property is used to configure the Web API versioning method. ApiFrame allows two different method to implement versioning. It can be configured using "VersionNamespaceKey" property and it defaults to "area"

    public class ApiConfiguration
    {
        static ApiConfiguration()
        {
            Instance = new ApiConfiguration();
            Instance.RequestValidityInMinutes = 10;
            Instance.VersionNamespaceKey = "area";
        }

        public static ApiConfiguration Instance { get; private set; }
        public double RequestValidityInMinutes { get; set; }
        public string VersionNamespaceKey { get; set; }
    }

Utilities

This part of the library includes utility classes such as helpers, extensions and validation methods to access and validate the HTTP request and headers.

Server

The server components implements the required filters for Authentication and Authorization. The filters verifies the HTTP request and identifies the requester. Verifying a HTTP request on the server side involves three steps which is shown in the below diagram. The first step is to retrieve the secret token using the access token. The second step is to calculate the signature from the request parameters and secret token. The third step is to verify if the calculated signature matches the received signature.

Security

The class diagram below shows the design of the server component that handles the Authentication and Authorization.

Authentication

Authentication is identifying the user based on username and password. ApiFrame implements an authorization filter attribute “ApiAuthentication” that follows the HMAC method such as validating the signature to authenticate a user. It reads the username and password from HTTP request header and uses it to identify the user. If the user is valid, then it creates a custom identity and principal object. This custom principal object is set to the user property of current HttpContext.

    public class ApiAuthentication : AuthorizationFilterAttribute
    {
        public override void OnAuthorization(HttpActionContext actionContext)
        {
            // Get the request for the action context
            HttpRequestMessage request = actionContext.Request;

            // Create an instance of ApiValidation  
            IApiValidation apiValidation = new ApiValidation();

            // Call the validation method to validate the request
            apiValidation.ValidateRequest(request);            

            // Get the access token from the request header parameter and validate
            string apiAccessToken = request.GetAccessToken();

            // Get the instance of ApiInception
            IApiInception apiInception = ApiObjectFactory.GetInstance<IApiInception>();

            // Get the application token
            ApiApplicationToken applicationToken = apiInception.GetApplicationToken(apiAccessToken);

            // Call the validation method to validate the token and signature
            apiValidation.ValidateToken(request, applicationToken);
            apiValidation.ValidateSignature(request, applicationToken.SecretToken);

            // Read the username and password from current http request paramters
            var username = HttpContext.Current.Request.Params["Username"];
            var password = HttpContext.Current.Request.Params["Password"];

            if (username == null || password == null)
            {
                throw new ApiRequestException(ApiErrorCode.MissingRequiredParamter);
            }

            // Call the service method to get the user details
            ApiUserToken user = apiInception.GetUserToken(username, password);
            bool isAuthenticated = user != null;
            
            if (isAuthenticated)
            {
                // Set the user principal for the current http request
                string authenticationType = ApiConstants.AUTHTYPE;
                IIdentity userIdentity = new ApiIdentity(authenticationType, isAuthenticated, user.Name, user.UserId);
                IPrincipal principal = new ApiPrincipal(userIdentity, user.Roles);
                HttpContext.Current.User = principal;                
            }
            else
            {
                throw new ApiRequestException(ApiErrorCode.AuthenticationFailed);
            }
        }     
    }

Authorization

Authorization is for checking whether the authenticated user is allowed to perform an action or access a protected resource. In ApiFrame, for authorization we have an authorization attribute class named “ApiAuthorization” derived from “AuthorizeAttribute” that validates the signature and identifies the authenticated user using the User Token in the HTTP request.

    public class ApiAuthorization : AuthorizeAttribute
    {
        public override void OnAuthorization(HttpActionContext actionContext)
        {
            HttpRequestMessage request = actionContext.Request;

            // Get the instnce of ApiValidation 
            IApiValidation apiValidation = new ApiValidation();

            // Call the validation method to validate the request
            apiValidation.ValidateRequest(request);            

            string accessToken = request.GetAccessToken();

            // Get the instance of ApiInception
            IApiInception apiInception = ApiObjectFactory.GetInstance<IApiInception>();
            ApiUserToken user = apiInception.GetUserToken(accessToken);

            apiValidation.ValidateToken(request, user);
            apiValidation.ValidateSignature(request, user.SecretToken);

            if (!string.IsNullOrEmpty(Roles))
            {
                apiValidation.ValidateRole(Roles, user.Roles);
            }

            string authenticationType = ApiConstants.AUTHTYPE;
            IIdentity userIdentity = new ApiIdentity(authenticationType, true, user.Name, user.UserId);
            IPrincipal principal = new ApiPrincipal(userIdentity, user.Roles);
            HttpContext.Current.User = principal;           
        }
    }

Authorized Request

If the using application doesn't require authentication and authorization for public API methods but want to allow access to athorized clients then In that case, an authorization filter attribute "ApiAuthorizedRequest" is defined. This can be applied to action methods to checks if the request is from an authorized client.

    public class ApiAuthorizedRequest : AuthorizationFilterAttribute
    {
        public override void OnAuthorization(HttpActionContext actionContext)
        {
            HttpRequestMessage request = actionContext.Request;

            // Get the instnce of ApiValidation 
            IApiValidation apiValidation = new ApiValidation();

            // Call the validation method to validate the request
            apiValidation.ValidateRequest(request); 
            
            string apiAccessToken = request.GetAccessToken();

            IApiInception apiInception = ApiObjectFactory.GetInstance<IApiInception>();

            // Get the application token
            ApiApplicationToken applicationToken = apiInception.GetApplicationToken(apiAccessToken);

            apiValidation.ValidateToken(request, applicationToken);
            apiValidation.ValidateSignature(request, applicationToken.SecretToken);
        }
    }

Custom Identity and Principal

When the server authenticates / authorizes the user, it creates a custom principal (ApiPrincipal) which is an IPrincipal object that represents the security context under which code is running. The principal contains an associated custom identity object (ApiIdentity) that contains information about the user. This security information (ApiPrincipal) is set for the current HTTP request (HttpContext.Current.User) when authenticated / authorized. The below code shows the custom Identity and Principal class

    public class ApiIdentity : IIdentity
    {
        public ApiIdentity(string authenticationType, bool isAuthenticated, string userName, string userId)
        {
            this.AuthenticationType = authenticationType;            
            this.IsAuthenticated = isAuthenticated;
            this.Name = userName;
            this.UserId = userId;
        }

        public string AuthenticationType { get; private set; }
        public bool IsAuthenticated { get; private set; }
        public string Name { get; private set; }
        public string UserId { get; private set; }
    }

    public class ApiPrincipal : IPrincipal
    {
        public ApiPrincipal(IIdentity identity, string roles)
        {
            this.Identity = identity;
            this.Roles = roles.Split(',');
        }

        public IIdentity Identity { get; private set; }
        public string[] Roles { get; private set; }

        public bool IsInRole(string roles) 
        {
            return Roles.Intersect(roles.Split(',')).Count() > 0;
        }
    }

Enforcing HTTPS

The communication through plain HTTP is not secure though we have secure authentication schemes. Enabling SSL is another layer of protection to data. We may need HTTPS for access to some protected resources. ApiFrame implements an authorization filter named “ApiHttpsRequired” that checks for SSL. This can be used for Web API methods that require HTTPS.

    public class ApiHttpsRequired : AuthorizationFilterAttribute
    {
        public override void OnAuthorization(HttpActionContext actionContext)
        {
            if (actionContext.Request.RequestUri.Scheme != Uri.UriSchemeHttps)
            {
                throw new ApiRequestException(ApiErrorCode.InvalidUriScheme);
            }
            else
            {
                base.OnAuthorization(actionContext);
            }
        }
    }

Exception

Error Code

A set of error codes are defined in a enum type called ApiErrorCode that contains the list of error types handled by the ApiFrame.

Error Response

Error response are created by ApiErrorResponse class that contains a static method "GetErrorMessage" which returns a HttpResponseMessage for a given ApiErrorCode. The method identifies the custom error with the error code and creates a HttpError with the custom message, type and appropriate HttpStatusCode. This HttpError is serialized and assigned to the content of HttpResponseMesssage. ApiErrorResponse class provides another method "GetHttpError" to deserialize the content of HttpResponseMessage to HttpError.

    public class ApiErrorResponse
    {
        private const string Code = "Code";
        private const string Type = "Type";

        public static HttpResponseMessage GetErrorMessage(ApiErrorCode errorCode)
        {
            HttpError error;

            switch (errorCode)
            {
                case ApiErrorCode.InvalidRequestHeader:
                case ApiErrorCode.InvalidMD5:
                case ApiErrorCode.InvalidSignature:
                    error = new HttpError("Problem communicating with the application. Invalid Request.");
                    error[Code] = HttpStatusCode.ExpectationFailed;
                    break;
                case ApiErrorCode.InvalidTimestamp:
                    error = new HttpError("The date and time is incorrect.");
                    error[Code] = HttpStatusCode.Forbidden;
                    break;
                case ApiErrorCode.InvalidScheme:
                    error = new HttpError("This version of application is outdated.");
                    error[Code] = HttpStatusCode.BadRequest;
                    break;
                case ApiErrorCode.InvalidUriScheme:
                    error = new HttpError("There has been problem processing your request.");
                    error[Code] = HttpStatusCode.Forbidden;
                    break;
                case ApiErrorCode.AuthenticationFailed:
                    error = new HttpError("The username/passowrd you have entered is incorrect");
                    error[Code] = HttpStatusCode.Forbidden;
                    break;
                case ApiErrorCode.InvalidToken:
                case ApiErrorCode.InvalidRole:
                    error = new HttpError("Authorization has been denied for this request");
                    error[Code] = HttpStatusCode.Unauthorized;
                    break;
                case ApiErrorCode.MissingRequiredParamter:
                    error = new HttpError("The username/passowrd parameter is missing");
                    error[Code] = HttpStatusCode.Forbidden;
                    break;
                case ApiErrorCode.MissingRequiredType:
                    error = new HttpError("The required type is not registered");
                    error[Code] = HttpStatusCode.Forbidden;
                    break;
                default:
                    error = new HttpError("Server error.");
                    error[Code] = HttpStatusCode.InternalServerError;
                    break;
            }

            error[Type] = errorCode.ToString();

            var response = new HttpResponseMessage((HttpStatusCode)error[Code])
            {
                Content = new StringContent(new JavaScriptSerializer().Serialize(error)),
                ReasonPhrase = errorCode.ToString()
            };            

            return response;
        }

        public static HttpError GetHttpError(HttpResponseMessage response)
        {
            if (!response.IsSuccessStatusCode)
            {
                string responseError = response.Content.ReadAsStringAsync().Result;
                var httpError = new JavaScriptSerializer().Deserialize<HttpError>(responseError);
                return httpError;
            }

            return null;
        }
    }

Custom Http Request Exception

A class "ApiRequestException" is derived from HttpResponseException that provides a way to define the custom exception. The ApiRequestException class offers two constructors. One is to define the internal exceptions of ApiFrame and the other constructor allows the calling assembly to initialize its own custom exception.

    public class ApiRequestException : HttpRequestException
    {
        public ApiRequestException(ApiErrorCode errorType)
        {
            this.ErrorType = errorType;
            this.ErrorResponseMessage = ApiErrorResponse.GetErrorMessage(errorType);
        }

        public ApiRequestException(HttpStatusCode statusCode, string errorMessage, string reasonPhrase)
        {
            this.ErrorType = ApiErrorCode.InvalidOperation;
            this.ErrorResponseMessage = new HttpResponseMessage(statusCode)
            {
                Content = new StringContent(errorMessage),
                ReasonPhrase = reasonPhrase
            };
        }

        public ApiErrorCode ErrorType { get; set; }
        public HttpResponseMessage ErrorResponseMessage { get; set; }
    }

Exception Filter attribute

Web API exceptions can be handled by an exception filter. ApiExceptionAttribute is an exception filter derived from ExceptionFilterAttribute class and overrides the OnException method.

    public class ApiExceptionAttribute : ExceptionFilterAttribute
    {
        public override void OnException(HttpActionExecutedContext context)
        {
            HttpResponseMessage response;

            if (context.Exception is ApiRequestException)
            {
                ApiRequestException apiException = (ApiRequestException)context.Exception;
                response = apiException.ErrorResponseMessage;
            }
            else
            {
                ApiObjectFactory.GetInstance<IApiException>().
                    LogMessage(context.Exception);

                response = ApiErrorResponse.GetErrorMessage(ApiErrorCode.InvalidProgramException);
            }

            context.Response = response;
        }
    }

Versioning

A custom class “ApiControllerSelector” is defined that implement IHttpControllerSelector to support versioning. ApiFrame provides an option to version Web APIs using Namespaces or Areas. The code is taken from this MSDN blog[^] and is slightly tweaked to work with MVC Areas.

Controller Selector

The interface that Web API uses to select a controller is IHttpControllerSelector. The method on this interface is SelectController, which selects a controller for an HttpRequestMessage. A custom class “ApiControllerSelector” is defined that implement IHttpControllerSelector to support versioning.

    public HttpControllerDescriptor SelectController(HttpRequestMessage request)
        {
            IHttpRouteData routeData = request.GetRouteData();
            if (routeData == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }

            // Get the namespace and controller variables from the route data.
            string namespaceName = GetRouteVariable<string>(routeData, ApiConfiguration.Instance.VersionNamespaceKey);
            if (namespaceName == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }

            string controllerName = GetRouteVariable<string>(routeData, ControllerKey);
            if (controllerName == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }

            // Find a matching controller.
            // string key = String.Format(CultureInfo.InvariantCulture, "{0}.{1}", namespaceName, controllerName);
            string key = ApiConfiguration.Instance.VersionNamespaceKey == NamespaceKey ?
                String.Format(CultureInfo.InvariantCulture, "{0}.Controllers.{1}", namespaceName, controllerName) :
                String.Format(CultureInfo.InvariantCulture, "{0}.{1}", namespaceName, controllerName);

            HttpControllerDescriptor controllerDescriptor;
            if (controllers.Value.TryGetValue(key, out controllerDescriptor))
            {
                return controllerDescriptor;
            }
            else if (duplicates.Contains(key))
            {
                throw new HttpResponseException(
                    request.CreateErrorResponse(HttpStatusCode.InternalServerError,
                    "Multiple controllers were found that match this request."));
            }
            else
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
        }

Client

The client part implements a Gateway component which can be used by API clients to consume the WEB API methods. Below shown is the class diagram of the Client. The ApiRequestToken contains the required properties to create an Http request which is passed as an input parameter to the execute method of Gateway class component. The primary job of the Gateway is to send the HTTP request to the server and receive the response.

Gateway

Gateway component is responsible for creating a signed HTTP request and sending it to server.Sending a signed HTTP request involves three steps as shown in the below diagram. The first step is to construct a HTTP request with all the required request header parameters. The second step is to create a HMAC-SHA signature using the secret token and “StringToSign” message based on request content. The third step is to send the request and the signature to the server.

        private HttpResponseMessage SendHttpRequest(ApiRequestToken requestToken)
        {
            DateTime requestDate = ApiHelper.GetCurrentDateTime();
            string contentType = string.Empty;
            string contentMD5 = string.Empty;

            if (!string.IsNullOrEmpty(requestToken.Content))
            {
                contentType = ApiConstants.CONTENTTYPE;
                contentMD5 = ApiHelper.ComputeMD5Hash(requestToken.Content);
            }

            // Step 1: Create the http request
            HttpRequestMessage request = new HttpRequestMessage(requestToken.Verb, requestToken.RelativeUrl);
            request.Headers.Date = requestDate;

            if (!string.IsNullOrEmpty(requestToken.Content))
            {
                request.Content = new StringContent(requestToken.Content);
                request.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType);
                request.Content.Headers.Add("Content-MD5", contentMD5);
            }

            // Step 2: Sign the request
            string stringToSign = ApiHelper.BuildMessageRepresentation(requestToken.Verb.ToString(), contentType, contentMD5, requestDate, ApiConstants.FORWARDSLASH + requestToken.RelativeUrl);
            string signature = this.signature.CalculateHmac(requestToken.SecretToken, stringToSign);
            string authorizationHeader = ApiHelper.BuildAuthorizationHeader(requestToken.AuthScheme, requestToken.AccessToken, signature);
            request.Headers.Add("Authorization", authorizationHeader);

            // Step 3: Send the request
            using (var client = new HttpClient())
            {
                client.BaseAddress = new Uri(this.baseUrl);
                client.DefaultRequestHeaders.Accept.Clear();
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(ApiConstants.APPJSON));
                ServicePointManager.ServerCertificateValidationCallback += (sender, certificate, chain, sslPolicyErrors) => true;
                HttpResponseMessage response = client.SendAsync(request).Result;
                return response;
            }
        }

“Message” based on request content (StringToSign)

StringToSign is a message that is constructed using the elements of a HTTP request. Following is the sample code that illustrates how the StringToSign is constructed. Content-MD5 and Content-Type will be available only for HTTP POST request. For other http request such as GET, PUT, DELETE, the Content value is simply represented as empty.

    StringToSign = HTTP-VERB + "\n" +
    Content-MD5 + "\n" +
    Content-Type + "\n" +
    TimeStamp + "\n" +
    RequestUri; 

Signing the Request

Signing the request is adding the HMAC signature to the authorization header of the HTTP request in the below given form:

    Authorization: AuthScheme AccessToken:Signature

Conclusion

The article described the parts of the library and what it contains. A related article posted on How to integrate ApiFrame in ASP.NET Web API Application[^] that provides the guidelines to use APIFrame.

References

License

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

Share

About the Author

John-ph
Software Developer (Senior)
India India


Comments and Discussions

 
Questioni need source Pinmemberdannychou7530-Oct-14 20:51 
QuestionI need source! but can not download!! Pinmemberxingxing7128-Oct-14 13:26 
QuestionCan not download the source-code Pinmemberlucasthehacker29-Sep-14 2:32 
Questionhow to call in Javascript ? PinmemberMember 463126324-Jun-14 23:18 
Questionsource code file link broken PinmemberTridip Bhattacharjee13-May-14 22:22 
plzz have a look source code file link broken.
tbhattacharjee

AnswerRe: source code file link broken PinmemberJohn-ph13-May-14 22:53 
GeneralRe: source code file link broken PinmemberMehul.8923-Sep-14 5:05 
GeneralRe: source code file link broken PinmemberJohn-ph23-Sep-14 5:21 
GeneralMy vote of 5 PinprofessionalAgent__0075-May-14 20:32 
GeneralRe: My vote of 5 PinmemberJohn-ph14-May-14 4:50 
GeneralMy vote of 5 PinprofessionalVolynsky Alex2-May-14 21:28 
GeneralRe: My vote of 5 PinmemberJohn-ph3-May-14 23:07 
GeneralRe: My vote of 5 PinprofessionalVolynsky Alex4-May-14 22:08 
QuestionMy Vote of 5 [modified] Pinmembermick_lennon2-May-14 0:10 
AnswerRe: My Vote of 5 PinmemberJohn-ph3-May-14 23:07 
GeneralMy vote of 5 PinmemberHumayun Kabir Mamun30-Apr-14 21:16 
GeneralRe: My vote of 5 PinmemberJohn-ph3-May-14 23:06 

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

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

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.1411019.1 | Last Updated 3 May 2014
Article Copyright 2014 by John-ph
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid