Randy
Randy

Reputation: 291

What T4 file is used to generate an EDMX from database via "Update Model From Database"?

When working with an EF4 (edmx) model, we frequently need to process an "Update Model From Database". Commonly, we need to just delete table(s) and let them fully regenerate from the database.

The issue at hand is that we have multiple recursive relationships/properties. By default, the "update Model From Database" process creates the property with the object's name and then adds a 1, 2, 3, etc. for each additional relationship. So if I have a table of "companies" where it points to itself multiple times (like parent company and dba company), currently the edmx results in Company1 and Company2. I need to control the naming of them....not manually.

If i could find the T4 file (or a way to intercept and control) the generation of the edmx file itself, i could fix this problem.

Upvotes: 1

Views: 1869

Answers (2)

bitval
bitval

Reputation: 41

Thanks to James Close, this really works.

This is C# T4 template(it looks like James VB template) that rewrites edmx navigation & simple properties and then fixes mappings and associations:

<#@ template  debug="true" hostSpecific="true" #>
<#@ assembly name="System.Text.RegularExpressions"#>
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ include file="EF.Utility.CS.ttinclude"#>
<#/*CodeGenerationCommon.ttinclude contains TypeMapper and EdmMetadataLoader from Model.tt, moved it from there to avoid duplication*/#>
<#@ include file="CodeGenerationCommon.ttinclude" #>
<#@ output extension=".txt" #>
Edmx fixer template
Started at: <#= DateTime.Now #>
<#
    const string inputFile = @"Model.edmx";
    var textTransform = DynamicTextTransformation.Create(this);
    var edmx = XElement.Load(textTransform.Host.ResolvePath(inputFile), LoadOptions.SetBaseUri | LoadOptions.SetLineInfo);
    var code = new CodeGenerationTools(this);
    var ef = new MetadataTools(this);
    var typeMapper = new TypeMapper(code, ef, textTransform.Errors);
    var itemCollection = new EdmMetadataLoader(textTransform.Host, textTransform.Errors).CreateEdmItemCollection(inputFile);
    var navigationProperties = typeMapper.GetItemsToGenerate<EntityType>(itemCollection).SelectMany(item => typeMapper.GetNavigationProperties(item));
    Fix(navigationProperties, edmx);
    edmx.Save(textTransform.Host.ResolvePath(inputFile));
#>
Finished at: <#= DateTime.Now #>
<#+ 
    public void Fix(IEnumerable<NavigationProperty> navigationProperties, XElement edmx)
    {
        foreach(var navigationProperty in navigationProperties)
        {
            if((navigationProperty.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many && navigationProperty.FromEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many) || 
                (navigationProperty.ToEndMember.RelationshipMultiplicity != RelationshipMultiplicity.Many && navigationProperty.FromEndMember.RelationshipMultiplicity != RelationshipMultiplicity.Many))
            {
                continue;
            }
            var fk = navigationProperty.GetDependentProperties().FirstOrDefault();
            if(fk == null)
            {
                var mirrorFk = navigationProperties.FirstOrDefault(item => !item.Equals(navigationProperty) && item.RelationshipType.Name == navigationProperty.RelationshipType.Name).GetDependentProperties().First();
                RewriteNavigationProperty(navigationProperty, mirrorFk.Name, edmx, true);
                continue;
            }
            RewriteNavigationProperty(navigationProperty, fk.Name, edmx, false);
        }
    }

    public void RewriteNavigationProperty(NavigationProperty navigationProperty, string fkName, XElement edmx, bool isCollection)
    {
        var entity = edmx
            .Descendants()
            .Where(item => item.Name.LocalName == "ConceptualModels")
            .Descendants()
            .First(item => item.Name.LocalName == "EntityType" && item.Attribute("Name").Value == navigationProperty.DeclaringType.Name);
        var element = entity
            .Elements()
            .First(item => item.Name.LocalName == "NavigationProperty" && item.Attribute("Relationship").Value == navigationProperty.RelationshipType.ToString());
        var trimId = new Regex(@"(.*)(ID|Id|id)$").Match(fkName).Groups[1].Value;
        var trimDigits = new Regex(@"(.*)(\d*)$").Match(navigationProperty.Name).Groups[1].Value;
        var suffix = string.IsNullOrEmpty(trimDigits) ? navigationProperty.Name : trimDigits;
        var prefix = string.IsNullOrEmpty(trimId) ? fkName : trimId;
        if(string.IsNullOrEmpty(trimId) && !isCollection)
        {
            FixFk(edmx, entity, fkName, navigationProperty);
        }
        element.SetAttributeValue("Name", isCollection ? prefix + suffix : prefix);
    }

    public void FixFk(XElement edmx, XElement entity, string fkName, NavigationProperty navigationProperty)
    {
        var newFkName = fkName + "Id";
        var fk = entity
            .Elements()
            .First(item => item.Name.LocalName == "Property" && item.Attribute("Name").Value == fkName);
        fk.SetAttributeValue("Name", newFkName);
        var association = edmx
            .Descendants()
            .Where(item => item.Name.LocalName == "ConceptualModels")
            .Descendants()
            .FirstOrDefault(item => item.Name.LocalName == "Association" && item.Attribute("Name").Value == navigationProperty.RelationshipType.Name)
            .Descendants()
            .FirstOrDefault(item => item.Name.LocalName == "Dependent" && item.Attribute("Role").Value == navigationProperty.DeclaringType.Name)
            .Elements()
            .First(item => item.Name.LocalName == "PropertyRef");
        association.SetAttributeValue("Name", newFkName);
        var mapping = edmx
            .Descendants()
            .Where(item => item.Name.LocalName == "Mappings")
            .Descendants()
            .FirstOrDefault(item => item.Name.LocalName == "EntityTypeMapping" && item.Attribute("TypeName").Value == navigationProperty.DeclaringType.FullName)
            .Descendants()
            .First(item => item.Name.LocalName == "ScalarProperty" && item.Attribute("Name").Value == fkName);
        mapping.SetAttributeValue("Name", newFkName);
    }
