0

I have .net core 2.1 project. And my repository classes like below. But because of MyDbContext constructor has parameter, I'm getting error like below. When I remove JwtHelper parametrer, it is working perfectly. But, I need adding JwtHelper in MyDbContext.cs for logging auditings. How can I achieve this?

'MyDbContext' must be a non-abstract type with a public parameterless constructor in order to use it as parameter 'TContext' in the generic type or method 'UnitOfWork'

UnitOfWork.cs

public class UnitOfWork<TContext> : IUnitOfWork<TContext> where TContext : DbContext, new()
{ 
    protected readonly DbContext DataContext;

    public UnitOfWork()
    {
        DataContext = new TContext();
    }

    public virtual async Task<int> CompleteAsync()
    {
        return await DataContext.SaveChangesAsync();
    }

    public void Dispose()
    {
        DataContext?.Dispose();
    }
}

IUnitOfWork.cs

public interface IUnitOfWork<U> where U : DbContext
{ 
    Task<int> CompleteAsync();
}

MyRepos.cs

public class MyRepos : UnitOfWork<MyDbContext>, IMyRepos
{
    private IUserRepository userRepo;
    public IUserRepository UserRepo { get { return userRepo ?? (userRepo = new UserRepository(DataContext)); } }
}

IMyRepos.cs

public interface IMyRepos : IUnitOfWork<MyDbContext>
{
  IUserRepository UserRepo { get; }
}

MyDbContext.cs

public class MyDbContext : DbContext
{
    private readonly IJwtHelper jwtHelper;

    public MyDbContext(IJwtHelper jwtHelper) : base()
    {
        this.jwtHelper= jwtHelper;
    }

    public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken))
    {
        var userId=jwtHelper.GetUserId();
        SaveAudits(userId,base.ChangeTracker);
        return (await base.SaveChangesAsync(true, cancellationToken));
    }
}

UserRepository.cs

public class UserRepository : Repository<User>, IUserRepository
{
    private readonly MyDbContext_context;

    public UserRepository(DbContext context) : base(context)
    {
        _context = _context ?? (MyDbContext)context;
    }
}

IUserRepository.cs

public interface IUserRepository : IRepository<User>
{ }

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IJwtHelper, JwtHelper>();
    services.AddScoped<DbContext, MyDbContext>();
    services.AddTransient<IMyRepos, MyRepos>();
}
realist
  • 2,155
  • 12
  • 38
  • 77
  • Do you need the `new()` constraint on `TContext` parameter in the `UnitOfWork` class? If so, you'll need to have a parameterless construction. You can have more than one constructor, btw. – CoolBots Feb 18 '19 at 07:10
  • So, `UnitOfWork` wants to run `DataContext = new TContext();`. Where is it meant to get a `IJwtHelper` from (if this were allowed to be parametrized)? – Damien_The_Unbeliever Feb 18 '19 at 07:40
  • Thanks @CoolBots. I edited my question now by adding content of my UnitOfWork class. Can you check my UnitOfWork class? **Are there any different way `DataContext = new TContext();` ?** If is it possible, then I remove `new()` constraint. – realist Feb 18 '19 at 07:41
  • Based on your code, you need the `new()` constraint, and a parameterless constructor in `TContext`. Perhaps `IJwtHelper` can be supplied some other way - as a property, for instance? Then it can be passed into the constructor of `UnitOfWork`, and set as a property on `TContext` – CoolBots Feb 18 '19 at 07:57
  • Thanks @Damien_The_Unbeliever. I converted my UnitOfWork constructor to `public UnitOfWork(IAuditHelper auditHelper) { this.auditHelper = auditHelper; DataContext = new TContext(auditHelper); }` . But,I'm getting `'TContext': cannot provide arguments when creating an instance of a variable type` error. ` – realist Feb 18 '19 at 07:57
  • 1
    If you can pass parameters to your `UnitOfWork` constructor, just hand it an already constructed `TContext` and eliminate the `new()` constraint and that'll work. – Damien_The_Unbeliever Feb 18 '19 at 08:01
  • Can I pass my JwtHelper from my startup.cs to UnitOfWork or any other way? @Damien_The_Unbeliever – realist Feb 18 '19 at 08:07
  • 1
    Yes, but if you still want `UnitOfWork` to create the instance, passing the helper, you'll need to give it a `Func` helper that can contain the specific `new` code that you cannot have inside the generic. – Damien_The_Unbeliever Feb 18 '19 at 08:09
  • My aim is only logging audits by userId. I edited my question by adding all of my codes to my question. I also added my startup.cs. Where can I add `Func` in my classes ? @Damien_The_Unbeliever – realist Feb 18 '19 at 08:43

