dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
34.88k stars 9.85k forks source link

Consider IStartup alternative #11050

Open Tratcher opened 5 years ago

Tratcher commented 5 years ago

IStartup isn't going to work with generic host which is the default in 3.0. It also conflicts with a few other patterns like ConfigureTestServices.

See https://github.com/aspnet/AspNetCore/issues/11001

@davidfowl

javiercn commented 5 years ago

Yes!! Lets kill anything that is not convention based startup and have 1 way / signature to configure everything.

jzabroski commented 5 years ago

I'm a bit confused. How is removing an explicit interface killing anything not convention based startup? Usually in my experience an interface adds structure/convention, rather than kills it...

I also don't understand the comment:

It also conflicts with a few other patterns like ConfigureTestServices.

@javiercn said in https://github.com/aspnet/AspNetCore/issues/11001#issuecomment-500528103 that ConfigureTestServices is also going to be no longer required:

In 3.0 there is only one DI container and ConfigureTestServices is not required as configure methods are applied in order (which was the issue ConfigureTestServices was trying to solve).

ConfigureTestServices is meant for the common case, which is a Startup class without extending/implementing any inferface

It sounds like in 3.0, ConfigureTestServices becomes obsolete as well.

Tratcher commented 5 years ago

The convention for ConfigureServices allows multiple signatures. The convention we prefer is public void ConfigureServices(IServiceCollection services), but IStartup dictates public IServiceProvider ConfigureServices(IServiceCollection services)

jzabroski commented 5 years ago

I get it - basically, you prefer the fluent building to be internal to the Startup object, and the IStartup interface allows fluent external configuration (via escaping an IServiceProvider), which also makes it harder to make consistent mock-able POCO.

davidfowl commented 5 years ago

We can break the IStartup interface instead of deprecating it. We need to remove the return type from ConfigureServices

jzabroski commented 5 years ago

@davidfowl I like this approach better, as it makes it easier to write architecture verification tests - I can then explicitly decouple my Pipeline pre-processing from my Pipeline processor from my Pipeline post-processor - these could all live in different (sets of!) assemblies. This is more or less the goals of more functional frameworks like F# Giraffe.

Tratcher commented 5 years ago

@davidfowl breaking IStartup will hurt existing apps upgrading to 3.0. Those apps were still going to work using the old WebHost.

jzabroski commented 5 years ago

