Using Dapper in C# MVVM and SQL Server 2019/Visual Studios Pro 2019 - dapper

I am writing an application using C#, WPF, MVVM and SQL Server, and I am using Dapper as an ORM. Below is a snippet of the sample code I am trying to make work:
public List<MAKE THIS GENERIC> ExecuteSQLSPROC(string SPROCName, SqlParameter[] parameters, IEnumerable<MAKE THIS GENERIC> model)
{
using (IDbConnection connection = new System.Data.SqlClient.SqlConnection(DBHelper.CNNVal("MyDatabaseName")))
{
System.Data.SqlClient.SqlCommand command = new System.Data.SqlClient.SqlCommand();
command.Connection = (SqlConnection)connection;
command.CommandText = SPROCName;
DynamicParameters parms = new DynamicParameters();
for (int i = 0; i < parameters.Length; i++)
{
var parmname = parameters[i].ParameterName;
var parmvalue = parameters[i].Value;
// I KNOW I can put DIRECTION as a parameter variable but right now I don't want to!!
parms.Add(parmname, parmvalue);
}
var output = connection.Query<Make THIS GENERIC>(SPROCName, parms, commandType: CommandType.StoredProcedure).ToList();
return output; // return the recordset to the caller - a List<> of one object/model is acceptable - NOT POPULATING EXCEL spreadsheet with
// the results
}
}
I want to call this piece of code (which does not compile right now for obvious reasons) by supplying any ViewModel and be able to execute this code as if the ViewModel is generic within the routine. I need to know how to correctly code the above and also make the call to this routine with generic ViewModels (probably using a Lambda expression of some sort).
Basically, I want to be able to say "execute this SPROC, with these dynamic parameters and map the output to a ViewModel that is generic and return the results to the caller. Next time you're called it will be same logic, different SPROC, different parameters and ViewModel". I do NOT want extraneous comments that DO NOT directly answer this question. I know that I can add the parameter direction in a call the SQL SPROCs; but that knowledge is irrelevant to this question I am asking!

You can put your method in a generic class if you have other methods:
public class DbStuff<T>
{
public IList<T> ExecuteSQLSPROC(string SPROCName, SqlParameter[] parameters, IEnumerable<T> model)
{
using (IDbConnection connection = new System.Data.SqlClient.SqlConnection(DBHelper.CNNVal("MyDatabaseName")))
{
System.Data.SqlClient.SqlCommand command = new System.Data.SqlClient.SqlCommand();
command.Connection = (SqlConnection)connection;
command.CommandText = SPROCName;
DynamicParameters parms = new DynamicParameters();
for (int i = 0; i < parameters.Length; i++)
{
var parmname = parameters[i].ParameterName;
var parmvalue = parameters[i].Value;
// I KNOW I can put DIRECTION as a parameter variable but right now I don't want to!!
parms.Add(parmname, parmvalue);
}
var output = connection.Query<T>(SPROCName, parms, commandType: CommandType.StoredProcedure).ToList();
return output; // return the recordset to the caller - a List<> of one object/model is acceptable - NOT POPULATING EXCEL spreadsheet with
// the results
}
}
}
Then call it via:
var stuff = new DbStuff<MyViewModel>();
var results = stuff.ExecuteSQLSPROC("", blah, blah);
Or just make the method generic:
public IList<T> ExecuteSQLSPROC<T>(string SPROCName, SqlParameter[] parameters, IEnumerable<T> model)

Related

How to call stored procedure from EntityFramework 6 with 'hierarchyid' parameter

