简体   繁体   中英

What Strongly-Typed Data-Structure can I use to hold a collection of multiple RecordSets with different shape

At the request of a responder to my original question here , I have been asked to reword the question in an effort to get to the bottom of the actual requirement.

What Strongly-Typed Data-Structure can I use to hold a collection of multiple RecordSets where each RecordSet will contain rows that may be of a different shape to the other RecordSets ?

This need is driven by the need to handle the data coming back from a Stored Procedure via a DbDataReader . The Stored Procedure may have multiple RecordSets , with each RecordSet returning different columns and number of columns.

There will be DTO classes to represent each row of the respective data sets. These are known at compile time. What I need is a data structure that can hold the multiple RecordSets , and for each RecordSet hold the DTOs representing rows returned from the RecordSet .

Example Stored procedure:

CREATE PROCEDURE [dbo].[MultipleRecordSetStoredProcedure]
    @Id                 INT
,   @Name               VARCHAR(20)
,   @Active             BIT
,   @Price              DECIMAL(10, 4)
,   @UniqueIdentifier   UNIQUEIDENTIFIER
,   @Count              TINYINT
AS
BEGIN
    /* First Record Set */
    SELECT 
        @Id AS Id
    ,   @Name AS Name
    UNION
    SELECT
        17 AS Id
    ,   'Bill' AS Name;

    /* Second Record Set */
    SELECT 
        @Name AS Name,
        @Active as Active
    UNION
    SELECT
        'Bill' AS Name
    ,   CAST(0 AS BIT) AS Active;

    /* Third Record Set */
    SELECT
        @UniqueIdentifier   AS UNIQUEIDENTIFIER
    ,   @Count              AS Count
    UNION
    SELECT
        NEWID() AS UNIQUEIDENTIFIER
   ,    CAST(10 AS TINYINT) AS Count;
END

Example calling code:

DbConnection connection = CreateConnection();
CommandBehavior commandBehavior = CommandBehavior.Default;

// Create a command to execute the stored storedProcedure
using (DbCommand command = connection.CreateStoredProcedureCommand(
    procedureName,
    procedureParameters,
    commandTimeout,
    transaction))
{
    // Populate a DataReder by calling the command
    using (DbDataReader reader = command.ExecuteReader(commandBehavior))
    {
        // Iterate through each result set...
        do
        {
            // Process the result set line by line
            while (reader.Read())
            {   
                // Read data into DataStructure
            }

        } while (reader.NextResult());
    }    
}

Example DTOs here:

internal class MultipleRecordSetStoredProcedureReturnType1
{
    public int Id { get; set; }
    public string Name { get; set; }
}

internal class MultipleRecordSetStoredProcedureReturnType2
{
    public bool Active { get; set; }
    public decimal Decimal { get; set; }
}

internal class MultipleRecordSetStoredProcedureReturnType3
{
    public Guid UniqueIdentifier { get; set; }
    public Byte Count { get; set; }
}

Ideally I do not want a list of objects or dynamics but a list of my DTOs for the recordset contents. Hopefully this will better clarify my original question.

In this case, I think it's better to keep things simple.

  1. You have a stored procedure that returns 3 result sets, so create a model that contains those 3 result sets.
  2. To fill your ResultModel , its better to use SqlDataAdapter and DataSet . It make things very simple, very very easier than data reader.

The code for ResultModel:

public class ResultModel
{
    public List<Type1> List1 { get; set; }
    public List<Type2> List2 { get; set; }
    public List<Type3> List3 { get; set; }
}

And I suppose these are your types:

public class Type1
{
    public int A { get; set; }
}
public class Type2
{
    public int B { get; set; }
    public string C { get; set; }
}
public class Type3
{
    public int D { get; set; }
    public string E { get; set; }
    public string F { get; set; }
}

And you can fill your ResultModel, using SqlDataAdapter and DataSet:

