Monday, January 24, 2011

Using Dynamic Data with EF Code First and NuGet

Note: this post is a bit outdated. Checkout this other post for more up to date information on this topic.

Dynamic Data works out of the box with Entity Framework, but it takes a small trick to get it working with the latest EF Code First bits (known as CTP5).

Here is quick walk through of what you need to do.

As a first step, create a new ASP.NET Dynamic Data Entities Web Application. Then, let’s use NuGet to add EF Code First to your project (I never miss a chance to pitch my new product!). We’ll use it with SQL Compact, and also bring in a sample to get started.

Right click on References and choose ‘Add Library Package Reference’ to bring in the NuGet dialog. Go to the Online tab and type ‘efc’ (for EFCodeFirst) in the search box. Then install the EFCodeFirst.SqlServerCompact and EFCodeFirst.Sample packages:

image

Now we need to register our context with Dynamic Data, which is the part that requires special handling. The reason it doesn’t work the ‘usual’ way is that when using Code First, your context extends DbContext instead of ObjectContext, and Dynamic Data doesn’t know about DbContext (as it didn’t exist at the time).

I will show you two different approaches. The first is simpler but doesn’t work quite as well. The second works better but requires using a new library.

Approach #1: dig the ObjectContext out of the DbContext

The workaround is quite simple. In your RegisterRoutes method in global.asax, just add the following code (you’ll need to import System.Data.Entity.Infrastructure and the namespace where your context lives):

public static void RegisterRoutes(RouteCollection routes) {
DefaultModel.RegisterContext(() => {
    return ((IObjectContextAdapter)new BlogContext()).ObjectContext;
}, new ContextConfiguration() { ScaffoldAllTables = true });

So what this is really doing differently is provide a Lambda that can dig the ObjectContext out of your DbContext, instead of just passing the type to the context directly.

And that’s it, your app is ready to run!

image

One small glitch you’ll notice is that you get this EdmMetadatas entry in the list. This is a table that EF creates in the database to keep track of schema versions, but since we told Dynamic Data to Scaffold All Tables, it shows up. You can get rid of it by turning off ScaffoldAllTables, and adding a [ScaffoldTable(true)] attribute to the entity classes that you do want to see in there.

Another issue is that this approach doesn’t work when you need to register multiple models, due to the way the default provider uses the ObjectContext type as a key. Since we don’t actually extend ObjectContext, all contexts end up claiming the same key.

Approach #2: use the DynamicData.EFCodeFirstProvider library

This approach is simple to use, but just requires getting a library with a custom provider. If you don’t already have NuGet, get it from here.

Then install the DynamicData.EFCodeFirstProvider package in your project:

PM> Install-Package DynamicData.EFCodeFirstProvider
'EFCodeFirst 0.8' already installed.
Successfully installed 'DynamicData.EFCodeFirstProvider 0.1.0.0'.
WebApplicationDDEFCodeFirst already has a reference to 'EFCodeFirst 0.8'.
Successfully added 'DynamicData.EFCodeFirstProvider 0.1.0.0' to WebApplicationDDEFCodeFirst.

After that, this is what you would write to register the context in your global.asax:

DefaultModel.RegisterContext(
   new EFCodeFirstDataModelProvider(() => new BlogContext()),
   new ContextConfiguration() { ScaffoldAllTables = true });

And that’s it! This approach allows registering multiple contexts, and also fixes the issue mentioned above where EdmMetadatas shows up in the table list.

33 comments:

  1. Hi,

    I recently posted a question related to exactly this topic. I am not able to make this work and have tried various ways after reading in different articles.

    I even tried the method explained by you but still get following error

    I am using EF CodeFirst CTP5 with .Net Framework 4.0, Visual studio 2010.

    Complete post is at
    http://social.msdn.microsoft.com/Forums/en/adonetefx/thread/53fc5d64-5ff3-4e9f-ae7c-795d1eb750d2?outputAs=rss

    The member with identity '' does not exist in the metadata collection.
    Parameter name: identity
    Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.

    Exception Details: System.ArgumentException: The member with identity '' does not exist in the metadata collection.
    Parameter name: identity

    Source Error:


    Line 42: //DefaultModel.RegisterContext(dataModelprovider, new ContextConfiguration() { ScaffoldAllTables = true });
    Line 43:
    Line 44: DefaultModel.RegisterContext(() => { return ((IObjectContextAdapter)new ONS_DB2Model.ONS_DB2Entities()).ObjectContext; },
    Line 45: new ContextConfiguration()
    Line 46: { ScaffoldAllTables = true});




    I will really appreciate your help...

    ReplyDelete
  2. really helpful thanks david
    the only thing i found was that i had to install sqlcompact via the package manager console as it contains powershell scripts - other than that it worked like a charm thank you

    ReplyDelete
  3. @Dave: ah yes, this is something we fixed in NuGet 1.1, which we haven't quite released yet. With 1.0, using the Console is the way to go for this package.

    ReplyDelete
  4. Does it works with many to many relashionships now? Last time I tried (some month ago) they didn't play nice together.

    ReplyDelete
  5. @guillaume: hmmm, I did not try that. What issue were you running into? You may want to post to the Dynamic Data forum to make it easier to discuss.

    ReplyDelete
  6. me again :-)
    is there any way i can use multiple models only i've got an mvc web ptroject and am using areas to isolate the functionality and when i try to register the second model I get an "Server Error in '/' Application.
    Item has already been added. Key in dictionary: 'System.Data.Objects.ObjectContext' Key being added: 'System.Data.Objects.ObjectContext' " exception? I tried using various overloads of the registercontext method (eg not the factory ones) then i get the exception "The context type 'namespace.ContextDerivedClass' is not supported."

    I'm sure its possible but think i must be doing something wrong?
    Thanks in advance
    Dave

    ReplyDelete
  7. @Dave: You're right, I think there is a bug there that we need to look into.

    ReplyDelete
  8. Thanks David, I tried all sorts of workarounds but none of them work :-(

    ReplyDelete
  9. @Dave: I just updated the post with a whole new way to make this work. Please let me know how that works out for you.

    ReplyDelete
  10. Hi David. This is great thank you. Is the source code available? I tried going down the datamodelprovider route but couldn't find any documentation on it? Im guessing its somekind of internal dictionary, am i close?

    ReplyDelete
  11. @Dave: for the most part, it's a copy of the EF provider that comes with DD, with some small changes. At this time, I can't share the sources, but we are working on making this possible, hopefully within a couple weeks. When that happens, I'll update the post with a link to the sources. In the mean time, you can use reflector if you're curious :)

    ReplyDelete
  12. D'oh of course, should have tried that - very long day today. Thanks again

    ReplyDelete
  13. David, are there updated instructions for doing this with the official EF4.1 release? I have tried both approaches to no avail. My Dynamic Data project was working fine with EF4.0, but I want to take advantage of the new DBContext API in 4.1 (I am sticking with DB First however). Please advise?

    1. I initially tried the EFCodeFirstProvider approach, but found the version on NuGet pulls in CTP5 which conflicts with 4.1

    2. I then tried approach #1, but I get the following exception. Note that "Ticker" is one of my entity types which is defined in an entity model defined in a shared lib referenced by my web app project. The correct qualified name for the type is "MmmSharedLib.Ticker".

    System.ArgumentException was unhandled by user code
    Message=Could not find the CLR type for 'MmmModel.Ticker'.
    Source=System.Data.Entity
    StackTrace:
    at System.Data.Metadata.Edm.MetadataWorkspace.GetObjectSpaceType(StructuralType edmSpaceType)
    at System.Web.DynamicData.ModelProviders.EFDataModelProvider.GetClrType(EntityType entityType)
    at System.Web.DynamicData.ModelProviders.EFDataModelProvider.CreateTableProvider(EntitySet entitySet, EntityType entityType)
    at System.Web.DynamicData.ModelProviders.EFDataModelProvider..ctor(Object contextInstance, Func`1 contextFactory)
    at System.Web.DynamicData.ModelProviders.SchemaCreator.CreateDataModel(Object contextInstance, Func`1 contextFactory)
    at System.Web.DynamicData.MetaModel.RegisterContext(Func`1 contextFactory, ContextConfiguration configuration)
    at SitefinityWebApp.Global.RegisterRoutes(RouteCollection routes) in C:\Documents and Settings\Adam\My Documents\MMM\Dev\Sitefinity\TestProj2\Global.asax.cs:line 40
    at SitefinityWebApp.Global.Application_Start(Object sender, EventArgs e) in C:\Documents and Settings\Adam\My Documents\MMM\Dev\Sitefinity\TestProj2\Global.asax.cs:line 73
    InnerException:

    ReplyDelete
  14. I just updated the DynamicData.EFCodeFirstProvider package to work with the newer EntityFramework.

    ReplyDelete
  15. Hello,

    I'm using your package with CF context in which I have overriden SaveChanges, but it does not get called. It does not happen with first approach neither, DD takes base object and ignores my implementation. I suppose there is no way to get this working?

    ReplyDelete
  16. @obrad: you're right, this does seem broken. I think it's because we extract the ObjectContext out of the DbContext and then work with that, leaving the DbContext unused. I'd need to talk with EF team to see if there is a way around.

    ReplyDelete
  17. @obrad: dev from EF team wrote "This overridden SaveChanges was the one gotcha with that approach. You can use the “SavingChanges” event on the ObjectContext to do some of your pre-save logic, but there isn’t a way to do any post-save logic unless there was some callback they could hook into in dynamic data that was post save".

    Made pre-save logic is what you need anyway?

    ReplyDelete
  18. I'm getting "Item has already been added" exception on debugging my asp.net dynamic data entities web application at this line,

    XModel.RegisterContext(typeof(XModel.XEntities), new ContextConfiguration() { ScaffoldAllTables = true });

    What could be the reason? And, how to address?

    I do not get this error while running the app for the first time...

    ReplyDelete
  19. @Anand: I'd need to see more info, like a complete stack trace. Can you post a question to StackOverflow with more details? Blog comments get messy quickly when we need to go back and forth :)

    ReplyDelete
  20. @David: I have posted the question in Stackoverflow http://stackoverflow.com/questions/7851440/exception-item-has-already-been-added-in-metamodel-register. Your inputs on resolving this is much appreciated..Thanks...sorry for posting this info in this blog...

    ReplyDelete
  21. @Anand: no problem, I answered it there.

    ReplyDelete
  22. This is great. I've been trying for two days to add MVC 3 functionality to this also. Can you give me an idea of what the best way to do that would be?

    ReplyDelete
  23. @Ben: I'm not sure I understand how MVC3 relates to this. Can you be more specific?

    ReplyDelete
  24. @David: I'm sorry, it might be unrelated. I'm not very experienced w/ .NET. I think it is great that code first works with dynamic data. I was trying to reinvent it in MVC. But now I would just like to use MVC for custom views and dynamic data with the rest. I'm trying to figure out how to combine the two in a single app using the same code first model. It is painful teaching myself, but maybe it is an unrelated challenge. On another subject, would you still recommend a custom MetaModel for dealing w/ complex types in Dynamic Data? Will DD ever support complex types? Thanks.

    ReplyDelete
  25. @Ben: I would suggest posting the various questions to the Dynamic Data forum (http://forums.asp.net/1145.aspx) if they don't directly relate to the post. Blog comments just don't work well for general discussion, as no one else from the community sees it here. :)

    ReplyDelete
  26. Actually, if you don't mind, I thought of another and probably more important question. Does Dynamic Data still work with Code First when using EntityFramework.Migrations? Thanks again.

    ReplyDelete
  27. @Ben: yes, it should still work. In fact, the NuGet gallery (https://github.com/NuGet/NuGetGallery) is doing that. It uses migrations, and uses DD for its admin UI. Which might actually help with your other question about mixing MVC and DD :)

    ReplyDelete
  28. This comment has been removed by the author.

    ReplyDelete
  29. Hi, just in case someone wants to use Dynamic Data with EF v4.2... The second approach worked for me but I've also had to redirect the EF v4.1 to the EF v4.2 in the web.config. More info on how to do that in MSDN (http://msdn.microsoft.com/en-us/library/7wd6ex19%28v=vs.100%29.aspx)

    ReplyDelete
  30. David thanks again for your help. I ended up going with MvcScaffolding for the admin UI after I learned that I could foreach the scaffolding process right from the package manager console. It is a little slow when doing 50+ classes and requires playing with the T4 if you want to make changes but the performance seemed better than DD in the end. It would be cool to see T4 templates for MVC scaffolding in a similar structure to the Dynamic Data templates.

    ReplyDelete
  31. i have issue with the Dynamic Data Server Error with Entity Framework 4.3 and i have gone through the below thread and here i have found the solution. so if anyone have issue with Dynamic Data Server Error with Entity Framework 4.3 then you go through here:
    http://forums.techarena.in/software-development/1460986.htm

    ReplyDelete
  32. Hello, I ran into an issue when using Dynamic Data with EF 5.x DBContext generated classes and ManyToMany_Edit.ascx Dynamic Data Field Template, I order to make the Dynamic Data Field you need to change the code in ManyToMany_Edit.ascx.vb from :
    ' Go through all the territories (not just those for this employee)
    For Each childEntity As Object In childTable.GetQuery(objectContext)
    Dim actualTable As MetaTable = MetaTable.GetTable(childEntity.GetType())
    ' Create a checkbox for it
    Dim listItem As New ListItem(actualTable.GetDisplayString(childEntity), actualTable.GetPrimaryKeyString(childEntity))
    ' Make it selected if the current employee has that territory
    If Mode = DataBoundControlMode.Edit Then
    listItem.Selected = entityList.Contains(childEntity)
    End If
    CheckBoxList1.Items.Add(listItem)
    Next

    To this one:

    ' Go through all the territories (not just those for this employee)
    For Each childEntity As Object In childTable.GetQuery() ' GetQuery(objectContext)
    Dim actualTable As MetaTable = MetaTable.GetTable(childEntity.GetType().BaseType)
    ' Create a checkbox for it
    Dim listItem As New ListItem(actualTable.GetDisplayString(childEntity), actualTable.GetPrimaryKeyString(childEntity))
    ' Make it selected if the current employee has that territory
    If Mode = DataBoundControlMode.Edit Then
    listItem.Selected = entityList.Contains(childEntity)
    End If
    CheckBoxList1.Items.Add(listItem)
    Next
    Hope it Helps
    Regards,
    Cédric

    ReplyDelete
  33. Hello David!

    On my search to solve my problem with registering multiple models I found your homepage. I downloaded the sources and compiled it with EntityFramework 5.
    When I run my code, I get now always the error:

    The context type 'DynamicData.EFCodeFirstProvider.EFCodeFirstDataModelProvider' is not supported.

    When I switch the registration back how it was originally

    metaModel.RegisterContext(
    () => { return ((IObjectContextAdapter)MyContext()).ObjectContext; },
    ...);

    it works, but only for one model. Can you please help me? I am stuck...

    Thanks,
    Stefan

    ReplyDelete