EF6 auto created Stored Procedures for CRUD - c#

Sorry long explanation but short question. For question you can directy look to 2 bold styled text except this.
I created my first EF6 CODE FIRST project to learn new features. I tried to implement new stored procedure based CRUD operations.
One of models entity for demonstration for here like this:
public partial class POCO
{
//identy
public int ID { get; set; }
// SQL Server Timestamp
// this.Property(t => t.RowVersion).IsFixedLength().HasMaxLength(8).IsRowVersion();
public byte[] RowVersion { get; set; }
public int MiscPropery { get; set; }
}
This entity uses SQL server's RowVersion as concurency token, which is "Database Generated Concurency" of course.
First: I created releted stored procedures on my own. I fallowed the EF6 documentation Stored Procedure Mapping
Then: EntityFramework 6 created database and stored procedures.
EntityFramework 6 created insert stored procedure:
CREATE PROCEDURE [dbo].[POCO_Update]
#ID [int],
#RowVersion_Original [rowversion],
#MiscPropery [int]
-- ------- I hope here: #RowsAffected int OUTPUT
AS
BEGIN
UPDATE [dbo].[POCOs]
SET [MiscPropery] = #MiscPropery
WHERE (([ID] = #ID) AND (([RowVersion] = #RowVersion_Original)
OR ([RowVersion] IS NULL AND #RowVersion_Original IS NULL)))
SELECT t0.[RowVersion], t0.[FaturaNo]
FROM [dbo].[POCOs] AS t0
WHERE ##ROWCOUNT > 0 AND t0.[ID] = #ID
END
EntityFramework 6 created delete stored procedure:
CREATE PROCEDURE [dbo].[POCO_Delete]
#ID [int],
#RowVersion_Original [rowversion]
-- ------- I hope here: #RowsAffected int OUTPUT
AS
BEGIN
DELETE [dbo].[POCOs]
WHERE (([ID] = #ID) AND (([RowVersion] = #RowVersion_Original)
OR ([RowVersion] IS NULL AND #RowVersion_Original IS NULL)))
END
My problems came at this point:
IN EntityFramework 6 documentation page, in "Concurrency Tokens" section says: (http://entityframework.codeplex.com/wikipage?title=Code%20First%20Insert%2fUpdate%2fDelete%20Stored%20Procedure%20Mapping)
Concurrency Tokens
Update and delete stored procedures may also need to deal with concurrency:
If the entity contains any concurrency tokens, the stored procedure should have an output parameter named RowsAffected that returns the number of rows updated/deleted.
And gives these samples on that documentation page:
// from documentation page
public class Person
{
public int PersonId { get; set; }
public string Name { get; set; }
[Timestamp]
public byte[] Timestamp { get; set; }
}
CREATE PROCEDURE [dbo].[Person_Update]
#PersonId int,
#Name nvarchar(max),
#Timestamp_Original rowversion,
#RowsAffected int OUTPUT -- ====> !!!!!
AS
BEGIN
UPDATE [dbo].[People]
SET [Name] = #Name
WHERE PersonId = #PersonId AND [Timestamp] = #Timestamp_Original
SET #RowsAffected = ##RowCount
END
But there is no output parameter named RowsAffected in stored procedures created by EF6.
Im a have to change autocreated stored procedures to follow documantation, or documentation is broken? Or everything goes under the curtains.

Related

The data reader is incompatible. A member of the type, 'RoleId', does not have a corresponding column in the data reader with the same name

Stored procedure works and deletes what I want but I still get this error after deleting:
The data reader is incompatible with the specified 'AMSIdentity.Models.RemoveRoleFromUserViewModel'. A member of the type, 'RoleId', does not have a corresponding column in the data reader with the same name.
I need to run the code without this error in the above
This code using ASP.NET MVC 5 and EF6 code first approach; I tried to use this code but always throws this error after delete.
This is the action method that I use
public ActionResult RemoveRoleFromUserConfirmed(string UserName, string RoleId)
{
if (UserName == null && RoleId == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
SqlParameter param1 = new SqlParameter("#RoleId", RoleId);
SqlParameter param2= new SqlParameter("#UserName", UserName);
var remove = Identitydb.Database.SqlQuery<RemoveRoleFromUserViewModel>("admin.sp_RemoveUserFromRole #RoleId, #UserName",
((ICloneable)param1).Clone(),
((ICloneable)param2).Clone()).ToArray().ToList().FirstOrDefault();
if (remove == null)
{
return HttpNotFound();
}
return RedirectToAction("Roles");
}
This is the view model that I use :
public class RemoveRoleFromUserViewModel
{
[Key]
[DisplayName("Role Id")]
public string RoleId { get; set; }
[DisplayName("Username")]
public string UserName { get; set; }
}
This is the stored procedure code:
ALTER PROCEDURE [Admin].[sp_RemoveUserFromRole]
#RoleId NVARCHAR(50),
#UserName NVARCHAR(50)
AS
BEGIN
DELETE FROM AspNetUserRoles
WHERE UserId = (SELECT Id
FROM AspNetUsers
WHERE UserName = #UserName)
AND RoleId = #RoleId
END
I expect that this code will delete role from the specific user.
When you perform a DELETE in the stored procedure, you need to "audit" what got deleted. Then perform a SELECT on that audit-table.
You are taking advantage of the OUTPUT feature of sql server.
see:
https://learn.microsoft.com/en-us/sql/t-sql/statements/delete-transact-sql?view=sql-server-2017
and/or
https://www.sqlservercentral.com/articles/the-output-clause-for-insert-and-delete-statements
Below is a generic example of the TSQL you need.
DROP TABLE IF EXISTS [dbo].[Patient]
GO
CREATE TABLE [dbo].[Patient]
(
[PatientKey] BIGINT NOT NULL IDENTITY(1, 1),
[PatientUniqueIdentifier] VARCHAR(256) NOT NULL,
[CreateDate] DATETIMEOFFSET NOT NULL,
CONSTRAINT [UC_Patient_PatientUniqueIdentifier] UNIQUE (PatientUniqueIdentifier)
)
/* now insert 3 sets of rows, with different create-dates */
INSERT INTO dbo.Patient (PatientUniqueIdentifier, [CreateDate]) SELECT TOP 10 NEWID() , '01/01/2001' from sys.objects
INSERT INTO dbo.Patient (PatientUniqueIdentifier, [CreateDate]) SELECT TOP 10 NEWID() , '02/02/2002' from sys.objects
INSERT INTO dbo.Patient (PatientUniqueIdentifier, [CreateDate]) SELECT TOP 10 NEWID() , '03/03/2003' from sys.objects
SELECT 'SeedDataResult' as Lable1, * FROM dbo.Patient
/* everything above is just setting up the example */
/* below would be the "guts"/implementation of your stored procedure */
DECLARE #PatientAffectedRowsCountUsingAtAtRowCount BIGINT
DECLARE #PatientAffectedRowsCountUsingCountOfOutputTable BIGINT
DECLARE #PatientCrudActivityAuditTable TABLE ( [PatientUniqueIdentifier] VARCHAR(256), DatabaseKey BIGINT , MyCrudLabelForKicks VARCHAR(16));
/* now delete a subset of all the patient rows , your delete will be whatever logic you implement */
DELETE FROM [dbo].[Patient]
OUTPUT deleted.PatientUniqueIdentifier , deleted.PatientKey , 'mydeletelabel'
INTO #PatientCrudActivityAuditTable ([PatientUniqueIdentifier] ,DatabaseKey , MyCrudLabelForKicks )
WHERE
CreateDate = '02/02/2002'
/*you don't need this update statement, but i'm showing the audit table can be used with delete and update and insert (update here) */
/*
UPDATE [dbo].[Patient]
SET CreateDate = '03/03/2003'
OUTPUT inserted.PatientUniqueIdentifier , inserted.PatientKey, 'myupdatelabel'
INTO #PatientCrudActivityAuditTable ([PatientUniqueIdentifier] ,DatabaseKey , MyCrudLabelForKicks)
FROM [dbo].[Patient] realTable
WHERE CreateDate != '03/03/2003'
*/
/* optionally, capture how many rows were deleted using ##ROWCOUNT */
SELECT #PatientAffectedRowsCountUsingAtAtRowCount = ##ROWCOUNT
/* or, capture how many rows were deleted using a simple count on the audit-table */
SELECT #PatientAffectedRowsCountUsingCountOfOutputTable = COUNT(*) FROM #PatientCrudActivityAuditTable
SELECT 'ResultSetOneForKicks' as Label1, 'Rows that I Deleted' as MyLabel_YouCanRemoveThisColumn, DatabaseKey , PatientUniqueIdentifier FROM #PatientCrudActivityAuditTable
/* if so inclined, you can also send back the delete-COUNTS to the caller. You'll have to code your IDataReader (ORM, whatever) to handle the multiple return result-sets */
/* most people will put the "counts" as the first result-set, and the rows themselves as the second result-set ... i have them in the opposite in this example */
SELECT 'ResultSetTwoForKicks' as Label1, #PatientAffectedRowsCountUsingAtAtRowCount as '#PatientAffectedRowsCountUsingAtAtRowCountCoolAliasName' , #PatientAffectedRowsCountUsingAtAtRowCount as '#PatientAffectedRowsCountUsingAtAtRowCountCoolAliasName'
In my example, you would write the dotNet serialize code...(whatever flavor you use, raw IDataReader, ORM tool, whatever) against the PatientKey and PatientUniqueIdentifier columns coming back from the #PatientSurrogateKeyAudit table.
Hi All,
I got the answer from #Jeroen Mostert, The solution is to use the
(ExecuteSqlCommand) rather than (SqlQuery) because I will never return
data, I only execute the stored procedure with two parameters.
This is the answer
SqlParameter param1 = new SqlParameter("#RoleId", RoleId);
SqlParameter param2= new SqlParameter("#UserName", UserName);
//I change this line from SqlQuery to ExecuteSqlCommand
var remove = Identitydb.Database.ExecuteSqlCommand("admin.sp_RemoveUserFromRole #RoleId, #UserName", param1, param2);
Thank you very much #Jeroen Mostert.
Regards,
Ali Mosaad
Software Developer

On Insert Trigger breaks EF Add

I added a SQL Server table ON INSERT trigger and it broke my API's Entity Framework insert code. It throws an error:
InnerException = "Store update, insert, or delete statement affected an unexpected number of rows (0). Entities may have been modified or deleted since entities were loaded.
I tried setting EntityState.Added; suggested by Ben here, but this didn't work.
I tried the suggestion from chamara here.
Added the following SQL Server trigger:
ALTER TRIGGER [dbo].[tr_TargetUserDel]
ON [dbo].[TargetUser]
AFTER INSERT
AS
BEGIN
DECLARE #UserId AS INT
DECLARE #TargetId AS INT
DECLARE #TargetUserId AS INT
SELECT #UserId = i.[UserId], #TargetId = i.[TargetId]
FROM inserted i
INSERT INTO [dbo].[TargetUser] ([UserId], [TargetId])
VALUES (#UserId, #TargetId)
SELECT #TargetUserId = CAST(##Identity AS INTEGER)
DELETE [dbo].[TargetUser]
WHERE TargetUserId <> #TargetUserId
AND TargetId = #TargetId
END
This caused the following EF insert to fail:
targetuser.UserId = interfaceUser.UserId;
targetuser.TargetId = interfaceUserDTO.TargetId;
db.InterfaceTargetUsers.Add(targetuser);
db.SaveChanges(); // <<Error thrown
The model object targetuser:
[Table("TargetUser")]
public partial class InterfaceTargetUser
{
[Key]
[Column(Order = 0)]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int TargetUserId { get; set; }
public int UserId { get; set; }
public int TargetId { get; set; }
}
EF is having trouble with the insert, but if I run the insert directly in SQL Server, it runs fine:
INSERT INTO targetuser (TargetId, UserId)
VALUES (5276, 572)
Modifying the trigger as suggested by chamara here works, but am looking for a EF solution.
chamara's workaround is to add this to the end of the trigger:
SELECT * FROM deleted UNION ALL
SELECT * FROM inserted;
This works but it has no up votes. Why does this work? Seems like there should be an EF solution?
You have many problems there:
select #UserId = i.[UserId], #TargetId=i.[TargetId] from inserted i
Will works only when the pseudo INSERTED hase only 1 rows.
Also for the line
SELECT #TargetUserId =CAST(##Identity AS INTEGER)
Will only return the last generated identity value, not all of them.
Alter your trigger to be like
ALTER TRIGGER [dbo].[tr_TargetUserDel] ON [dbo].[TargetUser]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO [dbo].[TargetUser]([UserId], [TargetId])
SELECT UserId,
TargetId
FROM INSERTED;
DELETE T
FROM [dbo].[TargetUser] T INNER JOIN INSERTED TT
ON T.TargetId = TT.TargetId
AND
T.TargetUserId <> TT.TargetUserId;
SET NOCOUNT OFF;
END

Using LINQ to insert data into a table that uses a sequence as primary key generator

I have a table which generates its primary key from a sequence (that just counts up from 0):
CREATE TABLE [dbo].[testTable](
[id] [int] NOT NULL,
[a] [int] NOT NULL,
CONSTRAINT [PK_testTable] PRIMARY KEY CLUSTERED ([id] ASC))
ALTER TABLE [dbo].[tblTestTable] ADD CONSTRAINT [DF_tblTestTable_id] DEFAULT (NEXT VALUE FOR [seq_PK_tblTestTable]) FOR [id]
I've used Visual Studio's O/R Designer to create the mapping files for the table; the id field is defined as:
[global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_id", DbType="Int NOT NULL", IsPrimaryKey=true)]
public int id {…}
and now I'm trying to insert data via LINQ.
var testTableRecord = new testTable()
{
a = 1,
};
db.Connection.Open();
db.testTables.InsertOnSubmit(testTableRecord);
db.SubmitChanges();
Console.WriteLine($"id: {testTableRecord.id}");
The problem I'm encountering is, that LINQ seems unable to handle the id generation via sequence as it sets the id implicitly to 0 when inserting.
When I set the id to CanBeNull, the insert fails because it tries to insert NULL into a non-nullable field.
When I set the id to IsDbGenerated, the insert works but it expects an IDENTITY field and tries to load the generated id with SELECT CONVERT(Int,SCOPE_IDENTITY()) AS [value]',N'#p0 int',#p0=1 and than sets the id in the object to null because SCOPE_IDENTITY() returns null…
I've been thinking about just using IsDbGenerated, destroying the LINQ object and querying the DB for the id, but I don't have anything unique to search for.
Unfortunately changing the id creation mechanism to IDENTITY is not an option.
Do I have to explicitly query the DB for the next sequence value and set the id manually?
Whats the best way to handle these inserts via LINQ?
PS: I need the id because I have to insert more data that uses the id as FK.
Looking at solutions from the raw sql perspective:
1.
INSERT INTO [dbo].[testTable] VALUES (NEXT VALUE FOR [dbo].[seq_PK_tblTestTable], 1)
Simply can't be done in LINQ to SQL as far as I can tell
2.
INSERT INTO [dbo].[testTable] (a) VALUES (1)
This can be achieved in LINQ to SQL by excluding the id property from the testTable entity.
If you need to retrieve ids from the table, you could create separate entities for inserting and querying:
public class testTableInsert {
[ColumnAttribute(...)]
public int a
}
public class testTableResult {
[ColumnAttribute(...)]
public int id
[ColumnAttribute(...)]
public int a
}
3.
DECLARE #nextId INT;
SELECT #nextId = NEXT VALUE FOR [dbo].[seq_PK_tblTestTable];
INSERT INTO [dbo].[testTable] VALUES (#nextId, 1)
As you mentioned, this can be essentially achieved by manually requesting the next id before each insert. If you go this route there are multiple ways to achieve it in your code, you can consider stored procedures or use the LINQ data context to manually execute the sql to retrieve the next sequence value.
Here's a code sample demonstrating how to extend the generated DataContext using partial methods.
public partial class MyDataContext : System.Data.Linq.DataContext
{
partial void InsertTestTable(TestTable instance)
{
using (var cmd = Connection.CreateCommand())
{
cmd.CommandText = "SELECT NEXT VALUE FOR [dbo].[seq_PK_TestTable] as NextId";
cmd.Transaction = Transaction;
int nextId = (int) cmd.ExecuteScalar();
instance.id = nextId;
ExecuteDynamicInsert(instance);
}
}
}
Once the above is implemented, you can safely insert entities like this, and they will generate the correct sequence id.
TestTable entity = new TestTable { a = 2 };
dataContext.TestTables.InsertOnSubmit(entity);
dataContext.SubmitChanges();
Your only hope is a pretty profound refactoring and use a stored procedure to insert records. The stored procedure can be mapped to the class's Insert method in the data context designer.
Using your table definition, the stored is nothing but this:
CREATE PROCEDURE InsertTestTable
(
#id int OUTPUT,
#a AS int
)
AS
BEGIN
INSERT dbo.testTable (a) VALUES (#a);
SET #id = (SELECT CONVERT(int, current_value)
FROM sys.sequences WHERE name = 'seq_PK_tblTestTable')
END
You can import this stored procedure into the context by dragging it from the Sql Object Explorer onto the designer surface, which will then look like this:
The next step is to click the testTable class and click the ellipses button for the Insert method (which got enabled by adding the stored procedure to the context):
And customize it as follows:
That's all. Now LINQ-to-SQL will generate a stored procedure call to insert a record, for example:
declare #p3 int
set #p3=8
declare #p5 int
set #p5=0
exec sp_executesql N'EXEC #RETURN_VALUE = [dbo].[InsertTestTable] #id = #p0 OUTPUT,
#a = #p1',N'#p0 int output,#p1 int,#RETURN_VALUE int output',
#p0=#p3 output,#p1=123,#RETURN_VALUE=#p5 output
select #p3, #p5
Of course you may have to wonder how long you're going to hang on to LINQ-to-SQL. Entity Framework Core has sequence support out of the box. And much more.

How to use Table valued parameter in Stored Procedure and then call it in MVC C# Visual studio?

I have created user defined datatype which is of type table as:
Create type Employeetable as Table
(
FirstName varchar(50),
LastName varchar(50),
States varchar(50),
City varchar(50),
AddressLine1 varchar(100),
AddressLine2 varchar(100)
)
But now I am consfused as to how should I use it in stored procedure , doing a basic CRUD operation here . THis is how i have used Create Procedure:
Create Procedure InsertEmployees
#employeetable Employeetable Readonly
as
begin
Insert into Testing.Employees
select * from #employeetable
end
Also I don't know how to use it in MVC C# in visual studio because when I added stored procedure it showed this error:
Error 6005: The function 'InsertEmployees' has a parameter 'employeetable' at parameter index 0 that has a data type 'table type' which is currently not supported for the target Entity Framework version. The function was excluded
Also in controller how should I proceed with the code , in normal stored procedure I would write these in [HttpPost]:
public ActionResult AddProduct([Table name] [table object])
{
ObjectParameter objParam = new ObjectParameter("productID", typeof(int));
int result =
db.usp_SAVE_tbl_Products_PG_ADODotNetDemoCode(objProduct.ProductID,
objProduct.ProductName, objProduct.Rate, objProduct.Quantity, objParam);
this.db.SaveChanges();
int resultVal = Convert.ToInt32(objParam.Value);
return RedirectToAction("ListProducts");
}
Kindly help with this thanks

Retrieve table data from stored procedure using entity framework

I'm using Entity Framework v6. I have a stored procedure as shown below
CREATE PROCEDURE [dbo].[GetCountryList]
(
#CustomerName VARCHAR(MAX),
#SearchCriteria VARCHAR(MAX)
)
AS
BEGIN
SET NOCOUNT ON
SELECT CountryID, CountryName FROM dbo.Table1
WHERE CustomerName = #CustomerName AND CountryName = #SearchCriteria
END
Now I have a model class
public class CountryName
{
public int CountryId { get; set; }
public string CountryName { get; set; }
}
So I want to get the result of the SELECT query in a List<CountryName> type
List<CountryName> countryList = null;
using (DbEntities dbContext = new DbEntities())
{
countryList = //my code to collect the result
}
Well, I could have run a LINQ to SQL directly on the table but unfortunately my requirement in to get the data from stored procedure. So, how can I do it?
You need to Import the stored procedure as a Function. Right-click on the workspace area of your Entity model and choose Add -> Function Import.
In the Add Function Import dialog, enter the name you want your stored procedure to be referred to in your model for example GetCountryListSP, choose your procedure from the drop down list, and choose the return value of the procedure to be Entities and choose CountryName from the drop down list.
Then in the code:
var result = db.GetCountryListSP();//Send parameters too
With this approach you prevent returning -1 of the stored procedure. Please check this post for more details about stored procedure problem with Entity Framework.
You can do it without importing. Something like that:
var countryList = dbContext.Database.SqlQuery<CountryName>("[GetCountryList]").ToList();
EntityFramework sometimes won't recognize or import SPs ))) So, that's why I saving my hours with this snippet.

Categories

Resources