Then create a IStartup2 interface and deprecate the old IStartup with an Obsolete warning telling people to forward to IStartup2. A bit klunky, but better than this ridiculous POCO approach (sorry, can't hide my true feelings for what a mess that is).

On Mon, Jun 10, 2019 at 3:45 PM Chris Ross notifications@github.com wrote:

@davidfowl https://github.com/davidfowl breaking IStartup will hurt existing apps upgrading to 3.0. Those apps were still going to work using the old WebHost.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/aspnet/AspNetCore/issues/11050?email_source=notifications&email_token=AADNH7LQHRHK6CLBOJG2ZQTPZ2VMBA5CNFSM4HWV5FPKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODXLARUY#issuecomment-500566227, or mute the thread https://github.com/notifications/unsubscribe-auth/AADNH7OGVGGDVZL5FBUQL5DPZ2VMBANCNFSM4HWV5FPA .

davidfowl commented 5 years ago

@jzabroski The poco story supports more scenarios. I rather not have something called IStartup2, because it sounds awful 😄. How IStartupBase/Core something other than 2?

jzabroski commented 5 years ago

IStartupCore is clever and good.

When you say the poco story supports more scenarios, are you thinking of deploying a Startup engine in multiple contexts, e.g. non-MVC, such that the Startup class does not even have to have a reference to MVC? Good in theory, strange in practice: Startup will reference a concrete WebHostBuilder, right? So how do you eliminate the private dependency on Microsoft.ASP.NET? Would you just use C# pre-processor #ifdef commands and compile two versions of your Bootstrap/Startup engine?

I ask because this is an interesting user story I mostly hate in existing IoC projects: the whole "extend the IoC container via extension method" thing seems quite amateurish. It started with Ninject and that's been the standard design pattern ever since, and I think it was a mistake that needs to stop. The fundamental problem is that IoC frameworks are tightly coupled to the framework runtime hosting policy and how it handles assembly load contexts, threads and processes. - This is what extension methods SHOULD be doing, not some Ninject.Mvc thing you jam into your bootstrap.

I can give you a concrete user story to illustrate my point: How do you have a Controller method that delegates to PLINQ and passes the currently authenticated user to the child threads spawned by PLINQ?

Can you give me a concrete user story to demonstrate why a "poco story supports more scenarios" that are actually useful and solve real problems I have?

davidfowl commented 5 years ago

I can give you a concrete user story to illustrate my point: How do you have a Controller method that delegates to PLINQ and passes the currently authenticated user to the child threads spawned by PLINQ?

What does that have to do with anything?

Can you give me a concrete user story to demonstrate why a "poco story supports more scenarios" that are actually useful and solve real problems I have?

The Startup class is a bootstrapper, fundamentally it can't really have any dependencies inject into it since it's the one that has to configure more services. That limitation is even more evident in 3.0 as we disallow injecting anything into the startup constructor that wasn't already provided by the hosting (configuration and IHostEnvironment etc). To support injecting services, you can inject into the Configure method directly. That's the main use case that is broken by using the IStartup interface.

jzabroski commented 5 years ago

What does that have to do with anything?

I discovered two nights ago that Principals are dependency injected in ASP.NET Core, so they are bound to a scope, not a thread, so my point is likely irrelevant. Sorry.

The Startup class is a bootstrapper, fundamentally it can't really have any dependencies inject into it since it's the one that has to configure more services. That limitation is even more evident in 3.0 as we disallow injecting anything into the startup constructor that wasn't already provided by the hosting (configuration and IHostEnvironment etc). To support injecting services, you can inject into the Configure method directly. That's the main use case that is broken by using the IStartup interface.

An interface is not a dependency; the Startup class is the dependency.

Dependency inversion principle: Depend on abstractions, not implementation details.

Interfaces do NOT break use cases. They enable use cases.

If the goal is to disallow configuring the Microsoft DI plumbing from binding IStartupCore to a Startup class, then add a feature to Microsoft DI to throw exceptions on certain invalid bindings.

davidfowl commented 5 years ago

Startup is a bootstrapper not a dependency. It's the composition root for the application level container (well Configure is really).

jzabroski commented 5 years ago

Having an interface is orthogonal to the fact it is a bootstrapper.

The interface allows a generic constraint on the fluent interface for setting the composition root. You even agree by saying:

well Configure is really

Netting it out, here are the correct tasks:

  1. Keep IStartup for backward compatibility.
  2. Mark IStartup with Obsolete
  3. Add IStartupCore with proper Configure signature that is currently resolved through reflection.
  4. Update fluent interface for setting a composition root to have a type constraint on the startup type.
  5. Test that the integration test helper works with new interface. It should.
  6. Update https://github.com/aspnet/AspNetCore.Docs/blob/master/aspnetcore/fundamentals/dependency-injection/samples/2.x/DependencyInjectionSample/Startup.cs to use IStartupCore as best practice
  7. Consider updating the popular third party ASP.NET Boilerplate app to use IStartupCore as well.
mmiller678 commented 5 years ago

I agree there should be an interface as the startup class must adhere to a contract. Using convention is fine for people who don't wish to implement the interface, but it requires you "know" the convention or run the application then look at the resulting exception to learn what the convention is. Even then you don't have exact types, so you probably end up needing to review the documentation. I don't see how providing a strongly typed contract is not ideal. Can someone explain to my why convention is a better approach? And if so why aren't we using convention in ASP.NET core for things like identity, principal, etc. vs contracts (interfaces). As I said before I don't think the two need to be mutually exclusive.

jzabroski commented 5 years ago

Yeah, it's a bit like the convention allowed in a C# foreach statement:

There is a convention, but the convention also has a contract: IEnumerator.

Hope this parallel design helps explain what I'm looking for.

davidfowl commented 5 years ago

I understand why people want for the interface and we’ll add one back. I was just clarifying some misconceptions about why both patterns exist and why IStartup is a bootstrapper/composition root and not a dependency

jzabroski commented 5 years ago

Another suggested task (will update my previous comment as well):

@davidfowl It's an interesting side discussion I wish we could just have via instant message. I do not really understand (and thus cannot agree or disagree) with your stance that a Startup class is not a dependency. I agree that it is a bootstrapper/composition root, but at least when deploying code to environments where the host exposes a lot of static state, the Startup is actually the most critical dependency of all:

  1. Sequential Coupling as a Dependency. IStartupCore is the source of all sequential coupling between components, as it lays out the initialization sequence of modules
  2. Convention/Contract as Dependency (Design by Contract) Any convention (or contract) creates a dependency on consumers of that convention (or contract). It seems to me the most natural way to express that convention is through code, rather than documentation, ergo why we have weird edge cases writing ASP.NET integration tests where IStartup interface doesn't work today.
  3. Powerbox Pattern/Ambient Context as Dependency. Encapsulating global/static data into a set of microservices, such that consumers of those microservices are blissfully unaware it is global state under the hood.
davidfowl commented 5 years ago

update https://github.com/aspnet/AspNetCore.Docs/blob/master/aspnetcore/fundamentals/dependency-injection/samples/2.x/DependencyInjectionSample/Startup.cs to use IStartupCore as best practice

No, I don't think we'll do that.

@davidfowl It's an interesting side discussion I wish we could just have via instant message. I do not really understand (and thus cannot agree or disagree) with your stance that a Startup class is not a dependency. I agree that it is a bootstrapper/composition root, but at least when deploying code to environments where the host exposes a lot of static state, the Startup is actually the most critical dependency of all:

It's OK to disagree, it's not worth a further discussion.

analogrelay commented 5 years ago

@davidfowl @Tratcher we need to find the minimal thing needed in 3.0 as time is running very short. Can we do this in 3.1, since we weren't actually going to remove anything, just obsolete it?

Tratcher commented 5 years ago

shrug either way. As you say, it's just guidance.

jjxtra commented 4 years ago

So there is no way to add an IStartup instance in net core 3? Why? I don't want to pass a type, I already have an instance I am using, I want to use that. Impossible now in .net core 3?

jzabroski commented 4 years ago

Jeff, you can still pass an instance but the contract isn't public. "Why" isn't worthy of discussion according to the team, despite the hundreds of lost hours of productivity this simple thing will cause across developers worldwide.

jjxtra commented 4 years ago

Jeff, you can still pass an instance but the contract isn't public. "Why" isn't worthy of discussion according to the team, despite the hundreds of lost hours of productivity this simple thing will cause across developers worldwide.

How do I pass an instance? I only see the method that takes no parameter, i.e. UseStartup<T>

RehanSaeed commented 4 years ago

This should be listed as a breaking change in the docs. I came here because the following working code now throws:

System.NotSupportedException : Microsoft.AspNetCore.Hosting.IStartup isn't supported

public class CustomWebApplicationFactory<TEntryPoint> : WebApplicationFactory<TEntryPoint>
    where TEntryPoint : class
{
    protected override TestServer CreateServer(IWebHostBuilder builder)
    {
        builder.UseEnvironment("Testing").UseStartup<TestStartup>();
        return base.CreateServer(builder);
    }
}

public class TestStartup : Startup
{
    public TestStartup(IConfiguration configuration, IWebHostEnvironment webHostEnvironment)
        : base(configuration, webHostEnvironment)  { }

    public override void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton(...);
        base.ConfigureServices(services);
        services.AddSingleton(...);
    }
}

public class Startup : StartupBase { ... }

I personally prefer the strongly typed class and am not sure why the convention based approach is pushed in this instance when it isn't for Controller's.

jzabroski commented 4 years ago

@RehanSaeed Welcome to my world, where most of ASP.NET has become pleasant save for a few small issues, like this insane one where you have to "know" the signature of the Startup class and can't just hit control+. in order to generate the correct interface to implement, and then instantly get your integration tests working for almost free.

Maybe someone will one day change ASP.NET's mind.

jzabroski commented 4 years ago

I think the part that annoys me most about this whole thing is we went from WebExtensionEx Nuget package and literally not needing to care at all about how our Startup is constructed unless we really cared to customize it, to this mess. It went from something most people didn't even have to care about, to something most people can't just implement an interface to get going with.

davidfowl commented 4 years ago

An interface is completely reasonable. It just can’t be the same IStartup (I think i mentioned this already). So we can look at that. The thread kinda digressed into throwing stones at the other approach.

So I’ll rename this thread to something constructive so we can make progress.

syndicatedshannon commented 4 years ago

I ended up here because of the: IServiceProvider ConfigureServices(IServiceCollection services); signature.

But, it took a bit of effort. The analyzer response from the interface-supporting return services.BuildServiceProvider(); sent me on a goose chase, until I came across: David's comment https://github.com/aspnet/AspNetCore/issues/14587#issuecomment-537963678

The comment was helpful, but avoiding the chase somehow would have been more helpful. Perhaps an analyzer warning on ConfigureServices, or [Obsolete] on IStartup, if this decision is final.

syndicatedshannon commented 4 years ago

I guess I'm saying, in addition to the issue this topic has centered around, I'm also interested in the original title :D . I just want to make sure that part of the issue, which may be simplest and separate from the other debate, isn't lost.

jzabroski commented 4 years ago

How hard is it to add an interface for these conventions? Can I do it? I don't understand why this issue is just sitting here doing nothing. It seems like a 15 minute thing? I realize we all have lives and stuff, but the amount of time myself and friends have wasted trying to learn these conventions by reading docs vs. just having IntelliSense DO IT is a bit silly :P

jzabroski commented 4 years ago

How is this for an interface? It would also allow Roslyn analyzers to be run that warn people not to call BuildServiceProvider, one of @davidfowl pet peeves.

public interface IStartupConvention
{
  IConfiguration Configuration { get; }
  /// <summary>
  /// Completely optional. method to configure the app's services. A service is a reusable component that provides app functionality. Services are registered in ConfigureServices and consumed across the app via dependency injection (DI) or ApplicationServices.
  /// </summary>
  /// <remarks>
  /// 1. Completely optional.
  /// 2. Called by the host before the Configure method to configure the app's services.
  /// 3. Where configuration options are set by convention.
  /// 4. Do not call BuildServiceProvider in this method.
  /// </remarks>
  void ConfigureServices(IServiceCollection services);
  /// <summary>
  /// adas
  /// </summary>
  /// <remarks>Most services are not available until the Configure method is called.</remarks>
  void Configure(IApplicationBuilder app, IWebHostEnvironment env);

}
davidfowl commented 4 years ago

Assigning this to myself for the 5.0 time frame for investigation.

arialdomartini commented 4 years ago

Instead of being helped by IntelliSense I always find myself reading over and over the documentation and other people's code, and delving with run time errors.

Sorry for being direct but this use of conventions over strongly typed contracts is just frustrating. It makes me miss the pre-Core ASP.NET frameworks.

arialdomartini commented 4 years ago

@jzabroski

Jeff, you can still pass an instance but the contract isn't public

How? I tried with no success. Would you share a code snippet to do that?

JustinKaffenberger commented 4 years ago

To have the most flexibility the startup system should allow a developer to:

It makes sense that the framework is designed to dissuade from these practices, but the reality is there are certain scenarios that this flexibility is needed, so they can't be completely eliminated. I don't have time to elaborate on these scenarios right now, but if there is enough interest I can make time.

jzabroski commented 4 years ago

@jzabroski

Jeff, you can still pass an instance but the contract isn't public

How? I tried with no success. Would you share a code snippet to do that?

@arialdomartini I already proposed how, above: https://github.com/dotnet/aspnetcore/issues/11050#issuecomment-579484487

I designed the contract based on the magic implicit contract described here: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/environments?view=aspnetcore-3.1#environments

Don't you just love magic numbers?

jzabroski commented 4 years ago
  • The startup type should allow a custom DI container to be built using the provided IServiceCollection instance.

@JustinKaffenberger The whole point is that your custom DI container should not build its own type universe. I doubt your proposal would ever get accepted. David has said repeatedly its a mistake to build a service provider yourself and the host should do that work. Just use a "Membrane pattern" and have your DI container be wrapped by Microsoft DI. Let Microsoft DI manage the web host, and then create a cross-wiring layer so that your container can get objects it needs from Microsoft DI.

davidfowl commented 4 years ago

Specify the instance of the startup type, not just the type.

This would be the thing to enable.

The startup type should allow a custom DI container to be built using the provided IServiceCollection instance.

You can do this on your own once you can pass the instance.

arialdomartini commented 4 years ago

Being unable to provide an instance makes testing a real nightmare.

Replacing

UseStartup<Startup>()

with

UseStartup<TestStartup>()

with an innocent

public class TestStartup : Startup {}

just breaks the API. Why? Isn't it a violation of the Liskov Substitution Principle? It seems it's abusing reflection so much that the result is not OO anymore.

JustinKaffenberger commented 4 years ago

@davidfowl thanks, that's what we're doing now to get this to work. It just felt sketchy since I had to look into the ASP.NET source to make sure manually registering the instance would be allowed. So I guess my suggestion is to just make this functionality more obvious.

SetTrend commented 3 years ago

How is this for an interface? It would also allow Roslyn analyzers to be run that warn people not to call BuildServiceProvider, one of @davidfowl pet peeves.

