From a1581296385adb0c51287f5c5f29f0ad87f1b8b3 Mon Sep 17 00:00:00 2001 From: Pop Catalin Date: Fri, 15 May 2020 12:59:58 +0300 Subject: [PATCH] Add Geco project Move Project out of .Tools folder Update solution to use Geco.Core submodule Changed Default generate directories Update to .Net Core 3.1 --- .gitattributes | 63 +++ .gitignore | 164 +----- CODE_OF_CONDUCT.md | 46 ++ Geco/Common/BaseGenerator.cs | 250 +++++++++ Geco/Common/BaseGeneratorWithMetadata.cs | 50 ++ Geco/Common/IRunnable.cs | 43 ++ Geco/Common/Inflector/HumanizerInflector.cs | 67 +++ Geco/Common/Inflector/IInflector.cs | 172 ++++++ .../MetadataCollectionExtensions.cs | 21 + .../MetadataProviders/MetadataProviderBase.cs | 213 ++++++++ Geco/Common/MetadataProviders/QueryUtil.cs | 78 +++ .../SqlServer/SqlServerMetadataProvider.cs | 163 ++++++ Geco/Common/OptionsAttribute.cs | 13 + Geco/Common/SimpleMetadata/Column.cs | 59 ++ Geco/Common/SimpleMetadata/DataBaseIndex.cs | 36 ++ .../Common/SimpleMetadata/DatabaseMetadata.cs | 34 ++ Geco/Common/SimpleMetadata/ForeignKey.cs | 53 ++ Geco/Common/SimpleMetadata/IMetadataItem.cs | 75 +++ .../SimpleMetadata/IMetadataProvider.cs | 17 + .../SimpleMetadata/MetadataCollection.cs | 110 ++++ .../SimpleMetadata/MetadataDictionary.cs | 82 +++ Geco/Common/SimpleMetadata/Schema.cs | 40 ++ Geco/Common/SimpleMetadata/Table.cs | 59 ++ Geco/Common/SimpleMetadata/Trigger.cs | 19 + .../Util/OrderedInterceptableDictionary.cs | 136 +++++ Geco/Common/Util/ColorConsole.cs | 91 ++++ Geco/Common/Util/EnumerableExtensions.cs | 158 ++++++ Geco/Config/RootConfig.cs | 9 + Geco/Config/Task.cs | 18 + Geco/Database/DatabaseCleaner.cs | 74 +++ Geco/Database/DatabaseCleanerOptions.cs | 7 + ...ntityFrameworkCoreReverseModelGenerator.cs | 505 ++++++++++++++++++ ...ameworkCoreReverseModelGeneratorOptions.cs | 23 + Geco/Database/SeedDataGenerator.cs | 233 ++++++++ Geco/Database/SeedDataGeneratorOptions.cs | 15 + Geco/Database/SeedScriptRunner.cs | 102 ++++ Geco/Database/SeedScriptRunnerOptions.cs | 11 + Geco/Geco.Targets | 12 + Geco/Geco.csproj | 56 ++ LICENSE => Geco/LICENSE.txt | 4 +- Geco/Logo.png | Bin 0 -> 80015 bytes Geco/Program.cs | 288 ++++++++++ Geco/Properties/launchSettings.json | 7 + Geco/Util/Util.cs | 30 ++ Geco/appsettings.json | 63 +++ LICENSE.md | 201 +++++++ README.md | 219 +++++++- 47 files changed, 4050 insertions(+), 139 deletions(-) create mode 100644 .gitattributes create mode 100644 CODE_OF_CONDUCT.md create mode 100644 Geco/Common/BaseGenerator.cs create mode 100644 Geco/Common/BaseGeneratorWithMetadata.cs create mode 100644 Geco/Common/IRunnable.cs create mode 100644 Geco/Common/Inflector/HumanizerInflector.cs create mode 100644 Geco/Common/Inflector/IInflector.cs create mode 100644 Geco/Common/MetadataProviders/MetadataCollectionExtensions.cs create mode 100644 Geco/Common/MetadataProviders/MetadataProviderBase.cs create mode 100644 Geco/Common/MetadataProviders/QueryUtil.cs create mode 100644 Geco/Common/MetadataProviders/SqlServer/SqlServerMetadataProvider.cs create mode 100644 Geco/Common/OptionsAttribute.cs create mode 100644 Geco/Common/SimpleMetadata/Column.cs create mode 100644 Geco/Common/SimpleMetadata/DataBaseIndex.cs create mode 100644 Geco/Common/SimpleMetadata/DatabaseMetadata.cs create mode 100644 Geco/Common/SimpleMetadata/ForeignKey.cs create mode 100644 Geco/Common/SimpleMetadata/IMetadataItem.cs create mode 100644 Geco/Common/SimpleMetadata/IMetadataProvider.cs create mode 100644 Geco/Common/SimpleMetadata/MetadataCollection.cs create mode 100644 Geco/Common/SimpleMetadata/MetadataDictionary.cs create mode 100644 Geco/Common/SimpleMetadata/Schema.cs create mode 100644 Geco/Common/SimpleMetadata/Table.cs create mode 100644 Geco/Common/SimpleMetadata/Trigger.cs create mode 100644 Geco/Common/SimpleMetadata/Util/OrderedInterceptableDictionary.cs create mode 100644 Geco/Common/Util/ColorConsole.cs create mode 100644 Geco/Common/Util/EnumerableExtensions.cs create mode 100644 Geco/Config/RootConfig.cs create mode 100644 Geco/Config/Task.cs create mode 100644 Geco/Database/DatabaseCleaner.cs create mode 100644 Geco/Database/DatabaseCleanerOptions.cs create mode 100644 Geco/Database/EntityFrameworkCoreReverseModelGenerator.cs create mode 100644 Geco/Database/EntityFrameworkCoreReverseModelGeneratorOptions.cs create mode 100644 Geco/Database/SeedDataGenerator.cs create mode 100644 Geco/Database/SeedDataGeneratorOptions.cs create mode 100644 Geco/Database/SeedScriptRunner.cs create mode 100644 Geco/Database/SeedScriptRunnerOptions.cs create mode 100644 Geco/Geco.Targets create mode 100644 Geco/Geco.csproj rename LICENSE => Geco/LICENSE.txt (99%) create mode 100644 Geco/Logo.png create mode 100644 Geco/Program.cs create mode 100644 Geco/Properties/launchSettings.json create mode 100644 Geco/Util/Util.cs create mode 100644 Geco/appsettings.json create mode 100644 LICENSE.md diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore index dfcfd56..c40ba0a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,7 @@ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files -*.rsuser *.suo *.user *.userosscache @@ -13,68 +10,50 @@ # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs -# Mono auto generated files -mono_crash.* - # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ -x64/ -x86/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ +[Xx]64/ +[Xx]86/ +[Bb]uild/ bld/ [Bb]in/ [Oo]bj/ -[Ll]og/ -[Ll]ogs/ +[Gg]enerated/ +[Gg]enerated[Mm]odel/ -# Visual Studio 2015/2017 cache/options directory +# Visual Studio 2015 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ -# Visual Studio 2017 auto generated files -Generated\ Files/ - # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* -# NUnit +# NUNIT *.VisualState.xml TestResult.xml -nunit-*.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core +# DNX project.lock.json -project.fragment.lock.json artifacts/ -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio *_i.c *_p.c -*_h.h +*_i.h *.ilk *.meta *.obj -*.iobj *.pch *.pdb -*.ipdb *.pgc *.pgd *.rsp @@ -84,7 +63,6 @@ StyleCopReport.xml *.tlh *.tmp *.tmp_proj -*_wpftmp.csproj *.log *.vspscc *.vssscc @@ -105,7 +83,6 @@ ipch/ *.sdf *.cachefile *.VC.db -*.VC.VC.opendb # Visual Studio profiler *.psess @@ -113,9 +90,6 @@ ipch/ *.vspx *.sap -# Visual Studio Trace Files -*.e2e - # TFS 2012 Local Workspace $tf/ @@ -127,20 +101,15 @@ _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user +# JustCode is a .NET coding add-in +.JustCode + # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Visual Studio code coverage results -*.coverage -*.coveragexml - # NCrunch _NCrunch_* .*crunch*.local.xml @@ -172,27 +141,22 @@ publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ +# TODO: Un-comment the next line if you do not want to checkin +# your web deploy settings because they may include unencrypted +# passwords +#*.pubxml +*.publishproj # NuGet Packages *.nupkg -# NuGet Symbol Packages -*.snupkg # The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* +**/packages/* # except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ +!**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files *.nuget.props *.nuget.targets @@ -204,40 +168,28 @@ csx/ ecf/ rcf/ -# Windows Store app package directories and files +# Windows Store app package directory AppPackages/ BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx -*.appxbundle -*.appxupload # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache -!?*.[Cc]ache/ +!*.[Cc]ache/ # Others ClientBin/ +[Ss]tyle[Cc]op.* ~$* *~ *.dbmdl *.dbproj.schemaview -*.jfm *.pfx *.publishsettings +node_modules/ orleans.codegen.cs -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - # RIA/Silverlight projects Generated_Code/ @@ -248,22 +200,15 @@ _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak # SQL Server files *.mdf *.ldf -*.ndf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings -*.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl # Microsoft Fakes FakesAssemblies/ @@ -273,7 +218,6 @@ FakesAssemblies/ # Node.js Tools for Visual Studio .ntvs_analysis.dat -node_modules/ # Visual Studio 6 build log *.plg @@ -281,9 +225,6 @@ node_modules/ # Visual Studio 6 workspace options file *.opt -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts @@ -292,59 +233,12 @@ node_modules/ **/*.Server/ModelManifest.xml _Pvt_Extensions +# LightSwitch generated files +GeneratedArtifacts/ +ModelManifest.xml + # Paket dependency manager .paket/paket.exe -paket-files/ # FAKE - F# Make .fake/ - -# CodeRush personal settings -.cr/personal - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ - -# Local History for Visual Studio -.localhistory/ - -# BeatPulse healthcheck temp database -healthchecksdb - -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ - -# Ionide (cross platform F# VS Code tools) working folder -.ionide/ diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..9c7f312 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at info@iquarc.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/Geco/Common/BaseGenerator.cs b/Geco/Common/BaseGenerator.cs new file mode 100644 index 0000000..c0ab658 --- /dev/null +++ b/Geco/Common/BaseGenerator.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Geco.Common +{ + public abstract class BaseGenerator : IOutputRunnable, IRunableConfirmation + { + private const string IndentString = " "; + + private TextWriter _tw; + private int _indent; + private bool initialized; + private readonly HashSet filesToDelete = new HashSet(); + private bool commaNewLine; + + protected BaseGenerator(IInflector inf) + { + Inf = inf; + } + + protected IInflector Inf { get; } + + public bool OutputToConsole { get; set; } + protected abstract void Generate(); + + public void Run() + { + DetermineFilesToClean(); + Generate(); + CleanFiles(); + } + + private void CleanFiles() + { + foreach (var filePath in filesToDelete) + { + File.Delete(filePath); + } + } + + public virtual bool GetUserConfirmation() + { + if (string.IsNullOrEmpty(CleanFilesPattern)) + return true; + ColorConsole.Write($"Clean all files with pattern [{(CleanFilesPattern, ConsoleColor.Yellow)}] in the target folder [{(Path.GetFullPath(BaseOutputPath), ConsoleColor.Yellow)}] (y/n)?", ConsoleColor.White); + return string.Equals(Console.ReadLine(), "y", StringComparison.OrdinalIgnoreCase); + } + + private void DetermineFilesToClean() + { + if (!String.IsNullOrWhiteSpace(CleanFilesPattern) && Directory.Exists(BaseOutputPath)) + { + foreach (var file in Directory.EnumerateFiles(BaseOutputPath, CleanFilesPattern, SearchOption.TopDirectoryOnly)) + { + filesToDelete.Add(file); + } + } + } + + protected IDisposable BeginFile(string file, bool option = true) + { + if (!option) + return new DisposableAction(null); + + initialized = false; + var fileName = Path.Combine(BaseOutputPath, file); + EnsurePath(fileName); + filesToDelete.Remove(fileName); + _tw = File.CreateText(fileName); + return _tw; + } + + private void EnsurePath(string fileName) + { + var folders = Path.GetDirectoryName(fileName); + if (!String.IsNullOrEmpty(folders)) + Directory.CreateDirectory(folders); + } + + + /// + /// Write semicolon ; on the previous line + /// + protected void SemiColon() + { + _tw.Write(";"); + if (OutputToConsole) Console.Write(";"); + } + + /// + /// Write comma , on the previous line + /// + protected void Comma() + { + _tw.Write(","); + if (OutputToConsole) Console.Write(","); + } + + /// + /// Write comma , on the previous line if a new line is written + /// + protected void CommaIfNewLine() + { + this.commaNewLine = true; + } + + /// + /// Stops write comma , on the previous line if a line is written + /// + protected void NoCommaIfNewLine() + { + this.commaNewLine = false; + } + + /// + /// Write line and increase indent + /// + /// The text to write + /// boolean parameter to indicate if the text should be written or not + protected void WI(string text = "", bool write = true) + { + + W(text, write); + if (write) + Indent(); + } + + /// + /// Increase indent and write line + /// + /// The text to write + /// boolean parameter to indicate if the text should be written or not + protected void IW(string text = "", bool write = true) + { + if (write) + Indent(); + W(text, write); + } + + /// + /// Decrease indent and write line + /// + /// The text to write + /// boolean parameter to indicate if the text should be written or not + protected void DW(string text = "", bool write = true) + { + if (write) + Dedent(); + W(text, write); + } + + /// + /// Write line and decrease indent + /// + /// The text to write + /// boolean parameter to indicate if the text should be written or not + protected void WD(string text = "", bool write = true) + { + W(text, write); + if (write) + Dedent(); + } + + /// + /// Write line with current indent + /// + /// The text to write + /// boolean parameter to indicate if the text should be written or not + protected void W(string text = "", bool write = true) + { + if (!write) + return; + if (initialized) + { + if (commaNewLine) + { + _tw.Write(","); + if (OutputToConsole) Console.Write(","); + } + _tw.WriteLine(); + if (OutputToConsole) Console.WriteLine(); + } + else + initialized = true; + + if (string.IsNullOrWhiteSpace(text)) + return; + + for (int i = 0; i < _indent; i++) + { + _tw.Write(IndentString); + if (OutputToConsole) Console.Write(IndentString); + } + _tw.Write(text); + if (OutputToConsole) Console.Write(text); + } + + /// + /// Increase indent + /// + protected void Indent() + { + _indent++; + } + + /// + /// Decrease indent + /// + protected void Dedent() + { + _indent--; + } + + protected string CommaJoin(IEnumerable values) + { + return string.Join(", ", values); + } + + protected string CommaJoin(IEnumerable values, Func selector) + { + return string.Join(", ", values.Select(selector)); + } + + public string BaseOutputPath { get; set; } + public string CleanFilesPattern { get; set; } + public bool Interactive { get; set; } + + protected IDisposable OnBlockEnd(Action action = null, bool write = true) + { + return new DisposableAction(write ? action : null); + } + + private class DisposableAction : IDisposable + { + private readonly Action action; + + public DisposableAction(Action action) + { + this.action = action; + } + + public void Dispose() + { + action?.Invoke(); + } + } + } +} \ No newline at end of file diff --git a/Geco/Common/BaseGeneratorWithMetadata.cs b/Geco/Common/BaseGeneratorWithMetadata.cs new file mode 100644 index 0000000..aaff337 --- /dev/null +++ b/Geco/Common/BaseGeneratorWithMetadata.cs @@ -0,0 +1,50 @@ +using System; +using Geco.Common.SimpleMetadata; + +namespace Geco.Common +{ + public abstract class BaseGeneratorWithMetadata : BaseGenerator + { + protected readonly string ConnectionName; + public DatabaseMetadata Db => Provider.GetMetadata(ConnectionName); + public IMetadataProvider Provider { get; } + + protected BaseGeneratorWithMetadata(IMetadataProvider provider, IInflector inf, string connectionName) + : base(inf) + { + this.ConnectionName = connectionName; + Provider = provider; + } + + protected void ReloadMetadata() + { + Provider.Reload(); + this.Db.Freeze(); + } + + protected virtual void OnMetadataLoaded(DatabaseMetadata db) + { + + } + + protected string GetCharpTypeName(Type type) + { + if (type == typeof(bool)) return "bool"; + if (type == typeof(byte)) return "byte"; + if (type == typeof(sbyte)) return "sbyte"; + if (type == typeof(char)) return "char"; + if (type == typeof(decimal)) return "decimal"; + if (type == typeof(double)) return "double"; + if (type == typeof(float)) return "float"; + if (type == typeof(int)) return "int"; + if (type == typeof(uint)) return "uint"; + if (type == typeof(long)) return "long"; + if (type == typeof(ulong)) return "ulong"; + if (type == typeof(object)) return "object"; + if (type == typeof(short)) return "short"; + if (type == typeof(ushort)) return "ushort"; + if (type == typeof(string)) return "string"; + return type.Name; + } + } +} \ No newline at end of file diff --git a/Geco/Common/IRunnable.cs b/Geco/Common/IRunnable.cs new file mode 100644 index 0000000..6828b7a --- /dev/null +++ b/Geco/Common/IRunnable.cs @@ -0,0 +1,43 @@ +namespace Geco.Common +{ + /// + /// Represents a runable task, for code generation or purposes or others + /// + public interface IRunnable + { + /// + /// Invoked when this task is executed + /// + void Run(); + } + + /// + /// Represents a runable task that outputs files to a location + /// + public interface IOutputRunnable : IRunnable + { + + /// + /// true if the task output should be mirrored to the console. false otherwise + /// + bool OutputToConsole { get; set; } + /// + /// Base path which will be combined with the file names of the files output by this task. + /// + string BaseOutputPath { get; set; } + /// + /// A wild card base pattern for deleting the files from the prior to generation. + /// + string CleanFilesPattern { get; set; } + /// + /// true if the Geco is running in interactive mode and the task may ask for additional user input. false otherwise + /// + bool Interactive { get; set; } + } + + + public interface IRunableConfirmation + { + bool GetUserConfirmation(); + } +} \ No newline at end of file diff --git a/Geco/Common/Inflector/HumanizerInflector.cs b/Geco/Common/Inflector/HumanizerInflector.cs new file mode 100644 index 0000000..b2bbf0c --- /dev/null +++ b/Geco/Common/Inflector/HumanizerInflector.cs @@ -0,0 +1,67 @@ +using Humanizer; + +namespace Geco.Common +{ + public class HumanizerInflector : IInflector + { + public string Pluralise(string word) + { + return word.Pluralize(false); + } + + public string Singularise(string word) + { + return word.Singularize(false); + } + + public string Titleise(string word) + { + return word.Titleize(); + } + + public string Humanise(string lowercaseAndUnderscoredWord) + { + return lowercaseAndUnderscoredWord.Humanize(); + } + + public string Pascalise(string lowercaseAndUnderscoredWord) + { + return lowercaseAndUnderscoredWord.Pascalize().Replace(" ", ""); + } + + public string Camelise(string lowercaseAndUnderscoredWord) + { + return lowercaseAndUnderscoredWord.Camelize().Replace(" ", ""); + } + + public string Underscore(string pascalCasedWord) + { + return pascalCasedWord.Underscore().Replace(" ", ""); + } + + public string Capitalise(string word) + { + return word.Substring(0, 1).ToUpper() + word.Substring(1).ToLower(); + } + + public string Uncapitalise(string word) + { + return word.Substring(0, 1).ToLower() + word.Substring(1); + } + + public string Ordinalise(string number) + { + return number.Ordinalize(); + } + + public string Ordinalise(int number) + { + return number.Ordinalize(); + } + + public string Dasherise(string underscoredWord) + { + return underscoredWord.Dasherize(); + } + } +} \ No newline at end of file diff --git a/Geco/Common/Inflector/IInflector.cs b/Geco/Common/Inflector/IInflector.cs new file mode 100644 index 0000000..323b097 --- /dev/null +++ b/Geco/Common/Inflector/IInflector.cs @@ -0,0 +1,172 @@ +namespace Geco.Common +{ + /// + /// Specifies the inflection contract. + /// + public interface IInflector + { + /// + /// Pluralises a word. + /// + /// + /// inflect.Pluralise("search").ShouldBe("searches"); + /// inflect.Pluralise("stack").ShouldBe("stacks"); + /// inflect.Pluralise("fish").ShouldBe("fish"); + /// + /// The word to pluralise. + /// The pluralised word. + string Pluralise(string word); + + /// + /// Singularises a word. + /// + /// + /// inflect.Singularise("searches").ShouldBe("search"); + /// inflect.Singularise("stacks").ShouldBe("stack"); + /// inflect.Singularise("fish").ShouldBe("fish"); + /// + /// The word to signularise. + /// The signularised word. + string Singularise(string word); + + /// + /// Titleises the word. (title => Title, the_brown_fox => TheBrownFox) + /// + /// + /// inflect.Titleise("some title").ShouldBe("Some Title"); + /// inflect.Titleise("some-title").ShouldBe("Some Title"); + /// inflect.Titleise("sometitle").ShouldBe("Sometitle"); + /// inflect.Titleise("some_title:_the_beginning").ShouldBe("Some Title: The Beginning"); + /// + /// The word to titleise. + /// The titleised word. + string Titleise(string word); + + /// + /// Humanizes the word. + /// + /// + /// inflect.Humanise("some_title").ShouldBe("Some title"); + /// inflect.Humanise("some-title").ShouldBe("Some-title"); + /// inflect.Humanise("Some_title").ShouldBe("Some title"); + /// inflect.Humanise("someTitle").ShouldBe("Sometitle"); + /// inflect.Humanise("someTitle_Another").ShouldBe("Sometitle another"); + /// + /// The word to humanise. + /// The humanized word. + string Humanise(string lowercaseAndUnderscoredWord); + + /// + /// Pascalises the word. + /// + /// + /// inflect.Pascalise("customer").ShouldBe("Customer"); + /// inflect.Pascalise("customer_name").ShouldBe("CustomerName"); + /// inflect.Pascalise("customer name").ShouldBe("CustomerName"); + /// + /// The word to pascalise. + /// The pascalised word. + string Pascalise(string lowercaseAndUnderscoredWord); + + /// + /// Camelises the word. + /// + /// + /// inflect.Camelise("Customer").ShouldBe("customer"); + /// inflect.Camelise("customer_name").ShouldBe("customerName"); + /// inflect.Camelise("customer_first_name").ShouldBe("customerFirstName"); + /// inflect.Camelise("customer name").ShouldBe("customer name"); + /// + /// The word to camelise. + /// The camelised word. + string Camelise(string lowercaseAndUnderscoredWord); + + /// + /// Underscores the word. + /// + /// + /// inflect.Underscore("SomeTitle").ShouldBe("some_title"); + /// inflect.Underscore("someTitle").ShouldBe("some_title"); + /// inflect.Underscore("some title that will be underscored").ShouldBe("some_title_that_will_be_underscored"); + /// inflect.Underscore("SomeTitleThatWillBeUnderscored").ShouldBe("some_title_that_will_be_underscored"); + /// + /// The word to underscore. + /// The underscored word. + string Underscore(string pascalCasedWord); + + /// + /// Capitalises the word. + /// + /// + /// inflect.Capitalise("some title").ShouldBe("Some title"); + /// inflect.Capitalise("some Title").ShouldBe("Some title"); + /// inflect.Capitalise("SOMETITLE").ShouldBe("Sometitle"); + /// inflect.Capitalise("someTitle").ShouldBe("Sometitle"); + /// inflect.Capitalise("some title goes here").ShouldBe("Some title goes here"); + /// + /// The word to capitalise. + /// The capitalised word. + string Capitalise(string word); + + /// + /// Uncapitalises the word. + /// + /// + /// inflect.Uncapitalise("Some title").ShouldBe("some title"); + /// inflect.Uncapitalise("Some Title").ShouldBe("some Title"); + /// inflect.Uncapitalise("SOMETITLE").ShouldBe("sOMETITLE"); + /// inflect.Uncapitalise("someTitle").ShouldBe("someTitle"); + /// inflect.Uncapitalise("Some title goes here").ShouldBe("some title goes here"); + /// + /// The word to uncapitalise. + /// The uncapitalised word. + string Uncapitalise(string word); + + /// + /// Ordinalises the number. + /// + /// + /// inflect.Ordinalise(0).ShouldBe("0th"); + /// inflect.Ordinalise(1).ShouldBe("1st"); + /// inflect.Ordinalise(2).ShouldBe("2nd"); + /// inflect.Ordinalise(3).ShouldBe("3rd"); + /// inflect.Ordinalise(101).ShouldBe("101st"); + /// inflect.Ordinalise(104).ShouldBe("104th"); + /// inflect.Ordinalise(1000).ShouldBe("1000th"); + /// inflect.Ordinalise(1001).ShouldBe("1001st"); + /// + /// The number to ordinalise. + /// The ordinalised number. + string Ordinalise(string number); + + /// + /// Ordinalises the number. + /// + /// + /// inflect.Ordinalise("0").ShouldBe("0th"); + /// inflect.Ordinalise("1").ShouldBe("1st"); + /// inflect.Ordinalise("2").ShouldBe("2nd"); + /// inflect.Ordinalise("3").ShouldBe("3rd"); + /// inflect.Ordinalise("100").ShouldBe("100th"); + /// inflect.Ordinalise("101").ShouldBe("101st"); + /// inflect.Ordinalise("1000").ShouldBe("1000th"); + /// inflect.Ordinalise("1001").ShouldBe("1001st"); + /// + /// The number to ordinalise. + /// The ordinalised number. + string Ordinalise(int number); + + /// + /// Dasherises the word. + /// + /// + /// inflect.Dasherise("some_title").ShouldBe("some-title"); + /// inflect.Dasherise("some-title").ShouldBe("some-title"); + /// inflect.Dasherise("some_title_goes_here").ShouldBe("some-title-goes-here"); + /// inflect.Dasherise("some_title and_another").ShouldBe("some-title and-another"); + /// + /// The word to dasherise. + /// The dasherised word. + string Dasherise(string underscoredWord); + } +} \ No newline at end of file diff --git a/Geco/Common/MetadataProviders/MetadataCollectionExtensions.cs b/Geco/Common/MetadataProviders/MetadataCollectionExtensions.cs new file mode 100644 index 0000000..6225e48 --- /dev/null +++ b/Geco/Common/MetadataProviders/MetadataCollectionExtensions.cs @@ -0,0 +1,21 @@ +using System; +using Geco.Common.SimpleMetadata; + +namespace Geco.Common.MetadataProviders +{ + public static class MetadataCollectionExtensions + { + public static T GetOrAdd(this MetadataCollection collection, string key, Func factory) + where T : class, IMetadataItem + { + if (!collection.ContainsKey(key)) + { + var item = factory(); + var writable = collection.GetWritable(); + writable[key] = item; + return item; + } + return collection[key]; + } + } +} \ No newline at end of file diff --git a/Geco/Common/MetadataProviders/MetadataProviderBase.cs b/Geco/Common/MetadataProviders/MetadataProviderBase.cs new file mode 100644 index 0000000..1b9c501 --- /dev/null +++ b/Geco/Common/MetadataProviders/MetadataProviderBase.cs @@ -0,0 +1,213 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Data.Common; +using System.Diagnostics; +using System.Linq; +using Geco.Common.SimpleMetadata; + +namespace Geco.Common.MetadataProviders +{ + public abstract class MetadataProviderBase : IMetadataProvider + { + private DatabaseMetadata metadata = null; + + /// + /// Loads metadata from a database + /// + /// + /// + public DatabaseMetadata LoadMetadata(string connectionName) + { + var sw = new Stopwatch(); + sw.Start(); + this.ConnectionName = connectionName; + DatabaseMetadata db; + using (Connection = CreateConnection()) + { + Connection.Open(); + db = new DatabaseMetadata(GetName(), GetClrTypeMappings()); + foreach (var tableInfo in LoadTables()) + { + var schema = db.Schemas.GetOrAdd(tableInfo.SchemaName, () => new Schema(tableInfo.SchemaName)); + schema.Tables.Add(new Table(tableInfo.Name, schema).WithMetadata(tableInfo)); + } + + foreach (var columnInfo in LoadColumns()) + { + var schema = db.Schemas[columnInfo.SchemaName]; + var table = schema.Tables[columnInfo.TableName]; + var index = table.Columns.Count; + table.Columns.Add(new Column(columnInfo.Name, table, index, columnInfo.DataType, columnInfo.Precision, columnInfo.Scale, columnInfo.MaxLength, + columnInfo.IsNullable, columnInfo.IsKey, columnInfo.IsIdentity, columnInfo.IsRowGuidCol, columnInfo.IsComputed, columnInfo.DefaultValue).WithMetadata(columnInfo)); + } + + foreach (var foreignKeyInfo in LoadForeignKeys()) + { + var parentTable = db.Schemas[foreignKeyInfo.ParentTableSchema].Tables[foreignKeyInfo.ParentTable]; + var targetTable = db.Schemas[foreignKeyInfo.ReferencedTableSchema] + .Tables[foreignKeyInfo.ReferencedTable]; + + var fk = parentTable.ForeignKeys.GetOrAdd(foreignKeyInfo.Name, + () => new ForeignKey(foreignKeyInfo.Name, parentTable, targetTable, foreignKeyInfo.UpdateAction, foreignKeyInfo.DeleteAction).WithMetadata(foreignKeyInfo)); + + var parentColumn = parentTable.Columns[foreignKeyInfo.ParentColumn]; + fk.FromColumns.Add(parentColumn); + parentColumn.ForeignKey = fk; + + var targetColumn = targetTable.Columns[foreignKeyInfo.ReferencedColumn]; + fk.ToColumns.Add(targetColumn); + + targetTable.IncomingForeignKeys.GetOrAdd(foreignKeyInfo.Name, () => fk); + } + + foreach (var triggerInfo in LoadTriggerInfo()) + { + var schema = db.Schemas[triggerInfo.ParentTableSchema]; + var table = schema.Tables[triggerInfo.ParentTable]; + + table.Triggers.GetOrAdd(triggerInfo.Name, () => new Trigger(triggerInfo.Name, table).WithMetadata(triggerInfo)); + } + + foreach (var indexInfo in LoadIndexInfo()) + { + var schema = db.Schemas[indexInfo.SchemaName]; + var table = schema.Tables[indexInfo.TableName]; + var column = table.Columns[indexInfo.ColumnName]; + + var index = table.Indexes.GetOrAdd(indexInfo.IndexName, () => new DataBaseIndex(indexInfo.IndexName, table, indexInfo.IsUnique, indexInfo.IsClustered).WithMetadata(indexInfo)); + if (indexInfo.IsIncluded) + index.IncludedColumns.Add(column); + else + index.Columns.Add(column); + } + } + sw.Stop(); + ColorConsole.WriteLine(("Database Metadata loaded in ", ConsoleColor.DarkYellow), ($"{sw.ElapsedMilliseconds} ms", ConsoleColor.Green)); + return db; + } + protected string ConnectionName { get; private set; } + private DbConnection Connection { get; set; } + + public DatabaseMetadata GetMetadata(string connectionName) + { + return metadata ??= LoadMetadata(connectionName); + } + + public void Reload() + { + metadata = null; + } + + protected abstract string GetName(); + protected abstract IEnumerable LoadTables(); + protected abstract IEnumerable LoadColumns(); + protected abstract IEnumerable LoadForeignKeys(); + protected abstract IEnumerable LoadTriggerInfo(); + protected abstract IEnumerable LoadIndexInfo(); + + + protected abstract DbConnection CreateConnection(); + protected abstract DbCommand CreateCommand(DbConnection cnn, string commandText); + protected abstract IReadOnlyDictionary GetClrTypeMappings(); + + protected virtual IEnumerable Query(string query) + where T : IMetadataItem, new() + { + using (var cmd = CreateCommand(Connection, query)) + { + using (var rdr = cmd.ExecuteReader()) + { + foreach (var value in QueryUtil.MaterializeReader(rdr)) + { + yield return value; + } + } + } + } + + protected virtual T? Scalar(string query) + where T : struct + { + using (var cmd = CreateCommand(Connection, query)) + { + var result = cmd.ExecuteScalar(); + return result == DBNull.Value ? default(T) : (T)result; + } + } + + protected virtual string Scalar(string query) + { + using (var cmd = CreateCommand(Connection, query)) + { + var result = cmd.ExecuteScalar(); + return result == DBNull.Value ? null : (string)result; + } + } + + protected class TableInfo : IMetadataItem + { + public string Name { get; set; } + public string SchemaName { get; set; } + + public IDictionary Metadata { get; } = new ConcurrentDictionary(); + } + + protected class TriggerInfo : IMetadataItem + { + public string Name { get; set; } + public string ParentTableSchema { get; set; } + public string ParentTable { get; set; } + + public IDictionary Metadata { get; } = new ConcurrentDictionary(); + } + + protected class ColumnInfo : IMetadataItem + { + public string DataType { get; set; } + public bool IsKey { get; set; } + public bool IsIdentity { get; set; } + public bool IsNullable { get; set; } + public bool IsRowGuidCol { get; set; } + public bool IsComputed { get; set; } + public int MaxLength { get; set; } + public string Name { get; set; } + public int Precision { get; set; } + public int Scale { get; set; } + public string SchemaName { get; set; } + public string TableName { get; set; } + public string DefaultValue { get; set; } + + public IDictionary Metadata { get; } = new ConcurrentDictionary(); + } + + protected class ForeignKeyInfo : IMetadataItem + { + public string Name { get; set; } + public string ParentTableSchema { get; set; } + public string ParentTable { get; set; } + public string ReferencedTableSchema { get; set; } + public string ReferencedTable { get; set; } + public string ParentColumn { get; set; } + public string ReferencedColumn { get; set; } + public ForeignKeyAction UpdateAction { get; set; } + public ForeignKeyAction DeleteAction { get; set; } + public IDictionary Metadata { get; } = new ConcurrentDictionary(); + } + + protected class IndexColumnInfo : IMetadataItem + { + public string SchemaName { get; set; } + public string TableName { get; set; } + public string ColumnName { get; set; } + public string IndexName { get; set; } + public bool IsUnique { get; set; } + public bool IsClustered { get; set; } + public bool IsIncluded { get; set; } + + public string Name => $"[{TableName}].[{ColumnName}]"; + public IDictionary Metadata { get; } = new ConcurrentDictionary(); + } + } +} \ No newline at end of file diff --git a/Geco/Common/MetadataProviders/QueryUtil.cs b/Geco/Common/MetadataProviders/QueryUtil.cs new file mode 100644 index 0000000..db1a360 --- /dev/null +++ b/Geco/Common/MetadataProviders/QueryUtil.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using Geco.Common.SimpleMetadata; + +namespace Geco.Common.MetadataProviders +{ + public static class QueryUtil + { + public static IEnumerable MaterializeReader(DbDataReader reader) + where T : IMetadataItem, new() + { + Func materialize = GetCachedMaterializeFunc(); + while (reader.Read()) + { + yield return materialize(reader); + } + } + + private static readonly ConcurrentDictionary MaterializeFunctions = new ConcurrentDictionary(); + private static Func GetCachedMaterializeFunc() + where T:IMetadataItem + { + return (Func)MaterializeFunctions.GetOrAdd(typeof(T), t => + { + var r = Expression.Parameter(typeof(DbDataReader), "r"); + var exceptProperties = new HashSet(t.GetProperties().Select(p => p.Name).Where(p => nameof(IMetadataItem.Metadata) != p)); + var lambda = Expression.Lambda( + Expression.Call(typeof(QueryUtil).GetMethod(nameof(AddMetadata), BindingFlags.Static | BindingFlags.NonPublic).MakeGenericMethod(t), + Expression.MemberInit + ( + Expression.New(t), + t.GetTypeInfo().GetProperties() + .Where(p => nameof(IMetadataItem.Metadata) != p.Name) + .Where(p => p.CanWrite) + .Select(p => Expression.Bind(p, GetAssignmentExpression(r, p))) + ), r, Expression.Constant(exceptProperties)), EnumerableExtensions.Yield(r) + ); + + return (Func)lambda.Compile(); + }); + } + + private static Expression GetAssignmentExpression(ParameterExpression r, PropertyInfo p) + { + if (!p.CanWrite) + throw new InvalidOperationException($"Property: {p.Name} of type:{p.DeclaringType.FullName} is not writable property!"); + + return Expression.Call(ReadValueOrDefaultMethod.MakeGenericMethod(p.PropertyType), EnumerableExtensions.Yield(r, Expression.Constant(p.Name))); + } + + private static T AddMetadata(T item, DbDataReader r, HashSet exceptProperties) + where T : IMetadataItem + { + for (int i = 0; i < r.FieldCount; i++) + { + var fieldName = r.GetName(i); + if (exceptProperties.Contains(fieldName)) + continue; + item.Metadata.Add(fieldName, r.GetValue(i).ToString()); + } + return item; + } + + + private static readonly MethodInfo ReadValueOrDefaultMethod = typeof(QueryUtil).GetTypeInfo().GetMethods(BindingFlags.NonPublic | BindingFlags.Static).First(m => m.Name == nameof(ReadValueOrDefault)); + private static T ReadValueOrDefault(DbDataReader r, string name) + { + var index = r.GetOrdinal(name); + return r.IsDBNull(index) ? default(T) : r.GetFieldValue(index); + } + } +} \ No newline at end of file diff --git a/Geco/Common/MetadataProviders/SqlServer/SqlServerMetadataProvider.cs b/Geco/Common/MetadataProviders/SqlServer/SqlServerMetadataProvider.cs new file mode 100644 index 0000000..ece858a --- /dev/null +++ b/Geco/Common/MetadataProviders/SqlServer/SqlServerMetadataProvider.cs @@ -0,0 +1,163 @@ +using Microsoft.Extensions.Configuration; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Data.Common; +using System.Data.SqlClient; +using System.Linq; + +namespace Geco.Common.MetadataProviders.SqlServer +{ + public class SqlServerMetadataProvider : MetadataProviderBase + { + private readonly IConfigurationRoot configurationRoot; + public SqlServerMetadataProvider(IConfigurationRoot configurationRoot) + { + this.configurationRoot = configurationRoot; + } + + protected override string GetName() + { + return Scalar("SELECT DB_NAME()"); + } + + protected override IEnumerable LoadTables() + { + return Query( + @"SELECT + t.name as Name, + OBJECT_SCHEMA_NAME(t.object_id) as [SchemaName], + t.is_memory_optimized, + t.is_tracked_by_cdc, + t.temporal_type + FROM sys.tables t "); + } + + protected override IEnumerable LoadColumns() + { + return Query( + @"SELECT + SCHEMA_NAME(t.schema_id) as SchemaName, + t.name as TableName, + c.name as Name, + ISNULL(sty.name, ty.name) as DataType, + CAST(c.precision as int) as Precision, + CAST(c.scale as int) as Scale, + CAST(ISNULL(COLUMNPROPERTY(c.object_id, c.name, 'charmaxlen'), c.max_length) AS int) as MaxLength, + c.is_nullable as IsNullable, + c.is_rowguidcol as IsRowGuidCol, + CAST((CASE WHEN EXISTS (SELECT 1 + FROM sys.index_columns ic + INNER JOIN sys.indexes i ON ic.object_id = i.object_id AND i.index_id = ic.index_id + INNER JOIN sys.key_constraints kc ON kc.parent_object_id = i.object_id + WHERE i.is_primary_key = 1 AND kc.type = 'PK' AND ic.column_id = c.column_id AND ic.object_id = c.object_id) + THEN 1 + ELSE 0 END) AS Bit) as IsKey, + CAST(columnproperty(t.object_id, c.name ,'IsIdentity') AS Bit) as IsIdentity, + object_definition(c.default_object_id) as DefaultValue, + is_computed as IsComputed + FROM sys.columns c + INNER JOIN sys.tables t ON c.object_id = t.object_id + INNER JOIN sys.types ty ON c.user_type_id = ty.user_type_id + LEFT JOIN sys.types sty ON ty.system_type_id = sty.user_type_id + ORDER BY SchemaName, TableName, c.column_id"); + } + + protected override IEnumerable LoadForeignKeys() + { + return Query( + @"SELECT + f.name as Name, + OBJECT_SCHEMA_NAME(f.parent_object_id) as ParentTableSchema, + OBJECT_NAME(f.parent_object_id) as ParentTable, + OBJECT_SCHEMA_NAME(f.referenced_object_id) as ReferencedTableSchema, + OBJECT_NAME(f.referenced_object_id) as ReferencedTable, + cp.name as ParentColumn, + cr.name as ReferencedColumn, + f.update_referential_action AS UpdateAction, + f.delete_referential_action as DeleteAction + FROM sys.foreign_keys f + INNER JOIN sys.foreign_key_columns fkp ON fkp.constraint_object_id = f.object_id + INNER JOIN sys.columns cp ON fkp.parent_object_id = cp.object_id AND fkp.parent_column_id = cp.column_id + INNER JOIN sys.columns cr ON fkp.referenced_object_id = cr.object_id AND fkp.referenced_column_id= cr.column_id"); + } + + protected override IEnumerable LoadTriggerInfo() + { + return Query( + @"SELECT + tr.name, OBJECT_NAME(tr.parent_id) as ParentTable, OBJECT_SCHEMA_NAME(tr.parent_id) as ParentTableSchema + FROM sys.triggers tr + WHERE tr.parent_class <> 0"); + } + + protected override IEnumerable LoadIndexInfo() + { + return Query(@" + SELECT + OBJECT_SCHEMA_NAME(si.object_id) as SchemaName, + OBJECT_NAME(si.object_id) as TableName, + c.name as ColumnName, + si.name as IndexName, + si.is_unique as IsUnique, + CAST(CASE WHEN si.type = 1 THEN 1 ELSE 0 END AS Bit) AS IsClustered, + ic.is_included_column as IsIncluded + FROM sys.index_columns ic + INNER JOIN sys.indexes si ON ic.object_id = si.object_id AND ic.index_id = si.index_id + INNER JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id + WHERE OBJECTPROPERTY(si.object_id, 'IsUserTable') = 1 AND OBJECT_NAME(si.object_id) NOT LIKE 'sysdiagrams' + ORDER BY SchemaName, TableName, si.index_id, ic.column_id"); + } + + protected override DbConnection CreateConnection() + { + return new SqlConnection(configurationRoot.GetConnectionString(ConnectionName)); + } + + protected override DbCommand CreateCommand(DbConnection cnn, string commandText) + { + return new SqlCommand(commandText, (SqlConnection)cnn); + } + + protected override IReadOnlyDictionary GetClrTypeMappings() + { + return new ReadOnlyDictionary + ( + new Dictionary + { + { "bigint", typeof(long) }, + { "binary", typeof(byte[]) }, + { "bit", typeof(bool) }, + { "char", typeof(char) }, + { "date", typeof(DateTime) }, + { "datetime", typeof(DateTime) }, + { "datetime2", typeof(DateTime) }, + { "datetimeoffset", typeof(DateTimeOffset) }, + { "decimal", typeof(decimal) }, + { "float", typeof(double) }, + { "image", typeof(byte[]) }, + { "int", typeof(int) }, + { "money", typeof(decimal) }, + { "nchar", typeof(char) }, + { "ntext", typeof(string) }, + { "numeric", typeof(decimal) }, + { "nvarchar", typeof(string) }, + { "real", typeof(float) }, + { "smalldatetime", typeof(DateTime) }, + { "smallint", typeof(short) }, + { "smallmoney", typeof(decimal) }, + { "sql_variant", typeof(byte[]) }, + { "sysname", typeof(string) }, + { "text", typeof(string) }, + { "time", typeof(TimeSpan) }, + { "timestamp", typeof(byte[]) }, + { "tinyint", typeof(byte) }, + { "uniqueidentifier", typeof(Guid) }, + { "varbinary", typeof(byte[]) }, + { "varchar", typeof(string) }, + { "xml", typeof(string) }, + } + ); + } + } +} \ No newline at end of file diff --git a/Geco/Common/OptionsAttribute.cs b/Geco/Common/OptionsAttribute.cs new file mode 100644 index 0000000..a05a1b4 --- /dev/null +++ b/Geco/Common/OptionsAttribute.cs @@ -0,0 +1,13 @@ +using System; + +namespace Geco.Common +{ + public class OptionsAttribute : Attribute + { + public Type OptionType { get; } + public OptionsAttribute(Type optionsType) + { + this.OptionType = optionsType ?? throw new ArgumentNullException(nameof(optionsType)); + } + } +} \ No newline at end of file diff --git a/Geco/Common/SimpleMetadata/Column.cs b/Geco/Common/SimpleMetadata/Column.cs new file mode 100644 index 0000000..11073df --- /dev/null +++ b/Geco/Common/SimpleMetadata/Column.cs @@ -0,0 +1,59 @@ +using System.Diagnostics; + +namespace Geco.Common.SimpleMetadata +{ + [DebuggerDisplay("[{Name}] {DataType}({MaxLength}) Nullable:{IsNullable} Key:{IsKey}")] + public class Column: MetadataItem + { + public Column(string name, Table table, int ordinal, string dataType, int precision, int scale, int maxLength, bool isNullable, bool isKey, bool isIdentity, bool isRowguidCol, bool isComputed, string defaultValue) + { + Name = name; + Ordinal = ordinal; + DataType = dataType; + Precision = precision; + Scale = scale; + IsNullable = isNullable; + IsKey = isKey; + IsIdentity = isIdentity; + IsRowguidCol = isRowguidCol; + IsComputed = isComputed; + MaxLength = maxLength; + Table = table; + DefaultValue = defaultValue; + + Indexes = new MetadataCollection(); + IndexIncludes = new MetadataCollection(); + IncommingForeignKeys = new MetadataCollection(); + } + + public override string Name { get; } + public int Ordinal { get; } + public string DataType { get;} + public int Precision { get;} + public int Scale { get; } + public int MaxLength { get; } + public bool IsNullable { get; } + public bool IsKey { get; } + public bool IsIdentity { get; } + public bool IsRowguidCol { get; } + public bool IsComputed { get; } + + public Table Table { get; } + public ForeignKey ForeignKey { get; set; } + public MetadataCollection IncommingForeignKeys { get; set; } + public MetadataCollection Indexes { get; set; } + public MetadataCollection IndexIncludes { get; set; } + + public string DefaultValue { get;} + + protected override void OnRemove() + { + Table.Columns.GetWritable().Remove(this.Name); + ForeignKey?.GetWritable().Remove(); + Indexes.GetWritable().Remove(this.Name); + IndexIncludes.GetWritable().Remove(this.Name); + foreach (var foreignKey in IncommingForeignKeys) + foreignKey.GetWritable().Remove(); + } + } +} \ No newline at end of file diff --git a/Geco/Common/SimpleMetadata/DataBaseIndex.cs b/Geco/Common/SimpleMetadata/DataBaseIndex.cs new file mode 100644 index 0000000..9b0ffdc --- /dev/null +++ b/Geco/Common/SimpleMetadata/DataBaseIndex.cs @@ -0,0 +1,36 @@ +using System.Diagnostics; + +namespace Geco.Common.SimpleMetadata +{ + [DebuggerDisplay("[{Name}] IsUnique:{IsUnique} IsClustered:{IsClustered} Columns:{Columns} IncludedColumns:{IncludedColumns}")] + public class DataBaseIndex : MetadataItem + { + public DataBaseIndex(string name, Table table, bool isUnique, bool isClustered) + { + Name = name; + Table = table; + IsUnique = isUnique; + IsClustered = isClustered; + Columns = new MetadataCollection(OnColumnAdded, null); + IncludedColumns = new MetadataCollection(OnIncludedColumnAdded); + } + + private void OnColumnAdded(Column column) + { + column.Indexes.Add(this); + } + + private void OnIncludedColumnAdded(Column column) + { + column.IndexIncludes.Add(this); + } + + public override string Name { get; } + public Table Table { get; } + public bool IsUnique { get; } + public bool IsClustered { get; } + + public MetadataCollection Columns { get; } + public MetadataCollection IncludedColumns { get; } + } +} \ No newline at end of file diff --git a/Geco/Common/SimpleMetadata/DatabaseMetadata.cs b/Geco/Common/SimpleMetadata/DatabaseMetadata.cs new file mode 100644 index 0000000..98c21f1 --- /dev/null +++ b/Geco/Common/SimpleMetadata/DatabaseMetadata.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; + +namespace Geco.Common.SimpleMetadata +{ + public class DatabaseMetadata + { + public DatabaseMetadata(string name, IReadOnlyDictionary typeMappings) + { + Schemas = new MetadataCollection(null, OnRemoveSchema); + TypeMappings = typeMappings; + Name = name; + } + + public string Name { get; } + public MetadataCollection Schemas { get; } + public bool IsFrozen { get; private set; } + + public IReadOnlyDictionary TypeMappings { get; } + + /// + /// Freezes current database metadata instance so it cannot be modified any more + /// + public void Freeze() + { + this.IsFrozen = true; + } + + private void OnRemoveSchema(Schema schema) + { + schema.GetWritable().Remove(); + } + } +} \ No newline at end of file diff --git a/Geco/Common/SimpleMetadata/ForeignKey.cs b/Geco/Common/SimpleMetadata/ForeignKey.cs new file mode 100644 index 0000000..015832a --- /dev/null +++ b/Geco/Common/SimpleMetadata/ForeignKey.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; + +namespace Geco.Common.SimpleMetadata +{ + public class ForeignKey : MetadataItem + { + public ForeignKey(string name, Table parentTable, Table targetTable, ForeignKeyAction updateAction, ForeignKeyAction deleteAction) + { + Name = name; + ParentTable = parentTable; + TargetTable = targetTable; + FromColumns = new MetadataCollection(OnFromColumnAdd, null); + ToColumns = new MetadataCollection(OnToColumnsAdd, null); + UpdateAction = updateAction; + DeleteAction = deleteAction; + } + + public override string Name { get; } + public Table ParentTable { get; } + public Table TargetTable { get; } + public MetadataCollection FromColumns { get; } + public MetadataCollection ToColumns { get; } + + public ForeignKeyAction UpdateAction { get;} + public ForeignKeyAction DeleteAction { get;} + + protected override void OnRemove() + { + this.ParentTable.ForeignKeys.GetWritable().Remove(this.Name); + foreach (var fromColumn in FromColumns) + fromColumn.ForeignKey = this; + this.TargetTable.ForeignKeys.GetWritable().Remove(this.Name); + } + + private void OnFromColumnAdd(Column column) + { + column.ForeignKey = this; + } + + private void OnToColumnsAdd(Column column) + { + column.IncommingForeignKeys.Add(this); + } + } + + public enum ForeignKeyAction : byte + { + NoAction = 0, + Cascade = 1, + SetNull = 2, + SetDefault = 3 + } +} \ No newline at end of file diff --git a/Geco/Common/SimpleMetadata/IMetadataItem.cs b/Geco/Common/SimpleMetadata/IMetadataItem.cs new file mode 100644 index 0000000..125e71d --- /dev/null +++ b/Geco/Common/SimpleMetadata/IMetadataItem.cs @@ -0,0 +1,75 @@ +using System.Collections.Generic; +using System.Reflection.Metadata.Ecma335; + +namespace Geco.Common.SimpleMetadata +{ + /// + /// Represents a metadata item + /// + public interface IMetadataItem + { + /// + /// The name of current metadata item + /// + string Name { get; } + + /// + /// A mutable dictionary for additional metadata for current + /// + IDictionary Metadata { get; } + } + + /// + /// Base class for all metadata items + /// + public abstract class MetadataItem : IMetadataItem, IMetadataItemWriter + { + private bool inRemove; + + /// + /// The name of current metadata item + /// + public abstract string Name { get; } + /// + /// A mutable dictionary for additional metadata for current + /// + public IDictionary Metadata { get; } = new MetadataDictionary(); + + /// + /// Called when item is to be removed from the metadata graph. This is where the current item should remove all links from other items to itself. + /// + protected virtual void OnRemove() + { + + } + + void IMetadataItemWriter.Remove() + { + if (inRemove) + return; + try + { + inRemove = true; + OnRemove(); + } + finally + { + inRemove = false; + } + } + } + + + internal static partial class MetadataExtensions + { + public static T WithMetadata(this T target, IMetadataItem medatata) + where T : IMetadataItem + { + foreach (var (key, val) in medatata.Metadata) + { + target.Metadata.Add(key, val); + } + return target; + } + } +} \ No newline at end of file diff --git a/Geco/Common/SimpleMetadata/IMetadataProvider.cs b/Geco/Common/SimpleMetadata/IMetadataProvider.cs new file mode 100644 index 0000000..6d6e5a6 --- /dev/null +++ b/Geco/Common/SimpleMetadata/IMetadataProvider.cs @@ -0,0 +1,17 @@ +namespace Geco.Common.SimpleMetadata +{ + public interface IMetadataProvider + { + /// + /// Loads meta data from a database and caches it + /// + /// + /// + DatabaseMetadata GetMetadata(string connectionName = "DefaultConnection"); + + /// + /// Reloads the metadata from the database and caches it + /// + void Reload(); + } +} \ No newline at end of file diff --git a/Geco/Common/SimpleMetadata/MetadataCollection.cs b/Geco/Common/SimpleMetadata/MetadataCollection.cs new file mode 100644 index 0000000..2e0fa6e --- /dev/null +++ b/Geco/Common/SimpleMetadata/MetadataCollection.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Geco.Common.SimpleMetadata.Util; + +namespace Geco.Common.SimpleMetadata +{ + [DebuggerNonUserCode] + public class MetadataCollection : IReadOnlyList, IMetadataCollectionWriteAccessor + where TEntity : IMetadataItem + { + private readonly OrderedInterceptableDictionary innerDictionary; + + public MetadataCollection(Action onAdded = null, Action onRemoved = null) + :this(Enumerable.Empty(), onAdded, onRemoved) + { + } + + public MetadataCollection(IEnumerable source, Action onAdded = null, Action onRemoved = null) + { + innerDictionary = new OrderedInterceptableDictionary(StringComparer.OrdinalIgnoreCase, onAdded, onRemoved); + foreach (var entity in source) + innerDictionary.Add(entity.Name, entity); + } + + public void Add(TEntity item) + { + if (item == null) + throw new ArgumentNullException(nameof(item)); + innerDictionary.Add(item.Name, item); + } + + public bool Contains(TEntity item) + { + return innerDictionary.Contains( new KeyValuePair(item.Name, item)); + } + + public int Count => innerDictionary.Count; + + public bool ContainsKey(string key) + { + return innerDictionary.ContainsKey(key); + } + + public bool TryGetValue(string key, out TEntity value) + { + return innerDictionary.TryGetValue(key, out value); + } + + public TEntity this[string key] => innerDictionary[key]; + public TEntity this[int index] => innerDictionary.ElementAt(index).Value; + + public IEnumerable Keys => innerDictionary.Keys; + public IEnumerable Values => innerDictionary.Values; + + public IEnumerator GetEnumerator() + { + return innerDictionary.Values.ToList().GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + IDictionary IMetadataCollectionWriteAccessor.GetWritable() + { + return this.innerDictionary; + } + + public override string ToString() + { + return String.Join(",", innerDictionary.Keys); + } + } + + [DebuggerNonUserCode] + internal static partial class MetadataExtensions + { + public static IDictionary GetWritable(this IMetadataCollectionWriteAccessor metadataCollection) + where TEntity : IMetadataItem + { + return metadataCollection.GetWritable(); + } + + public static IMetadataItemWriter GetWritable(this IMetadataItemWriter metadataItem) + { + return metadataItem; + } + } + + internal interface IMetadataCollectionWriteAccessor + where TEntity : IMetadataItem + { + IDictionary GetWritable(); + } + + internal interface IMetadataItemWriteAccessor + { + IMetadataItemWriter GetWritable(); + } + + public interface IMetadataItemWriter + { + void Remove(); + } +} + diff --git a/Geco/Common/SimpleMetadata/MetadataDictionary.cs b/Geco/Common/SimpleMetadata/MetadataDictionary.cs new file mode 100644 index 0000000..c0961c2 --- /dev/null +++ b/Geco/Common/SimpleMetadata/MetadataDictionary.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Geco.Common.SimpleMetadata +{ + public class MetadataDictionary : IDictionary + { + private readonly IDictionary innerDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public IEnumerator> GetEnumerator() + { + return innerDictionary.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable) innerDictionary).GetEnumerator(); + } + + public void Add(KeyValuePair item) + { + innerDictionary.Add(item); + } + + public void Clear() + { + innerDictionary.Clear(); + } + + public bool Contains(KeyValuePair item) + { + return innerDictionary.Contains(item); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + innerDictionary.CopyTo(array, arrayIndex); + } + + public bool Remove(KeyValuePair item) + { + return innerDictionary.Remove(item); + } + + public int Count => innerDictionary.Count; + + public bool IsReadOnly => innerDictionary.IsReadOnly; + + public void Add(string key, string value) + { + innerDictionary.Add(key, value); + } + + public bool ContainsKey(string key) + { + return innerDictionary.ContainsKey(key); + } + + public bool Remove(string key) + { + return innerDictionary.Remove(key); + } + + public bool TryGetValue(string key, out string value) + { + return innerDictionary.TryGetValue(key, out value); + } + + public string this[string key] + { + get { + innerDictionary.TryGetValue(key, out string value); + return value; + } + set => innerDictionary[key] = value; + } + + public ICollection Keys => innerDictionary.Keys; + public ICollection Values => innerDictionary.Values; + } +} \ No newline at end of file diff --git a/Geco/Common/SimpleMetadata/Schema.cs b/Geco/Common/SimpleMetadata/Schema.cs new file mode 100644 index 0000000..238add0 --- /dev/null +++ b/Geco/Common/SimpleMetadata/Schema.cs @@ -0,0 +1,40 @@ +using System.Diagnostics; +using System.Linq; + +namespace Geco.Common.SimpleMetadata +{ + [DebuggerDisplay("[{Name}]")] + public class Schema : MetadataItem + { + public Schema(string name) + { + Name = name; + Tables = new MetadataCollection(OnAdd, OnRemove); + } + + private void OnAdd(Table table) + { + + } + + private void OnRemove(Table table) + { + // Remove all FK references a table when it is removed from the model + foreach (var fk in table.ForeignKeys) + { + fk.TargetTable.IncomingForeignKeys.GetWritable().Remove(fk.Name); + foreach (var fkToColumn in fk.ToColumns) + fkToColumn.ForeignKey = null; + } + foreach (var fk in table.IncomingForeignKeys) + { + fk.ParentTable.ForeignKeys.GetWritable().Remove(fk.Name); + foreach (var fkToColumn in fk.FromColumns) + fkToColumn.ForeignKey = null; + } + } + + public override string Name { get; } + public MetadataCollection
Tables { get; } + } +} \ No newline at end of file diff --git a/Geco/Common/SimpleMetadata/Table.cs b/Geco/Common/SimpleMetadata/Table.cs new file mode 100644 index 0000000..3b6e76b --- /dev/null +++ b/Geco/Common/SimpleMetadata/Table.cs @@ -0,0 +1,59 @@ +using System.Diagnostics; +using System.Linq; +using Geco.Common.MetadataProviders; + +namespace Geco.Common.SimpleMetadata +{ + [DebuggerDisplay("[{Schema.Name}].[{Name}]")] + public class Table : MetadataItem + { + public Table(string name, Schema schema) + { + Name = name; + Schema = schema; + Indexes = new MetadataCollection(null, OnIndexRemove); + Triggers = new MetadataCollection(null, OnRemoveIndex); + IncomingForeignKeys = new MetadataCollection(null, OnRemoveForeignKey); + ForeignKeys = new MetadataCollection(null, OnRemoveForeignKey); + Columns = new MetadataCollection(null, OnRemoveColumn); + } + + public override string Name { get; } + public Schema Schema { get; } + + public MetadataCollection Columns { get; } + public MetadataCollection ForeignKeys { get; } + public MetadataCollection IncomingForeignKeys { get; } + public MetadataCollection Triggers { get; } + public MetadataCollection Indexes { get; } + + protected override void OnRemove() + { + Schema.Tables.GetWritable().Remove(this.Name); + foreach (var foreignKey in ForeignKeys) + foreignKey.GetWritable().Remove(); + foreach (var incomingForeignKey in IncomingForeignKeys) + incomingForeignKey.GetWritable().Remove(); + } + + private void OnRemoveIndex(Trigger trigger) + { + trigger.GetWritable().Remove(); + } + + private void OnRemoveColumn(Column column) + { + column.GetWritable().Remove(); + } + + private void OnRemoveForeignKey(ForeignKey foreignKey) + { + foreignKey.GetWritable().Remove(); + } + + private void OnIndexRemove(DataBaseIndex index) + { + index.GetWritable().Remove(); + } + } +} \ No newline at end of file diff --git a/Geco/Common/SimpleMetadata/Trigger.cs b/Geco/Common/SimpleMetadata/Trigger.cs new file mode 100644 index 0000000..c37d561 --- /dev/null +++ b/Geco/Common/SimpleMetadata/Trigger.cs @@ -0,0 +1,19 @@ +namespace Geco.Common.SimpleMetadata +{ + public class Trigger : MetadataItem + { + public Trigger(string name, Table table) + { + Table = table; + Name = name; + } + + public Table Table { get; } + public override string Name { get; } + + protected override void OnRemove() + { + Table.Triggers.GetWritable().Remove(this.Name); + } + } +} \ No newline at end of file diff --git a/Geco/Common/SimpleMetadata/Util/OrderedInterceptableDictionary.cs b/Geco/Common/SimpleMetadata/Util/OrderedInterceptableDictionary.cs new file mode 100644 index 0000000..d396e83 --- /dev/null +++ b/Geco/Common/SimpleMetadata/Util/OrderedInterceptableDictionary.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace Geco.Common.SimpleMetadata.Util +{ + /// + /// A string dictionary which preserves the order of elements which were added and allows intercepting additions and deletions + /// + /// The key type + /// The element type + [DebuggerNonUserCode] + internal class OrderedInterceptableDictionary : IDictionary + { + private readonly IDictionary keyOrder; + private readonly SortedDictionary elements; + + private readonly Action onAdded; + private readonly Action onRemoved; + private int curentOrder; + + public OrderedInterceptableDictionary(IEqualityComparer comparer, Action onAdd, Action onRemoved) + { + this.onAdded = onAdd; + keyOrder = new Dictionary(comparer ?? EqualityComparer.Default); + elements = new SortedDictionary(); + this.onRemoved = onRemoved; + } + + public int Count => elements.Count; + + public TElement this[TKey key] + { + get => GetElement(key); + set + { + if (keyOrder.ContainsKey(key)) + { + Remove(key); + } + Add(key, value); + } + } + + public ICollection Keys => elements.Values.Select(v => v.Key).ToList(); + public ICollection Values => elements.Values.Select(v => v.Element).ToList(); + bool ICollection>.IsReadOnly => false; + + public void Clear() + { + var values = elements.Values.ToList(); + elements.Clear(); + keyOrder.Clear(); + if (onRemoved != null) + foreach (var item in values) + onRemoved(item.Element); + } + + public void Add(TKey key, TElement value) + { + keyOrder.Add(key, curentOrder); + elements.Add(curentOrder, (key,value)); + onAdded?.Invoke(value); + curentOrder++; + } + + public bool ContainsKey(TKey key) + { + return keyOrder.ContainsKey(key); + } + + public bool Remove(TKey key) + { + if (keyOrder.TryGetValue(key, out var idx)) + { + var element = elements[idx].Element; + keyOrder.Remove(key); + elements.Remove(idx); + onRemoved?.Invoke(element); + return true; + } + return false; + } + + public bool TryGetValue(TKey key, out TElement value) + { + if (keyOrder.TryGetValue(key, out var idx)) + { + value = elements[idx].Element; + return true; + } + value = default; + return false; + } + + public IEnumerator> GetEnumerator() + { + foreach (var value in elements.Values) + { + yield return new KeyValuePair(value.Key, value.Element); + } + } + + private TElement GetElement(TKey key) + { + return elements[keyOrder[key]].Element; + } + + void ICollection>.Add(KeyValuePair item) + { + Add(item.Key, item.Value); + } + + bool ICollection>.Contains(KeyValuePair item) + { + return keyOrder.ContainsKey(item.Key) && Object.Equals(elements[keyOrder[item.Key]].Element, item.Value); + } + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + throw new NotSupportedException(); + } + + bool ICollection>.Remove(KeyValuePair item) + { + return ContainsKey(item.Key) && GetElement(item.Key).Equals(item.Value) && Remove(item.Key); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/Geco/Common/Util/ColorConsole.cs b/Geco/Common/Util/ColorConsole.cs new file mode 100644 index 0000000..567f1f4 --- /dev/null +++ b/Geco/Common/Util/ColorConsole.cs @@ -0,0 +1,91 @@ +using System; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; + +namespace Geco.Common +{ + public static class ColorConsole + { + static readonly Regex splitRegex = new Regex(@"{\d+}", RegexOptions.Compiled); + + public static void WriteLine(FormattableString value, ConsoleColor color) + { + if (value == null) + { + Console.WriteLine(); + return; + } + Write(value, color); + WriteLine(); + } + + public static void WriteLine(params (string value, ConsoleColor color)[] values) + { + var curentColor = Console.ForegroundColor; + try + { + foreach (var valueColorPair in values) + { + Console.ForegroundColor = valueColorPair.color; + Console.Write(valueColorPair.value); + } + Console.WriteLine(); + } + finally + { + Console.ForegroundColor = curentColor; + } + } + + public static void Write(FormattableString value, ConsoleColor color) + { + + var parts = splitRegex.Split(value.Format); + for (int i = 0; i < parts.Length; i++) + { + Write(parts[i], color); + if (i < value.ArgumentCount) + { + var arg = value.GetArgument(i); + if (arg is ITuple v && v.Length == 2 && v[1] is ConsoleColor c) + Write(v[0].ToString(), c); + else + Write(arg.ToString(), color); + } + } + } + + private static void Write(string value, ConsoleColor color) + { + if (string.IsNullOrEmpty(value)) + return; + var curentColor = Console.ForegroundColor; + try + { + Console.ForegroundColor = color; + Console.Write(value); + } + finally + { + Console.ForegroundColor = curentColor; + } + } + + public static void Write(params (string value, ConsoleColor color)[] values) + { + var curentColor = Console.ForegroundColor; + try + { + foreach (var valueColorPair in values) + { + Console.ForegroundColor = valueColorPair.color; + Console.Write(valueColorPair.value); + } + } + finally + { + Console.ForegroundColor = curentColor; + } + } + } +} \ No newline at end of file diff --git a/Geco/Common/Util/EnumerableExtensions.cs b/Geco/Common/Util/EnumerableExtensions.cs new file mode 100644 index 0000000..b296fd4 --- /dev/null +++ b/Geco/Common/Util/EnumerableExtensions.cs @@ -0,0 +1,158 @@ +using System; +using System.Linq; +using System.Collections.Generic; + +namespace Geco.Common +{ + /// + /// Extensions class with enumerable extensions targeted at code generation + /// + public static class EnumerableExtensions + { + /// + /// Yields a single value + /// + /// + /// + /// + public static IEnumerable Yield(T instance) + { + yield return instance; + } + + /// + /// Yields two values + /// + /// + /// + /// + /// + public static IEnumerable Yield(T instance1, T instance2) + { + yield return instance1; + yield return instance2; + } + + /// + /// Yields tree values + /// + /// + /// + /// + /// + /// + public static IEnumerable Yield(T instance1, T instance2, T instance3) + { + yield return instance1; + yield return instance2; + yield return instance3; + } + + /// + /// Batches the source enumerable into a sequence of enumerable each containing size elements + /// + /// Element Type + /// The source + /// Size of the batch indicating the number of elements in each batch + /// + public static IEnumerable> Batch(this IEnumerable source, int count) + { + if (count < 1) throw new ArgumentOutOfRangeException(nameof(count)); + var enumerator = source.GetEnumerator(); + + IEnumerable BatchCounter(int curentCount) + { + do + { + yield return enumerator.Current; + } while (--curentCount > 0 && enumerator.MoveNext()); + } + + while (enumerator.MoveNext()) + { + yield return BatchCounter(count); + } + } + + /// + /// Returns a wrapped enumerable that contains info for each enumerated item, + /// like the index in original source, and whether it's first or last element in the original source + /// + /// the type of elements in sequence + /// the source + /// wrapped enumerable sequence + public static IEnumerable> WithInfo(this IEnumerable source) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + IEnumerable> WithInfoIterator() + { + using (var enumerator = source.GetEnumerator()) + { + int index = 0; + if (!enumerator.MoveNext()) + yield break; + + T current = enumerator.Current; + + if (enumerator.MoveNext()) + { + yield return new ItemInfo(true, false, index++, current); + current = enumerator.Current; + } + else + { + yield return new ItemInfo(true, true, index, current); + yield break; + } + + + while (enumerator.MoveNext()) + { + var next = enumerator.Current; + yield return new ItemInfo(false, false, index++, current); + current = next; + } + yield return new ItemInfo(false, true, index, current); + } + } + + return WithInfoIterator(); + } + + /// + /// Recreates the info for an enumerable sequence + /// + /// the type of elements in sequence + /// the source + /// wrapped enumerable sequence + public static IEnumerable> WithInfo(this IEnumerable> source) + { + return WithInfo(source.Select(s => s.Item)); + } + } + + public struct ItemInfo + { + [Flags] + private enum Info + { + None = 0, + IsFirst, + IsLast + } + internal ItemInfo(bool first, bool last, int index, T item) + { + info = (first ? Info.IsFirst : 0) | (last ? Info.IsLast : 0); + Index = index; + Item = item; + } + + private readonly Info info; + public bool IsFirst => info.HasFlag(Info.IsFirst); + public bool IsLast => info.HasFlag(Info.IsLast); + + public readonly int Index; + public readonly T Item; + } +} \ No newline at end of file diff --git a/Geco/Config/RootConfig.cs b/Geco/Config/RootConfig.cs new file mode 100644 index 0000000..a00f312 --- /dev/null +++ b/Geco/Config/RootConfig.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Geco.Config +{ + public class RootConfig + { + public List Tasks { get; } = new List(); + } +} \ No newline at end of file diff --git a/Geco/Config/Task.cs b/Geco/Config/Task.cs new file mode 100644 index 0000000..555b8ea --- /dev/null +++ b/Geco/Config/Task.cs @@ -0,0 +1,18 @@ +namespace Geco.Config +{ + public class TaskConfig + { + /// + /// Name of the task + /// + public string Name { get; set; } + /// + /// Class name of task to run + /// + public string TaskClass { get; set; } + public bool OutputToConsole { get; set; } + public string BaseOutputPath { get; set; } + public string CleanFilesPattern { get; set; } + internal int ConfigIndex { get; set; } + } +} \ No newline at end of file diff --git a/Geco/Database/DatabaseCleaner.cs b/Geco/Database/DatabaseCleaner.cs new file mode 100644 index 0000000..efec531 --- /dev/null +++ b/Geco/Database/DatabaseCleaner.cs @@ -0,0 +1,74 @@ +using System; +using System.Data.SqlClient; +using Geco.Common; +using Microsoft.Extensions.Configuration; +using static System.ConsoleColor; + +namespace Geco.Database +{ + /// + /// Deletes all the data in the specified database (SqlServer only) by disabling all triggers and constraints, deleting the data then re enabling them back. + /// + /// + /// Deleting of data is done in a transaction, so either all data is deleted or none is. + /// + [Options(typeof(DatabaseCleanerOptions))] + public class DatabaseCleaner : BaseGenerator + { + private readonly IConfigurationRoot configurationRoot; + private readonly DatabaseCleanerOptions options; + private static string where = "AND o.Name NOT IN (''sysdiagrams'', ''__RefactorLog'')"; + private static string ctx = "SET QUOTED_IDENTIFIER, ANSI_NULLS, ANSI_PADDING, ANSI_WARNINGS ON;SET NUMERIC_ROUNDABORT OFF;"; + private static readonly FormattableString[] Statements = + { + $@"EXEC sp_MSForEachTable @command1='{ctx}DISABLE TRIGGER ALL ON ?', @whereand='{where}'", + $@"EXEC sp_MSForEachTable @command1='{ctx}ALTER TABLE ? NOCHECK CONSTRAINT ALL', @whereand='{where}'", + $@"EXEC sp_MSForEachTable @command1='{ctx}DELETE FROM ?', @whereand='{where}'", + $@"EXEC sp_MSForEachTable @command1='{ctx}ALTER TABLE ? CHECK CONSTRAINT ALL', @whereand='{where}'", + $@"EXEC sp_MSForEachTable @command1='{ctx}ENABLE TRIGGER ALL ON ?', @whereand='{where}'", + $@"EXEC sp_MSforeachtable @command1 = 'DBCC CHECKIDENT (''?'', RESEED, 1)', @whereand = 'AND EXISTS (SELECT 1 FROM sys.columns c WHERE c.object_id = o.ID AND c.is_identity = 1)'" + }; + + private bool exit = false; + + public DatabaseCleaner(IConfigurationRoot configurationRoot, DatabaseCleanerOptions options, IInflector inf) : base(inf) + { + this.configurationRoot = configurationRoot; + this.options = options; + } + + protected override void Generate() + { + if (exit) + return; + var connectionString = configurationRoot.GetConnectionString(options.ConnectionName ?? "DefaultConnection"); + CleanDatabase(connectionString); + } + + public static void CleanDatabase(string connectionString) + { + using (var cnn = new SqlConnection(connectionString)) + { + cnn.Open(); + using (var tran = cnn.BeginTransaction()) + { + foreach (var statement in Statements) + { + using (var cmd = new SqlCommand(statement.ToString(), cnn, tran)) + { + ColorConsole.WriteLine(("Running: ", Yellow), (string.Format(statement.Format, "", "***"), White)); + cmd.ExecuteNonQuery(); + } + } + tran.Commit(); + } + } + } + + public override bool GetUserConfirmation() + { + ColorConsole.Write($"Are you sure you want to delete all data in the target database? (y/n):", White); + return string.Equals(Console.ReadLine(), "y", StringComparison.OrdinalIgnoreCase); + } + } +} \ No newline at end of file diff --git a/Geco/Database/DatabaseCleanerOptions.cs b/Geco/Database/DatabaseCleanerOptions.cs new file mode 100644 index 0000000..56829d4 --- /dev/null +++ b/Geco/Database/DatabaseCleanerOptions.cs @@ -0,0 +1,7 @@ +namespace Geco.Database +{ + public class DatabaseCleanerOptions + { + public string ConnectionName { get; set; } + } +} \ No newline at end of file diff --git a/Geco/Database/EntityFrameworkCoreReverseModelGenerator.cs b/Geco/Database/EntityFrameworkCoreReverseModelGenerator.cs new file mode 100644 index 0000000..b35eb92 --- /dev/null +++ b/Geco/Database/EntityFrameworkCoreReverseModelGenerator.cs @@ -0,0 +1,505 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Geco.Common; +using Geco.Common.SimpleMetadata; +using Microsoft.Extensions.Options; +using System.Text; + +// ReSharper disable PossibleMultipleEnumeration + +namespace Geco.Database +{ + /// + /// Model Generator for Entity Framework Core + /// + [Options(typeof(EntityFrameworkCoreReverseModelGeneratorOptions))] + public class EntityFrameworkCoreReverseModelGenerator : BaseGeneratorWithMetadata + { + private readonly EntityFrameworkCoreReverseModelGeneratorOptions options; + + public EntityFrameworkCoreReverseModelGenerator(IMetadataProvider provider, IInflector inf, EntityFrameworkCoreReverseModelGeneratorOptions options) : base(provider, inf, options.ConnectionName) + { + this.options = options; + } + + protected override void Generate() + { + IgnoreUnsuportedColumns(); + FilterTables(); + WriteEntityFiles(); + WriteContextFile(); + } + + private void WriteEntityFiles() + { + using (BeginFile($"{options.ContextName ?? Inf.Pascalise(Db.Name)}Entities.cs", options.OneFilePerEntity == false)) + using (WriteHeader(options.OneFilePerEntity == false)) + foreach (var table in Db.Schemas.SelectMany(s => s.Tables).OrderBy(t => t.Name)) + { + var className = Inf.Pascalise(Inf.Singularise(table.Name)); + table.Metadata["Class"] = className; + + using (BeginFile($"{className}.cs", options.OneFilePerEntity)) + using (WriteHeader(options.OneFilePerEntity)) + { + WriteEntity(table); + } + } + } + + + private void WriteContextFile() + { + using (BeginFile($"{options.ContextName ?? Inf.Pascalise(Db.Name)}.cs")) + using (WriteHeader()) + { + W($"[GeneratedCode(\"Geco\", \"{Assembly.GetEntryAssembly().GetName().Version}\")]", options.GeneratedCodeAttribute); + W($"public partial class {options.ContextName ?? Inf.Pascalise(Db.Name)}Context : DbContext"); + WI("{"); + { + if (options.NetCore) + { + W("public IConfigurationRoot Configuration {get;}"); + W(); + W( + $"public {options.ContextName ?? Inf.Pascalise(Db.Name)}Context(IConfigurationRoot configuration)"); + WI("{"); + W("this.Configuration = configuration;"); + DW("}"); + W(); + } + + W("protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)"); + WI("{"); + WI("if (optionsBuilder.IsConfigured)"); + { + W("return;"); + } + DW(); + + if (options.UseSqlServer) + { + W($"optionsBuilder.UseSqlServer(ConfigurationManager.ConnectionStrings[\"{options.ConnectionName}\"].ConnectionString, opt =>", !options.NetCore); + W($"optionsBuilder.UseSqlServer(Configuration.GetConnectionString(\"{options.ConnectionName}\"), opt =>", options.NetCore); + WI("{"); + { + W("//opt.EnableRetryOnFailure();"); + } + DW("});"); + W(); + } + + if (options.ConfigureWarnings) + { + W("optionsBuilder.ConfigureWarnings(w =>"); + WI("{"); + { + W("w.Ignore(RelationalEventId.AmbientTransactionWarning);"); + W("w.Ignore(RelationalEventId.QueryClientEvaluationWarning);"); + } + DW("});"); + } + + DW("}"); + W(); + { + WriteDbSets(); + } + W(); + W("protected override void OnModelCreating(ModelBuilder modelBuilder)"); + WI("{"); + { + WriteModelBuilderConfigurations(); + } + DW("}"); + } + DW("}"); + } + } + + private IDisposable WriteHeader(bool write = true) + { + if (!write) + return base.OnBlockEnd(); + + if (options.DisableCodeWarnings) + { + W("// ReSharper disable RedundantUsingDirective"); + W("// ReSharper disable DoNotCallOverridableMethodsInConstructor"); + W("// ReSharper disable InconsistentNaming"); + W("// ReSharper disable PartialTypeWithSinglePart"); + W("// ReSharper disable PartialMethodWithSinglePart"); + W("// ReSharper disable RedundantNameQualifier"); + W("// ReSharper disable UnusedMember.Global"); + W("#pragma warning disable 1591 // Ignore \"Missing XML Comment\" warning"); + W(); + } + W("using System;"); + W("using System.CodeDom.Compiler;"); + W("using System.Collections.Generic;"); + W("using Microsoft.Extensions.Configuration;", options.NetCore); + W("using Microsoft.EntityFrameworkCore;"); + W("using Microsoft.EntityFrameworkCore.Diagnostics;"); + W("using Newtonsoft.Json;", options.JsonSerialization); + W(); + W($"namespace {options.Namespace}"); + WI("{"); + + return base.OnBlockEnd(() => + { + DW("}"); + }); + } + + private void WriteDbSets() + { + foreach (var table in Db.Schemas.SelectMany(s => s.Tables).OrderBy(t => t.Name)) + { + var className = table.Metadata["Class"]; + var plural = Inf.Pluralise(className); + table.Metadata["DbSet"] = plural; + W($"public virtual DbSet<{className}> {plural} {{ get; set; }}"); + } + } + + private void WriteEntity(Table table) + { + var existingNames = new HashSet(); + var className = table.Metadata["Class"]; + var classInterfaces = ""; + int i = 1; + existingNames.Add(className); + + W($"[GeneratedCode(\"Geco\", \"{Assembly.GetEntryAssembly().GetName().Version}\")]", options.GeneratedCodeAttribute); + W($"public partial class {className}{(!String.IsNullOrWhiteSpace(classInterfaces) ? ": " + classInterfaces : "")}"); + WI("{"); + { + var keyProperties = table.Columns.Where(c => c.IsKey); + if (keyProperties.Any()) + { + W("// Key Properties", options.GenerateComments); + foreach (var column in keyProperties) + { + var propertyName = Inf.Pascalise(column.Name); + CheckClash(ref propertyName, existingNames, ref i); + column.Metadata["Property"] = propertyName; + W($"public {GetClrTypeName(column.DataType)}{GetNullable(column)} {propertyName} {{ get; set; }}"); + } + W(); + } + + + var scalarProperties = table.Columns.Where(c => !c.IsKey); + if (scalarProperties.Any()) + { + W("// Scalar Properties", options.GenerateComments); + foreach (var column in scalarProperties) + { + var propertyName = Inf.Pascalise(column.Name); + CheckClash(ref propertyName, existingNames, ref i); + column.Metadata["Property"] = propertyName; + W($"public {GetClrTypeName(column.DataType)}{GetNullable(column)} {propertyName} {{ get; set; }}"); + } + W(); + } + + if (table.ForeignKeys.Any()) + { + W("// Foreign keys", options.GenerateComments); + foreach (var fk in table.ForeignKeys.OrderBy(t => t.ParentTable.Name).ThenBy(t => t.FromColumns.First().Name)) + { + var targetClassName = Inf.Pascalise(Inf.Singularise(fk.TargetTable.Name)); + string propertyName; + if (table.ForeignKeys.Count(f => f.TargetTable == fk.TargetTable) > 1) + propertyName = GetFkName(fk.FromColumns); + else + propertyName = Inf.Singularise(targetClassName); + + if (CheckClash(ref propertyName, existingNames, ref i)) + { + propertyName = Inf.Pascalise(Inf.Singularise(fk.TargetTable.Name)) + GetFkName(fk.FromColumns); + CheckClash(ref propertyName, existingNames, ref i); + } + + fk.Metadata["NavProperty"] = propertyName; + foreach (var column in fk.FromColumns) + { + column.Metadata["NavProperty"] = propertyName; + } + W("[JsonIgnore]", options.JsonSerialization); + W($"public {targetClassName} {propertyName} {{ get; set; }}"); + } + W(); + } + + if (table.IncomingForeignKeys.Any()) + { + W("// Reverse navigation", options.GenerateComments); + foreach (var fk in table.IncomingForeignKeys.OrderBy(t => t.ParentTable.Name).ThenBy(t => t.FromColumns.First().Name)) + { + var targetClassName = Inf.Pascalise(Inf.Singularise(fk.ParentTable.Name)); + string propertyName; + if (table.IncomingForeignKeys.Count(f => f.ParentTable == fk.ParentTable) > 1) + propertyName = Inf.Pluralise(targetClassName) + GetFkName(fk.FromColumns); + else + propertyName = Inf.Pluralise(targetClassName); + + if (CheckClash(ref propertyName, existingNames, ref i)) + { + propertyName = Inf.Pascalise(Inf.Pluralise(fk.ParentTable.Name)) + GetFkName(fk.FromColumns); + CheckClash(ref propertyName, existingNames, ref i); + } + fk.Metadata["Property"] = propertyName; + fk.Metadata["Type"] = targetClassName; + W("[JsonIgnore]", options.JsonSerialization); + W($"public List<{targetClassName}> {propertyName} {{ get; set; }}"); + } + W(); + + W($"public {className}()"); + WI("{"); + { + foreach (var fk in table.IncomingForeignKeys.OrderBy(t => t.ParentTable.Name).ThenBy(t => t.FromColumns.First().Name)) + { + W($"this.{fk.Metadata["Property"]} = new List<{fk.Metadata["Type"]}>();"); + } + } + DW("}"); + } + + } + DW("}"); + W("", !options.OneFilePerEntity); + } + + private string GetFkName(IEnumerable fromColumns) + { + var sb = new StringBuilder(); + foreach (var fromCol in fromColumns) + { + sb.Append(Inf.Pascalise(Inf.Singularise(RemoveSuffix(fromCol.Name)))); + } + return sb.ToString(); + } + + private void WriteModelBuilderConfigurations() + { + foreach (var table in Db.Schemas.SelectMany(s => s.Tables).OrderBy(t => t.Name)) + { + var className = table.Metadata["Class"]; + W($"modelBuilder.Entity<{className}>(entity =>"); + WI("{"); + { + W($"entity.ToTable(\"{table.Name}\", \"{table.Schema.Name}\");"); + + if (table.Columns.Count(c => c.IsKey) == 1) + { + var col = table.Columns.First(c => c.IsKey); + W($"entity.HasKey(e => e.{col.Metadata["Property"]})"); + SemiColon(); + } + else if (table.Columns.Count(c => c.IsKey) > 1) + { + W($"entity.HasKey(e => new {{ {string.Join(", ", table.Columns.Where(c => c.IsKey).Select(c => "e." + c.Metadata["Property"]))} }});"); + } + + WI(); + foreach (var column in table.Columns.Where(c => c.ForeignKey == null)) + { + var propertyName = column.Metadata["Property"]; + DW($"entity.Property(e => e.{propertyName})"); + IW($".HasColumnName(\"{column.Name}\")"); + W($".HasColumnType(\"{GetColumnType(column)}\")"); + if (!String.IsNullOrEmpty(column.DefaultValue)) + { + W($".HasDefaultValueSql(\"{RemoveExtraParantesis(column.DefaultValue)}\")"); + } + if (IsString(column.DataType) && !column.IsNullable) + { + W($".IsRequired()"); + } + if (IsString(column.DataType) && column.MaxLength != -1) + { + W($".HasMaxLength({column.MaxLength})"); + } + if (column.DataType == "uniqueidentifier") + { + W(".ValueGeneratedOnAdd()"); + } + if (column.IsIdentity) + { + W(".UseSqlServerIdentityColumn()"); + W(".ValueGeneratedOnAdd()"); + } + SemiColon(); + W(); + } + + foreach (var fk in table.ForeignKeys) + { + var propertyName = fk.Metadata["NavProperty"]; + var reverse = fk.Metadata["Property"]; + DW($"entity.HasOne(e => e.{propertyName})"); + IW($".WithMany(p => p.{reverse})"); + W($".HasForeignKey(p => p.{fk.FromColumns.First().Name})", fk.FromColumns.Count == 1); + W($".HasForeignKey(p => new {{{string.Join(", ", fk.FromColumns.Select(c => "p." + c.Metadata["Property"]))}}})", fk.FromColumns.Count > 1); + W($".OnDelete(DeleteBehavior.{GetBehavior(fk.DeleteAction)})"); + W($".HasConstraintName(\"{fk.Name}\")"); + SemiColon(); + W(); + } + Dedent(); + } + DW("});"); + } + } + + private string GetBehavior(ForeignKeyAction fkDeleteAction) + { + switch (fkDeleteAction) + { + case ForeignKeyAction.NoAction: + return "Restrict"; + case ForeignKeyAction.Cascade: + return "Cascade"; + case ForeignKeyAction.SetNull: + return "SetNull"; + case ForeignKeyAction.SetDefault: + return "ClientSetNull"; + default: + throw new ArgumentOutOfRangeException(nameof(fkDeleteAction), fkDeleteAction, null); + } + } + + private bool CheckClash(ref string propertyName, HashSet existingNames, ref int i) + { + if (existingNames.Contains(propertyName)) + { + propertyName += i++; + existingNames.Add(propertyName); + return true; + } + existingNames.Add(propertyName); + return false; + } + + private readonly HashSet stringTypes = new HashSet() { "nvarchar", "varchar", "char", "nchar" }; + private readonly HashSet binaryTypes = new HashSet() { "varbinary" }; + private readonly HashSet numericTypes = new HashSet() { "numeric", "decimal" }; + + private bool IsString(string dataType) + { + return stringTypes.Contains(dataType.ToLower()); + } + + private bool IsBinary(string dataType) + { + return binaryTypes.Contains(dataType.ToLower()); + } + + private bool IsNumeric(string dataType) + { + return numericTypes.Contains(dataType.ToLower()); + } + + private string RemoveSuffix(string name) + { + if (name.EndsWith("id", StringComparison.OrdinalIgnoreCase)) + return name.Substring(0, name.Length - 2); + return name; + } + + private string GetNullable(Column column) + { + if (column.IsNullable && Db.TypeMappings[column.DataType].GetTypeInfo().IsValueType && Db.TypeMappings[column.DataType] != typeof(char)) + { + return "?"; + } + return ""; + } + + private string GetClrTypeName(string sqlType) + { + string sysType = "string"; + if (Db.TypeMappings.ContainsKey(sqlType)) + { + var clrType = Db.TypeMappings[sqlType]; + if (clrType == typeof(char)) + return sysType; + + sysType = GetCharpTypeName(clrType); + } + return sysType; + } + + private string GetColumnType(Column column) + { + if (IsString(column.DataType)) + { + return $"{column.DataType}({(column.MaxLength == -1 || column.MaxLength >= 8000 ? "MAX" : column.MaxLength.ToString())})"; + } + + if (IsBinary(column.DataType)) + { + return $"{column.DataType}({(column.MaxLength == -1 ? "MAX" : column.MaxLength.ToString())})"; + } + + if (IsNumeric(column.DataType)) + { + return $"{column.DataType}({column.Precision}, {column.Scale})"; + } + + return column.DataType; + } + + private string RemoveExtraParantesis(string stringValue) + { + if (stringValue.StartsWith("(") && stringValue.EndsWith(")")) + return RemoveExtraParantesis(stringValue.Substring(1, stringValue.Length - 2)); + return stringValue; + } + + private void IgnoreUnsuportedColumns() + { + foreach (var schema in Db.Schemas) + foreach (var table in schema.Tables) + { + foreach (var column in table.Columns.ToList()) + if (!Db.TypeMappings.TryGetValue(column.DataType, out var type) || type == null) + { + ColorConsole.WriteLine($"Column [{schema.Name}].[{table.Name}].[{column.Name}] has unsupported data type [{column.DataType}] and was Ignored.", ConsoleColor.DarkYellow); + table.Columns.GetWritable().Remove(column.Name); + } + + if (!table.Columns.Any(c => c.IsKey)) + { + ColorConsole.WriteLine($"Table [{schema.Name}].[{table.Name}] does not have a primary key and was Ignored.", ConsoleColor.DarkYellow); + table.GetWritable().Remove(); + } + } + } + + private void FilterTables() + { + if (options.Tables.Count == 0 && String.IsNullOrEmpty(options.TablesRegex) && + options.ExcludedTables.Count == 0 && String.IsNullOrEmpty(options.ExcludedTablesRegex)) + return; + + var tables = new HashSet
( + Db.Schemas.SelectMany(s => s.Tables) + .Where(t => (options.Tables.Any(n => Util.TableNameMaches(t, n)) || + Util.TableNameMachesRegex(t, options.TablesRegex, true)) + && !options.ExcludedTables.Any(n => Util.TableNameMaches(t, n)) + && !Util.TableNameMachesRegex(t, options.ExcludedTablesRegex, false))); + + foreach (var schema in Db.Schemas) + foreach (var table in schema.Tables) + { + if (!tables.Contains(table)) + schema.Tables.GetWritable().Remove(table.Name); + } + } + } +} \ No newline at end of file diff --git a/Geco/Database/EntityFrameworkCoreReverseModelGeneratorOptions.cs b/Geco/Database/EntityFrameworkCoreReverseModelGeneratorOptions.cs new file mode 100644 index 0000000..4c741fa --- /dev/null +++ b/Geco/Database/EntityFrameworkCoreReverseModelGeneratorOptions.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; + +namespace Geco.Database +{ + public class EntityFrameworkCoreReverseModelGeneratorOptions + { + public string ConnectionName { get; set; } + public string Namespace { get; set; } + public bool OneFilePerEntity { get; set; } + public bool JsonSerialization { get; set; } + public bool GenerateComments { get; set; } + public bool UseSqlServer { get; set; } + public bool ConfigureWarnings { get; set; } + public bool DisableCodeWarnings { get; set; } + public bool GeneratedCodeAttribute { get; set; } + public bool NetCore { get; set; } + public string ContextName { get; set; } + public List Tables { get; } = new List(); + public string TablesRegex { get; set; } + public List ExcludedTables { get; } = new List(); + public string ExcludedTablesRegex { get; set; } + } +} \ No newline at end of file diff --git a/Geco/Database/SeedDataGenerator.cs b/Geco/Database/SeedDataGenerator.cs new file mode 100644 index 0000000..9f3ffcd --- /dev/null +++ b/Geco/Database/SeedDataGenerator.cs @@ -0,0 +1,233 @@ +using System; +using System.Collections.Generic; +using System.Data.SqlClient; +using System.IO; +using System.Linq; +using System.Text; +using Geco.Common; +using Geco.Common.SimpleMetadata; +using Microsoft.Extensions.Configuration; + +namespace Geco.Database +{ + /// + /// Generates seed scripts with merge statements for (Sql Server) + /// + [Options(typeof(SeedDataGeneratorOptions))] + public class SeedDataGenerator : BaseGeneratorWithMetadata + { + private readonly SeedDataGeneratorOptions options; + private readonly IConfigurationRoot configurationRoot; + + private readonly Func columnsFilter = c => !c.IsComputed; + private readonly Func whereClause = _ => null; + private readonly Func mergeFilter = _ => null; + + public SeedDataGenerator(SeedDataGeneratorOptions options, IMetadataProvider provider, IInflector inflector, IConfigurationRoot configurationRoot) : base(provider, inflector, options.ConnectionName) + { + this.options = options; + this.configurationRoot = configurationRoot; + } + + protected override void Generate() + { + if (options.Tables.Count == 0 && String.IsNullOrEmpty(options.TablesRegex) && + options.ExcludedTables.Count == 0 && String.IsNullOrEmpty(options.ExcludedTablesRegex)) + { + ColorConsole.WriteLine($"No tables were selected. Use options Tables, TableRegex, ExcludedTables or ExcludedTablesRegex to specity the tables for which Seed data will be generated ", ConsoleColor.Red); + return; + } + + var tables = Db.Schemas.SelectMany(s => s.Tables) + .Where(t => (options.Tables.Any(n => Util.TableNameMaches(t, n)) + || Util.TableNameMachesRegex(t, options.TablesRegex, true)) + && !options.ExcludedTables.Any(n => Util.TableNameMaches(t, n)) + && !Util.TableNameMachesRegex(t, options.ExcludedTablesRegex, false)).OrderBy(t => t.Schema.Name + "." + t.Name).ToArray(); + TopologicalSort(tables); + GenerateSeedFile(options.OutputFileName, tables); + + ColorConsole.WriteLine($"File: '{Path.GetFileName(options.OutputFileName)}' was generated.", ConsoleColor.Yellow); + } + + private void GenerateSeedFile(string file, IEnumerable
tables) + { + using (BeginFile(file)) + { + foreach (var table in tables) + { + if (table.Metadata["is_memory_optimized"] == "False" && table.Metadata["temporal_type"] == "0") + foreach (var tableValues in GetTableValues(table) + .Batch(options.ItemsPerStatement)) + GenerateTableSeed(table, tableValues); + } + } + } + + private void GenerateTableSeed(Table table, IEnumerable> rowValues) + { + var columns = table.Columns.Where(columnsFilter).ToList(); + var rows = rowValues.WithInfo().ToList(); + if (!rows.Any()) + return; + + + if (table.Columns.Any(c => c.IsIdentity)) + W($"SET IDENTITY_INSERT [{table.Schema.Name}].[{table.Name}] ON"); + + W($"MERGE [{table.Schema.Name}].[{table.Name}] AS Target"); + WI($"USING ( VALUES "); + int count = 0; + foreach (var rowData in rows) + { + W($"({CommaJoin(rowData.Item, QuoteValue)})"); + if (!rowData.IsLast) + Comma(); + count++; + } + DW($") As Source ({CommaJoin(columns, c => $"[{c.Name}]")}) "); + W($"ON {string.Join(" AND ", GetMatchColumns(table))}"); + if (table.Columns.Any(c => !c.IsKey)) + { + WI($"WHEN MATCHED {mergeFilter(table)}THEN UPDATE SET"); + + foreach (var columnInfo in table.Columns.Where(c => columnsFilter(c) && !c.IsKey).WithInfo()) + { + W($"Target.[{columnInfo.Item.Name}] = Source.[{columnInfo.Item.Name}]"); + if (!columnInfo.IsLast) + Comma(); + } + Dedent(); + } + W("WHEN NOT MATCHED THEN"); + IW($"INSERT ({CommaJoin(columns, c => $"[{c.Name}]")})"); + WD($"VALUES ({CommaJoin(columns, c => $"Source.[{c.Name}]")});"); + + if (table.Columns.Any(c => c.IsIdentity)) + W($"SET IDENTITY_INSERT [{table.Schema.Name}].[{table.Name}] OFF"); + + W("--GO"); + W(); + ColorConsole.WriteLine($"Generated merge script for {count} row{(count >= 2 ? "s" : "")} for [{table.Schema.Name}].[{table.Name}].", ConsoleColor.DarkYellow); + } + + private static IEnumerable GetMatchColumns(Table table) + { + if (table.Columns.Any(c => c.IsKey)) + return table.Columns.Where(c => c.IsKey).Select(c => $"Source.[{c.Name}] = Target.[{c.Name}]"); + else + return table.Columns.Select(c => $"Source.[{c.Name}] = Target.[{c.Name}]"); + } + + private IEnumerable> GetTableValues(Table table) + { + var connectionString = configurationRoot.GetConnectionString(ConnectionName); + using (var cnn = new SqlConnection(connectionString)) + using (var cmd = new SqlCommand()) + { + var columns = table.Columns + .Where(columnsFilter) + .ToList(); + var where = whereClause(table); + cmd.CommandText = $"SELECT {CommaJoin(columns, ColumnExpression)} FROM [{table.Schema.Name}].[{table.Name}] WHERE {(String.IsNullOrEmpty(where) ? "1=1" : where)}"; + cnn.Open(); + cmd.Connection = cnn; + using (var rdr = cmd.ExecuteReader()) + { + while (rdr.Read()) + { + var arr = new object[rdr.FieldCount]; + rdr.GetValues(arr); + yield return arr; + } + } + } + } + + private string ColumnExpression(Column column) + { + if (!Db.TypeMappings.ContainsKey(column.DataType)) + { + return $"CAST([{column.Name}] as NVARCHAR(MAX)) as[{column.Name}]"; + } + return $"[{column.Name}]"; + } + + + private string QuoteValue(Object value) + { + if (value == null || value == DBNull.Value) + return "NULL"; + if (value is bool) + return ((bool)value) ? "1" : "0"; + if (value is string || value is Guid) + return "N'" + value.ToString().Trim().Replace("'", "''") + "'"; + if (value is DateTime) + return "N'" + ((DateTime)value).ToString("yyyy-MM-dd HH:mm:ss:fff") + "'"; + if (value is DateTimeOffset) + return "N'" + ((DateTimeOffset)value).ToString("yyyy-MM-dd HH:mm:ss.fffffff K") + "'"; + if (value is TimeSpan t) + return "N'" + t + "'"; + + var bs = value as byte[]; + if (bs != null) + { + var sb = new StringBuilder(bs.Length * 2 + 30); + sb.Append("CONVERT(VARBINARY(MAX),N'"); + foreach (var b in bs) + { + sb.Append(b.ToString("X2")); + } + sb.Append("',2)"); + return sb.ToString(); + } + + return value.ToString(); + } + + + private void TopologicalSort(IList
tables) + { + bool sorted = false; + var comparer = new SeedDataGenerator.TopologicalComparer(); + var iterations = 0; + while (!sorted) + { + sorted = true; + iterations++; + if (iterations > 100) + throw new InvalidOperationException("Cannot sort tables due to cyclic relation between selected tables."); + + for (int i = 0; i < tables.Count - 1; i++) + for (int j = i + 1; j < tables.Count; j++) + { + if (comparer.Compare(tables[i], tables[j]) > 0) + { + var aux = tables[i]; + tables[i] = tables[j]; + tables[j] = aux; + sorted = false; + } + } + } + } + + private class TopologicalComparer : IComparer
+ { + public int Compare(Table source, Table target) + { + if (source == null || target == null) + return 0; + + // Source goes before any table that references is + if (source.IncomingForeignKeys.Any(fk => fk.ParentTable == target) || target.ForeignKeys.Any(fk => fk.TargetTable == source)) + return -1; + + // Source goes after any table which it references + if (source.ForeignKeys.Any(fk => fk.TargetTable == target) || target.IncomingForeignKeys.Any(fk => fk.ParentTable == source)) + return 1; + + return 0; + } + } + } +} \ No newline at end of file diff --git a/Geco/Database/SeedDataGeneratorOptions.cs b/Geco/Database/SeedDataGeneratorOptions.cs new file mode 100644 index 0000000..4e4e843 --- /dev/null +++ b/Geco/Database/SeedDataGeneratorOptions.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace Geco.Database +{ + public class SeedDataGeneratorOptions + { + public string ConnectionName { get; set; } + public string OutputFileName { get; set; } + public List Tables { get; } = new List(); + public string TablesRegex { get; set; } + public List ExcludedTables { get; } = new List(); + public string ExcludedTablesRegex { get; set; } + public int ItemsPerStatement { get; set; } = 1000; + } +} \ No newline at end of file diff --git a/Geco/Database/SeedScriptRunner.cs b/Geco/Database/SeedScriptRunner.cs new file mode 100644 index 0000000..fadc76d --- /dev/null +++ b/Geco/Database/SeedScriptRunner.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Data.SqlClient; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; +using Geco.Common; +using Microsoft.Extensions.Configuration; +using static System.ConsoleColor; +using static Geco.Common.ColorConsole; + +namespace Geco.Database +{ + [Options(typeof(SeedScriptRunnerOptions))] + public class SeedScriptRunner : BaseGenerator + { + private readonly SeedScriptRunnerOptions options; + private readonly IConfigurationRoot configurationRoot; + + public SeedScriptRunner(SeedScriptRunnerOptions options, IConfigurationRoot configurationRoot, IInflector inf) : base(inf) + { + this.options = options; + this.configurationRoot = configurationRoot; + } + + protected override void Generate() + { + string currentFileNamme = null; + try + { + foreach (var fileName in options.Files) + { + currentFileNamme = fileName; + RunScripts(Path.Combine(BaseOutputPath, fileName)); + } + } + catch (Exception ex) + { + WriteLine(("Error running merge script:", Red), (currentFileNamme, Yellow)); + WriteLine($"{ex}", DarkRed); + } + } + + private void RunScripts(string file) + { + WriteLine($"Running scripts from: {(file, Yellow)}", Gray); + var connectionString = configurationRoot.GetConnectionString(options.ConnectionName); + if (!File.Exists(file)) + { + + WriteLine($"File:[{(Path.GetFullPath(file), Yellow)}] does not exit on disk. {("Skipping!", Red)}", Gray); + return; + } + using (var f = File.OpenText(file)) + using (var cnn = new SqlConnection(connectionString)) + { + cnn.Open(); + using (var tran = cnn.BeginTransaction()) + { + foreach (var commandText in GetCommands(f)) + { + using (var cmd = new SqlCommand(commandText.Command, cnn, tran){CommandTimeout = options.CommandTimeout}) + { + Write($"{commandText.TableName}", Cyan); + var affectedRows = cmd.ExecuteNonQuery(); + WriteLine($" ({affectedRows} row(s) affected)", White); + } + } + tran.Commit(); + Console.WriteLine(); + Console.WriteLine(); + } + } + } + + private IEnumerable<(string Command, string TableName)> GetCommands(StreamReader streamReader) + { + var tableName = ""; + var buffer = new StringBuilder(); + while (!streamReader.EndOfStream) + { + var line = streamReader.ReadLine(); + if (String.IsNullOrWhiteSpace(line)) + continue; + + var tableMatch = Regex.Match(line, @"\s*MERGE\s*(.*)\s*AS"); + if (tableMatch.Success) + tableName = tableMatch.Groups[1].Value; + + if (Regex.IsMatch(line, @"^\s*-{0,2}GO\s*$")) + { + if (buffer.Length > 0) + yield return (buffer.ToString(), tableName); + buffer.Clear(); + tableName = ""; + } + else + buffer.AppendLine(line); + } + } + } +} \ No newline at end of file diff --git a/Geco/Database/SeedScriptRunnerOptions.cs b/Geco/Database/SeedScriptRunnerOptions.cs new file mode 100644 index 0000000..05f7943 --- /dev/null +++ b/Geco/Database/SeedScriptRunnerOptions.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Geco.Database +{ + public class SeedScriptRunnerOptions + { + public string ConnectionName { get; set; } + public List Files { get; } = new List(); + public int CommandTimeout { get; set; } = 60; + } +} \ No newline at end of file diff --git a/Geco/Geco.Targets b/Geco/Geco.Targets new file mode 100644 index 0000000..63e4ba0 --- /dev/null +++ b/Geco/Geco.Targets @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/Geco/Geco.csproj b/Geco/Geco.csproj new file mode 100644 index 0000000..f3b929f --- /dev/null +++ b/Geco/Geco.csproj @@ -0,0 +1,56 @@ + + + Exe + netcoreapp3.1 + false + false + As simple as it gets code generator, which is a console application that runs code generation tasks written in C# using interpolated strings. + iQuarc + Code-Generator Code-Generation Template-Engine CSharp Interpolated-Strings + iQuarc 2017 + https://github.com/iQuarc/Geco/blob/master/LICENSE.md + https://github.com/iQuarc/Geco + https://github.com/iQuarc/Geco/blob/master/.Tools/Geco/Logo.png?raw=true + https://github.com/iQuarc/Geco.git + iQuarc + Geco.Core + Geco Core + 1.0.9 + false + + + TRACE;DEBUG + latest + pdbonly + true + bin\ + + + + + + + + + + + + PreserveNewest + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LICENSE b/Geco/LICENSE.txt similarity index 99% rename from LICENSE rename to Geco/LICENSE.txt index 261eeb9..83f5ff0 100644 --- a/LICENSE +++ b/Geco/LICENSE.txt @@ -178,7 +178,7 @@ APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" + boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright © iQuarc 2017 - Pop Catalin Sever Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Geco/Logo.png b/Geco/Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..cce2a562928256ebc00fb1d09d968a0fa973f309 GIT binary patch literal 80015 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 K4oTtlJxTx9PUN^|B;tk(hzCLXI$)DF!w{Du( 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 literal 0 HcmV?d00001 diff --git a/Geco/Program.cs b/Geco/Program.cs new file mode 100644 index 0000000..9215dc6 --- /dev/null +++ b/Geco/Program.cs @@ -0,0 +1,288 @@ +// Copyright © iQuarc 2017 - Pop Catalin Sever +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using Geco.Common; +using Geco.Common.MetadataProviders.SqlServer; +using Geco.Common.SimpleMetadata; +using Geco.Config; +using Microsoft.Extensions.CommandLineUtils; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using static Geco.Common.ColorConsole; +using static System.ConsoleColor; + +namespace Geco +{ + ///

+ /// As simple as it gets code generator, which is a console application that runs code generation tasks written in C#. + /// + /// + /// Task discovery is done at runtime by scanning current assembly for all the types that implement interfaces. + /// The tasks are resolved using a . Generator tasks can declare a options class using the + /// in order to have the options be read from the appsettings.json configuration file and registered in the + /// + public class Program + { + private Dictionary runnableTypes; + private IServiceCollection serviceCollection; + private RootConfig rootConfig; + private IConfigurationRoot configurationRoot; + private bool Interactive { get; set; } + static int Main(string[] args) + { + var p = new Program(); + return p.Run(args); + } + + private int Run(string[] args) + { + try + { + Directory.SetCurrentDirectory(AppDomain.CurrentDomain.BaseDirectory); + + var app = new CommandLineApplication(); + app.Name = "Geco"; + app.HelpOption("-?|-h|--help"); + + app.Command("run", command => + { + command.HelpOption("-?|-h|--help"); + var taskList = command.Option("-tl|--tasklist " + , "The name of the task list from appsettings.json to execute. The task list is an array of task names.", CommandOptionType.SingleValue); + var taskNames = command.Option("-tn|--taskname " + , "The name(s) of the tasks to execute. The task names is an list of task names parameters -tn -tn .", CommandOptionType.MultipleValue); + command.OnExecute(() => + { + ConfigureServices(app.RemainingArguments.ToArray()); + if (taskList.HasValue()) + RunTaskListFromConfig(taskList.Value()); + if (taskNames.HasValue()) + RunTasksList(taskNames.Values); + return 0; + }); + }); + app.OnExecute(() => + { + WriteLogo(); + ConfigureServices(app.RemainingArguments.ToArray()); + WriteLine($"<< Geco is running in interactive mode! >>", Yellow); + InteractiveLoop(); + WriteLine($"C ya!", Yellow); + return 0; + }); + + return app.Execute(args); + } + catch (Exception ex) + { + WriteLine($"==============================================", Red); + WriteLine($"=== Geco stopped due to error:", Red); + WriteLine($"{ex}", Yellow); + WriteLine($"==============================================", Red); + return -1; + } + } + + private void InteractiveLoop() + { + Interactive = true; + Func displayAndRun; + do + { + displayAndRun = BuildMenu(); + } while (displayAndRun()); + } + + private Func BuildMenu() + { + Console.WriteLine(); + WriteLine($"Select option {("(then press Enter)", Gray)}:", White); + var actions = new Dictionary(); + foreach (var taskInfo in rootConfig.Tasks.WithInfo()) + { + var taskNr = (taskInfo.Index + 1).ToString(); + WriteLine(($"{taskNr}. ", White), ($"{taskInfo.Item.Name}", Blue)); + actions.Add(taskNr, () => RunTask(taskInfo.Item)); + } + WriteLine(("q. ", White), ("Quit", ConsoleColor.Yellow)); + Write($">>", White); + + bool Choose() + { + var command = Console.ReadLine(); + if (string.Equals(command, "q", StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine(); + return false; + } + if (actions.TryGetValue(command, out var action)) + action(); + return true; + } + return Choose; + } + + private void ConfigureServices(string[] args) + { + configurationRoot = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json") + .AddCommandLine(args) + .Build(); + + //setup the DI + serviceCollection = new ServiceCollection() + .AddLogging() + .AddSingleton(configurationRoot) + .AddOptions() + .AddSingleton() + .AddSingleton(); + + ScanTasks(); + } + + private static void WriteLogo() + { + var version = Assembly.GetEntryAssembly().GetName().Version; + WriteLine($"********************************************************", Blue); + WriteLine($"* ** Geco v{version} ** *", Blue); + WriteLine($"* *", Blue); + WriteLine(("*", Blue), (@" .)/ )/, ", Green), (" Copyright (c) *", Blue)); + WriteLine(("*", Blue), (@" /`-._,-'`._,@`-, ", Green), (" iQuarc 2017 *", Blue)); + WriteLine(("*", Blue), (@" , _,-=\,-.__,-.-.__@/ ", Green), (" - Generator Console - *", Blue)); + WriteLine(("*", Blue), (@" (_,' )\` '(` ", Green), (" - Geco - *", Blue)); + WriteLine($"* *", Blue); + WriteLine($"* {("https://github.com/iQuarc/Geco.Core", DarkMagenta)} *", Blue); + WriteLine($"********************************************************", Blue); + } + + private void ScanTasks() + { + rootConfig = new RootConfig(); + configurationRoot.Bind(rootConfig); + + runnableTypes = Assembly.GetAssembly(typeof(Program)) + .GetTypes() + .Where(t => typeof(IRunnable).IsAssignableFrom(t)) + .Where(t => !t.IsAbstract && !t.IsGenericTypeDefinition && t.GetConstructors().Any()) + .ToDictionary(t => t.FullName); + + foreach (var runnableType in runnableTypes.Values) + { + serviceCollection.Add(new ServiceDescriptor(runnableType, runnableType, ServiceLifetime.Transient)); + } + + //RootConfig rootConfig + foreach (var taskConfig in rootConfig.Tasks.WithInfo()) + { + if (!runnableTypes.ContainsKey(taskConfig.Item.TaskClass)) + { + WriteLine($"Task configuration for:[{taskConfig.Item.TaskClass}] has no corresponding service to be applied to", DarkYellow); + continue; + } + var taskType = runnableTypes[taskConfig.Item.TaskClass]; + var optionsAttribute = (OptionsAttribute)taskType.GetCustomAttribute(typeof(OptionsAttribute)); + if (optionsAttribute != null) + { + taskConfig.Item.ConfigIndex = taskConfig.Index; + var options = Activator.CreateInstance(optionsAttribute.OptionType); + configurationRoot.GetSection($"Tasks:{taskConfig.Item.ConfigIndex}:Options").Bind(options); + serviceCollection.Replace(new ServiceDescriptor(optionsAttribute.OptionType, options)); + } + } + } + + private void RunTaskListFromConfig(string taskListName) + { + var taskList = new List(); + configurationRoot.Bind(taskListName, taskList); + RunTasksList(taskList); + } + + private void RunTasksList(IEnumerable taskList) + { + foreach (var taskName in taskList) + { + var task = rootConfig.Tasks.Find(t => t.Name == taskName); + task.OutputToConsole = false; + RunTask(task); + } + } + + private void RunTask(TaskConfig itemInfo) + { + var sw = new Stopwatch(); + try + { + WriteLine($"--------------------------------------------------------", Yellow); + WriteLine(("*** Starting ", Yellow), ($" {itemInfo.Name} ", Blue)); + + var taskType = runnableTypes[itemInfo.TaskClass]; + var optionsAttribute = (OptionsAttribute)taskType.GetCustomAttribute(typeof(OptionsAttribute)); + if (optionsAttribute != null) + { + var options = Activator.CreateInstance(optionsAttribute.OptionType); + configurationRoot.GetSection($"Tasks:{itemInfo.ConfigIndex}:Options").Bind(options); + serviceCollection.Replace(new ServiceDescriptor(optionsAttribute.OptionType, options)); + } + + using (var provider = serviceCollection.BuildServiceProvider()) + { + sw.Start(); + var task = (IRunnable)provider.GetService(taskType); + if (task is IOutputRunnable to) + { + to.OutputToConsole = itemInfo.OutputToConsole; + to.BaseOutputPath = itemInfo.BaseOutputPath; + to.CleanFilesPattern = itemInfo.CleanFilesPattern; + to.Interactive = Interactive; + } + + if (task is IRunableConfirmation co && Interactive) + { + sw.Stop(); + if (!co.GetUserConfirmation()) + { + WriteLine(("*** Task was canceled ", Yellow), ($" {itemInfo.Name} ", Blue)); + return; + } + sw.Start(); + } + try + { + task.Run(); + } + catch (OperationCanceledException) + { + WriteLine(("*** Task was aborted ", Yellow), ($" {itemInfo.Name} ", Blue)); + } + } + } + catch (Exception ex) when (Interactive) + { + WriteLine($"Error running {(itemInfo.Name, Blue)}: Error:{(ex.Message, Red)}", DarkRed); + WriteLine($"Detail: {ex}", DarkYellow); + } + sw.Stop(); + Console.WriteLine(); + WriteLine(("Task", Yellow), ($" {itemInfo.Name} ", Blue), ("completed in", Yellow), ($" {sw.ElapsedMilliseconds} ms", Green)); + } + } +} \ No newline at end of file diff --git a/Geco/Properties/launchSettings.json b/Geco/Properties/launchSettings.json new file mode 100644 index 0000000..f5dad07 --- /dev/null +++ b/Geco/Properties/launchSettings.json @@ -0,0 +1,7 @@ +{ + "profiles": { + "Geco": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/Geco/Util/Util.cs b/Geco/Util/Util.cs new file mode 100644 index 0000000..2a44ea1 --- /dev/null +++ b/Geco/Util/Util.cs @@ -0,0 +1,30 @@ +using System; +using System.Text.RegularExpressions; +using Geco.Common.SimpleMetadata; + +namespace Geco +{ + public class Util + { + public static bool TableNameMachesRegex(Table table, string tablesRegex, bool onNull) + { + if (String.IsNullOrWhiteSpace(tablesRegex)) + return onNull; + return + ( + Regex.IsMatch(table.Name, tablesRegex) || + Regex.IsMatch($"[{table.Name}]", tablesRegex) || + Regex.IsMatch($"{table.Schema.Name}.{table.Name}", tablesRegex) || + Regex.IsMatch($"[{table.Schema.Name}].[{table.Name}]", tablesRegex) + ); + } + + public static bool TableNameMaches(Table table, string name) + { + return string.Equals(name, table.Name, StringComparison.OrdinalIgnoreCase) || + string.Equals(name, $"[{table.Name}]", StringComparison.OrdinalIgnoreCase) || + string.Equals(name, $"{table.Schema.Name}.{table.Name}", StringComparison.OrdinalIgnoreCase) || + string.Equals(name, $"[{table.Schema.Name}].[{table.Name}]", StringComparison.OrdinalIgnoreCase); + } + } +} \ No newline at end of file diff --git a/Geco/appsettings.json b/Geco/appsettings.json new file mode 100644 index 0000000..246d778 --- /dev/null +++ b/Geco/appsettings.json @@ -0,0 +1,63 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Integrated Security=True;Initial Catalog=AdventureWorks;Data Source=.\\SQLEXPRESS;" + }, + // Uncomment task to run it during build + "RunAtBuildTasks": [ + //"Generate EF Core model" + ], + "Tasks": [ + { + "Name": "Generate EF Core model", + "TaskClass": "Geco.Database.EntityFrameworkCoreReverseModelGenerator", + "BaseOutputPath": "..\\..\\GeneratedModel\\", + "OutputToConsole": "false", + "CleanFilesPattern": "*.cs", + "Options": { + "ConnectionName": "DefaultConnection", + "Namespace": "Geco.Tests.Database.Model", + "OneFilePerEntity": true, + "JsonSerialization": false, + "GenerateComments": true, + "UseSqlServer": true, + "ConfigureWarnings": true, + "DisableCodeWarnings": true, + "GeneratedCodeAttribute": true, + "NetCore": true + } + }, + { + "Name": "Clean database", + "TaskClass": "Geco.Database.DatabaseCleaner", + "OutputToConsole": "true", + "Options": { + "ConnectionName": "DefaultConnection" + } + }, + { + "Name": "Generate seed data", + "TaskClass": "Geco.Database.SeedDataGenerator", + "BaseOutputPath": "..\\..\\GeneratedModel\\", + "OutputToConsole": "false", + "Options": { + "ConnectionName": "DefaultConnection", + "OutputFileName": "DataSeed.sql", + "Tables": [], + "ExcludedTables": [], + "TablesRegex": "\\..*", + "ItemsPerStatement": 500 + } + }, + { + "Name": "Run seed scripts", + "TaskClass": "Geco.Database.SeedScriptRunner", + "BaseOutputPath": "..\\..\\GeneratedModel\\", + "OutputToConsole": "false", + "Options": { + "ConnectionName": "DefaultConnection", + "Files": [ "DataSeed.sql" ], + "CommandTimeout": 300 + } + } + ] +} diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..35ec662 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright © iQuarc 2020 - Pop Catalin Sever + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 8cb6b91..d2a7301 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,217 @@ -# Geco.Core -Core Repository for Geco, A Simple code generator based on a console project, running on .Net core and using C# interpolated strings. Format suitable to be used a s git submodule +# Geco, (Ge)nerator (Co)nsole +Simple code generator based on a console project, running on .Net Core and using C# interpolated strings. + +Geco runs on .Net Core 3.1. [Download and Install](http://dot.net) + +What's the reasoning behind this utility? + +One of the most popular code generation tools for Visual Studio over the years was T4 (Text Template Transformation Toolkit), which saw a great deal of success for simple to complex code generation tasks. +Scott Hanselman has a nice [blog post](https://www.hanselman.com/blog/T4TextTemplateTransformationToolkitCodeGenerationBestKeptVisualStudioSecret.aspx) about it. However, since the advent of **.Net Core** and .Net going cross platform there is no simple way to generate code or other artifacts that would also work in Visual Studio Code (or other code editors). + +This is where the idea for this simple utility came from. Have a small utility for code generation that is **debuggable** *(one of the long standing issues of T4)*, provides some level of **intellisense**, can be run at build time, is higly customizable, generic and has an as small as possible footprint. + +## Installation + +Geco can be installed as a [Visual Studio Project Template](https://marketplace.visualstudio.com/items?itemName=catalinpop.Geco101) or as `dotnet new` template. + +To install Geco as a `dotnet new template` run the following command on a console or terminal: + +```Batchfile +dotnet new -i iQuarc.Geco.CSharp +``` + +Then create a new project using the newly installed template: + +```Batchfile +dotnet new geco +``` + +To create a Geco project when installed as a Visual Studio Project template, Select File -> New project select the Geco template. + +### A more indetail Getting Sstarted with Visual Studio walkthrough can be found on the Wiki: [Geting Started](https://github.com/iQuarc/Geco/wiki) + +Geco can also be installed as a Git submodule. `Geco.Core` is the git repository which should be added. The recomendation is to add it to a folder called .Tools + +```Batchfile +git submodule add https://github.com/iQuarc/Geco.Core.git .Tools +``` + +## Code Generation examples + +Bellow are some snippets from the included code generation tasks that are in the default Geco package. + +Next snippet is from the included Entity Framework Core Reverse model generator [EntityFrameworkCoreReverseModelGenerator.cs](https://github.com/iQuarc/Geco/blob/master/src/Geco/Database/EntityFrameworkCoreReverseModelGenerator.cs), which exemplifies part of the code generation: + +![Geco Preview1](https://github.com/iQuarc/Geco/blob/dev/dist/GecoResources/PreviewImage.JPG?raw=true) + +This snippet is from the SQL Seed Scrip Generator [SeedDataGenerator.cs](https://github.com/iQuarc/Geco/blob/master/src/Geco/Database/SeedDataGenerator.cs), which generates SQL Merge scripts for Seed data: + +![Geco Preview2](https://github.com/iQuarc/Geco/blob/dev/dist/GecoResources/PreviewImage2.JPG?raw=true) + +Next screen shot shows Geco running in interactive mode (as a Console App), and the menu that is presented to the user: + +![Geco Preview3](https://github.com/iQuarc/Geco/blob/dev/dist/GecoResources/PreviewImage3.JPG?raw=true) + + +## Description + +Geco uses C# 6.0 string interpolation as a template engine for code generation, in order to allow: + + - Easy customization of templates (Simply edit the .cs file) + - Easy debugging (Place a breakpoint an run) + - Easy extensibility (Add a new task by creating a new C# class and implement a simple interface `IRunnable`) + +Geco uses task discovery at runtime and each task is configured using Dependency Injection. The generation tasks can be run during build or from interactive mode (Debug Run). + +## Included generators + +The following generator tasks are included in current version. + + - Entity Framework Core 1.1, 2.0 Reverse model generator + - SQL Seed data script generator (Generates MERGE scripts) + - SQL Script runner + - Database cleaner + +## Customizing + +1. The first customization option involves changing task parameters from `appsettings.json` configuration file. Each Geco task defines its own set of parameters (see the corresponding Options class): + +```JSon +{ + "ConnectionStrings": { + "DefaultConnection": "Integrated Security=True;Initial Catalog=AdventureWorks;Data Source=.\\SQLEXPRESS;" + }, + "RunAtBuildTasks": [ + "Generate EF Core model" + ], + "Tasks": [ + { + "Name": "Generate EF Core model", + "TaskClass": "Geco.Database.EntityFrameworkCoreReverseModelGenerator", + "BaseOutputPath": "..\\..\\Generated\\Model\\", + "OutputToConsole": "false", + "CleanFilesPattern": "*.cs", + "Options": { + "ConnectionName": "DefaultConnection", + "Namespace": "Model", + "OneFilePerEntity": true, + "JsonSerialization": true, + "GenerateComments": true, + "UseSqlServer": true, + "ConfigureWarnings": true, + "GeneratedCodeAttribute": false, + "NetCore": true, + "ContextName":"AdventureworksContext" + } + } + ] +} +``` + +2. By customizing existing templates directly. + +3. By creating new code generation tasks as a C# class which implements the `IRunnable` interface. + +`IRunnable` is a single method interface containing the `Run` method: + + ```CSharp + /// + /// Represents a runable task, for code generation or purposes or others + /// + public interface IRunnable + { + /// + /// Invoked when this task is executed + /// + void Run(); + } + ``` + or by deriving from one of the helper base classes `BaseGenerator` or `BaseGeneratorWithMetadata`. + +A code generation class can be accompanied by an Options POCO class to hold customization parameters from `appsettings.json` + +Example: + +```CSharp + public class SeedDataGeneratorOptions + { + public string ConnectionName { get; set; } + public string OutputFileName { get; set; } + public List Tables { get; } = new List(); + public string TablesRegex { get; set; } + public List ExcludedTables { get; } = new List(); + public string ExcludedTablesRegex { get; set; } + public int ItemsPerStatement { get; set; } = 1000; + } + /// + /// Generates seed scripts with merge statements for (Sql Server) + /// + [Options(typeof(SeedDataGeneratorOptions))] + public class SeedDataGenerator : BaseGeneratorWithMetadata + { + // ... + } + +``` +## Roadmap + + - ASP.Net MVC Template (scaffolds controllers and views) + - ASP.Net + Angular scaffolder (scaffolds Web Api and components) + - SQL Sever to SQL Compact data sync template (Api to synchronize schema compatible SQL Server and SQL Compact databases including Web Api and .Net client methods) + +## Version History + +**Version 1.0.9** + - Updated Geco to .Net Core 3.1 + +**Version 1.0.8** + - Fix VS dependency version + +**Version 1.0.7** + - Add support for VS 2019 + - Add support for Ignored columns in Seed Data generator (Sergiu Damian) + - Add support in Seed Data Generator to order rows by clustered Index (Sergiu Damian) + +**Version 1.0.6** + - Update to *netcoreapp2.1* + +**Version 1.0.5** + - Fixed pluralization bug + - Fixed issue with current directory, not being set to bin\ + - Fixed nullable date time not being generated correctly + +**Version 1.0.4** + - Replaced `EnglishInflector` with Humanizer based version + - Improved metadata API to allow easier removal of tables and columns along with dependent objects + - Improved `SeedScriptRunner` error message when the seed file does not exist + +**Version 1.0.3** + - Fix SqlServerMetadataProvider bug with global database triggers + - Added display of errors in interactive + +**Version 1.0.2** + + - Added constraint delete behavior support + - Modified logo + +**Version 1.0.1** + + - Changed Entity Framework Core generator to allow multi column foreign keys + - Fixed MetaData for MaxLength nvarchar and nchar columns + - Removed tables without primary key from generated model + - Switched to semver + +**Version 1.0.0.2** + +- Changed user confirmation mechanism +- Added user confirmation for file cleaning +- Added more options for color output to console, formattable strings with tuple (value, ConsoleColor) parameters + +**Version 1.0.0.1** + +- First release as a `nuget dotnet` new template + +**Version 1.0.0.0-beta** + +- Initial Version +- Contains code generation core functionality and SimpleMetadata model for databases