Faactory.Channels.Abstractions 0.1.0

Suggested Alternatives

Faactory.Channels.Core

There is a newer version of this package available.
See the version list below for details.
dotnet add package Faactory.Channels.Abstractions --version 0.1.0
NuGet\Install-Package Faactory.Channels.Abstractions -Version 0.1.0
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Faactory.Channels.Abstractions" Version="0.1.0" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Faactory.Channels.Abstractions --version 0.1.0
#r "nuget: Faactory.Channels.Abstractions, 0.1.0"
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install Faactory.Channels.Abstractions as a Cake Addin
#addin nuget:?package=Faactory.Channels.Abstractions&version=0.1.0

// Install Faactory.Channels.Abstractions as a Cake Tool
#tool nuget:?package=Faactory.Channels.Abstractions&version=0.1.0

Channels

A TCP communication library based on middleware components.

dotnet workflow

Design

The concept behind this library is to apply a middleware pipeline to data coming in and out from the open communication channels.

For data coming through the channel input, two middleware components can be applied: adapters and handlers.

graph LR;
    channelInput((Input)) --> a1[/Adapter/]
    subgraph adapters
    a1 --> a2[/Adapter/]
    end
    a2 --> Handlers

For data going through the channel output, only adapters are applicable.

graph LR;
    openChannel((Channel)) --> a1[/Adapter/]
    subgraph adapters
    a1 --> a2[/Adapter/]
    end
    a2 --> channelOutput([Output])

Adapters

An adapter is a middleware component that can be executed at any point in the pipeline and it has a single conceptual purpose: to adapt data.

graph LR;
    in[/Data In/] --> Adapter --> out[/Data Out/]

An adapter is expected to forward data to next component in the pipeline, although that is not mandatory. However, if the adapter doesn't forward any data, the pipeline is interrupted and no other components will be executed.

Implementing an Adapter

Unless you have very specific needs, you should inherit your adapter from the ChannelAdapter<T> abstract class and not implementing the IChannelAdapter interface directly. This is because the base class does a few things for us that, if implementing the interface directly, won't be available. That includes

  • Type checking
  • Type mutation

Type checking is essentially making sure the type of the data is intended for an adapter. If it's not, data is automatically forwarded to the next middleware component in the pipeline.

Type mutation is the capacity to change the data type, if compatible with the expected data of the adapter. The base class already deals with IByteBuffer ←-> Byte[] and T ←-> IEnumerable<T> mutations, but it also provides an opportunity to override/extend this behaviour.

Here's an example of how to implement an adapter that adapts from an IByteBuffer (or Byte[]).

public class MyChannelAdapter : ChannelAdapter<IByteBuffer>
{
    public override Task ExecuteAsync( IAdapterContext context, IByteBuffer data )
    {
        // adapt/transform data
        var adaptedData = ...

        // forward adapted data
        context.Forward( adaptedData );
    }
}

Ready-made Adapters

In addition to the abstract ChannelAdapter<T> adapter, you have a few more ready-made adapters that you can use.

Adapter Target Description
AnonymousChannelAdapter Input/Output A quick way to implement an anonymous adapter
BufferLengthAdapter Input Ensures the input buffer doesn't exceed in length

Handlers

Although handlers are very similar to adapters, their conceptual purpose is different: to handle data. That means that business logic should be applied here and not on an adapter. Also, handlers are executed at the end of the pipeline and as such, they don't forward data.

graph LR;
    in[/Data In/] --> Handler

Implementing an Handler

Similarly to the adapter, unless you have very specific needs, you should inherit your handler from the ChannelHandler<T> class and not the IChannelHandler interface directly. This is because, again, similarly to the adapter, the base class does a type checking for us; if the data type is not intended for the handler, then it (the handler) won't be executed.

Similarly to the adapter, the base class also deals with T ←-> IEnumerable<T> mutations.

public class MyChannelHandler : ChannelHandler<MyData>
{
    public override Task ExecuteAsync( IChannelContext context, MyData data )
    {
        // implement your handler here
    }
}

Adapters vs Handlers

Because adapters and handlers are so similar, there might be a temptation to do everything with adapters. And while that's feasable, it's not recommended. Adapters should be used to adapt data and handlers to handle data (business logic).

  • Adapters adapt and forward data
  • Handlers handle data and business logic
  • Adapters can run at any point in the pipeline
  • Handlers run at the end of the pipeline

Writing to Output

At any point, within an adapter or handler, we can write data to the channel output; this will trigger the output pipeline and at the end, send the data to the other party. However, there are two distinct ways of doing so, with distinct behaviour.

1. Write directly to the Channel

This is the most straightforward method and it will immediately trigger the output pipeline, but it is not the recommended way, unless you need the data to reach the other party as soon as possible, no matter what happens next (current or next middleware component).

public override async Task ExecuteAsync( IAdapterContext context, IEnumerable<Message> data )
{
    // ...

    await context.Channel.WriteAsync( replyData );
}

The context of an adapter or handler, gives us access to an output buffer that we can write to. This is the recommended method. Writing to the output buffer doesn't immediately trigger the output pipeline, instead, it is only triggered at the end of the pipeline, after all adapters and handlers have executed - without interruption. If an adapter interrupts the pipeline, or a handler crashes and interrupts the pipeline, the data in the buffer will never be written to the channel.

public override async Task ExecuteAsync( IAdapterContext context, IEnumerable<Message> data )
{
    // ...

    context.Output.Write( replyData );
}

Getting Started

Install the package from NuGet

dotnet add package Faactory.Channels --prerelease

To quickly bootstrap a server, we need an HostBuilder to inject a hosted service. Then we need to configure the listening options and set up the input and output pipelines. Here's an example

var builder = new HostBuilder()
    .ConfigureServices( ( context, services) =>
    {
        // add logging
        services.AddLogging( loggingBuilder =>
        {
            loggingBuilder.AddConsole()
                .SetMinimumLevel( LogLevel.Debug );
        } );

        // add our hosted service
        services.AddChannelsHostedService( builder =>
        {
            // configure options
            builder.Configure( options =>
            {
                options.Port = 8080;
                options.Backlog = 30;
            } );

            // set up input pipeline
            builder.AddInputAdapter<ExampleDecoderChannelAdapter>()
                .AddInputHandler<MyChannelHandler>();

            // set up output pipeline
            builder.AddOutputAdapter<ExampleEncoderAdapter>();
        } );
    } )
    .UseConsoleLifetime();

await builder.Build().RunAsync();

To boostrap the client, we'll need to register the factory with a service provider. Then, similarly to the server, we need to configure the channel options and set up the input and output pipelines. Here's an example

IServiceCollection services = ...

// add logging
services.AddLogging( loggingBuilder =>
{
    loggingBuilder.AddConsole()
        .SetMinimumLevel( LogLevel.Debug );
} );

// add our client factory
services.AddChannelsClient( builder =>
{
    // configure options
    builder.Configure( options =>
    {
        options.Host = "localhost";
        options.Port = 8080;
    } );
    
    // set up input pipeline
    builder.AddInputAdapter<ExampleDecoderChannelAdapter>()
        .AddInputHandler<MyChannelHandler>();

    // set up output pipeline
    builder.AddOutputAdapter<ExampleEncoderAdapter>();
} );

var provider = services.BuildServiceProvider();
var channelFactory = provider.GetRequiredService<IClientChannelFactory>();
var channel = await channelFactory.CreateAsync();

await channel.WriteAsync( new MyData
{
    // ...
} );

Adapters and Buffers

Although raw data handling in the adapters can be done with Byte[], it is recommended to use an IByteBuffer instance instead, particularly for reading data. You can read more about it here.

Service Scope

Every channel instance (client or service) uses a new IServiceScope. This means that if you add a scoped service to the DI container and use it in an adapter or handler, you'll have a unique instance per channel.

Channel Events

In some cases, you might need to tap into channel events. This can be useful for logging, statistics or a custom scenario. The following events are available

  • Channel Created
  • Channel Closed
  • Data Received
  • Data Sent

To receive channel events, you'll need to create a class that implements IChannelEvents interface and then add it to the DI container. You can have multiple implementations.

public class MyChannelEvents : IChannelEvents
{
    // ...
}

// ...

IServiceCollection services = ...;

services.AddTransient<IChannelEvents, MyChannelEvents>();

Parcel Protocol

If you intend to use Parcel Protocol for communication, you have available an extension library. You can learn more about it here.

Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Faactory.Channels.Abstractions:

Package Downloads
Faactory.Channels.Ruptela

Channels - Ruptela Protocol

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
0.7.0-preview-3 153 2/7/2023
0.7.0-preview-2 134 2/7/2023
0.7.0-preview-1 151 2/7/2023
0.6.1 370 1/27/2023
0.6.0 341 1/27/2023
0.5.2 385 1/26/2023
0.5.1 705 1/24/2023
0.5.0 666 1/6/2023
0.5.0-preview-4 155 1/5/2023
0.5.0-preview-3 159 1/4/2023
0.5.0-preview-2 164 1/4/2023
0.5.0-preview-1 178 12/23/2022
0.4.0 874 11/21/2022
0.3.2 565 11/7/2022
0.3.1 794 7/14/2022
0.3.0 1,049 7/4/2022
0.2.0 544 6/10/2022
0.2.0-preview-1 167 6/10/2022
0.1.1 826 4/12/2022
0.1.0 1,017 4/12/2022
0.1.0-preview-9 190 4/8/2022
0.1.0-preview-11 421 4/12/2022
0.1.0-preview-10 207 4/11/2022