dotnet / csharplang

The official repo for the design of the C# programming language
11.61k stars 1.03k forks source link

Anonymous Interface Objects #4301

Open khalidabuhakmeh opened 3 years ago

khalidabuhakmeh commented 3 years ago

Anonymous Interface Objects

Summary

Give C# developers the ability to instantiate anonymous objects that implement a specific interface without first implementing a concrete type.

Motivation

In scenarios where developers need to stub a type for unit tests, it becomes increasingly tedious to create classes that are essentially throw away. They add to the complexity of the project and ultimately increase the noise/signal ratio.

Detailed design

public interface IClient
{
    string Name { get; }
}

// single interface
var client = new IClient {
    Name = "Khalid"
};

We can also implement multiple interfaces on an anonymous type.

public interface IClient
{
    string Name { get; }
}

public interface ICustomer
{
    string AccountNumber { get; }
}

// multiple interface
var client = new IClient, ICustomer {
    Name = "Khalid",
    AccountNumber = "8675309"
};

Method implementations can be implemented using lambda methods.

public interface ICashier
{
    decimal CheckOut(Cart cart);
}

var cashier = new ICashier {
   CheckOut = (c) => cart.Total() * 0.2d;
}

Drawbacks

Alternatives

Create stub types for every new scenario you need an interface.

Use Kotlin. Here is a working example where the khalid instance is an anonymous object that happens to implement the IGreeting interface.

image

    interface IGreeting {
        val name: String
    }

    class Default(override val name: String) : IGreeting

    fun main() {

        val csharp = Default("C# Developer")
        // an anonymous object
        // that implements IGreeting
        val khalid = object: IGreeting {
            override val name: String
                get() = "Khalid (You're Awesome)"
        }

        var folks = listOf(csharp, khalid)
        folks.forEach { println("Hello ${it.name}!") }
    }

Unresolved questions

Design meetings

toupswork commented 4 months ago

@toupswork No one on the LDM think this is beneficial enough to to in the near term. That may change if the landscape of the ecosystem changes. But currently this isn't rising to that level for any of us, or our partner teams.

Bummer. Ok thank you for taking the time to reply.

codetuner commented 1 month ago

@CharlieDigital

I think if we choose to add it to C#, it should support all kinds of types that can be casted or boxed into interfaces: [...] var client = new class : IClient, ICustomer { [...] } var client = new struct : IClient, ICustomer { [...] } var client = new record class : IClient, ICustomer { [...] } [...]

Today you can't choose to create an anonymous new class { ... } or a new struct { ... }, so why add that ability when creating an anonymous object that implements an interface. What's the difference really ?

I admit the proposal makes sense, but then the ability to choose the kind of type should also be available to "regular" (non-interface implementing) anonymous objects.

Though, having not the ability to choose the kind of type could be a blessing: the compiler could be smart enough to determine the optimal kind to compile to depending on it's definition and use. Who knows what (AI-driven?) compilers well be capable to in the future...

codetuner commented 1 month ago

Anonymous objects implementing interfaces is a great feature I have been missing a few times. But not that often. Why is that ?

Today I face the following example: I want to add a HealthCheck to an ASP.NET Core app. For that I need to call the following method:

builder.Services.AddHealthChecks().AddHealthCheck(aName, anIHealthCheck)

Without anonymous object implementing an interface, I need to create a separate class that implements IHealthCheck for every check I want to make. HealthChecks are sometimes very specific to the application and do not result in reusable types. So it is forcing me to do more ceremonial than I want, and result in the healthcheck implementation code to be on a distant place which you could argue lowers cohesion of the code.

It is one of the reasons C# is such a great language: linq and lambda expressions allow for very low ceremonial and inline code to increase cohesion and readability.

So how does it come that I don't really miss the feature of anonymous objects implementing interfaces here ? Because MS provided a "workaround": there's an overload of the AddHealthCheck method that takes a lambda expression:

builder.Services.AddHealthChecks().AddHealthCheck(aName, aFunc<HealthCheckresult>)

That's the workaround. The only reason this exists, is because anonymous objects can't implement interfaces.

And it looks great: "I can even just use a lambda expression! :)"

But in reality, it is awful. Mainly because it requires every implementor of methods that consume interfaces, to also provide overloads that take lambda expressions. Oh you can use extension methods. But then you still need the overhead of creating a class that implements the interface to pass to the real (unextended) method. (In case of the HealthChecks, that class is named DelegateHealthCheck). And it is again some more (boilerplate)code to write that doesn't really add value.

Anonymous objects implementing interfaces would solve this in a structural manner: whenever an interface is expected, you could pass in an anonymous object:

Moreover, the lambda-expressions workaround only really works for interfaces having a single method. You can have multiple lambda expression parameters of course, but it is hard to correlate their actions.

Conclusion: anonymous objects implementing interfaces is a clean and structural solution to a real problem and is more powerfull than the actual workaround. The workaround currently used, lowers the need for this feature, but comes at a heavy cost: every single place an interface is used, overloads or extension methods should be provided as well, causing lot's more code to write, but also bloating the .NET Core library and jeopardizing it's future as a "lean and mean" runtime capable of - for instance - being efficiently used for webassemblies.

FANMixco commented 1 month ago

Anonymous objects implementing interfaces is a great feature I have been missing a few times. But not that often. Why is that ?

Today I face the following example: I want to add a HealthCheck to an ASP.NET Core app. For that I need to call the following method:

builder.Services.AddHealthChecks().AddHealthCheck(aName, anIHealthCheck)

Without anonymous object implementing an interface, an need to create a separate class that implements IHealthCheck for every check I want to make. HealthChecks are sometimes very specific to the application and do not result in reusable types. So it is forcing me to do more ceremonial than I want, and result in the healthcheck implementation code to be on a distant place which you could argue lowers cohesion of the code.

It is one of the reasons C# is such a great language: linq and lambda expressions allow for very low ceremonial and inline code to increase cohesion and readability.

So how does it come that I don't really miss the feature of anonymous objects implementing interfaces here ? Because MS provided a "workaround": there's an overload of the AddHealthCheck method that takes a lambda expression:

builder.Services.AddHealthChecks().AddHealthCheck(aName, aFunc<HealthCheckresult>)

That's the workaround. The only reason this exists, is because anonymous objects can't implement interfaces.

And it looks great: "I can even just use a lambda expression! :)"

But in reality, it is awful. Mainly because it requires every implementor of methods that consume interfaces, to also provide overloads that take lambda expressions. Oh you can use extension methods. But then you still need the overhead of creating a class that implements the interface to pass to the real (unextended) method. (In case of the HealthChecks, that class is named DelegateHealthCheck). And it is again some more code to write that doesn't really add value.

Anonymous objects implementing interfaces would solve this in a structured manner: whenever an interface is expected, you could pass in an anonymous object:

  • no need to a separate named class that has no other use then provide implementation for this single use
  • no need to implement overloads of methods taking interfaces so lambda expressions are supported too
  • no need to implement extension methods that also require a "Delegate" type to fix the lack of overload

Moreover, the lambda-expressions workaround only really works for interfaces having a single method. You can have multiple lambda expression parameters of course, but it is hard to correlate their actions.

Conclusion: anonymous objects implementing interfaces is a clean and structural solution to a real problem and is more powerfull than the actual workaround. The workaround currently used, lowers the need for this feature, but comes at a heavy cost: every single place an interface is used, overloads or extension methods should be provided as well, causing lot's more code to write, but also bloating the .NET Core library and jeopardizing it's future as a "lean and mean" runtime capable of - for instance - being efficiently used for webassemblies.

Awesome answer, if you haven't voted in the original question, please give us your 👍! Thanks!

HaloFour commented 1 month ago

I think the evidence in the Java language demonstrates the opposite. Java has had anonymous implementation since Java 1.1 (1997), and didn't get lambdas until Java 8 (2014). Since that time most libraries seem to be moving from interfaces to functional interfaces and adding overloads that accept multiple lambdas rather than a single object. The number of times that I've actually needed to use anonymous classes in Java has dropped significantly, all of the major frameworks are lambda-first.

julealgon commented 1 month ago

@HaloFour what if C# provided a transparent way to convert a lambda into a single-method interface implementation?

Most of these scenarios where there is a single action to be taken, are covered by a simple interface that has a single method in it. If the language had a way to simply "accept" a lambda in place of one of those interface implementations, this problem would be mostly solved, and there would be no need to create the boilerplate code to create a "lambda adapter implementation", which is what happens today.

If this was supported natively, then the caller would decide whether he wants to have his implementation in a separate class, or just pass it inline as a lambda.

In a sense, this would be similar to how delegates work with lambdas today, but extended to work with single-method interfaces, which are extremely prominent.

And, if the interface itself has more than one method, but relies on default interface implementations for all but one of them, this would still work with those interfaces too.

HaloFour commented 1 month ago

@julealgon

See: #2517 😄

julealgon commented 1 month ago

@julealgon

See: #2517 😄

Nice, thanks for the link.

Kinda sad that the discussion seems to have died down... this seems quite useful, super obvious, intuitive and simple to implement. I wish the .NET team picked up more of these low-hanging-fruit stuff over time than they do currently.

HaloFour commented 1 month ago

this seems quite useful, super obvious, intuitive and simple to implement

Maybe, but there also doesn't seem to be a lot of demand for something like this. I thought interop with Java/Android would be that motivating factor as manually bridging to functional interfaces definitely sounds like a pain to me. Beyond that, is there really much value to functional interfaces or SAM types? Probably not, unless someone can demonstrate additional benefits. Maybe value delegates, when combined with generics?

julealgon commented 1 month ago

this seems quite useful, super obvious, intuitive and simple to implement

Maybe, but there also doesn't seem to be a lot of demand for something like this.

To be honest, I think there isn't much demand because it cannot be used, so people don't even realize the potential.

Beyond that, is there really much value to functional interfaces or SAM types? Probably not, unless someone can demonstrate additional benefits. Maybe value delegates, when combined with generics?

I've seen many use-cases similar to the one @codetuner provided above, where something is abstracted as a class, but can have super simple implementations that would fit a small lambda and thus could benefit from something like this.

Additionally, it would keep API surfaces simpler, with less overloads, less need for creating adapter implementations.

Another use case would be ASP.NET Core Middlewares, which have the 2 API forms and could be collapsed into one:

CyrusNajmabadi commented 1 month ago

This would def have more interest if an API like asp was interested in this sort of support.

julealgon commented 1 month ago

Another obvious example that I see would be IConfigureOptions, which today, again, is exposed "in multiple different APIs" as either a raw Action<T> or a IConfigureOptions parameter of some sort. The entire thing could probably be abstracted as just the interface if this seamless conversion existed.

Perhaps @davidfowl wants to chime in here?

It could become even stronger if static methods also seamlessly converted to implicit implementations, kinda like method groups work today with normal lambdas.

I'm pretty sure a lot of people would appreciate being able to pass functions around as interface implementations. I've seen many complain about this from OOP languages and "how much simpler" and "more composable" functional languages are because of that. Well... this feature would open the door for exactly that in C#. Suddenly, a "composite implementation" can be represented by actual function composition without having to define a whole class for that, too.

The applicability is very broad.

CyrusNajmabadi commented 1 month ago

We already have delegates for that case anyways. Why not just have that API take a delegate?

julealgon commented 1 month ago

We already have delegates for that case anyways. Why not just have that API take a delegate?

Which case exactly are you referring to @CyrusNajmabadi ?

CyrusNajmabadi commented 1 month ago

I'm pretty sure a lot of people would appreciate being able to pass functions around as interface implementations

If someone has such an interface, why not just model as a delegate instead?

julealgon commented 1 month ago

I'm pretty sure a lot of people would appreciate being able to pass functions around as interface implementations

If someone has such an interface, why not just model as a delegate instead?

Because then you can't have it be a class implementation, which might make sense in a few cases.

For example, an IConfigureOptions might be a scoped implementation that injects scoped services into it. If you remove the interface, and make it a delegate, suddenly you lose that capability of having a scoped object with DI support there.

Both should be available: the class + dependencies (and lifetime) would be for more complex use cases, while the inline lambda would work for more trivial, usually pure, implementations.

And this is exactly how IConfiguration customization works today. If methods, lambdas and interface implementations were interchangeable, the entire API could be unified.

CyrusNajmabadi commented 1 month ago

Because then you can't have it be a class implementation, which might make sense in a few cases.

Why can't it be?

I don't get your example. You can still have a class, just pass the method you care about as a delegate.

CyrusNajmabadi commented 1 month ago

If you remove the interface

You don't have to remove the interface. You just pass the method of interest to the delegate-taking API.

julealgon commented 1 month ago

Why can't it be?

I don't get your example. You can still have a class, just pass the method you care about as a delegate.

But then I'd have to resolve the class myself through the IServiceProvider, to be able to pass its method. Which is possible if you provide an overload with IServiceProvider, but again, this is then defeating the purpose as you are now back to having multiple distinct APIs and have to add a lot of boilerplate code to pass your implementation's method as a delegate (instead of just having the container resolve it as usual).

julealgon commented 1 month ago

@CyrusNajmabadi , here is a concrete example of what I mean.

If you check the IServiceCollection.Configure<TOptions>(Action<TOptions>) method, it delegates that Action you pass in to an implementation of IConfigureOptions, called ConfigureNamedOptions<TOptions>.

This is precisely the adapter I was mentioning before. All this implemntation does is "convert" the Action into an IConfigureOptions<T> implementation, so that it can be used as any other IConfigureOptions<T> instance.

Such adapters would become obsolete with conversion support between lambdas (and method groups) and interfaces, as it would just register the interface directly: there would be no Action<T> overload for configuration: everything would always take in a IConfigureOptions, and it would work seamlessly whether you wanted to provide some inline configuration as a lambda, or a more elaborate configuration via a custom implementation.

The fact that the consumer could be passing in an Action<T> would not be relevant to the API, which would only see IConfigureOptions instances.

CyrusNajmabadi commented 1 month ago

I would turn it around. I don't see the need to ever pass in IConfigOptions instances. I would just pass in the delegate.

julealgon commented 4 weeks ago

@CyrusNajmabadi

I would turn it around. I don't see the need to ever pass in IConfigOptions instances. I would just pass in the delegate.

And then, how would the underlying configuration API be able to resolve and use scoped IConfigureOptions registrations for use with, say, IOptionsSnapshot?

I think you are missing some of the power that the class-based approach provides here. If you invert it and have everything be based on delegates, you lose a lot of that power.

The more powerful/flexible abstraction (classes/interfaces) should win for things like this IMHO, and the less flexible one (delegates) should convert to it seamlessly.

Keep me honest here though in case I missed something.

CyrusNajmabadi commented 4 weeks ago

I honestly don't know what you're referring to.

Either the interface just has a single member in it, in which case you can just use a delegate for it. Or it has more, in which case a lambda wouldn't be convertible to it anyways.

CyrusNajmabadi commented 4 weeks ago

I'd need you to show an example of the API here and why taking a delegate is not sufficient.

julealgon commented 4 weeks ago

@CyrusNajmabadi can you check the link I provided earlier and how the underlying IConfiguration system relies on IConfigureOptions implementations to build up IOptions instances, even though it provides Action<T>-based overloads today?

I'm not sure how you would switch all that abstraction to rely on delegates instead of instances of IConfigureOptions.

The point I'm making here is that, if C# allowed these seamless conversions between lambdas/method groups to single-method interfaces, that all public APIs surrounding IConfiguration could unify on taking IConfigureOptions as parameters, and the callers would be free to either use lambdas or class implementations, while the underlying system would treat everything as IConfigureOptions instances and still be able to leverage DI + lifetimes etc as it does today.

This is not possible today, since you either must pass a delegate for inline configuration, or you must inject a IConfigureOptions instance manually or using services.ConfigureOptions<YourConfiguratorImplementation>().

CyrusNajmabadi commented 4 weeks ago

I'm honestly not seeing how it relies on anything here

IConfigureOptions just has a Configure method. What about their system relies on getting an actual interface here instead of a delegate?

and still be able to leverage DI + lifetimes etc as it does today.

I think, fundamentally, there is a disconnect in this discussion.

Nothing any di, or interfaces (afaict) precludes passing delegates around. Delegates are nicer, imo, because you have more flexibility with them. They are just a bound method.

That single bound method could come from a lambda, a local function, a method (from any type (class, interface, etc)), etc.

As a consumer, I would expect asp to give you the most flexibility in calling into it. While you could still design things on your end however you want (di, interfaces, etc).

CyrusNajmabadi commented 4 weeks ago

The point I'm making here is that, if C# allowed these seamless conversions between lambdas/method groups to single-method interfaces,

I agree that it would make cases like this nicer. But I'm trying to figure out why cases like this exist. Why was it modeled as. A single method interface instead of a delegate :-)

hez2010 commented 4 weeks ago

I can only see the necessity of this feature in a language where you have to implement a lot of interfaces or abstract classes, especially when each of them has multiple abstract methods need to be implemented, to serve like an adapter in order to call some functions.

For example, in Java, if you use swing to create GUI apps you will be likely end up with code for action listener like

control.addMouseListener(new MouseListener() {
    @Override
    public void mouseClicked(MouseEvent e) { ... }
    @Override
    public void mousePressed(MouseEvent e) { ... }
    @Override
    public void mouseReleased(MouseEvent e) { ... }
    @Override
    public void mouseEntered(MouseEvent e) { ... }
    @Override
    public void mouseExited(MouseEvent e) { ... }
});

This is because in Java they don't have first-class delegate support, which means that you have to use OOP to simulate how delegates work, i.e. implement a closure class manually and override all those virtual/abstract methods.

However, this is not the case in C#. I really don't see why you would want anonymous class in a language which already has delegates and events, where function is first-class so that you can just do:

control.MouseClicked += (obj, e) => { ... }
control.MousePressed += (obj, e) => { ... }
control.MouseReleased += (obj, e) => { ... }
control.MouseEntered += (obj, e) => { ... }
control.MouseExited += (obj, e) => { ... }

If you are using a library which forces you to implement some interface/abstract class for exactly the above purpose, it's the fault of the library author who failed to design the API surface in a C# way.

MaxGuernseyIII commented 4 weeks ago

C# is superior to Java in almost every way imaginable. However, what you call the "C# way" is not one of them.

Anyway, the primary use case for this, in my opinion, is mocking/faking, in my opinion.

I try to test as much of my own code together as possible, these days, but it is necessary to mock out certain external dependencies. Often, these dependencies don't really have interfaces that are easy to mock. You want to create fakes for them that work a certain way, but there are deep traversable structures with a lot of intermediate steps.

Currently, one has to either twist something like Moq into solving that problem, or create an entire litter of classes that will be instantiated and used as waypoints to find other objects, which then are used to do the same thing.

This could all be a lot simpler and more expressive with such a feature.

You can say "well the designers of those libraries aren't very smart for making it work that way" and, in a lot of cases, you'd be right. It doesn't change the reality that we have to deal with it.

julealgon commented 4 weeks ago

@hez2010

If you are using a library which forces you to implement some interface/abstract class for exactly the above purpose, it's the fault of the library author who failed to design the API surface in a C# way.

I think you are missing the point with this statement. The point is not that "an API was designed in a non-C# way by exposing interfaces instead of delegates", but that "if the API could expose an interface and still allow delegates to be used, that would have benefits to the API author".

Of course its not "the C# way" now precisely because it is not yet supported in C#! The moment the feature is implemented, and you get the ability to provide a delegate in place of an interface implementation, it starts being part of "the C# way", and library authors can now tap into this to create simpler APIs, with less overloads, while still working exactly the same internally, as my examples of IConfiguration and IConfigureOptions or the ASP.NET Core Middlewares demonstrate: both of these systems today need to provide 2 "APIs", one that takes an Action or Func (for inline delegates, commonly used for "simple", no-dependency logic), and another that allows for using an interface or base class implementation (for more elaborate scenarios that require precise lifetimes and dependencies). Supporting transparent conversion of delegates into interfaces would allow the unification of these 2 forms of APIs leading to much simpler code on both ends: consumer and library.

HaloFour commented 4 weeks ago

The question is whether such a change would bring a substantial improvement to the consumption of such APIs. I would suggest that it wouldn't. You might have a single overload, but the way in which you interact with those APIs would still be quite different. You'd still be using two different syntaxes based on what scenario you had. To the consumer is still looks and feels like two completely separate API. Might such a change enable API authors to consolidate those overloads? Sure. But if the end result is the same for consumers that doesn't feel like much of a benefit.

Suggesting that it would become the C# way implies that the benefit it brings would be substantial enough to change the paradigms followed by libraries and consumers, and I doubt that is the case. If anything, Java appears to prove this, as what was the "Java way" has been quickly replaced by lambdas and functional interfaces instead of anonymous implementation, to the extent that the latter just feels archaic these days.

CyrusNajmabadi commented 4 weeks ago