I am developing a service using WebApi2 and EntityFramework6.
I have a legacy SQLServer DB that my service must work with.
That DB is using heavily the 'hierarchyid' data type and this type is used internally in DB's stored procedures.
Seems like EF6 is not supporting 'hierarchyid' data type, so i used this fork that adds support for 'hierarchyid'.
While the retrieval from the DB is working great with the 'hierarchyid' type, my problem is with the Stored Procedures that need a 'hierarchyid' as a parameter.
The stored procedure looks like this:
CREATE PROCEDURE [dbo].[GetSomethingByNodeId]
(
#startingRoot HIERARCHYID
,#return HIERARCHYID OUTPUT
)
My client code for invoking this stored procedure looks like this:
var param1 = new SqlParameter("#startingRoot", new HierarchyId("/"));
var param2 = new SqlParameter{ ParameterName = "#return", Value = 0, Direction = ParameterDirection.Output };
var obj = context.Database.SqlQuery<HierarchyId>("GetSomethingByNodeId" #startingRoot, #return out", param1, param2).ToList();
But unfortunately calling this query throws an exception that says:
An unhandled exception of type 'System.ArgumentException' occurred in EntityFramework.SqlServer.dll
Additional information: No mapping exists from object type System.Data.Entity.Hierarchy.HierarchyId to a known managed provider native type.
Any ideas on how i can make this work?
Unfortunately, MetaType.GetMetaTypeFromValue does not allow to add types (all supported types are hardcoded).
I think you can accomplish your goal with nvarchar parameters and conversions.
In your C# code:
var param1 = new SqlParameter("#startingRoot", "/1/");
var param2 = new SqlParameter { ParameterName = "#return", Value = "", Size = 1000, Direction = ParameterDirection.Output };
var ids = context.Database.SqlQuery<HierarchyId>("GetSomethingByNodeId #startingRoot, #return out", param1, param2).ToList();
var returnedId = new HierarchyId(param2.Value.ToString());
In your procedure (I wrote some test code inside):
CREATE PROCEDURE [dbo].[GetSomethingByNodeId]
(
#startingRoot nvarchar(max), #return nvarchar(max) OUTPUT
)
as
declare #hid hierarchyid = hierarchyid::Parse('/1/')
select #return = #hid.ToString()
declare #root hierarchyid = hierarchyid::Parse(#startingRoot)
select #root as field
Also, you can try to use Microsoft.SqlServer.Types and SqlHierarchyId type like this:
var sqlHierarchyId = SqlHierarchyId.Parse("/");
var param1 = new SqlParameter("#startingRoot", sqlHierarchyId) { UdtTypeName = "HierarchyId" };
But, I think, this is wrong direction.
Oleg's answer is correct, hierarchyid is still not integrated to the EF very well, and you should operate with strings in .net. Here is one more approach which was used from the first days of HierarchyId datatype:
Stored Procedure:
CREATE PROCEDURE GetSomethingByNodeId
#startingRoot hierarchyid, -- you don't need to use nvarchar here. String which will come from the application will be converted to hierarchyId implicitly
#return nvarchar(500) OUTPUT
AS
BEGIN
SELECT #return = #startingRoot.GetAncestor(1).ToString();
END
In an application you are adding a partial class for your EF data context with the SP call using plain old ADO.NET. Probably you will write this other way or use Dapper instead, but the main idea here is passing parameter as string to SQL Server, and it will convert to the HierarchyId implicitly:
public partial class TestEntities
{
public string GetSomethingByNodeId(string startingRoot)
{
using (var connection = new SqlConnection(this.Database.Connection.ConnectionString))
{
var command = new SqlCommand("GetSomethingByNodeId", connection);
command.CommandType = CommandType.StoredProcedure;
command.Parameters.AddWithValue("#startingRoot", startingRoot);
var outParameter = new SqlParameter("#return", SqlDbType.NVarChar, 500);
outParameter.Direction = ParameterDirection.Output;
command.Parameters.Add(outParameter);
connection.Open();
command.ExecuteNonQuery();
return outParameter.Value.ToString();
}
}
}
Then call this method as any other stored procedure using your EF context:
using (var context = new TestEntities())
{
var s = context.GetSomethingByNodeId("/1/1.3/");
}
UPD: here is how the extension method for legacy HierarchyId procedure call will look like with Dapper (as for me it looks much better than plain ADO.NET):
public string GetSomethingByNodeId(string startingRoot)
{
using (var connection = new SqlConnection(this.Database.Connection.ConnectionString))
{
var parameters = new DynamicParameters();
parameters.Add("startingRoot", startingRoot);
parameters.Add("return", null, DbType.String, ParameterDirection.Output, 500);
connection.Open();
connection.Execute("GetSomethingByNodeId", parameters, commandType: CommandType.StoredProcedure);
return parameters.Get<string>("return");
}
}

How do I use Activator.CreateInstance to do the following please

I use a similar style of code many times in my application to read in records from a database
WorkoutResultsRecord is inherited from a class called BaseRecord. One of the base constructors takes a IDataReader parameter to read fields into the class (as seen below).
What I want to do is define a generic function that will do the following for any/all of my 60+ xxxRecord type classes ie I can pass in a type as a parameter and it will return the correct type of objects as a typed List. Is it possible with Activator class? I've not used it before and my results just wouldn't compile
protected List<WorkoutResultsRecord> ReadRecordList(string sql,
IDbConnection connection)
{
var results = new List<WorkoutResultsRecord>();
using (IDbCommand command = GetCommand(sql, connection))
using (IDataReader reader = command.ExecuteReader())
while (reader.Read())
results.Add(new WorkoutResultsRecord(reader));
return results;
}
My really bad, failed attempt :(
private void sfsdf(Type type)
{
List<typeof(type)> lst = new List<type>();
Activator.CreateInstance(List<typeof(type)>);
}// function
this should work:
private void sfsdf(Type type)
{
Type genericType = typeof(List<>).MakeGenericType(type);
System.Collections.IList theList = (IList) Activator.CreateInstance(genericType);
// do whatever you like with this list...
}
Note: as the type is known at runtime only, it's not possible for you to declare a List when you write the code, so rather, use IList interface instead, but the created object theList should be of the expected type...
The following is the full function with all the generics done as I wanted. This will save a lot of typing!! thanks very much
allResults = (List<WorkoutResultsRecord>)FillList(typeof(WorkoutResultsRecord),
sql, connection, new KVP("FROMDATE", fromUtf.Date),
new KVP("TODATE", endDate.AddDays(1).Date));
IList FillList(Type type,string sql,IDbConnection connection,
params KVP[] parameters)
{
Type genericType = typeof(List<>).MakeGenericType(type);
IList results = (IList)Activator.CreateInstance(genericType);
using (var command= Command(sql,connection))
{
foreach(KVP parameter in parameters)
CreateParam(command,parameter.Key,parameter.Value);
using (IDataReader reader = command.ExecuteReader())
while (reader.Read())
results.Add(Activator.CreateInstance(type,reader));
}
return results;
}

Winforms ReportViewer passing null for parameters even when the parameters are set

Even though I set the parameters in the code, I keep getting that a parameter must be set error. I have run profiler to see what is being passed to SSRS, and profiler indicates that parameters = null. Yet all of them are set in code. Anyone have any ideas? The code is as follows:
string strReportPath;
Microsoft.Reporting.WinForms.ReportParameter prmFranchiseOID;
Microsoft.Reporting.WinForms.ReportParameter prmSchoolOID;
Microsoft.Reporting.WinForms.ReportParameter prmRoomOID;
Microsoft.Reporting.WinForms.ReportParameter prmOrderDate;
Microsoft.Reporting.WinForms.ReportParameter prmLanguage;
Microsoft.Reporting.WinForms.ReportParameter prmContrast;
List<Microsoft.Reporting.WinForms.ReportParameter> prms = new List<ReportParameter>();
byte[] pdf = null;
try
{
prmFranchiseOID = new Microsoft.Reporting.WinForms.ReportParameter("FranchiseOID", "8D126AA2-2E5C-4B2B-8D19-167027F8C7D8");
prmSchoolOID = new Microsoft.Reporting.WinForms.ReportParameter("SchoolOID", "96FEE335-0CB9-413A-9DDC-78F8C67770C4");
prmRoomOID = new Microsoft.Reporting.WinForms.ReportParameter("RoomOID", "null");
prmOrderDate = new Microsoft.Reporting.WinForms.ReportParameter("OrderDate", DateTime.Now.AddDays(1).Date.ToString());
prmLanguage = new Microsoft.Reporting.WinForms.ReportParameter("Language", "en-CA");
prmContrast = new Microsoft.Reporting.WinForms.ReportParameter("Contrast", "true");
prms.Add(prmFranchiseOID);
prms.Add(prmSchoolOID);
prms.Add(prmRoomOID);
prms.Add(prmOrderDate);
prms.Add(prmLanguage);
prms.Add(prmContrast);
// Note: For Account Holder users, their specified report folder is "/LunchLady/User".
strReportPath = "/LunchLady/Franchise/" + urlReportName;
try
{
rvReport.ServerReport.ReportServerUrl = new System.Uri("https://testsql.thelunchlady.ca/ReportServer");
rvReport.ServerReport.ReportPath = strReportPath;
rvReport.ServerReport.SetParameters(prms);
string ReportType = "PDF";
pdf = rvReport.ServerReport.Render(ReportType);
Thanks
Having done extensive programming on the SSRS controls in ASP.NET, one thing that I've found which may or may not be relevant for WinForms is that each SSRS parameter is actually a collection in itself (due to parameters being able to be multi-select).
So what worked for us is that the collection (prms in your case) was of type
List<IEnumerable<ReportViewer.ReportParameter>> prms
Also when adding parameters using the SetParameters function we added them one at a time:
for (int i = 0; i < prms.Count; i++)
{
rvReport.ServerReport.SetParameters(prms[i]);
}
Again, this is what worked for us in ASP.NET, could be something for you to try.

Does dapper support .net dataset

in my opinion for dapper.query object there is a datareader, for dapper.Execute there is a ExectureNonQuery object. Correct me if i am wrong .
Can we use dapper for dataset which returns multiple tables?
No, there is not any built in support for DataSet, primarily because it seems largely redundant, but also because that isn't what dapper targets. But that doesn't mean it doesn't include an API for handling a query that selects multiple results; see QueryMultiple:
using (var multi = conn.QueryMultiple(sql, args))
{
var ids = multi.Read<int>().ToList();
var customers = multi.Read<Customer>().ToList();
dynamic someOtherRow = multi.Read().Single();
int qty = someOtherRow.Quantity, price = someOtherRow.Price;
}
Note that this API is forwards only (due to the nature of IDataReader etc) - basically, each Read / Read<T> etc maps to the next result grid in turn.
I might be late here but this is how I am doing the conversion of the IDataReader to a DataSet. Dapper returns a IDataReader when we use the ExecuteReaderAsync method. More information on this addition can be found here and here.
This is my attempt on this:
public async Task<DataSet> GetUserInformationOnUserId(int UserId)
{
var storedprocedure = "usp_getUserInformation";
var param = new DynamicParameters();
param.Add("#userId", UserId);
var list = await SqlMapper.ExecuteReaderAsync(_connectionFactory.GetEpaperDBConnection, storedprocedure, param, commandType: CommandType.StoredProcedure);
var dataset = ConvertDataReaderToDataSet(list);
return dataset;
}
And the ConvertDataReaderToDataSet will take in the IDataReader, you can use this method to convert the IReader to Dataset:
public DataSet ConvertDataReaderToDataSet(IDataReader data)
{
DataSet ds = new DataSet();
int i = 0;
while (!data.IsClosed)
{
ds.Tables.Add("Table" + (i + 1));
ds.EnforceConstraints = false;
ds.Tables[i].Load(data);
i++;
}
return ds;
}

Is it possible to use `SqlDbType.Structured` to pass Table-Valued Parameters in NHibernate?

I want to pass a collection of ids to a stored procedure that will be mapped using NHibernate. This technique was introduced in Sql Server 2008 ( more info here => Table-Valued Parameters ). I just don't want to pass multiple ids within an nvarchar parameter and then chop its value on the SQL Server side.
My first, ad hoc, idea was to implement my own IType.
public class Sql2008Structured : IType {
private static readonly SqlType[] x = new[] { new SqlType(DbType.Object) };
public SqlType[] SqlTypes(NHibernate.Engine.IMapping mapping) {
return x;
}
public bool IsCollectionType {
get { return true; }
}
public int GetColumnSpan(NHibernate.Engine.IMapping mapping) {
return 1;
}
public void NullSafeSet(DbCommand st, object value, int index, NHibernate.Engine.ISessionImplementor session) {
var s = st as SqlCommand;
if (s != null) {
s.Parameters[index].SqlDbType = SqlDbType.Structured;
s.Parameters[index].TypeName = "IntTable";
s.Parameters[index].Value = value;
}
else {
throw new NotImplementedException();
}
}
#region IType Members...
#region ICacheAssembler Members...
}
No more methods are implemented; a throw new NotImplementedException(); is in all the rest. Next, I created a simple extension for IQuery.
public static class StructuredExtensions {
private static readonly Sql2008Structured structured = new Sql2008Structured();
public static IQuery SetStructured(this IQuery query, string name, DataTable dt) {
return query.SetParameter(name, dt, structured);
}
}
Typical usage for me is
DataTable dt = ...;
ISession s = ...;
var l = s.CreateSQLQuery("EXEC some_sp #id = :id, #par1 = :par1")
.SetStructured("id", dt)
.SetParameter("par1", ...)
.SetResultTransformer(Transformers.AliasToBean<SomeEntity>())
.List<SomeEntity>();
Ok, but what is an "IntTable"? It's the name of SQL type created to pass table value arguments.
CREATE TYPE IntTable AS TABLE
(
ID INT
);
And some_sp could be like
CREATE PROCEDURE some_sp
#id IntTable READONLY,
#par1 ...
AS
BEGIN
...
END
It only works with Sql Server 2008 of course and in this particular implementation with a single-column DataTable.
var dt = new DataTable();
dt.Columns.Add("ID", typeof(int));
It's POC only, not a complete solution, but it works and might be useful when customized. If someone knows a better/shorter solution let us know.
A simpler solution than the accepted answer would be to use ADO.NET. NHibernate allows users to enlist IDbCommands into NHibernate transactions.
DataTable myIntsDataTable = new DataTable();
myIntsDataTable.Columns.Add("ID", typeof(int));
// ... Add rows to DataTable
ISession session = sessionFactory.GetSession();
using(ITransaction transaction = session.BeginTransaction())
{
IDbCommand command = new SqlCommand("StoredProcedureName");
command.Connection = session.Connection;
command.CommandType = CommandType.StoredProcedure;
var parameter = new SqlParameter();
parameter.ParameterName = "IntTable";
parameter.SqlDbType = SqlDbType.Structured;
parameter.Value = myIntsDataTable;
command.Parameters.Add(parameter);
session.Transaction.Enlist(command);
command.ExecuteNonQuery();
}
For my case, my stored procedure needs to be called in the middle of an open transaction.
If there is an open transaction, this code works because it is automatically reusing the existing transaction of the NHibernate session:
NHibernateSession.GetNamedQuery("SaveStoredProc")
.SetInt64("spData", 500)
.ExecuteUpdate();
However, for my new Stored Procedure, the parameter is not as simple as an Int64. It's a table-valued-parameter (User Defined Table Type)
My problem is that I cannot find the proper Set function.
I tried SetParameter("spData", tvpObj), but it's returning this error:
Could not determine a type for class: …
Anyways, after some trial and error, this approach below seems to work.
The Enlist() function is the key in this approach. It basically tells the SQLCommand to use the existing transaction. Without it, there will be an error saying
ExecuteNonQuery requires the command to have a transaction when the
connection assigned to the command is in a pending local transaction…
using (SqlCommand cmd = NHibernateSession.Connection.CreateCommand() as SqlCommand)
{
cmd.CommandText = "MyStoredProc";
NHibernateSession.Transaction.Enlist(cmd); // Because there is a pending transaction
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add(new SqlParameter("#wiData", SqlDbType.Structured) { Value = wiSnSqlList });
int affected = cmd.ExecuteNonQuery();
}
Since I am using the SqlParameter class with this approach, SqlDbType.Structured is available.
This is the function where wiSnList gets assigned:
private IEnumerable<SqlDataRecord> TransformWiSnListToSql(IList<SHWorkInstructionSnapshot> wiSnList)
{
if (wiSnList == null)
{
yield break;
}
var schema = new[]
{
new SqlMetaData("OriginalId", SqlDbType.BigInt), //0
new SqlMetaData("ReportId", SqlDbType.BigInt), //1
new SqlMetaData("Description", SqlDbType.DateTime), //2
};
SqlDataRecord row = new SqlDataRecord(schema);
foreach (var wi in wiSnList)
{
row.SetSqlInt64(0, wi.OriginalId);
row.SetSqlInt64(1, wi.ShiftHandoverReportId);
if (wi.Description == null)
{
row.SetDBNull(2);
}
else
{
row.SetSqlString(2, wi.Description);
}
yield return row;
}
}
You can pass collections of values without the hassle.
Example:
var ids = new[] {1, 2, 3};
var query = session.CreateQuery("from Foo where id in (:ids)");
query.SetParameterList("ids", ids);
NHibernate will create a parameter for each element.

Resources