diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2f5fbf7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,263 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+# User-specific files
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+
+# Visual Studio 2015 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# DNX
+project.lock.json
+project.fragment.lock.json
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_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
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# TODO: 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/
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/packages/*
+# except build/, which is used as an MSBuild target.
+!**/packages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/packages/repositories.config
+# NuGet v3's project.json files produces more ignoreable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+node_modules/
+orleans.codegen.cs
+
+# 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/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+# CodeRush
+.cr/
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+#Mac Store files
+.DS_Store
diff --git a/Logo_Symbol_Black_Outline.png b/Logo_Symbol_Black_Outline.png
new file mode 100644
index 0000000..b30f9bc
Binary files /dev/null and b/Logo_Symbol_Black_Outline.png differ
diff --git a/MySqlPlus.Example/MySqlPlus.Example.csproj b/MySqlPlus.Example/MySqlPlus.Example.csproj
new file mode 100644
index 0000000..724234d
--- /dev/null
+++ b/MySqlPlus.Example/MySqlPlus.Example.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ net6.0
+ disable
+ disable
+ MontoyaTech.MySqlPlus.Example
+ MontoyaTech.MySqlPlus.Example
+
+
+
+
+
+
+
diff --git a/MySqlPlus.Example/Program.cs b/MySqlPlus.Example/Program.cs
new file mode 100644
index 0000000..ae34b7e
--- /dev/null
+++ b/MySqlPlus.Example/Program.cs
@@ -0,0 +1,34 @@
+using System;
+
+namespace MontoyaTech.MySqlPlus.Example
+{
+ public class Program
+ {
+ public class Car : MySqlRow
+ {
+ [MySqlRowId]
+ [MySqlColumn]
+ public ulong Id = 0;
+
+ [MySqlColumn("make")]
+ [MySqlColumnAlias("Make")]
+ public string Make = null;
+
+ [MySqlColumn("mode")]
+ public string Model = null;
+
+ [MySqlColumn("dateCreated", typeof(DateTime2UnixConverter))]
+ public DateTime DateCreated = DateTime.UtcNow;
+ }
+
+ public class DateTime2UnixConverter
+ {
+
+ }
+
+ public static void Main(string[] args)
+ {
+ Console.WriteLine("Hello, World!");
+ }
+ }
+}
\ No newline at end of file
diff --git a/MySqlPlus.Tests/MySqlPlus.Tests.csproj b/MySqlPlus.Tests/MySqlPlus.Tests.csproj
new file mode 100644
index 0000000..6183c01
--- /dev/null
+++ b/MySqlPlus.Tests/MySqlPlus.Tests.csproj
@@ -0,0 +1,30 @@
+
+
+
+ net6.0
+ disable
+ disable
+ false
+ MontoyaTech.MySqlPlus.Tests
+ MontoyaTech.MySqlPlus.Tests
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
diff --git a/MySqlPlus.sln b/MySqlPlus.sln
new file mode 100644
index 0000000..221c398
--- /dev/null
+++ b/MySqlPlus.sln
@@ -0,0 +1,37 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.4.33213.308
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MySqlPlus", "MySqlPlus\MySqlPlus.csproj", "{EC8F451D-E711-4FF2-87F5-B65F21570785}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MySqlPlus.Example", "MySqlPlus.Example\MySqlPlus.Example.csproj", "{2DC4B2B2-D391-475C-A792-D451A765259E}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MySqlPlus.Tests", "MySqlPlus.Tests\MySqlPlus.Tests.csproj", "{D10C9285-D11A-43FC-A234-1D5846CD97F8}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {EC8F451D-E711-4FF2-87F5-B65F21570785}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EC8F451D-E711-4FF2-87F5-B65F21570785}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EC8F451D-E711-4FF2-87F5-B65F21570785}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EC8F451D-E711-4FF2-87F5-B65F21570785}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2DC4B2B2-D391-475C-A792-D451A765259E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2DC4B2B2-D391-475C-A792-D451A765259E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2DC4B2B2-D391-475C-A792-D451A765259E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2DC4B2B2-D391-475C-A792-D451A765259E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D10C9285-D11A-43FC-A234-1D5846CD97F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D10C9285-D11A-43FC-A234-1D5846CD97F8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D10C9285-D11A-43FC-A234-1D5846CD97F8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D10C9285-D11A-43FC-A234-1D5846CD97F8}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {AC9C73B2-A516-4430-9434-CB2AF7B47147}
+ EndGlobalSection
+EndGlobal
diff --git a/MySqlPlus/GlobalTimeStamp.cs b/MySqlPlus/GlobalTimeStamp.cs
new file mode 100644
index 0000000..648cd8f
--- /dev/null
+++ b/MySqlPlus/GlobalTimeStamp.cs
@@ -0,0 +1,165 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MontoyaTech.MySqlPlus
+{
+ ///
+ /// This timestamp runs on its own and it
+ /// uses very little CPU usage to keep track of time. This is a very
+ /// useful system. This timestamp is in elapsed miliseconds.
+ ///
+ internal class GlobalTimeStamp : IDisposable
+ {
+ ///
+ /// This is the instance of the time stamp which is used
+ /// to auto start this.
+ ///
+ public static GlobalTimeStamp instance = new GlobalTimeStamp();
+
+ ///
+ /// The Current Timestamp, elapsed time since the begining in Miliseconds.
+ ///
+ public static ulong Current
+ {
+ get
+ {
+ return instance._Current;
+ }
+ }
+
+ ///
+ /// The StopWatch that the TimeStamp uses
+ ///
+ private Stopwatch Watch = null;
+
+ ///
+ /// Whether or not this Timestamp Is Running.
+ ///
+ private bool Running = false;
+
+ ///
+ /// The Refresh Thread for this GlobalTimeStamp.
+ ///
+ private Thread RefreshThread = null;
+
+ ///
+ /// The Current Timestamp, elapsed time since the begining in Miliseconds.
+ ///
+ public ulong _Current = 777; //Initialize this value so we have something.
+
+ ///
+ /// A new instance of the GlobalTimeStamp
+ ///
+ public GlobalTimeStamp()
+ {
+ //Start the watch.
+ this.Watch = new Stopwatch();
+ this.Watch.Start();
+
+ //Setup the Refresh Thread
+ this.Running = true;
+ this.RefreshThread = new Thread(Refresh);
+ this.RefreshThread.IsBackground = true;
+ this.RefreshThread.Start();
+ }
+
+ ///
+ /// The method that handles Refreshing the TimeStamp.
+ ///
+ private void Refresh()
+ {
+ while (this.Running)
+ {
+ //Get the elapsed miliseconds from the watch
+ this._Current = (ulong)this.Watch.ElapsedMilliseconds;
+
+ //Now sleep 1MS
+ Thread.Sleep(1);
+ }
+ }
+
+ ///
+ /// Returns the Difference between two TimeStamps in miliseconds.
+ /// Note:
+ /// We needed this function because sometimes we
+ /// store a timestamp of when an event occured but when
+ /// using multi threading if this is greater than the last
+ /// time we got the current time, then we need to select the bigger one.
+ ///
+ ///
+ ///
+ /// The difference in miliseconds.
+ public static ulong GetDifference(ulong a, ulong b)
+ {
+ if (a > b)
+ return (a - b);
+
+ return (b - a);
+ }
+
+ ///
+ /// Returns the Difference in miliseconds between the Current Time and the passed
+ /// Time stamp.
+ /// Note:
+ /// We needed this function because sometimes we
+ /// store a timestamp of when an event occured but when
+ /// using multi threading if this is greater than the last
+ /// time we got the current time, then we need to select the bigger one.
+ ///
+ ///
+ /// The difference in miliseconds.
+ public static ulong GetDifference(ulong b)
+ {
+ //Get the current timestamp
+ ulong current = instance._Current;
+ //Compare it.
+ if (current > b)
+ return (current - b);
+
+ return (b - current);
+ }
+
+ ///
+ /// Disposes this GlobalTimeStamp if its Running.
+ ///
+ public void Dispose()
+ {
+ try
+ {
+ if (this.Running)
+ {
+ this.Running = false;
+
+ this.Watch.Stop();
+
+ this.RefreshThread = null;
+ }
+ }
+ catch { }
+ }
+
+ ///
+ /// Called when this GlobalTimeStamp is going to be destroyed.
+ ///
+ ~GlobalTimeStamp()
+ {
+ try
+ {
+ if (this.Running)
+ {
+ this.Running = false;
+
+ this.Watch.Stop();
+
+ this.RefreshThread = null;
+ }
+ }
+ catch { }
+ }
+ }
+}
diff --git a/MySqlPlus/MySqlColumn.cs b/MySqlPlus/MySqlColumn.cs
new file mode 100644
index 0000000..aacbffa
--- /dev/null
+++ b/MySqlPlus/MySqlColumn.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MontoyaTech.MySqlPlus
+{
+ public class MySqlColumn : Attribute
+ {
+ public string Name = null;
+
+ public Type Converter = null;
+
+ public MySqlColumn(string name = null, Type converter = null)
+ {
+ this.Name = name;
+
+ this.Converter = converter;
+ }
+ }
+}
diff --git a/MySqlPlus/MySqlColumnAlias.cs b/MySqlPlus/MySqlColumnAlias.cs
new file mode 100644
index 0000000..d825c3a
--- /dev/null
+++ b/MySqlPlus/MySqlColumnAlias.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MontoyaTech.MySqlPlus
+{
+ public class MySqlColumnAlias : Attribute
+ {
+ public string Alias = null;
+
+ public MySqlColumnAlias(string alias)
+ {
+ this.Alias = alias;
+ }
+ }
+}
diff --git a/MySqlPlus/MySqlManagedConnection.cs b/MySqlPlus/MySqlManagedConnection.cs
new file mode 100644
index 0000000..8a874b1
--- /dev/null
+++ b/MySqlPlus/MySqlManagedConnection.cs
@@ -0,0 +1,324 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using MySql.Data.MySqlClient;
+
+namespace MontoyaTech.MySqlPlus
+{
+ ///
+ /// The outline of a managed MySqlConnection that adds extra functionality like automatic
+ /// query retrying and reconnecting if the remote DB goes away or becomes busy.
+ ///
+ public class MySqlManagedConnection : IDisposable
+ {
+ ///
+ /// The Internal MySqlConnection for this managed copy.
+ ///
+ public MySqlConnection Internal = null;
+
+ ///
+ /// The connection string that is used to repoen the connection.
+ ///
+ private string ConnectionString = null;
+
+ ///
+ /// Wether or not we are connected to the remote server.
+ ///
+ public bool IsConnected
+ {
+ get
+ {
+ try
+ {
+ if (this.Internal == null || this.Internal.State != System.Data.ConnectionState.Open || !this.Internal.Ping())
+ return false;
+ }
+ catch
+ {
+ return false;
+ }
+
+ return true;
+ }
+ }
+
+ ///
+ /// Creates a new managed MySqlConnection and sets its internal
+ /// MySql connection.
+ ///
+ ///
+ public MySqlManagedConnection(MySqlConnection conn)
+ {
+ if (string.IsNullOrWhiteSpace(conn.ConnectionString))
+ throw new Exception("Connection string must be set on MySqlConnection.");
+
+ this.Internal = conn;
+ this.ConnectionString = conn.ConnectionString;
+ }
+
+ ///
+ /// Creates a new managed MySqlConnection and setups up the internal connection string and gets everything ready.
+ ///
+ ///
+ public MySqlManagedConnection(string conn)
+ {
+ if (string.IsNullOrWhiteSpace(conn))
+ throw new Exception("Invalid connection string passed, it must not be null or empty.");
+
+ this.Internal = new MySqlConnection(conn);
+ this.ConnectionString = conn;
+ }
+
+ ///
+ /// Attempts to reconnect to the server and retrys if needed.
+ ///
+ public void Reconnect(int maxRetrys = 5, bool exponentialBackoff = true, bool retry = true)
+ {
+ int backoffSleep = 2000;
+ for (int i = 0; i < maxRetrys; i++)
+ {
+ try
+ {
+ if (Internal == null)
+ {
+ Internal = new MySqlConnection(this.ConnectionString);
+ }
+ else
+ {
+ try { Internal.Close(); } catch { }
+
+ try { Internal.Dispose(); } catch { }
+
+ Internal = new MySqlConnection(this.ConnectionString);
+ }
+
+ Internal.Open();
+
+ //If we are now connected then stop trying.
+ if (this.IsConnected)
+ break;
+ else
+ throw new Exception("Failed to open a valid MySqlConnection.");
+ }
+ catch (Exception ex)
+ {
+ //See if we have reached our max retry.
+ if (i + 1 >= maxRetrys || !retry)
+ throw;
+
+ //If not backoff for a bit and see if the server comes back online.
+ if (exponentialBackoff)
+ {
+ Thread.Sleep(backoffSleep);
+ backoffSleep *= 2;
+ }
+ }
+ }
+ }
+
+ ///
+ /// Opens a connection the remote server if we can.
+ ///
+ public void Open()
+ {
+ //Just invoke reconnect it handles everything for us.
+ this.Reconnect();
+ }
+
+ ///
+ /// Closes this managed connection.
+ ///
+ public void Close()
+ {
+ if (Internal != null)
+ {
+ try { Internal.Close(); } catch { }
+ }
+ }
+
+ ///
+ /// Executes a data reader for a command and retrys
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public MySqlDataReader ExecuteReader(MySqlCommand command, int maxRetrys = 5, bool exponentialBackoff = true, bool retry = true)
+ {
+ int backoffSleep = 2000;
+ command.CommandTimeout = 60; //Time in seconds
+
+ for (int i = 0; i < maxRetrys; i++)
+ {
+ ulong startTimestamp = GlobalTimeStamp.Current;
+
+ try
+ {
+ command.Connection = this.Internal;
+ return command.ExecuteReader();
+ }
+ catch (Exception ex)
+ {
+ //Get the inner most exception.
+ var innerException = ex;
+ while (innerException.InnerException != null)
+ innerException = innerException.InnerException;
+
+ //Try to get the MySql error code
+ int code = -1;
+ if (ex is MySqlException)
+ code = ((MySqlException)ex).Number;
+
+ //See if we have reached our max retry.
+ if (i + 1 >= maxRetrys || !retry)
+ throw;
+
+ //See if the connection was invalid
+ if (innerException.GetType().IsAssignableFrom(typeof(System.Net.Sockets.SocketException)) ||
+ innerException.GetType().IsAssignableFrom(typeof(MySqlConnection)) ||
+ ex.GetType().IsAssignableFrom(typeof(MySqlConnection)) ||
+ ex.Message.ToLower().Contains("connection must be valid and open") ||
+ ex.Message.ToLower().Contains("a connection attempt failed because the connected party did not properly respond after a period of time") ||
+ ex.Message.ToLower().Contains("an existing connection was forcibly closed by the remote host"))
+ {
+ //Attempt to reopen the connection.
+ this.Reconnect(maxRetrys, exponentialBackoff);
+ }
+
+ //See if we should retry or just throw.
+ if (!ShouldRetryBasedOnMySqlErrorNum(code))
+ throw;
+
+ //If the operation took less than 5 seconds, then sleep. Otherwise continue right away. This is to prevent OpenRetry from making us wait even longer.
+ if (GlobalTimeStamp.GetDifference(startTimestamp) <= 5000 && exponentialBackoff)
+ {
+ Thread.Sleep(backoffSleep);
+ backoffSleep *= 2;
+ }
+ }
+ }
+
+ throw new Exception($"MySql ExecuteReader failed. Max timeout reached. Query: {command.CommandText}");
+ }
+
+ ///
+ /// Executes a mysql command without a reader.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public int ExecuteNonQuery(MySqlCommand command, int maxRetrys = 5, bool exponentialBackoff = true, bool retry = true)
+ {
+ int backoffSleep = 2000;
+ command.CommandTimeout = 60; // time in seconds
+
+ for (int i = 0; i < maxRetrys; i++)
+ {
+ ulong startTimestamp = GlobalTimeStamp.Current;
+
+ try
+ {
+ command.Connection = this.Internal;
+
+ return command.ExecuteNonQuery();
+ }
+ catch (Exception ex)
+ {
+ //Get the inner most exception.
+ var innerException = ex;
+ while (innerException.InnerException != null)
+ innerException = innerException.InnerException;
+
+ //Try to get the MySql error code if we can
+ int code = -1;
+ if (ex is MySqlException)
+ code = ((MySqlException)ex).Number;
+
+ //See if we have reached our max retry.
+ if (i + 1 >= maxRetrys || !retry)
+ throw;
+
+ //See if the connection was invalid
+ if (innerException.GetType().IsAssignableFrom(typeof(System.Net.Sockets.SocketException)) ||
+ innerException.GetType().IsAssignableFrom(typeof(MySqlConnection)) ||
+ ex.GetType().IsAssignableFrom(typeof(MySqlConnection)) ||
+ ex.Message.ToLower().Contains("connection must be valid and open") ||
+ ex.Message.ToLower().Contains("a connection attempt failed because the connected party did not properly respond after a period of time") ||
+ ex.Message.ToLower().Contains("an existing connection was forcibly closed by the remote host"))
+ {
+ this.Reconnect(maxRetrys, exponentialBackoff);
+ }
+
+ //See if we should retry or just throw.
+ if (!ShouldRetryBasedOnMySqlErrorNum(code))
+ throw;
+
+ //If the operation took less than 5 seconds, then sleep. Otherwise continue right away.
+ if (GlobalTimeStamp.GetDifference(startTimestamp) <= 5000 && exponentialBackoff)
+ {
+ Thread.Sleep(backoffSleep);
+ backoffSleep *= 2;
+ }
+ }
+ }
+
+ throw new Exception($"MySql ExecuteNonQuery failed. Max timeout reached. Query: {command.CommandText}");
+ }
+
+ ///
+ /// Returns whether or not we should retry a query based on the MySql error number.
+ ///
+ ///
+ ///
+ private static bool ShouldRetryBasedOnMySqlErrorNum(int number)
+ {
+ //List of codes here: https://www.briandunning.com/error-codes/?source=MySQL
+ if (number >= 1044 && number <= 1052)
+ return false;
+ else if (number >= 1054 && number <= 1075)
+ return false;
+ else if (number == 1265)
+ return false; //Data truncation (Don't try again)
+
+ return true;
+ }
+
+ ///
+ /// Disposes this managed connection and releases all resources.
+ ///
+ public void Dispose()
+ {
+ if (Internal != null)
+ {
+ try { Internal.Close(); } catch { }
+
+ try { Internal.Dispose(); } catch { }
+
+ Internal = null;
+ }
+ }
+
+ ///
+ /// Called when this managed connections needs to be deallocated.
+ ///
+ ///
+ ~MySqlManagedConnection()
+ {
+ Dispose();
+ }
+
+ ///
+ /// Allows the exposal of the internal MySqlConnection from the managed version.
+ ///
+ ///
+ public static implicit operator MySqlConnection(MySqlManagedConnection connection)
+ {
+ return connection.Internal;
+ }
+ }
+}
diff --git a/MySqlPlus/MySqlPlus.csproj b/MySqlPlus/MySqlPlus.csproj
new file mode 100644
index 0000000..5b99c77
--- /dev/null
+++ b/MySqlPlus/MySqlPlus.csproj
@@ -0,0 +1,15 @@
+
+
+
+ net6.0
+ disable
+ disable
+ MontoyaTech.MySqlPlus
+ MontoyaTech.MySqlPlus
+
+
+
+
+
+
+
diff --git a/MySqlPlus/MySqlRow.cs b/MySqlPlus/MySqlRow.cs
new file mode 100644
index 0000000..c8f17e5
--- /dev/null
+++ b/MySqlPlus/MySqlRow.cs
@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MontoyaTech.MySqlPlus
+{
+ public class MySqlRow
+ {
+ }
+}
diff --git a/MySqlPlus/MySqlRowId.cs b/MySqlPlus/MySqlRowId.cs
new file mode 100644
index 0000000..6ca10da
--- /dev/null
+++ b/MySqlPlus/MySqlRowId.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MontoyaTech.MySqlPlus
+{
+ public class MySqlRowId : Attribute
+ {
+ public MySqlRowId() { }
+ }
+}
diff --git a/MySqlPlus/MySqlSession.cs b/MySqlPlus/MySqlSession.cs
new file mode 100644
index 0000000..983e7d2
--- /dev/null
+++ b/MySqlPlus/MySqlSession.cs
@@ -0,0 +1,128 @@
+using Microsoft.VisualBasic;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MontoyaTech.MySqlPlus
+{
+ ///
+ /// The outline of a MySqlSession which contains a MySqlManagedConnection
+ /// and a few other help functions.
+ ///
+ public class MySqlSession : IDisposable
+ {
+ ///
+ /// Returns whether or not this MySqlSession has a valid connection.
+ ///
+ public bool IsConnected
+ {
+ get
+ {
+ try
+ {
+ if (this.Connection == null || !this.Connection.IsConnected)
+ return false;
+ }
+ catch
+ {
+ return false;
+ }
+
+ return true;
+ }
+ }
+
+ ///
+ /// The underlying MySql Connection that this Session is using.
+ ///
+ public MySqlManagedConnection Connection = null;
+
+ ///
+ /// The raw connection string used to open a MySqlConnection.
+ ///
+ protected string ConnectionStr = null;
+
+ ///
+ /// Creates a new MySqlSession with a connection string.
+ ///
+ ///
+ public MySqlSession(string connectionStr)
+ {
+ if (string.IsNullOrWhiteSpace(this.ConnectionStr))
+ throw new Exception("Must provide a valid non null or empty MySql connection string.");
+
+ this.ConnectionStr = connectionStr;
+ }
+
+ ///
+ /// Attempts to connect to the MySql Server using the connection information.
+ ///
+ ///
+ public void Connect()
+ {
+ lock (this)
+ {
+ //Dont connect again if we are already connected.
+ if (this.Connection == null)
+ {
+ if (!string.IsNullOrWhiteSpace(this.ConnectionStr))
+ {
+ try
+ {
+ //Setup the mysql connection.
+ this.Connection = new MySqlManagedConnection(this.ConnectionStr);
+
+ //Attempt to open the connection, this will make sure its valid and works for us.
+ this.Connection.Open();
+ }
+ catch (Exception ex)
+ {
+ throw new Exception("Failed to connect to MySql database.", ex);
+ }
+ }
+ else
+ {
+ throw new Exception("Missing connection details to connect to MySql database.");
+ }
+ }
+ }
+ }
+
+ ///
+ /// Disconnects from the MySql Server this Session is connected to.
+ ///
+ public void Disconnect()
+ {
+ lock (this)
+ {
+ try
+ {
+ if (this.Connection != null)
+ {
+ try { this.Connection.Close(); } catch { }
+
+ try { this.Connection.Dispose(); } catch { }
+
+ this.Connection = null;
+ }
+ }
+ catch
+ {
+ this.Connection = null;
+ }
+ }
+ }
+
+ ~MySqlSession()
+ {
+ this.Disconnect();
+ }
+
+ public virtual void Dispose()
+ {
+ this.Disconnect();
+ }
+ }
+}
diff --git a/MySqlPlus/MySqlSessionCache.cs b/MySqlPlus/MySqlSessionCache.cs
new file mode 100644
index 0000000..c5966f7
--- /dev/null
+++ b/MySqlPlus/MySqlSessionCache.cs
@@ -0,0 +1,264 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MontoyaTech.MySqlPlus
+{
+ ///
+ /// The outline of a MySqlSession Cache which will cache and automatically free MySql Sessions
+ /// in order to speed up query times.
+ ///
+ public class MySqlSessionCache
+ {
+ ///
+ /// The outline of a cached MySqlSession.
+ ///
+ public class CachedMySqlSession : MySqlSession
+ {
+ ///
+ /// The Last Time this cached API Session was used.
+ ///
+ public DateTime LastUse = DateTime.MinValue;
+
+ ///
+ /// The method that requested this cache.
+ ///
+ public string CallerMethod = "";
+
+ ///
+ /// The line of code that requested this cache.
+ ///
+ public string CallerLine = "";
+
+ ///
+ /// The Id of this CachedAPISession.
+ ///
+ public string Id = "";
+
+ ///
+ /// Whether or not this Cached API Session is in use.
+ ///
+ public bool InUse = false;
+
+ ///
+ /// Creates a new default CachedAPISession.
+ ///
+ public CachedMySqlSession(string connectionStr) : base(connectionStr)
+ {
+ this.Id = Guid.NewGuid().ToString();
+ }
+
+ ///
+ /// Dont allow the Session to be killed. Instead set in use to false.
+ ///
+ public override void Dispose()
+ {
+ InUse = false;
+
+ //Set this again because we just stopped using this cached session.
+ this.LastUse = DateTime.UtcNow;
+ }
+ }
+
+ ///
+ /// The connection string to use when creating MySqlSessions.
+ ///
+ private static string ConnectionStr = null;
+
+ ///
+ /// The list of Cached Sessions currently being maintained by the Cache.
+ ///
+ private static List Sessions = new List();
+
+ ///
+ /// The Thread that will handle uncaching.
+ ///
+ private static Thread UnCacheThread = null;
+
+ ///
+ /// The amount of time in minutes that a cached api session is allowed to live.
+ ///
+ private static int CacheLifeTime = 20;
+
+ ///
+ /// The amount of time in minutes before a cache is considered to be a zombie
+ /// and the result of a code bug somewhere.
+ ///
+ private static int ZombieCacheLifeTime = 5;
+
+ ///
+ /// The amount of time in seconds before we are allowed to attempt to uncache sessions.
+ ///
+ private static int UnCacheFrequency = 30;
+
+ ///
+ /// Whether or not the API Session Cache is running.
+ ///
+ private static bool Running = false;
+
+ ///
+ /// Starts this MySqlSessionCache with a given MySqlConnectionString.
+ ///
+ ///
+ public static void Start(string connectionStr)
+ {
+ ConnectionStr = connectionStr;
+
+ lock (Sessions)
+ {
+ if (!Running)
+ {
+ Running = true;
+
+ UnCacheThread = new Thread(UnCache);
+ UnCacheThread.IsBackground = true;
+ UnCacheThread.Start();
+ }
+ }
+ }
+
+ ///
+ /// Stops the APISessionCache Service.
+ ///
+ public static void Stop()
+ {
+ lock (Sessions)
+ {
+ if (Running)
+ {
+ UnCacheThread = null;
+
+ try
+ {
+ for (int i = 0; i < Sessions.Count; i++)
+ {
+ Sessions[i].Disconnect();
+ }
+ }
+ catch { }
+
+ Sessions = null;
+
+ Running = false;
+ }
+ }
+ }
+
+ ///
+ /// Kills all the cached api sessions if we can.
+ ///
+ public static void KillCache()
+ {
+ lock (Sessions)
+ {
+ for (int i = 0; i < Sessions.Count; i++)
+ {
+ var session = Sessions[i];
+
+ //Remove the sessions not in use and the ones who are zombies.
+
+ if (session.InUse == false)
+ {
+ Sessions.RemoveAt(i);
+ session.Disconnect();
+ i--;
+ }
+ else if (session.InUse && (DateTime.UtcNow - session.LastUse).Minutes >= ZombieCacheLifeTime)
+ {
+ Sessions.RemoveAt(i);
+ session.Disconnect();
+ i--;
+ }
+ }
+ }
+ }
+
+ ///
+ /// Returns a APISession that is either new or cached depending on whats available.
+ ///
+ ///
+ public static CachedMySqlSession Get()
+ {
+ if (!Running)
+ throw new Exception("API Session Cache is not running!");
+
+ lock (Sessions)
+ {
+ CachedMySqlSession result = null;
+
+ //See if we can find an existing session to use.
+ foreach (var cached in Sessions)
+ {
+ if (cached.InUse == false)
+ {
+ result = cached;
+ break;
+ }
+ }
+
+ //Create a new Session if we didnt find one.
+ if (result == null)
+ {
+ result = new CachedMySqlSession(ConnectionStr);
+ result.Connect();
+ Sessions.Add(result);
+ }
+
+ result.InUse = true;
+ result.LastUse = DateTime.UtcNow;
+
+ //Get the caller information
+ var trace = new StackTrace();
+
+ var method = trace.GetFrame(1).GetMethod();
+ var methodName = method.Name;
+ var className = method.ReflectedType.Name;
+ var currNamespace = method.ReflectedType.Namespace;
+
+ result.CallerMethod = $"{currNamespace}.{className}.{methodName}()";
+ result.CallerLine = trace.GetFrame(1).GetFileLineNumber().ToString();
+
+ return result;
+ }
+ }
+
+ ///
+ /// The code that handles uncaching api sessions.
+ ///
+ private static void UnCache()
+ {
+ while (Running)
+ {
+ lock (Sessions)
+ {
+ for (int i = 0; i < Sessions.Count; i++)
+ {
+ var session = Sessions[i];
+
+ //If this session is older than it's lifetime and it's not in use, kill it.
+ if (session.InUse == false && (DateTime.UtcNow - session.LastUse).Minutes >= CacheLifeTime)
+ {
+ Sessions.RemoveAt(i);
+ session.Disconnect();
+ i--;
+ }
+ //If this session is in use but it appears to be a zombie session, kill it.
+ else if (session.InUse && (DateTime.UtcNow - session.LastUse).Minutes >= ZombieCacheLifeTime)
+ {
+ Sessions.RemoveAt(i);
+ session.Disconnect();
+ i--;
+ }
+ }
+ }
+
+ Thread.Sleep(1000 * UnCacheFrequency);
+ }
+ }
+ }
+}