How to return stored procedure results into a List of Class when not using entity framework?

StackOverflow https://stackoverflow.com/questions/23553526

  •  18-07-2023
  •  | 
  •  

Question

I have been working with entity framework for quite some time and have gotten quite used to using this to dump data into Lists of Classes.

I am now working on a very old project that doesn't have EF and I don't have the time to convert it over to EF (nor would I as it needs a full rewrite).

I am in there though making some minor adjustments and need to pull back data from a stored procedure. I want to store the data in a List of Class, class being one that I created on my own.

I know I can use a DataReader and just read each record passed back and create an item of Class and add that to the list.

                List<MyClass> myClassGuy = new List<MyClass>();
                dr = cmd.ExecuteReader(CommandBehavior.Default);

                while (dr.Read())
                {
                    MyClass myClassItemToAdd = new MyClass();
                    myClassItemToAdd.VarA = (dr, "VARA");
                    myClassItemToAdd.VarB = (dr, "VARB");
                    myClassItemToAdd.VarC = (dr, "VARC");
                    //etc
                    myClassGuy.Add(myClassItemToAdd);
                }

I know I can do the above but after using EF for such a long time. I have to believe there is a better way of doing this? Is there?

Was it helpful?

Solution 2

One of the great things about EF is that it automates the process of materializing objects stored in the database.

Using ADO.Net, that work falls on you. You need to read out column values, transform them if appropriate, and put them in the right part of your object.

There's no significantly easier way, without introducing additional frameworks (e.g. AutoMapper), to do that than what you have now.

OTHER TIPS

I wrote a whole bunch of code to handle this stuff, then found AutoMapper which (generally) does a pretty good job when working with results from SQL queries:

// setup mapping (this is a once-per-execution thing)
if (AutoMapper.Mapper.FindTypeMapFor<IDataReader, MyData>() == null)
    AutoMapper.Mapper.CreateMap<IDataReader, MyData>();

// Read from the DataReader into a list
IList<MyClass> data = AutoMapper.Mapper.Map<IDataReader, IList<MyData>>(dr);

You should write your own reusable methods to make things easier when you work with your next similar task. As about your exact question, no, there is no simpler solution, but nothing stops you from simplifying your future tasks as much as possible.

You could refactor what you have, one DAL and SomeClass.

The nice thing about not using EF is you have a lot more control on your app's performance. Doesn't matter how much horse power you have still your jockey must be light.

That is of course if your app is critical or some trivial web page.

If you are absolutely unable to use an ORM framework, then you can make then code more generic by modifying your stored procedures to return an XML stream (which should be fairly easy to do) and on the application side, deserialize the stream into objects.

For example, I can define the following data access method which takes a generic type, a name of stored procedure and any number of SqlParameters (optional) and deserializes and returns a list of T. The helper method Deserialize<T> takes in a an XML stream and deserializes it to a List<T>:

public static IEnumerable<T> GetEntity<T>(string storedProcedureName, params SqlParameter[] parameters)
{
    try
    {
        using (SqlConnection connection =
            new SqlConnection("connectionString"))
        {
            connection.Open();
            SqlCommand command = new SqlCommand(storedProcedureName, connection);
            command.CommandType = System.Data.CommandType.StoredProcedure;

            if (parameters != null && parameters.Any())
            {
                command.Parameters.AddRange(parameters);
            }

            string result = (string)command.ExecuteScalar();

            return Deserialize<T>(result);
        }
    }
    catch (Exception ex)
    {
        // Handle the exception
        return (IEnumerable<T>)default(T);
    }
}

private static IEnumerable<T> Deserialize<T>(string xmlStream, params Type[] additionalTypes)
{
    XmlSerializer serializer = additionalTypes == null ? new XmlSerializer(typeof(List<T>))
        : new XmlSerializer(typeof(List<T>), additionalTypes);


    using (var reader = new XmlTextReader(new StringReader(xmlStream)))
    {
        if (!serializer.CanDeserialize(reader))
        {
            return (IEnumerable<T>)default(T);
        }

        return (IEnumerable<T>)serializer.Deserialize(reader);
    }
}

And to use it, I can have something as simple as

IEnumerable<MyObject> myObjects = DataAccess.GetEntity<MyObject>("MyStoredProc");

The stored procedure looks like this:

SELECT t.Id as 'Id', t.Name as 'Name'
FROM MyTable t

FOR XML PATH('MyObject'), ROOT('ArrayOfMyObject')

The only tricky part is to make sure column aliases match the properties on the object. You'll also need to add the [XmlRoot("ArrayOfMyObject")] attribute to the MyObject class.

