使用属性值

大多数情况下,实体框架将负责跟踪实体实例属性的状态、原始值和当前值。 但在某些情况下(例如断开连接时),可能需要查看或操作 EF 拥有的属性相关信息。 本主题所介绍的方法同样适用于查询使用 Code First 和 EF 设计器创建的模型。

实体框架跟踪被跟踪实体的每个属性的两个值。 如名称所示,当前值是实体中属性的当前值。 原始值是从数据库中查询实体时或将实体附加到上下文时该属性具有的值。

使用属性值有两种常规机制:

  • 可以使用 Property 方法以强类型方式获取单个属性的值。
  • 可以将实体的所有属性的值读入 DbPropertyValues 对象。 然后,DbPropertyValues 充当类似字典的对象,以允许读取和设置属性值。 DbPropertyValues 对象中的值可以从另一个 DbPropertyValues 对象中的值设置,也可以从某个其他对象中的值设置,例如实体的另一个副本或一个简单数据传输对象 (DTO)。

以下部分演示了使用上述两种机制的示例。

获取并设置单个属性的当前值或原始值

下面的示例演示如何读取属性的当前值,然后将该值设置为新值:

using (var context = new BloggingContext())
{
    var blog = context.Blogs.Find(3);

    // Read the current value of the Name property
    string currentName1 = context.Entry(blog).Property(u => u.Name).CurrentValue;

    // Set the Name property to a new value
    context.Entry(blog).Property(u => u.Name).CurrentValue = "My Fancy Blog";

    // Read the current value of the Name property using a string for the property name
    object currentName2 = context.Entry(blog).Property("Name").CurrentValue;

    // Set the Name property to a new value using a string for the property name
    context.Entry(blog).Property("Name").CurrentValue = "My Boring Blog";
}

使用 OriginalValue 属性(而不是 CurrentValue 属性)读取或设置原始值。

请注意,使用字符串指定属性名称时,返回的值类型为“对象”。 另一方面,如果使用 Lambda 表达式,则返回的值是强类型。

仅当新值不同于旧值时,像这样设置属性值才会将属性标记为已修改。

如果以此方式设置属性值,即使 AutoDetectChanges 已关闭,也会自动检测更改。

获取并设置未映射属性的当前值

还可以读取未映射到数据库的属性的当前值。 有关未映射属性的示例,可以看看博客上的 RssLink 属性。 此值可能基于 BlogId 计算,因此不需要存储在数据库中。 例如:

using (var context = new BloggingContext())
{
    var blog = context.Blogs.Find(1);
    // Read the current value of an unmapped property
    var rssLink = context.Entry(blog).Property(p => p.RssLink).CurrentValue;

    // Use a string to specify the property name
    var rssLinkAgain = context.Entry(blog).Property("RssLink").CurrentValue;
}

如果属性公开 setter,也可以设置当前值。

对未映射属性执行实体框架验证时,读取未映射属性的值非常有用。 出于相同的原因,可以读取和设置当前未被上下文跟踪的实体的属性的当前值。 例如:

using (var context = new BloggingContext())
{
    // Create an entity that is not being tracked
    var blog = new Blog { Name = "ADO.NET Blog" };

    // Read and set the current value of Name as before
    var currentName1 = context.Entry(blog).Property(u => u.Name).CurrentValue;
    context.Entry(blog).Property(u => u.Name).CurrentValue = "My Fancy Blog";
    var currentName2 = context.Entry(blog).Property("Name").CurrentValue;
    context.Entry(blog).Property("Name").CurrentValue = "My Boring Blog";
}

请注意,对于未映射的属性或未被上下文跟踪的实体的属性,原始值不可用。

检查属性是否标记为已修改

下面的示例演示如何检查单个属性是否标记为已修改:

using (var context = new BloggingContext())
{
    var blog = context.Blogs.Find(1);

    var nameIsModified1 = context.Entry(blog).Property(u => u.Name).IsModified;

    // Use a string for the property name
    var nameIsModified2 = context.Entry(blog).Property("Name").IsModified;
}

调用 SaveChanges 时,已修改的属性的值将作为更新发送到数据库。

将属性标记为已修改

以下示例演示如何强制将单个属性标记为已修改:

