-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathDatabase.cs
More file actions
187 lines (152 loc) · 7.87 KB
/
Copy pathDatabase.cs
File metadata and controls
187 lines (152 loc) · 7.87 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
namespace CoreEx.Database;
/// <summary>
/// Provides the common/base <see cref="IDatabase"/> access functionality.
/// </summary>
/// <typeparam name="TConnection">The <see cref="DbConnection"/> <see cref="Type"/>.</typeparam>
/// <typeparam name="TCommand">The <see cref="DatabaseCommand{TDatabaseArgs, TSelf}"/> <see cref="Type"/>.</typeparam>
/// <typeparam name="TDatabaseArgs">The <see cref="DatabaseArgs"/> <see cref="Type"/>.</typeparam>
/// <typeparam name="TDatabaseColumns">The <see cref="DatabaseColumns"/> <see cref="Type"/>.</typeparam>
/// <param name="connection">The <typeparamref name="TConnection"/> <see cref="DbConnection"/>.</param>
/// <param name="invoker">The <see cref="DatabaseInvoker"/>.</param>
/// <param name="namedColumns">The convention-based column names.</param>
/// <param name="jsonSerializerOptions">The optional <see cref="JsonSerializerOptions"/>.</param>
/// <param name="logger">The optional <see cref="ILogger"/>.</param>
public abstract class Database<TConnection, TCommand, TDatabaseArgs, TDatabaseColumns>(TConnection connection, DatabaseInvoker invoker, TDatabaseColumns namedColumns, JsonSerializerOptions? jsonSerializerOptions = null, ILogger<Database<TConnection, TCommand, TDatabaseArgs, TDatabaseColumns>>? logger = null) : IDatabase, IDisposable
where TConnection : DbConnection where TCommand : DatabaseCommand<TDatabaseArgs, TCommand> where TDatabaseArgs : DatabaseArgs, new() where TDatabaseColumns : DatabaseColumns
{
private static readonly TDatabaseArgs _defaultDbArgs = new();
private static readonly DatabaseWildcard _defaultWildcard = new();
private readonly SemaphoreSlim _semaphore = new(1, 1);
private readonly TConnection _dbConn = connection.ThrowIfNull().ThrowWhen(connection => connection.State != ConnectionState.Closed && connection.State != ConnectionState.Open);
private int _savePointCounter = 0;
private bool _disposed;
/// <inheritdoc/>
public string DatabaseId { get; } = Guid.NewGuid().ToString();
/// <inheritdoc/>
public ILogger? Logger { get; } = logger ?? ExecutionContext.GetService<ILogger<Database<TConnection, TCommand, TDatabaseArgs, TDatabaseColumns>>>();
/// <inheritdoc/>
public DatabaseInvoker Invoker { get; } = invoker.ThrowIfNull();
/// <inheritdoc/>
DatabaseArgs IDatabase.DbArgs => DbArgs;
/// <summary>
/// Gets or sets the default <typeparamref name="TDatabaseArgs"/> used where not explicitly specified for an operation.
/// </summary>
public TDatabaseArgs DbArgs { get; set; } = _defaultDbArgs;
/// <inheritdoc/>
public DateTimeTransform DateTimeTransform { get; set; } = DateTimeTransform.UseDefault;
/// <inheritdoc/>
public bool DateTimeOffsetTransform { get; set; } = true;
/// <inheritdoc/>
DatabaseColumns IDatabase.NamedColumns => NamedColumns;
/// <summary>
/// Gets or sets the names of the convention-based <see cref="Extended.DatabaseColumns"/>.
/// </summary>
public TDatabaseColumns NamedColumns { get; set; } = namedColumns.ThrowIfNull();
/// <summary>
/// Gets or sets the <see cref="DatabaseWildcard"/> to enable wildcard replacement.
/// </summary>
public DatabaseWildcard Wildcard { get; set; } = _defaultWildcard;
/// <inheritdoc/>
public abstract ISourceConverter<string?> RowVersionConverter { get; }
/// <inheritdoc/>
public JsonSerializerOptions JsonSerializerOptions { get; } = jsonSerializerOptions ?? JsonDefaults.SerializerOptions;
/// <inheritdoc/>
public DbTransaction? CurrentTransaction { get; protected set; }
/// <inheritdoc/>
public bool IsInTransaction => CurrentTransaction is not null;
/// <inheritdoc/>
public void UseTransaction(DbTransaction? transaction)
{
if (CurrentTransaction != transaction)
{
CurrentTransaction = transaction;
UseTransactionChanged?.Invoke(this, EventArgs.Empty);
}
}
/// <inheritdoc/>
public event EventHandler? UseTransactionChanged;
/// <inheritdoc/>
DbConnection IDatabase.Connection => _dbConn;
/// <inheritdoc/>
async Task<DbConnection> IDatabase.GetConnectionAsync(CancellationToken cancellationToken) => await GetConnectionAsync(cancellationToken).ConfigureAwait(false);
/// <summary>
/// Gets the <typeparamref name="TConnection"/>.
/// </summary>
/// <remarks>The connection is opened on first use.</remarks>
public async Task<TConnection> GetConnectionAsync(CancellationToken cancellationToken = default)
{
if (_dbConn.State == ConnectionState.Open)
return _dbConn;
await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
if (_dbConn.State == ConnectionState.Open)
return _dbConn;
if (_dbConn.State != ConnectionState.Closed)
throw new InvalidOperationException($"The database connection is in an invalid state: {_dbConn.State}.");
if (Logger is not null && Logger.IsEnabled(LogLevel.Debug))
Logger.LogDebug("Opening the database connection. [DatabaseId: {DatabaseId}]", DatabaseId);
await _dbConn.OpenAsync(cancellationToken).ConfigureAwait(false);
return _dbConn;
}
catch (Exception ex)
{
if (Logger is not null && Logger.IsEnabled(LogLevel.Error))
Logger.LogError(ex, "Error occurred whilst opening the database connection. [DatabaseId: {DatabaseId}]", DatabaseId);
throw;
}
finally
{
_semaphore.Release();
}
}
/// <inheritdoc/>
DatabaseCommand IDatabase.Statement(SqlStatement statement) => Statement(statement);
/// <summary>
/// Creates a<typeparamref name="TCommand"/> for the <see cref="SqlStatement"/>.
/// </summary>
/// <param name="statement">The <see cref="SqlStatement"/>.</param>
/// <returns>The <typeparamref name="TCommand"/>.</returns>
public abstract TCommand Statement(SqlStatement statement);
/// <inheritdoc/>
public abstract DbParameter CreateParameter();
/// <inheritdoc/>
public Exception? HandleDbException(DbException dbex) => OnDbException(dbex);
/// <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="Exception"/> where handled (converted); otherwise, <see langword="null"/> indicating that the exception is unexpected and will continue to be thrown/bubbled as such.</returns>
/// <remarks>Provides an opportunity to inspect and convert the exception before it continues to bubble.
/// <para>Where overriding and the <see cref="DbException"/> is not specifically handled then invoke the base to ensure any standard handling is executed.</para></remarks>
protected virtual Exception? OnDbException(DbException dbex) => null;
/// <summary>
/// Gets the next (monotonic counter) save-point name.
/// </summary>
/// <returns>The save-point name.</returns>
public string GetNextSavePointName()
{
var counter = Interlocked.Increment(ref _savePointCounter);
return $"SP_{counter}";
}
/// <summary>
/// Dispose of resources.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases the unmanaged resources used by the <see cref="Database"/> and optionally releases the managed resources.
/// </summary>
/// <param name="disposing"><see langword="true"/> to release both managed and unmanaged resources; <see langword="false"/> to release only unmanaged resources.</param>
protected virtual void Dispose(bool disposing)
{
if (disposing && !_disposed)
{
_disposed = true;
_semaphore.Dispose();
}
}
}