Hi SharePoint Devs,
Today, I am going to discuss a small Proof of Concept I worked today that might be interesting to write SPFx code: Easy and elegant Dependency Injection !
Services in SharePoint Framework
SPFx has built-in artifacts to create services and consume them. It is a class named ServiceScope. It is a container in which you can register service instances. From your consumer code, you call its consume() method to get an instance of the service you need.
Register a custom service
To register a service of our own, we need to declare 3 things:
- The service contract (a TypeScript interface)
- The service default implementation
- The ServiceKey to identify the service
In SPFx, services must always have a default implementation ! Let's create a Greetings Service that will return a message
import { ServiceKey } from "@microsoft/sp-core-library";
export interface IGreetingsService {
sayHello(who: string) : string;
}
export class GreetingsService implements IGreetingsService {
public sayHello(who: string) : string {
return `Hello ${who}`;
}
}
export const GreetingsServiceKey = ServiceKey.create<IGreetingsService>( 'YPCODE:GreetingsService', GreetingsService );
From a consumer code, we can call the consume() method of the ServiceScope instance to be able to consume the appropriate service. Obiously, the consumer code must have a reference to that ServiceScope instance
import {GreetingsServiceKey} from "../services/GreetingsService";
// ...
public Greet() {
let service = this.serviceScope.consume(GreetingsServiceKey);
let message = service.sayHello("Yannick");
console.log(message);
}
// ...
Default and child Scopes
There is a default instance of the ServiceScope that we might consider as the Root Service Scope. We can also create child scopes, on which we can override the instances of some services. It is particularly useful when we need to have specific implementations according to environment (Unit testing, Workbench, Prod, ...) or specific configurations (e.g. WebPart or Extensions properties).
Typically, the configuration of child scope is done at the startup of the application, in the onInit() method of the WebPart or Extension. This method returns a promise, until this promise is resolved, a loading screen will be displayed and then the render() method is called.
The ServiceScope has a whenFinished() method that calls a specified callback when the scope configuration is finished. We can use this method in conjunction with the onInit() to make sure the services are properly configured before starting the rendering.
public onInit(): Promise<any> {
return (
super
.onInit()
// Set the global configuration of the application
// This is where we will define the proper services according to the context (Local, Test, Prod,...)
// or according to specific settings
.then(() => {
let scope = this.context.serviceScope;
return new Promise((resolve, reject) => {
if (this.properties.config) {
switch (this.properties.config) {
case 'config2':
let childScope = scope.startNewChild();
childScope.createAndProvide(GreetingsServiceKey, AltGreetingsService);
childScope.finish();
usedScope = childScope;
break;
case 'config1':
default:
break;
}
}
scope.whenFinished(() => {
resolve();
});
});
})
); }
The ServiceScope reference
Something that might happen to be pretty annoying is that we need to pass a reference to the ServiceScope in any consumer code. A easier way would be to keep that reference in a Singleton instance accessible from anywhere. Let's create a DependenciesManager class and a Single exported instance
export class DependenciesManager {
private serviceScope: ServiceScope;
public configure(
rootServiceScope: ServiceScope,
serviceScopeConfiguration: (rootServiceScope: ServiceScope) => Promise<ServiceScope>
): Promise<any> {
return new Promise((resolve, reject) => {
serviceScopeConfiguration(rootServiceScope)
.then((usedScope) => {
this.serviceScope = usedScope;
resolve();
})
.catch((error) => {
reject(error);
});
});
}
public inject<TService>(serviceKey: ServiceKey<TService>): TService {
if (this.serviceScope) {
return this.serviceScope.consume(serviceKey);
} else {
return null;
}
}
}
const Dependencies: DependenciesManager = new DependenciesManager();
export default Dependencies;
And the consumer code
import Dependencies from "../di/DependencyManager";
//...
// We can now avoid to pass the ServiceScope instance reference
let service = Dependencies.inject(GreetingsServiceKey);
service.sayHello("Yannick");
// ...
A touch of Elegance
So far, we ease up the problem of the ServiceScope reference to pass in to each consumer code. Inspired by serveral Frameworks and techniques I've been using, I would like to simply declare the services I need and use them seamlessly. In TypeScript, we can use decorators to annotate some code and execute some logic in a declarative way. The idea was, when I annotate a field of a class with @inject() decorator, it calls the inject method of the DependenciesManager. Some tricky part with the SharePoint Framework is that decorators will be evaluated before the ServiceScope is actually ready and available. I then need to go the other way around. The decorators will add the references they are applied to in an array. In The Dependencies Manager, when the ServiceScope is configured, all the references will be assigned with the appropriate service instance. An elegant dependency injection might look like
import * as React from 'react';
import styles from './HelloWorld.module.scss';
import { IHelloWorldProps } from './IHelloWorldProps';
import { IGreetingsService, GreetingsServiceKey } from '../../../services/GreetingsService';
import { inject } from '../../../di/DependenciesManager';
export default class HelloWorld extends React.Component<IHelloWorldProps, {}> {
// We declare a field that will be injected with the GreetingsService registered instance
@inject(GreetingsServiceKey) private greetingsService: IGreetingsService;
public render(): React.ReactElement<IHelloWorldProps> {
let hello = this.greetingsService.sayHello('Yannick');
return (
<div className={styles.helloWorld}>
<div className={styles.container}>
<div className={styles.row}>
<div className={styles.column}>
<span className={styles.title}>Welcome to Dependency Injection!</span>
<p>{this.props.description}</p>
<p className={styles.subTitle}>
Result from service: {hello}.
</p>
</div>
</div>
</div>
</div>
);
}
}
Isn't it elegant code ? All the code of this PoC could be found here. Please give your feedback and thoughts about it !
Cheers,
Yannick