Skip to content

Commit a01d3c8

Browse files
committed
Add ServiceCollection.MakeReadOnly()
With new hosting API patterns, it is useful to be able to mark a ServiceCollection as read-only and disallow for any more modifications to it. Fix dotnet#66126
1 parent b13513a commit a01d3c8

File tree

6 files changed

+88
-71
lines changed

6 files changed

+88
-71
lines changed

src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/ref/Microsoft.Extensions.DependencyInjection.Abstractions.cs

+1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ public void CopyTo(Microsoft.Extensions.DependencyInjection.ServiceDescriptor[]
6565
public System.Collections.Generic.IEnumerator<Microsoft.Extensions.DependencyInjection.ServiceDescriptor> GetEnumerator() { throw null; }
6666
public int IndexOf(Microsoft.Extensions.DependencyInjection.ServiceDescriptor item) { throw null; }
6767
public void Insert(int index, Microsoft.Extensions.DependencyInjection.ServiceDescriptor item) { }
68+
public void MakeReadOnly() { }
6869
public bool Remove(Microsoft.Extensions.DependencyInjection.ServiceDescriptor item) { throw null; }
6970
public void RemoveAt(int index) { }
7071
void System.Collections.Generic.ICollection<Microsoft.Extensions.DependencyInjection.ServiceDescriptor>.Add(Microsoft.Extensions.DependencyInjection.ServiceDescriptor item) { }

src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/Resources/Strings.resx

+3
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,9 @@
133133
<value>No service for type '{0}' has been registered.</value>
134134
<comment>{0} = service type</comment>
135135
</data>
136+
<data name="ServiceCollectionReadOnly" xml:space="preserve">
137+
<value>The service collection cannot be modified because it is read-only.</value>
138+
</data>
136139
<data name="TryAddIndistinguishableTypeToEnumerable" xml:space="preserve">
137140
<value>Implementation type cannot be '{0}' because it is indistinguishable from other services registered for '{1}'.</value>
138141
<comment>{0} = implementation type, {1} = service type</comment>

src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/ServiceCollection.cs

+31-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System;
45
using System.Collections;
56
using System.Collections.Generic;
67

@@ -12,12 +13,13 @@ namespace Microsoft.Extensions.DependencyInjection
1213
public class ServiceCollection : IServiceCollection
1314
{
1415
private readonly List<ServiceDescriptor> _descriptors = new List<ServiceDescriptor>();
16+
private bool _isReadOnly;
1517

1618
/// <inheritdoc />
1719
public int Count => _descriptors.Count;
1820

1921
/// <inheritdoc />
20-
public bool IsReadOnly => false;
22+
public bool IsReadOnly => _isReadOnly;
2123

2224
/// <inheritdoc />
2325
public ServiceDescriptor this[int index]
@@ -28,13 +30,15 @@ public ServiceDescriptor this[int index]
2830
}
2931
set
3032
{
33+
CheckReadOnly();
3134
_descriptors[index] = value;
3235
}
3336
}
3437

3538
/// <inheritdoc />
3639
public void Clear()
3740
{
41+
CheckReadOnly();
3842
_descriptors.Clear();
3943
}
4044

@@ -53,6 +57,7 @@ public void CopyTo(ServiceDescriptor[] array, int arrayIndex)
5357
/// <inheritdoc />
5458
public bool Remove(ServiceDescriptor item)
5559
{
60+
CheckReadOnly();
5661
return _descriptors.Remove(item);
5762
}
5863

@@ -64,6 +69,7 @@ public IEnumerator<ServiceDescriptor> GetEnumerator()
6469

6570
void ICollection<ServiceDescriptor>.Add(ServiceDescriptor item)
6671
{
72+
CheckReadOnly();
6773
_descriptors.Add(item);
6874
}
6975

@@ -81,13 +87,37 @@ public int IndexOf(ServiceDescriptor item)
8187
/// <inheritdoc />
8288
public void Insert(int index, ServiceDescriptor item)
8389
{
90+
CheckReadOnly();
8491
_descriptors.Insert(index, item);
8592
}
8693

8794
/// <inheritdoc />
8895
public void RemoveAt(int index)
8996
{
97+
CheckReadOnly();
9098
_descriptors.RemoveAt(index);
9199
}
100+
101+
/// <summary>
102+
/// Makes this collection read-only.
103+
/// </summary>
104+
/// <remarks>
105+
/// After the collection is marked as read-only, any further attempt to modify it throws an <see cref="InvalidOperationException" />.
106+
/// </remarks>
107+
public void MakeReadOnly()
108+
{
109+
_isReadOnly = true;
110+
}
111+
112+
private void CheckReadOnly()
113+
{
114+
if (_isReadOnly)
115+
{
116+
ThrowReadOnlyException();
117+
}
118+
}
119+
120+
private static void ThrowReadOnlyException() =>
121+
throw new InvalidOperationException(SR.ServiceCollectionReadOnly);
92122
}
93123
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using Microsoft.Extensions.DependencyInjection.Extensions;
6+
using Microsoft.Extensions.DependencyInjection.Specification.Fakes;
7+
using Xunit;
8+
9+
namespace Microsoft.Extensions.DependencyInjection
10+
{
11+
public class ServiceCollectionTests
12+
{
13+
[Fact]
14+
public void TestMakeReadOnly()
15+
{
16+
var serviceCollection = new ServiceCollection();
17+
var descriptor = new ServiceDescriptor(typeof(IFakeService), new FakeService());
18+
serviceCollection.Add(descriptor);
19+
20+
serviceCollection.MakeReadOnly();
21+
22+
var descriptor2 = new ServiceDescriptor(typeof(IFakeEveryService), new FakeService());
23+
24+
Assert.Throws<InvalidOperationException>(() => serviceCollection[0] = descriptor2);
25+
Assert.Throws<InvalidOperationException>(() => serviceCollection.Clear());
26+
Assert.Throws<InvalidOperationException>(() => serviceCollection.Remove(descriptor));
27+
Assert.Throws<InvalidOperationException>(() => serviceCollection.Add(descriptor2));
28+
Assert.Throws<InvalidOperationException>(() => serviceCollection.Insert(0, descriptor2));
29+
Assert.Throws<InvalidOperationException>(() => serviceCollection.RemoveAt(0));
30+
31+
Assert.True(serviceCollection.IsReadOnly);
32+
Assert.Equal(1, serviceCollection.Count);
33+
foreach (ServiceDescriptor d in serviceCollection)
34+
{
35+
Assert.Equal(descriptor, d);
36+
}
37+
Assert.Equal(descriptor, serviceCollection[0]);
38+
Assert.True(serviceCollection.Contains(descriptor));
39+
Assert.Equal(0, serviceCollection.IndexOf(descriptor));
40+
41+
ServiceDescriptor[] copyArray = new ServiceDescriptor[1];
42+
serviceCollection.CopyTo(copyArray, 0);
43+
Assert.Equal(descriptor, copyArray[0]);
44+
45+
// ensure MakeReadOnly can be called twice, and it is just ignored
46+
serviceCollection.MakeReadOnly();
47+
Assert.True(serviceCollection.IsReadOnly);
48+
}
49+
}
50+
}

