Skip to content

Commit

Permalink
Merge pull request Kros-sk#7 Migrations middleware supports multiple …
Browse files Browse the repository at this point in the history
…databases

- Migrations middleware supports multiple databases. To migrate specific database, add its name to the URL: /kormmigration/dbname. If the name is not specified (middleware is called with just /kormmigration URL), deafult connection string name will be used: DefaultConnection.
- A small breaking change is introduces. Endpoint URL for migrations is /kormmigration to make it consistent with documentation (was /kormmigrate before).
  • Loading branch information
satano authored Jul 28, 2019
2 parents b65889b + b129fe3 commit c93fe6e
Show file tree
Hide file tree
Showing 13 changed files with 154 additions and 216 deletions.
40 changes: 25 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,33 +89,43 @@ CONSTRAINT [PK_People] PRIMARY KEY CLUSTERED ([Id] ASC)
GO
```

Migration can also be executed through an HTTP request. By calling the `/kormmigration` endpoint, the necessary migrations will be executed.
However, you need to add middleware:
Migration can also be executed through an HTTP request. By calling the `/kormmigration` endpoint, the necessary migrations will be executed. However, you need to add middleware:

``` csharp
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseKormMigrations(o =>
{
o.EndpointUrl = "/kormmigration";
});
app.UseKormMigrations();
}
```

You can change the endpoint URL by configuration:

``` csharp
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseKormMigrations(options =>
{
options.EndpointUrl = "/loremipsum";
});
}
```

If multiple KORM databases are registered, all of them have unique name. Migrations are performed per database and the name of the database is specified in URL as another path segment: `/kormmigration/dbname` If the name is not specified, default connection string will be used (`DefaultConnection`).

If you have scripts stored in a different way (for example, somewhere on a disk or in another assembly), you can configure your own providers to get these scripts.

``` csharp
public void ConfigureServices(IServiceCollection services)
{
services.AddKorm(Configuration)
.AddKormMigrations(o =>
{
var assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(x => x.FullName.StartWith("Demo.DatabaseLayer"));
o.AddAssemblyScriptsProvider(assembly, "Demo.DatabaseLayer.Resources");
o.AddFileScriptsProvider(@"C:\scripts\");
o.AddScriptsProvider(new MyCustomScriptsProvider());
})
.Migrate();
services.AddKorm(Configuration)
.AddKormMigrations(o =>
{
var assembly = AppDomain.CurrentDomain.GetAssemblies() .FirstOrDefault(x => x.FullName.StartWith ("Demo.DatabaseLayer") );
o.AddAssemblyScriptsProvider(assembly, "Demo.DatabaseLayer.Resources");
o.AddFileScriptsProvider(@"C:\scripts\");
o.AddScriptsProvider(new MyCustomScriptsProvider());
})
.Migrate();
}
```

Expand Down
21 changes: 15 additions & 6 deletions src/DatabaseFactory.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Kros.KORM.Extensions.Asp.Properties;
using Kros.KORM.Migrations;
using Kros.Utils;
using Microsoft.Extensions.DependencyInjection;
using System;
Expand All @@ -23,18 +24,14 @@ private static Dictionary<string, KormBuilder> AddBuildersDictionary(IServiceCol
/// <param name="name">The name of the database builder.</param>
/// <param name="builder">Database builder.</param>
/// <returns><see langword="true"/>, if this was the first builder added, otherwise <see langword="false"/>.</returns>
internal static bool AddBuilder(IServiceCollection services, string name, KormBuilder builder)
internal static void AddBuilder(IServiceCollection services, string name, KormBuilder builder)
{
Dictionary<string, KormBuilder> builders = AddBuildersDictionary(services);
if (builders.ContainsKey(name))
{
throw new ArgumentException(string.Format(Resources.DuplicateDatabaseName, name), nameof(name));
}
builders.Add(name, builder);

// We need to know if it was the first builder added.
// The first builder is added into the service container also as IDatabase dependency.
return builders.Count == 1;
}

private readonly ConcurrentDictionary<string, IDatabase> _databases = new ConcurrentDictionary<string, IDatabase>();
Expand All @@ -47,6 +44,18 @@ internal DatabaseFactory(IServiceCollection services)
}

IDatabase IDatabaseFactory.GetDatabase(string name)
{
KormBuilder builder = GetBuilder(name);
return _databases.GetOrAdd(name, _ => builder.Build());
}

IMigrationsRunner IDatabaseFactory.GetMigrationsRunner(string name)
{
KormBuilder builder = GetBuilder(name);
return builder.MigrationsRunner;
}

private KormBuilder GetBuilder(string name)
{
if (_disposed)
{
Expand All @@ -60,7 +69,7 @@ IDatabase IDatabaseFactory.GetDatabase(string name)
string.Format(Resources.InvalidDatabaseName, name, nameof(ServiceCollectionExtensions.AddKorm)),
nameof(name));
}
return _databases.GetOrAdd(name, _ => builder.Build());
return builder;
}

public void Dispose()
Expand Down
23 changes: 22 additions & 1 deletion src/IDatabaseFactory.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using Kros.KORM.Migrations;
using System;

namespace Kros.KORM.Extensions.Asp
{
Expand All @@ -22,5 +23,25 @@ public interface IDatabaseFactory : IDisposable
/// </list>
/// </exception>
IDatabase GetDatabase(string name);

/// <summary>
/// Returns the migrations runner for database with specified <paramref name="name"/>.
/// </summary>
/// <param name="name">Name of the database.</param>
/// <returns>Implementation of <see cref="IMigrationsRunner"/> or <see langword="null"/>.</returns>
/// <remarks>
/// Migrations must be added by <see cref="KormBuilder.AddKormMigrations(Action{MigrationOptions})"/>.
/// If migrations was not added for specified database, this method returns <see langword="null"/>.
/// </remarks>
/// <exception cref="ArgumentNullException">The value of <paramref name="name"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">
/// The value of <paramref name="name"/> is:
/// <list type="bullet">
/// <item>Empty string.</item>
/// <item>String containing whitespace characters.</item>
/// <item>Ivalid name. The database with that name is not registered.</item>
/// </list>
/// </exception>
IMigrationsRunner GetMigrationsRunner(string name);
}
}
71 changes: 24 additions & 47 deletions src/KormBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Kros.Data;
using Kros.KORM.Extensions.Asp.Properties;
using Kros.KORM.Migrations;
using Kros.KORM.Migrations.Middleware;
using Kros.KORM.Migrations.Providers;
Expand All @@ -18,63 +19,47 @@ public class KormBuilder
/// </summary>
public const string DefaultConnectionStringName = "DefaultConnection";

internal const string DefaultProviderName = Kros.Data.SqlServer.SqlServerDataHelper.ClientId;
internal const bool DefaultAutoMigrate = false;

private readonly IDatabaseBuilder _builder;
private IMigrationsRunner _migrationsRunner;

/// <summary>
/// Initializes a new instance of the <see cref="KormBuilder"/> class. Automatic migrations are off and
/// Microsoft SQL Server KORM provider is used.
/// Initializes a new instance of the <see cref="KormBuilder"/> class.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="connectionString">The database connection string.</param>
public KormBuilder(IServiceCollection services, string connectionString)
: this(services, connectionString, DefaultAutoMigrate, DefaultProviderName)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="KormBuilder"/> class. Microsoft SQL Server KORM provider is used.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="connectionString">The database connection string.</param>
/// <param name="autoMigrate">
/// Value for setting if automatic migrations (<see cref="Migrate()"/>) are allowed or not.
/// </param>
public KormBuilder(IServiceCollection services, string connectionString, bool autoMigrate)
: this(services, connectionString, autoMigrate, DefaultProviderName)
: this(services, new KormConnectionSettings(connectionString))
{
}

/// <summary>
/// Initializes a new instance of the <see cref="KormBuilder"/> class.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="connectionString">The database connection string.</param>
/// <param name="autoMigrate">
/// Value for setting if automatic migrations (<see cref="Migrate()"/>) are allowed or not.
/// </param>
/// <param name="kormProvider">KORM provider value.</param>
public KormBuilder(IServiceCollection services, string connectionString, bool autoMigrate, string kormProvider)
/// <param name="connectionSettings">The database connection settings.</param>
public KormBuilder(IServiceCollection services, KormConnectionSettings connectionSettings)
{
Services = Check.NotNull(services, nameof(services));
ConnectionString = Check.NotNullOrWhiteSpace(connectionString, nameof(connectionString));
KormProvider = Check.NotNullOrWhiteSpace(kormProvider, nameof(kormProvider));
AutoMigrate = autoMigrate;
ConnectionSettings = Check.NotNull(connectionSettings, nameof(connectionSettings));
Check.NotNullOrWhiteSpace(
connectionSettings.ConnectionString, nameof(connectionSettings), Resources.EmptyConnectionStringInSettings);

_builder = Database.Builder;
_builder.UseConnection(connectionString, kormProvider);
_builder.UseConnection(connectionSettings);
}

/// <summary>
/// Gets the service collection.
/// </summary>
public IServiceCollection Services { get; }

internal string ConnectionString { get; }
internal string KormProvider { get; }
internal bool AutoMigrate { get; }
/// <summary>
/// <see cref="MigrationsRunner"/> for this database, if it was set
/// using <see cref="AddKormMigrations(Action{MigrationOptions})"/> method.
/// </summary>
public IMigrationsRunner MigrationsRunner => _migrationsRunner;

internal KormConnectionSettings ConnectionSettings { get; }

/// <summary>
/// Use database configuration.
Expand Down Expand Up @@ -104,7 +89,8 @@ public KormBuilder UseDatabaseConfiguration(DatabaseConfigurationBase databaseCo
/// <returns>This instance.</returns>
public KormBuilder InitDatabaseForIdGenerator()
{
IIdGeneratorFactory factory = IdGeneratorFactories.GetFactory(ConnectionString, KormProvider);
IIdGeneratorFactory factory = IdGeneratorFactories.GetFactory(
ConnectionSettings.ConnectionString, ConnectionSettings.KormProvider);
using (IIdGenerator idGenerator = factory.GetGenerator(string.Empty))
{
idGenerator.InitDatabaseForIdGenerator();
Expand All @@ -119,15 +105,9 @@ public KormBuilder InitDatabaseForIdGenerator()
/// <returns>This instance of <see cref="KormBuilder"/>.</returns>
public KormBuilder AddKormMigrations(Action<MigrationOptions> setupAction = null)
{
Services
.AddMemoryCache()
.AddTransient((Func<IServiceProvider, IMigrationsRunner>)(s =>
{
var database = new Database(ConnectionString, KormProvider);
MigrationOptions options = SetupMigrationOptions(setupAction);
return new MigrationsRunner(database, options);
}));

Services.AddMemoryCache();
MigrationOptions options = SetupMigrationOptions(setupAction);
_migrationsRunner = new MigrationsRunner(ConnectionSettings.GetFullConnectionString(), options);
return this;
}

Expand All @@ -152,12 +132,9 @@ private static MigrationOptions SetupMigrationOptions(Action<MigrationOptions> s
/// </summary>
public void Migrate()
{
if (AutoMigrate)
if (ConnectionSettings.AutoMigrate && (MigrationsRunner != null))
{
Services.BuildServiceProvider()
.GetService<IMigrationsRunner>()
.MigrateAsync()
.Wait();
MigrationsRunner.MigrateAsync().Wait();
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/Kros.KORM.Extensions.Asp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Kros.KORM" Version="4.0.0-alfa5" />
<PackageReference Include="Kros.KORM" Version="4.0.0-alpha.8" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="2.2.0" />
Expand Down
4 changes: 2 additions & 2 deletions src/Migrations/Middleware/MigrationMiddlewareOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ namespace Kros.KORM.Migrations.Middleware
public class MigrationMiddlewareOptions
{
/// <summary>
/// Migrations endpoint URL.
/// Migrations endpoint URL. Default value is <c>/kormmigration</c>.
/// </summary>
public string EndpointUrl { get; set; } = "/kormmigrate";
public string EndpointUrl { get; set; } = "/kormmigration";

/// <summary>
/// Minimum time between two migrations.
Expand Down
39 changes: 23 additions & 16 deletions src/Migrations/Middleware/MigrationsMiddleware.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Kros.Utils;
using Kros.KORM.Extensions.Asp;
using Kros.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory;
using System.Threading.Tasks;
Expand All @@ -10,7 +11,7 @@ namespace Kros.KORM.Migrations.Middleware
/// </summary>
public class MigrationsMiddleware
{
private const string WasMigrationExecutedKey = "WasMigrationExecuted";
private const string MigrationExecutedKey = "KormMigrationsExecuted";

#pragma warning disable IDE0052 // Remove unread private members
private readonly RequestDelegate _next;
Expand All @@ -24,40 +25,46 @@ public class MigrationsMiddleware
/// <param name="next">The next delegate.</param>
/// <param name="cache">Memory cache.</param>
/// <param name="options">Migration options.</param>
public MigrationsMiddleware(
RequestDelegate next,
IMemoryCache cache,
MigrationMiddlewareOptions options)
public MigrationsMiddleware(RequestDelegate next, IMemoryCache cache, MigrationMiddlewareOptions options)
{
_next = next;
_options = Check.NotNull(options, nameof(options));
_cache = Check.NotNull(cache, nameof(cache));
}

#pragma warning disable IDE0060 // Remove unused parameter
/// <summary>
/// Invokes the specified context.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="migrationsRunner">Migrations runner.</param>
public async Task Invoke(HttpContext context, IMigrationsRunner migrationsRunner)
/// <param name="databaseFactory">Database factory for getting <see cref="IMigrationsRunner"/>.</param>
public async Task Invoke(HttpContext context, IDatabaseFactory databaseFactory)
{
if (CanMigrate())
string databaseName = null;
if (context.Request.Path.HasValue)
{
SetupCache();
databaseName = context.Request.Path.Value.Trim('/');
}
if (string.IsNullOrEmpty(databaseName))
{
databaseName = KormBuilder.DefaultConnectionStringName;
}
IMigrationsRunner migrationsRunner = databaseFactory.GetMigrationsRunner(databaseName);
if ((migrationsRunner != null) && CanMigrate(databaseName))
{
SetupCache(databaseName);
await migrationsRunner.MigrateAsync();
}
}
#pragma warning restore IDE0060 // Remove unused parameter

private bool CanMigrate()
=> !_cache.TryGetValue(WasMigrationExecutedKey, out bool migrated) || !migrated;
private bool CanMigrate(string name) => !_cache.TryGetValue(GetCacheKey(name), out bool migrated) || !migrated;

private void SetupCache()
private void SetupCache(string name)
{
var options = new MemoryCacheEntryOptions();
options.SetSlidingExpiration(_options.SlidingExpirationBetweenMigrations);
_cache.Set(WasMigrationExecutedKey, true, options);
_cache.Set(GetCacheKey(name), true, options);
}

private string GetCacheKey(string name) => MigrationExecutedKey + "-" + name;
}
}
9 changes: 9 additions & 0 deletions src/Properties/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/Properties/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@
<value>Database with name "{0}" was already added.</value>
<comment>0 - name of the database/connection string</comment>
</data>
<data name="EmptyConnectionStringInSettings" xml:space="preserve">
<value>Connection string in connection settings is empty.</value>
</data>
<data name="InvalidConnectionStringName" xml:space="preserve">
<value>Connection strings section does not contain a connection string with name '{0}'.</value>
<comment>0 - connection string name in appsettings</comment>
Expand Down
Loading

0 comments on commit c93fe6e

Please sign in to comment.