0

I have more DbContext's. I want to use just one GenericRepository. I try to create a GenericDbContextFactory. But how can I create TContext? What do I have to do, so the context is not null?

public class GenericRepository<TTable, TContext> : IGenericRepository<TTable, TContext>
    where TTable : class
    where TContext : DbContext
{
    private TContext _context { get; set; } = default!;
    private IGenericDbContextFactory _contextFactory;
    private DbSet<TTable> _table { get; set; } = default!;
    private readonly string _connectionString;

    public GenericRepository(IGenericDbContextFactory contextFactory, string connectionString)
    {
        _connectionString = connectionString;
        _contextFactory = contextFactory;
        _context = GetNew();
    }

    public virtual void Reset()
    {
        _context.Dispose();
        _context = GetNew();
    }

    public TContext GetNew()
    {
        var context = _contextFactory.Create(_connectionString) as TContext;
        _table = context.Set<TTable>();

        return context;
    }

    public async Task Save()
    {
        try
        {
            await _context.SaveChangesAsync();
        }
        catch (Exception ex)
        {
            Reset();
            throw new Exception(ex.Message);
        }

        _context.ChangeTracker.Clear();
    }


public class GenericDbContextFactory : IGenericDbContextFactory
{
    public DbContext Create(string connectionString)
    {
        if (!string.IsNullOrEmpty(connectionString))
        {
            var optionsBuilder = new DbContextOptionsBuilder<DbContext>();
            optionsBuilder.UseSqlServer(connectionString);
            var context = new DbContext(optionsBuilder.Options);
            return context;
        }
        else
        {
            throw new ArgumentNullException("ConnectionId");
        }
    }
}
user1531040
  • 2,143
  • 6
  • 28
  • 48
  • Can you show the class `GenericDbContext`? – vernou Oct 26 '22 at 11:51
  • That's depends on DbContext. I shall add it. – user1531040 Oct 26 '22 at 11:54
  • It's Entity Framework Core? If yes, what version? – vernou Oct 26 '22 at 11:56
  • Yes EF & Dependency Injection – user1531040 Oct 26 '22 at 11:57
  • What version? EF Core or EF 6? Can you add the corresponding tags? – vernou Oct 26 '22 at 11:59
  • You have mixed reference to the old alias `Entity Framework 6` and the new alias `Entity Framework Core`. See [Compare EF Core & EF6](https://learn.microsoft.com/en-us/ef/efcore-and-ef6/) – vernou Oct 26 '22 at 12:05
  • You need uninstall the package `EntityFramework`. – vernou Oct 26 '22 at 12:05
  • And than it will work? – user1531040 Oct 26 '22 at 12:10
  • Maybe... The `DbContext` class can be from the package `EntityFramework` or `Microsoft.EntityFrameworkCore.SqlServer`. But it's a welcome cleaning. – vernou Oct 26 '22 at 12:14
  • 1
    DbContext is already generic. It's a multi-entity Repository and Unit-of-Work. All entities are defines as single-entity DbSet repositories. There's no need for a `GenericRepository` like this, it actually *breaks* the Unit-of-Work pattern – Panagiotis Kanavos Oct 26 '22 at 12:24
  • The reason this code breaks UoW is because DbContext already detects changes to *all* the entities it tracks. These are only persisted when `SaveChanges` is called, thus providing UoW semantics. Until that moment, DbContext doesn't even keep a connection to the database. `SaveChanges` persists all changes in a single internal database transaction. If you want to "rollback" just dispose the DbContext – Panagiotis Kanavos Oct 26 '22 at 12:26
  • The question's code on the other hand can't roll back anything. It does leak DbContext instances and their tracked objects though, because `GenericRepository` doesn't implement IDisposable and therefore doesn't dispose the DbContext it wraps – Panagiotis Kanavos Oct 26 '22 at 12:27
  • With a single defined DbContext, the code works fine. But then I have to create for all DbContext objects a new GenericRepository. – user1531040 Oct 26 '22 at 12:36

1 Answers1

1

GetNew return null (throw NullReferenceExceptioninstead?) because :

public TContext GetNew()
{
    // GenericDbContextFactory.Create return GenericDbContext
    GenericDbContext genericDbContext = _contextFactory.Create(_connectionString);
    // GenericDbContext isn't TContext, then as operator return null
    // context is set with null
    var context = genericDbContext as TContext;
    // throw NullReference Exception
    _table = context.Set<TTable>();
    return context;
}

To resolve this, you need GenericDbContextFactory.Create return TContext instance. But it isn't possible to have generic constrain with constructor parameters. One solution :

public class GenericRepository<TTable, TContext> : IGenericRepository<TTable, TContext>
    where TTable : class
    where TContext : GenericDbContext, new()
{
    public TContext GetNew()
    {
        var context = _contextFactory.Create<TContext>(_connectionString);
        _table = context.Set<TTable>();
        return context;
    }

    ...
}

public class GenericDbContextFactory : IGenericDbContextFactory
{
    public TContext Create<TContext>(string connectionString) where TContext : GenericDbContext, new()
    {
        if (!string.IsNullOrEmpty(connectionString))
        {
            var context = new TContext();
            context.Initialize(connectionString);
            return context;
        }
        ...
    }
}

public abstract class GenericDbContext : DbContext
{
    private string _connectionString;

    public GenericDbContext()
    { }

    public abstract void Initialize(string connectionString)
        => _connectionString = connectionString;

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        if (string.IsNullOrEmpty(_connectionString))
            throw new InvalidOperationException();
        optionsBuilder.UseSqlServer(_connectionString);
    }
}

You can found other possibilities in this other question :

Is there a generic constructor with parameter constraint in C#?


But please read carefully the remark of @PanagiotisKanavos. It's a really bad implementation of the repository pattern, because EF Core isn't hidden.

To use GenericRepository, it need to specify the real DbContext type and DbSet is raw exposed. With a good repository, you only manipulate the repository, without know what is under the hood.

In the majority of cases, EF Core can be use directly like a repository.

vernou
  • 6,818
  • 5
  • 30
  • 58
  • I don't how to specify the real DbContext. That's what I'm searching the whole day. – user1531040 Oct 26 '22 at 14:20
  • The context shows all DbSets. I implement your code in my Repository. – user1531040 Oct 26 '22 at 14:28
  • You want determine TContext from TTable without specify the need to specify TContext? – vernou Oct 26 '22 at 15:55
  • When I am debugging and I hit in GetNew() the "_table = context.DbSet..." I get the error message: System.InvalidOperationException: 'No database provider has been configured for this DbContext. A provider can be configured by overriding the 'DbContext.OnConfiguring' method or by using 'AddDbContext' on the application service provider. If 'AddDbContext' is used, then also ensure that your DbContext type accepts a DbContextOptions object in its constructor and passes it to the base constructor for DbContext.' – user1531040 Oct 27 '22 at 07:31
  • I use AddDbContext (in program.cs) as well OnConfiguring. – user1531040 Oct 27 '22 at 08:20
  • 1
    It's like it don't call `GenericDbContext.OnConfiguring`. In the real DbContext class, is this method overloaded? If yes, you need the first ligne to be `base.OnConfiguring(optionsBuilder);` – vernou Oct 27 '22 at 08:33