I want to dynamically generate a class based on the results from a query that user submits. For instance, if the user enters Select name, age from tbl, the result is a name column which is string and age which is an int. The resulting class should be:
public class Test
{
public string Name { get; set; }
public int Age { get; set; }
}
Is there an efficient way to do this via EntityFramework or features in C# or I have to use maybe reflection to create a new type and instantiate it.
PS: My purpose is to run this query on the database and show the results in a Grid to the user and run some filter/sort/etc. on it.
You could use TypeBuilder to create a new type and execute the query against database using EF's SqlQuery() as mentioned here.
OR
A cleaner method would be to use dynamic objects to bind the grid. Extend EF to return a collection of dynamic objects as suggested by ChristineBoersen here. The code was written before EF went to RTM. Here's a version that works:
public static class EFExtensions
{
public static IEnumerable<dynamic> CollectionFromSql(this DbContext dbContext, string Sql, Dictionary<string, object> Parameters)
{
using (var cmd = dbContext.Database.Connection.CreateCommand())
{
cmd.CommandText = Sql;
if (cmd.Connection.State != ConnectionState.Open)
cmd.Connection.Open();
foreach (KeyValuePair<string, object> param in Parameters)
{
DbParameter dbParameter = cmd.CreateParameter();
dbParameter.ParameterName = param.Key;
dbParameter.Value = param.Value;
cmd.Parameters.Add(dbParameter);
}
//var retObject = new List<dynamic>();
using (var dataReader = cmd.ExecuteReader())
{
while (dataReader.Read())
{
var dataRow = GetDataRow(dataReader);
yield return dataRow;
}
}
}
}
private static dynamic GetDataRow(DbDataReader dataReader)
{
var dataRow = new ExpandoObject() as IDictionary<string, object>;
for (var fieldCount = 0; fieldCount < dataReader.FieldCount; fieldCount++)
dataRow.Add(dataReader.GetName(fieldCount), dataReader[fieldCount]);
return dataRow;
}
}
You could invoke the above method as follows:
var results = context.CollectionFromSql("Select Name, Age from tbl", new Dictionary<string, object>()).ToList();
// Bind results to grid
Related
I need to execute SP, I need to call SP by SP name and get back object and returning value. My problem that my object related to several tables in DB and I cannot use: Context.ExampleTable.FromSQl(...), because it's returning values only from ExampleTable, however I need to get object which having fields from 3 separated tables and also my SP returning value. Someone could help me? I'm using EntityFrameworkCore 2.2.6
i was trying to use Context.ExampleTable.FromSQl(...), but it's not what I need.
----Edited----
SqlParameter returnVal = new SqlParameter("#return", SqlDbType.Int);
returnVal.Direction = ParameterDirection.ReturnValue;
using (var context = new Context())
{
var test = context.Set<RequiredObject>().FromSql("EXEC SP_Name #return", returnVal);
}
EF Core 2.2.6 is out of support.
But you can define a new class that represents your result shape, and add that as a DbSet to your DbContext, and use the new class with FromSQL.
Great question! Unfotunately, it seems that EF Core does not have this possibility.
Tried this:
await db.Database.SqlQueryRaw<SomeComplexModel>("select * from Receivers inner join ... ").ToListAsync()
and this:
public DbSet<SomeComplexModel> Items { get; set; }
[NotMapped] public class SomeComplexModel { ... }
await db.Items.FromSqlRaw("select * from Receivers inner join ... ").ToListAsync();
Both failed with exceptions...
https://learn.microsoft.com/en-us/ef/core/querying/sql-queries
"The SQL query can't contain related data. However, in many cases you can compose on top of the query using the Include operator to return related data (see Including related data)."
What you definitely could do (I believe that's not the best way, may be someone would suggest option better): additionally install in the application some micro-ORM f.e. Dapper, share same db connection string and in this rare case when multiple related entities have to be queried via pure SQL - utilize not EF, but this ORM
Here you have extension methods in dotnet 6 that you can adapt and use.
For scalar results:
Usage:
var sql = "select count(*) from my_custom_view";
var count = await db.ExecuteScalar<int>(sql);
public static class EFExtensions
{
//...
public static async Task<T> ExecuteScalar<T>(this DbContext db, string query)
{
T obj = default;
using (var cmd = db.Database.GetDbConnection().CreateCommand())
{
cmd.CommandText = query;
cmd.CommandType = CommandType.Text;
db.Database.OpenConnection();
obj = (T)(await cmd.ExecuteScalarAsync());
}
return obj;
}
//...
}
Entities and complex nested types:
Usage:
// the parameters should be pass in SqlParameter types
var sql = $"exec my_custom_sp '{id}', '{date.ToString("yyyy-MM-dd")}'";
var myEntityFromDB = await db.ExecuteToEntity<MyStruct>(sql);
public static class EFExtensions
{
//...
public static async Task<T> ExecuteToEntity<T>(this DbContext db, string query, DbTransaction trx = null)
{
T obj = default;
using (var cmd = db.Database.GetDbConnection().CreateCommand())
{
cmd.CommandText = query;
cmd.CommandType = CommandType.Text;
cmd.Transaction = trx;
db.Database.OpenConnection();
using (var reader = await cmd.ExecuteReaderAsync())
{
if (!reader.Read())
{
throw new InvalidOperationException();
}
var dict = new Dictionary<string, object>();
for (var i = 0; i < reader.FieldCount; ++i)
{
var key = reader.GetName(i);
var value = reader[key];
if (value == DBNull.Value)
{
continue;
}
dict.Add(key, value);
}
obj = Tools.CloneJson<Dictionary<string, object>, T>(dict);
}
}
return obj;
}
//...
}
CloneJson method:
//...
using System.Text.Json;
using System.Text.Json.Serialization;
//...
public static class Tools
{
//...
public static JsonSerializerOptions jsonSettings = new()
{
ReferenceHandler = ReferenceHandler.IgnoreCycles,
WriteIndented = false,
AllowTrailingCommas = true
};
public static TOut CloneJson<TIn, TOut>(TIn source)
{
if (Object.ReferenceEquals(source, null))
return default(TOut);
return JDeserialize<TOut>(JSerialize(source));
}
//...
}
Instances of SqlDataReader can be indexed by both integers and strings (see below: reader[col]). So, I want the wrapping function to accept both List<string> and List<int> as an argument. I really don't want type-checking at runtime, though, and, as C# doesn't have something like type unions, I assume overloading is the best way to allow a function to accept different types without involving generics and at-runtime type-checking.
So... I can easily just copy the method with List<int> instead of List<string> and overload it that way, but I don't want (and shouldn't) duplicate code like this. There must be a better way.
public List<List<object>> Query(string query, List<string> relevantColumns)
{
var rows = new List<List<object>>();
using (SqlConnection connection =
new SqlConnection(this.ConnectionString))
{
var command = new SqlCommand(query, connection);
command.Connection.Open();
using (SqlDataReader reader = command.ExecuteReader())
{
while (reader.Read())
{
List<object> row = (from col
in relevantColumns
select reader[col]).ToList();
rows.Add(row);
}
}
}
return rows;
}
Instead of asking for columns in a List<object>, we can take this a step further and create a collection of the specific target type. For example, let's say you are querying an Employee table, and you need to fill an Employee object with columns for EmployeeNumber, FirstName, and LastName. The class looks like this:
public class Employee
{
public int EmployeeNumber {get; set;}
public string FirstName {get;set;}
public string LastName {get;set;}
}
And you have a query like this:
SELECT *
FROM Employee
WHERE HireDate<=current_timestamp and COALESCE(TermDate,'29991231') > current_timestamp
We can make a basic method like this:
private IEnumerable<T> GetData<T>(string SQL, Func<IDataRecord, T> translate, Action<SqlParameterCollection> addParams)
{
using var con = new SqlConnection(this.ConnectionString);
using var cmd = new SqlCommand(SQL, con);
if(addParams is object) addParams(cmd.Parameters);
con.Open();
using var rdr = cmd.ExecuteReader();
while(rdr.Read())
{
yield return translate(rdr);
}
rdr.Close();
}
Notice the method is private. Now we also add a public method like this:
public IEnumerable<Employee> GetEmployees()
{
string SQL = #"
SELECT EmployeeNumber, FirstName, LastName
FROM Employee
WHERE HireDate<=current_timestamp and COALESCE(TermDate,'29991231') > current_timestamp";
return GetData(SQL, row => new Employee {
EmployeeNumber = row["EmployeeNumber"],
FirstName = row["FirstName"],
LastName = row["LastName"]
}, null);
}
Now we have pushed the column mapping into the translate() lambda, and we get back typed results, instead of just an Object. Notice I also specified the column list in the SQL. You should be doing this with every query.
Let's look at one more example that uses the parameter lambda:
public Employee GetEmployeeByNumber(int EmployeeNumber)
{
string SQL = #"
SELECT EmployeeNumber, FirstName, LastName
FROM Employee
WHERE EmployeeNumber = #EmployeeNumber";
return GetData(SQL, row => new Employee {
EmployeeNumber = row["EmployeeNumber"],
FirstName = row["FirstName"],
LastName = row["LastName"]
}, p => {
p.Add("#EmployeeNumber", SqlDbType.Int).Value = EmployeeNumber;
}).FirstOrDefault();
}
We see how this also lets us safely include parameter data for the query. The original method would have practically forced you to write unsafe code.
You could have a third overload which takes a Func<SqlDataReader, List<object>>. Then call the other two overloads in terms of that overload. Something like...
public static class SqlUtilities
{
public static List<List<object>> Query(string query, List<string> relevantColumns)
{
return Query(query, reader => (from col in relevantColumns select reader[col]).ToList());
}
public static List<List<object>> Query(string query, List<int> relevantColumns)
{
return Query(query, reader => (from col in relevantColumns select reader[col]).ToList());
}
private static List<List<object>> Query(
string query,
Func<SqlDataReader, List<object>> selector)
{
var rows = new List<List<object>>();
using (SqlConnection connection =
new SqlConnection(this.ConnectionString))
{
var command = new SqlCommand(query, connection);
command.Connection.Open();
using (SqlDataReader reader = command.ExecuteReader())
{
while (reader.Read())
{
List<object> row = selector(reader);
rows.Add(row);
}
}
}
return rows;
}
}
In this specific case, however, my opinion is that a better way to solve it would be to rewrite query if that is an option. relevantColumns as a list of names or a list of ordinals is unnecessary if query is only selecting the relevant columns.
i am trying to convert a list to dataTable and then save it to the database , but i am facing a problem . I get an error that column Mapping does not match .
This is my List
public static class Program
{
static Logger _myLogger = LogManager.GetCurrentClassLogger();
public class Student
{
public int int { get; set; }
public string name { get; set; }
public string email { get; set; }
public string phoneNumber { get; set; }
public virtual ICollection<tblStudentCourses> tblStudentCourses { get; set; }
}
List<Student> student = new List<Student>();
This is the extensions that i am using
public static DataTable AsDataTable<T>(this IList<T> data)
{
DataTable dataTable = new DataTable(typeof(T).Name);
//Get all the properties
PropertyInfo[] Props = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (PropertyInfo prop in Props)
{
//Defining type of data column gives proper data table
var type = (prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>) ? Nullable.GetUnderlyingType(prop.PropertyType) : prop.PropertyType);
//Setting column names as Property names
dataTable.Columns.Add(prop.Name, type);
}
foreach (T item in data)
{
var values = new object[Props.Length];
for (int i = 0; i < Props.Length; i++)
{
//inserting property values to datatable rows
values[i] = Props[i].GetValue(item, null);
}
dataTable.Rows.Add(values);
}
//put a breakpoint here and check datatable
return dataTable;
}
This how i am calling the extension
using (var connection = new SqlConnection(ConfigurationManager.AppSettings["connectionString"]))
{
connection.Open();
SqlTransaction transaction = connection.BeginTransaction();
using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.Default, transaction))
{
bulkCopy.DestinationTableName = "dbo.Student";
bulkCopy.WriteToServer(student.AsDataTable());
connection.Close();
}
transaction.Commit();
}
The error :
The given ColumnMapping does not match up with any column in the source or destination
Use FastMember's ObjectReader to create an IDataReader on top of any collection, eg :
var student = new List<Student>();
...
using(var bcp = new SqlBulkCopy(connection))
using(var reader = ObjectReader.Create(student, "Id", "Name", "Email","PhoneNumber"))
{
bcp.DestinationTableName = "SomeTable";
bcp.WriteToServer(reader);
}
SqlBulkCopy can use either a DataTable or IDataReader. ObjectReader.Create creates an object that wraps any collection and exposes it through an IDataReader interface that can be used with SqlBulkCopy.
It's also possible to use Linq-to-Dataset's CopyToDataTable or MoreLinq's ToDataTable extension methods to create a DataTable from an IEnumerable. These will have to read the entire IEnumerable though and cache all data in the DataTable. This can be expensive if there are a lot of rows.
ObjectReader on the other hand doesn't need to cache anything
The error The given ColumnMapping does not match up with any column in the source or destination
happen usually for 3 causes:
You didn't provide any ColumnMappings, and there is more column in the source than in the destination.
You provided an invalid column name for the source.
You provided an invalid column name for the destination.
In your case, you didn't supply column mapping. Here is an online example similar to your scenario: https://dotnetfiddle.net/WaeUi9
To fix it:
Provide a ColumnMappings
For example: https://dotnetfiddle.net/Zry2tb
More information about this error can be found here: https://sqlbulkcopy-tutorial.net/columnmapping-does-not-match
If you are able to read data in data table then change your code like below
bulkCopy.DestinationTableName = "dbo.Student";
bulkCopy.ColumnMappings.Add("<list field name>", "<database field name>");
//Map all your column as above
bulkCopy.WriteToServer(dataTable);
I hope this works for your problem.
I am trying the below way to return the dynamic results using dapper and stored procedure. Am I doing it in correct way?
using (IDbConnection dbConnection = Connection)
{
dbConnection.Open();
var result = dbConnection.Query<dynamic>("LMSSP_GetSelectedTableData",
new
{
TableName = TableName,
LangaugeID = AppTenant.SelectedLanguageID,
UserID = AppTenant.UserID
}, commandType: CommandType.StoredProcedure).ToList();
if (result != null)
{
// Added just for checking the data
foreach (var item in (IDictionary<string, object>)result.FirstOrDefault())
{
string key = item.Key;
string value = item.Value.ToString();
}
}
}
What my stored procedure do is, I will pass any table name and based on that it will return the results/records.So, obviously my number of records, columns will be varied as per the table name passed.
To achieve this I have used dynamic keyword along with dapper.
So my question is how can I pass this data to view as a model and render the controls on the view as per the properties/column data type. Can I get the data type of column OR PropertyInfo?
But, when dapper retrieves the records from database it returns as dapper row type?
Using same SP to fetch data from different table would be confusing (not good design). However to solve your problem technically, you can create model having list of control information. Example of control information
public class ControlInformation
{
public string Name { get; set; }
public dynamic Value { get; set; }
public string ControlType { get; set; }
// Applicable for drop down or multi select
public string AllValues { get; set; }
}
Model will have list of ControlInformations
public List<ControlInformation> ControlInformations { get; set; }
View will render the controls (partial view based on control type) Ex: very basic case to render different view for int and another view for rest. I have 2 partial views "IntCtrl" and "StringCtrl".
#foreach (var item in Model.ControlInformations)
{
if (#item.ControlType == "System.Int32")
{
Html.RenderPartial("IntCtrl", item);
}
else
{
Html.RenderPartial("StringCtrl", item);
}
}
Hope this help.
Here we are calling method which returns Datatable :
public DataTable GetMTDReport(bool isDepot, int userId)
{
using (IDbConnection _connection = DapperConnection)
{
var parameters = new DynamicParameters();
parameters.Add("#IsDepot", isDepot);
parameters.Add("#userId", userId);
var res = this.ExecuteSP<dynamic>(SPNames.SSP_MTDReport, parameters);
return ToDataTable(res);
}
}
In this we can call stored procedures by calling our custom method "ExecuteSP" :
public virtual IEnumerable<TEntity> ExecuteSP<TEntity>(string spName, object parameters = null)
{
using (IDbConnection _connection = DapperConnection)
{
_connection.Open();
return _connection.Query<TEntity>(spName, parameters, commandTimeout:0 , commandType: CommandType.StoredProcedure);
}
}
and here is "DapperConnection" method to connect the database:
You can give connection string with key ["MainConnection"]
public class DataConnection
{
public IDbConnection DapperConnection
{
get
{
return new SqlConnection(ConfigurationManager.ConnectionStrings["MainConnection"].ToString());
}
}
}
And at last we call "ToDataTable" method to change our response in datatable . We will receive response in DapperRow from the database because we passsed dynamic type in stored procedure.
public DataTable ToDataTable(IEnumerable<dynamic> items)
{
if (items == null) return null;
var data = items.ToArray();
if (data.Length == 0) return null;
var dt = new DataTable();
foreach (var pair in ((IDictionary<string, object>)data[0]))
{
dt.Columns.Add(pair.Key, (pair.Value ?? string.Empty).GetType());
}
foreach (var d in data)
{
dt.Rows.Add(((IDictionary<string, object>)d).Values.ToArray());
}
return dt;
}
I am getting following error when I call the Select function:
The incoming tabular data stream (TDS) remote procedure call (RPC)
protocol stream is incorrect. Table-valued parameter 3
("#SearchTableVar"), row 0, column 0: Data type 0xF3 (user-defined
table type) has a non-zero length database name specified. Database
name is not allowed with a table-valued parameter, only schema name
and type name are valid.
C# code
//DTO
public class SP_SearchEntity_Result
{
public string ID { get; set; }
public string NAME { get; set; }
}
//Businesslogic
public IQueryable Select(int PageIndex, int PageSize, List<KeyValuePair<string, string>> SearchBy, List<KeyValuePair<string, System.Data.SqlClient.SortOrder>> SortBy)
{
SqlDatabase obj = (SqlDatabase)DatabaseFactory.CreateDatabase();//System.Configuration.ConfigurationManager.ConnectionStrings["MySqlServer"].ConnectionString
return obj.ExecuteSprocAccessor<SP_SearchEntity_Result>("SP_SearchEntity", PageIndex, PageSize, SearchBy.ToDataTable(), SortBy.ToDataTable()).AsQueryable<SP_SearchEntity_Result>();
}
//Extension methods
public static DataTable ToDataTable(this List<KeyValuePair<string, string>> source)
{
DataTable dataTable = new DataTable("Test");
dataTable.Columns.Add("KEY",typeof(System.String));
dataTable.Columns.Add("VALUE", typeof(System.String));
foreach (KeyValuePair<string, string> data in source)
{
var dr = dataTable.NewRow();
dr["KEY"] = data.Key;
dr["VALUE"] = data.Value;
dataTable.Rows.Add(dr);
}
return dataTable;
}
public static DataTable ToDataTable(this List<KeyValuePair<string, System.Data.SqlClient.SortOrder>> source)
{
DataTable dataTable = new DataTable("Test");
dataTable.Columns.Add("KEY", typeof(System.String));
dataTable.Columns.Add("VALUE", typeof(System.String));
foreach (KeyValuePair<string, System.Data.SqlClient.SortOrder> data in source)
{
var dr = dataTable.NewRow();
dr["KEY"] = data.Key;
dr["VALUE"] = data.Value == System.Data.SqlClient.SortOrder.Ascending ? "ASC" : "DESC";
dataTable.Rows.Add(dr);
}
return dataTable;
}
The stored procedure returns two tables in result
SQL proc definition
CREATE TYPE KeyValueTableVariable AS TABLE
(
[KEY] NVARCHAR(800),
[VALUE] NVARCHAR(800)
)
GO
CREATE PROCEDURE SP_SearchEntity
#PageIndex INT=NULL,
#PageSize INT=NULL,
#SearchTableVar dbo.KeyValueTableVariable READONLY,
#SortTableVar dbo.KeyValueTableVariable READONLY
AS
BEGIN
/*Bla bla bla*/
SELECT '1' as [ID], 'Nitin' as [NAME]
SELECT '1' as [COUNT]
END
There are a number of requirements/limitations for passing Table Valued parameters to SQL Server. See e.g. the example under "Passing a Table-Valued Parameter to a Stored Procedure":
The code then defines a SqlCommand, setting the CommandType property to StoredProcedure. The SqlParameter is populated by using the AddWithValue method and the SqlDbType is set to Structured.
And note that just using AddWithValue is insufficient - the SqlDbType has to be changed to Structured.
I believe that the ExecuteSprocAccessor method isn't performing this change (or possibly, as in some other examples, where the TypeName has to be set to the name of the table type). I can't chase this all through the enterprise library source code, but since I can't find the word "Structured" anywhere in the solution, that's what leads me to this conclusion.
So, if you want to use TVPs, I think you have to abandon the Enterprise Library and write the data access code yourself using SqlClient types. (Since you're using TVPs, you're already abandoning the possibility of switching to a different RDBMS anyway).
I find that the xml datatype for stored proc parameters is easier to use. Rather than casting the parameters to DataTables, you would cast them to XML for the following example:
CREATE PROCEDURE SP_SearchEntity
#PageIndex INT=NULL,
#PageSize INT=NULL,
#SearchTableVar xml=NULL,
#SortTableVar xml=NULL
AS
BEGIN
/*Bla bla bla*/
SELECT '1' as [ID], 'Nitin' as [NAME]
SELECT '1' as [COUNT]
END
Here's a sample of the KeyValuePair and a query, after it is serialized as XML:
declare #sampleXml xml = '
<ArrayOfKeyValuePairOfstringstring xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/System.Collections.Generic">
<KeyValuePairOfstringstring>
<key>foo</key>
<value>bar</value>
</KeyValuePairOfstringstring>
<KeyValuePairOfstringstring>
<key>hello</key>
<value>world</value>
</KeyValuePairOfstringstring>
</ArrayOfKeyValuePairOfstringstring>'
select
Node.Elem.value('*:key[1]', 'nvarchar(800)') as [Key]
,Node.Elem.value('*:value[1]', 'nvarchar(800)') as Value
from #sampleXml.nodes(N'/*:ArrayOfKeyValuePairOfstringstring/*:KeyValuePairOfstringstring') Node(Elem)
go
and a XML Serializer:
// from Plinqo: http://www.codesmithtools.com/product/frameworks
public static string ToXml<T>(this T item)
{
var settings = new XmlWriterSettings();
settings.Indent = true;
settings.OmitXmlDeclaration = true;
var sb = new System.Text.StringBuilder();
using (var writer = XmlWriter.Create(sb, settings))
{
var serializer = new DataContractSerializer(typeof(T));
serializer.WriteObject(writer, item);
}
return sb.ToString();
}
EDIT : Returning Multiple Result Sets and Binding them to objects
I'll show you how to do that, but I'm not sure it's what you want to do, based on your mock SQL. If you are really just returning a count of the objects that were returned, you can count your results after they are IQueryable.
First you'll need a way of binding the objects, which you can get by extending MVC. These model binders expect your query to return column names that match your model properties.
using System;
using System.Collections.Generic;
using System.Web.Mvc;
public partial class ModelBinder
{
/// <summary>
/// Binds the values of an Dictionary to a POCO model
/// </summary>
public virtual T BindModel<T>(IDictionary<string, object> dictionary)
{
DictionaryValueProvider<object> _dictionaryValueProvider = new DictionaryValueProvider<object>(dictionary, null);
return BindModel<T>(_dictionaryValueProvider);
}
/// <summary>
/// Binds the values of an IValueProvider collection to a POCO model
/// </summary>
public virtual T BindModel<T>(IValueProvider dictionary)
{
Type _modelType = typeof(T);
var _modelConstructor = _modelType.GetConstructor(new Type[] { });
object[] _params = new object[] { };
string _modelName = _modelType.Name;
ModelMetadata _modelMetaData = ModelMetadataProviders.Current.GetMetadataForType(() => _modelConstructor.Invoke(_params), _modelType);
var _bindingContext = new ModelBindingContext() { ModelName = _modelName, ValueProvider = dictionary, ModelMetadata = _modelMetaData };
DefaultModelBinder _binder = new DefaultModelBinder();
ControllerContext _controllerContext = new ControllerContext();
T _object = (T)_binder.BindModel(_controllerContext, _bindingContext);
return _object;
}
}
Example conventions for model binding:
public partial class Person
{
public int Id { get; set; }
public string Name { get; set; }
public Project Project { get; set; }
public List<Person> Friends { get; set; }
}
public partial class Project
{
public int Id { get; set; }
public string Name { get; set; }
}
select
1 as [Id]
, 'NitinJs' as [Name]
, 5 as [Project.Id]
, 'Model Binding' as [Project.Name]
, 2 as [Friends[0]].Id]
, 'John' as [Friends[0]].Name]
, 3 as [Friends[1]].Id]
, 'Jane' as [Friends[1]].Name]
Now, you need a method that will read your Data results and bind them to a model:
/// <summary>
/// Reads a record from a SqlDataReader, binds it to a model, and adds the object to the results parameter
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="reader"></param>
/// <param name="modelName"></param>
/// <param name="results"></param>
private void ReadAs<T>(SqlDataReader reader, string modelName, List<T> results, string commandText)
{
Dictionary<string, object> _result = new Dictionary<string, object>();
for (int i = 0; i < reader.FieldCount; i++)
{
string _key = modelName + "." + reader.GetName(i);
object _value = reader.GetValue(i);
if (_result.Keys.Contains(_key)) // Dictionaries may not have more than one instance of a key, but a query can return the same column twice
{ // Since we are returning a strong type, we ignore columns that exist more than once.
throw new Exception("The following query is returning more than one field with the same key, " + _key + ": " + commandText); // command.CommandText
}
_result.Add(_key, _value);
}
T _object = new ModelBinder().BindModel<T>(_result);
if (_object != null)
{
results.Add((T)_object);
}
}
Next, you need a way of getting an open connection to your Database (note: you'll probaly want to grab _dbConnectionString from your config):
public SqlConnection GetOpenConnection()
{
_sqlConnection = new SqlConnection(_dbConnectionString);
_sqlConnection.Open();
return _sqlConnection;
}
Finally, you need to connect to your database to get your result sets:
/// <summary>
/// Executes a SqlCommand that expects four result sets and binds the results to the given models
/// </summary>
/// <typeparam name="T1">Type: the type of object for the first result set</typeparam>
/// <typeparam name="T2">Type: the type of object for the second result set</typeparam>
/// <returns>List of Type T: the results in a collection</returns>
public void ExecuteAs<T1, T2>(SqlCommand command, List<T1> output1, List<T2> output2)
{
string _modelName1 = typeof(T1).Name;
string _modelName2 = typeof(T2).Name;
string _commandText = command.CommandText;
using (SqlConnection connection = GetOpenConnection())
{
using (command)
{
command.Connection = connection;
command.CommandTimeout = _defaultCommandTimeout;
using (SqlDataReader reader = command.ExecuteReader())
{
while (reader.Read()) // Call Read before accessing data.
{
ReadAs<T1>(reader, _modelName1, output1, _commandText);
}
reader.NextResult();
while (reader.Read()) // Call Read before accessing data.
{
ReadAs<T2>(reader, _modelName2, output2, _commandText);
}
} // end using reader
} // end using command
} // end using connection
}
Then your select method would look more like this:
public void SelectInto<SP_SearchEntity_Result, int>(int PageIndex, int PageSize, List<KeyValuePair<string, string>> SearchBy, List<KeyValuePair<string, System.Data.SqlClient.SortOrder>> SortBy, List<<SP_SearchEntity_Result> result1, List<int> result2)
{
SqlCommand command = new SqlCommand("SP_SearchEntity");
command.CommandType = System.Data.CommandType.StoredProcedure;
command.Parameters.Add("PageIndex", SqlDbType.Int).Value = PageIndex;
command.Parameters.Add("SearchTableVar", SqlDbType.Xml).Value = SearchBy.ToXml();
List<KeyValuePair<string, string>> SortByCastToString = // modify your ToDataTable method so you can pass a List<KeyValuePair<string, string>> for SortBy
command.Parameters.Add("SortTableVar", SqlDbType.Xml).Value = SortByCastToString.ToXml();
ExecuteAs<SP_SearchEntity_Result, int>(command, result1, result2);
}
public void SomeCallingMethod()
{
List<SP_SearchEntity_Result> _results = new List<SP_SearchEntity_Result>{};
List<int> _counts = new List<int>{};
// ...
// setup your SearchBy and SortBy
// ...
SelectInto<SP_SearchEntity_Result, int>(1, 20, SearchBy, SortBy, _results, _counts);
}
TVPs as parameters to a stored procedure work for me just fine using Enterprise Library Data Access Application Block v6.0.1304. My C# code looks like this:
public static DataSet SomeHelperMethod(DataTable tvp1, DataTable tvp2)
{
DbCommand cmd = <SqlDatabase>.GetStoredProcCommand("StoredProcName");
SqlParameter p1 = new SqlParameter("#p1", tvp1);
p1.SqlDbType = SqlDbType.Structured;
cmd.Parameters.Add(p1);
SqlParameter p2= new SqlParameter("#p2", tvp2);
p2.SqlDbType = SqlDbType.Structured;
cmd.Parameters.Add(p2);
return <SqlDatabase>.ExecuteDataSet(cmd);
}