#>

Upvotes: 2

James Close
James Close

Reputation: 932

Just stumbled on this question whilst looking for something else, so I expect you have solved it yourself. A while back I had the exact same issue as you however. The way I got round it was by using an EDMX.tt "prewash" T4 template, which re-named those properties in the EDMX file. The only wrinkle is remembering to run it after saving EDM designer changes (and also ensuring the EDMX file is checked out and editable!)

I think this is another feature that may need to be looked at in later versions of EF. Having navigation properties named Address1, Address2, etc. is not helpful.

The basic inspiration about pulling the EDMX file into memory and parsing it came from here: http://www.codeproject.com/KB/library/EdmxParsing.aspx

Bit of a long lump of code and in VB to boot but here you are:

<#@ template language="VB" debug="false" hostspecific="true"#>
<#@ import namespace="<xmlns=\"http://schemas.microsoft.com/ado/2008/09/edm\">" #>
<#@ import namespace="<xmlns:edmx=\"http://schemas.microsoft.com/ado/2008/10/edmx\">" #>
<#@ import namespace="System.Xml" #>
<#@ import namespace="System.Xml.Linq" #>
'EDMX pre wash template
'Last run:<#= GetDate() #>
<#
  Main()
#>
<#+
  '----------------------------------------------------------------------------------------------------------
  ' Main
  '----------------------------------------------------------------------------------------------------------
  ''' 
  '''  Parses the EDMX file and renames all navigation properties which are not collections and do not
  '''  reference types by primary key with a their FK name, e.g. navigation property for DefaultAddress_FK is
  '''  renamed to DefaultAddress
  ''' 
  Public Sub Main()

    Dim strPath As String = System.IO.Path.GetDirectoryName(Host.TemplateFile) & "\MyDataModel.edmx"
    Dim edmx As XElement = XElement.Load(strPath)
    Dim itemCol As EdmItemCollection = ReadEdmItemCollection(edmx)
    Dim entity As EntityType
    Dim entityTo As EntityType
    Dim navigationProperties As IEnumerable(Of NavigationProperty)
    Dim navigationProperty As NavigationProperty
    Dim updatableProperty As XElement
    Dim assType As AssociationType
    Dim rc As ReferentialConstraint
    Dim strPropertyName As String
    Dim bModifyProperty As Boolean = False

    For Each entity In itemCol.GetItems(Of EntityType)().OrderBy(Function(e) e.Name)

      navigationProperties = From n In entity.NavigationProperties
                             Where n.DeclaringType Is entity AndAlso
                                   n.ToEndMember.RelationshipMultiplicity  RelationshipMultiplicity.Many

      If navigationProperties.Any() Then
        For Each navigationProperty In navigationProperties
          bModifyProperty = False
          ' Get the association for this navigation property
          assType = (From ass As AssociationType In itemCol.GetItems(Of AssociationType)() _
                     Where ass.AssociationEndMembers IsNot Nothing _
                        AndAlso ass.Name = navigationProperty.RelationshipType.Name _
                     Select ass).AsQueryable().FirstOrDefault()
          If (assType IsNot Nothing) Then

            rc = assType.ReferentialConstraints.FirstOrDefault()
            If (rc IsNot Nothing AndAlso rc.ToProperties.Any) Then
              strPropertyName = rc.ToProperties.First.Name
              ' Make sure the FK is not also a PK on the entity referenced
              entityTo = (From e In itemCol.GetItems(Of EntityType)() Where e.Name = rc.ToRole.Name).FirstOrDefault()
              If (entityTo IsNot Nothing AndAlso
                  Not (From km In entityTo.KeyMembers() Where km.Name = strPropertyName).Any) Then
                ' Get the new name of the property - this uses a little extension
                ' method I wrote to Trim characters at the end of a string matching a regex
                strPropertyName = strPropertyName.TrimEnd("_FK[0-9]{0,1}", options:=0)
                ' Ensure there are no already existant properties with that name on the entity
                If (Not (From p In entity.Properties Where p IsNot navigationProperty AndAlso p.Name = strPropertyName).Any) Then
                  bModifyProperty = True
                End If
              End If

              If (bModifyProperty) Then
                updatableProperty = (From n In (From e In edmx...
                                                Where e.@Name = entity.Name).
                                     Where n.@Name = navigationProperty.Name).FirstOrDefault
                If (updatableProperty IsNot Nothing AndAlso updatableProperty.@Name  strPropertyName) Then
#>'Renaming navigation property on <#= entity.Name #> from <#= updatableProperty.@Name #> to <#= strPropertyName #> in EDMX file
<#+
                  updatableProperty.@Name = strPropertyName
                End If
              End If
            End If

          End If
        Next
      End If

    Next entity

    edmx.Save(strPath)

  End Sub

  '----------------------------------------------------------------------------------------------------------
  ' ReadEdmItemCollection
  '----------------------------------------------------------------------------------------------------------
  ''' 
  '''  Code to parse the EDMX xml document and return the managed EdmItemCollection class
  ''' 
  ''' Taken from here: http://www.codeproject.com/KB/library/EdmxParsing.aspx 
  Public Shared Function ReadEdmItemCollection(edmx As XElement) As EdmItemCollection

    Dim csdlNodes As IEnumerable(Of XElement) = edmx....First.Elements
    Dim readers As IEnumerable(Of XMLReader) = From c As XElement In csdlNodes Select c.CreateReader()
    Return New EdmItemCollection(readers)

  End Function
#>

Upvotes: 4

Related Questions