2 Answers2

3

The problem is in the constructor of your UnitOfWork:

public UnitOfWork()
{
    DataContext = new TContext();
}

Here you construct a new object of class MyDbContext using a default constructor, but MyDbContext does not have a default constructor.

You decided to make your UnitOfWork very generic. That is great, because this enables you to use our UnitOfWork with all kinds of DbContexts. The only limitation you told your UnitOfWork is that your DbContext should have a default constructor.

A good method would be to create the factory yourself and pass it to the UnitOfWork.

If you don't want, or can't give MyDbContext a default constructor, consider telling your UnitOfWork how it can create one: "Hey unit of work, if you need to create the DbContext that I want you to use, use this function"

In fact, you will be using the factory design pattern

Old fashioned interface method

Step 1: Create a class, with a function Create(), that will create exactly the DbContext that you want to use.

interface IDbContextFactory<TContext>
   where TContext : DbContext
{
    DbContext Create();
}

// The MyDbContextFactory is a factory that upon request will create one MyDbcontext object
// passing the JwtHelper in the constructor of MyDbContext
class MyDbContextFactory : IDbContextFactory<MyDbContext>
{
      public IJwthHelper JwtHelper {get; set;}

      public MyDbContext Create()
      {
           return new MyDbContext(this.JwtHelper);
      }


      DbContext IDbContextFactory<HsysDbContext>.Create()
      {
           throw new NotImplementedException();
      }
  }

Step 2: tell your UnitOfWork how it should create the DbContext.

public class UnitOfWork<TContext> : IUnitOfWork<TContext> where TContext : DbContext 
{ 
    public static IDbContextFactory<TContext> DbContextFactory {get; set;}
    protected readonly DbContext DataContext;

    public UnitOfWork()
    {
        this.DataContext = dbContextFactory.Create();
    }
    ...
}

public void ConfigureServices(IServiceCollection services)
{
    // create a factory that creates MyDbContexts, with a specific JwtHelper
    IJwtHelper jwtHelper = ...
    var factory = new MyDbContextFactory
    {
         JwtHelper = jwtHelper,
    }

    // Tell the UnitOfWork class that whenever it has to create a MyDbContext
    // it should use this factory
    UnitOfWork<MyDbContext>.DbContextFactory = factory;

    ... // etc
}

From now on, whenever a UnitOfWork<MyDbContext> object is constructed, using the default constructor, this constructor will order the factory to create a new MyDbContext.

Lambda expression

You don't really have to implement an interface. All that your UnitOfWork needs to know is how to create a DbContext.

Instead of an interface, you could pass it a Func:

public class UnitOfWork<TContext> : IUnitOfWork<TContext> where TContext : DbContext 
{ 
    // use this function to create a DbContext:
    public static Func<TContext> CreateDbContextFunction {get; set;}
    protected readonly DbContext DataContext;

    public UnitOfWork()
    {
        // call the CreateDbContextFunction. It will create a fresh DbContext for you:
        this.DataContext = CreateDbContextFunction();
    }
}