using (var context = new BloggingContext())
{
    var blog = context.Blogs.Find(1);

    context.Entry(blog).Property(u => u.Name).IsModified = true;

    // Use a string for the property name
    context.Entry(blog).Property("Name").IsModified = true;
}

将属性标记为“已修改”会强制在调用 SaveChanges 时将更新发送到属性的数据库,即使该属性的当前值与原始值相同。

当前无法在将单个属性标记为已修改后,将该属性重置为未修改。 我们计划在将来的版本中支持此功能。

读取实体的所有属性的当前值、原始值和数据库值

下面的示例演示如何读取实体的所有映射属性的当前值、原始值以及数据中的实际值。

using (var context = new BloggingContext())
{
    var blog = context.Blogs.Find(1);

    // Make a modification to Name in the tracked entity
    blog.Name = "My Cool Blog";

    // Make a modification to Name in the database
    context.Database.SqlCommand("update dbo.Blogs set Name = 'My Boring Blog' where Id = 1");

    // Print out current, original, and database values
    Console.WriteLine("Current values:");
    PrintValues(context.Entry(blog).CurrentValues);

    Console.WriteLine("\nOriginal values:");
    PrintValues(context.Entry(blog).OriginalValues);

    Console.WriteLine("\nDatabase values:");
    PrintValues(context.Entry(blog).GetDatabaseValues());
}

public static void PrintValues(DbPropertyValues values)
{
    foreach (var propertyName in values.PropertyNames)
    {
        Console.WriteLine("Property {0} has value {1}",
                          propertyName, values[propertyName]);
    }
}

当前值是实体的属性当前包含的值。 原始值是查询实体时从数据库读取的值。 数据库值是当前存储在数据库中的值。 当数据库中的值在查询实体后可能发生更改时(例如,其他用户对数据库进行并发编辑时),获取数据库值非常有用。

设置另一个对象中的当前值或原始值

可以通过复制另一个对象中的值来更新被跟踪实体的当前值或原始值。 例如:

using (var context = new BloggingContext())
{
    var blog = context.Blogs.Find(1);
    var coolBlog = new Blog { Id = 1, Name = "My Cool Blog" };
    var boringBlog = new BlogDto { Id = 1, Name = "My Boring Blog" };

    // Change the current and original values by copying the values from other objects
    var entry = context.Entry(blog);
    entry.CurrentValues.SetValues(coolBlog);
    entry.OriginalValues.SetValues(boringBlog);

    // Print out current and original values
    Console.WriteLine("Current values:");
    PrintValues(entry.CurrentValues);

    Console.WriteLine("\nOriginal values:");
    PrintValues(entry.OriginalValues);
}

public class BlogDto
{
    public int Id { get; set; }
    public string Name { get; set; }
}

运行上述代码将打印出以下内容:

Current values:
Property Id has value 1
Property Name has value My Cool Blog

Original values:
Property Id has value 1
Property Name has value My Boring Blog

使用通过服务调用或 n 层应用程序中的客户端获取的值更新实体时,有时会使用此方法。 请注意,只要使用的对象具有与实体名称匹配的属性,该对象的类型就不必与实体的类型相同。 在以上示例中,BlogDTO 实例用于更新原始值。

请注意,只有从另一个对象复制时设置为不同值的属性才会标记为已修改。

设置字典中的当前值或原始值

可以通过复制字典中或某个其他数据结构中的值来更新被跟踪实体的当前值或原始值。 例如:

using (var context = new BloggingContext())
{
    var blog = context.Blogs.Find(1);

    var newValues = new Dictionary<string, object>
    {
        { "Name", "The New ADO.NET Blog" },
        { "Url", "blogs.msdn.com/adonet" },
    };

    var currentValues = context.Entry(blog).CurrentValues;

    foreach (var propertyName in newValues.Keys)
    {
        currentValues[propertyName] = newValues[propertyName];
    }

    PrintValues(currentValues);
}

使用 OriginalValues 属性(而不是 CurrentValues 属性)设置原始值。

使用 Property 设置字典中的当前值或原始值

使用上述 CurrentValues 或 OriginalValues 的替代方法是使用 Property 方法设置每个属性的值。 如果需要设置复杂属性的值,建议使用此方法。 例如:

using (var context = new BloggingContext())
{
    var user = context.Users.Find("johndoe1987");

    var newValues = new Dictionary<string, object>
    {
        { "Name", "John Doe" },
        { "Location.City", "Redmond" },
        { "Location.State.Name", "Washington" },
        { "Location.State.Code", "WA" },
    };

    var entry = context.Entry(user);

    foreach (var propertyName in newValues.Keys)
    {
        entry.Property(propertyName).CurrentValue = newValues[propertyName];
    }
}

在以上示例中,使用以点分隔的名称访问复杂属性。 有关访问复杂属性的其他方法,请参阅本主题后面专门介绍复杂属性的两个部分。

创建包含当前值、原始值或数据库值的克隆对象

从 CurrentValues、OriginalValues 或 GetDatabaseValues 中返回的 DbPropertyValues 对象可用于创建实体克隆。 此克隆将包含用于创建它的 DbPropertyValues 对象中的属性值。 例如:

using (var context = new BloggingContext())
{
    var blog = context.Blogs.Find(1);

    var clonedBlog = context.Entry(blog).GetDatabaseValues().ToObject();
}

请注意,返回的对象不是实体,并且不会被上下文跟踪。 返回的对象也未设置与其他对象的任何关系。

克隆的对象有助于解决与数据库的并发更新相关的问题,尤其是使用的 UI 涉及数据绑定到特定类型的对象时。

获取并设置复杂属性的当前值或原始值

可以使用 Property 方法读取和设置整个复杂对象的值,就像对基元属性一样。 此外,还可以深入到复杂对象,并读取或设置该对象(甚至嵌套对象)的属性。 下面是一些示例:

using (var context = new BloggingContext())
{
    var user = context.Users.Find("johndoe1987");

    // Get the Location complex object
    var location = context.Entry(user)
                       .Property(u => u.Location)
                       .CurrentValue;

    // Get the nested State complex object using chained calls
    var state1 = context.Entry(user)
                     .ComplexProperty(u => u.Location)
                     .Property(l => l.State)
                     .CurrentValue;

    // Get the nested State complex object using a single lambda expression
    var state2 = context.Entry(user)
                     .Property(u => u.Location.State)
                     .CurrentValue;

    // Get the nested State complex object using a dotted string
    var state3 = context.Entry(user)
                     .Property("Location.State")
                     .CurrentValue;

    // Get the value of the Name property on the nested State complex object using chained calls
    var name1 = context.Entry(user)
                       .ComplexProperty(u => u.Location)
                       .ComplexProperty(l => l.State)
                       .Property(s => s.Name)
                       .CurrentValue;

    // Get the value of the Name property on the nested State complex object using a single lambda expression
    var name2 = context.Entry(user)
                       .Property(u => u.Location.State.Name)
                       .CurrentValue;

    // Get the value of the Name property on the nested State complex object using a dotted string
    var name3 = context.Entry(user)
                       .Property("Location.State.Name")
                       .CurrentValue;
}

使用 OriginalValue 属性(而不是 CurrentValue 属性)获取或设置原始值。

请注意,Property 或 ComplexProperty 方法均可用于访问复杂属性。 但是,若要通过其他 Property 或 ComplexProperty 调用深入到复杂对象,必须使用 ComplexProperty 方法。

使用 DbPropertyValues 访问复杂属性

使用 CurrentValues、OriginalValues 或 GetDatabaseValues 获取实体的所有当前值、原始值或数据库值时,任何复杂属性的值都将作为嵌套的 DbPropertyValues 对象返回。 然后,可以使用这些嵌套对象获取复杂对象的值。 例如,以下方法将打印出所有属性的值,包括任何复杂属性和嵌套的复杂属性的值。

public static void WritePropertyValues(string parentPropertyName, DbPropertyValues propertyValues)
{
    foreach (var propertyName in propertyValues.PropertyNames)
    {
        var nestedValues = propertyValues[propertyName] as DbPropertyValues;
        if (nestedValues != null)
        {
            WritePropertyValues(parentPropertyName + propertyName + ".", nestedValues);
        }
        else
        {
            Console.WriteLine("Property {0}{1} has value {2}",
                              parentPropertyName, propertyName,
                              propertyValues[propertyName]);
        }
    }
}

若要打印出所有当前属性值,应按如下所示调用该方法:

using (var context = new BloggingContext())
{
    var user = context.Users.Find("johndoe1987");

    WritePropertyValues("", context.Entry(user).CurrentValues);
}