Skip to content

Compared with the Microsoft scaffolder

Simon Hughes edited this page Sep 10, 2026 · 3 revisions

Entity Framework Core ships its own reverse engineer: dotnet ef dbcontext scaffold on the command line, or Scaffold-DbContext in the Package Manager Console. It is free, Microsoft maintains it, but lacks some functionality. This page is for deciding whether this generator is worth paying for on yours.

Checked against EF Core 10 in September 2026.

The code blocks on this page are generated by the test suite from a small fixture schema, not typed by hand, so they show exactly what the current release produces.

The short version

dotnet ef dbcontext scaffold This generator
Entity classes and a DbContext Yes Yes
Fluent configuration One OnModelCreating method for the whole database One IEntityTypeConfiguration<T> class per entity, or data annotations
Stored procedures No Typed callers: sync, async, return code, and a return model per procedure
Table-valued and scalar functions No Yes
Interface on the context No IMyDbContext, so the context can be injected and substituted
Fake context for unit tests No Yes. FakeMyDbContextName, generated alongside the real one
Enumerations from lookup tables No Yes, from any table with a name and value column
Choosing what to generate --table and --schema include lists Regex include and exclude filters on schemas, tables, columns and procedures, custom filter classes, and a GUI picker in Visual Studio
Naming Pluralise or not, database names or not Per-table, per-column and per-procedure rename callbacks in C#, plus the usual switches
Customising the output Install its T4 templates and edit them; you then own the templates Settings and callbacks in your .tt; the templates stay upgradeable
Regenerating Re-run the command; --force overwrites Save the .tt; output is deterministic, so the diff in source control is only what changed in the database
Entity Framework 6 No Yes, from the same template
Databases Any EF Core provider with design-time support SQL Server, PostgreSQL, MySQL, MariaDB, Oracle, SQLite
Runs on Windows, macOS and Linux, no IDE needed Visual Studio 2022 or later, or JetBrains Rider, on Windows
Price Free Licence for commercial use; free for academic use

Both need a global dotnet tool installed: dotnet-ef plus a design-time package in your project for the scaffolder, efrpg for this generator.

What you cannot get from the free scaffolder

Stored procedures and functions

The Microsoft scaffolder reads tables and views and stops. Every stored procedure call in a scaffolded project is hand-written: a FromSqlRaw or ExecuteSqlRaw, a SqlParameter per argument, and a class for the result that you keep in step with the procedure yourself.

This generator reads the procedure's parameters and result set and writes that code for you. For a procedure dbo.GetStudentsByCourse @CourseId int, the efrpg generator creates:

public List<GetStudentsByCourseReturnModel> GetStudentsByCourse(int? courseId, out int procResult)
{
    var courseIdParam = new SqlParameter
    {
        ParameterName = "@CourseId",
        SqlDbType = SqlDbType.Int,
        Direction = ParameterDirection.Input,
        Value = courseId.GetValueOrDefault(),
        Precision = 10,
        Scale = 0
    };

    if (!courseId.HasValue)
        courseIdParam.Value = DBNull.Value;

    var procResultParam = new SqlParameter
    {
        ParameterName = "@procResult",
        SqlDbType = SqlDbType.Int,
        Direction = ParameterDirection.Output
    };
    
    const string sqlCommand = "EXEC @procResult = [dbo].[GetStudentsByCourse] @CourseId";
    var procResultData = Set<GetStudentsByCourseReturnModel>()
        .FromSqlRaw(sqlCommand, courseIdParam, procResultParam)
        .ToList();

    procResult = (int) procResultParam.Value;
    return procResultData;
}

plus an overload without the return code and an Async version, and a return model shaped from the result set:

public class GetStudentsByCourseReturnModel
{
    public int StudentId { get; set; }
    public string StudentName { get; set; }
}

When someone adds a column to the procedure's SELECT, you save the .tt and the model follows. Table-valued and scalar functions get callers the same way; turn them on with FilterSettings.IncludeTableValuedFunctions and FilterSettings.IncludeScalarValuedFunctions (see Filtering). Procedures returning several result sets are supported on EF6; EF Core cannot materialise them, and the generator says so in a comment rather than generating a caller that fails at runtime.

An interface and a fake context

