@arekushii/ts-aspect
This is a fork of a original repo - https://github.com/engelmi/ts-aspect
A simplistic library for Aspect Oriented Programming (AOP) in TypeScript. Aspects can be injected on pointcuts via regular expressions for a class or object.
One application of AOP is the encapsulation of cross-cutting concerns, like logging, and keep the original business logic clean. Although a powerful tool, it should be used with care as it hides logic and complexity.
Installation
To get started, install @arekushii/ts-aspect
with npm.
npm i @arekushii/ts-aspect
Usage
An aspect can be injected to the target
class instance or object via
function addAspect(target: any, methodName: string, advice: Advice, aspect: Aspect, params: any): void
or
function addAspectToPointcut(target: any, pointcut: string, advice: Advice, aspect: Aspect, params: any): void
The aspect
parameter is the actual behavior that extends the target
code. When the aspect
is about to be executed is defined by the advice
parameter. Currently, the following advices are available:
- Before
- After
- Around
- AfterReturn
- TryCatch
- TryFinally
For example, the AfterReturn enables you to access the return value of the original function and execute additional logic on it (like logging).
Finally, the pointcut
parameter describes the where - so basically for which functions the aspect
should be executed. For this a regular expression can be used.
If an aspect
is called, it creates a new context. The context itself is defined as
export interface AspectContext = {
target: any; // injected object
methodName: string; // the name of the injected function
functionParams: any[]; // parameters passed to the call of the injected function
params?: any // parameters passed in using the aspect
advice: Advice // advice used in aspect
returnValue: any; // only set for the AfterReturn-Aspect
error: any; // only set for the TryCatch-Aspect when an error is thrown
};
Aspects await the execution of asynchronous functions - and are asynchronous themselves in this case. This enables an aspect for the advices Advice.After
, Advice.AfterReturn
and at the second execution of Advice.Around
to work with the resolved return value of the injected function.
Also, ts-aspect
provides a method decorator to attach an aspect to a all instances of a class in a declarative manner:
function UseAspect(
advice: Advice,
aspect: Aspect | (new () => Aspect),
params: any
): MethodDecorator
Example
Assume the following aspect class which simply logs the current aspect context passed to it to the console:
class LogAspect implements Aspect {
function execute(ctx: AspectContext): void {
console.log(ctx);
}
};
Also, we create the following Calculator
class:
class Calculator {
public add(a: number, b: number) {
return a + b;
}
public subtract(a: number, b: number) {
return a - b;
}
public divide(a: number, b: number) {
if(b === 0){
throw new Error('Division by zero!');
}
return a / b;
}
public multiply(a: number, b: number) {
return a * b;
}
};
Now the logArgsAspect
can be injected to an instance of Calculator
. In the following example, the aspect is supposed to be executed before running the actual business logic:
const calculator = new Calculator();
addAspectToPointcut(calculator, '.*', Advice.Before, new LogAspect());
By defining the pointcut
as '.*'
, the aspect
is executed when calling any of the functions of the respective Calculator
instance. Therefore, a call to
calculator.add(1, 2);
should output
{
target: Calculator {},
methodName: 'add',
advice: Advice.Before,
functionParams: [1, 2],
params: null,
returnValue: null,
error: null
}
And further calls to other functions like
calculator.subtract(1, 2);
calculator.divide(1, 2);
calculator.multiply(1, 2);
should result in the same output (except for the changing methodName
).
An aspect can also be applied in case an exception occurs in the target code:
const calculator = new Calculator();
addAspect(calculator, 'divide', Advice.TryCatch, new LogAspect());
calculator.divide(1, 0);
In this case, the divide
function throws the division by zero exception. Due to Advice.TryCatch
the error is being caught and control is handed over to the aspect, which logs the error as well as both input parameters of the divide function call.
Note:
Because the aspect does not rethrow the exception implicitly, the handling will stop here. Rethrowing the error in the aspect is necessary if it is supposed to be handled elsewhere.
UseAspect
In addition, aspects can be added to a all class instances in a declarative manner by using the decorator UseAspect
. Based on the Calculator example above, lets add another LogAspect to the add
method so that the result gets logged to the console as well:
class Calculator {
@UseAspect(Advice.AfterReturn, LogAspect)
public add(a: number, b: number) {
return a + b;
}
// ...
}
const calculator = new Calculator();
calculator.add(1300, 37);
The aspect passed to the decorator can be either a class which provides a constructor with no arguments or an instance of an aspect.
Parameters
You can pass additional parameters when using an aspect.
class ServiceExample {
@UseAspect(
Advice.AfterReturn,
CheckNullReturnAspect,
{ exception: new MyException() }
)
public getSomething() {
return null;
}
// ...
}
So in Aspect you can recover this parameter.
class CheckNullReturnAspect implements Aspect {
execute(ctx: AspectContext): any {
const exception = ctx.params.exception;
const value = ctx.returnValue;
if (!value) {
throw exception;
}
}
}
Aspect with Before Advice
You can update the value of parameters passed in the injected function.
class ServiceExample {
getRequest(route: string) {
//...
}
}
In this example we can handle the parameter (route: string)
and return it with the updated value.
export class RouteProcessingAspect implements Aspect {
execute(ctx: AspectContext): any {
const route: string = ctx.functionParams.shift();
return [route.replace(/[&\/\\#,+()$~%.'":*?<>{}]/g, '')];
}
}
In the getRequest
method, the route
parameter will be handled before the method call, without the need for the getRequest
method itself to do so.