public void ConfigureServices(IServiceCollection services)
{
    // create a factory that creates MyDbContexts, with a specific JwtHelper
    IJwtHelper jwtHelper = ...
    var factory = new MyDbContextFactory
    {
         JwtHelper = jwtHelper,
    }

    // Tell the UnitOfWork class that whenever it has to create a MyDbContext
    // it should use this factory


    UnitOfWork<MyDbContext>.CreateDbContextFunction = () => factory.Create();

Added after comment

The part: () => factory.Create(); in the last statement is called a lambda expression. It means: create a function without input parameters (that is the () part) and a double return value equal to factory.Create().

Bit off topic: explanation of Lambda expression

Similarly if you need to create a lambda expression that represents a function with input parameter a Rectangle and as output the surface of the rectangle:

Func<Rectangle, double> myFunc = (rectangle) => rectangle.X * rectangle.Y;

In words: myFunc is a function that has a Rectangle as input, and a double as output. The function is like:

double MyFunc (Rectangle rectangle)
{
    return rectangle.X * rectangle.Y;
}

You call it like:

Func<Rectangle, double> calcSurface = (rectangle) => rectangle.X * rectangle.Y;
Rectangle r = ...;
double surface = calcSurface(r);

Similarly, a lambda expression that represents a function with two input parameters and one output parameter:

Func<double, double, Rectangle> createRectangle = 
    (width, height) => new Rectangle {Width = width, Height = height};

The last parameter of Func<..., ..., ..., x> is always the return value

And for completeness: a method with a void return is called an Action:

Action(Rectangle) displayRectangle = (r) => this.Form.DrawRectangle(r);
realist
  • 2,155
  • 12
  • 38
  • 77
Harald Coppoolse
  • 28,834
  • 7
  • 67
  • 116
  • Thanks a lot @HaraldCoppoolse. I did your said. But when I add `UnitOfWork.CreateDbContextFunction = factory.Create();` to my `Startup.cs` class, I get error **Cannot implicitly convert type 'MyDbContext' to 'System.Func** – realist Feb 18 '19 at 12:11
  • Thanks a lot @HaraldCoppoolse. This is what I was looking for. You saved my a lot of time. It's worked perfectly. Only disadvantage of this approach is we creating a lot of ContextFactory. For example if we have 8 context, then will be `MyDb1ContextFactory, MyDb2ContextFactory,.....,MyDb8ContextFactory`. So, this is not generic. **Lastly, please add to MyDbContextFactory** `DbContext IDbContextFactory.Create() { throw new NotImplementedException(); }` for people who will see this post in future. Because class require implementation. – realist Feb 18 '19 at 13:25
  • 1
    Well go ahead, edit the question. And of course you can make a generic function that instantiates a factory object that uses the correct constructor function, but that is worth a new question – Harald Coppoolse Feb 18 '19 at 13:50
  • I edited your answer. You are right. I will create new question for that now. – realist Feb 18 '19 at 13:55
  • I create a new question go generic DbContextFactory @HaraldCoppoolse .But, everyone opposed this UnitOfWork approach. They said that, you shouldn't use UnitOfWork. So, anybody don't answer my question. https://stackoverflow.com/questions/54749019/creating-generic-dbcontext-factory-in-entity-framework – realist Feb 19 '19 at 07:09
1

The new() constraint requires a parameterless constructor; however, since you need IJwtHelper in your DbContext, and that property only exists in MyDbContext, you can make your own base class to derive other contexts from instead of DbContext:

public class MyDbContextBase : DbContext
{
    public IJwtHelper JwtHelper { get; set; } 
} 
  • Remove IJwtHelper property from MyDbContext; remove the constructor; make it inherit MyDbContextBase instead of DbContext

  • Change the U constraint on the IUnitOfWork<U> interface to MyDbContextBase

  • Change TContext constraint from DbContext to MyDbContextBase on UnitOfWork<TContext> class; add IJwtHelper as a constructor parameter

  • Once you instantiate a TContext in the constructor of UnitOfWork<TContext> class, assign IJwtHelper through the public property.

CoolBots
  • 4,770
  • 2
  • 16
  • 30
  • I added my all code as answer. I change my IJwtHelper public from private as you said. And I added my `startup.cs.` But, I can't change TContext. Because it is dynamic. Forexample, 2 weeks later, I can create MyRepos2. If, I changed TContext, then I can use only one context. Can you edit my answer @CoolBots? – realist Feb 18 '19 at 08:25
  • 1
    You can make `MyDbContext` a base class - just a `DbContext` with `IJwtHelper` property. You can then create sub-classes as needed, such as `class MyDbContext2 : MyDbContext` – CoolBots Feb 18 '19 at 08:34
  • Ok. I changed my question and I deleted my answer. @CoolBots – realist Feb 18 '19 at 08:40
  • `class MyDbContext2 : MyDbContext` can be incorrrect. Because, MyDbContext2 and MyDbContext very different context. So, inherit from MyDbContext can be problem in future of project @CoolBots – realist Feb 18 '19 at 08:46
  • 1
    I clarified my answer – CoolBots Feb 18 '19 at 08:56
  • Thanks a lot for your helps @CoolBots. But, I can't achieve that way because of JwtHelper injection. HaraldCoppoolse's DbContextFactory solution is worked. – realist Feb 18 '19 at 13:32