Components

Components and their interfaces

The Pip.Services toolkit is based on components. The component definition is very flexible. It allows users to create components from scratch, convert existing pieces of code into a component or choose from a large collection of prebuilt components. In the toolkit, any class (or struct in non-OOP languages) can be a component. Additional capabilities can be added via a few standard interfaces that enable specific states in the component lifecycle.

figure 1

The component interfaces are optional, and can be used in any combination. They are defined in the commons module:

  • The IConfigurable interface with the configure method allows passing component configuration parameters. The configurations defined in the ConfigParams object may come from different sources and can be defined during design, runtime or deployment time. Typically components are configured once, right after creation. IReconfigurable interface signifies that components can receive and process configurations more than once.

  • The IReferenceable interface sets component dependencies. It represents the locator pattern, then dependencies are retrieved from an IReferences object passed to the component via the setReferences method using a special locator. Locators can be any values, but the PipServices toolkit most often uses Descriptors, which allow matching dependencies using 5 fields: logical group, logical type, implementation type (kind), unique object name and implementation version. The IUnreferenceable interface notifies components via the unsetReferences method to release dependencies before the component is destroyed.

  • The IOpenable interface allows components to establish connections, start active threads, or do other things when they are open to prepare for handling incoming calls. On close, the collections are released and resources are freed. The IClosable interface is a subset of IOpenable with only the close method in it.

  • The IExecutable interface allows components to process incoming calls by implementing an execute method. And the INotifiable interface receives asynchronous notifications via the notify method.

  • The ICleanable method is used to clear a component’s state. That can be handy in situations like resetting components in automated tests.

A component that implements all standard interfaces looks the following way:

import { 
    Parameters, ConfigParams, 
    Descriptor, ICleanable, IConfigurable, 
    IExecutable, INotifiable, IOpenable, 
    IReferenceable, IReferences, 
    IUnreferenceable 
} from "pip-services3-commons-nodex";

class MyComponent implements IConfigurable, IReferenceable, IUnreferenceable, IOpenable, IExecutable, INotifiable, ICleanable {
    public constructor() { /* Initialize the component */ }
    public configure(config: ConfigParams) { /* configure the component */ }
    public setReferences(refs: IReferences) { /* set component dependencies */ }
    public unsetReferences() { /* unset component references */ }
    public isOpen(): boolean { /* return the component open state */ }
    public open(correlationId: string): Promise<void> { /* open the component */ }
    public close(correlationId: string): Promise<void> { /* close the component */ }
    public execute(correlationId: string, args: Parameters): Promise<any> { /* execute the component transaction */ }
    public notify(correlationId: string, args: Parameters) { /* notify the component about events */ }
    public clear(correlationId: string): Promise<void> { /* clear the component state */ }
}

using PipServices3.Commons.Config;
using PipServices3.Commons.Refer;
using PipServices3.Commons.Run;
using System.Threading.Tasks;

public class MyComponent: IConfigurable, IReferenceable, IUnreferenceable, IOpenable, IExecutable, INotifiable, 
ICleanable
{
    public MyComponent() { /* Initialize the component */ }
    public void Configure(ConfigParams config) { /* configure the component */ }
    public void SetReferences(IReferences references) { /* set component dependencies */ }
    public void UnsetReferences() { /* unset component references */ }
    public bool IsOpen() { /* return the component open state */ }
    public Task OpenAsync(string correlationId) { /* open the component */ }
    public Task CloseAsync(string correlationId) { /* close the component */ }
    public Task<object> ExecuteAsync(string correlationId, Parameters args) { /* execute the component 
transaction */ }
    public Task NotifyAsync(string correlationId, Parameters args) { /* notify the component about events*/ }
    public Task ClearAsync(string correlationId) { /* clear the component state */ }
}

import (
	cconf "github.com/pip-services3-go/pip-services3-commons-go/config"
	cref "github.com/pip-services3-go/pip-services3-commons-go/refer"
	crun "github.com/pip-services3-go/pip-services3-commons-go/run"

	// ------------
	cproc "github.com/pip-services3-go/pip-services3-container-go/container"
	gbuild "github.com/pip-services3-go/pip-services3-grpc-go/build"
	rbuild "github.com/pip-services3-go/pip-services3-rpc-go/build"
	factory "github.com/pip-templates/pip-templates-microservice-go/build"
)

// Implements IConfigurable, IReferenceable, IUnreferenceable, IOpenable, IExecutable, INotifiable, ICleanable
type MyComponent struct{}

func (c *MyComponent) NewMyComponent() { /* Initialize the component */ }

func (c *MyComponent) Configure(config *cconf.ConfigParams) { /* configure the component */ }

func (c *MyComponent) SetReferences(refs cref.IReferences) { /* set component dependencies */ }

func (c *MyComponent) UnsetReferences() { /* unset component references */ }

func (c *MyComponent) IsOpen() bool { /* return the component open state */ }

