プロパティ値の操作

多くの場合、Entity Framework は、エンティティ インスタンスのプロパティの状態、元の値、および現在の値の追跡を行います。 ただし、接続しないシナリオなどでは、プロパティについて EF にある情報を表示または操作したい場合もあります。 このトピックで紹介するテクニックは、Code First および EF Designer で作成されたモデルに等しく使用できます。

Entity Framework は、追跡対象のエンティティのプロパティごとに、2 つの値を追跡します。 現在の値は、名前のとおり、エンティティのプロパティの現在の値です。 元の値は、エンティティがデータベースからクエリを実行されたとき、またはコンテキストにアタッチされたときに、プロパティに保持されていた値です。

プロパティ値を操作するために、次の 2 つの一般的なメカニズムがあります。

  • 1 つのプロパティの値は、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";
}

元の値の読み取りまたは設定には、CurrentValue プロパティの代わりに OriginalValue プロパティを使用します。

プロパティ名の指定に文字列を使用した場合、戻り値は "object" として型指定されることに注意してください。 一方、ラムダ式を使用した場合、戻り値は厳密に型指定されます。

このようにプロパティ値を設定した場合、新しい値が古い値と異なる場合にのみ、プロパティが変更済みとしてマークされます。

プロパティ値がこの方法で設定される場合、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;
}

プロパティがセッターを公開している場合は、現在の値を設定することもできます。

マップされていないプロパティの値の読み取りは、マップされていないプロパティの Entity Framework 検証を実行する場合に便利です。 同じ理由で、現在コンテキストによって追跡されていないエンティティのプロパティで、現在の値の読み取りおよび設定ができます。 次に例を示します。

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);
}

元の値を設定するには、CurrentValues プロパティの代わりに OriginalValues プロパティを使用します。

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];
    }
}

上の例では、ドット形式の名前を使用して複合プロパティにアクセスしています。 複合プロパティにアクセスするためのその他の方法については、このトピックの後の方の複合プロパティに関する 2 つのセクションを参照してください。

現在の値、元の値、またはデータベースの値を含む複製オブジェクトを作成する

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;
}

元の値の取得または設定には、CurrentValue プロパティの代わりに OriginalValue プロパティを使用します。

複合プロパティにアクセスするには、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);
}