Search code examples
c#compiler-constructioncompact-frameworkclrdispose

Weird CLR/Compiler behavior in Release Mode but not in Debug Mode


I am developing an application for Windows CE and .NET Compact Framework 3.5.

The code runs fine in Debug mode but when I change the mode to Release I get various exceptions. I think it is related to some optimizations compiler trying to achieve in Release mode, like disposing and garbage collecting an object too early.

Following method, trying to insert entities to Sql Compact database, throws two exceptions(I think it is randomly):

public int Add<T>(List<T> entities)
    {
        int rowsEffected = 0;
        EntityMetadata metadata = GetMetadata<T>();
        using (SqlCeCommand command = new SqlCeCommand())
        {
            command.Connection = connection;
            command.CommandType = CommandType.TableDirect;
            command.CommandText = metadata.EntityMapAttribute.TableName;
            SqlCeResultSet set = command.ExecuteResultSet(ResultSetOptions.Scrollable | ResultSetOptions.Updatable);

            // Get generated Id, used in the loop below
            command.CommandType = CommandType.Text;
            command.CommandText = "SELECT @@IDENTITY";
            foreach (var entity in entities)
            {
                SqlCeUpdatableRecord record = set.CreateRecord();
                PropertyMetadata pkPropertyMetadata = null;
                foreach (var prop in metadata.PropertyMetadataList)
                {
                    if (prop.Attribute.IsPK)
                    {
                        // Identify PK Property, avoid setting values (db automatically sets id)
                        pkPropertyMetadata = prop;
                    }
                    else
                    {
                        object columnValue = prop.GetAccesssorDelegates<T>().Getter(entity);
                        record.SetValue(prop.Attribute.ColumnNumber, columnValue);
                    }
                }
                set.Insert(record);

                // Get Id of the inserted entity
                if (pkPropertyMetadata != null)
                {
                    object rawid = command.ExecuteScalar();
                    object convertedId = Convert.ChangeType(rawid, pkPropertyMetadata.Attribute.ColumnType, null);
                    pkPropertyMetadata.GetAccesssorDelegates<T>().Setter(entity, convertedId);
                }
                rowsEffected++;
            }
            return rowsEffected;
        }
    }

Exception 1:

Test 'M:Babil04_Mobil.Tests.ORMTests.Engine_Works' failed: Attempted to read or write protected memory. This is often an indication that other memory is corrupt.
System.AccessViolationException: Attempted to read or write protected memory. This is often an indication that other memory is corrupt.
at System.Data.SqlServerCe.NativeMethods.ExecuteQueryPlan(IntPtr pTx, IntPtr pQpServices, IntPtr pQpCommand, IntPtr pQpPlan, IntPtr prgBinding, Int32 cDbBinding, IntPtr pData, Int32& recordsAffected, ResultSetOptions& cursorCapabilities, IntPtr& pSeCursor, Int32& fIsBaseTableCursor, IntPtr pError)
at System.Data.SqlServerCe.SqlCeCommand.ExecuteCommandText(IntPtr& pCursor, Boolean& isBaseTableCursor)
at System.Data.SqlServerCe.SqlCeCommand.ExecuteCommand(CommandBehavior behavior, String method, ResultSetOptions options)
at System.Data.SqlServerCe.SqlCeCommand.ExecuteScalar()
MORMEngine.cs(182,0): at MORM.MORMEngine.Add[T](List`1 entities)
Tests\ORMTests.cs(187,0): at Babil04_Mobil.Tests.ORMTests.Engine_Works()

Exception 2:

Test 'M:Babil04_Mobil.Tests.ORMTests.Can_Insert_Multiple' failed: Cannot access a disposed object.
Object name: 'SqlCeResultSet'.
    System.ObjectDisposedException: Cannot access a disposed object.
    Object name: 'SqlCeResultSet'.
    at System.Data.SqlServerCe.SqlCeResultSet.CreateRecord()
    MORMEngine.cs(162,0): at MORM.MORMEngine.Add[T](List`1 entities)
    Tests\ORMTests.cs(187,0): at Babil04_Mobil.Tests.ORMTests.Can_Insert_Multiple()

The unit test that calls the method throwing exception:

[Test]
    public void Can_Insert_Multiple()
    {
        MORMEngine engine = new MORMEngine(connectionString);
        engine.OpenConnection();

        List<TestInventory> inventories = new List<TestInventory>();
        for (int i = 0; i < 10000; i++)
        {
            inventories.Add(new TestInventory
            {
                Code = "test" + i
            });
        }

        Stopwatch watch = new Stopwatch();
        watch.Start();

        int rows = engine.Add<TestInventory>(inventories);

        watch.Stop();
        Console.WriteLine("Completed in {0} ms.", watch.ElapsedMilliseconds);
        Assert.That(rows == 10000);
    }

Expcetions say that SqlCeResultSet is already disposed. Neither I call Dispose() method on the object nor set it to null. Why does it get disposed? Why does it run fine in Debug mode but not in Release mode?

Any ideas would be appreciated.


Solution

  • Per my earlier comments - it appears that there are no references to engine in the test after the int rows = engine.Add<TestInventory>(inventories); line, and the only reference to engine (via an implicit this) in Add<T>(List<T>) is the line where the connection is accessed. Subsequent to this line, there are no further references to the engine, and so in Release mode it will become eligible for GC. It appears (given that my suggestion corrected the issue) that there may be a finalizer somewhere (either in your MORMEngine, or perhaps elsewhere) that is causing the connection to be disposed whilst Add<>() is still running. (In Debug mode, the lifetimes of objects is extended until they exit scope so as to simplify debugging, which is likely why this issue only appears in Release mode)

    To ensure that the engine remains alive throughout the call to Add<>(), the following approaches appear to work:

    ...
            watch.Start();
    
            int rows = engine.Add<TestInventory>(inventories);
    
            GC.KeepAlive(engine); // Ensure that the engine remains alive until Add completes
    
            watch.Stop();
    ...
    

    Or preferably explicitly dispose the engine with a using() statement:

    [Test]
    public void Can_Insert_Multiple()
    {
        using (MORMEngine engine = new MORMEngine(connectionString))
        {
            engine.OpenConnection();
    
            List<TestInventory> inventories = new List<TestInventory>();
            for (int i = 0; i < 10000; i++)
            {
                inventories.Add(new TestInventory
                {
                    Code = "test" + i
                });
            }
    
            Stopwatch watch = new Stopwatch();
            watch.Start();
    
            int rows = engine.Add<TestInventory>(inventories);
    
            watch.Stop();
            Console.WriteLine("Completed in {0} ms.", watch.ElapsedMilliseconds);
            Assert.That(rows == 10000);
        }
    }