LinqConnect Documentation
Processing Errors During Submit - OnSubmitError Partial Method

While LinqConnect provides a convenient way of resolving concurrency conflicts after the SubmitChanges method fails, it is also possible to process errors (and not only those related to concurrency) along the way, immediately when they arise.

For example, consider an error with using an existing primary key value: suppose no generators are configured for the table primary key, or that the key generator works in such a way that it can eventually generate an existing key. Technically, this is true for the GuidGenerator, as (theoretically) it may generate two equal GUIDs. When inserting a new entity, you can thus get a constraint violation (which is not a concurrency issue, so you'll get LinqCommandExecutionException instead of ChangeConflictException). It would be convenient in such a situation if you could just select another key and try persisting this entity again without interrupting the whole submit changes process.

For this purpose, LinqConnect provides a partial method OnSubmitError in the generated code of DataContext descendants:


C#csharpCopy Code
public partial class CrmDemoDataContext : Devart.Data.Linq.DataContext
{
    ... 
    #region Extensibility Method Definitions
    ...
    partial void OnSubmitError(SubmitErrorEventArgs args);
    ...
}
Visual BasicCopy Code
Public Partial Class CrmDemoDataContext
    Inherits Devart.Data.Linq.DataContext
    ... 
    #Region "Extensibility Method Definitions"    
    ...
    Partial Private Sub OnSubmitError(args As SubmitErrorEventArgs)
    End Sub
    ...
End Class

If this method is implemented, it will be invoked each time you get an error during a submit. The only argument passed to this method is of type SubmitErrorEventArgs, which includes the information about the operation that failed and wraps the exception that was thrown.

SubmitErrorEventArgs and Sample Implementation of OnSubmitError

The following sample demonstrates a possible implementation of the OnSubmitError method:


C#csharpCopy Code
namespace CrmDemoContext
{
 
    public partial class CrmDemoDataContext
    {
 
        partial void OnSubmitError(SubmitErrorEventArgs args)
        {
 
            // Ensure that submitting the current entity does not enter an infinite loop.
            if (args.CurrentUnitSubmitAttempt >= 3)
                return;
 
            // Resolve errors occurred with a particular entity only.
            if (args.Object == null || args.MetaType == null)
                return;
 
            Type errorType = args.Error.GetType();
 
            // Handle common server errors (e.g., invalid key).
            if (errorType == typeof(LinqCommandExecutionException))
            {
 
                Exception inner = args.Error.InnerException;
 
                // Cast the inner exception to a provider-specific one to be able to get more details 
                // about it.
                if (inner is OracleException)
                {
 
                    OracleException oracleException = inner as OracleException;
 
                    switch (oracleException.Code)
                    {
 
                        // "ORA-0001: unique constraint violated."
                        case 1:
 
                            // Ensure that the problem occurred when inserting a new entity.
                            if (args.ChangeAction != ChangeAction.Insert)
                                return;
 
                            // Should use more subtle logic here.
                            // E.g., should get a new value from the key generator (if any is defined 
                            // for one of identity members). Also may use a switch on the entity's 
                            // metatype and implement some specific processing for each one of 
                            // registered types.
                            object objectRef = args.Object;
                            var keyMember = args.MetaType.IdentityMembers.First();
                            int keyValue = (int)keyMember.StorageAccessor.GetBoxedValue(objectRef);
                            keyMember.StorageAccessor.SetBoxedValue(ref objectRef, ++keyValue);
                            args.ContinueAction = ContinueAction.Retry;
                            break;
                        default:
                            break;
                    }
                }
            }
            // Handle concurrency errors.
            else if (errorType == typeof(ChangeConflictException))
            {
 
                if (args.ChangeConflict != null)
                {
 
                    // Resolve the conflict based on a certain approach.
                    args.ChangeConflict.Resolve(RefreshMode.KeepChanges, true);
 
                    // If the corresponding row is deleted from the database, pass to the next entities.
                    // Otherwise, retry updating the entity.
                    if (args.ChangeConflict.IsDeleted)
                        args.ContinueAction = ContinueAction.Continue;
                    else
                        args.ContinueAction = ContinueAction.Retry;
 
                    // It is hardly the case for the current sample, but the conflict may be unresolved 
                    // for some reason. It is then a good idea to nevertheless follow the conflict mode 
                    // specified in the SubmitChanges call.
                    if (!args.ChangeConflict.IsResolved)
                        args.ContinueAction = args.ConflictMode == ConflictMode.ContinueOnConflict ?
                            ContinueAction.Continue :
                            ContinueAction.Abort;
                }
 
            }
 
        }
    }
}
Visual BasicCopy Code
Namespace CrmDemoContext
 
    Partial Public Class CrmDemoDataContext
 
        Private Sub OnSubmitError(args As SubmitErrorEventArgs)
 
            ' Ensure that submitting the current entity does not enter an infinite loop.
            If args.CurrentUnitSubmitAttempt >= 3 Then
                Return
            End If
 
            ' Resolve errors occurred with a particular entity only.
            If args.[Object] Is Nothing OrElse args.MetaType Is Nothing Then
                Return
            End If
 
            Dim errorType As Type = args.[Error].[GetType]()
 
            ' Handle common server errors (e.g., invalid key).
            If errorType = GetType(LinqCommandExecutionException) Then
 
                Dim inner As Exception = args.[Error].InnerException
 
                ' Cast the inner exception to a provider-specific one to be able to get more details 
                ' about it.
                If TypeOf inner Is OracleException Then
 
                    Dim oracleException As OracleException = TryCast(inner, OracleException)
 
                    Select Case oracleException.Code
 
                        ' "ORA-0001: unique constraint violated."
                        Case 1
 
                            ' Ensure that the problem occurred when inserting a new entity.
                            If args.ChangeAction <> ChangeAction.Insert Then
                                Return
                            End If
 
                            ' Should use more subtle logic here.
                            ' E.g., should get a new value from the key generator (if any is defined 
                            ' for one of identity members). Also may use a switch on the entity's 
                            ' metatype and implement some specific processing for each one of 
                            ' registered types.
                            Dim objectRef As Object = args.[Object]
                            Dim keyMember = args.MetaType.IdentityMembers.First()
                            Dim keyValue As Integer = CInt(keyMember.StorageAccessor.GetBoxedValue(objectRef))
                            keyMember.StorageAccessor.SetBoxedValue( _
                                objectRef, System.Threading.Interlocked.Increment(keyValue) _
                                )
                            args.ContinueAction = ContinueAction.Retry
                            Exit Select
                        Case Else
                            Exit Select
                    End Select
                End If
                ' Handle concurrency errors.
            ElseIf errorType = GetType(ChangeConflictException) Then
 
                If args.ChangeConflict IsNot Nothing Then
 
                    ' Resolve the conflict based on a certain approach.
                    args.ChangeConflict.Resolve(RefreshMode.KeepChanges, True)
 
                    ' If the corresponding row is deleted from the database, pass to the next entities.
                    ' Otherwise, retry updating the entity.
                    If args.ChangeConflict.IsDeleted Then
                        args.ContinueAction = ContinueAction.[Continue]
                    Else
                        args.ContinueAction = ContinueAction.Retry
                    End If
 
                    ' It is hardly the case for the current sample, but the conflict may be unresolved 
                    ' for some reason. It is then a good idea to nevertheless follow the conflict mode 
                    ' specified in the SubmitChanges call.
                    If Not args.ChangeConflict.IsResolved Then
                        args.ContinueAction = _
                            If(args.ConflictMode = ConflictMode.ContinueOnConflict, _
                               ContinueAction.[Continue], ContinueAction.Abort)
                    End If
 
                End If
            End If
 
        End Sub
    End Class
End Namespace

The SubmitErrorEventArgs type provides the ContinueAction property to control the way the context acts after the issue is (or is not) resolved. It may be set to one of the ContinueAction enumeration values:

The default value is ContinueAction.Abort, so args.ContinueAction should be changed to either Continue or Retry if you do want to proceed with the submit.

If the errors are not resolved in the OnSubmitError method, the exception that will be thrown depending on the ContinueAction modes you used:

When using the ContinueAction.Retry option, it is possible that the error occurs again (e.g., the new key already exists as well). To ensure that the current entity (or batch) is not being submitted in an infinite loop, the SubmitErrorEventArgs type provides the CurrentUnitSubmitAttempt property. It specifies how many times we've tried to resolve the issue before:


C#csharpCopy Code
// Ensure that submitting the current entity does not enter an infinite loop.
if (args.CurrentUnitSubmitAttempt >= 3)
    return;
Visual BasicCopy Code
' Ensure that submitting the current entity does not enter an infinite loop.
If args.CurrentUnitSubmitAttempt >= 3 Then
    Return
End If

If you use this approach, it is recommended to use some maximal number of update attempts, to ensure that the loop does end eventually.

If the exact entity on which the submit failed can be determined, the SubmitErrorEventArgs parameter includes the information about it: the 'Object' property holds the reference to this entity, 'MetaType' keeps the entity type metadata, and 'ChangAction' specifies the kind of the operation during which the problem occurred. In some situations, like during batch updates, the specific entity causing the issue cannot be detected, so the Object and MetaType properties are set to null. We don't treat this type of problems in the sample, so just let the context to throw an exception:


C#csharpCopy Code
// Resolve errors occurred with a particular entity only.
if (args.Object == null || args.MetaType == null)
    return;
Visual BasicCopy Code
' Resolve errors occurred with a particular entity only.
If args.[Object] Is Nothing OrElse args.MetaType Is Nothing Then
    Return
End If

The most common errors that may occur during a submit are server exceptions and concurrency conflicts. Based on the Error property of SubmitErrorEventArgs, we determine the exception type and resolve the server and concurrency errors separately:


C#csharpCopy Code
Type errorType = args.Error.GetType();
 
// Handle common server errors (e.g., invalid key).
if (errorType == typeof(LinqCommandExecutionException)) {
    ...
}
// Handle concurrency errors.
else if (errorType == typeof(ChangeConflictException)) {
    ... 
}
Visual BasicCopy Code
Dim errorType As Type = args.[Error].[GetType]()
 
' Handle common server errors (e.g., invalid key).
If errorType = GetType(LinqCommandExecutionException) Then
 
' Handle concurrency errors.
ElseIf errorType = GetType(ChangeConflictException) Then
End If

General Server Errors

As for the server errors, they are wrapped into LinqCommandExecutionExceptions, so we should get the inner exception of 'args.Error'. After that, we may process this exception's message to determine what exactly happened, or cast it to a provider-specific exception type to get some more details. For example, Oracle servers return error codes precisely specifying the error's origin. The dotConnect for Oracle provider keeps such codes in the OracleException.Code property:


C#csharpCopy Code
// Cast the inner exception to a provider-specific one to be able to get more details 
// about it.
if (inner is OracleException)
{
 
    OracleException oracleException = inner as OracleException;
    ...
    switch (oracleException.Code) {
         // "ORA-0001: unique constraint violated."
        case 1: ...
    }
}
Visual BasicCopy Code
' Cast the inner exception to a provider-specific one to be able to get more details 
' about it.
If TypeOf inner Is OracleException Then
 
    Dim oracleException As OracleException = TryCast(inner, OracleException)
    ...
    Select Case oracleException.Code
         ' "ORA-0001: unique constraint violated."
        Case 1
            ...
    End Select
End If

For example, we may try resolving the issue with using an existing primary key. A minor difficulty with this is that the entity is passed to OnSubmitError 'typeless', so we have to address to the metadata to work with the entity in a type-independent way, or implement a switch clause based on the MetaType property. In the sample, we just increment the key value:


C#csharpCopy Code
object objectRef = args.Object;
var keyMember = args.MetaType.IdentityMembers.First();
int keyValue = (int)keyMember.StorageAccessor.GetBoxedValue(objectRef);
keyMember.StorageAccessor.SetBoxedValue(ref objectRef, ++keyValue);
Visual BasicCopy Code
Dim objectRef As Object = args.[Object]
Dim keyMember = args.MetaType.IdentityMembers.First()
Dim keyValue As Integer = CInt(keyMember.StorageAccessor.GetBoxedValue(objectRef))
keyMember.StorageAccessor.SetBoxedValue( _
    objectRef, System.Threading.Interlocked.Increment(keyValue) _
    )

In a real application, a better way would be, e.g., to search for a key generator and (if any generator is found) get a new value from it.

After getting a new key value, we instruct the context to retry inserting this entity:


C#csharpCopy Code
args.ContinueAction = ContinueAction.Retry;
Visual BasicCopy Code
args.ContinueAction = ContinueAction.Retry

Concurrency Conflicts

As for concurrency conflicts, the SubmitErrorEventArgs type has the ChangeConflict property specifying the conflict details. Using this property, one can resolve concurrency issues just as it is described in the corresponding topic.

The minor difference is in treating the entities that were deleted: when you try to update/delete a row that is already deleted by someone else, resolving such a conflict means detaching the entity from the cache. The proper action in this case is ContinueAction.Continue rather than ContinueAction.Retry:


C#csharpCopy Code
// Resolve the conflict based on a certain approach.
args.ChangeConflict.Resolve(RefreshMode.KeepChanges, true);
 
// If the corresponding row is deleted from the database, pass to the next entities.
// Otherwise, retry updating the entity.
if (args.ChangeConflict.IsDeleted)
    args.ContinueAction = ContinueAction.Continue;
else
    args.ContinueAction = ContinueAction.Retry;
Visual BasicCopy Code
' Resolve the conflict based on a certain approach.
args.ChangeConflict.Resolve(RefreshMode.KeepChanges, True)
 
' If the corresponding row is deleted from the database, pass to the next entities.
' Otherwise, retry updating the entity.
If args.ChangeConflict.IsDeleted Then
    args.ContinueAction = ContinueAction.[Continue]
Else
    args.ContinueAction = ContinueAction.Retry
End If

The reason is that no actual commands should be executed at the server, and making the context to submit the entity again will indeed result in repeating the error.

Another note on resolving concurrency issues in OnSubmitError is that ContinueAction has higher priority than ConflictMode: if the changes are being submitted with the ConflictMode.ContinueOnConflict option, and the ContinueAction is set to 'Abort', the context does fail on the very first error. So it may be useful to follow the specified ConflictMode if the concurrency error is not resolved in the OnSubmitError method:


C#csharpCopy Code
if (!args.ChangeConflict.IsResolved)
    args.ContinueAction = args.ConflictMode == ConflictMode.ContinueOnConflict ?
        ContinueAction.Continue :
        ContinueAction.Abort;
Visual BasicCopy Code
If Not args.ChangeConflict.IsResolved Then
    args.ContinueAction = _
        If(args.ConflictMode = ConflictMode.ContinueOnConflict, _
           ContinueAction.[Continue], ContinueAction.Abort)
End If

In the sample, we treat all conflicts in the same way and so there should be no situation in which a concurrency issue is not resolved. However, this can occur if some subtler logic is used. In such a situation, it is recommended to continue submitting other entities if the ConflictMode option is 'ContinueOnConflict'.

Restrictions

There are some restrictions on the OnSubmitError implementation: