diff --git a/NewRelic.Microsoft.SqlServer.Plugin.sln b/NewRelic.Microsoft.SqlServer.Plugin.sln index 6d93137..1ac251c 100644 --- a/NewRelic.Microsoft.SqlServer.Plugin.sln +++ b/NewRelic.Microsoft.SqlServer.Plugin.sln @@ -1,6 +1,6 @@  -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2012 +Microsoft Visual Studio Solution File, Format Version 11.00 +# Visual Studio 2010 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NewRelic.Microsoft.SqlServer.Plugin", "src\NewRelic.Microsoft.SqlServer.Plugin\NewRelic.Microsoft.SqlServer.Plugin.csproj", "{D5E2A43F-3302-420E-BFD0-798A328390C4}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{E17681A6-21E5-4111-8E50-37A442286BB6}" diff --git a/README.md b/README.md index c2e13cb..267b2a4 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ A plugin for monitoring Microsoft SQL Server using the New Relic platform. 1. Run a text editor **as administrator** and open the file `INSTALLDIR\NewRelic.Microsoft.SqlServer.Plugin.exe.config`. 2. Find the setting `` and replace `YOUR_KEY_HERE` with your New Relic license key. 3. Configure one or more SQL Servers or Azure SQL Databases - * In the `` section, add a `` setting for a SQL Server. + * In the `` section, add a `` setting for _each_ SQL Server instance you wish to monitor. * `name="Production Database"` The name of your server is visible on the New Relic dashboard. * `connectionString="Server=prd.domain.com,1433;Database=master;Trusted_Connection=True;"` Any valid connection string to your database. * In the `` section, add a `` setting for _each_ Windows Azure SQL Database. diff --git a/build/versions.targets b/build/versions.targets index 6a65dbc..ede352d 100644 --- a/build/versions.targets +++ b/build/versions.targets @@ -3,7 +3,7 @@ 1 0 - 11 + 12 diff --git a/lib/NewRelic.Platform.Sdk.dll b/lib/NewRelic.Platform.Sdk.dll new file mode 100644 index 0000000..1f85614 Binary files /dev/null and b/lib/NewRelic.Platform.Sdk.dll differ diff --git a/src/Common/CommonAssemblyInfo.cs b/src/Common/CommonAssemblyInfo.cs index 4b2cf58..ae45373 100644 --- a/src/Common/CommonAssemblyInfo.cs +++ b/src/Common/CommonAssemblyInfo.cs @@ -7,6 +7,6 @@ [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: ComVisible(false)] -[assembly: AssemblyVersion("1.0.11")] -[assembly: AssemblyFileVersion("1.0.11")] -[assembly: AssemblyInformationalVersion("1.0.11")] +[assembly: AssemblyVersion("1.0.12")] +[assembly: AssemblyFileVersion("1.0.12")] +[assembly: AssemblyInformationalVersion("1.0.12")] diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/Configuration/ComponentDataRetrieverTests.cs b/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/Configuration/ComponentDataRetrieverTests.cs deleted file mode 100644 index be864bf..0000000 --- a/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/Configuration/ComponentDataRetrieverTests.cs +++ /dev/null @@ -1,224 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -using NSubstitute; - -using NUnit.Framework; - -using NewRelic.Microsoft.SqlServer.Plugin.Core; -using NewRelic.Microsoft.SqlServer.Plugin.Core.Extensions; -using NewRelic.Platform.Binding.DotNET; - -namespace NewRelic.Microsoft.SqlServer.Plugin.Configuration -{ - [TestFixture] - public class ComponentDataRetrieverTests - { - public class GenerateComponentDataInput - { - public string QueryName { get; set; } - public bool DataSent { get; set; } - public MetricTransformEnum MetricTransformEnum { get; set; } - public Dictionary Metrics { get; set; } - } - - public IEnumerable GetComponentDataTestCases - { - get - { - return new[] - { - new TestCaseData((object) new[] - { - new GenerateComponentDataInput - { - QueryName = "RegularQuery", - DataSent = true, - MetricTransformEnum = MetricTransformEnum.Simple, - Metrics = new[] - { - new KeyValuePair("Component/Metric/Foo", 1), - new KeyValuePair("Component/Metric/Bar", 3), - new KeyValuePair("Component/Metric/Baz", 45), - }.ToDictionary(k => k.Key, k => k.Value), - }, - new GenerateComponentDataInput - { - QueryName = "RegularQuery", - DataSent = false, - MetricTransformEnum = MetricTransformEnum.Simple, - Metrics = new[] - { - new KeyValuePair("Component/Metric/Foo", 6), - new KeyValuePair("Component/Metric/Bar", 5), - new KeyValuePair("Component/Metric/Baz", 90), - }.ToDictionary(k => k.Key, k => k.Value), - } - }).Returns(new[] - { - "Component/Metric/Foo:6", - "Component/Metric/Bar:5", - "Component/Metric/Baz:90", - }).SetName("Simple non-delta test"), - new TestCaseData((object) new[] - { - //Oldest - new GenerateComponentDataInput - { - QueryName = "RegularQuery", - DataSent = true, - MetricTransformEnum = MetricTransformEnum.Delta, - Metrics = new[] - { - new KeyValuePair("Component/Metric/Foo", 25), - new KeyValuePair("Component/Metric/Bar", 100), - new KeyValuePair("Component/Metric/Baz", 50), - }.ToDictionary(k => k.Key, k => k.Value), - }, - //Newest - new GenerateComponentDataInput - { - QueryName = "RegularQuery", - DataSent = false, - MetricTransformEnum = MetricTransformEnum.Delta, - Metrics = new[] - { - new KeyValuePair("Component/Metric/Foo", 30), - new KeyValuePair("Component/Metric/Bar", 140), - new KeyValuePair("Component/Metric/Baz", 60), - }.ToDictionary(k => k.Key, k => k.Value), - } - }).Returns(new[] - { - "Component/Metric/Foo:5", - "Component/Metric/Bar:40", - "Component/Metric/Baz:10", - }).SetName("Delta simple test"), - new TestCaseData((object) new[] - { - new GenerateComponentDataInput - { - QueryName = "RegularQuery", - DataSent = true, - MetricTransformEnum = MetricTransformEnum.Delta, - Metrics = new[] - { - new KeyValuePair("Component/Metric/Foo", 10), - new KeyValuePair("Component/Metric/Bar", 105), - new KeyValuePair("Component/Metric/Baz", 49), - }.ToDictionary(k => k.Key, k => k.Value), - }, - new GenerateComponentDataInput - { - QueryName = "RegularQuery", - DataSent = false, - MetricTransformEnum = MetricTransformEnum.Delta, - Metrics = new[] - { - new KeyValuePair("Component/Metric/Foo", 20), - new KeyValuePair("Component/Metric/Bar", 100), - new KeyValuePair("Component/Metric/Baz", 50), - }.ToDictionary(k => k.Key, k => k.Value), - } - }).Returns(new[] - { - "Component/Metric/Foo:10", - "Component/Metric/Bar:0", - "Component/Metric/Baz:1", - }).SetName("Delta with drop in int metric value should send zero test"), - new TestCaseData((object) new[] - { - new GenerateComponentDataInput - { - QueryName = "RegularQuery", - DataSent = true, - MetricTransformEnum = MetricTransformEnum.Delta, - Metrics = new[] - { - new KeyValuePair("Component/Metric/Foo", 10.0m), - new KeyValuePair("Component/Metric/Bar", 105.0m), - new KeyValuePair("Component/Metric/Baz", 49.0m), - }.ToDictionary(k => k.Key, k => k.Value), - }, - new GenerateComponentDataInput - { - QueryName = "RegularQuery", - DataSent = false, - MetricTransformEnum = MetricTransformEnum.Delta, - Metrics = new[] - { - new KeyValuePair("Component/Metric/Foo", 20.0m), - new KeyValuePair("Component/Metric/Bar", 100.0m), - new KeyValuePair("Component/Metric/Baz", 50.0m), - }.ToDictionary(k => k.Key, k => k.Value), - } - }).Returns(new[] - { - "Component/Metric/Foo:10.0", - "Component/Metric/Bar:0.0", - "Component/Metric/Baz:1.0", - }).SetName("Delta with drop in decimal metric value should send zero test"), - new TestCaseData((object) new[] - { - new GenerateComponentDataInput - { - QueryName = "RegularQuery", - DataSent = true, - MetricTransformEnum = MetricTransformEnum.Simple, - Metrics = new[] - { - new KeyValuePair("Component/Metric/Foo", 1), - new KeyValuePair("Component/Metric/Bar", 3), - new KeyValuePair("Component/Metric/Baz", 45), - }.ToDictionary(k => k.Key, k => k.Value), - }, - new GenerateComponentDataInput - { - QueryName = "WackyQuery", - DataSent = false, - MetricTransformEnum = MetricTransformEnum.Simple, - Metrics = new[] - { - new KeyValuePair("Component/Metric/Foo", 6), - new KeyValuePair("Component/Metric/Bar", 5), - new KeyValuePair("Component/Metric/Baz", 89), - }.ToDictionary(k => k.Key, k => k.Value), - } - }).Throws(typeof (ArgumentException)).SetName("Assert Different Query Names throws Exception Non-Aggregate Test"), - }; - } - } - - [Test] - [TestCaseSource("GetComponentDataTestCases")] - public string[] AssertThatComponentDataGeneratedCorrectly(GenerateComponentDataInput[] inputdata) - { - var queryContextHistory = inputdata.Select(input => - { - var queryContext = Substitute.For(); - queryContext.QueryName.Returns(input.QueryName); - queryContext.DataSent.Returns(input.DataSent); - queryContext.MetricTransformEnum.Returns(input.MetricTransformEnum); - queryContext.ComponentData = new ComponentData(); - input.Metrics.ForEach(m => - { - if (m.Value is decimal) - { - queryContext.ComponentData.AddMetric(m.Key, (decimal) m.Value); - } - - if (m.Value is int) - { - queryContext.ComponentData.AddMetric(m.Key, (int) m.Value); - } - }); - return queryContext; - }).ToArray(); - - var componentData = ComponentDataRetriever.GetData(queryContextHistory); - - return componentData.Metrics.Select(m => String.Format("{0}:{1}", m.Key, m.Value)).ToArray(); - } - } -} diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/Configuration/SqlServerEndpointTests.cs b/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/Configuration/SqlServerEndpointTests.cs index 68b9dfc..4ee4569 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/Configuration/SqlServerEndpointTests.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/Configuration/SqlServerEndpointTests.cs @@ -14,329 +14,78 @@ namespace NewRelic.Microsoft.SqlServer.Plugin.Configuration { - [TestFixture] - public class SqlServerEndpointTests - { - public IEnumerable QueryHistoryTestData - { - get - { - return new[] - { - new TestCaseData((object) new[] {new[] {"QueryOne", "QueryTwo", "QueryThree"}}) - .Returns(new[] - { - "QueryOne:1", - "QueryTwo:1", - "QueryThree:1" - }.ToArray()).SetName("Simple History"), - new TestCaseData((object) new[] {new[] {"QueryOne", "QueryOne", "QueryTwo", "QueryTwo", "QueryTwo", "QueryThree", "QueryThree", "QueryThree", "QueryThree"}}) - .Returns(new[] - { - "QueryOne:2", - "QueryTwo:2", - "QueryThree:2", - }.ToArray()).SetName("Limit to 2 single pass"), - new TestCaseData((object) new[] - { - new[] - { - "QueryOne", "QueryTwo", "QueryThree" - }, - new[] - { - "QueryOne", "QueryTwo", "QueryThree" - }, - new[] - { - "QueryOne", "QueryTwo", "QueryThree" - }, - new[] - { - "QueryOne", "QueryTwo", "QueryThree" - }, - new[] - { - "QueryOne", "QueryTwo", "QueryThree" - }, - }) - .Returns(new[] - { - "QueryOne:2", - "QueryTwo:2", - "QueryThree:2", - }.ToArray()).SetName("Limit to 2 multi pass"), - }; - } - } - - public IEnumerable GenerateComponentDataTestCases - { - get - { - return new[] - { - new TestCaseData((object) new[] - { - new ComponentDataRetrieverTests.GenerateComponentDataInput - { - QueryName = "RegularQuery", - DataSent = false, - MetricTransformEnum = MetricTransformEnum.Simple, - Metrics = new[] - { - new KeyValuePair("Component/Metric/Foo", 1), - new KeyValuePair("Component/Metric/Bar", 3), - new KeyValuePair("Component/Metric/Baz", 45), - }.ToDictionary(k => k.Key, k => k.Value), - }, - new ComponentDataRetrieverTests.GenerateComponentDataInput - { - QueryName = "RegularQuery", - DataSent = true, - MetricTransformEnum = MetricTransformEnum.Simple, - Metrics = new[] - { - new KeyValuePair("Component/Metric/Foo", 6), - new KeyValuePair("Component/Metric/Bar", 5), - new KeyValuePair("Component/Metric/Baz", 89), - }.ToDictionary(k => k.Key, k => k.Value), - } - }).Returns(new[] - { - "Component/Metric/Foo:1", - "Component/Metric/Bar:3", - "Component/Metric/Baz:45", - }).SetName("Simple non-delta test"), - new TestCaseData((object) new[] - { - new ComponentDataRetrieverTests.GenerateComponentDataInput - { - QueryName = "RegularQuery", - DataSent = false, - MetricTransformEnum = MetricTransformEnum.Delta, - Metrics = new[] - { - new KeyValuePair("Component/Metric/Foo", 25), - new KeyValuePair("Component/Metric/Bar", 140), - new KeyValuePair("Component/Metric/Baz", 60), - }.ToDictionary(k => k.Key, k => k.Value), - }, - new ComponentDataRetrieverTests.GenerateComponentDataInput - { - QueryName = "RegularQuery", - DataSent = true, - MetricTransformEnum = MetricTransformEnum.Delta, - Metrics = new[] - { - new KeyValuePair("Component/Metric/Foo", 20), - new KeyValuePair("Component/Metric/Bar", 100), - new KeyValuePair("Component/Metric/Baz", 50), - }.ToDictionary(k => k.Key, k => k.Value), - } - }).Returns(new[] - { - "Component/Metric/Foo:5", - "Component/Metric/Bar:40", - "Component/Metric/Baz:10", - }).SetName("Delta simple test"), - new TestCaseData((object) new[] - { - new ComponentDataRetrieverTests.GenerateComponentDataInput - { - QueryName = "RegularQuery", - DataSent = false, - MetricTransformEnum = MetricTransformEnum.Delta, - Metrics = new[] - { - new KeyValuePair("Component/Metric/Foo", 10), - new KeyValuePair("Component/Metric/Bar", 105), - new KeyValuePair("Component/Metric/Baz", 49), - }.ToDictionary(k => k.Key, k => k.Value), - }, - new ComponentDataRetrieverTests.GenerateComponentDataInput - { - QueryName = "RegularQuery", - DataSent = true, - MetricTransformEnum = MetricTransformEnum.Delta, - Metrics = new[] - { - new KeyValuePair("Component/Metric/Foo", 20), - new KeyValuePair("Component/Metric/Bar", 100), - new KeyValuePair("Component/Metric/Baz", 50), - }.ToDictionary(k => k.Key, k => k.Value), - } - }).Returns(new[] - { - "Component/Metric/Foo:0", - "Component/Metric/Bar:5", - "Component/Metric/Baz:0", - }).SetName("Delta with drop in int metric value should send zero test"), - new TestCaseData((object) new[] - { - new ComponentDataRetrieverTests.GenerateComponentDataInput - { - QueryName = "RegularQuery", - DataSent = false, - MetricTransformEnum = MetricTransformEnum.Delta, - Metrics = new[] - { - new KeyValuePair("Component/Metric/Foo", 10.0m), - new KeyValuePair("Component/Metric/Bar", 105.0m), - new KeyValuePair("Component/Metric/Baz", 49.0m), - }.ToDictionary(k => k.Key, k => k.Value), - }, - new ComponentDataRetrieverTests.GenerateComponentDataInput - { - QueryName = "RegularQuery", - DataSent = true, - MetricTransformEnum = MetricTransformEnum.Delta, - Metrics = new[] - { - new KeyValuePair("Component/Metric/Foo", 20.0m), - new KeyValuePair("Component/Metric/Bar", 100.0m), - new KeyValuePair("Component/Metric/Baz", 50.0m), - }.ToDictionary(k => k.Key, k => k.Value), - } - }).Returns(new[] - { - "Component/Metric/Foo:0.0", - "Component/Metric/Bar:5.0", - "Component/Metric/Baz:0.0", - }).SetName("Delta with drop in decimal metric value should send zero test"), - new TestCaseData((object) new[] - { - new ComponentDataRetrieverTests.GenerateComponentDataInput - { - QueryName = "RegularQuery", - DataSent = false, - MetricTransformEnum = MetricTransformEnum.Simple, - Metrics = new[] - { - new KeyValuePair("Component/Metric/Foo", 1), - new KeyValuePair("Component/Metric/Bar", 3), - new KeyValuePair("Component/Metric/Baz", 45), - }.ToDictionary(k => k.Key, k => k.Value), - }, - new ComponentDataRetrieverTests.GenerateComponentDataInput - { - QueryName = "WackyQuery", - DataSent = true, - MetricTransformEnum = MetricTransformEnum.Simple, - Metrics = new[] - { - new KeyValuePair("Component/Metric/Foo", 6), - new KeyValuePair("Component/Metric/Bar", 5), - new KeyValuePair("Component/Metric/Baz", 89), - }.ToDictionary(k => k.Key, k => k.Value), - } - }).Throws(typeof (ArgumentException)).SetName("Assert Different Query Names throws Exception Non-Aggregate Test"), - }; - } - } - - [TestCaseSource("QueryHistoryTestData")] - public string[] Assert_that_query_history_updated_appropriately(string[][] queryNames) - { - var sqlServerToMonitor = new SqlServerEndpoint("Best_DB_Ever", "", false); - - Assert.That(sqlServerToMonitor.QueryHistory.Count, Is.EqualTo(0), "History Should start off empty"); - - queryNames.ForEach(queryNamesPass => - { - IQueryContext[] queryContexts = queryNamesPass.Select(queryName => - { - var queryContext = Substitute.For(); - queryContext.QueryName.Returns(queryName); - return queryContext; - }).ToArray(); - sqlServerToMonitor.UpdateHistory(queryContexts); - }); - - string[] actual = sqlServerToMonitor.QueryHistory.Select(qh => string.Format("{0}:{1}", qh.Key, qh.Value.Count)).ToArray(); - - return actual; - } - - [Test] - public void Assert_complex_include_system_databases_works() - { - var includeDbNames = new[] {"FooDb", "BarDb"}; - IEnumerable includedDbs = includeDbNames.Select(x => new Database {Name = x,}); - - var sqlServerToMonitor = new SqlServerEndpoint("FooServer", ".", true, includedDbs, null); - Assert.That(sqlServerToMonitor.ExcludedDatabaseNames.Length, Is.EqualTo(0)); - - IEnumerable expectedIncludes = Constants.SystemDatabases.ToList().Concat(includeDbNames); - Assert.That(sqlServerToMonitor.IncludedDatabaseNames, Is.EquivalentTo(expectedIncludes)); - } - - [Test] - public void Assert_include_exclude_lists_built_appropriately() - { - IEnumerable includedDbs = new[] {"FooDb", "BarDb"}.Select(x => new Database {Name = x,}); - var sqlServerToMonitor = new SqlServerEndpoint("FooServer", ".", false, includedDbs, new[] {"Baz"}); - Assert.That(sqlServerToMonitor.IncludedDatabaseNames, Is.EquivalentTo(new[] {"FooDb", "BarDb"})); - Assert.That(sqlServerToMonitor.ExcludedDatabaseNames, Is.EquivalentTo(Constants.SystemDatabases.Concat(new[] {"Baz"}))); - } - - [Test] - public void Assert_include_system_databases_works() - { - var sqlServerToMonitor = new SqlServerEndpoint("FooServer", ".", false); - Assert.That(sqlServerToMonitor.IncludedDatabaseNames.Length, Is.EqualTo(0)); - Assert.That(sqlServerToMonitor.ExcludedDatabaseNames, Is.EquivalentTo(Constants.SystemDatabases)); - - sqlServerToMonitor = new SqlServerEndpoint("FooServer", ".", true); - Assert.That(sqlServerToMonitor.IncludedDatabaseNames.Length, Is.EqualTo(0)); - Assert.That(sqlServerToMonitor.ExcludedDatabaseNames.Length, Is.EqualTo(0)); - } - - [Test] - public void Assert_that_duration_is_reported_correctly() - { - var sqlServerToMonitor = new SqlServerEndpoint("", "", false); - Assert.That(sqlServerToMonitor.Duration, Is.EqualTo(0), "Expected 0 second Duration immediately after .ctor called"); - - Thread.Sleep(1000); - Assert.That(sqlServerToMonitor.Duration, Is.EqualTo(1), "Expected 1 second Duration after Thread.Sleep(1000)"); - } - - [Test] - public void Assert_that_max_recompile_summary_ignores_other_metrics() - { - object[] metrics = {new SqlCpuUsage()}; - var results = new SqlServerEndpoint(null, null, false).GetMaxRecompileSummaryMetric(metrics); - Assert.That(results, Is.Null, "Implementation should have ignored bad data."); - } - - [Test] - public void Assert_that_max_recompile_summary_ignores_empty_metric_array() - { - object[] metrics = {}; - var results = new SqlServerEndpoint(null, null, false).GetMaxRecompileSummaryMetric(metrics); - Assert.That(results, Is.Null, "Implementation should have ignored empty data."); - } - - [Test] - public void Assert_that_max_recompile_summary_is_reported() - { - object[] metrics = - { - new RecompileSummary { DatabaseName = "A", SingleUseObjects = 100, SingleUsePercent = 0, MultipleUseObjects = 1, }, - new RecompileSummary { DatabaseName = "B", SingleUseObjects = 1, SingleUsePercent = 80, MultipleUseObjects = 1, }, - new RecompileSummary { DatabaseName = "C", SingleUseObjects = 1, SingleUsePercent = 0, MultipleUseObjects = 50, }, - }; - var queryContext = new SqlServerEndpoint(null, null, false).GetMaxRecompileSummaryMetric(metrics); - var results = queryContext.Results.ToArray(); - - Assert.That(queryContext, Is.Not.SameAs(metrics), "Expected a new Array"); - - var max = results.OfType().SingleOrDefault(); - Assert.That(max, Is.Not.Null, "Expected a new metric in the results"); - Assert.That(max.SingleUseObjects, Is.EqualTo(100), "Wrong SingleUseObjects value"); - Assert.That(max.SingleUsePercent, Is.EqualTo(80m), "Wrong SingleUsePercent value"); - Assert.That(max.MultipleUseObjects, Is.EqualTo(50), "Wrong MultipleUseObjects value"); - } - } + [TestFixture] + public class SqlServerEndpointTests + { + [Test] + public void Assert_complex_include_system_databases_works() + { + var includeDbNames = new[] {"FooDb", "BarDb"}; + IEnumerable includedDbs = includeDbNames.Select(x => new Database {Name = x,}); + + var sqlServerToMonitor = new SqlServerEndpoint("FooServer", ".", true, includedDbs, null); + Assert.That(sqlServerToMonitor.ExcludedDatabaseNames.Length, Is.EqualTo(0)); + + IEnumerable expectedIncludes = Constants.SystemDatabases.ToList().Concat(includeDbNames); + Assert.That(sqlServerToMonitor.IncludedDatabaseNames, Is.EquivalentTo(expectedIncludes)); + } + + [Test] + public void Assert_include_exclude_lists_built_appropriately() + { + IEnumerable includedDbs = new[] {"FooDb", "BarDb"}.Select(x => new Database {Name = x,}); + var sqlServerToMonitor = new SqlServerEndpoint("FooServer", ".", false, includedDbs, new[] {"Baz"}); + Assert.That(sqlServerToMonitor.IncludedDatabaseNames, Is.EquivalentTo(new[] {"FooDb", "BarDb"})); + Assert.That(sqlServerToMonitor.ExcludedDatabaseNames, Is.EquivalentTo(Constants.SystemDatabases.Concat(new[] {"Baz"}))); + } + + [Test] + public void Assert_include_system_databases_works() + { + var sqlServerToMonitor = new SqlServerEndpoint("FooServer", ".", false); + Assert.That(sqlServerToMonitor.IncludedDatabaseNames.Length, Is.EqualTo(0)); + Assert.That(sqlServerToMonitor.ExcludedDatabaseNames, Is.EquivalentTo(Constants.SystemDatabases)); + + sqlServerToMonitor = new SqlServerEndpoint("FooServer", ".", true); + Assert.That(sqlServerToMonitor.IncludedDatabaseNames.Length, Is.EqualTo(0)); + Assert.That(sqlServerToMonitor.ExcludedDatabaseNames.Length, Is.EqualTo(0)); + } + + [Test] + public void Assert_that_max_recompile_summary_ignores_other_metrics() + { + object[] metrics = {new SqlCpuUsage()}; + var results = new SqlServerEndpoint(null, null, false).GetMaxRecompileSummaryMetric(metrics); + Assert.That(results, Is.Null, "Implementation should have ignored bad data."); + } + + [Test] + public void Assert_that_max_recompile_summary_ignores_empty_metric_array() + { + object[] metrics = {}; + var results = new SqlServerEndpoint(null, null, false).GetMaxRecompileSummaryMetric(metrics); + Assert.That(results, Is.Null, "Implementation should have ignored empty data."); + } + + [Test] + public void Assert_that_max_recompile_summary_is_reported() + { + object[] metrics = + { + new RecompileSummary { DatabaseName = "A", SingleUseObjects = 100, SingleUsePercent = 0, MultipleUseObjects = 1, }, + new RecompileSummary { DatabaseName = "B", SingleUseObjects = 1, SingleUsePercent = 80, MultipleUseObjects = 1, }, + new RecompileSummary { DatabaseName = "C", SingleUseObjects = 1, SingleUsePercent = 0, MultipleUseObjects = 50, }, + }; + var queryContext = new SqlServerEndpoint(null, null, false).GetMaxRecompileSummaryMetric(metrics); + var results = queryContext.Results.ToArray(); + + Assert.That(queryContext, Is.Not.SameAs(metrics), "Expected a new Array"); + + var max = results.OfType().SingleOrDefault(); + Assert.That(max, Is.Not.Null, "Expected a new metric in the results"); + Assert.That(max.SingleUseObjects, Is.EqualTo(100), "Wrong SingleUseObjects value"); + Assert.That(max.SingleUsePercent, Is.EqualTo(80m), "Wrong SingleUsePercent value"); + Assert.That(max.MultipleUseObjects, Is.EqualTo(50), "Wrong MultipleUseObjects value"); + } + } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/MetricMapperTests.cs b/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/MetricMapperTests.cs index c78ebf3..cbcd9b8 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/MetricMapperTests.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/MetricMapperTests.cs @@ -3,226 +3,107 @@ using NUnit.Framework; using NewRelic.Microsoft.SqlServer.Plugin.Core; -using NewRelic.Platform.Binding.DotNET; namespace NewRelic.Microsoft.SqlServer.Plugin { - [TestFixture] - public class MetricMapperTests - { - protected class FakeQueryType - { - public long Long { get; set; } - public int Integer { get; set; } - public short Short { get; set; } - public byte Byte { get; set; } - public decimal Decimal { get; set; } - public string Comment { get; set; } - public DateTime EventTime { get; set; } - } - - protected class MarkedUpFakeQueryType - { - public string IgnoredCompletely { get; set; } - - [Metric(Ignore = true)] - public DateTime DoublyIgnored { get; set; } - - [Metric(Ignore = false)] - public object IgnoreAnyways { get; set; } - - [Metric(MetricName = "FancyName")] - public int MetricNameOverridden { get; set; } - - [Metric(MetricName = "SuperImportant", Ignore = true)] - public int NiceNameButIgnored { get; set; } - - [Metric(Ignore = true)] - public int SimplyIgnored { get; set; } - - [Metric(MetricValueType = MetricValueType.Value)] - public long LongValueMetric { get; set; } - - [Metric(MetricValueType = MetricValueType.Value)] - public int IntValueMetric { get; set; } - - [Metric(MetricValueType = MetricValueType.Value)] - public byte ByteValueMetric { get; set; } - - [Metric(MetricValueType = MetricValueType.Count)] - public decimal DecimalCountMetric { get; set; } - - public long ConventionalProperty { get; set; } - } - - protected class FakeTypeWithUnits - { - [Metric(Units = "[bytes/sec]")] - public int RateOfSomething { get; set; } - } - - private static ComponentData CreateComponentDataForFake(object fake, string propertyName) - { - var componentData = new ComponentData(); - - var query = new SqlQuery(fake.GetType(), new SqlServerQueryAttribute(null, "Fake"), null, ""); - var queryContext = new QueryContext(query) {ComponentData = componentData,}; - - var metricMapper = new MetricMapper(fake.GetType().GetProperty(propertyName)); - - metricMapper.AddMetric(queryContext, fake); - return componentData; - } - - [Test] - public void Assert_decimal_is_mapped() - { - var fake = new FakeQueryType {Decimal = 12.3m,}; - var componentData = CreateComponentDataForFake(fake, "Decimal"); - - const string metricKey = "Fake/Decimal"; - Assert.That(componentData.Metrics.ContainsKey(metricKey), "Expected metric with correct name to be added"); - var condition = componentData.Metrics[metricKey]; - Assert.That(condition, Is.EqualTo(fake.Decimal), "Metric not mapped correctly"); - } - - [Test] - [TestCase("Comment", TestName = "string")] - [TestCase("EventTime", TestName = "DateTime")] - public void Assert_invalid_type_throws(string propertyName) - { - var propertyInfo = typeof (FakeQueryType).GetProperty(propertyName); - Assume.That(propertyInfo, Is.Not.Null, "Expected a property name '{0}' on {1}", propertyName, typeof (FakeQueryType).Name); - - Assert.Throws(() => new MetricMapper(propertyInfo)); - } - - [Test] - public void Assert_metric_attribute_on_decimal_property_is_mapped_to_int_count() - { - var fake = new MarkedUpFakeQueryType {DecimalCountMetric = 12.3m,}; - var componentData = CreateComponentDataForFake(fake, "DecimalCountMetric"); - - const string metricKey = "Fake/DecimalCountMetric"; - Assert.That(componentData.Metrics.ContainsKey(metricKey), "Expected metric with correct name to be added"); - var condition = componentData.Metrics[metricKey]; - Assert.That(condition, Is.EqualTo(12), "Metric not mapped to int correctly"); - } - - [Test] - [TestCase("Byte")] - [TestCase("Short")] - [TestCase("Integer")] - [TestCase("Long")] - public void Assert_metric_attribute_on_integer_properties_is_mapped_to_decimal_value(string propertyName) - { - var fake = new FakeQueryType(); - var propertyInfo = fake.GetType().GetProperty(propertyName); - propertyInfo.SetValue(fake, (byte) 100, null); - - var componentData = CreateComponentDataForFake(fake, propertyName); - - var metricKey = "Fake/" + propertyName; - Assert.That(componentData.Metrics.ContainsKey(metricKey), "Expected metric with correct name to be added"); - - var condition = componentData.Metrics[metricKey]; - Assert.That(condition, Is.EqualTo(100m), "Metric not mapped to decimal correctly"); - } - - [Test] - public void Assert_metric_units_are_stored_by_mapper() - { - var mapper = MetricMapper.TryCreate(typeof(FakeTypeWithUnits).GetProperty("RateOfSomething")); - Assert.That(mapper, Is.Not.Null, "Mapping of FakeTypeWithUnits failed"); - Assert.That(mapper.MetricUnits, Is.EqualTo("[bytes/sec]"), "Units from MetricAttribute not mapped correctly"); - } - - [Test] - [TestCase("Byte", (byte) 27, TestName = "Byte")] - [TestCase("Short", (short) 32045, TestName = "Short")] - [TestCase("Integer", 42, TestName = "Integer")] - [TestCase("Long", 555L, TestName = "Long")] - public void Assert_numeric_types_are_mapped(string propertyName, object value) - { - var fake = new FakeQueryType(); - var propertyInfo = fake.GetType().GetProperty(propertyName); - propertyInfo.SetValue(fake, value, null); - - var componentData = CreateComponentDataForFake(fake, propertyName); - - var metricKey = "Fake/" + propertyName; - Assert.That(componentData.Metrics.ContainsKey(metricKey), "Expected metric with correct name to be added"); - var condition = componentData.Metrics[metricKey]; - Assert.That(condition, Is.EqualTo(value), "Metric not mapped correctly"); - } - - [Test(Description = "Returns the MetricName when the map is created. Otherwise, null.")] - [TestCase("IgnoredCompletely", Result = null, TestName = "Assert non-numeric is still ignored without an attribute")] - [TestCase("ConventionalProperty", Result = "ConventionalProperty", TestName = "Assert numeric is still discovered without an attribute")] - [TestCase("DoublyIgnored", Result = null, TestName = "Assert non-numeric is ignored with an attribute where Ignore == true")] - [TestCase("IgnoreAnyways", Result = null, TestName = "Assert non-numeric is ignored despite an attribute where Ignore == false")] - [TestCase("MetricNameOverridden", Result = "FancyName", TestName = "Assert numeric with attribute has MetricName overriden")] - [TestCase("NiceNameButIgnored", Result = null, TestName = "Assert numeric is ignored with attribute where MetricName is set and Ignore == true")] - [TestCase("SimplyIgnored", Result = null, TestName = "Assert numeric is ignored with attribute where Ignore == true")] - public string Assert_that_metric_attribute_is_respected(string propertyName) - { - var fakeType = typeof (MarkedUpFakeQueryType); - var metricMapper = MetricMapper.TryCreate(fakeType.GetProperty(propertyName)); - return metricMapper != null ? metricMapper.MetricName : null; - } - - [Test] - [TestCase("Comment", TestName = "string")] - [TestCase("EventTime", TestName = "DateTime")] - public void Assert_try_create_mapper_gracefully_refuses_invalid_types(string propertyName) - { - var propertyInfo = typeof (FakeQueryType).GetProperty(propertyName); - Assume.That(propertyInfo, Is.Not.Null, "Expected a property name '{0}' on {1}", propertyName, typeof (FakeQueryType).Name); - - var mapper = MetricMapper.TryCreate(propertyInfo); - Assert.That(mapper, Is.Null, "Should not have mapped {0}", propertyName); - } - - [Test] - public void Assert_try_create_mapper_handles_decimal_correctly() - { - var fake = new FakeQueryType {Decimal = 12.3m}; - - var componentData = new ComponentData(); - - var query = new SqlQuery(fake.GetType(), new SqlServerQueryAttribute(null, "Fake"), null, ""); - var queryContext = new QueryContext(query) {ComponentData = componentData,}; - - var mapper = MetricMapper.TryCreate(fake.GetType().GetProperty("Decimal")); - Assert.That(mapper, Is.Not.Null, "Mapping Decimal failed"); - - mapper.AddMetric(queryContext, fake); - - const string metricKey = "Fake/Decimal"; - Assert.That(componentData.Metrics.ContainsKey(metricKey), "Expected metric with correct name to be added"); - - var condition = componentData.Metrics[metricKey]; - Assert.That(condition, Is.EqualTo(fake.Decimal), "Metric not mapped correctly"); - } - - [Test] - [TestCase("Byte", (byte) 27, TestName = "Byte")] - [TestCase("Short", (short) 32045, TestName = "Short")] - [TestCase("Integer", 42, TestName = "Integer")] - [TestCase("Long", 555L, TestName = "Long")] - public void Assert_try_create_mapper_handles_numerics_correctly(string propertyName, object value) - { - var fake = new FakeQueryType(); - var propertyInfo = fake.GetType().GetProperty(propertyName); - propertyInfo.SetValue(fake, value, null); - - var componentData = CreateComponentDataForFake(fake, propertyName); - - var metricKey = "Fake/" + propertyName; - Assert.That(componentData.Metrics.ContainsKey(metricKey), "Expected metric with correct name to be added"); - - var condition = componentData.Metrics[metricKey]; - Assert.That(condition, Is.EqualTo(value), "Metric not mapped correctly"); - } - } + [TestFixture] + public class MetricMapperTests + { + protected class FakeQueryType + { + public long Long { get; set; } + public int Integer { get; set; } + public short Short { get; set; } + public byte Byte { get; set; } + public decimal Decimal { get; set; } + public string Comment { get; set; } + public DateTime EventTime { get; set; } + } + + protected class MarkedUpFakeQueryType + { + public string IgnoredCompletely { get; set; } + + [Metric(Ignore = true)] + public DateTime DoublyIgnored { get; set; } + + [Metric(Ignore = false)] + public object IgnoreAnyways { get; set; } + + [Metric(MetricName = "FancyName")] + public int MetricNameOverridden { get; set; } + + [Metric(MetricName = "SuperImportant", Ignore = true)] + public int NiceNameButIgnored { get; set; } + + [Metric(Ignore = true)] + public int SimplyIgnored { get; set; } + + [Metric(MetricValueType = MetricValueType.Value)] + public long LongValueMetric { get; set; } + + [Metric(MetricValueType = MetricValueType.Value)] + public int IntValueMetric { get; set; } + + [Metric(MetricValueType = MetricValueType.Value)] + public byte ByteValueMetric { get; set; } + + [Metric(MetricValueType = MetricValueType.Count)] + public decimal DecimalCountMetric { get; set; } + + public long ConventionalProperty { get; set; } + } + + protected class FakeTypeWithUnits + { + [Metric(Units = "bytes/sec")] + public int RateOfSomething { get; set; } + } + + [Test] + [TestCase("Comment", TestName = "string")] + [TestCase("EventTime", TestName = "DateTime")] + public void Assert_invalid_type_throws(string propertyName) + { + var propertyInfo = typeof (FakeQueryType).GetProperty(propertyName); + Assume.That(propertyInfo, Is.Not.Null, "Expected a property name '{0}' on {1}", propertyName, typeof (FakeQueryType).Name); + + Assert.Throws(() => new MetricMapper(propertyInfo)); + } + + [Test] + public void Assert_metric_units_are_stored_by_mapper() + { + var mapper = MetricMapper.TryCreate(typeof(FakeTypeWithUnits).GetProperty("RateOfSomething")); + Assert.That(mapper, Is.Not.Null, "Mapping of FakeTypeWithUnits failed"); + Assert.That(mapper.MetricUnits, Is.EqualTo("bytes/sec"), "Units from MetricAttribute not mapped correctly"); + } + + [Test(Description = "Returns the MetricName when the map is created. Otherwise, null.")] + [TestCase("IgnoredCompletely", Result = null, TestName = "Assert non-numeric is still ignored without an attribute")] + [TestCase("ConventionalProperty", Result = "ConventionalProperty", TestName = "Assert numeric is still discovered without an attribute")] + [TestCase("DoublyIgnored", Result = null, TestName = "Assert non-numeric is ignored with an attribute where Ignore == true")] + [TestCase("IgnoreAnyways", Result = null, TestName = "Assert non-numeric is ignored despite an attribute where Ignore == false")] + [TestCase("MetricNameOverridden", Result = "FancyName", TestName = "Assert numeric with attribute has MetricName overriden")] + [TestCase("NiceNameButIgnored", Result = null, TestName = "Assert numeric is ignored with attribute where MetricName is set and Ignore == true")] + [TestCase("SimplyIgnored", Result = null, TestName = "Assert numeric is ignored with attribute where Ignore == true")] + public string Assert_that_metric_attribute_is_respected(string propertyName) + { + var fakeType = typeof (MarkedUpFakeQueryType); + var metricMapper = MetricMapper.TryCreate(fakeType.GetProperty(propertyName)); + return metricMapper != null ? metricMapper.MetricName : null; + } + + [Test] + [TestCase("Comment", TestName = "string")] + [TestCase("EventTime", TestName = "DateTime")] + public void Assert_try_create_mapper_gracefully_refuses_invalid_types(string propertyName) + { + var propertyInfo = typeof (FakeQueryType).GetProperty(propertyName); + Assume.That(propertyInfo, Is.Not.Null, "Expected a property name '{0}' on {1}", propertyName, typeof (FakeQueryType).Name); + + var mapper = MetricMapper.TryCreate(propertyInfo); + Assert.That(mapper, Is.Null, "Should not have mapped {0}", propertyName); + } + } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/NewRelic.Microsoft.SqlServer.Plugin.Tests.csproj b/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/NewRelic.Microsoft.SqlServer.Plugin.Tests.csproj index 26cc28e..c3d06db 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/NewRelic.Microsoft.SqlServer.Plugin.Tests.csproj +++ b/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/NewRelic.Microsoft.SqlServer.Plugin.Tests.csproj @@ -29,9 +29,8 @@ False ..\..\packages\log4net.2.0.0\lib\net35-full\log4net.dll - - False - ..\NewRelic.Microsoft.SqlServer.Plugin\bin\Debug\NewRelic.Platform.Binding.DotNET.dll + + ..\..\lib\NewRelic.Platform.Sdk.dll ..\..\packages\NSubstitute.1.6.0.0\lib\NET35\NSubstitute.dll @@ -53,7 +52,6 @@ Properties\CommonAssemblyInfo.cs - diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/QueryContextTests.cs b/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/QueryContextTests.cs index 1f696e1..9fd75e3 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/QueryContextTests.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/QueryContextTests.cs @@ -8,179 +8,179 @@ namespace NewRelic.Microsoft.SqlServer.Plugin { - [TestFixture] - public class QueryContextTests - { - private class FakeQueryWithCustomPlaceHolder - { - // ReSharper disable UnusedAutoPropertyAccessor.Local - // ReSharper disable UnusedMember.Local - public string ThePlaceholder { get; set; } - public string AnotherPlaceholder { get; set; } - internal string NotPublicProperty { get; set; } - public static string NotInstanceProperty { get; set; } - public string NotPublicGetterProperty { private get; set; } - public string CaseMattersPeople { get; set; } - - public string PropertyWithoutGetter - { - // ReSharper disable ValueParameterNotUsed - set { } - // ReSharper restore ValueParameterNotUsed - } - - public int TheMetric { get; set; } - // ReSharper restore UnusedMember.Local - // ReSharper restore UnusedAutoPropertyAccessor.Local - } - - [Test] - public void Assert_custom_placeholders_have_non_alphanumerics_replaced_with_underbar() - { - const string metricPattern = "Component/Memory/{ThePlaceholder}/{MetricName}"; - var queryResult = new FakeQueryWithCustomPlaceHolder {ThePlaceholder = "I ->have.bad_45+stuff!", TheMetric = 42,}; - var metricName = QueryContext.FormatMetricKey(metricPattern, queryResult, "TheMetric"); - - Assert.That(metricName, Is.EqualTo("Component/Memory/I___have_bad_45_stuff_/TheMetric"), "Substitution failed"); - } - - [Test] - public void Assert_custom_placeholders_have_whitespace_trimmed() - { - const string metricPattern = "Component/Memory/{ThePlaceholder}/{MetricName}"; - var queryResult = new FakeQueryWithCustomPlaceHolder {ThePlaceholder = " space then tab\t", TheMetric = 42,}; - var metricName = QueryContext.FormatMetricKey(metricPattern, queryResult, "TheMetric"); - - Assert.That(metricName, Is.EqualTo("Component/Memory/space_then_tab/TheMetric"), "Substitution failed"); - } - - [Test] - public void Assert_custom_placeholders_in_pattern_are_replaced() - { - const string metricPattern = "Component/Memory/{ThePlaceholder}/Machine/{AnotherPlaceholder}/{MetricName}"; - var queryResult = new FakeQueryWithCustomPlaceHolder {ThePlaceholder = "Tada", AnotherPlaceholder = "Flexible", TheMetric = 42,}; - var metricName = QueryContext.FormatMetricKey(metricPattern, queryResult, "TheMetric"); - - Assert.That(metricName, Is.EqualTo("Component/Memory/Tada/Machine/Flexible/TheMetric"), "Substitution failed"); - } - - [Test] - public void Assert_custom_placeholders_with_empty_values_are_replaced_with_empty_string() - { - const string metricPattern = "Component/Memory/{ThePlaceholder}/{MetricName}"; - var queryResult = new FakeQueryWithCustomPlaceHolder {ThePlaceholder = "", TheMetric = 42,}; - var metricName = QueryContext.FormatMetricKey(metricPattern, queryResult, "TheMetric"); - - Assert.That(metricName, Is.EqualTo("Component/Memory//TheMetric"), "Substitution failed"); - } - - [Test] - public void Assert_custom_placeholders_with_null_values_are_replaced_with_null_text() - { - const string metricPattern = "Component/Memory/{ThePlaceholder}/{MetricName}"; - var queryResult = new FakeQueryWithCustomPlaceHolder {ThePlaceholder = null, TheMetric = 42,}; - var metricName = QueryContext.FormatMetricKey(metricPattern, queryResult, "TheMetric"); - - Assert.That(metricName, Is.EqualTo("Component/Memory/null/TheMetric"), "Substitution failed"); - } - - [Test] - public void Assert_metric_pattern_substitution_appends_metric_name_if_metric_name_wildcard_missing() - { - var databaseMetric = Substitute.For(); - databaseMetric.DatabaseName.Returns("Tableriffic"); - - // With trailing slash in the pattern - var metricName = QueryContext.FormatMetricKey("Component/Memory/{DatabaseName}/", databaseMetric, "MyMetric"); - Assert.That(metricName, Is.EqualTo("Component/Memory/Tableriffic/MyMetric"), "Substitution failed when trailing slash present"); - - // Without the trailing slash - metricName = QueryContext.FormatMetricKey("Component/Memory/{DatabaseName}", databaseMetric, "MyMetric"); - Assert.That(metricName, Is.EqualTo("Component/Memory/Tableriffic/MyMetric"), "Substitution failed when trailing slash is missing"); - } - - [Test] - public void Assert_metric_pattern_substitution_appends_units_text_to_the_end() - { - const string metricPattern = "Component/Memory/{MetricName}/Foo"; - - var metricName = QueryContext.FormatMetricKey(metricPattern, new object(), "MyMetric", "[bytes/min]"); - Assert.That(metricName, Is.EqualTo("Component/Memory/MyMetric/Foo[bytes/min]"), "Substitution failed"); - } - - [Test] - public void Assert_metric_pattern_substitution_replaces_database_with_name_from_result() - { - const string metricPattern = "Component/Memory/{MetricName}/{DatabaseName}/Foo"; - - var databaseMetric = Substitute.For(); - databaseMetric.DatabaseName.Returns("ManyTablesDB"); - - var metricName = QueryContext.FormatMetricKey(metricPattern, databaseMetric, "MyMetric"); - Assert.That(metricName, Is.EqualTo("Component/Memory/MyMetric/ManyTablesDB/Foo"), "Substitution failed"); - } - - [Test] - public void Assert_missing_database_name_replaced_with_none() - { - const string metricPattern = "Component/Memory/{DatabaseName}/{MetricName}"; - var queryResult = new object(); - var metricName = QueryContext.FormatMetricKey(metricPattern, queryResult, "MyMetric"); - Assert.That(metricName, Is.EqualTo("Component/Memory/(none)/MyMetric"), "Substitution failed"); - } - - [Test] - public void Should_throw_when_placeholder_case_is_not_exact() - { - const string metricPattern = "Component/Memory/{CASEMATTERSPEOPLE}/{MetricName}"; - var queryResult = new FakeQueryWithCustomPlaceHolder {CaseMattersPeople = "Why are you yelling?",}; - var exception = Assert.Throws(() => QueryContext.FormatMetricKey(metricPattern, queryResult, "TheMetric")); - Assert.That(exception.Message.ToLower(), Is.StringMatching("case-sensitive"), "Expected a helpful error message"); - } - - [Test] - public void Should_throw_with_custom_placeholders_in_pattern_without_getter_property() - { - const string metricPattern = "Component/Memory/{PropertyWithoutGetter}/{MetricName}"; - var queryResult = new FakeQueryWithCustomPlaceHolder(); - - Assert.Throws(() => QueryContext.FormatMetricKey(metricPattern, queryResult, "TheMetric")); - } - - [Test] - public void Should_throw_with_custom_placeholders_in_pattern_without_matching_property() - { - const string metricPattern = "Component/Memory/{ThisMatchesNothing}/{MetricName}"; - var queryResult = new FakeQueryWithCustomPlaceHolder {ThePlaceholder = "BooHiss"}; - - Assert.Throws(() => QueryContext.FormatMetricKey(metricPattern, queryResult, "TheMetric")); - } - - [Test] - public void Should_throw_with_custom_placeholders_in_pattern_without_public_getter_property() - { - const string metricPattern = "Component/Memory/{NotPublicGetterProperty}/{MetricName}"; - var queryResult = new FakeQueryWithCustomPlaceHolder {NotPublicGetterProperty = "BooHiss"}; - - Assert.Throws(() => QueryContext.FormatMetricKey(metricPattern, queryResult, "TheMetric")); - } - - [Test] - public void Should_throw_with_custom_placeholders_in_pattern_without_public_instance_property() - { - const string metricPattern = "Component/Memory/{NotInstanceProperty}/{MetricName}"; - var queryResult = new FakeQueryWithCustomPlaceHolder(); - - Assert.Throws(() => QueryContext.FormatMetricKey(metricPattern, queryResult, "TheMetric")); - } - - [Test] - public void Should_throw_with_custom_placeholders_in_pattern_without_public_property() - { - const string metricPattern = "Component/Memory/{NotPublicProperty}/{MetricName}"; - var queryResult = new FakeQueryWithCustomPlaceHolder {NotPublicProperty = "BooHiss",}; - - Assert.Throws(() => QueryContext.FormatMetricKey(metricPattern, queryResult, "TheMetric")); - } - } + [TestFixture] + public class QueryContextTests + { + private class FakeQueryWithCustomPlaceHolder + { + // ReSharper disable UnusedAutoPropertyAccessor.Local + // ReSharper disable UnusedMember.Local + public string ThePlaceholder { get; set; } + public string AnotherPlaceholder { get; set; } + internal string NotPublicProperty { get; set; } + public static string NotInstanceProperty { get; set; } + public string NotPublicGetterProperty { private get; set; } + public string CaseMattersPeople { get; set; } + + public string PropertyWithoutGetter + { + // ReSharper disable ValueParameterNotUsed + set { } + // ReSharper restore ValueParameterNotUsed + } + + public int TheMetric { get; set; } + // ReSharper restore UnusedMember.Local + // ReSharper restore UnusedAutoPropertyAccessor.Local + } + + [Test] + public void Assert_custom_placeholders_have_non_alphanumerics_replaced_with_underbar() + { + const string metricPattern = "Memory/{ThePlaceholder}/{MetricName}"; + var queryResult = new FakeQueryWithCustomPlaceHolder {ThePlaceholder = "I ->have.bad_45+stuff!", TheMetric = 42,}; + var metricName = QueryContext.FormatMetricKey(metricPattern, queryResult, "TheMetric"); + + Assert.That(metricName, Is.EqualTo("Memory/I___have_bad_45_stuff_/TheMetric"), "Substitution failed"); + } + + [Test] + public void Assert_custom_placeholders_have_whitespace_trimmed() + { + const string metricPattern = "Memory/{ThePlaceholder}/{MetricName}"; + var queryResult = new FakeQueryWithCustomPlaceHolder {ThePlaceholder = " space then tab\t", TheMetric = 42,}; + var metricName = QueryContext.FormatMetricKey(metricPattern, queryResult, "TheMetric"); + + Assert.That(metricName, Is.EqualTo("Memory/space_then_tab/TheMetric"), "Substitution failed"); + } + + [Test] + public void Assert_custom_placeholders_in_pattern_are_replaced() + { + const string metricPattern = "Memory/{ThePlaceholder}/Machine/{AnotherPlaceholder}/{MetricName}"; + var queryResult = new FakeQueryWithCustomPlaceHolder {ThePlaceholder = "Tada", AnotherPlaceholder = "Flexible", TheMetric = 42,}; + var metricName = QueryContext.FormatMetricKey(metricPattern, queryResult, "TheMetric"); + + Assert.That(metricName, Is.EqualTo("Memory/Tada/Machine/Flexible/TheMetric"), "Substitution failed"); + } + + [Test] + public void Assert_custom_placeholders_with_empty_values_are_replaced_with_empty_string() + { + const string metricPattern = "Memory/{ThePlaceholder}/{MetricName}"; + var queryResult = new FakeQueryWithCustomPlaceHolder {ThePlaceholder = "", TheMetric = 42,}; + var metricName = QueryContext.FormatMetricKey(metricPattern, queryResult, "TheMetric"); + + Assert.That(metricName, Is.EqualTo("Memory//TheMetric"), "Substitution failed"); + } + + [Test] + public void Assert_custom_placeholders_with_null_values_are_replaced_with_null_text() + { + const string metricPattern = "Memory/{ThePlaceholder}/{MetricName}"; + var queryResult = new FakeQueryWithCustomPlaceHolder {ThePlaceholder = null, TheMetric = 42,}; + var metricName = QueryContext.FormatMetricKey(metricPattern, queryResult, "TheMetric"); + + Assert.That(metricName, Is.EqualTo("Memory/null/TheMetric"), "Substitution failed"); + } + + [Test] + public void Assert_metric_pattern_substitution_appends_metric_name_if_metric_name_wildcard_missing() + { + var databaseMetric = Substitute.For(); + databaseMetric.DatabaseName.Returns("Tableriffic"); + + // With trailing slash in the pattern + var metricName = QueryContext.FormatMetricKey("Memory/{DatabaseName}/", databaseMetric, "MyMetric"); + Assert.That(metricName, Is.EqualTo("Memory/Tableriffic/MyMetric"), "Substitution failed when trailing slash present"); + + // Without the trailing slash + metricName = QueryContext.FormatMetricKey("Memory/{DatabaseName}", databaseMetric, "MyMetric"); + Assert.That(metricName, Is.EqualTo("Memory/Tableriffic/MyMetric"), "Substitution failed when trailing slash is missing"); + } + + [Test] + public void Assert_metric_pattern_substitution_appends_units_text_to_the_end() + { + const string metricPattern = "Memory/{MetricName}/Foo"; + + var metricName = QueryContext.FormatMetricKey(metricPattern, new object(), "MyMetric"); + Assert.That(metricName, Is.EqualTo("Memory/MyMetric/Foo"), "Substitution failed"); + } + + [Test] + public void Assert_metric_pattern_substitution_replaces_database_with_name_from_result() + { + const string metricPattern = "Memory/{MetricName}/{DatabaseName}/Foo"; + + var databaseMetric = Substitute.For(); + databaseMetric.DatabaseName.Returns("ManyTablesDB"); + + var metricName = QueryContext.FormatMetricKey(metricPattern, databaseMetric, "MyMetric"); + Assert.That(metricName, Is.EqualTo("Memory/MyMetric/ManyTablesDB/Foo"), "Substitution failed"); + } + + [Test] + public void Assert_missing_database_name_replaced_with_none() + { + const string metricPattern = "Memory/{DatabaseName}/{MetricName}"; + var queryResult = new object(); + var metricName = QueryContext.FormatMetricKey(metricPattern, queryResult, "MyMetric"); + Assert.That(metricName, Is.EqualTo("Memory/(none)/MyMetric"), "Substitution failed"); + } + + [Test] + public void Should_throw_when_placeholder_case_is_not_exact() + { + const string metricPattern = "Memory/{CASEMATTERSPEOPLE}/{MetricName}"; + var queryResult = new FakeQueryWithCustomPlaceHolder {CaseMattersPeople = "Why are you yelling?",}; + var exception = Assert.Throws(() => QueryContext.FormatMetricKey(metricPattern, queryResult, "TheMetric")); + Assert.That(exception.Message.ToLower(), Is.StringMatching("case-sensitive"), "Expected a helpful error message"); + } + + [Test] + public void Should_throw_with_custom_placeholders_in_pattern_without_getter_property() + { + const string metricPattern = "Memory/{PropertyWithoutGetter}/{MetricName}"; + var queryResult = new FakeQueryWithCustomPlaceHolder(); + + Assert.Throws(() => QueryContext.FormatMetricKey(metricPattern, queryResult, "TheMetric")); + } + + [Test] + public void Should_throw_with_custom_placeholders_in_pattern_without_matching_property() + { + const string metricPattern = "Memory/{ThisMatchesNothing}/{MetricName}"; + var queryResult = new FakeQueryWithCustomPlaceHolder {ThePlaceholder = "BooHiss"}; + + Assert.Throws(() => QueryContext.FormatMetricKey(metricPattern, queryResult, "TheMetric")); + } + + [Test] + public void Should_throw_with_custom_placeholders_in_pattern_without_public_getter_property() + { + const string metricPattern = "Memory/{NotPublicGetterProperty}/{MetricName}"; + var queryResult = new FakeQueryWithCustomPlaceHolder {NotPublicGetterProperty = "BooHiss"}; + + Assert.Throws(() => QueryContext.FormatMetricKey(metricPattern, queryResult, "TheMetric")); + } + + [Test] + public void Should_throw_with_custom_placeholders_in_pattern_without_public_instance_property() + { + const string metricPattern = "Memory/{NotInstanceProperty}/{MetricName}"; + var queryResult = new FakeQueryWithCustomPlaceHolder(); + + Assert.Throws(() => QueryContext.FormatMetricKey(metricPattern, queryResult, "TheMetric")); + } + + [Test] + public void Should_throw_with_custom_placeholders_in_pattern_without_public_property() + { + const string metricPattern = "Memory/{NotPublicProperty}/{MetricName}"; + var queryResult = new FakeQueryWithCustomPlaceHolder {NotPublicProperty = "BooHiss",}; + + Assert.Throws(() => QueryContext.FormatMetricKey(metricPattern, queryResult, "TheMetric")); + } + } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/QueryLocatorTests.cs b/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/QueryLocatorTests.cs index 86bbf6e..a929713 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/QueryLocatorTests.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/QueryLocatorTests.cs @@ -11,147 +11,147 @@ namespace NewRelic.Microsoft.SqlServer.Plugin { - [TestFixture] - public class QueryLocatorTests - { - [SqlServerQuery("NewRelic.Microsoft.SqlServer.Plugin.Core.ExampleEmbeddedFile.sql", "")] - private class QueryTypeWithExactResourceName {} - - [SqlServerQuery("Queries.ExampleEmbeddedFile.sql", "")] - private class QueryTypeWithPartialResourceName {} - - [SqlServerQuery("AnotherQuery.sql", "")] - private class QueryTypeWithJustFileName {} - - [SqlServerQuery("AnotherQuery.sql", "")] - [SqlServerQuery("Queries.ExampleEmbeddedFile.sql", "")] - private class QueryTypeWithTwoQueries {} - - [SqlServerQuery("Foo.sql", "", Enabled = false)] - private class QueryTypeDisabled {} - - [SqlServerQuery("Foo.sql", "", Enabled = false)] - [SqlServerQuery("AnotherQuery.sql", "", QueryName = "This is enabled")] - private class QueryTypeSomeEnabled {} - - public class FakeDatabaseMetric : IDatabaseMetric - { - public string DatabaseName { get; set; } - public string ParameterizeQuery(string commandText, ISqlEndpoint endpoint) - { - return "zoinks"; - } - } - - [Test] - public void Assert_command_text_is_parameterized() - { - var actual = SqlQuery.PrepareCommandText("I have the power!", new SqlServerEndpoint("Local", ".", false)); - Assert.That(actual, Is.EqualTo("zoinks"), "Parameterization failed"); - } - - [Test] - public void Assert_funcs_are_correctly_configured() - { - var dapperWrapper = Substitute.For(); - - var queries = new QueryLocator(dapperWrapper).PrepareQueries(); - foreach (var query in queries) - { - var results = query.Query(null, Substitute.For()); - Assert.That(results, Is.EqualTo(new object[0])); - } - } - - [Test] - public void Assert_multiple_query_attributes_yield_multiple_queries() - { - var assembly = Assembly.GetExecutingAssembly(); - var queryLocator = new QueryLocator(null, assembly); - - var queries = queryLocator.PrepareQueries(new[] {typeof (QueryTypeWithTwoQueries)}); - Assert.That(queries, Is.Not.Null); - var queryNames = queries.Select(q => q.ResourceName).ToArray(); - var expected = new[] {"AnotherQuery.sql", "Queries.ExampleEmbeddedFile.sql"}; - Assert.That(queryNames, Is.EquivalentTo(expected)); - } - - [Test] - public void Assert_resource_with_exact_resource_name_is_located() - { - var assembly = Assembly.GetExecutingAssembly(); - var queryLocator = new QueryLocator(null, assembly); - - var queries = queryLocator.PrepareQueries(new[] {typeof (QueryTypeWithExactResourceName)}); - Assert.That(queries, Is.Not.Null); - var queryNames = queries.Select(q => q.ResultTypeName).ToArray(); - Assert.That(queryNames, Is.EqualTo(new[] {typeof (QueryTypeWithExactResourceName).Name})); - } - - [Test] - public void Assert_resource_with_only_file_name_is_located() - { - var assembly = Assembly.GetExecutingAssembly(); - var queryLocator = new QueryLocator(null, assembly); - - var queries = queryLocator.PrepareQueries(new[] {typeof (QueryTypeWithJustFileName)}); - Assert.That(queries, Is.Not.Null); - var queryNames = queries.Select(q => q.ResultTypeName).ToArray(); - Assert.That(queryNames, Is.EqualTo(new[] {typeof (QueryTypeWithJustFileName).Name})); - } - - [Test] - public void Assert_resource_with_partial_resource_name_is_located() - { - var assembly = Assembly.GetExecutingAssembly(); - var queryLocator = new QueryLocator(null, assembly); - - var queries = queryLocator.PrepareQueries(new[] {typeof (QueryTypeWithPartialResourceName)}); - Assert.That(queries, Is.Not.Null); - var queryNames = queries.Select(q => q.ResultTypeName).ToArray(); - Assert.That(queryNames, Is.EqualTo(new[] {typeof (QueryTypeWithPartialResourceName).Name})); - } - - [Test] - public void Assert_some_query_types_are_found() - { - var assembly = Assembly.GetExecutingAssembly(); - - var types = assembly.GetTypes(); - Assume.That(types, Is.Not.Empty, "Expected at least one type in the test assembly"); - - var typesWithAttribute = types.Where(t => t.GetCustomAttributes().Any()); - Assert.That(typesWithAttribute, Is.Not.Empty, "Expected at least one QueryType using the " + typeof (SqlServerQueryAttribute).Name); - } - - [Test] - public void Assert_that_disabled_queries_are_ignored() - { - var assembly = Assembly.GetExecutingAssembly(); - var queryLocator = new QueryLocator(null, assembly); - - var queries = queryLocator.PrepareQueries(new[] {typeof (QueryTypeDisabled)}); - Assert.That(queries, Is.Empty); - } - - [Test] - public void Assert_that_only_enabled_queries_are_found() - { - var assembly = Assembly.GetExecutingAssembly(); - var queryLocator = new QueryLocator(null, assembly); - - var queries = queryLocator.PrepareQueries(new[] {typeof (QueryTypeSomeEnabled)}) - .Select(q => q.QueryName) - .ToArray(); - - Assert.That(queries, Is.EqualTo(new[] {"This is enabled"})); - } - - [Test] - public void Assert_that_queries_are_located() - { - var queries = new QueryLocator(new DapperWrapper(), Assembly.GetExecutingAssembly()).PrepareQueries(); - Assert.That(queries, Is.Not.Empty, "Expected some queries to be returned"); - } - } + [TestFixture] + public class QueryLocatorTests + { + [SqlServerQuery("NewRelic.Microsoft.SqlServer.Plugin.Core.ExampleEmbeddedFile.sql", "")] + private class QueryTypeWithExactResourceName {} + + [SqlServerQuery("Queries.ExampleEmbeddedFile.sql", "")] + private class QueryTypeWithPartialResourceName {} + + [SqlServerQuery("AnotherQuery.sql", "")] + private class QueryTypeWithJustFileName {} + + [SqlServerQuery("AnotherQuery.sql", "")] + [SqlServerQuery("Queries.ExampleEmbeddedFile.sql", "")] + private class QueryTypeWithTwoQueries {} + + [SqlServerQuery("Foo.sql", "", Enabled = false)] + private class QueryTypeDisabled {} + + [SqlServerQuery("Foo.sql", "", Enabled = false)] + [SqlServerQuery("AnotherQuery.sql", "", QueryName = "This is enabled")] + private class QueryTypeSomeEnabled {} + + public class FakeDatabaseMetric : IDatabaseMetric + { + public string DatabaseName { get; set; } + public string ParameterizeQuery(string commandText, ISqlEndpoint endpoint) + { + return "zoinks"; + } + } + + [Test] + public void Assert_command_text_is_parameterized() + { + var actual = SqlQuery.PrepareCommandText("I have the power!", new SqlServerEndpoint("Local", ".", false)); + Assert.That(actual, Is.EqualTo("zoinks"), "Parameterization failed"); + } + + [Test] + public void Assert_funcs_are_correctly_configured() + { + var dapperWrapper = Substitute.For(); + + var queries = new QueryLocator(dapperWrapper).PrepareQueries(); + foreach (var query in queries) + { + var results = query.Query(null, Substitute.For()); + Assert.That(results, Is.EqualTo(new object[0])); + } + } + + [Test] + public void Assert_multiple_query_attributes_yield_multiple_queries() + { + var assembly = Assembly.GetExecutingAssembly(); + var queryLocator = new QueryLocator(null, assembly); + + var queries = queryLocator.PrepareQueries(new[] {typeof (QueryTypeWithTwoQueries)}); + Assert.That(queries, Is.Not.Null); + var queryNames = queries.Select(q => q.ResourceName).ToArray(); + var expected = new[] {"AnotherQuery.sql", "Queries.ExampleEmbeddedFile.sql"}; + Assert.That(queryNames, Is.EquivalentTo(expected)); + } + + [Test] + public void Assert_resource_with_exact_resource_name_is_located() + { + var assembly = Assembly.GetExecutingAssembly(); + var queryLocator = new QueryLocator(null, assembly); + + var queries = queryLocator.PrepareQueries(new[] {typeof (QueryTypeWithExactResourceName)}); + Assert.That(queries, Is.Not.Null); + var queryNames = queries.Select(q => q.ResultTypeName).ToArray(); + Assert.That(queryNames, Is.EqualTo(new[] {typeof (QueryTypeWithExactResourceName).Name})); + } + + [Test] + public void Assert_resource_with_only_file_name_is_located() + { + var assembly = Assembly.GetExecutingAssembly(); + var queryLocator = new QueryLocator(null, assembly); + + var queries = queryLocator.PrepareQueries(new[] {typeof (QueryTypeWithJustFileName)}); + Assert.That(queries, Is.Not.Null); + var queryNames = queries.Select(q => q.ResultTypeName).ToArray(); + Assert.That(queryNames, Is.EqualTo(new[] {typeof (QueryTypeWithJustFileName).Name})); + } + + [Test] + public void Assert_resource_with_partial_resource_name_is_located() + { + var assembly = Assembly.GetExecutingAssembly(); + var queryLocator = new QueryLocator(null, assembly); + + var queries = queryLocator.PrepareQueries(new[] {typeof (QueryTypeWithPartialResourceName)}); + Assert.That(queries, Is.Not.Null); + var queryNames = queries.Select(q => q.ResultTypeName).ToArray(); + Assert.That(queryNames, Is.EqualTo(new[] {typeof (QueryTypeWithPartialResourceName).Name})); + } + + [Test] + public void Assert_some_query_types_are_found() + { + var assembly = Assembly.GetExecutingAssembly(); + + var types = assembly.GetTypes(); + Assume.That(types, Is.Not.Empty, "Expected at least one type in the test assembly"); + + var typesWithAttribute = types.Where(t => t.GetCustomAttributes().Any()); + Assert.That(typesWithAttribute, Is.Not.Empty, "Expected at least one QueryType using the " + typeof (SqlServerQueryAttribute).Name); + } + + [Test] + public void Assert_that_disabled_queries_are_ignored() + { + var assembly = Assembly.GetExecutingAssembly(); + var queryLocator = new QueryLocator(null, assembly); + + var queries = queryLocator.PrepareQueries(new[] {typeof (QueryTypeDisabled)}); + Assert.That(queries, Is.Empty); + } + + [Test] + public void Assert_that_only_enabled_queries_are_found() + { + var assembly = Assembly.GetExecutingAssembly(); + var queryLocator = new QueryLocator(null, assembly); + + var queries = queryLocator.PrepareQueries(new[] {typeof (QueryTypeSomeEnabled)}) + .Select(q => q.QueryName) + .ToArray(); + + Assert.That(queries, Is.EqualTo(new[] {"This is enabled"})); + } + + [Test] + public void Assert_that_queries_are_located() + { + var queries = new QueryLocator(new DapperWrapper(), Assembly.GetExecutingAssembly()).PrepareQueries(); + Assert.That(queries, Is.Not.Empty, "Expected some queries to be returned"); + } + } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/QueryTypes/ConnectionsTests.cs b/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/QueryTypes/ConnectionsTests.cs index fa48360..5026d70 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/QueryTypes/ConnectionsTests.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/QueryTypes/ConnectionsTests.cs @@ -6,55 +6,55 @@ namespace NewRelic.Microsoft.SqlServer.Plugin.QueryTypes { - [TestFixture] - public class ConnectionsTests - { - [Test] - public void Should_replace_where_with_excluded_databases() - { - var queryType = new SqlServerConnections(); - - var sqlServer = new SqlServerEndpoint("foo", "foo", true, null, new[] {"blah"}); - - var queryLocator = new QueryLocator(null); - IEnumerable queries = queryLocator.PrepareQueries(new[] {queryType.GetType()}, false); - foreach (SqlQuery query in queries) - { - string actual = queryType.ParameterizeQuery(query.CommandText, sqlServer); - Assert.That(actual, Is.StringContaining("AND (d.Name NOT IN ('blah')")); - } - } - - [Test] - public void Should_replace_where_with_included_and_system_databases() - { - var queryType = new SqlServerConnections(); - - var sqlServer = new SqlServerEndpoint("foo", "foo", true, new[] {new Database {Name = "bar"},}, null); - - var queryLocator = new QueryLocator(null); - IEnumerable queries = queryLocator.PrepareQueries(new[] {queryType.GetType()}, false); - foreach (SqlQuery query in queries) - { - string actual = queryType.ParameterizeQuery(query.CommandText, sqlServer); - Assert.That(actual, Is.StringContaining("AND (d.Name IN ('bar', 'tempdb', 'master', 'model', 'msdb')")); - } - } - - [Test] - public void Should_replace_where_with_included_databases() - { - var queryType = new SqlServerConnections(); - - var sqlServer = new SqlServerEndpoint("foo", "foo", false, new[] {new Database {Name = "bar"},}, null); - - var queryLocator = new QueryLocator(null); - IEnumerable queries = queryLocator.PrepareQueries(new[] {queryType.GetType()}, false); - foreach (SqlQuery query in queries) - { - string actual = queryType.ParameterizeQuery(query.CommandText, sqlServer); - Assert.That(actual, Is.StringContaining("AND (d.Name IN ('bar')")); - } - } - } + [TestFixture] + public class ConnectionsTests + { + [Test] + public void Should_replace_where_with_excluded_databases() + { + var queryType = new SqlServerConnections(); + + var sqlServer = new SqlServerEndpoint("foo", "foo", true, null, new[] {"blah"}); + + var queryLocator = new QueryLocator(null); + IEnumerable queries = queryLocator.PrepareQueries(new[] {queryType.GetType()}, false); + foreach (SqlQuery query in queries) + { + string actual = queryType.ParameterizeQuery(query.CommandText, sqlServer); + Assert.That(actual, Is.StringContaining("AND (d.Name NOT IN ('blah')")); + } + } + + [Test] + public void Should_replace_where_with_included_and_system_databases() + { + var queryType = new SqlServerConnections(); + + var sqlServer = new SqlServerEndpoint("foo", "foo", true, new[] {new Database {Name = "bar"},}, null); + + var queryLocator = new QueryLocator(null); + IEnumerable queries = queryLocator.PrepareQueries(new[] {queryType.GetType()}, false); + foreach (SqlQuery query in queries) + { + string actual = queryType.ParameterizeQuery(query.CommandText, sqlServer); + Assert.That(actual, Is.StringContaining("AND (d.Name IN ('bar', 'tempdb', 'master', 'model', 'msdb')")); + } + } + + [Test] + public void Should_replace_where_with_included_databases() + { + var queryType = new SqlServerConnections(); + + var sqlServer = new SqlServerEndpoint("foo", "foo", false, new[] {new Database {Name = "bar"},}, null); + + var queryLocator = new QueryLocator(null); + IEnumerable queries = queryLocator.PrepareQueries(new[] {queryType.GetType()}, false); + foreach (SqlQuery query in queries) + { + string actual = queryType.ParameterizeQuery(query.CommandText, sqlServer); + Assert.That(actual, Is.StringContaining("AND (d.Name IN ('bar')")); + } + } + } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/QueryTypes/MemoryViewTests.cs b/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/QueryTypes/MemoryViewTests.cs index 58135d8..3c78ac5 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/QueryTypes/MemoryViewTests.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/QueryTypes/MemoryViewTests.cs @@ -37,6 +37,10 @@ public void Assert_page_threat_stays_between_0_and_100() var memoryView = new MemoryView {PageLife = 0}; Assert.That(memoryView.PageLifeThreat, Is.EqualTo(100m), "0 page life maxes at 100%"); + // Try just above 0 + memoryView.PageLife = 1; + Assert.That(memoryView.PageLifeThreat, Is.EqualTo(100m), "1 page life maxes at 100%"); + // Try the default memoryView.PageLife = 300; Assert.That(memoryView.PageLifeThreat, Is.EqualTo(100m), "300 page life maxes at 100%"); diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/SqlEndpointTests.cs b/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/SqlEndpointTests.cs index 24fdd7b..8468b2b 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/SqlEndpointTests.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/SqlEndpointTests.cs @@ -2,323 +2,297 @@ using System.Collections.Generic; using System.Linq; using System.Text; - -using NSubstitute; - -using NUnit.Framework; - +using log4net; using NewRelic.Microsoft.SqlServer.Plugin.Configuration; using NewRelic.Microsoft.SqlServer.Plugin.Properties; using NewRelic.Microsoft.SqlServer.Plugin.QueryTypes; - -using log4net; +using NSubstitute; +using NUnit.Framework; namespace NewRelic.Microsoft.SqlServer.Plugin { - [TestFixture] - public class SqlEndpointTests - { - public IEnumerable ComponentGuidTestCases - { - get - { - yield return new TestCaseData(new SqlServerEndpoint("FooServer", ".", false)).Returns(Constants.SqlServerComponentGuid).SetName("SqlServer Sets Appropriate Guid"); - yield return new TestCaseData(new AzureSqlEndpoint("FooServer", "")).Returns(Constants.SqlAzureComponentGuid).SetName("AzureSqlEndpoint Sets Appropriate Guid"); - } - } - - //Tests fix for issue where Plugin gets 400's after server is unavailable for a time https://support.newrelic.com/tickets/55385 - [Test] - [TestCase(1, TestName = "A Minute Since Success")] - [TestCase(5, TestName = "5 Minutes Since Success")] - [TestCase(30, TestName = "30 Minutes Since Success")] - [TestCase(60, TestName = "An Hour Since Success")] - [TestCase(120, TestName = "Two Hours Since Success")] - [TestCase(1440, TestName = "A Day Since Success")] - [TestCase(2880, TestName = "Two Days Since Success")] - public void Assert_duration_does_not_exceed_allowed_max(int minutesSinceLastSuccessful) - { - var endpoint = new SqlServerEndpoint("Foo",".",false); - endpoint.MetricReportSuccessful(DateTime.Now.AddMinutes(minutesSinceLastSuccessful * -1)); - - const int thirtyMinutesInSeconds = 30 * 60; - Assert.That(endpoint.Duration, Is.LessThanOrEqualTo(thirtyMinutesInSeconds), "Duration should never be longer than 30 minutes, regardless of last succssful reported time"); - - endpoint.MetricReportSuccessful(DateTime.Now.AddMinutes(-.5)); - Assert.That(endpoint.Duration, Is.LessThanOrEqualTo(31), "Duration should reset to be around 30 seconds regardless of previous value"); - - } - - [Test] - public void Assert_endpoint_appropriately_massages_duplicated_data() - { - var endpoint = Substitute.For("", ""); - - var resultSet1 = new object[] - { - new SqlDmlActivity - { - PlanHandle = Encoding.UTF8.GetBytes("AA11"), - SqlStatementHash = Encoding.UTF8.GetBytes("INSERT INTO FOO"), - ExecutionCount = 10, - QueryType = "Writes", - }, - new SqlDmlActivity - { - PlanHandle = Encoding.UTF8.GetBytes("AA11"), - SqlStatementHash = Encoding.UTF8.GetBytes("INSERT INTO BAR"), - ExecutionCount = 8, - QueryType = "Writes", - }, - new SqlDmlActivity - { - PlanHandle = Encoding.UTF8.GetBytes("AA11"), - SqlStatementHash = Encoding.UTF8.GetBytes("INSERT INTO BAR"), - ExecutionCount = 8, - QueryType = "Writes", - }, - new SqlDmlActivity - { - PlanHandle = Encoding.UTF8.GetBytes("BB12"), - SqlStatementHash = Encoding.UTF8.GetBytes("SELECT * FROM FOO"), - ExecutionCount = 500, - QueryType = "Reads", - }, - new SqlDmlActivity - { - PlanHandle = Encoding.UTF8.GetBytes("CC12"), - SqlStatementHash = Encoding.UTF8.GetBytes("SELECT * FROM FOO"), - ExecutionCount = 600, - QueryType = "Reads", - }, - new SqlDmlActivity - { - PlanHandle = Encoding.UTF8.GetBytes("EE12"), - SqlStatementHash = Encoding.UTF8.GetBytes("SELECT * FROM BAR"), - ExecutionCount = 100, - QueryType = "Reads", - }, - }; - - IEnumerable outputResults1 = endpoint.CalculateSqlDmlActivityIncrease(resultSet1, Substitute.For()).Cast().ToArray(); - - Assert.That(outputResults1, Is.Not.Null); - Assert.That(outputResults1.Count(), Is.EqualTo(1)); - - SqlDmlActivity sqlDmlActivity = outputResults1.First(); - Assert.That(string.Format("Reads:{0} Writes:{1}", sqlDmlActivity.Reads, sqlDmlActivity.Writes), Is.EqualTo("Reads:0 Writes:0")); - } - - [Test] - [TestCaseSource("ComponentGuidTestCases")] - public string Assert_correct_component_guid_supplied_to_query_context(SqlEndpointBase endpoint) - { - QueryContext queryContext = endpoint.CreateQueryContext(Substitute.For(), new object[0]); - return queryContext.ComponentData.Guid; - } - - [Test] - public void Assert_endpoint_appropriately_massages_data() - { - var endpoint = Substitute.For("", ""); - - var resultSet1 = new object[] - { - new SqlDmlActivity - { - PlanHandle = Encoding.UTF8.GetBytes("AA11"), - SqlStatementHash = Encoding.UTF8.GetBytes("INSERT INTO FOO"), - ExecutionCount = 10, - QueryType = "Writes", - }, - new SqlDmlActivity - { - PlanHandle = Encoding.UTF8.GetBytes("AA11"), - SqlStatementHash = Encoding.UTF8.GetBytes("INSERT INTO BAR"), - ExecutionCount = 8, - QueryType = "Writes", - }, - new SqlDmlActivity - { - PlanHandle = Encoding.UTF8.GetBytes("BB12"), - SqlStatementHash = Encoding.UTF8.GetBytes("SELECT * FROM FOO"), - ExecutionCount = 500, - QueryType = "Reads", - }, - new SqlDmlActivity - { - PlanHandle = Encoding.UTF8.GetBytes("CC12"), - SqlStatementHash = Encoding.UTF8.GetBytes("SELECT * FROM FOO"), - ExecutionCount = 600, - QueryType = "Reads", - }, - new SqlDmlActivity - { - PlanHandle = Encoding.UTF8.GetBytes("EE12"), - SqlStatementHash = Encoding.UTF8.GetBytes("SELECT * FROM BAR"), - ExecutionCount = 100, - QueryType = "Reads", - }, - }; - - IEnumerable outputResults1 = endpoint.CalculateSqlDmlActivityIncrease(resultSet1, Substitute.For()).Cast().ToArray(); - - Assert.That(outputResults1, Is.Not.Null); - Assert.That(outputResults1.Count(), Is.EqualTo(1)); - - SqlDmlActivity sqlDmlActivity = outputResults1.First(); - Assert.That(string.Format("Reads:{0} Writes:{1}", sqlDmlActivity.Reads, sqlDmlActivity.Writes), Is.EqualTo("Reads:0 Writes:0")); - - var resultSet2 = new object[] - { - new SqlDmlActivity - { - PlanHandle = Encoding.UTF8.GetBytes("AA11"), - SqlStatementHash = Encoding.UTF8.GetBytes("INSERT INTO FOO"), - ExecutionCount = 14, - QueryType = "Writes", - }, - new SqlDmlActivity - { - PlanHandle = Encoding.UTF8.GetBytes("AA11"), - SqlStatementHash = Encoding.UTF8.GetBytes("INSERT INTO BAR"), - ExecutionCount = 18, - QueryType = "Writes", - }, - //Tests Duplicate Results gets aggregated - new SqlDmlActivity - { - PlanHandle = Encoding.UTF8.GetBytes("AA11"), - SqlStatementHash = Encoding.UTF8.GetBytes("INSERT INTO BAR"), - ExecutionCount = 2, - QueryType = "Writes", - }, - new SqlDmlActivity - { - PlanHandle = Encoding.UTF8.GetBytes("BB12"), - SqlStatementHash = Encoding.UTF8.GetBytes("SELECT * FROM FOO"), - ExecutionCount = 550, - QueryType = "Reads", - }, - new SqlDmlActivity - { - PlanHandle = Encoding.UTF8.GetBytes("CC12"), - SqlStatementHash = Encoding.UTF8.GetBytes("SELECT * FROM FOO"), - ExecutionCount = 625, - QueryType = "Reads", - }, - new SqlDmlActivity - { - PlanHandle = Encoding.UTF8.GetBytes("DD12"), - SqlStatementHash = Encoding.UTF8.GetBytes("SELECT * FROM BAR"), - ExecutionCount = 1, - QueryType = "Reads", - }, - }; - - SqlDmlActivity[] outputResults2 = endpoint.CalculateSqlDmlActivityIncrease(resultSet2, Substitute.For()).Cast().ToArray(); - Assert.That(outputResults2, Is.Not.Null); - Assert.That(outputResults2.Count(), Is.EqualTo(1)); - - sqlDmlActivity = outputResults2.First(); - - Assert.That(string.Format("Reads:{0} Writes:{1}", sqlDmlActivity.Reads, sqlDmlActivity.Writes), Is.EqualTo("Reads:76 Writes:16")); - } - - [Test] - public void Assert_sql_dml_actvity_data_takes_create_time_ms_into_account() - { - var endpoint = Substitute.For("", ""); - var d1 = new DateTime(2013, 06, 20, 8, 28, 10, 100); - var d2 = new DateTime(2013, 06, 20, 8, 28, 10, 200); - - var resultSet1 = new object[] - { - new SqlDmlActivity - { - PlanHandle = Encoding.UTF8.GetBytes("AA11"), - SqlStatementHash = Encoding.UTF8.GetBytes("INSERT INTO FOO"), - CreationTime = d1, - ExecutionCount = 10, - QueryType = "Writes", - }, - new SqlDmlActivity - { - PlanHandle = Encoding.UTF8.GetBytes("AA11"), - SqlStatementHash = Encoding.UTF8.GetBytes("INSERT INTO FOO"), - CreationTime = d2, - ExecutionCount = 12, - QueryType = "Writes", - }, - }; - - IEnumerable outputResults1 = endpoint.CalculateSqlDmlActivityIncrease(resultSet1, Substitute.For()).Cast().ToArray(); - - Assert.That(outputResults1, Is.Not.Null); - Assert.That(outputResults1.Count(), Is.EqualTo(1)); - - SqlDmlActivity sqlDmlActivity = outputResults1.First(); - Assert.That(string.Format("Reads:{0} Writes:{1}", sqlDmlActivity.Reads, sqlDmlActivity.Writes), Is.EqualTo("Reads:0 Writes:0")); - - var resultSet2 = new object[] - { - new SqlDmlActivity - { - PlanHandle = Encoding.UTF8.GetBytes("AA11"), - SqlStatementHash = Encoding.UTF8.GetBytes("INSERT INTO FOO"), - CreationTime = d2, - ExecutionCount = 15, - QueryType = "Writes", - }, - }; - - SqlDmlActivity[] outputResults2 = endpoint.CalculateSqlDmlActivityIncrease(resultSet2, Substitute.For()).Cast().ToArray(); - Assert.That(outputResults2, Is.Not.Null); - Assert.That(outputResults2.Count(), Is.EqualTo(1)); - - sqlDmlActivity = outputResults2.First(); - - Assert.That(string.Format("Reads:{0} Writes:{1}", sqlDmlActivity.Reads, sqlDmlActivity.Writes), Is.EqualTo("Reads:0 Writes:3")); - } - - [Test] - public void Assert_database_names_are_replaced_when_included_databases_with_display_names_are_configured() - { - var includedDatabases = new[] - { - new Database {Name = "Foo", DisplayName = "Fantastic",}, - new Database {Name = "Bar", DisplayName = "Baracuda",}, - new Database {Name = "Baz", DisplayName = "Assassins",}, - new Database {Name = "Quux",}, - }; - - var databaseMetric1 = Substitute.For(); - databaseMetric1.DatabaseName = "Foo"; - - // Test for case-insensitivity - var databaseMetric2 = Substitute.For(); - databaseMetric2.DatabaseName = "BAZ"; - - var databaseMetric3 = Substitute.For(); - databaseMetric3.DatabaseName = "Bar"; - - var databaseMetric4 = Substitute.For(); - databaseMetric4.DatabaseName = "Quux"; - - var results = new object[] - { - databaseMetric1, - databaseMetric2, - databaseMetric3, - databaseMetric4, - }; - - SqlServerEndpoint.ApplyDatabaseDisplayNames(includedDatabases, results); - - Assert.That(databaseMetric1.DatabaseName, Is.EqualTo("Fantastic")); - Assert.That(databaseMetric2.DatabaseName, Is.EqualTo("Assassins")); - Assert.That(databaseMetric3.DatabaseName, Is.EqualTo("Baracuda")); - Assert.That(databaseMetric4.DatabaseName, Is.EqualTo("Quux")); - } - } + [TestFixture] + public class SqlEndpointTests + { + public IEnumerable ComponentGuidTestCases + { + get + { + yield return new TestCaseData(new SqlServerEndpoint("FooServer", ".", false)).Returns(Constants.SqlServerComponentGuid).SetName("SqlServer Sets Appropriate Guid"); + yield return new TestCaseData(new AzureSqlEndpoint("FooServer", "")).Returns(Constants.SqlAzureComponentGuid).SetName("AzureSqlEndpoint Sets Appropriate Guid"); + } + } + + [Test] + public void Assert_endpoint_appropriately_massages_duplicated_data() + { + var endpoint = Substitute.For("", ""); + + var resultSet1 = new object[] + { + new SqlDmlActivity + { + PlanHandle = Encoding.UTF8.GetBytes("AA11"), + SqlStatementHash = Encoding.UTF8.GetBytes("INSERT INTO FOO"), + ExecutionCount = 10, + QueryType = "Writes", + }, + new SqlDmlActivity + { + PlanHandle = Encoding.UTF8.GetBytes("AA11"), + SqlStatementHash = Encoding.UTF8.GetBytes("INSERT INTO BAR"), + ExecutionCount = 8, + QueryType = "Writes", + }, + new SqlDmlActivity + { + PlanHandle = Encoding.UTF8.GetBytes("AA11"), + SqlStatementHash = Encoding.UTF8.GetBytes("INSERT INTO BAR"), + ExecutionCount = 8, + QueryType = "Writes", + }, + new SqlDmlActivity + { + PlanHandle = Encoding.UTF8.GetBytes("BB12"), + SqlStatementHash = Encoding.UTF8.GetBytes("SELECT * FROM FOO"), + ExecutionCount = 500, + QueryType = "Reads", + }, + new SqlDmlActivity + { + PlanHandle = Encoding.UTF8.GetBytes("CC12"), + SqlStatementHash = Encoding.UTF8.GetBytes("SELECT * FROM FOO"), + ExecutionCount = 600, + QueryType = "Reads", + }, + new SqlDmlActivity + { + PlanHandle = Encoding.UTF8.GetBytes("EE12"), + SqlStatementHash = Encoding.UTF8.GetBytes("SELECT * FROM BAR"), + ExecutionCount = 100, + QueryType = "Reads", + }, + }; + + IEnumerable outputResults1 = endpoint.CalculateSqlDmlActivityIncrease(resultSet1, Substitute.For()).Cast().ToArray(); + + Assert.That(outputResults1, Is.Not.Null); + Assert.That(outputResults1.Count(), Is.EqualTo(1)); + + SqlDmlActivity sqlDmlActivity = outputResults1.First(); + Assert.That(string.Format("Reads:{0} Writes:{1}", sqlDmlActivity.Reads, sqlDmlActivity.Writes), Is.EqualTo("Reads:0 Writes:0")); + } + + [Test] + [TestCaseSource("ComponentGuidTestCases")] + public string Assert_correct_component_guid_supplied_to_query_context(SqlEndpointBase endpoint) + { + QueryContext queryContext = endpoint.CreateQueryContext(Substitute.For(), new object[0]); + return queryContext.ComponentGuid; + } + + [Test] + public void Assert_endpoint_appropriately_massages_data() + { + var endpoint = Substitute.For("", ""); + + var resultSet1 = new object[] + { + new SqlDmlActivity + { + PlanHandle = Encoding.UTF8.GetBytes("AA11"), + SqlStatementHash = Encoding.UTF8.GetBytes("INSERT INTO FOO"), + ExecutionCount = 10, + QueryType = "Writes", + }, + new SqlDmlActivity + { + PlanHandle = Encoding.UTF8.GetBytes("AA11"), + SqlStatementHash = Encoding.UTF8.GetBytes("INSERT INTO BAR"), + ExecutionCount = 8, + QueryType = "Writes", + }, + new SqlDmlActivity + { + PlanHandle = Encoding.UTF8.GetBytes("BB12"), + SqlStatementHash = Encoding.UTF8.GetBytes("SELECT * FROM FOO"), + ExecutionCount = 500, + QueryType = "Reads", + }, + new SqlDmlActivity + { + PlanHandle = Encoding.UTF8.GetBytes("CC12"), + SqlStatementHash = Encoding.UTF8.GetBytes("SELECT * FROM FOO"), + ExecutionCount = 600, + QueryType = "Reads", + }, + new SqlDmlActivity + { + PlanHandle = Encoding.UTF8.GetBytes("EE12"), + SqlStatementHash = Encoding.UTF8.GetBytes("SELECT * FROM BAR"), + ExecutionCount = 100, + QueryType = "Reads", + }, + }; + + IEnumerable outputResults1 = endpoint.CalculateSqlDmlActivityIncrease(resultSet1, Substitute.For()).Cast().ToArray(); + + Assert.That(outputResults1, Is.Not.Null); + Assert.That(outputResults1.Count(), Is.EqualTo(1)); + + SqlDmlActivity sqlDmlActivity = outputResults1.First(); + Assert.That(string.Format("Reads:{0} Writes:{1}", sqlDmlActivity.Reads, sqlDmlActivity.Writes), Is.EqualTo("Reads:0 Writes:0")); + + var resultSet2 = new object[] + { + new SqlDmlActivity + { + PlanHandle = Encoding.UTF8.GetBytes("AA11"), + SqlStatementHash = Encoding.UTF8.GetBytes("INSERT INTO FOO"), + ExecutionCount = 14, + QueryType = "Writes", + }, + new SqlDmlActivity + { + PlanHandle = Encoding.UTF8.GetBytes("AA11"), + SqlStatementHash = Encoding.UTF8.GetBytes("INSERT INTO BAR"), + ExecutionCount = 18, + QueryType = "Writes", + }, + //Tests Duplicate Results gets aggregated + new SqlDmlActivity + { + PlanHandle = Encoding.UTF8.GetBytes("AA11"), + SqlStatementHash = Encoding.UTF8.GetBytes("INSERT INTO BAR"), + ExecutionCount = 2, + QueryType = "Writes", + }, + new SqlDmlActivity + { + PlanHandle = Encoding.UTF8.GetBytes("BB12"), + SqlStatementHash = Encoding.UTF8.GetBytes("SELECT * FROM FOO"), + ExecutionCount = 550, + QueryType = "Reads", + }, + new SqlDmlActivity + { + PlanHandle = Encoding.UTF8.GetBytes("CC12"), + SqlStatementHash = Encoding.UTF8.GetBytes("SELECT * FROM FOO"), + ExecutionCount = 625, + QueryType = "Reads", + }, + new SqlDmlActivity + { + PlanHandle = Encoding.UTF8.GetBytes("DD12"), + SqlStatementHash = Encoding.UTF8.GetBytes("SELECT * FROM BAR"), + ExecutionCount = 1, + QueryType = "Reads", + }, + }; + + SqlDmlActivity[] outputResults2 = endpoint.CalculateSqlDmlActivityIncrease(resultSet2, Substitute.For()).Cast().ToArray(); + Assert.That(outputResults2, Is.Not.Null); + Assert.That(outputResults2.Count(), Is.EqualTo(1)); + + sqlDmlActivity = outputResults2.First(); + + Assert.That(string.Format("Reads:{0} Writes:{1}", sqlDmlActivity.Reads, sqlDmlActivity.Writes), Is.EqualTo("Reads:76 Writes:16")); + } + + [Test] + public void Assert_sql_dml_actvity_data_takes_create_time_ms_into_account() + { + var endpoint = Substitute.For("", ""); + var d1 = new DateTime(2013, 06, 20, 8, 28, 10, 100); + var d2 = new DateTime(2013, 06, 20, 8, 28, 10, 200); + + var resultSet1 = new object[] + { + new SqlDmlActivity + { + PlanHandle = Encoding.UTF8.GetBytes("AA11"), + SqlStatementHash = Encoding.UTF8.GetBytes("INSERT INTO FOO"), + CreationTime = d1, + ExecutionCount = 10, + QueryType = "Writes", + }, + new SqlDmlActivity + { + PlanHandle = Encoding.UTF8.GetBytes("AA11"), + SqlStatementHash = Encoding.UTF8.GetBytes("INSERT INTO FOO"), + CreationTime = d2, + ExecutionCount = 12, + QueryType = "Writes", + }, + }; + + IEnumerable outputResults1 = endpoint.CalculateSqlDmlActivityIncrease(resultSet1, Substitute.For()).Cast().ToArray(); + + Assert.That(outputResults1, Is.Not.Null); + Assert.That(outputResults1.Count(), Is.EqualTo(1)); + + SqlDmlActivity sqlDmlActivity = outputResults1.First(); + Assert.That(string.Format("Reads:{0} Writes:{1}", sqlDmlActivity.Reads, sqlDmlActivity.Writes), Is.EqualTo("Reads:0 Writes:0")); + + var resultSet2 = new object[] + { + new SqlDmlActivity + { + PlanHandle = Encoding.UTF8.GetBytes("AA11"), + SqlStatementHash = Encoding.UTF8.GetBytes("INSERT INTO FOO"), + CreationTime = d2, + ExecutionCount = 15, + QueryType = "Writes", + }, + }; + + SqlDmlActivity[] outputResults2 = endpoint.CalculateSqlDmlActivityIncrease(resultSet2, Substitute.For()).Cast().ToArray(); + Assert.That(outputResults2, Is.Not.Null); + Assert.That(outputResults2.Count(), Is.EqualTo(1)); + + sqlDmlActivity = outputResults2.First(); + + Assert.That(string.Format("Reads:{0} Writes:{1}", sqlDmlActivity.Reads, sqlDmlActivity.Writes), Is.EqualTo("Reads:0 Writes:3")); + } + + [Test] + public void Assert_database_names_are_replaced_when_included_databases_with_display_names_are_configured() + { + var includedDatabases = new[] + { + new Database {Name = "Foo", DisplayName = "Fantastic",}, + new Database {Name = "Bar", DisplayName = "Baracuda",}, + new Database {Name = "Baz", DisplayName = "Assassins",}, + new Database {Name = "Quux",}, + }; + + var databaseMetric1 = Substitute.For(); + databaseMetric1.DatabaseName = "Foo"; + + // Test for case-insensitivity + var databaseMetric2 = Substitute.For(); + databaseMetric2.DatabaseName = "BAZ"; + + var databaseMetric3 = Substitute.For(); + databaseMetric3.DatabaseName = "Bar"; + + var databaseMetric4 = Substitute.For(); + databaseMetric4.DatabaseName = "Quux"; + + var results = new object[] + { + databaseMetric1, + databaseMetric2, + databaseMetric3, + databaseMetric4, + }; + + SqlServerEndpoint.ApplyDatabaseDisplayNames(includedDatabases, results); + + Assert.That(databaseMetric1.DatabaseName, Is.EqualTo("Fantastic")); + Assert.That(databaseMetric2.DatabaseName, Is.EqualTo("Assassins")); + Assert.That(databaseMetric3.DatabaseName, Is.EqualTo("Baracuda")); + Assert.That(databaseMetric4.DatabaseName, Is.EqualTo("Quux")); + } + } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/SqlMonitorQueryTests.cs b/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/SqlMonitorQueryTests.cs index 256aeb7..238ce89 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/SqlMonitorQueryTests.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin.Tests/SqlMonitorQueryTests.cs @@ -3,7 +3,6 @@ using NewRelic.Microsoft.SqlServer.Plugin.Core; using NewRelic.Microsoft.SqlServer.Plugin.QueryTypes; -using NewRelic.Platform.Binding.DotNET; using NSubstitute; @@ -11,68 +10,30 @@ namespace NewRelic.Microsoft.SqlServer.Plugin { - [TestFixture] - public class SqlMonitorQueryTests - { - protected class FakeQueryType - { - public long Long { get; set; } - public int Integer { get; set; } - public short Short { get; set; } - public byte Byte { get; set; } - public decimal Decimal { get; set; } - public string Comment { get; set; } - public DateTime EventTime { get; set; } - } - - [Test] - public void Assert_only_numerics_are_returned() - { - MetricMapper[] metricMappers = MetricQuery.GetMappers(typeof (FakeQueryType)); - Assert.That(metricMappers, Is.Not.Null); - - // Keep these out of order to ensure we don't depend on it - var expected = new[] {"Long", "Integer", "Short", "Decimal", "Byte"}; - string[] actual = metricMappers.Select(m => m.MetricName).ToArray(); - Assert.That(actual, Is.EquivalentTo(expected), "Properties discovered and mapped wrong"); - } - - [Test] - [TestCase(MetricTransformEnum.Delta)] - [TestCase(MetricTransformEnum.Simple)] - public void Assert_query_sets_appropriate_metric_transform(MetricTransformEnum metricTransformEnum) - { - var attribute = new SqlServerQueryAttribute("FileIO.sql", "Foo/Bar") {MetricTransformEnum = metricTransformEnum}; - var sqlQuery = new SqlQuery(typeof (FileIoView), attribute, Substitute.For(), ""); - - Assert.That(sqlQuery.MetricTransformEnum, Is.EqualTo(attribute.MetricTransformEnum), "SqlQuery did not set correct value from attribute for MetricTransformEnum"); - } - - [Test] - public void Assert_results_are_mapped_into_metrics() - { - var fakes = new[] - { - new FakeQueryType - { - Long = 42, - Integer = 27, - Short = 12, - Byte = 255, - Decimal = 407.54m, - Comment = "Utterly worthless... except for logging", - EventTime = DateTime.Now, - } - }; - - var sqlQuery = new SqlQuery(typeof (FakeQueryType), new SqlServerQueryAttribute("foo.sql", "Fake/"), Substitute.For(), ""); - - var componentData = new ComponentData(); - sqlQuery.AddMetrics(new QueryContext(sqlQuery) {ComponentData = componentData, Results = fakes,}); - - var expected = new[] {"Fake/Long", "Fake/Integer", "Fake/Short", "Fake/Decimal", "Fake/Byte"}; - string[] actual = componentData.Metrics.Keys.ToArray(); - Assert.That(actual, Is.EquivalentTo(expected), "Properties discovered and mapped wrong"); - } - } + [TestFixture] + public class SqlMonitorQueryTests + { + protected class FakeQueryType + { + public long Long { get; set; } + public int Integer { get; set; } + public short Short { get; set; } + public byte Byte { get; set; } + public decimal Decimal { get; set; } + public string Comment { get; set; } + public DateTime EventTime { get; set; } + } + + [Test] + public void Assert_only_numerics_are_returned() + { + MetricMapper[] metricMappers = MetricQuery.GetMappers(typeof (FakeQueryType)); + Assert.That(metricMappers, Is.Not.Null); + + // Keep these out of order to ensure we don't depend on it + var expected = new[] {"Long", "Integer", "Short", "Decimal", "Byte"}; + string[] actual = metricMappers.Select(m => m.MetricName).ToArray(); + Assert.That(actual, Is.EquivalentTo(expected), "Properties discovered and mapped wrong"); + } + } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/AzureSqlEndpoint.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/AzureSqlEndpoint.cs index 3383db9..2f2eef7 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/AzureSqlEndpoint.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/AzureSqlEndpoint.cs @@ -1,14 +1,12 @@ using System.Collections.Generic; using System.Data.SqlClient; using System.Linq; - +using log4net; using NewRelic.Microsoft.SqlServer.Plugin.Configuration; using NewRelic.Microsoft.SqlServer.Plugin.Core; using NewRelic.Microsoft.SqlServer.Plugin.Properties; using NewRelic.Microsoft.SqlServer.Plugin.QueryTypes; -using log4net; - namespace NewRelic.Microsoft.SqlServer.Plugin { public class AzureSqlEndpoint : SqlEndpointBase diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/Configuration/ConfigurationParser.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/Configuration/ConfigurationParser.cs index 7ed1ccb..d8166a8 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/Configuration/ConfigurationParser.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/Configuration/ConfigurationParser.cs @@ -33,7 +33,7 @@ public static Settings ParseSettings(ILog log, string configFilePath = null) throw new FileNotFoundException("Unable to locate config file", configFilePath); } - return LoadConfigurationFromFile(configFilePath, log); + return LoadConfigurationFromFile(configFilePath, log); } /// @@ -61,7 +61,7 @@ private static Settings GetSettingsFromAppConfig(ILog log) log.Debug("No external configuration path given, attempting to load settings from from default configuration file"); var section = (NewRelicConfigurationSection) ConfigurationManager.GetSection("newRelic"); var settingsFromAppConfig = Settings.FromConfigurationSection(section,log); - log.InfoFormat("Settings loaded successfully"); + log.InfoFormat("Settings loaded successfully"); return settingsFromAppConfig; } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/Configuration/NewRelicConfigurationSection.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/Configuration/NewRelicConfigurationSection.cs index 580a1e0..c3abdf9 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/Configuration/NewRelicConfigurationSection.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/Configuration/NewRelicConfigurationSection.cs @@ -2,30 +2,30 @@ namespace NewRelic.Microsoft.SqlServer.Plugin.Configuration { - internal class NewRelicConfigurationSection : ConfigurationSection - { - [ConfigurationProperty("service", IsRequired = true)] - public ServiceElement Service - { - get { return ((ServiceElement) (base["service"])); } - } + internal class NewRelicConfigurationSection : ConfigurationSection + { + [ConfigurationProperty("service", IsRequired = true)] + public ServiceElement Service + { + get { return ((ServiceElement) (base["service"])); } + } - [ConfigurationProperty("proxy")] - public ProxyElement Proxy - { - get { return ((ProxyElement)(base["proxy"])); } - } + [ConfigurationProperty("proxy")] + public ProxyElement Proxy + { + get { return ((ProxyElement)(base["proxy"])); } + } - [ConfigurationProperty("sqlServers")] - public SqlServerCollection SqlServers - { - get { return ((SqlServerCollection) (base["sqlServers"])); } - } + [ConfigurationProperty("sqlServers")] + public SqlServerCollection SqlServers + { + get { return ((SqlServerCollection) (base["sqlServers"])); } + } - [ConfigurationProperty("azure")] - public AzureCollection AzureSqlDatabases - { - get { return ((AzureCollection)(base["azure"])); } - } - } + [ConfigurationProperty("azure")] + public AzureCollection AzureSqlDatabases + { + get { return ((AzureCollection)(base["azure"])); } + } + } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/Configuration/ServiceElement.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/Configuration/ServiceElement.cs index f99b798..cdd76f2 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/Configuration/ServiceElement.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/Configuration/ServiceElement.cs @@ -4,36 +4,36 @@ namespace NewRelic.Microsoft.SqlServer.Plugin.Configuration { - internal class ServiceElement : ConfigurationElement - { - /// - /// New Relic license key. Required. - /// - [ConfigurationProperty("licenseKey", DefaultValue = "", IsKey = false, IsRequired = true)] - public string LicenseKey - { - get { return ((string) (base["licenseKey"])); } - set { base["licenseKey"] = value; } - } + internal class ServiceElement : ConfigurationElement + { + /// + /// New Relic license key. Required. + /// + [ConfigurationProperty("licenseKey", DefaultValue = "", IsKey = false, IsRequired = false)] + public string LicenseKey + { + get { return ((string) (base["licenseKey"])); } + set { base["licenseKey"] = value; } + } - /// - /// Number of seconds between polling of the servers. Default is 60. - /// - [ConfigurationProperty("pollIntervalSeconds", DefaultValue = 60, IsKey = false, IsRequired = false)] - public int PollIntervalSeconds - { - get { return (int?)(base["pollIntervalSeconds"]) ?? 60; } - set { base["pollIntervalSeconds"] = value; } - } + /// + /// Number of seconds between polling of the servers. Default is 60. + /// + [ConfigurationProperty("pollIntervalSeconds", DefaultValue = 60, IsKey = false, IsRequired = false)] + public int PollIntervalSeconds + { + get { return (int?)(base["pollIntervalSeconds"]) ?? 60; } + set { base["pollIntervalSeconds"] = value; } + } - /// - /// Override the default Windows service name. Must contain only letters or the default is used. - /// - [ConfigurationProperty("serviceName", DefaultValue = ServiceConstants.ServiceName, IsKey = false, IsRequired = false)] - public string ServiceName - { - get { return ((string)(base["serviceName"])); } - set { base["serviceName"] = value; } - } - } + /// + /// Override the default Windows service name. Must contain only letters or the default is used. + /// + [ConfigurationProperty("serviceName", DefaultValue = ServiceConstants.ServiceName, IsKey = false, IsRequired = false)] + public string ServiceName + { + get { return ((string)(base["serviceName"])); } + set { base["serviceName"] = value; } + } + } } \ No newline at end of file diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/Configuration/Settings.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/Configuration/Settings.cs index e709986..a8b6bb8 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/Configuration/Settings.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/Configuration/Settings.cs @@ -4,199 +4,198 @@ using System.Reflection; using System.Security.Principal; using System.Text.RegularExpressions; - -using NewRelic.Microsoft.SqlServer.Plugin.Properties; - using log4net; +using NewRelic.Microsoft.SqlServer.Plugin.Properties; namespace NewRelic.Microsoft.SqlServer.Plugin.Configuration { - public class Settings - { - private string _version; - private static string _ProxyDetails; - - public Settings(ISqlEndpoint[] endpoints) - { - Endpoints = endpoints; - PollIntervalSeconds = 60; - ServiceName = ServiceConstants.ServiceName; - - var identity = WindowsIdentity.GetCurrent(); - if (identity != null) - { - var principal = new WindowsPrincipal(identity); - IsProcessElevated = principal.IsInRole(WindowsBuiltInRole.Administrator); - } - } - - public static Settings Default { get; internal set; } - - public string LicenseKey { get; set; } - public int PollIntervalSeconds { get; set; } - public string ServiceName { get; set; } - public ISqlEndpoint[] Endpoints { get; private set; } - public bool TestMode { get; set; } - public bool IsProcessElevated { get; private set; } - - public string Version - { - get - { - if (_version == null) - { - var versionAttr = (AssemblyInformationalVersionAttribute) Assembly.GetExecutingAssembly().GetCustomAttributes(typeof (AssemblyInformationalVersionAttribute), true).First(); - _version = versionAttr.InformationalVersion; - } - return _version; - } - } - - internal static Settings FromConfigurationSection(NewRelicConfigurationSection section, ILog log) - { - var sqlEndpoints = section.SqlServers - .Select(s => - { - var includedDatabaseNames = s.IncludedDatabases.Select(d => d.ToDatabase()).ToArray(); - var excludedDatabaseNames = s.ExcludedDatabases.Select(d => d.Name).ToArray(); - return - (ISqlEndpoint) new SqlServerEndpoint(s.Name, s.ConnectionString, s.IncludeSystemDatabases, includedDatabaseNames, excludedDatabaseNames); - }) - .Union(section.AzureSqlDatabases.Select(s => (ISqlEndpoint) new AzureSqlEndpoint(s.Name, s.ConnectionString))); - - var service = section.Service; - var settings = new Settings(sqlEndpoints.ToArray()) - { - LicenseKey = service.LicenseKey, - PollIntervalSeconds = service.PollIntervalSeconds, - }; - - if (!string.IsNullOrEmpty(service.ServiceName) && Regex.IsMatch(service.ServiceName, "^[a-zA-Z_0-9-]{6,32}$")) - { - settings.ServiceName = service.ServiceName; - } - - var webProxy = GetWebProxy(section, log); - if (webProxy != null) - { - WebRequest.DefaultWebProxy = webProxy; - } - - return settings; - } - - private static IWebProxy GetWebProxy(NewRelicConfigurationSection section, ILog log) - { - var proxyElement = section.Proxy; - if (proxyElement == null || !proxyElement.ElementInformation.IsPresent) return null; - - Uri uri; - if (!Uri.TryCreate(proxyElement.Host, UriKind.RelativeOrAbsolute, out uri)) - { - log.ErrorFormat("Proxy host '{0}' is not a valid URI, skipping proxy.", proxyElement.Host); - return null; - } - - int port; - if (!int.TryParse(proxyElement.Port, out port)) - { - log.ErrorFormat("Unable to parse proxy port from '{0}', skipping proxy. Expecting a number from 1-65535.", proxyElement.Port); - return null; - } - - WebProxy webProxy; - try - { - webProxy = new WebProxy(proxyElement.Host, port); - } - catch (Exception e) - { - log.ErrorFormat("Proxy settings are invalid. {0}", e.Message); - return null; - } - - if ("true".Equals(proxyElement.UseDefaultCredentials, StringComparison.InvariantCultureIgnoreCase)) - { - webProxy.UseDefaultCredentials = true; - webProxy.Credentials = CredentialCache.DefaultCredentials; - _ProxyDetails = string.Format("Proxy Server: {0}:{1} with default credentials", proxyElement.Host, port); - } - else if (!string.IsNullOrEmpty(proxyElement.User)) - { - if (string.IsNullOrEmpty(proxyElement.Domain)) - { - webProxy.Credentials = new NetworkCredential(proxyElement.User, proxyElement.Password); - _ProxyDetails = string.Format("Proxy Server: {0}@{1}:{2}", proxyElement.User, proxyElement.Host, port); - } - else - { - webProxy.Credentials = new NetworkCredential(proxyElement.User, proxyElement.Password, proxyElement.Domain); - _ProxyDetails = string.Format("Proxy Server: {0}\\{1}@{2}:{3}", proxyElement.Domain, proxyElement.User, proxyElement.Host, port); - } - } - else - { - _ProxyDetails = string.Format("Proxy Server: {0}:{1}", proxyElement.Host, port); - } - - return webProxy; - } - - public void ToLog(ILog log) - { - // Pending review by New Relic before adding this information - // log.Info(" New Relic Key: " + LicenseKey); - - log.Info(" Version: " + Version); - log.Info(" Test Mode: " + (TestMode ? "Yes" : "No")); - log.Info(" Windows Service: " + (Environment.UserInteractive ? "No" : "Yes")); - log.InfoFormat(@" User: {0}\{1}", Environment.UserDomainName, Environment.UserName); - log.Info(" Run as Administrator: " + (IsProcessElevated ? "Yes" : "No")); - - if (_ProxyDetails != null) - { - log.Info(" " + _ProxyDetails); - } - - log.Info(" Total Endpoints: " + Endpoints.Length); - log.Info(" Poll Interval Seconds: " + PollIntervalSeconds); - - var sqlServerEndpoints = Endpoints.OfType().ToArray(); - if (sqlServerEndpoints.Any()) - { - log.InfoFormat(" SqlServerEndpoints: {0}", sqlServerEndpoints.Count()); - log.InfoFormat(" PluginGUID: {0}", Constants.SqlServerComponentGuid); - foreach (ISqlEndpoint endpoint in sqlServerEndpoints) - { - endpoint.ToLog(log); - } - log.Info(string.Empty); - } - else - { - log.Debug("No SQL Server endpoints configured."); - } - - var azureEndpoints = Endpoints.OfType().ToArray(); - if (azureEndpoints.Any()) - { - log.InfoFormat(" AzureEndpoints: {0}", azureEndpoints.Count()); - log.InfoFormat(" PluginGUID: {0}", Constants.SqlAzureComponentGuid); - foreach (ISqlEndpoint endpoint in azureEndpoints) - { - endpoint.ToLog(log); - } - log.Info(string.Empty); - } - else - { - log.Debug("No Azure SQL endpoints configured."); - } - } - - public override string ToString() - { - return string.Format("Version: {0}, PollIntervalSeconds: {1}, TestMode: {2}", Version, PollIntervalSeconds, TestMode); - } - } + public class Settings + { + private string _version; + private static string _ProxyDetails; + + public Settings(ISqlEndpoint[] endpoints) + { + Endpoints = endpoints; + PollIntervalSeconds = 60; + ServiceName = ServiceConstants.ServiceName; + + var identity = WindowsIdentity.GetCurrent(); + if (identity != null) + { + var principal = new WindowsPrincipal(identity); + IsProcessElevated = principal.IsInRole(WindowsBuiltInRole.Administrator); + } + } + + public static Settings Default { get; internal set; } + + public string LicenseKey { get; set; } + public int PollIntervalSeconds { get; set; } + public string ServiceName { get; set; } + public ISqlEndpoint[] Endpoints { get; private set; } + public bool TestMode { get; set; } + public bool IsProcessElevated { get; private set; } + + public string Version + { + get + { + if (_version == null) + { + var versionAttr = (AssemblyInformationalVersionAttribute) Assembly.GetExecutingAssembly().GetCustomAttributes(typeof (AssemblyInformationalVersionAttribute), true).First(); + _version = versionAttr.InformationalVersion; + } + return _version; + } + } + + internal static Settings FromConfigurationSection(NewRelicConfigurationSection section, ILog log) + { + var sqlEndpoints = section.SqlServers + .Select(s => + { + var includedDatabaseNames = s.IncludedDatabases.Select(d => d.ToDatabase()).ToArray(); + var excludedDatabaseNames = s.ExcludedDatabases.Select(d => d.Name).ToArray(); + return + (ISqlEndpoint) new SqlServerEndpoint(s.Name, s.ConnectionString, s.IncludeSystemDatabases, includedDatabaseNames, excludedDatabaseNames); + }) + .Union(section.AzureSqlDatabases.Select(s => (ISqlEndpoint) new AzureSqlEndpoint(s.Name, s.ConnectionString))); + + var service = section.Service; + var settings = new Settings(sqlEndpoints.ToArray()) + { + LicenseKey = service.LicenseKey, + PollIntervalSeconds = service.PollIntervalSeconds, + }; + + + if (!string.IsNullOrEmpty(service.ServiceName) && Regex.IsMatch(service.ServiceName, "^[a-zA-Z_0-9-]{6,32}$")) + { + settings.ServiceName = service.ServiceName; + } + + var webProxy = GetWebProxy(section, log); + if (webProxy != null) + { + WebRequest.DefaultWebProxy = webProxy; + } + + return settings; + } + + private static IWebProxy GetWebProxy(NewRelicConfigurationSection section, ILog log) + { + var proxyElement = section.Proxy; + if (proxyElement == null || !proxyElement.ElementInformation.IsPresent) return null; + + Uri uri; + if (!Uri.TryCreate(proxyElement.Host, UriKind.RelativeOrAbsolute, out uri)) + { + log.ErrorFormat("Proxy host '{0}' is not a valid URI, skipping proxy.", proxyElement.Host); + return null; + } + + int port; + if (!int.TryParse(proxyElement.Port, out port)) + { + log.ErrorFormat("Unable to parse proxy port from '{0}', skipping proxy. Expecting a number from 1-65535.", proxyElement.Port); + return null; + } + + WebProxy webProxy; + try + { + webProxy = new WebProxy(proxyElement.Host, port); + } + catch (Exception e) + { + log.ErrorFormat("Proxy settings are invalid. {0}", e.Message); + return null; + } + + if ("true".Equals(proxyElement.UseDefaultCredentials, StringComparison.InvariantCultureIgnoreCase)) + { + webProxy.UseDefaultCredentials = true; + webProxy.Credentials = CredentialCache.DefaultCredentials; + _ProxyDetails = string.Format("Proxy Server: {0}:{1} with default credentials", proxyElement.Host, port); + } + else if (!string.IsNullOrEmpty(proxyElement.User)) + { + if (string.IsNullOrEmpty(proxyElement.Domain)) + { + webProxy.Credentials = new NetworkCredential(proxyElement.User, proxyElement.Password); + _ProxyDetails = string.Format("Proxy Server: {0}@{1}:{2}", proxyElement.User, proxyElement.Host, port); + } + else + { + webProxy.Credentials = new NetworkCredential(proxyElement.User, proxyElement.Password, proxyElement.Domain); + _ProxyDetails = string.Format("Proxy Server: {0}\\{1}@{2}:{3}", proxyElement.Domain, proxyElement.User, proxyElement.Host, port); + } + } + else + { + _ProxyDetails = string.Format("Proxy Server: {0}:{1}", proxyElement.Host, port); + } + + return webProxy; + } + + public void ToLog(ILog log) + { + // Pending review by New Relic before adding this information + // log.Info(" New Relic Key: " + LicenseKey); + + log.Info(" Version: " + Version); + log.Info(" Test Mode: " + (TestMode ? "Yes" : "No")); + log.Info(" Windows Service: " + (Environment.UserInteractive ? "No" : "Yes")); + log.InfoFormat(@" User: {0}\{1}", Environment.UserDomainName, Environment.UserName); + log.Info(" Run as Administrator: " + (IsProcessElevated ? "Yes" : "No")); + + if (_ProxyDetails != null) + { + log.Info(" " + _ProxyDetails); + } + + log.Info(" Total Endpoints: " + Endpoints.Length); + log.Info(" Poll Interval Seconds: " + PollIntervalSeconds); + + var sqlServerEndpoints = Endpoints.OfType().ToArray(); + if (sqlServerEndpoints.Any()) + { + log.InfoFormat(" SqlServerEndpoints: {0}", sqlServerEndpoints.Count()); + log.InfoFormat(" PluginGUID: {0}", Constants.SqlServerComponentGuid); + foreach (ISqlEndpoint endpoint in sqlServerEndpoints) + { + endpoint.ToLog(log); + } + log.Info(string.Empty); + } + else + { + log.Debug("No SQL Server endpoints configured."); + } + + var azureEndpoints = Endpoints.OfType().ToArray(); + if (azureEndpoints.Any()) + { + log.InfoFormat(" AzureEndpoints: {0}", azureEndpoints.Count()); + log.InfoFormat(" PluginGUID: {0}", Constants.SqlAzureComponentGuid); + foreach (ISqlEndpoint endpoint in azureEndpoints) + { + endpoint.ToLog(log); + } + log.Info(string.Empty); + } + else + { + log.Debug("No Azure SQL endpoints configured."); + } + } + + public override string ToString() + { + return string.Format("Version: {0}, PollIntervalSeconds: {1}, TestMode: {2}", Version, PollIntervalSeconds, TestMode); + } + } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/Configuration/SqlServerElement.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/Configuration/SqlServerElement.cs index da5e7a5..ca1326c 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/Configuration/SqlServerElement.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/Configuration/SqlServerElement.cs @@ -2,39 +2,39 @@ namespace NewRelic.Microsoft.SqlServer.Plugin.Configuration { - internal class SqlServerElement : ConfigurationElement - { - [ConfigurationProperty("name", DefaultValue = "", IsKey = true, IsRequired = true)] - public string Name - { - get { return ((string) (base["name"])); } - set { base["name"] = value; } - } + internal class SqlServerElement : ConfigurationElement + { + [ConfigurationProperty("name", DefaultValue = "", IsKey = true, IsRequired = true)] + public string Name + { + get { return ((string) (base["name"])); } + set { base["name"] = value; } + } - [ConfigurationProperty("connectionString", DefaultValue = "", IsKey = false, IsRequired = true)] - public string ConnectionString - { - get { return ((string) (base["connectionString"])); } - set { base["connectionString"] = value; } - } + [ConfigurationProperty("connectionString", DefaultValue = "", IsKey = false, IsRequired = true)] + public string ConnectionString + { + get { return ((string) (base["connectionString"])); } + set { base["connectionString"] = value; } + } - [ConfigurationProperty("includeSystemDatabases", DefaultValue = false, IsKey = false, IsRequired = false)] - public bool IncludeSystemDatabases - { - get { return ((bool) (base["includeSystemDatabases"])); } - set { base["includeSystemDatabases"] = value; } - } + [ConfigurationProperty("includeSystemDatabases", DefaultValue = false, IsKey = false, IsRequired = false)] + public bool IncludeSystemDatabases + { + get { return ((bool) (base["includeSystemDatabases"])); } + set { base["includeSystemDatabases"] = value; } + } - [ConfigurationProperty("includes")] - public DatabaseCollection IncludedDatabases - { - get { return ((DatabaseCollection) (base["includes"])); } - } + [ConfigurationProperty("includes")] + public DatabaseCollection IncludedDatabases + { + get { return ((DatabaseCollection) (base["includes"])); } + } - [ConfigurationProperty("excludes")] - public DatabaseCollection ExcludedDatabases - { - get { return ((DatabaseCollection) (base["excludes"])); } - } - } + [ConfigurationProperty("excludes")] + public DatabaseCollection ExcludedDatabases + { + get { return ((DatabaseCollection) (base["excludes"])); } + } + } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/Core/Dapper.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/Core/Dapper.cs index 0daed85..201812e 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/Core/Dapper.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/Core/Dapper.cs @@ -23,3340 +23,3340 @@ namespace NewRelic.Microsoft.SqlServer.Plugin.Core { - /// - /// Dapper, a light weight object mapper for ADO.NET - /// - static partial class SqlMapper - { - /// - /// Implement this interface to pass an arbitrary db specific set of parameters to Dapper - /// - public partial interface IDynamicParameters - { - /// - /// Add all the parameters needed to the command just before it executes - /// - /// The raw command prior to execution - /// Information about the query - void AddParameters(IDbCommand command, Identity identity); - } - - /// - /// Implement this interface to change default mapping of reader columns to type members - /// - public interface ITypeMap - { - /// - /// Finds best constructor - /// - /// DataReader column names - /// DataReader column types - /// Matching constructor or default one - ConstructorInfo FindConstructor(string[] names, Type[] types); - - /// - /// Gets mapping for constructor parameter - /// - /// Constructor to resolve - /// DataReader column name - /// Mapping implementation - IMemberMap GetConstructorParameter(ConstructorInfo constructor, string columnName); - - /// - /// Gets member mapping for column - /// - /// DataReader column name - /// Mapping implementation - IMemberMap GetMember(string columnName); - } - - /// - /// Implements this interface to provide custom member mapping - /// - public interface IMemberMap - { - /// - /// Source DataReader column name - /// - string ColumnName { get; } - - /// - /// Target member type - /// - Type MemberType { get; } - - /// - /// Target property - /// - PropertyInfo Property { get; } - - /// - /// Target field - /// - FieldInfo Field { get; } - - /// - /// Target constructor parameter - /// - ParameterInfo Parameter { get; } - } - - static Link> bindByNameCache; - static Action GetBindByName(Type commandType) - { - if (commandType == null) return null; // GIGO - Action action; - if (Link>.TryGet(bindByNameCache, commandType, out action)) - { - return action; - } - var prop = commandType.GetProperty("BindByName", BindingFlags.Public | BindingFlags.Instance); - action = null; - ParameterInfo[] indexers; - MethodInfo setter; - if (prop != null && prop.CanWrite && prop.PropertyType == typeof(bool) - && ((indexers = prop.GetIndexParameters()) == null || indexers.Length == 0) - && (setter = prop.GetSetMethod()) != null - ) - { - var method = new DynamicMethod(commandType.Name + "_BindByName", null, new Type[] { typeof(IDbCommand), typeof(bool) }); - var il = method.GetILGenerator(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Castclass, commandType); - il.Emit(OpCodes.Ldarg_1); - il.EmitCall(OpCodes.Callvirt, setter, null); - il.Emit(OpCodes.Ret); - action = (Action)method.CreateDelegate(typeof(Action)); - } - // cache it - Link>.TryAdd(ref bindByNameCache, commandType, ref action); - return action; - } - /// - /// This is a micro-cache; suitable when the number of terms is controllable (a few hundred, for example), - /// and strictly append-only; you cannot change existing values. All key matches are on **REFERENCE** - /// equality. The type is fully thread-safe. - /// - partial class Link where TKey : class - { - public static bool TryGet(Link link, TKey key, out TValue value) - { - while (link != null) - { - if ((object)key == (object)link.Key) - { - value = link.Value; - return true; - } - link = link.Tail; - } - value = default(TValue); - return false; - } - public static bool TryAdd(ref Link head, TKey key, ref TValue value) - { - bool tryAgain; - do - { - var snapshot = Interlocked.CompareExchange(ref head, null, null); - TValue found; - if (TryGet(snapshot, key, out found)) - { // existing match; report the existing value instead - value = found; - return false; - } - var newNode = new Link(key, value, snapshot); - // did somebody move our cheese? - tryAgain = Interlocked.CompareExchange(ref head, newNode, snapshot) != snapshot; - } while (tryAgain); - return true; - } - private Link(TKey key, TValue value, Link tail) - { - Key = key; - Value = value; - Tail = tail; - } - public TKey Key { get; private set; } - public TValue Value { get; private set; } - public Link Tail { get; private set; } - } - partial class CacheInfo - { - public DeserializerState Deserializer { get; set; } - public Func[] OtherDeserializers { get; set; } - public Action ParamReader { get; set; } - private int hitCount; - public int GetHitCount() { return Interlocked.CompareExchange(ref hitCount, 0, 0); } - public void RecordHit() { Interlocked.Increment(ref hitCount); } - } - static int GetColumnHash(IDataReader reader) - { - unchecked - { - int colCount = reader.FieldCount, hash = colCount; - for (int i = 0; i < colCount; i++) - { // binding code is only interested in names - not types - object tmp = reader.GetName(i); - hash = (hash * 31) + (tmp == null ? 0 : tmp.GetHashCode()); - } - return hash; - } - } - struct DeserializerState - { - public readonly int Hash; - public readonly Func Func; - - public DeserializerState(int hash, Func func) - { - Hash = hash; - Func = func; - } - } - - /// - /// Called if the query cache is purged via PurgeQueryCache - /// - public static event EventHandler QueryCachePurged; - private static void OnQueryCachePurged() - { - var handler = QueryCachePurged; - if (handler != null) handler(null, EventArgs.Empty); - } + /// + /// Dapper, a light weight object mapper for ADO.NET + /// + static partial class SqlMapper + { + /// + /// Implement this interface to pass an arbitrary db specific set of parameters to Dapper + /// + public partial interface IDynamicParameters + { + /// + /// Add all the parameters needed to the command just before it executes + /// + /// The raw command prior to execution + /// Information about the query + void AddParameters(IDbCommand command, Identity identity); + } + + /// + /// Implement this interface to change default mapping of reader columns to type members + /// + public interface ITypeMap + { + /// + /// Finds best constructor + /// + /// DataReader column names + /// DataReader column types + /// Matching constructor or default one + ConstructorInfo FindConstructor(string[] names, Type[] types); + + /// + /// Gets mapping for constructor parameter + /// + /// Constructor to resolve + /// DataReader column name + /// Mapping implementation + IMemberMap GetConstructorParameter(ConstructorInfo constructor, string columnName); + + /// + /// Gets member mapping for column + /// + /// DataReader column name + /// Mapping implementation + IMemberMap GetMember(string columnName); + } + + /// + /// Implements this interface to provide custom member mapping + /// + public interface IMemberMap + { + /// + /// Source DataReader column name + /// + string ColumnName { get; } + + /// + /// Target member type + /// + Type MemberType { get; } + + /// + /// Target property + /// + PropertyInfo Property { get; } + + /// + /// Target field + /// + FieldInfo Field { get; } + + /// + /// Target constructor parameter + /// + ParameterInfo Parameter { get; } + } + + static Link> bindByNameCache; + static Action GetBindByName(Type commandType) + { + if (commandType == null) return null; // GIGO + Action action; + if (Link>.TryGet(bindByNameCache, commandType, out action)) + { + return action; + } + var prop = commandType.GetProperty("BindByName", BindingFlags.Public | BindingFlags.Instance); + action = null; + ParameterInfo[] indexers; + MethodInfo setter; + if (prop != null && prop.CanWrite && prop.PropertyType == typeof(bool) + && ((indexers = prop.GetIndexParameters()) == null || indexers.Length == 0) + && (setter = prop.GetSetMethod()) != null + ) + { + var method = new DynamicMethod(commandType.Name + "_BindByName", null, new Type[] { typeof(IDbCommand), typeof(bool) }); + var il = method.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, commandType); + il.Emit(OpCodes.Ldarg_1); + il.EmitCall(OpCodes.Callvirt, setter, null); + il.Emit(OpCodes.Ret); + action = (Action)method.CreateDelegate(typeof(Action)); + } + // cache it + Link>.TryAdd(ref bindByNameCache, commandType, ref action); + return action; + } + /// + /// This is a micro-cache; suitable when the number of terms is controllable (a few hundred, for example), + /// and strictly append-only; you cannot change existing values. All key matches are on **REFERENCE** + /// equality. The type is fully thread-safe. + /// + partial class Link where TKey : class + { + public static bool TryGet(Link link, TKey key, out TValue value) + { + while (link != null) + { + if ((object)key == (object)link.Key) + { + value = link.Value; + return true; + } + link = link.Tail; + } + value = default(TValue); + return false; + } + public static bool TryAdd(ref Link head, TKey key, ref TValue value) + { + bool tryAgain; + do + { + var snapshot = Interlocked.CompareExchange(ref head, null, null); + TValue found; + if (TryGet(snapshot, key, out found)) + { // existing match; report the existing value instead + value = found; + return false; + } + var newNode = new Link(key, value, snapshot); + // did somebody move our cheese? + tryAgain = Interlocked.CompareExchange(ref head, newNode, snapshot) != snapshot; + } while (tryAgain); + return true; + } + private Link(TKey key, TValue value, Link tail) + { + Key = key; + Value = value; + Tail = tail; + } + public TKey Key { get; private set; } + public TValue Value { get; private set; } + public Link Tail { get; private set; } + } + partial class CacheInfo + { + public DeserializerState Deserializer { get; set; } + public Func[] OtherDeserializers { get; set; } + public Action ParamReader { get; set; } + private int hitCount; + public int GetHitCount() { return Interlocked.CompareExchange(ref hitCount, 0, 0); } + public void RecordHit() { Interlocked.Increment(ref hitCount); } + } + static int GetColumnHash(IDataReader reader) + { + unchecked + { + int colCount = reader.FieldCount, hash = colCount; + for (int i = 0; i < colCount; i++) + { // binding code is only interested in names - not types + object tmp = reader.GetName(i); + hash = (hash * 31) + (tmp == null ? 0 : tmp.GetHashCode()); + } + return hash; + } + } + struct DeserializerState + { + public readonly int Hash; + public readonly Func Func; + + public DeserializerState(int hash, Func func) + { + Hash = hash; + Func = func; + } + } + + /// + /// Called if the query cache is purged via PurgeQueryCache + /// + public static event EventHandler QueryCachePurged; + private static void OnQueryCachePurged() + { + var handler = QueryCachePurged; + if (handler != null) handler(null, EventArgs.Empty); + } #if CSHARP30 - private static readonly Dictionary _queryCache = new Dictionary(); - // note: conflicts between readers and writers are so short-lived that it isn't worth the overhead of - // ReaderWriterLockSlim etc; a simple lock is faster - private static void SetQueryCache(Identity key, CacheInfo value) - { - lock (_queryCache) { _queryCache[key] = value; } - } - private static bool TryGetQueryCache(Identity key, out CacheInfo value) - { - lock (_queryCache) { return _queryCache.TryGetValue(key, out value); } - } - private static void PurgeQueryCacheByType(Type type) - { - lock (_queryCache) - { - var toRemove = _queryCache.Keys.Where(id => id.type == type).ToArray(); - foreach (var key in toRemove) - _queryCache.Remove(key); - } - } - /// - /// Purge the query cache - /// - public static void PurgeQueryCache() - { - lock (_queryCache) - { - _queryCache.Clear(); - } - OnQueryCachePurged(); - } + private static readonly Dictionary _queryCache = new Dictionary(); + // note: conflicts between readers and writers are so short-lived that it isn't worth the overhead of + // ReaderWriterLockSlim etc; a simple lock is faster + private static void SetQueryCache(Identity key, CacheInfo value) + { + lock (_queryCache) { _queryCache[key] = value; } + } + private static bool TryGetQueryCache(Identity key, out CacheInfo value) + { + lock (_queryCache) { return _queryCache.TryGetValue(key, out value); } + } + private static void PurgeQueryCacheByType(Type type) + { + lock (_queryCache) + { + var toRemove = _queryCache.Keys.Where(id => id.type == type).ToArray(); + foreach (var key in toRemove) + _queryCache.Remove(key); + } + } + /// + /// Purge the query cache + /// + public static void PurgeQueryCache() + { + lock (_queryCache) + { + _queryCache.Clear(); + } + OnQueryCachePurged(); + } #else - static readonly System.Collections.Concurrent.ConcurrentDictionary _queryCache = new System.Collections.Concurrent.ConcurrentDictionary(); - private static void SetQueryCache(Identity key, CacheInfo value) - { - if (Interlocked.Increment(ref collect) == COLLECT_PER_ITEMS) - { - CollectCacheGarbage(); - } - _queryCache[key] = value; - } - - private static void CollectCacheGarbage() - { - try - { - foreach (var pair in _queryCache) - { - if (pair.Value.GetHitCount() <= COLLECT_HIT_COUNT_MIN) - { - CacheInfo cache; - _queryCache.TryRemove(pair.Key, out cache); - } - } - } - - finally - { - Interlocked.Exchange(ref collect, 0); - } - } - - private const int COLLECT_PER_ITEMS = 1000, COLLECT_HIT_COUNT_MIN = 0; - private static int collect; - private static bool TryGetQueryCache(Identity key, out CacheInfo value) - { - if (_queryCache.TryGetValue(key, out value)) - { - value.RecordHit(); - return true; - } - value = null; - return false; - } - - /// - /// Purge the query cache - /// - public static void PurgeQueryCache() - { - _queryCache.Clear(); - OnQueryCachePurged(); - } - - private static void PurgeQueryCacheByType(Type type) - { - foreach (var entry in _queryCache) - { - CacheInfo cache; - if (entry.Key.type == type) - _queryCache.TryRemove(entry.Key, out cache); - } - } - - /// - /// Return a count of all the cached queries by dapper - /// - /// - public static int GetCachedSQLCount() - { - return _queryCache.Count; - } - - /// - /// Return a list of all the queries cached by dapper - /// - /// - /// - public static IEnumerable> GetCachedSQL(int ignoreHitCountAbove = int.MaxValue) - { - var data = _queryCache.Select(pair => Tuple.Create(pair.Key.connectionString, pair.Key.sql, pair.Value.GetHitCount())); - if (ignoreHitCountAbove < int.MaxValue) data = data.Where(tuple => tuple.Item3 <= ignoreHitCountAbove); - return data; - } - - /// - /// Deep diagnostics only: find any hash collisions in the cache - /// - /// - public static IEnumerable> GetHashCollissions() - { - var counts = new Dictionary(); - foreach (var key in _queryCache.Keys) - { - int count; - if (!counts.TryGetValue(key.hashCode, out count)) - { - counts.Add(key.hashCode, 1); - } - else - { - counts[key.hashCode] = count + 1; - } - } - return from pair in counts - where pair.Value > 1 - select Tuple.Create(pair.Key, pair.Value); - - } + static readonly System.Collections.Concurrent.ConcurrentDictionary _queryCache = new System.Collections.Concurrent.ConcurrentDictionary(); + private static void SetQueryCache(Identity key, CacheInfo value) + { + if (Interlocked.Increment(ref collect) == COLLECT_PER_ITEMS) + { + CollectCacheGarbage(); + } + _queryCache[key] = value; + } + + private static void CollectCacheGarbage() + { + try + { + foreach (var pair in _queryCache) + { + if (pair.Value.GetHitCount() <= COLLECT_HIT_COUNT_MIN) + { + CacheInfo cache; + _queryCache.TryRemove(pair.Key, out cache); + } + } + } + + finally + { + Interlocked.Exchange(ref collect, 0); + } + } + + private const int COLLECT_PER_ITEMS = 1000, COLLECT_HIT_COUNT_MIN = 0; + private static int collect; + private static bool TryGetQueryCache(Identity key, out CacheInfo value) + { + if (_queryCache.TryGetValue(key, out value)) + { + value.RecordHit(); + return true; + } + value = null; + return false; + } + + /// + /// Purge the query cache + /// + public static void PurgeQueryCache() + { + _queryCache.Clear(); + OnQueryCachePurged(); + } + + private static void PurgeQueryCacheByType(Type type) + { + foreach (var entry in _queryCache) + { + CacheInfo cache; + if (entry.Key.type == type) + _queryCache.TryRemove(entry.Key, out cache); + } + } + + /// + /// Return a count of all the cached queries by dapper + /// + /// + public static int GetCachedSQLCount() + { + return _queryCache.Count; + } + + /// + /// Return a list of all the queries cached by dapper + /// + /// + /// + public static IEnumerable> GetCachedSQL(int ignoreHitCountAbove = int.MaxValue) + { + var data = _queryCache.Select(pair => Tuple.Create(pair.Key.connectionString, pair.Key.sql, pair.Value.GetHitCount())); + if (ignoreHitCountAbove < int.MaxValue) data = data.Where(tuple => tuple.Item3 <= ignoreHitCountAbove); + return data; + } + + /// + /// Deep diagnostics only: find any hash collisions in the cache + /// + /// + public static IEnumerable> GetHashCollissions() + { + var counts = new Dictionary(); + foreach (var key in _queryCache.Keys) + { + int count; + if (!counts.TryGetValue(key.hashCode, out count)) + { + counts.Add(key.hashCode, 1); + } + else + { + counts[key.hashCode] = count + 1; + } + } + return from pair in counts + where pair.Value > 1 + select Tuple.Create(pair.Key, pair.Value); + + } #endif - static readonly Dictionary typeMap; - - static SqlMapper() - { - typeMap = new Dictionary(); - typeMap[typeof(byte)] = DbType.Byte; - typeMap[typeof(sbyte)] = DbType.SByte; - typeMap[typeof(short)] = DbType.Int16; - typeMap[typeof(ushort)] = DbType.UInt16; - typeMap[typeof(int)] = DbType.Int32; - typeMap[typeof(uint)] = DbType.UInt32; - typeMap[typeof(long)] = DbType.Int64; - typeMap[typeof(ulong)] = DbType.UInt64; - typeMap[typeof(float)] = DbType.Single; - typeMap[typeof(double)] = DbType.Double; - typeMap[typeof(decimal)] = DbType.Decimal; - typeMap[typeof(bool)] = DbType.Boolean; - typeMap[typeof(string)] = DbType.String; - typeMap[typeof(char)] = DbType.StringFixedLength; - typeMap[typeof(Guid)] = DbType.Guid; - typeMap[typeof(DateTime)] = DbType.DateTime; - typeMap[typeof(DateTimeOffset)] = DbType.DateTimeOffset; - typeMap[typeof(TimeSpan)] = DbType.Time; - typeMap[typeof(byte[])] = DbType.Binary; - typeMap[typeof(byte?)] = DbType.Byte; - typeMap[typeof(sbyte?)] = DbType.SByte; - typeMap[typeof(short?)] = DbType.Int16; - typeMap[typeof(ushort?)] = DbType.UInt16; - typeMap[typeof(int?)] = DbType.Int32; - typeMap[typeof(uint?)] = DbType.UInt32; - typeMap[typeof(long?)] = DbType.Int64; - typeMap[typeof(ulong?)] = DbType.UInt64; - typeMap[typeof(float?)] = DbType.Single; - typeMap[typeof(double?)] = DbType.Double; - typeMap[typeof(decimal?)] = DbType.Decimal; - typeMap[typeof(bool?)] = DbType.Boolean; - typeMap[typeof(char?)] = DbType.StringFixedLength; - typeMap[typeof(Guid?)] = DbType.Guid; - typeMap[typeof(DateTime?)] = DbType.DateTime; - typeMap[typeof(DateTimeOffset?)] = DbType.DateTimeOffset; - typeMap[typeof(TimeSpan?)] = DbType.Time; - typeMap[typeof(Object)] = DbType.Object; - } - /// - /// Configire the specified type to be mapped to a given db-type - /// - public static void AddTypeMap(Type type, DbType dbType) - { - typeMap[type] = dbType; - } - - internal const string LinqBinary = "System.Data.Linq.Binary"; - internal static DbType LookupDbType(Type type, string name) - { - DbType dbType; - var nullUnderlyingType = Nullable.GetUnderlyingType(type); - if (nullUnderlyingType != null) type = nullUnderlyingType; - if (type.IsEnum && !typeMap.ContainsKey(type)) - { - type = Enum.GetUnderlyingType(type); - } - if (typeMap.TryGetValue(type, out dbType)) - { - return dbType; - } - if (type.FullName == LinqBinary) - { - return DbType.Binary; - } - if (typeof(IEnumerable).IsAssignableFrom(type)) - { - return DynamicParameters.EnumerableMultiParameter; - } - - - throw new NotSupportedException(string.Format("The member {0} of type {1} cannot be used as a parameter value", name, type)); - } - - - /// - /// Identity of a cached query in Dapper, used for extensability - /// - public partial class Identity : IEquatable - { - internal Identity ForGrid(Type primaryType, int gridIndex) - { - return new Identity(sql, commandType, connectionString, primaryType, parametersType, null, gridIndex); - } - - internal Identity ForGrid(Type primaryType, Type[] otherTypes, int gridIndex) - { - return new Identity(sql, commandType, connectionString, primaryType, parametersType, otherTypes, gridIndex); - } - /// - /// Create an identity for use with DynamicParameters, internal use only - /// - /// - /// - public Identity ForDynamicParameters(Type type) - { - return new Identity(sql, commandType, connectionString, this.type, type, null, -1); - } - - internal Identity(string sql, CommandType? commandType, IDbConnection connection, Type type, Type parametersType, Type[] otherTypes) - : this(sql, commandType, connection.ConnectionString, type, parametersType, otherTypes, 0) - { } - private Identity(string sql, CommandType? commandType, string connectionString, Type type, Type parametersType, Type[] otherTypes, int gridIndex) - { - this.sql = sql; - this.commandType = commandType; - this.connectionString = connectionString; - this.type = type; - this.parametersType = parametersType; - this.gridIndex = gridIndex; - unchecked - { - hashCode = 17; // we *know* we are using this in a dictionary, so pre-compute this - hashCode = hashCode * 23 + commandType.GetHashCode(); - hashCode = hashCode * 23 + gridIndex.GetHashCode(); - hashCode = hashCode * 23 + (sql == null ? 0 : sql.GetHashCode()); - hashCode = hashCode * 23 + (type == null ? 0 : type.GetHashCode()); - if (otherTypes != null) - { - foreach (var t in otherTypes) - { - hashCode = hashCode * 23 + (t == null ? 0 : t.GetHashCode()); - } - } - hashCode = hashCode * 23 + (connectionString == null ? 0 : connectionString.GetHashCode()); - hashCode = hashCode * 23 + (parametersType == null ? 0 : parametersType.GetHashCode()); - } - } - - /// - /// - /// - /// - /// - public override bool Equals(object obj) - { - return Equals(obj as Identity); - } - /// - /// The sql - /// - public readonly string sql; - /// - /// The command type - /// - public readonly CommandType? commandType; - - /// - /// - /// - public readonly int hashCode, gridIndex; - /// - /// - /// - public readonly Type type; - /// - /// - /// - public readonly string connectionString; - /// - /// - /// - public readonly Type parametersType; - /// - /// - /// - /// - public override int GetHashCode() - { - return hashCode; - } - /// - /// Compare 2 Identity objects - /// - /// - /// - public bool Equals(Identity other) - { - return - other != null && - gridIndex == other.gridIndex && - type == other.type && - sql == other.sql && - commandType == other.commandType && - connectionString == other.connectionString && - parametersType == other.parametersType; - } - } + static readonly Dictionary typeMap; + + static SqlMapper() + { + typeMap = new Dictionary(); + typeMap[typeof(byte)] = DbType.Byte; + typeMap[typeof(sbyte)] = DbType.SByte; + typeMap[typeof(short)] = DbType.Int16; + typeMap[typeof(ushort)] = DbType.UInt16; + typeMap[typeof(int)] = DbType.Int32; + typeMap[typeof(uint)] = DbType.UInt32; + typeMap[typeof(long)] = DbType.Int64; + typeMap[typeof(ulong)] = DbType.UInt64; + typeMap[typeof(float)] = DbType.Single; + typeMap[typeof(double)] = DbType.Double; + typeMap[typeof(decimal)] = DbType.Decimal; + typeMap[typeof(bool)] = DbType.Boolean; + typeMap[typeof(string)] = DbType.String; + typeMap[typeof(char)] = DbType.StringFixedLength; + typeMap[typeof(Guid)] = DbType.Guid; + typeMap[typeof(DateTime)] = DbType.DateTime; + typeMap[typeof(DateTimeOffset)] = DbType.DateTimeOffset; + typeMap[typeof(TimeSpan)] = DbType.Time; + typeMap[typeof(byte[])] = DbType.Binary; + typeMap[typeof(byte?)] = DbType.Byte; + typeMap[typeof(sbyte?)] = DbType.SByte; + typeMap[typeof(short?)] = DbType.Int16; + typeMap[typeof(ushort?)] = DbType.UInt16; + typeMap[typeof(int?)] = DbType.Int32; + typeMap[typeof(uint?)] = DbType.UInt32; + typeMap[typeof(long?)] = DbType.Int64; + typeMap[typeof(ulong?)] = DbType.UInt64; + typeMap[typeof(float?)] = DbType.Single; + typeMap[typeof(double?)] = DbType.Double; + typeMap[typeof(decimal?)] = DbType.Decimal; + typeMap[typeof(bool?)] = DbType.Boolean; + typeMap[typeof(char?)] = DbType.StringFixedLength; + typeMap[typeof(Guid?)] = DbType.Guid; + typeMap[typeof(DateTime?)] = DbType.DateTime; + typeMap[typeof(DateTimeOffset?)] = DbType.DateTimeOffset; + typeMap[typeof(TimeSpan?)] = DbType.Time; + typeMap[typeof(Object)] = DbType.Object; + } + /// + /// Configire the specified type to be mapped to a given db-type + /// + public static void AddTypeMap(Type type, DbType dbType) + { + typeMap[type] = dbType; + } + + internal const string LinqBinary = "System.Data.Linq.Binary"; + internal static DbType LookupDbType(Type type, string name) + { + DbType dbType; + var nullUnderlyingType = Nullable.GetUnderlyingType(type); + if (nullUnderlyingType != null) type = nullUnderlyingType; + if (type.IsEnum && !typeMap.ContainsKey(type)) + { + type = Enum.GetUnderlyingType(type); + } + if (typeMap.TryGetValue(type, out dbType)) + { + return dbType; + } + if (type.FullName == LinqBinary) + { + return DbType.Binary; + } + if (typeof(IEnumerable).IsAssignableFrom(type)) + { + return DynamicParameters.EnumerableMultiParameter; + } + + + throw new NotSupportedException(string.Format("The member {0} of type {1} cannot be used as a parameter value", name, type)); + } + + + /// + /// Identity of a cached query in Dapper, used for extensability + /// + public partial class Identity : IEquatable + { + internal Identity ForGrid(Type primaryType, int gridIndex) + { + return new Identity(sql, commandType, connectionString, primaryType, parametersType, null, gridIndex); + } + + internal Identity ForGrid(Type primaryType, Type[] otherTypes, int gridIndex) + { + return new Identity(sql, commandType, connectionString, primaryType, parametersType, otherTypes, gridIndex); + } + /// + /// Create an identity for use with DynamicParameters, internal use only + /// + /// + /// + public Identity ForDynamicParameters(Type type) + { + return new Identity(sql, commandType, connectionString, this.type, type, null, -1); + } + + internal Identity(string sql, CommandType? commandType, IDbConnection connection, Type type, Type parametersType, Type[] otherTypes) + : this(sql, commandType, connection.ConnectionString, type, parametersType, otherTypes, 0) + { } + private Identity(string sql, CommandType? commandType, string connectionString, Type type, Type parametersType, Type[] otherTypes, int gridIndex) + { + this.sql = sql; + this.commandType = commandType; + this.connectionString = connectionString; + this.type = type; + this.parametersType = parametersType; + this.gridIndex = gridIndex; + unchecked + { + hashCode = 17; // we *know* we are using this in a dictionary, so pre-compute this + hashCode = hashCode * 23 + commandType.GetHashCode(); + hashCode = hashCode * 23 + gridIndex.GetHashCode(); + hashCode = hashCode * 23 + (sql == null ? 0 : sql.GetHashCode()); + hashCode = hashCode * 23 + (type == null ? 0 : type.GetHashCode()); + if (otherTypes != null) + { + foreach (var t in otherTypes) + { + hashCode = hashCode * 23 + (t == null ? 0 : t.GetHashCode()); + } + } + hashCode = hashCode * 23 + (connectionString == null ? 0 : connectionString.GetHashCode()); + hashCode = hashCode * 23 + (parametersType == null ? 0 : parametersType.GetHashCode()); + } + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + return Equals(obj as Identity); + } + /// + /// The sql + /// + public readonly string sql; + /// + /// The command type + /// + public readonly CommandType? commandType; + + /// + /// + /// + public readonly int hashCode, gridIndex; + /// + /// + /// + public readonly Type type; + /// + /// + /// + public readonly string connectionString; + /// + /// + /// + public readonly Type parametersType; + /// + /// + /// + /// + public override int GetHashCode() + { + return hashCode; + } + /// + /// Compare 2 Identity objects + /// + /// + /// + public bool Equals(Identity other) + { + return + other != null && + gridIndex == other.gridIndex && + type == other.type && + sql == other.sql && + commandType == other.commandType && + connectionString == other.connectionString && + parametersType == other.parametersType; + } + } #if CSHARP30 - /// - /// Execute parameterized SQL - /// - /// Number of rows affected - public static int Execute(this IDbConnection cnn, string sql, object param) - { - return Execute(cnn, sql, param, null, null, null); - } - - /// - /// Execute parameterized SQL - /// - /// Number of rows affected - public static int Execute(this IDbConnection cnn, string sql, object param, IDbTransaction transaction) - { - return Execute(cnn, sql, param, transaction, null, null); - } - - /// - /// Execute parameterized SQL - /// - /// Number of rows affected - public static int Execute(this IDbConnection cnn, string sql, object param, CommandType commandType) - { - return Execute(cnn, sql, param, null, null, commandType); - } - - /// - /// Execute parameterized SQL - /// - /// Number of rows affected - public static int Execute(this IDbConnection cnn, string sql, object param, IDbTransaction transaction, CommandType commandType) - { - return Execute(cnn, sql, param, transaction, null, commandType); - } - - /// - /// Executes a query, returning the data typed as per T - /// - /// A sequence of data of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column in assumed, otherwise an instance is - /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). - /// - public static IEnumerable Query(this IDbConnection cnn, string sql, object param) - { - return Query(cnn, sql, param, null, true, null, null); - } - - /// - /// Executes a query, returning the data typed as per T - /// - /// A sequence of data of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column in assumed, otherwise an instance is - /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). - /// - public static IEnumerable Query(this IDbConnection cnn, string sql, object param, IDbTransaction transaction) - { - return Query(cnn, sql, param, transaction, true, null, null); - } - - /// - /// Executes a query, returning the data typed as per T - /// - /// A sequence of data of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column in assumed, otherwise an instance is - /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). - /// - public static IEnumerable Query(this IDbConnection cnn, string sql, object param, CommandType commandType) - { - return Query(cnn, sql, param, null, true, null, commandType); - } - - /// - /// Executes a query, returning the data typed as per T - /// - /// A sequence of data of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column in assumed, otherwise an instance is - /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). - /// - public static IEnumerable Query(this IDbConnection cnn, string sql, object param, IDbTransaction transaction, CommandType commandType) - { - return Query(cnn, sql, param, transaction, true, null, commandType); - } - - /// - /// Execute a command that returns multiple result sets, and access each in turn - /// - public static GridReader QueryMultiple(this IDbConnection cnn, string sql, object param, IDbTransaction transaction) - { - return QueryMultiple(cnn, sql, param, transaction, null, null); - } - - /// - /// Execute a command that returns multiple result sets, and access each in turn - /// - public static GridReader QueryMultiple(this IDbConnection cnn, string sql, object param, CommandType commandType) - { - return QueryMultiple(cnn, sql, param, null, null, commandType); - } - - /// - /// Execute a command that returns multiple result sets, and access each in turn - /// - public static GridReader QueryMultiple(this IDbConnection cnn, string sql, object param, IDbTransaction transaction, CommandType commandType) - { - return QueryMultiple(cnn, sql, param, transaction, null, commandType); - } + /// + /// Execute parameterized SQL + /// + /// Number of rows affected + public static int Execute(this IDbConnection cnn, string sql, object param) + { + return Execute(cnn, sql, param, null, null, null); + } + + /// + /// Execute parameterized SQL + /// + /// Number of rows affected + public static int Execute(this IDbConnection cnn, string sql, object param, IDbTransaction transaction) + { + return Execute(cnn, sql, param, transaction, null, null); + } + + /// + /// Execute parameterized SQL + /// + /// Number of rows affected + public static int Execute(this IDbConnection cnn, string sql, object param, CommandType commandType) + { + return Execute(cnn, sql, param, null, null, commandType); + } + + /// + /// Execute parameterized SQL + /// + /// Number of rows affected + public static int Execute(this IDbConnection cnn, string sql, object param, IDbTransaction transaction, CommandType commandType) + { + return Execute(cnn, sql, param, transaction, null, commandType); + } + + /// + /// Executes a query, returning the data typed as per T + /// + /// A sequence of data of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column in assumed, otherwise an instance is + /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). + /// + public static IEnumerable Query(this IDbConnection cnn, string sql, object param) + { + return Query(cnn, sql, param, null, true, null, null); + } + + /// + /// Executes a query, returning the data typed as per T + /// + /// A sequence of data of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column in assumed, otherwise an instance is + /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). + /// + public static IEnumerable Query(this IDbConnection cnn, string sql, object param, IDbTransaction transaction) + { + return Query(cnn, sql, param, transaction, true, null, null); + } + + /// + /// Executes a query, returning the data typed as per T + /// + /// A sequence of data of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column in assumed, otherwise an instance is + /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). + /// + public static IEnumerable Query(this IDbConnection cnn, string sql, object param, CommandType commandType) + { + return Query(cnn, sql, param, null, true, null, commandType); + } + + /// + /// Executes a query, returning the data typed as per T + /// + /// A sequence of data of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column in assumed, otherwise an instance is + /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). + /// + public static IEnumerable Query(this IDbConnection cnn, string sql, object param, IDbTransaction transaction, CommandType commandType) + { + return Query(cnn, sql, param, transaction, true, null, commandType); + } + + /// + /// Execute a command that returns multiple result sets, and access each in turn + /// + public static GridReader QueryMultiple(this IDbConnection cnn, string sql, object param, IDbTransaction transaction) + { + return QueryMultiple(cnn, sql, param, transaction, null, null); + } + + /// + /// Execute a command that returns multiple result sets, and access each in turn + /// + public static GridReader QueryMultiple(this IDbConnection cnn, string sql, object param, CommandType commandType) + { + return QueryMultiple(cnn, sql, param, null, null, commandType); + } + + /// + /// Execute a command that returns multiple result sets, and access each in turn + /// + public static GridReader QueryMultiple(this IDbConnection cnn, string sql, object param, IDbTransaction transaction, CommandType commandType) + { + return QueryMultiple(cnn, sql, param, transaction, null, commandType); + } #endif - /// - /// Execute parameterized SQL - /// - /// Number of rows affected - public static int Execute( + /// + /// Execute parameterized SQL + /// + /// Number of rows affected + public static int Execute( #if CSHARP30 - this IDbConnection cnn, string sql, object param, IDbTransaction transaction, int? commandTimeout, CommandType? commandType + this IDbConnection cnn, string sql, object param, IDbTransaction transaction, int? commandTimeout, CommandType? commandType #else this IDbConnection cnn, string sql, dynamic param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null #endif ) - { - IEnumerable multiExec = (object)param as IEnumerable; - Identity identity; - CacheInfo info = null; - if (multiExec != null && !(multiExec is string)) - { - bool isFirst = true; - int total = 0; - using (var cmd = SetupCommand(cnn, transaction, sql, null, null, commandTimeout, commandType)) - { - - string masterSql = null; - foreach (var obj in multiExec) - { - if (isFirst) - { - masterSql = cmd.CommandText; - isFirst = false; - identity = new Identity(sql, cmd.CommandType, cnn, null, obj.GetType(), null); - info = GetCacheInfo(identity); - } - else - { - cmd.CommandText = masterSql; // because we do magic replaces on "in" etc - cmd.Parameters.Clear(); // current code is Add-tastic - } - info.ParamReader(cmd, obj); - total += cmd.ExecuteNonQuery(); - } - } - return total; - } - - // nice and simple - if ((object)param != null) - { - identity = new Identity(sql, commandType, cnn, null, (object)param == null ? null : ((object)param).GetType(), null); - info = GetCacheInfo(identity); - } - return ExecuteCommand(cnn, transaction, sql, (object)param == null ? null : info.ParamReader, (object)param, commandTimeout, commandType); - } + { + IEnumerable multiExec = (object)param as IEnumerable; + Identity identity; + CacheInfo info = null; + if (multiExec != null && !(multiExec is string)) + { + bool isFirst = true; + int total = 0; + using (var cmd = SetupCommand(cnn, transaction, sql, null, null, commandTimeout, commandType)) + { + + string masterSql = null; + foreach (var obj in multiExec) + { + if (isFirst) + { + masterSql = cmd.CommandText; + isFirst = false; + identity = new Identity(sql, cmd.CommandType, cnn, null, obj.GetType(), null); + info = GetCacheInfo(identity); + } + else + { + cmd.CommandText = masterSql; // because we do magic replaces on "in" etc + cmd.Parameters.Clear(); // current code is Add-tastic + } + info.ParamReader(cmd, obj); + total += cmd.ExecuteNonQuery(); + } + } + return total; + } + + // nice and simple + if ((object)param != null) + { + identity = new Identity(sql, commandType, cnn, null, (object)param == null ? null : ((object)param).GetType(), null); + info = GetCacheInfo(identity); + } + return ExecuteCommand(cnn, transaction, sql, (object)param == null ? null : info.ParamReader, (object)param, commandTimeout, commandType); + } #if !CSHARP30 - /// - /// Return a list of dynamic objects, reader is closed after the call - /// - public static IEnumerable Query(this IDbConnection cnn, string sql, dynamic param = null, IDbTransaction transaction = null, bool buffered = true, int? commandTimeout = null, CommandType? commandType = null) - { - return Query(cnn, sql, param as object, transaction, buffered, commandTimeout, commandType); - } + /// + /// Return a list of dynamic objects, reader is closed after the call + /// + public static IEnumerable Query(this IDbConnection cnn, string sql, dynamic param = null, IDbTransaction transaction = null, bool buffered = true, int? commandTimeout = null, CommandType? commandType = null) + { + return Query(cnn, sql, param as object, transaction, buffered, commandTimeout, commandType); + } #else - /// - /// Return a list of dynamic objects, reader is closed after the call - /// - public static IEnumerable> Query(this IDbConnection cnn, string sql, object param) { - return Query(cnn, sql, param, null, true, null, null); - } - - /// - /// Return a list of dynamic objects, reader is closed after the call - /// - public static IEnumerable> Query(this IDbConnection cnn, string sql, object param, IDbTransaction transaction) { - return Query(cnn, sql, param, transaction, true, null, null); - } - - /// - /// Return a list of dynamic objects, reader is closed after the call - /// - public static IEnumerable> Query(this IDbConnection cnn, string sql, object param, CommandType? commandType) { - return Query(cnn, sql, param, null, true, null, commandType); - } - - /// - /// Return a list of dynamic objects, reader is closed after the call - /// - public static IEnumerable> Query(this IDbConnection cnn, string sql, object param, IDbTransaction transaction, CommandType? commandType) { - return Query(cnn, sql, param, transaction, true, null, commandType); - } - - /// - /// Return a list of dynamic objects, reader is closed after the call - /// - public static IEnumerable> Query(this IDbConnection cnn, string sql, object param, IDbTransaction transaction, bool buffered, int? commandTimeout, CommandType? commandType) { - return Query>(cnn, sql, param, transaction, buffered, commandTimeout, commandType); - } + /// + /// Return a list of dynamic objects, reader is closed after the call + /// + public static IEnumerable> Query(this IDbConnection cnn, string sql, object param) { + return Query(cnn, sql, param, null, true, null, null); + } + + /// + /// Return a list of dynamic objects, reader is closed after the call + /// + public static IEnumerable> Query(this IDbConnection cnn, string sql, object param, IDbTransaction transaction) { + return Query(cnn, sql, param, transaction, true, null, null); + } + + /// + /// Return a list of dynamic objects, reader is closed after the call + /// + public static IEnumerable> Query(this IDbConnection cnn, string sql, object param, CommandType? commandType) { + return Query(cnn, sql, param, null, true, null, commandType); + } + + /// + /// Return a list of dynamic objects, reader is closed after the call + /// + public static IEnumerable> Query(this IDbConnection cnn, string sql, object param, IDbTransaction transaction, CommandType? commandType) { + return Query(cnn, sql, param, transaction, true, null, commandType); + } + + /// + /// Return a list of dynamic objects, reader is closed after the call + /// + public static IEnumerable> Query(this IDbConnection cnn, string sql, object param, IDbTransaction transaction, bool buffered, int? commandTimeout, CommandType? commandType) { + return Query>(cnn, sql, param, transaction, buffered, commandTimeout, commandType); + } #endif - /// - /// Executes a query, returning the data typed as per T - /// - /// the dynamic param may seem a bit odd, but this works around a major usability issue in vs, if it is Object vs completion gets annoying. Eg type new [space] get new object - /// A sequence of data of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column in assumed, otherwise an instance is - /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). - /// - public static IEnumerable Query( + /// + /// Executes a query, returning the data typed as per T + /// + /// the dynamic param may seem a bit odd, but this works around a major usability issue in vs, if it is Object vs completion gets annoying. Eg type new [space] get new object + /// A sequence of data of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column in assumed, otherwise an instance is + /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). + /// + public static IEnumerable Query( #if CSHARP30 - this IDbConnection cnn, string sql, object param, IDbTransaction transaction, bool buffered, int? commandTimeout, CommandType? commandType + this IDbConnection cnn, string sql, object param, IDbTransaction transaction, bool buffered, int? commandTimeout, CommandType? commandType #else this IDbConnection cnn, string sql, dynamic param = null, IDbTransaction transaction = null, bool buffered = true, int? commandTimeout = null, CommandType? commandType = null #endif ) - { - var data = QueryInternal(cnn, sql, param as object, transaction, commandTimeout, commandType); - return buffered ? data.ToList() : data; - } - - /// - /// Execute a command that returns multiple result sets, and access each in turn - /// - public static GridReader QueryMultiple( + { + var data = QueryInternal(cnn, sql, param as object, transaction, commandTimeout, commandType); + return buffered ? data.ToList() : data; + } + + /// + /// Execute a command that returns multiple result sets, and access each in turn + /// + public static GridReader QueryMultiple( #if CSHARP30 - this IDbConnection cnn, string sql, object param, IDbTransaction transaction, int? commandTimeout, CommandType? commandType + this IDbConnection cnn, string sql, object param, IDbTransaction transaction, int? commandTimeout, CommandType? commandType #else - this IDbConnection cnn, string sql, dynamic param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null + this IDbConnection cnn, string sql, dynamic param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null #endif ) - { - Identity identity = new Identity(sql, commandType, cnn, typeof(GridReader), (object)param == null ? null : ((object)param).GetType(), null); - CacheInfo info = GetCacheInfo(identity); - - IDbCommand cmd = null; - IDataReader reader = null; - bool wasClosed = cnn.State == ConnectionState.Closed; - try - { - if (wasClosed) cnn.Open(); - cmd = SetupCommand(cnn, transaction, sql, info.ParamReader, (object)param, commandTimeout, commandType); - reader = cmd.ExecuteReader(wasClosed ? CommandBehavior.CloseConnection : CommandBehavior.Default); - - var result = new GridReader(cmd, reader, identity); - wasClosed = false; // *if* the connection was closed and we got this far, then we now have a reader - // with the CloseConnection flag, so the reader will deal with the connection; we - // still need something in the "finally" to ensure that broken SQL still results - // in the connection closing itself - return result; - } - catch - { - if (reader != null) - { - if (!reader.IsClosed) try { cmd.Cancel(); } - catch { /* don't spoil the existing exception */ } - reader.Dispose(); - } - if (cmd != null) cmd.Dispose(); - if (wasClosed) cnn.Close(); - throw; - } - } - - /// - /// Return a typed list of objects, reader is closed after the call - /// - private static IEnumerable QueryInternal(this IDbConnection cnn, string sql, object param, IDbTransaction transaction, int? commandTimeout, CommandType? commandType) - { - var identity = new Identity(sql, commandType, cnn, typeof(T), param == null ? null : param.GetType(), null); - var info = GetCacheInfo(identity); - - IDbCommand cmd = null; - IDataReader reader = null; - - bool wasClosed = cnn.State == ConnectionState.Closed; - try - { - cmd = SetupCommand(cnn, transaction, sql, info.ParamReader, param, commandTimeout, commandType); - - if (wasClosed) cnn.Open(); - reader = cmd.ExecuteReader(wasClosed ? CommandBehavior.CloseConnection : CommandBehavior.Default); - wasClosed = false; // *if* the connection was closed and we got this far, then we now have a reader - // with the CloseConnection flag, so the reader will deal with the connection; we - // still need something in the "finally" to ensure that broken SQL still results - // in the connection closing itself - var tuple = info.Deserializer; - int hash = GetColumnHash(reader); - if (tuple.Func == null || tuple.Hash != hash) - { - tuple = info.Deserializer = new DeserializerState(hash, GetDeserializer(typeof(T), reader, 0, -1, false)); - SetQueryCache(identity, info); - } - - var func = tuple.Func; - - while (reader.Read()) - { - yield return (T)func(reader); - } - // happy path; close the reader cleanly - no - // need for "Cancel" etc - reader.Dispose(); - reader = null; - } - finally - { - if (reader != null) - { - if (!reader.IsClosed) try { cmd.Cancel(); } - catch { /* don't spoil the existing exception */ } - reader.Dispose(); - } - if (wasClosed) cnn.Close(); - if (cmd != null) cmd.Dispose(); - } - } - - /// - /// Maps a query to objects - /// - /// The first type in the recordset - /// The second type in the recordset - /// The return type - /// - /// - /// - /// - /// - /// - /// The Field we should split and read the second object from (default: id) - /// Number of seconds before command execution timeout - /// Is it a stored proc or a batch? - /// - public static IEnumerable Query( + { + Identity identity = new Identity(sql, commandType, cnn, typeof(GridReader), (object)param == null ? null : ((object)param).GetType(), null); + CacheInfo info = GetCacheInfo(identity); + + IDbCommand cmd = null; + IDataReader reader = null; + bool wasClosed = cnn.State == ConnectionState.Closed; + try + { + if (wasClosed) cnn.Open(); + cmd = SetupCommand(cnn, transaction, sql, info.ParamReader, (object)param, commandTimeout, commandType); + reader = cmd.ExecuteReader(wasClosed ? CommandBehavior.CloseConnection : CommandBehavior.Default); + + var result = new GridReader(cmd, reader, identity); + wasClosed = false; // *if* the connection was closed and we got this far, then we now have a reader + // with the CloseConnection flag, so the reader will deal with the connection; we + // still need something in the "finally" to ensure that broken SQL still results + // in the connection closing itself + return result; + } + catch + { + if (reader != null) + { + if (!reader.IsClosed) try { cmd.Cancel(); } + catch { /* don't spoil the existing exception */ } + reader.Dispose(); + } + if (cmd != null) cmd.Dispose(); + if (wasClosed) cnn.Close(); + throw; + } + } + + /// + /// Return a typed list of objects, reader is closed after the call + /// + private static IEnumerable QueryInternal(this IDbConnection cnn, string sql, object param, IDbTransaction transaction, int? commandTimeout, CommandType? commandType) + { + var identity = new Identity(sql, commandType, cnn, typeof(T), param == null ? null : param.GetType(), null); + var info = GetCacheInfo(identity); + + IDbCommand cmd = null; + IDataReader reader = null; + + bool wasClosed = cnn.State == ConnectionState.Closed; + try + { + cmd = SetupCommand(cnn, transaction, sql, info.ParamReader, param, commandTimeout, commandType); + + if (wasClosed) cnn.Open(); + reader = cmd.ExecuteReader(wasClosed ? CommandBehavior.CloseConnection : CommandBehavior.Default); + wasClosed = false; // *if* the connection was closed and we got this far, then we now have a reader + // with the CloseConnection flag, so the reader will deal with the connection; we + // still need something in the "finally" to ensure that broken SQL still results + // in the connection closing itself + var tuple = info.Deserializer; + int hash = GetColumnHash(reader); + if (tuple.Func == null || tuple.Hash != hash) + { + tuple = info.Deserializer = new DeserializerState(hash, GetDeserializer(typeof(T), reader, 0, -1, false)); + SetQueryCache(identity, info); + } + + var func = tuple.Func; + + while (reader.Read()) + { + yield return (T)func(reader); + } + // happy path; close the reader cleanly - no + // need for "Cancel" etc + reader.Dispose(); + reader = null; + } + finally + { + if (reader != null) + { + if (!reader.IsClosed) try { cmd.Cancel(); } + catch { /* don't spoil the existing exception */ } + reader.Dispose(); + } + if (wasClosed) cnn.Close(); + if (cmd != null) cmd.Dispose(); + } + } + + /// + /// Maps a query to objects + /// + /// The first type in the recordset + /// The second type in the recordset + /// The return type + /// + /// + /// + /// + /// + /// + /// The Field we should split and read the second object from (default: id) + /// Number of seconds before command execution timeout + /// Is it a stored proc or a batch? + /// + public static IEnumerable Query( #if CSHARP30 - this IDbConnection cnn, string sql, Func map, object param, IDbTransaction transaction, bool buffered, string splitOn, int? commandTimeout, CommandType? commandType + this IDbConnection cnn, string sql, Func map, object param, IDbTransaction transaction, bool buffered, string splitOn, int? commandTimeout, CommandType? commandType #else this IDbConnection cnn, string sql, Func map, dynamic param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null #endif ) - { - return MultiMap(cnn, sql, map, param as object, transaction, buffered, splitOn, commandTimeout, commandType); - } - - /// - /// Maps a query to objects - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// The Field we should split and read the second object from (default: id) - /// Number of seconds before command execution timeout - /// - /// - public static IEnumerable Query( + { + return MultiMap(cnn, sql, map, param as object, transaction, buffered, splitOn, commandTimeout, commandType); + } + + /// + /// Maps a query to objects + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// The Field we should split and read the second object from (default: id) + /// Number of seconds before command execution timeout + /// + /// + public static IEnumerable Query( #if CSHARP30 - this IDbConnection cnn, string sql, Func map, object param, IDbTransaction transaction, bool buffered, string splitOn, int? commandTimeout, CommandType? commandType + this IDbConnection cnn, string sql, Func map, object param, IDbTransaction transaction, bool buffered, string splitOn, int? commandTimeout, CommandType? commandType #else this IDbConnection cnn, string sql, Func map, dynamic param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null #endif ) - { - return MultiMap(cnn, sql, map, param as object, transaction, buffered, splitOn, commandTimeout, commandType); - } - - /// - /// Perform a multi mapping query with 4 input parameters - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public static IEnumerable Query( + { + return MultiMap(cnn, sql, map, param as object, transaction, buffered, splitOn, commandTimeout, commandType); + } + + /// + /// Perform a multi mapping query with 4 input parameters + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static IEnumerable Query( #if CSHARP30 - this IDbConnection cnn, string sql, Func map, object param, IDbTransaction transaction, bool buffered, string splitOn, int? commandTimeout, CommandType? commandType + this IDbConnection cnn, string sql, Func map, object param, IDbTransaction transaction, bool buffered, string splitOn, int? commandTimeout, CommandType? commandType #else this IDbConnection cnn, string sql, Func map, dynamic param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null #endif ) - { - return MultiMap(cnn, sql, map, param as object, transaction, buffered, splitOn, commandTimeout, commandType); - } + { + return MultiMap(cnn, sql, map, param as object, transaction, buffered, splitOn, commandTimeout, commandType); + } #if !CSHARP30 - /// - /// Perform a multi mapping query with 5 input parameters - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public static IEnumerable Query(this IDbConnection cnn, string sql, Func map, dynamic param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) - { - return MultiMap(cnn, sql, map, param as object, transaction, buffered, splitOn, commandTimeout, commandType); - } + /// + /// Perform a multi mapping query with 5 input parameters + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static IEnumerable Query(this IDbConnection cnn, string sql, Func map, dynamic param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) + { + return MultiMap(cnn, sql, map, param as object, transaction, buffered, splitOn, commandTimeout, commandType); + } #endif - partial class DontMap { } - static IEnumerable MultiMap( - this IDbConnection cnn, string sql, object map, object param, IDbTransaction transaction, bool buffered, string splitOn, int? commandTimeout, CommandType? commandType) - { - var results = MultiMapImpl(cnn, sql, map, param, transaction, splitOn, commandTimeout, commandType, null, null); - return buffered ? results.ToList() : results; - } - - - static IEnumerable MultiMapImpl(this IDbConnection cnn, string sql, object map, object param, IDbTransaction transaction, string splitOn, int? commandTimeout, CommandType? commandType, IDataReader reader, Identity identity) - { - identity = identity ?? new Identity(sql, commandType, cnn, typeof(TFirst), (object)param == null ? null : ((object)param).GetType(), new[] { typeof(TFirst), typeof(TSecond), typeof(TThird), typeof(TFourth), typeof(TFifth) }); - CacheInfo cinfo = GetCacheInfo(identity); - - IDbCommand ownedCommand = null; - IDataReader ownedReader = null; - - try - { - if (reader == null) - { - ownedCommand = SetupCommand(cnn, transaction, sql, cinfo.ParamReader, (object)param, commandTimeout, commandType); - ownedReader = ownedCommand.ExecuteReader(); - reader = ownedReader; - } - DeserializerState deserializer = default(DeserializerState); - Func[] otherDeserializers = null; - - int hash = GetColumnHash(reader); - if ((deserializer = cinfo.Deserializer).Func == null || (otherDeserializers = cinfo.OtherDeserializers) == null || hash != deserializer.Hash) - { - var deserializers = GenerateDeserializers(new Type[] { typeof(TFirst), typeof(TSecond), typeof(TThird), typeof(TFourth), typeof(TFifth) }, splitOn, reader); - deserializer = cinfo.Deserializer = new DeserializerState(hash, deserializers[0]); - otherDeserializers = cinfo.OtherDeserializers = deserializers.Skip(1).ToArray(); - SetQueryCache(identity, cinfo); - } - - Func mapIt = GenerateMapper(deserializer.Func, otherDeserializers, map); - - if (mapIt != null) - { - while (reader.Read()) - { - yield return mapIt(reader); - } - } - } - finally - { - try - { - if (ownedReader != null) - { - ownedReader.Dispose(); - } - } - finally - { - if (ownedCommand != null) - { - ownedCommand.Dispose(); - } - } - } - } - - private static Func GenerateMapper(Func deserializer, Func[] otherDeserializers, object map) - { - switch (otherDeserializers.Length) - { - case 1: - return r => ((Func)map)((TFirst)deserializer(r), (TSecond)otherDeserializers[0](r)); - case 2: - return r => ((Func)map)((TFirst)deserializer(r), (TSecond)otherDeserializers[0](r), (TThird)otherDeserializers[1](r)); - case 3: - return r => ((Func)map)((TFirst)deserializer(r), (TSecond)otherDeserializers[0](r), (TThird)otherDeserializers[1](r), (TFourth)otherDeserializers[2](r)); + partial class DontMap { } + static IEnumerable MultiMap( + this IDbConnection cnn, string sql, object map, object param, IDbTransaction transaction, bool buffered, string splitOn, int? commandTimeout, CommandType? commandType) + { + var results = MultiMapImpl(cnn, sql, map, param, transaction, splitOn, commandTimeout, commandType, null, null); + return buffered ? results.ToList() : results; + } + + + static IEnumerable MultiMapImpl(this IDbConnection cnn, string sql, object map, object param, IDbTransaction transaction, string splitOn, int? commandTimeout, CommandType? commandType, IDataReader reader, Identity identity) + { + identity = identity ?? new Identity(sql, commandType, cnn, typeof(TFirst), (object)param == null ? null : ((object)param).GetType(), new[] { typeof(TFirst), typeof(TSecond), typeof(TThird), typeof(TFourth), typeof(TFifth) }); + CacheInfo cinfo = GetCacheInfo(identity); + + IDbCommand ownedCommand = null; + IDataReader ownedReader = null; + + try + { + if (reader == null) + { + ownedCommand = SetupCommand(cnn, transaction, sql, cinfo.ParamReader, (object)param, commandTimeout, commandType); + ownedReader = ownedCommand.ExecuteReader(); + reader = ownedReader; + } + DeserializerState deserializer = default(DeserializerState); + Func[] otherDeserializers = null; + + int hash = GetColumnHash(reader); + if ((deserializer = cinfo.Deserializer).Func == null || (otherDeserializers = cinfo.OtherDeserializers) == null || hash != deserializer.Hash) + { + var deserializers = GenerateDeserializers(new Type[] { typeof(TFirst), typeof(TSecond), typeof(TThird), typeof(TFourth), typeof(TFifth) }, splitOn, reader); + deserializer = cinfo.Deserializer = new DeserializerState(hash, deserializers[0]); + otherDeserializers = cinfo.OtherDeserializers = deserializers.Skip(1).ToArray(); + SetQueryCache(identity, cinfo); + } + + Func mapIt = GenerateMapper(deserializer.Func, otherDeserializers, map); + + if (mapIt != null) + { + while (reader.Read()) + { + yield return mapIt(reader); + } + } + } + finally + { + try + { + if (ownedReader != null) + { + ownedReader.Dispose(); + } + } + finally + { + if (ownedCommand != null) + { + ownedCommand.Dispose(); + } + } + } + } + + private static Func GenerateMapper(Func deserializer, Func[] otherDeserializers, object map) + { + switch (otherDeserializers.Length) + { + case 1: + return r => ((Func)map)((TFirst)deserializer(r), (TSecond)otherDeserializers[0](r)); + case 2: + return r => ((Func)map)((TFirst)deserializer(r), (TSecond)otherDeserializers[0](r), (TThird)otherDeserializers[1](r)); + case 3: + return r => ((Func)map)((TFirst)deserializer(r), (TSecond)otherDeserializers[0](r), (TThird)otherDeserializers[1](r), (TFourth)otherDeserializers[2](r)); #if !CSHARP30 - case 4: - return r => ((Func)map)((TFirst)deserializer(r), (TSecond)otherDeserializers[0](r), (TThird)otherDeserializers[1](r), (TFourth)otherDeserializers[2](r), (TFifth)otherDeserializers[3](r)); + case 4: + return r => ((Func)map)((TFirst)deserializer(r), (TSecond)otherDeserializers[0](r), (TThird)otherDeserializers[1](r), (TFourth)otherDeserializers[2](r), (TFifth)otherDeserializers[3](r)); #endif - default: - throw new NotSupportedException(); - } - } - - private static Func[] GenerateDeserializers(Type[] types, string splitOn, IDataReader reader) - { - int current = 0; - var splits = splitOn.Split(',').ToArray(); - var splitIndex = 0; - - Func nextSplit = type => - { - var currentSplit = splits[splitIndex].Trim(); - if (splits.Length > splitIndex + 1) - { - splitIndex++; - } - - bool skipFirst = false; - int startingPos = current + 1; - // if our current type has the split, skip the first time you see it. - if (type != typeof(Object)) - { - var props = DefaultTypeMap.GetSettableProps(type); - var fields = DefaultTypeMap.GetSettableFields(type); - - foreach (var name in props.Select(p => p.Name).Concat(fields.Select(f => f.Name))) - { - if (string.Equals(name, currentSplit, StringComparison.OrdinalIgnoreCase)) - { - skipFirst = true; - startingPos = current; - break; - } - } - - } - - int pos; - for (pos = startingPos; pos < reader.FieldCount; pos++) - { - // some people like ID some id ... assuming case insensitive splits for now - if (splitOn == "*") - { - break; - } - if (string.Equals(reader.GetName(pos), currentSplit, StringComparison.OrdinalIgnoreCase)) - { - if (skipFirst) - { - skipFirst = false; - } - else - { - break; - } - } - } - current = pos; - return pos; - }; - - var deserializers = new List>(); - int split = 0; - bool first = true; - foreach (var type in types) - { - if (type != typeof(DontMap)) - { - int next = nextSplit(type); - deserializers.Add(GetDeserializer(type, reader, split, next - split, /* returnNullIfFirstMissing: */ !first)); - first = false; - split = next; - } - } - - return deserializers.ToArray(); - } - - private static CacheInfo GetCacheInfo(Identity identity) - { - CacheInfo info; - if (!TryGetQueryCache(identity, out info)) - { - info = new CacheInfo(); - if (identity.parametersType != null) - { - if (typeof(IDynamicParameters).IsAssignableFrom(identity.parametersType)) - { - info.ParamReader = (cmd, obj) => { (obj as IDynamicParameters).AddParameters(cmd, identity); }; - } + default: + throw new NotSupportedException(); + } + } + + private static Func[] GenerateDeserializers(Type[] types, string splitOn, IDataReader reader) + { + int current = 0; + var splits = splitOn.Split(',').ToArray(); + var splitIndex = 0; + + Func nextSplit = type => + { + var currentSplit = splits[splitIndex].Trim(); + if (splits.Length > splitIndex + 1) + { + splitIndex++; + } + + bool skipFirst = false; + int startingPos = current + 1; + // if our current type has the split, skip the first time you see it. + if (type != typeof(Object)) + { + var props = DefaultTypeMap.GetSettableProps(type); + var fields = DefaultTypeMap.GetSettableFields(type); + + foreach (var name in props.Select(p => p.Name).Concat(fields.Select(f => f.Name))) + { + if (string.Equals(name, currentSplit, StringComparison.OrdinalIgnoreCase)) + { + skipFirst = true; + startingPos = current; + break; + } + } + + } + + int pos; + for (pos = startingPos; pos < reader.FieldCount; pos++) + { + // some people like ID some id ... assuming case insensitive splits for now + if (splitOn == "*") + { + break; + } + if (string.Equals(reader.GetName(pos), currentSplit, StringComparison.OrdinalIgnoreCase)) + { + if (skipFirst) + { + skipFirst = false; + } + else + { + break; + } + } + } + current = pos; + return pos; + }; + + var deserializers = new List>(); + int split = 0; + bool first = true; + foreach (var type in types) + { + if (type != typeof(DontMap)) + { + int next = nextSplit(type); + deserializers.Add(GetDeserializer(type, reader, split, next - split, /* returnNullIfFirstMissing: */ !first)); + first = false; + split = next; + } + } + + return deserializers.ToArray(); + } + + private static CacheInfo GetCacheInfo(Identity identity) + { + CacheInfo info; + if (!TryGetQueryCache(identity, out info)) + { + info = new CacheInfo(); + if (identity.parametersType != null) + { + if (typeof(IDynamicParameters).IsAssignableFrom(identity.parametersType)) + { + info.ParamReader = (cmd, obj) => { (obj as IDynamicParameters).AddParameters(cmd, identity); }; + } #if !CSHARP30 - else if (typeof(IEnumerable>).IsAssignableFrom(identity.parametersType) && typeof(System.Dynamic.IDynamicMetaObjectProvider).IsAssignableFrom(identity.parametersType)) - { - info.ParamReader = (cmd, obj) => - { - IDynamicParameters mapped = new DynamicParameters(obj); - mapped.AddParameters(cmd, identity); - }; - } + else if (typeof(IEnumerable>).IsAssignableFrom(identity.parametersType) && typeof(System.Dynamic.IDynamicMetaObjectProvider).IsAssignableFrom(identity.parametersType)) + { + info.ParamReader = (cmd, obj) => + { + IDynamicParameters mapped = new DynamicParameters(obj); + mapped.AddParameters(cmd, identity); + }; + } #endif - else - { - info.ParamReader = CreateParamInfoGenerator(identity, false); - } - } - SetQueryCache(identity, info); - } - return info; - } - - private static Func GetDeserializer(Type type, IDataReader reader, int startBound, int length, bool returnNullIfFirstMissing) - { + else + { + info.ParamReader = CreateParamInfoGenerator(identity, false); + } + } + SetQueryCache(identity, info); + } + return info; + } + + private static Func GetDeserializer(Type type, IDataReader reader, int startBound, int length, bool returnNullIfFirstMissing) + { #if !CSHARP30 - // dynamic is passed in as Object ... by c# design - if (type == typeof(object) - || type == typeof(DapperRow)) - { - return GetDapperRowDeserializer(reader, startBound, length, returnNullIfFirstMissing); - } + // dynamic is passed in as Object ... by c# design + if (type == typeof(object) + || type == typeof(DapperRow)) + { + return GetDapperRowDeserializer(reader, startBound, length, returnNullIfFirstMissing); + } #else - if(type.IsAssignableFrom(typeof(Dictionary))) - { - return GetDictionaryDeserializer(reader, startBound, length, returnNullIfFirstMissing); - } + if(type.IsAssignableFrom(typeof(Dictionary))) + { + return GetDictionaryDeserializer(reader, startBound, length, returnNullIfFirstMissing); + } #endif - Type underlyingType = null; - if (!(typeMap.ContainsKey(type) || type.IsEnum || type.FullName == LinqBinary || - (type.IsValueType && (underlyingType = Nullable.GetUnderlyingType(type)) != null && underlyingType.IsEnum))) - { - return GetTypeDeserializer(type, reader, startBound, length, returnNullIfFirstMissing); - } - return GetStructDeserializer(type, underlyingType ?? type, startBound); + Type underlyingType = null; + if (!(typeMap.ContainsKey(type) || type.IsEnum || type.FullName == LinqBinary || + (type.IsValueType && (underlyingType = Nullable.GetUnderlyingType(type)) != null && underlyingType.IsEnum))) + { + return GetTypeDeserializer(type, reader, startBound, length, returnNullIfFirstMissing); + } + return GetStructDeserializer(type, underlyingType ?? type, startBound); - } + } #if !CSHARP30 - sealed partial class DapperTable - { - string[] fieldNames; - readonly Dictionary fieldNameLookup; - - internal string[] FieldNames { get { return fieldNames; } } - - public DapperTable(string[] fieldNames) - { - if (fieldNames == null) throw new ArgumentNullException("fieldNames"); - this.fieldNames = fieldNames; - - fieldNameLookup = new Dictionary(fieldNames.Length, StringComparer.Ordinal); - // if there are dups, we want the **first** key to be the "winner" - so iterate backwards - for (int i = fieldNames.Length - 1; i >= 0; i--) - { - string key = fieldNames[i]; - if (key != null) fieldNameLookup[key] = i; - } - } - - internal int IndexOfName(string name) - { - int result; - return (name != null && fieldNameLookup.TryGetValue(name, out result)) ? result : -1; - } - internal int AddField(string name) - { - if (name == null) throw new ArgumentNullException("name"); - if (fieldNameLookup.ContainsKey(name)) throw new InvalidOperationException("Field already exists: " + name); - int oldLen = fieldNames.Length; - Array.Resize(ref fieldNames, oldLen + 1); // yes, this is sub-optimal, but this is not the expected common case - fieldNameLookup[name] = oldLen; - return oldLen; - } - - - internal bool FieldExists(string key) - { - return key != null && fieldNameLookup.ContainsKey(key); - } - - public int FieldCount { get { return fieldNames.Length; } } - } - - sealed partial class DapperRowMetaObject : System.Dynamic.DynamicMetaObject - { - static readonly MethodInfo getValueMethod = typeof(IDictionary).GetProperty("Item").GetGetMethod(); - static readonly MethodInfo setValueMethod = typeof(DapperRow).GetMethod("SetValue"); - - public DapperRowMetaObject( - System.Linq.Expressions.Expression expression, - System.Dynamic.BindingRestrictions restrictions - ) - : base(expression, restrictions) - { - } - - public DapperRowMetaObject( - System.Linq.Expressions.Expression expression, - System.Dynamic.BindingRestrictions restrictions, - object value - ) - : base(expression, restrictions, value) - { - } - - System.Dynamic.DynamicMetaObject CallMethod( - MethodInfo method, - System.Linq.Expressions.Expression[] parameters - ) - { - var callMethod = new System.Dynamic.DynamicMetaObject( - System.Linq.Expressions.Expression.Call( - System.Linq.Expressions.Expression.Convert(Expression, LimitType), - method, - parameters), - System.Dynamic.BindingRestrictions.GetTypeRestriction(Expression, LimitType) - ); - return callMethod; - } - - public override System.Dynamic.DynamicMetaObject BindGetMember(System.Dynamic.GetMemberBinder binder) - { - var parameters = new System.Linq.Expressions.Expression[] - { - System.Linq.Expressions.Expression.Constant(binder.Name) - }; - - var callMethod = CallMethod(getValueMethod, parameters); - - return callMethod; - } - - public override System.Dynamic.DynamicMetaObject BindSetMember(System.Dynamic.SetMemberBinder binder, System.Dynamic.DynamicMetaObject value) - { - var parameters = new System.Linq.Expressions.Expression[] - { - System.Linq.Expressions.Expression.Constant(binder.Name), - value.Expression, - }; - - var callMethod = CallMethod(setValueMethod, parameters); - - return callMethod; - } - } - - sealed partial class DapperRow - : System.Dynamic.IDynamicMetaObjectProvider - , IDictionary - { - readonly DapperTable table; - object[] values; - - public DapperRow(DapperTable table, object[] values) - { - if (table == null) throw new ArgumentNullException("table"); - if (values == null) throw new ArgumentNullException("values"); - this.table = table; - this.values = values; - } - private sealed class DeadValue - { - public static readonly DeadValue Default = new DeadValue(); - private DeadValue() { } - } - int ICollection>.Count - { - get - { - int count = 0; - for (int i = 0; i < values.Length; i++) - { - if (!(values[i] is DeadValue)) count++; - } - return count; - } - } - - public bool TryGetValue(string name, out object value) - { - var index = table.IndexOfName(name); - if (index < 0) - { // doesn't exist - value = null; - return false; - } - // exists, **even if** we don't have a value; consider table rows heterogeneous - value = index < values.Length ? values[index] : null; - if (value is DeadValue) - { // pretend it isn't here - value = null; - return false; - } - return true; - } - - public override string ToString() - { - var sb = new StringBuilder("{DapperRow"); - foreach (var kv in this) - { - var value = kv.Value; - sb.Append(", ").Append(kv.Key); - if (value != null) - { - sb.Append(" = '").Append(kv.Value).Append('\''); - } - else - { - sb.Append(" = NULL"); - } - } - - return sb.Append('}').ToString(); - } - - System.Dynamic.DynamicMetaObject System.Dynamic.IDynamicMetaObjectProvider.GetMetaObject( - System.Linq.Expressions.Expression parameter) - { - return new DapperRowMetaObject(parameter, System.Dynamic.BindingRestrictions.Empty, this); - } - - public IEnumerator> GetEnumerator() - { - var names = table.FieldNames; - for (var i = 0; i < names.Length; i++) - { - object value = i < values.Length ? values[i] : null; - if (!(value is DeadValue)) - { - yield return new KeyValuePair(names[i], value); - } - } - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - #region Implementation of ICollection> - - void ICollection>.Add(KeyValuePair item) - { - IDictionary dic = this; - dic.Add(item.Key, item.Value); - } - - void ICollection>.Clear() - { // removes values for **this row**, but doesn't change the fundamental table - for (int i = 0; i < values.Length; i++) - values[i] = DeadValue.Default; - } - - bool ICollection>.Contains(KeyValuePair item) - { - object value; - return TryGetValue(item.Key, out value) && Equals(value, item.Value); - } - - void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) - { - foreach (var kv in this) - { - array[arrayIndex++] = kv; // if they didn't leave enough space; not our fault - } - } - - bool ICollection>.Remove(KeyValuePair item) - { - IDictionary dic = this; - return dic.Remove(item.Key); - } - - bool ICollection>.IsReadOnly - { - get { return false; } - } - - #endregion - - #region Implementation of IDictionary - - bool IDictionary.ContainsKey(string key) - { - int index = table.IndexOfName(key); - if (index < 0 || index >= values.Length || values[index] is DeadValue) return false; - return true; - } - - void IDictionary.Add(string key, object value) - { - IDictionary dic = this; - dic[key] = value; - } - - bool IDictionary.Remove(string key) - { - int index = table.IndexOfName(key); - if (index < 0 || index >= values.Length || values[index] is DeadValue) return false; - values[index] = DeadValue.Default; - return true; - } - - object IDictionary.this[string key] - { - get { object val; TryGetValue(key, out val); return val; } - set { SetValue(key, value); } - } - public object SetValue(string key, object value) - { - if (key == null) throw new ArgumentNullException("key"); - int index = table.IndexOfName(key); - if (index < 0) - { - index = table.AddField(key); - } - if (values.Length <= index) - { // we'll assume they're doing lots of things, and - // grow it to the full width of the table - Array.Resize(ref values, table.FieldCount); - } - return values[index] = value; - } - - ICollection IDictionary.Keys - { - get { return this.Select(kv => kv.Key).ToArray(); } - } - - ICollection IDictionary.Values - { - get { return this.Select(kv => kv.Value).ToArray(); } - } - - #endregion - } + sealed partial class DapperTable + { + string[] fieldNames; + readonly Dictionary fieldNameLookup; + + internal string[] FieldNames { get { return fieldNames; } } + + public DapperTable(string[] fieldNames) + { + if (fieldNames == null) throw new ArgumentNullException("fieldNames"); + this.fieldNames = fieldNames; + + fieldNameLookup = new Dictionary(fieldNames.Length, StringComparer.Ordinal); + // if there are dups, we want the **first** key to be the "winner" - so iterate backwards + for (int i = fieldNames.Length - 1; i >= 0; i--) + { + string key = fieldNames[i]; + if (key != null) fieldNameLookup[key] = i; + } + } + + internal int IndexOfName(string name) + { + int result; + return (name != null && fieldNameLookup.TryGetValue(name, out result)) ? result : -1; + } + internal int AddField(string name) + { + if (name == null) throw new ArgumentNullException("name"); + if (fieldNameLookup.ContainsKey(name)) throw new InvalidOperationException("Field already exists: " + name); + int oldLen = fieldNames.Length; + Array.Resize(ref fieldNames, oldLen + 1); // yes, this is sub-optimal, but this is not the expected common case + fieldNameLookup[name] = oldLen; + return oldLen; + } + + + internal bool FieldExists(string key) + { + return key != null && fieldNameLookup.ContainsKey(key); + } + + public int FieldCount { get { return fieldNames.Length; } } + } + + sealed partial class DapperRowMetaObject : System.Dynamic.DynamicMetaObject + { + static readonly MethodInfo getValueMethod = typeof(IDictionary).GetProperty("Item").GetGetMethod(); + static readonly MethodInfo setValueMethod = typeof(DapperRow).GetMethod("SetValue"); + + public DapperRowMetaObject( + System.Linq.Expressions.Expression expression, + System.Dynamic.BindingRestrictions restrictions + ) + : base(expression, restrictions) + { + } + + public DapperRowMetaObject( + System.Linq.Expressions.Expression expression, + System.Dynamic.BindingRestrictions restrictions, + object value + ) + : base(expression, restrictions, value) + { + } + + System.Dynamic.DynamicMetaObject CallMethod( + MethodInfo method, + System.Linq.Expressions.Expression[] parameters + ) + { + var callMethod = new System.Dynamic.DynamicMetaObject( + System.Linq.Expressions.Expression.Call( + System.Linq.Expressions.Expression.Convert(Expression, LimitType), + method, + parameters), + System.Dynamic.BindingRestrictions.GetTypeRestriction(Expression, LimitType) + ); + return callMethod; + } + + public override System.Dynamic.DynamicMetaObject BindGetMember(System.Dynamic.GetMemberBinder binder) + { + var parameters = new System.Linq.Expressions.Expression[] + { + System.Linq.Expressions.Expression.Constant(binder.Name) + }; + + var callMethod = CallMethod(getValueMethod, parameters); + + return callMethod; + } + + public override System.Dynamic.DynamicMetaObject BindSetMember(System.Dynamic.SetMemberBinder binder, System.Dynamic.DynamicMetaObject value) + { + var parameters = new System.Linq.Expressions.Expression[] + { + System.Linq.Expressions.Expression.Constant(binder.Name), + value.Expression, + }; + + var callMethod = CallMethod(setValueMethod, parameters); + + return callMethod; + } + } + + sealed partial class DapperRow + : System.Dynamic.IDynamicMetaObjectProvider + , IDictionary + { + readonly DapperTable table; + object[] values; + + public DapperRow(DapperTable table, object[] values) + { + if (table == null) throw new ArgumentNullException("table"); + if (values == null) throw new ArgumentNullException("values"); + this.table = table; + this.values = values; + } + private sealed class DeadValue + { + public static readonly DeadValue Default = new DeadValue(); + private DeadValue() { } + } + int ICollection>.Count + { + get + { + int count = 0; + for (int i = 0; i < values.Length; i++) + { + if (!(values[i] is DeadValue)) count++; + } + return count; + } + } + + public bool TryGetValue(string name, out object value) + { + var index = table.IndexOfName(name); + if (index < 0) + { // doesn't exist + value = null; + return false; + } + // exists, **even if** we don't have a value; consider table rows heterogeneous + value = index < values.Length ? values[index] : null; + if (value is DeadValue) + { // pretend it isn't here + value = null; + return false; + } + return true; + } + + public override string ToString() + { + var sb = new StringBuilder("{DapperRow"); + foreach (var kv in this) + { + var value = kv.Value; + sb.Append(", ").Append(kv.Key); + if (value != null) + { + sb.Append(" = '").Append(kv.Value).Append('\''); + } + else + { + sb.Append(" = NULL"); + } + } + + return sb.Append('}').ToString(); + } + + System.Dynamic.DynamicMetaObject System.Dynamic.IDynamicMetaObjectProvider.GetMetaObject( + System.Linq.Expressions.Expression parameter) + { + return new DapperRowMetaObject(parameter, System.Dynamic.BindingRestrictions.Empty, this); + } + + public IEnumerator> GetEnumerator() + { + var names = table.FieldNames; + for (var i = 0; i < names.Length; i++) + { + object value = i < values.Length ? values[i] : null; + if (!(value is DeadValue)) + { + yield return new KeyValuePair(names[i], value); + } + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #region Implementation of ICollection> + + void ICollection>.Add(KeyValuePair item) + { + IDictionary dic = this; + dic.Add(item.Key, item.Value); + } + + void ICollection>.Clear() + { // removes values for **this row**, but doesn't change the fundamental table + for (int i = 0; i < values.Length; i++) + values[i] = DeadValue.Default; + } + + bool ICollection>.Contains(KeyValuePair item) + { + object value; + return TryGetValue(item.Key, out value) && Equals(value, item.Value); + } + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + foreach (var kv in this) + { + array[arrayIndex++] = kv; // if they didn't leave enough space; not our fault + } + } + + bool ICollection>.Remove(KeyValuePair item) + { + IDictionary dic = this; + return dic.Remove(item.Key); + } + + bool ICollection>.IsReadOnly + { + get { return false; } + } + + #endregion + + #region Implementation of IDictionary + + bool IDictionary.ContainsKey(string key) + { + int index = table.IndexOfName(key); + if (index < 0 || index >= values.Length || values[index] is DeadValue) return false; + return true; + } + + void IDictionary.Add(string key, object value) + { + IDictionary dic = this; + dic[key] = value; + } + + bool IDictionary.Remove(string key) + { + int index = table.IndexOfName(key); + if (index < 0 || index >= values.Length || values[index] is DeadValue) return false; + values[index] = DeadValue.Default; + return true; + } + + object IDictionary.this[string key] + { + get { object val; TryGetValue(key, out val); return val; } + set { SetValue(key, value); } + } + public object SetValue(string key, object value) + { + if (key == null) throw new ArgumentNullException("key"); + int index = table.IndexOfName(key); + if (index < 0) + { + index = table.AddField(key); + } + if (values.Length <= index) + { // we'll assume they're doing lots of things, and + // grow it to the full width of the table + Array.Resize(ref values, table.FieldCount); + } + return values[index] = value; + } + + ICollection IDictionary.Keys + { + get { return this.Select(kv => kv.Key).ToArray(); } + } + + ICollection IDictionary.Values + { + get { return this.Select(kv => kv.Value).ToArray(); } + } + + #endregion + } #endif #if !CSHARP30 - internal static Func GetDapperRowDeserializer(IDataRecord reader, int startBound, int length, bool returnNullIfFirstMissing) - { - var fieldCount = reader.FieldCount; - if (length == -1) - { - length = fieldCount - startBound; - } - - if (fieldCount <= startBound) - { - throw new ArgumentException("When using the multi-mapping APIs ensure you set the splitOn param if you have keys other than Id", "splitOn"); - } - - var effectiveFieldCount = fieldCount - startBound; - - DapperTable table = null; - - return - r => - { - if (table == null) - { - string[] names = new string[effectiveFieldCount]; - for (int i = 0; i < effectiveFieldCount; i++) - { - names[i] = r.GetName(i + startBound); - } - table = new DapperTable(names); - } - - var values = new object[effectiveFieldCount]; - - if (returnNullIfFirstMissing) - { - values[0] = r.GetValue(startBound); - if (values[0] is DBNull) - { - return null; - } - } - - if (startBound == 0) - { - r.GetValues(values); - } - else - { - var begin = returnNullIfFirstMissing ? 1 : 0; - for (var iter = begin; iter < effectiveFieldCount; ++iter) - { - values[iter] = r.GetValue(iter + startBound); - } - } - return new DapperRow(table, values); - }; - } + internal static Func GetDapperRowDeserializer(IDataRecord reader, int startBound, int length, bool returnNullIfFirstMissing) + { + var fieldCount = reader.FieldCount; + if (length == -1) + { + length = fieldCount - startBound; + } + + if (fieldCount <= startBound) + { + throw new ArgumentException("When using the multi-mapping APIs ensure you set the splitOn param if you have keys other than Id", "splitOn"); + } + + var effectiveFieldCount = fieldCount - startBound; + + DapperTable table = null; + + return + r => + { + if (table == null) + { + string[] names = new string[effectiveFieldCount]; + for (int i = 0; i < effectiveFieldCount; i++) + { + names[i] = r.GetName(i + startBound); + } + table = new DapperTable(names); + } + + var values = new object[effectiveFieldCount]; + + if (returnNullIfFirstMissing) + { + values[0] = r.GetValue(startBound); + if (values[0] is DBNull) + { + return null; + } + } + + if (startBound == 0) + { + r.GetValues(values); + } + else + { + var begin = returnNullIfFirstMissing ? 1 : 0; + for (var iter = begin; iter < effectiveFieldCount; ++iter) + { + values[iter] = r.GetValue(iter + startBound); + } + } + return new DapperRow(table, values); + }; + } #else - internal static Func GetDictionaryDeserializer(IDataRecord reader, int startBound, int length, bool returnNullIfFirstMissing) - { - var fieldCount = reader.FieldCount; - if (length == -1) - { - length = fieldCount - startBound; - } - - if (fieldCount <= startBound) - { - throw new ArgumentException("When using the multi-mapping APIs ensure you set the splitOn param if you have keys other than Id", "splitOn"); - } - - return - r => - { - IDictionary row = new Dictionary(length); - for (var i = startBound; i < startBound + length; i++) - { - var tmp = r.GetValue(i); - tmp = tmp == DBNull.Value ? null : tmp; - row[r.GetName(i)] = tmp; - if (returnNullIfFirstMissing && i == startBound && tmp == null) - { - return null; - } - } - return row; - }; - } + internal static Func GetDictionaryDeserializer(IDataRecord reader, int startBound, int length, bool returnNullIfFirstMissing) + { + var fieldCount = reader.FieldCount; + if (length == -1) + { + length = fieldCount - startBound; + } + + if (fieldCount <= startBound) + { + throw new ArgumentException("When using the multi-mapping APIs ensure you set the splitOn param if you have keys other than Id", "splitOn"); + } + + return + r => + { + IDictionary row = new Dictionary(length); + for (var i = startBound; i < startBound + length; i++) + { + var tmp = r.GetValue(i); + tmp = tmp == DBNull.Value ? null : tmp; + row[r.GetName(i)] = tmp; + if (returnNullIfFirstMissing && i == startBound && tmp == null) + { + return null; + } + } + return row; + }; + } #endif - /// - /// Internal use only - /// - /// - /// - [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("This method is for internal usage only", false)] - public static char ReadChar(object value) - { - if (value == null || value is DBNull) throw new ArgumentNullException("value"); - string s = value as string; - if (s == null || s.Length != 1) throw new ArgumentException("A single-character was expected", "value"); - return s[0]; - } - - /// - /// Internal use only - /// - [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("This method is for internal usage only", false)] - public static char? ReadNullableChar(object value) - { - if (value == null || value is DBNull) return null; - string s = value as string; - if (s == null || s.Length != 1) throw new ArgumentException("A single-character was expected", "value"); - return s[0]; - } - - - /// - /// Internal use only - /// - [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("This method is for internal usage only", true)] - public static IDbDataParameter FindOrAddParameter(IDataParameterCollection parameters, IDbCommand command, string name) - { - IDbDataParameter result; - if (parameters.Contains(name)) - { - result = (IDbDataParameter)parameters[name]; - } - else - { - result = command.CreateParameter(); - result.ParameterName = name; - parameters.Add(result); - } - return result; - } - - /// - /// Internal use only - /// - [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("This method is for internal usage only", false)] - public static void PackListParameters(IDbCommand command, string namePrefix, object value) - { - // initially we tried TVP, however it performs quite poorly. - // keep in mind SQL support up to 2000 params easily in sp_executesql, needing more is rare - - var list = value as IEnumerable; - var count = 0; - - if (list != null) - { - if (FeatureSupport.Get(command.Connection).Arrays) - { - var arrayParm = command.CreateParameter(); - arrayParm.Value = list; - arrayParm.ParameterName = namePrefix; - command.Parameters.Add(arrayParm); - } - else - { - bool isString = value is IEnumerable; - bool isDbString = value is IEnumerable; - foreach (var item in list) - { - count++; - var listParam = command.CreateParameter(); - listParam.ParameterName = namePrefix + count; - listParam.Value = item ?? DBNull.Value; - if (isString) - { - listParam.Size = 4000; - if (item != null && ((string)item).Length > 4000) - { - listParam.Size = -1; - } - } - if (isDbString && item as DbString != null) - { - var str = item as DbString; - str.AddParameter(command, listParam.ParameterName); - } - else - { - command.Parameters.Add(listParam); - } - } - - if (count == 0) - { - command.CommandText = Regex.Replace(command.CommandText, @"[?@:]" + Regex.Escape(namePrefix), "(SELECT NULL WHERE 1 = 0)"); - } - else - { - command.CommandText = Regex.Replace(command.CommandText, @"[?@:]" + Regex.Escape(namePrefix), match => - { - var grp = match.Value; - var sb = new StringBuilder("(").Append(grp).Append(1); - for (int i = 2; i <= count; i++) - { - sb.Append(',').Append(grp).Append(i); - } - return sb.Append(')').ToString(); - }); - } - } - } - - } - - private static IEnumerable FilterParameters(IEnumerable parameters, string sql) - { - return parameters.Where(p => Regex.IsMatch(sql, "[@:]" + p.Name + "([^a-zA-Z0-9_]+|$)", RegexOptions.IgnoreCase | RegexOptions.Multiline)); - } - - /// - /// Internal use only - /// - public static Action CreateParamInfoGenerator(Identity identity, bool checkForDuplicates) - { - Type type = identity.parametersType; - bool filterParams = identity.commandType.GetValueOrDefault(CommandType.Text) == CommandType.Text; - var dm = new DynamicMethod(string.Format("ParamInfo{0}", Guid.NewGuid()), null, new[] { typeof(IDbCommand), typeof(object) }, type, true); - - var il = dm.GetILGenerator(); - - il.DeclareLocal(type); // 0 - bool haveInt32Arg1 = false; - il.Emit(OpCodes.Ldarg_1); // stack is now [untyped-param] - il.Emit(OpCodes.Unbox_Any, type); // stack is now [typed-param] - il.Emit(OpCodes.Stloc_0);// stack is now empty - - il.Emit(OpCodes.Ldarg_0); // stack is now [command] - il.EmitCall(OpCodes.Callvirt, typeof(IDbCommand).GetProperty("Parameters").GetGetMethod(), null); // stack is now [parameters] - - IEnumerable props = type.GetProperties().Where(p => p.GetIndexParameters().Length == 0).OrderBy(p => p.Name); - if (filterParams) - { - props = FilterParameters(props, identity.sql); - } - foreach (var prop in props) - { - if (filterParams) - { - if (identity.sql.IndexOf("@" + prop.Name, StringComparison.InvariantCultureIgnoreCase) < 0 - && identity.sql.IndexOf(":" + prop.Name, StringComparison.InvariantCultureIgnoreCase) < 0) - { // can't see the parameter in the text (even in a comment, etc) - burn it with fire - continue; - } - } - if (prop.PropertyType == typeof(DbString)) - { - il.Emit(OpCodes.Ldloc_0); // stack is now [parameters] [typed-param] - il.Emit(OpCodes.Callvirt, prop.GetGetMethod()); // stack is [parameters] [dbstring] - il.Emit(OpCodes.Ldarg_0); // stack is now [parameters] [dbstring] [command] - il.Emit(OpCodes.Ldstr, prop.Name); // stack is now [parameters] [dbstring] [command] [name] - il.EmitCall(OpCodes.Callvirt, typeof(DbString).GetMethod("AddParameter"), null); // stack is now [parameters] - continue; - } - DbType dbType = LookupDbType(prop.PropertyType, prop.Name); - if (dbType == DynamicParameters.EnumerableMultiParameter) - { - // this actually represents special handling for list types; - il.Emit(OpCodes.Ldarg_0); // stack is now [parameters] [command] - il.Emit(OpCodes.Ldstr, prop.Name); // stack is now [parameters] [command] [name] - il.Emit(OpCodes.Ldloc_0); // stack is now [parameters] [command] [name] [typed-param] - il.Emit(OpCodes.Callvirt, prop.GetGetMethod()); // stack is [parameters] [command] [name] [typed-value] - if (prop.PropertyType.IsValueType) - { - il.Emit(OpCodes.Box, prop.PropertyType); // stack is [parameters] [command] [name] [boxed-value] - } - il.EmitCall(OpCodes.Call, typeof(SqlMapper).GetMethod("PackListParameters"), null); // stack is [parameters] - continue; - } - il.Emit(OpCodes.Dup); // stack is now [parameters] [parameters] - - il.Emit(OpCodes.Ldarg_0); // stack is now [parameters] [parameters] [command] - - if (checkForDuplicates) - { - // need to be a little careful about adding; use a utility method - il.Emit(OpCodes.Ldstr, prop.Name); // stack is now [parameters] [parameters] [command] [name] - il.EmitCall(OpCodes.Call, typeof(SqlMapper).GetMethod("FindOrAddParameter"), null); // stack is [parameters] [parameter] - } - else - { - // no risk of duplicates; just blindly add - il.EmitCall(OpCodes.Callvirt, typeof(IDbCommand).GetMethod("CreateParameter"), null);// stack is now [parameters] [parameters] [parameter] - - il.Emit(OpCodes.Dup);// stack is now [parameters] [parameters] [parameter] [parameter] - il.Emit(OpCodes.Ldstr, prop.Name); // stack is now [parameters] [parameters] [parameter] [parameter] [name] - il.EmitCall(OpCodes.Callvirt, typeof(IDataParameter).GetProperty("ParameterName").GetSetMethod(), null);// stack is now [parameters] [parameters] [parameter] - } - if (dbType != DbType.Time) // https://connect.microsoft.com/VisualStudio/feedback/details/381934/sqlparameter-dbtype-dbtype-time-sets-the-parameter-to-sqldbtype-datetime-instead-of-sqldbtype-time - { - il.Emit(OpCodes.Dup);// stack is now [parameters] [[parameters]] [parameter] [parameter] - EmitInt32(il, (int)dbType);// stack is now [parameters] [[parameters]] [parameter] [parameter] [db-type] - - il.EmitCall(OpCodes.Callvirt, typeof(IDataParameter).GetProperty("DbType").GetSetMethod(), null);// stack is now [parameters] [[parameters]] [parameter] - } - - il.Emit(OpCodes.Dup);// stack is now [parameters] [[parameters]] [parameter] [parameter] - EmitInt32(il, (int)ParameterDirection.Input);// stack is now [parameters] [[parameters]] [parameter] [parameter] [dir] - il.EmitCall(OpCodes.Callvirt, typeof(IDataParameter).GetProperty("Direction").GetSetMethod(), null);// stack is now [parameters] [[parameters]] [parameter] - - il.Emit(OpCodes.Dup);// stack is now [parameters] [[parameters]] [parameter] [parameter] - il.Emit(OpCodes.Ldloc_0); // stack is now [parameters] [[parameters]] [parameter] [parameter] [typed-param] - il.Emit(OpCodes.Callvirt, prop.GetGetMethod()); // stack is [parameters] [[parameters]] [parameter] [parameter] [typed-value] - bool checkForNull = true; - if (prop.PropertyType.IsValueType) - { - il.Emit(OpCodes.Box, prop.PropertyType); // stack is [parameters] [[parameters]] [parameter] [parameter] [boxed-value] - if (Nullable.GetUnderlyingType(prop.PropertyType) == null) - { // struct but not Nullable; boxed value cannot be null - checkForNull = false; - } - } - if (checkForNull) - { - if (dbType == DbType.String && !haveInt32Arg1) - { - il.DeclareLocal(typeof(int)); - haveInt32Arg1 = true; - } - // relative stack: [boxed value] - il.Emit(OpCodes.Dup);// relative stack: [boxed value] [boxed value] - Label notNull = il.DefineLabel(); - Label? allDone = dbType == DbType.String ? il.DefineLabel() : (Label?)null; - il.Emit(OpCodes.Brtrue_S, notNull); - // relative stack [boxed value = null] - il.Emit(OpCodes.Pop); // relative stack empty - il.Emit(OpCodes.Ldsfld, typeof(DBNull).GetField("Value")); // relative stack [DBNull] - if (dbType == DbType.String) - { - EmitInt32(il, 0); - il.Emit(OpCodes.Stloc_1); - } - if (allDone != null) il.Emit(OpCodes.Br_S, allDone.Value); - il.MarkLabel(notNull); - if (prop.PropertyType == typeof(string)) - { - il.Emit(OpCodes.Dup); // [string] [string] - il.EmitCall(OpCodes.Callvirt, typeof(string).GetProperty("Length").GetGetMethod(), null); // [string] [length] - EmitInt32(il, 4000); // [string] [length] [4000] - il.Emit(OpCodes.Cgt); // [string] [0 or 1] - Label isLong = il.DefineLabel(), lenDone = il.DefineLabel(); - il.Emit(OpCodes.Brtrue_S, isLong); - EmitInt32(il, 4000); // [string] [4000] - il.Emit(OpCodes.Br_S, lenDone); - il.MarkLabel(isLong); - EmitInt32(il, -1); // [string] [-1] - il.MarkLabel(lenDone); - il.Emit(OpCodes.Stloc_1); // [string] - } - if (prop.PropertyType.FullName == LinqBinary) - { - il.EmitCall(OpCodes.Callvirt, prop.PropertyType.GetMethod("ToArray", BindingFlags.Public | BindingFlags.Instance), null); - } - if (allDone != null) il.MarkLabel(allDone.Value); - // relative stack [boxed value or DBNull] - } - il.EmitCall(OpCodes.Callvirt, typeof(IDataParameter).GetProperty("Value").GetSetMethod(), null);// stack is now [parameters] [[parameters]] [parameter] - - if (prop.PropertyType == typeof(string)) - { - var endOfSize = il.DefineLabel(); - // don't set if 0 - il.Emit(OpCodes.Ldloc_1); // [parameters] [[parameters]] [parameter] [size] - il.Emit(OpCodes.Brfalse_S, endOfSize); // [parameters] [[parameters]] [parameter] - - il.Emit(OpCodes.Dup);// stack is now [parameters] [[parameters]] [parameter] [parameter] - il.Emit(OpCodes.Ldloc_1); // stack is now [parameters] [[parameters]] [parameter] [parameter] [size] - il.EmitCall(OpCodes.Callvirt, typeof(IDbDataParameter).GetProperty("Size").GetSetMethod(), null); // stack is now [parameters] [[parameters]] [parameter] - - il.MarkLabel(endOfSize); - } - if (checkForDuplicates) - { - // stack is now [parameters] [parameter] - il.Emit(OpCodes.Pop); // don't need parameter any more - } - else - { - // stack is now [parameters] [parameters] [parameter] - // blindly add - il.EmitCall(OpCodes.Callvirt, typeof(IList).GetMethod("Add"), null); // stack is now [parameters] - il.Emit(OpCodes.Pop); // IList.Add returns the new index (int); we don't care - } - } - // stack is currently [parameters] - il.Emit(OpCodes.Pop); // stack is now empty - il.Emit(OpCodes.Ret); - return (Action)dm.CreateDelegate(typeof(Action)); - } - - private static IDbCommand SetupCommand(IDbConnection cnn, IDbTransaction transaction, string sql, Action paramReader, object obj, int? commandTimeout, CommandType? commandType) - { - var cmd = cnn.CreateCommand(); - var bindByName = GetBindByName(cmd.GetType()); - if (bindByName != null) bindByName(cmd, true); - if (transaction != null) - cmd.Transaction = transaction; - cmd.CommandText = sql; - if (commandTimeout.HasValue) - cmd.CommandTimeout = commandTimeout.Value; - if (commandType.HasValue) - cmd.CommandType = commandType.Value; - if (paramReader != null) - { - paramReader(cmd, obj); - } - return cmd; - } - - - private static int ExecuteCommand(IDbConnection cnn, IDbTransaction transaction, string sql, Action paramReader, object obj, int? commandTimeout, CommandType? commandType) - { - IDbCommand cmd = null; - bool wasClosed = cnn.State == ConnectionState.Closed; - try - { - cmd = SetupCommand(cnn, transaction, sql, paramReader, obj, commandTimeout, commandType); - if (wasClosed) cnn.Open(); - return cmd.ExecuteNonQuery(); - } - finally - { - if (wasClosed) cnn.Close(); - if (cmd != null) cmd.Dispose(); - } - } - - private static Func GetStructDeserializer(Type type, Type effectiveType, int index) - { - // no point using special per-type handling here; it boils down to the same, plus not all are supported anyway (see: SqlDataReader.GetChar - not supported!) + /// + /// Internal use only + /// + /// + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This method is for internal usage only", false)] + public static char ReadChar(object value) + { + if (value == null || value is DBNull) throw new ArgumentNullException("value"); + string s = value as string; + if (s == null || s.Length != 1) throw new ArgumentException("A single-character was expected", "value"); + return s[0]; + } + + /// + /// Internal use only + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This method is for internal usage only", false)] + public static char? ReadNullableChar(object value) + { + if (value == null || value is DBNull) return null; + string s = value as string; + if (s == null || s.Length != 1) throw new ArgumentException("A single-character was expected", "value"); + return s[0]; + } + + + /// + /// Internal use only + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This method is for internal usage only", true)] + public static IDbDataParameter FindOrAddParameter(IDataParameterCollection parameters, IDbCommand command, string name) + { + IDbDataParameter result; + if (parameters.Contains(name)) + { + result = (IDbDataParameter)parameters[name]; + } + else + { + result = command.CreateParameter(); + result.ParameterName = name; + parameters.Add(result); + } + return result; + } + + /// + /// Internal use only + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This method is for internal usage only", false)] + public static void PackListParameters(IDbCommand command, string namePrefix, object value) + { + // initially we tried TVP, however it performs quite poorly. + // keep in mind SQL support up to 2000 params easily in sp_executesql, needing more is rare + + var list = value as IEnumerable; + var count = 0; + + if (list != null) + { + if (FeatureSupport.Get(command.Connection).Arrays) + { + var arrayParm = command.CreateParameter(); + arrayParm.Value = list; + arrayParm.ParameterName = namePrefix; + command.Parameters.Add(arrayParm); + } + else + { + bool isString = value is IEnumerable; + bool isDbString = value is IEnumerable; + foreach (var item in list) + { + count++; + var listParam = command.CreateParameter(); + listParam.ParameterName = namePrefix + count; + listParam.Value = item ?? DBNull.Value; + if (isString) + { + listParam.Size = 4000; + if (item != null && ((string)item).Length > 4000) + { + listParam.Size = -1; + } + } + if (isDbString && item as DbString != null) + { + var str = item as DbString; + str.AddParameter(command, listParam.ParameterName); + } + else + { + command.Parameters.Add(listParam); + } + } + + if (count == 0) + { + command.CommandText = Regex.Replace(command.CommandText, @"[?@:]" + Regex.Escape(namePrefix), "(SELECT NULL WHERE 1 = 0)"); + } + else + { + command.CommandText = Regex.Replace(command.CommandText, @"[?@:]" + Regex.Escape(namePrefix), match => + { + var grp = match.Value; + var sb = new StringBuilder("(").Append(grp).Append(1); + for (int i = 2; i <= count; i++) + { + sb.Append(',').Append(grp).Append(i); + } + return sb.Append(')').ToString(); + }); + } + } + } + + } + + private static IEnumerable FilterParameters(IEnumerable parameters, string sql) + { + return parameters.Where(p => Regex.IsMatch(sql, "[@:]" + p.Name + "([^a-zA-Z0-9_]+|$)", RegexOptions.IgnoreCase | RegexOptions.Multiline)); + } + + /// + /// Internal use only + /// + public static Action CreateParamInfoGenerator(Identity identity, bool checkForDuplicates) + { + Type type = identity.parametersType; + bool filterParams = identity.commandType.GetValueOrDefault(CommandType.Text) == CommandType.Text; + var dm = new DynamicMethod(string.Format("ParamInfo{0}", Guid.NewGuid()), null, new[] { typeof(IDbCommand), typeof(object) }, type, true); + + var il = dm.GetILGenerator(); + + il.DeclareLocal(type); // 0 + bool haveInt32Arg1 = false; + il.Emit(OpCodes.Ldarg_1); // stack is now [untyped-param] + il.Emit(OpCodes.Unbox_Any, type); // stack is now [typed-param] + il.Emit(OpCodes.Stloc_0);// stack is now empty + + il.Emit(OpCodes.Ldarg_0); // stack is now [command] + il.EmitCall(OpCodes.Callvirt, typeof(IDbCommand).GetProperty("Parameters").GetGetMethod(), null); // stack is now [parameters] + + IEnumerable props = type.GetProperties().Where(p => p.GetIndexParameters().Length == 0).OrderBy(p => p.Name); + if (filterParams) + { + props = FilterParameters(props, identity.sql); + } + foreach (var prop in props) + { + if (filterParams) + { + if (identity.sql.IndexOf("@" + prop.Name, StringComparison.InvariantCultureIgnoreCase) < 0 + && identity.sql.IndexOf(":" + prop.Name, StringComparison.InvariantCultureIgnoreCase) < 0) + { // can't see the parameter in the text (even in a comment, etc) - burn it with fire + continue; + } + } + if (prop.PropertyType == typeof(DbString)) + { + il.Emit(OpCodes.Ldloc_0); // stack is now [parameters] [typed-param] + il.Emit(OpCodes.Callvirt, prop.GetGetMethod()); // stack is [parameters] [dbstring] + il.Emit(OpCodes.Ldarg_0); // stack is now [parameters] [dbstring] [command] + il.Emit(OpCodes.Ldstr, prop.Name); // stack is now [parameters] [dbstring] [command] [name] + il.EmitCall(OpCodes.Callvirt, typeof(DbString).GetMethod("AddParameter"), null); // stack is now [parameters] + continue; + } + DbType dbType = LookupDbType(prop.PropertyType, prop.Name); + if (dbType == DynamicParameters.EnumerableMultiParameter) + { + // this actually represents special handling for list types; + il.Emit(OpCodes.Ldarg_0); // stack is now [parameters] [command] + il.Emit(OpCodes.Ldstr, prop.Name); // stack is now [parameters] [command] [name] + il.Emit(OpCodes.Ldloc_0); // stack is now [parameters] [command] [name] [typed-param] + il.Emit(OpCodes.Callvirt, prop.GetGetMethod()); // stack is [parameters] [command] [name] [typed-value] + if (prop.PropertyType.IsValueType) + { + il.Emit(OpCodes.Box, prop.PropertyType); // stack is [parameters] [command] [name] [boxed-value] + } + il.EmitCall(OpCodes.Call, typeof(SqlMapper).GetMethod("PackListParameters"), null); // stack is [parameters] + continue; + } + il.Emit(OpCodes.Dup); // stack is now [parameters] [parameters] + + il.Emit(OpCodes.Ldarg_0); // stack is now [parameters] [parameters] [command] + + if (checkForDuplicates) + { + // need to be a little careful about adding; use a utility method + il.Emit(OpCodes.Ldstr, prop.Name); // stack is now [parameters] [parameters] [command] [name] + il.EmitCall(OpCodes.Call, typeof(SqlMapper).GetMethod("FindOrAddParameter"), null); // stack is [parameters] [parameter] + } + else + { + // no risk of duplicates; just blindly add + il.EmitCall(OpCodes.Callvirt, typeof(IDbCommand).GetMethod("CreateParameter"), null);// stack is now [parameters] [parameters] [parameter] + + il.Emit(OpCodes.Dup);// stack is now [parameters] [parameters] [parameter] [parameter] + il.Emit(OpCodes.Ldstr, prop.Name); // stack is now [parameters] [parameters] [parameter] [parameter] [name] + il.EmitCall(OpCodes.Callvirt, typeof(IDataParameter).GetProperty("ParameterName").GetSetMethod(), null);// stack is now [parameters] [parameters] [parameter] + } + if (dbType != DbType.Time) // https://connect.microsoft.com/VisualStudio/feedback/details/381934/sqlparameter-dbtype-dbtype-time-sets-the-parameter-to-sqldbtype-datetime-instead-of-sqldbtype-time + { + il.Emit(OpCodes.Dup);// stack is now [parameters] [[parameters]] [parameter] [parameter] + EmitInt32(il, (int)dbType);// stack is now [parameters] [[parameters]] [parameter] [parameter] [db-type] + + il.EmitCall(OpCodes.Callvirt, typeof(IDataParameter).GetProperty("DbType").GetSetMethod(), null);// stack is now [parameters] [[parameters]] [parameter] + } + + il.Emit(OpCodes.Dup);// stack is now [parameters] [[parameters]] [parameter] [parameter] + EmitInt32(il, (int)ParameterDirection.Input);// stack is now [parameters] [[parameters]] [parameter] [parameter] [dir] + il.EmitCall(OpCodes.Callvirt, typeof(IDataParameter).GetProperty("Direction").GetSetMethod(), null);// stack is now [parameters] [[parameters]] [parameter] + + il.Emit(OpCodes.Dup);// stack is now [parameters] [[parameters]] [parameter] [parameter] + il.Emit(OpCodes.Ldloc_0); // stack is now [parameters] [[parameters]] [parameter] [parameter] [typed-param] + il.Emit(OpCodes.Callvirt, prop.GetGetMethod()); // stack is [parameters] [[parameters]] [parameter] [parameter] [typed-value] + bool checkForNull = true; + if (prop.PropertyType.IsValueType) + { + il.Emit(OpCodes.Box, prop.PropertyType); // stack is [parameters] [[parameters]] [parameter] [parameter] [boxed-value] + if (Nullable.GetUnderlyingType(prop.PropertyType) == null) + { // struct but not Nullable; boxed value cannot be null + checkForNull = false; + } + } + if (checkForNull) + { + if (dbType == DbType.String && !haveInt32Arg1) + { + il.DeclareLocal(typeof(int)); + haveInt32Arg1 = true; + } + // relative stack: [boxed value] + il.Emit(OpCodes.Dup);// relative stack: [boxed value] [boxed value] + Label notNull = il.DefineLabel(); + Label? allDone = dbType == DbType.String ? il.DefineLabel() : (Label?)null; + il.Emit(OpCodes.Brtrue_S, notNull); + // relative stack [boxed value = null] + il.Emit(OpCodes.Pop); // relative stack empty + il.Emit(OpCodes.Ldsfld, typeof(DBNull).GetField("Value")); // relative stack [DBNull] + if (dbType == DbType.String) + { + EmitInt32(il, 0); + il.Emit(OpCodes.Stloc_1); + } + if (allDone != null) il.Emit(OpCodes.Br_S, allDone.Value); + il.MarkLabel(notNull); + if (prop.PropertyType == typeof(string)) + { + il.Emit(OpCodes.Dup); // [string] [string] + il.EmitCall(OpCodes.Callvirt, typeof(string).GetProperty("Length").GetGetMethod(), null); // [string] [length] + EmitInt32(il, 4000); // [string] [length] [4000] + il.Emit(OpCodes.Cgt); // [string] [0 or 1] + Label isLong = il.DefineLabel(), lenDone = il.DefineLabel(); + il.Emit(OpCodes.Brtrue_S, isLong); + EmitInt32(il, 4000); // [string] [4000] + il.Emit(OpCodes.Br_S, lenDone); + il.MarkLabel(isLong); + EmitInt32(il, -1); // [string] [-1] + il.MarkLabel(lenDone); + il.Emit(OpCodes.Stloc_1); // [string] + } + if (prop.PropertyType.FullName == LinqBinary) + { + il.EmitCall(OpCodes.Callvirt, prop.PropertyType.GetMethod("ToArray", BindingFlags.Public | BindingFlags.Instance), null); + } + if (allDone != null) il.MarkLabel(allDone.Value); + // relative stack [boxed value or DBNull] + } + il.EmitCall(OpCodes.Callvirt, typeof(IDataParameter).GetProperty("Value").GetSetMethod(), null);// stack is now [parameters] [[parameters]] [parameter] + + if (prop.PropertyType == typeof(string)) + { + var endOfSize = il.DefineLabel(); + // don't set if 0 + il.Emit(OpCodes.Ldloc_1); // [parameters] [[parameters]] [parameter] [size] + il.Emit(OpCodes.Brfalse_S, endOfSize); // [parameters] [[parameters]] [parameter] + + il.Emit(OpCodes.Dup);// stack is now [parameters] [[parameters]] [parameter] [parameter] + il.Emit(OpCodes.Ldloc_1); // stack is now [parameters] [[parameters]] [parameter] [parameter] [size] + il.EmitCall(OpCodes.Callvirt, typeof(IDbDataParameter).GetProperty("Size").GetSetMethod(), null); // stack is now [parameters] [[parameters]] [parameter] + + il.MarkLabel(endOfSize); + } + if (checkForDuplicates) + { + // stack is now [parameters] [parameter] + il.Emit(OpCodes.Pop); // don't need parameter any more + } + else + { + // stack is now [parameters] [parameters] [parameter] + // blindly add + il.EmitCall(OpCodes.Callvirt, typeof(IList).GetMethod("Add"), null); // stack is now [parameters] + il.Emit(OpCodes.Pop); // IList.Add returns the new index (int); we don't care + } + } + // stack is currently [parameters] + il.Emit(OpCodes.Pop); // stack is now empty + il.Emit(OpCodes.Ret); + return (Action)dm.CreateDelegate(typeof(Action)); + } + + private static IDbCommand SetupCommand(IDbConnection cnn, IDbTransaction transaction, string sql, Action paramReader, object obj, int? commandTimeout, CommandType? commandType) + { + var cmd = cnn.CreateCommand(); + var bindByName = GetBindByName(cmd.GetType()); + if (bindByName != null) bindByName(cmd, true); + if (transaction != null) + cmd.Transaction = transaction; + cmd.CommandText = sql; + if (commandTimeout.HasValue) + cmd.CommandTimeout = commandTimeout.Value; + if (commandType.HasValue) + cmd.CommandType = commandType.Value; + if (paramReader != null) + { + paramReader(cmd, obj); + } + return cmd; + } + + + private static int ExecuteCommand(IDbConnection cnn, IDbTransaction transaction, string sql, Action paramReader, object obj, int? commandTimeout, CommandType? commandType) + { + IDbCommand cmd = null; + bool wasClosed = cnn.State == ConnectionState.Closed; + try + { + cmd = SetupCommand(cnn, transaction, sql, paramReader, obj, commandTimeout, commandType); + if (wasClosed) cnn.Open(); + return cmd.ExecuteNonQuery(); + } + finally + { + if (wasClosed) cnn.Close(); + if (cmd != null) cmd.Dispose(); + } + } + + private static Func GetStructDeserializer(Type type, Type effectiveType, int index) + { + // no point using special per-type handling here; it boils down to the same, plus not all are supported anyway (see: SqlDataReader.GetChar - not supported!) #pragma warning disable 618 - if (type == typeof(char)) - { // this *does* need special handling, though - return r => SqlMapper.ReadChar(r.GetValue(index)); - } - if (type == typeof(char?)) - { - return r => SqlMapper.ReadNullableChar(r.GetValue(index)); - } - if (type.FullName == LinqBinary) - { - return r => Activator.CreateInstance(type, r.GetValue(index)); - } + if (type == typeof(char)) + { // this *does* need special handling, though + return r => SqlMapper.ReadChar(r.GetValue(index)); + } + if (type == typeof(char?)) + { + return r => SqlMapper.ReadNullableChar(r.GetValue(index)); + } + if (type.FullName == LinqBinary) + { + return r => Activator.CreateInstance(type, r.GetValue(index)); + } #pragma warning restore 618 - if (effectiveType.IsEnum) - { // assume the value is returned as the correct type (int/byte/etc), but box back to the typed enum - return r => - { - var val = r.GetValue(index); - return val is DBNull ? null : Enum.ToObject(effectiveType, val); - }; - } - return r => - { - var val = r.GetValue(index); - return val is DBNull ? null : val; - }; - } - - static readonly MethodInfo - enumParse = typeof(Enum).GetMethod("Parse", new Type[] { typeof(Type), typeof(string), typeof(bool) }), - getItem = typeof(IDataRecord).GetProperties(BindingFlags.Instance | BindingFlags.Public) - .Where(p => p.GetIndexParameters().Any() && p.GetIndexParameters()[0].ParameterType == typeof(int)) - .Select(p => p.GetGetMethod()).First(); - - /// - /// Gets type-map for the given type - /// - /// Type map implementation, DefaultTypeMap instance if no override present - public static ITypeMap GetTypeMap(Type type) - { - if (type == null) throw new ArgumentNullException("type"); - var map = (ITypeMap)_typeMaps[type]; - if (map == null) - { - lock (_typeMaps) - { // double-checked; store this to avoid reflection next time we see this type - // since multiple queries commonly use the same domain-entity/DTO/view-model type - map = (ITypeMap)_typeMaps[type]; - if (map == null) - { - map = new DefaultTypeMap(type); - _typeMaps[type] = map; - } - } - } - return map; - } - - // use Hashtable to get free lockless reading - private static readonly Hashtable _typeMaps = new Hashtable(); - - /// - /// Set custom mapping for type deserializers - /// - /// Entity type to override - /// Mapping rules impementation, null to remove custom map - public static void SetTypeMap(Type type, ITypeMap map) - { - if (type == null) - throw new ArgumentNullException("type"); - - if (map == null || map is DefaultTypeMap) - { - lock (_typeMaps) - { - _typeMaps.Remove(type); - } - } - else - { - lock (_typeMaps) - { - _typeMaps[type] = map; - } - } - - PurgeQueryCacheByType(type); - } - - /// - /// Internal use only - /// - /// - /// - /// - /// - /// - /// - public static Func GetTypeDeserializer( + if (effectiveType.IsEnum) + { // assume the value is returned as the correct type (int/byte/etc), but box back to the typed enum + return r => + { + var val = r.GetValue(index); + return val is DBNull ? null : Enum.ToObject(effectiveType, val); + }; + } + return r => + { + var val = r.GetValue(index); + return val is DBNull ? null : val; + }; + } + + static readonly MethodInfo + enumParse = typeof(Enum).GetMethod("Parse", new Type[] { typeof(Type), typeof(string), typeof(bool) }), + getItem = typeof(IDataRecord).GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(p => p.GetIndexParameters().Any() && p.GetIndexParameters()[0].ParameterType == typeof(int)) + .Select(p => p.GetGetMethod()).First(); + + /// + /// Gets type-map for the given type + /// + /// Type map implementation, DefaultTypeMap instance if no override present + public static ITypeMap GetTypeMap(Type type) + { + if (type == null) throw new ArgumentNullException("type"); + var map = (ITypeMap)_typeMaps[type]; + if (map == null) + { + lock (_typeMaps) + { // double-checked; store this to avoid reflection next time we see this type + // since multiple queries commonly use the same domain-entity/DTO/view-model type + map = (ITypeMap)_typeMaps[type]; + if (map == null) + { + map = new DefaultTypeMap(type); + _typeMaps[type] = map; + } + } + } + return map; + } + + // use Hashtable to get free lockless reading + private static readonly Hashtable _typeMaps = new Hashtable(); + + /// + /// Set custom mapping for type deserializers + /// + /// Entity type to override + /// Mapping rules impementation, null to remove custom map + public static void SetTypeMap(Type type, ITypeMap map) + { + if (type == null) + throw new ArgumentNullException("type"); + + if (map == null || map is DefaultTypeMap) + { + lock (_typeMaps) + { + _typeMaps.Remove(type); + } + } + else + { + lock (_typeMaps) + { + _typeMaps[type] = map; + } + } + + PurgeQueryCacheByType(type); + } + + /// + /// Internal use only + /// + /// + /// + /// + /// + /// + /// + public static Func GetTypeDeserializer( #if CSHARP30 - Type type, IDataReader reader, int startBound, int length, bool returnNullIfFirstMissing + Type type, IDataReader reader, int startBound, int length, bool returnNullIfFirstMissing #else Type type, IDataReader reader, int startBound = 0, int length = -1, bool returnNullIfFirstMissing = false #endif ) - { - - var dm = new DynamicMethod(string.Format("Deserialize{0}", Guid.NewGuid()), typeof(object), new[] { typeof(IDataReader) }, true); - var il = dm.GetILGenerator(); - il.DeclareLocal(typeof(int)); - il.DeclareLocal(type); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Stloc_0); - - if (length == -1) - { - length = reader.FieldCount - startBound; - } - - if (reader.FieldCount <= startBound) - { - throw new ArgumentException("When using the multi-mapping APIs ensure you set the splitOn param if you have keys other than Id", "splitOn"); - } - - var names = Enumerable.Range(startBound, length).Select(i => reader.GetName(i)).ToArray(); - - ITypeMap typeMap = GetTypeMap(type); - - int index = startBound; - - ConstructorInfo specializedConstructor = null; - - if (type.IsValueType) - { - il.Emit(OpCodes.Ldloca_S, (byte)1); - il.Emit(OpCodes.Initobj, type); - } - else - { - var types = new Type[length]; - for (int i = startBound; i < startBound + length; i++) - { - types[i - startBound] = reader.GetFieldType(i); - } - - if (type.IsValueType) - { - il.Emit(OpCodes.Ldloca_S, (byte)1); - il.Emit(OpCodes.Initobj, type); - } - else - { - var ctor = typeMap.FindConstructor(names, types); - if (ctor == null) - { - string proposedTypes = "(" + String.Join(", ", types.Select((t, i) => t.FullName + " " + names[i]).ToArray()) + ")"; - throw new InvalidOperationException(String.Format("A parameterless default constructor or one matching signature {0} is required for {1} materialization", proposedTypes, type.FullName)); - } - - if (ctor.GetParameters().Length == 0) - { - il.Emit(OpCodes.Newobj, ctor); - il.Emit(OpCodes.Stloc_1); - } - else - specializedConstructor = ctor; - } - } - - il.BeginExceptionBlock(); - if (type.IsValueType) - { - il.Emit(OpCodes.Ldloca_S, (byte)1);// [target] - } - else if (specializedConstructor == null) - { - il.Emit(OpCodes.Ldloc_1);// [target] - } - - var members = (specializedConstructor != null - ? names.Select(n => typeMap.GetConstructorParameter(specializedConstructor, n)) - : names.Select(n => typeMap.GetMember(n))).ToList(); - - // stack is now [target] - - bool first = true; - var allDone = il.DefineLabel(); - int enumDeclareLocal = -1; - foreach (var item in members) - { - if (item != null) - { - if (specializedConstructor == null) - il.Emit(OpCodes.Dup); // stack is now [target][target] - Label isDbNullLabel = il.DefineLabel(); - Label finishLabel = il.DefineLabel(); - - il.Emit(OpCodes.Ldarg_0); // stack is now [target][target][reader] - EmitInt32(il, index); // stack is now [target][target][reader][index] - il.Emit(OpCodes.Dup);// stack is now [target][target][reader][index][index] - il.Emit(OpCodes.Stloc_0);// stack is now [target][target][reader][index] - il.Emit(OpCodes.Callvirt, getItem); // stack is now [target][target][value-as-object] - - Type memberType = item.MemberType; - - if (memberType == typeof(char) || memberType == typeof(char?)) - { - il.EmitCall(OpCodes.Call, typeof(SqlMapper).GetMethod( - memberType == typeof(char) ? "ReadChar" : "ReadNullableChar", BindingFlags.Static | BindingFlags.Public), null); // stack is now [target][target][typed-value] - } - else - { - il.Emit(OpCodes.Dup); // stack is now [target][target][value][value] - il.Emit(OpCodes.Isinst, typeof(DBNull)); // stack is now [target][target][value-as-object][DBNull or null] - il.Emit(OpCodes.Brtrue_S, isDbNullLabel); // stack is now [target][target][value-as-object] - - // unbox nullable enums as the primitive, i.e. byte etc - - var nullUnderlyingType = Nullable.GetUnderlyingType(memberType); - var unboxType = nullUnderlyingType != null && nullUnderlyingType.IsEnum ? nullUnderlyingType : memberType; - - if (unboxType.IsEnum) - { - if (enumDeclareLocal == -1) - { - enumDeclareLocal = il.DeclareLocal(typeof(string)).LocalIndex; - } - - Label isNotString = il.DefineLabel(); - il.Emit(OpCodes.Dup); // stack is now [target][target][value][value] - il.Emit(OpCodes.Isinst, typeof(string)); // stack is now [target][target][value-as-object][string or null] - il.Emit(OpCodes.Dup);// stack is now [target][target][value-as-object][string or null][string or null] - StoreLocal(il, enumDeclareLocal); // stack is now [target][target][value-as-object][string or null] - il.Emit(OpCodes.Brfalse_S, isNotString); // stack is now [target][target][value-as-object] - - il.Emit(OpCodes.Pop); // stack is now [target][target] - - il.Emit(OpCodes.Ldtoken, unboxType); // stack is now [target][target][enum-type-token] - il.EmitCall(OpCodes.Call, typeof(Type).GetMethod("GetTypeFromHandle"), null);// stack is now [target][target][enum-type] - il.Emit(OpCodes.Ldloc_2); // stack is now [target][target][enum-type][string] - il.Emit(OpCodes.Ldc_I4_1); // stack is now [target][target][enum-type][string][true] - il.EmitCall(OpCodes.Call, enumParse, null); // stack is now [target][target][enum-as-object] - - il.MarkLabel(isNotString); - - il.Emit(OpCodes.Unbox_Any, unboxType); // stack is now [target][target][typed-value] - - if (nullUnderlyingType != null) - { - il.Emit(OpCodes.Newobj, memberType.GetConstructor(new[] { nullUnderlyingType })); // stack is now [target][target][enum-value] - } - } - else if (memberType.FullName == LinqBinary) - { - il.Emit(OpCodes.Unbox_Any, typeof(byte[])); // stack is now [target][target][byte-array] - il.Emit(OpCodes.Newobj, memberType.GetConstructor(new Type[] { typeof(byte[]) }));// stack is now [target][target][binary] - } - else - { - Type dataType = reader.GetFieldType(index); - TypeCode dataTypeCode = Type.GetTypeCode(dataType), unboxTypeCode = Type.GetTypeCode(unboxType); - if (dataType == unboxType || dataTypeCode == unboxTypeCode || dataTypeCode == Type.GetTypeCode(nullUnderlyingType)) - { - il.Emit(OpCodes.Unbox_Any, unboxType); // stack is now [target][target][typed-value] - } - else - { - // not a direct match; need to tweak the unbox - bool handled = true; - OpCode opCode = default(OpCode); - if (dataTypeCode == TypeCode.Decimal || unboxTypeCode == TypeCode.Decimal) - { // no IL level conversions to/from decimal; I guess we could use the static operators, but - // this feels an edge-case - handled = false; - } - else - { - switch (unboxTypeCode) - { - case TypeCode.Byte: - opCode = OpCodes.Conv_Ovf_I1_Un; break; - case TypeCode.SByte: - opCode = OpCodes.Conv_Ovf_I1; break; - case TypeCode.UInt16: - opCode = OpCodes.Conv_Ovf_I2_Un; break; - case TypeCode.Int16: - opCode = OpCodes.Conv_Ovf_I2; break; - case TypeCode.UInt32: - opCode = OpCodes.Conv_Ovf_I4_Un; break; - case TypeCode.Boolean: // boolean is basically an int, at least at this level - case TypeCode.Int32: - opCode = OpCodes.Conv_Ovf_I4; break; - case TypeCode.UInt64: - opCode = OpCodes.Conv_Ovf_I8_Un; break; - case TypeCode.Int64: - opCode = OpCodes.Conv_Ovf_I8; break; - case TypeCode.Single: - opCode = OpCodes.Conv_R4; break; - case TypeCode.Double: - opCode = OpCodes.Conv_R8; break; - default: - handled = false; - break; - } - } - if (handled) - { // unbox as the data-type, then use IL-level convert - il.Emit(OpCodes.Unbox_Any, dataType); // stack is now [target][target][data-typed-value] - il.Emit(opCode); // stack is now [target][target][typed-value] - if (unboxTypeCode == TypeCode.Boolean) - { // compare to zero; I checked "csc" - this is the trick it uses; nice - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Ceq); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Ceq); - } - } - else - { // use flexible conversion - il.Emit(OpCodes.Ldtoken, unboxType); // stack is now [target][target][value][member-type-token] - il.EmitCall(OpCodes.Call, typeof(Type).GetMethod("GetTypeFromHandle"), null); // stack is now [target][target][value][member-type] - il.EmitCall(OpCodes.Call, typeof(Convert).GetMethod("ChangeType", new Type[] { typeof(object), typeof(Type) }), null); // stack is now [target][target][boxed-member-type-value] - il.Emit(OpCodes.Unbox_Any, unboxType); // stack is now [target][target][typed-value] - } - - } - - } - } - if (specializedConstructor == null) - { - // Store the value in the property/field - if (item.Property != null) - { - if (type.IsValueType) - { - il.Emit(OpCodes.Call, DefaultTypeMap.GetPropertySetter(item.Property, type)); // stack is now [target] - } - else - { - il.Emit(OpCodes.Callvirt, DefaultTypeMap.GetPropertySetter(item.Property, type)); // stack is now [target] - } - } - else - { - il.Emit(OpCodes.Stfld, item.Field); // stack is now [target] - } - } - - il.Emit(OpCodes.Br_S, finishLabel); // stack is now [target] - - il.MarkLabel(isDbNullLabel); // incoming stack: [target][target][value] - if (specializedConstructor != null) - { - il.Emit(OpCodes.Pop); - if (item.MemberType.IsValueType) - { - int localIndex = il.DeclareLocal(item.MemberType).LocalIndex; - LoadLocalAddress(il, localIndex); - il.Emit(OpCodes.Initobj, item.MemberType); - LoadLocal(il, localIndex); - } - else - { - il.Emit(OpCodes.Ldnull); - } - } - else - { - il.Emit(OpCodes.Pop); // stack is now [target][target] - il.Emit(OpCodes.Pop); // stack is now [target] - } - - if (first && returnNullIfFirstMissing) - { - il.Emit(OpCodes.Pop); - il.Emit(OpCodes.Ldnull); // stack is now [null] - il.Emit(OpCodes.Stloc_1); - il.Emit(OpCodes.Br, allDone); - } - - il.MarkLabel(finishLabel); - } - first = false; - index += 1; - } - if (type.IsValueType) - { - il.Emit(OpCodes.Pop); - } - else - { - if (specializedConstructor != null) - { - il.Emit(OpCodes.Newobj, specializedConstructor); - } - il.Emit(OpCodes.Stloc_1); // stack is empty - } - il.MarkLabel(allDone); - il.BeginCatchBlock(typeof(Exception)); // stack is Exception - il.Emit(OpCodes.Ldloc_0); // stack is Exception, index - il.Emit(OpCodes.Ldarg_0); // stack is Exception, index, reader - il.EmitCall(OpCodes.Call, typeof(SqlMapper).GetMethod("ThrowDataException"), null); - il.EndExceptionBlock(); - - il.Emit(OpCodes.Ldloc_1); // stack is [rval] - if (type.IsValueType) - { - il.Emit(OpCodes.Box, type); - } - il.Emit(OpCodes.Ret); - - return (Func)dm.CreateDelegate(typeof(Func)); - } - - private static void LoadLocal(ILGenerator il, int index) - { - if (index < 0 || index >= short.MaxValue) throw new ArgumentNullException("index"); - switch (index) - { - case 0: il.Emit(OpCodes.Ldloc_0); break; - case 1: il.Emit(OpCodes.Ldloc_1); break; - case 2: il.Emit(OpCodes.Ldloc_2); break; - case 3: il.Emit(OpCodes.Ldloc_3); break; - default: - if (index <= 255) - { - il.Emit(OpCodes.Ldloc_S, (byte)index); - } - else - { - il.Emit(OpCodes.Ldloc, (short)index); - } - break; - } - } - private static void StoreLocal(ILGenerator il, int index) - { - if (index < 0 || index >= short.MaxValue) throw new ArgumentNullException("index"); - switch (index) - { - case 0: il.Emit(OpCodes.Stloc_0); break; - case 1: il.Emit(OpCodes.Stloc_1); break; - case 2: il.Emit(OpCodes.Stloc_2); break; - case 3: il.Emit(OpCodes.Stloc_3); break; - default: - if (index <= 255) - { - il.Emit(OpCodes.Stloc_S, (byte)index); - } - else - { - il.Emit(OpCodes.Stloc, (short)index); - } - break; - } - } - private static void LoadLocalAddress(ILGenerator il, int index) - { - if (index < 0 || index >= short.MaxValue) throw new ArgumentNullException("index"); - - if (index <= 255) - { - il.Emit(OpCodes.Ldloca_S, (byte)index); - } - else - { - il.Emit(OpCodes.Ldloca, (short)index); - } - } - /// - /// Throws a data exception, only used internally - /// - /// - /// - /// - public static void ThrowDataException(Exception ex, int index, IDataReader reader) - { - Exception toThrow; - try - { - string name = "(n/a)", value = "(n/a)"; - if (reader != null && index >= 0 && index < reader.FieldCount) - { - name = reader.GetName(index); - object val = reader.GetValue(index); - if (val == null || val is DBNull) - { - value = ""; - } - else - { - value = Convert.ToString(val) + " - " + Type.GetTypeCode(val.GetType()); - } - } - toThrow = new DataException(string.Format("Error parsing column {0} ({1}={2})", index, name, value), ex); - } - catch - { // throw the **original** exception, wrapped as DataException - toThrow = new DataException(ex.Message, ex); - } - throw toThrow; - } - private static void EmitInt32(ILGenerator il, int value) - { - switch (value) - { - case -1: il.Emit(OpCodes.Ldc_I4_M1); break; - case 0: il.Emit(OpCodes.Ldc_I4_0); break; - case 1: il.Emit(OpCodes.Ldc_I4_1); break; - case 2: il.Emit(OpCodes.Ldc_I4_2); break; - case 3: il.Emit(OpCodes.Ldc_I4_3); break; - case 4: il.Emit(OpCodes.Ldc_I4_4); break; - case 5: il.Emit(OpCodes.Ldc_I4_5); break; - case 6: il.Emit(OpCodes.Ldc_I4_6); break; - case 7: il.Emit(OpCodes.Ldc_I4_7); break; - case 8: il.Emit(OpCodes.Ldc_I4_8); break; - default: - if (value >= -128 && value <= 127) - { - il.Emit(OpCodes.Ldc_I4_S, (sbyte)value); - } - else - { - il.Emit(OpCodes.Ldc_I4, value); - } - break; - } - } - - /// - /// The grid reader provides interfaces for reading multiple result sets from a Dapper query - /// - public partial class GridReader : IDisposable - { - private IDataReader reader; - private IDbCommand command; - private Identity identity; - - internal GridReader(IDbCommand command, IDataReader reader, Identity identity) - { - this.command = command; - this.reader = reader; - this.identity = identity; - } + { + + var dm = new DynamicMethod(string.Format("Deserialize{0}", Guid.NewGuid()), typeof(object), new[] { typeof(IDataReader) }, true); + var il = dm.GetILGenerator(); + il.DeclareLocal(typeof(int)); + il.DeclareLocal(type); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Stloc_0); + + if (length == -1) + { + length = reader.FieldCount - startBound; + } + + if (reader.FieldCount <= startBound) + { + throw new ArgumentException("When using the multi-mapping APIs ensure you set the splitOn param if you have keys other than Id", "splitOn"); + } + + var names = Enumerable.Range(startBound, length).Select(i => reader.GetName(i)).ToArray(); + + ITypeMap typeMap = GetTypeMap(type); + + int index = startBound; + + ConstructorInfo specializedConstructor = null; + + if (type.IsValueType) + { + il.Emit(OpCodes.Ldloca_S, (byte)1); + il.Emit(OpCodes.Initobj, type); + } + else + { + var types = new Type[length]; + for (int i = startBound; i < startBound + length; i++) + { + types[i - startBound] = reader.GetFieldType(i); + } + + if (type.IsValueType) + { + il.Emit(OpCodes.Ldloca_S, (byte)1); + il.Emit(OpCodes.Initobj, type); + } + else + { + var ctor = typeMap.FindConstructor(names, types); + if (ctor == null) + { + string proposedTypes = "(" + String.Join(", ", types.Select((t, i) => t.FullName + " " + names[i]).ToArray()) + ")"; + throw new InvalidOperationException(String.Format("A parameterless default constructor or one matching signature {0} is required for {1} materialization", proposedTypes, type.FullName)); + } + + if (ctor.GetParameters().Length == 0) + { + il.Emit(OpCodes.Newobj, ctor); + il.Emit(OpCodes.Stloc_1); + } + else + specializedConstructor = ctor; + } + } + + il.BeginExceptionBlock(); + if (type.IsValueType) + { + il.Emit(OpCodes.Ldloca_S, (byte)1);// [target] + } + else if (specializedConstructor == null) + { + il.Emit(OpCodes.Ldloc_1);// [target] + } + + var members = (specializedConstructor != null + ? names.Select(n => typeMap.GetConstructorParameter(specializedConstructor, n)) + : names.Select(n => typeMap.GetMember(n))).ToList(); + + // stack is now [target] + + bool first = true; + var allDone = il.DefineLabel(); + int enumDeclareLocal = -1; + foreach (var item in members) + { + if (item != null) + { + if (specializedConstructor == null) + il.Emit(OpCodes.Dup); // stack is now [target][target] + Label isDbNullLabel = il.DefineLabel(); + Label finishLabel = il.DefineLabel(); + + il.Emit(OpCodes.Ldarg_0); // stack is now [target][target][reader] + EmitInt32(il, index); // stack is now [target][target][reader][index] + il.Emit(OpCodes.Dup);// stack is now [target][target][reader][index][index] + il.Emit(OpCodes.Stloc_0);// stack is now [target][target][reader][index] + il.Emit(OpCodes.Callvirt, getItem); // stack is now [target][target][value-as-object] + + Type memberType = item.MemberType; + + if (memberType == typeof(char) || memberType == typeof(char?)) + { + il.EmitCall(OpCodes.Call, typeof(SqlMapper).GetMethod( + memberType == typeof(char) ? "ReadChar" : "ReadNullableChar", BindingFlags.Static | BindingFlags.Public), null); // stack is now [target][target][typed-value] + } + else + { + il.Emit(OpCodes.Dup); // stack is now [target][target][value][value] + il.Emit(OpCodes.Isinst, typeof(DBNull)); // stack is now [target][target][value-as-object][DBNull or null] + il.Emit(OpCodes.Brtrue_S, isDbNullLabel); // stack is now [target][target][value-as-object] + + // unbox nullable enums as the primitive, i.e. byte etc + + var nullUnderlyingType = Nullable.GetUnderlyingType(memberType); + var unboxType = nullUnderlyingType != null && nullUnderlyingType.IsEnum ? nullUnderlyingType : memberType; + + if (unboxType.IsEnum) + { + if (enumDeclareLocal == -1) + { + enumDeclareLocal = il.DeclareLocal(typeof(string)).LocalIndex; + } + + Label isNotString = il.DefineLabel(); + il.Emit(OpCodes.Dup); // stack is now [target][target][value][value] + il.Emit(OpCodes.Isinst, typeof(string)); // stack is now [target][target][value-as-object][string or null] + il.Emit(OpCodes.Dup);// stack is now [target][target][value-as-object][string or null][string or null] + StoreLocal(il, enumDeclareLocal); // stack is now [target][target][value-as-object][string or null] + il.Emit(OpCodes.Brfalse_S, isNotString); // stack is now [target][target][value-as-object] + + il.Emit(OpCodes.Pop); // stack is now [target][target] + + il.Emit(OpCodes.Ldtoken, unboxType); // stack is now [target][target][enum-type-token] + il.EmitCall(OpCodes.Call, typeof(Type).GetMethod("GetTypeFromHandle"), null);// stack is now [target][target][enum-type] + il.Emit(OpCodes.Ldloc_2); // stack is now [target][target][enum-type][string] + il.Emit(OpCodes.Ldc_I4_1); // stack is now [target][target][enum-type][string][true] + il.EmitCall(OpCodes.Call, enumParse, null); // stack is now [target][target][enum-as-object] + + il.MarkLabel(isNotString); + + il.Emit(OpCodes.Unbox_Any, unboxType); // stack is now [target][target][typed-value] + + if (nullUnderlyingType != null) + { + il.Emit(OpCodes.Newobj, memberType.GetConstructor(new[] { nullUnderlyingType })); // stack is now [target][target][enum-value] + } + } + else if (memberType.FullName == LinqBinary) + { + il.Emit(OpCodes.Unbox_Any, typeof(byte[])); // stack is now [target][target][byte-array] + il.Emit(OpCodes.Newobj, memberType.GetConstructor(new Type[] { typeof(byte[]) }));// stack is now [target][target][binary] + } + else + { + Type dataType = reader.GetFieldType(index); + TypeCode dataTypeCode = Type.GetTypeCode(dataType), unboxTypeCode = Type.GetTypeCode(unboxType); + if (dataType == unboxType || dataTypeCode == unboxTypeCode || dataTypeCode == Type.GetTypeCode(nullUnderlyingType)) + { + il.Emit(OpCodes.Unbox_Any, unboxType); // stack is now [target][target][typed-value] + } + else + { + // not a direct match; need to tweak the unbox + bool handled = true; + OpCode opCode = default(OpCode); + if (dataTypeCode == TypeCode.Decimal || unboxTypeCode == TypeCode.Decimal) + { // no IL level conversions to/from decimal; I guess we could use the static operators, but + // this feels an edge-case + handled = false; + } + else + { + switch (unboxTypeCode) + { + case TypeCode.Byte: + opCode = OpCodes.Conv_Ovf_I1_Un; break; + case TypeCode.SByte: + opCode = OpCodes.Conv_Ovf_I1; break; + case TypeCode.UInt16: + opCode = OpCodes.Conv_Ovf_I2_Un; break; + case TypeCode.Int16: + opCode = OpCodes.Conv_Ovf_I2; break; + case TypeCode.UInt32: + opCode = OpCodes.Conv_Ovf_I4_Un; break; + case TypeCode.Boolean: // boolean is basically an int, at least at this level + case TypeCode.Int32: + opCode = OpCodes.Conv_Ovf_I4; break; + case TypeCode.UInt64: + opCode = OpCodes.Conv_Ovf_I8_Un; break; + case TypeCode.Int64: + opCode = OpCodes.Conv_Ovf_I8; break; + case TypeCode.Single: + opCode = OpCodes.Conv_R4; break; + case TypeCode.Double: + opCode = OpCodes.Conv_R8; break; + default: + handled = false; + break; + } + } + if (handled) + { // unbox as the data-type, then use IL-level convert + il.Emit(OpCodes.Unbox_Any, dataType); // stack is now [target][target][data-typed-value] + il.Emit(opCode); // stack is now [target][target][typed-value] + if (unboxTypeCode == TypeCode.Boolean) + { // compare to zero; I checked "csc" - this is the trick it uses; nice + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ceq); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ceq); + } + } + else + { // use flexible conversion + il.Emit(OpCodes.Ldtoken, unboxType); // stack is now [target][target][value][member-type-token] + il.EmitCall(OpCodes.Call, typeof(Type).GetMethod("GetTypeFromHandle"), null); // stack is now [target][target][value][member-type] + il.EmitCall(OpCodes.Call, typeof(Convert).GetMethod("ChangeType", new Type[] { typeof(object), typeof(Type) }), null); // stack is now [target][target][boxed-member-type-value] + il.Emit(OpCodes.Unbox_Any, unboxType); // stack is now [target][target][typed-value] + } + + } + + } + } + if (specializedConstructor == null) + { + // Store the value in the property/field + if (item.Property != null) + { + if (type.IsValueType) + { + il.Emit(OpCodes.Call, DefaultTypeMap.GetPropertySetter(item.Property, type)); // stack is now [target] + } + else + { + il.Emit(OpCodes.Callvirt, DefaultTypeMap.GetPropertySetter(item.Property, type)); // stack is now [target] + } + } + else + { + il.Emit(OpCodes.Stfld, item.Field); // stack is now [target] + } + } + + il.Emit(OpCodes.Br_S, finishLabel); // stack is now [target] + + il.MarkLabel(isDbNullLabel); // incoming stack: [target][target][value] + if (specializedConstructor != null) + { + il.Emit(OpCodes.Pop); + if (item.MemberType.IsValueType) + { + int localIndex = il.DeclareLocal(item.MemberType).LocalIndex; + LoadLocalAddress(il, localIndex); + il.Emit(OpCodes.Initobj, item.MemberType); + LoadLocal(il, localIndex); + } + else + { + il.Emit(OpCodes.Ldnull); + } + } + else + { + il.Emit(OpCodes.Pop); // stack is now [target][target] + il.Emit(OpCodes.Pop); // stack is now [target] + } + + if (first && returnNullIfFirstMissing) + { + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldnull); // stack is now [null] + il.Emit(OpCodes.Stloc_1); + il.Emit(OpCodes.Br, allDone); + } + + il.MarkLabel(finishLabel); + } + first = false; + index += 1; + } + if (type.IsValueType) + { + il.Emit(OpCodes.Pop); + } + else + { + if (specializedConstructor != null) + { + il.Emit(OpCodes.Newobj, specializedConstructor); + } + il.Emit(OpCodes.Stloc_1); // stack is empty + } + il.MarkLabel(allDone); + il.BeginCatchBlock(typeof(Exception)); // stack is Exception + il.Emit(OpCodes.Ldloc_0); // stack is Exception, index + il.Emit(OpCodes.Ldarg_0); // stack is Exception, index, reader + il.EmitCall(OpCodes.Call, typeof(SqlMapper).GetMethod("ThrowDataException"), null); + il.EndExceptionBlock(); + + il.Emit(OpCodes.Ldloc_1); // stack is [rval] + if (type.IsValueType) + { + il.Emit(OpCodes.Box, type); + } + il.Emit(OpCodes.Ret); + + return (Func)dm.CreateDelegate(typeof(Func)); + } + + private static void LoadLocal(ILGenerator il, int index) + { + if (index < 0 || index >= short.MaxValue) throw new ArgumentNullException("index"); + switch (index) + { + case 0: il.Emit(OpCodes.Ldloc_0); break; + case 1: il.Emit(OpCodes.Ldloc_1); break; + case 2: il.Emit(OpCodes.Ldloc_2); break; + case 3: il.Emit(OpCodes.Ldloc_3); break; + default: + if (index <= 255) + { + il.Emit(OpCodes.Ldloc_S, (byte)index); + } + else + { + il.Emit(OpCodes.Ldloc, (short)index); + } + break; + } + } + private static void StoreLocal(ILGenerator il, int index) + { + if (index < 0 || index >= short.MaxValue) throw new ArgumentNullException("index"); + switch (index) + { + case 0: il.Emit(OpCodes.Stloc_0); break; + case 1: il.Emit(OpCodes.Stloc_1); break; + case 2: il.Emit(OpCodes.Stloc_2); break; + case 3: il.Emit(OpCodes.Stloc_3); break; + default: + if (index <= 255) + { + il.Emit(OpCodes.Stloc_S, (byte)index); + } + else + { + il.Emit(OpCodes.Stloc, (short)index); + } + break; + } + } + private static void LoadLocalAddress(ILGenerator il, int index) + { + if (index < 0 || index >= short.MaxValue) throw new ArgumentNullException("index"); + + if (index <= 255) + { + il.Emit(OpCodes.Ldloca_S, (byte)index); + } + else + { + il.Emit(OpCodes.Ldloca, (short)index); + } + } + /// + /// Throws a data exception, only used internally + /// + /// + /// + /// + public static void ThrowDataException(Exception ex, int index, IDataReader reader) + { + Exception toThrow; + try + { + string name = "(n/a)", value = "(n/a)"; + if (reader != null && index >= 0 && index < reader.FieldCount) + { + name = reader.GetName(index); + object val = reader.GetValue(index); + if (val == null || val is DBNull) + { + value = ""; + } + else + { + value = Convert.ToString(val) + " - " + Type.GetTypeCode(val.GetType()); + } + } + toThrow = new DataException(string.Format("Error parsing column {0} ({1}={2})", index, name, value), ex); + } + catch + { // throw the **original** exception, wrapped as DataException + toThrow = new DataException(ex.Message, ex); + } + throw toThrow; + } + private static void EmitInt32(ILGenerator il, int value) + { + switch (value) + { + case -1: il.Emit(OpCodes.Ldc_I4_M1); break; + case 0: il.Emit(OpCodes.Ldc_I4_0); break; + case 1: il.Emit(OpCodes.Ldc_I4_1); break; + case 2: il.Emit(OpCodes.Ldc_I4_2); break; + case 3: il.Emit(OpCodes.Ldc_I4_3); break; + case 4: il.Emit(OpCodes.Ldc_I4_4); break; + case 5: il.Emit(OpCodes.Ldc_I4_5); break; + case 6: il.Emit(OpCodes.Ldc_I4_6); break; + case 7: il.Emit(OpCodes.Ldc_I4_7); break; + case 8: il.Emit(OpCodes.Ldc_I4_8); break; + default: + if (value >= -128 && value <= 127) + { + il.Emit(OpCodes.Ldc_I4_S, (sbyte)value); + } + else + { + il.Emit(OpCodes.Ldc_I4, value); + } + break; + } + } + + /// + /// The grid reader provides interfaces for reading multiple result sets from a Dapper query + /// + public partial class GridReader : IDisposable + { + private IDataReader reader; + private IDbCommand command; + private Identity identity; + + internal GridReader(IDbCommand command, IDataReader reader, Identity identity) + { + this.command = command; + this.reader = reader; + this.identity = identity; + } #if !CSHARP30 - /// - /// Read the next grid of results, returned as a dynamic object - /// - public IEnumerable Read(bool buffered = true) - { - return Read(buffered); - } + /// + /// Read the next grid of results, returned as a dynamic object + /// + public IEnumerable Read(bool buffered = true) + { + return Read(buffered); + } #endif #if CSHARP30 - /// - /// Read the next grid of results - /// - public IEnumerable Read() - { - return Read(true); - } + /// + /// Read the next grid of results + /// + public IEnumerable Read() + { + return Read(true); + } #endif - /// - /// Read the next grid of results - /// + /// + /// Read the next grid of results + /// #if CSHARP30 - public IEnumerable Read(bool buffered) + public IEnumerable Read(bool buffered) #else - public IEnumerable Read(bool buffered = true) + public IEnumerable Read(bool buffered = true) #endif - { - if (reader == null) throw new ObjectDisposedException(GetType().FullName, "The reader has been disposed; this can happen after all data has been consumed"); - if (consumed) throw new InvalidOperationException("Query results must be consumed in the correct order, and each result can only be consumed once"); - var typedIdentity = identity.ForGrid(typeof(T), gridIndex); - CacheInfo cache = GetCacheInfo(typedIdentity); - var deserializer = cache.Deserializer; - - int hash = GetColumnHash(reader); - if (deserializer.Func == null || deserializer.Hash != hash) - { - deserializer = new DeserializerState(hash, GetDeserializer(typeof(T), reader, 0, -1, false)); - cache.Deserializer = deserializer; - } - consumed = true; - var result = ReadDeferred(gridIndex, deserializer.Func, typedIdentity); - return buffered ? result.ToList() : result; - } - - private IEnumerable MultiReadInternal(object func, string splitOn) - { - var identity = this.identity.ForGrid(typeof(TReturn), new Type[] { - typeof(TFirst), - typeof(TSecond), - typeof(TThird), - typeof(TFourth), - typeof(TFifth) - }, gridIndex); - try - { - foreach (var r in SqlMapper.MultiMapImpl(null, null, func, null, null, splitOn, null, null, reader, identity)) - { - yield return r; - } - } - finally - { - NextResult(); - } - } + { + if (reader == null) throw new ObjectDisposedException(GetType().FullName, "The reader has been disposed; this can happen after all data has been consumed"); + if (consumed) throw new InvalidOperationException("Query results must be consumed in the correct order, and each result can only be consumed once"); + var typedIdentity = identity.ForGrid(typeof(T), gridIndex); + CacheInfo cache = GetCacheInfo(typedIdentity); + var deserializer = cache.Deserializer; + + int hash = GetColumnHash(reader); + if (deserializer.Func == null || deserializer.Hash != hash) + { + deserializer = new DeserializerState(hash, GetDeserializer(typeof(T), reader, 0, -1, false)); + cache.Deserializer = deserializer; + } + consumed = true; + var result = ReadDeferred(gridIndex, deserializer.Func, typedIdentity); + return buffered ? result.ToList() : result; + } + + private IEnumerable MultiReadInternal(object func, string splitOn) + { + var identity = this.identity.ForGrid(typeof(TReturn), new Type[] { + typeof(TFirst), + typeof(TSecond), + typeof(TThird), + typeof(TFourth), + typeof(TFifth) + }, gridIndex); + try + { + foreach (var r in SqlMapper.MultiMapImpl(null, null, func, null, null, splitOn, null, null, reader, identity)) + { + yield return r; + } + } + finally + { + NextResult(); + } + } #if CSHARP30 - /// - /// Read multiple objects from a single recordset on the grid - /// - public IEnumerable Read(Func func, string splitOn) - { - return Read(func, splitOn, true); - } + /// + /// Read multiple objects from a single recordset on the grid + /// + public IEnumerable Read(Func func, string splitOn) + { + return Read(func, splitOn, true); + } #endif - /// - /// Read multiple objects from a single recordset on the grid - /// + /// + /// Read multiple objects from a single recordset on the grid + /// #if CSHARP30 - public IEnumerable Read(Func func, string splitOn, bool buffered) + public IEnumerable Read(Func func, string splitOn, bool buffered) #else - public IEnumerable Read(Func func, string splitOn = "id", bool buffered = true) + public IEnumerable Read(Func func, string splitOn = "id", bool buffered = true) #endif - { - var result = MultiReadInternal(func, splitOn); - return buffered ? result.ToList() : result; - } + { + var result = MultiReadInternal(func, splitOn); + return buffered ? result.ToList() : result; + } #if CSHARP30 - /// - /// Read multiple objects from a single recordset on the grid - /// - public IEnumerable Read(Func func, string splitOn) - { - return Read(func, splitOn, true); - } + /// + /// Read multiple objects from a single recordset on the grid + /// + public IEnumerable Read(Func func, string splitOn) + { + return Read(func, splitOn, true); + } #endif - /// - /// Read multiple objects from a single recordset on the grid - /// + /// + /// Read multiple objects from a single recordset on the grid + /// #if CSHARP30 - public IEnumerable Read(Func func, string splitOn, bool buffered) + public IEnumerable Read(Func func, string splitOn, bool buffered) #else - public IEnumerable Read(Func func, string splitOn = "id", bool buffered = true) + public IEnumerable Read(Func func, string splitOn = "id", bool buffered = true) #endif - { - var result = MultiReadInternal(func, splitOn); - return buffered ? result.ToList() : result; - } + { + var result = MultiReadInternal(func, splitOn); + return buffered ? result.ToList() : result; + } #if CSHARP30 - /// - /// Read multiple objects from a single record set on the grid - /// - public IEnumerable Read(Func func, string splitOn) - { - return Read(func, splitOn, true); - } + /// + /// Read multiple objects from a single record set on the grid + /// + public IEnumerable Read(Func func, string splitOn) + { + return Read(func, splitOn, true); + } #endif - /// - /// Read multiple objects from a single record set on the grid - /// + /// + /// Read multiple objects from a single record set on the grid + /// #if CSHARP30 - public IEnumerable Read(Func func, string splitOn, bool buffered) + public IEnumerable Read(Func func, string splitOn, bool buffered) #else - public IEnumerable Read(Func func, string splitOn = "id", bool buffered = true) + public IEnumerable Read(Func func, string splitOn = "id", bool buffered = true) #endif - { - var result = MultiReadInternal(func, splitOn); - return buffered ? result.ToList() : result; - } + { + var result = MultiReadInternal(func, splitOn); + return buffered ? result.ToList() : result; + } #if !CSHARP30 - /// - /// Read multiple objects from a single record set on the grid - /// - public IEnumerable Read(Func func, string splitOn = "id", bool buffered = true) - { - var result = MultiReadInternal(func, splitOn); - return buffered ? result.ToList() : result; - } + /// + /// Read multiple objects from a single record set on the grid + /// + public IEnumerable Read(Func func, string splitOn = "id", bool buffered = true) + { + var result = MultiReadInternal(func, splitOn); + return buffered ? result.ToList() : result; + } #endif - private IEnumerable ReadDeferred(int index, Func deserializer, Identity typedIdentity) - { - try - { - while (index == gridIndex && reader.Read()) - { - yield return (T)deserializer(reader); - } - } - finally // finally so that First etc progresses things even when multiple rows - { - if (index == gridIndex) - { - NextResult(); - } - } - } - private int gridIndex, readCount; - private bool consumed; - private void NextResult() - { - if (reader.NextResult()) - { - readCount++; - gridIndex++; - consumed = false; - } - else - { - // happy path; close the reader cleanly - no - // need for "Cancel" etc - reader.Dispose(); - reader = null; - - Dispose(); - } - - } - /// - /// Dispose the grid, closing and disposing both the underlying reader and command. - /// - public void Dispose() - { - if (reader != null) - { - if (!reader.IsClosed && command != null) command.Cancel(); - reader.Dispose(); - reader = null; - } - if (command != null) - { - command.Dispose(); - command = null; - } - } - } - } - - /// - /// A bag of parameters that can be passed to the Dapper Query and Execute methods - /// - partial class DynamicParameters : SqlMapper.IDynamicParameters - { - internal const DbType EnumerableMultiParameter = (DbType)(-1); - static Dictionary> paramReaderCache = new Dictionary>(); - - Dictionary parameters = new Dictionary(); - List templates; - - partial class ParamInfo - { - public string Name { get; set; } - public object Value { get; set; } - public ParameterDirection ParameterDirection { get; set; } - public DbType? DbType { get; set; } - public int? Size { get; set; } - public IDbDataParameter AttachedParam { get; set; } - } - - /// - /// construct a dynamic parameter bag - /// - public DynamicParameters() { } - - /// - /// construct a dynamic parameter bag - /// - /// can be an anonymous type or a DynamicParameters bag - public DynamicParameters(object template) - { - AddDynamicParams(template); - } - - /// - /// Append a whole object full of params to the dynamic - /// EG: AddDynamicParams(new {A = 1, B = 2}) // will add property A and B to the dynamic - /// - /// - public void AddDynamicParams( + private IEnumerable ReadDeferred(int index, Func deserializer, Identity typedIdentity) + { + try + { + while (index == gridIndex && reader.Read()) + { + yield return (T)deserializer(reader); + } + } + finally // finally so that First etc progresses things even when multiple rows + { + if (index == gridIndex) + { + NextResult(); + } + } + } + private int gridIndex, readCount; + private bool consumed; + private void NextResult() + { + if (reader.NextResult()) + { + readCount++; + gridIndex++; + consumed = false; + } + else + { + // happy path; close the reader cleanly - no + // need for "Cancel" etc + reader.Dispose(); + reader = null; + + Dispose(); + } + + } + /// + /// Dispose the grid, closing and disposing both the underlying reader and command. + /// + public void Dispose() + { + if (reader != null) + { + if (!reader.IsClosed && command != null) command.Cancel(); + reader.Dispose(); + reader = null; + } + if (command != null) + { + command.Dispose(); + command = null; + } + } + } + } + + /// + /// A bag of parameters that can be passed to the Dapper Query and Execute methods + /// + partial class DynamicParameters : SqlMapper.IDynamicParameters + { + internal const DbType EnumerableMultiParameter = (DbType)(-1); + static Dictionary> paramReaderCache = new Dictionary>(); + + Dictionary parameters = new Dictionary(); + List templates; + + partial class ParamInfo + { + public string Name { get; set; } + public object Value { get; set; } + public ParameterDirection ParameterDirection { get; set; } + public DbType? DbType { get; set; } + public int? Size { get; set; } + public IDbDataParameter AttachedParam { get; set; } + } + + /// + /// construct a dynamic parameter bag + /// + public DynamicParameters() { } + + /// + /// construct a dynamic parameter bag + /// + /// can be an anonymous type or a DynamicParameters bag + public DynamicParameters(object template) + { + AddDynamicParams(template); + } + + /// + /// Append a whole object full of params to the dynamic + /// EG: AddDynamicParams(new {A = 1, B = 2}) // will add property A and B to the dynamic + /// + /// + public void AddDynamicParams( #if CSHARP30 - object param + object param #else dynamic param #endif ) - { - var obj = param as object; - if (obj != null) - { - var subDynamic = obj as DynamicParameters; - if (subDynamic == null) - { - var dictionary = obj as IEnumerable>; - if (dictionary == null) - { - templates = templates ?? new List(); - templates.Add(obj); - } - else - { - foreach (var kvp in dictionary) - { + { + var obj = param as object; + if (obj != null) + { + var subDynamic = obj as DynamicParameters; + if (subDynamic == null) + { + var dictionary = obj as IEnumerable>; + if (dictionary == null) + { + templates = templates ?? new List(); + templates.Add(obj); + } + else + { + foreach (var kvp in dictionary) + { #if CSHARP30 - Add(kvp.Key, kvp.Value, null, null, null); + Add(kvp.Key, kvp.Value, null, null, null); #else - Add(kvp.Key, kvp.Value); + Add(kvp.Key, kvp.Value); #endif - } - } - } - else - { - if (subDynamic.parameters != null) - { - foreach (var kvp in subDynamic.parameters) - { - parameters.Add(kvp.Key, kvp.Value); - } - } - - if (subDynamic.templates != null) - { - templates = templates ?? new List(); - foreach (var t in subDynamic.templates) - { - templates.Add(t); - } - } - } - } - } - - /// - /// Add a parameter to this dynamic parameter list - /// - /// - /// - /// - /// - /// - public void Add( + } + } + } + else + { + if (subDynamic.parameters != null) + { + foreach (var kvp in subDynamic.parameters) + { + parameters.Add(kvp.Key, kvp.Value); + } + } + + if (subDynamic.templates != null) + { + templates = templates ?? new List(); + foreach (var t in subDynamic.templates) + { + templates.Add(t); + } + } + } + } + } + + /// + /// Add a parameter to this dynamic parameter list + /// + /// + /// + /// + /// + /// + public void Add( #if CSHARP30 - string name, object value, DbType? dbType, ParameterDirection? direction, int? size + string name, object value, DbType? dbType, ParameterDirection? direction, int? size #else string name, object value = null, DbType? dbType = null, ParameterDirection? direction = null, int? size = null #endif ) - { - parameters[Clean(name)] = new ParamInfo() { Name = name, Value = value, ParameterDirection = direction ?? ParameterDirection.Input, DbType = dbType, Size = size }; - } - - static string Clean(string name) - { - if (!string.IsNullOrEmpty(name)) - { - switch (name[0]) - { - case '@': - case ':': - case '?': - return name.Substring(1); - } - } - return name; - } - - void SqlMapper.IDynamicParameters.AddParameters(IDbCommand command, SqlMapper.Identity identity) - { - AddParameters(command, identity); - } - - /// - /// Add all the parameters needed to the command just before it executes - /// - /// The raw command prior to execution - /// Information about the query - protected void AddParameters(IDbCommand command, SqlMapper.Identity identity) - { - if (templates != null) - { - foreach (var template in templates) - { - var newIdent = identity.ForDynamicParameters(template.GetType()); - Action appender; - - lock (paramReaderCache) - { - if (!paramReaderCache.TryGetValue(newIdent, out appender)) - { - appender = SqlMapper.CreateParamInfoGenerator(newIdent, true); - paramReaderCache[newIdent] = appender; - } - } - - appender(command, template); - } - } - - foreach (var param in parameters.Values) - { - var dbType = param.DbType; - var val = param.Value; - string name = Clean(param.Name); - - if (dbType == null && val != null) dbType = SqlMapper.LookupDbType(val.GetType(), name); - - if (dbType == DynamicParameters.EnumerableMultiParameter) - { + { + parameters[Clean(name)] = new ParamInfo() { Name = name, Value = value, ParameterDirection = direction ?? ParameterDirection.Input, DbType = dbType, Size = size }; + } + + static string Clean(string name) + { + if (!string.IsNullOrEmpty(name)) + { + switch (name[0]) + { + case '@': + case ':': + case '?': + return name.Substring(1); + } + } + return name; + } + + void SqlMapper.IDynamicParameters.AddParameters(IDbCommand command, SqlMapper.Identity identity) + { + AddParameters(command, identity); + } + + /// + /// Add all the parameters needed to the command just before it executes + /// + /// The raw command prior to execution + /// Information about the query + protected void AddParameters(IDbCommand command, SqlMapper.Identity identity) + { + if (templates != null) + { + foreach (var template in templates) + { + var newIdent = identity.ForDynamicParameters(template.GetType()); + Action appender; + + lock (paramReaderCache) + { + if (!paramReaderCache.TryGetValue(newIdent, out appender)) + { + appender = SqlMapper.CreateParamInfoGenerator(newIdent, true); + paramReaderCache[newIdent] = appender; + } + } + + appender(command, template); + } + } + + foreach (var param in parameters.Values) + { + var dbType = param.DbType; + var val = param.Value; + string name = Clean(param.Name); + + if (dbType == null && val != null) dbType = SqlMapper.LookupDbType(val.GetType(), name); + + if (dbType == DynamicParameters.EnumerableMultiParameter) + { #pragma warning disable 612, 618 - SqlMapper.PackListParameters(command, name, val); + SqlMapper.PackListParameters(command, name, val); #pragma warning restore 612, 618 - } - else - { - - bool add = !command.Parameters.Contains(name); - IDbDataParameter p; - if (add) - { - p = command.CreateParameter(); - p.ParameterName = name; - } - else - { - p = (IDbDataParameter)command.Parameters[name]; - } - - p.Value = val ?? DBNull.Value; - p.Direction = param.ParameterDirection; - var s = val as string; - if (s != null) - { - if (s.Length <= 4000) - { - p.Size = 4000; - } - } - if (param.Size != null) - { - p.Size = param.Size.Value; - } - if (dbType != null) - { - p.DbType = dbType.Value; - } - if (add) - { - command.Parameters.Add(p); - } - param.AttachedParam = p; - } - - } - } - - /// - /// All the names of the param in the bag, use Get to yank them out - /// - public IEnumerable ParameterNames - { - get - { - return parameters.Select(p => p.Key); - } - } - - - /// - /// Get the value of a parameter - /// - /// - /// - /// The value, note DBNull.Value is not returned, instead the value is returned as null - public T Get(string name) - { - var val = parameters[Clean(name)].AttachedParam.Value; - if (val == DBNull.Value) - { - if (default(T) != null) - { - throw new ApplicationException("Attempting to cast a DBNull to a non nullable type!"); - } - return default(T); - } - return (T)val; - } - } - - /// - /// This class represents a SQL string, it can be used if you need to denote your parameter is a Char vs VarChar vs nVarChar vs nChar - /// - sealed partial class DbString - { - /// - /// Create a new DbString - /// - public DbString() { Length = -1; } - /// - /// Ansi vs Unicode - /// - public bool IsAnsi { get; set; } - /// - /// Fixed length - /// - public bool IsFixedLength { get; set; } - /// - /// Length of the string -1 for max - /// - public int Length { get; set; } - /// - /// The value of the string - /// - public string Value { get; set; } - /// - /// Add the parameter to the command... internal use only - /// - /// - /// - public void AddParameter(IDbCommand command, string name) - { - if (IsFixedLength && Length == -1) - { - throw new InvalidOperationException("If specifying IsFixedLength, a Length must also be specified"); - } - var param = command.CreateParameter(); - param.ParameterName = name; - param.Value = (object)Value ?? DBNull.Value; - if (Length == -1 && Value != null && Value.Length <= 4000) - { - param.Size = 4000; - } - else - { - param.Size = Length; - } - param.DbType = IsAnsi ? (IsFixedLength ? DbType.AnsiStringFixedLength : DbType.AnsiString) : (IsFixedLength ? DbType.StringFixedLength : DbType.String); - command.Parameters.Add(param); - } - } - - /// - /// Handles variances in features per DBMS - /// - partial class FeatureSupport - { - /// - /// Dictionary of supported features index by connection type name - /// - private static readonly Dictionary FeatureList = new Dictionary(StringComparer.InvariantCultureIgnoreCase) { - {"sqlserverconnection", new FeatureSupport { Arrays = false}}, - {"npgsqlconnection", new FeatureSupport {Arrays = true}} - }; - - /// - /// Gets the featureset based on the passed connection - /// - public static FeatureSupport Get(IDbConnection connection) - { - string name = connection.GetType().Name; - FeatureSupport features; - return FeatureList.TryGetValue(name, out features) ? features : FeatureList.Values.First(); - } - - /// - /// True if the db supports array columns e.g. Postgresql - /// - public bool Arrays { get; set; } - } - - /// - /// Represents simple memeber map for one of target parameter or property or field to source DataReader column - /// - sealed partial class SimpleMemberMap : SqlMapper.IMemberMap - { - private readonly string _columnName; - private readonly PropertyInfo _property; - private readonly FieldInfo _field; - private readonly ParameterInfo _parameter; - - /// - /// Creates instance for simple property mapping - /// - /// DataReader column name - /// Target property - public SimpleMemberMap(string columnName, PropertyInfo property) - { - if (columnName == null) - throw new ArgumentNullException("columnName"); - - if (property == null) - throw new ArgumentNullException("property"); - - _columnName = columnName; - _property = property; - } - - /// - /// Creates instance for simple field mapping - /// - /// DataReader column name - /// Target property - public SimpleMemberMap(string columnName, FieldInfo field) - { - if (columnName == null) - throw new ArgumentNullException("columnName"); - - if (field == null) - throw new ArgumentNullException("field"); - - _columnName = columnName; - _field = field; - } - - /// - /// Creates instance for simple constructor parameter mapping - /// - /// DataReader column name - /// Target constructor parameter - public SimpleMemberMap(string columnName, ParameterInfo parameter) - { - if (columnName == null) - throw new ArgumentNullException("columnName"); - - if (parameter == null) - throw new ArgumentNullException("parameter"); - - _columnName = columnName; - _parameter = parameter; - } - - /// - /// DataReader column name - /// - public string ColumnName - { - get { return _columnName; } - } - - /// - /// Target member type - /// - public Type MemberType - { - get - { - if (_field != null) - return _field.FieldType; - - if (_property != null) - return _property.PropertyType; - - if (_parameter != null) - return _parameter.ParameterType; - - return null; - } - } - - /// - /// Target property - /// - public PropertyInfo Property - { - get { return _property; } - } - - /// - /// Target field - /// - public FieldInfo Field - { - get { return _field; } - } - - /// - /// Target constructor parameter - /// - public ParameterInfo Parameter - { - get { return _parameter; } - } - } - - /// - /// Represents default type mapping strategy used by Dapper - /// - sealed partial class DefaultTypeMap : SqlMapper.ITypeMap - { - private readonly List _fields; - private readonly List _properties; - private readonly Type _type; - - /// - /// Creates default type map - /// - /// Entity type - public DefaultTypeMap(Type type) - { - if (type == null) - throw new ArgumentNullException("type"); - - _fields = GetSettableFields(type); - _properties = GetSettableProps(type); - _type = type; - } - - internal static MethodInfo GetPropertySetter(PropertyInfo propertyInfo, Type type) - { - return propertyInfo.DeclaringType == type ? - propertyInfo.GetSetMethod(true) : - propertyInfo.DeclaringType.GetProperty(propertyInfo.Name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).GetSetMethod(true); - } - - internal static List GetSettableProps(Type t) - { - return t - .GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) - .Where(p => GetPropertySetter(p, t) != null) - .ToList(); - } - - internal static List GetSettableFields(Type t) - { - return t.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).ToList(); - } - - /// - /// Finds best constructor - /// - /// DataReader column names - /// DataReader column types - /// Matching constructor or default one - public ConstructorInfo FindConstructor(string[] names, Type[] types) - { - var constructors = _type.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - foreach (ConstructorInfo ctor in constructors.OrderBy(c => c.IsPublic ? 0 : (c.IsPrivate ? 2 : 1)).ThenBy(c => c.GetParameters().Length)) - { - ParameterInfo[] ctorParameters = ctor.GetParameters(); - if (ctorParameters.Length == 0) - return ctor; - - if (ctorParameters.Length != types.Length) - continue; - - int i = 0; - for (; i < ctorParameters.Length; i++) - { - if (!String.Equals(ctorParameters[i].Name, names[i], StringComparison.OrdinalIgnoreCase)) - break; - if (types[i] == typeof(byte[]) && ctorParameters[i].ParameterType.FullName == SqlMapper.LinqBinary) - continue; - var unboxedType = Nullable.GetUnderlyingType(ctorParameters[i].ParameterType) ?? ctorParameters[i].ParameterType; - if (unboxedType != types[i] - && !(unboxedType.IsEnum && Enum.GetUnderlyingType(unboxedType) == types[i]) - && !(unboxedType == typeof(char) && types[i] == typeof(string))) - break; - } - - if (i == ctorParameters.Length) - return ctor; - } - - return null; - } - - /// - /// Gets mapping for constructor parameter - /// - /// Constructor to resolve - /// DataReader column name - /// Mapping implementation - public SqlMapper.IMemberMap GetConstructorParameter(ConstructorInfo constructor, string columnName) - { - var parameters = constructor.GetParameters(); - - return new SimpleMemberMap(columnName, parameters.FirstOrDefault(p => string.Equals(p.Name, columnName, StringComparison.OrdinalIgnoreCase))); - } - - /// - /// Gets member mapping for column - /// - /// DataReader column name - /// Mapping implementation - public SqlMapper.IMemberMap GetMember(string columnName) - { - var property = _properties.FirstOrDefault(p => string.Equals(p.Name, columnName, StringComparison.Ordinal)) - ?? _properties.FirstOrDefault(p => string.Equals(p.Name, columnName, StringComparison.OrdinalIgnoreCase)); - - if (property != null) - return new SimpleMemberMap(columnName, property); - - var field = _fields.FirstOrDefault(p => string.Equals(p.Name, columnName, StringComparison.Ordinal)) - ?? _fields.FirstOrDefault(p => string.Equals(p.Name, columnName, StringComparison.OrdinalIgnoreCase)); - - if (field != null) - return new SimpleMemberMap(columnName, field); - - return null; - } - } - - /// - /// Implements custom property mapping by user provided criteria (usually presence of some custom attribute with column to member mapping) - /// - sealed partial class CustomPropertyTypeMap : SqlMapper.ITypeMap - { - private readonly Type _type; - private readonly Func _propertySelector; - - /// - /// Creates custom property mapping - /// - /// Target entity type - /// Property selector based on target type and DataReader column name - public CustomPropertyTypeMap(Type type, Func propertySelector) - { - if (type == null) - throw new ArgumentNullException("type"); - - if (propertySelector == null) - throw new ArgumentNullException("propertySelector"); - - _type = type; - _propertySelector = propertySelector; - } - - /// - /// Always returns default constructor - /// - /// DataReader column names - /// DataReader column types - /// Default constructor - public ConstructorInfo FindConstructor(string[] names, Type[] types) - { - return _type.GetConstructor(new Type[0]); - } - - /// - /// Not impelmeneted as far as default constructor used for all cases - /// - /// - /// - /// - public SqlMapper.IMemberMap GetConstructorParameter(ConstructorInfo constructor, string columnName) - { - throw new NotSupportedException(); - } - - /// - /// Returns property based on selector strategy - /// - /// DataReader column name - /// Poperty member map - public SqlMapper.IMemberMap GetMember(string columnName) - { - var prop = _propertySelector(_type, columnName); - return prop != null ? new SimpleMemberMap(columnName, prop) : null; - } - } - - // Define DAPPER_MAKE_PRIVATE if you reference Dapper by source - // and you like to make the Dapper types private (in order to avoid - // conflicts with other projects that also reference Dapper by source) + } + else + { + + bool add = !command.Parameters.Contains(name); + IDbDataParameter p; + if (add) + { + p = command.CreateParameter(); + p.ParameterName = name; + } + else + { + p = (IDbDataParameter)command.Parameters[name]; + } + + p.Value = val ?? DBNull.Value; + p.Direction = param.ParameterDirection; + var s = val as string; + if (s != null) + { + if (s.Length <= 4000) + { + p.Size = 4000; + } + } + if (param.Size != null) + { + p.Size = param.Size.Value; + } + if (dbType != null) + { + p.DbType = dbType.Value; + } + if (add) + { + command.Parameters.Add(p); + } + param.AttachedParam = p; + } + + } + } + + /// + /// All the names of the param in the bag, use Get to yank them out + /// + public IEnumerable ParameterNames + { + get + { + return parameters.Select(p => p.Key); + } + } + + + /// + /// Get the value of a parameter + /// + /// + /// + /// The value, note DBNull.Value is not returned, instead the value is returned as null + public T Get(string name) + { + var val = parameters[Clean(name)].AttachedParam.Value; + if (val == DBNull.Value) + { + if (default(T) != null) + { + throw new ApplicationException("Attempting to cast a DBNull to a non nullable type!"); + } + return default(T); + } + return (T)val; + } + } + + /// + /// This class represents a SQL string, it can be used if you need to denote your parameter is a Char vs VarChar vs nVarChar vs nChar + /// + sealed partial class DbString + { + /// + /// Create a new DbString + /// + public DbString() { Length = -1; } + /// + /// Ansi vs Unicode + /// + public bool IsAnsi { get; set; } + /// + /// Fixed length + /// + public bool IsFixedLength { get; set; } + /// + /// Length of the string -1 for max + /// + public int Length { get; set; } + /// + /// The value of the string + /// + public string Value { get; set; } + /// + /// Add the parameter to the command... internal use only + /// + /// + /// + public void AddParameter(IDbCommand command, string name) + { + if (IsFixedLength && Length == -1) + { + throw new InvalidOperationException("If specifying IsFixedLength, a Length must also be specified"); + } + var param = command.CreateParameter(); + param.ParameterName = name; + param.Value = (object)Value ?? DBNull.Value; + if (Length == -1 && Value != null && Value.Length <= 4000) + { + param.Size = 4000; + } + else + { + param.Size = Length; + } + param.DbType = IsAnsi ? (IsFixedLength ? DbType.AnsiStringFixedLength : DbType.AnsiString) : (IsFixedLength ? DbType.StringFixedLength : DbType.String); + command.Parameters.Add(param); + } + } + + /// + /// Handles variances in features per DBMS + /// + partial class FeatureSupport + { + /// + /// Dictionary of supported features index by connection type name + /// + private static readonly Dictionary FeatureList = new Dictionary(StringComparer.InvariantCultureIgnoreCase) { + {"sqlserverconnection", new FeatureSupport { Arrays = false}}, + {"npgsqlconnection", new FeatureSupport {Arrays = true}} + }; + + /// + /// Gets the featureset based on the passed connection + /// + public static FeatureSupport Get(IDbConnection connection) + { + string name = connection.GetType().Name; + FeatureSupport features; + return FeatureList.TryGetValue(name, out features) ? features : FeatureList.Values.First(); + } + + /// + /// True if the db supports array columns e.g. Postgresql + /// + public bool Arrays { get; set; } + } + + /// + /// Represents simple memeber map for one of target parameter or property or field to source DataReader column + /// + sealed partial class SimpleMemberMap : SqlMapper.IMemberMap + { + private readonly string _columnName; + private readonly PropertyInfo _property; + private readonly FieldInfo _field; + private readonly ParameterInfo _parameter; + + /// + /// Creates instance for simple property mapping + /// + /// DataReader column name + /// Target property + public SimpleMemberMap(string columnName, PropertyInfo property) + { + if (columnName == null) + throw new ArgumentNullException("columnName"); + + if (property == null) + throw new ArgumentNullException("property"); + + _columnName = columnName; + _property = property; + } + + /// + /// Creates instance for simple field mapping + /// + /// DataReader column name + /// Target property + public SimpleMemberMap(string columnName, FieldInfo field) + { + if (columnName == null) + throw new ArgumentNullException("columnName"); + + if (field == null) + throw new ArgumentNullException("field"); + + _columnName = columnName; + _field = field; + } + + /// + /// Creates instance for simple constructor parameter mapping + /// + /// DataReader column name + /// Target constructor parameter + public SimpleMemberMap(string columnName, ParameterInfo parameter) + { + if (columnName == null) + throw new ArgumentNullException("columnName"); + + if (parameter == null) + throw new ArgumentNullException("parameter"); + + _columnName = columnName; + _parameter = parameter; + } + + /// + /// DataReader column name + /// + public string ColumnName + { + get { return _columnName; } + } + + /// + /// Target member type + /// + public Type MemberType + { + get + { + if (_field != null) + return _field.FieldType; + + if (_property != null) + return _property.PropertyType; + + if (_parameter != null) + return _parameter.ParameterType; + + return null; + } + } + + /// + /// Target property + /// + public PropertyInfo Property + { + get { return _property; } + } + + /// + /// Target field + /// + public FieldInfo Field + { + get { return _field; } + } + + /// + /// Target constructor parameter + /// + public ParameterInfo Parameter + { + get { return _parameter; } + } + } + + /// + /// Represents default type mapping strategy used by Dapper + /// + sealed partial class DefaultTypeMap : SqlMapper.ITypeMap + { + private readonly List _fields; + private readonly List _properties; + private readonly Type _type; + + /// + /// Creates default type map + /// + /// Entity type + public DefaultTypeMap(Type type) + { + if (type == null) + throw new ArgumentNullException("type"); + + _fields = GetSettableFields(type); + _properties = GetSettableProps(type); + _type = type; + } + + internal static MethodInfo GetPropertySetter(PropertyInfo propertyInfo, Type type) + { + return propertyInfo.DeclaringType == type ? + propertyInfo.GetSetMethod(true) : + propertyInfo.DeclaringType.GetProperty(propertyInfo.Name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).GetSetMethod(true); + } + + internal static List GetSettableProps(Type t) + { + return t + .GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + .Where(p => GetPropertySetter(p, t) != null) + .ToList(); + } + + internal static List GetSettableFields(Type t) + { + return t.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).ToList(); + } + + /// + /// Finds best constructor + /// + /// DataReader column names + /// DataReader column types + /// Matching constructor or default one + public ConstructorInfo FindConstructor(string[] names, Type[] types) + { + var constructors = _type.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + foreach (ConstructorInfo ctor in constructors.OrderBy(c => c.IsPublic ? 0 : (c.IsPrivate ? 2 : 1)).ThenBy(c => c.GetParameters().Length)) + { + ParameterInfo[] ctorParameters = ctor.GetParameters(); + if (ctorParameters.Length == 0) + return ctor; + + if (ctorParameters.Length != types.Length) + continue; + + int i = 0; + for (; i < ctorParameters.Length; i++) + { + if (!String.Equals(ctorParameters[i].Name, names[i], StringComparison.OrdinalIgnoreCase)) + break; + if (types[i] == typeof(byte[]) && ctorParameters[i].ParameterType.FullName == SqlMapper.LinqBinary) + continue; + var unboxedType = Nullable.GetUnderlyingType(ctorParameters[i].ParameterType) ?? ctorParameters[i].ParameterType; + if (unboxedType != types[i] + && !(unboxedType.IsEnum && Enum.GetUnderlyingType(unboxedType) == types[i]) + && !(unboxedType == typeof(char) && types[i] == typeof(string))) + break; + } + + if (i == ctorParameters.Length) + return ctor; + } + + return null; + } + + /// + /// Gets mapping for constructor parameter + /// + /// Constructor to resolve + /// DataReader column name + /// Mapping implementation + public SqlMapper.IMemberMap GetConstructorParameter(ConstructorInfo constructor, string columnName) + { + var parameters = constructor.GetParameters(); + + return new SimpleMemberMap(columnName, parameters.FirstOrDefault(p => string.Equals(p.Name, columnName, StringComparison.OrdinalIgnoreCase))); + } + + /// + /// Gets member mapping for column + /// + /// DataReader column name + /// Mapping implementation + public SqlMapper.IMemberMap GetMember(string columnName) + { + var property = _properties.FirstOrDefault(p => string.Equals(p.Name, columnName, StringComparison.Ordinal)) + ?? _properties.FirstOrDefault(p => string.Equals(p.Name, columnName, StringComparison.OrdinalIgnoreCase)); + + if (property != null) + return new SimpleMemberMap(columnName, property); + + var field = _fields.FirstOrDefault(p => string.Equals(p.Name, columnName, StringComparison.Ordinal)) + ?? _fields.FirstOrDefault(p => string.Equals(p.Name, columnName, StringComparison.OrdinalIgnoreCase)); + + if (field != null) + return new SimpleMemberMap(columnName, field); + + return null; + } + } + + /// + /// Implements custom property mapping by user provided criteria (usually presence of some custom attribute with column to member mapping) + /// + sealed partial class CustomPropertyTypeMap : SqlMapper.ITypeMap + { + private readonly Type _type; + private readonly Func _propertySelector; + + /// + /// Creates custom property mapping + /// + /// Target entity type + /// Property selector based on target type and DataReader column name + public CustomPropertyTypeMap(Type type, Func propertySelector) + { + if (type == null) + throw new ArgumentNullException("type"); + + if (propertySelector == null) + throw new ArgumentNullException("propertySelector"); + + _type = type; + _propertySelector = propertySelector; + } + + /// + /// Always returns default constructor + /// + /// DataReader column names + /// DataReader column types + /// Default constructor + public ConstructorInfo FindConstructor(string[] names, Type[] types) + { + return _type.GetConstructor(new Type[0]); + } + + /// + /// Not impelmeneted as far as default constructor used for all cases + /// + /// + /// + /// + public SqlMapper.IMemberMap GetConstructorParameter(ConstructorInfo constructor, string columnName) + { + throw new NotSupportedException(); + } + + /// + /// Returns property based on selector strategy + /// + /// DataReader column name + /// Poperty member map + public SqlMapper.IMemberMap GetMember(string columnName) + { + var prop = _propertySelector(_type, columnName); + return prop != null ? new SimpleMemberMap(columnName, prop) : null; + } + } + + // Define DAPPER_MAKE_PRIVATE if you reference Dapper by source + // and you like to make the Dapper types private (in order to avoid + // conflicts with other projects that also reference Dapper by source) #if !DAPPER_MAKE_PRIVATE - public partial class SqlMapper - { - } + public partial class SqlMapper + { + } - public partial class DynamicParameters - { + public partial class DynamicParameters + { - } + } - public partial class DbString - { + public partial class DbString + { - } + } - public partial class SimpleMemberMap - { + public partial class SimpleMemberMap + { - } + } - public partial class DefaultTypeMap - { + public partial class DefaultTypeMap + { - } + } - public partial class CustomPropertyTypeMap - { + public partial class CustomPropertyTypeMap + { - } + } - public partial class FeatureSupport - { + public partial class FeatureSupport + { - } + } #endif diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/Core/DeltaProcessor.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/Core/DeltaProcessor.cs new file mode 100644 index 0000000..8e5706d --- /dev/null +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/Core/DeltaProcessor.cs @@ -0,0 +1,32 @@ +using NewRelic.Platform.Sdk.Processors; + +namespace NewRelic.Microsoft.SqlServer.Plugin.Core +{ + /// + /// A processor for processing delta of metrics from the last poll cycle + /// + public class DeltaProcessor : IProcessor + { + private float? _lastVal = null; + + public float? Process(float? val) + { + float? returnVal = null; + + if (val.HasValue && _lastVal.HasValue) + { + returnVal = (val.Value - _lastVal.Value); + + // Negative values are not supported + if (returnVal < 0) + { + return null; + } + } + + _lastVal = val; + + return returnVal; + } + } +} diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/Core/Extensions/ExtensionsForEnumerable.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/Core/Extensions/ExtensionsForEnumerable.cs index a992d70..cd82491 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/Core/Extensions/ExtensionsForEnumerable.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/Core/Extensions/ExtensionsForEnumerable.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; namespace NewRelic.Microsoft.SqlServer.Plugin.Core.Extensions { public static class ExtensionsForEnumerable { + [DebuggerStepThrough] public static IEnumerable ForEach(this IEnumerable values, Action action) { var array = values.ToArray(); diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/Core/InstallController.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/Core/InstallController.cs index 248fcf5..9ddf330 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/Core/InstallController.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/Core/InstallController.cs @@ -16,196 +16,196 @@ namespace NewRelic.Microsoft.SqlServer.Plugin.Core { - internal class InstallController - { - private readonly ILog _installLog; - private readonly bool _isProcessElevated; - private readonly string _serviceName; - - public InstallController(string serviceName, bool isProcessElevated) - { - _serviceName = serviceName; - _isProcessElevated = isProcessElevated; - _installLog = LogManager.GetLogger(Constants.InstallLogger); - } - - public void Install() - { - Install(true); - StartService(); - } - - public void Uninstall() - { - Install(false); - } - - private void Install(bool install) - { - try - { - _installLog.Info(install ? "Installing" : "Uninstalling"); - using (var inst = new AssemblyInstaller(typeof (Program).Assembly, null)) - { - IDictionary state = new Hashtable(); - inst.UseNewContext = true; - - try - { - EnsureEveryoneHasPermissionsToWriteToLogFiles(); - - if (install) - { - inst.Install(state); - inst.Commit(state); - } - else - { - inst.Uninstall(state); - } - } - catch - { - try - { - inst.Rollback(state); - } - catch (Exception ex) - { - _installLog.Error("Error Rolling back"); - _installLog.Error(ex.Message); - } - throw; - } - } - } - catch (Exception ex) - { - ReportError(ex); - } - } - - public void StartService() - { - AdjustService(false); - } - - public void StopService() - { - AdjustService(true); - } - - private void AdjustService(bool stop) - { - var controller = GetServiceController(); - - if (controller == null) - { - _installLog.Info("Service not found"); - return; - } - - _installLog.Info("Service found"); - - try - { - EnsureEveryoneHasPermissionsToWriteToLogFiles(); - - if (stop) - { - if (controller.Status.Equals(ServiceControllerStatus.Running) && controller.CanStop) - { - _installLog.Info("Service is running. Attempting to stop service"); - controller.Stop(); - _installLog.Info("Service successfully stopped"); - } - else - { - _installLog.Info("Service is not running; skipping attempt to stop service"); - } - } - else - { - _installLog.Info("Attempting to start service"); - controller.Start(); - _installLog.Info("Service successfully started"); - } - } - catch (InvalidOperationException e) - { - ReportError(e); - } - } - - private ServiceController GetServiceController() - { - _installLog.InfoFormat("Checking if service {0} exists", _serviceName); - return ServiceController.GetServices().FirstOrDefault(s => s.ServiceName == _serviceName); - } - - public void InstallOrStart() - { - var serviceController = GetServiceController(); - if (serviceController == null) - { - Install(); - } - else - { - StartService(); - } - } - - private void ReportError(Exception ex) - { - if (_isProcessElevated) - { - _installLog.Error(ex.Message); - } - else - { - const string message = "This must be run from an Administrator prompt. Please see the readme.md for details."; - _installLog.Error(message); - } - } - - /// - /// The install controller requires that the process is elevated (aka Run as Administrator). - /// The log files are created by this administrative account. - /// Any non-admin account will NOT have permissions to write to these log files. - /// By adding full control to "Everyone", the non-admin service can write to these logs. - /// - private void EnsureEveryoneHasPermissionsToWriteToLogFiles() - { - _installLog.Debug("Resetting permissions on all log files"); - - const string log4NetConfig = "log4net.config"; - var assemblyPath = Assembly.GetExecutingAssembly().GetLocalPath(); - var configPath = Path.Combine(assemblyPath, log4NetConfig); - var log4netConfig = File.ReadAllText(configPath); - - var matches = Regex.Matches(log4netConfig, @"[^""]+)""\s*/>", RegexOptions.ExplicitCapture); - foreach (var logPath in matches.Cast().Select(m => m.Groups["logPath"].Value)) - { - if (!File.Exists(logPath)) - { - _installLog.WarnFormat("Attempting to reset permissions for log file but it is missing at '{0}'", logPath); - continue; - } - - try - { - var fileSecurity = new FileSecurity(logPath, AccessControlSections.Access); - var everyoneSid = new SecurityIdentifier(WellKnownSidType.WorldSid, null); - fileSecurity.SetAccessRule(new FileSystemAccessRule(everyoneSid, FileSystemRights.FullControl, AccessControlType.Allow)); - File.SetAccessControl(logPath, fileSecurity); - _installLog.DebugFormat("Reset permissions on '{0}'", logPath); - } - catch (Exception e) - { - _installLog.Error(string.Format("Failed to reset permissions on '{0}'", logPath), e); - } - } - } - } + internal class InstallController + { + private readonly ILog _installLog; + private readonly bool _isProcessElevated; + private readonly string _serviceName; + + public InstallController(string serviceName, bool isProcessElevated) + { + _serviceName = serviceName; + _isProcessElevated = isProcessElevated; + _installLog = LogManager.GetLogger(Constants.InstallLogger); + } + + public void Install() + { + Install(true); + StartService(); + } + + public void Uninstall() + { + Install(false); + } + + private void Install(bool install) + { + try + { + _installLog.Info(install ? "Installing" : "Uninstalling"); + using (var inst = new AssemblyInstaller(typeof (Program).Assembly, null)) + { + IDictionary state = new Hashtable(); + inst.UseNewContext = true; + + try + { + EnsureEveryoneHasPermissionsToWriteToLogFiles(); + + if (install) + { + inst.Install(state); + inst.Commit(state); + } + else + { + inst.Uninstall(state); + } + } + catch + { + try + { + inst.Rollback(state); + } + catch (Exception ex) + { + _installLog.Error("Error Rolling back"); + _installLog.Error(ex.Message); + } + throw; + } + } + } + catch (Exception ex) + { + ReportError(ex); + } + } + + public void StartService() + { + AdjustService(false); + } + + public void StopService() + { + AdjustService(true); + } + + private void AdjustService(bool stop) + { + var controller = GetServiceController(); + + if (controller == null) + { + _installLog.Info("Service not found"); + return; + } + + _installLog.Info("Service found"); + + try + { + EnsureEveryoneHasPermissionsToWriteToLogFiles(); + + if (stop) + { + if (controller.Status.Equals(ServiceControllerStatus.Running) && controller.CanStop) + { + _installLog.Info("Service is running. Attempting to stop service"); + controller.Stop(); + _installLog.Info("Service successfully stopped"); + } + else + { + _installLog.Info("Service is not running; skipping attempt to stop service"); + } + } + else + { + _installLog.Info("Attempting to start service"); + controller.Start(); + _installLog.Info("Service successfully started"); + } + } + catch (InvalidOperationException e) + { + ReportError(e); + } + } + + private ServiceController GetServiceController() + { + _installLog.InfoFormat("Checking if service {0} exists", _serviceName); + return ServiceController.GetServices().FirstOrDefault(s => s.ServiceName == _serviceName); + } + + public void InstallOrStart() + { + var serviceController = GetServiceController(); + if (serviceController == null) + { + Install(); + } + else + { + StartService(); + } + } + + private void ReportError(Exception ex) + { + if (_isProcessElevated) + { + _installLog.Error(ex.Message); + } + else + { + const string message = "This must be run from an Administrator prompt. Please see the readme.md for details."; + _installLog.Error(message); + } + } + + /// + /// The install controller requires that the process is elevated (aka Run as Administrator). + /// The log files are created by this administrative account. + /// Any non-admin account will NOT have permissions to write to these log files. + /// By adding full control to "Everyone", the non-admin service can write to these logs. + /// + private void EnsureEveryoneHasPermissionsToWriteToLogFiles() + { + _installLog.Debug("Resetting permissions on all log files"); + + const string log4NetConfig = "log4net.config"; + var assemblyPath = Assembly.GetExecutingAssembly().GetLocalPath(); + var configPath = Path.Combine(assemblyPath, log4NetConfig); + var log4netConfig = File.ReadAllText(configPath); + + var matches = Regex.Matches(log4netConfig, @"[^""]+)""\s*/>", RegexOptions.ExplicitCapture); + foreach (var logPath in matches.Cast().Select(m => m.Groups["logPath"].Value)) + { + if (!File.Exists(logPath)) + { + _installLog.WarnFormat("Attempting to reset permissions for log file but it is missing at '{0}'", logPath); + continue; + } + + try + { + var fileSecurity = new FileSecurity(logPath, AccessControlSections.Access); + var everyoneSid = new SecurityIdentifier(WellKnownSidType.WorldSid, null); + fileSecurity.SetAccessRule(new FileSystemAccessRule(everyoneSid, FileSystemRights.FullControl, AccessControlType.Allow)); + File.SetAccessControl(logPath, fileSecurity); + _installLog.DebugFormat("Reset permissions on '{0}'", logPath); + } + catch (Exception e) + { + _installLog.Error(string.Format("Failed to reset permissions on '{0}'", logPath), e); + } + } + } + } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/Core/MetricAttribute.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/Core/MetricAttribute.cs index 81ebca6..b9391ba 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/Core/MetricAttribute.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/Core/MetricAttribute.cs @@ -3,33 +3,38 @@ namespace NewRelic.Microsoft.SqlServer.Plugin.Core { - /// - /// Allows override of default conventions for discovering and naming metrics on QueryTypes. - /// - [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] - public sealed class MetricAttribute : Attribute - { - /// - /// Override the name of the metric recorded by the property the attribute is applied to. By default, the is used as the metric name. - /// - public string MetricName { get; set; } + /// + /// Allows override of default conventions for discovering and naming metrics on QueryTypes. + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] + public sealed class MetricAttribute : Attribute + { + /// + /// Override the name of the metric recorded by the property the attribute is applied to. By default, the is used as the metric name. + /// + public string MetricName { get; set; } - /// - /// Metrics have a notion of units that describe what the count or value represent. The format is [value_unit|count_unit]. - /// Common units include [MB/sec], [bytes], and [failure|attempts]. - /// - /// See more at https://newrelic.com/docs/platform/metric-api-and-naming-reference. - public string Units { get; set; } + /// + /// Metrics have a notion of units that describe what the count or value represent. The format is [value_unit|count_unit]. + /// Common units include [MB/sec], [bytes], and [failure|attempts]. + /// + /// See more at https://newrelic.com/docs/platform/metric-api-and-naming-reference. + public string Units { get; set; } - /// - /// Default is false. When true, prevents the property from being reported as a metric. - /// - public bool Ignore { get; set; } + /// + /// Default is false. When true, prevents the property from being reported as a metric. + /// + public bool Ignore { get; set; } - /// - /// Metric value type is based on the property type (e.g. int is a count, decimal is a value). The default is . - /// Any other value overrides the type-based determination of the value type. - /// - public MetricValueType MetricValueType { get; set; } - } + /// + /// Metric value type is based on the property type (e.g. int is a count, decimal is a value). The default is . + /// Any other value overrides the type-based determination of the value type. + /// + public MetricValueType MetricValueType { get; set; } + + /// + /// Dictates whether or not additional processing should be done over an individual metric before sending it to the New Relic servic.e + /// + public MetricTransformEnum MetricTransform { get; set; } + } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/Core/PollingThread.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/Core/PollingThread.cs index 5ed75ef..dbc7845 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/Core/PollingThread.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/Core/PollingThread.cs @@ -4,193 +4,187 @@ namespace NewRelic.Microsoft.SqlServer.Plugin.Core { - internal class PollingThread - { - private readonly ILog _log; - private readonly PollingThreadState _threadState; - - public PollingThread(PollingThreadSettings threadSettings, ILog log) - { - _log = log; - ThreadSettings = threadSettings; - _threadState = new PollingThreadState(); - } - - public bool Running - { - get { return _threadState.IsRunning; } - } - - public PollingThreadSettings ThreadSettings { get; private set; } - - public event Action ExceptionThrown = delegate { }; - - public void Start() - { - if (!_threadState.IsRunning) - { - ThreadStart tStart = ThreadStart; - ThreadSettings.Thread = new Thread(tStart) {Name = string.IsNullOrEmpty(ThreadSettings.Name) ? "PollingThread" : ThreadSettings.Name, IsBackground = true}; - ThreadSettings.Thread.Start(); - } - else if (_threadState.IsPaused(false)) - { - ThreadSettings.AutoResetEvent.Set(); - _threadState.Resume(); - } - } - - public void Pause() - { - _threadState.Pause(); - } - - public void Stop(bool waitForThreadExit) - { - _threadState.Stop(); - - if (ThreadSettings.AutoResetEvent != null) - { - ThreadSettings.AutoResetEvent.Set(); - } - - if (ThreadSettings.Thread != null && waitForThreadExit) - { - if (!ThreadSettings.Thread.Join(new TimeSpan(0, 0, 20))) - { - ThreadSettings.Thread.Abort(); - } - } - } - - private void ThreadStart() - { - try - { - ThreadLoop(); - } - catch (ThreadAbortException) - { - Thread.ResetAbort(); - } - finally - { - ThreadSettings.AutoResetEvent = null; - ThreadSettings.Thread = null; - _threadState.Stop(); - } - } - - private void ThreadLoop() - { - var errorCount = 0; - - _log.DebugFormat("{0}: Entering Thread Loop", ThreadSettings.Name); - - _threadState.Start(); - - if (ThreadSettings.InitialPollDelaySeconds > 0) - { - _log.DebugFormat("{0}: Initial Delay Sleep for {1}", ThreadSettings.Name, ThreadSettings.InitialPollDelaySeconds); - ThreadSettings.AutoResetEvent.WaitOne(TimeSpan.FromSeconds(ThreadSettings.InitialPollDelaySeconds), true); - } - - while (_threadState.IsRunning) - { - if (!_threadState.IsPaused(true)) - { - try - { - ThreadSettings.PollAction(); - } - catch (ThreadAbortException) - { - break; - } - catch (Exception ex) - { - ExceptionThrown(ex); - - if (++errorCount >= 3) - { - throw; - } - - _log.DebugFormat("Ignoring Error ({0} of 3):\r\n{1}", errorCount, ex); - } - } - else - { - _log.DebugFormat("{0}: Paused - skipping pass", ThreadSettings.Name); - } - - var interval = TimeSpan.FromSeconds(ThreadSettings.PollIntervalSeconds); - - _log.DebugFormat("{0}: Sleeping for {1}", ThreadSettings.Name, interval); - - if (ThreadSettings.AutoResetEvent.WaitOne(interval, true)) - { - _log.DebugFormat("{0}: Interrupted - woken up early", ThreadSettings.Name); - } - } - - _log.DebugFormat("{0}: Exiting Thread Loop", ThreadSettings.Name); - } - - private class PollingThreadState - { - private bool _pauseReset; - private bool _paused; - - public bool IsRunning { get; private set; } - - public bool IsPaused(bool reset) - { - if (_paused) - { - return true; - } - - if (_pauseReset) - { - if (reset) - { - _pauseReset = false; - } - - return true; - } - - return false; - } - - public void Start() - { - IsRunning = true; - _paused = false; - _pauseReset = false; - } - - public void Pause() - { - IsRunning = true; - _paused = true; - _pauseReset = false; - } - - public void Resume() - { - IsRunning = true; - _paused = true; - _pauseReset = true; - } - - public void Stop() - { - IsRunning = false; - _paused = false; - _pauseReset = false; - } - } - } + internal class PollingThread + { + private readonly ILog _log; + private readonly PollingThreadState _threadState; + + public PollingThread(PollingThreadSettings threadSettings, ILog log) + { + _log = log; + ThreadSettings = threadSettings; + _threadState = new PollingThreadState(); + } + + public bool Running + { + get { return _threadState.IsRunning; } + } + + public PollingThreadSettings ThreadSettings { get; private set; } + + public event Action ExceptionThrown = delegate { }; + + public void Start() + { + if (!_threadState.IsRunning) + { + ThreadStart tStart = ThreadStart; + ThreadSettings.Thread = new Thread(tStart) {Name = string.IsNullOrEmpty(ThreadSettings.Name) ? "PollingThread" : ThreadSettings.Name, IsBackground = true}; + ThreadSettings.Thread.Start(); + } + else if (_threadState.IsPaused(false)) + { + ThreadSettings.AutoResetEvent.Set(); + _threadState.Resume(); + } + } + + public void Pause() + { + _threadState.Pause(); + } + + public void Stop(bool waitForThreadExit) + { + _threadState.Stop(); + + if (ThreadSettings.AutoResetEvent != null) + { + ThreadSettings.AutoResetEvent.Set(); + } + + if (ThreadSettings.Thread != null && waitForThreadExit) + { + if (!ThreadSettings.Thread.Join(new TimeSpan(0, 0, 20))) + { + ThreadSettings.Thread.Abort(); + } + } + } + + private void ThreadStart() + { + try + { + ThreadLoop(); + } + catch (ThreadAbortException) + { + Thread.ResetAbort(); + } + finally + { + ThreadSettings.AutoResetEvent = null; + ThreadSettings.Thread = null; + _threadState.Stop(); + } + } + + private void ThreadLoop() + { + var errorCount = 0; + + _log.DebugFormat("{0}: Entering Thread Loop", ThreadSettings.Name); + + _threadState.Start(); + + while (_threadState.IsRunning) + { + if (!_threadState.IsPaused(true)) + { + try + { + ThreadSettings.PollAction(); + } + catch (ThreadAbortException) + { + break; + } + catch (Exception ex) + { + ExceptionThrown(ex); + + if (++errorCount >= 3) + { + throw; + } + + _log.DebugFormat("Ignoring Error ({0} of 3):\r\n{1}", errorCount, ex); + } + } + else + { + _log.DebugFormat("{0}: Paused - skipping pass", ThreadSettings.Name); + } + + var interval = TimeSpan.FromSeconds(ThreadSettings.PollIntervalSeconds); + + _log.DebugFormat("{0}: Sleeping for {1}", ThreadSettings.Name, interval); + + if (ThreadSettings.AutoResetEvent.WaitOne(interval, true)) + { + _log.DebugFormat("{0}: Interrupted - woken up early", ThreadSettings.Name); + } + } + + _log.DebugFormat("{0}: Exiting Thread Loop", ThreadSettings.Name); + } + + private class PollingThreadState + { + private bool _pauseReset; + private bool _paused; + + public bool IsRunning { get; private set; } + + public bool IsPaused(bool reset) + { + if (_paused) + { + return true; + } + + if (_pauseReset) + { + if (reset) + { + _pauseReset = false; + } + + return true; + } + + return false; + } + + public void Start() + { + IsRunning = true; + _paused = false; + _pauseReset = false; + } + + public void Pause() + { + IsRunning = true; + _paused = true; + _pauseReset = false; + } + + public void Resume() + { + IsRunning = true; + _paused = true; + _pauseReset = true; + } + + public void Stop() + { + IsRunning = false; + _paused = false; + _pauseReset = false; + } + } + } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/Core/PollingThreadSettings.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/Core/PollingThreadSettings.cs index 22532db..6b36592 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/Core/PollingThreadSettings.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/Core/PollingThreadSettings.cs @@ -3,40 +3,26 @@ namespace NewRelic.Microsoft.SqlServer.Plugin.Core { - internal class PollingThreadSettings - { - private long _initialPollDelaySeconds; - private long _pollIntervalSeconds = 60; + internal class PollingThreadSettings + { + private long _pollIntervalSeconds = 60; - public AutoResetEvent AutoResetEvent { get; set; } - public string Name { get; set; } - public Thread Thread { get; set; } - public Action PollAction { get; set; } + public AutoResetEvent AutoResetEvent { get; set; } + public string Name { get; set; } + public Thread Thread { get; set; } + public Action PollAction { get; set; } - public long InitialPollDelaySeconds - { - get { return _initialPollDelaySeconds; } - set - { - if (value < 0) - { - throw new ArgumentOutOfRangeException("value", "Value cannot be less than 0"); - } - _initialPollDelaySeconds = value; - } - } - - public long PollIntervalSeconds - { - get { return _pollIntervalSeconds; } - set - { - if (value < 0) - { - throw new ArgumentOutOfRangeException("value", "Value cannot be less than 0"); - } - _pollIntervalSeconds = value; - } - } - } + public long PollIntervalSeconds + { + get { return _pollIntervalSeconds; } + set + { + if (value < 0) + { + throw new ArgumentOutOfRangeException("value", "Value cannot be less than 0"); + } + _pollIntervalSeconds = value; + } + } + } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/Core/QueryAttribute.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/Core/QueryAttribute.cs index 8ab5518..2066208 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/Core/QueryAttribute.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/Core/QueryAttribute.cs @@ -42,11 +42,6 @@ protected QueryAttribute(string resourceName, string metricPattern) /// public bool Enabled { get; set; } - /// - /// Optional. Indicates how, once collected the metrics should be sent to New Relic. Defaults to Simple which does no transform and sends the most recent values. - /// - public MetricTransformEnum MetricTransformEnum { get; set; } - /// /// Required. The pattern is formatted and sent to New Relic. /// A pattern must start with 'Custom/'. A good metric supports wildcards for supporting better charts. diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/Core/SqlMonitorService.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/Core/SqlMonitorService.cs index b0fa853..2b38555 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/Core/SqlMonitorService.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/Core/SqlMonitorService.cs @@ -4,38 +4,38 @@ namespace NewRelic.Microsoft.SqlServer.Plugin.Core { - /// - /// Windows Service wrapper class. - /// - public class SqlMonitorService : ServiceBase - { - private readonly Settings _settings; - private SqlPoller _sqlPoller; + /// + /// Windows Service wrapper class. + /// + public class SqlMonitorService : ServiceBase + { + private readonly Settings _settings; + private SqlPoller _sqlPoller; - public SqlMonitorService(Settings settings) - { - _settings = settings; - ServiceName = settings.ServiceName; - } + public SqlMonitorService(Settings settings) + { + _settings = settings; + ServiceName = settings.ServiceName; + } - protected override void OnStart(string[] args) - { - if (_sqlPoller == null) - { - _sqlPoller = new SqlPoller(_settings); - } + protected override void OnStart(string[] args) + { + if (_sqlPoller == null) + { + _sqlPoller = new SqlPoller(_settings); + } - _sqlPoller.Start(); - } + _sqlPoller.Start(); + } - protected override void OnStop() - { - if (_sqlPoller != null) - { - _sqlPoller.Stop(); - } + protected override void OnStop() + { + if (_sqlPoller != null) + { + _sqlPoller.Stop(); + } - _sqlPoller = null; - } - } + _sqlPoller = null; + } + } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/Core/SqlPoller.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/Core/SqlPoller.cs index 73f6759..d69c721 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/Core/SqlPoller.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/Core/SqlPoller.cs @@ -1,108 +1,97 @@ using System; using System.Threading; - -using NewRelic.Microsoft.SqlServer.Plugin.Configuration; -using NewRelic.Microsoft.SqlServer.Plugin.Properties; - using log4net; - +using NewRelic.Microsoft.SqlServer.Plugin.Configuration; using NewRelic.Microsoft.SqlServer.Plugin.Core.Extensions; +using NewRelic.Microsoft.SqlServer.Plugin.Properties; namespace NewRelic.Microsoft.SqlServer.Plugin.Core { - public class SqlPoller - { - private readonly ILog _log; - private readonly MetricCollector _metricCollector; - private readonly Settings _settings; - private readonly object _syncRoot; - private PollingThread _pollingThread; - - public SqlPoller(Settings settings, ILog log = null) - { - _settings = settings; - _syncRoot = new object(); - _log = log ?? LogManager.GetLogger(Constants.SqlMonitorLogger); - _metricCollector = new MetricCollector(settings, _log); - } - - public void Start() - { - try - { - lock (_syncRoot) - { - if (_pollingThread != null) - { - return; - } - - _log.Info("----------------"); - _log.Info("Service Starting"); - - var queries = new QueryLocator(new DapperWrapper()).PrepareQueries(); - _settings.Endpoints.ForEach(e => e.SetQueries(queries)); - - var initialDelaySeconds = _settings.TestMode ? 0 : _settings.PollIntervalSeconds; - var pollingThreadSettings = new PollingThreadSettings - { - Name = "SqlPoller", - // Set to immediate when in TestMode for instant gratification. - InitialPollDelaySeconds = initialDelaySeconds, - PollIntervalSeconds = _settings.PollIntervalSeconds, - PollAction = () => _metricCollector.QueryEndpoints(queries), - AutoResetEvent = new AutoResetEvent(false), - }; - - _pollingThread = new PollingThread(pollingThreadSettings, _log); - _pollingThread.ExceptionThrown += e => _log.Error("Polling thread exception", e); - - _log.Debug("Service Threads Starting..."); - - if (initialDelaySeconds > 0) - { - _log.InfoFormat("Waiting {0} seconds to begin polling", initialDelaySeconds); - } - - _pollingThread.Start(); - - _log.Debug("Service Threads Started"); - } - } - catch (Exception e) - { - _log.Fatal("Failed while attempting to start service"); - _log.Warn(e); - throw; - } - } - - public void Stop() - { - lock (_syncRoot) - { - if (_pollingThread == null) - { - return; - } - - try - { - if (!_pollingThread.Running) - { - return; - } - - _log.Debug("Service Threads Stopping..."); - _pollingThread.Stop(true); - _log.Debug("Service Threads Stopped"); - } - finally - { - _pollingThread = null; - _log.Info("Service Stopped"); - } - } - } - } + public class SqlPoller + { + private readonly ILog _log; + private readonly MetricCollector _metricCollector; + private readonly Settings _settings; + private readonly object _syncRoot; + private PollingThread _pollingThread; + + public SqlPoller(Settings settings, ILog log = null) + { + _settings = settings; + _syncRoot = new object(); + _log = log ?? LogManager.GetLogger(Constants.SqlMonitorLogger); + _metricCollector = new MetricCollector(settings, _log); + } + + public void Start() + { + try + { + lock (_syncRoot) + { + if (_pollingThread != null) + { + return; + } + + _log.Info("----------------"); + _log.Info("Service Starting"); + + var queries = new QueryLocator(new DapperWrapper()).PrepareQueries(); + _settings.Endpoints.ForEach(e => e.SetQueries(queries)); + + var pollingThreadSettings = new PollingThreadSettings + { + Name = "SqlPoller", + PollIntervalSeconds = _settings.PollIntervalSeconds, + PollAction = () => _metricCollector.QueryEndpoints(queries), + AutoResetEvent = new AutoResetEvent(false), + }; + + _pollingThread = new PollingThread(pollingThreadSettings, _log); + _pollingThread.ExceptionThrown += e => _log.Error("Polling thread exception", e); + + _log.Debug("Service Threads Starting..."); + + _pollingThread.Start(); + + _log.Debug("Service Threads Started"); + } + } + catch (Exception e) + { + _log.Fatal("Failed while attempting to start service"); + _log.Warn(e); + throw; + } + } + + public void Stop() + { + lock (_syncRoot) + { + if (_pollingThread == null) + { + return; + } + + try + { + if (!_pollingThread.Running) + { + return; + } + + _log.Debug("Service Threads Stopping..."); + _pollingThread.Stop(true); + _log.Debug("Service Threads Stopped"); + } + finally + { + _pollingThread = null; + _log.Info("Service Stopped"); + } + } + } + } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/IMetricQuery.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/IMetricQuery.cs index fb63f26..8155c5d 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/IMetricQuery.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/IMetricQuery.cs @@ -4,13 +4,12 @@ namespace NewRelic.Microsoft.SqlServer.Plugin { - public interface IMetricQuery - { - MetricTransformEnum MetricTransformEnum { get; } - string QueryName { get; } - Type QueryType { get; } - string MetricPattern { get; } - string ResultTypeName { get; } - void AddMetrics(QueryContext context); - } + public interface IMetricQuery + { + string QueryName { get; } + Type QueryType { get; } + string MetricPattern { get; } + string ResultTypeName { get; } + void AddMetrics(QueryContext context); + } } \ No newline at end of file diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/IQueryContext.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/IQueryContext.cs index 86a2793..3c0d88e 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/IQueryContext.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/IQueryContext.cs @@ -2,24 +2,25 @@ using System.Collections.Generic; using NewRelic.Microsoft.SqlServer.Plugin.Core; -using NewRelic.Platform.Binding.DotNET; +using NewRelic.Platform.Sdk.Binding; +using NewRelic.Platform.Sdk.Processors; namespace NewRelic.Microsoft.SqlServer.Plugin { - public interface IQueryContext - { - string QueryName { get; } - Type QueryType { get; } - IEnumerable Results { get; set; } - ComponentData ComponentData { get; set; } - int MetricsRecorded { get; } - bool DataSent { get; set; } - MetricTransformEnum MetricTransformEnum { get; } - - string FormatMetricKey(object queryResult, string metricName, string metricUnits); - void AddAllMetrics(); - void AddMetric(string name, int value); - void AddMetric(string name, decimal value); - void AddMetric(string name, MinMaxMetricValue value); - } + public interface IQueryContext + { + string QueryName { get; } + Type QueryType { get; } + IEnumerable Results { get; set; } + string ComponentName { get; set; } + string ComponentGuid { get; set; } + IContext Context { get; set; } + int MetricsRecorded { get; } + IDictionary MetricProcessors { get; set; } + + string FormatMetricKey(object queryResult, string metricName); + void AddAllMetrics(); + void AddMetric(string name, string units, int value, MetricTransformEnum metricTransform); + void AddMetric(string name, string units, decimal value, MetricTransformEnum metricTransform); + } } \ No newline at end of file diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/ISqlEndpoint.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/ISqlEndpoint.cs index 6c0a535..8af8fd0 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/ISqlEndpoint.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/ISqlEndpoint.cs @@ -1,38 +1,21 @@ -using System; using System.Collections.Generic; -using NewRelic.Platform.Binding.DotNET; - using log4net; namespace NewRelic.Microsoft.SqlServer.Plugin { - public interface ISqlEndpoint - { - string Name { get; } - string ConnectionString { get; } - - /// - /// The number of seconds since the last recorded successful report of metrics. - /// - int Duration { get; } - - string[] IncludedDatabaseNames { get; } - string[] ExcludedDatabaseNames { get; } - - void SetQueries(IEnumerable queries); + public interface ISqlEndpoint + { + string Name { get; } + string ConnectionString { get; } - IEnumerable ExecuteQueries(ILog log); + string[] IncludedDatabaseNames { get; } + string[] ExcludedDatabaseNames { get; } - /// - /// Inform the server context that a report was sent on its behalf. Used to determine the - /// - /// - void MetricReportSuccessful(DateTime? reportDate = null); + void SetQueries(IEnumerable queries); - void UpdateHistory(IQueryContext[] queryContexts); - PlatformData GeneratePlatformData(AgentData agentData); + IEnumerable ExecuteQueries(ILog log); - void ToLog(ILog log); - } + void ToLog(ILog log); + } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/MetricCollector.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/MetricCollector.cs index da5039c..c9c1ee5 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/MetricCollector.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/MetricCollector.cs @@ -1,97 +1,92 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Threading.Tasks; - -using NewRelic.Microsoft.SqlServer.Plugin.Communication; +using log4net; using NewRelic.Microsoft.SqlServer.Plugin.Configuration; using NewRelic.Microsoft.SqlServer.Plugin.Core.Extensions; -using NewRelic.Platform.Binding.DotNET; - -using log4net; +using NewRelic.Platform.Sdk.Binding; +using NewRelic.Platform.Sdk.Processors; namespace NewRelic.Microsoft.SqlServer.Plugin { - /// - /// Polls SQL databases and reports the data back to a collector. - /// - internal class MetricCollector - { - private readonly AgentData _agentData; - private readonly ILog _log; - private readonly Settings _settings; - - public MetricCollector(Settings settings, ILog log) - { - _settings = settings; - _log = log; - _agentData = new AgentData {Host = Environment.MachineName, Pid = Process.GetCurrentProcess().Id, Version = _settings.Version,}; - } + /// + /// Polls SQL databases and reports the data back to a collector. + /// + internal class MetricCollector + { + private readonly IContext _context; + private readonly IDictionary _processors; + private readonly ILog _log; + private readonly Settings _settings; - /// - /// Performs the queries against the databases. - /// - /// - internal void QueryEndpoints(IEnumerable queries) - { - try - { - var tasks = _settings.Endpoints - .Select(endpoint => Task.Factory - .StartNew(() => endpoint.ExecuteQueries(_log)) - .Catch(e => _log.Error(e)) - .ContinueWith(t => t.Result.ForEach(ctx => ctx.AddAllMetrics())) - .Catch(e => _log.Error(e)) - .ContinueWith(t => - { - var queryContexts = t.Result.ToArray(); - endpoint.UpdateHistory(queryContexts); - SendComponentDataToCollector(endpoint); - return queryContexts.Sum(q => q.MetricsRecorded); - })) - .ToArray(); + public MetricCollector(Settings settings, ILog log) + { + _settings = settings; + _log = log; + _context = new Context(_settings.LicenseKey) { Version = _settings.Version }; + _processors = new Dictionary(); + } - // Wait for all of them to complete - Task.WaitAll(tasks.ToArray()); + /// + /// Performs the queries against the databases. + /// + /// + internal void QueryEndpoints(IEnumerable queries) + { + try + { + var tasks = _settings.Endpoints + .Select(endpoint => Task.Factory + .StartNew(() => endpoint.ExecuteQueries(_log)) + .Catch(e => _log.Error(e)) + .ContinueWith(t => t.Result.ForEach(ctx => + { + ctx.Context = _context; + ctx.MetricProcessors = _processors; + ctx.AddAllMetrics(); + })) + .Catch(e => _log.Error(e)) + .ContinueWith(t => + { + var queryContexts = t.Result.ToArray(); + return queryContexts.Sum(q => q.MetricsRecorded); + })) + .ToArray(); - _log.InfoFormat("Recorded {0} metrics", tasks.Sum(t => t.Result)); - } - catch (Exception e) - { - _log.Error(e); - } - } + // Wait for all of them to complete + Task.WaitAll(tasks.ToArray()); - /// - /// Sends data to New Relic, unless in "collect only" mode. - /// - /// SQL endpoint from which the metrics were harvested. - internal void SendComponentDataToCollector(ISqlEndpoint endpoint) - { - var platformData = endpoint.GeneratePlatformData(_agentData); + // This sends all components to the server in a single request, we may run into performance issues with one component delaying the others. + SendComponentDataToCollector(); - // Allows a testing mode that does not send data to New Relic - if (_settings.TestMode) - { - return; - } + _log.InfoFormat("Recorded {0} metrics", tasks.Sum(t => t.Result)); + } + catch (Exception e) + { + _log.Error(e); + } + } - try - { - _log.DebugFormat("Reporting metrics for {0} with duration {1}s", endpoint.Name, endpoint.Duration); + /// + /// Sends data to New Relic, unless in "collect only" mode. + /// + internal void SendComponentDataToCollector() + { + // Allows a testing mode that does not send data to New Relic + if (_settings.TestMode) + { + return; + } - // Record the report time as now. If SendData takes very long, the duration comes up short and the chart shows a drop out - var reportTime = DateTime.Now; - // Send the data to New Relic - new SqlRequest(_settings.LicenseKey) {Data = platformData}.SendData(); - // If send is error free, reset the last report date to calculate accurate duration - endpoint.MetricReportSuccessful(reportTime); - } - catch (Exception e) - { - _log.Error("Error sending data to connector", e); - } - } - } + try + { + _context.SendMetricsToService(); + } + catch (Exception e) + { + _log.Error("Error sending data to connector", e); + } + } + } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/MetricMapper.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/MetricMapper.cs index d41a07f..fc32cf6 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/MetricMapper.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/MetricMapper.cs @@ -7,181 +7,185 @@ namespace NewRelic.Microsoft.SqlServer.Plugin { - public class MetricMapper - { - private static readonly Type[] _NumericMappingTypes = new[] {typeof (long), typeof(int), typeof (byte), typeof (short), typeof (decimal),}; - private static readonly Type[] _NumericMetricTypes = new[] {typeof (decimal), typeof (int)}; - private readonly MapAction _metricSetter; - private readonly PropertyInfo _propertyInfo; - - internal MetricMapper(PropertyInfo propertyInfo) - { - _propertyInfo = propertyInfo; - - var attribute = propertyInfo.GetCustomAttribute(); - if (attribute != null && attribute.Ignore) - { - throw new ArgumentException(string.Format("The property {0}.{1} is marked to ignore and cannot be mapped.", propertyInfo.ReflectedType.FullName, propertyInfo.Name)); - } - - _metricSetter = GetMetricSetter(propertyInfo); - - if (_metricSetter == null) - { - throw new ArgumentException(string.Format("No convention-based mapping for {0} property {1}.{2}", propertyInfo.PropertyType.Name, propertyInfo.ReflectedType.FullName, propertyInfo.Name)); - } - - MetricName = GetMetricName(_propertyInfo, attribute); - MetricUnits = attribute != null ? attribute.Units : null; - } - - private MetricMapper(PropertyInfo propertyInfo, MapAction metricSetter, string metricName, string metricUnits) - { - _propertyInfo = propertyInfo; - _metricSetter = metricSetter; - MetricName = metricName; - MetricUnits = metricUnits; - } - - /// - /// The part of the metric name for this metric value. - /// - /// For example, the metric Custom/SqlCpuUsage/*/SystemIdle, would be SystemIdle. - public string MetricName { get; set; } - - /// - /// Metrics have a notion of units that describe what the count field and value field represent. Should be surrounded by braces. - /// - /// [bytes], [sec|op], [bytes/sec] - /// More info at https://newrelic.com/docs/platform/metric-api-and-naming-reference - public string MetricUnits { get; set; } - - private static string GetMetricName(PropertyInfo propertyInfo, MetricAttribute attribute) - { - return attribute == null || string.IsNullOrEmpty(attribute.MetricName) ? propertyInfo.Name : attribute.MetricName; - } - - private static MapAction GetMetricSetter(PropertyInfo propertyInfo) - { - var attribute = propertyInfo.GetCustomAttribute(); - var metricValueType = attribute != null ? attribute.MetricValueType : MetricValueType.Default; - switch (metricValueType) - { - case MetricValueType.Count: - if (propertyInfo.PropertyType == typeof (int)) - { - return AddCountMetric; - } - if (_NumericMappingTypes.Contains(propertyInfo.PropertyType)) - { - return ConvertToCountMetric; - } - return null; - - case MetricValueType.Value: - if (propertyInfo.PropertyType == typeof (decimal)) - { - return AddValueMetric; - } - if (_NumericMappingTypes.Contains(propertyInfo.PropertyType)) - { - return ConvertToValueMetric; - } - - return null; - - default: - return GetMetricSetterByConvention(propertyInfo); - } - } - - private static MapAction GetMetricSetterByConvention(PropertyInfo propertyInfo) - { - if (propertyInfo.PropertyType == typeof (int)) - { - return AddCountMetric; - } - - if (propertyInfo.PropertyType == typeof (decimal)) - { - return AddValueMetric; - } - - if (_NumericMappingTypes.Contains(propertyInfo.PropertyType)) - { - return ConvertToCountMetric; - } - - return null; - } - - public void AddMetric(QueryContext queryContext, object result) - { - var metricName = queryContext.FormatMetricKey(result, MetricName, MetricUnits); - _metricSetter(queryContext, metricName, _propertyInfo, result); - } - - private static void AddCountMetric(QueryContext queryContext, string metricName, PropertyInfo propertyInfo, object result) - { - queryContext.AddMetric(metricName, (int) propertyInfo.GetValue(result, null)); - } - - private static void ConvertToCountMetric(QueryContext queryContext, string metricName, PropertyInfo propertyInfo, object result) - { - // If the query result is a bigint, an exception is thrown when attempting to make it an int. - var potentiallyLargeValue = Convert.ToInt64(propertyInfo.GetValue(result, null)); - if (potentiallyLargeValue >= int.MaxValue) - { - // Best we can do when the value is > int.MaxValue - queryContext.AddMetric(metricName, int.MaxValue); - } - else - { - var value = Convert.ToInt32(potentiallyLargeValue); - queryContext.AddMetric(metricName, value); - } - } - - private static void AddValueMetric(QueryContext queryContext, string metricName, PropertyInfo propertyInfo, object result) - { - queryContext.AddMetric(metricName, (decimal) propertyInfo.GetValue(result, null)); - } - - private static void ConvertToValueMetric(QueryContext queryContext, string metricName, PropertyInfo propertyInfo, object result) - { - // If the query result is a bigint, an exception is thrown when attempting to make it an decimal. - var potentiallyLargeValue = Convert.ToInt64(propertyInfo.GetValue(result, null)); - if (potentiallyLargeValue >= decimal.MaxValue) - { - // Best we can do when the value is > decimal.MaxValue - queryContext.AddMetric(metricName, decimal.MaxValue); - } - else - { - var value = Convert.ToDecimal(potentiallyLargeValue); - queryContext.AddMetric(metricName, value); - } - } - - public static bool IsMetricNumeric(object metricValue) - { - return _NumericMetricTypes.Contains(metricValue.GetType()); - } - - public static MetricMapper TryCreate(PropertyInfo propertyInfo) - { - var attribute = propertyInfo.GetCustomAttribute(); - if (attribute != null && attribute.Ignore) - { - return null; - } - - var setter = GetMetricSetter(propertyInfo); - var metricName = GetMetricName(propertyInfo, attribute); - var metricUnits = attribute != null ? attribute.Units : null; - return setter != null ? new MetricMapper(propertyInfo, setter, metricName, metricUnits) : null; - } - - private delegate void MapAction(QueryContext queryContext, string metricName, PropertyInfo propertyInfo, object result); - } + public class MetricMapper + { + private static readonly Type[] _NumericMappingTypes = new[] {typeof (long), typeof(int), typeof (byte), typeof (short), typeof (decimal),}; + private static readonly Type[] _NumericMetricTypes = new[] {typeof (decimal), typeof (int)}; + private readonly MapAction _metricSetter; + private readonly PropertyInfo _propertyInfo; + + internal MetricMapper(PropertyInfo propertyInfo) + { + _propertyInfo = propertyInfo; + + var attribute = propertyInfo.GetCustomAttribute(); + if (attribute != null && attribute.Ignore) + { + throw new ArgumentException(string.Format("The property {0}.{1} is marked to ignore and cannot be mapped.", propertyInfo.ReflectedType.FullName, propertyInfo.Name)); + } + + _metricSetter = GetMetricSetter(propertyInfo); + + if (_metricSetter == null) + { + throw new ArgumentException(string.Format("No convention-based mapping for {0} property {1}.{2}", propertyInfo.PropertyType.Name, propertyInfo.ReflectedType.FullName, propertyInfo.Name)); + } + + MetricName = GetMetricName(_propertyInfo, attribute); + MetricUnits = attribute != null ? attribute.Units : "unit"; + } + + private MetricMapper(PropertyInfo propertyInfo, MapAction metricSetter, string metricName, string metricUnits, MetricTransformEnum metricTransform) + { + _propertyInfo = propertyInfo; + _metricSetter = metricSetter; + MetricName = metricName; + MetricUnits = metricUnits; + MetricTransform = metricTransform; + } + + /// + /// The part of the metric name for this metric value. + /// + /// For example, the metric Custom/SqlCpuUsage/*/SystemIdle, would be SystemIdle. + public string MetricName { get; set; } + + /// + /// Metrics have a notion of units that describe what the count field and value field represent. Should be surrounded by braces. + /// + /// [bytes], [sec|op], [bytes/sec] + /// More info at https://newrelic.com/docs/platform/metric-api-and-naming-reference + public string MetricUnits { get; set; } + + public MetricTransformEnum MetricTransform { get; set; } + + private static string GetMetricName(PropertyInfo propertyInfo, MetricAttribute attribute) + { + return attribute == null || string.IsNullOrEmpty(attribute.MetricName) ? propertyInfo.Name : attribute.MetricName; + } + + private static MapAction GetMetricSetter(PropertyInfo propertyInfo) + { + var attribute = propertyInfo.GetCustomAttribute(); + var metricValueType = attribute != null ? attribute.MetricValueType : MetricValueType.Default; + switch (metricValueType) + { + case MetricValueType.Count: + if (propertyInfo.PropertyType == typeof (int)) + { + return AddCountMetric; + } + if (_NumericMappingTypes.Contains(propertyInfo.PropertyType)) + { + return ConvertToCountMetric; + } + return null; + + case MetricValueType.Value: + if (propertyInfo.PropertyType == typeof (decimal)) + { + return AddValueMetric; + } + if (_NumericMappingTypes.Contains(propertyInfo.PropertyType)) + { + return ConvertToValueMetric; + } + + return null; + + default: + return GetMetricSetterByConvention(propertyInfo); + } + } + + private static MapAction GetMetricSetterByConvention(PropertyInfo propertyInfo) + { + if (propertyInfo.PropertyType == typeof (int)) + { + return AddCountMetric; + } + + if (propertyInfo.PropertyType == typeof (decimal)) + { + return AddValueMetric; + } + + if (_NumericMappingTypes.Contains(propertyInfo.PropertyType)) + { + return ConvertToCountMetric; + } + + return null; + } + + public void AddMetric(QueryContext queryContext, object result) + { + var metricName = queryContext.FormatMetricKey(result, MetricName); + _metricSetter(queryContext, metricName, MetricUnits, MetricTransform, _propertyInfo, result); + } + + private static void AddCountMetric(QueryContext queryContext, string metricName, string metricUnits, MetricTransformEnum metricTransform, PropertyInfo propertyInfo, object result) + { + queryContext.AddMetric(metricName, metricUnits, (int) propertyInfo.GetValue(result, null), metricTransform); + } + + private static void ConvertToCountMetric(QueryContext queryContext, string metricName, string metricUnits, MetricTransformEnum metricTransform, PropertyInfo propertyInfo, object result) + { + // If the query result is a bigint, an exception is thrown when attempting to make it an int. + var potentiallyLargeValue = Convert.ToInt64(propertyInfo.GetValue(result, null)); + if (potentiallyLargeValue >= int.MaxValue) + { + // Best we can do when the value is > int.MaxValue + queryContext.AddMetric(metricName, metricUnits, int.MaxValue, metricTransform); + } + else + { + var value = Convert.ToInt32(potentiallyLargeValue); + queryContext.AddMetric(metricName, metricUnits, value, metricTransform); + } + } + + private static void AddValueMetric(QueryContext queryContext, string metricName, string metricUnits, MetricTransformEnum metricTransform, PropertyInfo propertyInfo, object result) + { + queryContext.AddMetric(metricName, metricUnits, (decimal) propertyInfo.GetValue(result, null), metricTransform); + } + + private static void ConvertToValueMetric(QueryContext queryContext, string metricName, string metricUnits, MetricTransformEnum metricTransform, PropertyInfo propertyInfo, object result) + { + // If the query result is a bigint, an exception is thrown when attempting to make it an decimal. + var potentiallyLargeValue = Convert.ToInt64(propertyInfo.GetValue(result, null)); + if (potentiallyLargeValue >= decimal.MaxValue) + { + // Best we can do when the value is > decimal.MaxValue + queryContext.AddMetric(metricName, metricUnits, decimal.MaxValue, metricTransform); + } + else + { + var value = Convert.ToDecimal(potentiallyLargeValue); + queryContext.AddMetric(metricName, metricUnits, value, metricTransform); + } + } + + public static bool IsMetricNumeric(object metricValue) + { + return _NumericMetricTypes.Contains(metricValue.GetType()); + } + + public static MetricMapper TryCreate(PropertyInfo propertyInfo) + { + var attribute = propertyInfo.GetCustomAttribute(); + if (attribute != null && attribute.Ignore) + { + return null; + } + + var setter = GetMetricSetter(propertyInfo); + var metricName = GetMetricName(propertyInfo, attribute); + var metricUnits = attribute != null ? attribute.Units : "unit"; + var metricTransform = attribute != null ? attribute.MetricTransform : MetricTransformEnum.Simple; + return setter != null ? new MetricMapper(propertyInfo, setter, metricName, metricUnits, metricTransform) : null; + } + + private delegate void MapAction(QueryContext queryContext, string metricName, string metricUnits, MetricTransformEnum processor, PropertyInfo propertyInfo, object result); + } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/MetricQuery.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/MetricQuery.cs index e837be2..a213013 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/MetricQuery.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/MetricQuery.cs @@ -2,55 +2,51 @@ using System.Linq; using System.Reflection; -using NewRelic.Microsoft.SqlServer.Plugin.Core; using NewRelic.Microsoft.SqlServer.Plugin.Core.Extensions; namespace NewRelic.Microsoft.SqlServer.Plugin { - public class MetricQuery : IMetricQuery - { - private readonly MetricMapper[] _metricMappers; - - public MetricQuery(Type queryType, string queryName, string resultTypeName, MetricTransformEnum metricTransformEnum = MetricTransformEnum.Simple) - { - QueryType = queryType; - QueryName = queryName; - ResultTypeName = resultTypeName; - _metricMappers = GetMappers(QueryType); - MetricPattern = string.Format("Component/{0}", QueryType.Name); - MetricTransformEnum = metricTransformEnum; - } - - public Type QueryType { get; private set; } - - public string QueryName { get; private set; } - - public MetricTransformEnum MetricTransformEnum { get; protected set; } - - public string MetricPattern { get; protected set; } - - public string ResultTypeName { get; private set; } - - public void AddMetrics(QueryContext context) - { - context.Results.ForEach(r => _metricMappers.ForEach(m => m.AddMetric(context, r))); - } - - /// - /// Sets up the mappers that take the values on the query result and records each one as a metric. - /// - /// - /// QueryType to look for metric properties - /// - /// - /// An array of mappers capable of creating metrics for a QueryType - /// - internal static MetricMapper[] GetMappers(Type type) - { - return type.GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Select(MetricMapper.TryCreate) - .Where(m => m != null) - .ToArray(); - } - } + public class MetricQuery : IMetricQuery + { + private readonly MetricMapper[] _metricMappers; + + public MetricQuery(Type queryType, string queryName, string resultTypeName) + { + QueryType = queryType; + QueryName = queryName; + MetricPattern = string.Format("{0}", QueryType.Name); + ResultTypeName = resultTypeName; + _metricMappers = GetMappers(QueryType); + } + + public Type QueryType { get; private set; } + + public string QueryName { get; private set; } + + public string MetricPattern { get; protected set; } + + public string ResultTypeName { get; private set; } + + public void AddMetrics(QueryContext context) + { + context.Results.ForEach(r => _metricMappers.ForEach(m => m.AddMetric(context, r))); + } + + /// + /// Sets up the mappers that take the values on the query result and records each one as a metric. + /// + /// + /// QueryType to look for metric properties + /// + /// + /// An array of mappers capable of creating metrics for a QueryType + /// + internal static MetricMapper[] GetMappers(Type type) + { + return type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Select(MetricMapper.TryCreate) + .Where(m => m != null) + .ToArray(); + } + } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/NewRelic.Microsoft.SqlServer.Plugin.csproj b/src/NewRelic.Microsoft.SqlServer.Plugin/NewRelic.Microsoft.SqlServer.Plugin.csproj index e766740..e26ac93 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/NewRelic.Microsoft.SqlServer.Plugin.csproj +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/NewRelic.Microsoft.SqlServer.Plugin.csproj @@ -41,8 +41,8 @@ ..\..\packages\log4net.2.0.0\lib\net35-full\log4net.dll - - ..\..\lib\NewRelic.Platform.Binding.DotNET.dll + + ..\..\lib\NewRelic.Platform.Sdk.dll ..\..\packages\Newtonsoft.Json.5.0.4\lib\net35\Newtonsoft.Json.dll @@ -68,12 +68,12 @@ - + @@ -136,7 +136,6 @@ Component - diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/Program.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/Program.cs index e1de7b2..7209369 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/Program.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/Program.cs @@ -16,124 +16,124 @@ namespace NewRelic.Microsoft.SqlServer.Plugin { - internal class Program - { - private static int Main(string[] args) - { - try - { - var log = SetUpLogConfig(); - - var options = Options.ParseArguments(args); - - var settings = ConfigurationParser.ParseSettings(log, options.ConfigFile); - Settings.Default = settings; - - var installController = new InstallController(settings.ServiceName, settings.IsProcessElevated); - if (options.Uninstall) - { - installController.Uninstall(); - } - else if (options.Install) - { - installController.Install(); - if (options.Start) - installController.StartService(); - } - else if (options.Start) - { - installController.StartService(); - } - else if (options.Stop) - { - installController.StopService(); - } - else if (options.InstallOrStart) - { - installController.InstallOrStart(); - } - else - { - Thread.CurrentThread.Name = "Main"; - settings.TestMode = options.TestMode; - log.InfoFormat("New Relic Sql Server Plugin"); - log.Info("Loaded Settings:"); - settings.ToLog(log); - - if (!settings.Endpoints.Any()) - { - log.Error("No sql endpoints found please, update the configuration file to monitor one or more sql server instances."); - } - - if (Environment.UserInteractive) - { - RunInteractive(settings); - } - else - { - ServiceBase[] services = {new SqlMonitorService(settings)}; - ServiceBase.Run(services); - } - } - - return 0; - } - catch (Exception ex) - { - Console.Out.WriteLine(ex.Message); - - if (Environment.UserInteractive) - { - Console.Out.WriteLine(); - Console.Out.WriteLine("Press any key to exit..."); - Console.ReadKey(); - } - - return -1; - } - } - - public static ILog SetUpLogConfig() - { - const string log4NetConfig = "log4net.config"; - var assemblyPath = Assembly.GetExecutingAssembly().GetLocalPath(); - var configPath = Path.Combine(assemblyPath, log4NetConfig); - XmlConfigurator.ConfigureAndWatch(new FileInfo(configPath)); - return LogManager.GetLogger(Constants.SqlMonitorLogger); - } - - /// - /// Runs from the command shell, printing to the Console. - /// - /// - private static void RunInteractive(Settings settings) - { - // Start our services - var poller = new SqlPoller(settings); - poller.Start(); - - // Capture Ctrl+C - Console.TreatControlCAsInput = true; - - char key; - do - { - Console.Out.WriteLine("Press Q to quit..."); - var consoleKeyInfo = Console.ReadKey(true); - Console.WriteLine(); - key = consoleKeyInfo.KeyChar; - } while (key != 'q' && key != 'Q'); - - // Stop our services - poller.Stop(); + internal class Program + { + private static int Main(string[] args) + { + try + { + var log = SetUpLogConfig(); + + var options = Options.ParseArguments(args); + + var settings = ConfigurationParser.ParseSettings(log, options.ConfigFile); + Settings.Default = settings; + + var installController = new InstallController(settings.ServiceName, settings.IsProcessElevated); + if (options.Uninstall) + { + installController.Uninstall(); + } + else if (options.Install) + { + installController.Install(); + if (options.Start) + installController.StartService(); + } + else if (options.Start) + { + installController.StartService(); + } + else if (options.Stop) + { + installController.StopService(); + } + else if (options.InstallOrStart) + { + installController.InstallOrStart(); + } + else + { + Thread.CurrentThread.Name = "Main"; + settings.TestMode = options.TestMode; + log.InfoFormat("New Relic Sql Server Plugin"); + log.Info("Loaded Settings:"); + settings.ToLog(log); + + if (!settings.Endpoints.Any()) + { + log.Error("No sql endpoints found please, update the configuration file to monitor one or more sql server instances."); + } + + if (Environment.UserInteractive) + { + RunInteractive(settings); + } + else + { + ServiceBase[] services = {new SqlMonitorService(settings)}; + ServiceBase.Run(services); + } + } + + return 0; + } + catch (Exception ex) + { + Console.Out.WriteLine(ex.Message); + + if (Environment.UserInteractive) + { + Console.Out.WriteLine(); + Console.Out.WriteLine("Press any key to exit..."); + Console.ReadKey(); + } + + return -1; + } + } + + public static ILog SetUpLogConfig() + { + const string log4NetConfig = "log4net.config"; + var assemblyPath = Assembly.GetExecutingAssembly().GetLocalPath(); + var configPath = Path.Combine(assemblyPath, log4NetConfig); + XmlConfigurator.ConfigureAndWatch(new FileInfo(configPath)); + return LogManager.GetLogger(Constants.SqlMonitorLogger); + } + + /// + /// Runs from the command shell, printing to the Console. + /// + /// + private static void RunInteractive(Settings settings) + { + // Start our services + var poller = new SqlPoller(settings); + poller.Start(); + + // Capture Ctrl+C + Console.TreatControlCAsInput = true; + + char key; + do + { + Console.Out.WriteLine("Press Q to quit..."); + var consoleKeyInfo = Console.ReadKey(true); + Console.WriteLine(); + key = consoleKeyInfo.KeyChar; + } while (key != 'q' && key != 'Q'); + + // Stop our services + poller.Stop(); #if DEBUG - if (Debugger.IsAttached) - { - Console.Out.WriteLine("Press any key to stop debugging..."); - Console.ReadKey(); - } + if (Debugger.IsAttached) + { + Console.Out.WriteLine("Press any key to stop debugging..."); + Console.ReadKey(); + } #endif - } - } + } + } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/Properties/ServiceConstants.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/Properties/ServiceConstants.cs index 2984540..218c03a 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/Properties/ServiceConstants.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/Properties/ServiceConstants.cs @@ -2,11 +2,11 @@ namespace NewRelic.Microsoft.SqlServer.Plugin.Properties { - public static class ServiceConstants - { - public const string Description = "Gathers SQL Server metrics and reports them to New Relic"; - public const string DisplayName = "New Relic SQL Server Plugin"; - public const string ServiceName = "NewRelicSQLServerPlugin"; - public const ServiceStartMode StartType = ServiceStartMode.Automatic; - } + public static class ServiceConstants + { + public const string Description = "Gathers SQL Server metrics and reports them to New Relic"; + public const string DisplayName = "New Relic SQL Server Plugin"; + public const string ServiceName = "NewRelicSQLServerPlugin"; + public const ServiceStartMode StartType = ServiceStartMode.Automatic; + } } \ No newline at end of file diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/QueryContext.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/QueryContext.cs index 6f4f80c..5d72f19 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/QueryContext.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/QueryContext.cs @@ -3,164 +3,177 @@ using System.Reflection; using System.Text.RegularExpressions; +using log4net; + using NewRelic.Microsoft.SqlServer.Plugin.Core; using NewRelic.Microsoft.SqlServer.Plugin.Properties; using NewRelic.Microsoft.SqlServer.Plugin.QueryTypes; -using NewRelic.Platform.Binding.DotNET; - -using log4net; +using NewRelic.Platform.Sdk.Binding; +using NewRelic.Platform.Sdk.Processors; namespace NewRelic.Microsoft.SqlServer.Plugin { - public class QueryContext : IQueryContext - { - private static readonly ILog _VerboseMetricsLogger = LogManager.GetLogger(Constants.VerboseMetricsLogger); - - private readonly DateTime _creationTime; - private readonly IMetricQuery _query; - - public QueryContext(IMetricQuery query) - { - _query = query; - _creationTime = DateTime.Now; - } - - public DateTime CreationTime - { - get { return _creationTime; } - } - - public bool DataSent { get; set; } - - public string QueryName - { - get { return _query.QueryName; } - } - - public Type QueryType - { - get { return _query.QueryType; } - } - - public MetricTransformEnum MetricTransformEnum - { - get { return _query.MetricTransformEnum; } - } - - public IEnumerable Results { get; set; } - public ComponentData ComponentData { get; set; } - public int MetricsRecorded { get; private set; } - - public string FormatMetricKey(object queryResult, string metricName, string metricUnits) - { - return FormatMetricKey(_query.MetricPattern, queryResult, metricName, metricUnits); - } - - public void AddAllMetrics() - { - _query.AddMetrics(this); - } - - public void AddMetric(string name, int value) - { - _VerboseMetricsLogger.InfoFormat("Gathering Component: {0}; Metric: {1}; Value: {2}", ComponentData.Name, name, value); - ComponentData.AddMetric(name, value); - MetricsRecorded++; - } - - public void AddMetric(string name, decimal value) - { - _VerboseMetricsLogger.InfoFormat("Gathering Component: {0}; Metric: {1}; Value: {2}", ComponentData.Name, name, value); - ComponentData.AddMetric(name, value); - MetricsRecorded++; - } - - public void AddMetric(string name, MinMaxMetricValue value) - { - _VerboseMetricsLogger.InfoFormat("Gathering Component: {0}; Metric: {1}; Value: {2}", ComponentData.Name, name, value); - ComponentData.AddMetric(name, value); - MetricsRecorded++; - } - - internal static string FormatMetricKey(string pattern, object queryResult, string metricName, string units = null) - { - var result = pattern; - - if (result.Contains("{DatabaseName}")) - { - var databaseMetric = queryResult as IDatabaseMetric; - var databaseName = databaseMetric != null ? databaseMetric.DatabaseName : "(none)"; - result = result.Replace("{DatabaseName}", databaseName); - } - - if (result.Contains("{MetricName}")) - { - result = result.Replace("{MetricName}", metricName); - } - else - { - result = result.EndsWith("/") ? result + metricName : result + "/" + metricName; - } - - if (!string.IsNullOrEmpty(units)) - { - result += units; - } - - var matches = Regex.Matches(result, @"\{(?[^}]+?)\}", RegexOptions.ExplicitCapture); - if (matches.Count <= 0) - { - return result; - } - - // Find placeholders - var queryType = queryResult.GetType(); - foreach (Match match in matches) - { - // Get the property match - var propertyName = match.Groups["property"].Value; - // Get the property - var propertyInfo = queryType.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance); - // Expect it to be a public instance property - if (propertyInfo == null) - { - // Look for a similarly named property where maybe the case is mismatched. Performance is unimportant as this is a fatal error. - if (queryType.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase) != null) - { - throw new Exception(string.Format("MetricPattern '{0}' contains a placeholder '{1}' for '{2}', however, it seems the placeholder has a case-mismatch." + - "The placeholder is case-sensitive.", - pattern, - propertyName, - queryType.Name)); - } - throw new Exception(string.Format("MetricPattern '{0}' contains a placeholder '{1}' which was not found as a property on '{2}'. " + - "It must be a public, instance property with a getter.", - pattern, - propertyName, - queryType.Name)); - } - // It must have a public getter - if (!propertyInfo.CanRead || propertyInfo.GetGetMethod(false) == null) - { - throw new Exception(string.Format("MetricPattern '{0}' contains a placeholder for the property '{1}' on '{2}', however, it does not have a getter. " + - "It must be a public, instance property with a getter.", - pattern, - propertyName, - queryType.Name)); - } - // Get the value - var propertyValue = propertyInfo.GetValue(queryResult, null); - // Try first as a string (most common), then ToString() when not null, else just the word "null" - var replacement = propertyValue as string ?? (propertyValue != null ? propertyValue.ToString() : "null"); - // No leading or trailing whitespace - replacement = replacement.Trim(); - // Replace all non-alphanumerics with underbar - var safeReplacement = Regex.Replace(replacement, @"[^\w\d]", "_", RegexOptions.Singleline); - // Finally, replace it in the metric pattern - result = result.Replace("{" + propertyName + "}", safeReplacement); - } - - return result; - } - } + public class QueryContext : IQueryContext + { + private static readonly ILog _VerboseMetricsLogger = LogManager.GetLogger(Constants.VerboseMetricsLogger); + + private readonly DateTime _creationTime; + private readonly IMetricQuery _query; + + public QueryContext(IMetricQuery query) + { + _query = query; + _creationTime = DateTime.Now; + } + + public string QueryName + { + get { return _query.QueryName; } + } + + public Type QueryType + { + get { return _query.QueryType; } + } + + public IEnumerable Results { get; set; } + public string ComponentName { get; set; } + public string ComponentGuid { get; set; } + public IContext Context { get; set; } + public int MetricsRecorded { get; private set; } + public IDictionary MetricProcessors { get; set; } + + public string FormatMetricKey(object queryResult, string metricName) + { + return FormatMetricKey(_query.MetricPattern, queryResult, metricName); + } + + public void AddAllMetrics() + { + _query.AddMetrics(this); + } + + public void AddMetric(string name, string units, int value, MetricTransformEnum transform) + { + _VerboseMetricsLogger.InfoFormat("Gathering Component: {0}; Metric: {1}; Value: {2}", ComponentName, name, value); + float? val = value; + + if (transform != MetricTransformEnum.Simple) + { + string key = string.Format("{0}:{1}", ComponentName, name); + val = GetProcessor(key, transform).Process(val); + } + + Context.ReportMetric(ComponentGuid, ComponentName, name, units, val); + MetricsRecorded++; + } + + public void AddMetric(string name, string units, decimal value, MetricTransformEnum transform) + { + _VerboseMetricsLogger.InfoFormat("Gathering Component: {0}; Metric: {1}; Value: {2}", ComponentName, name, value); + float? val = (float) value; + + if (transform != MetricTransformEnum.Simple) + { + string key = string.Format("{0}:{1}", ComponentName, name); + val = GetProcessor(key, transform).Process(val); + } + + Context.ReportMetric(ComponentGuid, ComponentName, name, units, val); + MetricsRecorded++; + } + + internal IProcessor GetProcessor(string key, MetricTransformEnum metricTransform) + { + IProcessor processor = null; + + if (MetricProcessors.ContainsKey(key)) + { + processor = MetricProcessors[key]; + } + else if(metricTransform == MetricTransformEnum.Delta) + { + processor = new DeltaProcessor(); + MetricProcessors.Add(key, processor); + } + + return processor; + } + + internal static string FormatMetricKey(string pattern, object queryResult, string metricName) + { + var result = pattern; + + if (result.Contains("{DatabaseName}")) + { + var databaseMetric = queryResult as IDatabaseMetric; + var databaseName = databaseMetric != null ? databaseMetric.DatabaseName : "(none)"; + result = result.Replace("{DatabaseName}", databaseName); + } + + if (result.Contains("{MetricName}")) + { + result = result.Replace("{MetricName}", metricName); + } + else + { + result = result.EndsWith("/") ? result + metricName : result + "/" + metricName; + } + + var matches = Regex.Matches(result, @"\{(?[^}]+?)\}", RegexOptions.ExplicitCapture); + if (matches.Count <= 0) + { + return result; + } + + // Find placeholders + var queryType = queryResult.GetType(); + foreach (Match match in matches) + { + // Get the property match + var propertyName = match.Groups["property"].Value; + // Get the property + var propertyInfo = queryType.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance); + // Expect it to be a public instance property + if (propertyInfo == null) + { + // Look for a similarly named property where maybe the case is mismatched. Performance is unimportant as this is a fatal error. + if (queryType.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase) != null) + { + throw new Exception(string.Format("MetricPattern '{0}' contains a placeholder '{1}' for '{2}', however, it seems the placeholder has a case-mismatch." + + "The placeholder is case-sensitive.", + pattern, + propertyName, + queryType.Name)); + } + throw new Exception(string.Format("MetricPattern '{0}' contains a placeholder '{1}' which was not found as a property on '{2}'. " + + "It must be a public, instance property with a getter.", + pattern, + propertyName, + queryType.Name)); + } + // It must have a public getter + if (!propertyInfo.CanRead || propertyInfo.GetGetMethod(false) == null) + { + throw new Exception(string.Format("MetricPattern '{0}' contains a placeholder for the property '{1}' on '{2}', however, it does not have a getter. " + + "It must be a public, instance property with a getter.", + pattern, + propertyName, + queryType.Name)); + } + // Get the value + var propertyValue = propertyInfo.GetValue(queryResult, null); + // Try first as a string (most common), then ToString() when not null, else just the word "null" + var replacement = propertyValue as string ?? (propertyValue != null ? propertyValue.ToString() : "null"); + // No leading or trailing whitespace + replacement = replacement.Trim(); + // Replace all non-alphanumerics with underbar + var safeReplacement = Regex.Replace(replacement, @"[^\w\d]", "_", RegexOptions.Singleline); + // Finally, replace it in the metric pattern + result = result.Replace("{" + propertyName + "}", safeReplacement); + } + + return result; + } + } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/AzureServiceInterruptionEvents.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/AzureServiceInterruptionEvents.cs index 7616bef..72ac368 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/AzureServiceInterruptionEvents.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/AzureServiceInterruptionEvents.cs @@ -2,35 +2,35 @@ namespace NewRelic.Microsoft.SqlServer.Plugin.QueryTypes { - /// - /// Used in AzureSqlEndpoint as a special query. Disabled to prevent auto-detection and execution. - /// - [AzureSqlQuery("ServiceInterruptionEvents.AzureSQL.sql", "Component/{MetricName}/{Description}", QueryName = "Azure SQL Service Interuptions", Enabled = false)] - public class AzureServiceInterruptionEvents : DatabaseMetricBase - { - [Metric(Ignore = true)] - public string EventType { get; set; } + /// + /// Used in AzureSqlEndpoint as a special query. Disabled to prevent auto-detection and execution. + /// + [AzureSqlQuery("ServiceInterruptionEvents.AzureSQL.sql", "{MetricName}/{Description}", QueryName = "Azure SQL Service Interuptions", Enabled = false)] + public class AzureServiceInterruptionEvents : DatabaseMetricBase + { + [Metric(Ignore = true)] + public string EventType { get; set; } - [Metric(Ignore = true)] - public string Description { get; set; } + [Metric(Ignore = true)] + public string Description { get; set; } - [Metric(MetricName = "ServiceInterruptionEvent", MetricValueType = MetricValueType.Value, Units = "[count]")] - public int EventCount { get; set; } + [Metric(MetricName = "ServiceInterruptionEvent", MetricValueType = MetricValueType.Value, Units = "count")] + public int EventCount { get; set; } - protected override WhereClauseTokenEnum WhereClauseToken - { - get { return WhereClauseTokenEnum.Where; } - } + protected override WhereClauseTokenEnum WhereClauseToken + { + get { return WhereClauseTokenEnum.Where; } + } - protected override string DbNameForWhereClause - { - get { return "t.DatabaseName"; } - } + protected override string DbNameForWhereClause + { + get { return "t.DatabaseName"; } + } - public override string ToString() - { - return string.Format("DatabaseName: {0},\tEventType: {1}\tDescription: {2}\tEventCount: {3}", - DatabaseName, EventType, Description, EventCount); - } - } + public override string ToString() + { + return string.Format("DatabaseName: {0},\tEventType: {1}\tDescription: {2}\tEventCount: {3}", + DatabaseName, EventType, Description, EventCount); + } + } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/AzureSqlDatabaseSummary.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/AzureSqlDatabaseSummary.cs index 43fba9f..abe4b49 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/AzureSqlDatabaseSummary.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/AzureSqlDatabaseSummary.cs @@ -2,66 +2,66 @@ namespace NewRelic.Microsoft.SqlServer.Plugin.QueryTypes { - [AzureSqlQuery("Summary.AzureSql.sql", "Component/Summary/{MetricName}", QueryName = "Azure SQL Database Summary", Enabled = true)] - public class AzureSqlDatabaseSummary - { - [Metric(MetricValueType = MetricValueType.Value, Units = "[MB]")] - public long DbSizeInMB { get; set; } + [AzureSqlQuery("Summary.AzureSql.sql", "Summary/{MetricName}", QueryName = "Azure SQL Database Summary", Enabled = true)] + public class AzureSqlDatabaseSummary + { + [Metric(MetricValueType = MetricValueType.Value, Units = "MB")] + public long DbSizeInMB { get; set; } - [Metric(MetricValueType = MetricValueType.Value, Units = "[connections]")] - public int NumberOfConnections { get; set; } + [Metric(MetricValueType = MetricValueType.Value, Units = "connections")] + public int NumberOfConnections { get; set; } - [Metric(MetricValueType = MetricValueType.Value, Units = "[reads]")] - public int NumberOfReads { get; set; } + [Metric(MetricValueType = MetricValueType.Value, Units = "reads")] + public int NumberOfReads { get; set; } - [Metric(MetricValueType = MetricValueType.Value, Units = "[writes]")] - public int NumberOfWrites { get; set; } + [Metric(MetricValueType = MetricValueType.Value, Units = "writes")] + public int NumberOfWrites { get; set; } - [Metric(MetricValueType = MetricValueType.Value, Units = "[sessions]")] - public int NumberOfSessions { get; set; } + [Metric(MetricValueType = MetricValueType.Value, Units = "sessions")] + public int NumberOfSessions { get; set; } - [Metric(MetricValueType = MetricValueType.Value, Units = "[requests]")] - public int TotalCurrentRequests { get; set; } + [Metric(MetricValueType = MetricValueType.Value, Units = "requests")] + public int TotalCurrentRequests { get; set; } - [Metric(MetricValueType = MetricValueType.Value, Units = "[KB]")] - public int SumSessionMemoryUsageInKB { get; set; } + [Metric(MetricValueType = MetricValueType.Value, Units = "KB")] + public int SumSessionMemoryUsageInKB { get; set; } - [Metric(MetricValueType = MetricValueType.Value, Units = "[KB]")] - public int MinSessionMemoryUsageInKB { get; set; } + [Metric(MetricValueType = MetricValueType.Value, Units = "KB")] + public int MinSessionMemoryUsageInKB { get; set; } - [Metric(MetricValueType = MetricValueType.Value, Units = "[KB]")] - public int MaxSessionMemoryUsageInKB { get; set; } + [Metric(MetricValueType = MetricValueType.Value, Units = "KB")] + public int MaxSessionMemoryUsageInKB { get; set; } - [Metric(MetricValueType = MetricValueType.Value, Units = "[KB]")] - public int AvgSessionMemoryUsageInKB { get; set; } + [Metric(MetricValueType = MetricValueType.Value, Units = "KB")] + public int AvgSessionMemoryUsageInKB { get; set; } - [Metric(MetricValueType = MetricValueType.Value, Units = "[objects]")] - public int SingleUseObjects { get; set; } + [Metric(MetricValueType = MetricValueType.Value, Units = "objects")] + public int SingleUseObjects { get; set; } - [Metric(MetricValueType = MetricValueType.Value, Units = "[objects]")] - public int MultipleUseObjects { get; set; } + [Metric(MetricValueType = MetricValueType.Value, Units = "objects")] + public int MultipleUseObjects { get; set; } - [Metric(MetricValueType = MetricValueType.Value, Units = "[%_Single_Use]")] - public decimal SingleUsePercent { get; set; } + [Metric(MetricValueType = MetricValueType.Value, Units = "%_Single_Use")] + public decimal SingleUsePercent { get; set; } - public override string ToString() - { - return string.Format("DbSizeInMB: {0},\t" + - "NumberOfConnections: {1},\t" + - "NumberOfReads: {2},\t" + - "NumberOfWrites: {3},\t" + - "NumberOfSessions: {4},\t" + - "TotalCurrentRequests: {5},\t" + - "SumSessionMemoryUsageInKB: {6},\t" + - "MinSessionMemoryUsageInKB: {7},\t" + - "MaxSessionMemoryUsageInKB: {8},\t" + - "AvgSessionMemoryUsageInKB: {9},\t" + - "SingleUseObjects: {10},\t" + - "MultipleUseObjects: {11},\t" + - "SingleUsePercent: {12}", - DbSizeInMB, NumberOfConnections, NumberOfReads, NumberOfWrites, NumberOfSessions, TotalCurrentRequests, - SumSessionMemoryUsageInKB, MinSessionMemoryUsageInKB, MaxSessionMemoryUsageInKB, - AvgSessionMemoryUsageInKB, SingleUseObjects, MultipleUseObjects, SingleUsePercent); - } - } + public override string ToString() + { + return string.Format("DbSizeInMB: {0},\t" + + "NumberOfConnections: {1},\t" + + "NumberOfReads: {2},\t" + + "NumberOfWrites: {3},\t" + + "NumberOfSessions: {4},\t" + + "TotalCurrentRequests: {5},\t" + + "SumSessionMemoryUsageInKB: {6},\t" + + "MinSessionMemoryUsageInKB: {7},\t" + + "MaxSessionMemoryUsageInKB: {8},\t" + + "AvgSessionMemoryUsageInKB: {9},\t" + + "SingleUseObjects: {10},\t" + + "MultipleUseObjects: {11},\t" + + "SingleUsePercent: {12}", + DbSizeInMB, NumberOfConnections, NumberOfReads, NumberOfWrites, NumberOfSessions, TotalCurrentRequests, + SumSessionMemoryUsageInKB, MinSessionMemoryUsageInKB, MaxSessionMemoryUsageInKB, + AvgSessionMemoryUsageInKB, SingleUseObjects, MultipleUseObjects, SingleUsePercent); + } + } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/DatabaseDetails.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/DatabaseDetails.cs index f93475f..3a3837c 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/DatabaseDetails.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/DatabaseDetails.cs @@ -4,110 +4,110 @@ namespace NewRelic.Microsoft.SqlServer.Plugin.QueryTypes { - [SqlServerQuery("DatabaseDetails.SqlServer.sql", "Component/DatabaseDetails/{MetricName}/{DatabaseName}", QueryName = "Database Details", Enabled = false)] - public class DatabaseDetails : DatabaseMetricBase - { - // ReSharper disable InconsistentNaming - public int database_id { get; set; } - public int? source_database_id { get; set; } - public DateTime create_date { get; set; } - public int compatibility_level { get; set; } - public string collation_name { get; set; } - public byte? user_access { get; set; } - public string user_access_desc { get; set; } - public bool? is_read_only { get; set; } - public bool is_auto_close_on { get; set; } - public bool? is_auto_shrink_on { get; set; } - public byte? state { get; set; } - public string state_desc { get; set; } - public bool? is_in_standby { get; set; } - public bool? is_cleanly_shutdown { get; set; } - public bool? is_supplemental_logging_enabled { get; set; } - public byte? snapshot_isolation_state { get; set; } - public string snapshot_isolation_state_desc { get; set; } - public bool? is_read_committed_snapshot_on { get; set; } - public byte? recovery_model { get; set; } - public string recovery_model_desc { get; set; } - public byte? page_verify_option { get; set; } - public string page_verify_option_desc { get; set; } - public bool? is_auto_create_stats_on { get; set; } - public bool? is_auto_update_stats_on { get; set; } - public bool? is_auto_update_stats_async_on { get; set; } - public bool? is_ansi_null_default_on { get; set; } - public bool? is_ansi_nulls_on { get; set; } - public bool? is_ansi_padding_on { get; set; } - public bool? is_ansi_warnings_on { get; set; } - public bool? is_arithabort_on { get; set; } - public bool? is_concat_null_yields_null_on { get; set; } - public bool? is_numeric_roundabort_on { get; set; } - public bool? is_quoted_identifier_on { get; set; } - public bool? is_recursive_triggers_on { get; set; } - public bool? is_cursor_close_on_commit_on { get; set; } - public bool? is_local_cursor_default { get; set; } - public bool? is_fulltext_enabled { get; set; } - public bool? is_trustworthy_on { get; set; } - public bool? is_db_chaining_on { get; set; } - public bool? is_parameterization_forced { get; set; } - public bool is_master_key_encrypted_by_server { get; set; } - public bool is_published { get; set; } - public bool is_subscribed { get; set; } - public bool is_merge_published { get; set; } - public bool is_distributor { get; set; } - public bool is_sync_with_backup { get; set; } - public bool is_broker_enabled { get; set; } - public byte? log_reuse_wait { get; set; } - public string log_reuse_wait_desc { get; set; } - public bool is_date_correlation_on { get; set; } - public bool is_cdc_enabled { get; set; } - public bool? is_encrypted { get; set; } - public bool? is_honor_broker_priority_on { get; set; } - public string replica_id { get; set; } - public string group_database_id { get; set; } - public short? default_language_lcid { get; set; } - public string default_language_name { get; set; } - public int? default_fulltext_language_lcid { get; set; } - public string default_fulltext_language_name { get; set; } - public bool? is_nested_triggers_on { get; set; } - public bool? is_transform_noise_words_on { get; set; } - public short? two_digit_year_cutoff { get; set; } - public byte? containment { get; set; } - public string containment_desc { get; set; } - public int? target_recovery_time_in_seconds { get; set; } - // ReSharper restore InconsistentNaming + [SqlServerQuery("DatabaseDetails.SqlServer.sql", "DatabaseDetails/{MetricName}/{DatabaseName}", QueryName = "Database Details", Enabled = false)] + public class DatabaseDetails : DatabaseMetricBase + { + // ReSharper disable InconsistentNaming + public int database_id { get; set; } + public int? source_database_id { get; set; } + public DateTime create_date { get; set; } + public int compatibility_level { get; set; } + public string collation_name { get; set; } + public byte? user_access { get; set; } + public string user_access_desc { get; set; } + public bool? is_read_only { get; set; } + public bool is_auto_close_on { get; set; } + public bool? is_auto_shrink_on { get; set; } + public byte? state { get; set; } + public string state_desc { get; set; } + public bool? is_in_standby { get; set; } + public bool? is_cleanly_shutdown { get; set; } + public bool? is_supplemental_logging_enabled { get; set; } + public byte? snapshot_isolation_state { get; set; } + public string snapshot_isolation_state_desc { get; set; } + public bool? is_read_committed_snapshot_on { get; set; } + public byte? recovery_model { get; set; } + public string recovery_model_desc { get; set; } + public byte? page_verify_option { get; set; } + public string page_verify_option_desc { get; set; } + public bool? is_auto_create_stats_on { get; set; } + public bool? is_auto_update_stats_on { get; set; } + public bool? is_auto_update_stats_async_on { get; set; } + public bool? is_ansi_null_default_on { get; set; } + public bool? is_ansi_nulls_on { get; set; } + public bool? is_ansi_padding_on { get; set; } + public bool? is_ansi_warnings_on { get; set; } + public bool? is_arithabort_on { get; set; } + public bool? is_concat_null_yields_null_on { get; set; } + public bool? is_numeric_roundabort_on { get; set; } + public bool? is_quoted_identifier_on { get; set; } + public bool? is_recursive_triggers_on { get; set; } + public bool? is_cursor_close_on_commit_on { get; set; } + public bool? is_local_cursor_default { get; set; } + public bool? is_fulltext_enabled { get; set; } + public bool? is_trustworthy_on { get; set; } + public bool? is_db_chaining_on { get; set; } + public bool? is_parameterization_forced { get; set; } + public bool is_master_key_encrypted_by_server { get; set; } + public bool is_published { get; set; } + public bool is_subscribed { get; set; } + public bool is_merge_published { get; set; } + public bool is_distributor { get; set; } + public bool is_sync_with_backup { get; set; } + public bool is_broker_enabled { get; set; } + public byte? log_reuse_wait { get; set; } + public string log_reuse_wait_desc { get; set; } + public bool is_date_correlation_on { get; set; } + public bool is_cdc_enabled { get; set; } + public bool? is_encrypted { get; set; } + public bool? is_honor_broker_priority_on { get; set; } + public string replica_id { get; set; } + public string group_database_id { get; set; } + public short? default_language_lcid { get; set; } + public string default_language_name { get; set; } + public int? default_fulltext_language_lcid { get; set; } + public string default_fulltext_language_name { get; set; } + public bool? is_nested_triggers_on { get; set; } + public bool? is_transform_noise_words_on { get; set; } + public short? two_digit_year_cutoff { get; set; } + public byte? containment { get; set; } + public string containment_desc { get; set; } + public int? target_recovery_time_in_seconds { get; set; } + // ReSharper restore InconsistentNaming - protected override WhereClauseTokenEnum WhereClauseToken - { - get { return WhereClauseTokenEnum.Where; } - } + protected override WhereClauseTokenEnum WhereClauseToken + { + get { return WhereClauseTokenEnum.Where; } + } - protected override string DbNameForWhereClause - { - get { return "d.[name]"; } - } + protected override string DbNameForWhereClause + { + get { return "d.[name]"; } + } - public override string ToString() - { - return string.Format( - "database_id: {0}, source_database_id: {1}, create_date: {2}, compatibility_level: {3}, collation_name: {4}, user_access: {5}, user_access_desc: {6}, is_read_only: {7}, " + - "is_auto_close_on: {8}, is_auto_shrink_on: {9}, state: {10}, state_desc: {11}, is_in_standby: {12}, is_cleanly_shutdown: {13}, is_supplemental_logging_enabled: {14}, " + - "snapshot_isolation_state: {15}, snapshot_isolation_state_desc: {16}, is_read_committed_snapshot_on: {17}, recovery_model: {18}, recovery_model_desc: {19}, " + - "page_verify_option: {20}, page_verify_option_desc: {21}, is_auto_create_stats_on: {22}, is_auto_update_stats_on: {23}, is_auto_update_stats_async_on: {24}, " + - "is_ansi_null_default_on: {25}, is_ansi_nulls_on: {26}, is_ansi_padding_on: {27}, is_ansi_warnings_on: {28}, is_arithabort_on: {29}, is_concat_null_yields_null_on: {30}, " + - "is_numeric_roundabort_on: {31}, is_quoted_identifier_on: {32}, is_recursive_triggers_on: {33}, is_cursor_close_on_commit_on: {34}, is_local_cursor_default: {35}, " + - "is_fulltext_enabled: {36}, is_trustworthy_on: {37}, is_db_chaining_on: {38}, is_parameterization_forced: {39}, is_master_key_encrypted_by_server: {40}, is_published: {41}, " + - "is_subscribed: {42}, is_merge_published: {43}, is_distributor: {44}, is_sync_with_backup: {45}, is_broker_enabled: {46}, log_reuse_wait: {47}, log_reuse_wait_desc: {48}, " + - "is_date_correlation_on: {49}, is_cdc_enabled: {50}, is_encrypted: {51}, is_honor_broker_priority_on: {52}, replica_id: {53}, group_database_id: {54}, " + - "default_language_lcid: {55}, default_language_name: {56}, default_fulltext_language_lcid: {57}, default_fulltext_language_name: {58}, is_nested_triggers_on: {59}, " + - "is_transform_noise_words_on: {60}, two_digit_year_cutoff: {61}, containment: {62}, containment_desc: {63}, target_recovery_time_in_seconds: {64}", - database_id, source_database_id, create_date, compatibility_level, collation_name, user_access, user_access_desc, is_read_only, is_auto_close_on, is_auto_shrink_on, state, - state_desc, is_in_standby, is_cleanly_shutdown, is_supplemental_logging_enabled, snapshot_isolation_state, snapshot_isolation_state_desc, is_read_committed_snapshot_on, - recovery_model, recovery_model_desc, page_verify_option, page_verify_option_desc, is_auto_create_stats_on, is_auto_update_stats_on, is_auto_update_stats_async_on, - is_ansi_null_default_on, is_ansi_nulls_on, is_ansi_padding_on, is_ansi_warnings_on, is_arithabort_on, is_concat_null_yields_null_on, is_numeric_roundabort_on, - is_quoted_identifier_on, is_recursive_triggers_on, is_cursor_close_on_commit_on, is_local_cursor_default, is_fulltext_enabled, is_trustworthy_on, is_db_chaining_on, - is_parameterization_forced, is_master_key_encrypted_by_server, is_published, is_subscribed, is_merge_published, is_distributor, is_sync_with_backup, is_broker_enabled, - log_reuse_wait, log_reuse_wait_desc, is_date_correlation_on, is_cdc_enabled, is_encrypted, is_honor_broker_priority_on, replica_id, group_database_id, default_language_lcid, - default_language_name, default_fulltext_language_lcid, default_fulltext_language_name, is_nested_triggers_on, is_transform_noise_words_on, two_digit_year_cutoff, containment, - containment_desc, target_recovery_time_in_seconds); - } - } + public override string ToString() + { + return string.Format( + "database_id: {0}, source_database_id: {1}, create_date: {2}, compatibility_level: {3}, collation_name: {4}, user_access: {5}, user_access_desc: {6}, is_read_only: {7}, " + + "is_auto_close_on: {8}, is_auto_shrink_on: {9}, state: {10}, state_desc: {11}, is_in_standby: {12}, is_cleanly_shutdown: {13}, is_supplemental_logging_enabled: {14}, " + + "snapshot_isolation_state: {15}, snapshot_isolation_state_desc: {16}, is_read_committed_snapshot_on: {17}, recovery_model: {18}, recovery_model_desc: {19}, " + + "page_verify_option: {20}, page_verify_option_desc: {21}, is_auto_create_stats_on: {22}, is_auto_update_stats_on: {23}, is_auto_update_stats_async_on: {24}, " + + "is_ansi_null_default_on: {25}, is_ansi_nulls_on: {26}, is_ansi_padding_on: {27}, is_ansi_warnings_on: {28}, is_arithabort_on: {29}, is_concat_null_yields_null_on: {30}, " + + "is_numeric_roundabort_on: {31}, is_quoted_identifier_on: {32}, is_recursive_triggers_on: {33}, is_cursor_close_on_commit_on: {34}, is_local_cursor_default: {35}, " + + "is_fulltext_enabled: {36}, is_trustworthy_on: {37}, is_db_chaining_on: {38}, is_parameterization_forced: {39}, is_master_key_encrypted_by_server: {40}, is_published: {41}, " + + "is_subscribed: {42}, is_merge_published: {43}, is_distributor: {44}, is_sync_with_backup: {45}, is_broker_enabled: {46}, log_reuse_wait: {47}, log_reuse_wait_desc: {48}, " + + "is_date_correlation_on: {49}, is_cdc_enabled: {50}, is_encrypted: {51}, is_honor_broker_priority_on: {52}, replica_id: {53}, group_database_id: {54}, " + + "default_language_lcid: {55}, default_language_name: {56}, default_fulltext_language_lcid: {57}, default_fulltext_language_name: {58}, is_nested_triggers_on: {59}, " + + "is_transform_noise_words_on: {60}, two_digit_year_cutoff: {61}, containment: {62}, containment_desc: {63}, target_recovery_time_in_seconds: {64}", + database_id, source_database_id, create_date, compatibility_level, collation_name, user_access, user_access_desc, is_read_only, is_auto_close_on, is_auto_shrink_on, state, + state_desc, is_in_standby, is_cleanly_shutdown, is_supplemental_logging_enabled, snapshot_isolation_state, snapshot_isolation_state_desc, is_read_committed_snapshot_on, + recovery_model, recovery_model_desc, page_verify_option, page_verify_option_desc, is_auto_create_stats_on, is_auto_update_stats_on, is_auto_update_stats_async_on, + is_ansi_null_default_on, is_ansi_nulls_on, is_ansi_padding_on, is_ansi_warnings_on, is_arithabort_on, is_concat_null_yields_null_on, is_numeric_roundabort_on, + is_quoted_identifier_on, is_recursive_triggers_on, is_cursor_close_on_commit_on, is_local_cursor_default, is_fulltext_enabled, is_trustworthy_on, is_db_chaining_on, + is_parameterization_forced, is_master_key_encrypted_by_server, is_published, is_subscribed, is_merge_published, is_distributor, is_sync_with_backup, is_broker_enabled, + log_reuse_wait, log_reuse_wait_desc, is_date_correlation_on, is_cdc_enabled, is_encrypted, is_honor_broker_priority_on, replica_id, group_database_id, default_language_lcid, + default_language_name, default_fulltext_language_lcid, default_fulltext_language_name, is_nested_triggers_on, is_transform_noise_words_on, two_digit_year_cutoff, containment, + containment_desc, target_recovery_time_in_seconds); + } + } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/FileIoView.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/FileIoView.cs index b3049a5..1ed9261 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/FileIoView.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/FileIoView.cs @@ -2,43 +2,43 @@ namespace NewRelic.Microsoft.SqlServer.Plugin.QueryTypes { - [SqlServerQuery("FileIOView.sql", "Component/FileIO/{MetricName}/{DatabaseName}", QueryName = "File I/O", MetricTransformEnum = MetricTransformEnum.Delta, Enabled = true)] - public class FileIoView : DatabaseMetricBase - { - [Metric(MetricValueType = MetricValueType.Value, Units = "[bytes]")] - public long BytesRead { get; set; } - - [Metric(MetricValueType = MetricValueType.Value, Units = "[bytes]")] - public long BytesWritten { get; set; } - - [Metric(MetricValueType = MetricValueType.Count, Units = "[reads]")] - public long NumberOfReads { get; set; } - - [Metric(MetricValueType = MetricValueType.Count, Units = "[writes]")] - public long NumberOfWrites { get; set; } - - [Metric(MetricValueType = MetricValueType.Value, Units = "[bytes]")] - public long SizeInBytes { get; set; } - - protected override string DbNameForWhereClause - { - get { return "d.name"; } - } - - protected override WhereClauseTokenEnum WhereClauseToken - { - get { return WhereClauseTokenEnum.Where; } - } - - public override string ToString() - { - return string.Format("DatabaseName: {0},\t" + - "BytesRead: {1},\t" + - "BytesWritten: {2},\t" + - "NumberOfReads: {3},\t" + - "NumberOfWrites: {4},\t" + - "SizeInBytes: {5}", - DatabaseName, BytesRead, BytesWritten, NumberOfReads, NumberOfWrites, SizeInBytes); - } - } + [SqlServerQuery("FileIOView.sql", "FileIO/{MetricName}/{DatabaseName}", QueryName = "File I/O", Enabled = true)] + public class FileIoView : DatabaseMetricBase + { + [Metric(MetricValueType = MetricValueType.Value, Units = "bytes", MetricTransform = MetricTransformEnum.Delta)] + public long BytesRead { get; set; } + + [Metric(MetricValueType = MetricValueType.Value, Units = "bytes", MetricTransform = MetricTransformEnum.Delta)] + public long BytesWritten { get; set; } + + [Metric(MetricValueType = MetricValueType.Count, Units = "reads", MetricTransform = MetricTransformEnum.Delta)] + public long NumberOfReads { get; set; } + + [Metric(MetricValueType = MetricValueType.Count, Units = "writes", MetricTransform = MetricTransformEnum.Delta)] + public long NumberOfWrites { get; set; } + + [Metric(MetricValueType = MetricValueType.Value, Units = "bytes", MetricTransform = MetricTransformEnum.Delta)] + public long SizeInBytes { get; set; } + + protected override string DbNameForWhereClause + { + get { return "d.name"; } + } + + protected override WhereClauseTokenEnum WhereClauseToken + { + get { return WhereClauseTokenEnum.Where; } + } + + public override string ToString() + { + return string.Format("DatabaseName: {0},\t" + + "BytesRead: {1},\t" + + "BytesWritten: {2},\t" + + "NumberOfReads: {3},\t" + + "NumberOfWrites: {4},\t" + + "SizeInBytes: {5}", + DatabaseName, BytesRead, BytesWritten, NumberOfReads, NumberOfWrites, SizeInBytes); + } + } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/MemoryView.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/MemoryView.cs index e57c66b..041d70e 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/MemoryView.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/MemoryView.cs @@ -4,70 +4,70 @@ namespace NewRelic.Microsoft.SqlServer.Plugin.QueryTypes { - [SqlServerQuery("MemoryView.sql", "Component/Memory", QueryName = "Memory View", Enabled = true)] - public class MemoryView - { - private decimal _bufferCacheHitRatio; + [SqlServerQuery("MemoryView.sql", "Memory", QueryName = "Memory View", Enabled = true)] + public class MemoryView + { + private decimal _bufferCacheHitRatio; - [Metric(MetricValueType = MetricValueType.Value, Units = "[%_hit]")] - public decimal BufferCacheHitRatio - { - get { return _bufferCacheHitRatio; } - set - { - // Denominator and numerator in SQL are not populated at precisely at the same time. This results in values > 100%. - // Normalize this between 0 and 100% - _bufferCacheHitRatio = Math.Max(Math.Min(value, 100), 0); - } - } + [Metric(MetricValueType = MetricValueType.Value, Units = "%_hit")] + public decimal BufferCacheHitRatio + { + get { return _bufferCacheHitRatio; } + set + { + // Denominator and numerator in SQL are not populated at precisely at the same time. This results in values > 100%. + // Normalize this between 0 and 100% + _bufferCacheHitRatio = Math.Max(Math.Min(value, 100), 0); + } + } - [Metric(MetricValueType = MetricValueType.Value, Units = "[sec]")] - public long PageLife { get; set; } + [Metric(MetricValueType = MetricValueType.Value, Units = "sec")] + public long PageLife { get; set; } - /// - /// is an important metric, however, it is an even increasing metric. - /// Such metrics, where higher is better, are not supported as Summary metrics in the New Relic dashboard. - /// The Page Life Threat is a metric that takes the inverse of the page life and scores it against a default minimum page life. - /// As the page life reaches this mininum, the value approaches 100% and this an indicator that something is bad. - /// - [Metric(MetricValueType = MetricValueType.Value, Units = "[%_threat]")] - public decimal PageLifeThreat - { - get - { - // Defend against bad page life - if (PageLife <= 0) return 100m; - // Minimum is 5 mins - const decimal minimumPageLifeInSeconds = 300; - // Get a "threat" value that maxes out at 1 - var threat = Math.Min(1m, minimumPageLifeInSeconds/PageLife); - // Minimize decimal length - // Square it to minimize the threat at values far away from 300 - var result = threat*threat - // Multiply for percentage - *100; - // Reduce number of digits - return decimal.Round(result, 2); - } - } + /// + /// is an important metric, however, it is an even increasing metric. + /// Such metrics, where higher is better, are not supported as Summary metrics in the New Relic dashboard. + /// The Page Life Threat is a metric that takes the inverse of the page life and scores it against a default minimum page life. + /// As the page life reaches this mininum, the value approaches 100% and this an indicator that something is bad. + /// + [Metric(MetricValueType = MetricValueType.Value, Units = "%_threat")] + public decimal PageLifeThreat + { + get + { + // Defend against bad page life + if (PageLife <= 0) return 100m; + // Minimum is 5 mins + const decimal minimumPageLifeInSeconds = 300; + // Get a "threat" value that maxes out at 1 + var threat = Math.Min(1m, minimumPageLifeInSeconds/PageLife); + // Minimize decimal length + // Square it to minimize the threat at values far away from 300 + var result = threat*threat + // Multiply for percentage + *100; + // Reduce number of digits + return decimal.Round(result, 2); + } + } - [Metric(MetricValueType = MetricValueType.Value, Units = "[%_miss]")] - public decimal BufferCacheMissRatio - { - get - { - // Normalize this between 0 and 100% - var missRatio = 100m - BufferCacheHitRatio; - return Math.Max(Math.Min(100, missRatio), 0); - } - } + [Metric(MetricValueType = MetricValueType.Value, Units = "%_miss")] + public decimal BufferCacheMissRatio + { + get + { + // Normalize this between 0 and 100% + var missRatio = 100m - BufferCacheHitRatio; + return Math.Max(Math.Min(100, missRatio), 0); + } + } - public override string ToString() - { - return string.Format("BufferCacheHitRatio: {0},\t" + - "PageLife: {1},\t" + - "BufferCacheMissRatio: {2}", - BufferCacheHitRatio, PageLife, BufferCacheMissRatio); - } - } + public override string ToString() + { + return string.Format("BufferCacheHitRatio: {0},\t" + + "PageLife: {1},\t" + + "BufferCacheMissRatio: {2}", + BufferCacheHitRatio, PageLife, BufferCacheMissRatio); + } + } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/MemoryViewNuma.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/MemoryViewNuma.cs index aeebd7d..1fb730a 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/MemoryViewNuma.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/MemoryViewNuma.cs @@ -2,20 +2,20 @@ namespace NewRelic.Microsoft.SqlServer.Plugin.QueryTypes { - [SqlServerQuery("MemoryView.Numa.sql", "Component/Memory", QueryName = "Memory View (NUMA)", Enabled = true)] - public class MemoryViewNuma - { - [Metric(Ignore = true)] - public string Node { get; set; } + [SqlServerQuery("MemoryView.Numa.sql", "Memory", QueryName = "Memory View (NUMA)", Enabled = true)] + public class MemoryViewNuma + { + [Metric(Ignore = true)] + public string Node { get; set; } - [Metric(MetricValueType = MetricValueType.Value, Units = "[sec]", MetricName = "PageLifeNuma/Node_{Node}")] - public long PageLife { get; set; } + [Metric(MetricValueType = MetricValueType.Value, Units = "sec", MetricName = "PageLifeNuma/Node_{Node}")] + public long PageLife { get; set; } - public override string ToString() - { - return string.Format("Node: {0},\t" + - "PageLife: {1}", - Node != null ? Node.Trim() : "N/A", PageLife); - } - } + public override string ToString() + { + return string.Format("Node: {0},\t" + + "PageLife: {1}", + Node != null ? Node.Trim() : "N/A", PageLife); + } + } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/RecompileMaximums.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/RecompileMaximums.cs index 0b7fb8f..4e1f7b9 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/RecompileMaximums.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/RecompileMaximums.cs @@ -2,23 +2,23 @@ namespace NewRelic.Microsoft.SqlServer.Plugin.QueryTypes { - public class RecompileMaximums - { - [Metric(MetricValueType = MetricValueType.Count, Units = "[objects]")] - public int SingleUseObjects { get; set; } + public class RecompileMaximums + { + [Metric(MetricValueType = MetricValueType.Count, Units = "objects")] + public int SingleUseObjects { get; set; } - [Metric(MetricValueType = MetricValueType.Count, Units = "[objects]")] - public int MultipleUseObjects { get; set; } + [Metric(MetricValueType = MetricValueType.Count, Units = "objects")] + public int MultipleUseObjects { get; set; } - [Metric(MetricValueType = MetricValueType.Value, Units = "[%_Single_Use]")] - public decimal SingleUsePercent { get; set; } + [Metric(MetricValueType = MetricValueType.Value, Units = "%_Single_Use")] + public decimal SingleUsePercent { get; set; } - public override string ToString() - { - return string.Format("SingleUseObjects: {0},\t" + - "MultipleUseObjects: {1},\t" + - "SingleUsePercent: {2}", - SingleUseObjects, MultipleUseObjects, SingleUsePercent); - } - } + public override string ToString() + { + return string.Format("SingleUseObjects: {0},\t" + + "MultipleUseObjects: {1},\t" + + "SingleUsePercent: {2}", + SingleUseObjects, MultipleUseObjects, SingleUsePercent); + } + } } \ No newline at end of file diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/RecompileSummary.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/RecompileSummary.cs index 1ce0f6f..d9cd351 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/RecompileSummary.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/RecompileSummary.cs @@ -2,35 +2,35 @@ namespace NewRelic.Microsoft.SqlServer.Plugin.QueryTypes { - [SqlServerQuery("RecompileSummary.sql", "Component/Recompiles/{MetricName}/{DatabaseName}", QueryName = "Recompile Summary", Enabled = true)] - public class RecompileSummary : DatabaseMetricBase - { - [Metric(MetricValueType = MetricValueType.Count, Units = "[objects]")] - public int SingleUseObjects { get; set; } + [SqlServerQuery("RecompileSummary.sql", "Recompiles/{MetricName}/{DatabaseName}", QueryName = "Recompile Summary", Enabled = true)] + public class RecompileSummary : DatabaseMetricBase + { + [Metric(MetricValueType = MetricValueType.Count, Units = "objects")] + public int SingleUseObjects { get; set; } - [Metric(MetricValueType = MetricValueType.Count, Units = "[objects]")] - public int MultipleUseObjects { get; set; } + [Metric(MetricValueType = MetricValueType.Count, Units = "objects")] + public int MultipleUseObjects { get; set; } - [Metric(MetricValueType = MetricValueType.Value, Units = "[%_Single_Use]")] - public decimal SingleUsePercent { get; set; } + [Metric(MetricValueType = MetricValueType.Value, Units = "%_Single_Use")] + public decimal SingleUsePercent { get; set; } - protected override string DbNameForWhereClause - { - get { return "d.name"; } - } + protected override string DbNameForWhereClause + { + get { return "d.name"; } + } - protected override WhereClauseTokenEnum WhereClauseToken - { - get { return WhereClauseTokenEnum.Where; } - } + protected override WhereClauseTokenEnum WhereClauseToken + { + get { return WhereClauseTokenEnum.Where; } + } - public override string ToString() - { - return string.Format("DatabaseName: {0},\t" + - "SingleUseObjects: {1},\t" + - "MultipleUseObjects: {2},\t" + - "SingleUsePercent: {3}", - DatabaseName, SingleUseObjects, MultipleUseObjects, SingleUsePercent); - } - } + public override string ToString() + { + return string.Format("DatabaseName: {0},\t" + + "SingleUseObjects: {1},\t" + + "MultipleUseObjects: {2},\t" + + "SingleUsePercent: {3}", + DatabaseName, SingleUseObjects, MultipleUseObjects, SingleUsePercent); + } + } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/SqlCpuUsage.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/SqlCpuUsage.cs index 5da51c8..186eb4c 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/SqlCpuUsage.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/SqlCpuUsage.cs @@ -4,34 +4,34 @@ namespace NewRelic.Microsoft.SqlServer.Plugin.QueryTypes { - [SqlServerQuery("SQL-NonSQL-IdleCPUUsage.sql", "Component/SqlCpuUsage", QueryName = "SQL CPU Usage", Enabled = true)] - public class SqlCpuUsage - { - [Metric(Ignore = true)] - public long RecordID { get; set; } + [SqlServerQuery("SQL-NonSQL-IdleCPUUsage.sql", "SqlCpuUsage", QueryName = "SQL CPU Usage", Enabled = true)] + public class SqlCpuUsage + { + [Metric(Ignore = true)] + public long RecordID { get; set; } - public DateTime EventTime { get; set; } + public DateTime EventTime { get; set; } - [Metric(MetricName = "SQLProcess", MetricValueType = MetricValueType.Value, Units = "[%_CPU]")] - public byte SQLProcessUtilization { get; set; } + [Metric(MetricName = "SQLProcess", MetricValueType = MetricValueType.Value, Units = "%_CPU")] + public byte SQLProcessUtilization { get; set; } - [Metric(MetricValueType = MetricValueType.Value, Units = "[%_CPU]")] - public byte SystemIdle - { - get { return (byte) (100 - SQLProcessUtilization - OtherProcessUtilization); } - } + [Metric(MetricValueType = MetricValueType.Value, Units = "%_CPU")] + public byte SystemIdle + { + get { return (byte) (100 - SQLProcessUtilization - OtherProcessUtilization); } + } - [Metric(MetricName = "OtherProcess", MetricValueType = MetricValueType.Value, Units = "[%_CPU]")] - public byte OtherProcessUtilization { get; set; } + [Metric(MetricName = "OtherProcess", MetricValueType = MetricValueType.Value, Units = "%_CPU")] + public byte OtherProcessUtilization { get; set; } - public override string ToString() - { - return string.Format("RecordID: {0},\t" + - "EventTime: {1},\t" + - "SQLProcessUtilization: {2},\t" + - "SystemIdle: {3},\t" + - "OtherProcessUtilization: {4}", - RecordID, EventTime, SQLProcessUtilization, SystemIdle, OtherProcessUtilization); - } - } + public override string ToString() + { + return string.Format("RecordID: {0},\t" + + "EventTime: {1},\t" + + "SQLProcessUtilization: {2},\t" + + "SystemIdle: {3},\t" + + "OtherProcessUtilization: {4}", + RecordID, EventTime, SQLProcessUtilization, SystemIdle, OtherProcessUtilization); + } + } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/SqlDmlActivity.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/SqlDmlActivity.cs index b16430c..e0f23d7 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/SqlDmlActivity.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/SqlDmlActivity.cs @@ -4,42 +4,42 @@ namespace NewRelic.Microsoft.SqlServer.Plugin.QueryTypes { - [AzureSqlQuery("SqlDMLActivity.SqlServerAndAzureSQL.sql", "Component/DMLActivity/{MetricName}", QueryName = "Sql DML Activity", Enabled = true)] - [SqlServerQuery("SqlDMLActivity.SqlServerAndAzureSQL.sql", "Component/DMLActivity/{MetricName}", QueryName = "Sql DML Activity", Enabled = false)] - public class SqlDmlActivity - { - [Metric(Ignore = true)] - public Byte[] PlanHandle { get; set; } - - [Metric(Ignore = true)] - public long ExecutionCount { get; set; } - - [Metric(Ignore = true)] - public string QueryType { get; set; } - - [Metric(Ignore = true)] - public Byte[] SqlStatementHash { get; set; } - - [Metric(Ignore = true)] - public DateTime CreationTime { get; set; } - - public long Writes { get; set; } - - public long Reads { get; set; } - - public override string ToString() - { - return string.Format("PlanHandle: {0},\t" + - "SqlStatementHash: {1},\t" + - "ExecutionCount: {2},\t" + - "CreationTime: {3},\t" + - "QueryType: {4},", - PlanHandle != null ? BitConverter.ToString(PlanHandle) : string.Empty, - SqlStatementHash != null ? BitConverter.ToString(SqlStatementHash) : string.Empty, - ExecutionCount, - CreationTime, - QueryType - ); - } - } + [AzureSqlQuery("SqlDMLActivity.SqlServerAndAzureSQL.sql", "DMLActivity/{MetricName}", QueryName = "Sql DML Activity", Enabled = true)] + [SqlServerQuery("SqlDMLActivity.SqlServerAndAzureSQL.sql", "DMLActivity/{MetricName}", QueryName = "Sql DML Activity", Enabled = false)] + public class SqlDmlActivity + { + [Metric(Ignore = true)] + public Byte[] PlanHandle { get; set; } + + [Metric(Ignore = true)] + public long ExecutionCount { get; set; } + + [Metric(Ignore = true)] + public string QueryType { get; set; } + + [Metric(Ignore = true)] + public Byte[] SqlStatementHash { get; set; } + + [Metric(Ignore = true)] + public DateTime CreationTime { get; set; } + + public long Writes { get; set; } + + public long Reads { get; set; } + + public override string ToString() + { + return string.Format("PlanHandle: {0},\t" + + "SqlStatementHash: {1},\t" + + "ExecutionCount: {2},\t" + + "CreationTime: {3},\t" + + "QueryType: {4},", + PlanHandle != null ? BitConverter.ToString(PlanHandle) : string.Empty, + SqlStatementHash != null ? BitConverter.ToString(SqlStatementHash) : string.Empty, + ExecutionCount, + CreationTime, + QueryType + ); + } + } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/SqlServerConnections.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/SqlServerConnections.cs index 7e91fd3..6101a29 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/SqlServerConnections.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/SqlServerConnections.cs @@ -2,35 +2,35 @@ namespace NewRelic.Microsoft.SqlServer.Plugin.QueryTypes { - [SqlServerQuery("Connections.SqlServer.sql", "Component/Connections/{MetricName}/{DatabaseName}", QueryName = "SQL Connections", Enabled = true)] - public class SqlServerConnections : DatabaseMetricBase - { - [Metric(MetricValueType = MetricValueType.Count, Units = "[connections]")] - public int NumberOfConnections { get; set; } + [SqlServerQuery("Connections.SqlServer.sql", "Connections/{MetricName}/{DatabaseName}", QueryName = "SQL Connections", Enabled = true)] + public class SqlServerConnections : DatabaseMetricBase + { + [Metric(MetricValueType = MetricValueType.Count, Units = "connections")] + public int NumberOfConnections { get; set; } - [Metric(MetricValueType = MetricValueType.Value, Units = "[reads]")] - public int NumberOfReads { get; set; } + [Metric(MetricValueType = MetricValueType.Value, Units = "reads")] + public int NumberOfReads { get; set; } - [Metric(MetricValueType = MetricValueType.Value, Units = "[writes]")] - public int NumberOfWrites { get; set; } + [Metric(MetricValueType = MetricValueType.Value, Units = "writes")] + public int NumberOfWrites { get; set; } - protected override WhereClauseTokenEnum WhereClauseToken - { - get { return WhereClauseTokenEnum.WhereAnd; } - } + protected override WhereClauseTokenEnum WhereClauseToken + { + get { return WhereClauseTokenEnum.WhereAnd; } + } - protected override string DbNameForWhereClause - { - get { return "d.Name"; } - } + protected override string DbNameForWhereClause + { + get { return "d.Name"; } + } - public override string ToString() - { - return string.Format("DatabaseName: {0},\t" + - "NumberOfConnections: {1},\t" + - "NumberOfReads: {2},\t" + - "NumberOfWrites: {3}", - DatabaseName, NumberOfConnections, NumberOfReads, NumberOfWrites); - } - } + public override string ToString() + { + return string.Format("DatabaseName: {0},\t" + + "NumberOfConnections: {1},\t" + + "NumberOfReads: {2},\t" + + "NumberOfWrites: {3}", + DatabaseName, NumberOfConnections, NumberOfReads, NumberOfWrites); + } + } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/SqlServerDetails.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/SqlServerDetails.cs index 123631f..10d590f 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/SqlServerDetails.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/SqlServerDetails.cs @@ -2,18 +2,18 @@ namespace NewRelic.Microsoft.SqlServer.Plugin.QueryTypes { - [SqlServerQuery("ServerDetails.SqlServer.sql", "Component/SqlServerDetails", QueryName = "SQL Server Details", Enabled = false)] - public class SqlServerDetails - { - public string ProductVersion { get; set; } - public string SQLTitle { get; set; } - public string ProductLevel { get; set; } - public string Edition { get; set; } + [SqlServerQuery("ServerDetails.SqlServer.sql", "SqlServerDetails", QueryName = "SQL Server Details", Enabled = false)] + public class SqlServerDetails + { + public string ProductVersion { get; set; } + public string SQLTitle { get; set; } + public string ProductLevel { get; set; } + public string Edition { get; set; } - public override string ToString() - { - return string.Format("ProductVersion: {0},\tSQLTitle: {1},\tProductLevel: {2},\tEdition: {3}", - ProductVersion, SQLTitle, ProductLevel, Edition); - } - } + public override string ToString() + { + return string.Format("ProductVersion: {0},\tSQLTitle: {1},\tProductLevel: {2},\tEdition: {3}", + ProductVersion, SQLTitle, ProductLevel, Edition); + } + } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/WaitState.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/WaitState.cs index 515017e..548dbf9 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/WaitState.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/QueryTypes/WaitState.cs @@ -2,48 +2,48 @@ namespace NewRelic.Microsoft.SqlServer.Plugin.QueryTypes { - [AzureSqlQuery("WaitStates.AzureSql.sql", "Component/WaitState/{MetricName}/{WaitType}", QueryName = "Wait States", Enabled = true)] - [SqlServerQuery("WaitStates.SqlServer.sql", "Component/WaitState/{MetricName}/{WaitType}", QueryName = "Wait States", Enabled = true)] - public class WaitState - { - public string WaitType { get; set; } - - [Metric(MetricValueType = MetricValueType.Value, Units = "[sec]")] - public decimal WaitSeconds { get; set; } - - [Metric(MetricValueType = MetricValueType.Value, Units = "[sec]")] - public decimal ResourceSeconds { get; set; } - - [Metric(MetricValueType = MetricValueType.Value, Units = "[sec]")] - public decimal SignalSeconds { get; set; } - - [Metric(MetricValueType = MetricValueType.Count, Units = "[wait]")] - public long WaitCount { get; set; } - - [Metric(MetricValueType = MetricValueType.Value, Units = "[%_wait]")] - public decimal Percentage { get; set; } - - [Metric(MetricValueType = MetricValueType.Value, Units = "[sec/wait]")] - public decimal AvgWaitSeconds { get; set; } - - [Metric(MetricValueType = MetricValueType.Value, Units = "[sec/resource]")] - public decimal AvgResourceSeconds { get; set; } - - [Metric(MetricValueType = MetricValueType.Value, Units = "[sec/signal]")] - public decimal AvgSignalSeconds { get; set; } - - public override string ToString() - { - return string.Format("WaitType: {0},\t" + - "WaitSeconds: {1},\t" + - "ResourceSeconds: {2},\t" + - "SignalSeconds: {3},\t" + - "WaitCount: {4},\t" + - "Percentage: {5},\t" + - "AvgWaitSeconds: {6},\t" + - "AvgResourceSeconds: {7},\t" + - "AvgSignalSeconds: {8}", - WaitType, WaitSeconds, ResourceSeconds, SignalSeconds, WaitCount, Percentage, AvgWaitSeconds, AvgResourceSeconds, AvgSignalSeconds); - } - } + [AzureSqlQuery("WaitStates.AzureSql.sql", "WaitState/{MetricName}/{WaitType}", QueryName = "Wait States", Enabled = true)] + [SqlServerQuery("WaitStates.SqlServer.sql", "WaitState/{MetricName}/{WaitType}", QueryName = "Wait States", Enabled = true)] + public class WaitState + { + public string WaitType { get; set; } + + [Metric(MetricValueType = MetricValueType.Value, Units = "sec")] + public decimal WaitSeconds { get; set; } + + [Metric(MetricValueType = MetricValueType.Value, Units = "sec")] + public decimal ResourceSeconds { get; set; } + + [Metric(MetricValueType = MetricValueType.Value, Units = "sec")] + public decimal SignalSeconds { get; set; } + + [Metric(MetricValueType = MetricValueType.Count, Units = "wait")] + public long WaitCount { get; set; } + + [Metric(MetricValueType = MetricValueType.Value, Units = "%_wait")] + public decimal Percentage { get; set; } + + [Metric(MetricValueType = MetricValueType.Value, Units = "sec/wait")] + public decimal AvgWaitSeconds { get; set; } + + [Metric(MetricValueType = MetricValueType.Value, Units = "sec/resource")] + public decimal AvgResourceSeconds { get; set; } + + [Metric(MetricValueType = MetricValueType.Value, Units = "sec/signal")] + public decimal AvgSignalSeconds { get; set; } + + public override string ToString() + { + return string.Format("WaitType: {0},\t" + + "WaitSeconds: {1},\t" + + "ResourceSeconds: {2},\t" + + "SignalSeconds: {3},\t" + + "WaitCount: {4},\t" + + "Percentage: {5},\t" + + "AvgWaitSeconds: {6},\t" + + "AvgResourceSeconds: {7},\t" + + "AvgSignalSeconds: {8}", + WaitType, WaitSeconds, ResourceSeconds, SignalSeconds, WaitCount, Percentage, AvgWaitSeconds, AvgResourceSeconds, AvgSignalSeconds); + } + } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/SqlEndpointBase.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/SqlEndpointBase.cs index 30da068..09d712c 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/SqlEndpointBase.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/SqlEndpointBase.cs @@ -10,301 +10,249 @@ using NewRelic.Microsoft.SqlServer.Plugin.Core.Extensions; using NewRelic.Microsoft.SqlServer.Plugin.Properties; using NewRelic.Microsoft.SqlServer.Plugin.QueryTypes; -using NewRelic.Platform.Binding.DotNET; namespace NewRelic.Microsoft.SqlServer.Plugin { - public abstract class SqlEndpointBase : ISqlEndpoint - { - /// - /// Metrics with a Duration greater than this value will be rejected by the server (400) - /// - private const int MaximumAllowedDuration = 1800; - - private static readonly ILog _ErrorDetailOutputLogger = LogManager.GetLogger(Constants.ErrorDetailLogger); - private static readonly ILog _VerboseSqlOutputLogger = LogManager.GetLogger(Constants.VerboseSqlLogger); - - private DateTime _lastSuccessfulReportTime; - private SqlQuery[] _queries; - - protected SqlEndpointBase(string name, string connectionString) - { - Name = name; - ConnectionString = connectionString; - _lastSuccessfulReportTime = DateTime.Now; - - QueryHistory = new Dictionary>(); - SqlDmlActivityHistory = new Dictionary(); - - IncludedDatabases = new Database[0]; - ExcludedDatabaseNames = new string[0]; - } - - protected abstract string ComponentGuid { get; } - - public IDictionary> QueryHistory { get; private set; } - protected Dictionary SqlDmlActivityHistory { get; set; } - - public Database[] IncludedDatabases { get; protected set; } - - public string[] IncludedDatabaseNames - { - get { return IncludedDatabases.Select(d => d.Name).ToArray(); } - } - - public string[] ExcludedDatabaseNames { get; protected set; } - - public string Name { get; private set; } - public string ConnectionString { get; private set; } - - public int Duration - { - get - { - var secondsSinceLastSuccessfulReport = (int) DateTime.Now.Subtract(_lastSuccessfulReportTime).TotalSeconds; - - return secondsSinceLastSuccessfulReport <= MaximumAllowedDuration - ? secondsSinceLastSuccessfulReport - : MaximumAllowedDuration; - } - } - - public void SetQueries(IEnumerable queries) - { - _queries = FilterQueries(queries).ToArray(); - } - - public virtual IEnumerable ExecuteQueries(ILog log) - { - return ExecuteQueries(_queries, ConnectionString, log); - } - - public void MetricReportSuccessful(DateTime? reportDate = null) - { - _lastSuccessfulReportTime = reportDate ?? DateTime.Now; - QueryHistory.Values.ForEach(histories => histories.ForEach(qc => qc.DataSent = true)); - } - - public void UpdateHistory(IQueryContext[] queryContexts) - { - queryContexts.ForEach(queryContext => - { - Queue queryHistory = QueryHistory.GetOrCreate(queryContext.QueryName); - if (queryHistory.Count >= 2) //Only track up to last run of this query - { - queryHistory.Dequeue(); - } - queryHistory.Enqueue(queryContext); - }); - } - - public PlatformData GeneratePlatformData(AgentData agentData) - { - var platformData = new PlatformData(agentData); - - ComponentData[] pendingComponentData = QueryHistory.Select(qh => ComponentDataRetriever.GetData(qh.Value.ToArray())) - .Where(c => c != null).ToArray(); - - pendingComponentData.ForEach(platformData.AddComponent); - - return platformData; - } - - public virtual void ToLog(ILog log) - { - // Remove password from logging - var safeConnectionString = new SqlConnectionStringBuilder(ConnectionString); - if (!string.IsNullOrEmpty(safeConnectionString.Password)) - { - safeConnectionString.Password = "[redacted]"; - } - - log.InfoFormat(" {0}: {1}", Name, safeConnectionString); - - // Validate that connection string do not provide both Trusted Security AND user/password - bool hasUserCreds = !string.IsNullOrEmpty(safeConnectionString.UserID) || !string.IsNullOrEmpty(safeConnectionString.Password); - if (safeConnectionString.IntegratedSecurity == hasUserCreds) - { - log.Error("=================================================="); - log.ErrorFormat("Connection string for '{0}' may not contain both Integrated Security and User ID/Password credentials. " + - "Review the readme.md and update the config file.", - safeConnectionString.DataSource); - log.Error("=================================================="); - } - } - - protected IEnumerable ExecuteQueries(SqlQuery[] queries, string connectionString, ILog log) - { - // Remove password from logging - var safeConnectionString = new SqlConnectionStringBuilder(connectionString); - if (!string.IsNullOrEmpty(safeConnectionString.Password)) - { - safeConnectionString.Password = "[redacted]"; - } - - _VerboseSqlOutputLogger.InfoFormat("Connecting with {0}", safeConnectionString); - - using (var conn = new SqlConnection(connectionString)) - { - foreach (SqlQuery query in queries) - { - object[] results; - try - { - // Raw results from the database - results = query.Query(conn, this).ToArray(); - // Log them - LogVerboseSqlResults(query, results); - // Allow them to be transformed - results = OnQueryExecuted(query, results, log); - } - catch (Exception e) - { - _ErrorDetailOutputLogger.Error(string.Format("Error with query '{0}' at endpoint '{1}'", query.QueryName, safeConnectionString), e); - LogErrorSummary(log, e, query); - continue; - } - yield return CreateQueryContext(query, results); - } - } - } - - protected static void LogVerboseSqlResults(ISqlQuery query, IEnumerable results) - { - // This could be slow, so only proceed if it actually gets logged - if (!_VerboseSqlOutputLogger.IsInfoEnabled) return; - - var verboseLogging = new StringBuilder(); - verboseLogging.AppendFormat("Executed {0}", query.ResourceName).AppendLine(); - - foreach (object result in results) - { - verboseLogging.AppendLine(result.ToString()); - } - - _VerboseSqlOutputLogger.Info(verboseLogging.ToString()); - } - - internal QueryContext CreateQueryContext(IMetricQuery query, IEnumerable results) - { - return new QueryContext(query) {Results = results, ComponentData = new ComponentData(Name, ComponentGuid, Duration)}; - } - - protected internal abstract IEnumerable FilterQueries(IEnumerable queries); - - protected virtual object[] OnQueryExecuted(ISqlQuery query, object[] results, ILog log) - { - return query.QueryType == typeof (SqlDmlActivity) ? CalculateSqlDmlActivityIncrease(results, log) : results; - } - - public override string ToString() - { - return string.Format("Name: {0}, ConnectionString: {1}", Name, ConnectionString); - } - - internal object[] CalculateSqlDmlActivityIncrease(object[] inputResults, ILog log) - { - if (inputResults == null || inputResults.Length == 0) - { - log.Error("No values passed to CalculateSqlDmlActivityIncrease"); - return inputResults; - } - - SqlDmlActivity[] sqlDmlActivities = inputResults.OfType().ToArray(); - - if (!sqlDmlActivities.Any()) - { - log.Error("In trying to Process results for SqlDmlActivity, results were NULL or not of the appropriate type"); - return inputResults; - } - - Dictionary currentValues = sqlDmlActivities - .GroupBy(a => string.Format("{0}:{1}:{2}:{3}", BitConverter.ToString(a.PlanHandle), BitConverter.ToString(a.SqlStatementHash), a.CreationTime.Ticks, a.QueryType)) - .Select(a => new - { - a.Key, - //If we ever gets dupes, sum Excution Count - Activity = new SqlDmlActivity - { - CreationTime = a.First().CreationTime, - SqlStatementHash = a.First().SqlStatementHash, - PlanHandle = a.First().PlanHandle, - QueryType = a.First().QueryType, - ExecutionCount = a.Sum(dml => dml.ExecutionCount), - } - }) - .ToDictionary(a => a.Key, a => a.Activity); - - long reads = 0; - long writes = 0; - - // If this is the first time through, reads and writes are definitely 0 - if (SqlDmlActivityHistory.Count > 0) - { - currentValues - .ForEach(a => - { - long increase; - - // Find a matching previous value for a delta - SqlDmlActivity previous; - if (!SqlDmlActivityHistory.TryGetValue(a.Key, out previous)) - { - // Nothing previous, the delta is the absolute value here - increase = a.Value.ExecutionCount; - } - else if (a.Value.QueryType == previous.QueryType) - { - // Calculate the delta - increase = a.Value.ExecutionCount - previous.ExecutionCount; - - // Only record positive deltas, though theoretically impossible here - if (increase <= 0) return; - } - else - { - return; - } - - switch (a.Value.QueryType) - { - case "Writes": - writes += increase; - break; - case "Reads": - reads += increase; - break; - } - }); - } - - //Current Becomes the new history - SqlDmlActivityHistory = currentValues; - - if (_VerboseSqlOutputLogger.IsInfoEnabled) - { - _VerboseSqlOutputLogger.InfoFormat("SQL DML Activity: Reads={0} Writes={1}", reads, writes); - _VerboseSqlOutputLogger.Info(""); - } - - //return the sum of all increases for reads and writes - //if there is was no history (first time for this db) then reads and writes will be 0 - return new object[] - { - new SqlDmlActivity - { - Reads = reads, - Writes = writes, - }, - }; - } - - private void LogErrorSummary(ILog log, Exception e, ISqlQuery query) - { - var sqlException = e.InnerException as SqlException; - if (sqlException == null) return; - - log.LogSqlException(sqlException, query, ConnectionString); - } - } + public abstract class SqlEndpointBase : ISqlEndpoint + { + private static readonly ILog _ErrorDetailOutputLogger = LogManager.GetLogger(Constants.ErrorDetailLogger); + private static readonly ILog _VerboseSqlOutputLogger = LogManager.GetLogger(Constants.VerboseSqlLogger); + + private SqlQuery[] _queries; + + protected SqlEndpointBase(string name, string connectionString) + { + Name = name; + ConnectionString = connectionString; + + SqlDmlActivityHistory = new Dictionary(); + + IncludedDatabases = new Database[0]; + ExcludedDatabaseNames = new string[0]; + } + + protected abstract string ComponentGuid { get; } + + protected Dictionary SqlDmlActivityHistory { get; set; } + + public Database[] IncludedDatabases { get; protected set; } + + public string[] IncludedDatabaseNames + { + get { return IncludedDatabases.Select(d => d.Name).ToArray(); } + } + + public string[] ExcludedDatabaseNames { get; protected set; } + + public string Name { get; private set; } + public string ConnectionString { get; private set; } + + public void SetQueries(IEnumerable queries) + { + _queries = FilterQueries(queries).ToArray(); + } + + public virtual IEnumerable ExecuteQueries(ILog log) + { + return ExecuteQueries(_queries, ConnectionString, log); + } + + public virtual void ToLog(ILog log) + { + // Remove password from logging + var safeConnectionString = new SqlConnectionStringBuilder(ConnectionString); + if (!string.IsNullOrEmpty(safeConnectionString.Password)) + { + safeConnectionString.Password = "[redacted]"; + } + + log.InfoFormat(" {0}: {1}", Name, safeConnectionString); + + // Validate that connection string do not provide both Trusted Security AND user/password + bool hasUserCreds = !string.IsNullOrEmpty(safeConnectionString.UserID) || !string.IsNullOrEmpty(safeConnectionString.Password); + if (safeConnectionString.IntegratedSecurity == hasUserCreds) + { + log.Error("=================================================="); + log.ErrorFormat("Connection string for '{0}' may not contain both Integrated Security and User ID/Password credentials. " + + "Review the readme.md and update the config file.", + safeConnectionString.DataSource); + log.Error("=================================================="); + } + } + + protected IEnumerable ExecuteQueries(SqlQuery[] queries, string connectionString, ILog log) + { + // Remove password from logging + var safeConnectionString = new SqlConnectionStringBuilder(connectionString); + if (!string.IsNullOrEmpty(safeConnectionString.Password)) + { + safeConnectionString.Password = "[redacted]"; + } + + _VerboseSqlOutputLogger.InfoFormat("Connecting with {0}", safeConnectionString); + + using (var conn = new SqlConnection(connectionString)) + { + foreach (SqlQuery query in queries) + { + object[] results; + try + { + // Raw results from the database + results = query.Query(conn, this).ToArray(); + // Log them + LogVerboseSqlResults(query, results); + // Allow them to be transformed + results = OnQueryExecuted(query, results, log); + } + catch (Exception e) + { + _ErrorDetailOutputLogger.Error(string.Format("Error with query '{0}' at endpoint '{1}'", query.QueryName, safeConnectionString), e); + LogErrorSummary(log, e, query); + continue; + } + yield return CreateQueryContext(query, results); + } + } + } + + protected static void LogVerboseSqlResults(ISqlQuery query, IEnumerable results) + { + // This could be slow, so only proceed if it actually gets logged + if (!_VerboseSqlOutputLogger.IsInfoEnabled) return; + + var verboseLogging = new StringBuilder(); + verboseLogging.AppendFormat("Executed {0}", query.ResourceName).AppendLine(); + + foreach (object result in results) + { + verboseLogging.AppendLine(result.ToString()); + } + + _VerboseSqlOutputLogger.Info(verboseLogging.ToString()); + } + + internal QueryContext CreateQueryContext(IMetricQuery query, IEnumerable results) + { + return new QueryContext(query) {Results = results, ComponentName = Name, ComponentGuid = ComponentGuid }; + } + + protected internal abstract IEnumerable FilterQueries(IEnumerable queries); + + protected virtual object[] OnQueryExecuted(ISqlQuery query, object[] results, ILog log) + { + // TODO: We should be able to remove the special casing of SqlDmlActivity here, but simply changing it to a delta counter doe + return query.QueryType == typeof (SqlDmlActivity) ? CalculateSqlDmlActivityIncrease(results, log) : results; + } + + public override string ToString() + { + return string.Format("Name: {0}, ConnectionString: {1}", Name, ConnectionString); + } + + internal object[] CalculateSqlDmlActivityIncrease(object[] inputResults, ILog log) + { + if (inputResults == null || inputResults.Length == 0) + { + log.Error("No values passed to CalculateSqlDmlActivityIncrease"); + return inputResults; + } + + SqlDmlActivity[] sqlDmlActivities = inputResults.OfType().ToArray(); + + if (!sqlDmlActivities.Any()) + { + log.Error("In trying to Process results for SqlDmlActivity, results were NULL or not of the appropriate type"); + return inputResults; + } + + Dictionary currentValues = sqlDmlActivities + .GroupBy(a => string.Format("{0}:{1}:{2}:{3}", BitConverter.ToString(a.PlanHandle), BitConverter.ToString(a.SqlStatementHash), a.CreationTime.Ticks, a.QueryType)) + .Select(a => new + { + a.Key, + //If we ever gets dupes, sum Excution Count + Activity = new SqlDmlActivity + { + CreationTime = a.First().CreationTime, + SqlStatementHash = a.First().SqlStatementHash, + PlanHandle = a.First().PlanHandle, + QueryType = a.First().QueryType, + ExecutionCount = a.Sum(dml => dml.ExecutionCount), + } + }) + .ToDictionary(a => a.Key, a => a.Activity); + + long reads = 0; + long writes = 0; + + // If this is the first time through, reads and writes are definitely 0 + if (SqlDmlActivityHistory.Count > 0) + { + currentValues + .ForEach(a => + { + long increase; + + // Find a matching previous value for a delta + SqlDmlActivity previous; + if (!SqlDmlActivityHistory.TryGetValue(a.Key, out previous)) + { + // Nothing previous, the delta is the absolute value here + increase = a.Value.ExecutionCount; + } + else if (a.Value.QueryType == previous.QueryType) + { + // Calculate the delta + increase = a.Value.ExecutionCount - previous.ExecutionCount; + + // Only record positive deltas, though theoretically impossible here + if (increase <= 0) return; + } + else + { + return; + } + + switch (a.Value.QueryType) + { + case "Writes": + writes += increase; + break; + case "Reads": + reads += increase; + break; + } + }); + } + + //Current Becomes the new history + SqlDmlActivityHistory = currentValues; + + if (_VerboseSqlOutputLogger.IsInfoEnabled) + { + _VerboseSqlOutputLogger.InfoFormat("SQL DML Activity: Reads={0} Writes={1}", reads, writes); + _VerboseSqlOutputLogger.Info(""); + } + + //return the sum of all increases for reads and writes + //if there is was no history (first time for this db) then reads and writes will be 0 + return new object[] + { + new SqlDmlActivity + { + Reads = reads, + Writes = writes, + }, + }; + } + + private void LogErrorSummary(ILog log, Exception e, ISqlQuery query) + { + var sqlException = e.InnerException as SqlException; + if (sqlException == null) return; + + log.LogSqlException(sqlException, query, ConnectionString); + } + } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/SqlQuery.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/SqlQuery.cs index 9e4af16..c9a0f6c 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/SqlQuery.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/SqlQuery.cs @@ -23,7 +23,7 @@ public class SqlQuery : MetricQuery, ISqlQuery internal QueryAttribute QueryAttribute; public SqlQuery(Type queryType, QueryAttribute attribute, IDapperWrapper dapperWrapper, string commandText = null) - : base(queryType, attribute.QueryName, queryType.Name, attribute.MetricTransformEnum) + : base(queryType, attribute.QueryName, queryType.Name) { _dapperWrapper = dapperWrapper; QueryAttribute = attribute; diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/SqlServerEndpoint.cs b/src/NewRelic.Microsoft.SqlServer.Plugin/SqlServerEndpoint.cs index 0a5a527..749cd51 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/SqlServerEndpoint.cs +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/SqlServerEndpoint.cs @@ -12,239 +12,239 @@ namespace NewRelic.Microsoft.SqlServer.Plugin { - public class SqlServerEndpoint : SqlEndpointBase - { - public SqlServerEndpoint(string name, string connectionString, bool includeSystemDatabases) - : this(name, connectionString, includeSystemDatabases, null, null) {} - - public SqlServerEndpoint(string name, string connectionString, bool includeSystemDatabases, IEnumerable includedDatabases, IEnumerable excludedDatabaseNames) - : base(name, connectionString) - { - var excludedDbs = new List(); - var includedDbs = new List(); - - if (excludedDatabaseNames != null) - { - excludedDbs.AddRange(excludedDatabaseNames); - } - - if (includedDatabases != null) - { - includedDbs.AddRange(includedDatabases); - } - - if (includeSystemDatabases && includedDbs.Any()) - { - IEnumerable systemDbsToAdd = Constants.SystemDatabases - .Where(dbName => includedDbs.All(db => db.Name != dbName)) - .Select(dbName => new Database {Name = dbName}); - includedDbs.AddRange(systemDbsToAdd); - } - else if (!includeSystemDatabases) - { - IEnumerable systemDbsToAdd = Constants.SystemDatabases - .Where(dbName => excludedDbs.All(db => db != dbName)); - excludedDbs.AddRange(systemDbsToAdd); - } - - IncludedDatabases = includedDbs.ToArray(); - ExcludedDatabaseNames = excludedDbs.ToArray(); - } - - protected override string ComponentGuid - { - get { return Constants.SqlServerComponentGuid; } - } - - public override string ToString() - { - return FormatProperties(Name, ConnectionString, IncludedDatabaseNames, ExcludedDatabaseNames); - } - - internal static string FormatProperties(string name, string connectionString, string[] includedDatabases, string[] excludedDatabases) - { - return String.Format("Name: {0}, ConnectionString: {1}, IncludedDatabaseNames: {2}, ExcludedDatabaseNames: {3}", - name, - connectionString, - string.Join(", ", includedDatabases), - string.Join(", ", excludedDatabases)); - } - - /// - /// Replaces the database name on results with the - /// when possible. - /// - /// - /// - internal static void ApplyDatabaseDisplayNames(IEnumerable includedDatabases, object[] results) - { - if (includedDatabases == null) - { - return; - } - - Dictionary renameMap = includedDatabases.Where(d => !string.IsNullOrEmpty(d.DisplayName)).ToDictionary(d => d.Name.ToLower(), d => d.DisplayName); - if (!renameMap.Any()) - { - return; - } - - IDatabaseMetric[] databaseMetrics = results.OfType().Where(d => !string.IsNullOrEmpty(d.DatabaseName)).ToArray(); - if (!databaseMetrics.Any()) - { - return; - } - - foreach (IDatabaseMetric databaseMetric in databaseMetrics) - { - string displayName; - if (renameMap.TryGetValue(databaseMetric.DatabaseName.ToLower(), out displayName)) - { - databaseMetric.DatabaseName = displayName; - } - } - } - - protected internal override IEnumerable FilterQueries(IEnumerable queries) - { - return queries.Where(q => q.QueryAttribute is SqlServerQueryAttribute); - } - - public override void ToLog(ILog log) - { - base.ToLog(log); - - // Attempt to connect to the server and get basic details about the server and the databases. - Dictionary databaseDetailsByName; - try - { - var queryLocator = new QueryLocator(new DapperWrapper()); - SqlQuery serverDetailsQuery = queryLocator.PrepareQueries(new[] {typeof (SqlServerDetails),}, false).Single(); - SqlQuery databasesDetailsQuery = queryLocator.PrepareQueries(new[] {typeof (DatabaseDetails),}, false).Single(); - using (var conn = new SqlConnection(ConnectionString)) - { - // Log the server details - SqlServerDetails serverDetails = serverDetailsQuery.Query(conn, this).Single(); - LogVerboseSqlResults(serverDetailsQuery, new[] {serverDetails}); - log.InfoFormat(" {0} {1} {2} ({3})", serverDetails.SQLTitle, serverDetails.Edition, serverDetails.ProductLevel, serverDetails.ProductVersion); - - // Sotre these for reporting below - DatabaseDetails[] databasesDetails = databasesDetailsQuery.DatabaseMetricQuery(conn, this).ToArray(); - LogVerboseSqlResults(databasesDetailsQuery, databasesDetails); - databaseDetailsByName = databasesDetails.ToDictionary(d => d.DatabaseName); - } - } - catch (Exception e) - { - // Just log some details here. The subsequent queries for metrics yields more error details. - log.ErrorFormat(" Unable to connect: {0}", e.Message); - databaseDetailsByName = null; - } - - bool hasExplicitIncludedDatabases = IncludedDatabaseNames.Any(); - if (hasExplicitIncludedDatabases) - { - // Show the user the databases we'll be working from - foreach (Database database in IncludedDatabases) - { - string message = " Including DB: " + database.Name; - - // When the details are reachable, show them - if (databaseDetailsByName != null) - { - DatabaseDetails details; - if (databaseDetailsByName.TryGetValue(database.Name, out details)) - { - message += string.Format(" [CompatibilityLevel={0};State={1}({2});CreateDate={3:yyyy-MM-dd};UserAccess={4}({5})]", - details.compatibility_level, - details.state_desc, - details.state, - details.create_date, - details.user_access_desc, - details.user_access); - } - else - { - // More error details are reported with metric queries - message += " [Unable to find database information]"; - } - } - - log.Info(message); - } - } - else if (databaseDetailsByName != null) - { - // The user didn't specifically include any databases - // Report details for all of the DBs we expect to gather metrics against - foreach (DatabaseDetails details in databaseDetailsByName.Values) - { - log.InfoFormat(" Including DB: {0} [CompatibilityLevel={1};State={2}({3});CreateDate={4:yyyy-MM-dd};UserAccess={5}({6})]", - details.DatabaseName, - details.compatibility_level, - details.state_desc, - details.state, - details.create_date, - details.user_access_desc, - details.user_access); - } - } - - // If there are included DB's, log the Excluded DB's as DEBUG info. - Action logger = hasExplicitIncludedDatabases ? (Action) log.Debug : log.Info; - foreach (string database in ExcludedDatabaseNames) - { - logger(" Excluding DB: " + database); - } - } - - protected override object[] OnQueryExecuted(ISqlQuery query, object[] results, ILog log) - { - results = base.OnQueryExecuted(query, results, log); - ApplyDatabaseDisplayNames(IncludedDatabases, results); - return results; - } - - public override IEnumerable ExecuteQueries(ILog log) - { - IEnumerable queries = base.ExecuteQueries(log); - - foreach (IQueryContext query in queries) - { - yield return query; - - if (query.QueryType != typeof (RecompileSummary)) continue; - - // Manually add this summary metric - IQueryContext max = GetMaxRecompileSummaryMetric(query.Results); - if (max != null) - { - yield return max; - } - } - } - - /// - /// Reports a maximum set of values for the recompiles to support the Summary Metric on the New Relic dashboard - /// - /// - /// - internal IQueryContext GetMaxRecompileSummaryMetric(IEnumerable results) - { - if (results == null) return null; - - RecompileSummary[] recompileSummaries = results.OfType().ToArray(); - if (!recompileSummaries.Any()) return null; - - var max = new RecompileMaximums - { - SingleUsePercent = recompileSummaries.Max(s => s.SingleUsePercent), - SingleUseObjects = recompileSummaries.Max(s => s.SingleUseObjects), - MultipleUseObjects = recompileSummaries.Max(s => s.MultipleUseObjects), - }; - - var metricQuery = new MetricQuery(typeof (RecompileMaximums), typeof (RecompileMaximums).Name, typeof (RecompileMaximums).Name); - return CreateQueryContext(metricQuery, new[] {max}); - } - } + public class SqlServerEndpoint : SqlEndpointBase + { + public SqlServerEndpoint(string name, string connectionString, bool includeSystemDatabases) + : this(name, connectionString, includeSystemDatabases, null, null) {} + + public SqlServerEndpoint(string name, string connectionString, bool includeSystemDatabases, IEnumerable includedDatabases, IEnumerable excludedDatabaseNames) + : base(name, connectionString) + { + var excludedDbs = new List(); + var includedDbs = new List(); + + if (excludedDatabaseNames != null) + { + excludedDbs.AddRange(excludedDatabaseNames); + } + + if (includedDatabases != null) + { + includedDbs.AddRange(includedDatabases); + } + + if (includeSystemDatabases && includedDbs.Any()) + { + IEnumerable systemDbsToAdd = Constants.SystemDatabases + .Where(dbName => includedDbs.All(db => db.Name != dbName)) + .Select(dbName => new Database {Name = dbName}); + includedDbs.AddRange(systemDbsToAdd); + } + else if (!includeSystemDatabases) + { + IEnumerable systemDbsToAdd = Constants.SystemDatabases + .Where(dbName => excludedDbs.All(db => db != dbName)); + excludedDbs.AddRange(systemDbsToAdd); + } + + IncludedDatabases = includedDbs.ToArray(); + ExcludedDatabaseNames = excludedDbs.ToArray(); + } + + protected override string ComponentGuid + { + get { return Constants.SqlServerComponentGuid; } + } + + public override string ToString() + { + return FormatProperties(Name, ConnectionString, IncludedDatabaseNames, ExcludedDatabaseNames); + } + + internal static string FormatProperties(string name, string connectionString, string[] includedDatabases, string[] excludedDatabases) + { + return String.Format("Name: {0}, ConnectionString: {1}, IncludedDatabaseNames: {2}, ExcludedDatabaseNames: {3}", + name, + connectionString, + string.Join(", ", includedDatabases), + string.Join(", ", excludedDatabases)); + } + + /// + /// Replaces the database name on results with the + /// when possible. + /// + /// + /// + internal static void ApplyDatabaseDisplayNames(IEnumerable includedDatabases, object[] results) + { + if (includedDatabases == null) + { + return; + } + + Dictionary renameMap = includedDatabases.Where(d => !string.IsNullOrEmpty(d.DisplayName)).ToDictionary(d => d.Name.ToLower(), d => d.DisplayName); + if (!renameMap.Any()) + { + return; + } + + IDatabaseMetric[] databaseMetrics = results.OfType().Where(d => !string.IsNullOrEmpty(d.DatabaseName)).ToArray(); + if (!databaseMetrics.Any()) + { + return; + } + + foreach (IDatabaseMetric databaseMetric in databaseMetrics) + { + string displayName; + if (renameMap.TryGetValue(databaseMetric.DatabaseName.ToLower(), out displayName)) + { + databaseMetric.DatabaseName = displayName; + } + } + } + + protected internal override IEnumerable FilterQueries(IEnumerable queries) + { + return queries.Where(q => q.QueryAttribute is SqlServerQueryAttribute); + } + + public override void ToLog(ILog log) + { + base.ToLog(log); + + // Attempt to connect to the server and get basic details about the server and the databases. + Dictionary databaseDetailsByName; + try + { + var queryLocator = new QueryLocator(new DapperWrapper()); + SqlQuery serverDetailsQuery = queryLocator.PrepareQueries(new[] {typeof (SqlServerDetails),}, false).Single(); + SqlQuery databasesDetailsQuery = queryLocator.PrepareQueries(new[] {typeof (DatabaseDetails),}, false).Single(); + using (var conn = new SqlConnection(ConnectionString)) + { + // Log the server details + SqlServerDetails serverDetails = serverDetailsQuery.Query(conn, this).Single(); + LogVerboseSqlResults(serverDetailsQuery, new[] {serverDetails}); + log.InfoFormat(" {0} {1} {2} ({3})", serverDetails.SQLTitle, serverDetails.Edition, serverDetails.ProductLevel, serverDetails.ProductVersion); + + // Sotre these for reporting below + DatabaseDetails[] databasesDetails = databasesDetailsQuery.DatabaseMetricQuery(conn, this).ToArray(); + LogVerboseSqlResults(databasesDetailsQuery, databasesDetails); + databaseDetailsByName = databasesDetails.ToDictionary(d => d.DatabaseName); + } + } + catch (Exception e) + { + // Just log some details here. The subsequent queries for metrics yields more error details. + log.ErrorFormat(" Unable to connect: {0}", e.Message); + databaseDetailsByName = null; + } + + bool hasExplicitIncludedDatabases = IncludedDatabaseNames.Any(); + if (hasExplicitIncludedDatabases) + { + // Show the user the databases we'll be working from + foreach (Database database in IncludedDatabases) + { + string message = " Including DB: " + database.Name; + + // When the details are reachable, show them + if (databaseDetailsByName != null) + { + DatabaseDetails details; + if (databaseDetailsByName.TryGetValue(database.Name, out details)) + { + message += string.Format(" [CompatibilityLevel={0};State={1}({2});CreateDate={3:yyyy-MM-dd};UserAccess={4}({5})]", + details.compatibility_level, + details.state_desc, + details.state, + details.create_date, + details.user_access_desc, + details.user_access); + } + else + { + // More error details are reported with metric queries + message += " [Unable to find database information]"; + } + } + + log.Info(message); + } + } + else if (databaseDetailsByName != null) + { + // The user didn't specifically include any databases + // Report details for all of the DBs we expect to gather metrics against + foreach (DatabaseDetails details in databaseDetailsByName.Values) + { + log.InfoFormat(" Including DB: {0} [CompatibilityLevel={1};State={2}({3});CreateDate={4:yyyy-MM-dd};UserAccess={5}({6})]", + details.DatabaseName, + details.compatibility_level, + details.state_desc, + details.state, + details.create_date, + details.user_access_desc, + details.user_access); + } + } + + // If there are included DB's, log the Excluded DB's as DEBUG info. + Action logger = hasExplicitIncludedDatabases ? (Action) log.Debug : log.Info; + foreach (string database in ExcludedDatabaseNames) + { + logger(" Excluding DB: " + database); + } + } + + protected override object[] OnQueryExecuted(ISqlQuery query, object[] results, ILog log) + { + results = base.OnQueryExecuted(query, results, log); + ApplyDatabaseDisplayNames(IncludedDatabases, results); + return results; + } + + public override IEnumerable ExecuteQueries(ILog log) + { + IEnumerable queries = base.ExecuteQueries(log); + + foreach (IQueryContext query in queries) + { + yield return query; + + if (query.QueryType != typeof (RecompileSummary)) continue; + + // Manually add this summary metric + IQueryContext max = GetMaxRecompileSummaryMetric(query.Results); + if (max != null) + { + yield return max; + } + } + } + + /// + /// Reports a maximum set of values for the recompiles to support the Summary Metric on the New Relic dashboard + /// + /// + /// + internal IQueryContext GetMaxRecompileSummaryMetric(IEnumerable results) + { + if (results == null) return null; + + RecompileSummary[] recompileSummaries = results.OfType().ToArray(); + if (!recompileSummaries.Any()) return null; + + var max = new RecompileMaximums + { + SingleUsePercent = recompileSummaries.Max(s => s.SingleUsePercent), + SingleUseObjects = recompileSummaries.Max(s => s.SingleUseObjects), + MultipleUseObjects = recompileSummaries.Max(s => s.MultipleUseObjects), + }; + + var metricQuery = new MetricQuery(typeof (RecompileMaximums), typeof (RecompileMaximums).Name, typeof (RecompileMaximums).Name); + return CreateQueryContext(metricQuery, new[] {max}); + } + } } diff --git a/src/NewRelic.Microsoft.SqlServer.Plugin/app.config b/src/NewRelic.Microsoft.SqlServer.Plugin/app.config index 3f1486d..fd31ff3 100644 --- a/src/NewRelic.Microsoft.SqlServer.Plugin/app.config +++ b/src/NewRelic.Microsoft.SqlServer.Plugin/app.config @@ -1,106 +1,106 @@ - -
- + +
+ - - - + pollIntervalSeconds [Default: 60] + ====== + Specify the frequency of the polling in seconds. + ###### + --> + - - + + Attributes: + host - The proxy server host name. + port - The proxy server port (optional - defaults to 8080). + user - The username used to authenticate with the proxy server (optional). + password - The password used to authenticate with the proxy server (optional). + domain - The domain used to authenticate with the proxy server (optional). + useDefaultCredentials - 'true' or 'false. Uses the credentials of the account running the plugin (optional - defaults to false). + If specified, 'user' and 'password' are ignored. + --> - + - - - + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - + connectionString [Required] + ====== + Connection string to the Azure SQL Database. + ###### + --> + + + \ No newline at end of file