Tuesday, 19 March 2024

ASP.NET Hosting Tutorial: A Complete Guide on Secure Coding in C#

Leave a Comment

Writing safe code is critical for protecting programs against various attacks, which is a top priority in software development. This comprehensive article delves into secure coding methods in C#, covering important subjects including input validation, encryption, and authentication. Following these recommended practices allows developers to design powerful and secure C# apps.

using Peter.CSharpSecureCodingGuide.Console;

Console.WriteLine("Hello, from Peter!");

Console.WriteLine("Enter your email address:");
string? email = Console.ReadLine();

if(!string.IsNullOrEmpty(email))
{
    Console.WriteLine(EmailValidator.IsValidEmail(email) ? "Email address is valid." : "Invalid email address format.");
}
else
{
    Console.WriteLine("Your email address is empty!");
}


using System.Text.RegularExpressions;

namespace Peter.CSharpSecureCodingGuide.Console;
public static class EmailValidator
{
   public static bool IsValidEmail(string email)
    {
        // Regular expression for validating email addresses
        string emailPattern = @"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$";

        // Check if email matches the pattern
        return Regex.IsMatch(email, emailPattern);
    }

}

In the code example above an email address can be validated using the IsValidEmail method based on a regular expression pattern in this example. Input validation ensures that the email provided by the user follows the expected format before proceeding with further processing in the application by checking for typical email address format rules.

Validating inputs to prevent injection attacks 
SQL Injection Example

 An attacker can gain unauthorized access to the underlying database by injecting malicious SQL code into input fields of a web form or application through SQL injection. When strings are concatenated to form SQL queries, the application becomes vulnerable to manipulation, leading to SQL injection.

In the following two examples, one is vulnerable code and the other is secure code written correctly.

Vulnerable Code

This code snippet is vulnerable to SQL injection attacks since inputUsername and inputPassword are concatenated directly into the SQL query string.

/*************************
 * SQL Injection Example *
 ************************/
SqlConnection connection = new SqlConnection("our_connection_string");
string inputUsername = "username"; // Example input
string inputPassword = "password"; // Example input

//Vulnerable Code
string vulnerableQuery = "SELECT * FROM Users WHERE Username = '" + inputUsername + "' AND Password = '" + inputPassword + "'";
SqlCommand vulneraCmd = new SqlCommand(vulnerableQuery, connection);

Secure Code

As demonstrated in the secure code example, parameterized queries are crucial for preventing SQL injection. By separating the SQL query from the user input, parameterized queries ensure that input is treated as parameters rather than being concatenated directly into the query string. By preventing malicious SQL code injection, effectively mitigates SQL injection attacks.

/*************************
 * SQL Injection Example *
 ************************/
SqlConnection connection = new SqlConnection("our_connection_string");
string inputUsername = "username"; // Example input
string inputPassword = "password"; // Example input


//Secure Code
string secureQuery = "SELECT * FROM Users WHERE Username = @Username AND Password = @Password";
SqlCommand secureCmd = new SqlCommand(secureQuery, connection);
secureCmd.Parameters.AddWithValue("@Username", inputUsername);
secureCmd.Parameters.AddWithValue("@Password", inputPassword);

Cross-Site Scripting (XSS) Example

The Cross-Site Scripting vulnerability (XSS) allows attackers to inject malicious scripts into web pages viewed by other users. To prevent XSS attacks, user inputs must be sanitized before they are displayed.

In the following two examples, one is vulnerable code and the other is secure code written correctly.

Vulnerable Code

The user's browser can execute arbitrary JavaScript code if this code snippet is displayed in a web page without proper sanitation.

/**************************************
 * Cross-Site Scripting (XSS) Example *
 **************************************/
//Vulnerable Code
string userInput = "<script>alert('XSS')</script>";

The code contains a variable called userInput, which is vulnerable to cross-site scripting (XSS) attacks. It has a script tag <script>alert('XSS')</script> as its value, which is a commonly used payload for such attacks. If this input is not properly sanitized before being displayed on a webpage, the script will execute on the user's browser, leading to potentially harmful actions like stealing cookies or redirecting the user to a malicious site.

