Open Tratcher opened 5 years ago
Yes!! Lets kill anything that is not convention based startup and have 1 way / signature to configure everything.
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.
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)
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.
We can break the IStartup
interface instead of deprecating it. We need to remove the return type from ConfigureServices
@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.
@davidfowl breaking IStartup will hurt existing apps upgrading to 3.0. Those apps were still going to work using the old WebHost.
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 .
@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?
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?
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.
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.
Startup is a bootstrapper not a dependency. It's the composition root for the application level container (well Configure is really).
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:
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.
Yeah, it's a bit like the convention allowed in a C# foreach statement:
foreach
needs is a GetEnumerator()
method that returns an object that has a MoveNext()
method returning a bool, and a Current
property returning an objectIEnumerator
, which is the same as implementing GetEnumerator()
, MoveNext()
and Current
There is a convention, but the convention also has a contract: IEnumerator.
Hope this parallel design helps explain what I'm looking for.
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
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:
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.
@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?
shrug either way. As you say, it's just guidance.
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?
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.
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 UseStartup<T>
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.
@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.
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.
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.
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.
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.
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
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);
}
Assigning this to myself for the 5.0 time frame for investigation.
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.
@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?
To have the most flexibility the startup system should allow a developer to:
IServiceCollection
instance.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
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?
- 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.
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.
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.
@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.
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.
Proposal for specifying the startup instance is here https://github.com/dotnet/aspnetcore/issues/19809#issuecomment-661675618
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
.
@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:
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.
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.
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.
@pranavkm @Pilchie can we write a code fix for this?
Is the codefix to warn about the use of IStartup
and turn it into IStartupConvention
? That's not already a type, is it?
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