Etl.Net is a free, open source data engineering & integration project written in C# and released under MIT. It has 813 GitHub stars, 106 forks and 46 open issues, and was last pushed 24 days ago. On this registry it ranks #43 of 53 tracked projects in Data Engineering & Integration, with 5 head-to-head comparisons available. It gained 2 stars over the last 30 tracked days.

What is Etl.Net?

Etl.Net is an MIT-licensed, open-source mass-processing ETL engine for .NET developers that lets extract, transform and load pipelines be built directly inside ordinary .NET applications through a reactive, LINQ-style programming model.

What it is

Etl.Net is a mass processing engine written in C# for the .NET ecosystem, targeting .NET, .NET Core and .NET Standard so that the same pipeline code runs across platforms. Its stated design goal is to be used in a similar way to Linq while covering every SQL Server Integration Services (SSIS) feature and more. The engine is built on a reactive approach, which the project describes as the reason it can run parallelized multi-streams with high performance and a low memory footprint, even when processing millions of rows. It is distributed as a family of NuGet packages rather than as a standalone server, and the README positions extension as a first-class concern, claiming that extending it takes five minutes.

The concrete problem it solves is the need to stand up separate ETL tooling for data integration work that belongs next to application code. Instead of maintaining SSIS packages in a separate environment, a .NET team writes pipelines in C# using the same reactive composition style it already knows, and ships them as part of its own application. Because the engine is library-shaped, it can be embedded wherever .NET runs, and the connector packages let a single pipeline read from or write to file systems, cloud storage, Excel files and databases without leaving the runtime.

Key capabilities

  • Core engine ships as Paillave.EtlNet.Core, with the reactive pipeline execution model providing parallelized multi-streams and low memory use on million-row workloads.
  • CSV handling is a first-class concern, reflected in the csv, csv-parser, csv-reader and csv-writer topics.
  • Database integration through Paillave.EtlNet.EntityFrameworkCore for Entity Framework based sources and targets.
  • Cloud and file connectors: Paillave.EtlNet.AzureStorageAccount, Paillave.EtlNet.Dropbox and Paillave.EtlNet.FileSystem.
  • Spreadsheet ingestion via the Paillave.EtlNet.ExcelFile package.
  • Dependency injection integration through Paillave.EtlNet.Autofac.
  • Configuration-driven connector wiring with Paillave.EtlNet.FromConfigurationConnectors, plus Paillave.EtlNet.ExecutionToolkit for running jobs.

Who uses it and how

  • .NET teams that want ETL job definitions to live in the same repository and deployment pipeline as the application, rather than in a separate ETL server.
  • Business intelligence and data engineering workloads listed under the project's topics, including extract and etl-job scenarios.
  • Applications that must pull from mixed sources — Azure Storage Account, Dropbox, local file systems, Excel workbooks — and consolidate into a database through Entity Framework.
  • Long-running batch jobs processing millions of rows where memory headroom is a constraint, since the reactive engine is designed to keep the footprint low.
  • Teams using an IoC container, who wire pipeline dependencies through the Autofac package instead of manual construction.

Getting started

Install the NuGet package Paillave.EtlNet.Core (alongside the connector and toolkit packages a given pipeline needs) into a .NET project, and follow the documentation site at https://paillave.github.io/Etl.Net/ for the getting-started material.

How it compares

SSIS is the reference point the README names, offering feature parity with it and more, but Etl.Net differs in form: it is a .NET library under the MIT licence rather than a proprietary Microsoft toolchain, so pipelines are code in the repository and the engine runs inside the application rather than on a dedicated ETL server. That shifts the cost model from licensed tooling and server infrastructure toward ordinary application hosting, and keeps the pipeline definitions under the team's own source control.

When to use it — and when not to

A self-hoster operates nothing extra beyond the .NET runtime and the external systems the pipelines actually touch, so there is no separate database, queue or scheduler to run — but that also means the team supplies its own scheduling and operational wrapping. The project's own README is unusually frank about its gaps: it explicitly asks for help with documentation, saying that a dramatic amount of serious features is not documented, and notes that the unit tests that once covered the reactive core of Paillave.EtlNet.Core were decommissioned at the V2 release. Teams that need complete documentation and a serious test-coverage signal before adopting a library should weigh that against the 46 open issues and the maintainer's open call for contributors.

project readme (upstream, from github) — read inline

Etl.Net

ETL.NET Go to full documentation

Last release

:rotating_light: I need some help!!! :ambulance:

I need some serious help for the following subject:

  • :books: Documentation. The website of ETL.NET needs to be completed. It contains the essential to start and get in touch, but a dramatic amount of serious features is not documented in there.
  • :heavy_check_mark: Unit tests. A proper test code coverage is the only real way to be taken seriously when it is about selecting the right open source library. There was a time when some units tests existed only for the reactive engine of the core (Paillave.EtlNet.Core). But it happened I had to decommission it in the even of the V2 release.

:envelope_with_arrow: Anybody who is keen to participate to the project in these regards is very welcome to contact me at admroyer@hotmail.com.

Presentation

Implementation of a mass processing engine to use in a similar way than Linq with every SSIS features and much more. The reactive approach for the implementation of this engine ensures parallelized multi streams, high performances and low memory foot print even with million rows to process.

ETL.NET is fully written in .NET for a multi platform usage and for a straight forward integration in any application.

Extend it takes 5mn... literally.

Package nuget version nuget downloads
Paillave.EtlNet.Core NuGet NuGet
Paillave.EtlNet.Autofac NuGet NuGet
Paillave.EtlNet.AzureStorageAccount NuGet NuGet
Paillave.EtlNet.Dropbox NuGet NuGet
Paillave.EtlNet.EntityFrameworkCore NuGet NuGet
Paillave.EtlNet.ExcelFile NuGet NuGet
Paillave.EtlNet.ExecutionToolkit NuGet NuGet
Paillave.EtlNet.FileSystem NuGet NuGet
Paillave.EtlNet.FromConfigurationConnectors NuGet NuGet
Paillave.EtlNet.Ftp NuGet NuGet
Paillave.EtlNet.Mail NuGet NuGet
Paillave.EtlNet.GraphApi NuGet NuGet
Paillave.EtlNet.Http NuGet NuGet
Paillave.EtlNet.JsonFile NuGet NuGet
Paillave.EtlNet.Sftp NuGet NuGet
Paillave.EtlNet.Pgp NuGet NuGet
Paillave.EtlNet.S3 NuGet NuGet
Paillave.EtlNet.SqlServer NuGet NuGet
Paillave.EtlNet.TextFile NuGet NuGet
Paillave.EtlNet.Bloomberg NuGet NuGet
Paillave.EtlNet.XmlFile NuGet NuGet
Paillave.EtlNet.Zip NuGet NuGet
Paillave.EtlNet.Pdf NuGet NuGet
Paillave.EntityFrameworkCoreExtension NuGet NuGet

Examples

Extract it, parse it, upload it

Read all Zip files from a folder, unzip Csv files that are inside, parse them, exclude duplicates, and upsert them into database.

dotnet new console -o SimpleTutorial
cd SimpleTutorial
dotnet add package Paillave.EtlNet.Core
dotnet add package Paillave.EtlNet.FileSystem
dotnet add package Paillave.EtlNet.Zip
dotnet add package Paillave.EtlNet.TextFile
dotnet add package Paillave.EtlNet.SqlServer
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.DependencyInjection;
using Paillave.Etl.Core;
using Paillave.Etl.FileSystem;
using Paillave.Etl.SqlServer;
using Paillave.Etl.TextFile;
using Paillave.Etl.Zip;
using System.Data;

