Skip to content

Commit

Permalink
Fix getting the connection string when it's a Cloud-providerd LocalDb
Browse files Browse the repository at this point in the history
Use the constants approach from #7
  • Loading branch information
nul800sebastiaan committed Mar 7, 2022
1 parent 11c6b95 commit e6d213b
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 31 deletions.
151 changes: 151 additions & 0 deletions Cultiv.Hangfire/ConfigurationExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
using System;
using System.Data.Common;
using Microsoft.Extensions.Configuration;

namespace Cultiv.Hangfire
{
/// <summary>
/// Extension methods for configuration.
/// Borrowed from: https://github.com/umbraco/Umbraco-CMS/pull/11610
/// </summary>
public static class ConfigurationExtensions
{
/// <summary>
/// Gets the provider name for the connection string name (shorthand for <c>GetSection("ConnectionStrings")[name + "_ProviderName"]</c>).
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="name">The connection string key.</param>
/// <returns>The provider name.</returns>
/// <remarks>
/// This uses the same convention as the <a href="https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-5.0#connection-string-prefixes-1">Configuration API for connection string environment variables</a>.
/// </remarks>
public static string GetConnectionStringProviderName(this IConfiguration configuration, string name)
=> configuration.GetConnectionString(name + "_ProviderName");

/// <summary>
/// Gets the Umbraco connection string (shorthand for <c>GetSection("ConnectionStrings")[name]</c> and replacing the <c>|DataDirectory|</c> placeholder).
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="name">The connection string key.</param>
/// <returns>The Umbraco connection string.</returns>
public static string GetUmbracoConnectionString(this IConfiguration configuration, string name = Umbraco.Cms.Core.Constants.System.UmbracoConnectionName)
=> configuration.GetUmbracoConnectionString(name, out _);

/// <summary>
/// Gets the Umbraco connection string and provider name (shorthand for <c>GetSection("ConnectionStrings")[Constants.System.UmbracoConnectionName]</c> and replacing the <c>|DataDirectory|</c> placeholder).
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="providerName">The provider name.</param>
/// <returns>The Umbraco connection string.</returns>
public static string GetUmbracoConnectionString(this IConfiguration configuration, out string providerName)
=> configuration.GetUmbracoConnectionString(Umbraco.Cms.Core.Constants.System.UmbracoConnectionName, out providerName);

/// <summary>
/// Gets the Umbraco connection string and provider name (shorthand for <c>GetSection("ConnectionStrings")[name]</c> and replacing the <c>|DataDirectory|</c> placeholder).
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="name">The name.</param>
/// <param name="providerName">The provider name.</param>
/// <returns>The Umbraco connection string.</returns>
public static string GetUmbracoConnectionString(this IConfiguration configuration, string name, out string providerName)
{
var connectionString = configuration.GetConnectionString(name);
if (!string.IsNullOrEmpty(connectionString))
{
var builder = new DbConnectionStringBuilder
{
ConnectionString = connectionString
};

// Replace data directory placeholder
if (TryReplaceDataDirectory(builder))
{
// Mutate the existing connection string (note: the builder also lowercases the properties)
connectionString = builder.ToString();
}

// Get or parse provider name
providerName = configuration.GetConnectionStringProviderName(name);
if (string.IsNullOrEmpty(providerName))
{
providerName = ParseProviderName(builder);
}
}
else
{
providerName = null;
}

return connectionString;
}

/// <summary>
/// Gets the provider name or parse it from the Umbraco connection string.
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="name">The connection string key.</param>
/// <returns>The provider name.</returns>
public static string GetUmbracoConnectionStringProviderName(this IConfiguration configuration, string name = Umbraco.Cms.Core.Constants.System.UmbracoConnectionName)
{
var providerName = configuration.GetConnectionStringProviderName(name);
if (string.IsNullOrEmpty(providerName))
{
var connectionString = configuration.GetConnectionString(name);
if (!string.IsNullOrEmpty(connectionString))
{
var builder = new DbConnectionStringBuilder
{
ConnectionString = connectionString
};

providerName = ParseProviderName(builder);
}
}

return providerName;
}

/// <summary>
/// Replaces the <c>|DataDirectory|</c> placeholder in the <c>AttachDbFileName</c> key of the connection string.
/// </summary>
/// <param name="builder">The database connection string builder.</param>
/// <returns><c>true</c> if the placeholder was replaced; otherwise, <c>false</c>.</returns>
internal static bool TryReplaceDataDirectory(DbConnectionStringBuilder builder)
{
const string attachDbFileNameKey = "AttachDbFileName";
const string dataDirectoryPlaceholder = "|DataDirectory|";

if (builder.TryGetValue(attachDbFileNameKey, out var attachDbFileNameValue) &&
attachDbFileNameValue is string attachDbFileName &&
attachDbFileName.Contains(dataDirectoryPlaceholder))
{
var dataDirectory = AppDomain.CurrentDomain.GetData("DataDirectory")?.ToString();
if (!string.IsNullOrEmpty(dataDirectory))
{
builder[attachDbFileNameKey] = attachDbFileName.Replace(dataDirectoryPlaceholder, dataDirectory);

return true;
}
}

return false;
}

/// <summary>
/// Parses the provider name from the connection string.
/// </summary>
/// <param name="builder">The database connection string builder.</param>
/// <returns>The provider name.</returns>
internal static string ParseProviderName(DbConnectionStringBuilder builder)
{
if ((builder.TryGetValue("Data Source", out var dataSourceValue) || builder.TryGetValue("DataSource", out dataSourceValue)) &&
dataSourceValue is string dataSource &&
dataSource.EndsWith(".sdf", StringComparison.OrdinalIgnoreCase))
{
return Umbraco.Cms.Core.Constants.DbProviderNames.SqlCe;
}

return Umbraco.Cms.Core.Constants.DbProviderNames.SqlServer;
}
}
}
1 change: 1 addition & 0 deletions Cultiv.Hangfire/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ public static class Constants
public static class System
{
public const string HangfireDashboard = nameof(HangfireDashboard);
public const string Endpoint = "/umbraco/backoffice/hangfire";
}
}
}
64 changes: 33 additions & 31 deletions Cultiv.Hangfire/HangfireComposer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using Hangfire.Console;
using Hangfire.SqlServer;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.DependencyInjection;
Expand All @@ -16,36 +15,39 @@ public class HangfireComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
var connectionString = builder.Config.GetUmbracoConnectionString(out var providerName);
if (string.IsNullOrEmpty(connectionString) ||
providerName != Umbraco.Cms.Core.Constants.DatabaseProviders.SqlServer)
{
return;
}

// Configure Hangfire to use our current database and add the option to write console messages
var connectionString = builder.Config.GetConnectionString(Umbraco.Cms.Core.Constants.System.UmbracoConnectionName);
if (string.IsNullOrEmpty(connectionString) == false)
builder.Services.AddHangfire(configuration =>
{
builder.Services.AddHangfire(configuration =>
{
configuration
.SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseConsole()
.UseSqlServerStorage(connectionString, new SqlServerStorageOptions
{
CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
QueuePollInterval = TimeSpan.Zero,
UseRecommendedIsolationLevel = true,
DisableGlobalLocks = true,
});
});
configuration
.SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseConsole()
.UseSqlServerStorage(connectionString, new SqlServerStorageOptions
{
CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
QueuePollInterval = TimeSpan.Zero,
UseRecommendedIsolationLevel = true,
DisableGlobalLocks = true,
});
});

// Run the required server so your queued jobs will get executed
builder.Services.AddHangfireServer();
// Run the required server so your queued jobs will get executed
builder.Services.AddHangfireServer();

AddAuthorizedUmbracoDashboard(builder);

AddAuthorizedUmbracoDashboard(builder);

// For some reason we need to give it the connection string again, else we get this error:
// https://discuss.hangfire.io/t/jobstorage-current-property-value-has-not-been-initialized/884
JobStorage.Current = new SqlServerStorage(connectionString);
}
// For some reason we need to give it the connection string again, else we get this error:
// https://discuss.hangfire.io/t/jobstorage-current-property-value-has-not-been-initialized/884
JobStorage.Current = new SqlServerStorage(connectionString);
}

private static void AddAuthorizedUmbracoDashboard(IUmbracoBuilder builder)
Expand All @@ -58,12 +60,12 @@ private static void AddAuthorizedUmbracoDashboard(IUmbracoBuilder builder)
Endpoints = app => app.UseEndpoints(endpoints =>
{
endpoints.MapHangfireDashboardWithAuthorizationPolicy(
pattern: "/umbraco/backoffice/hangfire",
options: new DashboardOptions(),
authorizationPolicyName: AuthorizationPolicies.SectionAccessSettings);
pattern: Constants.System.Endpoint,
options: new DashboardOptions(),
authorizationPolicyName: AuthorizationPolicies.SectionAccessSettings);
}).UseHangfireDashboard()
});
});
}
}
}
}

0 comments on commit e6d213b

Please sign in to comment.