src/libraries/Microsoft.Extensions.Hosting/src/HostApplicationBuilder.cs

+3-67
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
#nullable enable
55

66
using System;
7-
using System.Collections;
87
using System.Collections.Generic;
98
using System.Diagnostics;
109
using System.IO;
@@ -22,7 +21,7 @@ namespace Microsoft.Extensions.Hosting
2221
public sealed class HostApplicationBuilder
2322
{
2423
private readonly HostBuilderContext _hostBuilderContext;
25-
private readonly CheckedServiceCollection _checkedServiceCollection = new();
24+
private readonly ServiceCollection _serviceCollection = new();
2625

2726
private Func<IServiceProvider> _createServiceProvider;
2827
private Action<object> _configureContainer = _ => { };
@@ -165,7 +164,7 @@ public HostApplicationBuilder(HostApplicationBuilderSettings? settings)
165164
/// <summary>
166165
/// A collection of services for the application to compose. This is useful for adding user provided or framework provided services.
167166
/// </summary>
168-
public IServiceCollection Services => _checkedServiceCollection;
167+
public IServiceCollection Services => _serviceCollection;
169168

170169
/// <summary>
171170
/// A collection of logging providers for the application to compose. This is useful for adding new logging providers.
@@ -224,7 +223,7 @@ public IHost Build()
224223
_appServices = _createServiceProvider();
225224

226225
// Prevent further modification of the service collection now that the provider is built.
227-
_checkedServiceCollection.IsReadOnly = true;
226+
_serviceCollection.MakeReadOnly();
228227

229228
return HostBuilder.ResolveHost(_appServices, diagnosticListener);
230229
}
@@ -362,69 +361,6 @@ public IHostBuilder ConfigureContainer<TContainerBuilder>(Action<HostBuilderCont
362361
}
363362
}
364363

365-
private sealed class CheckedServiceCollection : IServiceCollection
366-
{
367-
private readonly IServiceCollection _services = new ServiceCollection();
368-
369-
public bool IsReadOnly { get; set; }
370-
public int Count => _services.Count;
371-
372-
public ServiceDescriptor this[int index]
373-
{
374-
get => _services[index];
375-
set
376-
{
377-
CheckReadOnly();
378-
_services[index] = value;
379-
}
380-
}
381-
382-
public IEnumerator<ServiceDescriptor> GetEnumerator() => _services.GetEnumerator();
383-
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
384-
385-
public int IndexOf(ServiceDescriptor item) => _services.IndexOf(item);
386-
public bool Contains(ServiceDescriptor item) => _services.Contains(item);
387-
public void CopyTo(ServiceDescriptor[] array, int arrayIndex) => _services.CopyTo(array, arrayIndex);
388-
389-
public void Add(ServiceDescriptor item)
390-
{
391-
CheckReadOnly();
392-
_services.Add(item);
393-
}
394-
395-
public void Clear()
396-
{
397-
CheckReadOnly();
398-
_services.Clear();
399-
}
400-
401-
public void Insert(int index, ServiceDescriptor item)
402-
{
403-
CheckReadOnly();
404-
_services.Insert(index, item);
405-
}
406-
407-
public bool Remove(ServiceDescriptor item)
408-
{
409-
CheckReadOnly();
410-
return _services.Remove(item);
411-
}
412-
413-
public void RemoveAt(int index)
414-
{
415-
CheckReadOnly();
416-
_services.RemoveAt(index);
417-
}
418-
419-
private void CheckReadOnly()
420-
{
421-
if (IsReadOnly)
422-
{
423-
throw new InvalidOperationException(SR.ServiceCollectionModificationInvalidAfterBuild);
424-
}
425-
}
426-
}
427-
428364
private sealed class LoggingBuilder : ILoggingBuilder
429365
{
430366
public LoggingBuilder(IServiceCollection services)

src/libraries/Microsoft.Extensions.Hosting/src/Resources/Strings.resx

-3
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,4 @@
144144
<data name="ResolverReturnedNull" xml:space="preserve">
145145
<value>The resolver returned a null IServiceProviderFactory</value>
146146
</data>
147-
<data name="ServiceCollectionModificationInvalidAfterBuild" xml:space="preserve">
148-
<value>The service collection cannot be modified after the application is built.</value>
149-
</data>
150147
</root>

0 commit comments

Comments
 (0)