Issue 1:
I'm submitting a ... (check one with "x")

[ ] bug report
[x] feature request
[ ] support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

With the current injectior options you can't redefine a service that uses his parent, because of a circular dependency.

For instance, imagine I make a new service, called HttpA that uses the Http service but adding a functionality. Now, as long as my service extends Http or explicitly uses Http (by creating an instance) I can declare a provider for this service with:

provide(HttpA, {
useFactory: (xhrBackend: XHRBackend, requestOptions: RequestOptions) => {
return new HttpA(xhrBackend, requestOptions);
},
deps: [XHRBackend, RequestOptions]
})

But now, maybe I don't want to go through all my application and change all the Http references to HttpA, so I can do something like this:

provide(Http, {
useFactory: (xhrBackend: XHRBackend, requestOptions: RequestOptions) => {
return new HttpA(xhrBackend, requestOptions);
},
deps: [XHRBackend, RequestOptions]
})

And this will work perfectly.

But what will happen if we create another Http-based service called HttpB that adds a different functionality from HttpA? If we use the same provider as HttpA it will replace that implementation with the new one, because we will be telling the injector that whenever someone asks for the Http service, give him the HttpB service instance that we just created, that it uses or extends @angular/core Http, thus the functionality in HttpA will be lost.

The problem then is that when we try to use the injector to get the last Http implementation we get a cyclic dependency error:

provide(Http, {
useFactory: (http:Http) => {
return new HttpA(http);
},
deps: [Http]
})

And it makes sense if you read it like "When someone asks the Http service, get the Http service and use HttpA instead", but wouldn't it be cool the injector to know that the Http service we are injecting in the provider is not the same we are returning? (i.e., it's the last defined implementation)? In fact, the behaviour that I want would be like:

provide(Http, {
useFactory: (http:OldHttp) => {
return new HttpA(http);
},
deps: [OldHttp]
})

But of course this won't work now because OldHttp is not defined (this is just the behaviour I mean)

This would let the developers do these kind of chaining, with any service (Angular 2 or custom)

PS: This feature was crafted from a talk in AngularCamp Barcelona June 2016

Issue 2:
I'm submitting a...

[ ] Regression (a behavior that used to work and stopped working in a new release)
[ ] Bug report
[x] Feature request
[ ] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

Supersedes #14620 with more concrete summary and design.

Current behavior

It's currently not possible to wrap an existing provider in the same injector, which causing:

Class inheritance problems

#19181, #21697, #22011 (comment), ... (class extends ErrorHandler)

#18976 (class extends Location)

In Angular, the constructor is not consider public API due to the DI system, so extending class would be problematic and may require lots of boilerplate (declare deps again).

Customize existing service problems

#22024 (RendererFactory2)

#16184 (DefaultRouteReuseStrategy)

The declared class may not be same to the provided one, the latter may not be exposed to user.

Strategy pattern design problems

#3929 (Renderer)

#11309, #18554 (IterableDiffers)

#18155, #19852 (HttpInterceptor)

Extends behavior of some poor-designed services.

Expected behavior

API Design

The term decorator is not fit to this any more (due to other meanings), currently choose intercept for similar usage to HttpInterceptor. (still open to bikeshed)

An optional intercept property like multi applies to all kinds of providers:

@Injectable()
class CustomErrorHandler implements ErrorHandler {
constructor(private handler: ErrorHandler) {}

handleError(error: any) {
// custom error collecting or reporting
this.handler.handleError(error)
}
}

@NgModule({
providers: [
{ provide: ErrorHandler, useClass: CustomErrorHandler, intercept: true }
]
})
class AppModule { }

Semantics

Ref: AngularJS Decorator API.

By setting intercept: true, it would allowed to inject (previous instance of) the token it providers, so it helps reusing existing logic (no need to rewrite) without modifying the consumer part (in same token).

So only combining with useClass or useFactory makes sense.

{ provide: TOKEN, useExisting: TOKEN, intercept: true } would not be allowed.

{ provide: TOKEN, useValue: VALUE, intercept: true } is technically valid but meaningless, could cause either warning or error.

Edge case: for multi: true provider, interceptor should transform the instances array, namely getting the existing array and returning a new array.

{ provide: TOKEN, useClass: CLASS, multi: true, intercept: true } would not be allowed, as it cannot return an array.

Inconsistent multi value between concrete provider and interceptor in same injector would not be allowed.

Scoping

Any interceptor would belong to a specific Injector thus supporting inheritance. For example:

@Component({
providers: [
{ provide: ErrorHandler, useFactory: func2, deps: [ErrorHandler], intercept: true },
]
})
class MyComponent {
constructor(handler: ErrorHandler) {
// handler -> func2(func1(/*original*/ErrorHandler))
}
}

@NgModule({
providers: [
{ provide: ErrorHandler, useFactory: func1, deps: [ErrorHandler], intercept: true },
]
})
class MyModule {
constructor(handler: ErrorHandler) {
// handler -> func1(/*original*/ErrorHandler)
}
}

Providing the normal instance won't be affected by interceptor in parent Injector. For example:

@Component({
providers: [
{ provide: ErrorHandler, useClass: CustomErrorHandler },
]
})
class MyComponent {
constructor(handler: ErrorHandler) {
// handler -> CustomErrorHandler
}
}

@NgModule({
providers: [
{ provide: ErrorHandler, useFactory: func1, deps: [ErrorHandler], intercept: true },
]
})
class MyModule {
constructor(handler: ErrorHandler) {
// handler -> func1(/*original*/ErrorHandler)
}
}

Should intercepting multi: true providers at each level. For example:

(Without #18894)

@Component({
providers: [
{ provide: ERROR_HANDLERS, useValue: errorHandler3, multi: true },
{ provide: ERROR_HANDLERS, useValue: errorHandler4, multi: true },
{ provide: ERROR_HANDLERS, useFactory: func2, deps: [ErrorHandler], multi: true, intercept: true },
]
})
class MyComponent {
constructor(handler: ErrorHandler) {
// handler -> func2([errorHandler3, errorHandler4])
}
}

@NgModule({
providers: [
{ provide: ERROR_HANDLERS, useValue: errorHandler1, multi: true },
{ provide: ERROR_HANDLERS, useValue: errorHandler2, multi: true },
{ provide: ERROR_HANDLERS, useFactory: func1, deps: [ErrorHandler], multi: true, intercept: true },
]
})
class MyModule {
constructor(handler: ErrorHandler) {
// handler -> func1([errorHandler1, errorHandler2])
}
}

(With #18894)

@Component({
providers: [
{ provide: ERROR_HANDLERS, useValue: errorHandler3, multi: true },
{ provide: ERROR_HANDLERS, useValue: errorHandler4, multi: true },
{ provide: ERROR_HANDLERS, useFactory: func2, deps: [ErrorHandler], multi: true, intercept: true },
]
})
class MyComponent {
constructor(handler: ErrorHandler) {
// handler -> func2([...func1([errorHandler1, errorHandler2]), errorHandler3, errorHandler4])
}
}

@NgModule({
providers: [
{ provide: ERROR_HANDLERS, useValue: errorHandler1, multi: true },
{ provide: ERROR_HANDLERS, useValue: errorHandler2, multi: true },
{ provide: ERROR_HANDLERS, useFactory: func1, deps: [ErrorHandler], multi: true, intercept: true },
]
})
class MyModule {
constructor(handler: ErrorHandler) {
// handler -> func1([errorHandler1, errorHandler2])
}
}

Ordering

All interceptors are considered multi provider and applied last (local to Injector). For example:

@NgModule({
providers: [
{ provide: ErrorHandler, useClass: CustomErrorHandler1 },
{ provide: ErrorHandler, useClass: CustomErrorHandler2, intercept: true },
{ provide: ErrorHandler, useClass: CustomErrorHandler3 },
{ provide: ErrorHandler, useClass: CustomErrorHandler4, intercept: true },
]
})
class AppModule {
constructor(handler: ErrorHandler) {
// handler -> new CustomErrorHandler4(new CustomErrorHandler2(new CustomErrorHandler3(/* ... */)))
}
}

Examples

Reduce class inheritance:

@Injectable()
class CustomErrorHandler implements ErrorHandler {
constructor(private handler: ErrorHandler) {}

handleError(error: any) {
// custom error collecting or reporting
this.handler.handleError(error)
}
}

@NgModule({
providers: [
{ provide: ErrorHandler, useClass: CustomErrorHandler, intercept: true }
]
})
class AppModule { }

Protect private APIs:

@NgModule({
providers: [
{ provide: RendererFactory2, useClass: CustomRendererFactory2, intercept: true }
]
})
class AppModule { }

Applying new strategy:

Actually this is not the best way to solve that problem, exposing DifferFactory as a multi provider would be better, but this could work without changing existing APIs.

@Injectable()
class CustomIterableDiffers implements IterableDiffers {
constructor(private differs: IterableDiffers) {}

get factories(): IterableDifferFactory[] { return this.differs.factories }

find(iterable: any): IterableDifferFactory {
if (iterable[SOME_SPECIAL_TOKEN]) {
return superPerformantDifferFactory
}
return this.differs.find(iterable)
}
}

@NgModule({
providers: [
{ provide: IterableDiffers, useClass: CustomIterableDiffers, intercept: true }
]
})
class AppModule { }

Custom control over existing strategy:

const disabledInterceptors = new WeakMap<HttpRequest, Set<Type<HttpInterceptor>>>()

@NgModule({
providers: [
{ provide: HTTP_INTERCEPTORS, useFactory: enableFiltering, multi: true, intercept: true },
InterceptorControlService,
]
})
class AppModule { }

function enableFiltering(interceptors: HttpInterceptor) {
return interceptors.map(interceptor => ({
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (disabledInterceptors.has(req) && disabledInterceptors.get(req).has(interceptor.constructor)) {
return next.handle(req)
}
return next.handle(interceptor.intercept(req, next))
}
}))
}

@Injectable()
class InterceptorControlService {
disable(request: HttpRequest, interceptorType: Type<HttpInterceptor>) {
if (!disabledInterceptors.has(request)) {
disabledInterceptors.set(request, new Set())
}
disabledInterceptors.get(request).add(interceptorType)
}
}

@Component({ /*...*/ })
class MyComponent {
constructor(http: HttpClient, interceptorControl: InterceptorControlService) {
const request = http.post(/*...*/)
interceptorControl.disable(request, SomeCustomInterceptor)
request.subscribe(/*...*/)
}
}

Implementation

Each Injector should hold a internal slot of interceptors, a naive logical demo could be:

// Didn't consider `multi: true` yet
class Injector_ {
private interceptedInjectables = new Map</*token*/any, /*instance*/any>()
private providerInterceptors = new Map</*token*/any, /*interceptors*/Provider[]>()

hasInterceptors(token: any) {
return this.providerInterceptors.has(token) || (!this.hasLocalProvider(token) && this.parent.hasInterceptors(token))
}

getInterceptors(token: any) {
return [...(this.hasLocalProvider(token) ? [] : this.parent.getInterceptors(token)), this.providerInterceptors.get(token)]
}

get(token: any) {
if (this.interceptedInjectables.has(token)) {
return this.interceptedInjectables.get(token)
}
if (this.hasInterceptors(token)) {
const raw = getRawProviderSomehow(token)
const interceptors = this.getInterceptors(token)
const res = interceptors.reduce(
(previousInstance, currentProvider) => instantiateSomehow(currentProvider, previousInstance), raw)
this.interceptedInjectables.set(token, res)
return res
}
// original logic
}
}

For performance consideration (static ones):

To get rid of mapping, the interceptors slot should be directly added to providerDef instance via Compiler;

If an injector has only provider interceptor but not concrete provider, use a providerDef with value of special symbol NO_VALUE to ship interceptors;

Tree-shakable consideration (static ones):

Extend @Inject() decorator to accept an additional optional parameter:

@Component({ /*...*/ })
class MyComponent {
constructor(
@Inject(HttpClient, logRequest) http: HttpClient
) {}
}

// makeParamDecorator('Inject', (token: any, ...interceptors: any[]) => ({token, interceptors}))

Or introduce a new decorator:

@Component({ /*...*/ })
class MyComponent {
constructor(
@Intercept(logRequest) http: HttpClient
) {}
}

// makeParamDecorator('Intercept', (...interceptors: any[]) => ({interceptors}))

Child injectors should not be affected in this way.

Minimal reproduction of the problem with instructions

Could go to #14620 for repro of current behavior.

What is the motivation / use case for changing the behavior?

Provide a demo of how to raise concrete feature request

Make the DI mechanism more powerful and useful.

Environment

Angular version: X.Y.Z

Browser:
- [ ] Chrome (desktop) version XX
- [ ] Chrome (Android) version XX
- [ ] Chrome (iOS) version XX
- [ ] Firefox version XX
- [ ] Safari (desktop) version XX
- [ ] Safari (iOS) version XX
- [ ] IE version XX
- [ ] Edge version XX

For Tooling issues:
- Node version: XX
- Platform:

Others: