13

Is it ever a good idea to work directly with the context? For example, say I have a database of customers and a user can search them by name, display a list, choose one, then edit that customer's properties.

It seems I should use the context to get a list of customers (mapped to POCOs or CustomerViewModels) and then immediately close the context. Then, when the user selects one of the CustomerViewModels in the list the customer properties section of the UI populates.

Next they can change the name, type, website address, company size, etc. Upon hitting a save button, I then open a new context, use the ID from the CustomerViewModel to retrieve that customer record, and update each of its properties. Finally, I call SaveChanges() and close the context. This is a LOT OF WORK.

My question is why not just work directly with the context leaving it open throughout? I have read using the same context with a long lifetime scope is very bad and will inevitably cause problems. My assumption is if the application will only be used by ONE person I can leave the context open and do everything. However, if there will be many users, I want to maintain a concise unit of work and thus open and close the context on a per request basis.

Any suggestions? Thanks.


@PGallagher - Thanks for the thorough answer.
@Brice - your input is helpful as well

However, @Manos D. the 'epitome of redundant code' comment concerns me a bit. Let me go through an example. Lets say I'm storing customers in a database and one of my customer properties is CommunicationMethod.

[Flags]
public enum CommunicationMethod
{
    None = 0,
    Print = 1,
    Email = 2,
    Fax = 4
}

The UI for my manage customers page in WPF will contain three check boxes under the customer communication method (Print, Email, Fax). I can't bind each checkbox to that enum, it doesn't make sense. Also, what if the user clicked that customer, gets up and goes to lunch... the context sits there for hours which is bad. Instead, this is my thought process.

End user chooses a customer from the list. I new up a context, find that customer and return a CustomerViewModel, then the context is closed (I've left repositories out for simplicity here).

using(MyContext ctx = new MyContext())
{
    CurrentCustomerVM = new CustomerViewModel(ctx.Customers.Find(customerId));
}

Now the user can check/uncheck the Print, Email, Fax buttons as they are bound to three bool properties in the CustomerViewModel, which also has a Save() method. Here goes.

public class CustomerViewModel : ViewModelBase
{
    Customer _customer;

    public CustomerViewModel(Customer customer)
    {
        _customer = customer;
    }


    public bool CommunicateViaEmail
    {
        get { return _customer.CommunicationMethod.HasFlag(CommunicationMethod.Email); }
        set
        {
            if (value == _customer.CommunicationMethod.HasFlag(CommunicationMethod.Email)) return;

            if (value)
                _customer.CommunicationMethod |= CommunicationMethod.Email;
            else
                _customer.CommunicationMethod &= ~CommunicationMethod.Email;
        }
    }
    public bool CommunicateViaFax
    {
        get { return _customer.CommunicationMethod.HasFlag(CommunicationMethod.Fax); }
        set
        {
            if (value == _customer.CommunicationMethod.HasFlag(CommunicationMethod.Fax)) return;

            if (value)
                _customer.CommunicationMethod |= CommunicationMethod.Fax;
            else
                _customer.CommunicationMethod &= ~CommunicationMethod.Fax;
        }
    }
    public bool CommunicateViaPrint
    {
        get { return _customer.CommunicateViaPrint.HasFlag(CommunicationMethod.Print); }
        set
        {
            if (value == _customer.CommunicateViaPrint.HasFlag(CommunicationMethod.Print)) return;

            if (value)
                _customer.CommunicateViaPrint |= CommunicationMethod.Print;
            else
                _customer.CommunicateViaPrint &= ~CommunicationMethod.Print;
        }
    }

    public void Save()
    {
        using (MyContext ctx = new MyContext())
        {
            var toUpdate = ctx.Customers.Find(_customer.Id);
            toUpdate.CommunicateViaEmail = _customer.CommunicateViaEmail;
            toUpdate.CommunicateViaFax = _customer.CommunicateViaFax;
            toUpdate.CommunicateViaPrint = _customer.CommunicateViaPrint;

            ctx.SaveChanges();
        }
    }
}

Do you see anything wrong with this?

BBauer42
  • 3,219
  • 7
  • 37
  • 74

3 Answers3

17

It is OK to use a long-running context; you just need to be aware of the implications.

A context represents a unit of work. Whenever you call SaveChanges, all the pending changes to the entities being tracked will be saved to the database. Because of this, you'll need to scope each context to what makes sense. For example, if you have a tab to manage customers and another to manage products, you might use one context for each so that when a users clicks save on the customer tab, all of the changes they made to products are not also saved.

Having a lot of entities tracked by a context could also slow down DetectChanges. One way to mitigate this is by using change tracking proxies.

Since the time between loading an entity and saving that entity could be quite long, the chance of hitting an optimistic concurrency exception is greater than with short-lived contexts. These exceptions occur when an entity is changed externally between loading and saving it. Handling these exceptions is pretty straightforward, but it's still something to be aware of.

One cool thing you can do with long-lived contexts in WPF is bind to the DbSet.Local property (e.g. context.Customers.Local). this is an ObservableCollection that contains all of the tracked entities that are not marked for deletion.

Hopefully this gives you a bit more information to help you decide which approach to help.

bricelam
  • 23,776
  • 7
  • 82
  • 103
  • Great answer. Thanks for your reply : the answer of an EF team member is always valuable here. – JYL Mar 05 '14 at 15:20
3

Microsoft Reference:

http://msdn.microsoft.com/en-gb/library/cc853327.aspx

They say;

Limit the scope of the ObjectContext

In most cases, you should create an ObjectContext instance within a using statement (Using…End Using in Visual Basic).

This can increase performance by ensuring that the resources associated with the object context are disposed automatically when the code exits the statement block.

However, when controls are bound to objects managed by the object context, the ObjectContext instance should be maintained as long as the binding is needed and disposed of manually.

For more information, see Managing Resources in Object Services (Entity Framework). http://msdn.microsoft.com/en-gb/library/bb896325.aspx

Which says;

In a long-running object context, you must ensure that the context is disposed when it is no longer required.


StackOverflow Reference:

This StackOverflow question also has some useful answers...

Entity Framework Best Practices In Business Logic?

Where a few have suggested that you promote your context to a higher level and reference it from here, thus keeping only one single Context.


My ten pence worth:

Wrapping the Context in a Using Statement, allows the Garbage Collector to clean up the resources, and prevents memory leaks.

Obviously in simple apps, this isn't much of a problem, however, if you have multiple screens, all using alot of data, you could end up in trouble, unless you are certain to Dispose your Context correctly.

Hence I have employed a similar method to the one you have mentioned, where I've added an AddOrUpdate Method to each of my Repositories, where I pass in my New or Modified Entity, and Update or Add it depending upon whether it exists.


Updating Entity Properties:

Regarding updating properties however, I've used a simple function which uses reflection to copy all the properties from one Entity to Another;

Public Shared Function CopyProperties(Of sourceType As {Class, New}, targetType As {Class, New})(ByVal source As sourceType, ByVal target As targetType) As targetType
    Dim sourceProperties() As PropertyInfo = source.GetType().GetProperties()
    Dim targetProperties() As PropertyInfo = GetType(targetType).GetProperties()

    For Each sourceProp As PropertyInfo In sourceProperties
        For Each targetProp As PropertyInfo In targetProperties
            If sourceProp.Name <> targetProp.Name Then Continue For

            ' Only try to set property when able to read the source and write the target
            '
            ' *** Note: We are checking for Entity Types by Checking for the PropertyType to Start with either a Collection or a Member of the Context Namespace!
            '
            If sourceProp.CanRead And _
                  targetProp.CanWrite Then
                ' We want to leave System types alone
                If sourceProp.PropertyType.FullName.StartsWith("System.Collections") Or (sourceProp.PropertyType.IsClass And _
                       sourceProp.PropertyType.FullName.StartsWith("System.Collections")) Or sourceProp.PropertyType.FullName.StartsWith("MyContextNameSpace.") Then
                    '
                    ' Do Not Store
                    '
                Else

                    Try

                        targetProp.SetValue(target, sourceProp.GetValue(source, Nothing), Nothing)

                    Catch ex As Exception

                    End Try

                End If
            End If

            Exit For
        Next
    Next

    Return target
End Function

Where I do something like;

dbColour = Classes.clsHelpers.CopyProperties(Of Colour, Colour)(RecordToSave, dbColour)

This reduces the amount of code I need to write for each Repository of course!

Community
  • 1
  • 1
PGallagher
  • 3,037
  • 2
  • 23
  • 36
  • Thanks for the thorough comments. Do you see anything wrong with the update to the OP I made under the horizontal rule? Thanks again. – BBauer42 Feb 22 '13 at 13:59
0

The context is not permanently connected to the database. It is essentially an in-memory cache of records you have loaded from disk. It will only request records from the database when you request a record it has not previously loaded, if you force it to refresh or when you're saving your changes back to disk.

Opening a context, grabbing a record, closing the context and then copying modified properties to an object from a brand new context is the epitomy of redundant code. You are supposed to leave the original context alone and use that to do SaveChanges().

If you're looking to deal with concurrency issues you should do a google search about "handling concurrency" for your version of entity framework.

As an example I have found this.

Edit in response to comment:

So from what I understand you need a subset of the columns of a record to be overridden with new values while the rest is unaffected? If so, yes, you'll need to manually update these few columns on a "new" object.

I was under the impression that you were talking about a form that reflects all the fields of the customer object and is meant to provide edit access to the entire customer record. In this case there's no point to using a new context and painstakingly copying all properties one by one, because the end result (all data overridden with form values regardless of age) will be the same.

Manos Dilaverakis
  • 5,661
  • 4
  • 26
  • 56