Secure Code

As part of XSS mitigation, special characters in user input are encoded into HTML entity equivalents using the HtmlEncode method from System.Web.HttpUtility, which makes them inert when displayed. XSS vulnerabilities are effectively prevented because any potentially malicious scripts or HTML tags are rendered inert when displayed in the web page.

/**************************************
 * Cross-Site Scripting (XSS) Example *
 **************************************/
//Secure Code
string sanitizedInput = System.Web.HttpUtility.HtmlEncode(userInput);

In the code example above, a secure code snippet is shown where the userInput string is encoded using the HtmlEncode method from System.Web.HttpUtility. This encoding process converts special characters such as '<', '>', and '&' into their corresponding HTML entity representations ('&lt;', '&gt;', '&amp;'). This means that the script tag '<script>alert('XSS')</script>' is transformed into '&lt;script&gt;alert('XSS')&lt;/script&gt;', which is displayed as plain text on the web page instead of being interpreted as HTML code. By doing so, any potential XSS attacks are effectively neutralized as the execution of malicious scripts is prevented.

Developers can significantly enhance the security of their web applications by implementing proper input sanitization techniques such as HTML encoding.

Keeping Data Safe at Rest and in Transit with Encryption

The use of encryption in modern computing environments ensures data confidentiality and integrity at rest as well as in transit. Encryption renders plaintext data unreadable to unauthorized entities at rest, thwarting unauthorized access to stored data. Similarly, during transit, encryption secures data as it traverses networks or communication channels, shielding it from interception and tampering by malicious actors. By employing robust encryption algorithms and securely managing encryption keys, organizations can fortify their data protection measures, mitigate risks associated with data breaches, and uphold compliance with regulatory requirements, fostering trust among stakeholders and safeguarding critical assets.

Password Hashing Example

Security measures such as password hashing are important for protecting user passwords stored in databases. The process involves using a cryptographic hashing algorithm to convert a plaintext password into a hashed representation. Strong hashing algorithms, such as bcrypt, are widely recommended.

Hashing Password

A hashed representation of the plaintext password "user123" is produced by using the bcrypt hashing algorithm. This hashed password is then stored in the database.

/******************************************
 * Password Hashing Example               *
 ******************************************/
//Hashing Password
string password = "user123";
string hashedPassword = BCrypt.Net.BCrypt.HashPassword(password);
Verifying Password

 During authentication, the stored hashed password is compared with the hashed representation of the user's plaintext password using the Verify method from the bcrypt library. If the hashes match, the password is valid.

/******************************************
 * Password Hashing Example               *
 ******************************************/
//Verifying Password
bool isPasswordValid = BCrypt.Net.BCrypt.Verify("user123", hashedPassword);

It incorporates features such as salting and iteration count to improve security and thwart brute-force attacks. Bcrypt is a popular and secure hashing algorithm designed specifically for password hashing. A user's plaintext password is hashed using the same algorithm and parameters when they log in, and the result is compared with the stored hash. If both matches, then the password is authenticated.

It is possible to greatly improve the security of their authentication systems and protect user passwords from unauthorized access by using a strong hashing algorithm like bcrypt as well as following best practices for password storage and verification.

Data Encryption Example

It involves converting plaintext data into ciphertext using an encryption algorithm and a secret encryption key to protect sensitive information from unauthorized access.

using System.Security.Cryptography;
using System.Text;

