generated from Avanade/avanade-template
-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathDatabase.cs
More file actions
209 lines (176 loc) · 9.71 KB
/
Database.cs
File metadata and controls
209 lines (176 loc) · 9.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx
using CoreEx.Database.Extended;
using CoreEx.Entities;
using CoreEx.Json;
using CoreEx.Mapping.Converters;
using CoreEx.Results;
using Microsoft.Extensions.Logging;
using System;
using System.Data;
using System.Data.Common;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
namespace CoreEx.Database
{
/// <summary>
/// Provides the common/base database access functionality.
/// </summary>
/// <typeparam name="TConnection">The <see cref="DbConnection"/> <see cref="Type"/>.</typeparam>
/// <param name="create">The function to create the <typeparamref name="TConnection"/> <see cref="DbConnection"/>.</param>
/// <param name="provider">The underlying <see cref="DbProviderFactory"/>.</param>
/// <param name="logger">The optional <see cref="ILogger"/>.</param>
/// <param name="invoker">The optional <see cref="DatabaseInvoker"/>.</param>
public class Database<TConnection>(Func<TConnection> create, DbProviderFactory provider, ILogger<Database<TConnection>>? logger = null, DatabaseInvoker? invoker = null) : IDatabase where TConnection : DbConnection
{
private static readonly DatabaseColumns _defaultColumns = new();
private static readonly DatabaseWildcard _defaultWildcard = new();
private static DatabaseInvoker? _invoker;
private readonly Func<TConnection> _dbConnCreate = create.ThrowIfNull(nameof(create));
private TConnection? _dbConn;
private readonly SemaphoreSlim _semaphore = new(1, 1);
/// <inheritdoc/>
public DbProviderFactory Provider { get; } = provider.ThrowIfNull(nameof(provider));
/// <inheritdoc/>
public Guid DatabaseId { get; } = Guid.NewGuid();
/// <inheritdoc/>
public ILogger? Logger { get; } = logger ?? ExecutionContext.GetService<ILogger<Database<TConnection>>>();
/// <inheritdoc/>
public DatabaseInvoker Invoker { get; } = invoker ?? (_invoker ??= new DatabaseInvoker());
/// <inheritdoc/>
public DatabaseArgs DbArgs { get; set; } = new DatabaseArgs();
/// <inheritdoc/>
public DateTimeTransform DateTimeTransform { get; set; } = DateTimeTransform.UseDefault;
/// <inheritdoc/>
/// <remarks>Do not update the default properties directly as a shared static instance is used (unless this is the desired behaviour); create a new <see cref="Extended.DatabaseColumns"/> instance for overridding.</remarks>
public DatabaseColumns DatabaseColumns { get; set; } = _defaultColumns;
/// <summary>
/// Gets or sets the <see cref="DatabaseWildcard"/> to enable wildcard replacement.
/// </summary>
/// <remarks>Do not update the default properties directly as a shared static instance is used (unless this is the desired behaviour); create a new <see cref="DatabaseWildcard"/> instance for overridding.</remarks>
public DatabaseWildcard Wildcard { get; set; } = _defaultWildcard;
/// <inheritdoc/>
public bool EnableChangeLogMapperToDb { get; }
/// <inheritdoc/>
public virtual IConverter RowVersionConverter => throw new NotImplementedException();
/// <inheritdoc/>
public IJsonSerializer JsonSerializer { get; set; } = ExecutionContext.GetService<IJsonSerializer>() ?? CoreEx.Json.JsonSerializer.Default;
/// <inheritdoc/>
public DbConnection GetConnection() => _dbConn is not null ? _dbConn : Invokers.Invoker.RunSync(() => GetConnectionAsync());
/// <inheritdoc/>
public async Task<TConnection> GetConnectionAsync(CancellationToken cancellationToken = default)
{
if (_dbConn == null)
{
await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
if (_dbConn != null)
return _dbConn;
Logger?.LogDebug("Creating and opening the database connection. DatabaseId: {DatabaseId}", DatabaseId);
_dbConn = _dbConnCreate() ?? throw new InvalidOperationException($"The create function must create a valid {nameof(TConnection)} instance.");
await OnBeforeConnectionOpenAsync(_dbConn, cancellationToken).ConfigureAwait(false);
await _dbConn.OpenAsync(cancellationToken).ConfigureAwait(false);
await OnConnectionOpenAsync(_dbConn, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger?.LogError(ex, "Error occured whilst creating and opening the database connection. DatabaseId: {DatabaseId}", DatabaseId);
_dbConn = null;
throw;
}
finally
{
_semaphore.Release();
}
}
return _dbConn;
}
/// <summary>
/// Occurs before a connection is opened.
/// </summary>
/// <param name="connection">The <see cref="DbConnection"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
protected virtual Task OnBeforeConnectionOpenAsync(DbConnection connection, CancellationToken cancellationToken) => Task.CompletedTask;
/// <summary>
/// Occurs when a connection is opened before any corresponding data access is performed.
/// </summary>
/// <param name="connection">The <see cref="DbConnection"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
protected virtual Task OnConnectionOpenAsync(DbConnection connection, CancellationToken cancellationToken) => Task.CompletedTask;
/// <summary>
/// Occurs before a connection is closed.
/// </summary>
/// <param name="connection">The <see cref="DbConnection"/>.</param>
protected virtual Task OnConnectionCloseAsync(DbConnection connection) => Task.CompletedTask;
/// <inheritdoc/>
async Task<DbConnection> IDatabase.GetConnectionAsync(CancellationToken cancellationToken) => await GetConnectionAsync(cancellationToken).ConfigureAwait(false);
/// <inheritdoc/>
public DatabaseCommand StoredProcedure(string storedProcedure)
=> new(this, CommandType.StoredProcedure, storedProcedure.ThrowIfNull(nameof(storedProcedure)));
/// <inheritdoc/>
public DatabaseCommand SqlStatement(string sqlStatement)
=> new(this, CommandType.Text, sqlStatement.ThrowIfNull(nameof(sqlStatement)));
/// <inheritdoc/>
public DatabaseCommand SqlFromResource(string resourceName, Assembly? assembly = null)
=> SqlStatement(Abstractions.Resource.GetStreamReader(resourceName, assembly ?? Assembly.GetCallingAssembly()).ReadToEnd());
/// <inheritdoc/>
public DatabaseCommand SqlFromResource<TResource>(string resourceName)
=> SqlFromResource(resourceName, typeof(TResource).Assembly);
/// <inheritdoc/>
public Result? HandleDbException(DbException dbex)
{
var result = OnDbException(dbex);
return !result.HasValue || result.Value.IsSuccess ? Result.Fail(dbex) : result;
}
/// <summary>
/// Provides the <see cref="DbException"/> handling as a result of <see cref="HandleDbException(DbException)"/>.
/// </summary>
/// <param name="dbex">The <see cref="DbException"/>.</param>
/// <returns>The <see cref="Result"/> containing the appropriate <see cref="IResult.Error"/> where handled; otherwise, <c>null</c> indicating that the exception is unexpected and will continue to be thrown as such.</returns>
/// <remarks>Provides an opportunity to inspect and handle the exception before it is returned. A resulting <see cref="Result"/> that is <see cref="Result.IsSuccess"/> is not considered sensical; therefore, will result in the originating
/// exception being thrown.
/// <para>Where overridding and the <see cref="DbException"/> is not specifically handled then invoke the base to ensure any standard handling is executed.</para></remarks>
protected virtual Result? OnDbException(DbException dbex) => null;
/// <inheritdoc/>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Dispose of the resources.
/// </summary>
/// <param name="disposing">Indicates whether to dispose.</param>
protected virtual void Dispose(bool disposing)
{
if (disposing && _dbConn != null)
{
Logger?.LogDebug("Closing and disposing the database connection. DatabaseId: {DatabaseId}", DatabaseId);
Invokers.Invoker.RunSync(() => OnConnectionCloseAsync(_dbConn));
_dbConn.Dispose();
_dbConn = null;
}
}
/// <inheritdoc/>
public async ValueTask DisposeAsync()
{
await DisposeAsyncCore().ConfigureAwait(false);
GC.SuppressFinalize(this);
}
/// <summary>
/// Dispose of the resources asynchronously.
/// </summary>
public virtual async ValueTask DisposeAsyncCore()
{
if (_dbConn != null)
{
Logger?.LogDebug("Closing and disposing the database connection. DatabaseId: {DatabaseId}", DatabaseId);
await OnConnectionCloseAsync(_dbConn).ConfigureAwait(false);
await _dbConn.DisposeAsync().ConfigureAwait(false);
_dbConn = null;
}
Dispose();
}
}
}