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 [![Build status](https://github.com/hangfire-postgres/Hangfire.PostgreSql/actions/workflows/pack.yml/badge.svg)](https://github.com/hangfire-postgres/Hangfire.PostgreSql/actions/workflows/pack.yml) [![GitHub release (latest by date)](https://img.shields.io/github/v/release/hangfire-postgres/Hangfire.PostgreSql?label=Release)](https://github.com/hangfire-postgres/Hangfire.PostgreSql/releases/latest) [![Nuget](https://img.shields.io/nuget/v/Hangfire.PostgreSql?label=NuGet)](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(); + } + + } +}