Click here to Skip to main content
15,886,664 members
Articles / Web Development / HTML

Custom Tokenization Database First

Rate me:
Please Sign up or sign in to vote.
5.00/5 (2 votes)
27 Apr 2018CPOL3 min read 9.4K   186   8   1
This custom database first approach token authentication will enable you to apply token-based authentication in your application in a simple way with no hassle.

Introduction

When we want to restrict the un-authorized user to access data from our application, then we need to apply token-based authentication in our application so that only valid users can access data. Usually, ASP.NET Web API 2 provides Identity based authentication which is usually implemented in code first approach. But there is some scenario when we have to go with database-first approach in this aspect identity token may have some hassle in implementation. This custom database first approach token authentication will enable you to apply token-based authentication in your application in a simple way with no hassle. When we wanted to restrict the unauthorized user to access data from our application then we need to apply token-based authentication in our application so that only valid user can access data.

Steps of Implementation

You need to go through the following steps to implement custom token-based authentication:

Step 1

Create a database with name Tokenization and add 3 Tables there as follows:

SQL
CREATE DATABASE Tokenization

USE Tokenization
GO

CREATE TABLE Employees
(
    EmployeeId INT PRIMARY KEY IDENTITY(1,1) NOT NULL,
    EmployeeName VARCHAR(255) NULL,
    EmployeeEmail VARCHAR(255) NULL,
    EmployeeMobileNumber VARCHAR(12) NULL,
    EmployeeAddress VARCHAR(255) NULL
)
GO

CREATE TABLE Users
(
    UserId INT PRIMARY KEY IDENTITY(1,1) NOT NULL,
    FullName VARCHAR(255) NULL,
    LoginName VARCHAR(255) NOT NULL,
    PasswordNo VARCHAR(255) NOT NULL,
    EmployeeId INT NOT NULL
)
GO

CREATE TABLE TokenManager
(
    TokenID BIGINT PRIMARY KEY IDENTITY(1,1) NOT NULL,
    TokenKey VARCHAR(255) NULL,
    IssuedOn DATETIME NULL,
    ExpiresOn DATETIME NULL,
    CreatedOn DATETIME NULL,
    UserId INT NULL
)
GO

Step 2

Create a Web API 2 Project and name it as CustomTokenizationDatabaseFirst: See the following image:

Project Name

Step 3

Create a folder by right clicking on CustomTokenizationDatabaseFirst Project and name it as TokenFilter: under this folder, create the following class as ApiAuthorizeAttribute: See the following image:

Token Filter

Code Snippet for ApiAuthorizeAttribute Class

C#
public class ApiAuthorizeAttribute : AuthorizeAttribute
   {
       private readonly Entities _entities = new Entities();
       private readonly IUserRepository  _userRepository = new UserRepository();
       public override void OnAuthorization(HttpActionContext filterContext)
       {
           if (Authorize(filterContext))
           {
               return;
           }
           HandleUnauthorizedRequest(filterContext);
       }
       protected override void HandleUnauthorizedRequest(HttpActionContext filterContext)
       {
           base.HandleUnauthorizedRequest(filterContext);
       }

       //Here Getting the token from request header and decrypting real information which hidden in token key
       private bool Authorize(HttpActionContext actionContext)
       {
           try
           {
               var encodedString = actionContext.Request.Headers.GetValues("Token").FirstOrDefault();

               bool validFlag = false;

               if (!string.IsNullOrEmpty(encodedString))
               {
                   var key = PasswordHash.DecryptText(encodedString);


                   string[] parts = key.Split('|');
                   Type myType = typeof(WebApiConfig);
                   var myNamespace = myType.Namespace;

                   string protocol = HttpContext.Current.Request.IsSecureConnection ? "https://" : "http://";
                   string myApiUrl = protocol + HttpContext.Current.Request.Url.Authority;

                   var userId = Convert.ToInt32(parts[0]);     // UserID
                   var randomKey = parts[1];                   // Random Key
                   var nameSpace = parts[2];                   // NameSpace
                   var apiUrl = parts[3];                      // apiUrl
                   long ticks = long.Parse(parts[4]);          // Ticks
                   var issuedOn = new DateTime(ticks);

                   var userInfo = _userRepository.GetUserById(userId);
                   if (userInfo != null && myNamespace==nameSpace && myApiUrl==apiUrl)
                   {
                       // Validating Time
                       var expiresOn = (from token in _entities.TokenManagers
                                        where token.UserId == userId && token.TokenKey == encodedString
                                        select token.ExpiresOn).FirstOrDefault();

                       validFlag = (DateTime.Now <= expiresOn);
                   }
               }
               return validFlag;
           }
           catch (Exception ex)
           {
               return false;
           }
       }
   }

Step 4

How long will your token be valid? You have to define it in Web.Config file under the <appSettings></appSettings> XML tag. I have set up 45 minutes as token expiry time. You may customize it as per your requirements. See the following code.

Code Snippet for Token Expiry

XML
<appSettings>
  <add key="TokenExpiry" value="45" />
</appSettings>

Step 5

There will be a Model folder in your newly created project. Add the following JSON formatter class which will serialize JSON data. This Class will be used in web API controller Class.

Code Snippet for Json formatter

C#
public static class RequestFormat
    {
        public static JsonMediaTypeFormatter JsonFormaterString()
        {
            var formatter = new JsonMediaTypeFormatter();
            var json = formatter.SerializerSettings;

            json.DateFormatHandling = Newtonsoft.Json.DateFormatHandling.MicrosoftDateFormat;
            json.DateTimeZoneHandling = Newtonsoft.Json.DateTimeZoneHandling.Utc;
            json.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore;
            json.ContractResolver = new CamelCasePropertyNamesContractResolver();
            return formatter;
        }
    }

Add another class under Model folder which will be used for every API response model.

Code Snippet for API Response Model

C#
// This Class will be used for every API Response It's a response format actually
public class Confirmation
    {
        public string ResponseStatus { get; set; }
        public string Message { get; set; }
        public object ResponseData { get; set; }
    }

Add three (3) sub folders under Model folder with the following naming convention: See the following image:

Model Sub Folder

1. IRepository

Add the following code in IRepository sub folder with separate IInterface file:

C#
public interface IEmployeeRepository
{
    object GetAllEmployees();
}
public interface IUserRepository
{
    User GetUserById(int userId);
    User GetUserByLoginName(string userName);
}

internal interface ILoginRepository
{
    object LoginInformation(string userName, string password);
    bool IsTokenAlreadyExists(long userId);
    int DeleteGenerateToken(long userId);
    int InsertToken(TokenManager token);
    string GenerateToken(long userId, System.DateTime issuedOn);
}
2. Repository

Add the following code in Repository sub folder, each implementation should have a separate class file:

C#
//Implementation of IEmployeeRepository
   public class EmployeeRepository : IEmployeeRepository
   {
       private readonly Entities _entities;

       public EmployeeRepository()
       {
           this._entities = new Entities();
       }

       public object GetAllEmployees()
       {
           try
           {
               var employee = (from emp in _entities.Employees

                               select new
                               {
                                   EmployeeId = emp.EmployeeId,
                                   EmployeeName = emp.EmployeeName,
                                   EmployeeEmail = emp.EmployeeEmail,
                                   EmployeeMobileNumber = emp.EmployeeMobileNumber,
                                   EmployeeAddress = emp.EmployeeAddress
                               }).OrderByDescending(e => e.EmployeeId).ToList();

               return employee;
           }
           catch (Exception)
           {

               throw;
           }
       }
   }
   //Implementation of IUserRepository
   public class UserRepository : IUserRepository
   {
       private readonly Entities _entities;

       public UserRepository()
       {
           this._entities = new Entities();
       }

       public User GetUserById(int userId)
       {
           var user = _entities.Users.Find(userId);
           return user;
       }

       public User GetUserByLoginName(string userName)
       {
           try
           {
               var userInfo = _entities.Users.FirstOrDefault(x => x.LoginName == userName);
               return userInfo;
           }
           catch (Exception ex)
           {
               return null;
           }
       }
   }

   //Implementation of ILoginRepository

   public class LoginRepository : ILoginRepository
   {
       private readonly Entities _entities;
       private readonly IUserRepository _userRepository;

       public LoginRepository()
       {
           this._entities = new Entities();
           this._userRepository = new UserRepository();
       }

       public object LoginInformation(string userName, string password)
       {
           try
           {
               var checkIsUserExists =
                 _entities.Users.FirstOrDefault(x => x.LoginName == userName && x.PasswordNo == password);
               if (checkIsUserExists != null)
               {

                   LoginModel login = new LoginModel();

                   login.UserId = checkIsUserExists.UserId;
                   login.LoginName = checkIsUserExists.LoginName;
                   login.PasswordNo = checkIsUserExists.PasswordNo;
                   login.FullName = checkIsUserExists.FullName;
                   return login;
               }
               else
               {
                   return null;
               }
           }
           catch (Exception)
           {
               return null;
           }
       }
       public bool IsTokenAlreadyExists(long userId)
       {
           try
           {
               var result = (from token in _entities.TokenManagers
                             where token.UserId == userId
                             select token).Count();
               if (result > 0)
               {
                   return true;
               }
               else
               {
                   return false;
               }
           }
           catch (Exception ex)
           {
               return false;
           }
       }

       public int DeleteGenerateToken(long userId)
       {
           try
           {
               var token = _entities.TokenManagers.SingleOrDefault(x => x.UserId == userId);
               if (token != null) _entities.TokenManagers.Remove(token);
               return _entities.SaveChanges();
           }
           catch (Exception ex)
           {

               throw;
           }
       }

       public int InsertToken(TokenManager token)
       {
           try
           {
               _entities.TokenManagers.Add(token);
               return _entities.SaveChanges();
           }
           catch (Exception ex)
           {
               throw;
           }
       }

       public string GenerateToken(long userId, DateTime issuedOn)
       {
           try
           {
               Type myType = typeof(WebApiConfig);
               var myNamespace = myType.Namespace;

               string protocol = HttpContext.Current.Request.IsSecureConnection ? "https://" : "http://";
               string apiUrl = protocol + HttpContext.Current.Request.Url.Authority;

               string randomnumber =
                string.Join("|", new string[]{
           Convert.ToString(userId),
           KeyGenerator.GetUniqueKey(),
           myNamespace,
           apiUrl,
           Convert.ToString(issuedOn.Ticks)
                });

               return PasswordHash.EncryptText(randomnumber);
           }
           catch (Exception ex)
           {
               throw;
           }
       }
   }
   // Class Token Algorithm
   public static class KeyGenerator
   {
       //Here the algorithm of how token will be generated
       public static string GetUniqueKey(int maxSize = 15)
       {
           var chars = new char[62];
           chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".ToCharArray();
           var data = new byte[1];
           using (var crypto = new RNGCryptoServiceProvider())
           {
               data = new byte[maxSize];
               crypto.GetNonZeroBytes(data);
           }
           var result = new StringBuilder(maxSize);
           foreach (byte b in data)
           {
               result.Append(chars[b % (chars.Length)]);
           }
           return result.ToString();
       }
   }
   // Class Token Hash Algorithm
   public class PasswordHash
   {
       /// <summary>

       /// Functions: This class generates hashes for password and
       //  verify hashed password with hashed password saved in database,
               /// Verification process purposefully delays to give the hacker a lot of pain
               /// </summary>
       static Random rnd = new Random();
       public const int SaltByteSize = 24;
       public const int HashByteSize = 20; // to match the size of the PBKDF2-HMAC-SHA-1 hash
       // public static int Pbkdf2Iterations = rnd.Next(2000, 3000); // Maruf: 21.Jun.2017
       public const int IterationIndex = 0;
       public const int SaltIndex = 1;
       public const int Pbkdf2Index = 2;

       public static string HashPassword(string password)
       {
           try
           {
               int pbkdf2Iterations = rnd.Next(2000, 3000);
               var cryptoProvider = new RNGCryptoServiceProvider();
               var salt = new byte[SaltByteSize];
               cryptoProvider.GetBytes(salt);
               var hash = GetPbkdf2Bytes(password, salt, pbkdf2Iterations, HashByteSize);
               return pbkdf2Iterations + "|" +
                  Convert.ToBase64String(salt) + "|" +
                  Convert.ToBase64String(hash);
           }
           catch (Exception ex)
           {
               throw;
           }
       }

       public static byte[] AES_Encrypt(byte[] bytesToBeEncrypted, byte[] passwordBytes)
       {
           byte[] encryptedBytes = null;

           // Set your salt here, change it to meet your flavor:
           // The salt bytes must be at least 8 bytes.
           byte[] saltBytes = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 };

           using (MemoryStream ms = new MemoryStream())
           {
               using (RijndaelManaged AES = new RijndaelManaged())
               {
                   AES.KeySize = 256;
                   AES.BlockSize = 128;

                   var key = new Rfc2898DeriveBytes(passwordBytes, saltBytes, 1000);
                   AES.Key = key.GetBytes(AES.KeySize / 8);
                   AES.IV = key.GetBytes(AES.BlockSize / 8);

                   AES.Mode = CipherMode.CBC;

                   using (var cs = new CryptoStream(ms, AES.CreateEncryptor(), CryptoStreamMode.Write))
                   {
                       cs.Write(bytesToBeEncrypted, 0, bytesToBeEncrypted.Length);
                       cs.Close();
                   }
                   encryptedBytes = ms.ToArray();
               }
           }

           return encryptedBytes;
       }

       public static byte[] AES_Decrypt(byte[] bytesToBeDecrypted, byte[] passwordBytes)
       {
           byte[] decryptedBytes = null;

           // Set your salt here, change it to meet your flavor:
           // The salt bytes must be at least 8 bytes.
           byte[] saltBytes = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 };

           using (MemoryStream ms = new MemoryStream())
           {
               using (RijndaelManaged AES = new RijndaelManaged())
               {
                   AES.KeySize = 256;
                   AES.BlockSize = 128;

                   var key = new Rfc2898DeriveBytes(passwordBytes, saltBytes, 1000);
                   AES.Key = key.GetBytes(AES.KeySize / 8);
                   AES.IV = key.GetBytes(AES.BlockSize / 8);

                   AES.Mode = CipherMode.CBC;

                   using (var cs = new CryptoStream(ms, AES.CreateDecryptor(), CryptoStreamMode.Write))
                   {
                       cs.Write(bytesToBeDecrypted, 0, bytesToBeDecrypted.Length);
                       cs.Close();
                   }
                   decryptedBytes = ms.ToArray();
               }
           }

           return decryptedBytes;
       }

       public static string EncryptText(string input, string password = "E6t187^D43%F")
       {
           // Get the bytes of the string
           byte[] bytesToBeEncrypted = Encoding.UTF8.GetBytes(input);
           byte[] passwordBytes = Encoding.UTF8.GetBytes(password);

           // Hash the password with SHA256
           passwordBytes = SHA256.Create().ComputeHash(passwordBytes);

           byte[] bytesEncrypted = AES_Encrypt(bytesToBeEncrypted, passwordBytes);

           string result = Convert.ToBase64String(bytesEncrypted);

           return result;
       }

       public static string DecryptText(string input, string password = "E6t187^D43%F")
       {
           // Get the bytes of the string
           byte[] bytesToBeDecrypted = Convert.FromBase64String(input);
           byte[] passwordBytes = Encoding.UTF8.GetBytes(password);
           passwordBytes = SHA256.Create().ComputeHash(passwordBytes);

           byte[] bytesDecrypted = AES_Decrypt(bytesToBeDecrypted, passwordBytes);

           string result = Encoding.UTF8.GetString(bytesDecrypted);

           return result;
       }
       public static bool ValidatePassword(string password, string correctHash)
       {
           try
           {
               char[] delimiter = { '|' };
               var split = correctHash.Split(delimiter);
               var iterations = Int32.Parse(split[IterationIndex]);
               var salt = Convert.FromBase64String(split[SaltIndex]);
               var hash = Convert.FromBase64String(split[Pbkdf2Index]);

               var testHash = GetPbkdf2Bytes(password, salt, iterations, hash.Length);
               return SlowEquals(hash, testHash);
           }
           catch (Exception ex)
           {
               throw;
           }
       }

       private static bool SlowEquals(byte[] a, byte[] b)
       {
           try
           {
               var diff = (uint)a.Length ^ (uint)b.Length;
               for (int i = 0; i < a.Length && i < b.Length; i++)
               {
                   diff |= (uint)(a[i] ^ b[i]);
               }
               return diff == 0;
           }
           catch (Exception ex)
           {

               throw;
           }
       }

       private static byte[] GetPbkdf2Bytes(string password, byte[] salt, int iterations, int outputBytes)
       {
           try
           {
               var pbkdf2 = new Rfc2898DeriveBytes(password, salt) { IterationCount = iterations };
               return pbkdf2.GetBytes(outputBytes);
           }
           catch (Exception ex)
           {

               throw;
           }

       }
   }