and another that allows for using an interface or base class implementation (for more elaborate scenarios that require precise lifetimes and dependencies

I still don't know what this means.

jl0pd commented 4 weeks ago

One thing that's commonly overlooked is performance: delegate call is almost 2 times slower than interface call. So wrapping delegate inside interface is quite slow and could be improved dramatically if it was possible to implement interface in-place.

Benchmark code ```csharp using System; using System.Linq; using System.Runtime.CompilerServices; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Running; BenchmarkRunner.Run(); class MyLinq { [MethodImpl(MethodImplOptions.NoInlining)] public static T AggregateDelegate(T[] ar, Func func, T seed) { for (int i = 0; i < ar.Length; i++) { seed = func(seed, ar[i]); } return seed; } [MethodImpl(MethodImplOptions.NoInlining)] public static T AggregateInterface(T[] ar, IFunc func, T seed) { for (int i = 0; i < ar.Length; i++) { seed = func.Invoke(seed, ar[i]); } return seed; } } interface IFunc { TResult Invoke(T1 v1, T2 v2); } abstract class FuncBase : IFunc { public abstract TResult Invoke(T1 v1, T2 v2); } sealed class IntSummator : FuncBase { public static IntSummator Instance { get; } = new(); public override int Invoke(int v1, int v2) => v1 + v2; } public class Bench { int[] Data = Enumerable.Range(0, 100_000).ToArray(); [Benchmark] public int Delegate() { return MyLinq.AggregateDelegate(Data, (acc, v) => acc + v, 0); } [Benchmark] public int Interface() { return MyLinq.AggregateInterface(Data, IntSummator.Instance, 0); } } ```

BenchmarkDotNet v0.14.0, Windows 10 (10.0.19045.5011/22H2/2022Update)
Intel Core i7-9750H CPU 2.60GHz, 1 CPU, 12 logical and 6 physical cores
.NET SDK 8.0.306
  [Host]     : .NET 8.0.10 (8.0.1024.46610), X64 RyuJIT AVX2
  DefaultJob : .NET 8.0.10 (8.0.1024.46610), X64 RyuJIT AVX2
Method Mean Error StdDev
Delegate 57.01 μs 0.494 μs 0.462 μs
Interface 34.97 μs 0.685 μs 1.025 μs
hez2010 commented 4 weeks ago

One thing that's commonly overlooked is performance: delegate call is almost 2 times slower than interface call. So wrapping delegate inside interface is quite slow and could be improved dramatically if it was possible to implement interface in-place.

I run the benchmarks you provided locally, but performance of delegates is almost same with interfaces, even a little bit faster:

On .NET 8:

Method Mean Error StdDev Allocated
Delegate 24.93 μs 0.252 μs 0.235 μs -
Interface 25.40 μs 0.112 μs 0.105 μs -

On .NET 9:

Method Mean Error StdDev Allocated
Delegate 19.07 μs 0.125 μs 0.117 μs -
Interface 19.30 μs 0.076 μs 0.071 μs -
julealgon commented 4 weeks ago

@HaloFour

Suggesting that it would become the C# way implies that the benefit it brings would be substantial enough to change the paradigms followed by libraries and consumers, and I doubt that is the case. If anything, Java appears to prove this, as what was the "Java way" has been quickly replaced by lambdas and functional interfaces instead of anonymous implementation, to the extent that the latter just feels archaic these days.

But I've not suggested anonymous implementation. I'm confused now.


@CyrusNajmabadi

and another that allows for using an interface or base class implementation (for more elaborate scenarios that require precise lifetimes and dependencies

I still don't know what this means.

It means stuff like this would not work as an inline delegate:

    private sealed class MicrosoftIdentitySwaggerConfigurator(
        IOptions<MicrosoftIdentityOptions> identityOptions,
        IOptions<SwaggerAuthorizationOptions> swaggerOptions)
        : IPostConfigureOptions<SwaggerGenOptions>
    {
        private readonly MicrosoftIdentityOptions identityOptions = identityOptions.Value;
        private readonly SwaggerAuthorizationOptions swaggerOptions = swaggerOptions.Value;

        void IPostConfigureOptions<SwaggerGenOptions>.PostConfigure(string? name, SwaggerGenOptions options) 
        {
            ...
        }
    }

Or this:

public sealed class ConfigurePortalApiClientOptions(
    ICompanyIdContext companyIdContext,
    IPortalService portalService) 
    : IConfigureOptions<PortalApiClientOptions>
{
    private readonly ICompanyContext companyContext = companyContext;
    private readonly IPortalService portalService = portalService;

    public void Configure(PortalApiClientOptions options)
    {
        options.BaseUrl = this.portalService.GetPortalUrl(this.companyContext.CompanyId);
    }
}

...

services.AddScoped<ICompanyContext, HttpCompanyContext>
services.AddScoped<IConfigureOptions<PortalApiClientOptions>, ConfigurePortalApiClientOptions>()
CyrusNajmabadi commented 4 weeks ago

Which part of that would not work?

julealgon commented 3 weeks ago

@CyrusNajmabadi

Which part of that would not work?

Can you let me know how that would look like using delegates like you suggested, so removing the IConfigureOptions interface?

CyrusNajmabadi commented 3 weeks ago

Sure. Anywhere that previously took an IConfigureOptions<T> instance would now take an Action<T> instance. Any place where it previously called .Configure on the instance, passing a T, it would now just invoke the action instance, passing the same argument.

julealgon commented 3 weeks ago

Sure. Anywhere that previously took an IConfigureOptions<T> instance would now take an Action<T> instance. Any place where it previously called .Configure on the instance, passing a T, it would now just invoke the action instance, passing the same argument.

@CyrusNajmabadi and how do I as the consumer pass such delegate with scoped container dependencies?

Also, notice the library doesn't immediately call the Action: it registers the implementation in DI, and then later on calls the method when resolving the IOptions<T> or IOptionsSnapshot from DI. Where does it "store" that action to be called later and how does it do call it later when resolving option types from the container?

I feel like either I'm missing something super obvious here (to be asking these, IMHO, obvious questions) or you are the one missing important aspects of how this is supposed to work.

osexpert commented 3 weeks ago

I like the idea but IMO this should not require any syntax changes. It can just infere the interface from what you assign it?

interface IFoo{ string Bar {get;} }
IFoo i  = new{ Bar="foo" }

Duck typing?

CyrusNajmabadi commented 3 weeks ago

Sure. Anywhere that previously took an IConfigureOptions<T> instance would now take an Action<T> instance. Any place where it previously called .Configure on the instance, passing a T, it would now just invoke the action instance, passing the same argument.

@CyrusNajmabadi and how do I as the consumer pass such delegate with scoped container dependencies?

You pass the .Configure method of your instance.

Also, notice the library doesn't immediately call the Action: it registers the implementation in DI, and then later on calls the method when resolving the IOptions<T> or IOptionsSnapshot from DI. Where does it "store" that action to be called later and how does it do call it later when resolving option types from the container?

A delegate is just a normal type with normal instances. It stores those instances exactly the same way it stores the old interface values.

I feel like either I'm missing something super obvious here (to be asking these, IMHO, obvious questions)

A delegate is effectively an interface (just a name + shape) with a single method. So if you had an interface with just a single method, they should effectively be interchangeable. I haven't seen anything in what you've shown with an interface that couldn't be achieved with delegates.

quixoticaxis commented 3 weeks ago

I feel like either I'm missing something super obvious here (to be asking these, IMHO, obvious questions) or you are the one missing important aspects of how this is supposed to work.

What's the issue with resolving delegates from DI?

julealgon commented 3 weeks ago

@quixoticaxis

What's the issue with resolving delegates from DI?

I see at least 2 problems, or obstacles:

  1. How would dependencies be managed for that delegate? How can it depend on a scoped service, for instance?
  2. Conflicts if you use Action or Func types, so every single action would require a dedicated custom delegate definition to differentiate the registrations from other delegate registrations in the container so that each can be resolved independently. This is very unusual in C# after the generic Action and Func types were introduced (see things like Predicate, which basically became "obsolete"). Alternatively, I'd need to use keyed/named registrations (which might actually be simpler here, potentially)

I'm aware one can register delegates in DI, but there is more to it than just the raw registration aspect for it all to work properly.

quixoticaxis commented 3 weeks ago

@quixoticaxis

What's the issue with resolving delegates from DI?

I see at least 2 problems, or obstacles:

  1. How would dependencies be managed for that delegate? How can it depend on a scoped service, for instance?
  2. Conflicts if you use Action or Func types, so every single action would require a dedicated custom delegate definition to differentiate the registrations from other delegate registrations in the container so that each can be resolved independently. This is very unusual in C# after the generic Action and Func types were introduced (see things like Predicate, which basically became "obsolete"). Alternatively, I'd need to use keyed/named registrations (which might actually be simpler here, potentially)

I'm aware one can register delegates in DI, but there is more to it than just the raw registration aspect for it all to work properly.

I'm not trying to imply it is pretty, but I'vs seen a lot of code that injects logs and metrics as named delegates. Both do not need elaborate dependencies though.

DanFTRX commented 3 weeks ago

Sure. Anywhere that previously took an IConfigureOptions<T> instance would now take an Action<T> instance. Any place where it previously called .Configure on the instance, passing a T, it would now just invoke the action instance, passing the same argument.

@CyrusNajmabadi and how do I as the consumer pass such delegate with scoped container dependencies?

You pass the .Configure method of your instance.

In the use case provided, the instance doesn't exist at the time of registration so that's not really doable. In order to replace interfaces in service registration, delegate signatures would always need an IServiceProvider. But then they lose the lifetime management and service resolution will occur on each invocation. E.G.

public delegate void ConfigureOptions<T>(IServiceProvider provider, T options);
public void MyConfigure(IServiceProvider provider, Options options)
{
    options.Option = provider.GetRequiredService<OptionResolver>().Resolve();
}

Now if my "service" function is used multiple times in a single scope, provider.GetRequiredService() is repeatedly invoked, potentially to performance degradation.

jukkahyv commented 3 weeks ago

In the use case provided, the instance doesn't exist at the time of registration so that's not really doable. In order to replace interfaces in service registration, delegate signatures would always need an IServiceProvider. But then they lose the lifetime management and service resolution will occur on each invocation. E.G.

public delegate void ConfigureOptions(IServiceProvider provider, T options); public void MyConfigure(IServiceProvider provider, Options options) { options.Option = provider.GetRequiredService().Resolve(); } Now if my "service" function is used multiple times in a single scope, provider.GetRequiredService() is repeatedly invoked, potentially to performance degradation.

Also, calling GetRequiredService is not DI. It's service locator, which is an antipattern. Not a good solution.

CyrusNajmabadi commented 3 weeks ago

In the use case provided, the instance doesn't exist at the time of registration so that's not really doable.

I have no idea what this means. If there's no instance available, how were you calling the method before which took the IConfigureOptions value?

But then they lose the lifetime management

I have no idea what this means. Delegates are normal values, just like values of interface types.

--

Stepping back. A Delegate is just a normal .Net type. It just happens to be a type that subclasses System.Delegate. That's it. :) You can have values of that type, just like you have values of a particular interface. Each delegate specifies a few well known methods particular to its shape. Like its .Invoke method.

So everything you are doing with an interface with a single method is something you shoudl be able to do with a delegate with a particular Invoke method shape. There's nothing special about an interface unless you are using some library that literally does some sort of GetType().IsInterface check. And, if so, that's not our concern. Get that library to stop doing that.

jukkahyv commented 3 weeks ago

I think the confusion is because IConfigureOptions can actually take 2 types of parameters: constructor parameters (variable) and invocation parameters for the method (fixed). I guess this could be solved with anonymous type implementing interface, but it would require constructor also. I don't see how it can be implemented with just a delegate (because of the constructor parameters).

CyrusNajmabadi commented 3 weeks ago

Can you explain what you mean. The API definition is simply:

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.Extensions.Options
{
    /// <summary>
    /// Represents something that configures the <typeparamref name="TOptions"/> type.
    /// Note: These are run before all <see cref="IPostConfigureOptions{TOptions}"/>.
    /// </summary>
    /// <typeparam name="TOptions">The options type being configured.</typeparam>
    public interface IConfigureOptions<in TOptions> where TOptions : class
    {
        /// <summary>
        /// Invoked to configure a <typeparamref name="TOptions"/> instance.
        /// </summary>
        /// <param name="options">The options instance to configure.</param>
        void Configure(TOptions options);
    }
}

WDYM by "can actually take 2 types of parameters"?

CyrusNajmabadi commented 3 weeks ago

but it would require constructor also. I don't see how it can be implemented with just a delegate (because of the constructor parameters).

If it can't be implemented with just a delegate, i don't understand how it could be implemented with just a lambda (which people are asking for).