DEV Community

Safwan Abdulghani
Safwan Abdulghani

Posted on

RoomSharp: Source-Generated Data Layer for Modern .NET

If you’ve ever envied Android’s Room stack and wished you could have the same attribute-driven ergonomics, source-generated DAOs, and multi-database freedom in .NET 8/9/10, RoomSharp is for you. It’s a Room-inspired data layer built specifically for modern .NET: a Roslyn source generator, a set of lightweight providers (SQLite, SQL Server, PostgreSQL, MySQL/MariaDB), and companion packages that deliver zero-reflection persistence without hiding the power of raw ADO.NET. Grab the main package on NuGet: nuget.org/packages/RoomSharp.

The RoomSharp Package Lineup

  • RoomSharp – the runtime, attributes, relation loader, paging helpers, multi-instance invalidation logic, and type converter infrastructure.
  • RoomSharp.SourceGenerator – the Roslyn generator that turns [Dao], [Entity], [Insert], [Query], [Transaction], and friends into fully compiled implementations (AppDatabaseImpl, TodoDaoImpl, …) at build time. No runtime reflection, no IL weaving, just C#.
  • RoomSharp.SqlServer / RoomSharp.PostgreSql / RoomSharp.MySql / RoomSharp (for SQLite) – each provider implements IDatabaseProvider, so you can switch backends with a single builder call.
  • RoomSharp.DependencyInjectionIServiceCollection extensions that register databases and DAOs as first-class citizens in ASP.NET Core, worker services, and background jobs.

What Working With RoomSharp Looks Like

using RoomSharp.Attributes;
using RoomSharp.Core;

[Entity(TableName = "users")]
[Index(Value = ["Email"], Unique = true)]
public class User
{
    [PrimaryKey(AutoGenerate = true)]
    public long Id { get; set; }

    [Unique]
    public required string Email { get; set; }
    public string? Name { get; set; }
}

[Dao]
public interface IUserDao
{
    [Insert] long Insert(User user);

    [Query("SELECT * FROM users WHERE Email = :email")]
    Task<User?> FindByEmailAsync(string email);

    [Transaction]
    async Task<long> UpsertAsync(User user) {  }
}

[Database(Version = 1, Entities = [typeof(User)])]
public abstract class AppDatabase(IDatabaseProvider provider, ILogger? logger = null)
    : RoomDatabase(provider, logger)
{
    public abstract IUserDao UserDao { get; }
}

var db = RoomDatabase.Builder<AppDatabaseImpl>()
    .UseSqlite("app.db")
    .SetJournalMode(JournalMode.WAL)
    .EnableMultiInstanceInvalidation()
    .SetAutoCloseTimeout(TimeSpan.FromMinutes(5))
    .Build();
Enter fullscreen mode Exit fullscreen mode

You describe your schema with attributes, author DAOs as interfaces (with optional method bodies), and let the generator spit out the implementation. The builder wires in the provider, logging, callbacks, query executors, WAL settings, multi-instance invalidation, and auto-close semantics.

Calling DAOs from Your Application

Once the generator produces AppDatabaseImpl, you work with DAOs like any other service. Inject the database, grab the DAO, and call the generated members—transactions, type converters, and relation loaders are already baked in.

public sealed class UserService(AppDatabase db, ILogger<UserService> logger)
{
    public async Task<long> CreateUserAsync(string email, string? name)
    {
        var newUser = new User { Email = email, Name = name };
        var id = await db.UserDao.UpsertAsync(newUser);

        logger.LogInformation("Upserted user {Email} with id {Id}", email, id);
        return id;
    }

    public async Task<IReadOnlyList<User>> GetActiveUsersAsync()
    {
        // Generated query method with mapping handled by RoomSharp.
        var users = await db.UserDao.RunCustomQueryAsync(
            new QueryBuilder()
                .Select("*")
                .From("users")
                .Where("IsActive = 1")
                .OrderBy("CreatedAt DESC")
                .Build());

        return users;
    }
}

// Somewhere in your app's startup or minimal API endpoint.
var userId = await userService.CreateUserAsync("jane@contoso.dev", "Jane");
Enter fullscreen mode Exit fullscreen mode

Because the DAO implementation is emitted at compile time, there’s no reflection penalty or runtime codegen. Each call gets compiled SQL, parameter handling, and transaction boundaries designed specifically for the method you declared.

Builder Superpowers

RoomDatabaseBuilder<T> isn’t just “give me a connection string.” It can:

  • swap providers (UseSqlite, UseProvider(new PostgresProvider(), connString))
  • override metadata (SetVersion, SetEntities) when you need conditional builds
  • register callbacks (AddCallback) for OnCreate, OnOpen, OnDestructiveMigration
  • orchestrate migrations (AddMigrations, FallbackToDestructiveMigration)
  • plug diagnostics (SetQueryExecutor) or enable WAL + auto-close for desktop/mobile scenarios.

