5

I'm writing an auditing component for an application that uses EF4 for the data access layer. I'm able to very easily determine which entities have been modified, and via the ObjectStateEntry object I can extract the original values, current values, entity name, and property names that were modified, but I would also like to extract the raw table and and column names used in SQL Server (since they do not always match the entity and property names of the model)

Does anyone know of a good way to do this? Is it even possible? The mappings are obviously stored in the MSL, but I can't figure out a way to programmatically access those mappings.

Ladislav Mrnka
  • 360,892
  • 59
  • 660
  • 670
mrmcderm
  • 461
  • 1
  • 5
  • 7

5 Answers5

3

After a peek into a entity framework model designer I saw that it uses the EdmEntityTypeAttribute and DataMemberAttribute to decorate generated classes and properties. Each of them have a Name property which contains the name of the mapped entity (table, column respectively). When the property name matches the name of the column, the designer does not supply a value for positional argument Name. The code below works fine for me.

 private static string GetTableName<T>() where T : EntityObject
    {
        Type type = typeof(T);
        var at = GetAttribute<EdmEntityTypeAttribute>(type);
        return at.Name;
    }

    private static string GetColumnName<T>(Expression<Func<T, object>> propertySelector) where T : EntityObject
    {
        Contract.Requires(propertySelector != null, "propertySelector is null.");

        PropertyInfo propertyInfo = GetPropertyInfo(propertySelector.Body);
        DataMemberAttribute attribute = GetAttribute<DataMemberAttribute>(propertyInfo);
        if (String.IsNullOrEmpty(attribute.Name))
        {
            return propertyInfo.Name;
        }
        return attribute.Name;
    }

    private static T GetAttribute<T>(MemberInfo memberInfo) where T : class
    {
        Contract.Requires(memberInfo != null, "memberInfo is null.");
        Contract.Ensures(Contract.Result<T>() != null);

        object[] customAttributes = memberInfo.GetCustomAttributes(typeof(T), false);
        T attribute = customAttributes.Where(a => a is T).First() as T;
        return attribute;
    }

    private static PropertyInfo GetPropertyInfo(Expression propertySelector)
    {
        Contract.Requires(propertySelector != null, "propertySelector is null.");
        MemberExpression memberExpression = propertySelector as MemberExpression;
        if (memberExpression == null)
        {
            UnaryExpression unaryExpression = propertySelector as UnaryExpression;
            if (unaryExpression != null && unaryExpression.NodeType == ExpressionType.Convert)
            {
                memberExpression = unaryExpression.Operand as MemberExpression;
            }
        }
        if (memberExpression != null && memberExpression.Member.MemberType == MemberTypes.Property)
        {
            return memberExpression.Member as PropertyInfo;
        }
        throw new ArgumentException("No property reference was found.", "propertySelector");
    }

    // Invocation example
    private static Test()
    {
         string table = GetTableName<User>();
         string column = GetColumnName<User>(u=>u.Name);
    }
RePierre
  • 9,358
  • 2
  • 20
  • 37
2

All of the model data is available through this methods myObjectContext.MetadataWorkspace.GetEntityContainer(myObjectContext.DefaultContainerName, DataSpace.CSSpace);

that should at least give you a start on how to do what you want. DataSpace.CSSpace specifies the mapping between the Conceptual names and the Store names. DataSpace.CSpace gives you the Conceptual Model, and DataSpace.SSpace gives you the storage model.

Zackary Geers
  • 360
  • 3
  • 8
  • That is correct assumption but I tried it and it always threw exception that container with the name doesn't exists in metadata workspace. In the same time I saw in debugger that the container is there. Metadata was loaded before by executing separate query. – Ladislav Mrnka Mar 17 '11 at 14:08
  • What are you passing in for the first parameter? This block is used to get the names of types for us currently, so I know it works. – Zackary Geers Mar 17 '11 at 14:23
  • I'm passing correct container name. I know it works. I used it many times with `CSpace` but for some reason it didn't work with `CSSpace` when I tested it. – Ladislav Mrnka Mar 17 '11 at 14:26
  • `MetadataWorkspace.GetItemCollection(System.Data.Metadata.Edm.DataSpace.CSSpace, true)[0]` will give you the CSSpace – Zackary Geers Mar 17 '11 at 16:14
  • you probably also want to look at the `OCSpace` and the `OSpace` too, `MetadataWorkspace.GetItemCollection(System.Data.Metadata.Edm.DataSpace.OCSpace, true)` and `EFUtils.DB.MetadataWorkspace.GetItemCollection(System.Data.Metadata.Edm.DataSpace.OSpace, true)` – Zackary Geers Mar 17 '11 at 16:19
  • Comments can't be marked as answer. Only the answer itself. Also I can't mark your answer because I'm not the one who asked it. Perhaps if you show complete example how to get the name of column for a propery your answer will be marked. – Ladislav Mrnka Mar 17 '11 at 16:22
0

Here's a general-purpose algorithm for converting between conceptual and store information, written in Visual Basic 2010.

I have written a new routine that to convert an entity/property pair into a table/column pair. This class, MSLMappingAction, takes in its constructor the model name and either an XElement XML tree, an MSL mapping file, or an XML string. One then uses the ConceptualToStore method to take String specifying entity and property "expressions" (stored in the MSLConceptualInfo structure) and find the table and column names (stored in the MSLStoreInfo structure).

Notes:

  1. One could also write a "StoreToConceptual" method to convert in the other direction, but the XML queries would probably be a bit more complex. The same goes for handling navigation-propery/function/stored-procedure mappings.
  2. Beware inherited properties of derived entities! If a property is not specific to the derived entity, then you should use the base entity's name.)

Here's the code.

Host code: (For the XML sample given [see bottom], it returns table name "Locations" and column name "Address_Street" for the store information when given the entity "Location" and the property expression "Address.Street" [and the conceptual model name "SCTModel"]):

Dim MSL As MSLMappingAction = New MSLMappingAction(".\SCTModel.msl", "SCTModel")

Dim ConceptualInfo As MSLConceptualInfo = New MSLConceptualInfo With {.EntityName = "Location", .PropertyName = "Address.Street"}
Dim StoreInfo As MSLStoreInfo = MSL.ConceptualToStore(ConceptualInfo)
MessageBox.Show(StoreInfo.TableName & ": " & StoreInfo.ColumnName)

Class code:

Option Infer On
Imports System.Xml.Linq

''' <summary>
''' This class allows one to convert between an EF conceptual model's entity/property pair
''' and its database store's table/column pair.
''' </summary>
''' <remarks>It takes into account entity splitting and complex-property designations;
''' it DOES NOT take into account inherited properties
''' (in such a case, you should access the entity's base class)</remarks>
Public Class MSLMappingAction

'   private fields and routines
Private mmaMSLMapping As XElement
Private mmaModelName, mmaNamespace As String

Private Function FullElementName(ByVal ElementName As String) As String
'   pre-pend Namespace to ElementName
Return "{" & mmaNamespace & "}" & ElementName
End Function

Private Sub ValidateParams(ByVal MappingXML As XElement, Byval ModelName As String)
'   verify that model name is specified
If String.IsNullOrEmpty(ModelName) Then
    Throw New EntityException("Entity model name is not given!")
End If
'   verify that we're using C-S space
If MappingXML.@Space <> "C-S" Then
    Throw New MetadataException("XML is not C-S mapping data!")
End If
'   get Namespace and set private variables
mmaNamespace = MappingXML.@xmlns
mmaMSLMapping = MappingXML : mmaModelName = ModelName
End Sub

Private Function IsSequenceEmpty(Items As IEnumerable(Of XElement)) As Boolean
'   determine if query result is empty
Return _
    Items Is Nothing OrElse Items.Count = 0
End Function

'   properties
''' <summary>
''' Name of conceptual entity model
''' </summary>
''' <returns>Conceptual-model String</returns>
''' <remarks>Model name can only be set in constructor</remarks>
Public ReadOnly Property EntityModelName() As String
Get
    Return mmaModelName
End Get
End Property

''' <summary>
''' Name of mapping namespace
''' </summary>
''' <returns>Namespace String of C-S mapping layer</returns>
''' <remarks>This value is determined when the XML mapping
''' is first parsed in the constructor</remarks>
Public ReadOnly Property MappingNamespace() As String
Get
    Return mmaNamespace
End Get
End Property

'   constructors
''' <summary>
''' Get C-S mapping information for an entity model (with XML tree)
''' </summary>
''' <param name="MappingXML">XML mapping tree</param>
''' <param name="ModelName">Conceptual-model name</param>
''' <remarks></remarks>
Public Sub New(ByVal MappingXML As XElement, ByVal ModelName As String)
ValidateParams(MappingXML, ModelName)
End Sub

''' <summary>
''' Get C-S mapping information for an entity model (with XML file)
''' </summary>
''' <param name="MSLFile">MSL mapping file</param>
''' <param name="ModelName">Conceptual-model name</param>
''' <remarks></remarks>
Public Sub New(ByVal MSLFile As String, ByVal ModelName As String)
Dim MappingXML As XElement = XElement.Load(MSLFile)
ValidateParams(MappingXML, ModelName)
End Sub

'   methods
''' <summary>
''' Get C-S mapping infomration for an entity model (with XML String)
''' </summary>
''' <param name="XMLString">XML mapping String</param>
''' <param name="ModelName">Conceptual-model name</param>
''' <returns></returns>
Public Shared Function Parse(ByVal XMLString As String, ByVal ModelName As String)
Return New MSLMappingAction(XElement.Parse(XMLString), ModelName)
End Function

''' <summary>
''' Convert conceptual entity/property information into store table/column information
''' </summary>
''' <param name="ConceptualInfo">Conceptual-model data
''' (.EntityName = entity expression String, .PropertyName = property expression String)</param>
''' <returns>Store data (.TableName = table-name String, .ColumnName = column-name String)</returns>
''' <remarks></remarks>
Public Function ConceptualToStore(ByVal ConceptualInfo As MSLConceptualInfo) As MSLStoreInfo
Dim StoreInfo As New MSLStoreInfo
With ConceptualInfo
    '   prepare to query XML
    If Not .EntityName.Contains(".") Then
        '   make sure entity name is fully qualified
        .EntityName = mmaModelName & "." & .EntityName
    End If
    '   separate property names if there's complex-type nesting
    Dim Properties() As String = .PropertyName.Split(".")
    '   get relevant entity mapping
    Dim MappingInfo As IEnumerable(Of XElement) = _                 
        (From mi In mmaMSLMapping.Descendants(FullElementName("EntityTypeMapping")) _
            Where mi.@TypeName = "IsTypeOf(" & .EntityName & ")" _
                OrElse mi.@TypeName = .EntityName _
         Select mi)
    '   make sure entity is in model
    If IsSequenceEmpty(MappingInfo) Then
        Throw New EntityException("Entity """ & .EntityName & """ was not found!")
    End If
    '   get mapping fragments
    Dim MappingFragments As IEnumerable(Of XElement) = _
        (From mf In MappingInfo.Descendants(FullElementName("MappingFragment")) _
         Select mf)
    '   make sure there's at least 1 fragment
    If IsSequenceEmpty(MappingFragments) Then
        Throw New EntityException("Entity """ & .EntityName & """ was not mapped!")
    End If
    '   search each mapping fragment for the desired property
    For Each MappingFragment In MappingFragments
        '   get physical table for this fragment
        StoreInfo.TableName = MappingFragment.@StoreEntitySet
        '   search property expression chain
        Dim PropertyMapping As IEnumerable(Of XElement) = {MappingFragment}
        '   parse complex property info (if any)
        For index = 0 To UBound(Properties) - 1
            '   go down 1 level
            Dim ComplexPropertyName = Properties(index)
            PropertyMapping = _
                (From pm In PropertyMapping.Elements(FullElementName("ComplexProperty")) _
                    Where pm.@Name = ComplexPropertyName)
            '   verify that the property specified for this level exists
            If IsSequenceEmpty(PropertyMapping) Then
                Exit For 'go to next fragment if not
            End If
        Next index
        '   property not found? try next fragment
        If IsSequenceEmpty(PropertyMapping) Then
            Continue For
        End If
        '   parse scalar property info
        Dim ScalarPropertyName = Properties(UBound(Properties))
        Dim ColumnName As String = _
            (From pm In PropertyMapping.Elements(FullElementName("ScalarProperty")) _
                Where pm.@Name = ScalarPropertyName _
                Select CN = pm.@ColumnName).FirstOrDefault
        '   verify that scalar property exists
        If Not String.IsNullOrEmpty(ColumnName) Then
            '   yes? return (exit) with column info
            StoreInfo.ColumnName = ColumnName : Return StoreInfo
        End If
    Next MappingFragment
    '   property wasn't found
    Throw New EntityException("Property """ & .PropertyName _
        & """ of entity """ & .EntityName & """ was not found!")
End With
End Function
End Class

''' <summary>
''' Conceptual-model entity and property information  
''' </summary>
Public Structure MSLConceptualInfo
''' <summary>
''' Name of entity in conceptual model
''' </summary>
''' <value>Entity expression String</value>
''' <remarks>EntityName may or may not be fully qualified (i.e., "ModelName.EntityName");
''' when a mapping method is called by the MSLMappingAction class, the conceptual model's
''' name and a period will be pre-pended if it's omitted</remarks>
Public Property EntityName As String
''' <summary>
''' Name of property in entity
''' </summary>
''' <value>Property expression String</value>
''' <remarks>PropertyName may be either a stand-alone scalar property or a scalar property
''' within 1 or more levels of complex-type properties; in the latter case, it MUST be fully
''' qualified (i.e., "ComplexPropertyName.InnerComplexPropertyName.ScalarPropertyName")</remarks>
Public Property PropertyName As String
End Structure

''' <summary>
''' Database-store table and column information
''' </summary>
Public Structure MSLStoreInfo
''' <summary>
''' Name of table in database
''' </summary>
Public Property TableName As String
''' <summary>
''' Name of column in database table
''' </summary>
Public Property ColumnName As String
End Structure

The catch is that the node names all have to have a namespace prepended to them. It tripped me up until I checked my elements 1 at a time!

Here's the sample XML -- which I load from the ".\SCTModel.msl" file in the code above:

<?xml version="1.0" encoding="utf-8"?>
<Mapping Space="C-S" xmlns="http://schemas.microsoft.com/ado/2008/09/mapping/cs">
  <EntityContainerMapping StorageEntityContainer="SCTModelStoreContainer" CdmEntityContainer="SocialContactsTracker">
    <EntitySetMapping Name="SocialContacts">
      <EntityTypeMapping TypeName="IsTypeOf(SCTModel.SocialContact)">
        <MappingFragment StoreEntitySet="SocialContacts">
          <ScalarProperty Name="Id" ColumnName="Id" />
          <ScalarProperty Name="DateAdded" ColumnName="DateAdded" />
          <ScalarProperty Name="Information" ColumnName="Information" />
          <ComplexProperty Name="DefaultAssociations" TypeName="SCTModel.DefaultAssociations">
            <ScalarProperty Name="DefaultLocationID" ColumnName="DefaultAssociations_DefaultLocationID" />
            <ScalarProperty Name="DefaultEmailID" ColumnName="DefaultAssociations_DefaultEmailID" />
            <ScalarProperty Name="DefaultPhoneNumberID" ColumnName="DefaultAssociations_DefaultPhoneNumberID" />
            <ScalarProperty Name="DefaultWebsiteID" ColumnName="DefaultAssociations_DefaultWebsiteID" />
          </ComplexProperty>
          <ScalarProperty Name="Picture" ColumnName="Picture" />
        </MappingFragment>
      </EntityTypeMapping>
      <EntityTypeMapping TypeName="IsTypeOf(SCTModel.Person)">
        <MappingFragment StoreEntitySet="SocialContacts_Person">
          <ScalarProperty Name="Id" ColumnName="Id" />
          <ScalarProperty Name="DateOfBirth" ColumnName="DateOfBirth" />
          <ScalarProperty Name="FirstName" ColumnName="FirstName" />
          <ScalarProperty Name="LastName" ColumnName="LastName" />
        </MappingFragment>
      </EntityTypeMapping>
      <EntityTypeMapping TypeName="IsTypeOf(SCTModel.Organization)">
        <MappingFragment StoreEntitySet="SocialContacts_Organization">
          <ScalarProperty Name="Id" ColumnName="Id" />
          <ScalarProperty Name="Name" ColumnName="Name" />
          <ScalarProperty Name="DateOfCreation" ColumnName="DateOfCreation" />
        </MappingFragment>
      </EntityTypeMapping>
    </EntitySetMapping>
    <EntitySetMapping Name="Locations">
      <EntityTypeMapping TypeName="IsTypeOf(SCTModel.Location)">
        <MappingFragment StoreEntitySet="Locations">
          <ScalarProperty Name="Id" ColumnName="Id" />
          <ScalarProperty Name="City" ColumnName="City" />
          <ScalarProperty Name="State" ColumnName="State" />
          <ScalarProperty Name="ZIP" ColumnName="ZIP" />
          <ScalarProperty Name="Country" ColumnName="Country" />
          <ComplexProperty Name="Address" TypeName="SCTModel.Address">
            <ScalarProperty Name="Street" ColumnName="Address_Street" />
            <ScalarProperty Name="Apartment" ColumnName="Address_Apartment" />
            <ScalarProperty Name="HouseNumber" ColumnName="Address_HouseNumber" />
          </ComplexProperty>
        </MappingFragment>
      </EntityTypeMapping>
    </EntitySetMapping>
    <EntitySetMapping Name="PhoneNumbers">
      <EntityTypeMapping TypeName="IsTypeOf(SCTModel.PhoneNumber)">
        <MappingFragment StoreEntitySet="PhoneNumbers">
          <ScalarProperty Name="Id" ColumnName="Id" />
          <ScalarProperty Name="Number" ColumnName="Number" />
          <ScalarProperty Name="PhoneType" ColumnName="PhoneType" />
        </MappingFragment>
      </EntityTypeMapping>
    </EntitySetMapping>
    <EntitySetMapping Name="Emails">
      <EntityTypeMapping TypeName="IsTypeOf(SCTModel.Email)">
        <MappingFragment StoreEntitySet="Emails">
          <ScalarProperty Name="Id" ColumnName="Id" />
          <ScalarProperty Name="DomainName" ColumnName="DomainName" />
          <ScalarProperty Name="UserName" ColumnName="UserName" />
        </MappingFragment>
      </EntityTypeMapping>
    </EntitySetMapping>
    <EntitySetMapping Name="Websites">
      <EntityTypeMapping TypeName="IsTypeOf(SCTModel.Website)">
        <MappingFragment StoreEntitySet="Websites">
          <ScalarProperty Name="Id" ColumnName="Id" />
          <ScalarProperty Name="URL" ColumnName="URL" />
        </MappingFragment>
      </EntityTypeMapping>
    </EntitySetMapping>
    <AssociationSetMapping Name="SocialContactWebsite" TypeName="SCTModel.SocialContactWebsite" StoreEntitySet="SocialContactWebsite">
      <EndProperty Name="SocialContact">
        <ScalarProperty Name="Id" ColumnName="SocialContacts_Id" />
      </EndProperty>
      <EndProperty Name="Website">
        <ScalarProperty Name="Id" ColumnName="Websites_Id" />
      </EndProperty>
    </AssociationSetMapping>
    <AssociationSetMapping Name="SocialContactPhoneNumber" TypeName="SCTModel.SocialContactPhoneNumber" StoreEntitySet="SocialContactPhoneNumber">
      <EndProperty Name="SocialContact">
        <ScalarProperty Name="Id" ColumnName="SocialContacts_Id" />
      </EndProperty>
      <EndProperty Name="PhoneNumber">
        <ScalarProperty Name="Id" ColumnName="PhoneNumbers_Id" />
      </EndProperty>
    </AssociationSetMapping>
    <AssociationSetMapping Name="SocialContactEmail" TypeName="SCTModel.SocialContactEmail" StoreEntitySet="SocialContactEmail">
      <EndProperty Name="SocialContact">
        <ScalarProperty Name="Id" ColumnName="SocialContacts_Id" />
      </EndProperty>
      <EndProperty Name="Email">
        <ScalarProperty Name="Id" ColumnName="Emails_Id" />
      </EndProperty>
    </AssociationSetMapping>
    <AssociationSetMapping Name="SocialContactLocation" TypeName="SCTModel.SocialContactLocation" StoreEntitySet="SocialContactLocation">
      <EndProperty Name="SocialContact">
        <ScalarProperty Name="Id" ColumnName="SocialContacts_Id" />
      </EndProperty>
      <EndProperty Name="Location">
        <ScalarProperty Name="Id" ColumnName="Locations_Id" />
      </EndProperty>
    </AssociationSetMapping>
  </EntityContainerMapping>
</Mapping>

It should be noted that the above code only works when the XML for the MSL file is store as a separate file (like when the metadata is copied by the host project top the output path) or when the XML is already available. What I still need to know is how to extract the MSL info when it's stored solely as an assembly resource.

For those wanting to provide more input, keep in mind that any general-purpose solution should work with any version of .NET (from at least 4.0 up)--including pre 4.7 versions. It should also be capable of handling complex-property expressions.

0

If you write code to audit the mapping, aren't you really auditing/verifying Microsoft's EF code? Perhaps this can safely be defined out of the problem domain, unless the purpose of the auditing is to build confidence in the EF itself.

But if you really need to do that kind of auditing, one possibility might be to add a build step to embed the .edmx file as a resource in the DLL you're examining. You didn't say if you have that kind of control/input on the DLL under test. It would be a hack, though -- as JasCav said, the purpose of ORMs is to make exactly what you're attempting unnecessary.

David Pope
  • 6,457
  • 2
  • 35
  • 45
  • I do not want to audit the mappings, I want to audit changes to the entities - what property changed, when it changed, and who changed it. The reason I'm interested in the table and column names is because groups other than the main development team will need to provide production support to the application. If the entity names do not match the table names, and the support team doesn't have access to the code and/or mappings, that makes their job that much harder. – mrmcderm Mar 17 '11 at 13:56
-1

I'm a bit confused why the raw table and column names used in SQL Server aren't matching the entity and property names of the model. Except for the table used to provide the many-to-many mapping, there (typically) should be a direct correspondence between your object names/properties and the table names and column names.

With that said, Entity Framework is an ORM. The entire purpose of the framework is to provide an object-oriented view to your database and abstract away having to interact directly with the relational database. EF is not really meant to allow you to circumvent the framework and, as far as I know, what you are looking to do isn't possible. (However, if I am wrong, it is something new I will have learned today and I will delete or edit this answer accordingly.)

JasCav
  • 34,458
  • 20
  • 113
  • 170
  • 1
    It is quite common that properties in entities don't have same names as database column. For example C# had different naming conventions then SQL. One of our databases has columns like `[2_CODE_A2]` but the propery is named simply `Code`. – Ladislav Mrnka Mar 15 '11 at 15:36
  • @Ladislav - Ah...okay. I hadn't thought about that. (In my use of EF, the database column names match the object names, minus the pluralization of the names when necessary.) In either case, the second portion of my comment still stands, I believe. – JasCav Mar 15 '11 at 16:16
  • We are building an EF4 component against a legacy database that have table and column names that are less than meaningful, so @Ladislav's example applies in our situation. We would like to not only log/audit changes to the entities, but also track the source table and column names to make production support by the DBAs (who do not have visibility into the code) easier. @JasCav do you know for sure that EF4 does not expose this information, or are you just guessing based on other ORMs? – mrmcderm Mar 17 '11 at 13:52
  • @mrmcderm - I will admit that I am not an expert in the entity framework. However, as far as I know, I don't believe you can do what you are trying to do. I noticed in your comment above you said you want to audit changes to the entities. Ultimately, changes to the entities are tied back to the database. Why not just audit the database directly? (Your application works through the entities, but what you really care about is the data.) Maybe I'm confused about what you are trying to accomplish. My apologies for that. – JasCav Mar 17 '11 at 14:57