I love using Dapper for my ORM needs but I know there must be a better way to insert/update my sql server database using a stored procedure and strongly typed Lists.
For example:
I have a class Song:
public class Song
{
public int Id { get; set; }
public string title { get; set; }
public string genre { get; set; }
}
and somebody submits a List of songs:
List<Song> songs = new List<Song> {
new Song { Id = 1, title = "Song 1" , genre="rock"},
new Song { Id = 2, title = "Song 2" , genre="disco"}};
I want to update the database using my stored procedure which either inserts the new song or updates it if the song already exists. My stored procedure has two output parameters:
#success_added int = 0 and #success_updated int = 0
my sproc is as follows:
ALTER PROCEDURE [dbo].[UpdateSong]
#Id int = 0,
#title varchar(25) = NULL,
#genre varchar(25) = NULL,
#success_updated bit = 0 OUTPUT,
#success_added bit = 0 OUTPUT
AS
IF NOT EXISTS (SELECT Id FROM Songs WHERE Id = #Id)
BEGIN
INSERT INTO Songs
(
-- Id created by db
title,
genre
)
VALUES
(
#title,
#genre
)
SELECT #Success_Added = 1, #Success_Updated = 0
END
ELSE -- song already exists
BEGIN
UPDATE Songs
SET
title = #title,
#genre = #genre
WHERE Id = #Id
SELECT #Success_Added = 0, #Success_Updated = 1
END
RETURN
I know this works:
dbConn.Open();
DynamicParameters p = new DynamicParameters();
foreach (var song in songs)
{
p.Add("#Id", song.Id);
p.Add("#title", song.title);
p.Add("#genre", song.genre);
p.Add("#success_updated", dbType: DbType.Boolean, direction: ParameterDirection.Output);
p.Add("#success_added", dbType: DbType.Boolean, direction: ParameterDirection.Output);
dbConn.Execute("Test_UpdateSong", p, commandType: CommandType.StoredProcedure);
Console.WriteLine("#success_added: " + p.Get<Boolean>("#success_added"));
Console.WriteLine("#success_updated: " + p.Get<Boolean>("#success_updated"));
}
dbConn.Close();
But that requires manually converting each Song property to a anonymous type DynamicParameter. I'd rather simply do this:
dbConn.Open();
foreach (var song in songs)
{
var updateResult = dbConn.Query<dynamic>("Test_UpdateSong", song, commandType: CommandType.StoredProcedure);
}
dbConn.Close();
Which also works. But, now how do I get my output parameters?
As I stated originally, I didn't want to have to manually convert each class property to a Dapper dynamic parameter. This is critical because if I create a generic method, I may not know which class is being passed into the method and thus which properties to pass to convert to dynamic parameters. Taking #Metro Smurfs advice (when all else fails read the directions), I looked into the Dapper test class and found a solution that works for me:
DynamicParameters p = new DynamicParameters(song);
by adding the song object to the DynamicParameters constructor, a DynamicParameters template is created which will automatically convert all properties to parameters. Then I can simply add my two output parameters and execute the sproc:
p.Add("#success_updated", dbType: DbType.Boolean, direction: ParameterDirection.Output);
p.Add("#success_added", dbType: DbType.Boolean, direction: ParameterDirection.Output);
dbConn.Execute("Test_UpdateSong", p, commandType: CommandType.StoredProcedure);
// get my output parameters...
var success_added = p.Get<bool>("#success_Added");
var success_added = p.Get<bool>("#success_Updated");
and I'm good to go! Thanks to #Nick and #Metro Smurf for the suggestions!
Related
All of the Dapper posts on S/O related to returning a value after insert seem to relate only to the Identity value. I have tried to apply the logic in those answers to my issue, but it's not working.
I want to return a GUID generated by SQL Server which is not an Identity field. This is my code:
public bool Insert(Record record)
{
SqlConnection connection = new SqlConnection(_configuration.GetConnectionString("Production"));
connection.Open();
using (connection)
{
string query = "DECLARE #RSGUID uniqueidentifier SET #RSGUID = NEWID(); INSERT INTO [dbo].[Table] ([Result], [ResultSetKey]) VALUES (#Result, #RSGUID); SELECT #RSGUID";
// it's this next line that I'm confused on:
var resultSetKey = connection.Query<string>(query, #RSGUID).Single();
return connection.Execute(query, record) > 0;
}
}
I know the var resultsSetKey line is not correct, and this is what I need help with. How can I get the GUID generated by SQL Server into a variable?
I can't test right now, but this should work:
public bool Insert(Record record)
{
using (var connection = new SqlConnection(_configuration.GetConnectionString("Production"))
{
string query = "DECLARE #RSGUID uniqueidentifier; SET #RSGUID = NEWID(); INSERT INTO [dbo].[Table] ([Result], [ResultSetKey]) VALUES (#Result, #RSGUID); SELECT #RSGUID";
var resultSetKey = connection.ExecuteScalar<string>(query).SingleOrDefault();
return !String.IsNullOrEmpty(resultSetKey);
}
}
You can get a kickstart with Dapper following the tutorial here:
https://medium.com/dapper-net/get-started-with-dapper-net-591592c335aa
but honestly it's really not clear what you are trying to do. What is the #Result variable that you are using in the T-SQL code?
A very old question, I know - but I've just had exactly the same issue here and found a working solution, so I'm sure somebody else will in benefit from this late answer in the future.
The following works as desired. The key for me was understanding the datatype to use in C# to contain the returned stream_id value, which is System.Guid, not string. Though of course you may wish to convert it to a string to display it to a user for example as plain text.
My stored procedure for entering a new file into the filetable is
USE [dbname]
GO
/****** Object: StoredProcedure [dbo].[newFile] Script Date: 28/11/2022 14:34:50 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[newFile]
#fileBinary varbinary(max) = 0,
#fileName varchar(256) = 0
AS
BEGIN
SET NOCOUNT ON;
DECLARE #newFileId table (stream_id uniqueidentifier);
INSERT INTO dbo.files (file_stream, name) OUTPUT INSERTED.[stream_id] into #newFileId
VALUES (#fileBinary, #fileName);
SELECT * FROM #newFileId
END
And here is the method to add said new file programmatically (I'm fully aware this example includes no error handling etc)
public class fileAccess
{
public System.Guid newFileId;
public void uploadNewFile(byte[] newFileBinary, string newFileName)
{
var procedure = "[newFile]";
var parameters = new
{
fileBinary = newFileBinary,
fileName = newFileName,
};
using (DbConnection connection = new Microsoft.Data.SqlClient.SqlConnection(helper.cnnVal("dbname")))
{
newFileId = connection.ExecuteScalar<System.Guid>(procedure, parameters, commandType: CommandType.StoredProcedure);
}
}
}
Called here (a test example, uploading a file by clicking a button, which pulls a file from my desktop)
private void button6_Click(object sender, EventArgs e)
{
fileAccess db = new fileAccess();
byte[] fileBinary = File.ReadAllBytes(#"C:\Users\username\Desktop\test.docx");
db.uploadNewFile(fileBinary, "test.docx");
string message = "New file succesfully uploaded. File ID: " + Convert.ToString(db.newFileId);
string caption = "Success.";
MessageBox.Show(message, caption, MessageBoxButtons.OK);
}
I'm new to SQL Server and want to implement this scenario. My stored procedure gets 8 input parameters from a C# web application, and checks all input has into the table. For that purpose I wrote this simple stored procedure:
CREATE PROCEDURE CheckValid
#p_bank varchar,
#p_pay_date varchar,
#p_bill_id varchar,
#p_payment_id varchar,
#p_ref_code varchar,
#p_branch varchar,
#p_channel_type varchar,
#p_send_date varchar
AS
BEGIN
SELECT
[p_bank], [p_pay_date], [p_bill_id], [p_payment_id],
[p_ref_code], [p_branch], [p_channel_type], [p_send_date]
FROM
[SAMPLE].[dbo].[MixedTable]
WHERE
[p_bank] = #p_bank
AND [p_pay_date] = #p_pay_date
AND [p_bill_id] = #p_bill_id
AND [p_payment_id] = #p_payment_id
AND [p_ref_code] = #p_ref_code
AND [p_branch] = #p_branch
AND [p_channel_type] = #p_channel_type
AND [p_send_date] = #p_send_date
END
But want to return to c# application this scenario, for example c# sends all field but when stored procedure select run for this purpose can not find data, for example p_bill_id not correct into the table for more explain in select query into where clause in the [p_bill_id]=#p_bill_id not trust and now want to return sp this :
p_bill_id,not found
and other example c# all variable correct but two field [p_channel_type] and [p_payment_id] not correct into where clause but other 6 field correct now SP return this:
[p_channel_type],not found
[p_payment_id],not found
Summary of question:
When data for passed parameter value is not found, I want it to return that corresponding column.
For example:
[p_channel_type],not found
[p_payment_id],not found
Note, varchar means varchar(1) so you should specify length for each argument explicitly like varchar(100)
CREATE PROCEDURE CheckValid
#p_bank varchar(<length>),
#p_pay_date varchar(<length>),
#p_bill_id varchar(<length>),
#p_payment_id varchar(<length>),
#p_ref_code varchar(<length>),
#p_branch varchar(<length>),
#p_channel_type varchar(<length>),
#p_send_date varchar(<length>)
AS
BEGIN
if not exists(select 1 from dbo.BankTable where p_bank = #p_bank)
begin
raiserror('Bank %s not found', 16, 1, #p_bank)
return
end
if not exists(select 1 from dbo.BillTable where p_bill_id = #p_bill_id)
begin
raiserror('Bill %s not found', 16, 1, #p_bill_id)
return
end
...
SELECT [p_bank],[p_pay_date],[p_bill_id],[p_payment_id],[p_ref_code],[p_branch],[p_channel_type],[p_send_date]
FROM [SAMPLE].[dbo].[MixedTable]
where [p_bank]=#p_bank and [p_pay_date]=#p_pay_date
and [p_bill_id]=#p_bill_id and [p_payment_id]=#p_payment_id
and [p_ref_code]=#p_ref_code and [p_branch]=#p_branch
and [p_channel_type]=#p_channel_type and [p_send_date]=#p_send_date
END
GO
Instead of creating stored procedure for this move "validation" logic to your c# application.
Database is just IO device and I think keeping "business logic" in IO device not a good approach.
// Class which represent your eight parameters
public class Data
{
public string Bank { get; set; }
public string PayDate { get; set; }
public string BillId { get; set; }
public string PaymentId { get; set; }
public string RefCode { get; set; }
public string Branch { get; set; }
public string ChannelType { get; set; }
public string SendDate { get; set; }
}
public class Validation
{
private Data _data;
public Validation(Data data)
{
_data = data;
}
public IEnumerable<string> Validate()
{
var columns = new KeyValuePair<string, string>[]
{
new KeyValuePair("p_bank", _data.Bank),
new KeyValuePair("p_pay_date", _data.PayDate),
new KeyValuePair("p_bill_id", _data.BillId),
new KeyValuePair("p_payment_id", _data.PaymentId),
new KeyValuePair("p_ref_code], _data.RefCode),
new KeyValuePair("p_branch", _data.Branch),
new KeyValuePair("p_channel_type", _data.ChannelType),
new KeyValuePair("p_send_date", _data.SendDate)
};
return columns.Where(pair => IsValueExists(pair.Key, pair.Value) == false);
}
private bool IsValueExists(string columnName, string value)
{
var query =
$"SELECT [{columnName}]
FROM [SAMPLE].[dbo].[MixedTable]
WHERE [{columnName}] = #value";
var parameter = new SqlParameter
{
ParameterName = "#value",
SqlDbType = SqlDbType.VarChar,
Value = _data.Bank
};
using (var connection = new SqlConnection(yourConnectionString))
using (var command = new SqlCommand(query, connection))
{
command.Parameters.Add(parameter);
connection.Open();
var value = command.ExecuteScalar();
return value != null; // null returned if no rows exists
}
}
}
Then you can use this method somewhere
var data = new Data { Bank = "BankName", RefCode = "SomeRefcode" } // put all values
var validation = new Validation(data);
var invalidValues = validation.Validate();
foreach(var invalidValue in invalidValues)
{
// Print or save column names where value is invalid
}
I have the following that I declared in my database:
CREATE TYPE [dbo].[QuestionList] AS TABLE (
[QuestionUId] UNIQUEIDENTIFIER NULL);
I am using the datatype like this in the same database:
CREATE PROCEDURE dbo.sp_ins
#Title NVARCHAR (100) ,
#Questions QuestionList READONLY,
#TopicId INT
AS
BEGIN
In my C# calling program I have:
DataTable questions = new DataTable("QUIDs");
questions.Columns.Add("testId", typeof(Guid));
foreach (Guid id in tqh) {
questions.Rows.Add(id);
}
I call the stored procedure with:
parameterList.Add(new SqlParameter ("#Questions", questions));
The problem is I get the following message:
exceptionMessage=The table type parameter '#Questions' must have a valid type name.
Can anyone give me an idea what I may be doing wrong?
The problem is that I was missing specifying a TypeName on the SqlParameter. I corrected it like this:
var p = new SqlParameter ("#Questions", questions);
p.TypeName = "dbo.QuestionList";
You can execute your stored procedure from SSMS by following query without any problem.
DECLARE #Questions QuestionList
INSERT INTO #Questions( QuestionUId )VALUES (NEWID())
EXEC dbo.sp_ins N'', #Questions, 0
Also you can execute your stored procedure from C# code by following code (Sample):
Convet data to DataTable:
public class Record
{
public Guid QuestionUId { get; set; }
}
private DataTable CreateDataTable(IEnumerable<Record> records)
{
var table = new DataTable();
table.Columns.Add("QuestionUId", typeof(Guid));
foreach (Record record in records)
{
table.Rows.Add(record.QuestionUId);
}
return table;
}
Table Valued SP caller
public static IEnumerable<Record> TableValuedSPCaller(DataTable inputTable)
{
using (var context = new MyContext(MyConnectionString))
{
var tableParam = new SqlParameter
{
ParameterName="#Questions",
Value = inputTable,
TypeName = "dbo.QuestionList"
};
return context.Database.SqlQuery<Record/*ResultType*/>
("dbo.sp_ins #Questions", tableParam);
}
}
Call table valued SP:
var records = new List<Record>
{
new Record{QuestionUId=Guid1},
new Record{QuestionUId=Guid1},
new Record{QuestionUId=Guid1}
};
DataTable tableParameter = CreateDataTable(records);
var result = TableValuedSPCaller (tableParameter);
can you please try the following once in your Stored Procedure,
CREATE PROCEDURE dbo.sp_ins
#Title NVARCHAR (100) ,
--#Questions QuestionList READONLY,
#Questions AS QuestionList READONLY, <-------CHANGED
#TopicId INT
AS
BEGIN
Is there a way to specify that I want all of the DateTimes that OrmLite materializes to be set to UTC kind?
I store a lot of DateTimes in my database via stored procedures when a row is inserted:
insert [Comment] (
Body
, CreatedOn
) values (
#Body
, getutcdate()
);
When I retrieve the values via a select statement in ormlite, the datetimes come out in Unspecified kind (which is interpreted as the local timezone, I believe):
var comments = db.SqlList<Comment>("select * from [Comment] where ... ");
I would prefer not to set each DateTime object individually:
foreach (var comment in comments) {
comment.CreatedOn = DateTime.SpecifyKind(comment.CreatedOn, DateTimeKind.Utc);
}
I found this question, but I don't think it's quite what I'm asking for:
servicestack ormlite sqlite DateTime getting TimeZone adjustment on insert
Also found this pull request, but setting SqlServerOrmLiteDialectProvider.EnsureUtc(true) doesn't seem to do it either.
SqlServerOrmLiteDialectProvider.EnsureUtc(true) does work, there was something else going on with my test case that led me to believe that it didn't. Hopefully this will help someone else.
Here's some sample code:
model.cs
public class DateTimeTest {
[AutoIncrement]
public int Id { get; set; }
public DateTime CreatedOn { get; set; }
}
test.cs
var connectionString = "server=dblcl;database=flak;trusted_connection=true;";
var provider = new SqlServerOrmLiteDialectProvider();
provider.EnsureUtc(true);
var factory = new OrmLiteConnectionFactory(connectionString, provider);
var connection = factory.Open();
connection.CreateTable(true, typeof(DateTimeTest));
connection.ExecuteSql("insert DateTimeTest (CreatedOn) values (getutcdate())");
var results = connection.SqlList<DateTimeTest>("select * from DateTimeTest");
foreach(var result in results) {
Console.WriteLine("{0},{1},{2},{3},{4}", result.Id, result.CreatedOn, result.CreatedOn.Kind, result.CreatedOn.ToLocalTime(), result.CreatedOn.ToUniversalTime());
}
output
1,9/13/2013 5:19:12 PM,Utc,9/13/2013 10:19:12 AM,9/13/2013 5:19:12 PM
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.