Skip to content

Commit 3fe88fc

Browse files
committed
add Dapper.ProviderTools lib
1 parent b535277 commit 3fe88fc

File tree

7 files changed

+341
-6
lines changed

7 files changed

+341
-6
lines changed

Dapper.ProviderTools/BulkCopy.cs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
using System;
2+
using System.Collections.Concurrent;
3+
using System.Data;
4+
using System.Data.Common;
5+
using System.Linq.Expressions;
6+
using System.Text.RegularExpressions;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
using Dapper.ProviderTools.Internal;
10+
11+
namespace Dapper.ProviderTools
12+
{
13+
/// <summary>
14+
/// Provides provider-agnostic access to bulk-copy services
15+
/// </summary>
16+
public abstract class BulkCopy : IDisposable
17+
{
18+
/// <summary>
19+
/// Attempt to create a BulkCopy instance for the connection provided
20+
/// </summary>
21+
public static BulkCopy? TryCreate(DbConnection connection)
22+
{
23+
if (connection == null) return null;
24+
var type = connection.GetType();
25+
if (!s_bcpFactory.TryGetValue(type, out var func))
26+
{
27+
s_bcpFactory[type] = func = CreateBcpFactory(type);
28+
}
29+
var obj = func?.Invoke(connection);
30+
return DynamicBulkCopy.Create(obj);
31+
}
32+
33+
/// <summary>
34+
/// Provide an external registration for a given connection type
35+
/// </summary>
36+
public static void Register(Type type, Func<DbConnection, object>? factory)
37+
=> s_bcpFactory[type] = factory;
38+
39+
private static readonly ConcurrentDictionary<Type, Func<DbConnection, object>?> s_bcpFactory
40+
= new ConcurrentDictionary<Type, Func<DbConnection, object>?>();
41+
42+
internal static Func<DbConnection, object>? CreateBcpFactory(Type connectionType)
43+
{
44+
try
45+
{
46+
var match = Regex.Match(connectionType.Name, "^(.+)Connection$");
47+
if (match.Success)
48+
{
49+
var prefix = match.Groups[1].Value;
50+
var bcpType = connectionType.Assembly.GetType($"{connectionType.Namespace}.{prefix}BulkCopy");
51+
if (bcpType != null)
52+
{
53+
var ctor = bcpType.GetConstructor(new[] { connectionType });
54+
if (ctor == null) return null;
55+
56+
var p = Expression.Parameter(typeof(DbConnection), "conn");
57+
var body = Expression.New(ctor, Expression.Convert(p, connectionType));
58+
return Expression.Lambda<Func<DbConnection, object>>(body, p).Compile();
59+
}
60+
}
61+
}
62+
catch { }
63+
return null;
64+
}
65+
66+
/// <summary>
67+
/// Name of the destination table on the server.
68+
/// </summary>
69+
public abstract string DestinationTableName { get; set; }
70+
/// <summary>
71+
/// Write a set of data to the server
72+
/// </summary>
73+
public abstract void WriteToServer(DataTable source);
74+
/// <summary>
75+
/// Write a set of data to the server
76+
/// </summary>
77+
public abstract void WriteToServer(IDataReader source);
78+
/// <summary>
79+
/// Write a set of data to the server
80+
/// </summary>
81+
public abstract Task WriteToServerAsync(DbDataReader source, CancellationToken cancellationToken);
82+
/// <summary>
83+
/// Write a set of data to the server
84+
/// </summary>
85+
public abstract Task WriteToServerAsync(DataTable source, CancellationToken cancellationToken);
86+
/// <summary>
87+
/// Add a mapping between two columns by name
88+
/// </summary>
89+
public abstract void AddColumnMapping(string sourceColumn, string destinationColumn);
90+
/// <summary>
91+
/// Add a mapping between two columns by position
92+
/// </summary>
93+
public abstract void AddColumnMapping(int sourceColumn, int destinationColumn);
94+
/// <summary>
95+
/// The underlying untyped object providing the bulk-copy service
96+
/// </summary>
97+
public abstract object Wrapped { get; }
98+
99+
/// <summary>
100+
/// Enables or disables streaming from a data-reader
101+
/// </summary>
102+
public bool EnableStreaming { get; set; }
103+
/// <summary>
104+
/// Number of rows in each batch
105+
/// </summary>
106+
public int BatchSize { get; set; }
107+
/// <summary>
108+
/// Number of seconds for the operation to complete before it times out.
109+
/// </summary>
110+
public int BulkCopyTimeout { get; set; }
111+
112+
/// <summary>
113+
/// Release any resources associated with this instance
114+
/// </summary>
115+
public void Dispose() => Dispose(true);
116+
117+
/// <summary>
118+
/// Release any resources associated with this instance
119+
/// </summary>
120+
protected abstract void Dispose(bool disposing);
121+
}
122+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netstandard2.0</TargetFramework>
5+
6+
<AssemblyName>Dapper.ProviderTools</AssemblyName>
7+
<PackageTags>orm;sql;micro-orm</PackageTags>
8+
<Title>Dapper Provider Tools</Title>
9+
<Description>Provider-agnostic ADO.NET helper utilities</Description>
10+
<Authors>Marc Gravell</Authors>
11+
<TargetFrameworks>netstandard2.0</TargetFrameworks>
12+
<SignAssembly>true</SignAssembly>
13+
<Nullable>enable</Nullable>
14+
<LangVersion>8.0</LangVersion>
15+
</PropertyGroup>
16+
<ItemGroup>
17+
<PackageReference Include="Microsoft.CSharp" Version="4.5.0" />
18+
</ItemGroup>
19+
20+
</Project>
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using System;
2+
using System.Collections.Concurrent;
3+
using System.Data.Common;
4+
using System.Linq.Expressions;
5+
using System.Reflection;
6+
7+
namespace Dapper.ProviderTools
8+
{
9+
/// <summary>
10+
/// Helper utilties for working with database exceptions
11+
/// </summary>
12+
public static class DbExceptionExtensions
13+
{
14+
/// <summary>
15+
/// Indicates whether the provided exception has an integer Number property with the supplied value
16+
/// </summary>
17+
public static bool IsNumber(this DbException exception, int number)
18+
=> exception != null && ByTypeHelpers.Get(exception.GetType()).IsNumber(exception, number);
19+
20+
21+
private sealed class ByTypeHelpers
22+
{
23+
private static readonly ConcurrentDictionary<Type, ByTypeHelpers> s_byType
24+
= new ConcurrentDictionary<Type, ByTypeHelpers>();
25+
private readonly Func<DbException, int>? _getNumber;
26+
27+
public bool IsNumber(DbException exception, int number)
28+
=> _getNumber != null && _getNumber(exception) == number;
29+
30+
public static ByTypeHelpers Get(Type type)
31+
{
32+
if (!s_byType.TryGetValue(type, out var value))
33+
{
34+
s_byType[type] = value = new ByTypeHelpers(type);
35+
}
36+
return value;
37+
}
38+
39+
private ByTypeHelpers(Type type)
40+
{
41+
_getNumber = TryGetInstanceProperty<int>("Number", type);
42+
}
43+
44+
private static Func<DbException, T>? TryGetInstanceProperty<T>(string name, Type type)
45+
{
46+
try
47+
{
48+
var prop = type.GetProperty(name, BindingFlags.Public | BindingFlags.Instance);
49+
if (prop == null || !prop.CanRead) return null;
50+
if (prop.PropertyType != typeof(T)) return null;
51+
52+
var p = Expression.Parameter(typeof(DbException), "exception");
53+
var body = Expression.Property(Expression.Convert(p, type), prop);
54+
var lambda = Expression.Lambda<Func<DbException, T>>(body, p);
55+
return lambda.Compile();
56+
}
57+
catch
58+
{
59+
return null;
60+
}
61+
}
62+
}
63+
}
64+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using System;
2+
using System.Data;
3+
using System.Data.Common;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
7+
namespace Dapper.ProviderTools.Internal
8+
{
9+
internal sealed class DynamicBulkCopy : BulkCopy
10+
{
11+
internal static BulkCopy? Create(object? wrapped)
12+
=> wrapped == null ? null : new DynamicBulkCopy(wrapped);
13+
14+
private DynamicBulkCopy(object wrapped)
15+
=> _wrapped = wrapped;
16+
17+
private readonly dynamic _wrapped;
18+
19+
public override string DestinationTableName
20+
{
21+
get => _wrapped.DestinationTableName;
22+
set => _wrapped.DestinationTableName = value;
23+
}
24+
25+
public override object Wrapped => _wrapped;
26+
27+
public override void AddColumnMapping(string sourceColumn, string destinationColumn)
28+
=> _wrapped.ColumnMappings.Add(sourceColumn, destinationColumn);
29+
30+
public override void AddColumnMapping(int sourceColumn, int destinationColumn)
31+
=> _wrapped.ColumnMappings.Add(sourceColumn, destinationColumn);
32+
33+
public override void WriteToServer(DataTable source)
34+
=> _wrapped.WriteToServer(source);
35+
36+
public override void WriteToServer(IDataReader source)
37+
=> _wrapped.WriteToServer(source);
38+
39+
public override Task WriteToServerAsync(DbDataReader source, CancellationToken cancellationToken)
40+
=> _wrapped.WriteToServer(source, cancellationToken);
41+
42+
public override Task WriteToServerAsync(DataTable source, CancellationToken cancellationToken)
43+
=> _wrapped.WriteToServer(source, cancellationToken);
44+
45+
protected override void Dispose(bool disposing)
46+
{
47+
if (disposing)
48+
{
49+
if (_wrapped is IDisposable d)
50+
{
51+
try { d.Dispose(); } catch { }
52+
}
53+
}
54+
}
55+
}
56+
}

Dapper.Tests/Dapper.Tests.csproj

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,25 +23,23 @@
2323
<ItemGroup>
2424
<PackageReference Include="System.Data.SqlClient" Version="4.6.1" />
2525
<PackageReference Include="Microsoft.Data.SqlClient" Version="1.0.19239.1" />
26-
<PackageReference Include="Microsoft.SqlServer.Types" Version="14.0.1016.290"
27-
Condition="'$(TargetFramework)' == 'net462' OR '$(TargetFramework)' == 'net472'" />
26+
<PackageReference Include="Microsoft.SqlServer.Types" Version="14.0.1016.290" Condition="'$(TargetFramework)' == 'net462' OR '$(TargetFramework)' == 'net472'" />
2827
</ItemGroup>
2928
<ItemGroup Condition="'$(Platform)'=='x64'">
30-
<Content Include="$(USERPROFILE)\.nuget\packages\microsoft.sqlserver.types\14.0.1016.290\nativeBinaries\x64\*.dll"
31-
Condition="'$(TargetFramework)' == 'net462' OR '$(TargetFramework)' == 'net472'">
29+
<Content Include="$(USERPROFILE)\.nuget\packages\microsoft.sqlserver.types\14.0.1016.290\nativeBinaries\x64\*.dll" Condition="'$(TargetFramework)' == 'net462' OR '$(TargetFramework)' == 'net472'">
3230
<Link>%(Filename)%(Extension)</Link>
3331
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
3432
</Content>
3533
</ItemGroup>
3634
<ItemGroup Condition="'$(Platform)'=='x86'">
37-
<Content Include="$(USERPROFILE)\.nuget\packages\microsoft.sqlserver.types\14.0.1016.290\nativeBinaries\x86\*.dll"
38-
Condition="'$(TargetFramework)' == 'net462' OR '$(TargetFramework)' == 'net472'">
35+
<Content Include="$(USERPROFILE)\.nuget\packages\microsoft.sqlserver.types\14.0.1016.290\nativeBinaries\x86\*.dll" Condition="'$(TargetFramework)' == 'net462' OR '$(TargetFramework)' == 'net472'">
3936
<Link>%(Filename)%(Extension)</Link>
4037
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
4138
</Content>
4239
</ItemGroup>
4340

4441
<ItemGroup>
42+
<ProjectReference Include="..\Dapper.ProviderTools\Dapper.ProviderTools.csproj" />
4543
<ProjectReference Include="..\Dapper\Dapper.csproj" />
4644
<ProjectReference Include="..\Dapper.Contrib\Dapper.Contrib.csproj" />
4745
<PackageReference Include="FirebirdSql.Data.FirebirdClient" Version="7.0.0" />
@@ -54,6 +52,7 @@
5452
<PrivateAssets>all</PrivateAssets>
5553
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
5654
</PackageReference>
55+
<PackageReference Include="Oracle.ManagedDataAccess" Version="19.3.1" />
5756
</ItemGroup>
5857

5958
<ItemGroup Condition="'$(TargetFramework)' == 'net462' OR '$(TargetFramework)' == 'net472'">

Dapper.Tests/ProviderTests.cs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using System.Data.Common;
2+
using Dapper.ProviderTools;
3+
using Xunit;
4+
5+
namespace Dapper.Tests
6+
{
7+
public class ProviderTests
8+
{
9+
[Fact]
10+
public void BulkCopy_SystemDataSqlClient()
11+
{
12+
using (var conn = new System.Data.SqlClient.SqlConnection())
13+
{
14+
Test<System.Data.SqlClient.SqlBulkCopy>(conn);
15+
}
16+
}
17+
18+
[Fact]
19+
public void BulkCopy_MicrosoftDataSqlClient()
20+
{
21+
using (var conn = new Microsoft.Data.SqlClient.SqlConnection())
22+
{
23+
Test<Microsoft.Data.SqlClient.SqlBulkCopy>(conn);
24+
}
25+
}
26+
27+
private static void Test<T>(DbConnection connection)
28+
{
29+
using (var bcp = BulkCopy.TryCreate(connection))
30+
{
31+
Assert.NotNull(bcp);
32+
Assert.IsType<T>(bcp.Wrapped);
33+
bcp.EnableStreaming = true;
34+
}
35+
}
36+
37+
[Theory]
38+
[InlineData(51000, 51000, true)]
39+
[InlineData(51000, 43, false)]
40+
public void DbNumber_SystemData(int create, int test, bool result)
41+
=> Test<SystemSqlClientProvider>(create, test, result);
42+
43+
[Theory]
44+
[InlineData(51000, 51000, true)]
45+
[InlineData(51000, 43, false)]
46+
public void DbNumber_MicrosoftData(int create, int test, bool result)
47+
=> Test<MicrosoftSqlClientProvider>(create, test, result);
48+
49+
private void Test<T>(int create, int test, bool result)
50+
where T : SqlServerDatabaseProvider, new()
51+
{
52+
var provider = new T();
53+
using (var conn = provider.GetOpenConnection())
54+
{
55+
try
56+
{
57+
conn.Execute("throw @create, 'boom', 1;", new { create });
58+
Assert.False(true);
59+
}
60+
catch(DbException err)
61+
{
62+
Assert.Equal(result, err.IsNumber(test));
63+
}
64+
}
65+
}
66+
}
67+
}

Dapper.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.EntityFramework.Stro
4545
EndProject
4646
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.Tests.Performance", "Dapper.Tests.Performance\Dapper.Tests.Performance.csproj", "{F017075A-2969-4A8E-8971-26F154EB420F}"
4747
EndProject
48+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapper.ProviderTools", "Dapper.ProviderTools\Dapper.ProviderTools.csproj", "{B06DB435-0C74-4BD3-BC97-52AF7CF9916B}"
49+
EndProject
4850
Global
4951
GlobalSection(SolutionConfigurationPlatforms) = preSolution
5052
Debug|Any CPU = Debug|Any CPU
@@ -91,6 +93,10 @@ Global
9193
{F017075A-2969-4A8E-8971-26F154EB420F}.Debug|Any CPU.Build.0 = Debug|Any CPU
9294
{F017075A-2969-4A8E-8971-26F154EB420F}.Release|Any CPU.ActiveCfg = Release|Any CPU
9395
{F017075A-2969-4A8E-8971-26F154EB420F}.Release|Any CPU.Build.0 = Release|Any CPU
96+
{B06DB435-0C74-4BD3-BC97-52AF7CF9916B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
97+
{B06DB435-0C74-4BD3-BC97-52AF7CF9916B}.Debug|Any CPU.Build.0 = Debug|Any CPU
98+
{B06DB435-0C74-4BD3-BC97-52AF7CF9916B}.Release|Any CPU.ActiveCfg = Release|Any CPU
99+
{B06DB435-0C74-4BD3-BC97-52AF7CF9916B}.Release|Any CPU.Build.0 = Release|Any CPU
94100
EndGlobalSection
95101
GlobalSection(SolutionProperties) = preSolution
96102
HideSolutionNode = FALSE
@@ -106,6 +112,7 @@ Global
106112
{8A74F0B6-188F-45D2-8A4B-51E4F211805A} = {4E956F6B-6BD8-46F5-BC85-49292FF8F9AB}
107113
{39D3EEB6-9C05-4F4A-8C01-7B209742A7EB} = {4E956F6B-6BD8-46F5-BC85-49292FF8F9AB}
108114
{F017075A-2969-4A8E-8971-26F154EB420F} = {568BD46C-1C65-4D44-870C-12CD72563262}
115+
{B06DB435-0C74-4BD3-BC97-52AF7CF9916B} = {4E956F6B-6BD8-46F5-BC85-49292FF8F9AB}
109116
EndGlobalSection
110117
GlobalSection(ExtensibilityGlobals) = postSolution
111118
SolutionGuid = {928A4226-96F3-409A-8A83-9E7444488710}

0 commit comments

Comments
 (0)