The scaffolded DbContext is a concrete class with no interface, so code that takes it as a dependency can only be tested against a real database, the in-memory provider, or SQLite in memory. This generator writes an interface over the context, including the stored procedure callers:

    public interface IMyDbContext : IDisposable
    {
        DbSet<ActiveStudent> ActiveStudents { get; set; } // ActiveStudent
        DbSet<Course> Courses { get; set; } // Course
        DbSet<Document> Documents { get; set; } // Document
        DbSet<OrderLineItem> OrderLineItems { get; set; } // order_line_item
        DbSet<Student> Students { get; set; } // Student
        DbSet<StudentCourse> StudentCourses { get; set; } // StudentCourse

        // ... SaveChanges, Set<TEntity>, Add, Attach, Entry, Find, Remove and Update, as on DbContext

        // Stored Procedures
        // GetCourseReportReturnModel GetCourseReport(int? courseId); Cannot be created as EF Core does not yet support stored procedures with multiple result sets.
        // Task<GetCourseReportReturnModel> GetCourseReportAsync(int? courseId); Cannot be created as EF Core does not yet support stored procedures with multiple result sets.

        List<GetStudentsByCourseReturnModel> GetStudentsByCourse(int? courseId);
        List<GetStudentsByCourseReturnModel> GetStudentsByCourse(int? courseId, out int procResult);
        Task<List<GetStudentsByCourseReturnModel>> GetStudentsByCourseAsync(int? courseId, CancellationToken cancellationToken = default(CancellationToken));

        List<sales_GetOrderTotalsReturnModel> sales_GetOrderTotals(int? year);
        List<sales_GetOrderTotalsReturnModel> sales_GetOrderTotals(int? year, out int procResult);
        Task<List<sales_GetOrderTotalsReturnModel>> sales_GetOrderTotalsAsync(int? year, CancellationToken cancellationToken = default(CancellationToken));

    }

and, with Settings.AddUnitTestingDbContext = true, a fake that implements it with an in-memory FakeDbSet<T> per table and a stub for every stored procedure, so a repository or service can be unit tested with no database at all:

    public class FakeMyDbContext : IMyDbContext
    {
        public DbSet<ActiveStudent> ActiveStudents { get; set; } = null!; // ActiveStudent
        public DbSet<Course> Courses { get; set; } = null!; // Course
        public DbSet<Document> Documents { get; set; } = null!; // Document
        public DbSet<OrderLineItem> OrderLineItems { get; set; } = null!; // order_line_item
        public DbSet<Student> Students { get; set; } = null!; // Student
        public DbSet<StudentCourse> StudentCourses { get; set; } = null!; // StudentCourse

        public FakeMyDbContext()
        {
            _shim     = new FakeDbContextShim();
            _database = new FakeDatabaseFacade(_shim);

            ActiveStudents = new FakeDbSet<ActiveStudent>();
            Courses = new FakeDbSet<Course>("CourseId");
            Documents = new FakeDbSet<Document>("DocumentId");
            OrderLineItems = new FakeDbSet<OrderLineItem>("OrderLineItemId");
            Students = new FakeDbSet<Student>("StudentId");
            StudentCourses = new FakeDbSet<StudentCourse>("StudentId", "CourseId");

        }

        public int SaveChangesCount { get; private set; }
        public virtual int SaveChanges()
        {
            ++SaveChangesCount;
            return 1;
        }

        // ... every other DbContext member, and a fake caller for each stored procedure
    }

The fake knows each entity's key, so Find works, and it counts SaveChanges calls so a test can assert that the code under test saved. See FakeDbContext for a worked example.

Enumerations from lookup tables

A Status table with Id and Name columns is a C# enum waiting to happen, and the scaffolder gives you an int StatusId and a navigation property instead. This generator reads the rows and writes the enum, and can map the foreign key column to it, so order.Status == OrderStatus.Shipped compiles and the magic numbers disappear. See Enum Generation from Table Data.

Configuration you can find

The scaffolder writes every mapping into one OnModelCreating, which on a database of any size is a method several thousand lines long. This generator writes one IEntityTypeConfiguration<T> class per entity and applies them from OnModelCreating, so the mapping for Product is in ProductConfiguration and nowhere else. Settings.UseDataAnnotations switches to attributes on the entity instead if you prefer that.

Naming and filtering in C#

The scaffolder's naming is a choice between the database names and its own singularised, pluralised versions. This generator applies PascalCase and pluralisation too, and then hands every table, column and procedure to a callback in your .tt where ordinary C# decides the final name, marks a column read-only, changes its type, or drops it. Filtering is the same idea: regular expressions to include or exclude schemas, tables, columns and procedures, or a class of your own for anything a regex cannot say. See Settings Callbacks and Filtering.

Regeneration that shows up as a small diff

The scaffolder regenerates by overwriting, and customisations survive only if you kept them in partial classes. Here the customisations live in the .tt and are re-applied on every save, and the generated file is deterministic, so after a database change the commit shows the new column and nothing else. That is what makes it practical to regenerate on every schema change rather than once at the start of the project.

Trying it

Install the extension and generate against your own database. Without a licence key the generator runs as a trial and limits the output to ten tables (mapping tables do not count), which is enough to see what it produces for your schema, and that is the only comparison that matters. Then get a licence key if it earns its place.

  1. Install the VSIX extension from the Visual Studio Marketplace.

  2. Install the efrpg tool. Once per machine, shared by every project on it:

    dotnet tool install -g Efrpg
    

    Check it is on your PATH with efrpg --help. Restart Visual Studio afterwards - it caches the environment it was started with, so a PATH change made while it was running is invisible to it.

  3. Right-click your project → Add New Item

  4. Search for "reverse poco" in online templates

  5. Name the file (e.g. Database.tt) and click Add

  6. Edit the connection string and settings

  7. Save the file — Database.cs (and related files) are generated automatically

Every time your database changes, re-save the .tt file. Building does not re-run the generator; only saving does.

Clone this wiki locally