diff --git a/docs/configuration.md b/docs/configuration.md index 85dd26d3..d0011caa 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -43,7 +43,7 @@ database: # schemas to include or empty to include all schemas: - dbo - + # list of expressions for tables to exclude, source is Schema.TableName exclude: - exact: dbo.SchemaVersions @@ -65,7 +65,7 @@ data: # how to generate names for the DbSet properties on the data context. Default: Plural propertyNaming: Preserve|Plural|Suffix - #include XML documentation + # include XML documentation document: false # entity class file configuration @@ -78,7 +78,7 @@ data: # how to generate relationship collections names for the entity. Default: Plural relationshipNaming: Preserve|Plural|Suffix - #include XML documentation + # include XML documentation document: false # Generate class names with prefixed schema name eg. dbo.MyTable = DboMyTable @@ -88,8 +88,16 @@ data: mapping: namespace: '{Project.Namespace}.Data.Mapping' # the mapping class namespace directory: '{Project.Directory}\Data\Mapping' # the mapping class output directory - #include XML documentation + # include XML documentation document: false + # globally indicates how a delete operation is applied to dependent entities in a relationship + # when the principal is deleted or the relationship is severed. + # Default: Cascade + globalRelationshipCascadeDeleteBehavior: Cascade|ClientCascade + # Default: SetNull + globalRelationshipSetNullDeleteBehavior: SetNull|ClientSetNull + # Default: NoAction + globalRelationshipNoActionDeleteBehavior: NoAction|ClientNoAction # query extension class file configuration query: @@ -98,7 +106,7 @@ data: uniquePrefix: GetBy # Prefix for queries built from unique indexes namespace: '{Project.Namespace}.Data.Queries' # the mapping class namespace directory: '{Project.Directory}\Data\Queries' # the mapping class output directory - #include XML documentation + # include XML documentation document: false #---------------------------------# @@ -172,11 +180,11 @@ model: # script templates script: # collection script template with EntityContext as a variable - context: + context: - templatePath: '.\templates\context.csx' # path to script file fileName: 'ContextScript.cs' # filename to save script output directory: '{Project.Directory}\Domain\Context' # directory to save script output - namespace: '{Project.Namespace}.Domain.Context' + namespace: '{Project.Namespace}.Domain.Context' baseClass: ContextScriptBase overwrite: true # overwrite existing file # collection of script template with current Entity as a variable @@ -184,7 +192,7 @@ script: - templatePath: '.\templates\entity.csx' # path to script file fileName: '{Entity.Name}Script.cs' # filename to save script output directory: '{Project.Directory}\Domain\Entity' # directory to save script output - namespace: '{Project.Namespace}.Domain.Entity' + namespace: '{Project.Namespace}.Domain.Entity' baseClass: EntityScriptBase overwrite: true # overwrite existing file # collection script template with current Model as a variable @@ -192,13 +200,13 @@ script: - templatePath: '.\templates\model.csx' # path to script file fileName: '{Model.Name}Script.cs' # filename to save script output directory: '{Project.Directory}\Domain\Models' # directory to save script output - namespace: '{Project.Namespace}.Domain.Models' + namespace: '{Project.Namespace}.Domain.Models' baseClass: ModelScriptBase overwrite: true # overwrite existing file - templatePath: '.\templates\sample.csx' # path to script file fileName: '{Model.Name}Sample.cs' # filename to save script output directory: '{Project.Directory}\Domain\Models' # directory to save script output - namespace: '{Project.Namespace}.Domain.Models' + namespace: '{Project.Namespace}.Domain.Models' baseClass: ModelSampleBase overwrite: true # overwrite existing file ``` diff --git a/src/EntityFrameworkCore.Generator.Core/Metadata/Generation/Relationship.cs b/src/EntityFrameworkCore.Generator.Core/Metadata/Generation/Relationship.cs index a1666ac6..56cff1a9 100644 --- a/src/EntityFrameworkCore.Generator.Core/Metadata/Generation/Relationship.cs +++ b/src/EntityFrameworkCore.Generator.Core/Metadata/Generation/Relationship.cs @@ -1,4 +1,6 @@ -using System.Diagnostics; +using System.Diagnostics; + +using Microsoft.EntityFrameworkCore.Migrations; namespace EntityFrameworkCore.Generator.Metadata.Generation; @@ -32,9 +34,9 @@ public Relationship() public Cardinality PrimaryCardinality { get; set; } - public bool? CascadeDelete { get; set; } + public ReferentialAction? ReferentialAction { get; set; } public bool IsForeignKey { get; set; } public bool IsMapped { get; set; } public bool IsOneToOne => Cardinality != Cardinality.Many && PrimaryCardinality != Cardinality.Many; -} \ No newline at end of file +} diff --git a/src/EntityFrameworkCore.Generator.Core/ModelGenerator.cs b/src/EntityFrameworkCore.Generator.Core/ModelGenerator.cs index 07890545..91042870 100644 --- a/src/EntityFrameworkCore.Generator.Core/ModelGenerator.cs +++ b/src/EntityFrameworkCore.Generator.Core/ModelGenerator.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Data; using System.Linq; @@ -259,6 +259,7 @@ private void CreateRelationship(EntityContext entityContext, Entity foreignEntit }; foreignEntity.Relationships.Add(foreignRelationship); } + foreignRelationship.ReferentialAction = tableKeySchema.OnDelete; foreignRelationship.IsMapped = true; foreignRelationship.IsForeignKey = true; foreignRelationship.Cardinality = foreignMembersRequired ? Cardinality.One : Cardinality.ZeroOrOne; @@ -679,4 +680,4 @@ private static bool IsIgnored(string name, IEnumerable excludeExpr return false; } -} \ No newline at end of file +} diff --git a/src/EntityFrameworkCore.Generator.Core/Options/MappingClassOptions.cs b/src/EntityFrameworkCore.Generator.Core/Options/MappingClassOptions.cs index 1fd84530..d08e49ed 100644 --- a/src/EntityFrameworkCore.Generator.Core/Options/MappingClassOptions.cs +++ b/src/EntityFrameworkCore.Generator.Core/Options/MappingClassOptions.cs @@ -1,4 +1,11 @@ -namespace EntityFrameworkCore.Generator.Options; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; + +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace EntityFrameworkCore.Generator.Options; /// /// EntityFramework mapping class generation options @@ -6,6 +13,10 @@ /// public class MappingClassOptions : ClassOptionsBase { + static List _validCascadeBehaviors = new List { DeleteBehavior.Cascade, DeleteBehavior.ClientCascade }; + static List _validSetNullBehaviors = new List { DeleteBehavior.SetNull, DeleteBehavior.ClientSetNull }; + static List _validNoActionBehaviors = new List { DeleteBehavior.NoAction, DeleteBehavior.ClientNoAction }; + /// /// Initializes a new instance of the class. /// @@ -14,5 +25,61 @@ public MappingClassOptions(VariableDictionary variables, string prefix) { Namespace = "{Project.Namespace}.Data.Mapping"; Directory = @"{Project.Directory}\Data\Mapping"; + GlobalRelationshipCascadeDeleteBehavior = DeleteBehavior.Cascade; + GlobalRelationshipSetNullDeleteBehavior = DeleteBehavior.SetNull; + GlobalRelationshipNoActionDeleteBehavior = DeleteBehavior.NoAction; + } + + public override bool Validate(ILogger logger) + { + var errors = new List(); + if (_validCascadeBehaviors.Any(a => a == GlobalRelationshipCascadeDeleteBehavior) == false) + { + errors.Add(GetBehaviorValidationError(nameof(GlobalRelationshipCascadeDeleteBehavior), GlobalRelationshipCascadeDeleteBehavior, _validCascadeBehaviors)); + } + if (_validSetNullBehaviors.Any(a => a == GlobalRelationshipSetNullDeleteBehavior) == false) + { + errors.Add(GetBehaviorValidationError(nameof(GlobalRelationshipSetNullDeleteBehavior), GlobalRelationshipSetNullDeleteBehavior, _validSetNullBehaviors)); + } + if (_validNoActionBehaviors.Any(a => a == GlobalRelationshipNoActionDeleteBehavior) == false) + { + errors.Add(GetBehaviorValidationError(nameof(GlobalRelationshipNoActionDeleteBehavior), GlobalRelationshipNoActionDeleteBehavior, _validNoActionBehaviors)); + } + + if (errors.Any()) + { + errors.ForEach(err => + { + logger.LogError(err); + }); + throw new InvalidEnumArgumentException(errors.FirstOrDefault()); + } + + // always call the base + return base.Validate(logger); + + string GetBehaviorValidationError(string propertyName, DeleteBehavior behavior, List behaviors) + { + var error = $"{propertyName} can only be set to {string.Join(" or ", behaviors)}. Received {behavior}"; + return error; + } } -} \ No newline at end of file + + /// + /// Gets or sets the delete behavior globally for all relationships who's foreign keys have a insert delete Cascade rule set. + /// + [DefaultValue(DeleteBehavior.Cascade)] + public DeleteBehavior GlobalRelationshipCascadeDeleteBehavior { get; set; } + + /// + /// Gets or sets the delete behavior globally for all relationships who's foreign keys have a insert delete Set Null rule set. + /// + [DefaultValue(DeleteBehavior.SetNull)] + public DeleteBehavior GlobalRelationshipSetNullDeleteBehavior { get; set; } + + /// + /// Gets or sets the delete behavior globally for all relationships who's foreign keys have a insert delete No Action rule set. + /// + [DefaultValue(DeleteBehavior.NoAction)] + public DeleteBehavior GlobalRelationshipNoActionDeleteBehavior { get; set; } +} diff --git a/src/EntityFrameworkCore.Generator.Core/Options/OptionsBase.cs b/src/EntityFrameworkCore.Generator.Core/Options/OptionsBase.cs index 512769fb..58e3bf25 100644 --- a/src/EntityFrameworkCore.Generator.Core/Options/OptionsBase.cs +++ b/src/EntityFrameworkCore.Generator.Core/Options/OptionsBase.cs @@ -1,6 +1,9 @@ -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; + using EntityFrameworkCore.Generator.Extensions; +using Microsoft.Extensions.Logging; + namespace EntityFrameworkCore.Generator.Options; public class OptionsBase @@ -20,6 +23,7 @@ public OptionsBase(VariableDictionary variables, string prefix) protected string Prefix { get; } + public virtual bool Validate(ILogger logger) => true; protected string GetProperty([CallerMemberName] string propertyName = null) { @@ -43,4 +47,4 @@ protected static string AppendPrefix(string root, string prefix) ? $"{root}.{prefix}" : prefix; } -} \ No newline at end of file +} diff --git a/src/EntityFrameworkCore.Generator.Core/Templates/MappingClassTemplate.cs b/src/EntityFrameworkCore.Generator.Core/Templates/MappingClassTemplate.cs index d870b261..7bfac45f 100644 --- a/src/EntityFrameworkCore.Generator.Core/Templates/MappingClassTemplate.cs +++ b/src/EntityFrameworkCore.Generator.Core/Templates/MappingClassTemplate.cs @@ -1,10 +1,13 @@ using System.Globalization; using System.Linq; + using EntityFrameworkCore.Generator.Extensions; using EntityFrameworkCore.Generator.Metadata.Generation; using EntityFrameworkCore.Generator.Options; + using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; namespace EntityFrameworkCore.Generator.Templates; @@ -104,7 +107,7 @@ private void GenerateConstants() CodeBuilder.AppendLine($"public const string Name = \"{_entity.TableName}\";"); } - + CodeBuilder.AppendLine("}"); CodeBuilder.AppendLine(); @@ -229,6 +232,33 @@ private void GenerateRelationshipMapping(Relationship relationship) CodeBuilder.Append("\")"); } + /* + * applies global delete behavior options to ALL foreign keys/relationships. individual + * foreign key configuration may be needed in the future, and would need mapping at a more + * granular level - similar to exclude.entities + */ + var onDeleteOption = Options.Data.Mapping.GlobalRelationshipNoActionDeleteBehavior.ToString(); + if (relationship.ReferentialAction == ReferentialAction.Cascade) + { + // possible options: Cascade | ClientCascade + onDeleteOption = Options.Data.Mapping.GlobalRelationshipCascadeDeleteBehavior.ToString(); + } + else if (relationship.ReferentialAction == ReferentialAction.SetNull) + { + // possible options: SetNull | ClientSetNull + onDeleteOption = Options.Data.Mapping.GlobalRelationshipSetNullDeleteBehavior.ToString(); + } + else if (relationship.ReferentialAction == ReferentialAction.Restrict) + { + onDeleteOption = nameof(DeleteBehavior.Restrict); + } + else if (relationship.ReferentialAction == ReferentialAction.SetDefault) + { + // it's not clear what SetDefault should map to. assuming NoAction, and taking the default + } + CodeBuilder.AppendLine(); + CodeBuilder.Append($".OnDelete({nameof(DeleteBehavior)}.{onDeleteOption})"); + CodeBuilder.DecrementIndent(); CodeBuilder.AppendLine(";"); @@ -362,4 +392,4 @@ private void GenerateTableMapping() CodeBuilder.AppendLine(); } -} \ No newline at end of file +} diff --git a/src/EntityFrameworkCore.Generator/EntityFrameworkCore.Generator.csproj b/src/EntityFrameworkCore.Generator/EntityFrameworkCore.Generator.csproj index e6f4512f..61ad0464 100644 --- a/src/EntityFrameworkCore.Generator/EntityFrameworkCore.Generator.csproj +++ b/src/EntityFrameworkCore.Generator/EntityFrameworkCore.Generator.csproj @@ -15,7 +15,10 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/EntityFrameworkCore.Generator/GenerateCommand.cs b/src/EntityFrameworkCore.Generator/GenerateCommand.cs index aa1f7925..9fd40cf0 100644 --- a/src/EntityFrameworkCore.Generator/GenerateCommand.cs +++ b/src/EntityFrameworkCore.Generator/GenerateCommand.cs @@ -1,8 +1,11 @@ -using EntityFrameworkCore.Generator.Extensions; +using System; + +using EntityFrameworkCore.Generator.Extensions; using EntityFrameworkCore.Generator.Options; + using McMaster.Extensions.CommandLineUtils; + using Microsoft.Extensions.Logging; -using System; namespace EntityFrameworkCore.Generator; @@ -73,9 +76,24 @@ protected override int OnExecute(CommandLineApplication application) if (Validator.HasValue) options.Model.Validator.Generate = Validator.Value; + options.Model.Read.Validate(Logger); + options.Model.Create.Validate(Logger); + options.Model.Update.Validate(Logger); + options.Model.Mapper.Validate(Logger); + options.Model.Shared.Validate(Logger); + options.Model.Update.Validate(Logger); + options.Model.Validator.Validate(Logger); + options.Data.Mapping.Validate(Logger); + options.Data.Query.Validate(Logger); + options.Data.Entity.Validate(Logger); + options.Data.Context.Validate(Logger); + options.Database.Validate(Logger); + options.Project.Validate(Logger); + options.Script.Validate(Logger); + var result = _codeGenerator.Generate(options); return result ? 0 : 1; } -} \ No newline at end of file +}