This seems like a lot of code but if you are truly unable to use a framework or library, this would be a decent path to go IMO.

Well it kind of depends. How many properties are you mapping - if it's only a handful then your current approach is probably best, although it's initially onerous it still makes maintenance easy if the SP result set changes.

Then again if we're talking about many fields coming back from the stored procedure, you could use the very lightweight Linq to SQL - it's officially "dead" but still perfect for this kind of thing and it can map to stored procedure results. Or something even lighter like Rob Conery's Massive which at something like 700 loc is a good option.

And of course Automapper is always an option.

However. If for some reason you can't introduce L2S or any 3rd party packages into the legacy codebase, you could go with a simple reflection based mapper class like the example below.

Given this SQL:

create table MyRecords
(
    ID int identity,
    Name nvarchar(255),
    DateCreated datetime,
    IsSilly bit
)

Insert into MyRecords select 'John',getdate(),0
Insert into MyRecords select 'Andrew',getdate(),1
Insert into MyRecords select 'Steve',getdate(),0
Insert into MyRecords select 'Max',getdate(),1

Then this console app shows how simple it really is to generalise this kind of thing; error checking, etc, omitted for brevity, and it doesn't support multiple result sets, but you get the idea of how little code is required.

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Reflection;

namespace MiniORM
{
    //Attribute to map SQL result set field to class public instance property
    public class FieldInfo : Attribute
    {
        public string FieldName { get; set; }
        public Type DataType { get; set; }
    }

    public class MyRecord
    {
        [FieldInfo(DataType=typeof(int),FieldName="ID")]
        public int ID { get; set; }
        [FieldInfo(DataType = typeof(string), FieldName = "Name")]
        public string Name { get; set; }
        [FieldInfo(DataType = typeof(DateTime), FieldName = "DateCreated")]
        public DateTime DateCreated { get; set; }
        [FieldInfo(DataType = typeof(bool), FieldName = "IsSilly")]
        public bool IsSilly { get; set; }

        public override string ToString()
        {
            return string.Format("{0}-{1}-{2}-{3}", ID, Name, DateCreated, IsSilly);
        }
    }

    public class FieldInfoDBMapper<T> where T: class,new()
    {
        private readonly Dictionary<string,KeyValuePair<PropertyInfo,Type>> _mapping; 

        public FieldInfoDBMapper()
        {
            var t = typeof (T);
            _mapping = new Dictionary<string,KeyValuePair<PropertyInfo,Type>>();
            foreach (var pi in t.GetProperties(BindingFlags.Instance | BindingFlags.Public))
            {
                var infos = pi.GetCustomAttributes(typeof(FieldInfo));
                if (infos.Any())
                {
                    var fieldInfo = (FieldInfo) infos.First();
                    _mapping.Add(fieldInfo.FieldName,new KeyValuePair<PropertyInfo, Type>(pi,fieldInfo.DataType));
                }
            }

        }
        public List<T> MapFromReader(IDataReader reader)
        {
            List<T> data = new List<T>();
            while (reader.Read())
            {
                T item = new T();
                foreach (var entry in _mapping)
                {
                    var value = Convert.ChangeType(reader[entry.Key],entry.Value.Value);
                    entry.Value.Key.SetValue(item,value);
                }
                data.Add(item);
            }
            return data;
        }
    }

    public class Program
    {
        static void Main(string[] args)
        {
            List<MyRecord> records  =new List<MyRecord>();
            using (
                SqlConnection conn =
                    new SqlConnection("Your connection string here"))
            {
                conn.Open();
                using (SqlCommand comm = new SqlCommand("Select * from MyRecords", conn))
                {
                    var reader = comm.ExecuteReader();
                    var mapper = new FieldInfoDBMapper<MyRecord>();
                    records.AddRange(mapper.MapFromReader(reader));
                }
            }
            foreach (var record in records)
            {
                Console.WriteLine(record.ToString());
            }
            Console.ReadLine();
        }
    }
}

Output:

1-John-09/05/2014 00:39:37-False
2-Andrew-09/05/2014 00:39:37-True
3-Steve-09/05/2014 00:39:37-False
4-Max-09/05/2014 00:39:37-True

Hope that helps.

I have a mapping library for stuff like this. https://www.nuget.org/packages/SprocMapper/

Documentation: https://github.com/gtaylor44/SprocMapper

public class MyClass
{
    string VarA;
    string VarB;
    string VarC;
}

public List<MyClass> GetResults()
{
    using (SqlConnection conn = new SqlConnection("YourConnection"))
    {
        return conn.Select().ExecuteReader<MyClass>(conn, "YourStoredProcedure");
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top