Reputation: 291
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
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
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