public interface IStartupConvention
{
  void ConfigureServices(IServiceCollection services);
  void Configure(IApplicationBuilder app, IWebHostEnvironment env);
}

The Configure() method currently accepts additional parameters, fed by DI. It would be hard to put that into an interface, I believe.

davidfowl commented 3 years ago

Proposal for specifying the startup instance is here https://github.com/dotnet/aspnetcore/issues/19809#issuecomment-661675618

SetTrend commented 3 years ago

I still have strong bad feelings about having such a core class being exposed to throwing runtime exceptions instead of rendering compile time errors.

So, I would strive for an interface just providing a mandatory IStartup::ConfigureServices(), method similar to what @jzabroski suggested:

interface IStartup
{
  /// <summary>
  ///   This method gets called by the runtime once.
  ///   Use this method to add application specific services
  ///   to the <paramref name="services"/> collection.
  /// </summary>
  /// <param name="services">
  ///   Collection of services, pre-filled with systems services.
  ///   Supplement with additional, application specific services.
  /// </param>
  /// <param name="configuration">
  ///   Runtime environment configuration data.
  /// </param>
  void ConfigureServices(IServiceCollection services, IConfiguration configuration);
}

... and have Configure() be an optional, arbitrary method decorated by an attibute, e.g. WebConfigurationMethodAttribute.


Anyway, I feel the class name "Startup" is too general and misleading as it doesn't stand for what the class actually does.

I would suggest to rename Startup to WebConfiguration because that's what the class actually does. "Startup" actually is the Program class.

So, the interface name I suggest would correspondingly be IWebConfiguration.

davidfowl commented 3 years ago

@SetTrend I appreciate the sentiment but renaming the concept of Startup to IWebConfiguration may feel pure is an arbitrary change that adds no value and increases confusion and concepts. We already have configuration, dependency injection, hosts, startup classes.

... and have Configure() be an optional, arbitrary method decorated by an attibute, e.g. WebConfigurationMethodAttribute.

Instead of calling your method Configure? What's the benefit?

This feature exists for a small subset of people that want a contract and don't need injection of dependencies in to Configure. To that end, I think something like:

public interface IStartupConvention
{
  void ConfigureServices(IServiceCollection services, WebHostBuilderContext context);
  void Configure(IApplicationBuilder app);
}

Seems like it would make sense as a strawman proposal. Some things I would like to make clear:

jjxtra commented 3 years ago

Once I got my head around the configure services and configure app call, I decided to architect a delegate pattern. So I have a service runner class which has a constructor that takes an interface that allows hooking into various parts of the pipeline. The service runner calls the host builder, configure services, etc. then calls the delegate to give it an option to setup additional services. Same for configuring the application and various other bits in the setup pipeline.

Whether this should be built into the core framework, that's hard to say. But for now, I don't have any concern about IStartup being deprecated. Leave it up to the consumer of the framework if/how they want to delegate pipeline setup functions to a delegate/interface.

SetTrend commented 3 years ago

renaming the concept of Startup to IWebConfiguration may feel pure is an arbitrary change that adds no value and increases confusion and concepts.

On the contrary. Renaming would decrease confusion. "Startup" can be anything and nothing. Program.cs is a startup class, too, for instance. Without reading the docs or intimate knowledge of ASP.NET, one wouldn't know what the class does or what's it's about.

Instead of calling your method Configure? What's the benefit?

Compared to the current implementation, the benefit – again – would be that you wouldn't need a crystal ball to tell what the method is or does, nor do you need external sources to tell what the name of the method must be for the system to work. Interfaces, contracts ... that's what object oriented programming - and robust programming - is about. It's not a game of hide-and-seek.

This feature exists for a small subset of people that want a contract and don't need injection of dependencies in to Configure. To that end, I think something like:

public interface IStartupConvention
{
  void ConfigureServices(IServiceCollection services, WebHostBuilderContext context);
  void Configure(IApplicationBuilder app);
}

If dependency injection for Configure() is not mandatory, that's a fair enough interface.

davidfowl commented 3 years ago

On the contrary. Renaming would decrease confusion. "Startup" can be anything and nothing. Program.cs is a startup class, too, for instance. Without reading the docs or intimate knowledge of ASP.NET, one wouldn't know what the class does or what's it's about.

We'll have to agree to disagree on that one.

davidfowl commented 3 years ago

@pranavkm @Pilchie can we write a code fix for this?

pranavkm commented 3 years ago

Is the codefix to warn about the use of IStartup and turn it into IStartupConvention? That's not already a type, is it?