diff --git a/EFTests/EFTests.csproj b/EFTests/EFTests.csproj index 264c2e2..00617ad 100644 --- a/EFTests/EFTests.csproj +++ b/EFTests/EFTests.csproj @@ -57,6 +57,7 @@ + @@ -68,6 +69,8 @@ + + diff --git a/EFTests/Model/Eon.cs b/EFTests/Model/Eon.cs new file mode 100644 index 0000000..ae10cbc --- /dev/null +++ b/EFTests/Model/Eon.cs @@ -0,0 +1,11 @@ +namespace EFTests.Model +{ + /// + /// Only findable via the complex type Geology + /// + public enum Eon + { + Old, + ReallyOld, + } +} diff --git a/EFTests/Model/Geology.cs b/EFTests/Model/Geology.cs new file mode 100644 index 0000000..0b73586 --- /dev/null +++ b/EFTests/Model/Geology.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace EFTests.Model +{ + public class Geology + { + // no id to make this a complex type to be included in the Warren + public string Soil { get; set; } + public int Density { get; set; } + public Eon? Eon { get; set; } + + [Column("PreviousEon")] // supress the "Geology_" prefix that you'd normally get + public Eon? PreviousEon { get; set; } + } +} diff --git a/EFTests/Model/Pedigree.cs b/EFTests/Model/Pedigree.cs new file mode 100644 index 0000000..d4c2f0b --- /dev/null +++ b/EFTests/Model/Pedigree.cs @@ -0,0 +1,9 @@ +namespace EFTests.Model +{ + public enum Pedigree + { + Dubious, + Pure, + Inbred + } +} diff --git a/EFTests/Model/Rabbit.cs b/EFTests/Model/Rabbit.cs index 998f848..e91f347 100644 --- a/EFTests/Model/Rabbit.cs +++ b/EFTests/Model/Rabbit.cs @@ -1,4 +1,6 @@ -namespace EFTests.Model +using System.ComponentModel.DataAnnotations.Schema; + +namespace EFTests.Model { public class Rabbit { @@ -11,5 +13,8 @@ public class Rabbit public Legs? SpeedyLegs { get; set; } public Relation? Offspring { get; set; } + + [Column("Lineage")] + public Pedigree Pedigree { get; set; } } } diff --git a/EFTests/Model/Warren.cs b/EFTests/Model/Warren.cs index 2a1d272..6e60bc3 100644 --- a/EFTests/Model/Warren.cs +++ b/EFTests/Model/Warren.cs @@ -12,5 +12,8 @@ public class Warren public Heat? HowHot { get; set; } public ICollection HopesAndDreams { get; set; } + + // complex type: + public Geology Geology { get; set; } } } diff --git a/EFTests/Tests/ModelParsingTests.cs b/EFTests/Tests/ModelParsingTests.cs index 32e54a2..2011b02 100644 --- a/EFTests/Tests/ModelParsingTests.cs +++ b/EFTests/Tests/ModelParsingTests.cs @@ -26,7 +26,7 @@ public void FindsReferences() IList references; using (var context = new MagicContext()) { - references = _enumToLookup.FindReferences(context); + references = _enumToLookup.FindEnumReferences(context); } var legs = references.SingleOrDefault(r => r.ReferencingField == "SpeedyLegs"); Assert.IsNotNull(legs, "SpeedyLegs ref not found"); @@ -34,8 +34,10 @@ public void FindsReferences() Assert.IsNotNull(ears, "TehEars ref not found"); var echos = references.SingleOrDefault(r => r.ReferencingField == "EchoType"); Assert.IsNotNull(echos, "EchoType ref not found"); + var eons = references.Count(r => r.EnumType == typeof(Eon)); + Assert.AreEqual(2, eons, "Wrong number of Eon refs found"); Assert.IsTrue(references.All(r => r.EnumType.IsEnum), "Non-enum type found"); - Assert.AreEqual(8, references.Count); + Assert.AreEqual(11, references.Count); } [Test] diff --git a/EfEnumToLookup/EfEnumToLookup.csproj b/EfEnumToLookup/EfEnumToLookup.csproj index 54df143..6598004 100644 --- a/EfEnumToLookup/EfEnumToLookup.csproj +++ b/EfEnumToLookup/EfEnumToLookup.csproj @@ -56,9 +56,14 @@ + + + + + diff --git a/EfEnumToLookup/LookupGenerator/EnumToLookup.cs b/EfEnumToLookup/LookupGenerator/EnumToLookup.cs index 4c7f095..eb0fc3e 100644 --- a/EfEnumToLookup/LookupGenerator/EnumToLookup.cs +++ b/EfEnumToLookup/LookupGenerator/EnumToLookup.cs @@ -1,18 +1,16 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Data.Entity; -using System.Data.Entity.Core.Mapping; -using System.Data.Entity.Core.Metadata.Edm; -using System.Data.Entity.Infrastructure; -using System.Data.SqlClient; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Text.RegularExpressions; - -namespace EfEnumToLookup.LookupGenerator +namespace EfEnumToLookup.LookupGenerator { + using System; + using System.Collections.Generic; + using System.ComponentModel; + using System.Data.Entity; + using System.Data.Entity.Infrastructure; + using System.Data.SqlClient; + using System.Linq; + using System.Reflection; + using System.Text; + using System.Text.RegularExpressions; + /// /// Makes up for a missing feature in Entity Framework 6.1 /// Creates lookup tables and foreign key constraints based on the enums @@ -29,7 +27,8 @@ public class EnumToLookup : IEnumToLookup { public EnumToLookup() { - NameFieldLength = 255; // default + // set default behaviour, can be overridden by setting properties on object before calling Apply() + NameFieldLength = 255; TableNamePrefix = "Enum_"; SplitWords = true; } @@ -69,79 +68,36 @@ public EnumToLookup() /// context.Database.ExecuteSqlCommand() is used to apply changes. public void Apply(DbContext context) { - // recurese through dbsets and references finding anything that uses an enum - var enumReferences = FindReferences(context); - // for the list of enums generate tables + // recurse through dbsets and references finding anything that uses an enum + var enumReferences = FindEnumReferences(context); + + // for the list of enums generate and missing tables var enums = enumReferences.Select(r => r.EnumType).Distinct().ToList(); - CreateTables(enums, (sql) => context.Database.ExecuteSqlCommand(sql)); - // t-sql merge values into table - PopulateLookups(enums, (sql, parameters) => context.Database.ExecuteSqlCommand(sql, parameters.Cast().ToArray())); - // add fks from all referencing tables - AddForeignKeys(enumReferences, (sql) => context.Database.ExecuteSqlCommand(sql)); - } - private void AddForeignKeys(IEnumerable refs, Action runSql) - { - foreach (var enumReference in refs) - { - var fkName = string.Format("FK_{0}_{1}", enumReference.ReferencingTable, enumReference.ReferencingField); - var sql = - string.Format( - " IF OBJECT_ID('{0}', 'F') IS NULL ALTER TABLE [{1}] ADD CONSTRAINT {0} FOREIGN KEY ([{2}]) REFERENCES [{3}] (Id);", - fkName, enumReference.ReferencingTable, enumReference.ReferencingField, TableName(enumReference.EnumType.Name)); - runSql(sql); - } - } + var lookups = + (from enm in enums + select new LookupData + { + Name = enm.Name, + Values = GetLookupValues(enm), + }).ToList(); - private void PopulateLookups(IEnumerable enums, Action> runSql) - { - foreach (var lookup in enums) - { - PopulateLookup(lookup, runSql); - } + // todo: support MariaDb etc. Issue #16 + IDbHandler dbHandler = new SqlServerHandler(); + dbHandler.NameFieldLength = NameFieldLength; + dbHandler.TableNamePrefix = TableNamePrefix; + dbHandler.TableNameSuffix = TableNameSuffix; + + dbHandler.Apply(lookups, enumReferences, (sql, parameters) => ExecuteSqlCommand(context, sql, parameters)); } - private void PopulateLookup(Type lookup, Action> runSql) + private static int ExecuteSqlCommand(DbContext context, string sql, IEnumerable parameters = null) { - if (!lookup.IsEnum) + if (parameters == null) { - throw new ArgumentException("Lookup type must be an enum", "lookup"); + return context.Database.ExecuteSqlCommand(sql); } - - var sb = new StringBuilder(); - sb.AppendLine(string.Format("CREATE TABLE #lookups (Id int, Name nvarchar({0}) COLLATE database_default);", NameFieldLength)); - var parameters = new List(); - int paramIndex = 0; - foreach (var value in Enum.GetValues(lookup)) - { - if (IsRuntimeOnly(value, lookup)) - { - continue; - } - var id = (int)value; - var name = EnumName(value, lookup); - var idParamName = string.Format("id{0}", paramIndex++); - var nameParamName = string.Format("name{0}", paramIndex++); - sb.AppendLine(string.Format("INSERT INTO #lookups (Id, Name) VALUES (@{0}, @{1});", idParamName, nameParamName)); - parameters.Add(new SqlParameter(idParamName, id)); - parameters.Add(new SqlParameter(nameParamName, name)); - } - - sb.AppendLine(string.Format(@" -MERGE INTO [{0}] dst - USING #lookups src ON src.Id = dst.Id - WHEN MATCHED AND src.Name <> dst.Name THEN - UPDATE SET Name = src.Name - WHEN NOT MATCHED THEN - INSERT (Id, Name) - VALUES (src.Id, src.Name) - WHEN NOT MATCHED BY SOURCE THEN - DELETE -;" - , TableName(lookup.Name))); - - sb.AppendLine("DROP TABLE #lookups;"); - runSql(sb.ToString(), parameters); + return context.Database.ExecuteSqlCommand(sql, parameters.Cast().ToArray()); } private string EnumName(object value, Type lookup) @@ -168,7 +124,7 @@ private static string SplitCamelCase(string name) return name; } - private string DescriptionValue(object value, Type enumType) + private static string DescriptionValue(object value, Type enumType) { // https://stackoverflow.com/questions/1799370/getting-attributes-of-enums-value/1799401#1799401 var member = enumType.GetMember(value.ToString()).First(); @@ -176,126 +132,43 @@ private string DescriptionValue(object value, Type enumType) return description == null ? null : description.Description; } - private bool IsRuntimeOnly(object value, Type enumType) + private IEnumerable GetLookupValues(Type lookup) { - // https://stackoverflow.com/questions/1799370/getting-attributes-of-enums-value/1799401#1799401 - var member = enumType.GetMember(value.ToString()).First(); - return member.GetCustomAttributes(typeof(RuntimeOnlyAttribute)).Any(); - } + if (!lookup.IsEnum) + { + throw new ArgumentException("Lookup type must be an enum", "lookup"); + } - private void CreateTables(IEnumerable enums, Action runSql) - { - foreach (var lookup in enums) + var values = new List(); + foreach (var value in Enum.GetValues(lookup)) { - runSql(string.Format( - @"IF OBJECT_ID('{0}', 'U') IS NULL CREATE TABLE [{0}] (Id int PRIMARY KEY, Name nvarchar({1}));", - TableName(lookup.Name), NameFieldLength)); + if (IsRuntimeOnly(value, lookup)) + { + continue; + } + values.Add(new LookupValue + { + Id = (int)value, + Name = EnumName(value, lookup), + }); } + return values; } - private string TableName(string enumName) - { - return string.Format("{0}{1}{2}", TableNamePrefix, enumName, TableNameSuffix); - } - internal IList FindReferences(DbContext context) + private static bool IsRuntimeOnly(object value, Type enumType) { - var metadata = ((IObjectContextAdapter)context).ObjectContext.MetadataWorkspace; - - // Get the part of the model that contains info about the actual CLR types - var objectItemCollection = ((ObjectItemCollection)metadata.GetItemCollection(DataSpace.OSpace)); // OSpace = Object Space - - // find and return all the references to enum types - var enumReferences = (from entity in metadata.GetItems(DataSpace.OSpace) - from property in entity.Properties - where property.IsEnumType - select new EnumReference - { - ReferencingTable = GetTableName(metadata, entity), - ReferencingField = property.Name, - EnumType = objectItemCollection.GetClrType(property.EnumType), - }); - return enumReferences - .Where(r => r.ReferencingTable != null) // filter out child-types in Table-per-Hierarchy model - .ToList(); + // https://stackoverflow.com/questions/1799370/getting-attributes-of-enums-value/1799401#1799401 + var member = enumType.GetMember(value.ToString()).First(); + return member.GetCustomAttributes(typeof(RuntimeOnlyAttribute)).Any(); } - private static string GetTableName(MetadataWorkspace metadata, EntityType entityType) + internal IList FindEnumReferences(DbContext context) { - // refs: - // * http://romiller.com/2014/04/08/ef6-1-mapping-between-types-tables/ - // * http://blogs.msdn.com/b/appfabriccat/archive/2010/10/22/metadataworkspace-reference-in-wcf-services.aspx - // * http://msdn.microsoft.com/en-us/library/system.data.metadata.edm.dataspace.aspx - describes meaning of OSpace etc - - try - { - // Get the entity type from the model that maps to the CLR type - var entityTypes = metadata - .GetItems(DataSpace.OSpace) // OSpace = Object Space - .Where(e => e == entityType) - .ToList(); - if (entityTypes.Count() != 1) - { - throw new EnumGeneratorException(string.Format("{0} entities of type {1} found in mapping.", entityTypes.Count(), entityType)); - } - var entityMetadata = entityTypes.Single(); + var metadataWorkspace = ((IObjectContextAdapter)context).ObjectContext.MetadataWorkspace; - // Get the entity set that uses this entity type - var containers = metadata - .GetItems(DataSpace.CSpace); // CSpace = Conceptual model - if (containers.Count() != 1) - { - throw new EnumGeneratorException(string.Format("{0} EntityContainer's found.", containers.Count())); - } - var container = containers.Single(); - - var entitySets = container - .EntitySets - .Where(s => s.ElementType.Name == entityMetadata.Name) - .ToList(); - // Child types in Table-per-Hierarchy don't have any mapping so return null for the table name. Foreign key will be from the parent/base type. - if (!entitySets.Any()) - { - return null; - } - if (entitySets.Count() != 1) - { - throw new EnumGeneratorException(string.Format( - "{0} EntitySet's found for element type '{1}'.", entitySets.Count(), entityMetadata.Name)); - } - var entitySet = entitySets.Single(); - - // Find the mapping between conceptual and storage model for this entity set - var entityContainerMappings = metadata.GetItems(DataSpace.CSSpace); // CSSpace = Conceptual model to Storage model mappings - if (entityContainerMappings.Count() != 1) - { - throw new EnumGeneratorException(string.Format("{0} EntityContainerMappings found.", entityContainerMappings.Count())); - } - var containerMapping = entityContainerMappings.Single(); - var mappings = containerMapping.EntitySetMappings.Where(s => s.EntitySet == entitySet).ToList(); - if (mappings.Count() != 1) - { - throw new EnumGeneratorException(string.Format( - "{0} EntitySetMappings found for entitySet '{1}'.", mappings.Count(), entitySet.Name)); - } - var mapping = mappings.Single(); - - // Find the storage entity set (table) that the entity is mapped to - var entityTypeMappings = mapping.EntityTypeMappings; - var entityTypeMapping = entityTypeMappings.First(); // using First() because Table-per-Hierarchy (TPH) produces multiple copies of the entity type mapping - var fragments = entityTypeMapping.Fragments; - if (fragments.Count() != 1) - { - throw new EnumGeneratorException(string.Format("{0} Fragments found.", fragments.Count())); - } - var table = fragments.Single().StoreEntitySet; - var tableName = (string)table.MetadataProperties["Table"].Value ?? table.Name; - return tableName; - } - catch (Exception exception) - { - throw new EnumGeneratorException(string.Format("Error getting table name for entity type '{0}'", entityType.Name), exception); - } + var metadataHandler = new MetadataHandler(); + return metadataHandler.FindEnumReferences(metadataWorkspace); } internal IList FindDbSets(Type contextType) diff --git a/EfEnumToLookup/LookupGenerator/IDbHandler.cs b/EfEnumToLookup/LookupGenerator/IDbHandler.cs new file mode 100644 index 0000000..8a33253 --- /dev/null +++ b/EfEnumToLookup/LookupGenerator/IDbHandler.cs @@ -0,0 +1,29 @@ +namespace EfEnumToLookup.LookupGenerator +{ + using System; + using System.Collections.Generic; + using System.Data.SqlClient; + + internal interface IDbHandler + { + /// + /// The size of the Name field that will be added to the generated lookup tables. + /// Adjust to suit your data if required. + /// + int NameFieldLength { get; set; } + + /// + /// Prefix to add to all the generated tables to separate help group them together + /// and make them stand out as different from other tables. + /// + string TableNamePrefix { get; set; } + + /// + /// Suffix to add to all the generated tables to separate help group them together + /// and make them stand out as different from other tables. + /// + string TableNameSuffix { get; set; } + + void Apply(List lookups, IList enumReferences, Action> runSql); + } +} diff --git a/EfEnumToLookup/LookupGenerator/LookupData.cs b/EfEnumToLookup/LookupGenerator/LookupData.cs new file mode 100644 index 0000000..689921e --- /dev/null +++ b/EfEnumToLookup/LookupGenerator/LookupData.cs @@ -0,0 +1,10 @@ +namespace EfEnumToLookup.LookupGenerator +{ + using System.Collections.Generic; + + internal class LookupData + { + public string Name { get; set; } + public IEnumerable Values { get; set; } + } +} diff --git a/EfEnumToLookup/LookupGenerator/LookupValue.cs b/EfEnumToLookup/LookupGenerator/LookupValue.cs new file mode 100644 index 0000000..ac1c0ae --- /dev/null +++ b/EfEnumToLookup/LookupGenerator/LookupValue.cs @@ -0,0 +1,8 @@ +namespace EfEnumToLookup.LookupGenerator +{ + internal class LookupValue + { + public int Id { get; set; } + public string Name { get; set; } + } +} diff --git a/EfEnumToLookup/LookupGenerator/MetadataHandler.cs b/EfEnumToLookup/LookupGenerator/MetadataHandler.cs new file mode 100644 index 0000000..9672fe0 --- /dev/null +++ b/EfEnumToLookup/LookupGenerator/MetadataHandler.cs @@ -0,0 +1,265 @@ +namespace EfEnumToLookup.LookupGenerator +{ + using System; + using System.Collections.Generic; + using System.Data.Entity.Core.Mapping; + using System.Data.Entity.Core.Metadata.Edm; + using System.Linq; + + class MetadataHandler + { + // refs: + // * http://romiller.com/2014/04/08/ef6-1-mapping-between-types-tables/ + // * http://blogs.msdn.com/b/appfabriccat/archive/2010/10/22/metadataworkspace-reference-in-wcf-services.aspx + // * http://msdn.microsoft.com/en-us/library/system.data.metadata.edm.dataspace.aspx - describes meaning of OSpace etc + // * http://stackoverflow.com/questions/22999330/mapping-from-iedmentity-to-clr + + internal IList FindEnumReferences(MetadataWorkspace metadataWorkspace) + { + // Get the part of the model that contains info about the actual CLR types + var objectItemCollection = ((ObjectItemCollection)metadataWorkspace.GetItemCollection(DataSpace.OSpace)); + // OSpace = Object Space + + var entities = metadataWorkspace.GetItems(DataSpace.OSpace); + + // find and return all the references to enum types + var references = new List(); + foreach (var entityType in entities) + { + var mappingFragment = FindSchemaMappingFragment(metadataWorkspace, entityType); + + // child types in TPH don't get mappings + if (mappingFragment == null) + { + continue; + } + + references.AddRange(ProcessEdmProperties(entityType.Properties, mappingFragment, objectItemCollection)); + } + return references; + } + + /// + /// Loop through all the specified properties, including the children of any complex type properties, looking for enum types. + /// + /// The properties to search. + /// Information needed from ef metadata to map table and its columns + /// For looking up ClrTypes of any enums encountered + /// All the references that were found in a form suitable for creating lookup tables and foreign keys + private static IEnumerable ProcessEdmProperties(IEnumerable properties, MappingFragment mappingFragment, ObjectItemCollection objectItemCollection) + { + var references = new List(); + + foreach (var edmProperty in properties) + { + var table = mappingFragment.StoreEntitySet.Table; + + if (edmProperty.IsEnumType) + { + references.Add(new EnumReference + { + ReferencingTable = table, + ReferencingField = GetColumnName(mappingFragment, edmProperty), + EnumType = objectItemCollection.GetClrType(edmProperty.EnumType), + }); + continue; + } + + if (edmProperty.IsComplexType) + { + // Note that complex types can't be nested (ref http://stackoverflow.com/a/20332503/10245 ) + // so it's safe to not recurse even though the data model suggests you should have to. + references.AddRange( + from nestedProperty in edmProperty.ComplexType.Properties + where nestedProperty.IsEnumType + select new EnumReference + { + ReferencingTable = table, + ReferencingField = GetColumnName(mappingFragment, edmProperty, nestedProperty), + EnumType = objectItemCollection.GetClrType(nestedProperty.EnumType), + }); + } + } + + return references; + } + + /// + /// Gets the name of the column for the property from the metadata. + /// Set nestedProperty for the property of a complex type to lookup. + /// + /// EF metadata for finding mappings. + /// The of the model to find the column for (for simple types), or for complex types this is the containing complex type. + /// Only required to map complex types. The property of the complex type to find the column name for. + /// The column name for the property + /// + /// + private static string GetColumnName(StructuralTypeMapping mappingFragment, EdmProperty edmProperty, EdmProperty nestedProperty = null) + { + var propertyMapping = GetPropertyMapping(mappingFragment, edmProperty); + + if (nestedProperty != null) + { + var complexPropertyMapping = propertyMapping as ComplexPropertyMapping; + if (complexPropertyMapping == null) + { + throw new EnumGeneratorException(string.Format( + "Failed to cast complex property mapping for {0}.{1} to ComplexPropertyMapping", edmProperty, nestedProperty)); + } + var complexTypeMappings = complexPropertyMapping.TypeMappings; + if (complexTypeMappings.Count() != 1) + { + throw new EnumGeneratorException(string.Format( + "{0} complexPropertyMapping TypeMappings found for property {1}.{2}", complexTypeMappings.Count(), edmProperty, nestedProperty)); + } + var complexTypeMapping = complexTypeMappings.Single(); + var propertyMappings = complexTypeMapping.PropertyMappings.Where(pm => pm.Property.Name == nestedProperty.Name).ToList(); + if (propertyMappings.Count() != 1) + { + throw new EnumGeneratorException(string.Format( + "{0} complexMappings found for property {1}.{2}", propertyMappings.Count(), edmProperty, nestedProperty)); + } + + propertyMapping = propertyMappings.Single(); + } + + return GetColumnNameFromPropertyMapping(edmProperty, propertyMapping); + } + + private static string GetColumnNameFromPropertyMapping(EdmProperty edmProperty, PropertyMapping propertyMapping) + { + var colMapping = propertyMapping as ScalarPropertyMapping; + if (colMapping == null) + { + throw new EnumGeneratorException(string.Format( + "Expected ScalarPropertyMapping but found {0} when mapping property {1}", propertyMapping.GetType(), edmProperty)); + } + return colMapping.Column.Name; + } + + private static PropertyMapping GetPropertyMapping(StructuralTypeMapping mappingFragment, EdmProperty edmProperty) + { + var matches = mappingFragment.PropertyMappings.Where(m => m.Property.Name == edmProperty.Name).ToList(); + if (matches.Count() != 1) + { + throw new EnumGeneratorException(string.Format( + "{0} matches found for property {1}", matches.Count(), edmProperty)); + } + var match = matches.Single(); + return match; + } + + private static MappingFragment FindSchemaMappingFragment(MetadataWorkspace metadata, EntityType entityType) + { + try + { + var conceptualEntitySet = FindConceptualEntity(metadata, entityType); + + // Child types in Table-per-Hierarchy don't have any mappings defined as they don't add any new tables, so skip them. + if (conceptualEntitySet == null) + { + return null; + } + + return FindStorageMappingFragmentFromConceptual(metadata, conceptualEntitySet); + } + catch (Exception exception) + { + throw new EnumGeneratorException(string.Format("Error getting schema mappings for entity type '{0}'", entityType.Name), exception); + } + } + + private static EntitySet FindConceptualEntity(MetadataWorkspace metadata, EntityType entityType) + { + var entityMetadata = FindObjectSpaceEntityMetadata(metadata, entityType); + + // Get the entity set that uses this entity type + var containers = metadata + .GetItems(DataSpace.CSpace); // CSpace = Conceptual model + if (containers.Count() != 1) + { + throw new EnumGeneratorException(string.Format("{0} EntityContainer's found.", containers.Count())); + } + var container = containers.Single(); + + var entitySets = container + .EntitySets + .Where(s => s.ElementType.Name == entityMetadata.Name) + // doesn't seem to be possible to get at the Object-Conceptual mappings from the public API so match on name. + .ToList(); + + // Child types in Table-per-Hierarchy don't have any mappings defined as they don't add any new tables, so skip them. + if (!entitySets.Any()) + { + return null; + } + + if (entitySets.Count() != 1) + { + throw new EnumGeneratorException(string.Format( + "{0} EntitySet's found for element type '{1}'.", entitySets.Count(), entityMetadata.Name)); + } + var entitySet = entitySets.Single(); + + return entitySet; + } + + private static EntityType FindObjectSpaceEntityMetadata(MetadataWorkspace metadata, EntityType entityType) + { + // Get the entity type from the model that maps to the CLR type + var entityTypes = metadata + .GetItems(DataSpace.OSpace) // OSpace = Object Space + .Where(e => e == entityType) + .ToList(); + if (entityTypes.Count() != 1) + { + throw new EnumGeneratorException(string.Format("{0} entities of type {1} found in mapping.", entityTypes.Count(), + entityType)); + } + var entityMetadata = entityTypes.Single(); + return entityMetadata; + } + + private static MappingFragment FindStorageMappingFragmentFromConceptual(MetadataWorkspace metadata, EntitySet conceptualEntitySet) + { + var storageMapping = FindStorageMapping(metadata, conceptualEntitySet); + + return FindStorageMappingFragmentInStorageMapping(storageMapping); + } + + private static MappingFragment FindStorageMappingFragmentInStorageMapping(EntitySetMapping storageMapping) + { + // Find the storage mapping fragment that the entity is mapped to + var entityTypeMappings = storageMapping.EntityTypeMappings; + var entityTypeMapping = entityTypeMappings.First(); + // using First() because Table-per-Hierarchy (TPH) produces multiple copies of the entity type mapping + var fragments = entityTypeMapping.Fragments; + if (fragments.Count() != 1) + { + throw new EnumGeneratorException(string.Format("{0} Fragments found.", fragments.Count())); + } + var fragment = fragments.Single(); + return fragment; + } + + private static EntitySetMapping FindStorageMapping(MetadataWorkspace metadata, EntitySet conceptualEntitySet) + { + // Find the mapping between conceptual and storage model for this entity set + var entityContainerMappings = metadata.GetItems(DataSpace.CSSpace); + // CSSpace = Conceptual model to Storage model mappings + if (entityContainerMappings.Count() != 1) + { + throw new EnumGeneratorException(string.Format("{0} EntityContainerMappings found.", entityContainerMappings.Count())); + } + var containerMapping = entityContainerMappings.Single(); + var mappings = containerMapping.EntitySetMappings.Where(s => s.EntitySet == conceptualEntitySet).ToList(); + if (mappings.Count() != 1) + { + throw new EnumGeneratorException(string.Format( + "{0} EntitySetMappings found for entitySet '{1}'.", mappings.Count(), conceptualEntitySet.Name)); + } + var mapping = mappings.Single(); + return mapping; + } + } +} diff --git a/EfEnumToLookup/LookupGenerator/SqlServerHandler.cs b/EfEnumToLookup/LookupGenerator/SqlServerHandler.cs new file mode 100644 index 0000000..6af1c61 --- /dev/null +++ b/EfEnumToLookup/LookupGenerator/SqlServerHandler.cs @@ -0,0 +1,110 @@ +namespace EfEnumToLookup.LookupGenerator +{ + using System; + using System.Collections.Generic; + using System.Data.SqlClient; + using System.Text; + + class SqlServerHandler : IDbHandler + { + /// + /// The size of the Name field that will be added to the generated lookup tables. + /// Adjust to suit your data if required, defaults to 255. + /// + public int NameFieldLength { get; set; } + + /// + /// Prefix to add to all the generated tables to separate help group them together + /// and make them stand out as different from other tables. + /// Defaults to "Enum_" set to null or "" to not have any prefix. + /// + public string TableNamePrefix { get; set; } + + /// + /// Suffix to add to all the generated tables to separate help group them together + /// and make them stand out as different from other tables. + /// Defaults to "" set to null or "" to not have any suffix. + /// + public string TableNameSuffix { get; set; } + + + public void Apply(List lookups, IList enumReferences, Action> runSql) + { + CreateTables(lookups, (sql) => runSql(sql, null)); + PopulateLookups(lookups, runSql); + AddForeignKeys(enumReferences, (sql) => runSql(sql, null)); + } + + private void CreateTables(IEnumerable enums, Action runSql) + { + foreach (var lookup in enums) + { + runSql(string.Format( + @"IF OBJECT_ID('{0}', 'U') IS NULL CREATE TABLE [{0}] (Id int PRIMARY KEY, Name nvarchar({1}));", + TableName(lookup.Name), NameFieldLength)); + } + } + + private void AddForeignKeys(IEnumerable refs, Action runSql) + { + foreach (var enumReference in refs) + { + var fkName = string.Format("FK_{0}_{1}", enumReference.ReferencingTable, enumReference.ReferencingField); + + var sql = string.Format( + " IF OBJECT_ID('{0}', 'F') IS NULL ALTER TABLE [{1}] ADD CONSTRAINT {0} FOREIGN KEY ([{2}]) REFERENCES [{3}] (Id);", + fkName, enumReference.ReferencingTable, enumReference.ReferencingField, TableName(enumReference.EnumType.Name) + ); + + runSql(sql); + } + } + + private void PopulateLookups(IEnumerable lookupData, Action> runSql) + { + foreach (var lookup in lookupData) + { + PopulateLookup(lookup, runSql); + } + } + + private void PopulateLookup(LookupData lookup, Action> runSql) + { + var sb = new StringBuilder(); + sb.AppendLine(string.Format("CREATE TABLE #lookups (Id int, Name nvarchar({0}) COLLATE database_default);", NameFieldLength)); + var parameters = new List(); + int paramIndex = 0; + foreach (var value in lookup.Values) + { + var id = value.Id; + var name = value.Name; + var idParamName = string.Format("id{0}", paramIndex++); + var nameParamName = string.Format("name{0}", paramIndex++); + sb.AppendLine(string.Format("INSERT INTO #lookups (Id, Name) VALUES (@{0}, @{1});", idParamName, nameParamName)); + parameters.Add(new SqlParameter(idParamName, id)); + parameters.Add(new SqlParameter(nameParamName, name)); + } + + sb.AppendLine(string.Format(@" +MERGE INTO [{0}] dst + USING #lookups src ON src.Id = dst.Id + WHEN MATCHED AND src.Name <> dst.Name THEN + UPDATE SET Name = src.Name + WHEN NOT MATCHED THEN + INSERT (Id, Name) + VALUES (src.Id, src.Name) + WHEN NOT MATCHED BY SOURCE THEN + DELETE +;" + , TableName(lookup.Name))); + + sb.AppendLine("DROP TABLE #lookups;"); + runSql(sb.ToString(), parameters); + } + + private string TableName(string enumName) + { + return string.Format("{0}{1}{2}", TableNamePrefix, enumName, TableNameSuffix); + } + } +}