Complete Implementation Guide: Returning SELECT Query Results from Stored Procedures to C# Lists

Dec 04, 2025 · Programming · 14 views · 7.8

Keywords: Stored Procedures | SELECT Queries | C# Data Access | SqlDataReader | DataTable | GridView Binding

Abstract: This article provides a comprehensive guide on executing SELECT queries in SQL Server stored procedures and returning results to lists in C# applications. It analyzes three primary methods—SqlDataReader, DataTable, and SqlDataAdapter—with complete code examples and performance comparisons. The article also covers practical techniques for data binding to GridView components and optimizing stored procedure design for efficient data access.

Basic Structure of SELECT Queries in Stored Procedures

When creating stored procedures in SQL Server, SELECT queries are among the most common operations. Stored procedures not only encapsulate business logic but also enhance performance and security. Below is a basic stored procedure example for retrieving data from the jobs table:

CREATE PROCEDURE GetJobsData
    @FilterCondition NVARCHAR(100)
AS
BEGIN
    SELECT name, occupation, position 
    FROM jobs 
    WHERE condition_column = @FilterCondition
END

This stored procedure accepts a parameter @FilterCondition for dynamically filtering query results. When calling this stored procedure in C#, parameterized queries should be used to prevent SQL injection attacks.

Using SqlDataReader to Read Data into Lists

SqlDataReader provides an efficient, forward-only method for reading data, particularly suitable for handling large datasets. Here is the complete implementation code:

public List<Job> GetJobsUsingReader(string filterValue)
{
    List<Job> jobsList = new List<Job>();
    
    using (SqlConnection connection = new SqlConnection(connectionString))
    using (SqlCommand command = new SqlCommand("GetJobsData", connection))
    {
        command.CommandType = CommandType.StoredProcedure;
        command.Parameters.AddWithValue("@FilterCondition", filterValue);
        
        connection.Open();
        using (SqlDataReader reader = command.ExecuteReader())
        {
            while (reader.Read())
            {
                Job job = new Job
                {
                    Name = reader["name"] as string ?? string.Empty,
                    Occupation = reader["occupation"] as string ?? string.Empty,
                    Position = reader["position"] as string ?? string.Empty
                };
                jobsList.Add(job);
            }
        }
    }
    
    return jobsList;
}

The key advantage of this method is its high memory efficiency, as data is read in a streaming fashion. However, it requires manual mapping of each field to object properties, which can become cumbersome with complex data structures.

Simplifying Data Loading with DataTable

DataTable offers a simpler approach for loading and manipulating data, especially suitable for data-binding scenarios:

public DataTable GetJobsUsingDataTable(string filterValue)
{
    DataTable dataTable = new DataTable();
    
    using (SqlConnection connection = new SqlConnection(connectionString))
    using (SqlCommand command = new SqlCommand("GetJobsData", connection))
    {
        command.CommandType = CommandType.StoredProcedure;
        command.Parameters.AddWithValue("@FilterCondition", filterValue);
        
        connection.Open();
        using (SqlDataReader reader = command.ExecuteReader())
        {
            dataTable.Load(reader);
        }
    }
    
    return dataTable;
}

The Load method of DataTable automatically loads query results into a tabular structure, simplifying the code. This method is particularly well-suited for direct binding to data controls like GridView.

Data Population Using SqlDataAdapter

SqlDataAdapter provides more advanced data access capabilities, supporting disconnected data operations:

public DataSet GetJobsUsingAdapter(string filterValue)
{
    DataSet dataSet = new DataSet();
    
    using (SqlConnection connection = new SqlConnection(connectionString))
    using (SqlCommand command = new SqlCommand("GetJobsData", connection))
    {
        command.CommandType = CommandType.StoredProcedure;
        command.Parameters.AddWithValue("@FilterCondition", filterValue);
        
        SqlDataAdapter adapter = new SqlDataAdapter(command);
        adapter.Fill(dataSet, "Jobs");
    }
    
    return dataSet;
}

This method allows maintaining a complete in-memory copy of the data, supporting complex update operations. However, it requires more memory resources and is not suitable for very large datasets.

Data Binding to GridView Components

Binding retrieved data to ASP.NET GridView components is a common requirement. Below are examples using both list and DataTable approaches:

// Binding using a list
List<Job> jobsList = GetJobsUsingReader("Developer");
gvJobs.DataSource = jobsList;
gvJobs.DataBind();

// Binding using DataTable
DataTable jobsTable = GetJobsUsingDataTable("Developer");
gvJobs.DataSource = jobsTable;
gvJobs.DataBind();

GridView automatically generates display columns based on the data source columns. For better user experience, column templates and styles can be customized.

Performance Optimization and Best Practices

In practical applications, the following optimization strategies should be considered:

  1. Connection Management: Always use using statements to ensure proper closing and disposal of database connections.
  2. Parameterized Queries: Use parameters in stored procedures to avoid SQL injection, and correctly pass parameters in C# code.
  3. Asynchronous Operations: For long-running queries, consider asynchronous methods like ExecuteReaderAsync.
  4. Pagination: For large datasets, implement pagination logic in stored procedures to avoid loading all data at once.
  5. Error Handling: Add appropriate exception handling mechanisms to ensure application robustness.

Extending Stored Procedure Functionality

As business requirements evolve, stored procedures may need to be extended. For example, adding transaction handling, multiple query operations, or complex business logic:

CREATE PROCEDURE ProcessJobsData
    @FilterCondition NVARCHAR(100),
    @OperationType INT
AS
BEGIN
    BEGIN TRY
        BEGIN TRANSACTION
        
        -- Query data
        SELECT name, occupation, position 
        FROM jobs 
        WHERE condition_column = @FilterCondition
        
        -- Execute additional operations based on operation type
        IF @OperationType = 1
        BEGIN
            UPDATE jobs SET status = 'Processed' 
            WHERE condition_column = @FilterCondition
        END
        
        COMMIT TRANSACTION
    END TRY
    BEGIN CATCH
        ROLLBACK TRANSACTION
        THROW
    END CATCH
END

This design pattern ensures atomicity and consistency of data operations while maintaining code maintainability.

Conclusion

Returning SELECT query results from stored procedures to C# lists is a common yet important technical task. SqlDataReader, DataTable, and SqlDataAdapter each have their advantages and disadvantages; the choice depends on specific application scenarios. SqlDataReader is suitable for performance-sensitive situations, DataTable for simple data-binding needs, and SqlDataAdapter for scenarios requiring complex data operations. Regardless of the method chosen, best practices should be followed, including proper connection management, parameterized queries, and appropriate error handling. As applications evolve, stored procedures can be extended to include more complex business logic, while the C# code structure should remain flexible to accommodate these changes.

Copyright Notice: All rights in this article are reserved by the operators of DevGex. Reasonable sharing and citation are welcome; any reproduction, excerpting, or re-publication without prior permission is prohibited.