-
Notifications
You must be signed in to change notification settings - Fork 4.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Explore options immutability #43359
Comments
Tagging subscribers to this area: @maryamariyan |
I find a workaround for #46996:
|
Records with all "init" property setters partially works. I am running into an issue with deserializing immutable collection types. IOptions is not picking up any data from the app.settings files. |
I have a suggested API surface + implementation - is there an appropriate place to share this? PR, here in this issue...? |
At this point it would be for .NET 7. It's a bit late to take such a big change for .NET 6. Would love to see the proposed design in this issue though! |
Sure! So, essentialy, I've tried to go for the most straightforward approach to supporting immutable option types (and indirectly, Cumulative configurations of an immutable options type is done by simply returning a new copy of the options type, instead of mutating it in-place as is done today. The full source code for this proposal can be viewed here, and since I have a need for this right away, I intend to release it on nuget as an experimental package as soon as I've got some unit tests set up :) The proposed surface would do the following (with nullable reference types enabled). New MethodsThe following methods are defined as extension methods to IServiceCollection Configure<TOptions>(this IServiceCollection services, Func<TOptions> creator);
IServiceCollection Configure<TOptions>(this IServiceCollection services, string? name, Func<TOptions> creator);
IServiceCollection Configure<TOptions>(this IServiceCollection services, Func<TOptions,TOptions> configureOptions);
IServiceCollection Configure<TOptions>(this IServiceCollection services, string? name, Func<TOptions,TOptions> configureOptions);
IServiceCollection ConfigureAll<TOptions>(this IServiceCollection services, Func<TOptions,TOptions> configureOptions);
IServiceCollection PostConfigure<TOptions>(this IServiceCollection services, Func<TOptions,TOptions> configureOptions);
IServiceCollection PostConfigure<TOptions>(this IServiceCollection services, string name, Func<TOptions,TOptions> configureOptions);
IServiceCollection PostConfigureAll<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions); They behave effectively identical to their existing counterparts, save for operating on immutable types. New TypesThe following new types are added. InterfacesThe following new interfaces are added, generally matching the existing interfaces. public interface ICreateOptions<out TOptions> where TOptions : class
{
string? Name { get; }
TOptions Create();
}
public interface IReadOnlyConfigureNamedOptions<TOptions> : IReadOnlyConfigureOptions<TOptions> where TOptions : class
{
TOptions Configure(string name, TOptions options);
}
public interface IReadOnlyConfigureOptions<TOptions> where TOptions : class
{
TOptions Configure(TOptions options);
}
public interface IReadOnlyPostConfigureOptions<TOptions> where TOptions : class
{
TOptions PostConfigure(string name, TOptions options);
} RecordsThe following new records are added, generally matching the existing classes but utilizing new language features. It would of course be trivial to implement these as classes instead, but records are used here for brevity's sake. The full implementations are omitted from this section; see the linked experimental implementation for the complete source code. public record CreateOptions<TOptions>(string? Name, Func<TOptions> Creator) : ICreateOptions<TOptions> where TOptions : class;
public record ReadOnlyConfigureNamedOptions<TOptions>(string? Name, Func<TOptions, TOptions> Function) : IReadOnlyConfigureNamedOptions<TOptions> where TOptions : class;
public record ReadOnlyPostConfigureOptions<TOptions>(string? Name, Func<TOptions, TOptions> Function) : IReadOnlyPostConfigureOptions<TOptions> where TOptions : class; ClassesThe following new classes are added. public class ReadOnlyOptionsFactory<TOptions> : IOptionsFactory<TOptions> where TOptions : class UsageUsage of the new API differs in two major ways.
The second of the two points is solved by using Explicit initializationGiven the following options type, public record MyOptions(string Value, bool Flag); it could be utilized in the following way: var services = new ServiceCollection()
.Configure<MyOptions>(() => new MyOptions("Initial", false))
.Configure<MyOptions>(opts => opts with { Value = "Something else" })
.BuildServiceProvider(); Using a Parameterless constructorGiven the following options type, public record MyOptions()
{
public string Value { get; init; }
public bool Flag { get; init; }
} it could be utilized in the following way: var services = new ServiceCollection()
.Configure<MyOptions>(opts => opts with { Value = "Something else" })
.BuildServiceProvider(); In this case, the parameterless constructor is used to create the initial state, and the user does not need to specify anything. This is the most similar to the current API. All-optional constructorGiven the following options type, public record MyOptions(string Value = "Initial", bool Flag = true); it could be utilized in the following way: var services = new ServiceCollection()
.Configure<MyOptions>(opts => opts with { Value = "Something else" })
.BuildServiceProvider(); In this case, a constructor with all parameters defined as optional is detected and used to create the initial state. This is a best-of-both-worlds approach, which allows both terse usage and an appropriate initial state. |
@TorreyGarland - this sounds like #61547 which was fixed in #52514, which was part of .NET 6 Actually, scrap that - it's not part of .NET 6 as the PR was closed so that further thought could be given to the problem. A new PR was created and merged to main in March, so it'll be part of .NET 7: #66131 |
We will consider this in a future release. Moving this issue out of the 7.0 milestone. |
@SteveDunn By "immutable collection types" are we talking about System.Collections.Immutable? // app.settings
{
"MyStrings": ["a", "b", "c"]
} public class MyOptions
{
public ImmutableArray<string> MyStrings { get; set; }
//or ImmutableHashSet<string>, or ImmutableDictionary<string,string>, etc.
} Because I just attempted this and neither .NET 6 nor .NET 7 worked - the immutable array above was not bound (its I tried binding to the concrete Line 1272 in 0f3a88b
Nevertheless, without any guarantee on the generated underlying type, it's not as good as Immutable - especially considering thread safety (which a readonly interface does not guarantee). Plus these is no Opened an issue suggesting immutable collection binding: : #78592 |
Will this be shipped with .NET 8 or is there any workaround how immutable options can be used? Because at the moment with .NET 7, the following code doesn't compile: internal class WebApplicationFactoryForAny : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder) =>
builder.ConfigureTestServices(services => services.Configure<ScreenshotOptions>(options => options with { Url = "" }));
}
public record ScreenshotOptions
{
public const string SectionName = nameof(ScreenshotOptions);
public string Url { get; init; } = string.Empty;
public UrlType UrlType { get; init; }
public string Username { get; init; } = string.Empty;
public string Password { get; init; } = string.Empty;
public string ScreenshotFileName { get; init; } = "Screenshot.png";
public uint Width { get; init; }
public uint Height { get; init; }
public uint TimeBetweenHttpCallsInSeconds { get; init; }
public uint RefreshIntervalInSeconds { get; init; }
public bool BackgroundProcessingEnabled { get; init; }
public Activity? Activity { get; init; }
public string CalculateSleepBetweenUpdates() =>
Activity.DisplayShouldBeActive()
? RefreshIntervalInSeconds.ToString()
: Activity.RefreshIntervalWhenInactiveInSeconds.ToString();
}
public enum UrlType
{
Any,
OpenHab
}
public record Activity(TimeOnly ActiveFrom, TimeOnly ActiveTo, uint RefreshIntervalWhenInactiveInSeconds); |
It doesn't compile, or are you getting an Exception at startup? What is the error, you are seeing? |
It doesn't compile, I'm getting a |
That is expected, as the discussed functionality is not in .NET as of yet. The API surface I suggested above is available as a nuget package if you want to start using it right away - it's linked in the comment. |
And will the support for immutable options be part of the upcoming .NET 8 release? |
@mu88 No, it's in the "Future" milestone, which means it was deprioritized. |
Is there a chance it will be added in .NET 9? :) |
Today the
IOptions<TOptions>
pattern is built around the mutability of theTOptions
until first use (though nothing prevents later mutation). We should investigate what it would mean to consume immutableTOptions
in the options system. Today, the system constructsTOptions
usingActivator.CreateInstance
then executes a series of delegates on top of that instead to produce the "final" instance (seeruntime/src/libraries/Microsoft.Extensions.Options/src/OptionsFactory.cs
Line 46 in 546115d
An alternative model would be to do the same thing but execute delegates that return a new instance of the options object instead of mutating the current instance. We would need to figure out how to support both side by side but it would allow designing immutable options objects.
Here's an example of what we what it would look like:
cc @ericstj @eerhardt @maryamariyan
The text was updated successfully, but these errors were encountered: