1

I have a WCF project with a entities with lots of children. I have a business layer and a data access layer, in the data access layer I have repositories that retrieve and save to my database. My understanding of EF is that you create and destroy the DataContext as and when you need it. As an example, lets say I have the a Person entity, and a Book entity (this is not my application, but just to try and illustrate the problem).

Assume Person looks as follows.

Person
  string Name
  vitual ICollection<Book> Books

With Book maybe something like this

Book
 string Title
 Person PersonLending

Now in my BLL I want to read the person table and then assign a book to that person, but the person already exists in the database, so the BLL calls to the repository for a person entity.

var person = repository.GetPerson("John Doe");

My repository has this code.

using(var context = new MyContext())
{
  return (from p in context.Person
          where p.Name == person
          select p).FirstOrDefault());
}

Now in the BLL I create a new book and assign this person to it.

var book = new Book();
book.PersonLending = person;
book.Title = "New Book";

repository.SaveBook();

Finally in the repository I try to save back the book.

using(var context = new MyContext())
{
  context.Book.Add(book);
  context.SaveChanges();
}

Now what happens is I get two Person rows in the table. My understanding is that this is caused by the first context being destroyed, and the second context not knowing that Person already exists.

I have two questions I guess.

  1. What is the best practice for handling DataContext in WCF ? Should there just be a single datacontext being passed from class to class and down into the repositories.
  2. Or is there a way to make this save.

I have tried setting the EntityState to Unchanged on Person, but it doesn't seem to work.

Edit:

I have changed things to create a new DataContext per request (AfterReceiveRequest and BeforeSendReply).

public class EFWcfDataContextAttribute : Attribute, IServiceBehavior
{
    public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase){}

    public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters){}

    public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
    {
        foreach (ChannelDispatcher channelDispatcher in serviceHostBase.ChannelDispatchers)
        {
            foreach (var endpoint in channelDispatcher.Endpoints)
            {
                endpoint.DispatchRuntime.MessageInspectors.Add(new EFWcfDataContextInitializer());
                //endpoint.DispatchRuntime.InstanceContextInitializers.Add(new EFWcfDataContextInitializer());
            }
        }    
    }

Initializer

public class EFWcfDataContextInitializer : IDispatchMessageInspector
{
    public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
    {
        instanceContext.Extensions.Add(new EFWcfDataContextExtension(new MyDataContext()));
        return null;
    }

    public void BeforeSendReply(ref Message reply, object correlationState)
    {
        WcfDataContextFactory.Dispose();
    }
}

And the extension

public class EFWcfDataContextExtension : IExtension<InstanceContext>
{
    public ICoreDataContext DataContext { get; private set; }

    public EFWcfDataContextExtension(ICoreDataContext coreDataContext)
    {
        if(DataContext != null)
            throw new Exception("context is not null");

        DataContext = coreDataContext;
    }

    public void Attach(InstanceContext owner){}

    public void Detach(InstanceContext owner) {}
}

This seems to give a brand new problem. I get the current context by invoking OperationContext.Current.InstanceContext.Extensions.Find().DataContext, but it now seems that the two context influence each other. On the same request the first one will return a null record and the second one will succeed. They are both in unique sessions, and when they are both created they are null and created as new DataContext. When I check the Database.Connection property on the first it is closed, and manually trying to open it creates more errors. I really thought this would solve the issue.

I have also tried doing this with a IContractBehaviour, with the same result. So either I am doing something wrong or I am missing something obvious.

PS: I tried setting the state to Unchanged before I made the original post. PPS: In case anyone wonders, my datafactory simply has these two methods

  public static void Dispose()
    {
        ICoreDataContext coreDataContext = OperationContext.Current.InstanceContext.Extensions.Find<EFWcfDataContextExtension>().DataContext;
        coreDataContext.Dispose();
        coreDataContext = null;
    }

    public static ICoreDataContext GetCurrentContext()
    {
        var context =  OperationContext.Current.InstanceContext.Extensions.Find<EFWcfDataContextExtension>().DataContext;
        if (context != null)
        {
            if (context.Database.Connection.State == ConnectionState.Closed)
                context.Database.Connection.Open();
        }

        return context;
    }
Dirk
  • 794
  • 1
  • 5
  • 15

1 Answers1

1

Your understanding is absolutely correct. Once you pass the data back to the service the new context doesn't know neither the Book or Person. Calling Add on the Book has effect of marking every unknown entity in the object graph as Added. This is very big problem of detached scenarios.

The solution is not sharing the context That is the worst way to deal with the problem because it introduces a lot of other problems and at the end it will still not work. Use a new context per each service call.

Try this:

using(var context = new MyContext())
{
    context.Book.Attach(book);
    context.Entry(book).State = EntityState.Added;
    context.SaveChanges();
}

or this:

using(var context = new MyContext())
{
    context.Book.Add(book);
    context.Entry(book.PersonLending).State = EntityState.Unchanged;
    context.SaveChanges();
}

This problem is more complex and once you start to send more complicated object graphs with changes in relations. You will end up in loading object graph first and merging changes into attached entities.

Community
  • 1
  • 1
Ladislav Mrnka
  • 349,807
  • 56
  • 643
  • 654
  • Going to mark this as answered, though it didn't directly answer the question, it did point me in the right direction. http://mfelicio.wordpress.com/2010/02/07/managing-the-entity-framework-objectcontext-instance-lifetime-in-wcf-and-sharing-it-among-repositories/ is what I eventually used – Dirk Jun 15 '11 at 07:40