For Visual Studio 2010, we added a number of extensibility points to the Entity Designer.

  1. Property Annotations: This let's you add custom annotations to most of the items in your EF model (based on a new feature in the EF Runtime known as Structured Annotations).
  2. Model Generation: You can hook into the process of reverse engineering a database (as well as the update process).
  3. Serialization: You can add to, or replace, the process that the Entity Designer uses to read and write source text to disk.

In this series of posts, I am going to focus on the first type.

Scenario:

We have a large model that will end up generating a large number of C# types and we don't want all of these types to be in the same C# namespace.  We want to be able to control, on a per-entity basis, the C# namespace in which the type will be generated into.

Solution:

In this first post, we will create a new Property Annotation Extension that will add a "Code Namespace" property to an entity's property window.  In the second post, we will customize the code generation template to use this new information and generate the code correctly. 

NOTE: These posts are written to work with Visual Studio 2010 Beta 2.

Walk through: Creating the Property Annotation Extension

Lets get started by setting up our extension project.  One tool that will make our job much easier is the ADO.NET Entity Data Model Designer Extension Starter Kit.  Download this starter kit and install it on the machine where you have Visual Studio 2010 Beta 2 installed.

  1. Create a new project using the starter kit.  File > New > Project.  In the upper right, search for "Starter Kit".  I called my project "CodeNamespaceExtension".
  2. You will now have a starting project for your extension.  If you build the project, you will notice that the starter kit creates a VSIX file for you, ready to be installed into VS (choose to show all files and then browse into bin\Debug).
  3. Open the "extension.vsixmanifest" file and edit it as needed. 
  4. We can delete some files that we won't need: "ModelConversionExtension.cs", "ModelGenerationExtension.cs", "ModelTransformExtension.cs", "MyNewProperty2.cs", and "MyNewProperty2Factory.cs" can all be removed.
  5. Rename "MyNewProperty.cs" to "CodeNamespace.cs", choose Yes to update references.
  6. Rename "MyNewPropertyFactory.cs" to "CodeNamespaceFactory.cs", choose Yes to update references.

NOTE: If you click on the "extension.vsixmanifest" file in Solution Explorer and look at its properties (select it and hit F4), you will notice that the "Build Action" is set to "VsixContent".  This build action is what tells the starter kit to include the file (or any file) in the VSIX file. 