All of it is exposed through RoomSharp.Extensions, so even though the builder stays lean, the API surface feels cohesive.

Transactions, Converters, and Relations with Zero Boilerplate

  • [Transaction] wraps DAO methods—sync or async—in the right helper (RunInTransaction, RunInTransactionAsync), so you can just focus on domain logic.
  • Type conversion is handled via ITypeConverter<TFrom, TTo> implementations you can attach to properties or register globally (db.Converters.Register(new GuidToStringConverter())).
  • [Relation] + [Embedded] let DAOs project 1:1, 1:N, and N:N graphs into DTOs, and RelationLoader can hydrate navigation properties post-query.
  • Paging, LiveData-like observables, raw query helpers, and multi-map DTO support live directly inside the core package.

Migrations Without Drama

RoomSharp mixes declarative and imperative migrations:

  • [AutoMigration(From = 1, To = 2, Spec = typeof(UserSpec))] with IAutoMigrationSpec handles straightforward schema changes (rename table, rename column, add/drop column, etc.) and allows custom post-migrate hooks.
  • For the times you need hand-crafted SQL, derive from Migration and override Migrate(DbConnection connection).

MigrationManager keeps the internal __room_metadata table in sync, reports destructive migrations, and ensures callbacks can respond accordingly.

Raw SQL When You Need It

Prefer to craft SQL yourself? [RawQuery] methods accept ISupportSQLiteQuery, and RoomSharp.RawQuery.QueryBuilder lets you compose parameterized SQL fluently. For deeper control, implement IQueryExecutor (great for logging, caching, or plugging Dapper) and register it via SetQueryExecutor.

First-Class ASP.NET Core Integration

The RoomSharp.DependencyInjection package adds intuitive extensions:

services.AddRoomSharpDatabase<AppDatabase>(context =>
{
    var configuration = context.Services.GetRequiredService<IConfiguration>();
    var conn = configuration.GetConnectionString("RoomSharp")!;

    context.UseSqlite(conn);
    context.Builder
        .SetVersion(2)
        .AddMigrations(new InitialMigration())
        .SetEntities(typeof(User), typeof(Todo));
});

services.AddRoomSharpDao<AppDatabase, ITodoDao>(db => db.TodoDao);
Enter fullscreen mode Exit fullscreen mode
  • The logger resolved from DI is automatically attached to the builder.
  • Databases are registered as singletons so they can manage their own connection pools.
  • DAOs default to scoped lifetimes, but you can override the ServiceLifetime when calling AddRoomSharpDao.
  • You can bind connection strings from IConfiguration, resolve custom providers from DI (UseProvider<TProvider>()), or expose DAOs as factory delegates if you prefer.

Provider Packages Built for Production

The runtime models every backend via IDatabaseProvider, so swapping is a matter of configuration:

  • RoomSharp ships with SQLite support and helpers such as SetJournalMode(JournalMode) and multi-instance invalidation for WAL scenarios.
  • RoomSharp.SqlServer, RoomSharp.PostgreSql, and RoomSharp.MySql lean on the official ADO.NET drivers (Microsoft.Data.SqlClient, Npgsql, MySql.Data). Each provider fine-tunes dialect helpers so generated SQL uses the proper parameter styles and data type affinities.
  • Need custom behavior (connection resiliency, tracing)? Implement your own provider or wrap the existing ones.

Getting Started Today

  1. Pull RoomSharp from NuGet (dotnet add package RoomSharp) and pair it with any provider package you need (RoomSharp.SqlServer, RoomSharp.PostgreSql, RoomSharp.MySql) plus the source generator companion.
  2. Model your entities/DAOs exactly like the sample shown above, then run dotnet build to let the generator emit *Impl classes in your project.
  3. Wire the generated database into your host (minimal API, ASP.NET Core, worker service) via the builder or the RoomSharp.DependencyInjection extensions.
  4. Samples, templates, and the public GitHub repository are on the way—follow the NuGet page to get notified when they drop, and in the meantime keep an eye on the docs folder shipped with the package.

RoomSharp targets net8/net9/net10 today, is licensed under MIT, and is heading toward a public GitHub launch alongside full-text search improvements and a re-enabled test suite. Feedback, contributions, and real-world stories are very welcome—especially if you’ve been wanting a Room-style experience in the .NET ecosystem.

Give it a spin, tell me what hurts, and let’s build the data layer we’ve always wanted in C#.

Top comments (0)