3. StronglyTypeModel

Add the following code in StronglyTypeModel sub folder, each class should have a separate class file:

C#
//For Employee Will be call from Web API controller Class
public class EmployeeUserModel
   {
       public int EmployeeId { get; set; }
       public string EmployeeName { get; set; }
       public string EmployeeEmail { get; set; }
       public string EmployeeMobileNumber { get; set; }
       public string EmployeeAddress { get; set; }
       public int UserId { get; set; }
       public string FullName { get; set; }
       public string UserName { get; set; }
       public string PasswordNo { get; set; }
   }

   // For Login Will be call from Web API controller Class
   public class LoginModel
   {
       public int UserId { get; set; }
       public string FullName { get; set; }
       public string LoginName { get; set; }
       public string PasswordNo { get; set; }
       public int EmployeeId { get; set; }
       public string EmployeeName { get; set; }
   }

Step 6

Now create a ADO.NET Entity Data Model by right clicking on Model folder, give the model name as "DbModel" after providing all of your server credentials, name the entity (connectionString name) as "Entity" it's mandatory to use the specified naming convention. See the following image:

Ado dot net db model

Step 7

Now add Web API Controller Class under the controller folder as the following instruction:

Code Snippet for Web API Controller Class

Here, note that you will get your token from API Response header portion. I have used POST MAN for this request. You can find your generated token in POST MAN Response header part.