Next, we will edit the CodeNamespace.cs class so that it tracks our custom XML annotation and shows the correct UI to the user.

  1. Open up "CodeNamespace.cs"
  2. Edit the _namespace property to something meaningful to you.
  3. Edit the line that creates the _xnMyNamespace property to define the full name to your XML element.
  4. Edit the _category property to set the name of the Category used in the property window.  Example:

        internal static readonly string _namespace = "http://schemas.microsoft.com/EFToolTime";
        internal static XName _xnMyNamespace = XName.Get("CodeNamespace", _namespace);
        internal const string _category = "EF ToolTime Extensions";

  1. You don't need to make any changes to the constructor, but a quick word here will be helpful.  The "parent" argument that is passed in is the XML element in the EDMX file that backs the item the user has selected.  So, if the user clicks on an entity, your property will be sent the <EntityType /> for that entity.  The "context" argument will be discussed later.
  2. Change the public property to return a string and name it "Namespace".
  3. Change the "DisplayName" and "Description" attributes to show what you want the user to see.  Example:

        [DisplayName("Code Namespace")]
        [Description("The code namespace to generate this entity in")]
        [Category(CodeNamespace._category)]
        [DefaultValue(false)]
        public string Namespace
        { 

We need to update the getter so that it works with a string instead of a bool.  The code first checks to see if the passed in "parent" has any children.  If so, it loops over all of the child elements looking for our XML element.  If found, that element's value is returned.

            get
            {
                string propertyValue = string.Empty;
                if (_parent.HasElements)
                {
                    foreach (XElement child in _parent.Elements())
                    {
                        if (child.Name == CodeNamespace._xnMyNamespace)
                        {
                            // annotation element exists, so get its value.
                            propertyValue = child.Value.Trim();
                            break;
                        }
                    }
                }
                return propertyValue;
            }

The setter is a little more complicated.  The first thing that we do is ask the context (the second argument passed to our constructor) for a change scope.  The string argument to "CreateChangeScope" will be shown to the user in the Visual Studio undo list.  In order to make any changes to the user's model, you must first create a change scope.  If you manipulate the XLinq tree outside of a change scope, an exception will be thrown.

The code then does one of three things.  First, if the parent element has no children, we just add our new element.  Second, if the parent already has one of our elements, we update it.  Third, if the parent element has children but does not contain our element, we add our new element after the last child.  This last part is important as the XSDs for CSDL and SSDL dictate the order of child elements in some circumstances.  To be safe, you should always add your custom elements after the last child.

Finally, once all of the changes have been made we call Complete() on the change scope.  If the Dispose() method is called on a change scope before a call to Complete(), then all changes are rolled back.  For this reason, it is highly recommended that you always wrap your change scopes in a using statement.

            set
            {
                using (EntityDesignerChangeScope scope =
                    _context.CreateChangeScope("Set CodeNamespace"))
                {
                    if (_parent.HasElements)
                    {
                        XElement nsElement = null;
                        XElement lastChild = null;
                        foreach (XElement child in _parent.Elements())
                        {
                            lastChild = child;
                            if (child.Name == CodeNamespace._xnMyNamespace)
                            {
                                nsElement = child;
                                break;
                            }
                        }

                        if (nsElement == null)
                        {
                            // element does not exist, so create a new one as
                            // the last child of the EntityType element.
                            lastChild.AddAfterSelf(new XElement(_xnMyNamespace, value));
                        }
                        else
                        {
                            // element already exists under the EntityType element,
                            // so update its value.
                            nsElement.SetValue(value);
                        }
                    }
                    else
                    {
                        // element has no child elements so create a new MyNewProperty
                        // element as its first child.
                        _parent.Add(new XElement(_xnMyNamespace, value));
                    }

                    // commit the changes.
                    scope.Complete();
                }
            }

We are done with "CodeNamespace.cs".  Be sure that you can compile your project.  Fix any errors and close the file.

NOTE: The context class is passed to your property extension's constructor.  With it, you have access to the file being edited (via the ProjectItem property), the project the user is working in (via the Project property) and the version of the EF that the user's model is targeting (via the EntityFrameworkVersion property).

We don't need to make any changes to the "CodeNamespaceFactory.cs" class but some quick notes are warranted, so let's open the factory class.  The line here that you will probably find yourself changing the most as you develop extensions is the EntityDesignerExtendedProperty attribute.  This attribute tells the Entity Designer the type of items that this new property extension applies to.  The attribute can be a list of types OR'd together.  For example, the following line would cause our extension to show up for both EntityTypes and Properties in the CSDL.

    [EntityDesignerExtendedProperty(EntityDesignerSelection.ConceptualModelEntityType | EntityDesignerSelection.ConceptualModelProperty)]

For our case, we just want our extension to show up for EntityTypes, so we don't need to change any code in this file.

We are ready to build and install our extension.

  1. Build your solution, fix any errors that show up
  2. Right click on the project name in Solution Explorer and choose "Open Folder in Windows Explorer"
  3. Close Visual Studio
  4. Navigate into "bin\Debug"
  5. Double-click on your VSIX file, follow the instructions

Launch Visual Studio again and click on the Extension Manager.  It is on the standard tool bar, but also accessible from the Tools menu.  You should see your extension there, and it should be enabled.  If it isn't there, go back and double-click on your VSIX file again and ensure that it reports success.

Create a new project or open up an existing project with an EDMX file, and open the Entity Model.  When you click on an entity on the designer canvas, you will now see the line for "Code Namespace" in the property window.  If you select this line in the property window, you will see the help text display at the bottom of the property window.  If you choose the Categorized view (left most icon in the property window's tool bar), you will see it inside the custom category we defined in the Catogory attribute.

Here is what the XML looks like when I add a value in my extension.

<EntityType Name="employee">
  <Key>
    <PropertyRef Name="emp_id" />
  </Key>
  <Property Name="emp_id" Type="String" Nullable="false" MaxLength="9"
            Unicode="false" FixedLength="true" />
  <Property Name="fname" Type="String" Nullable="false" MaxLength="20"
            Unicode="false" FixedLength="false" />
  <Property Name="minit" Type="String" MaxLength="1" Unicode="false"
            FixedLength="true" />
  <Property Name="lname" Type="String" Nullable="false" MaxLength="30"
            Unicode="false" FixedLength="false" />
  <Property Name="job_id" Type="Int16" Nullable="false" />
  <Property Name="job_lvl" Type="Byte" />
  <Property Name="pub_id" Type="String" Nullable="false" MaxLength="4"
            Unicode="false" FixedLength="true" />
  <Property Name="hire_date" Type="DateTime" Nullable="false" />
  <NavigationProperty Name="job"
            Relationship="pubsModel.FK__employee__job_id__25869641"
            FromRole="employee" ToRole="jobs" />
  <NavigationProperty Name="publisher"
            Relationship="pubsModel.FK__employee__pub_id__286302EC"
            FromRole="employee" ToRole="publishers" />
  <a:CodeNamespace xmlns:a="http://schemas.microsoft.com/EFToolTime">Publishing</a:CodeNamespace>
</EntityType>
 

At the end we see our structured annotation correctly added to our entity.  Congratulations, you have now extended the Entity Designer!

In Part 2, we will look at customizing the default T4 code generation template to use this new data and correctly place our entity classes into their namespace.

More information on Entity Designer Extensibility @ MSDN.