Month: September 2025 Archives

Home / Archives for September 2025

The Architect's Guide to WordPress Performance at Scale: From Database to Delivery

The Architect's Guide to WordPress Performance at Scale: From Database to Delivery

Introduction: The Performance Imperative In the WordPress ecosystem, it is a powerhouse capable of driving enterprise-level applications, high-traffic media publications, and complex e-commerce platforms. However, this potential is unlocked only through a meticulous, architectural approach to performance. Speed is no longer a feature; it is a fundamental characteristic of your…

Building a High-Performance, Generic Data Access Layer in C# .NET: A Deep Dive

Introduction: Evolution of a Codebase Several years ago, I shared a data access layer born from a transition from Visual Basic to the "sexy" world of C#. That original code served a crucial purpose: providing a fast, reusable, and strongly-typed bridge between SQL Server and C# objects. It featured a…

Cookie Notice

This site utilizes cookies to improve your browsing experience, analyze the type of traffic we receive, and serve up proper content for you. If you wish to continue browsing, you must agree to allow us to set these cookies. If not, please visit another website.

Building a High-Performance, Generic Data Access Layer in C# .NET: A Deep Dive

Introduction: Evolution of a Codebase

Several years ago, I shared a data access layer born from a transition from Visual Basic to the “sexy” world of C#. That original code served a crucial purpose: providing a fast, reusable, and strongly-typed bridge between SQL Server and C# objects. It featured a custom ORM mapper, synchronous and asynchronous execution paths, and caching support.

The .NET ecosystem has evolved significantly since then. With the mainstream adoption of async/await, dependency injection, and new performance APIs, it’s time to revisit this codebase. This article presents a comprehensive refactor of the original data access layer, addressing its weaknesses, enhancing its performance, and aligning it with modern .NET best practices—all without relying on any external libraries.

Critical Analysis of the Original Code

Before we dive into the new implementation, let’s acknowledge the strengths and weaknesses of the original design.

Strengths:

  • Strong Typing: The generic GetResults<T> method provides excellent compile-time safety.
  • Flexibility: Supports stored procedures and ad-hoc queries with parameters.
  • Custom ORM: The Converter<T> class using Expression Trees is a powerful and fast mapping solution.
  • Dual Paths: Offers both synchronous and asynchronous APIs.

Weaknesses to Address:

  1. Async Anti-Patterns: The WrapperAsync class dangerously uses .Result, potentially causing deadlocks.
  2. Resource Management: Connection and reader lifecycle could be more robust.
  3. Parameter Handling: The manual SqlDbType mapping is complex and error-prone.
  4. Tight Coupling: Relies on static settings, making testing and DI difficult.
  5. Modern C# Features: Doesn’t leverage newer language features like record types, nullable contexts, or IAsyncEnumerable.

The Modernized Solution: Key Improvements

Our refactored code addresses these issues while maintaining the original architecture’s core benefits.

1. True Async/Await Implementation

The most critical fix is eliminating the dangerous Result and Wait() calls in the async path. We ensure all async operations are properly awaited throughout the call chain.

Before (Problematic in WrapperAsync):

using (DbDataReader r = db.GetResults().Result) // ❌ BLOCKING!
{
    _Query = Map(r).Result; // ❌ BLOCKING!
}

After (Proper Async in Refactored Code):

await using (var db = new DataAccessAsync(_connectionString))
{
    await using (var r = await db.GetResultsAsync()) // ✅ Proper await
    {
        _Query = await MapAsync(r); // ✅ Map is now truly async
    }
}

2. Simplified and Safer Parameter Handling

We replace the complex switch statement for SqlDbType with a more maintainable approach using SqlParameter directly, while still avoiding the performance pitfalls of AddWithValue for certain data types.

Refactored PrepareParams method:

