Entity Framework Core for IBM i with NTi
Overview
Aumerial.EntityFrameworkCore is the Entity Framework Core provider for IBM i, iSeries, and AS/400, built on the NTi ADO.NET provider: fully managed, no ODBC driver, no IBM i Access installation, no native dependency whatsoever. It brings LINQ, migrations, and reverse engineering to DB2 for i, with the standard EF Core tooling.
Compiled AnyCPU, it runs anywhere .NET 8 or later runs: Windows, Linux, and macOS, on x64 as well as ARM64, in a container, and all the way to Linux on Power (ppc64le) and Linux on IBM Z (s390x).
This documentation covers only what is specific to NTi and to IBM i. For general Entity Framework Core concepts (DbContext, LINQ, migrations), see the official Microsoft documentation.
Versions: the rule to follow
The rule is simple: the package's major version follows Entity Framework Core's major version, the minor version follows the NTi engine generation (x.5 runs on NTi 5).
| Your application | Package to install |
|---|---|
| EF Core 8 / .NET 8 | Aumerial.EntityFrameworkCore 8.5.0 |
| EF Core 9 / .NET 9 | Aumerial.EntityFrameworkCore 9.5.0 |
| EF Core 10 / .NET 10 | Aumerial.EntityFrameworkCore 10.5.0 |
All three versions share the same implementation and the same surface. The choice only ever depends on your EF Core version. The package automatically installs its Aumerial.Data.Nti dependency at version 5.0.0 or later.
On the server side, the EF Core layer requires IBM i version 7.2 at a minimum (V7R4 or later recommended). The ADO.NET provider alone reaches as far back as V5R4.
Installation
dotnet add package Aumerial.EntityFrameworkCore
Microsoft.EntityFrameworkCore.Designis required for thedotnet efcommands (migrations, scaffolding). Install it at the major version matching your EF Core version. See the EF Core tools.
Configuring the DbContext with UseNTi
UseNTi wires EF Core to DB2 for i. The connection string uses the same keywords as the NTi ADO.NET provider. database names the target schema (the library). See Connection for the full keyword reference.
using System;
using Microsoft.EntityFrameworkCore;
public class Order
{
public int Id { get; set; } // IDENTITY column, value returned on insert
public string Customer { get; set; } = "";
public decimal Amount { get; set; }
public DateTime PlacedOn { get; set; }
}
public class OrderContext : DbContext
{
public DbSet Orders => Set();
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseNTi("server=MYIBMI;user=MYUSER;password=MYPASSWORD;database=MYLIB;");
}
EF Core is then used as usual, with async as the provider's normal path (synchronous equivalents still exist):
using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
await using var db = new OrderContext();
await db.Database.MigrateAsync(); // creates the schema (library) if needed, then applies migrations
db.Orders.Add(new Order { Customer = "ACME", Amount = 1249.90m, PlacedOn = DateTime.Now });
await db.SaveChangesAsync(); // the IDENTITY value comes back through SELECT ... FROM FINAL TABLE
var top = await db.Orders
.Where(o => o.Amount > 1000m)
.OrderByDescending(o => o.Amount)
.Take(10)
.ToListAsync(); // FETCH FIRST / OFFSET pagination, DB2 for i dialect end to endDependency injection
In an ASP.NET Core or Blazor project, register the context in Program.cs:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")!;
builder.Services.AddDbContext(options => options.UseNTi(connectionString));
var app = builder.Build();
app.Run();
The context then receives its options through its constructor:
using Microsoft.EntityFrameworkCore;
public class OrderContext : DbContext
{
public OrderContext(DbContextOptions options) : base(options) { }
public DbSet Orders => Set();
}
For Blazor Server and background services, prefer
AddDbContextFactory.
Connection pooling is off by default. For a web application, add
pooling=trueto the connection string.
The *SQL naming convention is mandatory
The provider qualifies objects as SCHEMA.TABLE and requires the *SQL convention. A connection string that requests naming=*SYS is rejected as soon as the context is created, with a clear message. Leave the keyword out, or set naming=*SQL.
# accepted
server=MYIBMI;user=MYUSER;password=MYPASSWORD;database=MYLIB;
# rejected when the context is created, with a clear message
server=MYIBMI;user=MYUSER;password=MYPASSWORD;database=MYLIB;naming=*SYS;Uppercase, quoted identifiers
The SQL that gets emitted puts every identifier in uppercase and quotes it: "ORDERS", "CUSTOMER". A direct consequence: objects created by EF Core keep their SQL name as their system name, and what you see in ACS matches exactly what the model declares.
In ACS or STRSQL, SELECT * FROM MYLIB.ORDERS works as is, and the tables stay reachable from native tools. Reserved words (ORDER, USER, GROUP) are safe.
Provider options
Every option is optional. The defaults work against any existing schema.
using Microsoft.EntityFrameworkCore;
public class OrderContext : DbContext
{
public DbSet Orders => Set();
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseNTi(
"server=MYIBMI;user=MYUSER;password=MYPASSWORD;database=MYLIB;",
nti => nti
.UnicodeCcsid(1208) // CCSID for Unicode columns (default 1208 / UTF-8)
.ForceUnicode() // stores every text column as Unicode
.VarcharMaxLength(8000) // default length for VARCHAR without HasMaxLength
.VarbinaryMaxLength(8000) // same for VARBINARY
.VargraphicMaxLength(8000) // same for VARGRAPHIC
.DecimalDefaults(31, 8)); // precision and scale for decimal without HasPrecision
}
These options compose with the standard annotations ([Unicode], HasMaxLength, HasPrecision, HasColumnType), covered in detail in Mapping and DB2 for i types.
Async end to end
ToListAsync, SaveChangesAsync, MigrateAsync, and their siblings ride NTi's native async engine: no thread blocked on I/O, no sync-over-async, and cancellation tokens are honored at every phase, from opening the connection to reading the results.
using System;
using System.Linq;
using System.Threading;
using Microsoft.EntityFrameworkCore;
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
await using var db = new OrderContext();
var recent = await db.Orders
.Where(o => o.PlacedOn >= DateTime.Today.AddDays(-7))
.ToListAsync(cts.Token);
Under the NTi provider's contract, an effective cancellation invalidates the underlying connection. The
OperationCanceledExceptioncarries the caller's token, and the connection is considered broken.
What's next?
- Mapping and DB2 for i types : Unicode, zoned and packed, HiLo, ROW CHANGE TIMESTAMP
- Migrations : Migrate, idempotent scripts, DB2 for i limits
- Blazor Server CRUD with EF Core : a complete example with Entity Framework Core on DB2 for i
- Connection : connection string, pooling, MFA