private void WriteOnConfiguringEFCore(ModelRoot modelRoot, List <string> segments) { Output("/// <inheritdoc />"); Output("protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)"); Output("{"); segments.Clear(); if (modelRoot.GetEntityFrameworkPackageVersionNum() >= 2.1 && modelRoot.LazyLoadingEnabled) { segments.Add("UseLazyLoadingProxies()"); } if (segments.Any()) { segments.Insert(0, "optionsBuilder"); if (modelRoot.ChopMethodChains) { OutputChopped(segments); } else { Output(string.Join(".", segments) + ";"); } NL(); } Output("CustomInit(optionsBuilder);"); Output("}"); NL(); }
public void GenerateEFCore(Manager manager, ModelRoot modelRoot) { if (modelRoot.EntityFrameworkVersion != EFVersion.EFCore) { throw new InvalidOperationException("Wrong generator selected"); } EFModelGenerator generator; switch ((int)modelRoot.GetEntityFrameworkPackageVersionNum()) { case 2: generator = new EFCore2ModelGenerator(this); break; case 3: generator = new EFCore3ModelGenerator(this); break; default: generator = new EFCore5ModelGenerator(this); break; } generator.Generate(manager); }
// Revisit if/when supported in EFCore // void WriteDatabaseInitializerEFCore(ModelRoot modelRoot) // { // Output("using System.Data.Entity;"); // NL(); // // BeginNamespace(modelRoot.Namespace); // // if (modelRoot.DatabaseInitializerType == DatabaseInitializerKind.MigrateDatabaseToLatestVersion) // Output($"public partial class {modelRoot.EntityContainerName}DatabaseInitializer : MigrateDatabaseToLatestVersion<{modelRoot.EntityContainerName}, {modelRoot.EntityContainerName}DbMigrationConfiguration>"); // else // Output($"public partial class {modelRoot.EntityContainerName}DatabaseInitializer : {modelRoot.DatabaseInitializerType}<{modelRoot.EntityContainerName}>"); // // Output("{"); // Output("}"); // EndNamespace(modelRoot.Namespace); // } // // void WriteMigrationConfigurationEFCore(ModelRoot modelRoot) // { // //if (modelRoot.DatabaseInitializerType != DatabaseInitializerKind.MigrateDatabaseToLatestVersion) // // return; // // Output("using System.Data.Entity.Migrations;"); // NL(); // // BeginNamespace(modelRoot.Namespace); // Output("public sealed partial class {0}DbMigrationConfiguration : DbMigrationsConfiguration<{0}>", modelRoot.EntityContainerName); // // Output("{"); // Output("partial void Init();"); // NL(); // // Output("public {0}DbMigrationConfiguration()", modelRoot.EntityContainerName); // Output("{"); // Output("AutomaticMigrationsEnabled = {0};", modelRoot.AutomaticMigrationsEnabled.ToString().ToLower()); // Output("AutomaticMigrationDataLossAllowed = false;"); // Output("Init();"); // Output("}"); // // Output("}"); // EndNamespace(modelRoot.Namespace); // } void WriteDbContextEFCore(ModelRoot modelRoot) { List <string> segments = new List <string>(); Output("using System;"); Output("using System.Collections.Generic;"); Output("using System.Linq;"); Output("using System.ComponentModel.DataAnnotations.Schema;"); Output("using Microsoft.EntityFrameworkCore;"); NL(); BeginNamespace(modelRoot.Namespace); if (!string.IsNullOrEmpty(modelRoot.Summary)) { Output("/// <summary>"); WriteCommentBody(modelRoot.Summary); Output("/// </summary>"); if (!string.IsNullOrEmpty(modelRoot.Description)) { Output("/// <remarks>"); WriteCommentBody(modelRoot.Description); Output("/// </remarks>"); } } else { Output("/// <inheritdoc/>"); } Output($"{modelRoot.EntityContainerAccess.ToString().ToLower()} partial class {modelRoot.EntityContainerName} : Microsoft.EntityFrameworkCore.DbContext"); Output("{"); PluralizationService pluralizationService = ModelRoot.PluralizationService; /***********************************************************************/ // generate DBSets /***********************************************************************/ ModelClass[] classesWithTables = null; switch (modelRoot.InheritanceStrategy) { case CodeStrategy.TablePerType: classesWithTables = modelRoot.Classes.Where(mc => !mc.IsDependentType).OrderBy(x => x.Name).ToArray(); break; case CodeStrategy.TablePerConcreteType: classesWithTables = modelRoot.Classes.Where(mc => !mc.IsDependentType && !mc.IsAbstract).OrderBy(x => x.Name).ToArray(); break; case CodeStrategy.TablePerHierarchy: classesWithTables = modelRoot.Classes.Where(mc => !mc.IsDependentType && mc.Superclass == null).OrderBy(x => x.Name).ToArray(); break; } if (classesWithTables != null) { Output("#region DbSets"); foreach (ModelClass modelClass in modelRoot.Classes.Where(x => !x.IsDependentType).OrderBy(x => x.Name)) { string dbSetName; if (!string.IsNullOrEmpty(modelClass.DbSetName)) { dbSetName = modelClass.DbSetName; } else { dbSetName = pluralizationService?.IsSingular(modelClass.Name) == true ? pluralizationService.Pluralize(modelClass.Name) : modelClass.Name; } if (!string.IsNullOrEmpty(modelClass.Summary)) { NL(); Output("/// <summary>"); WriteCommentBody($"Repository for {modelClass.FullName} - {modelClass.Summary}"); Output("/// </summary>"); } Output($"{modelRoot.DbSetAccess.ToString().ToLower()} virtual Microsoft.EntityFrameworkCore.DbSet<{modelClass.FullName}> {dbSetName} {{ get; set; }}"); } Output("#endregion DbSets"); NL(); } /***********************************************************************/ // constructors /***********************************************************************/ if (!string.IsNullOrEmpty(modelRoot.ConnectionString) || !string.IsNullOrEmpty(modelRoot.ConnectionStringName)) { string connectionString = string.IsNullOrEmpty(modelRoot.ConnectionString) ? $"Name={modelRoot.ConnectionStringName}" : modelRoot.ConnectionString; Output("/// <summary>"); Output("/// Default connection string"); Output("/// </summary>"); Output($"public static string ConnectionString {{ get; set; }} = @\"{connectionString}\";"); NL(); } //Output("/// <inheritdoc />"); //Output($"public {modelRoot.EntityContainerName}() : base()"); //Output("{"); //Output("}"); //NL(); Output("/// <inheritdoc />"); Output($"public {modelRoot.EntityContainerName}(DbContextOptions<{modelRoot.EntityContainerName}> options) : base(options)"); Output("{"); Output("}"); NL(); Output("partial void CustomInit(DbContextOptionsBuilder optionsBuilder);"); NL(); Output("/// <inheritdoc />"); Output("protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)"); Output("{"); segments.Clear(); if (modelRoot.GetEntityFrameworkPackageVersionNum() >= 2.1 && modelRoot.LazyLoadingEnabled) { segments.Add("UseLazyLoadingProxies()"); } if (segments.Any()) { segments.Insert(0, "optionsBuilder"); if (modelRoot.ChopMethodChains) { OutputChopped(segments); } else { Output(string.Join(".", segments) + ";"); } NL(); } Output("CustomInit(optionsBuilder);"); Output("}"); NL(); Output("partial void OnModelCreatingImpl(ModelBuilder modelBuilder);"); Output("partial void OnModelCreatedImpl(ModelBuilder modelBuilder);"); NL(); /***********************************************************************/ // OnModelCreating /***********************************************************************/ Output("/// <inheritdoc />"); Output("protected override void OnModelCreating(ModelBuilder modelBuilder)"); Output("{"); Output("base.OnModelCreating(modelBuilder);"); Output("OnModelCreatingImpl(modelBuilder);"); NL(); Output($"modelBuilder.HasDefaultSchema(\"{modelRoot.DatabaseSchema}\");"); List <Association> visited = new List <Association>(); List <string> foreignKeyColumns = new List <string>(); foreach (ModelClass modelClass in modelRoot.Classes.OrderBy(x => x.Name)) { segments.Clear(); foreignKeyColumns.Clear(); NL(); // class level segments.Add($"modelBuilder.{(modelClass.IsDependentType ? "Owned" : "Entity")}<{modelClass.FullName}>()"); foreach (ModelAttribute transient in modelClass.Attributes.Where(x => !x.Persistent)) { segments.Add($"Ignore(t => t.{transient.Name})"); } if (!modelClass.IsDependentType) { // note: this must come before the 'ToTable' call or there's a runtime error if (modelRoot.InheritanceStrategy == CodeStrategy.TablePerConcreteType && modelClass.Superclass != null) { segments.Add("Map(x => x.MapInheritedProperties())"); } if (classesWithTables.Contains(modelClass)) { segments.Add(modelClass.DatabaseSchema == modelClass.ModelRoot.DatabaseSchema ? $"ToTable(\"{modelClass.TableName}\")" : $"ToTable(\"{modelClass.TableName}\", \"{modelClass.DatabaseSchema}\")"); // primary key code segments must be output last, since HasKey returns a different type List <ModelAttribute> identityAttributes = modelClass.IdentityAttributes.ToList(); if (identityAttributes.Count == 1) { segments.Add($"HasKey(t => t.{identityAttributes[0].Name})"); } else if (identityAttributes.Count > 1) { segments.Add($"HasKey(t => new {{ t.{string.Join(", t.", identityAttributes.Select(ia => ia.Name))} }})"); } } } if (segments.Count > 1 || modelClass.IsDependentType) { if (modelRoot.ChopMethodChains) { OutputChopped(segments); } else { Output(string.Join(".", segments) + ";"); } } if (modelClass.IsDependentType) { continue; } // indexed properties foreach (ModelAttribute indexed in modelClass.Attributes.Where(x => x.Indexed && !x.IsIdentity)) { segments.Clear(); segments.Add($"modelBuilder.Entity<{modelClass.FullName}>().HasIndex(t => t.{indexed.Name})"); if (indexed.IndexedUnique) { segments.Add("IsUnique()"); } if (modelRoot.ChopMethodChains) { OutputChopped(segments); } else { Output(string.Join(".", segments) + ";"); } } // attribute level foreach (ModelAttribute modelAttribute in modelClass.Attributes.Where(x => x.Persistent && !SpatialTypes.Contains(x.Type))) { segments.Clear(); if (modelAttribute.MaxLength > 0) { segments.Add($"HasMaxLength({((int?)modelAttribute.MaxLength).Value})"); } if (modelAttribute.Required) { segments.Add("IsRequired()"); } if (modelAttribute.ColumnName != modelAttribute.Name && !string.IsNullOrEmpty(modelAttribute.ColumnName)) { segments.Add($"HasColumnName(\"{modelAttribute.ColumnName}\")"); } if (!modelAttribute.AutoProperty) { segments.Add($"HasField(\"_{modelAttribute.Name}\")"); segments.Add($"UsePropertyAccessMode(PropertyAccessMode.{(modelAttribute.PersistencePoint == PersistencePointType.Field ? "Field" : "Property")})"); } if (!string.IsNullOrEmpty(modelAttribute.ColumnType) && modelAttribute.ColumnType.ToLowerInvariant() != "default") { if (modelAttribute.ColumnType.ToLowerInvariant() == "varchar" || modelAttribute.ColumnType.ToLowerInvariant() == "nvarchar" || modelAttribute.ColumnType.ToLowerInvariant() == "char") { segments.Add($"HasColumnType(\"{modelAttribute.ColumnType}({(modelAttribute.MaxLength > 0 ? modelAttribute.MaxLength.ToString() : "max")})\")"); } else { segments.Add($"HasColumnType(\"{modelAttribute.ColumnType}\")"); } } if (modelAttribute.IsConcurrencyToken) { segments.Add("IsRowVersion()"); } if (modelAttribute.IsIdentity) { segments.Add(modelAttribute.IdentityType == IdentityType.AutoGenerated ? "ValueGeneratedOnAdd()" : "ValueGeneratedNever()"); } if (segments.Any()) { segments.Insert(0, $"modelBuilder.{(modelClass.IsDependentType ? "Owned" : "Entity")}<{modelClass.FullName}>()"); segments.Insert(1, $"Property(t => t.{modelAttribute.Name})"); if (modelRoot.ChopMethodChains) { OutputChopped(segments); } else { Output(string.Join(".", segments) + ";"); } } } bool hasDefinedConcurrencyToken = modelClass.AllAttributes.Any(x => x.IsConcurrencyToken); if (!hasDefinedConcurrencyToken && modelClass.EffectiveConcurrency == ConcurrencyOverride.Optimistic) { Output($@"modelBuilder.Entity<{modelClass.FullName}>().Property<byte[]>(""Timestamp"").IsConcurrencyToken();"); } // Navigation endpoints are distingished as Source and Target. They are also distinguished as Principal // and Dependent. How do these map? // In the case of one-to-one or zero-to-one-to-zero-to-one, it's model dependent and the user has to tell us // In all other cases, we can tell by the cardinalities of the associations // What matters is the Principal and Dependent classifications, so we look at those. // Source and Target are accidents of where the user started drawing the association. // navigation properties // ReSharper disable once LoopCanBePartlyConvertedToQuery foreach (UnidirectionalAssociation association in Association.GetLinksToTargets(modelClass) .OfType <UnidirectionalAssociation>() .Where(x => x.Persistent && !x.Target.IsDependentType)) { if (visited.Contains(association)) { continue; } visited.Add(association); segments.Clear(); segments.Add($"modelBuilder.Entity<{modelClass.FullName}>()"); bool required = false; switch (association.TargetMultiplicity) // realized by property on source { case Sawczyn.EFDesigner.EFModel.Multiplicity.ZeroMany: // TODO: Implement many-to-many if (association.SourceMultiplicity != Sawczyn.EFDesigner.EFModel.Multiplicity.ZeroMany) { segments.Add($"HasMany(x => x.{association.TargetPropertyName})"); } else { continue; } break; case Sawczyn.EFDesigner.EFModel.Multiplicity.One: segments.Add($"HasOne(x => x.{association.TargetPropertyName})"); break; case Sawczyn.EFDesigner.EFModel.Multiplicity.ZeroOne: segments.Add($"HasOne(x => x.{association.TargetPropertyName})"); break; //case Sawczyn.EFDesigner.EFModel.Multiplicity.OneMany: // segments.Add($"HasMany(x => x.{association.TargetPropertyName})"); // break; } string columnPrefix = association.SourceRole == EndpointRole.Dependent ? "" : association.Target.Name + "_"; switch (association.SourceMultiplicity) // realized by shadow property on target { case Sawczyn.EFDesigner.EFModel.Multiplicity.ZeroMany: // TODO: Implement many-to-many if (association.TargetMultiplicity != Sawczyn.EFDesigner.EFModel.Multiplicity.ZeroMany) { segments.Add("WithMany()"); segments.Add($"HasForeignKey(\"{columnPrefix}{association.TargetPropertyName}_Id\")"); } else { continue; } break; case Sawczyn.EFDesigner.EFModel.Multiplicity.One: segments.Add("WithOne()"); segments.Add(association.TargetMultiplicity != Sawczyn.EFDesigner.EFModel.Multiplicity.ZeroMany ? $"HasForeignKey<{association.Source.FullName}>(\"{columnPrefix}{association.TargetPropertyName}_Id\")" : $"HasForeignKey(\"{columnPrefix}{association.TargetPropertyName}_Id\")"); required = true; break; case Sawczyn.EFDesigner.EFModel.Multiplicity.ZeroOne: segments.Add("WithOne()"); segments.Add(association.TargetMultiplicity != Sawczyn.EFDesigner.EFModel.Multiplicity.ZeroMany ? $"HasForeignKey<{association.Source.FullName}>(\"{columnPrefix}{association.TargetPropertyName}_Id\")" : $"HasForeignKey(\"{columnPrefix}{association.TargetPropertyName}_Id\")"); break; //case Sawczyn.EFDesigner.EFModel.Multiplicity.OneMany: // segments.Add("HasMany()"); // break; } if (required) { segments.Add("IsRequired()"); } if (association.TargetRole == EndpointRole.Principal || association.SourceRole == EndpointRole.Principal) { DeleteAction deleteAction = association.SourceRole == EndpointRole.Principal ? association.SourceDeleteAction : association.TargetDeleteAction; switch (deleteAction) { case DeleteAction.None: segments.Add("OnDelete(DeleteBehavior.Restrict)"); break; case DeleteAction.Cascade: segments.Add("OnDelete(DeleteBehavior.Cascade)"); break; } } if (modelRoot.ChopMethodChains) { OutputChopped(segments); } else { Output(string.Join(".", segments) + ";"); } } foreach (UnidirectionalAssociation association in Association.GetLinksToTargets(modelClass) .OfType <UnidirectionalAssociation>() .Where(x => x.Persistent && x.Target.IsDependentType)) { if (association.TargetMultiplicity == Sawczyn.EFDesigner.EFModel.Multiplicity.ZeroOne || association.TargetMultiplicity == Sawczyn.EFDesigner.EFModel.Multiplicity.One) { Output($"modelBuilder.Entity<{modelClass.FullName}>().OwnsOne(x => x.{association.TargetPropertyName});"); } else { Output($"// Dependent 1-many association seen ({association.TargetPropertyName}). Code generation still unsupported in designer."); } } // ReSharper disable once LoopCanBePartlyConvertedToQuery foreach (BidirectionalAssociation association in Association.GetLinksToSources(modelClass) .OfType <BidirectionalAssociation>() .Where(x => x.Persistent)) { if (visited.Contains(association)) { continue; } visited.Add(association); // TODO: fix cascade delete bool required = false; segments.Clear(); segments.Add($"modelBuilder.Entity<{modelClass.FullName}>()"); switch (association.SourceMultiplicity) // realized by property on target { case Sawczyn.EFDesigner.EFModel.Multiplicity.ZeroMany: // TODO: Implement many-to-many if (association.TargetMultiplicity != Sawczyn.EFDesigner.EFModel.Multiplicity.ZeroMany) { segments.Add($"HasMany(x => x.{association.SourcePropertyName})"); } else { continue; } break; case Sawczyn.EFDesigner.EFModel.Multiplicity.One: segments.Add($"HasOne(x => x.{association.SourcePropertyName})"); break; case Sawczyn.EFDesigner.EFModel.Multiplicity.ZeroOne: segments.Add($"HasOne(x => x.{association.SourcePropertyName})"); break; //case Sawczyn.EFDesigner.EFModel.Multiplicity.OneMany: // segments.Add($"HasMany(x => x.{association.SourcePropertyName})"); // break; } switch (association.TargetMultiplicity) // realized by property on source { case Sawczyn.EFDesigner.EFModel.Multiplicity.ZeroMany: // TODO: Implement many-to-many if (association.SourceMultiplicity != Sawczyn.EFDesigner.EFModel.Multiplicity.ZeroMany) { segments.Add($"WithMany(x => x.{association.TargetPropertyName})"); } else { continue; } break; case Sawczyn.EFDesigner.EFModel.Multiplicity.One: segments.Add($"WithOne(x => x.{association.TargetPropertyName})"); required = true; break; case Sawczyn.EFDesigner.EFModel.Multiplicity.ZeroOne: segments.Add($"WithOne(x => x.{association.TargetPropertyName})"); break; //case Sawczyn.EFDesigner.EFModel.Multiplicity.OneMany: // segments.Add($"HasMany(x => x.{association.TargetPropertyName})"); // break; } string foreignKeySegment = CreateForeignKeyColumnSegmentEFCore(association , foreignKeyColumns , association.SourceMultiplicity != Sawczyn.EFDesigner.EFModel.Multiplicity.ZeroMany && association.TargetMultiplicity != Sawczyn.EFDesigner.EFModel.Multiplicity.ZeroMany); if (foreignKeySegment != null) { segments.Add(foreignKeySegment); } if (required) { segments.Add("IsRequired()"); } if (association.TargetRole == EndpointRole.Principal || association.SourceRole == EndpointRole.Principal) { DeleteAction deleteAction = association.SourceRole == EndpointRole.Principal ? association.SourceDeleteAction : association.TargetDeleteAction; switch (deleteAction) { case DeleteAction.None: segments.Add("OnDelete(DeleteBehavior.Restrict)"); break; case DeleteAction.Cascade: segments.Add("OnDelete(DeleteBehavior.Cascade)"); break; } } if (modelRoot.ChopMethodChains) { OutputChopped(segments); } else { Output(string.Join(".", segments) + ";"); } } } NL(); Output("OnModelCreatedImpl(modelBuilder);"); Output("}"); Output("}"); EndNamespace(modelRoot.Namespace); }