protected void PrepareParameters(SqlCommand command)
{
    if (_parameterNames == null || _parameterValues == null) 
        return;

    for (int i = 0; i < _parameterNames.Length; i++)
    {
        var parameter = new SqlParameter(_parameterNames[i], _parameterValues[i]);

        // Only set explicit type if provided, otherwise let ADO.NET infer
        if (_parameterDataTypes != null && i < _parameterDataTypes.Length)
        {
            parameter.SqlDbType = _parameterDataTypes[i];
        }

        command.Parameters.Add(parameter);
    }
}

3. Modern Resource Management with IAsyncDisposable

We implement IAsyncDisposable alongside IDisposable for proper asynchronous resource cleanup, particularly important for the async data reader.

public class DataAccessAsync : IDisposable, IAsyncDisposable
{
    private SqlDataReader _reader;
    private SqlConnection _connection;
    private SqlCommand _command;
    private bool _disposed;

    public async ValueTask DisposeAsync()
    {
        await DisposeAsyncCore().ConfigureAwait(false);
        GC.SuppressFinalize(this);
    }

    protected virtual async ValueTask DisposeAsyncCore()
    {
        if (!_disposed)
        {
            if (_reader != null)
            {
                await _reader.DisposeAsync().ConfigureAwait(false);
                _reader = null;
            }

            if (_command != null)
            {
                _command.Dispose();
                _command = null;
            }

            if (_connection != null)
            {
                await _connection.DisposeAsync().ConfigureAwait(false);
                _connection = null;
            }

            _disposed = true;
        }
    }

    // IDisposable implementation remains for sync code
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

4. Dependency Injection Ready

We refactor the classes to accept dependencies (like connection strings) through constructors, making them testable and DI-friendly.

public class DataWrapper
{
    private readonly string _connectionString;
    private readonly ICacheProvider _cacheProvider;

    // Constructor injection enables proper testing and DI
    public DataWrapper(string connectionString, ICacheProvider cacheProvider = null)
    {
        _connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
        _cacheProvider = cacheProvider;
    }

    public async Task> GetResultsAsync(string query, CommandType queryType, 
        string[] paramNames = null, object[] paramValues = null, 
        SqlDbType[] paramDataTypes = null, bool shouldCache = false, string cacheId = "") 
        where T : new()
    {
        // Implementation using injected dependencies
    }
}

The Complete Refactored Codebase

Here are the key components of our modernized data access layer:

1. DataAccess Class (Base)

using System;
using System.Data;
using System.Data.SqlClient;
using System.Threading.Tasks;

namespace ModernDataAccess.Core
{
    public class DataAccess : IDisposable, IAsyncDisposable
    {
        private readonly string _connectionString;
        private SqlConnection _connection;
        private SqlCommand _command;
        private SqlDataReader _reader;
        private bool _disposed;

        // DI-friendly constructor
        public DataAccess(string connectionString)
        {
            _connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
            InitializeComponents();
        }

        private void InitializeComponents()
        {
            _connection = new SqlConnection(_connectionString);
            _command = new SqlCommand { Connection = _connection };
        }

        public void SetCommand(string query, CommandType commandType, 
            string[] paramNames = null, object[] paramValues = null, 
            SqlDbType[] paramDataTypes = null)
        {
            _command.CommandText = query;
            _command.CommandType = commandType;
            _command.Parameters.Clear();

            if (paramNames != null && paramValues != null)
            {
                PrepareParameters(paramNames, paramValues, paramDataTypes);
            }
        }

        private void PrepareParameters(string[] paramNames, object[] paramValues, SqlDbType[] paramDataTypes)
        {
            for (int i = 0; i < paramNames.Length; i++)
            {
                var param = new SqlParameter(paramNames[i], paramValues[i]);

                if (paramDataTypes != null && i < paramDataTypes.Length)
                {
                    param.SqlDbType = paramDataTypes[i];
                }

                _command.Parameters.Add(param);
            }
        }

        public async Task ExecuteReaderAsync()
        {
            if (_connection.State != ConnectionState.Open)
            {
                await _connection.OpenAsync().ConfigureAwait(false);
            }

            _reader = await _command.ExecuteReaderAsync(CommandBehavior.CloseConnection)
                .ConfigureAwait(false);

            return _reader;
        }

