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