namespace Peter.CSharpSecureCodingGuide.Console;
public static class EncryptionHelper
{
    // Encryption method
    public static string Encrypt(string plainText, string key)
    {
        byte[] encryptedBytes;
        using (Aes aesAlg = Aes.Create())
        {
            aesAlg.Key = Encoding.UTF8.GetBytes(key);
            aesAlg.IV = new byte[16]; // Initialization vector (IV) should be unique and random for each encryption

            // Create an encryptor to perform the stream transform.
            ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);

            // Create the streams used for encryption.
            using (MemoryStream msEncrypt = new MemoryStream())
            {
                using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
                {
                    using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
                    {
                        // Write all data to the stream.
                        swEncrypt.Write(plainText);
                    }
                    encryptedBytes = msEncrypt.ToArray();
                }
            }
        }
        return Convert.ToBase64String(encryptedBytes);
    }

    // Decryption method
    public static string Decrypt(string cipherText, string key)
    {
        byte[] cipherBytes = Convert.FromBase64String(cipherText);
        string plaintext = null;
        using (Aes aesAlg = Aes.Create())
        {
            aesAlg.Key = Encoding.UTF8.GetBytes(key);
            aesAlg.IV = new byte[16]; // Initialization vector (IV) should be unique and random for each encryption

            // Create a decryptor to perform the stream transform.
            ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);

            // Create the streams used for decryption.
            using (MemoryStream msDecrypt = new MemoryStream(cipherBytes))
            {
                using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
                {
                    using (StreamReader srDecrypt = new StreamReader(csDecrypt))
                    {
                        // Read the decrypted bytes from the decrypting stream and place them in a string.
                        plaintext = srDecrypt.ReadToEnd();
                    }
                }
            }
        }
        return plaintext;
    }
}
Encrypting Data

This code example below it demonstrates how sensitive information is encrypted using an encryption helper class. The encryption key "encryptionKey" is used to perform the encryption process, which results in encrypted data.

/******************************************
* Data Encryption Example                 *
******************************************/
//Encrypting Data
string originalData = "Some Sensitive information is here :)";
string encryptedData = EncryptionHelper.Encrypt(originalData, "encryptionKey");
Decrypting Data

In order to retrieve the original plaintext data, the encrypted data is decrypted using the Decrypt method provided by the encryption helper class. The same encryption key "encryptionKey" is used to decrypt the data again.

/******************************************
* Data Encryption Example                 *
******************************************/
//Decrypting Data
string decryptedData = EncryptionHelper.Decrypt(encryptedData, "encryptionKey");

During storage and transmission, data encryption ensures that sensitive information remains confidential and secure. An encryption helper class abstracts away the complexity of encryption algorithms and key management in this example by encapsulating the encryption and decryption logic.

Security of encrypted data depends heavily on the strength of the encryption algorithm and the secrecy of the encryption key. Maintaining data confidentiality and thwarting potential attacks require robust encryption algorithms and securely managing encryption keys.

Developers can protect sensitive data from unauthorized access and ensure compliance with data security regulations by encrypting sensitive data before storage and transmission.

Ensuring secure user access through authentication
Two-Factor Authentication (2FA) Example

In two-factor authentication (2FA), users are required to provide two forms of authentication in order to protect their accounts. It is a combination of something the user knows (such as a password) and something the user has (such as an OTP Generator). As the code example below show us the class I have created OTPGenerator.

using System.Security.Cryptography;

namespace Peter.CSharpSecureCodingGuide.Console;
public static class OTPGenerator
{
    // Method to generate OTP
    public static string GenerateOTP(string secretKey)
    {
        // Convert secret key to byte array
        byte[] keyBytes = Convert.FromBase64String(secretKey);

        // Set current timestamp divided by time step
        long counter = DateTimeOffset.UtcNow.ToUnixTimeSeconds() / 30; // 30-second time step

        // Convert counter to byte array (big-endian)
        byte[] counterBytes = BitConverter.GetBytes(counter);
        if (BitConverter.IsLittleEndian)
        {
            Array.Reverse(counterBytes);
        }

        // Create HMAC-SHA1 hash using secret key and counter
        using (HMACSHA1 hmac = new HMACSHA1(keyBytes))
        {
            byte[] hash = hmac.ComputeHash(counterBytes);

            // Get last 4 bits of the hash
            int offset = hash[hash.Length - 1] & 0x0F;

            // Get 4 bytes starting from offset
            byte[] otpValue = new byte[4];
            Array.Copy(hash, offset, otpValue, 0, 4);

            // Mask most significant bit of last byte
            otpValue[0] &= 0x7F;

            // Convert bytes to integer
            int otp = BitConverter.ToInt32(otpValue, 0);

            // Generate 6-digit OTP
            otp %= 1000000;

            // Format OTP with leading zeros if necessary
            return otp.ToString("D6");
        }
    }

