Skip to main content

C# Data Validation

Introduction

Data validation is a critical aspect of any application that works with user input or external data sources. When building applications that interact with databases in C#, proper validation ensures that only valid, properly formatted data enters your system, helping maintain data integrity and security.

In this tutorial, you'll learn various techniques for validating data in C# before it reaches your database. We'll cover basic validation strategies, built-in validation frameworks, and best practices to protect your application from invalid data, SQL injection attacks, and other potential issues.

Why Data Validation Matters

Before diving into implementation details, let's understand why data validation is crucial:

  1. Data Integrity - Prevents corrupt or inconsistent data from entering your database
  2. Security - Protects against malicious inputs and attacks like SQL injection
  3. User Experience - Provides immediate feedback to users about input errors
  4. Application Stability - Reduces runtime errors caused by unexpected data types or formats

Basic Validation Techniques in C#

Let's start with fundamental validation approaches that can be implemented in any C# application.

String Validation

String validation is among the most common forms of data validation. Here are some basic examples:

csharp
public static class StringValidator
{
// Check if string is null or empty
public static bool IsNullOrEmpty(string value)
{
return string.IsNullOrEmpty(value);
}

// Check if string meets minimum length requirement
public static bool HasMinimumLength(string value, int minLength)
{
if (IsNullOrEmpty(value)) return false;
return value.Length >= minLength;
}

// Check if string contains only letters
public static bool ContainsOnlyLetters(string value)
{
if (IsNullOrEmpty(value)) return false;
return value.All(char.IsLetter);
}

// Check if string matches email pattern
public static bool IsValidEmail(string email)
{
if (IsNullOrEmpty(email)) return false;

try {
// Use .NET's built-in email validation
var addr = new System.Net.Mail.MailAddress(email);
return addr.Address == email;
}
catch {
return false;
}
}
}

Numeric Validation

When working with numeric values, you need to validate ranges and formats:

csharp
public static class NumberValidator
{
// Check if string can be converted to integer
public static bool IsInteger(string value)
{
return int.TryParse(value, out _);
}

// Check if string can be converted to decimal
public static bool IsDecimal(string value)
{
return decimal.TryParse(value, out _);
}

// Check if number falls within range
public static bool IsInRange(int value, int min, int max)
{
return value >= min && value <= max;
}

// Check if value is positive
public static bool IsPositive(int value)
{
return value > 0;
}
}

Date Validation

Dates often require special validation rules:

csharp
public static class DateValidator
{
// Check if string can be converted to date
public static bool IsValidDate(string value)
{
return DateTime.TryParse(value, out _);
}

// Check if date is in the future
public static bool IsFutureDate(DateTime date)
{
return date > DateTime.Now;
}

// Check if date is in the past
public static bool IsPastDate(DateTime date)
{
return date < DateTime.Now;
}

// Check if user is at least certain age
public static bool IsMinimumAge(DateTime birthDate, int minAge)
{
var today = DateTime.Today;
int age = today.Year - birthDate.Year;

// Adjust age if birthday hasn't occurred this year
if (birthDate.Date > today.AddYears(-age)) age--;

return age >= minAge;
}
}

Data Annotations for Validation

C# offers data annotations through the System.ComponentModel.DataAnnotations namespace, which provides attributes to decorate model properties with validation rules:

csharp
using System;
using System.ComponentModel.DataAnnotations;

public class User
{
[Required(ErrorMessage = "Username is required")]
[StringLength(50, MinimumLength = 3, ErrorMessage = "Username must be between 3 and 50 characters")]
public string Username { get; set; }

[Required(ErrorMessage = "Email is required")]
[EmailAddress(ErrorMessage = "Invalid email format")]
public string Email { get; set; }

[Required(ErrorMessage = "Password is required")]
[StringLength(100, MinimumLength = 8, ErrorMessage = "Password must be at least 8 characters")]
public string Password { get; set; }

[Range(18, 120, ErrorMessage = "Age must be between 18 and 120")]
public int Age { get; set; }

[Phone(ErrorMessage = "Invalid phone number")]
public string PhoneNumber { get; set; }

[Url(ErrorMessage = "Invalid URL format")]
public string Website { get; set; }
}

To validate an object with these annotations:

csharp
public static bool ValidateModel(object model)
{
var validationContext = new ValidationContext(model);
var validationResults = new List<ValidationResult>();

bool isValid = Validator.TryValidateObject(model, validationContext, validationResults, true);

if (!isValid)
{
foreach (var validationResult in validationResults)
{
Console.WriteLine(validationResult.ErrorMessage);
}
}

return isValid;
}

Custom Validation Attributes

Sometimes built-in attributes aren't sufficient. Here's how to create custom validation attributes:

csharp
using System;
using System.ComponentModel.DataAnnotations;

// Custom attribute to validate a future date
public class FutureDateAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
DateTime date = (DateTime)value;

if (date <= DateTime.Now)
{
return new ValidationResult(ErrorMessage ?? "Date must be in the future");
}