        public async ValueTask DisposeAsync()
        {
            await DisposeAsyncCore().ConfigureAwait(false);
            GC.SuppressFinalize(this);
        }

        protected virtual async ValueTask DisposeAsyncCore()
        {
            if (!_disposed)
            {
                if (_reader != null)
                {
                    await _reader.DisposeAsync().ConfigureAwait(false);
                }

                _command?.Dispose();

                if (_connection != null)
                {
                    await _connection.DisposeAsync().ConfigureAwait(false);
                }

                _disposed = true;
            }
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!_disposed)
            {
                if (disposing)
                {
                    _reader?.Dispose();
                    _command?.Dispose();
                    _connection?.Dispose();
                }

                _disposed = true;
            }
        }
    }
}

2. Enhanced Object Mapper

using System;
using System.Collections.Concurrent;
using System.Data;
using System.Linq.Expressions;
using System.Reflection;

namespace ModernDataAccess.Core.Mapping
{
    public class ObjectMapper where T : new()
    {
        private static readonly ConcurrentDictionary> _cache = 
            new ConcurrentDictionary>();

        private readonly IDataRecord _record;
        private readonly Func _mapper;

        public ObjectMapper(IDataRecord record)
        {
            _record = record;
            var key = GenerateCacheKey(record);
            _mapper = _cache.GetOrAdd(key, k => CompileMapper(record));
        }

        private string GenerateCacheKey(IDataRecord record)
        {
            var fieldCount = record.FieldCount;
            var fieldNames = new string[fieldCount];

            for (int i = 0; i < fieldCount; i++)
            {
                fieldNames[i] = record.GetName(i);
            }

            return string.Join("|", fieldNames.OrderBy(n => n));
        }

        private Func CompileMapper(IDataRecord record)
        {
            var parameter = Expression.Parameter(typeof(IDataRecord), "record");
            var bindings = new List();
            var fieldCount = record.FieldCount;

            for (int i = 0; i < fieldCount; i++)
            {
                var fieldName = record.GetName(i);
                var property = typeof(T).GetProperty(fieldName, 
                    BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);

                if (property == null || !property.CanWrite) 
                    continue;

                var fieldIndex = Expression.Constant(i);
                var valueExpression = Expression.Call(
                    parameter,
                    typeof(IDataRecord).GetMethod("GetValue", new[] { typeof(int) }),
                    fieldIndex);

                // Handle DBNull conversion
                var condition = Expression.Condition(
                    Expression.Equal(valueExpression, Expression.Constant(DBNull.Value)),
                    Expression.Default(property.PropertyType),
                    Expression.Convert(valueExpression, property.PropertyType));

                bindings.Add(Expression.Bind(property, condition));
            }

            var newExpression = Expression.New(typeof(T));
            var memberInit = Expression.MemberInit(newExpression, bindings);
            var lambda = Expression.Lambda>(memberInit, parameter);

            return lambda.Compile();
        }

        public T Map()
        {
            return _mapper(_record);
        }

        public static async IAsyncEnumerable MapAllAsync(IDataReader reader)
        {
            var mapper = new ObjectMapper(reader);

            while (await reader.ReadAsync().ConfigureAwait(false))
            {
                yield return mapper.Map();
            }
        }
    }
}

3. Modern Wrapper Class with Async Support

using ModernDataAccess.Core;
using ModernDataAccess.Core.Mapping;
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Threading.Tasks;

namespace ModernDataAccess
{
    public class DataAccessWrapper : IDisposable, IAsyncDisposable
    {
        private readonly string _connectionString;
        private DataAccess _dataAccess;

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

        public async Task> GetResultsAsync(
            string query, 
            CommandType commandType = CommandType.Text,
            (string Name, object Value, SqlDbType? DataType)[] parameters = null,
            bool shouldCache = false, 
            string cacheKey = null) where T : new()
        {
            try
            {
                _dataAccess = new DataAccess(_connectionString);

                // Convert tuple parameters to arrays for backward compatibility
                var paramArrays = parameters != null ? 
                    ConvertParametersToArrays(parameters) : 
                    (null, null, null);

                _dataAccess.SetCommand(
                    query, 
                    commandType, 
                    paramArrays.Names, 
                    paramArrays.Values, 
                    paramArrays.DataTypes);

                await using var reader = await _dataAccess.ExecuteReaderAsync().ConfigureAwait(false);

                var results = new List();
                await foreach (var item in ObjectMapper.MapAllAsync(reader))
                {
                    results.Add(item);
                }

                return results;
            }
            finally
            {
                await DisposeAsync().ConfigureAwait(false);
            }
        }

        private (string[] Names, object[] Values, SqlDbType[] DataTypes) 
            ConvertParametersToArrays((string Name, object Value, SqlDbType? DataType)[] parameters)
        {
            var names = new string[parameters.Length];
            var values = new object[parameters.Length];
            var dataTypes = new SqlDbType[parameters.Length];

            for (int i = 0; i < parameters.Length; i++)
            {
                names[i] = parameters[i].Name;
                values[i] = parameters[i].Value;
                dataTypes[i] = parameters[i].DataType ?? SqlDbType.Variant;
            }

            return (names, values, dataTypes);
        }

        public async ValueTask DisposeAsync()
        {
            if (_dataAccess != null)
            {
                await _dataAccess.DisposeAsync().ConfigureAwait(false);
            }
        }

        public void Dispose()
        {
            _dataAccess?.Dispose();
        }
    }
}

Usage Example

using ModernDataAccess;
using System;
using System.Data;
using System.Threading.Tasks;

class Program
{
    static async Task Main(string[] args)
    {
        var connectionString = "Your_Connection_String_Here";

        using var wrapper = new DataAccessWrapper(connectionString);

        var parameters = new (string, object, SqlDbType?)[] 
        {
            ("@ZipCode", "01020", SqlDbType.VarChar),
            ("@RadiusMile", 100, SqlDbType.Float)
        };

        var results = await wrapper.GetResultsAsync(
            "List.ZipSearch",
            CommandType.StoredProcedure,
            parameters);

        Console.WriteLine($"Found {results.Count} locations");
    }
}

public class LocationInfo
{
    public long ZipID { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public double Latitude { get; set; }
    public double Longitude { get; set; }
}

Performance Considerations and Benchmarks

The refactored code maintains the performance characteristics of the original while adding safety:

  1. Compiled Expression Caching: The ObjectMapper caches compiled delegates for each unique result set shape.
  2. Async Overhead: Proper async/await has minimal overhead compared to the blocked threads in the original implementation.
  3. Connection Pooling: ADO.NET's built-in connection pooling ensures efficient connection management.
  4. Reduced Boxing: The improved parameter handling reduces boxing operations for value types.

Conclusion

This modernized data access layer preserves the original vision of a fast, flexible, and strongly-typed database access solution while addressing critical issues and aligning with contemporary .NET development practices. It demonstrates how to:

  • Implement true async/await throughout the stack
  • Manage resources safely with IAsyncDisposable
  • Create DI-friendly components
  • Maintain high performance with cached expression compilation
  • Use modern C# features effectively

The result is a robust, maintainable, and performant data access solution that remains completely original and dependency-free, ready for use in modern .NET applications.

Like This Article? Share It!

Our Privacy Policy

Last Updated: June 18th, 2025

Introduction

Western Mass Hosting (“we,” “our,” or “us”) respects the privacy of all individuals and organizations that interact with our services. This Privacy Policy establishes our practices regarding the collection, use, disclosure, and protection of personal information for visitors to our website and clients utilizing our managed hosting and WordPress services. By accessing our website or engaging our services, you acknowledge that you have read and understood this policy in its entirety.

Scope and Applicability

This Privacy Policy governs our handling of information collected through our corporate website and in the course of providing managed hosting, WordPress maintenance, and development services. In accordance with global privacy regulations, we serve as a Data Controller for information related to our business operations and client relationships. When processing data on behalf of our clients through hosted services, we act as a Data Processor under applicable data protection laws.

Information We Collect

We collect various categories of information necessary to provide and improve our services. This includes personal contact and payment details provided during account registration, technical information such as IP addresses and device characteristics for security purposes, and records of communications through support channels. For clients utilizing our hosting services, we may process end-user data stored within client websites, though we do not control or monitor the collection practices of such data.

Purpose and Legal Basis for Processing

We process personal information only when we have proper justification under applicable laws. The primary legal bases for our processing activities include the necessity to fulfill contractual obligations to our clients, our legitimate business interests in maintaining and improving our services, and in limited cases, explicit consent for specific marketing communications. We maintain detailed records of processing activities to demonstrate compliance with legal requirements.

Use of Collected Information

The information we collect serves multiple business purposes. Primarily, we use this data to deliver and maintain reliable hosting services, including server provisioning, performance monitoring, and technical support. We also utilize information for business operations such as billing, customer relationship management, and service improvement initiatives. Security represents another critical use case, where we analyze data to detect and prevent fraudulent activity or unauthorized access to our systems.

Data Sharing and Third-Party Disclosures

We engage with carefully selected third-party service providers to support our operations, including cloud infrastructure providers, payment processors, and customer support platforms. These relationships are governed by strict contractual agreements that mandate appropriate data protection measures. We may disclose information when legally required to comply with court orders, government requests, or to protect our legal rights and the security of our services.

International Data Transfers

As a global service provider, we may transfer and process data in various locations worldwide. When transferring personal data originating from the European Economic Area or other regulated jurisdictions, we implement appropriate safeguards such as Standard Contractual Clauses and rely on adequacy decisions where applicable. Our subprocessors, including AWS Lightsail, maintain robust compliance certifications to ensure the protection of transferred data.

Data Retention Practices

We retain personal information only for as long as necessary to fulfill the purposes outlined in this policy. Client account information is typically maintained for five years following service termination to comply with legal and financial reporting obligations. Backup data associated with hosting services is automatically purged after thirty days, as specified in our Terms of Service. For data processed on behalf of clients, retention periods are determined by the respective client’s policies and instructions.

Security Measures

We implement comprehensive technical and organizational security measures to protect personal information against unauthorized access, alteration, or destruction. Our security program includes network encryption protocols, regular vulnerability assessments, strict access controls, and employee training on data protection best practices. We maintain incident response procedures to address potential security breaches and will notify affected parties where required by law.

Individual Rights

Individuals whose personal data we process may exercise certain rights under applicable privacy laws. These rights may include requesting access to their information, seeking correction of inaccurate data, requesting deletion under specific circumstances, and objecting to particular processing activities. We have established procedures to handle such requests in accordance with legal requirements, typically responding within thirty days of receipt. Requests should be submitted to our designated Data Protection Officer through the contact information provided in this policy.

Cookies and Tracking Technologies

Our website employs various technologies to enhance user experience and analyze site performance. Essential cookies are used for basic functionality and security purposes, while analytics cookies help us understand how visitors interact with our site. Marketing cookies are only deployed with explicit user consent. Visitors can manage cookie preferences through their browser settings or our cookie consent tool.

Policy Updates and Notifications

We periodically review and update this Privacy Policy to reflect changes in our practices or legal obligations. Material changes will be communicated to affected clients through email notifications at least thirty days prior to implementation. Continued use of our services following such notifications constitutes acceptance of the revised policy.

Contact Information

For questions or concerns regarding this Privacy Policy or our privacy practices, please contact our Data Protection Officer at info@westernmasshosting.com or by mail at:

Western Mass Hosting
22 Orlando. St.,
Feeding Hills, MA 01030.

We take all privacy-related inquiries seriously and will respond promptly to legitimate requests. For clients with specific data processing agreements, please reference your contract for any additional terms that may apply to our handling of your data.

Like This Article? Share It!