//Register services
var serviceCollection = new ServiceCollection();
serviceCollection.AddTransient<IDbConnection>(_ =>
{
    var conn = new SqlConnection(@"Data Source=MyServer;Initial Catalog=MyDatabase;Integrated Security=True;");
    conn.Open();
    return conn;
});
var services = serviceCollection.BuildServiceProvider();


//Configure an ETL execution (defined below)
var processRunner = StreamProcessRunner.Create<string>(DefineProcess);
var executionOptions = new ExecutionOptions<string>
{
    Services = services,
};
var result = await processRunner.ExecuteAsync(@"C:\Path\To\My\Zip-Files", executionOptions);

Console.WriteLine(result.Failed
    ? $"{result.ErrorTraceEvent.NodeName} ({result.ErrorTraceEvent.NodeTypeName}): {result.ErrorTraceEvent.Content.Message}"
    : "Success!");


//Define the ETL process, in this case starting from a single path string
static void DefineProcess(ISingleStream<string> contextStream)
{
    contextStream
        .CrossApplyFolderFiles("List all Zip files", "*.zip", recursive: true)
        .CrossApplyZipFiles("Extract Csv files from Zips", "*.csv")
        .CrossApplyTextFile("Parse Csv files", FlatFileDefinition.Create(i => new Person
            {
                FirstName = i.ToColumn("first name"),
                LastName = i.ToColumn("last name"),
                DateOfBirth = i.ToDateColumn("date of birth", "yyyy-MM-dd"),
                Email = i.ToColumn("email"),
                Reputation = i.ToNumberColumn<int?>("reputation", ".")
            })
            .IsColumnSeparated(',')
        )
        .Distinct("Exclude duplicates based on the Email", p => p.Email)
        .SqlServerSave("Upsert using Email as key and ignore the Id", o => o
            .ToTable("dbo.Person")
            .SeekOn(p => p.Email)
            .DoNotSave(p => p.Id));
}


class Person
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime DateOfBirth { get; set; }
    public string Email { get; set; }
    public int? Reputation { get; set; }
}

Run it, debug it, track it, log it

Execute an ETL process, debug it by tracking debug events using the IDE debugger, catch execution events and log it into database.

using System;
using System.Threading.Tasks;
using Paillave.Etl.Core;
using Paillave.Etl.FileSystem;
using Paillave.Etl.Zip;
using Paillave.Etl.TextFile;
using Paillave.Etl.SqlServer;
using Microsoft.Data.SqlClient;

namespace SimpleTutorial
{
  class Program
  {
    static async Task Main(string[] args)
    {
      var processRunner = StreamProcessRunner.Create<string>(DefineProcess);
      processRunner.DebugNodeStream += (sender, e) => { /* PLACE A CONDITIONAL BREAKPOINT HERE FOR DEBUG */ };
      using (var cnx = new SqlConnection(args[1]))
      {
        cnx.Open();
        var executionOptions = new ExecutionOptions<string>
        {
          Resolver = new SimpleDependencyResolver().Register(cnx),
          TraceProcessDefinition = DefineTraceProcess,
          // UseDetailedTraces = true // activate only if per row traces are meant to be caught
        };
        var res = await processRunner.ExecuteAsync(args[0], executionOptions);
        Console.Write(res.Failed ? "Failed" : "Succeeded");
        if (res.Failed)
          Console.Write($"{res.ErrorTraceEvent.NodeName}({res.ErrorTraceEvent.NodeTypeName}):{res.ErrorTraceEvent.Content.Message}");
      }
    }
    private static void DefineProcess(ISingleStream<string> contextStream)
    {
      // TODO: define your ELT process here
    }
    private static void DefineTraceProcess(IStream<TraceEvent> traceStream, ISingleStream<string> contentStream)
    {
      traceStream
        .Where("keep only summary of node and errors", i => i.Content is CounterSummaryStreamTraceContent || i.Content is UnhandledExceptionStreamTraceContent)
        .Select("create log entry", i => new ExecutionLog
          {
            DateTime = i.DateTime,
            ExecutionId = i.ExecutionId,
            EventType = i.Content switch
            {
              CounterSummaryStreamTraceContent => "EndOfNode",
              UnhandledExceptionStreamTraceContent => "Error",
              _ => "Unknown"
            },
            Message = i.Content switch
            {
              CounterSummaryStreamTraceContent counterSummary => $"{i.NodeName}: {counterSummary.Counter}",
              UnhandledExceptionStreamTraceContent unhandledException => $"{i.NodeName}({i.NodeTypeName}): [{unhandledException.Level.ToString()}] {unhandledException.Message}",
              _ => "Unknown"
            }
          })
        .SqlServerSave("save traces", o => o.ToTable("dbo.ExecutionTrace"));
    }
    private class ExecutionLog
    {
      public DateTime DateTime { get; set; }
      public Guid ExecutionId { get; set; }
      public string EventType { get; set; }
      public string Message { get; set; }
    }
  }
}

