简体   繁体   中英

Fetching SQL data from different tables with ADO.NET, Generics

I often come to a point that when developing an application that connects to a MS SQL database for basic CRUD functions, I need to iterate through the returned results and populate a list of a particular data type that matches the table design.

Specific to .NET, I would just create a method (eg GetAllObjA()) and use ADO.NET to specify a SqlCommand that hooks up to a Stored Proc to fetch the data from the return SqlDataReader. I would then iterate through the returned rows, creating a new object and adding that to a list for each.

If however, I wanted to fetch data for different data type, I would rewrite this for methods GetAllObjB(), GetAllObjC() and so forth with the list's data type being different, which feels like a complete waste of rewriting code.

I realise Generics have a purpose here where the data type can be specified in the one method such as GetAll< T >(). Unfortunately this would still require me to define which table I would be fetching the data from and still require me to match the column names with the member names in the object, which doesn't really solve the problem as the application code has no way of knowing how the table is designed.

Is this the extent of Generics' usefulness in this instance? As the applications I am building are fairly small scale, I don't feel an ORM is warranted if it is possible to hand-code the solution instead.

I agree with the comments that micro-ORM's can either solve your scenario or give you some good ideas. Massive is just plain fun to read because it fits in one file. It uses the dynamic functionality of the Framework to solve your issue. Dapper is oriented towards the mapping aspect.

Also, take a look at the Data Access Application Block . Although this is now Open Source, it was originally maintained by Microsoft. It was an overall enterprise application framework, so there are several bloated dependencies that you do not need. But the data access block has some great prototypes for exactly what you are asking for: mapping a IDataReader resultset to a POCO using generics. So you write the mapping code only once, and the only thing you define per table is the actual reader-to-poco property mapping.

Sometimes a table or its mapping may have some quirks, so you want to keep the mapping definition by hand. For other simple tables with basic primitives, the use of dynamics as demonstrated by Rob Connery's Massive combined with the generic rowset mapper can make for some very easy-to-read-and-maintain code.

Disclaimer: this is not a judgement on the merits of this approach over EF. It is simply suggesting a simple, non-EF approach.

So, you might have a small library defining an interface:

public interface IResultSetMapper<TResult>
{
    Task<List<TResult>> MapSetAsync(IDataReader reader);
}

and a generic ADO.Net helper class that processes any reader:

// a method with a class that manages the Command (_Command) and Connection objects 
public async Task<List<TOut>> ExecuteReaderAsync<TOut>(IResultSetMapper<TOut> resultsMapper)
{
    List<TOut> results = null;
    try
    {
        using(var connection = await _DbContext.CreateOpenedConnectionAsync())
        {
            _Command.Connection = connection;

            // execute the reader and iterate the results; when done, the connection is closed
            using(var reader = await _Command.ExecuteReaderAsync())
            {
                results = await resultsMapper.MapSetAsync(reader);
            }
        }
        return results;
    }
    catch(Exception cmdEx)
    {
        // handle or log exception...
        throw;
    }
}

So, the above code would be a helper library, written once. Then your mapper in your application might look like;

internal class ProductReaderMap : IResultSetMapper<Product>
{
    public async Task<List<Product>> MapSetAsync(IDataReader reader)
    {
        List<Product> results = new List<Product>();
        using(reader)
        {
          results.Add(new Product
          {
            ProductId = r.GetInt32(0),
            ProductName = r.GetString(1),
            SupplierId = r.GetInt32(2),
            UnitsInStock = r.GetInt16(3)
          });
        }
    return results;
    }
}

You could break this out even further, defining a row mapper rather that a row set mapper, since the iteration over the reader can be abstracted as well.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM