LinqConnect Documentation
In This Topic
    Processing Errors During Submit - OnSubmitError Partial Method
    In This Topic
    Processing Errors During Submit - OnSubmitError Partial Method
    LinqConnect Documentation
    Processing Errors During Submit - OnSubmitError Partial Method
    [email protected]

    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:

    • Continue: specifies that the context should proceed with submitting the changes; for example, if the error is a concurrency conflict, this is equal to submitting changes in the ContinueOnConflict mode;
    • Retry: in this case, the context should try submitting the problem entity again; in particular, this is the way concurrency conflicts may be resolved;
    • Abort: the context should give up with the submit and throw an exception.

    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:

    • you get the exact error passed in the SubmitErrorEventArgs.Error property when you select the 'Abort' mode;
    • if you select the 'Continue' method for all unresolved errors, and all of them are related to concurrency, you get a ChangeConflictException;
    • if you select the 'Continue' method for all unresolved errors, but some of them are not caused by concurrency conflicts, you get a SubmitErrorException.

    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:

    • The SubmitChanges method should not be invoked inside of OnSubmitError. Just the fact the OnSubmitError method is invoked implies that submitting changes is already in progress. Trying to invoke SubmitChanges inside another SubmitChanges method is clearly an invalid action, so this will actually end with an InvalidOperationException.
    • Do not invoke the InsertOnSubmit/InsertAllOnSubmit, DeleteOnSubmit/DeleteAllOnSubmit, and Attach methods. Generally, there is no point to do so: up to the moment you get the submit error, the set of entities to persist has already been prepared, so the entities you would mark for insertion/deletion would not be saved in the current SubmitChanges call anyway. On the other hand, such operations need change tracking to be enabled, while the context temporarily disables tracking when persisting the changes. So again, trying to add or remove an entity will cause an InvalidOperationException.
    • It is not recommended to modify the other entities (i.e., those except the one causing an error) inside the OnSubmitError method. The reasons are the same: the changes won't be saved in the current SubmitChanges call, nor would they be saved during subsequent submits (as change tracking is disabled when you modify them). However, in this case you'll get no error since the list of tracked entities remains unchanged.