using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text.RegularExpressions; using CodeSmith.Engine; using SchemaExplorer; namespace SchemaMapper { public enum TableNaming { Mixed = 0, Plural = 1, Singular = 2 } public enum EntityNaming { Preserve = 0, Plural = 1, Singular = 2 } public enum RelationshipNaming { None = 0, Plural = 1, ListSuffix = 2 } public enum ContextNaming { Preserve = 0, Plural = 1, TableSuffix = 2 } public class GeneratorSettings { public GeneratorSettings() { RelationshipNaming = RelationshipNaming.ListSuffix; EntityNaming = EntityNaming.Singular; TableNaming = TableNaming.Singular; CleanExpressions = new List { @"^\d+" }; IgnoreExpressions = new List(); } public TableNaming TableNaming { get; set; } public EntityNaming EntityNaming { get; set; } public RelationshipNaming RelationshipNaming { get; set; } public ContextNaming ContextNaming { get; set; } public List IgnoreExpressions { get; set; } public List CleanExpressions { get; set; } public bool InclusionMode { get; set; } public bool IsIgnored(string name) { if (IgnoreExpressions.Count == 0) return false; bool isMatch = IgnoreExpressions.Any(regex => Regex.IsMatch(name, regex)); return InclusionMode ? !isMatch : isMatch; } public string CleanName(string name) { if (CleanExpressions.Count == 0) return name; foreach (var regex in CleanExpressions.Where(r => !string.IsNullOrEmpty(r))) if (Regex.IsMatch(name, regex)) return Regex.Replace(name, regex, ""); return name; } public string RelationshipName(string name) { if (RelationshipNaming == RelationshipNaming.None) return name; if (RelationshipNaming == RelationshipNaming.ListSuffix) return name + "List"; return StringUtil.ToPascalCase(StringUtil.ToPlural(name)); } public string ContextName(string name) { if (ContextNaming == ContextNaming.Preserve) return name; if (ContextNaming == ContextNaming.TableSuffix) return name + "Table"; return StringUtil.ToPascalCase(StringUtil.ToPlural(name)); } public string EntityName(string name) { if (TableNaming != TableNaming.Plural && EntityNaming == EntityNaming.Plural) name = StringUtil.ToPlural(name); else if (TableNaming != TableNaming.Singular && EntityNaming == EntityNaming.Singular) name = StringUtil.ToSingular(name); return StringUtil.ToPascalCase(name); } } public class SchemaItemProcessedEventArgs : EventArgs { public SchemaItemProcessedEventArgs(string name) { _name = name; } private readonly string _name; public string Name { get { return _name; } } } public class UniqueNamer { private readonly ConcurrentDictionary> _names; public UniqueNamer() { _names = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase); Comparer = StringComparer.CurrentCulture; // add existing UniqueContextName("ChangeTracker"); UniqueContextName("Configuration"); UniqueContextName("Database"); UniqueContextName("InternalContext"); } public IEqualityComparer Comparer { get; set; } public string UniqueName(string bucketName, string name) { var hashSet = _names.GetOrAdd(bucketName, k => new HashSet(Comparer)); string result = name.MakeUnique(hashSet.Contains); hashSet.Add(result); return result; } public string UniqueClassName(string className) { const string globalClassName = "global::ClassName"; return UniqueName(globalClassName, className); } public string UniqueContextName(string name) { const string globalContextname = "global::ContextName"; return UniqueName(globalContextname, name); } public string UniqueRelationshipName(string name) { const string globalContextname = "global::RelationshipName"; return UniqueName(globalContextname, name); } } public class Generator { private readonly UniqueNamer _namer; public Generator() { _settings = new GeneratorSettings(); _namer = new UniqueNamer(); } public event EventHandler SchemaItemProcessed; protected void OnSchemaItemProcessed(string name) { var handler = SchemaItemProcessed; if (handler == null) return; handler(this, new SchemaItemProcessedEventArgs(name)); } private readonly GeneratorSettings _settings; public GeneratorSettings Settings { get { return _settings; } } //按表信息创建DbContext public EntityContext Generate(TableSchema tableSchema) { // only DeepLoad when in ignore mode tableSchema.DeepLoad = !Settings.InclusionMode; var entityContext = new EntityContext(); entityContext.DatabaseName = tableSchema.Database.Name; string dataContextName = StringUtil.ToPascalCase(tableSchema.Database.Name) + "Context"; dataContextName = _namer.UniqueClassName(dataContextName); entityContext.ClassName = dataContextName; GetEntity(entityContext, tableSchema); return entityContext; } //按数据库连接信息创建DbContext public EntityContext Generate(DatabaseSchema databaseSchema) { // only DeepLoad when in ignore mode databaseSchema.DeepLoad = !Settings.InclusionMode; var entityContext = new EntityContext(); entityContext.DatabaseName = databaseSchema.Name; string dataContextName = StringUtil.ToPascalCase(databaseSchema.Name) + "Context"; dataContextName = _namer.UniqueClassName(dataContextName); entityContext.ClassName = dataContextName; foreach (TableSchema t in databaseSchema.Tables) { if (Settings.IsIgnored(t.FullName)) { Debug.WriteLine("Skipping Table: " + t.FullName); } else if (IsManyToMany(t)) { CreateManyToMany(entityContext, t); } else { Debug.WriteLine("Getting Table Schema: " + t.FullName); GetEntity(entityContext, t); } OnSchemaItemProcessed(t.FullName); } return entityContext; } //根据DbContext和tableSchema获取实体 public Entity GetEntity(EntityContext entityContext, TableSchema tableSchema, bool processRelationships = true, bool processMethods = true) { string key = tableSchema.FullName; Entity entity = entityContext.Entities.ByTable(key) ?? CreateEntity(entityContext, tableSchema); if (!entity.Properties.IsProcessed) CreateProperties(entity, tableSchema); if (processRelationships && !entity.Relationships.IsProcessed) CreateRelationships(entityContext, entity, tableSchema); if (processMethods && !entity.Methods.IsProcessed) CreateMethods(entity, tableSchema); entity.IsProcessed = true; return entity; } private Entity CreateEntity(EntityContext entityContext, TableSchema tableSchema) { var entity = new Entity { FullName = tableSchema.FullName, TableName = tableSchema.Name, TableSchema = tableSchema.Owner, Description = tableSchema.Description }; string className = ToClassName(tableSchema.Name); className = _namer.UniqueClassName(className); string mappingName = className + "Map"; mappingName = _namer.UniqueClassName(mappingName); string contextName = Settings.ContextName(className); contextName = ToPropertyName(entityContext.ClassName, contextName); contextName = _namer.UniqueContextName(contextName); entity.ClassName = className; entity.ContextName = contextName; entity.MappingName = mappingName; entityContext.Entities.Add(entity); return entity; } /// /// 创建实体的属性 /// private void CreateProperties(Entity entity, TableSchema tableSchema) { foreach (ColumnSchema columnSchema in tableSchema.Columns) { // skip unsupported type if (columnSchema.NativeType.Equals("hierarchyid", StringComparison.OrdinalIgnoreCase) || columnSchema.NativeType.Equals("sql_variant", StringComparison.OrdinalIgnoreCase)) { Debug.WriteLine(string.Format("Skipping column '{0}' because it has an unsupported db type '{1}'.", columnSchema.Name, columnSchema.NativeType)); continue; } Property property = entity.Properties.ByColumn(columnSchema.Name); if (property == null) { property = new Property { ColumnName = columnSchema.Name }; entity.Properties.Add(property); } string propertyName = ToPropertyName(entity.ClassName, columnSchema.Name); propertyName = _namer.UniqueName(entity.ClassName, propertyName); property.PropertyName = propertyName; property.DataType = columnSchema.DataType; property.SystemType = columnSchema.SystemType; property.NativeType = columnSchema.NativeType; property.Description = columnSchema.Description; property.IsPrimaryKey = columnSchema.IsPrimaryKeyMember; property.IsForeignKey = columnSchema.IsForeignKeyMember; property.IsNullable = columnSchema.AllowDBNull; property.IsIdentity = IsIdentity(columnSchema); property.IsRowVersion = IsRowVersion(columnSchema); property.IsAutoGenerated = IsDbGenerated(columnSchema); if (columnSchema.IsUnique) property.IsUnique = columnSchema.IsUnique; if (property.SystemType == typeof(string) || property.SystemType == typeof(byte[])) { property.MaxLength = columnSchema.Size; } if (property.SystemType == typeof(float) || property.SystemType == typeof(double) || property.SystemType == typeof(decimal)) { property.Precision = columnSchema.Precision; property.Scale = columnSchema.Scale; } property.IsProcessed = true; } entity.Properties.IsProcessed = true; } private void CreateRelationships(EntityContext entityContext, Entity entity, TableSchema tableSchema) { foreach (TableKeySchema tableKey in tableSchema.ForeignKeys) { if (Settings.IsIgnored(tableKey.ForeignKeyTable.FullName) || Settings.IsIgnored(tableKey.PrimaryKeyTable.FullName)) { Debug.WriteLine("Skipping relationship '{0}' because table '{1}' or '{2}' is ignored.", tableKey.FullName, tableKey.ForeignKeyTable.FullName, tableKey.PrimaryKeyTable.FullName); continue; } CreateRelationship(entityContext, entity, tableKey); } entity.Relationships.IsProcessed = true; } private void CreateRelationship(EntityContext entityContext, Entity foreignEntity, TableKeySchema tableKeySchema) { Entity primaryEntity = GetEntity(entityContext, tableKeySchema.PrimaryKeyTable, false, false); string primaryName = primaryEntity.ClassName; string foreignName = foreignEntity.ClassName; string relationshipName = tableKeySchema.Name; relationshipName = _namer.UniqueRelationshipName(relationshipName); bool isCascadeDelete = IsCascadeDelete(tableKeySchema); bool foreignMembersRequired; bool primaryMembersRequired; var foreignMembers = GetKeyMembers(foreignEntity, tableKeySchema.ForeignKeyMemberColumns, tableKeySchema.Name, out foreignMembersRequired); var primaryMembers = GetKeyMembers(primaryEntity, tableKeySchema.PrimaryKeyMemberColumns, tableKeySchema.Name, out primaryMembersRequired); Relationship foreignRelationship = foreignEntity.Relationships .FirstOrDefault(r => r.RelationshipName == relationshipName && r.IsForeignKey); if (foreignRelationship == null) { foreignRelationship = new Relationship { RelationshipName = relationshipName }; foreignEntity.Relationships.Add(foreignRelationship); } foreignRelationship.IsMapped = true; foreignRelationship.IsForeignKey = true; foreignRelationship.ThisCardinality = foreignMembersRequired ? Cardinality.One : Cardinality.ZeroOrOne; foreignRelationship.ThisEntity = foreignName; foreignRelationship.ThisProperties = new List(foreignMembers); foreignRelationship.OtherEntity = primaryName; foreignRelationship.OtherProperties = new List(primaryMembers); foreignRelationship.CascadeDelete = isCascadeDelete; string prefix = GetMemberPrefix(foreignRelationship, primaryName, foreignName); string foreignPropertyName = ToPropertyName(foreignEntity.ClassName, prefix + primaryName); foreignPropertyName = _namer.UniqueName(foreignEntity.ClassName, foreignPropertyName); foreignRelationship.ThisPropertyName = foreignPropertyName; // add reverse Relationship primaryRelationship = primaryEntity.Relationships .FirstOrDefault(r => r.RelationshipName == relationshipName && r.IsForeignKey == false); if (primaryRelationship == null) { primaryRelationship = new Relationship { RelationshipName = relationshipName }; primaryEntity.Relationships.Add(primaryRelationship); } primaryRelationship.IsMapped = false; primaryRelationship.IsForeignKey = false; primaryRelationship.ThisEntity = primaryName; primaryRelationship.ThisProperties = new List(primaryMembers); primaryRelationship.OtherEntity = foreignName; primaryRelationship.OtherProperties = new List(foreignMembers); primaryRelationship.CascadeDelete = isCascadeDelete; bool isOneToOne = IsOneToOne(tableKeySchema, foreignRelationship); if (isOneToOne) primaryRelationship.ThisCardinality = primaryMembersRequired ? Cardinality.One : Cardinality.ZeroOrOne; else primaryRelationship.ThisCardinality = Cardinality.Many; string primaryPropertyName = prefix + foreignName; if (!isOneToOne) primaryPropertyName = Settings.RelationshipName(primaryPropertyName); primaryPropertyName = ToPropertyName(primaryEntity.ClassName, primaryPropertyName); primaryPropertyName = _namer.UniqueName(primaryEntity.ClassName, primaryPropertyName); primaryRelationship.ThisPropertyName = primaryPropertyName; foreignRelationship.OtherPropertyName = primaryRelationship.ThisPropertyName; foreignRelationship.OtherCardinality = primaryRelationship.ThisCardinality; primaryRelationship.OtherPropertyName = foreignRelationship.ThisPropertyName; primaryRelationship.OtherCardinality = foreignRelationship.ThisCardinality; foreignRelationship.IsProcessed = true; primaryRelationship.IsProcessed = true; } private void CreateManyToMany(EntityContext entityContext, TableSchema joinTable) { if (joinTable.ForeignKeys.Count != 2) return; var joinTableName = joinTable.Name; var joinSchemaName = joinTable.Owner; // first fkey is always left, second fkey is right var leftForeignKey = joinTable.ForeignKeys[0]; var leftTable = leftForeignKey.PrimaryKeyTable; var joinLeftColumn = leftForeignKey.ForeignKeyMemberColumns.Select(c => c.Name).ToList(); var leftEntity = GetEntity(entityContext, leftTable, false, false); var rightForeignKey = joinTable.ForeignKeys[1]; var rightTable = rightForeignKey.PrimaryKeyTable; var joinRightColumn = rightForeignKey.ForeignKeyMemberColumns.Select(c => c.Name).ToList(); var rightEntity = GetEntity(entityContext, rightTable, false, false); string leftPropertyName = Settings.RelationshipName(rightEntity.ClassName); leftPropertyName = _namer.UniqueName(leftEntity.ClassName, leftPropertyName); string rightPropertyName = Settings.RelationshipName(leftEntity.ClassName); rightPropertyName = _namer.UniqueName(rightEntity.ClassName, rightPropertyName); string relationshipName = string.Format("{0}|{1}", leftForeignKey.Name, rightForeignKey.Name); relationshipName = _namer.UniqueRelationshipName(relationshipName); var left = new Relationship { RelationshipName = relationshipName }; left.IsForeignKey = false; left.IsMapped = true; left.ThisCardinality = Cardinality.Many; left.ThisEntity = leftEntity.ClassName; left.ThisPropertyName = leftPropertyName; left.OtherCardinality = Cardinality.Many; left.OtherEntity = rightEntity.ClassName; left.OtherPropertyName = rightPropertyName; left.JoinTable = joinTableName; left.JoinSchema = joinSchemaName; left.JoinThisColumn = new List(joinLeftColumn); left.JoinOtherColumn = new List(joinRightColumn); leftEntity.Relationships.Add(left); var right = new Relationship { RelationshipName = relationshipName }; right.IsForeignKey = false; right.IsMapped = false; right.ThisCardinality = Cardinality.Many; right.ThisEntity = rightEntity.ClassName; right.ThisPropertyName = rightPropertyName; right.OtherCardinality = Cardinality.Many; right.OtherEntity = leftEntity.ClassName; right.OtherPropertyName = leftPropertyName; right.JoinTable = joinTableName; right.JoinSchema = joinSchemaName; right.JoinThisColumn = new List(joinRightColumn); right.JoinOtherColumn = new List(joinLeftColumn); rightEntity.Relationships.Add(right); } private void CreateMethods(Entity entity, TableSchema tableSchema) { if (tableSchema.HasPrimaryKey) { var method = GetMethodFromColumns(entity, tableSchema.PrimaryKey.MemberColumns); if (method != null) { method.IsKey = true; method.SourceName = tableSchema.PrimaryKey.FullName; if (!entity.Methods.Any(m => m.NameSuffix == method.NameSuffix)) entity.Methods.Add(method); } } GetIndexMethods(entity, tableSchema); GetForeignKeyMethods(entity, tableSchema); entity.Methods.IsProcessed = true; } private static void GetForeignKeyMethods(Entity entity, TableSchema table) { var columns = new List(); foreach (ColumnSchema column in table.ForeignKeyColumns) { columns.Add(column); Method method = GetMethodFromColumns(entity, columns); if (method != null && !entity.Methods.Any(m => m.NameSuffix == method.NameSuffix)) entity.Methods.Add(method); columns.Clear(); } } private static void GetIndexMethods(Entity entity, TableSchema table) { foreach (IndexSchema index in table.Indexes) { Method method = GetMethodFromColumns(entity, index.MemberColumns); if (method == null) continue; method.SourceName = index.FullName; method.IsUnique = index.IsUnique; method.IsIndex = true; if (!entity.Methods.Any(m => m.NameSuffix == method.NameSuffix)) entity.Methods.Add(method); } } private static Method GetMethodFromColumns(Entity entity, IEnumerable columns) { var method = new Method(); string methodName = string.Empty; foreach (var column in columns) { var property = entity.Properties.ByColumn(column.Name); if (property == null) continue; method.Properties.Add(property); methodName += property.PropertyName; } if (method.Properties.Count == 0) return null; method.NameSuffix = methodName; return method; } private static List GetKeyMembers(Entity entity, IEnumerable members, string name, out bool isRequired) { var keyMembers = new List(); isRequired = false; foreach (var member in members) { var property = entity.Properties.ByColumn(member.Name); if (property == null) throw new InvalidOperationException(string.Format( "Could not find column {0} for relationship {1}.", member.Name, name)); if (!isRequired) isRequired = property.IsRequired; keyMembers.Add(property.PropertyName); } return keyMembers; } private static string GetMemberPrefix(Relationship relationship, string primaryClass, string foreignClass) { string thisKey = relationship.ThisProperties.FirstOrDefault() ?? string.Empty; string otherKey = relationship.OtherProperties.FirstOrDefault() ?? string.Empty; bool isSameName = thisKey.Equals(otherKey, StringComparison.OrdinalIgnoreCase); isSameName = (isSameName || thisKey.Equals(primaryClass + otherKey, StringComparison.OrdinalIgnoreCase)); string prefix = string.Empty; if (isSameName) return prefix; prefix = thisKey.Replace(otherKey, ""); prefix = prefix.Replace(primaryClass, ""); prefix = prefix.Replace(foreignClass, ""); prefix = Regex.Replace(prefix, @"(_ID|_id|_Id|\.ID|\.id|\.Id|ID|Id)$", ""); prefix = Regex.Replace(prefix, @"^\d", ""); return prefix; } private static bool IsOneToOne(TableKeySchema tableKeySchema, Relationship foreignRelationship) { bool isFkeyPkey = tableKeySchema.ForeignKeyTable.HasPrimaryKey && tableKeySchema.ForeignKeyTable.PrimaryKey != null && tableKeySchema.ForeignKeyTable.PrimaryKey.MemberColumns.Count == 1 && tableKeySchema.ForeignKeyTable.PrimaryKey.MemberColumns.Contains( foreignRelationship.ThisProperties.FirstOrDefault()); if (isFkeyPkey) return true; // if f.key is unique return tableKeySchema.ForeignKeyMemberColumns.All(column => column.IsUnique); } private static bool IsManyToMany(TableSchema tableSchema) { // 1) Table must have Two ForeignKeys. // 2) All columns must be either... // a) Member of a Foreign Key. // b) DbGenerated if (tableSchema.Columns.Count < 2) return false; if (tableSchema.ForeignKeyColumns.Count != 2) return false; // all columns are fkeys if (tableSchema.Columns.Count == 2 && tableSchema.ForeignKeyColumns.Count == 2) return true; // check all non fkey columns to make sure db gen'd return tableSchema.NonForeignKeyColumns.All(c => IsDbGenerated(c) || HasDefaultValue(c)); } #region Name Helpers private string ToClassName(string name) { name = Settings.EntityName(name); string legalName = ToLegalName(name); return legalName; } private string ToPropertyName(string className, string name) { string propertyName = ToLegalName(name); if (className.Equals(propertyName, StringComparison.OrdinalIgnoreCase)) propertyName += "Member"; return propertyName; } private string ToLegalName(string name) { string legalName = Settings.CleanName(name); legalName = StringUtil.ToPascalCase(legalName); return legalName; } #endregion #region Column Flag Helpers private static bool IsCascadeDelete(SchemaObjectBase column) { bool cascadeDelete = false; string value; try { if (column.ExtendedProperties.Contains(ExtendedPropertyNames.CascadeDelete)) { value = column.ExtendedProperties[ExtendedPropertyNames.CascadeDelete].Value.ToString(); bool.TryParse(value, out cascadeDelete); } } catch (Exception ex) { Debug.WriteLine("Error: " + ex.Message); } return cascadeDelete; } private static bool IsRowVersion(DataObjectBase column) { bool isTimeStamp = column.NativeType.Equals( "timestamp", StringComparison.OrdinalIgnoreCase); bool isRowVersion = column.NativeType.Equals( "rowversion", StringComparison.OrdinalIgnoreCase); return (isTimeStamp || isRowVersion); } private static bool IsDbGenerated(DataObjectBase column) { if (IsRowVersion(column)) return true; if (IsIdentity(column)) return true; bool isComputed = false; string value; try { if (column.ExtendedProperties.Contains(ExtendedPropertyNames.IsComputed)) { value = column.ExtendedProperties[ExtendedPropertyNames.IsComputed].Value.ToString(); bool.TryParse(value, out isComputed); } } catch (Exception ex) { Debug.WriteLine("Error: " + ex.Message); } return isComputed; } private static bool IsIdentity(DataObjectBase column) { string temp; bool isIdentity = false; try { if (column.ExtendedProperties.Contains(ExtendedPropertyNames.IsIdentity)) { temp = column.ExtendedProperties[ExtendedPropertyNames.IsIdentity].Value.ToString(); bool.TryParse(temp, out isIdentity); } } catch (Exception ex) { Debug.WriteLine("Error: " + ex.Message); } return isIdentity; } private static bool HasDefaultValue(DataObjectBase column) { try { if (!column.ExtendedProperties.Contains(ExtendedPropertyNames.DefaultValue)) return false; string value = column.ExtendedProperties[ExtendedPropertyNames.DefaultValue].Value.ToString(); return !string.IsNullOrEmpty(value); } catch (Exception ex) { Debug.WriteLine("Error: " + ex.Message); } return false; } #endregion } }