SQL 2012 CLR error: “The context transaction which was active before entering …”?
-
30-09-2020 - |
Question
I am experience strange error with SQL 2012
and CLR
:
The context transaction which was active before entering user defined routine, trigger or aggregate "My routine" has been ended inside of it, which is not allowed. Change application logic to enforce strict transaction nesting.
I have no idea what can be the problem. Can anyone give me some direction where to search?
In general I have SQL Stored procedure inside which in UPDATE
Clause I call scalar CLR function
. The function gets 2 xml
strings: one for xml
and other for xsd
an make validation. The function is called many times in the query.
EDIT:
ERROR message:
Error Number: 3991
Error Severity: 16
Error State: 1
Error Procedure: sproc_XXX
Error Line: 297
Error Message: The context transaction which was active before entering user defined routine, trigger or aggregate "CLR_XXX_Xml" has been ended inside of it, which is not allowed. Change application logic to enforce strict transaction nesting.
EDIT 2:
I find the problem. When we used CLR
to validate XML
, we sned 1st parameter to be the XSD
name. Then in the CLR
we extract XSD
schema from SQL Server. In our case we have empty string for XSD
name, but even we have try / catch
the error is passed upwards the SQL Server stored procedure.
Our function returns 0 / 1
so the recors with 0 must be updated with error code.
But in this case we receive exception in SQL.
My code is:
SQL for Update
UPDATE FR
SET ErrorCode = 'OUR ERROR CODE'
FROM dbo.DataTable FR
INNER JOIN dbo.TempTable FRN ON (FRN.SessionGUID = @sSessionGUID)
AND (FRN.IDLog = FR.IDLog)
AND (FRN.UniqueID = FR.UniqueID)
AND (
(FRN.Template LIKE '%##%')
OR (PANI_Core.dbo.funCLRs_CheckXML('schRPT_' + @sSchema + '_Peliminary',
FRN.Template) <> 1)
OR (NULLIF(FRN.Template, '') IS NULL)
OR (
(TRY_CONVERT(DATE, FRN.ExpirationDate) IS NULL)
AND (NULLIF(FRN.ExpirationDate, '') IS NOT NULL)
)
OR (
(TRY_CONVERT(VARCHAR(35), FRN.EGN) IS NULL)
AND (NULLIF(FRN.EGN, '') IS NOT NULL)
)
OR (NULLIF(FRN.EGN, '') IS NULL)
OR (
(TRY_CONVERT(MONEY, FRN.Amount) IS NULL)
AND (NULLIF(FRN.Amount, '') IS NOT NULL)
)
OR (NULLIF(FRN.Amount, '') IS NULL)
OR (
(TRY_CONVERT(VARCHAR(32), FRN.UniqueClientID) IS NULL)
AND (NULLIF(FRN.UniqueCID, '') IS NOT NULL)
)
OR (NULLIF(FRN.UniqueCID, '') IS NULL)
)
WHERE (FR.IDLog = @IDLog)
AND (FR.ErrorCode IS NULL);
CLR code:
public static SqlBoolean funCLRs_CheckXml(SqlString XsdSchemaString, SqlString ValueString)
{
XmlDocument asset1 = new XmlDocument();
XmlSchema schema1 = new XmlSchema();
System.Text.StringBuilder o = new System.Text.StringBuilder();
using (SqlConnection sqlLocalConn = new SqlConnection("context connection=true;"))
{
try
{
if (XsdSchemaString.ToString().Length == 0)
{
try
{
asset1.LoadXml(ValueString.ToString());
asset1 = null;
return new SqlBoolean(true);
}
catch (Exception ex)
{
return new SqlBoolean(false);
}
}
else
{
sqlLocalConn.Open();
SqlCommand cmd = new SqlCommand("SELECT XML_SCHEMA_NAMESPACE (N'dbo', N'" + XsdSchemaString.ToString() + "')", sqlLocalConn);
cmd.CommandTimeout = 240;
SqlDataReader reader = cmd.ExecuteReader();
if (reader.Read())
{
SqlXml xmlData = reader.GetSqlXml(0);
using (XmlReader xmlReader = xmlData.CreateReader())
{
schema1 = XmlSchema.Read(xmlReader, ValidateSchema);
}
asset1.Schemas.Add(schema1);
XDocument oDoc = new XDocument();
oDoc = XDocument.Parse(ValueString.ToString());
oDoc.Descendants().Where(e => string.IsNullOrEmpty(e.Value)).Remove();
asset1.LoadXml(oDoc.FirstNode.ToString());
oDoc = null;
asset1.Validate((o1, e) =>
{
if (e.Severity == XmlSeverityType.Error)
{
o.AppendLine("Error: " + e.Message);
}
});
if (asset1.SchemaInfo.Validity == XmlSchemaValidity.Valid)
{
asset1 = null;
return new SqlBoolean(true);
}
else
{
asset1 = null;
return new SqlBoolean(false);
}
}
else
{
asset1 = null;
return new SqlBoolean(false);
}
}
}
catch (Exception ex)
{
return new SqlBoolean(false);
}
finally
{
schema1 = null;
asset1 = null;
o = null;
}
}
}
Solution
The direct cause of the "The context transaction which was active before entering user defined routine, trigger or aggregate "..." has been ended inside of it, which is not allowed." error is:
You are calling the XML_SCHEMA_NAMESPACE() built-in function, and sometimes it fails due to not finding the specified XML Schema Collection. As per the info in the Question, you are passing in an empty string when this error happens, though passing in any string that doesn't match an existing XML Schema Collection would cause this error.
What is happening is you are currently in a Transaction when this SQLCLR function is called (i.e. the Transaction of the UPDATE
statement), and the XML_SCHEMA_NAMESPACE
failing is a batch-aborting error, in which case the Transaction is automatically rolled-back (hence the try...catch
in the .NET code can't help here). This is really the same error you would get if you had nested Stored Procedure calls where the outer Stored Procedure began a Transaction and the inner Stored Procedure issued a ROLLBACK
in which case the value of @@TRANCOUNT
would be lower upon exiting the inner Stored Procedure than it was upon entering it, and you would get an error stating that the starting and ending values of @@TRANCOUNT
were not the same.
Here is example code showing that XML_SCHEMA_NAMESPACE
failing is a batch (and hence Transaction) aborting error:
BEGIN TRAN;
PRINT @@TRANCOUNT;
DECLARE @SchemaCollectionName sysname = N'f';
PRINT CONVERT(NVARCHAR(4000), XML_SCHEMA_NAMESPACE(N'dbo', @SchemaCollectionName));
GO
PRINT @@TRANCOUNT;
ROLLBACK;
Returns:
1
Msg 6314, Level 16, State 1, Line 4
Collection specified does not exist in metadata : 'f'
0
Msg 3903, Level 16, State 1, Line 7
The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION.
The root cause of this error is:
You are allowing values to be passed to the XML_SCHEMA_NAMESPACE
function that cause it to error, specifically an empty string.
The fix for this error is to do the following:
- Also test for
NULL
and white-space-only string values. This will prevent errors due toNULL
values (which would cause an error in your current code anyway) as well as non-empty strings filled with any combination of spaces, tabs, returns, etc. (theIsNullOrWhiteSpace
method checks for all Unicode white-space characters). Replace:
if (XsdSchemaString.ToString().Length == 0)
with:
if (XsdSchemaString.IsNull || String.IsNullOrWhiteSpace(XsdSchemaString.Value))
Do an existence check for the XML Schema Collection name being passed in, prior to calling
XML_SCHEMA_NAMESPACE
so that you only ever call that function if the name actually exists. You can do this in the same step as the currentSELECT
by wrapping that in anIF EXISTS
:
DECLARE @SchemaCollectionName NVARCHAR(128) = N'not here'; IF (EXISTS( SELECT * FROM sys.xml_schema_collections WHERE [schema_id] = SCHEMA_ID(N'dbo') AND [name] = @SchemaCollectionName ) ) BEGIN SELECT XML_SCHEMA_NAMESPACE(N'dbo', @SchemaCollectionName) AS [MyXSD]; END;
This will prevent errors due to incorrect non-empty, non-white-space-only values. An incorrect value will not return a result set, and your code already handles that as the
else
condition forif (reader.Read())
.- While not critical to this issue, it would be best if you parameterized that query. The example code in both places show that the function does accept a variable for the XML Schema Collection name.