Serve your content in every language, from the entity classes you already have.
You add one attribute to a property. DotNetBoost.MultiLingual stores each language's value for you, serves the right one for every request, and falls back when a translation is missing — with no extra table to design, no repository code and no service code.
public class Product
{
public int Id { get; set; }
public string Sku { get; set; } = ""; // ordinary column
public decimal Price { get; set; } // ordinary column
[MultiLingual] public string? Name { get; set; } // translated
[MultiLingual] public string? Description { get; set; } // translated
}GET /api/products/1 Accept-Language: en -> { "name": "14-inch Laptop", … }
GET /api/products/1 Accept-Language: ar -> { "name": "حاسوب محمول 14 بوصة", … }
GET /api/products/1 Accept-Language: fr -> English, when there is no French yet
Reading and writing that product is still plain EF Core — Where(p => p.Name!.Contains(term))
filters on the translated text, in the database.
Works with .NET 8 and .NET 10, on SQL Server, PostgreSQL, SQLite or MongoDB.
Status: preview. The packages are on NuGet as
1.0.0-preview.1, so installing them needs--prerelease— every command below has it. The API may still change before 1.0.0.
This builds a minimal web app that stores its content in a local SQLite file. Nothing else to install.
1. Create a project and add the packages
dotnet new web -n MyShop
cd MyShop
dotnet add package DotNetBoost.MultiLingual.Core --prerelease
dotnet add package DotNetBoost.MultiLingual.EntityFrameworkCore --prerelease
dotnet add package DotNetBoost.MultiLingual.API --prerelease
dotnet add package Microsoft.EntityFrameworkCore.Sqlite2. Replace Program.cs with this
using DotNetBoost.MultiLingual.API;
using DotNetBoost.MultiLingual.Core.Attributes;
using DotNetBoost.MultiLingual.Core.Models;
using DotNetBoost.MultiLingual.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// Store translations in SQLite through EF Core.
builder.Services.AddMultiLingual()
.UseEntityFrameworkCore<AppDbContext>()
.Build();
builder.Services.AddDbContext<AppDbContext>((sp, o) => o
.UseSqlite("Data Source=shop.db")
.UseMultiLingual(sp)); // attaches the interceptors
builder.Services.AddProblemDetails();
builder.Services.AddExceptionHandler<MultiLingualExceptionHandler>();
var app = builder.Build();
// Create the database on first run. In a real app, use EF Core migrations instead.
using (var scope = app.Services.CreateScope())
{
await scope.ServiceProvider.GetRequiredService<AppDbContext>().Database.EnsureCreatedAsync();
}
app.UseExceptionHandler();
app.UseMultiLingual(); // Accept-Language -> the language this request reads and writes in
app.MapLanguageEndpoints(); // adds GET/POST/… /api/languages
app.MapGet("/products", (AppDbContext db) => db.Products.ToListAsync());
app.MapPost("/products", async (Product product, AppDbContext db) =>
{
db.Products.Add(product);
await db.SaveChangesAsync(); // Name is stored for the request's language
return product;
});
app.MapPut("/products/{id:int}/name", async (int id, NameUpdate body, AppDbContext db) =>
{
var product = await db.Products.FindAsync(id);
if (product is null)
return Results.NotFound();
product.Name = body.Name; // this language only; the others are untouched
await db.SaveChangesAsync();
return Results.Ok(product);
});
app.Run();
// Your entity: one attribute per translatable property.
public class Product
{
public int Id { get; set; }
public string Sku { get; set; } = "";
[MultiLingual] public string? Name { get; set; }
}
public record NameUpdate(string Name);
// Your EF Core context: add the two library tables, and call ApplyMultiLingual() LAST.
public class AppDbContext(DbContextOptions<AppDbContext> options)
: DbContext(options), IMultiLingualDbContext
{
public DbSet<Translation> Translations => Set<Translation>();
public DbSet<Language> Languages => Set<Language>();
public DbSet<Product> Products => Set<Product>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
=> modelBuilder.ApplyMultiLingual();
}3. Run it and add a language
dotnet run --urls http://localhost:5080In a second terminal. The first language created becomes the default:
curl -X POST http://localhost:5080/api/languages -H "Content-Type: application/json" -d '{"code":"en","name":"English"}'
curl -X POST http://localhost:5080/api/languages -H "Content-Type: application/json" -d '{"code":"ar","name":"العربية","direction":"rtl"}'
curl -X POST http://localhost:5080/api/languages -H "Content-Type: application/json" -d '{"code":"fr","name":"Français"}'4. Write one product in two languages
curl -X POST http://localhost:5080/products -H "Content-Type: application/json" -H "Accept-Language: en" -d '{"sku":"LAPTOP-14","name":"14-inch Laptop"}'
curl -X PUT http://localhost:5080/products/1/name -H "Content-Type: application/json" -H "Accept-Language: ar" -d '{"name":"حاسوب محمول 14 بوصة"}'5. Read it back in each one
curl http://localhost:5080/products -H "Accept-Language: ar"[{"id":1,"sku":"LAPTOP-14","name":"حاسوب محمول 14 بوصة"}]curl http://localhost:5080/products -H "Accept-Language: fr"[{"id":1,"sku":"LAPTOP-14","name":"14-inch Laptop"}]There is no French yet, so the read falls back to the default language. Nothing in the two product endpoints mentions a language: the values were filled in by the same SQL statement that loaded the row.
⚠️ The language endpoints allow anonymous access by default. They change what every client of your application sees, and one of them deletes content. Protect them before you deploy:app.MapLanguageEndpoints().RequireAuthorization("admin"). See Securing the endpoints.
Reading and writing translated properties needs no library call at all — it is the EF Core you
already write. For everything else, inject IMultiLingualManager:
public class ProductService(AppDbContext db, IMultiLingualManager multiLingual)
{
public async Task TranslateAsync(int id)
{
var product = await db.Products.FirstAsync(p => p.Id == id);
multiLingual.For(product).Set("fr", p => p.Name, "Portable"); // add a language
multiLingual.For(product).Clear("ar"); // remove one
await db.SaveChangesAsync(); // written with the entity, in the same transaction
}
}| You want to… | Call |
|---|---|
| know the request's language | multiLingual.CurrentLanguage, DefaultLanguage, WasRequestedLanguageHonoured |
| set it outside a request (jobs, seeders) | await multiLingual.UseLanguageAsync("ar") |
| read every stored language of an entity | await multiLingual.For(product).GetAllAsync() |
| add, change or remove a translation | .Set("fr", p => p.Name, "Chaise"), .Clear("fr") |
| find or administer languages | multiLingual.Languages.FindAsync("ar"), CreateAsync(…), SetDefaultAsync(…), … |
| fill translated properties yourself (Dapper, MongoDB) | await multiLingual.LocalizeAsync(products) |
Set and Clear check the language straight away, so a mistake surfaces where it was made — and
MultiLingualExceptionHandler turns it into problem details, so a controller needs no checks of its
own. Full tour: reading and writing.
The quick start uses EF Core with SQLite because that needs no setup. In a real app, use the database you already have:
| You use… | Package | Register with |
|---|---|---|
| Entity Framework Core | DotNetBoost.MultiLingual.EntityFrameworkCore |
.UseEntityFrameworkCore<AppDbContext>() |
| Dapper / plain ADO.NET | DotNetBoost.MultiLingual.Dapper |
.UseDapper(sp => new SqlConnection(cs), migrateSchema: true) |
| MongoDB | DotNetBoost.MultiLingual.MongoDb |
.UseMongoDb("mongodb://localhost:27017", "shop") |
EF Core needs two extra DbSets on your context. Dapper and MongoDB have no change tracker to
hook, so their users localize after a query and save translations explicitly — a few extra lines,
the same rules. See the storage providers
guide.
Already have a Products.Name column full of English? Then that content is a language. Set
BaseLanguage = "en" and it stays exactly where it is, with every other language in the
translations table falling back to it — nothing to import, no column dropped. See
base-language mode.
Each feature is optional and switched on with one line on AddMultiLingual():
builder.Services.AddMultiLingual(o => o.BaseLanguage = "en") // keep English in its own columns
.UseEntityFrameworkCore<AppDbContext>()
.AddJsonLocalization() // your own messages, in the same language
.SynchronizeLanguagesThroughDistributedCache() // several instances, one language list
.WithLanguageCacheDuration(TimeSpan.FromMinutes(5))
.Build();| I want to… | Guide |
|---|---|
| Mark content translatable, and rename a class safely | Declaring translatable content |
| Use SQL Server, PostgreSQL, Dapper or MongoDB | Storage providers |
| Filter, sort and project on translated text, in SQL | Translated queries |
| Keep one language in the entity's own columns | Base-language mode |
| Add, deactivate or promote a language at runtime | Languages |
| Expose and secure the language endpoints | REST API |
| Return my own error messages in the caller's language | Localized messages |
| Run on several servers (Redis) | Languages → the cache |
| Give editors a web UI | Dashboard |
| See every builder option | Configuration reference |
| Know what this does not do | Known limitations |
| Run the full demo stack (three APIs, dashboard, PostgreSQL, MongoDB) | .NET Aspire |
| Understand how it works inside, or contribute | Architecture |
samples/SampleApp is a
complete Products and Categories API in base-language mode, so Products.Name is a real column you
can read in any SQL client. Two more samples serve the same routes and the same bodies over the
Dapper and MongoDB providers. If you have Docker running, one command starts all of it — plus
PostgreSQL, MongoDB, a browser SQL client and the web dashboard:
dotnet run --project aspire/DotNetBoost.MultiLingual.AppHostThe API is then on http://localhost:5160, with its reference at /scalar. See
Running everything with .NET Aspire.
| Package | What it's for |
|---|---|
DotNetBoost.MultiLingual.Core |
Required. Attributes, the engine, IMultiLingualManager, the store contracts |
DotNetBoost.MultiLingual.EntityFrameworkCore |
EF Core provider: mapping and interceptors, so queries and saves translate themselves |
DotNetBoost.MultiLingual.Dapper |
Dapper provider, over the EF Core provider's own schema |
DotNetBoost.MultiLingual.MongoDb |
MongoDB provider |
DotNetBoost.MultiLingual.API |
ASP.NET Core: Accept-Language, language endpoints, problem details, OpenAPI |
DotNetBoost.MultiLingual.Localization |
IStringLocalizer over JSON files, in the content's language |
Contributions are welcome. See CONTRIBUTING.md and the architecture overview.
MIT. See LICENSE.