func (c *MyComponent) Open(correlationId string) error { /* open the component */ }

func (c *MyComponent) Close(correlationId string) error { /* close the component */ }

func (c *MyComponent) Execute(correlationId string, args *crun.Parameters) error { /* execute the component transaction */
}

func (c *MyComponent) Notify(correlationId string, args *crun.Parameters) { /* notify the component about events */
}

func (c *MyComponent) Clear(correlationId string) error { /* clear the component state */ }


import 'package:pip_services3_commons/pip_services3_commons.dart';

class MyComponent
    implements
        IConfigurable,
        IReferenceable,
        IUnreferenceable,
        IOpenable,
        IExecutable,
        INotifiable,
        ICleanable {

  MyComponent() {/* Initialize the component */}
  @override
  void configure(ConfigParams config) {/* configure the component */}

  @override
  void setReferences(IReferences references) {/* set component dependencies */}

  @override
  void unsetReferences() {/* unset component references */}

  @override
  bool isOpen() {/* return the component open state */}

  @override
  Future open(String? correlationId) {/* open the component */}

  @override
  Future close(String? correlationId) {/* close the component */}

  @override
  Future execute(String? correlationId, Parameters args) { /* execute the component transaction */ }

  @override
  void notify(String? correlationId, Parameters args) { /* notify the component about events */ }

  @override
  Future clear(String? correlationId) {/* clear the component state */}
}


from typing import Optional

from pip_services3_commons.config import IConfigurable, ConfigParams
from pip_services3_commons.refer import IReferenceable, IUnreferenceable, IReferences
from pip_services3_commons.run import IOpenable, IExecutable, INotifiable, ICleanable, Parameters


class MyComponent(IConfigurable, IReferenceable, IUnreferenceable, IOpenable, IExecutable, INotifiable, ICleanable):

    def __init__(self):
        """Initialize the component"""
    def configure(self, config: ConfigParams):
        """configure the component"""

    def set_references(self, references: IReferences):
        """set component dependencies"""

    def unset_references(self):
        """unset component references"""

    def is_open(self) -> bool:
        """return the component open state"""

    def open(self, correlation_id: Optional[str]):
        """open the component"""

    def close(self, correlation_id: Optional[str]):
        """close the component"""

    def execute(self, correlation_id: Optional[str], args: Parameters):
        """execute the component transaction"""

    def notify(self, correlation_id: Optional[str], args: Parameters):
        """notify the component about events"""

    def clear(self, correlation_id: Optional[str]):
        """clear the component state"""

Not available

Containers and their configurations

Components can be created, referenced and opened manually. That is usually done in unit tests. However, the most power and flexibility comes when components can be created and managed by inversion of control containers.

The most basic container can be instantiated in-process. On top of it, the Pip.Services toolkit creates a variety of other containers, that allow to assemble microservices from components and deploy them on different platforms. These are:

  • ProcessContainer: used to run microservices as system processes or package them into Docker containers.
  • LambdaFunction: used to deploy microservices as AWS Lambda.
  • AzureFunction: used to deploy microservices as Azure Functions.
  • CloudFunction: used to deploy microservices as Google Cloud Functions.
  • ServiceFabricService: used to deploy microservices as Service Fabric services on Azure cloud
  • ServiceFabricActor: used to deploy microservices as Service Fabric actors on Azure cloud
  • Servlet: used to deploy microservices in J2EE containers

Containers allow great flexibility to developers since they can repackage their microservices and run on drastically different platforms like Docker or Serverless reusing over 90% of their code. That also makes their code more future-proof, as they will be able to support the latest and greatest deployment platforms that may emerge in the future with just a few lines of code.

Components are driven by configurations that can be stored in JSON or YAML files. The Mustache templating language allows to inject deployment-time configuration parameters and change the composition of microservices by using command-line arguments and environment variables set during deployment time. This feature allows for the creation of microservices that can adjust themselves depending on the deployment configuration without changing and rebuilding the code. The example below shows how to configure several commonly used components.

# Container descriptor
- descriptor: "pip-services:context-info:default:default:1.0"
  name: "pip-service-data"
  description: "Entities data microservice"

# Console logger
- descriptor: "pip-services:logger:console:default:1.0"
  level: "trace"

# Performance log counters
- descriptor: "pip-services:counters:log:default:1.0"

{{#if MONGO_ENABLED}}
# MongoDb persistence
- descriptor: "pip-service-data:persistence:mongodb:default:1.0"
  connection:
    uri: {{MONGO_SERVICE_URI}}
    host: {{MONGO_SERVICE_HOST}}{{#unless MONGO_SERVICE_HOST}}"localhost"{{/unless}}
    port: {{MONGO_SERVICE_PORT}}{{#unless MONGO_SERVICE_PORT}}27017{{/unless}}
    database: {{MONGO_DB}}{{#unless MONGO_DB}}"test"{{/unless}}
  credential:
    username: {{MONGO_USER}}
    password: {{MONGO_PASS}}
{{/if}}

{{#unless MONGO_ENABLED}}
# Default to in-memory persistence, if nothing is set
- descriptor: "pip-service-data:persistence:memory:default:1.0"
{{/unless}}

# Controller
- descriptor: "pip-service-data:controller:default:default:1.0"

{{#if HTTP_ENABLED}}
# Common HTTP endpoint
- descriptor: "pip-services:endpoint:http:default:1.0"
  connection:
    protocol: http
    host: 0.0.0.0
    port: {{HTTP_PORT}}{{#unless HTTP_PORT}}8080{{/unless}}

# HTTP service version 1.0
- descriptor: "pip-service-data:service:http:default:1.0"
  swagger:
    enable: true

# Swagger service
- descriptor: "pip-services:swagger-service:http:default:1.0"
{{/if}}

{{#if GRPC_ENABLED}}
# Common GRPC endpoint
- descriptor: "pip-services:endpoint:grpc:default:1.0"
  connection:
    protocol: http
    host: 0.0.0.0
    port: {{GRPC_PORT}}{{#unless GRPC_PORT}}8090{{/unless}}

# GRPC service version 1.0
- descriptor: "pip-service-data:service:grpc:default:1.0"
{{/if}}

Component factories

To help containers instantiate components using their locators (descriptors) defined in the configuration files, the Pip.Services toolkit provides the IComponentFactory abstraction that has to be implemented by microservice developers. Standard components provide their corresponding factories that just get to be added to the microservice containers to enable new functionality.

import { ProcessContainer } from "pip-services3-container-nodex";
import { DefaultGrpcFactory } from "pip-services3-grpc-nodex";
import { DefaultRpcFactory } from "pip-services3-rpc-nodex";
import { DefaultSwaggerFactory } from "pip-services3-swagger-nodex";


class MyProcess extends ProcessContainer {
    public constructor() {
        super('mymicroservice', 'Sample microservice container');

        this._factories.add(new MyComponentFactory());
        this._factories.add(new DefaultRpcFactory());
        this._factories.add(new DefaultSwaggerFactory());
        this._factories.add(new DefaultGrpcFactory());
    }
}

using PipServices3.Container;
using PipServices3.Grpc.Build;
using PipServices3.Rpc.Build;
using PipServices3.Swagger.Build;


public class MyProcess : ProcessContainer
{
    public MyProcess() : base("mymicroservice", "Sample microservice container")
    {
        this._factories.Add(new MyComponentFactory());
        this._factories.Add(new DefaultRpcFactory());
        this._factories.Add(new DefaultSwaggerFactory());
        this._factories.Add(new DefaultGrpcFactory());
    }
}

type MyProcess struct {
	cproc.ProcessContainer
}

func NewMyProcess() *MyProcess {
	c := MyProcess{}
	c.ProcessContainer = *cproc.NewProcessContainer("mymicroservice", "Sample microservice container")
	c.AddFactory(factory.NewMyComponentFactory())
	c.AddFactory(rbuild.NewDefaultRpcFactory())
	c.AddFactory(rbuild.NewDefaultSwaggerFactory())
	c.AddFactory(gbuild.NewDefaultGrpcFactory())
	return &c
}

import 'package:pip_services3_container/pip_services3_container.dart';
import 'package:pip_services3_grpc/pip_services3_grpc.dart';
import 'package:pip_services3_rpc/pip_services3_rpc.dart';
import 'package:pip_services3_rpc/pip_services3_swagger.dart';

class MyProcess extends ProcessContainer {
  MyProcess() : super('mymicroservice', 'Sample microservice container') {
    factories.add(MyComponentFactory());
    factories.add(DefaultRpcFactory());
    factories.add(DefaultSwaggerFactory());
    factories.add(DefaultGrpcFactory());
  }
}


from pip_services3_container import ProcessContainer
from pip_services3_grpc.build.DefaultGrpcFactory import DefaultGrpcFactory
from pip_services3_rpc.build import DefaultRpcFactory
from pip_services3_swagger.build.DefaultSwaggerFactory import DefaultSwaggerFactory


class MyProcess(ProcessContainer):
    def __init__(self):
        super().__init__("mymicroservice", "Sample microservice container")

        self._factories.add(MyComponentFactory())
        self._factories.add(DefaultRpcFactory())
        self._factories.add(DefaultSwaggerFactory())
        self._factories.add(DefaultGrpcFactory())


Not available

Microservice componentized design

Components represent the most basic building block in the Pip.Services toolkit that allows assembling microservices from loosely coupled components mixed together with out-of-the-box components from the toolkit. The typical composition of a microservice is presented in the diagram below. It may contain components to persist data, implement business logic, expose functionality as external interfaces and address various cross-cutting concerns like logging, monitoring, health management, and others.

figure 2

References

For more information on components and containers see: