diff --git a/Examples/CSharpMirationStep/readme.md b/Examples/CSharpMirationStep/readme.md index e8d29050..8410acec 100644 --- a/Examples/CSharpMirationStep/readme.md +++ b/Examples/CSharpMirationStep/readme.md @@ -1,20 +1,46 @@ -The project contains an example of database migration step implementation. +# .NET assembly with a script implementation +The project contains an example of database migration step implementation. -Build output is 2.1_2.2.dll with taget framework 4.5.2 +Build output is 2.1_2.2.dll with target framework 4.5.2 -Assembly must contain only one "public class SqlDatabaseScript", namespace name doesn't matter. - -SqlDatabaseScript must contain "public void Execute(IDbCommand command, IReadOnlyDictionary variables)". - -Method Execute implements the logic of your migration step. See [code example](https://github.com/max-ieremenko/SqlDatabase/blob/master/Examples/CSharpMirationStep/SqlDatabaseScript.cs). +Method [SqlDatabaseScript.Execute](https://github.com/max-ieremenko/SqlDatabase/blob/master/Examples/CSharpMirationStep/SqlDatabaseScript.cs) implements the logic of your migration step. Use parameter "IDbCommand command" to affect database. Use Console.WriteLine() to write something into migration log. +## Runtime At runtime the assembly will be loaded into private application domain with * ApplicationBase: location of SqlDatabase.exe * ConfigurationFile: SqlDatabase.exe.config +Instance of migration step will be resolved via reflection: Activator.CreateInstance(typeof(SqlDatabaseScript)) + After the migration step is finished or failed -- instance of SqlDatabaseScript will be disposed (id IDisposable) +- instance of SqlDatabaseScript will be disposed (if IDisposable) - the domain will be unloaded + +## Reflection +The assembly must contain only one "public class SqlDatabaseScript", namespace doesn't matter. +Class SqlDatabaseScript must contain method "public void Execute(...)". + +Supported signatures of Execute method +* void Execute(IDbCommand command, IReadOnlyDictionary variables) +* void Execute(IReadOnlyDictionary variables, IDbCommand command) +* void Execute(IDbCommand command) +* void Execute(IDbConnection connection) + +## Configuration +name of class SqlDatabaseScript and method Execute can be changed in the [SqlDatabase.exe.config](https://github.com/max-ieremenko/SqlDatabase/blob/master/Sources/SqlDatabase/App.config): +```xml + + +
+ + + + + + +``` \ No newline at end of file diff --git a/Sources/GlobalAssemblyInfo.cs b/Sources/GlobalAssemblyInfo.cs index 1f70de6c..a6726f99 100644 --- a/Sources/GlobalAssemblyInfo.cs +++ b/Sources/GlobalAssemblyInfo.cs @@ -9,5 +9,5 @@ [assembly: AssemblyCulture("")] [assembly: ComVisible(false)] -[assembly: AssemblyVersion("1.4.2.0")] -[assembly: AssemblyFileVersion("1.4.2.0")] +[assembly: AssemblyVersion("1.5.0.0")] +[assembly: AssemblyFileVersion("1.5.0.0")] diff --git a/Sources/SqlDatabase.Test/Configuration/AppConfigurationTest.cs b/Sources/SqlDatabase.Test/Configuration/AppConfigurationTest.cs new file mode 100644 index 00000000..ff7e9c64 --- /dev/null +++ b/Sources/SqlDatabase.Test/Configuration/AppConfigurationTest.cs @@ -0,0 +1,62 @@ +using System.Configuration; +using NUnit.Framework; +using SqlDatabase.TestApi; + +namespace SqlDatabase.Configuration +{ + [TestFixture] + public class AppConfigurationTest + { + private TempDirectory _temp; + + [SetUp] + public void BeforeEachTest() + { + _temp = new TempDirectory(); + } + + [TearDown] + public void AfterEachTest() + { + _temp?.Dispose(); + } + + [Test] + public void LoadEmpty() + { + var configuration = LoadFromResource("AppConfiguration.empty.xml"); + Assert.IsNull(configuration); + } + + [Test] + public void LoadDefault() + { + var configuration = LoadFromResource("AppConfiguration.default.xml"); + Assert.IsNotNull(configuration); + + Assert.That(configuration.GetCurrentVersionScript, Is.Not.Null.And.Not.Empty); + Assert.That(configuration.SetCurrentVersionScript, Is.Not.Null.And.Not.Empty); + Assert.That(configuration.AssemblyScript.ClassName, Is.Not.Null.And.Not.Empty); + Assert.That(configuration.AssemblyScript.MethodName, Is.Not.Null.And.Not.Empty); + } + + [Test] + public void LoadFull() + { + var configuration = LoadFromResource("AppConfiguration.full.xml"); + Assert.IsNotNull(configuration); + + Assert.AreEqual("get-version", configuration.GetCurrentVersionScript); + Assert.AreEqual("set-version", configuration.SetCurrentVersionScript); + Assert.AreEqual("method-name", configuration.AssemblyScript.MethodName); + Assert.AreEqual("class-name", configuration.AssemblyScript.ClassName); + } + + private AppConfiguration LoadFromResource(string resourceName) + { + var fileName = _temp.CopyFileFromResources(resourceName); + var configuration = ConfigurationManager.OpenMappedExeConfiguration(new ExeConfigurationFileMap { ExeConfigFilename = fileName }, ConfigurationUserLevel.None); + return (AppConfiguration)configuration.GetSection(AppConfiguration.SectionName); + } + } +} diff --git a/Sources/SqlDatabase.Test/Resources/AppConfiguration.default.xml b/Sources/SqlDatabase.Test/Resources/AppConfiguration.default.xml new file mode 100644 index 00000000..879ae67c --- /dev/null +++ b/Sources/SqlDatabase.Test/Resources/AppConfiguration.default.xml @@ -0,0 +1,9 @@ + + + +
+ + + + diff --git a/Sources/SqlDatabase.Test/Resources/AppConfiguration.empty.xml b/Sources/SqlDatabase.Test/Resources/AppConfiguration.empty.xml new file mode 100644 index 00000000..ce55b931 --- /dev/null +++ b/Sources/SqlDatabase.Test/Resources/AppConfiguration.empty.xml @@ -0,0 +1,3 @@ + + + diff --git a/Sources/SqlDatabase.Test/Resources/AppConfiguration.full.xml b/Sources/SqlDatabase.Test/Resources/AppConfiguration.full.xml new file mode 100644 index 00000000..d2ebeeaa --- /dev/null +++ b/Sources/SqlDatabase.Test/Resources/AppConfiguration.full.xml @@ -0,0 +1,13 @@ + + + +
+ + + + + + diff --git a/Sources/SqlDatabase.Test/Scripts/AssemblyInternal/EntryPointResolverTest.Stubs.cs b/Sources/SqlDatabase.Test/Scripts/AssemblyInternal/EntryPointResolverTest.Stubs.cs index e358d5d4..9bd5f31d 100644 --- a/Sources/SqlDatabase.Test/Scripts/AssemblyInternal/EntryPointResolverTest.Stubs.cs +++ b/Sources/SqlDatabase.Test/Scripts/AssemblyInternal/EntryPointResolverTest.Stubs.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Data; +using System.Data.SqlClient; namespace SqlDatabase.Scripts.AssemblyInternal { @@ -26,5 +27,21 @@ public void Execute(IDbCommand command, IReadOnlyDictionary vari throw new NotImplementedException(); } } + + public sealed class DatabaseScriptWithOneParameter + { + public void ExecuteCommand(IDbCommand command) + { + throw new NotImplementedException(); + } + } + + public sealed class DatabaseScriptWithConnection + { + public void Run(SqlConnection connection) + { + throw new NotImplementedException(); + } + } } } diff --git a/Sources/SqlDatabase.Test/Scripts/AssemblyInternal/EntryPointResolverTest.cs b/Sources/SqlDatabase.Test/Scripts/AssemblyInternal/EntryPointResolverTest.cs index 14d465fd..97b89097 100644 --- a/Sources/SqlDatabase.Test/Scripts/AssemblyInternal/EntryPointResolverTest.cs +++ b/Sources/SqlDatabase.Test/Scripts/AssemblyInternal/EntryPointResolverTest.cs @@ -59,5 +59,37 @@ public void FailToCreateInstance() Assert.IsNull(_sut.Resolve(GetType().Assembly)); CollectionAssert.IsNotEmpty(_logErrorOutput); } + + [Test] + public void ResolveExecuteWithCommandOnly() + { + _sut.ExecutorClassName = nameof(DatabaseScriptWithOneParameter); + _sut.ExecutorMethodName = nameof(DatabaseScriptWithOneParameter.ExecuteCommand); + + var actual = _sut.Resolve(GetType().Assembly); + CollectionAssert.IsEmpty(_logErrorOutput); + Assert.IsInstanceOf(actual); + + var entryPoint = (DefaultEntryPoint)actual; + Assert.IsNotNull(entryPoint.Log); + Assert.IsInstanceOf(entryPoint.ScriptInstance); + Assert.IsNotNull(entryPoint.Method); + } + + [Test] + public void ResolveExecuteWithConnection() + { + _sut.ExecutorClassName = nameof(DatabaseScriptWithConnection); + _sut.ExecutorMethodName = nameof(DatabaseScriptWithConnection.Run); + + var actual = _sut.Resolve(GetType().Assembly); + CollectionAssert.IsEmpty(_logErrorOutput); + Assert.IsInstanceOf(actual); + + var entryPoint = (DefaultEntryPoint)actual; + Assert.IsNotNull(entryPoint.Log); + Assert.IsInstanceOf(entryPoint.ScriptInstance); + Assert.IsNotNull(entryPoint.Method); + } } } diff --git a/Sources/SqlDatabase.Test/Scripts/AssemblyInternal/ExecuteMethodResolverCommandDictionaryTest.cs b/Sources/SqlDatabase.Test/Scripts/AssemblyInternal/ExecuteMethodResolverCommandDictionaryTest.cs new file mode 100644 index 00000000..a02a45b3 --- /dev/null +++ b/Sources/SqlDatabase.Test/Scripts/AssemblyInternal/ExecuteMethodResolverCommandDictionaryTest.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.Data; +using System.Reflection; +using Moq; +using NUnit.Framework; + +namespace SqlDatabase.Scripts.AssemblyInternal +{ + [TestFixture] + public class ExecuteMethodResolverCommandDictionaryTest + { + private ExecuteMethodResolverCommandDictionary _sut; + private IDbCommand _executeCommand; + private IReadOnlyDictionary _executeVariables; + + [SetUp] + public void BeforeEachTest() + { + _sut = new ExecuteMethodResolverCommandDictionary(); + } + + [Test] + public void IsMatch() + { + var method = GetType().GetMethod(nameof(Execute), BindingFlags.Instance | BindingFlags.NonPublic); + Assert.IsTrue(_sut.IsMatch(method)); + } + + [Test] + public void CreateDelegate() + { + var method = GetType().GetMethod(nameof(Execute), BindingFlags.Instance | BindingFlags.NonPublic); + var actual = _sut.CreateDelegate(this, method); + Assert.IsNotNull(actual); + + var command = new Mock(MockBehavior.Strict); + var variables = new Mock>(MockBehavior.Strict); + + actual(command.Object, variables.Object); + + Assert.AreEqual(_executeCommand, command.Object); + Assert.AreEqual(_executeVariables, variables.Object); + } + + private void Execute(IDbCommand command, IReadOnlyDictionary variables) + { + Assert.IsNull(_executeCommand); + _executeCommand = command; + _executeVariables = variables; + } + } +} diff --git a/Sources/SqlDatabase.Test/Scripts/AssemblyInternal/ExecuteMethodResolverCommandTest.cs b/Sources/SqlDatabase.Test/Scripts/AssemblyInternal/ExecuteMethodResolverCommandTest.cs new file mode 100644 index 00000000..ab55f821 --- /dev/null +++ b/Sources/SqlDatabase.Test/Scripts/AssemblyInternal/ExecuteMethodResolverCommandTest.cs @@ -0,0 +1,45 @@ +using System.Data; +using System.Reflection; +using Moq; +using NUnit.Framework; + +namespace SqlDatabase.Scripts.AssemblyInternal +{ + [TestFixture] + public class ExecuteMethodResolverCommandTest + { + private ExecuteMethodResolverCommand _sut; + private IDbCommand _executeCommand; + + [SetUp] + public void BeforeEachTest() + { + _sut = new ExecuteMethodResolverCommand(); + } + + [Test] + public void IsMatch() + { + var method = GetType().GetMethod(nameof(Execute), BindingFlags.Instance | BindingFlags.NonPublic); + Assert.IsTrue(_sut.IsMatch(method)); + } + + [Test] + public void CreateDelegate() + { + var method = GetType().GetMethod(nameof(Execute), BindingFlags.Instance | BindingFlags.NonPublic); + var actual = _sut.CreateDelegate(this, method); + Assert.IsNotNull(actual); + + var command = new Mock(MockBehavior.Strict); + actual(command.Object, null); + Assert.AreEqual(_executeCommand, command.Object); + } + + private void Execute(IDbCommand command) + { + Assert.IsNull(_executeCommand); + _executeCommand = command; + } + } +} diff --git a/Sources/SqlDatabase.Test/Scripts/AssemblyInternal/ExecuteMethodResolverDbConnectionTest.cs b/Sources/SqlDatabase.Test/Scripts/AssemblyInternal/ExecuteMethodResolverDbConnectionTest.cs new file mode 100644 index 00000000..8775e601 --- /dev/null +++ b/Sources/SqlDatabase.Test/Scripts/AssemblyInternal/ExecuteMethodResolverDbConnectionTest.cs @@ -0,0 +1,52 @@ +using System.Data; +using System.Data.SqlClient; +using System.Reflection; +using Moq; +using NUnit.Framework; + +namespace SqlDatabase.Scripts.AssemblyInternal +{ + [TestFixture] + public class ExecuteMethodResolverDbConnectionTest + { + private ExecuteMethodResolverDbConnection _sut; + private IDbConnection _executeConnection; + + [SetUp] + public void BeforeEachTest() + { + _sut = new ExecuteMethodResolverDbConnection(); + } + + [Test] + public void IsMatch() + { + var method = GetType().GetMethod(nameof(Execute), BindingFlags.Instance | BindingFlags.NonPublic); + Assert.IsTrue(_sut.IsMatch(method)); + } + + [Test] + public void CreateDelegate() + { + var method = GetType().GetMethod(nameof(Execute), BindingFlags.Instance | BindingFlags.NonPublic); + var actual = _sut.CreateDelegate(this, method); + Assert.IsNotNull(actual); + + var connection = new Mock(MockBehavior.Strict); + + var command = new Mock(MockBehavior.Strict); + command + .SetupGet(c => c.Connection) + .Returns(connection.Object); + + actual(command.Object, null); + Assert.AreEqual(_executeConnection, connection.Object); + } + + private void Execute(IDbConnection connection) + { + Assert.IsNull(_executeConnection); + _executeConnection = connection; + } + } +} diff --git a/Sources/SqlDatabase.Test/Scripts/AssemblyInternal/ExecuteMethodResolverDictionaryCommandTest.cs b/Sources/SqlDatabase.Test/Scripts/AssemblyInternal/ExecuteMethodResolverDictionaryCommandTest.cs new file mode 100644 index 00000000..f50633dc --- /dev/null +++ b/Sources/SqlDatabase.Test/Scripts/AssemblyInternal/ExecuteMethodResolverDictionaryCommandTest.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.Data; +using System.Reflection; +using Moq; +using NUnit.Framework; + +namespace SqlDatabase.Scripts.AssemblyInternal +{ + [TestFixture] + public class ExecuteMethodResolverDictionaryCommandTest + { + private ExecuteMethodResolverDictionaryCommand _sut; + private IDbCommand _executeCommand; + private IReadOnlyDictionary _executeVariables; + + [SetUp] + public void BeforeEachTest() + { + _sut = new ExecuteMethodResolverDictionaryCommand(); + } + + [Test] + public void IsMatch() + { + var method = GetType().GetMethod(nameof(Execute), BindingFlags.Instance | BindingFlags.NonPublic); + Assert.IsTrue(_sut.IsMatch(method)); + } + + [Test] + public void CreateDelegate() + { + var method = GetType().GetMethod(nameof(Execute), BindingFlags.Instance | BindingFlags.NonPublic); + var actual = _sut.CreateDelegate(this, method); + Assert.IsNotNull(actual); + + var command = new Mock(MockBehavior.Strict); + var variables = new Mock>(MockBehavior.Strict); + + actual(command.Object, variables.Object); + + Assert.AreEqual(_executeCommand, command.Object); + Assert.AreEqual(_executeVariables, variables.Object); + } + + private void Execute(IReadOnlyDictionary variables, IDbCommand command) + { + Assert.IsNull(_executeCommand); + _executeCommand = command; + _executeVariables = variables; + } + } +} diff --git a/Sources/SqlDatabase.Test/Scripts/AssemblyInternal/ExecuteMethodResolverSqlConnectionTest.cs b/Sources/SqlDatabase.Test/Scripts/AssemblyInternal/ExecuteMethodResolverSqlConnectionTest.cs new file mode 100644 index 00000000..93da2083 --- /dev/null +++ b/Sources/SqlDatabase.Test/Scripts/AssemblyInternal/ExecuteMethodResolverSqlConnectionTest.cs @@ -0,0 +1,52 @@ +using System.Data; +using System.Data.SqlClient; +using System.Reflection; +using Moq; +using NUnit.Framework; + +namespace SqlDatabase.Scripts.AssemblyInternal +{ + [TestFixture] + public class ExecuteMethodResolverSqlConnectionTest + { + private ExecuteMethodResolverSqlConnection _sut; + private SqlConnection _executeConnection; + + [SetUp] + public void BeforeEachTest() + { + _sut = new ExecuteMethodResolverSqlConnection(); + } + + [Test] + public void IsMatch() + { + var method = GetType().GetMethod(nameof(Execute), BindingFlags.Instance | BindingFlags.NonPublic); + Assert.IsTrue(_sut.IsMatch(method)); + } + + [Test] + public void CreateDelegate() + { + var method = GetType().GetMethod(nameof(Execute), BindingFlags.Instance | BindingFlags.NonPublic); + var actual = _sut.CreateDelegate(this, method); + Assert.IsNotNull(actual); + + var connection = new SqlConnection(); + + var command = new Mock(MockBehavior.Strict); + command + .SetupGet(c => c.Connection) + .Returns(connection); + + actual(command.Object, null); + Assert.AreEqual(_executeConnection, connection); + } + + private void Execute(SqlConnection connection) + { + Assert.IsNull(_executeConnection); + _executeConnection = connection; + } + } +} diff --git a/Sources/SqlDatabase.Test/SqlDatabase.Test.csproj b/Sources/SqlDatabase.Test/SqlDatabase.Test.csproj index a8103b17..81659dd0 100644 --- a/Sources/SqlDatabase.Test/SqlDatabase.Test.csproj +++ b/Sources/SqlDatabase.Test/SqlDatabase.Test.csproj @@ -12,9 +12,18 @@ + + + + + + + + + diff --git a/Sources/SqlDatabase/App.config b/Sources/SqlDatabase/App.config index 8b1adadf..156caac7 100644 --- a/Sources/SqlDatabase/App.config +++ b/Sources/SqlDatabase/App.config @@ -19,7 +19,16 @@ setCurrentVersion="UPDATE info.Version SET Version='{{TargetVersion}}'"/> --> - + + + diff --git a/Sources/SqlDatabase/Configuration/AppConfiguration.cs b/Sources/SqlDatabase/Configuration/AppConfiguration.cs index 80888393..181571c2 100644 --- a/Sources/SqlDatabase/Configuration/AppConfiguration.cs +++ b/Sources/SqlDatabase/Configuration/AppConfiguration.cs @@ -8,6 +8,7 @@ public sealed class AppConfiguration : ConfigurationSection private const string PropertyGetCurrentVersionScript = "getCurrentVersion"; private const string PropertySetCurrentVersionScript = "setCurrentVersion"; + private const string PropertyAssemblyScript = "assemblyScript"; [ConfigurationProperty(PropertyGetCurrentVersionScript, DefaultValue = "SELECT value from sys.fn_listextendedproperty('version', default, default, default, default, default, default)")] public string GetCurrentVersionScript @@ -23,6 +24,9 @@ public string SetCurrentVersionScript set => this[PropertySetCurrentVersionScript] = value; } + [ConfigurationProperty(PropertyAssemblyScript)] + public AssemblyScriptConfiguration AssemblyScript => (AssemblyScriptConfiguration)this[PropertyAssemblyScript]; + public static AppConfiguration GetCurrent() { return (AppConfiguration)ConfigurationManager.GetSection(SectionName) ?? new AppConfiguration(); diff --git a/Sources/SqlDatabase/Configuration/AssemblyScriptConfiguration.cs b/Sources/SqlDatabase/Configuration/AssemblyScriptConfiguration.cs new file mode 100644 index 00000000..6ef77d36 --- /dev/null +++ b/Sources/SqlDatabase/Configuration/AssemblyScriptConfiguration.cs @@ -0,0 +1,24 @@ +using System.Configuration; + +namespace SqlDatabase.Configuration +{ + public sealed class AssemblyScriptConfiguration : ConfigurationElement + { + private const string PropertyClassName = "className"; + private const string PropertyMethodName = "methodName"; + + [ConfigurationProperty(PropertyClassName, DefaultValue = "SqlDatabaseScript")] + public string ClassName + { + get => (string)this[PropertyClassName]; + set => this[PropertyClassName] = value; + } + + [ConfigurationProperty(PropertyMethodName, DefaultValue = "Execute")] + public string MethodName + { + get => (string)this[PropertyMethodName]; + set => this[PropertyMethodName] = value; + } + } +} diff --git a/Sources/SqlDatabase/Scripts/AssemblyInternal/DomainAgent.cs b/Sources/SqlDatabase/Scripts/AssemblyInternal/DomainAgent.cs index be9795d5..8a0f4ac5 100644 --- a/Sources/SqlDatabase/Scripts/AssemblyInternal/DomainAgent.cs +++ b/Sources/SqlDatabase/Scripts/AssemblyInternal/DomainAgent.cs @@ -7,9 +7,6 @@ namespace SqlDatabase.Scripts.AssemblyInternal { internal sealed class DomainAgent : MarshalByRefObject { - public const string ExecutorClassName = "SqlDatabaseScript"; - public const string ExecutorMethodName = "Execute"; - internal Assembly Assembly { get; set; } internal IEntryPoint EntryPoint { get; set; } @@ -26,7 +23,7 @@ public void RedirectConsoleOut(ILogger logger) Console.SetOut(new ConsoleListener(logger)); } - public bool ResolveScriptExecutor(ILogger logger) + public bool ResolveScriptExecutor(ILogger logger, string className, string methodName) { // only for unit tests if (EntryPoint != null) @@ -37,8 +34,8 @@ public bool ResolveScriptExecutor(ILogger logger) var resolver = new EntryPointResolver { Log = logger, - ExecutorClassName = ExecutorClassName, - ExecutorMethodName = ExecutorMethodName + ExecutorClassName = className, + ExecutorMethodName = methodName }; EntryPoint = resolver.Resolve(Assembly); diff --git a/Sources/SqlDatabase/Scripts/AssemblyInternal/EntryPointResolver.cs b/Sources/SqlDatabase/Scripts/AssemblyInternal/EntryPointResolver.cs index 5e43da4c..c7efd500 100644 --- a/Sources/SqlDatabase/Scripts/AssemblyInternal/EntryPointResolver.cs +++ b/Sources/SqlDatabase/Scripts/AssemblyInternal/EntryPointResolver.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Data; using System.Linq; using System.Reflection; using System.Text; @@ -32,8 +30,8 @@ public IEntryPoint Resolve(Assembly assembly) } var message = new StringBuilder() - .AppendFormat("found {0}.{1}(", type.FullName, method.Name); - var args = method.GetParameters(); + .AppendFormat("found {0}.{1}(", type.FullName, method.Method.Name); + var args = method.Method.GetParameters(); for (var i = 0; i < args.Length; i++) { if (i > 0) @@ -62,16 +60,11 @@ public IEntryPoint Resolve(Assembly assembly) return null; } - var execute = (Action>)Delegate.CreateDelegate( - typeof(Action>), - scriptInstance, - method); - return new DefaultEntryPoint { Log = Log, ScriptInstance = scriptInstance, - Method = execute + Method = method.Resolver.CreateDelegate(scriptInstance, method.Method) }; } @@ -98,27 +91,58 @@ private Type ResolveClass(Assembly assembly) return candidates[0]; } - private MethodInfo ResolveMethod(Type type) + private ExecuteMethodRef ResolveMethod(Type type) { - var method = type + var methodResolvers = new ExecuteMethodResolverBase[] + { + new ExecuteMethodResolverCommandDictionary(), + new ExecuteMethodResolverDictionaryCommand(), + new ExecuteMethodResolverCommand(), + new ExecuteMethodResolverDbConnection(), + new ExecuteMethodResolverSqlConnection() + }; + + var methods = type .GetMethods(BindingFlags.Instance | BindingFlags.Public) .Where(i => i.ReturnType == typeof(void)) .Where(i => ExecutorMethodName.Equals(i.Name, StringComparison.OrdinalIgnoreCase)) - .Where(i => + .Select(i => { - var p = i.GetParameters(); - return p.Length == 2 - && typeof(IDbCommand) == p[0].ParameterType - && typeof(IReadOnlyDictionary) == p[1].ParameterType; + for (var priority = 0; priority < methodResolvers.Length; priority++) + { + var resolver = methodResolvers[priority]; + if (resolver.IsMatch(i)) + { + return new ExecuteMethodRef + { + Method = i, + Resolver = resolver, + Priority = priority + }; + } + } + + return null; }) - .FirstOrDefault(); + .Where(i => i != null) + .OrderBy(i => i.Priority) + .ToList(); - if (method == null) + if (methods.Count == 0) { Log.Error("public void {0}(IDbCommand command, IReadOnlyDictionary variables) not found in {1}.".FormatWith(ExecutorMethodName, type)); } - return method; + return methods[0]; + } + + private sealed class ExecuteMethodRef + { + public int Priority { get; set; } + + public MethodInfo Method { get; set; } + + public ExecuteMethodResolverBase Resolver { get; set; } } } } diff --git a/Sources/SqlDatabase/Scripts/AssemblyInternal/ExecuteMethodResolverBase.cs b/Sources/SqlDatabase/Scripts/AssemblyInternal/ExecuteMethodResolverBase.cs new file mode 100644 index 00000000..b91a0f43 --- /dev/null +++ b/Sources/SqlDatabase/Scripts/AssemblyInternal/ExecuteMethodResolverBase.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Reflection; + +namespace SqlDatabase.Scripts.AssemblyInternal +{ + internal abstract class ExecuteMethodResolverBase + { + public abstract bool IsMatch(MethodInfo method); + + public abstract Action> CreateDelegate(object instance, MethodInfo method); + } +} diff --git a/Sources/SqlDatabase/Scripts/AssemblyInternal/ExecuteMethodResolverCommand.cs b/Sources/SqlDatabase/Scripts/AssemblyInternal/ExecuteMethodResolverCommand.cs new file mode 100644 index 00000000..57a7f7eb --- /dev/null +++ b/Sources/SqlDatabase/Scripts/AssemblyInternal/ExecuteMethodResolverCommand.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Reflection; + +namespace SqlDatabase.Scripts.AssemblyInternal +{ + // public void Execute(IDbCommand command) + internal sealed class ExecuteMethodResolverCommand : ExecuteMethodResolverBase + { + public override bool IsMatch(MethodInfo method) + { + var parameters = method.GetParameters(); + return parameters.Length == 1 + && typeof(IDbCommand) == parameters[0].ParameterType; + } + + public override Action> CreateDelegate(object instance, MethodInfo method) + { + var execute = (Action)Delegate.CreateDelegate( + typeof(Action), + instance, + method); + + return (command, variables) => execute(command); + } + } +} \ No newline at end of file diff --git a/Sources/SqlDatabase/Scripts/AssemblyInternal/ExecuteMethodResolverCommandDictionary.cs b/Sources/SqlDatabase/Scripts/AssemblyInternal/ExecuteMethodResolverCommandDictionary.cs new file mode 100644 index 00000000..6c28fe69 --- /dev/null +++ b/Sources/SqlDatabase/Scripts/AssemblyInternal/ExecuteMethodResolverCommandDictionary.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Reflection; + +namespace SqlDatabase.Scripts.AssemblyInternal +{ + // public void Execute(IDbCommand command, IReadOnlyDictionary variables) + internal sealed class ExecuteMethodResolverCommandDictionary : ExecuteMethodResolverBase + { + public override bool IsMatch(MethodInfo method) + { + var parameters = method.GetParameters(); + return parameters.Length == 2 + && typeof(IDbCommand) == parameters[0].ParameterType + && typeof(IReadOnlyDictionary) == parameters[1].ParameterType; + } + + public override Action> CreateDelegate(object instance, MethodInfo method) + { + return (Action>)Delegate.CreateDelegate( + typeof(Action>), + instance, + method); + } + } +} \ No newline at end of file diff --git a/Sources/SqlDatabase/Scripts/AssemblyInternal/ExecuteMethodResolverDbConnection.cs b/Sources/SqlDatabase/Scripts/AssemblyInternal/ExecuteMethodResolverDbConnection.cs new file mode 100644 index 00000000..e0703ced --- /dev/null +++ b/Sources/SqlDatabase/Scripts/AssemblyInternal/ExecuteMethodResolverDbConnection.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Reflection; + +namespace SqlDatabase.Scripts.AssemblyInternal +{ + // public void Execute(IDbConnection connection) + internal sealed class ExecuteMethodResolverDbConnection : ExecuteMethodResolverBase + { + public override bool IsMatch(MethodInfo method) + { + var parameters = method.GetParameters(); + return parameters.Length == 1 + && typeof(IDbConnection) == parameters[0].ParameterType; + } + + public override Action> CreateDelegate(object instance, MethodInfo method) + { + var execute = (Action)Delegate.CreateDelegate( + typeof(Action), + instance, + method); + + return (command, variables) => execute(command.Connection); + } + } +} \ No newline at end of file diff --git a/Sources/SqlDatabase/Scripts/AssemblyInternal/ExecuteMethodResolverDictionaryCommand.cs b/Sources/SqlDatabase/Scripts/AssemblyInternal/ExecuteMethodResolverDictionaryCommand.cs new file mode 100644 index 00000000..d06b0fa3 --- /dev/null +++ b/Sources/SqlDatabase/Scripts/AssemblyInternal/ExecuteMethodResolverDictionaryCommand.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Reflection; + +namespace SqlDatabase.Scripts.AssemblyInternal +{ + // public void Execute(IReadOnlyDictionary variables, IDbCommand command) + internal sealed class ExecuteMethodResolverDictionaryCommand : ExecuteMethodResolverBase + { + public override bool IsMatch(MethodInfo method) + { + var parameters = method.GetParameters(); + return parameters.Length == 2 + && typeof(IReadOnlyDictionary) == parameters[0].ParameterType + && typeof(IDbCommand) == parameters[1].ParameterType; + } + + public override Action> CreateDelegate(object instance, MethodInfo method) + { + var execute = (Action, IDbCommand>)Delegate.CreateDelegate( + typeof(Action, IDbCommand>), + instance, + method); + + return (command, variables) => execute(variables, command); + } + } +} \ No newline at end of file diff --git a/Sources/SqlDatabase/Scripts/AssemblyInternal/ExecuteMethodResolverSqlConnection.cs b/Sources/SqlDatabase/Scripts/AssemblyInternal/ExecuteMethodResolverSqlConnection.cs new file mode 100644 index 00000000..f0d63de9 --- /dev/null +++ b/Sources/SqlDatabase/Scripts/AssemblyInternal/ExecuteMethodResolverSqlConnection.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.SqlClient; +using System.Reflection; + +namespace SqlDatabase.Scripts.AssemblyInternal +{ + // public void Execute(SqlConnection connection) + internal sealed class ExecuteMethodResolverSqlConnection : ExecuteMethodResolverBase + { + public override bool IsMatch(MethodInfo method) + { + var parameters = method.GetParameters(); + return parameters.Length == 1 + && typeof(SqlConnection) == parameters[0].ParameterType; + } + + public override Action> CreateDelegate(object instance, MethodInfo method) + { + var execute = (Action)Delegate.CreateDelegate( + typeof(Action), + instance, + method); + + return (command, variables) => execute((SqlConnection)command.Connection); + } + } +} \ No newline at end of file diff --git a/Sources/SqlDatabase/Scripts/AssemblyScript.cs b/Sources/SqlDatabase/Scripts/AssemblyScript.cs index 09817305..67930588 100644 --- a/Sources/SqlDatabase/Scripts/AssemblyScript.cs +++ b/Sources/SqlDatabase/Scripts/AssemblyScript.cs @@ -1,5 +1,6 @@ using System; using System.Data; +using SqlDatabase.Configuration; using SqlDatabase.Scripts.AssemblyInternal; namespace SqlDatabase.Scripts @@ -44,7 +45,10 @@ internal static void Execute( IVariables variables, ILogger logger) { - if (!agent.ResolveScriptExecutor(logger)) + var className = AppConfiguration.GetCurrent().AssemblyScript.ClassName; + var methodName = AppConfiguration.GetCurrent().AssemblyScript.MethodName; + + if (!agent.ResolveScriptExecutor(logger, className, methodName)) { throw new InvalidOperationException("Fail to resolve script executor."); }