C#
 // For Employee Controller
public class EmployeeController : ApiController
{
    private readonly IEmployeeRepository _employeeRepository;

    public EmployeeController()
    {
        this._employeeRepository = new EmployeeRepository();
    }

    public EmployeeController(IEmployeeRepository employeeRepository)
    {
        this._employeeRepository = employeeRepository;
    }
    //Can not access data unknown user without Valid Token
    [ApiAuthorize] // use this annotation for authentication I have used only here
    [HttpGet, ActionName("GetAllEmployeesWithToken")]
    public HttpResponseMessage GetAllEmployeesWithToken()
    {
        var data = _employeeRepository.GetAllEmployees();
        var formatter = RequestFormat.JsonFormaterString();
        return Request.CreateResponse(HttpStatusCode.OK, data, formatter);
    }
    //Can access data unknown user without Valid Token
    [AllowAnonymous]
    [HttpGet, ActionName("GetAllEmployeesWithOutToken")]
    public HttpResponseMessage GetAllEmployeesWithOutToken()
    {
        var data = _employeeRepository.GetAllEmployees();
        var formatter = RequestFormat.JsonFormaterString();
        return Request.CreateResponse(HttpStatusCode.OK, data, formatter);
    }
}

// For Login Controller
public class LoginController : ApiController
{
    private readonly ILoginRepository _loginRepository;
    private readonly IUserRepository _userRepository;

    public LoginController()
    {
        _userRepository = new UserRepository();
        this._loginRepository = new LoginRepository();
    }