Normalize it

Dispatch rows from a flat file into several tables to normalize data thanks to the correlation mechanism.

private static void DefineProcess(ISingleStream<string> contextStream)
{
  var rowStream = contextStream
    .CrossApplyFolderFiles("list all required files", "*.csv", true)
    .CrossApplyTextFile("parse file", FlatFileDefinition.Create(i => new
    {
      Author = i.ToColumn("author"),
      Email = i.ToColumn("email"),
      TimeSpan = i.ToDateColumn("timestamp", "yyyyMMddHHmmss"),
      Category = i.ToColumn("category"),
      Link = i.ToColumn("link"),
      Post = i.ToColumn("post"),
      Title = i.ToColumn("title"),
    }).IsColumnSeparated(','))
    .SetForCorrelation("set correlation for row");

  var authorStream = rowStream
    .Distinct("remove author duplicates based on emails", i => i.Email)
    .Select("create author instance", i => new Author { Email = i.Email, Name = i.Author })
    .EfCoreSaveCorrelated("save or update authors", o => o
      .SeekOn(i => i.Email)
      .AlternativelySeekOn(i => i.Name));

  var categoryStream = rowStream
    .Distinct("remove category duplicates", i => i.Category)
    .Select("create category instance", i => new Category { Code = i.Category, Name = i.Category })
    .EfCoreSaveCorrelated("insert categories if doesn't exist, get it otherwise", o => o
      .SeekOn(i => i.Code)
      .DoNotUpdateIfExists());

  var postStream = rowStream
    .CorrelateToSingle("get related category", categoryStream, (l, r) => new { Row = l, Category = r })
    .CorrelateToSingle("get related author", authorStream, (l, r) => new { l.Row, l.Category, Author = r })
    .Select("create post instance", i => string.IsNullOrWhiteSpace(i.Row.Post)
      ? new LinkPost
      {
        AuthorId = i.Author.Id,
        CategoryId = i.Category.Id,
        DateTime = i.Row.TimeSpan,
        Title = i.Row.Title,
        Url = new Uri(i.Row.Link)
      } as Post
      : new TextPost
      {
        AuthorId = i.Author.Id,
        CategoryId = i.Category.Id,
        DateTime = i.Row.TimeSpan,
        Title = i.Row.Title,
        Text = i.Row.Post
      })
    .EfCoreSaveCorrelated("save or update posts", o => o
      .SeekOn(i => new { i.AuthorId, i.DateTime }));
}

Frequently asked questions

Is Etl.Net free to use?

Etl.Net is open source under the MIT licence. There is no licence fee and no seat count — you can self-host it or, where the project offers one, pay a vendor for a managed version instead.

What does Etl.Net do?

Mass processing data with a complete ETL for .net developers

What is Etl.Net written in?

Etl.Net is primarily written in C#. Its source is publicly available at https://github.com/paillave/Etl.Net, and it has 813 GitHub stars.