    // Method to verify OTP
    public static bool VerifyOTP(string secretKey, string userEnteredOTP)
    {
        // Convert secret key to byte array
        byte[] keyBytes = Convert.FromBase64String(secretKey);

        // Set current timestamp divided by time step
        long counter = DateTimeOffset.UtcNow.ToUnixTimeSeconds() / 30; // 30-second time step

        // Convert counter to byte array (big-endian)
        byte[] counterBytes = BitConverter.GetBytes(counter);
        if (BitConverter.IsLittleEndian)
        {
            Array.Reverse(counterBytes);
        }

        // Create HMAC-SHA1 hash using secret key and counter
        using (HMACSHA1 hmac = new HMACSHA1(keyBytes))
        {
            byte[] hash = hmac.ComputeHash(counterBytes);

            // Get last 4 bits of the hash
            int offset = hash[hash.Length - 1] & 0x0F;

            // Get 4 bytes starting from offset
            byte[] otpValue = new byte[4];
            Array.Copy(hash, offset, otpValue, 0, 4);

            // Mask most significant bit of last byte
            otpValue[0] &= 0x7F;

            // Convert bytes to integer
            int otp = BitConverter.ToInt32(otpValue, 0);

            // Generate 6-digit OTP
            otp %= 1000000;

            // Compare generated OTP with user-entered OTP
            return otp.ToString("D6") == userEnteredOTP;
        }
    }
}
Generating and Verifying OTP

It is shown here how the GenerateOTP method in an OTP generator class is used to generate an OTP. The secret key for the user, typically retrieved from the database, is used as input for generating the OTP.

/*******************************************
* Two - Factor Authentication(2FA) Example *
*******************************************/
//Generating and Verifying OTP
string userSecretKey = "userSecretKeyFromDatabase";
string generatedOTP = OTPGenerator.GenerateOTP(userSecretKey);
Verifying OTP

Authentication takes place by verifying the OTP entered by the user against the OTP generated for the user's secret key. Using VerifyOTP, a boolean value is returned indicating whether the OTP entered matches the OTP generated for the user's secret key.

//Verifying OTP
string userEnteredOTP = "123YouGoFree";
bool isOTPValid = OTPGenerator.VerifyOTP(userSecretKey, userEnteredOTP);

 By requiring users to provide two forms of identification before granting access to their accounts, two-factor authentication adds an extra layer of security. Typically, an OTP is delivered by SMS, email, or a mobile authenticator app as the second factor in this example.

As part of the OTP generation process, the OTP generator class abstracts away the complexities of the OTP generation process. The secret key associated with the user's account is securely stored in the database and used to generate and verify OTPs.

Developers can enhance the security of user accounts and prevent unauthorized access, even if passwords are compromised, by implementing Two-Factor Authentication with OTP. In addition to reducing account breach risks, this additional layer of security strengthens the overall security posture.

Token-Based Authentication Example

A token-based authentication system allows users to authenticate themselves and gain access to protected resources by providing a token, typically a JSON Web Token (JWT). This approach provides a scalable and secure method for managing user sessions.

The class below I have created TokenValidator with two methods one to create the token and other to validation the token.

A token-based authentication system allows users to authenticate themselves and gain access to protected resources by providing a token, typically a JSON Web Token (JWT). This approach provides a scalable and secure method for managing user sessions.

The class below I have created TokenValidator with two methods one to create the token and other to validation the token.

using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace Peter.CSharpSecureCodingGuide.Console;
public static class TokenGenerator
{
    public static string GenerateToken(string userId)
    {
        var tokenHandler = new JwtSecurityTokenHandler();
        var key = Encoding.ASCII.GetBytes("our_secret_key_here");

        var tokenDescriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(new Claim[]
            {
                new Claim(ClaimTypes.NameIdentifier, userId)
            }),
            Expires = DateTime.UtcNow.AddHours(1),
            SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
        };

        var token = tokenHandler.CreateToken(tokenDescriptor);
        return tokenHandler.WriteToken(token);
    }

    public static bool ValidateToken(string token)
    {
        var tokenHandler = new JwtSecurityTokenHandler();
        var key = Encoding.ASCII.GetBytes("our_secret_key_here");

        try
        {
            tokenHandler.ValidateToken(token, new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(key),
                ValidateIssuer = false,
                ValidateAudience = false,
                ClockSkew = TimeSpan.Zero
            }, out _);
            return true;
        }
        catch
        {
            return false;
        }
    }
}

Generating and Validating JWT Token

UserId is typically used as part of the token payload to identify the user associated with the token. This example generates a JWT token using the GenerateToken method provided by a token generator class.

/*******************************************
* Token - Based Authentication Example     *
*******************************************/
//Generating and Validating JWT Token
string userId = "iAmDummyUser";
string token = TokenGenerator.GenerateToken(userId);

Validating JWT Token

An authenticated session is established by validating the generated JWT token using the ValidateToken method provided by a token validator class. This method verifies the token's signature, expiration, and other claims.

/*******************************************
* Token - Based Authentication Example     *
*******************************************/
//Validating JWT Token
bool isTokenValid = TokenGenerator.ValidateToken(token);

With JWTs, token-based authentication has several advantages, including stateless authentication, improved scalability, and reduced server-side storage requirements. JWTs are cryptographically signed to prevent tampering.

Typically, RSA or HMAC cryptographic algorithms are used to generate JWT tokens securely in this example. To ensure the authenticity and integrity of the JWT token, the token validator class verifies its signature and validates its claims.

Token-Based Authentication and JWTs enable developers to establish secure user sessions, and authorize access to protected resources efficiently. This approach is widely used in modern web applications and APIs because of its simplicity, scalability, and security.

Error Handling: Avoiding Information Disclosure|
Generic Error Messages Example

 It is imperative to avoid exposing sensitive information in error messages when handling exceptions in code to prevent potential security risks. By disclosing specific details about the error, attackers may be able to gain insights into the system's inner workings or launch targeted attacks by exploiting these vulnerabilities.

Vulnerable Code

The message of the exception is concatenated directly into the error message returned to the user in the vulnerable code snippet. By doing so, attackers may be able to discover sensitive information about the system inadvertently, such as database connection strings or internal implementation details.

try
{
    // Code that may throw exceptions
}
catch (Exception ex)
{
    return "Error: " + ex.Message; // May reveal sensitive details
}

Secure Code

The secure code snippet minimizes this risk by returning a generic error message instead of revealing detailed information about the exception. By providing a generic error message like "An error occurred. Please contact support.", the system maintains confidentiality and reduces the likelihood of potential attackers gaining access to sensitive information.

try
{
    // Code that may throw exceptions
}
catch (Exception)
{
    return "An error occurred. Please contact support.";
}

An error message is returned to the user in the secure code example without revealing the details of any exception that was thrown. Instead, a generic error message is provided, informing the user that an error occurred and advising them to contact support.

The developers can maintain the security of their applications and protect themselves from information disclosure vulnerabilities by following this approach. By minimizing the exposure of sensitive information in error messages and handling exceptions appropriately, user privacy and security must be prioritized.

Leveraging established solutions for security libraries

Use well-established security libraries to handle common security tasks, such as input validation, encryption, and hashing.

Using BCrypt.Net to hash passwords

 This widely used library implements the bcrypt hashing algorithm, a robust and secure method of hashing passwords, which is well known for its robustness and security.

BCrypt.Net password hashing example

BCrypt.Net's HashPassword method is used to hash the plaintext password "user123", and then it is stored in the database to ensure confidentiality.

/*******************************************
* Using BCrypt.Net to hash passwords. *
*******************************************/
//BCrypt.Net password hashing example
string passwordHashingExample = "user123";
string hashedPasswordHashingExample = BCrypt.Net.BCrypt.HashPassword(password);