return ValidationResult.Success;
}
}

// Usage example
public class Event
{
[Required]
public string Name { get; set; }

[Required]
[FutureDate(ErrorMessage = "Event date must be in the future")]
public DateTime EventDate { get; set; }
}

Database-Specific Validation: Preventing SQL Injection

When working with databases, validation takes on extra importance. Here's how to prevent SQL injection:

csharp
// INCORRECT - Vulnerable to SQL injection
public User GetUserUnsafe(string username, string password)
{
string connectionString = "Data Source=MyServer;Initial Catalog=MyDb;User Id=sa;Password=******;";
string query = $"SELECT * FROM Users WHERE Username = '{username}' AND Password = '{password}'";

using (var connection = new SqlConnection(connectionString))
using (var command = new SqlCommand(query, connection))
{
connection.Open();
using (var reader = command.ExecuteReader())
{
// Read data from reader
}
}

return null; // Simplified return
}

// CORRECT - Using parameters to prevent SQL injection
public User GetUserSafe(string username, string password)
{
string connectionString = "Data Source=MyServer;Initial Catalog=MyDb;User Id=sa;Password=******;";
string query = "SELECT * FROM Users WHERE Username = @Username AND Password = @Password";

using (var connection = new SqlConnection(connectionString))
using (var command = new SqlCommand(query, connection))
{
// Add parameters with validation
if (StringValidator.IsNullOrEmpty(username) || StringValidator.IsNullOrEmpty(password))
{
return null;
}

command.Parameters.AddWithValue("@Username", username);
command.Parameters.AddWithValue("@Password", password); // Note: Store hashed passwords!

connection.Open();
using (var reader = command.ExecuteReader())
{
// Read data from reader
}
}

return null; // Simplified return
}

Real-world Application: User Registration with Validation

Let's combine everything we've learned into a practical user registration system:

csharp
using System;
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;

// User registration model with validation
public class UserRegistration
{
[Required(ErrorMessage = "Username is required")]
[StringLength(50, MinimumLength = 3, ErrorMessage = "Username must be between 3 and 50 characters")]
[RegularExpression(@"^[a-zA-Z0-9_]+$", ErrorMessage = "Username can only contain letters, numbers and underscore")]
public string Username { get; set; }

[Required(ErrorMessage = "Email is required")]
[EmailAddress(ErrorMessage = "Invalid email format")]
public string Email { get; set; }

[Required(ErrorMessage = "Password is required")]
[StringLength(100, MinimumLength = 8, ErrorMessage = "Password must be at least 8 characters")]
[RegularExpression(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$",
ErrorMessage = "Password must contain at least one uppercase letter, one lowercase letter, one number and one special character")]
public string Password { get; set; }

[Required(ErrorMessage = "Confirm Password is required")]
[Compare("Password", ErrorMessage = "Password and Confirmation Password do not match")]
public string ConfirmPassword { get; set; }

[Required(ErrorMessage = "Date of birth is required")]
[DataType(DataType.Date)]
public DateTime DateOfBirth { get; set; }

// Custom validation method
public bool IsAtLeast18YearsOld()
{
return DateValidator.IsMinimumAge(DateOfBirth, 18);
}
}

// User service class
public class UserService
{
private readonly string _connectionString;

public UserService(string connectionString)
{
_connectionString = connectionString;
}

public bool RegisterUser(UserRegistration registration)
{
// Step 1: Validate the model
var validationContext = new ValidationContext(registration);
var validationResults = new List<ValidationResult>();
bool isValid = Validator.TryValidateObject(registration, validationContext, validationResults, true);

if (!isValid)
{
Console.WriteLine("Validation errors:");
foreach (var error in validationResults)
{
Console.WriteLine($"- {error.ErrorMessage}");
}
return false;
}

// Step 2: Additional custom validation
if (!registration.IsAtLeast18YearsOld())
{
Console.WriteLine("You must be at least 18 years old to register");
return false;
}

// Step 3: Check if user already exists in database
if (UserExists(registration.Username, registration.Email))
{
Console.WriteLine("A user with this username or email already exists");
return false;
}

// Step 4: Hash password (never store plain text passwords!)
string hashedPassword = HashPassword(registration.Password);

// Step 5: Save to database
return SaveUserToDatabase(registration, hashedPassword);
}

private bool UserExists(string username, string email)
{
using (var connection = new SqlConnection(_connectionString))
using (var command = new SqlCommand("SELECT COUNT(*) FROM Users WHERE Username = @Username OR Email = @Email", connection))
{
command.Parameters.AddWithValue("@Username", username);
command.Parameters.AddWithValue("@Email", email);

connection.Open();
int count = (int)command.ExecuteScalar();
return count > 0;
}
}

private string HashPassword(string password)
{
// In production, use a proper password hashing library like BCrypt.Net
// This is a simplified example!
return Convert.ToBase64String(
System.Security.Cryptography.SHA256.Create()
.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password)));
}

