RepoWrapper 1.0.0

dotnet add package RepoWrapper --version 1.0.0
                    
NuGet\Install-Package RepoWrapper -Version 1.0.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="RepoWrapper" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="RepoWrapper" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="RepoWrapper" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add RepoWrapper --version 1.0.0
                    
#r "nuget: RepoWrapper, 1.0.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.
#:package RepoWrapper@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=RepoWrapper&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=RepoWrapper&version=1.0.0
                    
Install as a Cake Tool

RepoWrapper

License: MIT NuGet NuGet Downloads

Auto-generates repository wrapper with lazy initialization using C# source generators. Simplifies Entity Framework Core repository pattern implementation.

🎯 What is RepoWrapper?

RepoWrapper is a NuGet package that eliminates boilerplate code when implementing the Repository pattern with Entity Framework Core.

Problem It Solves

Traditional repository pattern implementation requires:

  1. Manual registration of each repository in DI container
  2. Creating wrapper classes manually
  3. Writing lazy initialization code
  4. Managing multiple repository injections in services
// ❌ Traditional way (lots of boilerplate)
services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<IOrderRepository, OrderRepository>();
services.AddScoped<IPaymentRepository, PaymentRepository>();

public class UserService
{
    private readonly IUserRepository _userRepo;
    private readonly IOrderRepository _orderRepo;
    private readonly IPaymentRepository _paymentRepo;

    public UserService(IUserRepository userRepo, IOrderRepository orderRepo, IPaymentRepository paymentRepo)
    {
        _userRepo = userRepo;
        _orderRepo = orderRepo;
        _paymentRepo = paymentRepo;
    }
}

RepoWrapper Solution

// ✅ With RepoWrapper (one line!)
services.AddRepoWrapper();

public class UserService
{
    private readonly IRepoWrapper _wrapper;

    public UserService(IRepoWrapper wrapper)
    {
        _wrapper = wrapper;
    }

    public async Task GetUserDetails(int id)
    {
        var user = await _wrapper.UserRepo.GetByIdAsync(id);        // Lazy-loaded
        var orders = await _wrapper.OrderRepo.GetByUserIdAsync(id); // Lazy-loaded
        var payments = await _wrapper.PaymentRepo.GetByOrderIdAsync(orders[0].Id); // Lazy-loaded
    }
}

✨ Features

  • 🔍 Auto-Discovery: Automatically discovers repositories by naming convention
  • Lazy Initialization: Repositories are created only when accessed
  • 🎁 Zero Configuration: Works out of the box
  • 📝 Source Generator: Compile-time code generation, no runtime reflection
  • 🔒 Type-Safe: Full IntelliSense support, strongly-typed DbContext (no dynamic)
  • 📦 Scoped Lifetime: Integrated with ASP.NET Core DI
  • 📚 Well-Documented: Comprehensive XML documentation on generated code

📦 Installation

Install the NuGet package:

dotnet add package RepoWrapper

Or via Package Manager Console:

Install-Package RepoWrapper

🚀 Quick Start

Step 1: Define Repositories

Follow the naming convention: I{Name}Repository for interface, {Name}Repository for implementation. Each repository's constructor receives your DbContext.

// Repositories/IUserRepository.cs
public interface IUserRepository
{
    Task<User?> GetByIdAsync(int id);
    Task<List<User>> GetAllAsync();
    Task AddAsync(User user);
    Task UpdateAsync(User user);
    Task DeleteAsync(int id);
}

// Repositories/UserRepository.cs
public class UserRepository : IUserRepository
{
    private readonly AppDbContext _context;

    public UserRepository(AppDbContext context)
    {
        _context = context;
    }

    public async Task<User?> GetByIdAsync(int id)
        => await _context.Users.FindAsync(id);

    public async Task<List<User>> GetAllAsync()
        => await _context.Users.ToListAsync();

    public async Task AddAsync(User user)
    {
        _context.Users.Add(user);
        await _context.SaveChangesAsync();
    }

    public async Task UpdateAsync(User user)
    {
        _context.Users.Update(user);
        await _context.SaveChangesAsync();
    }

    public async Task DeleteAsync(int id)
    {
        var user = await GetByIdAsync(id);
        if (user != null)
        {
            _context.Users.Remove(user);
            await _context.SaveChangesAsync();
        }
    }
}

Step 2: Configure in Program.cs

var builder = WebApplication.CreateBuilder(args);

