J'utilise une bibliothèque tierce qui renvoie un lecteur de données. Je voudrais un moyen simple et aussi générique que possible pour le convertir en une liste d'objets.
Par exemple, disons que j'ai une classe 'Employee' avec 2 propriétés EmployeeId et Name, j'aimerais que le lecteur de données (qui contient une liste d'employés) soit converti en Liste <Employé>.
Je suppose que je n'ai d'autre choix que de parcourir les lignes du lecteur de données et de les convertir pour chacune d'elles en un objet Employee que je vais ajouter à la liste. Une meilleure solution? J'utilise C # 3.5 et, idéalement, j'aimerais qu'il soit aussi générique que possible pour qu'il fonctionne avec toutes les classes (les noms de champs dans DataReader correspondent aux noms de propriétés des différents objets).
Avez-vous vraiment besoin d'une liste ou IEnumerable serait-il suffisant?
Je sais que vous voulez que ce soit générique, mais un modèle beaucoup plus courant consiste à avoir une méthode Factory statique sur le type d'objet cible qui accepte un datarow (ou IDataRecord). Cela ressemblerait à ceci:
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public static Employee Create(IDataRecord record)
{
return new Employee
{
Id = record["id"],
Name = record["name"]
};
}
}
.
public IEnumerable<Employee> GetEmployees()
{
using (var reader = YourLibraryFunction())
{
while (reader.Read())
{
yield return Employee.Create(reader);
}
}
}
Ensuite, si vous vraiment avez besoin d'une liste plutôt que d'un IEnumerable, vous pouvez appeler .ToList()
sur les résultats. Je suppose que vous pouvez également utiliser génériques + un délégué pour rendre le code de ce modèle plus réutilisable.
Mise à jour: Je l'ai revu aujourd'hui et j'ai eu l'impression d'écrire le code générique:
public IEnumerable<T> GetData<T>(IDataReader reader, Func<IDataRecord, T> BuildObject)
{
try
{
while (reader.Read())
{
yield return BuildObject(reader);
}
}
finally
{
reader.Dispose();
}
}
//call it like this:
var result = GetData(YourLibraryFunction(), Employee.Create);
Vous pouvez construire une méthode d'extension telle que:
public static List<T> ReadList<T>(this IDataReader reader,
Func<IDataRecord, T> generator) {
var list = new List<T>();
while (reader.Read())
list.Add(generator(reader));
return list;
}
et l'utiliser comme:
var employeeList = reader.ReadList(x => new Employee {
Name = x.GetString(0),
Age = x.GetInt32(1)
});
La suggestion de Joel est bonne. Vous pouvez choisir de renvoyer IEnumerable<T>
. Il est facile de transformer le code ci-dessus:
public static IEnumerable<T> GetEnumerator<T>(this IDataReader reader,
Func<IDataRecord, T> generator) {
while (reader.Read())
yield return generator(reader);
}
Si vous souhaitez mapper automatiquement les colonnes aux propriétés, l'idée de code est la même. Vous pouvez simplement remplacer la fonction generator
dans le code ci-dessus par une fonction qui interroge typeof(T)
et définit les propriétés de l'objet en utilisant la réflexion en lisant la colonne correspondante. Cependant, je préfère personnellement définir une méthode d'usine (comme celle mentionnée dans la réponse de Joel) et en faire passer un délégué à cette fonction:
var list = dataReader.GetEnumerator(Employee.Create).ToList();
Bien que je ne recommande pas ceci pour le code de production, vous pouvez le faire automatiquement en utilisant la réflexion et les génériques:
public static class DataRecordHelper
{
public static void CreateRecord<T>(IDataRecord record, T myClass)
{
PropertyInfo[] propertyInfos = typeof(T).GetProperties();
for (int i = 0; i < record.FieldCount; i++)
{
foreach (PropertyInfo propertyInfo in propertyInfos)
{
if (propertyInfo.Name == record.GetName(i))
{
propertyInfo.SetValue(myClass, Convert.ChangeType(record.GetValue(i), record.GetFieldType(i)), null);
break;
}
}
}
}
}
public class Employee
{
public int Id { get; set; }
public string LastName { get; set; }
public DateTime? BirthDate { get; set; }
public static IDataReader GetEmployeesReader()
{
SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["NorthwindConnectionString"].ConnectionString);
conn.Open();
using (SqlCommand cmd = new SqlCommand("SELECT EmployeeID As Id, LastName, BirthDate FROM Employees"))
{
cmd.Connection = conn;
return cmd.ExecuteReader(CommandBehavior.CloseConnection);
}
}
public static IEnumerable GetEmployees()
{
IDataReader rdr = GetEmployeesReader();
while (rdr.Read())
{
Employee emp = new Employee();
DataRecordHelper.CreateRecord<Employee>(rdr, emp);
yield return emp;
}
}
}
Vous pouvez ensuite utiliser CreateRecord<T>()
pour instancier une classe à partir des champs d'un lecteur de données.
<asp:GridView ID="GvEmps" runat="server" AutoGenerateColumns="true"></asp:GridView>
GvEmps.DataSource = Employee.GetEmployees();
GvEmps.DataBind();
Nous avons mis en œuvre la solution suivante et estimons que cela fonctionne plutôt bien. C'est assez simple et nécessite un peu plus de câblage qu'un mappeur. Cependant, il est parfois agréable d'avoir le contrôle manuel et honnêtement, vous connectez une fois et vous avez terminé.
En résumé: Nos modèles de domaine implémentent une interface utilisant une méthode qui prend une variable IDataReader
et en remplit les propriétés du modèle. Nous utilisons ensuite Generics et Reflection pour créer une instance du modèle et appelons la méthode Parse
.
Nous avons envisagé d'utiliser un constructeur et de lui passer IDataReader
, mais les vérifications de performances de basic que nous avons effectuées semblaient suggérer que l'interface était toujours plus rapide (même si elle était de peu). En outre, la route d'interface fournit un retour instantané via des erreurs de compilation.
Une des choses que j’aime bien, c’est que vous pouvez utiliser private set
pour des propriétés comme Age
dans l’exemple ci-dessous et les définir directement à partir de la base de données.
public interface IDataReaderParser
{
void Parse(IDataReader reader);
}
public class Foo : IDataReaderParser
{
public string Name { get; set; }
public int Age { get; private set; }
public void Parse(IDataReader reader)
{
Name = reader["Name"] as string;
Age = Convert.ToInt32(reader["Age"]);
}
}
public class DataLoader
{
public static IEnumerable<TEntity> GetRecords<TEntity>(string connectionStringName, string storedProcedureName, IEnumerable<SqlParameter> parameters = null)
where TEntity : IDataReaderParser, new()
{
using (var sqlCommand = new SqlCommand(storedProcedureName, Connections.GetSqlConnection(connectionStringName)))
{
using (sqlCommand.Connection)
{
sqlCommand.CommandType = CommandType.StoredProcedure;
AssignParameters(parameters, sqlCommand);
sqlCommand.Connection.Open();
using (var sqlDataReader = sqlCommand.ExecuteReader())
{
while (sqlDataReader.Read())
{
//Create an instance and parse the reader to set the properties
var entity = new TEntity();
entity.Parse(sqlDataReader);
yield return entity;
}
}
}
}
}
}
Pour l'appeler, vous fournissez simplement le paramètre type
IEnumerable<Foo> foos = DataLoader.GetRecords<Foo>(/* params */)
Pour .NET Core 2.0:
Voici une méthode d'extension qui fonctionne avec .NET CORE 2.0 pour exécuter RAW SQL et mapper les résultats en LISTE de types arbitraires:
USAGE:
var theViewModel = new List();
string theQuery = @"SELECT * FROM dbo.Something";
theViewModel = DataSQLHelper.ExecSQL(theQuery,_context);
using Microsoft.EntityFrameworkCore;
using System.Data;
using System.Data.SqlClient;
using System.Reflection;
public static List ExecSQL(string query, myDBcontext context)
{
using (context)
{
using (var command = context.Database.GetDbConnection().CreateCommand())
{
command.CommandText = query;
command.CommandType = CommandType.Text;
context.Database.OpenConnection();
using (var result = command.ExecuteReader())
{
List<T> list = new List<T>();
T obj = default(T);
while (result.Read())
{
obj = Activator.CreateInstance<T>();
foreach (PropertyInfo prop in obj.GetType().GetProperties())
{
if (!object.Equals(result[prop.Name], DBNull.Value))
{
prop.SetValue(obj, result[prop.Name], null);
}
}
list.Add(obj);
}
return list;
}
}
}
}
La solution la plus simple:
var dt=new DataTable();
dt.Load(myDataReader);
list<DataRow> dr=dt.AsEnumerable().ToList();
Puis sélectionnez-les pour les mapper à n’importe quel type.
Remarque: il s'agit du code .NET Core
Une option stupidement performante, si vous ne craignez pas une dépendance externe (l'étonnant paquet de nuget Fast Member
):
public static T ConvertToObject<T>(this SqlDataReader rd) where T : class, new()
{
Type type = typeof(T);
var accessor = TypeAccessor.Create(type);
var members = accessor.GetMembers();
var t = new T();
for (int i = 0; i < rd.FieldCount; i++)
{
if (!rd.IsDBNull(i))
{
string fieldName = rd.GetName(i);
if (members.Any(m => string.Equals(m.Name, fieldName, StringComparison.OrdinalIgnoreCase)))
{
accessor[t, fieldName] = rd.GetValue(i);
}
}
}
return t;
}
Utiliser:
public IEnumerable<T> GetResults<T>(SqlDataReader dr) where T : class, new()
{
while (dr.Read())
{
yield return dr.ConvertToObject<T>());
}
}