I am mapping my table column name to generic way in to use them in model. like:
public UserEntityMapper()
{
ToTable(\"tbl_User\")
I've seen similar questions before and several less than satisfying solutions based on splitting the SQL string generated by a DbSet
. Attempts to access the mapping metadata are blocked by unaccessible internal types. It kept (and keeps) bugging me that this information is so hidden. Atter all, it's your own mapping.
So I did some fooling around to see if something can be improved here. Since everything can easily be read from an edmx file, that's where I started. And finally got there. It the following code, the edmx file is in an XDocument
called doc
. I'll explain the code below.
var xn = XName.Get("Mappings", doc.Root.GetDefaultNamespace().ToString());
var mappingsNode = doc.Descendants(xn).Descendants().First();
var mappingDoc = XDocument.Parse(mappingsNode.ToString());
xn = XName.Get("MappingFragment", mappingDoc.Root
.GetDefaultNamespace().ToString());
var mappings = mappingDoc
.Descendants(xn)
.Select(x => new
{
Entity = x.Attribute("StoreEntitySet").Value,
Mapping = x.Descendants()
.Select(d => new
{
Property = d.Attribute("Name")
.Value,
Column = d.Attribute("ColumnName")
.Value
})
});
Sample output from a very small edmx:
Entity Property Column
---------------------------------------------
Category CategoryID CategoryID
Name CategoryName
Description Description
Product ProductID ProductID
Name ProductName
QuantityPerUnit QuantityPerUnit
UnitPrice UnitPrice
StartDate StartDate
An edmx file has a fixed structure that I summarize here:
<Edmx Version='3.0' xmlns='http://schemas.microsoft.com/ado/2009/11/edmx'>
<Runtime>
<ConceptualModels>
...
</ConceptualModels>
<Mappings>
<Mapping Space='C-S' xmlns='http://schemas.microsoft.com/ado/2009/11/mapping/cs'>
<EntityContainerMapping StorageEntityContainer='CodeFirstDatabase' CdmEntityContainer='UserQuery'>
<EntitySetMapping Name='Categories'>
<EntityTypeMapping TypeName='CodeFirstNamespace.Category'>
<MappingFragment StoreEntitySet='Category'>
<ScalarProperty Name='CategoryID' ColumnName='CategoryID' />
...
</MappingFragment>
</EntityTypeMapping>
</EntitySetMapping>
<AssociationSetMapping Name='Category_Products' TypeName='CodeFirstNamespace.Category_Products' StoreEntitySet='CategoryProduct'>
</AssociationSetMapping>
</EntityContainerMapping>
</Mapping>
</Mappings>
<StorageModels>
...
</StorageModels>
</Runtime>
<Designer>
...
</Designer>
</Edmx>
The Mappings
part is what we're after. So I first create a new XDocument
of this part:
var xn = XName.Get("Mappings", doc.Root.GetDefaultNamespace().ToString());
// First descendant of the "Mappings" node, which is the "Mapping" node
var mappingsNode = doc.Descendants(xn).Descendants().First();
var mappingDoc = XDocument.Parse(mappingsNode.ToString());
The "Mapping" node has its own namespace, so I parse it into an XDocument
in order to get this namespace by which I can query its nodes. Thus, I can get to the mapping fragments:
xn = XName.Get("MappingFragment", mappingDoc.Root
.GetDefaultNamespace().ToString());
var mappings = mappingDoc.Descendants(xn) ...
Maybe there is a better way to get to these nodes, but I'm not very fluent in this XDocument
API.
Then it's a question of pulling out the right attributes and project them to a queryable structure.
It works, OK, but it's not elegant at all. It greatly depends on the edmx structure, so it can break any moment. I think there should come a decent, convenient way to read a context's metadata.
By the way, if you work code-first you can generate an edmx by an EdmxWriter.
Working on another problem I found a much better way to achieve this. I think this wasn't even possible when I wrote the first answer.
To find a store name starting from a CLR type name, we have to access the store-CLR space of the EDM model. That's where mappings between classes and properties on the one hand, and tables and column on the other hand are found. Knowing this it's just a matter of carefully dissecting the content of a deep tree of objects to get the desired results:
public IEnumerable<Tuple<string, string, string, string>> GetTableAndColumns<TEntity>()
{
var entityContainerMappings = (this as IObjectContextAdapter).ObjectContext
.MetadataWorkspace.GetItems(DataSpace.CSSpace)
.OfType<EntityContainerMapping>();
var entityTypeMappings = entityContainerMappings
.SelectMany(m => m.EntitySetMappings
.Where(esm => esm.EntitySet.ElementType.Name == typeof(TEntity).Name))
.SelectMany(esm => esm.EntityTypeMappings).ToArray();
var propertyMappings = (from etm in entityTypeMappings
from prop in etm.EntityType.Properties
join pm in entityTypeMappings.SelectMany(etm => etm.Fragments)
.SelectMany(mf => mf.PropertyMappings)
.OfType<ScalarPropertyMapping>()
on prop.Name equals pm.Property.Name
select new {etm, prop, pm}
);
return propertyMappings.Select(x => Tuple.Create(x.etm.EntitySetMapping.EntitySet.Name, x.pm.Column.Name, x.prop.DeclaringType.Name, x.prop.Name));
}
This is a method inside a DbContext
subtype. For the specified type, it returns tuples containing
For istance:
Item1 Item2 Item3 Item4
===================================================
Products ProductId Product Id
Products ProductName Product Name
Products QuantityPerUnit Product QuantityPerUnit
Products UnitPrice Product UnitPrice
Products StartDate Product StartDate
Products RowVersion Product RowVersion