// Register DbContext
builder.Services.AddDbContext<AppDbContext>(opt =>
    opt.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

// Register RepoWrapper - that's it!
builder.Services.AddRepoWrapper();

var app = builder.Build();
app.Run();

Note: AddRepoWrapper(), IRepoWrapper and RepoWrapper are generated into your project at compile time. They appear in IntelliSense after the first build.

Step 3: Use in Services

public class UserService
{
    private readonly IRepoWrapper _wrapper;

    public UserService(IRepoWrapper wrapper)
    {
        _wrapper = wrapper;
    }

    public async Task GetUserDetails(int id)
    {
        // Repositories are lazy-initialized when accessed
        var user = await _wrapper.UserRepo.GetByIdAsync(id);
        var orders = await _wrapper.OrderRepo.GetByUserIdAsync(id);

        return new { user, orders };
    }
}

📋 Naming Convention

The package auto-discovers repositories using a simple naming convention:

What Pattern Example
Repository Interface I{Name}Repository IUserRepository
Repository Class {Name}Repository UserRepository
Generated Property {Name}Repo UserRepo

Examples

IUserRepository → UserRepository → _wrapper.UserRepo
IOrderRepository → OrderRepository → _wrapper.OrderRepo
IPaymentRepository → PaymentRepository → _wrapper.PaymentRepo
IProductRepository → ProductRepository → _wrapper.ProductRepo

🔄 How It Works

  1. Source Generator Scans: When you build, the source generator scans your project for classes ending with Repository
  2. Auto-Discovery: Finds corresponding I{Name}Repository interfaces
  3. DbContext Discovery: Locates your DbContext (a class deriving from Microsoft.EntityFrameworkCore.DbContext); AppDbContext wins if several exist
  4. Code Generation: Generates:
    • IRepoWrapper interface with properties for each repository
    • RepoWrapper implementation class with lazy initialization and a strongly-typed constructor
    • RepoWrapperExtensions with the AddRepoWrapper() DI registration method
  5. Dependency Injection: AddRepoWrapper() registers IRepoWrapper as Scoped
  6. Usage: Inject IRepoWrapper in your services

Generated Code Example

After building with repositories like UserRepository, OrderRepository, PaymentRepository, the generator creates:

// Generated: IRepoWrapper.g.cs
public interface IRepoWrapper
{
    IOrderRepository OrderRepo { get; }
    IPaymentRepository PaymentRepo { get; }
    IUserRepository UserRepo { get; }
}

// Generated: RepoWrapper.g.cs
public sealed class RepoWrapper : IRepoWrapper
{
    private readonly AppDbContext _context;
    private IUserRepository? _userRepo;
    private IOrderRepository? _orderRepo;
    private IPaymentRepository? _paymentRepo;

    public RepoWrapper(AppDbContext context)
    {
        _context = context ?? throw new ArgumentNullException(nameof(context));
    }

    public IUserRepository UserRepo
        => _userRepo ??= new UserRepository(_context);

    // Similar for OrderRepo and PaymentRepo...
}

💡 Use Cases

Scenario 1: Web API with Multiple Repositories

[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    private readonly IRepoWrapper _wrapper;

    public UsersController(IRepoWrapper wrapper)
    {
        _wrapper = wrapper;
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<UserDto>> GetUser(int id)
    {
        var user = await _wrapper.UserRepo.GetByIdAsync(id);
        if (user == null)
            return NotFound();

        return Ok(MapToDto(user));
    }

    [HttpGet("{id}/orders")]
    public async Task<ActionResult<List<OrderDto>>> GetUserOrders(int id)
    {
        var orders = await _wrapper.OrderRepo.GetByUserIdAsync(id);
        return Ok(orders.Select(MapToDto).ToList());
    }
}

Scenario 2: Background Service

public class OrderProcessingService : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;

    public OrderProcessingService(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using var scope = _serviceProvider.CreateScope();
        var wrapper = scope.ServiceProvider.GetRequiredService<IRepoWrapper>();

        while (!stoppingToken.IsCancellationRequested)
        {
            var pendingOrders = await wrapper.OrderRepo.GetByStatusAsync("Pending");

            foreach (var order in pendingOrders)
            {
                await ProcessOrderAsync(order, wrapper);
            }

            await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
        }
    }

    private async Task ProcessOrderAsync(Order order, IRepoWrapper wrapper)
    {
        // Process payment
        var payment = new Payment { OrderId = order.Id, Amount = order.Amount };
        await wrapper.PaymentRepo.AddAsync(payment);

        // Update order status
        order.Status = "Completed";
        await wrapper.OrderRepo.UpdateAsync(order);
    }
}

🏗️ Layered Architecture Support

Source generators only see the source files of the project they run in. To support classic layered solutions, each project installs the package and the generator emits only the pieces that project can contribute, skipping anything already available from a referenced project:

Project Contains Generated
Application I{Name}Repository interfaces IRepoWrapper (properties typed by the interfaces)
Infrastructure {Name}Repository classes + AppDbContext RepoWrapper + RepoWrapperExtensions (AddRepoWrapper())
Api Controllers + Program.cs nothing — resolves the generated types via project references

The interfaces can live in either Application or Domain — the package just goes in whichever project holds them:

Option A — interfaces in Application          Option B — interfaces in Domain (Domain refs nothing)

Api/                    // refs App, Infra    Api/                    // refs App, Infra
    Controllers/                                   Controllers/
    Program.cs         // AddRepoWrapper()        Program.cs         // AddRepoWrapper()
Application/          // refs Domain → IRepoWrapper  Application/   // refs Domain (no package)
    Handlers/                                      Handlers/        // injects IRepoWrapper
    IUserRepository  ← package here               Domain/          // refs nothing → IRepoWrapper
Infrastructure/       // refs Domain, App → RepoWrapper   Infrastructure/ // refs Domain → RepoWrapper
    AppDbContext.cs                                   AppDbContext.cs
    UserRepository                                   UserRepository
Domain/               // entities only            Domain/           // IUserRepository ← package here

Note: The package must be installed in every project that needs to generate a piece (the project holding the interfaces, and the project holding the repository classes + DbContext). Consumers such as Api do not need the package, only a project reference. Repository interfaces referenced across projects must be public.

A single project containing repositories, interfaces and the DbContext still generates everything in one place, exactly as before.

⚙️ Requirements

  • .NET 8.0 or higher
  • Entity Framework Core 8.0 or higher
  • C# 11 or higher

🐛 Troubleshooting

Issue: IRepoWrapper not found

Cause: The project does not contain (or reference a project containing) any repository classes, repository interfaces or a DbContext, so nothing was generated.

Solution:

  1. Ensure repositories end with Repository (e.g., UserRepository)
  2. Ensure interfaces follow I{Name}Repository pattern (e.g., IUserRepository)
  3. In a layered solution, install the package in the project(s) that contain the interfaces and the repository classes — see Layered Architecture Support
  4. Rebuild the project to trigger source generator
  5. Check obj/Debug/net8.0/generated/ for generated files

Issue: Warning PRW004 — "No repositories found"

Cause: The package is installed but the project has no {Name}Repository classes, no I{Name}Repository interfaces, no DbContext, and the generated types are not available from any referenced project. Nothing was generated.

Solution:

  • Add repositories/interfaces to this project, or
  • Add a project reference to a project that generates them (Application/Infrastructure)

Issue: Warning PRW005 — "Repository interface is not public"

Cause: A repository class implements an interface that lives in another assembly and is not public. The generated RepoWrapper cannot reference it, so the repository was skipped.

Solution:

  • Make the interface public (e.g., public interface IUserRepository)

Issue: AppDbContext not found (warning PRW002)

Cause: DbContext not registered in DI container or not deriving from DbContext.

Solution:

// Make sure to register DbContext BEFORE calling AddRepoWrapper()
builder.Services.AddDbContext<AppDbContext>(opt =>
    opt.UseSqlServer(connectionString));

builder.Services.AddRepoWrapper();

Issue: Repository not appearing in IRepoWrapper (warning PRW001)

Cause: Naming convention not followed or class is abstract.

Solution:

  • Ensure class ends with Repository
  • Ensure class is not abstract
  • Ensure corresponding interface exists: I{ClassName} (e.g., IUserRepository)
  • Rebuild project

Issue: Multiple DbContexts found (error PRW003)

Cause: Several classes derive from DbContext and none is named AppDbContext.

Solution:

  • Rename your primary context to AppDbContext, or
  • Reduce to a single DbContext in the project

📖 API Reference

The following members are generated into your project by the source generator:

AddRepoWrapper()

public static IServiceCollection AddRepoWrapper(
    this IServiceCollection services)

Registers the auto-generated RepoWrapper as a Scoped service.

Prerequisites:

  • A DbContext (e.g., AppDbContext) must be registered via AddDbContext<TDbContext>()
  • Repositories must follow naming convention

Example:

builder.Services.AddDbContext<AppDbContext>(opt => opt.UseSqlServer(connectionString));
builder.Services.AddRepoWrapper();

AddRepoWrapper<TDbContext>()

public static IServiceCollection AddRepoWrapper<TDbContext>(
    this IServiceCollection services)
    where TDbContext : class

Provided for API symmetry. The wrapper's constructor type is fixed at generation time from the DbContext discovered in the compilation, so this overload registers the same generated wrapper.

Example:

builder.Services.AddDbContext<CustomDbContext>(opt => opt.UseSqlServer(connectionString));
builder.Services.AddRepoWrapper<CustomDbContext>();

IRepoWrapper

Generated interface exposing one lazy-initialized property per discovered repository, named {Name}Repo.

RepoWrapper

Generated sealed class implementing IRepoWrapper. Its constructor takes the discovered DbContext type and is resolved by the DI container.

⚠️ Known Limitation: Namespace Collision

The generated RepoWrapper class lives in the global namespace (so no using is needed). C# does not allow a type and a namespace of the same name in the same scope, so your project must not declare namespaces starting with RepoWrapper. (e.g. namespace RepoWrapper.Data;). Use a different root, e.g. App.Data or MyApp.Data.

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.

👤 Author

Ramanan

There are no supported framework assets in this package.

Learn more about Target Frameworks and .NET Standard.

This package has no dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 106 8/17/2026