-
Notifications
You must be signed in to change notification settings - Fork 3.2k
/
HistoryRepository.cs
375 lines (327 loc) · 15.9 KB
/
HistoryRepository.cs
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
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Text;
namespace Microsoft.EntityFrameworkCore.Migrations;
/// <summary>
/// A base class for the repository used to access the '__EFMigrationsHistory' table that tracks metadata
/// about EF Core Migrations such as which migrations have been applied.
/// </summary>
/// <remarks>
/// <para>
/// Database providers must inherit from this class to implement provider-specific functionality.
/// </para>
/// <para>
/// The service lifetime is <see cref="ServiceLifetime.Scoped" />. This means that each
/// <see cref="DbContext" /> instance will use its own instance of this service.
/// The implementation may depend on other services registered with any lifetime.
/// The implementation does not need to be thread-safe.
/// </para>
/// <para>
/// See <see href="https://aka.ms/efcore-docs-migrations">Database migrations</see> for more information and examples.
/// </para>
/// </remarks>
public abstract class HistoryRepository : IHistoryRepository
{
/// <summary>
/// The default name for the Migrations history table.
/// </summary>
public const string DefaultTableName = "__EFMigrationsHistory";
private IModel? _model;
private string? _migrationIdColumnName;
private string? _productVersionColumnName;
/// <summary>
/// Initializes a new instance of this class.
/// </summary>
/// <param name="dependencies">Parameter object containing dependencies for this service.</param>
protected HistoryRepository(HistoryRepositoryDependencies dependencies)
{
Dependencies = dependencies;
var relationalOptions = RelationalOptionsExtension.Extract(dependencies.Options);
TableName = relationalOptions.MigrationsHistoryTableName ?? DefaultTableName;
TableSchema = relationalOptions.MigrationsHistoryTableSchema;
}
/// <summary>
/// Relational provider-specific dependencies for this service.
/// </summary>
protected virtual HistoryRepositoryDependencies Dependencies { get; }
/// <summary>
/// A helper class for generation of SQL.
/// </summary>
protected virtual ISqlGenerationHelper SqlGenerationHelper
=> Dependencies.SqlGenerationHelper;
/// <summary>
/// THe history table name.
/// </summary>
protected virtual string TableName { get; }
/// <summary>
/// The schema that contains the history table, or <see langword="null" /> if the default schema should be used.
/// </summary>
protected virtual string? TableSchema { get; }
/// <summary>
/// The name of the column that holds the Migration identifier.
/// </summary>
protected virtual string MigrationIdColumnName
=> _migrationIdColumnName ??= EnsureModel()
.FindEntityType(typeof(HistoryRow))!
.FindProperty(nameof(HistoryRow.MigrationId))!
.GetColumnName();
private IModel EnsureModel()
{
if (_model == null)
{
var conventionSet = Dependencies.ConventionSetBuilder.CreateConventionSet();
conventionSet.Remove(typeof(DbSetFindingConvention));
conventionSet.Remove(typeof(RelationalDbFunctionAttributeConvention));
var modelBuilder = new ModelBuilder(conventionSet);
modelBuilder.Entity<HistoryRow>(
x =>
{
ConfigureTable(x);
x.ToTable(TableName, TableSchema);
});
_model = Dependencies.ModelRuntimeInitializer.Initialize(
(IModel)modelBuilder.Model, designTime: true, validationLogger: null);
}
return _model;
}
/// <summary>
/// The name of the column that contains the Entity Framework product version.
/// </summary>
protected virtual string ProductVersionColumnName
=> _productVersionColumnName ??= EnsureModel()
.FindEntityType(typeof(HistoryRow))!
.FindProperty(nameof(HistoryRow.ProductVersion))!
.GetColumnName();
/// <summary>
/// Overridden by database providers to generate SQL that tests for existence of the history table.
/// </summary>
protected abstract string ExistsSql { get; }
/// <summary>
/// Checks whether or not the history table exists.
/// </summary>
/// <returns><see langword="true" /> if the table already exists, <see langword="false" /> otherwise.</returns>
public virtual bool Exists()
=> InterpretExistsResult(
Dependencies.RawSqlCommandBuilder.Build(ExistsSql).ExecuteScalar(
new RelationalCommandParameterObject(
Dependencies.Connection,
null,
null,
Dependencies.CurrentContext.Context,
Dependencies.CommandLogger, CommandSource.Migrations)));
/// <summary>
/// Checks whether or not the history table exists.
/// </summary>
/// <param name="cancellationToken">A <see cref="CancellationToken" /> to observe while waiting for the task to complete.</param>
/// <returns>
/// A task that represents the asynchronous operation. The task result contains
/// <see langword="true" /> if the table already exists, <see langword="false" /> otherwise.
/// </returns>
/// <exception cref="OperationCanceledException">If the <see cref="CancellationToken" /> is canceled.</exception>
public virtual async Task<bool> ExistsAsync(CancellationToken cancellationToken = default)
=> InterpretExistsResult(
await Dependencies.RawSqlCommandBuilder.Build(ExistsSql).ExecuteScalarAsync(
new RelationalCommandParameterObject(
Dependencies.Connection,
null,
null,
Dependencies.CurrentContext.Context,
Dependencies.CommandLogger, CommandSource.Migrations),
cancellationToken).ConfigureAwait(false));
/// <summary>
/// Interprets the result of executing <see cref="ExistsSql" />.
/// </summary>
/// <returns><see langword="true" /> if the table already exists, <see langword="false" /> otherwise.</returns>
protected abstract bool InterpretExistsResult(object? value);
/// <summary>
/// Overridden by a database provider to generate a SQL script that will create the history table
/// if and only if it does not already exist.
/// </summary>
/// <returns>The SQL script.</returns>
public abstract string GetCreateIfNotExistsScript();
/// <summary>
/// Generates a SQL script that will create the history table.
/// </summary>
/// <returns>The SQL script.</returns>
public virtual string GetCreateScript()
=> string.Concat(GetCreateCommands().Select(c => c.CommandText));
/// <summary>
/// Creates the history table.
/// </summary>
public virtual void Create()
=> Dependencies.MigrationCommandExecutor.ExecuteNonQuery(GetCreateCommands(), Dependencies.Connection);
/// <summary>
/// Creates the history table.
/// </summary>
public virtual Task CreateAsync(CancellationToken cancellationToken = default)
=> Dependencies.MigrationCommandExecutor.ExecuteNonQueryAsync(GetCreateCommands(), Dependencies.Connection, cancellationToken);
/// <summary>
/// Returns the migration commands that will create the history table.
/// </summary>
/// <returns>The migration commands that will create the history table.</returns>
protected virtual IReadOnlyList<MigrationCommand> GetCreateCommands()
{
var model = EnsureModel();
var operations = Dependencies.ModelDiffer.GetDifferences(null, model.GetRelationalModel());
var commandList = Dependencies.MigrationsSqlGenerator.Generate(operations, model);
return commandList;
}
/// <summary>
/// Gets an exclusive lock on the database.
/// </summary>
/// <param name="timeout">The time to wait for the lock before an exception is thrown.</param>
/// <returns>An object that can be disposed to release the lock.</returns>
public abstract IDisposable GetDatabaseLock(TimeSpan timeout);
/// <summary>
/// Gets an exclusive lock on the database.
/// </summary>
/// <param name="timeout">The time to wait for the lock before an exception is thrown.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken" /> to observe while waiting for the task to complete.</param>
/// <returns>An object that can be disposed to release the lock.</returns>
/// <exception cref="OperationCanceledException">If the <see cref="CancellationToken" /> is canceled.</exception>
public abstract Task<IAsyncDisposable> GetDatabaseLockAsync(TimeSpan timeout, CancellationToken cancellationToken = default);
/// <summary>
/// Configures the entity type mapped to the history table.
/// </summary>
/// <remarks>
/// Database providers can override this to add or replace configuration.
/// </remarks>
/// <param name="history">A builder for the <see cref="HistoryRow" /> entity type.</param>
protected virtual void ConfigureTable(EntityTypeBuilder<HistoryRow> history)
{
history.ToTable(DefaultTableName);
history.HasKey(h => h.MigrationId);
history.Property(h => h.MigrationId).HasMaxLength(150);
history.Property(h => h.ProductVersion).HasMaxLength(32).IsRequired();
}
/// <summary>
/// Queries the history table for all migrations that have been applied.
/// </summary>
/// <returns>The list of applied migrations, as <see cref="HistoryRow" /> entities.</returns>
public virtual IReadOnlyList<HistoryRow> GetAppliedMigrations()
{
var rows = new List<HistoryRow>();
if (Exists())
{
var command = Dependencies.RawSqlCommandBuilder.Build(GetAppliedMigrationsSql);
using var reader = command.ExecuteReader(
new RelationalCommandParameterObject(
Dependencies.Connection,
null,
null,
Dependencies.CurrentContext.Context,
Dependencies.CommandLogger, CommandSource.Migrations));
while (reader.Read())
{
rows.Add(new HistoryRow(reader.DbDataReader.GetString(0), reader.DbDataReader.GetString(1)));
}
}
return rows;
}
/// <summary>
/// Queries the history table for all migrations that have been applied.
/// </summary>
/// <param name="cancellationToken">A <see cref="CancellationToken" /> to observe while waiting for the task to complete.</param>
/// <returns>
/// A task that represents the asynchronous operation. The task result contains
/// the list of applied migrations, as <see cref="HistoryRow" /> entities.
/// </returns>
/// <exception cref="OperationCanceledException">If the <see cref="CancellationToken" /> is canceled.</exception>
public virtual async Task<IReadOnlyList<HistoryRow>> GetAppliedMigrationsAsync(
CancellationToken cancellationToken = default)
{
var rows = new List<HistoryRow>();
if (await ExistsAsync(cancellationToken).ConfigureAwait(false))
{
var command = Dependencies.RawSqlCommandBuilder.Build(GetAppliedMigrationsSql);
var reader = await command.ExecuteReaderAsync(
new RelationalCommandParameterObject(
Dependencies.Connection,
null,
null,
Dependencies.CurrentContext.Context,
Dependencies.CommandLogger, CommandSource.Migrations),
cancellationToken).ConfigureAwait(false);
await using var _ = reader.ConfigureAwait(false);
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
rows.Add(new HistoryRow(reader.DbDataReader.GetString(0), reader.DbDataReader.GetString(1)));
}
}
return rows;
}
/// <summary>
/// Generates SQL to query for the migrations that have been applied.
/// </summary>
protected virtual string GetAppliedMigrationsSql
=> new StringBuilder()
.Append("SELECT ")
.Append(SqlGenerationHelper.DelimitIdentifier(MigrationIdColumnName))
.Append(", ")
.AppendLine(SqlGenerationHelper.DelimitIdentifier(ProductVersionColumnName))
.Append("FROM ")
.AppendLine(SqlGenerationHelper.DelimitIdentifier(TableName, TableSchema))
.Append("ORDER BY ")
.Append(SqlGenerationHelper.DelimitIdentifier(MigrationIdColumnName))
.Append(SqlGenerationHelper.StatementTerminator)
.ToString();
/// <summary>
/// Generates a SQL script to insert a row into the history table.
/// </summary>
/// <param name="row">The row to insert, represented as a <see cref="HistoryRow" /> entity.</param>
/// <returns>The generated SQL.</returns>
public virtual string GetInsertScript(HistoryRow row)
{
var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string));
return new StringBuilder().Append("INSERT INTO ")
.Append(SqlGenerationHelper.DelimitIdentifier(TableName, TableSchema))
.Append(" (")
.Append(SqlGenerationHelper.DelimitIdentifier(MigrationIdColumnName))
.Append(", ")
.Append(SqlGenerationHelper.DelimitIdentifier(ProductVersionColumnName))
.AppendLine(")")
.Append("VALUES (")
.Append(stringTypeMapping.GenerateSqlLiteral(row.MigrationId))
.Append(", ")
.Append(stringTypeMapping.GenerateSqlLiteral(row.ProductVersion))
.Append(')')
.AppendLine(SqlGenerationHelper.StatementTerminator)
.ToString();
}
/// <summary>
/// Generates a SQL script to delete a row from the history table.
/// </summary>
/// <param name="migrationId">The migration identifier of the row to delete.</param>
/// <returns>The generated SQL.</returns>
public virtual string GetDeleteScript(string migrationId)
{
var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string));
return new StringBuilder().Append("DELETE FROM ")
.AppendLine(SqlGenerationHelper.DelimitIdentifier(TableName, TableSchema))
.Append("WHERE ")
.Append(SqlGenerationHelper.DelimitIdentifier(MigrationIdColumnName))
.Append(" = ")
.Append(stringTypeMapping.GenerateSqlLiteral(migrationId))
.AppendLine(SqlGenerationHelper.StatementTerminator)
.ToString();
}
/// <summary>
/// Overridden by database providers to generate a SQL Script that will <c>BEGIN</c> a block
/// of SQL if and only if the migration with the given identifier does not already exist in the history table.
/// </summary>
/// <param name="migrationId">The migration identifier.</param>
/// <returns>The generated SQL.</returns>
public abstract string GetBeginIfNotExistsScript(string migrationId);
/// <summary>
/// Overridden by database providers to generate a SQL Script that will <c>BEGIN</c> a block
/// of SQL if and only if the migration with the given identifier already exists in the history table.
/// </summary>
/// <param name="migrationId">The migration identifier.</param>
/// <returns>The generated SQL.</returns>
public abstract string GetBeginIfExistsScript(string migrationId);
/// <summary>
/// Overridden by database providers to generate a SQL script to <c>END</c> the SQL block.
/// </summary>
/// <returns>The generated SQL.</returns>
public abstract string GetEndIfScript();
}