From 9de59d8b78a74757ab18978e9735aef4b54be0cd Mon Sep 17 00:00:00 2001 From: platyscript <244315239+platyscript@users.noreply.github.com> Date: Sat, 10 Jan 2026 10:56:22 +0530 Subject: [PATCH 1/4] Config changes --- sample.yaml | 49 ++++++++++ .../ConfigurationSerializer.cs | 2 +- .../OptionMapper.cs | 92 ++++++++++++++++++ .../Options/DatabaseMatchOptions.cs | 3 +- .../Options/GeneratorOptions.cs | 8 +- .../Options/MatchOptions.cs | 3 +- .../Options/OptionsBase.cs | 5 +- .../Options/SelectionOptions.cs | 3 +- .../SecretsStore.cs | 79 +++++++++++++++ .../CommandBase.cs | 23 +++++ .../DbApiBuilderEntityGenerator.csproj | 6 ++ .../GenerateCommand.cs | 53 ++++++++++ .../InitializeCommand.cs | 96 +++++++++++++++++++ .../OptionsCommandBase.cs | 23 +++++ src/DbApiBuilderEntityGenerator/Program.cs | 66 ++++++++++++- 15 files changed, 495 insertions(+), 16 deletions(-) create mode 100644 sample.yaml create mode 100644 src/DbApiBuilderEntityGenerator.Core/OptionMapper.cs create mode 100644 src/DbApiBuilderEntityGenerator.Core/SecretsStore.cs create mode 100644 src/DbApiBuilderEntityGenerator/CommandBase.cs create mode 100644 src/DbApiBuilderEntityGenerator/GenerateCommand.cs create mode 100644 src/DbApiBuilderEntityGenerator/InitializeCommand.cs create mode 100644 src/DbApiBuilderEntityGenerator/OptionsCommandBase.cs diff --git a/sample.yaml b/sample.yaml new file mode 100644 index 0000000..73181e6 --- /dev/null +++ b/sample.yaml @@ -0,0 +1,49 @@ +# the connection string to the database +connectionString: "Data Source=(local);Initial Catalog=Tracker;Integrated Security=True" + +# the database provider name. Default:SqlServer +provider: SqlServer + +# config name to read the connection string from the user secrets file +connectionName: "ConnectionStrings:Generator" + +# the user secret identifier, can be shared with .net core project +userSecretsId: "984ef0cf-2b22-4fd1-876d-e01499da4c1f" + +# the directory to output the configuration. Default is current. +outputDirectory: .\ + +# tables to include or empty to include all +tables: + - Priority + - Status + - Task + - User + +# schemas to include or empty to include all +schemas: + - dbo + +# exclude tables or columns +exclude: + # list of expressions for tables to exclude, source is Schema.TableName + tables: + - exact: dbo.SchemaVersions + - regex: dbo\.SchemaVersions$ + # list of expressions for columns to exclude, source is Schema.TableName.ColumnName + columns: + - exact: dbo.SchemaVersions\.Version + - regex: dbo\.SchemaVersions\.Version$ + +# how to generate entity class names from the table name. Preserve|Plural|Singular. Default: Singular +entityNaming: Singular + +# how to generate relationship collections names for the entity. Default: Plural +relationshipNaming: Plural + +# Rename entities and properties with regular expressions. Matched expressions will be removed. +renaming: + entities: + - ^(sp|tbl|udf|vw)_ + properties: + - ^{Table.Name}(?=Id|Name) diff --git a/src/DbApiBuilderEntityGenerator.Core/ConfigurationSerializer.cs b/src/DbApiBuilderEntityGenerator.Core/ConfigurationSerializer.cs index c27a73b..6e90040 100644 --- a/src/DbApiBuilderEntityGenerator.Core/ConfigurationSerializer.cs +++ b/src/DbApiBuilderEntityGenerator.Core/ConfigurationSerializer.cs @@ -25,7 +25,7 @@ public ConfigurationSerializer(ILogger logger) /// /// The options file name. Default 'generation.yml' /// - public const string OptionsFileName = "generation.yml"; + public const string OptionsFileName = "sample.yaml"; /// /// Loads the options file using the specified and . diff --git a/src/DbApiBuilderEntityGenerator.Core/OptionMapper.cs b/src/DbApiBuilderEntityGenerator.Core/OptionMapper.cs new file mode 100644 index 0000000..c3d6f3a --- /dev/null +++ b/src/DbApiBuilderEntityGenerator.Core/OptionMapper.cs @@ -0,0 +1,92 @@ +using System; +using DbApiBuilderEntityGenerator.Core.Options; +using DbApiBuilderEntityGenerator.Core.Serialization; + +namespace DbApiBuilderEntityGenerator.Core; + +public static class OptionMapper +{ + public static GeneratorOptions Map(GeneratorModel generator) + { + var options = new GeneratorOptions(); + options.Variables.ShouldEvaluate = false; + options.ConnectionName = generator.ConnectionName; + options.ConnectionString = generator.ConnectionString; + options.ConnectionName = generator.ConnectionName; + options.UserSecretsId = generator.UserSecretsId; + MapList(options.Tables, generator.Tables); + MapList(options.Schemas, generator.Schemas); + MapDatabaseMatch(options.Exclude, generator.Exclude); + options.EntityNaming = generator.EntityNaming; + options.RelationshipNaming = generator.RelationshipNaming; + MapSelection(options.Renaming, generator.Renaming); + + options.Variables.ShouldEvaluate = true; + + return options; + } + private static void MapSelection(SelectionOptions option, SelectionModel? selection) + { + if (selection == null) + return; + + MapList(option.Entities, selection.Entities, (match) => + { + var prefix = OptionsBase.AppendPrefix(option.Prefix, $"Entity{option.Entities.Count:0000}"); + return MapMatch(option.Variables, match, prefix); + }); + + MapList(option.Properties, selection.Properties, (match) => + { + var prefix = OptionsBase.AppendPrefix(option.Prefix, $"Property{option.Properties.Count:0000}"); + return MapMatch(option.Variables, match, prefix); + }); + } + private static void MapList(IList targetList, IList? sourceList) + { + if (sourceList == null || sourceList.Count == 0) + return; + + foreach (var source in sourceList) + targetList.Add(source); + } + + private static void MapList(IList targetList, IList? sourceList, Func factory) + { + if (sourceList == null || sourceList.Count == 0) + return; + + foreach (var source in sourceList) + { + var target = factory(source); + targetList.Add(target); + } + } + + private static MatchOptions MapMatch(VariableDictionary variables, MatchModel match, string? prefix) + { + return new MatchOptions() + { + Exact = match.Exact, + Expression = match.Expression + }; + } + + private static void MapDatabaseMatch(DatabaseMatchOptions option, DatabaseMatchModel? match) + { + if (match == null) + return; + + MapList(option.Tables, match.Tables, (match) => + { + var prefix = OptionsBase.AppendPrefix(option.Prefix, $"Table{option.Tables?.Count:0000}"); + return MapMatch(option.Variables, match, prefix); + }); + + MapList(option.Columns, match.Columns, (match) => + { + var prefix = OptionsBase.AppendPrefix(option.Prefix, $"Column{option.Columns?.Count:0000}"); + return MapMatch(option.Variables, match, prefix); + }); + } +} diff --git a/src/DbApiBuilderEntityGenerator.Core/Options/DatabaseMatchOptions.cs b/src/DbApiBuilderEntityGenerator.Core/Options/DatabaseMatchOptions.cs index cd47110..d9f6cec 100644 --- a/src/DbApiBuilderEntityGenerator.Core/Options/DatabaseMatchOptions.cs +++ b/src/DbApiBuilderEntityGenerator.Core/Options/DatabaseMatchOptions.cs @@ -12,8 +12,7 @@ public class DatabaseMatchOptions : OptionsBase /// /// A dictionary of variables used to configure the matching options. Cannot be null. /// An optional prefix used to filter or qualify the matching criteria. Can be null. - public DatabaseMatchOptions(VariableDictionary variables, string? prefix) - : base(variables, prefix) + public DatabaseMatchOptions() { Tables = []; Columns = []; diff --git a/src/DbApiBuilderEntityGenerator.Core/Options/GeneratorOptions.cs b/src/DbApiBuilderEntityGenerator.Core/Options/GeneratorOptions.cs index 92a3269..fbb2d0d 100644 --- a/src/DbApiBuilderEntityGenerator.Core/Options/GeneratorOptions.cs +++ b/src/DbApiBuilderEntityGenerator.Core/Options/GeneratorOptions.cs @@ -11,17 +11,17 @@ public class GeneratorOptions : OptionsBase /// /// Initializes a new instance of the class. /// - public GeneratorOptions(VariableDictionary variables, - string? prefix) : base(variables, null) + public GeneratorOptions() { - Variables = variables; + Variables = new VariableDictionary(); Provider = DatabaseProviders.SqlServer; Directory = @".\"; Tables = []; Schemas = []; - Exclude = new DatabaseMatchOptions(Variables, Prefix); + Exclude = new DatabaseMatchOptions(); EntityNaming = EntityNaming.Singular; RelationshipNaming = RelationshipNaming.Plural; + Renaming = new SelectionOptions(); } [YamlIgnore] diff --git a/src/DbApiBuilderEntityGenerator.Core/Options/MatchOptions.cs b/src/DbApiBuilderEntityGenerator.Core/Options/MatchOptions.cs index 02dddf1..dfa7ee8 100644 --- a/src/DbApiBuilderEntityGenerator.Core/Options/MatchOptions.cs +++ b/src/DbApiBuilderEntityGenerator.Core/Options/MatchOptions.cs @@ -15,8 +15,7 @@ public class MatchOptions : OptionsBase /// /// The shared variables dictionary. /// The variable key prefix. - public MatchOptions(VariableDictionary variables, string? prefix) - : base(variables, prefix) + public MatchOptions() { } diff --git a/src/DbApiBuilderEntityGenerator.Core/Options/OptionsBase.cs b/src/DbApiBuilderEntityGenerator.Core/Options/OptionsBase.cs index 07c9cad..dae0bd5 100644 --- a/src/DbApiBuilderEntityGenerator.Core/Options/OptionsBase.cs +++ b/src/DbApiBuilderEntityGenerator.Core/Options/OptionsBase.cs @@ -12,12 +12,11 @@ public class OptionsBase /// /// The shared variables dictionary. /// The variable key prefix. - public OptionsBase(VariableDictionary variables, string? prefix) + public OptionsBase() { - ArgumentNullException.ThrowIfNull(variables); } - public VariableDictionary Variables { get; } + public VariableDictionary Variables { get; } = new VariableDictionary(); public string? Prefix { get; } diff --git a/src/DbApiBuilderEntityGenerator.Core/Options/SelectionOptions.cs b/src/DbApiBuilderEntityGenerator.Core/Options/SelectionOptions.cs index eff102a..1a1c927 100644 --- a/src/DbApiBuilderEntityGenerator.Core/Options/SelectionOptions.cs +++ b/src/DbApiBuilderEntityGenerator.Core/Options/SelectionOptions.cs @@ -10,8 +10,7 @@ public class SelectionOptions : OptionsBase /// /// Initializes a new instance of the class. /// - public SelectionOptions(VariableDictionary variables, string? prefix) - : base(variables, prefix) + public SelectionOptions() { Entities = []; Properties = []; diff --git a/src/DbApiBuilderEntityGenerator.Core/SecretsStore.cs b/src/DbApiBuilderEntityGenerator.Core/SecretsStore.cs new file mode 100644 index 0000000..6b91e18 --- /dev/null +++ b/src/DbApiBuilderEntityGenerator.Core/SecretsStore.cs @@ -0,0 +1,79 @@ +using System; +using System.Text.Json; +using DbApiBuilderEntityGenerator.Core.Extensions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.UserSecrets; + +namespace DbApiBuilderEntityGenerator.Core; + +public class SecretsStore +{ + private readonly string _secretsFilePath; + private readonly IDictionary _secrets; + + public SecretsStore(string userSecretsId) + { + if (string.IsNullOrEmpty(userSecretsId)) + throw new ArgumentException("Value cannot be null or empty.", nameof(userSecretsId)); + + _secretsFilePath = PathHelper.GetSecretsPathFromSecretsId(userSecretsId); + + // workaround bug in configuration + var secretDir = Path.GetDirectoryName(_secretsFilePath); + if (secretDir.HasValue() && !Directory.Exists(secretDir)) + Directory.CreateDirectory(secretDir); + + _secrets = Load(userSecretsId); + } + + public string? this[string key] => _secrets[key]; + + public int Count => _secrets.Count; + + public bool ContainsKey(string key) => _secrets.ContainsKey(key); + + public IEnumerable> AsEnumerable() => _secrets; + + public void Clear() => _secrets.Clear(); + + public void Set(string key, string value) => _secrets[key] = value; + + public void Remove(string key) + { + _secrets.Remove(key); + } + + public virtual void Save() + { + var secretDir = Path.GetDirectoryName(_secretsFilePath); + if (secretDir.HasValue() && !Directory.Exists(secretDir)) + Directory.CreateDirectory(secretDir); + + var options = new JsonWriterOptions + { + Indented = true + }; + + using var stream = File.Create(_secretsFilePath); + using var writer = new Utf8JsonWriter(stream, options); + + writer.WriteStartObject(); + + foreach (var secret in _secrets.AsEnumerable()) + writer.WriteString(secret.Key, secret.Value); + + writer.WriteEndObject(); + writer.Flush(); + } + + protected virtual IDictionary Load(string userSecretsId) + { + return new ConfigurationBuilder() + .AddJsonFile(_secretsFilePath, optional: true) + .Build() + .AsEnumerable() + .Where(i => i.Value != null) + .ToDictionary(i => i.Key, i => i.Value, StringComparer.OrdinalIgnoreCase); + } + +} diff --git a/src/DbApiBuilderEntityGenerator/CommandBase.cs b/src/DbApiBuilderEntityGenerator/CommandBase.cs new file mode 100644 index 0000000..fccf666 --- /dev/null +++ b/src/DbApiBuilderEntityGenerator/CommandBase.cs @@ -0,0 +1,23 @@ +using System; +using McMaster.Extensions.CommandLineUtils; +using Microsoft.Extensions.Logging; + +namespace DbApiBuilderEntityGenerator; + +[HelpOption("--help")] +public abstract class CommandBase +{ + + protected CommandBase(ILoggerFactory logger, IConsole console) + { + Logger = logger.CreateLogger(GetType()); + Console = console; + } + + protected ILogger Logger { get; } + + protected IConsole Console { get; } + + protected abstract int OnExecute(CommandLineApplication application); + +} diff --git a/src/DbApiBuilderEntityGenerator/DbApiBuilderEntityGenerator.csproj b/src/DbApiBuilderEntityGenerator/DbApiBuilderEntityGenerator.csproj index 3d2e492..df84939 100644 --- a/src/DbApiBuilderEntityGenerator/DbApiBuilderEntityGenerator.csproj +++ b/src/DbApiBuilderEntityGenerator/DbApiBuilderEntityGenerator.csproj @@ -37,12 +37,18 @@ + + + + + + diff --git a/src/DbApiBuilderEntityGenerator/GenerateCommand.cs b/src/DbApiBuilderEntityGenerator/GenerateCommand.cs new file mode 100644 index 0000000..63dd119 --- /dev/null +++ b/src/DbApiBuilderEntityGenerator/GenerateCommand.cs @@ -0,0 +1,53 @@ +using System; +using DbApiBuilderEntityGenerator.Core; +using DbApiBuilderEntityGenerator.Core.Extensions; +using DbApiBuilderEntityGenerator.Core.Options; +using DbApiBuilderEntityGenerator.Core.Serialization; +using McMaster.Extensions.CommandLineUtils; +using Microsoft.Extensions.Logging; + +namespace DbApiBuilderEntityGenerator; + +[Command("generate", "gen")] +public class GenerateCommand : OptionsCommandBase +{ + public GenerateCommand(ILoggerFactory logger, IConsole console, IConfigurationSerializer serializer) : base(logger, console, serializer) + { + } + + [Option("-p ", Description = "Database provider to reverse engineer")] + public DatabaseProviders? Provider { get; set; } + + [Option("-c ", Description = "Database connection string to reverse engineer")] + public string? ConnectionString { get; set; } + + + protected override int OnExecute(CommandLineApplication application) + { + var workingDirectory = OutputDirectory ?? Environment.CurrentDirectory; + var configurationFile = OptionsFile ?? ConfigurationSerializer.OptionsFileName; + + var configuration = Serializer.Load(workingDirectory, configurationFile); + if (configuration == null) + { + Logger.LogInformation("Using default options"); + configuration = new GeneratorModel(); + } + + // override options + if (ConnectionString.HasValue()) + configuration.ConnectionString = ConnectionString; + + if (Provider.HasValue) + configuration.Provider = Provider.Value; + + + // convert to options format to support variables + var options = OptionMapper.Map(configuration); + + // var result = _codeGenerator.Generate(options); + + return 0; + + } +} diff --git a/src/DbApiBuilderEntityGenerator/InitializeCommand.cs b/src/DbApiBuilderEntityGenerator/InitializeCommand.cs new file mode 100644 index 0000000..b20ba49 --- /dev/null +++ b/src/DbApiBuilderEntityGenerator/InitializeCommand.cs @@ -0,0 +1,96 @@ +using System; +using DbApiBuilderEntityGenerator.Core.Serialization; +using DbApiBuilderEntityGenerator.Core.Options; +using McMaster.Extensions.CommandLineUtils; +using Microsoft.Extensions.Logging; +using DbApiBuilderEntityGenerator.Core; +using DbApiBuilderEntityGenerator.Core.Extensions; + +namespace DbApiBuilderEntityGenerator; + +[Command("initialize", "init")] +public class InitializeCommand : OptionsCommandBase +{ + public InitializeCommand(ILoggerFactory logger, IConsole console, IConfigurationSerializer serializer) : base(logger, console, serializer) + { + } + [Option("-p ", Description = "Database provider to reverse engineer")] + public DatabaseProviders? Provider { get; set; } + + [Option("-c ", Description = "Database connection string to reverse engineer")] + public string? ConnectionString { get; set; } + + [Option("--id ", Description = "The user secret ID to use")] + public string? UserSecretsId { get; set; } + + [Option("--name ", Description = "The user secret configuration name")] + public string? ConnectionName { get; set; } + protected override int OnExecute(CommandLineApplication application) + { + var outputDirectory = OutputDirectory ?? Environment.CurrentDirectory; + if (!Directory.Exists(outputDirectory)) + { + Logger.LogTrace($"Creating directory: {outputDirectory}"); + Directory.CreateDirectory(outputDirectory); + } + + var optionsFile = OptionsFile ?? ConfigurationSerializer.OptionsFileName; + + GeneratorModel? options = null; + if (Serializer.Exists(outputDirectory, optionsFile)) + options = Serializer.Load(outputDirectory, optionsFile); + if (options == null) + options = CreateOptionsFile(optionsFile); + + if (UserSecretsId.HasValue()) + options.UserSecretsId = UserSecretsId; + + if (ConnectionName.HasValue()) + options.ConnectionName = ConnectionName; + + if (Provider.HasValue) + options.Provider = Provider.Value; + + if (ConnectionString.HasValue()) + { + if (UserSecretsId.HasValue()) + options = CreateUserSecret(options, ConnectionString); + else + options.ConnectionString = ConnectionString; + } + + Serializer.Save(options, outputDirectory, optionsFile); + + return 0; + + } + + private GeneratorModel CreateUserSecret(GeneratorModel options, string connectionString) + { + if (options.UserSecretsId.IsNullOrWhiteSpace()) + options.UserSecretsId = Guid.NewGuid().ToString(); + + if (options.ConnectionName.IsNullOrWhiteSpace()) + options.ConnectionName = "ConnectionStrings:Generator"; + + Logger.LogInformation("Adding Connection String to User Secrets file"); + + // save connection string to user secrets file + var secretsStore = new SecretsStore(options.UserSecretsId); + secretsStore.Set(options.ConnectionName, connectionString); + secretsStore.Save(); + + return options; + } + + private GeneratorModel CreateOptionsFile(string optionsFile) + { + var options = new GeneratorModel(); + + options.OutputDirectory = ".\\"; + + Logger.LogInformation($"Creating options file: {optionsFile}"); + + return options; + } +} \ No newline at end of file diff --git a/src/DbApiBuilderEntityGenerator/OptionsCommandBase.cs b/src/DbApiBuilderEntityGenerator/OptionsCommandBase.cs new file mode 100644 index 0000000..f630f9d --- /dev/null +++ b/src/DbApiBuilderEntityGenerator/OptionsCommandBase.cs @@ -0,0 +1,23 @@ +using System; +using System.IdentityModel.Tokens.Jwt; +using DbApiBuilderEntityGenerator.Core; +using McMaster.Extensions.CommandLineUtils; +using Microsoft.Extensions.Logging; + +namespace DbApiBuilderEntityGenerator; + +public abstract class OptionsCommandBase : CommandBase +{ + protected OptionsCommandBase(ILoggerFactory logger, IConsole console, IConfigurationSerializer serializer) : base(logger, console) + { + this.Serializer = serializer; + } + + protected IConfigurationSerializer Serializer { get; private set; } + + [Option("-d ", Description = "The output directory")] + public string OutputDirectory { get; set; } = Environment.CurrentDirectory; + + [Option("-f ", Description = "The options file name")] + public string OptionsFile { get; set; } = ConfigurationSerializer.OptionsFileName; +} diff --git a/src/DbApiBuilderEntityGenerator/Program.cs b/src/DbApiBuilderEntityGenerator/Program.cs index 3751555..7b61cfc 100644 --- a/src/DbApiBuilderEntityGenerator/Program.cs +++ b/src/DbApiBuilderEntityGenerator/Program.cs @@ -1,2 +1,64 @@ -// See https://aka.ms/new-console-template for more information -Console.WriteLine("Hello, World!"); +using DbApiBuilderEntityGenerator; +using DbApiBuilderEntityGenerator.Core; +using McMaster.Extensions.CommandLineUtils; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Serilog; + +[Command("dab-entity-gen", Description = "Entity Framework Core model generation tool")] +[Subcommand(typeof(InitializeCommand))] +[Subcommand(typeof(GenerateCommand))] +[VersionOptionFromMember("--version", MemberName = nameof(GetVersion))] +public class Program : CommandBase +{ + public Program(ILoggerFactory logger, IConsole console) : base(logger, console) + { + } + + + public static int Main(string[] args) + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Verbose() + .Enrich.FromLogContext() + .WriteTo.Console(outputTemplate: "{Timestamp:HH:mm:ss} {Level:u1} {Message:lj}{NewLine}{Exception}") + .CreateLogger(); + + try + { + var services = new ServiceCollection() + .AddLogging(logger => logger + .AddSerilog() + .SetMinimumLevel(LogLevel.Information) + ) + .AddSingleton(PhysicalConsole.Singleton) + .AddTransient() + //.AddTransient() + .BuildServiceProvider(); + + var app = new CommandLineApplication(); + + app.Conventions + .UseDefaultConventions() + .UseConstructorInjection(services); + + return app.Execute(args); + } + catch (Exception ex) + { + Log.Fatal(ex, "Host terminated unexpectedly"); + return 1; + } + finally + { + Log.CloseAndFlush(); + } + } + + protected override int OnExecute(CommandLineApplication application) + { + application.ShowHelp(); + return 1; + } + private static string GetVersion() => ThisAssembly.InformationalVersion; +} \ No newline at end of file From c7496253cdc3dd74cdd6809376609d22f03f6291 Mon Sep 17 00:00:00 2001 From: platyscript <244315239+platyscript@users.noreply.github.com> Date: Sun, 11 Jan 2026 11:27:03 +0530 Subject: [PATCH 2/4] Commit test files --- .../CodeGenerator.cs | 150 ++++ .../DbApiBuilderEntityGenerator.Core.csproj | 2 + .../Extensions/EnumerableExtensions.cs | 45 ++ .../ICodeGenerator.cs | 9 + .../Metadata/Generation/Cardinality.cs | 9 + .../Metadata/Generation/Entity.cs | 229 ++++++ .../Metadata/Generation/EntityCollection.cs | 71 ++ .../Metadata/Generation/EntityContext.cs | 21 + .../Metadata/Generation/Method.cs | 25 + .../Metadata/Generation/MethodCollection.cs | 41 + .../Metadata/Generation/Model.cs | 56 ++ .../Metadata/Generation/ModelBase.cs | 17 + .../Metadata/Generation/ModelCollection.cs | 41 + .../Metadata/Generation/ModelType.cs | 11 + .../Metadata/Generation/Property.cs | 51 ++ .../Metadata/Generation/PropertyCollection.cs | 83 ++ .../Metadata/Generation/Relationship.cs | 41 + .../Generation/RelationshipCollection.cs | 67 ++ .../ModelGenerator.cs | 718 ++++++++++++++++++ .../UniqueNamer.cs | 70 ++ .../VariableConstants.cs | 12 + .../GenerateCommand.cs | 7 +- src/DbApiBuilderEntityGenerator/Program.cs | 2 +- tests/TestDatabase/Dockerfile | 10 + tests/TestDatabase/Script002.Tracker.Data.sql | 93 +++ tests/TestDatabase/Script003.Tracker.User.sql | 17 + tests/TestDatabase/Tracker.sql | 40 + tests/TestDatabase/build.sh | 34 + tests/TestDatabase/install-dw.sql | 8 + tests/TestDatabase/install.sh | 13 + tests/TestDatabase/install.sql | 8 + tests/TestDatabase/startup.sh | 4 + 32 files changed, 2002 insertions(+), 3 deletions(-) create mode 100644 src/DbApiBuilderEntityGenerator.Core/CodeGenerator.cs create mode 100644 src/DbApiBuilderEntityGenerator.Core/Extensions/EnumerableExtensions.cs create mode 100644 src/DbApiBuilderEntityGenerator.Core/ICodeGenerator.cs create mode 100644 src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/Cardinality.cs create mode 100644 src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/Entity.cs create mode 100644 src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/EntityCollection.cs create mode 100644 src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/EntityContext.cs create mode 100644 src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/Method.cs create mode 100644 src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/MethodCollection.cs create mode 100644 src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/Model.cs create mode 100644 src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/ModelBase.cs create mode 100644 src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/ModelCollection.cs create mode 100644 src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/ModelType.cs create mode 100644 src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/Property.cs create mode 100644 src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/PropertyCollection.cs create mode 100644 src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/Relationship.cs create mode 100644 src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/RelationshipCollection.cs create mode 100644 src/DbApiBuilderEntityGenerator.Core/ModelGenerator.cs create mode 100644 src/DbApiBuilderEntityGenerator.Core/UniqueNamer.cs create mode 100644 src/DbApiBuilderEntityGenerator.Core/VariableConstants.cs create mode 100644 tests/TestDatabase/Dockerfile create mode 100644 tests/TestDatabase/Script002.Tracker.Data.sql create mode 100644 tests/TestDatabase/Script003.Tracker.User.sql create mode 100644 tests/TestDatabase/Tracker.sql create mode 100644 tests/TestDatabase/build.sh create mode 100644 tests/TestDatabase/install-dw.sql create mode 100644 tests/TestDatabase/install.sh create mode 100644 tests/TestDatabase/install.sql create mode 100644 tests/TestDatabase/startup.sh diff --git a/src/DbApiBuilderEntityGenerator.Core/CodeGenerator.cs b/src/DbApiBuilderEntityGenerator.Core/CodeGenerator.cs new file mode 100644 index 0000000..6d1c677 --- /dev/null +++ b/src/DbApiBuilderEntityGenerator.Core/CodeGenerator.cs @@ -0,0 +1,150 @@ +using System; +using DbApiBuilderEntityGenerator.Core.Extensions; +using DbApiBuilderEntityGenerator.Core.Options; +using Microsoft.EntityFrameworkCore.Design; +using Microsoft.EntityFrameworkCore.Scaffolding; +using Microsoft.EntityFrameworkCore.Scaffolding.Metadata; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace DbApiBuilderEntityGenerator.Core; + +public class CodeGenerator : ICodeGenerator +{ + private readonly ILoggerFactory _loggerFactory; + private readonly ILogger _logger; + private readonly ModelGenerator _modelGenerator; + + public CodeGenerator(ILoggerFactory loggerFactory) + { + _loggerFactory = loggerFactory; + _logger = loggerFactory.CreateLogger(); + _modelGenerator = new ModelGenerator(loggerFactory); + // _synchronizer = new SourceSynchronizer(loggerFactory); + } + + public GeneratorOptions Options { get; set; } = null!; + public bool Generate(GeneratorOptions options) + { + Options = options ?? throw new ArgumentNullException(nameof(options)); + + var databaseProviders = GetDatabaseProviders(); + var databaseModel = GetDatabaseModel(databaseProviders.factory); + if (databaseModel == null) + throw new InvalidOperationException("Failed to create database model"); + + _logger.LogInformation("Loaded database model for: {databaseName}", databaseModel.DatabaseName); + + var context = _modelGenerator.Generate(Options, databaseModel, databaseProviders.mapping); + + return true; + } + private DatabaseModel GetDatabaseModel(IDatabaseModelFactory factory) + { + _logger.LogInformation("Loading database model ..."); + + + var connectionString = ResolveConnectionString(Options.ConnectionString, Options.UserSecretsId, Options.ConnectionName); + if (string.IsNullOrEmpty(connectionString)) + throw new InvalidOperationException("Could not find connection string."); + + var options = new DatabaseModelFactoryOptions(Options.Tables, Options.Schemas); + + return factory.Create(connectionString, options); + } + + private static string? ResolveConnectionString(string? connectionString, + string? userSecretsId, + string? connectionName) + { + if (connectionString.HasValue()) + return connectionString; + + if (userSecretsId.HasValue() && connectionName.HasValue()) + { + var secretsStore = new SecretsStore(userSecretsId); + if (secretsStore.ContainsKey(connectionName)) + return secretsStore[connectionName]; + } + + throw new InvalidOperationException("Could not find connection string."); + } + + private (IDatabaseModelFactory factory, IRelationalTypeMappingSource mapping) GetDatabaseProviders() + { + var provider = Options.Provider; + + _logger.LogDebug("Creating database model factory for: {provider}", provider); + + // start a new service container to create the database model factory + var services = new ServiceCollection() + .AddSingleton(_loggerFactory) + .AddEntityFrameworkDesignTimeServices(); + + switch (provider) + { + case DatabaseProviders.SqlServer: + ConfigureSqlServerServices(services); + break; + case DatabaseProviders.PostgreSQL: + ConfigurePostgresServices(services); + break; + case DatabaseProviders.MySQL: + ConfigureMySqlServices(services); + break; + case DatabaseProviders.Sqlite: + ConfigureSqliteServices(services); + break; + case DatabaseProviders.Oracle: + ConfigureOracleServices(services); + break; + default: + throw new NotSupportedException($"The specified provider '{provider}' is not supported."); + } + + var serviceProvider = services + .BuildServiceProvider(); + + var databaseModelFactory = serviceProvider + .GetRequiredService(); + + var typeMappingSource = serviceProvider + .GetRequiredService(); + + return (databaseModelFactory, typeMappingSource); + } + private static void ConfigureMySqlServices(IServiceCollection services) + { + var designTimeServices = new Pomelo.EntityFrameworkCore.MySql.Design.Internal.MySqlDesignTimeServices(); + designTimeServices.ConfigureDesignTimeServices(services); + services.AddEntityFrameworkMySqlNetTopologySuite(); + } + + private static void ConfigurePostgresServices(IServiceCollection services) + { + var designTimeServices = new Npgsql.EntityFrameworkCore.PostgreSQL.Design.Internal.NpgsqlDesignTimeServices(); + designTimeServices.ConfigureDesignTimeServices(services); + services.AddEntityFrameworkNpgsqlNetTopologySuite(); + } + + private static void ConfigureSqlServerServices(IServiceCollection services) + { + var designTimeServices = new Microsoft.EntityFrameworkCore.SqlServer.Design.Internal.SqlServerDesignTimeServices(); + designTimeServices.ConfigureDesignTimeServices(services); + services.AddEntityFrameworkSqlServerNetTopologySuite(); + } + + private static void ConfigureSqliteServices(IServiceCollection services) + { + var designTimeServices = new Microsoft.EntityFrameworkCore.Sqlite.Design.Internal.SqliteDesignTimeServices(); + designTimeServices.ConfigureDesignTimeServices(services); + services.AddEntityFrameworkSqliteNetTopologySuite(); + } + + private static void ConfigureOracleServices(IServiceCollection services) + { + var designTimeServices = new Oracle.EntityFrameworkCore.Design.Internal.OracleDesignTimeServices(); + designTimeServices.ConfigureDesignTimeServices(services); + } +} diff --git a/src/DbApiBuilderEntityGenerator.Core/DbApiBuilderEntityGenerator.Core.csproj b/src/DbApiBuilderEntityGenerator.Core/DbApiBuilderEntityGenerator.Core.csproj index 843930f..167a69b 100644 --- a/src/DbApiBuilderEntityGenerator.Core/DbApiBuilderEntityGenerator.Core.csproj +++ b/src/DbApiBuilderEntityGenerator.Core/DbApiBuilderEntityGenerator.Core.csproj @@ -24,6 +24,8 @@ + + diff --git a/src/DbApiBuilderEntityGenerator.Core/Extensions/EnumerableExtensions.cs b/src/DbApiBuilderEntityGenerator.Core/Extensions/EnumerableExtensions.cs new file mode 100644 index 0000000..a66fc3a --- /dev/null +++ b/src/DbApiBuilderEntityGenerator.Core/Extensions/EnumerableExtensions.cs @@ -0,0 +1,45 @@ +using System; + +namespace DbApiBuilderEntityGenerator.Core.Extensions; + +/// +/// Provides extension methods for to assist with string formatting and conversion. +/// +public static partial class EnumerableExtensions +{ + /// + /// Concatenates the members of a sequence, using the specified delimiter between each member, and returns the resulting string. + /// + /// + /// The type of the elements in the sequence. + /// + /// + /// The sequence of values to concatenate. Each value will be converted to a string using its ToString() method. + /// + /// + /// The string to use as a delimiter. If null, a comma (",") is used by default. + /// + /// + /// A string that consists of the elements in delimited by the string. + /// If is empty, returns . + /// + public static string ToDelimitedString(this IEnumerable values, string? delimiter = ",") + => string.Join(delimiter ?? ",", values); + + /// + /// Concatenates the members of a sequence of strings, using the specified delimiter between each member, and returns the resulting string. + /// + /// + /// The sequence of string values to concatenate. null values are treated as empty strings. + /// + /// + /// The string to use as a delimiter. If null, a comma (",") is used by default. + /// + /// + /// A string that consists of the elements in delimited by the string. + /// If is empty, returns . + /// + public static string ToDelimitedString(this IEnumerable values, string? delimiter = ",") + => string.Join(delimiter ?? ",", values); + +} diff --git a/src/DbApiBuilderEntityGenerator.Core/ICodeGenerator.cs b/src/DbApiBuilderEntityGenerator.Core/ICodeGenerator.cs new file mode 100644 index 0000000..67cc838 --- /dev/null +++ b/src/DbApiBuilderEntityGenerator.Core/ICodeGenerator.cs @@ -0,0 +1,9 @@ +using System; +using DbApiBuilderEntityGenerator.Core.Options; + +namespace DbApiBuilderEntityGenerator.Core; + +public interface ICodeGenerator +{ + bool Generate(GeneratorOptions options); +} diff --git a/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/Cardinality.cs b/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/Cardinality.cs new file mode 100644 index 0000000..d0b6115 --- /dev/null +++ b/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/Cardinality.cs @@ -0,0 +1,9 @@ +namespace DbApiBuilderEntityGenerator.Core.Metadata.Generation; + +public enum Cardinality +{ + ZeroOrOne, + One, + Many + +} diff --git a/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/Entity.cs b/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/Entity.cs new file mode 100644 index 0000000..476916a --- /dev/null +++ b/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/Entity.cs @@ -0,0 +1,229 @@ +using System; +using System.Diagnostics; +using DbApiBuilderEntityGenerator.Core; + +namespace DbApiBuilderEntityGenerator.Core.Metadata.Generation; + +/// +/// An entity model for a database table used when reverse engineering an existing database. +/// +/// +[DebuggerDisplay("Class: {EntityClass}, Table: {TableName}, Context: {ContextProperty}")] +public class Entity : ModelBase, IOptionVariable +{ + /// + /// Initializes a new instance of the class. + /// + public Entity() + { + Properties = []; + Relationships = []; + Methods = []; + Models = []; + } + + /// + /// Gets or sets the parent this entity belong to. + /// + /// + /// The parent context this entity belongs to. + /// + public EntityContext Context { get; set; } = null!; + + /// + /// Gets or sets the property name for this entity on the data context. + /// + /// + /// The property name for this entity on the data context. + /// + public string ContextProperty { get; set; } = null!; + + + /// + /// Gets or sets the entity namespace. + /// + /// + /// The entity namespace. + /// + public string EntityNamespace { get; set; } = null!; + + /// + /// Gets or sets the name of the entity class. + /// + /// + /// The name of the entity class. + /// + public string EntityClass { get; set; } = null!; + + /// + /// Gets or sets the entity base class. + /// + /// + /// The entity base class. + /// + public string? EntityBaseClass { get; set; } + + + /// + /// Gets or sets the mapping namespace. + /// + /// + /// The mapping namespace. + /// + public string MappingNamespace { get; set; } = null!; + + /// + /// Gets or sets the name of the table mapping class. + /// + /// + /// The name of the table mapping class. + /// + public string MappingClass { get; set; } = null!; + + + /// + /// Gets or sets the mapper class. + /// + /// + /// The mapper class. + /// + public string MapperClass { get; set; } = null!; + + /// + /// Gets or sets the mapper namespace. + /// + /// + /// The mapper namespace. + /// + public string MapperNamespace { get; set; } = null!; + + /// + /// Gets or sets the mapper base class. + /// + /// + /// The mapper base class. + /// + public string? MapperBaseClass { get; set; } + + + /// + /// Gets or sets the table schema. + /// + /// + /// The table schema. + /// + public string? TableSchema { get; set; } + + /// + /// Gets or sets the name of the table. + /// + /// + /// The name of the table. + /// + public string TableName { get; set; } = null!; + + + /// + /// Gets or sets the entity's properties. + /// + /// + /// The entity's properties. + /// + public PropertyCollection Properties { get; set; } + + /// + /// Gets or sets the entity's relationships. + /// + /// + /// The entity's relationships. + /// + public RelationshipCollection Relationships { get; set; } + + /// + /// Gets or sets the entity's methods. + /// + /// + /// The entity's methods. + /// + public MethodCollection Methods { get; set; } + + + /// + /// Gets or sets the models for this entity. + /// + /// + /// The models for this entity. + /// + public ModelCollection Models { get; set; } + + /// + /// Gets or sets a value indicating whether this instance is view. + /// + /// + /// true if this instance is view; otherwise, false. + /// + public bool IsView { get; set; } + + + /// + /// Gets or sets the name of the temporal table. + /// + /// + /// The name of the temporal table. + /// + public string? TemporalTableName { get; set; } + + /// + /// Gets or sets the temporal table schema. + /// + /// + /// The temporal table schema. + /// + public string? TemporalTableSchema { get; set; } + + /// + /// Gets or sets the temporal start property. + /// + /// + /// The temporal start property. + /// + public string? TemporalStartProperty { get; set; } + + /// + /// Gets or sets the temporal start column. + /// + /// + /// The temporal start column. + /// + public string? TemporalStartColumn { get; set; } + + /// + /// Gets or sets the temporal end property. + /// + /// + /// The temporal end property. + /// + public string? TemporalEndProperty { get; set; } + + /// + /// Gets or sets the temporal end column. + /// + /// + /// The temporal end column. + /// + public string? TemporalEndColumn { get; set; } + + void IOptionVariable.Set(VariableDictionary variableDictionary) + { + variableDictionary.Set(VariableConstants.TableSchema, TableSchema); + variableDictionary.Set(VariableConstants.TableName, TableName); + variableDictionary.Set(VariableConstants.EntityName, EntityClass); + } + + void IOptionVariable.Remove(VariableDictionary variableDictionary) + { + variableDictionary.Remove(VariableConstants.TableSchema); + variableDictionary.Remove(VariableConstants.TableName); + variableDictionary.Remove(VariableConstants.EntityName); + } +} diff --git a/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/EntityCollection.cs b/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/EntityCollection.cs new file mode 100644 index 0000000..9cb01b4 --- /dev/null +++ b/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/EntityCollection.cs @@ -0,0 +1,71 @@ +using System; + +namespace DbApiBuilderEntityGenerator.Core.Metadata.Generation; + +/// +/// A collection of +/// +public class EntityCollection : List +{ + /// + /// Initializes a new instance of the class. + /// + public EntityCollection() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The collection whose elements are copied to the new list. + public EntityCollection(IEnumerable collection) : base(collection) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The number of elements that the new list can initially store. + public EntityCollection(int capacity) : base(capacity) + { + } + + /// + /// Gets or sets a value indicating whether this instance is processed. + /// + /// + /// true if this instance is processed; otherwise, false. + /// + public bool IsProcessed { get; set; } + + /// + /// Get with the specified table and . + /// + /// The name of the table. + /// The table schema. + /// + /// The with the specified table and . + /// + public Entity? ByTable(string? tableName, string? tableSchema) + { + if (string.IsNullOrEmpty(tableName) && string.IsNullOrEmpty(tableSchema)) + return null; + + return this.FirstOrDefault(x => x.TableName == tableName && x.TableSchema == tableSchema); + } + + /// + /// Get with the specified . + /// + /// Name of the class. + /// + /// The with the specified . + /// + public Entity? ByClass(string? className) + { + if (string.IsNullOrEmpty(className)) + return null; + + return this.FirstOrDefault(x => x.EntityClass == className); + } +} diff --git a/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/EntityContext.cs b/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/EntityContext.cs new file mode 100644 index 0000000..dfd34e6 --- /dev/null +++ b/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/EntityContext.cs @@ -0,0 +1,21 @@ +using System; + +namespace DbApiBuilderEntityGenerator.Core.Metadata.Generation; + +public class EntityContext : ModelBase +{ + public EntityContext() + { + Entities = []; + } + + public string ContextNamespace { get; set; } = null!; + + public string ContextClass { get; set; } = null!; + + public string? ContextBaseClass { get; set; } + + public string? DatabaseName { get; set; } + + public EntityCollection Entities { get; set; } +} diff --git a/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/Method.cs b/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/Method.cs new file mode 100644 index 0000000..256a1f6 --- /dev/null +++ b/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/Method.cs @@ -0,0 +1,25 @@ +using System; +using System.Diagnostics; + +namespace DbApiBuilderEntityGenerator.Core.Metadata.Generation; + +[DebuggerDisplay("Suffix: {NameSuffix}, IsKey: {IsKey}, IsUnique: {IsUnique}")] +public class Method : ModelBase +{ + public Method() + { + Properties = []; + } + + public Entity Entity { get; set; } = null!; + + public string? NameSuffix { get; set; } + public string? SourceName { get; set; } + + public bool IsKey { get; set; } + public bool IsUnique { get; set; } + public bool IsIndex { get; set; } + + public PropertyCollection Properties { get; set; } + +} diff --git a/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/MethodCollection.cs b/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/MethodCollection.cs new file mode 100644 index 0000000..ef8ab08 --- /dev/null +++ b/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/MethodCollection.cs @@ -0,0 +1,41 @@ +using System; + +namespace DbApiBuilderEntityGenerator.Core.Metadata.Generation; + +/// +/// A collection of instances +/// +public class MethodCollection : List +{ + /// + /// Initializes a new instance of the class. + /// + public MethodCollection() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The collection whose elements are copied to the new list. + public MethodCollection(IEnumerable collection) : base(collection) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The number of elements that the new list can initially store. + public MethodCollection(int capacity) : base(capacity) + { + } + + /// + /// Gets or sets a value indicating whether this instance is processed. + /// + /// + /// true if this instance is processed; otherwise, false. + /// + public bool IsProcessed { get; set; } + +} diff --git a/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/Model.cs b/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/Model.cs new file mode 100644 index 0000000..1dbc531 --- /dev/null +++ b/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/Model.cs @@ -0,0 +1,56 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata.Internal; + +namespace DbApiBuilderEntityGenerator.Core.Metadata.Generation; + +public class Model : ModelBase, IOptionVariable +{ + public Model() + { + Properties = []; + } + + public Entity Entity { get; set; } = null!; + + public ModelType ModelType { get; set; } + + public string ModelNamespace { get; set; } = null!; + + public string ModelClass { get; set; } = null!; + + public string? ModelBaseClass { get; set; } + + public string? ModelAttributes { get; set; } + + public string? ModelHeader { get; internal set; } + + + public string? ValidatorNamespace { get; set; } + + public string? ValidatorClass { get; set; } + + public string? ValidatorBaseClass { get; set; } + + + public PropertyCollection Properties { get; set; } + + void IOptionVariable.Set(VariableDictionary variableDictionary) + { + variableDictionary.Set(VariableConstants.ModelName, ModelClass); + } + + void IOptionVariable.Remove(VariableDictionary variableDictionary) + { + variableDictionary.Remove(VariableConstants.ModelName); + } + + public void Remove(VariableDictionary variableDictionary) + { + throw new NotImplementedException(); + } + + public void Set(VariableDictionary variableDictionary) + { + throw new NotImplementedException(); + } +} diff --git a/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/ModelBase.cs b/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/ModelBase.cs new file mode 100644 index 0000000..1193495 --- /dev/null +++ b/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/ModelBase.cs @@ -0,0 +1,17 @@ +using System; + +namespace DbApiBuilderEntityGenerator.Core.Metadata.Generation; + +/// +/// A base class for entity generation models +/// +public class ModelBase +{ + /// + /// Gets or sets a value indicating whether this instance is processed. + /// + /// + /// true if this instance is processed; otherwise, false. + /// + public bool IsProcessed { get; set; } +} diff --git a/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/ModelCollection.cs b/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/ModelCollection.cs new file mode 100644 index 0000000..f183709 --- /dev/null +++ b/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/ModelCollection.cs @@ -0,0 +1,41 @@ +using System; + +namespace DbApiBuilderEntityGenerator.Core.Metadata.Generation; + +/// +/// A collection of +/// +public class ModelCollection : List +{ + /// + /// Initializes a new instance of the class. + /// + public ModelCollection() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The collection whose elements are copied to the new list. + public ModelCollection(IEnumerable collection) : base(collection) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The number of elements that the new list can initially store. + public ModelCollection(int capacity) : base(capacity) + { + } + + /// + /// Gets or sets a value indicating whether this instance is processed. + /// + /// + /// true if this instance is processed; otherwise, false. + /// + public bool IsProcessed { get; set; } + +} diff --git a/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/ModelType.cs b/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/ModelType.cs new file mode 100644 index 0000000..2245c44 --- /dev/null +++ b/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/ModelType.cs @@ -0,0 +1,11 @@ +using System; + +namespace DbApiBuilderEntityGenerator.Core.Metadata.Generation; + +public enum ModelType +{ + Read, + Create, + Update + +} diff --git a/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/Property.cs b/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/Property.cs new file mode 100644 index 0000000..c5b770a --- /dev/null +++ b/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/Property.cs @@ -0,0 +1,51 @@ +using System; +using System.Data; +using System.Diagnostics; +using Microsoft.EntityFrameworkCore.Metadata; + +namespace DbApiBuilderEntityGenerator.Core.Metadata.Generation; + +[DebuggerDisplay("Property: {PropertyName}, Column: {ColumnName}, Type: {StoreType}")] +public class Property : ModelBase +{ + public Entity Entity { get; set; } = null!; + + public string PropertyName { get; set; } = null!; + + public string ColumnName { get; set; } = null!; + + public string? StoreType { get; set; } + + public string? NativeType { get; set; } + + public DbType DataType { get; set; } + + public Type SystemType { get; set; } = null!; + + public bool? IsNullable { get; set; } + + public bool IsRequired => IsNullable == false; + + public bool IsOptional => IsNullable == true; + + public bool? IsPrimaryKey { get; set; } + + public bool? IsForeignKey { get; set; } + + public bool? IsReadOnly { get; set; } + + public bool? IsRowVersion { get; set; } + + public bool? IsConcurrencyToken { get; set; } + + public bool? IsUnique { get; set; } + + public int? Size { get; set; } + + public object? DefaultValue { get; set; } + + public string? Default { get; set; } + + public ValueGenerated? ValueGenerated { get; set; } + +} diff --git a/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/PropertyCollection.cs b/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/PropertyCollection.cs new file mode 100644 index 0000000..89ee2fc --- /dev/null +++ b/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/PropertyCollection.cs @@ -0,0 +1,83 @@ +using System; + +namespace DbApiBuilderEntityGenerator.Core.Metadata.Generation; + +/// +/// A collection of instances +/// +public class PropertyCollection : List +{ + /// + /// Initializes a new instance of the class. + /// + public PropertyCollection() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The collection whose elements are copied to the new list. + public PropertyCollection(IEnumerable collection) : base(collection) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The list. + public PropertyCollection(List list) : base(list) + { + } + + /// + /// Gets or sets a value indicating whether this instance is processed. + /// + /// + /// true if this instance is processed; otherwise, false. + /// + public bool IsProcessed { get; set; } + + /// + /// Gets the primary keys properties. + /// + /// + /// The primary keys properties. + /// + public IEnumerable PrimaryKeys => this.Where(p => p.IsPrimaryKey == true); + + /// + /// Gets the foreign keys properties. + /// + /// + /// The foreign keys. + /// + public IEnumerable ForeignKeys => this.Where(p => p.IsForeignKey == true); + + /// + /// Gets the property by column name + /// + /// Name of the column. + /// + public Property? ByColumn(string? columnName) + { + if (string.IsNullOrEmpty(columnName)) + return null; + + return this.FirstOrDefault(x => x.ColumnName == columnName); + } + + /// + /// Gets the property by property name + /// + /// Name of the property. + /// + public Property? ByProperty(string? propertyName) + { + if (string.IsNullOrEmpty(propertyName)) + return null; + + return this.FirstOrDefault(x => x.PropertyName == propertyName); + } + +} diff --git a/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/Relationship.cs b/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/Relationship.cs new file mode 100644 index 0000000..8d38443 --- /dev/null +++ b/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/Relationship.cs @@ -0,0 +1,41 @@ +using System; +using System.Diagnostics; + +namespace DbApiBuilderEntityGenerator.Core.Metadata.Generation; + +[DebuggerDisplay("Primary: {PrimaryEntity}, Property: {PropertyName}, Relationship: {RelationshipName}")] +public class Relationship : ModelBase +{ + public Relationship() + { + Properties = []; + PrimaryProperties = []; + } + + public string? RelationshipName { get; set; } + + + public Entity Entity { get; set; } = null!; + + public PropertyCollection Properties { get; set; } + + public string PropertyName { get; set; } = null!; + + public Cardinality Cardinality { get; set; } + + + public Entity PrimaryEntity { get; set; } = null!; + + public PropertyCollection PrimaryProperties { get; set; } + + public string PrimaryPropertyName { get; set; } = null!; + + public Cardinality PrimaryCardinality { get; set; } + + + public bool? CascadeDelete { get; set; } + public bool IsForeignKey { get; set; } + public bool IsMapped { get; set; } + + public bool IsOneToOne => Cardinality != Cardinality.Many && PrimaryCardinality != Cardinality.Many; +} diff --git a/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/RelationshipCollection.cs b/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/RelationshipCollection.cs new file mode 100644 index 0000000..5a355fb --- /dev/null +++ b/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/RelationshipCollection.cs @@ -0,0 +1,67 @@ +using System; + +namespace DbApiBuilderEntityGenerator.Core.Metadata.Generation; + +/// +/// A collection of instances +/// +public class RelationshipCollection : List +{ + /// + /// Initializes a new instance of the class. + /// + public RelationshipCollection() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The collection whose elements are copied to the new list. + public RelationshipCollection(IEnumerable collection) : base(collection) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The number of elements that the new list can initially store. + public RelationshipCollection(int capacity) : base(capacity) + { + } + + /// + /// Gets or sets a value indicating whether this instance is processed. + /// + /// + /// true if this instance is processed; otherwise, false. + /// + public bool IsProcessed { get; set; } + + /// + /// Gets a relationship by name. + /// + /// The name. + /// + public Relationship? ByName(string? name) + { + if (string.IsNullOrEmpty(name)) + return null; + + return this.FirstOrDefault(x => x.RelationshipName == name); + } + + /// + /// Gets a relationship by the property name. + /// + /// Name of the property. + /// + public Relationship? ByProperty(string propertyName) + { + if (string.IsNullOrEmpty(propertyName)) + return null; + + return this.FirstOrDefault(x => x.PropertyName == propertyName); + } + +} diff --git a/src/DbApiBuilderEntityGenerator.Core/ModelGenerator.cs b/src/DbApiBuilderEntityGenerator.Core/ModelGenerator.cs new file mode 100644 index 0000000..530adfc --- /dev/null +++ b/src/DbApiBuilderEntityGenerator.Core/ModelGenerator.cs @@ -0,0 +1,718 @@ +using System; +using System.Data; +using System.Text; +using System.Text.RegularExpressions; +using DbApiBuilderEntityGenerator.Core.Extensions; +using DbApiBuilderEntityGenerator.Core.Metadata.Generation; +using DbApiBuilderEntityGenerator.Core.Options; +using Humanizer; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Scaffolding.Metadata; +using Microsoft.EntityFrameworkCore.Scaffolding.Metadata.Internal; +using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.Extensions.Logging; + +using Model = DbApiBuilderEntityGenerator.Core.Metadata.Generation.Model; +using Property = DbApiBuilderEntityGenerator.Core.Metadata.Generation.Property; +using PropertyCollection = DbApiBuilderEntityGenerator.Core.Metadata.Generation.PropertyCollection; + +namespace DbApiBuilderEntityGenerator.Core; + +public partial class ModelGenerator +{ + private readonly UniqueNamer _namer; + + private readonly ILogger _logger; + + private GeneratorOptions _options = null!; + + private IRelationalTypeMappingSource _typeMapper = null!; + + public ModelGenerator(ILoggerFactory logger) + { + _logger = logger.CreateLogger(); + _namer = new UniqueNamer(); + + } + + public EntityContext Generate(GeneratorOptions options, DatabaseModel databaseModel, IRelationalTypeMappingSource typeMappingSource) + { + ArgumentNullException.ThrowIfNull(options); + ArgumentNullException.ThrowIfNull(databaseModel); + ArgumentNullException.ThrowIfNull(typeMappingSource); + + _logger.LogInformation("Building code generation model from database: {databaseName}", databaseModel.DatabaseName); + + _options = options; + _typeMapper = typeMappingSource; + + var entityContext = new EntityContext(); + entityContext.DatabaseName = databaseModel.DatabaseName; + + var tables = databaseModel.Tables; + + foreach (var table in tables) + { + if (IsIgnored(table, _options.Exclude.Tables)) + { + _logger.LogDebug(" Skipping Table : {schema}.{name}", table.Schema, table.Name); + continue; + } + + _logger.LogDebug(" Processing Table : {schema}.{name}", table.Schema, table.Name); + + _options.Variables.Set(VariableConstants.TableSchema, ToLegalName(table.Schema)); + _options.Variables.Set(VariableConstants.TableName, ToLegalName(table.Name)); + + var entity = GetEntity(entityContext, table); + } + + _options.Variables.Remove(VariableConstants.TableName); + _options.Variables.Remove(VariableConstants.TableSchema); + + return entityContext; + } + + private Entity GetEntity(EntityContext entityContext, DatabaseTable tableSchema, bool processRelationships = true, bool processMethods = true) + { + var entity = entityContext.Entities.ByTable(tableSchema.Name, tableSchema.Schema) + ?? 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, DatabaseTable tableSchema) + { + var entity = new Entity + { + Context = entityContext, + TableName = tableSchema.Name, + TableSchema = tableSchema.Schema + }; + + //var entityClass = _options.Data.Entity.Name; + //if (entityClass.IsNullOrEmpty()) + // Gets the entity class + var entityClass = ToClassName(tableSchema.Name, tableSchema.Schema); + entityClass = _namer.UniqueClassName(entityClass); + + //var entityNamespace = _options.Data.Entity.Namespace ?? "Data.Entities"; + //var entiyBaseClass = _options.Data.Entity.BaseClass; + + + //var mappingName = entityClass + "Map"; + //mappingName = _namer.UniqueClassName(mappingName); + + //var mappingNamespace = _options.Data.Mapping.Namespace ?? "Data.Mapping"; + + // var contextName = ContextName(entityClass); + // contextName = ToPropertyName(entityContext.ContextClass, contextName); + // contextName = _namer.UniqueContextName(contextName); + + entity.EntityClass = entityClass; + // entity.EntityNamespace = entityNamespace; + // entity.EntityBaseClass = entiyBaseClass; + + // entity.MappingClass = mappingName; + // entity.MappingNamespace = mappingNamespace; + + // entity.ContextProperty = contextName; + + entity.IsView = tableSchema is DatabaseView; + + bool? isTemporal = tableSchema[SqlServerAnnotationNames.IsTemporal] as bool?; + // if (isTemporal == true && _options.Data.Mapping.Temporal) + if (isTemporal == true) + { + entity.TemporalTableName = tableSchema[SqlServerAnnotationNames.TemporalHistoryTableName] as string; + entity.TemporalTableSchema = tableSchema[SqlServerAnnotationNames.TemporalHistoryTableSchema] as string; + + entity.TemporalStartProperty = tableSchema[SqlServerAnnotationNames.TemporalPeriodStartPropertyName] as string; + + entity.TemporalStartColumn = tableSchema[SqlServerAnnotationNames.TemporalPeriodStartColumnName] as string + ?? entity.TemporalStartProperty; + + entity.TemporalEndProperty = tableSchema[SqlServerAnnotationNames.TemporalPeriodEndPropertyName] as string; + + entity.TemporalEndColumn = tableSchema[SqlServerAnnotationNames.TemporalPeriodEndColumnName] as string + ?? entity.TemporalEndProperty; + } + + entityContext.Entities.Add(entity); + + return entity; + } + + private void CreateProperties(Entity entity, DatabaseTable tableSchema) + { + var columns = tableSchema.Columns; + foreach (var column in columns) + { + var table = column.Table; + if (IsIgnored(column, _options.Exclude.Columns)) + { + _logger.LogDebug(" Skipping Column : {Schema}.{Table}.{Column}", table.Schema, table.Name, column.Name); + continue; + } + + var mapping = column.StoreType.HasValue() ? _typeMapper.FindMapping(column.StoreType) : null; + if (mapping == null) + { + _logger.LogWarning("Failed to map type {storeType} for {column}.", column.StoreType, column.Name); + continue; + } + + var property = entity.Properties.ByColumn(column.Name); + + if (property == null) + { + property = new Metadata.Generation.Property() + { + Entity = entity, + ColumnName = column.Name + }; + entity.Properties.Add(property); + } + + string name = ToPropertyName(entity.EntityClass, column.Name); + string propertyName = name; + + foreach (var selection in _options.Renaming.Properties.Where(p => p.Expression.HasValue())) + { + if (selection.Expression.IsNullOrEmpty()) + continue; + + propertyName = Regex.Replace(propertyName, selection.Expression, string.Empty); + } + + // make sure regex doesn't remove everything + if (propertyName.IsNullOrEmpty()) + propertyName = name; + + propertyName = _namer.UniqueName(entity.EntityClass, propertyName); + + property.PropertyName = propertyName; + + property.IsNullable = column.IsNullable; + + property.IsRowVersion = column.IsRowVersion(); + property.IsConcurrencyToken = (bool?)column[ScaffoldingAnnotationNames.ConcurrencyToken] == true; + + property.IsPrimaryKey = table.PrimaryKey?.Columns.Contains(column) == true; + property.IsForeignKey = table.ForeignKeys.Any(c => c.Columns.Contains(column)); + + property.IsUnique = table.UniqueConstraints.Any(c => c.Columns.Contains(column)) + || table.Indexes.Where(i => i.IsUnique).Any(c => c.Columns.Contains(column)); + + property.DefaultValue = column.DefaultValue; + property.Default = column.DefaultValueSql; + + property.ValueGenerated = column.ValueGenerated; + + if (property.ValueGenerated == null && !string.IsNullOrWhiteSpace(column.ComputedColumnSql)) + property.ValueGenerated = ValueGenerated.OnAddOrUpdate; + + property.StoreType = mapping.StoreType; + property.NativeType = mapping.StoreTypeNameBase; + property.DataType = mapping.DbType ?? DbType.AnsiString; + property.SystemType = mapping.ClrType; + property.Size = mapping.Size; + + // overwrite row version type + if (property.IsRowVersion == true && property.SystemType == typeof(byte[])) + { + // property.SystemType = _options.Data.Mapping.RowVersion switch + // { + // RowVersionMapping.ByteArray => typeof(byte[]), + // RowVersionMapping.Long => typeof(long), + // RowVersionMapping.ULong => typeof(ulong), + // _ => typeof(byte[]) + // }; + property.SystemType = typeof(long); + } + + property.IsProcessed = true; + } + + entity.Properties.IsProcessed = true; + + bool? isTemporal = tableSchema[SqlServerAnnotationNames.IsTemporal] as bool?; + // if (isTemporal != true || _options.Data.Mapping.Temporal) + if (isTemporal != true) + return; + + // add temporal period columns + var temporalStartColumn = tableSchema[SqlServerAnnotationNames.TemporalPeriodStartColumnName] as string + ?? tableSchema[SqlServerAnnotationNames.TemporalPeriodStartPropertyName] as string; + + var temporalEndColumn = tableSchema[SqlServerAnnotationNames.TemporalPeriodEndColumnName] as string + ?? tableSchema[SqlServerAnnotationNames.TemporalPeriodEndPropertyName] as string; + + if (temporalStartColumn.IsNullOrEmpty() || temporalEndColumn.IsNullOrEmpty()) + return; + + var temporalStart = entity.Properties.ByColumn(temporalStartColumn); + + if (temporalStart == null) + { + temporalStart = new Property { Entity = entity, ColumnName = temporalStartColumn }; + entity.Properties.Add(temporalStart); + } + + temporalStart.PropertyName = ToPropertyName(entity.EntityClass, temporalStartColumn); + temporalStart.ValueGenerated = ValueGenerated.OnAddOrUpdate; + temporalStart.StoreType = "datetime2"; + temporalStart.DataType = DbType.DateTime2; + temporalStart.SystemType = typeof(DateTime); + + temporalStart.IsProcessed = true; + + var temporalEnd = entity.Properties.ByColumn(temporalEndColumn); + + if (temporalEnd == null) + { + temporalEnd = new Property { Entity = entity, ColumnName = temporalEndColumn }; + entity.Properties.Add(temporalEnd); + } + + temporalEnd.PropertyName = ToPropertyName(entity.EntityClass, temporalEndColumn); + temporalEnd.ValueGenerated = ValueGenerated.OnAddOrUpdate; + temporalEnd.StoreType = "datetime2"; + temporalEnd.DataType = DbType.DateTime2; + temporalEnd.SystemType = typeof(DateTime); + + temporalEnd.IsProcessed = true; + } + + + private void CreateRelationships(EntityContext entityContext, Entity entity, DatabaseTable tableSchema) + { + foreach (var foreignKey in tableSchema.ForeignKeys) + { + // skip relationship if principal table is ignored + if (IsIgnored(foreignKey.PrincipalTable, _options.Exclude.Tables)) + { + _logger.LogDebug(" Skipping Relationship : {name}", foreignKey.Name); + continue; + } + + CreateRelationship(entityContext, entity, foreignKey); + } + + entity.Relationships.IsProcessed = true; + } + + private void CreateRelationship(EntityContext entityContext, Entity foreignEntity, DatabaseForeignKey tableKeySchema) + { + Entity primaryEntity = GetEntity(entityContext, tableKeySchema.PrincipalTable, false, false); + + var primaryName = primaryEntity.EntityClass; + var foreignName = foreignEntity.EntityClass; + + var foreignMembers = GetKeyMembers(foreignEntity, tableKeySchema.Columns, tableKeySchema.Name); + bool foreignMembersRequired = foreignMembers.Any(c => c.IsRequired); + + var primaryMembers = GetKeyMembers(primaryEntity, tableKeySchema.PrincipalColumns, tableKeySchema.Name); + bool primaryMembersRequired = primaryMembers.Any(c => c.IsRequired); + + // skip invalid fkeys + if (foreignMembers.Count == 0 || primaryMembers.Count == 0) + return; + + var relationshipName = tableKeySchema.Name; + + // ensure relationship name for sync support + if (relationshipName.IsNullOrEmpty()) + relationshipName = $"FK_{foreignName}_{primaryName}_{primaryMembers.Select(p => p.PropertyName).ToDelimitedString("_")}"; + + relationshipName = _namer.UniqueRelationshipName(relationshipName); + + var 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.Cardinality = foreignMembersRequired ? Cardinality.One : Cardinality.ZeroOrOne; + + foreignRelationship.PrimaryEntity = primaryEntity; + foreignRelationship.PrimaryProperties = [.. primaryMembers]; + + foreignRelationship.Entity = foreignEntity; + foreignRelationship.Properties = [.. foreignMembers]; + + string prefix = GetMemberPrefix(foreignRelationship, primaryName, foreignName); + + string foreignPropertyName = ToPropertyName(foreignEntity.EntityClass, prefix + primaryName); + foreignPropertyName = _namer.UniqueName(foreignEntity.EntityClass, foreignPropertyName); + foreignRelationship.PropertyName = foreignPropertyName; + + // add reverse + var primaryRelationship = primaryEntity.Relationships + .FirstOrDefault(r => r.RelationshipName == relationshipName && !r.IsForeignKey); + + if (primaryRelationship == null) + { + primaryRelationship = new Relationship { RelationshipName = relationshipName }; + primaryEntity.Relationships.Add(primaryRelationship); + } + + primaryRelationship.IsMapped = false; + primaryRelationship.IsForeignKey = false; + + primaryRelationship.PrimaryEntity = foreignEntity; + primaryRelationship.PrimaryProperties = [.. foreignMembers]; + + primaryRelationship.Entity = primaryEntity; + primaryRelationship.Properties = [.. primaryMembers]; + + bool isOneToOne = IsOneToOne(tableKeySchema, foreignRelationship); + if (isOneToOne) + primaryRelationship.Cardinality = primaryMembersRequired ? Cardinality.One : Cardinality.ZeroOrOne; + else + primaryRelationship.Cardinality = Cardinality.Many; + + string primaryPropertyName = prefix + foreignName; + if (!isOneToOne) + primaryPropertyName = RelationshipName(primaryPropertyName); + + primaryPropertyName = ToPropertyName(primaryEntity.EntityClass, primaryPropertyName); + primaryPropertyName = _namer.UniqueName(primaryEntity.EntityClass, primaryPropertyName); + + primaryRelationship.PropertyName = primaryPropertyName; + + foreignRelationship.PrimaryPropertyName = primaryRelationship.PropertyName; + foreignRelationship.PrimaryCardinality = primaryRelationship.Cardinality; + + primaryRelationship.PrimaryPropertyName = foreignRelationship.PropertyName; + primaryRelationship.PrimaryCardinality = foreignRelationship.Cardinality; + + foreignRelationship.IsProcessed = true; + primaryRelationship.IsProcessed = true; + } + + + private static void CreateMethods(Entity entity, DatabaseTable tableSchema) + { + if (tableSchema.PrimaryKey != null) + { + var method = GetMethodFromColumns(entity, tableSchema.PrimaryKey.Columns); + if (method != null) + { + method.IsKey = true; + method.SourceName = tableSchema.PrimaryKey.Name; + + if (entity.Methods.All(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, DatabaseTable table) + { + var columns = new List(); + + foreach (var column in table.ForeignKeys.SelectMany(c => c.Columns)) + { + columns.Add(column); + + var method = GetMethodFromColumns(entity, columns); + if (method != null && entity.Methods.All(m => m.NameSuffix != method.NameSuffix)) + entity.Methods.Add(method); + + columns.Clear(); + } + } + + private static void GetIndexMethods(Entity entity, DatabaseTable table) + { + foreach (var index in table.Indexes) + { + var method = GetMethodFromColumns(entity, index.Columns); + if (method == null) + continue; + + method.SourceName = index.Name; + method.IsUnique = index.IsUnique; + method.IsIndex = true; + + if (entity.Methods.All(m => m.NameSuffix != method.NameSuffix)) + entity.Methods.Add(method); + } + } + + private static Method? GetMethodFromColumns(Entity entity, IEnumerable columns) + { + var method = new Method { Entity = entity }; + var methodName = new StringBuilder(); + + foreach (var column in columns) + { + var property = entity.Properties.ByColumn(column.Name); + if (property == null) + continue; + + method.Properties.Add(property); + methodName.Append(property.PropertyName); + } + + if (method.Properties.Count == 0) + return null; + + method.NameSuffix = methodName.ToString(); + return method; + } + + private List GetKeyMembers(Entity entity, IEnumerable members, string? relationshipName) + { + var keyMembers = new List(); + + foreach (var member in members) + { + var property = entity.Properties.ByColumn(member.Name); + + if (property == null) + _logger.LogWarning("Could not find column {columnName} for relationship {relationshipName}.", member.Name, relationshipName); + else + keyMembers.Add(property); + } + + return keyMembers; + } + + private static string GetMemberPrefix(Relationship relationship, string primaryClass, string foreignClass) + { + string thisKey = relationship.Properties + .Select(p => p.PropertyName) + .FirstOrDefault() ?? string.Empty; + + string otherKey = relationship.PrimaryProperties + .Select(p => p.PropertyName) + .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 = IdSuffixRegex().Replace(prefix, ""); + prefix = DigitPrefixRegex().Replace(prefix, ""); + + return prefix; + } + + private static bool IsOneToOne(DatabaseForeignKey tableKeySchema, Relationship foreignRelationship) + { + var foreignColumn = foreignRelationship.Properties + .Select(p => p.ColumnName) + .FirstOrDefault(); + + return tableKeySchema.PrincipalTable.PrimaryKey != null + && tableKeySchema.Table.PrimaryKey != null + && tableKeySchema.Table.PrimaryKey.Columns.Count == 1 + && tableKeySchema.Table.PrimaryKey.Columns.Any(c => c.Name == foreignColumn); + + // if f.key is unique + //return tableKeySchema.ForeignKeyMemberColumns.All(column => column.IsUnique); + } + + + private string RelationshipName(string name) + { + var naming = _options.RelationshipNaming; + if (naming == RelationshipNaming.Preserve) + return name; + + if (naming == RelationshipNaming.Suffix) + return name + "List"; + + return name.Pluralize(false); + } + + // private string ContextName(string name) + // { + // var naming = _options.Data.Context.PropertyNaming; + // if (naming == ContextNaming.Preserve) + // return name; + + // if (naming == ContextNaming.Suffix) + // return name + "DataSet"; + + // return name.Pluralize(false); + // } + + private string EntityName(string name) + { + var tableNaming = _options.EntityNaming; + var entityNaming = _options.EntityNaming; + + if (tableNaming != EntityNaming.Plural && entityNaming == EntityNaming.Plural) + name = name.Pluralize(false); + else if (tableNaming != EntityNaming.Singular && entityNaming == EntityNaming.Singular) + name = name.Singularize(false); + + var rename = name; + foreach (var selection in _options.Renaming.Entities.Where(p => p.Expression.HasValue())) + { + if (selection.Expression.IsNullOrEmpty()) + continue; + + rename = Regex.Replace(rename, selection.Expression, string.Empty); + } + + // make sure regex doesn't remove everything + return rename.HasValue() ? rename : name; + } + + + private string ToClassName(string tableName, string? tableSchema) + { + tableName = EntityName(tableName); + var className = tableName; + + // if (_options.Data.Entity.PrefixWithSchemaName && tableSchema != null) + // className = $"{tableSchema}{tableName}"; + + return ToLegalName(className); + } + + private string ToPropertyName(string className, string name) + { + string propertyName = ToLegalName(name); + if (className.Equals(propertyName, StringComparison.OrdinalIgnoreCase)) + propertyName += "Member"; + + return propertyName; + } + private static string ToLegalName(string? name) + { + if (string.IsNullOrWhiteSpace(name)) + return string.Empty; + + string legalName = name; + + // remove invalid leading + var expression = LeadingNonAlphaRegex(); + if (expression.IsMatch(name)) + legalName = expression.Replace(legalName, string.Empty); + + // prefix with column when all characters removed + if (legalName.IsNullOrWhiteSpace()) + legalName = "Number" + name; + + return legalName.ToPascalCase(); + } + + + private static bool IsIgnored(DatabaseTable table, IEnumerable exclude) + { + var name = $"{table.Schema}.{table.Name}"; + var includeExpressions = Enumerable.Empty(); + var excludeExpressions = exclude ?? []; + + return IsIgnored(name, excludeExpressions, includeExpressions); + } + + private static bool IsIgnored(DatabaseColumn column, IEnumerable exclude) + { + var table = column.Table; + var name = $"{table.Schema}.{table.Name}.{column.Name}"; + var includeExpressions = Enumerable.Empty(); + var excludeExpressions = exclude ?? []; + + return IsIgnored(name, excludeExpressions, includeExpressions); + } + + // private static bool IsIgnored(Property property, TOption options, SharedModelOptions sharedOptions) + // where TOption : ModelOptionsBase + // { + // var name = $"{property.Entity.EntityClass}.{property.PropertyName}"; + + // var includeExpressions = new HashSet(sharedOptions?.Include?.Properties ?? []); + // var excludeExpressions = new HashSet(sharedOptions?.Exclude?.Properties ?? []); + + // var includeProperties = options?.Include?.Properties ?? []; + // foreach (var expression in includeProperties) + // includeExpressions.Add(expression); + + // var excludeProperties = options?.Exclude?.Properties ?? []; + // foreach (var expression in excludeProperties) + // excludeExpressions.Add(expression); + + // return IsIgnored(name, excludeExpressions, includeExpressions); + // } + + // private static bool IsIgnored(Entity entity, TOption options, SharedModelOptions sharedOptions) + // where TOption : ModelOptionsBase + // { + // var name = entity.EntityClass; + + // var includeExpressions = new HashSet(sharedOptions?.Include?.Entities ?? []); + // var excludeExpressions = new HashSet(sharedOptions?.Exclude?.Entities ?? []); + + // var includeEntities = options?.Include?.Entities ?? []; + // foreach (var expression in includeEntities) + // includeExpressions.Add(expression); + + // var excludeEntities = options?.Exclude?.Entities ?? []; + // foreach (var expression in excludeEntities) + // excludeExpressions.Add(expression); + + // return IsIgnored(name, excludeExpressions, includeExpressions); + // } + + private static bool IsIgnored(string name, IEnumerable excludeExpressions, IEnumerable includeExpressions) + { + foreach (var expression in includeExpressions) + { + if (expression.IsMatch(name)) + return false; + } + + foreach (var expression in excludeExpressions) + { + if (expression.IsMatch(name)) + return true; + } + + return false; + } + + + [GeneratedRegex(@"^[^a-zA-Z_]+")] + private static partial Regex LeadingNonAlphaRegex(); + + [GeneratedRegex(@"(_ID|_id|_Id|\.ID|\.id|\.Id|ID|Id)$")] + private static partial Regex IdSuffixRegex(); + + [GeneratedRegex(@"^\d")] + private static partial Regex DigitPrefixRegex(); + +} + diff --git a/src/DbApiBuilderEntityGenerator.Core/UniqueNamer.cs b/src/DbApiBuilderEntityGenerator.Core/UniqueNamer.cs new file mode 100644 index 0000000..1bcfbfa --- /dev/null +++ b/src/DbApiBuilderEntityGenerator.Core/UniqueNamer.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Concurrent; + +namespace DbApiBuilderEntityGenerator.Core; + +public class UniqueNamer +{ + private readonly ConcurrentDictionary> _names; + + public UniqueNamer() + { + _names = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase); + Comparer = StringComparer.OrdinalIgnoreCase; + + // 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 = MakeUnique(name, hashSet.Contains); + hashSet.Add(result); + + return result; + } + + public string UniqueClassName(string className) + { + const string globalClassName = "global::ClassName"; + return UniqueName(globalClassName, className); + } + + public string UniqueModelName(string @namespace, string className) + { + string globalClassName = "global::ModelClass::" + @namespace; + 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 string MakeUnique(string name, Func exists) + { + string uniqueName = name; + int count = 1; + + while (exists(uniqueName)) + uniqueName = string.Concat(name, count++); + + return uniqueName; + } + + +} diff --git a/src/DbApiBuilderEntityGenerator.Core/VariableConstants.cs b/src/DbApiBuilderEntityGenerator.Core/VariableConstants.cs new file mode 100644 index 0000000..21621d3 --- /dev/null +++ b/src/DbApiBuilderEntityGenerator.Core/VariableConstants.cs @@ -0,0 +1,12 @@ +using System; + +namespace DbApiBuilderEntityGenerator.Core; + +public static class VariableConstants +{ + public const string TableSchema = "Table.Schema"; + public const string TableName = "Table.Name"; + public const string EntityName = "Entity.Name"; + public const string ModelName = "Model.Name"; + +} diff --git a/src/DbApiBuilderEntityGenerator/GenerateCommand.cs b/src/DbApiBuilderEntityGenerator/GenerateCommand.cs index 63dd119..d3434eb 100644 --- a/src/DbApiBuilderEntityGenerator/GenerateCommand.cs +++ b/src/DbApiBuilderEntityGenerator/GenerateCommand.cs @@ -11,8 +11,11 @@ namespace DbApiBuilderEntityGenerator; [Command("generate", "gen")] public class GenerateCommand : OptionsCommandBase { - public GenerateCommand(ILoggerFactory logger, IConsole console, IConfigurationSerializer serializer) : base(logger, console, serializer) + private readonly ICodeGenerator _codeGenerator; + + public GenerateCommand(ILoggerFactory logger, IConsole console, IConfigurationSerializer serializer, ICodeGenerator codeGenerator) : base(logger, console, serializer) { + this._codeGenerator = codeGenerator; } [Option("-p ", Description = "Database provider to reverse engineer")] @@ -45,7 +48,7 @@ protected override int OnExecute(CommandLineApplication application) // convert to options format to support variables var options = OptionMapper.Map(configuration); - // var result = _codeGenerator.Generate(options); + var result = _codeGenerator.Generate(options); return 0; diff --git a/src/DbApiBuilderEntityGenerator/Program.cs b/src/DbApiBuilderEntityGenerator/Program.cs index 7b61cfc..ea2b551 100644 --- a/src/DbApiBuilderEntityGenerator/Program.cs +++ b/src/DbApiBuilderEntityGenerator/Program.cs @@ -33,7 +33,7 @@ public static int Main(string[] args) ) .AddSingleton(PhysicalConsole.Singleton) .AddTransient() - //.AddTransient() + .AddTransient() .BuildServiceProvider(); var app = new CommandLineApplication(); diff --git a/tests/TestDatabase/Dockerfile b/tests/TestDatabase/Dockerfile new file mode 100644 index 0000000..9dae886 --- /dev/null +++ b/tests/TestDatabase/Dockerfile @@ -0,0 +1,10 @@ +FROM mcr.microsoft.com/mssql/server:2022-latest +ARG BAK_FILE +ARG SQL_FILE +ENV MSSQL_SA_PASSWORD=Test_123_Test +EXPOSE 1433 +WORKDIR / +COPY $SQL_FILE ./Tracker.sql +COPY ./install.sh . +#COPY ./startup.sh . +#CMD /bin/bash ./startup.sh \ No newline at end of file diff --git a/tests/TestDatabase/Script002.Tracker.Data.sql b/tests/TestDatabase/Script002.Tracker.Data.sql new file mode 100644 index 0000000..00b466f --- /dev/null +++ b/tests/TestDatabase/Script002.Tracker.Data.sql @@ -0,0 +1,93 @@ +-- Table [dbo].[Priority] data + +MERGE INTO [dbo].[Priority] AS t +USING +( + VALUES + (1, 'High', 'High Priority', 1, 1), + (2, 'Normal', 'Normal Priority', 2, 1), + (3, 'Low', 'Low Priority', 3, 1) +) +AS s +([Id], [Name], [Description], [DisplayOrder], [IsActive]) +ON (t.[Id] = s.[Id]) +WHEN NOT MATCHED BY TARGET THEN + INSERT ([Id], [Name], [Description], [DisplayOrder], [IsActive]) + VALUES (s.[Id], s.[Name], s.[Description], s.[DisplayOrder], s.[IsActive]) +WHEN MATCHED THEN + UPDATE SET t.[Name] = s.[Name], t.[Description] = s.[Description], t.[DisplayOrder] = s.[DisplayOrder], t.[IsActive] = s.[IsActive] +OUTPUT $action as [Action]; + +-- Table [dbo].[Status] data + +MERGE INTO [dbo].[Status] AS t +USING +( + VALUES + (1, 'Not Started', 'Not Starated', 1, 1), + (2, 'In Progress', 'In Progress', 2, 1), + (3, 'Completed', 'Completed', 3, 1), + (4, 'Blocked', 'Blocked', 4, 1), + (5, 'Deferred', 'Deferred', 5, 1), + (6, 'Done', 'Done', 6, 1) +) +AS s +([Id], [Name], [Description], [DisplayOrder], [IsActive]) +ON (t.[Id] = s.[Id]) +WHEN NOT MATCHED BY TARGET THEN + INSERT ([Id], [Name], [Description], [DisplayOrder], [IsActive]) + VALUES (s.[Id], s.[Name], s.[Description], s.[DisplayOrder], s.[IsActive]) +WHEN MATCHED THEN + UPDATE SET t.[Name] = s.[Name], t.[Description] = s.[Description], t.[DisplayOrder] = s.[DisplayOrder], t.[IsActive] = s.[IsActive] +OUTPUT $action as [Action]; + +-- Table [dbo].[User] data + +MERGE INTO [dbo].[User] AS t +USING +( + VALUES + ('83507c95-0744-e811-bd87-f8633fc30ac7', 'william.adama@battlestar.com', 1, 'William Adama'), + ('490312a6-0744-e811-bd87-f8633fc30ac7', 'laura.roslin@battlestar.com', 1, 'Laura Roslin'), + ('38da04bb-0744-e811-bd87-f8633fc30ac7', 'kara.thrace@battlestar.com', 1, 'Kara Thrace'), + ('589d67c6-0744-e811-bd87-f8633fc30ac7', 'lee.adama@battlestar.com', 1, 'Lee Adama'), + ('118b84d4-0744-e811-bd87-f8633fc30ac7', 'gaius.baltar@battlestar.com', 1, 'Gaius Baltar'), + ('fa7515df-0744-e811-bd87-f8633fc30ac7', 'saul.tigh@battlestar.com', 1, 'Saul Tigh') +) +AS s +([Id], [EmailAddress], [IsEmailAddressConfirmed], [DisplayName]) +ON (t.[Id] = s.[Id]) +WHEN NOT MATCHED BY TARGET THEN + INSERT ([Id], [EmailAddress], [IsEmailAddressConfirmed], [DisplayName]) + VALUES (s.[Id], s.[EmailAddress], s.[IsEmailAddressConfirmed], s.[DisplayName]) +WHEN MATCHED THEN + UPDATE SET t.[EmailAddress] = s.[EmailAddress], t.[IsEmailAddressConfirmed] = s.[IsEmailAddressConfirmed], t.[DisplayName] = s.[DisplayName] +OUTPUT $action as [Action]; + +-- Table [dbo].[Role] data + +MERGE INTO [dbo].[Role] AS t +USING +( + VALUES + ('b2d78522-0944-e811-bd87-f8633fc30ac7', 'Administrator', 'Administrator'), + ('b3d78522-0944-e811-bd87-f8633fc30ac7', 'Manager', 'Manager'), + ('acbffa29-0944-e811-bd87-f8633fc30ac7', 'Member', 'Member') +) +AS s +([Id], [Name], [Description]) +ON (t.[Id] = s.[Id]) +WHEN NOT MATCHED BY TARGET THEN + INSERT ([Id], [Name], [Description]) + VALUES (s.[Id], s.[Name], s.[Description]) +WHEN MATCHED THEN + UPDATE SET t.[Name] = s.[Name], t.[Description] = s.[Description] +OUTPUT $action as [Action]; + +-- Table [dbo].[CitiesSpatial] data + +INSERT INTO [dbo].[CitiesSpatial] VALUES +('Sydney', 'POINT(151.2 -33.8)', 'POINT(151.2 -33.8)'), +('Athens', 'POINT(23.7 38)', 'POINT(23.7 38)'), +('Beijing', 'POINT(116.4 39.9)', 'POINT(116.4 39.9)'), +('London', 'POINT(-0.15 51.5)', 'POINT(-0.15 51.5)'); \ No newline at end of file diff --git a/tests/TestDatabase/Script003.Tracker.User.sql b/tests/TestDatabase/Script003.Tracker.User.sql new file mode 100644 index 0000000..1266f16 --- /dev/null +++ b/tests/TestDatabase/Script003.Tracker.User.sql @@ -0,0 +1,17 @@ +IF NOT EXISTS + (SELECT name + FROM master.sys.sql_logins + WHERE name = 'testuser') +BEGIN + CREATE LOGIN [testuser] WITH PASSWORD = N'rglna{adQP123456'; +END +-- check our db +IF NOT EXISTS + (SELECT name + FROM sys.database_principals + WHERE name = 'testuser') +BEGIN + CREATE USER [testuser] FOR LOGIN [testuser] WITH DEFAULT_SCHEMA = dbo +END +exec sp_addrolemember db_datareader, 'testuser' +exec sp_addrolemember db_datawriter, 'testuser' diff --git a/tests/TestDatabase/Tracker.sql b/tests/TestDatabase/Tracker.sql new file mode 100644 index 0000000..84f9543 --- /dev/null +++ b/tests/TestDatabase/Tracker.sql @@ -0,0 +1,40 @@ +SET NOCOUNT ON +GO + +set nocount on +set dateformat mdy + +USE master + +declare @dttm varchar(55) +select @dttm=convert(varchar,getdate(),113) +raiserror('Beginning InstPubs.SQL at %s ....',1,1,@dttm) with nowait + +GO + +if exists (select * from sysdatabases where name='pubs') +begin + raiserror('Dropping existing pubs database ....',0,1) + DROP database pubs +end +GO + +CHECKPOINT +go + +raiserror('Creating pubs database....',0,1) +go +/* + Use default size with autogrow +*/ + +CREATE DATABASE pubs +GO + +CHECKPOINT + +GO + +USE pubs + +GO \ No newline at end of file diff --git a/tests/TestDatabase/build.sh b/tests/TestDatabase/build.sh new file mode 100644 index 0000000..751e9a4 --- /dev/null +++ b/tests/TestDatabase/build.sh @@ -0,0 +1,34 @@ +#!/bin/bash +set -e + +if [ ! -f adventureworks.bak ]; then + echo "Downloading AdventureWorks OLTP backup file from Microsoft..."; + wget https://github.com/Microsoft/sql-server-samples/releases/download/adventureworks/AdventureWorks2022.bak -O adventureworks.bak -q; + echo "Download complete."; +else + echo "AdventureWorks OLTP backup file already downloaded. Skipping."; +fi +if [ ! -f adventureworks-dw.bak ]; then + echo "Downloading AdventureWorks data warehouse backup file from Microsoft..."; + wget https://github.com/Microsoft/sql-server-samples/releases/download/adventureworks/AdventureWorksDW2022.bak -O adventureworks-dw.bak -q; + echo "Download complete."; +else + echo "AdventureWorks data warehouse backup file already downloaded. Skipping."; +fi +if [ ! -f adventureworks-light.bak ]; then + echo "Downloading AdventureWorks light backup file from Microsoft..."; + wget https://github.com/Microsoft/sql-server-samples/releases/download/adventureworks/AdventureWorks2022.bak -O adventureworks-light.bak -q; + echo "Download complete."; +else + echo "AdventureWorks light backup file already downloaded. Skipping."; +fi +echo "Building OLTP docker image."; +docker build --platform linux/386,linux/amd64 . -t chriseaton/adventureworks:latest --build-arg BAK_FILE=./adventureworks.bak --build-arg SQL_FILE=./install.sql; +docker tag chriseaton/adventureworks:latest chriseaton/adventureworks:oltp; +docker tag chriseaton/adventureworks:latest chriseaton/adventureworks:oltp-2022; +echo "Building data warehouse docker image."; +docker build --platform linux/386,linux/amd64 . -t chriseaton/adventureworks:datawarehouse --build-arg BAK_FILE=./adventureworks-dw.bak --build-arg SQL_FILE=./install-dw.sql; +docker tag chriseaton/adventureworks:datawarehouse chriseaton/adventureworks:datawarehouse-2022; +echo "Building light docker image."; +docker build --platform linux/386,linux/amd64 . -t chriseaton/adventureworks:light --build-arg BAK_FILE=./adventureworks-light.bak --build-arg SQL_FILE=./install.sql; +docker tag chriseaton/adventureworks:light chriseaton/adventureworks:light-2022; \ No newline at end of file diff --git a/tests/TestDatabase/install-dw.sql b/tests/TestDatabase/install-dw.sql new file mode 100644 index 0000000..ac6ab56 --- /dev/null +++ b/tests/TestDatabase/install-dw.sql @@ -0,0 +1,8 @@ +USE [master] +GO + +RESTORE DATABASE [AdventureWorks] + FROM DISK = '/adventureworks.bak' + WITH MOVE 'AdventureWorksDW2022' TO '/var/opt/mssql/data/AdventureWorks.mdf', + MOVE 'AdventureWorksDW2022_log' TO '/var/opt/mssql/data/AdventureWorks_log.ldf' +GO \ No newline at end of file diff --git a/tests/TestDatabase/install.sh b/tests/TestDatabase/install.sh new file mode 100644 index 0000000..8206cbe --- /dev/null +++ b/tests/TestDatabase/install.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +echo "Running installation script..." + +#wait for the SQL Server to come up +sleep 30s + +#run the setup script to create the DB and the schema in the DB +/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "Test_123_Test" -i Tracker.sql +echo +echo "Done installing database." +echo "Server is ready." +sleep infinity \ No newline at end of file diff --git a/tests/TestDatabase/install.sql b/tests/TestDatabase/install.sql new file mode 100644 index 0000000..19d5414 --- /dev/null +++ b/tests/TestDatabase/install.sql @@ -0,0 +1,8 @@ +USE [master] +GO + +RESTORE DATABASE [AdventureWorks] + FROM DISK = '/adventureworks.bak' + WITH MOVE 'AdventureWorks2022' TO '/var/opt/mssql/data/AdventureWorks.mdf', + MOVE 'AdventureWorks2022_log' TO '/var/opt/mssql/data/AdventureWorks_log.ldf' +GO \ No newline at end of file diff --git a/tests/TestDatabase/startup.sh b/tests/TestDatabase/startup.sh new file mode 100644 index 0000000..3dd61d3 --- /dev/null +++ b/tests/TestDatabase/startup.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +echo "Starting server..." +/opt/mssql/bin/sqlservr & ./install.sh \ No newline at end of file From 13f47ff38064f79da20cc2c109baeb16b714c025 Mon Sep 17 00:00:00 2001 From: platyscript <244315239+platyscript@users.noreply.github.com> Date: Mon, 12 Jan 2026 06:11:24 +0530 Subject: [PATCH 3/4] New changes --- .../CodeGenerator.cs | 36 + .../ConfigurationSerializer.cs | 8 + .../DbApiBuilderEntityGenerator.Core.csproj | 6 + .../Extensions/AssemblyExtensions.cs | 18 + .../Extensions/GenerationExtensions.cs | 229 +++++ .../Extensions/TypeExtensions.cs | 37 + .../Options/GeneratorOptions.cs | 9 + .../Options/OptionsBase.cs | 5 + .../Options/TemplateOptions.cs | 74 ++ .../Scripts/ContextScriptTemplate.cs | 30 + .../Scripts/ContextScriptVariables.cs | 16 + .../Scripts/ScriptTemplateBase.cs | 144 +++ .../Scripts/ScriptVariablesBase.cs | 21 + .../Serialization/GeneratorModel.cs | 11 + .../template/dab-config.csx | 65 ++ .../DbApiBuilderEntityGenerator.csproj | 14 +- .../GenerateCommand.cs | 1 - .../OptionsCommandBase.cs | 6 + .../template/dab-config.csx | 65 ++ yaml-entity-context.yaml | 895 ++++++++++++++++++ yaml-entity.csx | 65 ++ 21 files changed, 1746 insertions(+), 9 deletions(-) create mode 100644 src/DbApiBuilderEntityGenerator.Core/Extensions/AssemblyExtensions.cs create mode 100644 src/DbApiBuilderEntityGenerator.Core/Extensions/GenerationExtensions.cs create mode 100644 src/DbApiBuilderEntityGenerator.Core/Extensions/TypeExtensions.cs create mode 100644 src/DbApiBuilderEntityGenerator.Core/Options/TemplateOptions.cs create mode 100644 src/DbApiBuilderEntityGenerator.Core/Scripts/ContextScriptTemplate.cs create mode 100644 src/DbApiBuilderEntityGenerator.Core/Scripts/ContextScriptVariables.cs create mode 100644 src/DbApiBuilderEntityGenerator.Core/Scripts/ScriptTemplateBase.cs create mode 100644 src/DbApiBuilderEntityGenerator.Core/Scripts/ScriptVariablesBase.cs create mode 100644 src/DbApiBuilderEntityGenerator.Core/template/dab-config.csx create mode 100644 src/DbApiBuilderEntityGenerator/template/dab-config.csx create mode 100644 yaml-entity-context.yaml create mode 100644 yaml-entity.csx diff --git a/src/DbApiBuilderEntityGenerator.Core/CodeGenerator.cs b/src/DbApiBuilderEntityGenerator.Core/CodeGenerator.cs index 6d1c677..6c655ed 100644 --- a/src/DbApiBuilderEntityGenerator.Core/CodeGenerator.cs +++ b/src/DbApiBuilderEntityGenerator.Core/CodeGenerator.cs @@ -1,6 +1,8 @@ using System; using DbApiBuilderEntityGenerator.Core.Extensions; +using DbApiBuilderEntityGenerator.Core.Metadata.Generation; using DbApiBuilderEntityGenerator.Core.Options; +using DbApiBuilderEntityGenerator.Core.Scripts; using Microsoft.EntityFrameworkCore.Design; using Microsoft.EntityFrameworkCore.Scaffolding; using Microsoft.EntityFrameworkCore.Scaffolding.Metadata; @@ -38,8 +40,42 @@ public bool Generate(GeneratorOptions options) var context = _modelGenerator.Generate(Options, databaseModel, databaseProviders.mapping); + GenerateContextScriptTemplates(context); + return true; } + + private void GenerateContextScriptTemplates(EntityContext entityContext) + { + var templateOption = new TemplateOptions(); + templateOption.Directory = Environment.CurrentDirectory; + templateOption.FileName = "yaml-entity-context.yaml"; + templateOption.TemplatePath = Path.Combine(Environment.CurrentDirectory, "yaml-entity.csx"); + if (!VerifyScriptTemplate(templateOption)) + return; + + try + { + var template = new ContextScriptTemplate(_loggerFactory, Options, templateOption); + template.RunScript(entityContext); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error Running Context Template: {message}", ex.Message); + } + } + + private bool VerifyScriptTemplate(TemplateOptions templateOption) + { + var templatePath = templateOption.TemplatePath; + // var templatePath = Path.Combine(templateOption.Directory, templateOption.FileName); + + if (File.Exists(templatePath)) + return true; + + _logger.LogWarning("Template '{template}' could not be found.", templatePath); + return false; + } private DatabaseModel GetDatabaseModel(IDatabaseModelFactory factory) { _logger.LogInformation("Loading database model ..."); diff --git a/src/DbApiBuilderEntityGenerator.Core/ConfigurationSerializer.cs b/src/DbApiBuilderEntityGenerator.Core/ConfigurationSerializer.cs index 6e90040..4dcca90 100644 --- a/src/DbApiBuilderEntityGenerator.Core/ConfigurationSerializer.cs +++ b/src/DbApiBuilderEntityGenerator.Core/ConfigurationSerializer.cs @@ -27,6 +27,14 @@ public ConfigurationSerializer(ILogger logger) /// public const string OptionsFileName = "sample.yaml"; + + public const string OutputFileName = "data-config.yaml"; + + /// + /// The options file name. Default 'generation.yml' + /// + public const string TemplateFilePath = "DbApiBuilderEntityGenerator.template.config-template.csx"; + /// /// Loads the options file using the specified and . /// diff --git a/src/DbApiBuilderEntityGenerator.Core/DbApiBuilderEntityGenerator.Core.csproj b/src/DbApiBuilderEntityGenerator.Core/DbApiBuilderEntityGenerator.Core.csproj index 167a69b..553574b 100644 --- a/src/DbApiBuilderEntityGenerator.Core/DbApiBuilderEntityGenerator.Core.csproj +++ b/src/DbApiBuilderEntityGenerator.Core/DbApiBuilderEntityGenerator.Core.csproj @@ -26,6 +26,12 @@ + + + + + + diff --git a/src/DbApiBuilderEntityGenerator.Core/Extensions/AssemblyExtensions.cs b/src/DbApiBuilderEntityGenerator.Core/Extensions/AssemblyExtensions.cs new file mode 100644 index 0000000..ca3750f --- /dev/null +++ b/src/DbApiBuilderEntityGenerator.Core/Extensions/AssemblyExtensions.cs @@ -0,0 +1,18 @@ +using System; +using System.Reflection; + +namespace DbApiBuilderEntityGenerator.Core.Extensions; + +public static class AssemblyExtensions +{ + public static async Task ReadResourceAsync(this Assembly assembly, string name) + { + // Determine path + string resourcePath; // Format: "{Namespace}.{Folder}.{filename}.{Extension}" + resourcePath = assembly.GetManifestResourceNames().Single(str => str.EndsWith(name)); + + using Stream stream = assembly.GetManifestResourceStream(resourcePath)!; + using StreamReader reader = new(stream); + return await reader.ReadToEndAsync(); + } +} diff --git a/src/DbApiBuilderEntityGenerator.Core/Extensions/GenerationExtensions.cs b/src/DbApiBuilderEntityGenerator.Core/Extensions/GenerationExtensions.cs new file mode 100644 index 0000000..85dd883 --- /dev/null +++ b/src/DbApiBuilderEntityGenerator.Core/Extensions/GenerationExtensions.cs @@ -0,0 +1,229 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Text; + +namespace DbApiBuilderEntityGenerator.Core.Extensions; + +public static class GenerationExtensions +{ + #region Data + private static readonly HashSet _csharpKeywords = new(StringComparer.Ordinal) + { + "as", "do", "if", "in", "is", + "for", "int", "new", "out", "ref", "try", + "base", "bool", "byte", "case", "char", "else", "enum", "goto", "lock", "long", "null", "this", "true", "uint", "void", + "break", "catch", "class", "const", "event", "false", "fixed", "float", "sbyte", "short", "throw", "ulong", "using", "while", + "double", "extern", "object", "params", "public", "return", "sealed", "sizeof", "static", "string", "struct", "switch", "typeof", "unsafe", "ushort", + "checked", "decimal", "default", "finally", "foreach", "private", "virtual", + "abstract", "continue", "delegate", "explicit", "implicit", "internal", "operator", "override", "readonly", "volatile", + "__arglist", "__makeref", "__reftype", "interface", "namespace", "protected", "unchecked", + "__refvalue", "stackalloc" + }; + + private static readonly HashSet _defaultNamespaces = + [ + "System", + "System.Collections.Generic", + ]; + + private static readonly Dictionary _csharpTypeAlias = new(16) + { + { typeof(bool), "bool" }, + { typeof(byte), "byte" }, + { typeof(char), "char" }, + { typeof(decimal), "decimal" }, + { typeof(double), "double" }, + { typeof(float), "float" }, + { typeof(int), "int" }, + { typeof(long), "long" }, + { typeof(object), "object" }, + { typeof(sbyte), "sbyte" }, + { typeof(short), "short" }, + { typeof(string), "string" }, + { typeof(uint), "uint" }, + { typeof(ulong), "ulong" }, + { typeof(ushort), "ushort" }, + { typeof(void), "void" } + }; + #endregion + + public static string ToFieldName(this string name) + { + ArgumentException.ThrowIfNullOrEmpty(name); + + return "_" + name.ToCamelCase(); + } + + public static string MakeUnique(this string name, Func exists) + { + ArgumentException.ThrowIfNullOrEmpty(name); + ArgumentNullException.ThrowIfNull(exists); + + string uniqueName = name; + int count = 1; + + while (exists(uniqueName)) + uniqueName = string.Concat(name, count++); + + return uniqueName; + } + + public static bool IsKeyword(this string text) + { + ArgumentException.ThrowIfNullOrEmpty(text); + + return _csharpKeywords.Contains(text); + } + + [return: NotNullIfNotNull(nameof(name))] + public static string? ToSafeName(this string? name) + { + if (string.IsNullOrEmpty(name)) + return name; + + if (!name.IsKeyword()) + return name; + + return "@" + name; + } + + public static string ToType(this Type type) + { + ArgumentNullException.ThrowIfNull(type); + + var stringBuilder = new StringBuilder(); + ProcessType(stringBuilder, type); + return stringBuilder.ToString(); + } + + public static string? ToNullableType(this Type type, bool isNullable = false) + { + bool isValueType = type.IsValueType; + + var typeString = type.ToType(); + + if (!isValueType || !isNullable) + return typeString; + + return typeString.EndsWith('?') ? typeString : typeString + "?"; + } + + public static bool IsValueType(this string? type) + { + if (string.IsNullOrEmpty(type)) + return false; + + if (!type.StartsWith("System.")) + return false; + + var t = Type.GetType(type, false); + return t != null && t.IsValueType; + } + + public static string ToLiteral(this string value) + { + ArgumentException.ThrowIfNullOrEmpty(value); + + return value.Contains('\n') || value.Contains('\r') + ? "@\"" + value.Replace("\"", "\"\"") + "\"" + : "\"" + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\""; + } + + + + private static void ProcessType(StringBuilder builder, Type type) + { + if (type.IsGenericType) + { + var genericArguments = type.GetGenericArguments(); + ProcessGenericType(builder, type, genericArguments, genericArguments.Length); + } + else if (type.IsArray) + { + ProcessArrayType(builder, type); + } + else if (_csharpTypeAlias.TryGetValue(type, out var builtInName)) + { + builder.Append(builtInName); + } + else if (type.Namespace.HasValue() && _defaultNamespaces.Contains(type.Namespace)) + { + builder.Append(type.Name); + } + else + { + builder.Append(type.FullName ?? type.Name); + } + } + + private static void ProcessArrayType(StringBuilder builder, Type type) + { + var innerType = type; + while (innerType.IsArray) + { + innerType = innerType.GetElementType()!; + } + + ProcessType(builder, innerType); + + while (type.IsArray) + { + builder.Append('['); + builder.Append(',', type.GetArrayRank() - 1); + builder.Append(']'); + type = type.GetElementType()!; + } + } + + private static void ProcessGenericType(StringBuilder builder, Type type, Type[] genericArguments, int length) + { + if (type.IsConstructedGenericType + && type.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + ProcessType(builder, type.GetUnderlyingType()); + builder.Append('?'); + return; + } + + var offset = type.DeclaringType != null ? type.DeclaringType.GetGenericArguments().Length : 0; + var genericPartIndex = type.Name.IndexOf('`'); + if (genericPartIndex <= 0) + { + if (type.Namespace.HasValue() && _defaultNamespaces.Contains(type.Namespace)) + { + builder.Append(type.Name); + } + else + { + builder.Append(type.FullName ?? type.Name); + } + return; + } + + if (type.Namespace.HasValue() && !_defaultNamespaces.Contains(type.Namespace)) + { + builder.Append(type.Namespace); + builder.Append("."); + } + builder.Append(type.Name, 0, genericPartIndex); + builder.Append('<'); + + for (var i = offset; i < length; i++) + { + ProcessType(builder, genericArguments[i]); + if (i + 1 == length) + { + continue; + } + + builder.Append(','); + if (!genericArguments[i + 1].IsGenericParameter) + { + builder.Append(' '); + } + } + + builder.Append('>'); + } + +} diff --git a/src/DbApiBuilderEntityGenerator.Core/Extensions/TypeExtensions.cs b/src/DbApiBuilderEntityGenerator.Core/Extensions/TypeExtensions.cs new file mode 100644 index 0000000..1dbd283 --- /dev/null +++ b/src/DbApiBuilderEntityGenerator.Core/Extensions/TypeExtensions.cs @@ -0,0 +1,37 @@ +using System; + +namespace DbApiBuilderEntityGenerator.Core.Extensions; + +public static class TypeExtensions +{ + /// + /// Gets the underlying type dealing with . + /// + /// The type. + /// Returns a type dealing with . + public static Type GetUnderlyingType(this Type type) + { + ArgumentNullException.ThrowIfNull(type); + return Nullable.GetUnderlyingType(type) ?? type; + } + + /// + /// Determines whether the specified can be null. + /// + /// The type to check. + /// + /// true if the specified can be null; otherwise, false. + /// + public static bool IsNullable(this Type type) + { + ArgumentNullException.ThrowIfNull(type); + + if (!type.IsGenericType || type.IsGenericTypeDefinition) + return false; + + // Instantiated generic type only + Type genericType = type.GetGenericTypeDefinition(); + return ReferenceEquals(genericType, typeof(Nullable<>)); + } + +} diff --git a/src/DbApiBuilderEntityGenerator.Core/Options/GeneratorOptions.cs b/src/DbApiBuilderEntityGenerator.Core/Options/GeneratorOptions.cs index fbb2d0d..368f113 100644 --- a/src/DbApiBuilderEntityGenerator.Core/Options/GeneratorOptions.cs +++ b/src/DbApiBuilderEntityGenerator.Core/Options/GeneratorOptions.cs @@ -123,4 +123,13 @@ public string? Directory /// public SelectionOptions Renaming { get; } + /// + /// Gets or sets the renaming expressions. + /// + /// + /// The renaming expressions. + /// + public string? OutputFileName { get; set; } + + public string? TemplateFilePath { get; set; } } diff --git a/src/DbApiBuilderEntityGenerator.Core/Options/OptionsBase.cs b/src/DbApiBuilderEntityGenerator.Core/Options/OptionsBase.cs index dae0bd5..415fb7d 100644 --- a/src/DbApiBuilderEntityGenerator.Core/Options/OptionsBase.cs +++ b/src/DbApiBuilderEntityGenerator.Core/Options/OptionsBase.cs @@ -16,6 +16,11 @@ public OptionsBase() { } + public OptionsBase(VariableDictionary variables) + { + Variables = variables; + } + public VariableDictionary Variables { get; } = new VariableDictionary(); public string? Prefix { get; } diff --git a/src/DbApiBuilderEntityGenerator.Core/Options/TemplateOptions.cs b/src/DbApiBuilderEntityGenerator.Core/Options/TemplateOptions.cs new file mode 100644 index 0000000..95c8049 --- /dev/null +++ b/src/DbApiBuilderEntityGenerator.Core/Options/TemplateOptions.cs @@ -0,0 +1,74 @@ +using System; + +namespace DbApiBuilderEntityGenerator.Core.Options; + +/// +/// Script Template options +/// +public class TemplateOptions : OptionsBase +{ + /// + /// Initializes a new instance of the class. + /// + /// The shared variable dictionary. + /// The variable key prefix. + public TemplateOptions() + : base() + { + Parameters = []; + } + + /// + /// Gets or sets the template file path. + /// + /// + /// The template file path. + /// + public string? TemplatePath + { + get => GetProperty(); + set => SetProperty(value); + } + + /// + /// Gets or sets the name of the class + /// + /// + /// The name of the class. + /// + public string? FileName + { + get => GetProperty(); + set => SetProperty(value); + } + + + /// + /// Gets or sets the output directory. Default is the current working directory. + /// + /// + /// The output directory. + /// + public string? Directory + { + get => GetProperty(); + set => SetProperty(value); + } + + /// + /// Gets or sets a value indicating whether the generated file will be overwritten. + /// + /// + /// true to overwrite generated file; otherwise, false. + /// + public bool Overwrite { get; set; } + + /// + /// Gets or sets the template parameters. + /// + /// + /// The template parameters. + /// + public Dictionary Parameters { get; } + +} diff --git a/src/DbApiBuilderEntityGenerator.Core/Scripts/ContextScriptTemplate.cs b/src/DbApiBuilderEntityGenerator.Core/Scripts/ContextScriptTemplate.cs new file mode 100644 index 0000000..a9315c0 --- /dev/null +++ b/src/DbApiBuilderEntityGenerator.Core/Scripts/ContextScriptTemplate.cs @@ -0,0 +1,30 @@ +using System; +using DbApiBuilderEntityGenerator.Core.Metadata.Generation; +using DbApiBuilderEntityGenerator.Core.Options; +using Microsoft.Extensions.Logging; + +namespace DbApiBuilderEntityGenerator.Core.Scripts; + +public class ContextScriptTemplate : ScriptTemplateBase +{ + private EntityContext _entityContext = null!; + + public ContextScriptTemplate(ILoggerFactory loggerFactory, GeneratorOptions generatorOptions, TemplateOptions templateOptions) + : base(loggerFactory, generatorOptions, templateOptions) + { + } + + public void RunScript(EntityContext entityContext) + { + ArgumentNullException.ThrowIfNull(entityContext); + + _entityContext = entityContext; + + WriteCode(); + } + + protected override ContextScriptVariables CreateVariables() + { + return new ContextScriptVariables(_entityContext, GeneratorOptions, TemplateOptions); + } +} diff --git a/src/DbApiBuilderEntityGenerator.Core/Scripts/ContextScriptVariables.cs b/src/DbApiBuilderEntityGenerator.Core/Scripts/ContextScriptVariables.cs new file mode 100644 index 0000000..5005782 --- /dev/null +++ b/src/DbApiBuilderEntityGenerator.Core/Scripts/ContextScriptVariables.cs @@ -0,0 +1,16 @@ +using System; +using DbApiBuilderEntityGenerator.Core.Metadata.Generation; +using DbApiBuilderEntityGenerator.Core.Options; + +namespace DbApiBuilderEntityGenerator.Core.Scripts; + +public class ContextScriptVariables : ScriptVariablesBase +{ + public ContextScriptVariables(EntityContext entityContext, GeneratorOptions generatorOptions, TemplateOptions templateOptions) + : base(generatorOptions, templateOptions) + { + EntityContext = entityContext ?? throw new ArgumentNullException(nameof(entityContext)); + } + + public EntityContext EntityContext { get; } +} diff --git a/src/DbApiBuilderEntityGenerator.Core/Scripts/ScriptTemplateBase.cs b/src/DbApiBuilderEntityGenerator.Core/Scripts/ScriptTemplateBase.cs new file mode 100644 index 0000000..4df8961 --- /dev/null +++ b/src/DbApiBuilderEntityGenerator.Core/Scripts/ScriptTemplateBase.cs @@ -0,0 +1,144 @@ +using System; +using System.Reflection; +using DbApiBuilderEntityGenerator.Core.Extensions; +using DbApiBuilderEntityGenerator.Core.Options; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Scripting; +using Microsoft.CodeAnalysis.Scripting; +using Microsoft.Extensions.Logging; +using ScriptOptions = Microsoft.CodeAnalysis.Scripting.ScriptOptions; +namespace DbApiBuilderEntityGenerator.Core.Scripts; + +public abstract class ScriptTemplateBase where TVariable : ScriptVariablesBase +{ + + private Script? _scriptTemplate; + + protected ScriptTemplateBase(ILoggerFactory loggerFactory, GeneratorOptions generatorOptions, TemplateOptions templateOptions) + { + Logger = loggerFactory.CreateLogger(this.GetType()); + TemplateOptions = templateOptions; + GeneratorOptions = generatorOptions; + } + + protected ILogger Logger { get; } + + public TemplateOptions TemplateOptions { get; } + + public GeneratorOptions GeneratorOptions { get; } + protected abstract TVariable CreateVariables(); + protected virtual void WriteCode() + { + var templatePath = TemplateOptions.TemplatePath; + + // if (!File.Exists(templatePath)) + // { + // Logger.LogWarning("Template '{template}' could not be found.", templatePath); + // return; + // } + + // save file + var directory = TemplateOptions.Directory; + var fileName = TemplateOptions.FileName; + + if (directory.IsNullOrEmpty() || fileName.IsNullOrEmpty()) + { + Logger.LogWarning("Template '{template}' could not resolve output file.", templatePath); + return; + } + + var path = Path.Combine(directory, fileName); + var exists = File.Exists(path); + + if (File.Exists(path)) + Logger.LogInformation("Updating template script file: {fileName}", fileName); + else + Logger.LogInformation("Creating template script file: {fileName}", fileName); + + // get content + var content = ExecuteScript(); + + if (content.IsNullOrWhiteSpace()) + { + Logger.LogDebug("Skipping template '{template}' because it didn't return any text.", templatePath); + return; + } + + File.WriteAllText(path, content); + } + protected virtual string ExecuteScript() + { + var templatePath = TemplateOptions.TemplatePath; + if (!File.Exists(templatePath)) + { + Logger.LogWarning("Template '{template}' could not be found.", templatePath); + return string.Empty; + } + + var script = LoadScript(templatePath); + var variables = CreateVariables(); + + var scriptTask = script.RunAsync(variables); + var scriptState = scriptTask.Result; + + return scriptState.ReturnValue; + } + protected Script LoadScript(string scriptPath) + { + if (_scriptTemplate != null) + return _scriptTemplate; + + Logger.LogDebug("Loading template script: {script}", scriptPath); + + var scriptContent = File.ReadAllText(scriptPath); + + scriptContent = Assembly.GetExecutingAssembly() + .ReadResourceAsync("DbApiBuilderEntityGenerator.Core.template.dab-config.csx") + .Result; + var scriptOptions = ScriptOptions.Default + .WithReferences( + typeof(ScriptVariablesBase).Assembly + ) + .WithImports( + "System", + "System.Collections.Generic", + "System.Linq", + "System.Text", + "DbApiBuilderEntityGenerator.Core.Extensions", + "DbApiBuilderEntityGenerator.Core.Metadata.Generation", + "DbApiBuilderEntityGenerator.Core.Options", + "Microsoft.EntityFrameworkCore.Internal" + ); + + _scriptTemplate = CSharpScript.Create(scriptContent, scriptOptions, typeof(TVariable)); + var diagnostics = _scriptTemplate.Compile(); + + if (diagnostics.Length == 0) + return _scriptTemplate; + + Logger.LogInformation("Template Compile Diagnostics: "); + foreach (var diagnostic in diagnostics) + { + var message = diagnostic.GetMessage(); + switch (diagnostic.Severity) + { + case DiagnosticSeverity.Info: + Logger.LogDebug(message); + break; + case DiagnosticSeverity.Warning: + Logger.LogWarning(message); + break; + case DiagnosticSeverity.Error: + Logger.LogError(message); + break; + default: + Logger.LogDebug(message); + break; + } + } + + return _scriptTemplate; + } +} + + diff --git a/src/DbApiBuilderEntityGenerator.Core/Scripts/ScriptVariablesBase.cs b/src/DbApiBuilderEntityGenerator.Core/Scripts/ScriptVariablesBase.cs new file mode 100644 index 0000000..2d1da10 --- /dev/null +++ b/src/DbApiBuilderEntityGenerator.Core/Scripts/ScriptVariablesBase.cs @@ -0,0 +1,21 @@ +using System; +using DbApiBuilderEntityGenerator.Core.Options; +using Microsoft.EntityFrameworkCore.Infrastructure; + +namespace DbApiBuilderEntityGenerator.Core.Scripts; + +public abstract class ScriptVariablesBase +{ + protected ScriptVariablesBase(GeneratorOptions generatorOptions, TemplateOptions templateOptions) + { + GeneratorOptions = generatorOptions ?? throw new ArgumentNullException(nameof(generatorOptions)); + TemplateOptions = templateOptions ?? throw new ArgumentNullException(nameof(templateOptions)); + CodeBuilder = new IndentedStringBuilder(); + } + + public TemplateOptions TemplateOptions { get; } + + public GeneratorOptions GeneratorOptions { get; } + + public IndentedStringBuilder CodeBuilder { get; } +} diff --git a/src/DbApiBuilderEntityGenerator.Core/Serialization/GeneratorModel.cs b/src/DbApiBuilderEntityGenerator.Core/Serialization/GeneratorModel.cs index 7d110cf..946892e 100644 --- a/src/DbApiBuilderEntityGenerator.Core/Serialization/GeneratorModel.cs +++ b/src/DbApiBuilderEntityGenerator.Core/Serialization/GeneratorModel.cs @@ -103,5 +103,16 @@ public GeneratorModel() /// The renaming expressions. /// public SelectionModel? Renaming { get; set; } + + + /// + /// Gets or sets the renaming expressions. + /// + /// + /// The renaming expressions. + /// + public string? OutputFileName { get; set; } + + public string? TemplateFilePath { get; set; } } diff --git a/src/DbApiBuilderEntityGenerator.Core/template/dab-config.csx b/src/DbApiBuilderEntityGenerator.Core/template/dab-config.csx new file mode 100644 index 0000000..5afe8c2 --- /dev/null +++ b/src/DbApiBuilderEntityGenerator.Core/template/dab-config.csx @@ -0,0 +1,65 @@ +public string WriteCode() +{ + CodeBuilder.Clear(); + foreach (var entity in EntityContext.Entities) + { + CodeBuilder.Append("EntityClass: ").Append(entity.EntityClass.ToSafeName()).AppendLine(); + + CodeBuilder.Append("ContextProperty: ").Append(entity.ContextProperty.ToSafeName()).AppendLine(); + + CodeBuilder.Append("TableSchema: '").Append(entity.TableSchema.ToSafeName()).AppendLine("'"); + CodeBuilder.Append("TableName: '").Append(entity.TableName.ToSafeName()).AppendLine("'"); + + + CodeBuilder.Append("IsView: ").Append(entity.IsView.ToString()).AppendLine(); + + CodeBuilder.Append("Properties:").AppendLine(); + using (CodeBuilder.Indent()) + GenerateProperties(entity); + } + + return CodeBuilder.ToString(); +} + +private void GenerateProperties(Entity entity) +{ + foreach (var property in entity.Properties) + { + CodeBuilder.Append("- PropertyName: ").Append(property.PropertyName.ToSafeName()).AppendLine(); + CodeBuilder.Append(" ColumnName: '").Append(property.ColumnName.ToSafeName()).AppendLine("'"); + CodeBuilder.Append(" StoreType: ").Append(property.StoreType.ToSafeName()).AppendLine(); + CodeBuilder.Append(" NativeType: '").Append(property.NativeType.ToSafeName()).AppendLine("'"); + CodeBuilder.Append(" DataType: ").Append(property.DataType.ToString()).AppendLine(); + CodeBuilder.Append(" SystemType: ").Append(property.SystemType.Name).AppendLine(); + + if (property.Size != null) + CodeBuilder.Append(" Size: ").Append(property.Size.ToString()).AppendLine(); + + if (property.Default != null) + CodeBuilder.Append(" Default: '").Append(property.Default.ToSafeName()).AppendLine("'"); + + if (property.ValueGenerated != null) + CodeBuilder.Append(" ValueGenerated: ").Append(property.ValueGenerated.ToString()).AppendLine(); + + if (property.IsNullable != null) + CodeBuilder.Append(" IsNullable: ").Append(property.IsNullable.ToString()).AppendLine(); + + if (property.IsPrimaryKey != null) + CodeBuilder.Append(" IsPrimaryKey: ").Append(property.IsPrimaryKey.ToString()).AppendLine(); + + if (property.IsForeignKey != null) + CodeBuilder.Append(" IsForeignKey: ").Append(property.IsForeignKey.ToString()).AppendLine(); + + if (property.IsReadOnly != null) + CodeBuilder.Append(" IsReadOnly: ").Append(property.IsReadOnly.ToString()).AppendLine(); + + if (property.IsRowVersion != null) + CodeBuilder.Append(" IsRowVersion: ").Append(property.IsRowVersion.ToString()).AppendLine(); + + if (property.IsUnique != null) + CodeBuilder.Append(" IsUnique: ").Append(property.IsUnique.ToString()).AppendLine(); + } +} + +// run script +WriteCode() diff --git a/src/DbApiBuilderEntityGenerator/DbApiBuilderEntityGenerator.csproj b/src/DbApiBuilderEntityGenerator/DbApiBuilderEntityGenerator.csproj index df84939..d95b1ff 100644 --- a/src/DbApiBuilderEntityGenerator/DbApiBuilderEntityGenerator.csproj +++ b/src/DbApiBuilderEntityGenerator/DbApiBuilderEntityGenerator.csproj @@ -42,19 +42,13 @@ - - - - - + - + @@ -68,4 +62,8 @@ + + + + diff --git a/src/DbApiBuilderEntityGenerator/GenerateCommand.cs b/src/DbApiBuilderEntityGenerator/GenerateCommand.cs index d3434eb..1ee8a15 100644 --- a/src/DbApiBuilderEntityGenerator/GenerateCommand.cs +++ b/src/DbApiBuilderEntityGenerator/GenerateCommand.cs @@ -44,7 +44,6 @@ protected override int OnExecute(CommandLineApplication application) if (Provider.HasValue) configuration.Provider = Provider.Value; - // convert to options format to support variables var options = OptionMapper.Map(configuration); diff --git a/src/DbApiBuilderEntityGenerator/OptionsCommandBase.cs b/src/DbApiBuilderEntityGenerator/OptionsCommandBase.cs index f630f9d..8f8297a 100644 --- a/src/DbApiBuilderEntityGenerator/OptionsCommandBase.cs +++ b/src/DbApiBuilderEntityGenerator/OptionsCommandBase.cs @@ -20,4 +20,10 @@ protected OptionsCommandBase(ILoggerFactory logger, IConsole console, IConfigura [Option("-f ", Description = "The options file name")] public string OptionsFile { get; set; } = ConfigurationSerializer.OptionsFileName; + + [Option("-o ", Description = "The options file name")] + public string OutputFileName { get; set; } = ConfigurationSerializer.OutputFileName; + + [Option("-t ", Description = "The path to the template file name")] + public string TemplateFilePath { get; set; } = ConfigurationSerializer.TemplateFilePath; } diff --git a/src/DbApiBuilderEntityGenerator/template/dab-config.csx b/src/DbApiBuilderEntityGenerator/template/dab-config.csx new file mode 100644 index 0000000..5afe8c2 --- /dev/null +++ b/src/DbApiBuilderEntityGenerator/template/dab-config.csx @@ -0,0 +1,65 @@ +public string WriteCode() +{ + CodeBuilder.Clear(); + foreach (var entity in EntityContext.Entities) + { + CodeBuilder.Append("EntityClass: ").Append(entity.EntityClass.ToSafeName()).AppendLine(); + + CodeBuilder.Append("ContextProperty: ").Append(entity.ContextProperty.ToSafeName()).AppendLine(); + + CodeBuilder.Append("TableSchema: '").Append(entity.TableSchema.ToSafeName()).AppendLine("'"); + CodeBuilder.Append("TableName: '").Append(entity.TableName.ToSafeName()).AppendLine("'"); + + + CodeBuilder.Append("IsView: ").Append(entity.IsView.ToString()).AppendLine(); + + CodeBuilder.Append("Properties:").AppendLine(); + using (CodeBuilder.Indent()) + GenerateProperties(entity); + } + + return CodeBuilder.ToString(); +} + +private void GenerateProperties(Entity entity) +{ + foreach (var property in entity.Properties) + { + CodeBuilder.Append("- PropertyName: ").Append(property.PropertyName.ToSafeName()).AppendLine(); + CodeBuilder.Append(" ColumnName: '").Append(property.ColumnName.ToSafeName()).AppendLine("'"); + CodeBuilder.Append(" StoreType: ").Append(property.StoreType.ToSafeName()).AppendLine(); + CodeBuilder.Append(" NativeType: '").Append(property.NativeType.ToSafeName()).AppendLine("'"); + CodeBuilder.Append(" DataType: ").Append(property.DataType.ToString()).AppendLine(); + CodeBuilder.Append(" SystemType: ").Append(property.SystemType.Name).AppendLine(); + + if (property.Size != null) + CodeBuilder.Append(" Size: ").Append(property.Size.ToString()).AppendLine(); + + if (property.Default != null) + CodeBuilder.Append(" Default: '").Append(property.Default.ToSafeName()).AppendLine("'"); + + if (property.ValueGenerated != null) + CodeBuilder.Append(" ValueGenerated: ").Append(property.ValueGenerated.ToString()).AppendLine(); + + if (property.IsNullable != null) + CodeBuilder.Append(" IsNullable: ").Append(property.IsNullable.ToString()).AppendLine(); + + if (property.IsPrimaryKey != null) + CodeBuilder.Append(" IsPrimaryKey: ").Append(property.IsPrimaryKey.ToString()).AppendLine(); + + if (property.IsForeignKey != null) + CodeBuilder.Append(" IsForeignKey: ").Append(property.IsForeignKey.ToString()).AppendLine(); + + if (property.IsReadOnly != null) + CodeBuilder.Append(" IsReadOnly: ").Append(property.IsReadOnly.ToString()).AppendLine(); + + if (property.IsRowVersion != null) + CodeBuilder.Append(" IsRowVersion: ").Append(property.IsRowVersion.ToString()).AppendLine(); + + if (property.IsUnique != null) + CodeBuilder.Append(" IsUnique: ").Append(property.IsUnique.ToString()).AppendLine(); + } +} + +// run script +WriteCode() diff --git a/yaml-entity-context.yaml b/yaml-entity-context.yaml new file mode 100644 index 0000000..f3ab559 --- /dev/null +++ b/yaml-entity-context.yaml @@ -0,0 +1,895 @@ +EntityClass: Authors +ContextProperty: +TableSchema: 'dbo' +TableName: 'authors' +IsView: False +Properties: + - PropertyName: AuId + ColumnName: 'au_id' + StoreType: varchar(11) + NativeType: 'varchar' + DataType: AnsiString + SystemType: String + Size: 11 + IsNullable: False + IsPrimaryKey: True + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: AuLname + ColumnName: 'au_lname' + StoreType: varchar(40) + NativeType: 'varchar' + DataType: AnsiString + SystemType: String + Size: 40 + IsNullable: False + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: AuFname + ColumnName: 'au_fname' + StoreType: varchar(20) + NativeType: 'varchar' + DataType: AnsiString + SystemType: String + Size: 20 + IsNullable: False + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: Phone + ColumnName: 'phone' + StoreType: char(12) + NativeType: '@char' + DataType: AnsiStringFixedLength + SystemType: String + Size: 12 + Default: '('UNKNOWN')' + IsNullable: False + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: Address + ColumnName: 'address' + StoreType: varchar(40) + NativeType: 'varchar' + DataType: AnsiString + SystemType: String + Size: 40 + IsNullable: True + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: City + ColumnName: 'city' + StoreType: varchar(20) + NativeType: 'varchar' + DataType: AnsiString + SystemType: String + Size: 20 + IsNullable: True + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: State + ColumnName: 'state' + StoreType: char(2) + NativeType: '@char' + DataType: AnsiStringFixedLength + SystemType: String + Size: 2 + IsNullable: True + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: Zip + ColumnName: 'zip' + StoreType: char(5) + NativeType: '@char' + DataType: AnsiStringFixedLength + SystemType: String + Size: 5 + IsNullable: True + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: Contract + ColumnName: 'contract' + StoreType: bit + NativeType: 'bit' + DataType: Boolean + SystemType: Boolean + IsNullable: False + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False +EntityClass: Discounts +ContextProperty: +TableSchema: 'dbo' +TableName: 'discounts' +IsView: False +Properties: + - PropertyName: Discounttype + ColumnName: 'discounttype' + StoreType: varchar(40) + NativeType: 'varchar' + DataType: AnsiString + SystemType: String + Size: 40 + IsNullable: False + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: StorId + ColumnName: 'stor_id' + StoreType: char(4) + NativeType: '@char' + DataType: AnsiStringFixedLength + SystemType: String + Size: 4 + IsNullable: True + IsPrimaryKey: False + IsForeignKey: True + IsRowVersion: False + IsUnique: False + - PropertyName: Lowqty + ColumnName: 'lowqty' + StoreType: smallint + NativeType: 'smallint' + DataType: Int16 + SystemType: Int16 + IsNullable: True + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: Highqty + ColumnName: 'highqty' + StoreType: smallint + NativeType: 'smallint' + DataType: Int16 + SystemType: Int16 + IsNullable: True + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: Discount + ColumnName: 'discount' + StoreType: decimal(4,2) + NativeType: '@decimal' + DataType: Decimal + SystemType: Decimal + IsNullable: False + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False +EntityClass: Stores +ContextProperty: +TableSchema: 'dbo' +TableName: 'stores' +IsView: False +Properties: + - PropertyName: StorId + ColumnName: 'stor_id' + StoreType: char(4) + NativeType: '@char' + DataType: AnsiStringFixedLength + SystemType: String + Size: 4 + IsNullable: False + IsPrimaryKey: True + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: StorName + ColumnName: 'stor_name' + StoreType: varchar(40) + NativeType: 'varchar' + DataType: AnsiString + SystemType: String + Size: 40 + IsNullable: True + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: StorAddress + ColumnName: 'stor_address' + StoreType: varchar(40) + NativeType: 'varchar' + DataType: AnsiString + SystemType: String + Size: 40 + IsNullable: True + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: City + ColumnName: 'city' + StoreType: varchar(20) + NativeType: 'varchar' + DataType: AnsiString + SystemType: String + Size: 20 + IsNullable: True + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: State + ColumnName: 'state' + StoreType: char(2) + NativeType: '@char' + DataType: AnsiStringFixedLength + SystemType: String + Size: 2 + IsNullable: True + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: Zip + ColumnName: 'zip' + StoreType: char(5) + NativeType: '@char' + DataType: AnsiStringFixedLength + SystemType: String + Size: 5 + IsNullable: True + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False +EntityClass: Employee +ContextProperty: +TableSchema: 'dbo' +TableName: 'employee' +IsView: False +Properties: + - PropertyName: EmpId + ColumnName: 'emp_id' + StoreType: char(9) + NativeType: '@char' + DataType: AnsiStringFixedLength + SystemType: String + Size: 9 + IsNullable: False + IsPrimaryKey: True + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: Fname + ColumnName: 'fname' + StoreType: varchar(20) + NativeType: 'varchar' + DataType: AnsiString + SystemType: String + Size: 20 + IsNullable: False + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: Minit + ColumnName: 'minit' + StoreType: char(1) + NativeType: '@char' + DataType: AnsiStringFixedLength + SystemType: String + Size: 1 + IsNullable: True + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: Lname + ColumnName: 'lname' + StoreType: varchar(30) + NativeType: 'varchar' + DataType: AnsiString + SystemType: String + Size: 30 + IsNullable: False + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: JobId + ColumnName: 'job_id' + StoreType: smallint + NativeType: 'smallint' + DataType: Int16 + SystemType: Int16 + Default: '((1))' + IsNullable: False + IsPrimaryKey: False + IsForeignKey: True + IsRowVersion: False + IsUnique: False + - PropertyName: JobLvl + ColumnName: 'job_lvl' + StoreType: tinyint + NativeType: 'tinyint' + DataType: Byte + SystemType: Byte + Default: '((10))' + IsNullable: True + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: PubId + ColumnName: 'pub_id' + StoreType: char(4) + NativeType: '@char' + DataType: AnsiStringFixedLength + SystemType: String + Size: 4 + Default: '('9952')' + IsNullable: False + IsPrimaryKey: False + IsForeignKey: True + IsRowVersion: False + IsUnique: False + - PropertyName: HireDate + ColumnName: 'hire_date' + StoreType: datetime + NativeType: 'datetime' + DataType: DateTime + SystemType: DateTime + Default: '(getdate())' + IsNullable: False + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False +EntityClass: Jobs +ContextProperty: +TableSchema: 'dbo' +TableName: 'jobs' +IsView: False +Properties: + - PropertyName: JobId + ColumnName: 'job_id' + StoreType: smallint + NativeType: 'smallint' + DataType: Int16 + SystemType: Int16 + ValueGenerated: OnAdd + IsNullable: False + IsPrimaryKey: True + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: JobDesc + ColumnName: 'job_desc' + StoreType: varchar(50) + NativeType: 'varchar' + DataType: AnsiString + SystemType: String + Size: 50 + Default: '('New Position - title not formalized yet')' + IsNullable: False + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: MinLvl + ColumnName: 'min_lvl' + StoreType: tinyint + NativeType: 'tinyint' + DataType: Byte + SystemType: Byte + IsNullable: False + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: MaxLvl + ColumnName: 'max_lvl' + StoreType: tinyint + NativeType: 'tinyint' + DataType: Byte + SystemType: Byte + IsNullable: False + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False +EntityClass: Publishers +ContextProperty: +TableSchema: 'dbo' +TableName: 'publishers' +IsView: False +Properties: + - PropertyName: PubId + ColumnName: 'pub_id' + StoreType: char(4) + NativeType: '@char' + DataType: AnsiStringFixedLength + SystemType: String + Size: 4 + IsNullable: False + IsPrimaryKey: True + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: PubName + ColumnName: 'pub_name' + StoreType: varchar(40) + NativeType: 'varchar' + DataType: AnsiString + SystemType: String + Size: 40 + IsNullable: True + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: City + ColumnName: 'city' + StoreType: varchar(20) + NativeType: 'varchar' + DataType: AnsiString + SystemType: String + Size: 20 + IsNullable: True + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: State + ColumnName: 'state' + StoreType: char(2) + NativeType: '@char' + DataType: AnsiStringFixedLength + SystemType: String + Size: 2 + IsNullable: True + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: Country + ColumnName: 'country' + StoreType: varchar(30) + NativeType: 'varchar' + DataType: AnsiString + SystemType: String + Size: 30 + Default: '('USA')' + IsNullable: True + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False +EntityClass: PubInfo +ContextProperty: +TableSchema: 'dbo' +TableName: 'pub_info' +IsView: False +Properties: + - PropertyName: PubId + ColumnName: 'pub_id' + StoreType: char(4) + NativeType: '@char' + DataType: AnsiStringFixedLength + SystemType: String + Size: 4 + IsNullable: False + IsPrimaryKey: True + IsForeignKey: True + IsRowVersion: False + IsUnique: False + - PropertyName: Logo + ColumnName: 'logo' + StoreType: image + NativeType: 'image' + DataType: Binary + SystemType: Byte[] + IsNullable: True + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: PrInfo + ColumnName: 'pr_info' + StoreType: text + NativeType: 'text' + DataType: AnsiString + SystemType: String + IsNullable: True + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False +EntityClass: Roysched +ContextProperty: +TableSchema: 'dbo' +TableName: 'roysched' +IsView: False +Properties: + - PropertyName: TitleId + ColumnName: 'title_id' + StoreType: varchar(6) + NativeType: 'varchar' + DataType: AnsiString + SystemType: String + Size: 6 + IsNullable: False + IsPrimaryKey: False + IsForeignKey: True + IsRowVersion: False + IsUnique: False + - PropertyName: Lorange + ColumnName: 'lorange' + StoreType: @int + NativeType: '@int' + DataType: Int32 + SystemType: Int32 + IsNullable: True + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: Hirange + ColumnName: 'hirange' + StoreType: @int + NativeType: '@int' + DataType: Int32 + SystemType: Int32 + IsNullable: True + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: Royalty + ColumnName: 'royalty' + StoreType: @int + NativeType: '@int' + DataType: Int32 + SystemType: Int32 + IsNullable: True + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False +EntityClass: Titles +ContextProperty: +TableSchema: 'dbo' +TableName: 'titles' +IsView: False +Properties: + - PropertyName: TitleId + ColumnName: 'title_id' + StoreType: varchar(6) + NativeType: 'varchar' + DataType: AnsiString + SystemType: String + Size: 6 + IsNullable: False + IsPrimaryKey: True + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: Title + ColumnName: 'title' + StoreType: varchar(80) + NativeType: 'varchar' + DataType: AnsiString + SystemType: String + Size: 80 + IsNullable: False + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: Type + ColumnName: 'type' + StoreType: char(12) + NativeType: '@char' + DataType: AnsiStringFixedLength + SystemType: String + Size: 12 + Default: '('UNDECIDED')' + IsNullable: False + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: PubId + ColumnName: 'pub_id' + StoreType: char(4) + NativeType: '@char' + DataType: AnsiStringFixedLength + SystemType: String + Size: 4 + IsNullable: True + IsPrimaryKey: False + IsForeignKey: True + IsRowVersion: False + IsUnique: False + - PropertyName: Price + ColumnName: 'price' + StoreType: money + NativeType: 'money' + DataType: Currency + SystemType: Decimal + IsNullable: True + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: Advance + ColumnName: 'advance' + StoreType: money + NativeType: 'money' + DataType: Currency + SystemType: Decimal + IsNullable: True + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: Royalty + ColumnName: 'royalty' + StoreType: @int + NativeType: '@int' + DataType: Int32 + SystemType: Int32 + IsNullable: True + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: YtdSales + ColumnName: 'ytd_sales' + StoreType: @int + NativeType: '@int' + DataType: Int32 + SystemType: Int32 + IsNullable: True + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: Notes + ColumnName: 'notes' + StoreType: varchar(200) + NativeType: 'varchar' + DataType: AnsiString + SystemType: String + Size: 200 + IsNullable: True + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: Pubdate + ColumnName: 'pubdate' + StoreType: datetime + NativeType: 'datetime' + DataType: DateTime + SystemType: DateTime + Default: '(getdate())' + IsNullable: False + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False +EntityClass: Sales +ContextProperty: +TableSchema: 'dbo' +TableName: 'sales' +IsView: False +Properties: + - PropertyName: StorId + ColumnName: 'stor_id' + StoreType: char(4) + NativeType: '@char' + DataType: AnsiStringFixedLength + SystemType: String + Size: 4 + IsNullable: False + IsPrimaryKey: True + IsForeignKey: True + IsRowVersion: False + IsUnique: False + - PropertyName: OrdNum + ColumnName: 'ord_num' + StoreType: varchar(20) + NativeType: 'varchar' + DataType: AnsiString + SystemType: String + Size: 20 + IsNullable: False + IsPrimaryKey: True + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: OrdDate + ColumnName: 'ord_date' + StoreType: datetime + NativeType: 'datetime' + DataType: DateTime + SystemType: DateTime + IsNullable: False + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: Qty + ColumnName: 'qty' + StoreType: smallint + NativeType: 'smallint' + DataType: Int16 + SystemType: Int16 + IsNullable: False + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: Payterms + ColumnName: 'payterms' + StoreType: varchar(12) + NativeType: 'varchar' + DataType: AnsiString + SystemType: String + Size: 12 + IsNullable: False + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: TitleId + ColumnName: 'title_id' + StoreType: varchar(6) + NativeType: 'varchar' + DataType: AnsiString + SystemType: String + Size: 6 + IsNullable: False + IsPrimaryKey: True + IsForeignKey: True + IsRowVersion: False + IsUnique: False +EntityClass: Titleauthor +ContextProperty: +TableSchema: 'dbo' +TableName: 'titleauthor' +IsView: False +Properties: + - PropertyName: AuId + ColumnName: 'au_id' + StoreType: varchar(11) + NativeType: 'varchar' + DataType: AnsiString + SystemType: String + Size: 11 + IsNullable: False + IsPrimaryKey: True + IsForeignKey: True + IsRowVersion: False + IsUnique: False + - PropertyName: TitleId + ColumnName: 'title_id' + StoreType: varchar(6) + NativeType: 'varchar' + DataType: AnsiString + SystemType: String + Size: 6 + IsNullable: False + IsPrimaryKey: True + IsForeignKey: True + IsRowVersion: False + IsUnique: False + - PropertyName: AuOrd + ColumnName: 'au_ord' + StoreType: tinyint + NativeType: 'tinyint' + DataType: Byte + SystemType: Byte + IsNullable: True + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: Royaltyper + ColumnName: 'royaltyper' + StoreType: @int + NativeType: '@int' + DataType: Int32 + SystemType: Int32 + IsNullable: True + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False +EntityClass: Titleview +ContextProperty: +TableSchema: 'dbo' +TableName: 'titleview' +IsView: True +Properties: + - PropertyName: Title + ColumnName: 'title' + StoreType: varchar(80) + NativeType: 'varchar' + DataType: AnsiString + SystemType: String + Size: 80 + IsNullable: False + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: AuOrd + ColumnName: 'au_ord' + StoreType: tinyint + NativeType: 'tinyint' + DataType: Byte + SystemType: Byte + IsNullable: True + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: AuLname + ColumnName: 'au_lname' + StoreType: varchar(40) + NativeType: 'varchar' + DataType: AnsiString + SystemType: String + Size: 40 + IsNullable: False + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: Price + ColumnName: 'price' + StoreType: money + NativeType: 'money' + DataType: Currency + SystemType: Decimal + IsNullable: True + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: YtdSales + ColumnName: 'ytd_sales' + StoreType: @int + NativeType: '@int' + DataType: Int32 + SystemType: Int32 + IsNullable: True + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False + - PropertyName: PubId + ColumnName: 'pub_id' + StoreType: char(4) + NativeType: '@char' + DataType: AnsiStringFixedLength + SystemType: String + Size: 4 + IsNullable: True + IsPrimaryKey: False + IsForeignKey: False + IsRowVersion: False + IsUnique: False diff --git a/yaml-entity.csx b/yaml-entity.csx new file mode 100644 index 0000000..5afe8c2 --- /dev/null +++ b/yaml-entity.csx @@ -0,0 +1,65 @@ +public string WriteCode() +{ + CodeBuilder.Clear(); + foreach (var entity in EntityContext.Entities) + { + CodeBuilder.Append("EntityClass: ").Append(entity.EntityClass.ToSafeName()).AppendLine(); + + CodeBuilder.Append("ContextProperty: ").Append(entity.ContextProperty.ToSafeName()).AppendLine(); + + CodeBuilder.Append("TableSchema: '").Append(entity.TableSchema.ToSafeName()).AppendLine("'"); + CodeBuilder.Append("TableName: '").Append(entity.TableName.ToSafeName()).AppendLine("'"); + + + CodeBuilder.Append("IsView: ").Append(entity.IsView.ToString()).AppendLine(); + + CodeBuilder.Append("Properties:").AppendLine(); + using (CodeBuilder.Indent()) + GenerateProperties(entity); + } + + return CodeBuilder.ToString(); +} + +private void GenerateProperties(Entity entity) +{ + foreach (var property in entity.Properties) + { + CodeBuilder.Append("- PropertyName: ").Append(property.PropertyName.ToSafeName()).AppendLine(); + CodeBuilder.Append(" ColumnName: '").Append(property.ColumnName.ToSafeName()).AppendLine("'"); + CodeBuilder.Append(" StoreType: ").Append(property.StoreType.ToSafeName()).AppendLine(); + CodeBuilder.Append(" NativeType: '").Append(property.NativeType.ToSafeName()).AppendLine("'"); + CodeBuilder.Append(" DataType: ").Append(property.DataType.ToString()).AppendLine(); + CodeBuilder.Append(" SystemType: ").Append(property.SystemType.Name).AppendLine(); + + if (property.Size != null) + CodeBuilder.Append(" Size: ").Append(property.Size.ToString()).AppendLine(); + + if (property.Default != null) + CodeBuilder.Append(" Default: '").Append(property.Default.ToSafeName()).AppendLine("'"); + + if (property.ValueGenerated != null) + CodeBuilder.Append(" ValueGenerated: ").Append(property.ValueGenerated.ToString()).AppendLine(); + + if (property.IsNullable != null) + CodeBuilder.Append(" IsNullable: ").Append(property.IsNullable.ToString()).AppendLine(); + + if (property.IsPrimaryKey != null) + CodeBuilder.Append(" IsPrimaryKey: ").Append(property.IsPrimaryKey.ToString()).AppendLine(); + + if (property.IsForeignKey != null) + CodeBuilder.Append(" IsForeignKey: ").Append(property.IsForeignKey.ToString()).AppendLine(); + + if (property.IsReadOnly != null) + CodeBuilder.Append(" IsReadOnly: ").Append(property.IsReadOnly.ToString()).AppendLine(); + + if (property.IsRowVersion != null) + CodeBuilder.Append(" IsRowVersion: ").Append(property.IsRowVersion.ToString()).AppendLine(); + + if (property.IsUnique != null) + CodeBuilder.Append(" IsUnique: ").Append(property.IsUnique.ToString()).AppendLine(); + } +} + +// run script +WriteCode() From 8fbc172272d0f1a0c4771193c4a1049cf2c39827 Mon Sep 17 00:00:00 2001 From: platyscript <244315239+platyscript@users.noreply.github.com> Date: Tue, 13 Jan 2026 10:28:15 +0530 Subject: [PATCH 4/4] commit update --- dab-config.json | 368 ++++++++++++++++++ yaml-entity-context.yaml => dab-config.yaml | 0 sample.yaml | 2 + .../CodeGenerator.cs | 9 +- .../DbApiBuilderEntityGenerator.Core.csproj | 1 + .../Metadata/Generation/Relationship.cs | 1 - .../ModelGenerator.cs | 11 +- .../OptionMapper.cs | 1 + .../Options/GeneratorOptions.cs | 6 + .../Scripts/ScriptTemplateBase.cs | 33 +- .../template/dab-config1.csx | 155 ++++++++ src/DbApiBuilderEntityGenerator/sample.yaml | 2 + template/dab-config.csx | 65 ++++ template/dab-config1.csx | 135 +++++++ 14 files changed, 769 insertions(+), 20 deletions(-) create mode 100644 dab-config.json rename yaml-entity-context.yaml => dab-config.yaml (100%) create mode 100644 src/DbApiBuilderEntityGenerator.Core/template/dab-config1.csx create mode 100644 template/dab-config.csx create mode 100644 template/dab-config1.csx diff --git a/dab-config.json b/dab-config.json new file mode 100644 index 0000000..9660247 --- /dev/null +++ b/dab-config.json @@ -0,0 +1,368 @@ +"entities": { + "Authors": { + "source": { + "object": "dbo.authors", + "type": "table" + }, + "mappings": { + "au_id": "AuId", + "au_lname": "AuLname", + "au_fname": "AuFname", + "phone": "Phone", + "address": "Address", + "city": "City", + "state": "State", + "zip": "Zip", + "contract": "Contract" + }, + "relationships": { + "FK__titleauth__au_id__44FF419A": { + "cardinality": "Many", + "target.entity": "Titleauthor" + } + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + }] + }] + }, + "Discounts": { + "source": { + "object": "dbo.discounts", + "type": "table" + }, + "mappings": { + "discounttype": "Discounttype", + "stor_id": "StorId", + "lowqty": "Lowqty", + "highqty": "Highqty", + "discount": "Discount" + }, + "relationships": { + "FK__discounts__stor___4F7CD00D": { + "cardinality": "Many", + "target.entity": "Stores" + } + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + }] + }] + }, + "Stores": { + "source": { + "object": "dbo.stores", + "type": "table" + }, + "mappings": { + "stor_id": "StorId", + "stor_name": "StorName", + "stor_address": "StorAddress", + "city": "City", + "state": "State", + "zip": "Zip" + }, + "relationships": { + "FK__discounts__stor___4F7CD00D": { + "cardinality": "Many", + "target.entity": "Discounts" + }, + "FK__sales__stor_id__4AB81AF0": { + "cardinality": "Many", + "target.entity": "Sales" + } + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + }] + }] + }, + "Employee": { + "source": { + "object": "dbo.employee", + "type": "table" + }, + "mappings": { + "emp_id": "EmpId", + "fname": "Fname", + "minit": "Minit", + "lname": "Lname", + "job_id": "JobId", + "job_lvl": "JobLvl", + "pub_id": "PubId", + "hire_date": "HireDate" + }, + "relationships": { + "FK__employee__job_id__5BE2A6F2": { + "cardinality": "One", + "target.entity": "Jobs" + }, + "FK__employee__pub_id__5EBF139D": { + "cardinality": "One", + "target.entity": "Publishers" + } + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + }] + }] + }, + "Jobs": { + "source": { + "object": "dbo.jobs", + "type": "table" + }, + "mappings": { + "job_id": "JobId", + "job_desc": "JobDesc", + "min_lvl": "MinLvl", + "max_lvl": "MaxLvl" + }, + "relationships": { + "FK__employee__job_id__5BE2A6F2": { + "cardinality": "Many", + "target.entity": "Employee" + } + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + }] + }] + }, + "Publishers": { + "source": { + "object": "dbo.publishers", + "type": "table" + }, + "mappings": { + "pub_id": "PubId", + "pub_name": "PubName", + "city": "City", + "state": "State", + "country": "Country" + }, + "relationships": { + "FK__employee__pub_id__5EBF139D": { + "cardinality": "Many", + "target.entity": "Employee" + }, + "FK__pub_info__pub_id__571DF1D5": { + "cardinality": "One", + "target.entity": "PubInfo" + }, + "FK__titles__pub_id__412EB0B6": { + "cardinality": "Many", + "target.entity": "Titles" + } + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + }] + }] + }, + "PubInfo": { + "source": { + "object": "dbo.pub_info", + "type": "table" + }, + "mappings": { + "pub_id": "PubId", + "logo": "Logo", + "pr_info": "PrInfo" + }, + "relationships": { + "FK__pub_info__pub_id__571DF1D5": { + "cardinality": "One", + "target.entity": "Publishers" + } + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + }] + }] + }, + "Roysched": { + "source": { + "object": "dbo.roysched", + "type": "table" + }, + "mappings": { + "title_id": "TitleId", + "lorange": "Lorange", + "hirange": "Hirange", + "royalty": "Royalty" + }, + "relationships": { + "FK__roysched__title___4D94879B": { + "cardinality": "One", + "target.entity": "Titles" + } + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + }] + }] + }, + "Titles": { + "source": { + "object": "dbo.titles", + "type": "table" + }, + "mappings": { + "title_id": "TitleId", + "title": "Title", + "type": "Type", + "pub_id": "PubId", + "price": "Price", + "advance": "Advance", + "royalty": "Royalty", + "ytd_sales": "YtdSales", + "notes": "Notes", + "pubdate": "Pubdate" + }, + "relationships": { + "FK__roysched__title___4D94879B": { + "cardinality": "Many", + "target.entity": "Roysched" + }, + "FK__sales__title_id__4BAC3F29": { + "cardinality": "Many", + "target.entity": "Sales" + }, + "FK__titleauth__title__45F365D3": { + "cardinality": "Many", + "target.entity": "Titleauthor" + }, + "FK__titles__pub_id__412EB0B6": { + "cardinality": "Many", + "target.entity": "Publishers" + } + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + }] + }] + }, + "Sales": { + "source": { + "object": "dbo.sales", + "type": "table" + }, + "mappings": { + "stor_id": "StorId", + "ord_num": "OrdNum", + "ord_date": "OrdDate", + "qty": "Qty", + "payterms": "Payterms", + "title_id": "TitleId" + }, + "relationships": { + "FK__sales__stor_id__4AB81AF0": { + "cardinality": "One", + "target.entity": "Stores" + }, + "FK__sales__title_id__4BAC3F29": { + "cardinality": "One", + "target.entity": "Titles" + } + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + }] + }] + }, + "Titleauthor": { + "source": { + "object": "dbo.titleauthor", + "type": "table" + }, + "mappings": { + "au_id": "AuId", + "title_id": "TitleId", + "au_ord": "AuOrd", + "royaltyper": "Royaltyper" + }, + "relationships": { + "FK__titleauth__au_id__44FF419A": { + "cardinality": "One", + "target.entity": "Authors" + }, + "FK__titleauth__title__45F365D3": { + "cardinality": "One", + "target.entity": "Titles" + } + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + }] + }] + }, + "Titleview": { + "source": { + "object": "dbo.titleview", + "type": "view" + }, + "mappings": { + "title": "Title", + "au_ord": "AuOrd", + "au_lname": "AuLname", + "price": "Price", + "ytd_sales": "YtdSales", + "pub_id": "PubId" + }, + "relationships": { + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + }] + }] + }, +} diff --git a/yaml-entity-context.yaml b/dab-config.yaml similarity index 100% rename from yaml-entity-context.yaml rename to dab-config.yaml diff --git a/sample.yaml b/sample.yaml index 73181e6..566246a 100644 --- a/sample.yaml +++ b/sample.yaml @@ -47,3 +47,5 @@ renaming: - ^(sp|tbl|udf|vw)_ properties: - ^{Table.Name}(?=Id|Name) + +#templateFilePath: ./template/dab-config1.csx \ No newline at end of file diff --git a/src/DbApiBuilderEntityGenerator.Core/CodeGenerator.cs b/src/DbApiBuilderEntityGenerator.Core/CodeGenerator.cs index 6c655ed..a6d223d 100644 --- a/src/DbApiBuilderEntityGenerator.Core/CodeGenerator.cs +++ b/src/DbApiBuilderEntityGenerator.Core/CodeGenerator.cs @@ -48,10 +48,10 @@ public bool Generate(GeneratorOptions options) private void GenerateContextScriptTemplates(EntityContext entityContext) { var templateOption = new TemplateOptions(); - templateOption.Directory = Environment.CurrentDirectory; - templateOption.FileName = "yaml-entity-context.yaml"; - templateOption.TemplatePath = Path.Combine(Environment.CurrentDirectory, "yaml-entity.csx"); - if (!VerifyScriptTemplate(templateOption)) + templateOption.Directory = Options.Directory ?? Environment.CurrentDirectory; + templateOption.FileName = Options.OutputFileName ?? "dab-config.json"; + templateOption.TemplatePath = Options.TemplateFilePath; + if (!templateOption.TemplatePath.IsNullOrWhiteSpace() && !VerifyScriptTemplate(templateOption)) return; try @@ -68,7 +68,6 @@ private void GenerateContextScriptTemplates(EntityContext entityContext) private bool VerifyScriptTemplate(TemplateOptions templateOption) { var templatePath = templateOption.TemplatePath; - // var templatePath = Path.Combine(templateOption.Directory, templateOption.FileName); if (File.Exists(templatePath)) return true; diff --git a/src/DbApiBuilderEntityGenerator.Core/DbApiBuilderEntityGenerator.Core.csproj b/src/DbApiBuilderEntityGenerator.Core/DbApiBuilderEntityGenerator.Core.csproj index 553574b..c417bfb 100644 --- a/src/DbApiBuilderEntityGenerator.Core/DbApiBuilderEntityGenerator.Core.csproj +++ b/src/DbApiBuilderEntityGenerator.Core/DbApiBuilderEntityGenerator.Core.csproj @@ -32,6 +32,7 @@ + diff --git a/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/Relationship.cs b/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/Relationship.cs index 8d38443..3f99d09 100644 --- a/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/Relationship.cs +++ b/src/DbApiBuilderEntityGenerator.Core/Metadata/Generation/Relationship.cs @@ -14,7 +14,6 @@ public Relationship() public string? RelationshipName { get; set; } - public Entity Entity { get; set; } = null!; public PropertyCollection Properties { get; set; } diff --git a/src/DbApiBuilderEntityGenerator.Core/ModelGenerator.cs b/src/DbApiBuilderEntityGenerator.Core/ModelGenerator.cs index 530adfc..7c93c92 100644 --- a/src/DbApiBuilderEntityGenerator.Core/ModelGenerator.cs +++ b/src/DbApiBuilderEntityGenerator.Core/ModelGenerator.cs @@ -349,7 +349,8 @@ private void CreateRelationship(EntityContext entityContext, Entity foreignEntit } foreignRelationship.IsMapped = true; foreignRelationship.IsForeignKey = true; - foreignRelationship.Cardinality = foreignMembersRequired ? Cardinality.One : Cardinality.ZeroOrOne; + foreignRelationship.Cardinality = foreignMembersRequired ? Cardinality.One : Cardinality.Many; + // foreignRelationship.Cardinality = foreignMembersRequired ? Cardinality.One : Cardinality.ZeroOrOne; foreignRelationship.PrimaryEntity = primaryEntity; foreignRelationship.PrimaryProperties = [.. primaryMembers]; @@ -384,9 +385,15 @@ private void CreateRelationship(EntityContext entityContext, Entity foreignEntit bool isOneToOne = IsOneToOne(tableKeySchema, foreignRelationship); if (isOneToOne) - primaryRelationship.Cardinality = primaryMembersRequired ? Cardinality.One : Cardinality.ZeroOrOne; + { + // primaryRelationship.Cardinality = primaryMembersRequired ? Cardinality.One : Cardinality.ZeroOrOne; + primaryRelationship.Cardinality = primaryMembersRequired ? Cardinality.One : Cardinality.Many; + + } else + { primaryRelationship.Cardinality = Cardinality.Many; + } string primaryPropertyName = prefix + foreignName; if (!isOneToOne) diff --git a/src/DbApiBuilderEntityGenerator.Core/OptionMapper.cs b/src/DbApiBuilderEntityGenerator.Core/OptionMapper.cs index c3d6f3a..77bff2c 100644 --- a/src/DbApiBuilderEntityGenerator.Core/OptionMapper.cs +++ b/src/DbApiBuilderEntityGenerator.Core/OptionMapper.cs @@ -19,6 +19,7 @@ public static GeneratorOptions Map(GeneratorModel generator) MapDatabaseMatch(options.Exclude, generator.Exclude); options.EntityNaming = generator.EntityNaming; options.RelationshipNaming = generator.RelationshipNaming; + options.TemplateFilePath = generator.TemplateFilePath; MapSelection(options.Renaming, generator.Renaming); options.Variables.ShouldEvaluate = true; diff --git a/src/DbApiBuilderEntityGenerator.Core/Options/GeneratorOptions.cs b/src/DbApiBuilderEntityGenerator.Core/Options/GeneratorOptions.cs index 368f113..4859b7a 100644 --- a/src/DbApiBuilderEntityGenerator.Core/Options/GeneratorOptions.cs +++ b/src/DbApiBuilderEntityGenerator.Core/Options/GeneratorOptions.cs @@ -131,5 +131,11 @@ public string? Directory /// public string? OutputFileName { get; set; } + /// + /// Gets or sets the renaming expressions. + /// + /// + /// The renaming expressions. + /// public string? TemplateFilePath { get; set; } } diff --git a/src/DbApiBuilderEntityGenerator.Core/Scripts/ScriptTemplateBase.cs b/src/DbApiBuilderEntityGenerator.Core/Scripts/ScriptTemplateBase.cs index 4df8961..611ccbc 100644 --- a/src/DbApiBuilderEntityGenerator.Core/Scripts/ScriptTemplateBase.cs +++ b/src/DbApiBuilderEntityGenerator.Core/Scripts/ScriptTemplateBase.cs @@ -31,11 +31,11 @@ protected virtual void WriteCode() { var templatePath = TemplateOptions.TemplatePath; - // if (!File.Exists(templatePath)) - // { - // Logger.LogWarning("Template '{template}' could not be found.", templatePath); - // return; - // } + if (!templatePath.IsNullOrWhiteSpace() && !File.Exists(templatePath)) + { + Logger.LogWarning("Template '{template}' could not be found.", templatePath); + return; + } // save file var directory = TemplateOptions.Directory; @@ -43,7 +43,7 @@ protected virtual void WriteCode() if (directory.IsNullOrEmpty() || fileName.IsNullOrEmpty()) { - Logger.LogWarning("Template '{template}' could not resolve output file.", templatePath); + Logger.LogWarning("Template could not resolve output file."); return; } @@ -60,7 +60,7 @@ protected virtual void WriteCode() if (content.IsNullOrWhiteSpace()) { - Logger.LogDebug("Skipping template '{template}' because it didn't return any text.", templatePath); + Logger.LogDebug("Skipping template because it didn't return any text."); return; } @@ -69,7 +69,7 @@ protected virtual void WriteCode() protected virtual string ExecuteScript() { var templatePath = TemplateOptions.TemplatePath; - if (!File.Exists(templatePath)) + if (!templatePath.IsNullOrWhiteSpace() && !File.Exists(templatePath)) { Logger.LogWarning("Template '{template}' could not be found.", templatePath); return string.Empty; @@ -90,11 +90,20 @@ protected Script LoadScript(string scriptPath) Logger.LogDebug("Loading template script: {script}", scriptPath); - var scriptContent = File.ReadAllText(scriptPath); + string scriptContent = String.Empty; + + if (scriptPath.IsNullOrEmpty()) + { + scriptContent = Assembly.GetExecutingAssembly() + .ReadResourceAsync("DbApiBuilderEntityGenerator.Core.template.dab-config1.csx") + .Result; + + } + else + { + scriptContent = File.ReadAllText(scriptPath); + } - scriptContent = Assembly.GetExecutingAssembly() - .ReadResourceAsync("DbApiBuilderEntityGenerator.Core.template.dab-config.csx") - .Result; var scriptOptions = ScriptOptions.Default .WithReferences( typeof(ScriptVariablesBase).Assembly diff --git a/src/DbApiBuilderEntityGenerator.Core/template/dab-config1.csx b/src/DbApiBuilderEntityGenerator.Core/template/dab-config1.csx new file mode 100644 index 0000000..c7e340b --- /dev/null +++ b/src/DbApiBuilderEntityGenerator.Core/template/dab-config1.csx @@ -0,0 +1,155 @@ +public string WriteCode() +{ + CodeBuilder.Clear(); + // start entities object + CodeBuilder.AppendLine("\"entities\": {"); + foreach (var entity in EntityContext.Entities) + { + using (CodeBuilder.Indent()) + { + // start entity object + CodeBuilder.Append("\"").Append(entity.EntityClass.ToSafeName()).Append("\": {").AppendLine(); + + using (CodeBuilder.Indent()) + { + // start source object + CodeBuilder.Append("\"source\": {").AppendLine(); + CodeBuilder.Append("\"object\": \"").Append(entity.TableSchema.ToSafeName()).Append(".").Append(entity.TableName.ToSafeName()).Append("\",").AppendLine(); + if (entity.IsView) + { + CodeBuilder.Append("\"type\": \"view\"").AppendLine(); + } + else + { + CodeBuilder.Append("\"type\": \"table\"").AppendLine(); + } + + // end source object + CodeBuilder.Append("},").AppendLine(); + } + + using (CodeBuilder.Indent()) + { + // start mappings object + CodeBuilder.Append("\"mappings\": {").AppendLine(); + + var propertyCount = entity.Properties.Count - 1; + int count = 0; + + foreach (var property in entity.Properties) + { + CodeBuilder.Append("\"").Append(property.ColumnName.ToSafeName()).Append("\": "); + if (count == propertyCount) + { + CodeBuilder.Append("\"").Append(property.PropertyName.ToSafeName()).Append("\"").AppendLine(); + } + else + { + CodeBuilder.Append("\"").Append(property.PropertyName.ToSafeName()).Append("\",").AppendLine(); + } + count++; + } + // end mappings object + CodeBuilder.Append("},").AppendLine(); + } + + using (CodeBuilder.Indent()) + { + // start relationships object + CodeBuilder.Append("\"relationships\": {").AppendLine(); + var relCount = 0; + var relationshipCount = entity.Relationships.Count - 1; + using (CodeBuilder.Indent()) + { + foreach (var relationship in entity.Relationships) + { + CodeBuilder.Append("\"").Append(relationship.RelationshipName.ToSafeName()).Append("\": {").AppendLine(); + using (CodeBuilder.Indent()) + { + CodeBuilder.Append("\"cardinality\": \"").Append(relationship.Cardinality.ToString().ToSafeName()).Append("\",").AppendLine(); + CodeBuilder.Append("\"target.entity\": \"").Append(relationship.PrimaryEntity.EntityClass.ToSafeName().ToSafeName()).Append("\"").AppendLine(); + } + CodeBuilder.Append("}"); + if (relCount != relationshipCount) + { + CodeBuilder.Append(","); + } + relCount++; + CodeBuilder.AppendLine(); + } + } + // end relationships object + CodeBuilder.Append("},").AppendLine(); + } + + using (CodeBuilder.Indent()) + { + // start permissions object + CodeBuilder.Append("\"permissions\": [").AppendLine(); + CodeBuilder.Append("{").AppendLine(); + using (CodeBuilder.Indent()) + { + CodeBuilder.Append("\"role\": \"anonymous\",").AppendLine(); + CodeBuilder.Append("\"actions\": [").AppendLine(); + CodeBuilder.Append("{").AppendLine(); + using (CodeBuilder.Indent()) + { + CodeBuilder.Append("\"action\": \"*\"").AppendLine(); + } + CodeBuilder.Append("}]").AppendLine(); + + } + CodeBuilder.Append("}]").AppendLine(); + // end permissions object + } + // end entity object + CodeBuilder.Append("},").AppendLine(); + } + } + // end entities object + CodeBuilder.Append("}").AppendLine(); + return CodeBuilder.ToString(); +} + +private void GenerateProperties(Entity entity) +{ + foreach (var property in entity.Properties) + { + CodeBuilder.Append("- PropertyName: ").Append(property.PropertyName.ToSafeName()).AppendLine(); + CodeBuilder.Append(" ColumnName: '").Append(property.ColumnName.ToSafeName()).AppendLine("'"); + CodeBuilder.Append(" StoreType: ").Append(property.StoreType.ToSafeName()).AppendLine(); + CodeBuilder.Append(" NativeType: '").Append(property.NativeType.ToSafeName()).AppendLine("'"); + CodeBuilder.Append(" DataType: ").Append(property.DataType.ToString()).AppendLine(); + CodeBuilder.Append(" SystemType: ").Append(property.SystemType.Name).AppendLine(); + + if (property.Size != null) + CodeBuilder.Append(" Size: ").Append(property.Size.ToString()).AppendLine(); + + if (property.Default != null) + CodeBuilder.Append(" Default: '").Append(property.Default.ToSafeName()).AppendLine("'"); + + if (property.ValueGenerated != null) + CodeBuilder.Append(" ValueGenerated: ").Append(property.ValueGenerated.ToString()).AppendLine(); + + if (property.IsNullable != null) + CodeBuilder.Append(" IsNullable: ").Append(property.IsNullable.ToString()).AppendLine(); + + if (property.IsPrimaryKey != null) + CodeBuilder.Append(" IsPrimaryKey: ").Append(property.IsPrimaryKey.ToString()).AppendLine(); + + if (property.IsForeignKey != null) + CodeBuilder.Append(" IsForeignKey: ").Append(property.IsForeignKey.ToString()).AppendLine(); + + if (property.IsReadOnly != null) + CodeBuilder.Append(" IsReadOnly: ").Append(property.IsReadOnly.ToString()).AppendLine(); + + if (property.IsRowVersion != null) + CodeBuilder.Append(" IsRowVersion: ").Append(property.IsRowVersion.ToString()).AppendLine(); + + if (property.IsUnique != null) + CodeBuilder.Append(" IsUnique: ").Append(property.IsUnique.ToString()).AppendLine(); + } +} + +// run script +WriteCode() diff --git a/src/DbApiBuilderEntityGenerator/sample.yaml b/src/DbApiBuilderEntityGenerator/sample.yaml index 73181e6..891fa0c 100644 --- a/src/DbApiBuilderEntityGenerator/sample.yaml +++ b/src/DbApiBuilderEntityGenerator/sample.yaml @@ -47,3 +47,5 @@ renaming: - ^(sp|tbl|udf|vw)_ properties: - ^{Table.Name}(?=Id|Name) + +templateFilePath: ./template/dab-config1.csx diff --git a/template/dab-config.csx b/template/dab-config.csx new file mode 100644 index 0000000..5afe8c2 --- /dev/null +++ b/template/dab-config.csx @@ -0,0 +1,65 @@ +public string WriteCode() +{ + CodeBuilder.Clear(); + foreach (var entity in EntityContext.Entities) + { + CodeBuilder.Append("EntityClass: ").Append(entity.EntityClass.ToSafeName()).AppendLine(); + + CodeBuilder.Append("ContextProperty: ").Append(entity.ContextProperty.ToSafeName()).AppendLine(); + + CodeBuilder.Append("TableSchema: '").Append(entity.TableSchema.ToSafeName()).AppendLine("'"); + CodeBuilder.Append("TableName: '").Append(entity.TableName.ToSafeName()).AppendLine("'"); + + + CodeBuilder.Append("IsView: ").Append(entity.IsView.ToString()).AppendLine(); + + CodeBuilder.Append("Properties:").AppendLine(); + using (CodeBuilder.Indent()) + GenerateProperties(entity); + } + + return CodeBuilder.ToString(); +} + +private void GenerateProperties(Entity entity) +{ + foreach (var property in entity.Properties) + { + CodeBuilder.Append("- PropertyName: ").Append(property.PropertyName.ToSafeName()).AppendLine(); + CodeBuilder.Append(" ColumnName: '").Append(property.ColumnName.ToSafeName()).AppendLine("'"); + CodeBuilder.Append(" StoreType: ").Append(property.StoreType.ToSafeName()).AppendLine(); + CodeBuilder.Append(" NativeType: '").Append(property.NativeType.ToSafeName()).AppendLine("'"); + CodeBuilder.Append(" DataType: ").Append(property.DataType.ToString()).AppendLine(); + CodeBuilder.Append(" SystemType: ").Append(property.SystemType.Name).AppendLine(); + + if (property.Size != null) + CodeBuilder.Append(" Size: ").Append(property.Size.ToString()).AppendLine(); + + if (property.Default != null) + CodeBuilder.Append(" Default: '").Append(property.Default.ToSafeName()).AppendLine("'"); + + if (property.ValueGenerated != null) + CodeBuilder.Append(" ValueGenerated: ").Append(property.ValueGenerated.ToString()).AppendLine(); + + if (property.IsNullable != null) + CodeBuilder.Append(" IsNullable: ").Append(property.IsNullable.ToString()).AppendLine(); + + if (property.IsPrimaryKey != null) + CodeBuilder.Append(" IsPrimaryKey: ").Append(property.IsPrimaryKey.ToString()).AppendLine(); + + if (property.IsForeignKey != null) + CodeBuilder.Append(" IsForeignKey: ").Append(property.IsForeignKey.ToString()).AppendLine(); + + if (property.IsReadOnly != null) + CodeBuilder.Append(" IsReadOnly: ").Append(property.IsReadOnly.ToString()).AppendLine(); + + if (property.IsRowVersion != null) + CodeBuilder.Append(" IsRowVersion: ").Append(property.IsRowVersion.ToString()).AppendLine(); + + if (property.IsUnique != null) + CodeBuilder.Append(" IsUnique: ").Append(property.IsUnique.ToString()).AppendLine(); + } +} + +// run script +WriteCode() diff --git a/template/dab-config1.csx b/template/dab-config1.csx new file mode 100644 index 0000000..0ae5c54 --- /dev/null +++ b/template/dab-config1.csx @@ -0,0 +1,135 @@ +public string WriteCode() +{ + CodeBuilder.Clear(); + // start entities object + CodeBuilder.AppendLine("\"entities\": {"); + foreach (var entity in EntityContext.Entities) + { + using (CodeBuilder.Indent()) + { + // start entity object + CodeBuilder.Append("\"").Append(entity.EntityClass.ToSafeName()).Append("\": {").AppendLine(); + + using (CodeBuilder.Indent()) + { + // start source object + CodeBuilder.Append("\"source\": {").AppendLine(); + CodeBuilder.Append("\"object\": \"").Append(entity.TableSchema.ToSafeName()).Append(".").Append(entity.TableName.ToSafeName()).Append("\",").AppendLine(); + if (entity.IsView) + { + CodeBuilder.Append("\"type\": \"view\"").AppendLine(); + } + else + { + CodeBuilder.Append("\"type\": \"table\"").AppendLine(); + } + + // end source object + CodeBuilder.Append("},").AppendLine(); + } + + using (CodeBuilder.Indent()) + { + // start mappings object + CodeBuilder.Append("\"mappings\": {").AppendLine(); + + var propertyCount = entity.Properties.Count - 1; + int count = 0; + + foreach (var property in entity.Properties) + { + CodeBuilder.Append("\"").Append(property.ColumnName.ToSafeName()).Append("\": "); + if (count == propertyCount) + { + CodeBuilder.Append("\"").Append(property.PropertyName.ToSafeName()).Append("\"").AppendLine(); + } + else + { + CodeBuilder.Append("\"").Append(property.PropertyName.ToSafeName()).Append("\",").AppendLine(); + } + count++; + } + // end mappings object + CodeBuilder.Append("}").AppendLine(); + } + + using (CodeBuilder.Indent()) + { + // start relationships object + CodeBuilder.Append("\"relationships\": {").AppendLine(); + foreach (var relationship in entity.Relationships) + { + CodeBuilder.Append("\"").Append(relationship.RelationshipName.ToSafeName()).Append("\":{").AppendLine(); + using (CodeBuilder.Indent()) + { + CodeBuilder.Append("\"cardinality\": \"").Append(relationship.Cardinality.ToString().ToSafeName()).Append("\",").AppendLine(); + CodeBuilder.Append("\"target.entity\": \"").Append(relationship.PrimaryEntity.EntityClass.ToSafeName().ToSafeName()).Append("\",").AppendLine(); + } + // end relationships object + CodeBuilder.Append("}").AppendLine(); + } + } + + using (CodeBuilder.Indent()) + { + // start permissions object + CodeBuilder.Append("\"permissions\": [").AppendLine(); + CodeBuilder.Append("{").AppendLine(); + using (CodeBuilder.Indent()) + { + CodeBuilder.Append("\"role\": \"anonymous\"").AppendLine(); + } + CodeBuilder.Append("}").AppendLine(); + // end permissions object + } + } + // end entity object + CodeBuilder.Append("}").AppendLine(); + } + // end entities object + CodeBuilder.Append("}").AppendLine(); + return CodeBuilder.ToString(); +} + +private void GenerateProperties(Entity entity) +{ + foreach (var property in entity.Properties) + { + CodeBuilder.Append("- PropertyName: ").Append(property.PropertyName.ToSafeName()).AppendLine(); + CodeBuilder.Append(" ColumnName: '").Append(property.ColumnName.ToSafeName()).AppendLine("'"); + CodeBuilder.Append(" StoreType: ").Append(property.StoreType.ToSafeName()).AppendLine(); + CodeBuilder.Append(" NativeType: '").Append(property.NativeType.ToSafeName()).AppendLine("'"); + CodeBuilder.Append(" DataType: ").Append(property.DataType.ToString()).AppendLine(); + CodeBuilder.Append(" SystemType: ").Append(property.SystemType.Name).AppendLine(); + + if (property.Size != null) + CodeBuilder.Append(" Size: ").Append(property.Size.ToString()).AppendLine(); + + if (property.Default != null) + CodeBuilder.Append(" Default: '").Append(property.Default.ToSafeName()).AppendLine("'"); + + if (property.ValueGenerated != null) + CodeBuilder.Append(" ValueGenerated: ").Append(property.ValueGenerated.ToString()).AppendLine(); + + if (property.IsNullable != null) + CodeBuilder.Append(" IsNullable: ").Append(property.IsNullable.ToString()).AppendLine(); + + if (property.IsPrimaryKey != null) + CodeBuilder.Append(" IsPrimaryKey: ").Append(property.IsPrimaryKey.ToString()).AppendLine(); + + if (property.IsForeignKey != null) + CodeBuilder.Append(" IsForeignKey: ").Append(property.IsForeignKey.ToString()).AppendLine(); + + if (property.IsReadOnly != null) + CodeBuilder.Append(" IsReadOnly: ").Append(property.IsReadOnly.ToString()).AppendLine(); + + if (property.IsRowVersion != null) + CodeBuilder.Append(" IsRowVersion: ").Append(property.IsRowVersion.ToString()).AppendLine(); + + if (property.IsUnique != null) + CodeBuilder.Append(" IsUnique: ").Append(property.IsUnique.ToString()).AppendLine(); + } +} + +// run script +WriteCode()