diff --git a/Hangfire.PostgreSql.sln b/Hangfire.PostgreSql.sln
index 34e7fcf..f19e923 100644
--- a/Hangfire.PostgreSql.sln
+++ b/Hangfire.PostgreSql.sln
@@ -19,6 +19,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{
.github\workflows\pack.yml = .github\workflows\pack.yml
EndProjectSection
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hangfire.PostgreSql.Azure", "src\Hangfire.PostgreSql.Azure\Hangfire.PostgreSql.Azure.csproj", "{AF77F244-4B8B-4166-88D7-316A92CBABF5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hangfire.PostgreSql.Azure.Tests", "tests\Hangfire.PostgreSql.Azure.Tests\Hangfire.PostgreSql.Azure.Tests.csproj", "{61C844BE-C5D4-446D-BC5F-4561433BBE85}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -33,6 +37,14 @@ Global
{6044A48D-730B-4D1F-B03A-EB2B458DAF53}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6044A48D-730B-4D1F-B03A-EB2B458DAF53}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6044A48D-730B-4D1F-B03A-EB2B458DAF53}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AF77F244-4B8B-4166-88D7-316A92CBABF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AF77F244-4B8B-4166-88D7-316A92CBABF5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AF77F244-4B8B-4166-88D7-316A92CBABF5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AF77F244-4B8B-4166-88D7-316A92CBABF5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {61C844BE-C5D4-446D-BC5F-4561433BBE85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {61C844BE-C5D4-446D-BC5F-4561433BBE85}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {61C844BE-C5D4-446D-BC5F-4561433BBE85}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {61C844BE-C5D4-446D-BC5F-4561433BBE85}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -41,6 +53,8 @@ Global
{3E4DBC41-F38E-4D1C-A6A7-206A132A29D6} = {0D30A51B-814F-474E-93B8-44E9C155255C}
{6044A48D-730B-4D1F-B03A-EB2B458DAF53} = {766BE831-F758-46BC-AFD3-BBEEFE0F686F}
{AAA78654-9846-4870-A13C-D9DBAF0792C4} = {5CA38188-92EE-453C-A04E-A506DF15A787}
+ {AF77F244-4B8B-4166-88D7-316A92CBABF5} = {0D30A51B-814F-474E-93B8-44E9C155255C}
+ {61C844BE-C5D4-446D-BC5F-4561433BBE85} = {766BE831-F758-46BC-AFD3-BBEEFE0F686F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F7E32105-7F61-4127-8517-5E4275B9CABE}
diff --git a/README.md b/README.md
index 58a3d26..fb46ef7 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Hangfire.PostgreSql
+# Hangfire.PostgreSql
[](https://github.com/hangfire-postgres/Hangfire.PostgreSql/actions/workflows/pack.yml) [](https://github.com/hangfire-postgres/Hangfire.PostgreSql/releases/latest) [](https://www.nuget.org/packages/Hangfire.PostgreSql)
@@ -50,6 +50,20 @@ And... That's it. You are ready to go.
If you encounter any issues/bugs or have idea of a feature regarding Hangfire.Postgresql, [create us an issue](https://github.com/hangfire-postgres/Hangfire.PostgreSql/issues/new). Thanks!
+### Connecting to Azure Postgres Flexible Servers
+
+To connect to Azure PostgreSQL Flexible Servers, use need to use
+
+```csharp
+services.AddHangfire(config =>
+ config.UseAzurePostgreSqlStorage(c => Configuration.GetConnectionString("HangfireConnection"))
+ );
+```
+
+This factory generates a data source builder which behind the scenes configured a periodic password provider.
+This provider will use DefaultAzureCredential to fetch a token depending on the environment.
+If you need to customize the behavior, use the dataSourceBuilderSetup override. That one is called after the internal configuration.
+
### Enabling SSL support
SSL support can be enabled for Hangfire.PostgreSql library using the following mechanism:
@@ -82,6 +96,17 @@ app.UseHangfireServer(options);
this provider would first process jobs in `a-long-running-queue`, then `general-queue` and lastly `very-fast-queue`.
+### Running Unit Tests
+
+In order to run unit tests you need to setup a postgresql database. Simples way to do that is to use docker;
+
+Environment configurations:
+ POSTGRES_PASSWORD: postgres
+ POSTGRES_HOST_AUTH_METHOD: trust
+ports:
+ - 5432:5432
+
+
### License
Copyright © 2014-2024 Frank Hommers https://github.com/hangfire-postgres/Hangfire.PostgreSql.
diff --git a/src/Hangfire.PostgreSql.Azure/Factories/AzureNpgsqlConnectionFactory.cs b/src/Hangfire.PostgreSql.Azure/Factories/AzureNpgsqlConnectionFactory.cs
new file mode 100644
index 0000000..e150a65
--- /dev/null
+++ b/src/Hangfire.PostgreSql.Azure/Factories/AzureNpgsqlConnectionFactory.cs
@@ -0,0 +1,58 @@
+using Azure.Core;
+using Azure.Identity;
+using Hangfire.PostgreSql.Factories;
+using Hangfire.PostgreSql.Properties;
+using Npgsql;
+
+namespace Hangfire.PostgreSql.Azure.Factories
+{
+ public class AzureNpgsqlConnectionFactory : NpgsqlInstanceConnectionFactoryBase
+ {
+ private readonly NpgsqlDataSource _dataSource;
+
+ ///
+ /// Instatiates a factory already configured to fetch tokens from azure
+ /// Token is refreshed every 4 hours
+ ///
+ ///
+ ///
+ /// You have here the opportunity to override the datasource builder, including the password provider
+ public AzureNpgsqlConnectionFactory(string connectionString, PostgreSqlStorageOptions options, [CanBeNull] Action? dataSourceBuilderSetup = null) : base(options)
+ {
+ NpgsqlDataSourceBuilder dataSourceBuilder = new(connectionString);
+
+ ConfigurePeriodicPasswordProvider(dataSourceBuilder);
+
+ dataSourceBuilderSetup?.Invoke(dataSourceBuilder);
+ _dataSource = dataSourceBuilder.Build()!;
+ }
+
+
+ public override NpgsqlConnection GetOrCreateConnection()
+ {
+ return _dataSource.CreateConnection();
+ }
+
+ private static void ConfigurePeriodicPasswordProvider(NpgsqlDataSourceBuilder dataSourceBuilder)
+ {
+ //Kudos https://mattparker.dev/blog/azure-managed-identity-postgres-aspnetcore
+ dataSourceBuilder.UsePeriodicPasswordProvider(
+ async (connectionStringBuilder, cancellationToken) => {
+ try
+ {
+ DefaultAzureCredentialOptions options = new();
+ DefaultAzureCredential credentials = new(options);
+ AccessToken token = await credentials.GetTokenAsync(new TokenRequestContext(["https://ossrdbms-aad.database.windows.net/.default"]), cancellationToken);
+ return token.Token;
+ }
+ catch
+ {
+ throw;
+ }
+ },
+ TimeSpan.FromHours(4), // successRefreshInterval
+ TimeSpan.FromSeconds(10) // failureRefreshInterval
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Hangfire.PostgreSql.Azure/Hangfire.PostgreSql.Azure.csproj b/src/Hangfire.PostgreSql.Azure/Hangfire.PostgreSql.Azure.csproj
new file mode 100644
index 0000000..5f2d4f8
--- /dev/null
+++ b/src/Hangfire.PostgreSql.Azure/Hangfire.PostgreSql.Azure.csproj
@@ -0,0 +1,15 @@
+
+
+
+ net9.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
diff --git a/src/Hangfire.PostgreSql.Azure/PostgreSqlBootstrapperConfigurationExtensions.cs b/src/Hangfire.PostgreSql.Azure/PostgreSqlBootstrapperConfigurationExtensions.cs
new file mode 100644
index 0000000..1d6e72b
--- /dev/null
+++ b/src/Hangfire.PostgreSql.Azure/PostgreSqlBootstrapperConfigurationExtensions.cs
@@ -0,0 +1,25 @@
+using Hangfire.Annotations;
+using Hangfire.PostgreSql.Azure.Factories;
+using Npgsql;
+
+namespace Hangfire.PostgreSql.Azure
+{
+ public static class PostgreSqlBootstrapperConfigurationExtensions
+ {
+ ///
+ /// Tells the bootstrapper to use PostgreSQL as a job storage with the given options
+ ///
+ /// Configuration
+ /// Connection string
+ /// You have here the opportunity to override the datasource builder, including the password provider
+ public static IGlobalConfiguration UseAzurePostgreSqlStorage(
+ this IGlobalConfiguration configuration,
+ string connectionString,
+ PostgreSqlStorageOptions options,
+ [CanBeNull] Action? dataSourceBuilderSetup = null)
+ {
+ return configuration.UsePostgreSqlStorage(c => c.UseConnectionFactory(new AzureNpgsqlConnectionFactory(connectionString, options, dataSourceBuilderSetup)), options);
+ }
+ }
+
+}
diff --git a/tests/Hangfire.PostgreSql.Azure.Tests/Hangfire.PostgreSql.Azure.Tests.csproj b/tests/Hangfire.PostgreSql.Azure.Tests/Hangfire.PostgreSql.Azure.Tests.csproj
new file mode 100644
index 0000000..6f455d7
--- /dev/null
+++ b/tests/Hangfire.PostgreSql.Azure.Tests/Hangfire.PostgreSql.Azure.Tests.csproj
@@ -0,0 +1,20 @@
+
+
+
+ Hangfire.PostgreSql.Azure.Tests
+ net9.0
+ Hangfire.PostgreSql.Azure.Tests
+ enable
+ enable
+ Hangfire.PostgreSql.Azure.Tests
+ true
+ default
+
+
+
+
+
+
+
+
+
diff --git a/tests/Hangfire.PostgreSql.Azure.Tests/PostgreUseAzurePostgreSqlStorageFacts.cs b/tests/Hangfire.PostgreSql.Azure.Tests/PostgreUseAzurePostgreSqlStorageFacts.cs
new file mode 100644
index 0000000..06ccf7e
--- /dev/null
+++ b/tests/Hangfire.PostgreSql.Azure.Tests/PostgreUseAzurePostgreSqlStorageFacts.cs
@@ -0,0 +1,38 @@
+using Hangfire.PostgreSql.Azure.Factories;
+using Hangfire.PostgreSql.Tests.Utils;
+using Npgsql;
+using Xunit;
+
+namespace Hangfire.PostgreSql.Azure.Tests
+{
+ public class PostgreUseAzurePostgreSqlStorageFacts : IClassFixture
+ {
+ private readonly PostgreSqlStorageOptions _options;
+
+ public PostgreUseAzurePostgreSqlStorageFacts()
+ {
+ _options = new();
+ }
+
+ [Fact]
+ public async Task AzureNpgsqlConnectionFactory_Can_Generate_Connection()
+ {
+ AzureNpgsqlConnectionFactory factory = new(ConnectionUtils.GetConnectionString().Replace("Password=password", ""), _options, dsb => {
+ dsb.UsePeriodicPasswordProvider((_, _) => ValueTask.FromResult("password"), TimeSpan.FromHours(1), TimeSpan.FromSeconds(1));
+ });
+
+ using NpgsqlConnection connection = factory.GetOrCreateConnection();
+
+ await connection.OpenAsync();
+
+ await using NpgsqlCommand command = new("SELECT '8'", connection);
+ await using NpgsqlDataReader reader = await command.ExecuteReaderAsync();
+ await reader.ReadAsync();
+
+ Assert.Equal("8", reader.GetValue(0));
+
+ await connection.CloseAsync();
+ }
+
+ }
+}