OWASP AntiSamy is a powerful library that helps sanitize user inputs and prevent malicious HTML or scripting code from being injected, thus preventing XSS attacks.

Input validation and XSS prevention using OWASP AntiSamy

To prevent XSS attacks, OWASP AntiSamy uses the Sanitize method to sanitize the user input userInput. Any potentially malicious HTML or scripting code is removed or neutralized, ensuring the input is safe to display in a web page.

namespace Peter.CSharpSecureCodingGuide.Console;
public static class AntiSamy
{
    public static string Sanitize(string input)
    {
        // Implement your sanitization logic here
        // For example, you can use regular expressions to remove or neutralize malicious HTML/JS code
        string sanitizedInput = input.Replace("<script>", "").Replace("</script>", "");

        return sanitizedInput;
    }
}
/********************************************************
Input validation and XSS prevention using OWASP AntiSamy*
********************************************************/

string userInpuAntiSamyt = "<script>alert('XSS')</script>";
string sanitizedInputAntiSamy = AntiSamy.Sanitize(userInput);

Developers can enhance the security of their applications by utilizing BCrypt.Net for password hashing and OWASP AntiSamy for input validation and preventing XSS attacks.

Token-based authentication using JWT

JWT (JSON Web Tokens) is commonly used in C# for token-based authentication, providing secure access to protected resources and managing user sessions.

Below is an example of JWT for token-based authentication I have given.

Generating JWT Token

The GenerateToken method creates a JWT token with a specified expiration time and signs it using a secret key. The token contains a claim for the user's identifier (userId), which can be used to identify the user during authentication.

using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace Peter.CSharpSecureCodingGuide.Console;
public static class TokenGenerator
{
    public static string GenerateToken(string userId)
    {
        var tokenHandler = new JwtSecurityTokenHandler();
        var key = Encoding.ASCII.GetBytes("our_secret_key_here");

        var tokenDescriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(new Claim[]
            {
                new Claim(ClaimTypes.NameIdentifier, userId)
            }),
            Expires = DateTime.UtcNow.AddHours(1),
            SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
        };

        var token = tokenHandler.CreateToken(tokenDescriptor);
        return tokenHandler.WriteToken(token);
    }
}
Validating JWT Token

The ValidateToken method validates a JWT token by verifying its signature and ensuring that it is not expired. If the token is valid, it returns true; otherwise, it returns false.

using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace Peter.CSharpSecureCodingGuide.Console;
public static class TokenGenerator
{

    public static bool ValidateToken(string token)
    {
        var tokenHandler = new JwtSecurityTokenHandler();
        var key = Encoding.ASCII.GetBytes("our_secret_key_here");

        try
        {
            tokenHandler.ValidateToken(token, new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(key),
                ValidateIssuer = false,
                ValidateAudience = false,
                ClockSkew = TimeSpan.Zero
            }, out _);
            return true;
        }
        catch
        {
            return false;
        }
    }
}

For secure user sessions and authorization access to protected resources, developers can implement token-based authentication in C# by using JWT.

 Incorporating these secure coding practices into your C# development workflow will significantly improve the security of your applications and protect them from a variety of threats. Adapt your coding practices in response to emerging security challenges by staying on top of the latest security best practices.

Best ASP.NET Core 8.0.1 Hosting Recommendation

One of the most important things when choosing a good ASP.NET Core 8.0 hosting is the feature and reliability. HostForLIFE is the leading provider of Windows hosting and affordable ASP.NET Core, their servers are optimized for PHP web applications. The performance and the uptime of the hosting service are excellent and the features of the web hosting plan are even greater than what many hosting providers ask you to pay for. 

At HostForLIFE.eu, customers can also experience fast ASP.NET Core hosting. The company invested a lot of money to ensure the best and fastest performance of the datacenters, servers, network and other facilities. Its datacenters are equipped with the top equipments like cooling system, fire detection, high speed Internet connection, and so on. That is why HostForLIFEASP.NET guarantees 99.9% uptime for ASP.NET Core. And the engineers do regular maintenance and monitoring works to assure its Orchard hosting are security and always up.

0 comments:

Post a Comment