    [HttpPost, ActionName("UserLogin")]
    public HttpResponseMessage UserLogin([FromBody] Models.StronglyType.EmployeeUserModel objEmployeeUserModel)
    {
        try
        {
            var formatter = RequestFormat.JsonFormaterString();

            if (string.IsNullOrEmpty(objEmployeeUserModel.UserName))
            {
                return Request.CreateResponse(HttpStatusCode.NotAcceptable, 
                new Confirmation { ResponseStatus = "error", 
                Message = "User Name can not be empty" }, formatter);
            }
            if (string.IsNullOrEmpty(objEmployeeUserModel.PasswordNo))
            {
                return Request.CreateResponse(HttpStatusCode.NotAcceptable, 
                new Confirmation { ResponseStatus = "error", 
                Message = "password can not be empty" }, formatter);
            }

            var userInfo = _userRepository.GetUserByLoginName(objEmployeeUserModel.UserName);

            if (userInfo != null)
            {
                var login = _loginRepository.LoginInformation
                (objEmployeeUserModel.UserName, objEmployeeUserModel.PasswordNo);
                if (login != null)
                {
                    var oResponse = Request.CreateResponse(HttpStatusCode.OK,
                    new Confirmation { ResponseStatus = "success", 
                    Message = "Login Successfully", ResponseData = userInfo }, formatter);

                    if (_loginRepository.IsTokenAlreadyExists(userInfo.UserId))
                    {
                        _loginRepository.DeleteGenerateToken(userInfo.UserId);
                        return GenerateandSaveToken(userInfo.UserId, oResponse);
                    }
                    else
                    {
                        return GenerateandSaveToken(userInfo.UserId, oResponse);
                    }
                }
                return Request.CreateResponse(HttpStatusCode.Forbidden,
                new Confirmation { ResponseStatus = "error", 
                Message = "Please enter valid username or password" }, formatter);

            }
            return Request.CreateResponse(HttpStatusCode.Forbidden,
             new Confirmation { ResponseStatus = "error", 
             Message = "Please enter valid username or password" }, formatter);
        }
        catch (Exception ex)
        {
            var formatter = RequestFormat.JsonFormaterString();
            return Request.CreateResponse(HttpStatusCode.OK, new Confirmation 
            { ResponseStatus = "error", 
            Message = "Login is not successful" }, formatter);
        }
    }
    [NonAction]
    private HttpResponseMessage GenerateandSaveToken(int userId, HttpResponseMessage response)
    {
        try
        {
            var issuedOn = DateTime.Now;
            var newToken = _loginRepository.GenerateToken(userId, issuedOn);
            var token = new TokenManager();
            token.TokenID = 0;
            token.TokenKey = newToken;
            token.IssuedOn = issuedOn;
            token.ExpiresOn = DateTime.Now.AddMinutes(Convert.ToInt32
            (ConfigurationManager.AppSettings["TokenExpiry"]));
            token.CreatedOn = DateTime.Now;
            token.UserId = userId;
            var result = _loginRepository.InsertToken(token);

            if (result == 1)
            {
                response.Headers.Add("Token", newToken);
                response.Headers.Add("TokenExpiry", 
                ConfigurationManager.AppSettings["TokenExpiry"]);
                response.Headers.Add("Access-Control-Expose-Headers", "Token,TokenExpiry");
                return response;
            }
            var message = new HttpResponseMessage(HttpStatusCode.NotAcceptable);
            message.Content = new StringContent("Error in Creating Token");
            return message;
        }
        catch (Exception ex)
        {
            var formatter = RequestFormat.JsonFormaterString();
            return Request.CreateResponse(HttpStatusCode.InternalServerError, 
            new Confirmation { ResponseStatus = "error", 
            Message = "Cannot generate and Save Token" }, formatter);
        }
    }
}

Step 7

As you have seen above, I have not used RESTful API here, so you might get a 404 error while you attempt to send request from Post Man. To solve this issue, paste the following code in your WebApiConfig.cs file under App_Start folder so that it will allow you to have multiple http verb in controller class.

Code Snippet for WebApiConfig.cs

C#
config.Routes.MapHttpRoute(
name: "ControllersWithAction",
routeTemplate: "{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);

Final Step

Just build your project and send request from Post Man as JSON data format. See the following image:

Post Man Request Format

postman request

Post Man Response Format With Token

Response with token

Code Snippet for json Request Format

JavaScript
{
   "UserName": "admin",
   "PasswordNo": "123456"
}

Access your data in json request format through POST MAN: See the following image: Add your Token in post man request header part when you attempt to hit the controller class:

Data with tokekn

Points of Interest

When I attempt to write any topic, I feel a wonderful excitement. I have to go through with a range of RND. Finally, I have drawn a format in my thought. I wish to spread my knowledge to technology lovers. It really makes me feel good. I always try to describe the technology in a simple and easier way.

License

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


Written By
Software Developer Microsoft
China China
I started my programming career on .NET platform in early 2015. I have done many local and overseas application from scratch Currently I am working as Software Engineer at Microsoft Azure Team. I have couple of articles on Code Project and csharp corner. I occasionally post questions and answers on stackoverflow. My passion is to learn new technology through the professional approach.

Comments and Discussions

 
SuggestionYour introduction Pin
Paulus Koshivi27-Apr-18 20:10
Paulus Koshivi27-Apr-18 20:10 

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.