Skip to content

Commit 3b9f967

Browse files
jsquirem-reddingJoshLove-msft
authored
[Extensions] AzureClientFactory disposable support (Azure#34154)
* [Extensions] AzureClientFactory disposable support The focus of these changes is to add support for clients to be disposed via `IDisposable` or `IAsyncDisposable` when the service factory is disposed. * Update sdk/extensions/Microsoft.Extensions.Azure/tests/AzureClientFactoryTests.cs Co-authored-by: Madalyn Redding <66138537+m-redding@users.noreply.github.com> * Update sdk/extensions/Microsoft.Extensions.Azure/tests/AzureClientFactoryTests.cs Co-authored-by: Madalyn Redding <66138537+m-redding@users.noreply.github.com> * Updated the approach used for tracking client initialization and completed the changelog. * Update sdk/extensions/Microsoft.Extensions.Azure/tests/AzureClientFactoryTests.cs Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com> * Bump AspNetCore DI package to allow for async dispose testing. --------- Co-authored-by: Madalyn Redding <66138537+m-redding@users.noreply.github.com> Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com>
1 parent 0b7acde commit 3b9f967

File tree

11 files changed

+445
-48
lines changed

11 files changed

+445
-48
lines changed

eng/Packages.Data.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@
239239
<PackageReference Update="Microsoft.Extensions.Configuration.Binder" Version="2.1.10" />
240240
<PackageReference Update="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
241241
<PackageReference Update="Microsoft.Extensions.Configuration" Version="5.0.0" />
242-
<PackageReference Update="Microsoft.Extensions.DependencyInjection" Version="2.1.1" />
242+
<PackageReference Update="Microsoft.Extensions.DependencyInjection" Version="3.1.32" />
243243
<PackageReference Update="Microsoft.Extensions.PlatformAbstractions" Version="1.1.0" />
244244
<PackageReference Update="Microsoft.Graph" Version="4.52.0" />
245245
<PackageReference Update="Microsoft.NET.Test.Sdk" Version="17.0.0" />

sdk/extensions/Azure.Extensions.sln

Lines changed: 5 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
3-
# Visual Studio Version 16
4-
VisualStudioVersion = 16.0.30327.8
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.4.33213.308
55
MinimumVisualStudioVersion = 15.0.26124.0
66
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Azure.Extensions.AspNetCore.DataProtection.Blobs", "Azure.Extensions.AspNetCore.DataProtection.Blobs", "{12C39FF8-57E3-4A24-AE05-42A104EC9C69}"
77
EndProject
@@ -33,13 +33,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Azure.
3333
EndProject
3434
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Azure", "Microsoft.Extensions.Azure\src\Microsoft.Extensions.Azure.csproj", "{56BF6473-7C0F-451B-BC65-26F5DAB2B318}"
3535
EndProject
36-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure.Messaging.EventGrid", "..\eventgrid\Azure.Messaging.EventGrid\src\Azure.Messaging.EventGrid.csproj", "{076DF7F2-5FB4-47BD-90EB-D4F440FCE6D7}"
36+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Azure.WebJobs.Extensions.EventGrid", "..\eventgrid\Microsoft.Azure.WebJobs.Extensions.EventGrid\src\Microsoft.Azure.WebJobs.Extensions.EventGrid.csproj", "{55E693C2-FC07-4976-A55E-7937A9C65C36}"
3737
EndProject
38-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Azure.WebJobs.Extensions.EventGrid", "..\eventgrid\Microsoft.Azure.WebJobs.Extensions.EventGrid\src\Microsoft.Azure.WebJobs.Extensions.EventGrid.csproj", "{55E693C2-FC07-4976-A55E-7937A9C65C36}"
38+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Azure.WebJobs.Extensions.EventGrid.Tests", "..\eventgrid\Microsoft.Azure.WebJobs.Extensions.EventGrid\tests\Microsoft.Azure.WebJobs.Extensions.EventGrid.Tests.csproj", "{86AC9757-9309-4895-9547-4B28B7357617}"
3939
EndProject
40-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Azure.WebJobs.Extensions.EventGrid.Tests", "..\eventgrid\Microsoft.Azure.WebJobs.Extensions.EventGrid\tests\Microsoft.Azure.WebJobs.Extensions.EventGrid.Tests.csproj", "{86AC9757-9309-4895-9547-4B28B7357617}"
41-
EndProject
42-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Extensions.Azure.Samples", "Microsoft.Extensions.Azure\samples\Microsoft.Extensions.Azure.Samples.csproj", "{D131D37E-9E4E-4511-BBB1-28F9DC8C76CC}"
40+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Azure.Samples", "Microsoft.Extensions.Azure\samples\Microsoft.Extensions.Azure.Samples.csproj", "{D131D37E-9E4E-4511-BBB1-28F9DC8C76CC}"
4341
EndProject
4442
Global
4543
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -195,18 +193,6 @@ Global
195193
{56BF6473-7C0F-451B-BC65-26F5DAB2B318}.Release|x64.Build.0 = Release|Any CPU
196194
{56BF6473-7C0F-451B-BC65-26F5DAB2B318}.Release|x86.ActiveCfg = Release|Any CPU
197195
{56BF6473-7C0F-451B-BC65-26F5DAB2B318}.Release|x86.Build.0 = Release|Any CPU
198-
{076DF7F2-5FB4-47BD-90EB-D4F440FCE6D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
199-
{076DF7F2-5FB4-47BD-90EB-D4F440FCE6D7}.Debug|Any CPU.Build.0 = Debug|Any CPU
200-
{076DF7F2-5FB4-47BD-90EB-D4F440FCE6D7}.Debug|x64.ActiveCfg = Debug|Any CPU
201-
{076DF7F2-5FB4-47BD-90EB-D4F440FCE6D7}.Debug|x64.Build.0 = Debug|Any CPU
202-
{076DF7F2-5FB4-47BD-90EB-D4F440FCE6D7}.Debug|x86.ActiveCfg = Debug|Any CPU
203-
{076DF7F2-5FB4-47BD-90EB-D4F440FCE6D7}.Debug|x86.Build.0 = Debug|Any CPU
204-
{076DF7F2-5FB4-47BD-90EB-D4F440FCE6D7}.Release|Any CPU.ActiveCfg = Release|Any CPU
205-
{076DF7F2-5FB4-47BD-90EB-D4F440FCE6D7}.Release|Any CPU.Build.0 = Release|Any CPU
206-
{076DF7F2-5FB4-47BD-90EB-D4F440FCE6D7}.Release|x64.ActiveCfg = Release|Any CPU
207-
{076DF7F2-5FB4-47BD-90EB-D4F440FCE6D7}.Release|x64.Build.0 = Release|Any CPU
208-
{076DF7F2-5FB4-47BD-90EB-D4F440FCE6D7}.Release|x86.ActiveCfg = Release|Any CPU
209-
{076DF7F2-5FB4-47BD-90EB-D4F440FCE6D7}.Release|x86.Build.0 = Release|Any CPU
210196
{55E693C2-FC07-4976-A55E-7937A9C65C36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
211197
{55E693C2-FC07-4976-A55E-7937A9C65C36}.Debug|Any CPU.Build.0 = Debug|Any CPU
212198
{55E693C2-FC07-4976-A55E-7937A9C65C36}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -243,18 +229,6 @@ Global
243229
{D131D37E-9E4E-4511-BBB1-28F9DC8C76CC}.Release|x64.Build.0 = Release|Any CPU
244230
{D131D37E-9E4E-4511-BBB1-28F9DC8C76CC}.Release|x86.ActiveCfg = Release|Any CPU
245231
{D131D37E-9E4E-4511-BBB1-28F9DC8C76CC}.Release|x86.Build.0 = Release|Any CPU
246-
{D0D83FD0-44E7-4F8E-939F-8D8FDB2CCBAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
247-
{D0D83FD0-44E7-4F8E-939F-8D8FDB2CCBAD}.Debug|Any CPU.Build.0 = Debug|Any CPU
248-
{D0D83FD0-44E7-4F8E-939F-8D8FDB2CCBAD}.Debug|x64.ActiveCfg = Debug|Any CPU
249-
{D0D83FD0-44E7-4F8E-939F-8D8FDB2CCBAD}.Debug|x64.Build.0 = Debug|Any CPU
250-
{D0D83FD0-44E7-4F8E-939F-8D8FDB2CCBAD}.Debug|x86.ActiveCfg = Debug|Any CPU
251-
{D0D83FD0-44E7-4F8E-939F-8D8FDB2CCBAD}.Debug|x86.Build.0 = Debug|Any CPU
252-
{D0D83FD0-44E7-4F8E-939F-8D8FDB2CCBAD}.Release|Any CPU.ActiveCfg = Release|Any CPU
253-
{D0D83FD0-44E7-4F8E-939F-8D8FDB2CCBAD}.Release|Any CPU.Build.0 = Release|Any CPU
254-
{D0D83FD0-44E7-4F8E-939F-8D8FDB2CCBAD}.Release|x64.ActiveCfg = Release|Any CPU
255-
{D0D83FD0-44E7-4F8E-939F-8D8FDB2CCBAD}.Release|x64.Build.0 = Release|Any CPU
256-
{D0D83FD0-44E7-4F8E-939F-8D8FDB2CCBAD}.Release|x86.ActiveCfg = Release|Any CPU
257-
{D0D83FD0-44E7-4F8E-939F-8D8FDB2CCBAD}.Release|x86.Build.0 = Release|Any CPU
258232
EndGlobalSection
259233
GlobalSection(SolutionProperties) = preSolution
260234
HideSolutionNode = FALSE

sdk/extensions/Microsoft.Extensions.Azure/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88

99
### Bugs Fixed
1010

11+
- Added support for clients to be disposed via `IDisposable` or `IAsyncDisposable` when the service factory is disposed.
12+
- Changed tracking for client initialization to ensure that behavior is correct for value types registered as clients.
13+
1114
### Other Changes
1215

1316
## 1.6.0 (2022-10-12)

sdk/extensions/Microsoft.Extensions.Azure/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public void ConfigureServices(IServiceCollection services)
4848
builder.ConfigureDefaults(options => options.Retry.Mode = RetryMode.Exponential);
4949

5050
// Advanced configure global defaults
51-
builder.ConfigureDefaults((options, provider) => options.AddPolicy(provider.GetService<DependencyInjectionEnabledPolicy>(), HttpPipelinePosition.PerCall));
51+
builder.ConfigureDefaults((options, provider) => options.AddPolicy(provider.GetService<DependencyInjectionEnabledPolicy>(), HttpPipelinePosition.PerCall));
5252

5353
// Register blob service client and initialize it using the Storage section of configuration
5454
builder.AddBlobServiceClient(Configuration.GetSection("Storage"))

sdk/extensions/Microsoft.Extensions.Azure/samples/Startup.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public void ConfigureServices(IServiceCollection services)
5656
builder.ConfigureDefaults(options => options.Retry.Mode = RetryMode.Exponential);
5757

5858
// Advanced configure global defaults
59-
builder.ConfigureDefaults((options, provider) => options.AddPolicy(provider.GetService<DependencyInjectionEnabledPolicy>(), HttpPipelinePosition.PerCall));
59+
builder.ConfigureDefaults((options, provider) => options.AddPolicy(provider.GetService<DependencyInjectionEnabledPolicy>(), HttpPipelinePosition.PerCall));
6060

6161
// Register blob service client and initialize it using the Storage section of configuration
6262
builder.AddBlobServiceClient(Configuration.GetSection("Storage"))

sdk/extensions/Microsoft.Extensions.Azure/src/AzureClientFactoryBuilder.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4+
using System;
45
using Azure.Core;
56
using Azure.Core.Extensions;
67
using Microsoft.Extensions.Configuration;
78
using Microsoft.Extensions.DependencyInjection;
89
using Microsoft.Extensions.DependencyInjection.Extensions;
910
using Microsoft.Extensions.Options;
10-
using System;
11-
using Microsoft.Extensions.Logging;
12-
using Microsoft.Extensions.Logging.Abstractions;
1311

1412
namespace Microsoft.Extensions.Azure
1513
{

sdk/extensions/Microsoft.Extensions.Azure/src/Internal/AzureClientFactory.cs

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,23 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Threading.Tasks;
67
using Microsoft.Extensions.Options;
78

89
namespace Microsoft.Extensions.Azure
910
{
10-
internal class AzureClientFactory<TClient, TOptions>: IAzureClientFactory<TClient>
11+
internal class AzureClientFactory<TClient, TOptions>: IAzureClientFactory<TClient>, IDisposable, IAsyncDisposable
1112
{
1213
private readonly Dictionary<string, ClientRegistration<TClient>> _clientRegistrations;
1314

1415
private readonly IServiceProvider _serviceProvider;
1516
private readonly IOptionsMonitor<AzureClientsGlobalOptions> _globalOptions;
16-
1717
private readonly IOptionsMonitor<AzureClientCredentialOptions<TClient>> _clientsOptions;
18-
1918
private readonly IOptionsMonitor<TOptions> _monitor;
20-
2119
private readonly AzureEventSourceLogForwarder _logForwarder;
2220

21+
private volatile bool _disposed;
22+
2323
public AzureClientFactory(
2424
IServiceProvider serviceProvider,
2525
IOptionsMonitor<AzureClientsGlobalOptions> globalOptions,
@@ -29,6 +29,7 @@ public AzureClientFactory(
2929
AzureEventSourceLogForwarder logForwarder)
3030
{
3131
_clientRegistrations = new Dictionary<string, ClientRegistration<TClient>>();
32+
3233
foreach (var registration in clientRegistrations)
3334
{
3435
_clientRegistrations[registration.Name] = registration;
@@ -52,5 +53,35 @@ public TClient CreateClient(string name)
5253

5354
return registration.GetClient(_serviceProvider, _monitor.Get(name), _clientsOptions.Get(name).CredentialFactory(_serviceProvider));
5455
}
56+
57+
public void Dispose()
58+
{
59+
if (!_disposed)
60+
{
61+
_disposed = true;
62+
63+
foreach (var registration in _clientRegistrations.Values)
64+
{
65+
registration.Dispose();
66+
}
67+
}
68+
}
69+
70+
public async ValueTask DisposeAsync()
71+
{
72+
if (!_disposed)
73+
{
74+
_disposed = true;
75+
76+
var disposeTasks = new List<Task>(_clientRegistrations.Values.Count);
77+
78+
foreach (var registration in _clientRegistrations.Values)
79+
{
80+
disposeTasks.Add(registration.DisposeAsync().AsTask());
81+
}
82+
83+
await Task.WhenAll(disposeTasks).ConfigureAwait(false);
84+
}
85+
}
5586
}
5687
}

sdk/extensions/Microsoft.Extensions.Azure/src/Internal/ClientRegistration.cs

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33

44
using System;
55
using System.Runtime.ExceptionServices;
6+
using System.Threading.Tasks;
67
using Azure.Core;
78

89
namespace Microsoft.Extensions.Azure
910
{
10-
internal class ClientRegistration<TClient>
11+
internal class ClientRegistration<TClient> : IDisposable, IAsyncDisposable
1112
{
1213
public string Name { get; set; }
1314
public object Version { get; set; }
@@ -16,22 +17,27 @@ internal class ClientRegistration<TClient>
1617
private readonly Func<IServiceProvider, object, TokenCredential, TClient> _factory;
1718

1819
private readonly object _cacheLock = new object();
20+
private readonly bool _asyncDisposable;
21+
private readonly bool _disposable;
1922

23+
private bool _clientInitialized;
2024
private TClient _cachedClient;
21-
2225
private ExceptionDispatchInfo _cachedException;
2326

2427
public ClientRegistration(string name, Func<IServiceProvider, object, TokenCredential, TClient> factory)
2528
{
2629
Name = name;
2730
_factory = factory;
31+
32+
_asyncDisposable = typeof(IAsyncDisposable).IsAssignableFrom(typeof(TClient));
33+
_disposable = typeof(IDisposable).IsAssignableFrom(typeof(TClient));
2834
}
2935

3036
public TClient GetClient(IServiceProvider serviceProvider, object options, TokenCredential tokenCredential)
3137
{
3238
_cachedException?.Throw();
3339

34-
if (_cachedClient != null)
40+
if (_clientInitialized)
3541
{
3642
return _cachedClient;
3743
}
@@ -40,7 +46,7 @@ public TClient GetClient(IServiceProvider serviceProvider, object options, Token
4046
{
4147
_cachedException?.Throw();
4248

43-
if (_cachedClient != null)
49+
if (_clientInitialized)
4450
{
4551
return _cachedClient;
4652
}
@@ -53,6 +59,7 @@ public TClient GetClient(IServiceProvider serviceProvider, object options, Token
5359
try
5460
{
5561
_cachedClient = _factory(serviceProvider, options, tokenCredential);
62+
_clientInitialized = true;
5663
}
5764
catch (Exception e)
5865
{
@@ -63,5 +70,65 @@ public TClient GetClient(IServiceProvider serviceProvider, object options, Token
6370
return _cachedClient;
6471
}
6572
}
73+
74+
public async ValueTask DisposeAsync()
75+
{
76+
if (_clientInitialized)
77+
{
78+
if (_asyncDisposable)
79+
{
80+
IAsyncDisposable disposableClient;
81+
82+
lock (_cacheLock)
83+
{
84+
if (!_clientInitialized)
85+
{
86+
return;
87+
}
88+
89+
disposableClient = (IAsyncDisposable)_cachedClient;
90+
91+
_cachedClient = default;
92+
_clientInitialized = false;
93+
}
94+
95+
await disposableClient.DisposeAsync().ConfigureAwait(false);
96+
}
97+
else if (_disposable)
98+
{
99+
Dispose();
100+
}
101+
}
102+
}
103+
104+
public void Dispose()
105+
{
106+
if (_clientInitialized)
107+
{
108+
if (_disposable)
109+
{
110+
IDisposable disposableClient;
111+
112+
lock (_cacheLock)
113+
{
114+
if (!_clientInitialized)
115+
{
116+
return;
117+
}
118+
119+
disposableClient = (IDisposable)_cachedClient;
120+
121+
_cachedClient = default;
122+
_clientInitialized = false;
123+
}
124+
125+
disposableClient.Dispose();
126+
}
127+
else if (_asyncDisposable)
128+
{
129+
DisposeAsync().GetAwaiter().GetResult();
130+
}
131+
}
132+
}
66133
}
67134
}

0 commit comments

Comments
 (0)