private bool SaveUserToDatabase(UserRegistration user, string hashedPassword)
{
try
{
using (var connection = new SqlConnection(_connectionString))
using (var command = new SqlCommand(
"INSERT INTO Users (Username, Email, PasswordHash, DateOfBirth, CreatedAt) " +
"VALUES (@Username, @Email, @PasswordHash, @DateOfBirth, @CreatedAt)", connection))
{
command.Parameters.AddWithValue("@Username", user.Username);
command.Parameters.AddWithValue("@Email", user.Email);
command.Parameters.AddWithValue("@PasswordHash", hashedPassword);
command.Parameters.AddWithValue("@DateOfBirth", user.DateOfBirth);
command.Parameters.AddWithValue("@CreatedAt", DateTime.Now);

connection.Open();
int rowsAffected = command.ExecuteNonQuery();
return rowsAffected > 0;
}
}
catch (SqlException ex)
{
Console.WriteLine($"Database error: {ex.Message}");
return false;
}
}
}

// Usage example
public class Program
{
public static void Main()
{
// Sample user registration
var registration = new UserRegistration
{
Username = "johndoe",
Email = "[email protected]",
Password = "Passw0rd!@#",
ConfirmPassword = "Passw0rd!@#",
DateOfBirth = new DateTime(1990, 1, 1)
};

var userService = new UserService("Data Source=MyServer;Initial Catalog=MyDb;User Id=sa;Password=******;");
bool isRegistered = userService.RegisterUser(registration);

if (isRegistered)
{
Console.WriteLine("User registered successfully!");
}
else
{
Console.WriteLine("Registration failed. Please check the errors above.");
}
}
}

Advanced Validation with FluentValidation

For more complex validation scenarios, consider using the FluentValidation library, which offers a fluent interface for building validation rules:

csharp
// First, install the package:
// Install-Package FluentValidation

using FluentValidation;

public class UserValidator : AbstractValidator<UserRegistration>
{
public UserValidator()
{
RuleFor(x => x.Username)
.NotEmpty().WithMessage("Username is required")
.Length(3, 50).WithMessage("Username must be between 3 and 50 characters")
.Matches(@"^[a-zA-Z0-9_]+$").WithMessage("Username can only contain letters, numbers and underscore");

RuleFor(x => x.Email)
.NotEmpty().WithMessage("Email is required")
.EmailAddress().WithMessage("Invalid email format");

RuleFor(x => x.Password)
.NotEmpty().WithMessage("Password is required")
.MinimumLength(8).WithMessage("Password must be at least 8 characters")
.Matches(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$")
.WithMessage("Password must contain at least one uppercase letter, one lowercase letter, one number and one special character");

RuleFor(x => x.ConfirmPassword)
.Equal(x => x.Password).WithMessage("Passwords don't match");

RuleFor(x => x.DateOfBirth)
.NotEmpty().WithMessage("Date of birth is required")
.Must(dob => {
var today = DateTime.Today;
int age = today.Year - dob.Year;
if (dob.Date > today.AddYears(-age)) age--;
return age >= 18;
}).WithMessage("You must be at least 18 years old");
}
}

// Using the validator
public bool ValidateUser(UserRegistration user)
{
UserValidator validator = new UserValidator();
var results = validator.Validate(user);

if (!results.IsValid)
{
foreach (var failure in results.Errors)
{
Console.WriteLine($"Property {failure.PropertyName} failed validation: {failure.ErrorMessage}");
}
return false;
}

return true;
}

Summary

Effective data validation in C# is a multi-layer approach:

  1. Client-Side Validation: First line of defense but can be bypassed
  2. Server-Side Validation: Essential for security, includes:
    • Basic type and format checking
    • Data annotations
    • Custom validation rules
    • Parameterized queries to prevent SQL injection
  3. Database Constraints: Last line of defense with unique constraints, foreign keys, etc.

By implementing proper validation at all levels, you ensure data integrity, enhance security, and improve user experience. Remember that validation should be considered a critical part of any database application, not an afterthought.

Additional Resources and Exercises

Resources:

  1. Microsoft Docs: Data Annotations
  2. FluentValidation Documentation
  3. OWASP SQL Injection Prevention Cheat Sheet

Exercises:

  1. Exercise 1: Create a Product class with appropriate validation for an e-commerce system. Include properties like ProductName, Price, Description, and Weight.

  2. Exercise 2: Implement a custom validation attribute called StrongPasswordAttribute that checks if a password meets complex requirements (uppercase, lowercase, numbers, special characters).

  3. Exercise 3: Build a simple console application that reads user input for a bank account creation form, performs validation, and displays appropriate error messages.

  4. Exercise 4: Design a database layer that validates data before inserting into a database. Implement proper exception handling and error messages.

  5. Challenge Exercise: Create a validation framework for a specific domain (e.g., healthcare, finance) with domain-specific validation rules that extend beyond the basic validation demonstrated here.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)