kungcc
kungcc

Reputation: 1852

Entity Framework Context 6.1.3 not Refreshed / Destroyed?

In this unittest I will verify that the MD5 of the Content byte column is calculated, persisted and fetched correctly.

However, it seems that the Entity Framework (6.1.3) context is not refreshed / destroyed, since after the raw SQL UPDATE that clearly takes effect, but not shown when fetching the row with a new context.

namespace UnitTests
{
    [TestClass]
    public class TestDataPacketServiceDebug
    {
        [TestInitialize]
        public void Setup()
        {
            CommonMethods.ResetDatabase();
            try
            {
                CommonMethods.ResetDataPacketDirectory();
            }
            catch (DirectoryNotFoundException)
            {
            }
        }

        [TestCategory("DataPacketService"), TestMethod]
        public void TestGetLocalFilePathDebug()
        {
            // Persist a DataPacket
            int dataPacketId;
            using (var testDBContext = new TestDBContext())
            {
                DataPacket dataPacket = new DataPacket
                {
                    Content = File.ReadAllBytes(@"Resources\SampleResources.zip"),
                    Description = "DataPacketSample consist of some random found .DLL files on disk",
                    Name = "SampleResources",
                    Version = "1"
                };
                testDBContext.DataPackets.Add(dataPacket);
                testDBContext.SaveChanges();
                dataPacketId = dataPacket.DataPacketId;
            }

            // Verify file path extraction
            using (var testDBContext = new TestDBContext())
            {
                DataPacket dataPacket = DataPacketService.GetByNameAndVersion("SampleResources", "1",
                    testDBContext);

                string extractedFilePath = DataPacketService.GetLocalFilePath(testDBContext,
                    dataPacket, "EntityFramework.dll");

                string validDestinationPath = String.Format(@"{0}\DataPackets\{1}_v{2}\EntityFramework.dll",
                    AppDomain.CurrentDomain.BaseDirectory, dataPacket.Name, dataPacket.Version);

                Assert.AreEqual(validDestinationPath, extractedFilePath);

                if (File.Exists(extractedFilePath) == false)
                {
                    Assert.Fail("SampleResources was not extracted correctly");
                }
            }
            // When setting a breakpoint here and take a look with external SQL Browser
            // (e.g. Microsoft SQL Server Management Studio), following is in order:
            // Note! Not all columns are shown
            // -----------------------------------------------------------------------------------------------
            // DataPacketId | Name            | RowVersion | Content     | MD5                      | Version
            //            1 | SampleResources | NULL       | 0x504B03... | 2zSV8IChaiyf0UfnezDHKg== | 1


            // Manually modify MD5 field in database for MD5 verification
            using (var testDBContext = new TestDBContext())
            {
                string sqlUpdate = String.Format("UPDATE dbo.DataPackets SET MD5 = 'another_MD5' WHERE DataPacketId = {0}",
                    dataPacketId);
                testDBContext.Database.ExecuteSqlCommand(sqlUpdate);
            }
            // When setting a breakpoint here we can clearly see that the row has been changed:
            // Note! Not all columns are shown
            // ----------------------------------------------------------------------------------
            // DataPacketId | Name            | RowVersion | Content     | MD5         | Version
            //            1 | SampleResources | NULL       | 0x504B03... | another_MD5 | 1

            // Verify MD5
            using (var testDBContext = new TestDBContext())
            {   
                // Fetch dataPacket with modified MD5
                DataPacket dataPacket = DataPacketService.GetByNameAndVersion("SampleResources", "1", testDBContext);

                // Verify that the raw SQL command has been successful:
                Assert.AreEqual("another_MD5", dataPacket.MD5);
                // BANG!!!!!!!!!!!!!!
                // Result Message:  Assert.AreEqual failed. Expected:< another_MD5 >.Actual:< 2zSV8IChaiyf0UfnezDHKg== >.
            }
        }
    }
}

Entity:

public class DataPacket
{
    /// <summary>
    /// Identifier
    /// </summary>
    public int DataPacketId { get; set; }

    /// <summary>
    /// Concurrency Token
    /// </summary>
    public byte[] RowVersion { get; set; }

    /// <summary>
    /// Name
    /// </summary>
    public string Name { get; set; }

    /// <summary>
    /// Description of data packet
    /// </summary>
    public string Description { get; set; }

    /// <summary>
    /// Version of data packet
    /// </summary>
    public string Version { get; set; }

    /// <summary>
    /// MD5 of the data packet (i.e. MD5 of Content byte array)
    /// </summary>
    public string MD5 { get; private set; }

    private byte[] content;

    /// <summary>
    /// Byte content of the data packet (i.e. 
    /// </summary>
    public byte[] Content
    {
        get { return content; }
        set
        {
            content = value;
            UpdateMD5();
        }
    }

    /// <summary>
    /// TestCase navigation DataPacket <== One-To-Many ==> TestCases
    /// </summary>
    public ICollection<TestCase> TestCases { get; set; } // DataPacket <== One-To-Many ==> TestCases

    /// <summary>
    /// Update MD5 checksum depending on content
    /// </summary>
    private void UpdateMD5()
    {
        if (content != null)
        {
            this.MD5 = GetMD5ForBytes(content);
        }
    }

    /// <summary>
    /// Get MD5 checksum for content byte array
    /// </summary>
    /// <param name="content">Content byte array</param>
    /// <returns>MD5 checksum</returns>
    public static String GetMD5ForBytes(byte[] content)
    {
        if (content != null)
        {
            System.Security.Cryptography.MD5 md5Object = System.Security.Cryptography.MD5.Create();
            return System.BitConverter.ToString(md5Object.ComputeHash(content)).Replace("-", "");
        }

        return null;
    }
}

GetByNameAndVersion

public static DataPacket GetByNameAndVersion(string name, string version, TestDBContext testDBContext)
        {
            IQueryable<DataPacket> query = testDBContext.Set<DataPacket>();
            query = query.Where(t => t.Name == name).Where(t => t.Version == version);
            return query.Single();
        }

Note! I am using a localDB database.

Upvotes: 3

Views: 329

Answers (1)

Ivan Stoev
Ivan Stoev

Reputation: 205629

It's not a EF context issue (it works as expected), but incorrect test / logic in your DataPacket class.

You have two related properties, both mapped to database table columns:

/// <summary>
/// MD5 of the data packet (i.e. MD5 of Content byte array)
/// </summary>
public string MD5 { get; private set; }

private byte[] content;

/// <summary>
/// Byte content of the data packet (i.e. 
/// </summary>
public byte[] Content
{
    get { return content; }
    set
    {
        content = value;
        UpdateMD5();
    }
}

The client C# code can only set the Content which in turn updates the MD5 - fine. But what happens when EF loads the entity from the database? Indeed, it uses the same property setters (the private is not a problem because EF uses reflection / code generation, so it can call externally any type of setter).

Now everything depends on the order of calling setters. In your case the MD5 is called first, then the Content. Since your SQL command updated the MD5 column, but left Content unchanged, the first setter will set the MD5 value from the database, and the second setter will update it back from the Content. Which of course is causing the assertion to report failure.

It's up to you to decide whether updating the MD5 column in database through SQL is valid operation (basically leaving the MD5 and Content out of sync). The order of calling the property setters in undefined - currently if you move the MD5 property declaration after the Content property, the test will pass, but it's something you cannot rely on.

Upvotes: 1

Related Questions