public ResultModel GetData()
{
    var connection = @"data source=(localdb)\v11.0;initial catalog=TestDB;integrated security=True;MultipleActiveResultSets=True;";
    var command = "dbo.Procedure";
    var tableAdapter = new System.Data.SqlClient.SqlDataAdapter(command, connection);
    tableAdapter.SelectCommand.CommandType = CommandType.StoredProcedure;
    var dataSet = new DataSet();
    tableAdapter.Fill(dataSet); 

    var t1 = dataSet.Tables[0].Rows.Cast<DataRow>()
        .ToList().Select(row => new Type1
        {
            A = row.Field<int>("A"),
        }).ToList();

    var t2 = dataSet.Tables[1].Rows.Cast<DataRow>()
        .ToList().Select(row => new Type2
        {
            B = row.Field<int>("B"),
            C = row.Field<string>("C")
        }).ToList();

    var t3 = dataSet.Tables[1].Rows.Cast<DataRow>()
        .ToList().Select(row => new Type3
        {
            D = row.Field<int>("D"),
            E = row.Field<string>("E"),
            F = row.Field<string>("F")
        }).ToList();

    var result = new ResultModel() { List1 = t1, List2 = t2, List3 = t3 };

    return result;
}

The key points here are:

  • We have a clean and simple ResultModel
  • Using SqlDataAdapter and a DataSet makes reading multiple result so easy.
  • Using Cast<DataRow>() enables us to use Linq against DataTable.Rows
  • Using Field<T>("field") enables us to get typed value of field

Having read the original question, and this one, it sounds like you will ultimately end up with a List that contains within it other Lists each with their own type. As you iterate through the list you'll have no idea of what type to expect for the inner list, as it's variable. Given this you'll inevitably have to test for the type and then cast it, eg

foreach(var resultset in resultsets)
{
    if(resultset is MultipleRecordSetStoredProcedureReturnType1)
        //... cast it and do something...
}

Given that you'll inevitably have to test for the inner type and cast, even if you could achieve your goal I'm not sure it will serve any purpose in the end. The list may as well just be

List<List<Object>>

or perhaps

List<List<IResultSet>>

As per the final example, you could consider defining an Interface for the inner type just to ensure that only objects that inherit from that interface are contained within it. This may be beneficial even if the interface doesn't define any behaviour and is just a simple declaration.

I'm not sure it makes sense to return a List containing disparate results. I'd probably create a wrapper class to group your dto objects like below.

public class RecordSetContainer
{
    public RecordSetContainer()
    {
        RecordSet1 = new List<MultipleRecordSetStoredProcedureReturnType1>();
        RecordSet2 = new List<MultipleRecordSetStoredProcedureReturnType2>();
        RecordSet3 = new List<MultipleRecordSetStoredProcedureReturnType3>();
    }
    public List<MultipleRecordSetStoredProcedureReturnType1> RecordSet1 { get; set; }
    public List<MultipleRecordSetStoredProcedureReturnType2> RecordSet2 { get; set; }
    public List<MultipleRecordSetStoredProcedureReturnType3> RecordSet3 { get; set; }
}

If you wanted to take the db code a step further to make it generic you could do something along these lines.

public T CallMultipleRecordSetStoredProcedure<T>(
    params Expression<Func<T, IList>>[] recordSetPropertiesInOrder)
    where T : class, new()
{
    var outputType = typeof (T);
    var output = new T();

    // DbConnection & Command setup hidden 

    var recordSetNumber = 0;
    do
    {
        var outputRecordSetPropertyName =
            ((MemberExpression) recordSetPropertiesInOrder[recordSetNumber].Body).Member.Name;
        var dtoList = (IList) outputType.GetProperty(outputRecordSetPropertyName).GetValue(output);
        var dtoListType = dtoList.GetType().GetGenericArguments()[0];

        while (reader.Read())
        {
            var item = Activator.CreateInstance(dtoListType);
            var propertiesToWrite = dtoListType.GetProperties().Where(p => p.CanWrite);

            foreach (var property in propertiesToWrite)
            {
                property.SetValue(
                    item,
                    Convert.ChangeType(reader[property.Name], property.PropertyType));
            }
            dtoList.Add(item);
        }

        recordSetNumber++;
    } while (reader.NextResult());

    return output;
}

Not the prettiest or readable code I admit but it should allow you to call any multi record set stored procedure as long as the property names match the column names. One downside with this code is that while the compiler will enforce you to only select IList properties in you expressions it is unable to ensure you pick a generic list (which the method itself actually requires).

For your example case the method would be called as below

CallMultipleRecordSetStoredProcedure<RecordSetContainer>(
    rsc => rsc.RecordSet1,
    rsc => rsc.RecordSet2,
    rsc => rsc.RecordSet3);

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