LinqToSql is a convenient way to provide a light object mapping layer for your Sql Server database. Like many tools, funky things happen when you leave the main use case.
In our database, access is controlled through views, which provide a layer of indirection that facilitates schema refactoring and optimization, as well as making data security a little easier for the DBA and database developers to manage.
These views are typically constructed with INSTEAD OF triggers. In the case of a INSTEAD OF INSERT trigger, LinqToSql will have trouble finding any autogenerated identity values that are present on the table.
Here’s what the trigger might look like:
ALTER TRIGGER [dbo].[tr_v_TransactionItem_Insert]
ON [dbo].[v_TransactionItem]
INSTEAD OF INSERT
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
-- Insert statements for trigger here
INSERT dbo.TransactionItem
(
TransactionId,
InventoryItemId,
QuantityChange,
PriceEach,
PriceAdjustment,
Notes
)
SELECT
TransactionId,
InventoryItemId,
COALESCE(QuantityChange, 0),
COALESCE(PriceEach, 0),
COALESCE(PriceAdjustment, 0),
Notes
FROM inserted
END
I ran across this using the .net RIA services July 2009 preview, where you get a cryptic message of this sort:
The null value cannot be assigned to a member with type System.Int32 which is a non-nullable value type.
This actually shows up as a System.Windows.Ria.Data.EntityOperationException when handling the ChangesSubmitted event on the client side.
So what’s happening? Using SQL profiler, you can see that LinqToSql attempts to use SCOPE_IDENTITY() to access the identity value of the item inserted.
exec sp_executesql N'INSERT INTO [dbo].[v_TransactionItem](
[BottleSize], [SupplierName], [ProductName], [OrderId], [ProductionYear],
[BottleDate], [TransactionId], [InventoryItemId], [QuantityChange],
[PriceEach], [PriceAdjustment], [Notes])
VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10, @p11)
SELECT CONVERT(Int,SCOPE_IDENTITY()) AS [value]',
N'@p0 varchar(8000),@p1 varchar(8000),@p2 varchar(8000),@p3 nchar(10),
@p4 int,@p5 datetime,@p6 int,@p7 int,@p8 int,@p9 money,@p10 money,
@p11 varchar(8000)',
@p0=NULL,@p1=NULL,@p2=NULL,@p3=NULL,@p4=NULL,@p5=NULL,@p6=27,
@p7=10,@p8=0,@p9=$0.0000,@p10=$0.0000,@p11=NULL
Running this query in management studio shows that SCOPE_IDENTITY() returns NULL. That’s what’s generating the error message. So, what can we do about this?
A typical method to handle this would be to create a stored procedure and use a custom method to invoke the stored procedure. MSDN documents this
here:
http://msdn.microsoft.com/en-us/library/bb882646.aspx.
This didn’t seem entirely satisfactory since it seemed like a whole bunch of
extra work was needed to write the stored procedure. However, with some experimentation, it seems that
if the identity key values are explicitly selected in the trigger, the insert
works correctly.
In SQL Server 2008, it is a simple as inserting the OUTPUT clause in the
trigger:
ALTER TRIGGER [dbo].[tr_v_TransactionItem_Insert]
ON [dbo].[v_TransactionItem]
INSTEAD OF INSERT
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
-- Insert statements for trigger here
INSERT dbo.TransactionItem
(
TransactionId,
InventoryItemId,
QuantityChange,
PriceEach,
PriceAdjustment,
Notes
)
OUTPUT inserted.TransactionItemId
SELECT
TransactionId,
InventoryItemId,
COALESCE(QuantityChange, 0),
COALESCE(PriceEach, 0),
COALESCE(PriceAdjustment, 0),
Notes
FROM inserted
END