ServiceScope class

The service locator pattern used by the SharePoint Framework.

Remarks

ServiceScope provides a formalized way for components to register and consume dependencies ("services"), and to enable different implementations to be registered in different scopes. This improves modularity by decoupling components from their dependencies in an extensible way.

For example, suppose that various components need access to an IPageManager instance. We could simply make the PageManager a singleton (i.e. global variable), but this will not work e.g. if we need to create a pop-up dialog that requires a second PageManager instance. A better solution would be to add the PageManager as a constructor parameter for each component that requires it, however then we immediately face the problem that any code that calls these constructors also needs a PageManager parameter. In an application with many such dependencies, business logic that ties together many subsystems would eventually pick up a constructor parameter for every possible dependency, which is awkward. A natural solution would be to move all the dependencies into a class with name like "ApplicationContext", and then pass this around as our constructor parameter. This enables the PageManager to be passed to classes that need it without cluttering the intermediary classes that don't. However, it still has a design problem that "ApplicationContext" has hard-coded dependencies on many unrelated things. A more flexible approach is to make it a dictionary that can look up items for consumers/providers who know the right lookup key (i.e. ServiceKey). This is the popular "service locator" design pattern, familiar from the SPContext API in classic SharePoint.

ServiceScope takes this idea a step further in two important ways: First, it provides a scoping mechanism so that e.g. if we have two different pages, they can each provide a unique PageManager instance while still sharing other common dependencies. Secondly, it allows for a ServiceKey to provide a default implementation of the dependency. This is important for API stability in our modular client-side environment: For example, suppose that version 2.0 of our application introduced a new IDiagnosticTracing interface that a version 2.0 component will expect to consume. If the version 2.0 component gets loaded by an older 1.0 application, it would fail. We could fix this by requiring each consumer to check for any missing dependencies and handle that case, but it would require a lot of checks. A better solution is to ensure that a default implementation always exists, perhaps just a trivial behavior, so that components can assume that consume() will always return some object that implements the contract.

Usage: ServiceScope instances are created by calling either ServiceScope.startNewRoot() or ServiceScope.startNewChild(). They are initially in an "unfinished" state, during which provide() can be called to register service keys, but consume() is disallowed. After ServiceScope.finish() is called, consume() is allowed and provide() is now disallowed. These semantics ensure that ServiceScope.consume() always returns the same result for the same key, and does not depend on order of initialization. It also allows us to support circular dependencies without worrying about infinite loops. (Circular dependencies are best avoided, however this is difficult to guarantee when working with components that were contributed by various third parties without any coordination.) To avoid mistakes, it's best to always call consume() inside a callback from serviceScope.whenFinished().

Constructors

(constructor)(parent)

Constructs a new instance of the ServiceScope class

Methods

consume(serviceKey)

Consumes a service from the service scope.

createAndProvide(serviceKey, simpleServiceClass)

This is a shorthand function that is equivalent to constructing a new instance of the simpleServiceClass, then registering it by calling ServiceScope.provide().

createDefaultAndProvide(serviceKey)

This is a shorthand function that constructs the default implementation of the specified serviceKey, and then registers it by calling ServiceScope.provide().

finish()

Completes the initialization sequence for a service scope.

getParent()

Returns the parent of the current ServiceScope, or undefined if this is a root scope.

provide(serviceKey, service)

Add a new service to a service scope.

startNewChild()

Constructs a new ServiceScope that is a child of the current scope.

startNewRoot()

Create a new root-level ServiceScope. Only root-level scopes have the ability to autocreate default implementations of ServiceKeys.

whenFinished(callback)

Defer an operation until after ServiceScope.finish() has completed.

Constructor Details

(constructor)(parent)

Constructs a new instance of the ServiceScope class

protected constructor(parent: ServiceScope | undefined);

Parameters

parent

ServiceScope | undefined

Method Details

consume(serviceKey)

Consumes a service from the service scope.

consume<T>(serviceKey: ServiceKey<T>): T;

Parameters

serviceKey

ServiceKey<T>

the key that was used when provide() was called to register the service

Returns

T

the service instance

Remarks

Components should call this function to "consume" a dependency, i.e. look up the serviceKey and return the registered service instance. If the instance cannot be found, then a default instance will be automatically created and registered with the root ServiceScope.

createAndProvide(serviceKey, simpleServiceClass)

This is a shorthand function that is equivalent to constructing a new instance of the simpleServiceClass, then registering it by calling ServiceScope.provide().

createAndProvide<T>(serviceKey: ServiceKey<T>, simpleServiceClass: {
        new (serviceScope: ServiceScope): T;
    }): T;

Parameters

serviceKey

ServiceKey<T>

the key that can be used later to consume the service

simpleServiceClass

{ new (serviceScope: ServiceScope): T; }

the TypeScript class to be constructed

Returns

T

a newly constructed instance of simpleServiceClass

createDefaultAndProvide(serviceKey)

This is a shorthand function that constructs the default implementation of the specified serviceKey, and then registers it by calling ServiceScope.provide().

createDefaultAndProvide<T>(serviceKey: ServiceKey<T>): T;

Parameters

serviceKey

ServiceKey<T>

the key that can be used later to consume the service

Returns

T

a service instance that was constructed using ServiceKey.defaultCreator

finish()

Completes the initialization sequence for a service scope.

finish(): void;

Returns

void

Remarks

When a ServiceScope is first started, it is in an "unfinished" state where provide() is allowed but consume() is disallowed. After calling finish(), then consume() is allowed but provide() is disallowed.

This formalism prevents a number of complex situations that could lead to bugs. For example, supposed that Scope2 is a child of Scope1, and Scope1 provides instance A1 of interface A. If someone consumes A1 from Scope2 (via inheritance) before Scope2.provide() is called with A2, then a subsequent call to Scope2.consume() might return a different result than the previous call. This nondeterminism could cause unpredictable results that are difficult to diagnose.

getParent()

Returns the parent of the current ServiceScope, or undefined if this is a root scope.

getParent(): ServiceScope | undefined;

Returns

ServiceScope | undefined

the parent service scope

provide(serviceKey, service)

Add a new service to a service scope.

provide<T>(serviceKey: ServiceKey<T>, service: T): T;

Parameters

serviceKey

ServiceKey<T>

the key that will later be used to consume the service

service

T

the service instance that is being registered

Returns

T

the same object that was passed as the "service" parameter

Remarks

ServiceScope.provide() is used to register an implementation of the given serviceKey for the current scope, and an implementation of a given rootServiceKey for the root scope. It may only be used when the ServiceScope is in an "unfinished" state, i.e. before finish() has been called.

startNewChild()

Constructs a new ServiceScope that is a child of the current scope.

startNewChild(): ServiceScope;

Returns

the newly created root ServiceScope

Remarks

The service scopes form a tree structure, such that when consuming a service, if the key is not explicitly provided by a child scope, the parent hierarchy will be consulted.

startNewRoot()

Create a new root-level ServiceScope. Only root-level scopes have the ability to autocreate default implementations of ServiceKeys.

static startNewRoot(): ServiceScope;

Returns

the newly created root ServiceScope

whenFinished(callback)

Defer an operation until after ServiceScope.finish() has completed.

whenFinished(callback: () => void): void;

Parameters

callback

() => void

A block of code that needs to call ServiceScope.consume()

Returns

void

Remarks

It is an error to call ServiceScope.consume() before finish() has been called. The most reliable way to protect your component against this error is to perform the consume() calls inside a whenFinished() callback. If the service scope is already finished, then the callback will be executed immediately; otherwise, it will be executed later when the scope is finished.

NOTE: This is not an asynchronous callback. ServiceScope initialization is typically inexpensive and short lived. However, the control flow often threads through numerous constructors and base classes, which can be simplified using whenFinished().