Skip to content

support foreign key delete rule options #435

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 18 additions & 10 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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

#---------------------------------#
Expand Down Expand Up @@ -172,33 +180,33 @@ 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
entity:
- 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
model:
- 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
```
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Diagnostics;
using System.Diagnostics;

using Microsoft.EntityFrameworkCore.Migrations;

namespace EntityFrameworkCore.Generator.Metadata.Generation;

Expand Down Expand Up @@ -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;
}
}
5 changes: 3 additions & 2 deletions src/EntityFrameworkCore.Generator.Core/ModelGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -679,4 +680,4 @@ private static bool IsIgnored(string name, IEnumerable<MatchOptions> excludeExpr
return false;
}

}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
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;

/// <summary>
/// EntityFramework mapping class generation options
/// </summary>
/// <seealso cref="ClassOptionsBase" />
public class MappingClassOptions : ClassOptionsBase
{
static List<DeleteBehavior> _validCascadeBehaviors = new List<DeleteBehavior> { DeleteBehavior.Cascade, DeleteBehavior.ClientCascade };
static List<DeleteBehavior> _validSetNullBehaviors = new List<DeleteBehavior> { DeleteBehavior.SetNull, DeleteBehavior.ClientSetNull };
static List<DeleteBehavior> _validNoActionBehaviors = new List<DeleteBehavior> { DeleteBehavior.NoAction, DeleteBehavior.ClientNoAction };

/// <summary>
/// Initializes a new instance of the <see cref="MappingClassOptions"/> class.
/// </summary>
Expand All @@ -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<string>();
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<DeleteBehavior> behaviors)
{
var error = $"{propertyName} can only be set to {string.Join(" or ", behaviors)}. Received {behavior}";
return error;
}
}
}

/// <summary>
/// Gets or sets the delete behavior globally for all relationships who's foreign keys have a insert delete <i>Cascade</i> rule set.
/// </summary>
[DefaultValue(DeleteBehavior.Cascade)]
public DeleteBehavior GlobalRelationshipCascadeDeleteBehavior { get; set; }

/// <summary>
/// Gets or sets the delete behavior globally for all relationships who's foreign keys have a insert delete <i>Set Null</i> rule set.
/// </summary>
[DefaultValue(DeleteBehavior.SetNull)]
public DeleteBehavior GlobalRelationshipSetNullDeleteBehavior { get; set; }

/// <summary>
/// Gets or sets the delete behavior globally for all relationships who's foreign keys have a insert delete <i>No Action</i> rule set.
/// </summary>
[DefaultValue(DeleteBehavior.NoAction)]
public DeleteBehavior GlobalRelationshipNoActionDeleteBehavior { get; set; }
}
8 changes: 6 additions & 2 deletions src/EntityFrameworkCore.Generator.Core/Options/OptionsBase.cs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
{
Expand All @@ -43,4 +47,4 @@ protected static string AppendPrefix(string root, string prefix)
? $"{root}.{prefix}"
: prefix;
}
}
}
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -104,7 +107,7 @@ private void GenerateConstants()

CodeBuilder.AppendLine($"public const string Name = \"{_entity.TableName}\";");
}

CodeBuilder.AppendLine("}");

CodeBuilder.AppendLine();
Expand Down Expand Up @@ -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(";");
Expand Down Expand Up @@ -362,4 +392,4 @@ private void GenerateTableMapping()

CodeBuilder.AppendLine();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="4.0.2" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="5.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
<PackageReference Include="ThisAssembly" Version="1.1.3" />
<PackageReference Include="ThisAssembly" Version="1.2.12">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
Expand Down
24 changes: 21 additions & 3 deletions src/EntityFrameworkCore.Generator/GenerateCommand.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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;
}

}
}