Наследовать от общего класса

У меня есть проект .net core 2.1. И мои классы репозитория, как показано ниже. Но из-за того, что конструктор MyDbContext имеет параметр, я получаю сообщение об ошибке, как показано ниже. Когда я удаляю параметр JwtHelper, он работает отлично. Но мне нужно добавить JwtHelper в MyDbContext.cs для ведения журналов аудита. Как этого добиться?

«MyDbContext» должен быть неабстрактным типом с общедоступным конструктором без параметров, чтобы использовать его в качестве параметра «TContext» в универсальном типе или методе «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>();
}

person realist    schedule 18.02.2019    source источник
comment
Вам нужно ограничение new() для параметра TContext в классе UnitOfWork<TContext>? Если это так, вам понадобится конструкция без параметров. У вас может быть более одного конструктора, кстати.   -  person CoolBots    schedule 18.02.2019
comment
Итак, UnitOfWork хочет запустить DataContext = new TContext();. Откуда он должен получить IJwtHelper (если это можно параметризовать)?   -  person Damien_The_Unbeliever    schedule 18.02.2019
comment
Спасибо @CoolBots. Сейчас я отредактировал свой вопрос, добавив содержимое моего класса UnitOfWork. Вы можете проверить мой класс UnitOfWork? Есть ли другой способ DataContext = new TContext(); ? Если это возможно, я удаляю ограничение new().   -  person realist    schedule 18.02.2019
comment
Исходя из вашего кода, вам нужно ограничение new() и конструктор без параметров в TContext. Возможно, IJwtHelper можно передать каким-то другим способом, например, как свойство? Затем его можно передать в конструктор UnitOfWork и установить как свойство TContext.   -  person CoolBots    schedule 18.02.2019
comment
Спасибо @Damien_The_Unbeliever. Я преобразовал свой конструктор UnitOfWork в public UnitOfWork(IAuditHelper auditHelper) { this.auditHelper = auditHelper; DataContext = new TContext(auditHelper); } . Но я получаю ошибку 'TContext': cannot provide arguments when creating an instance of a variable type. `   -  person realist    schedule 18.02.2019
comment
Если вы можете передать параметры своему конструктору UnitOfWork, просто передайте ему уже созданный TContext и устраните ограничение new(), и это сработает.   -  person Damien_The_Unbeliever    schedule 18.02.2019
comment
Могу ли я передать свой JwtHelper из файла startup.cs в UnitOfWork или любым другим способом? @Damien_The_Unbeliever   -  person realist    schedule 18.02.2019
comment
Да, но если вы все еще хотите, чтобы UnitOfWork создавал экземпляр, передавая хелпер, вам нужно дать ему Func<IJwtHelper, TContext> хелпер, который может содержать определенный new код, который вы не можете иметь внутри универсального.   -  person Damien_The_Unbeliever    schedule 18.02.2019
comment
Моя цель - регистрировать аудиты только по userId. Я отредактировал свой вопрос, добавив все свои коды в свой вопрос. Я также добавил свой файл startup.cs. Где я могу добавить Func<IJwtHelper, TContext> в свои классы? @Damien_The_Unbeliever   -  person realist    schedule 18.02.2019


Ответы (2)


Проблема в конструкторе вашего UnitOfWork:

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

Здесь вы создаете новый объект класса MyDbContext, используя конструктор по умолчанию, но MyDbContext не имеет конструктора по умолчанию.

Вы решили сделать свой UnitOfWork очень общим. Это здорово, потому что это позволяет вам использовать наш UnitOfWork со всеми видами DbContexts. Единственное ограничение, которое вы указали своему UnitOfWork, заключается в том, что ваш DbContext должен иметь конструктор по умолчанию.

Хорошим методом было бы создать фабрику самостоятельно и передать ее в UnitOfWork.

Если вы не хотите или не можете предоставить MyDbContext конструктор по умолчанию, подумайте о том, чтобы сообщить своему UnitOfWork, как он может его создать: «Эй, единица работы, если вам нужно создать DbContext, который я хочу, чтобы вы использовали, используйте эту функцию. "

Фактически вы будете использовать шаблон проектирования factory.

Старомодный метод интерфейса

Шаг 1: Создайте класс с функцией Create(), которая создаст именно тот DbContext, который вы хотите использовать.

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();
      }
  }

Шаг 2: сообщите вашему UnitOfWork, как он должен создавать 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
}

Отныне всякий раз, когда создается объект UnitOfWork<MyDbContext> с использованием конструктора по умолчанию, этот конструктор будет приказывать фабрике создавать новый MyDbContext.

Лямбда-выражение

Вам действительно не нужно реализовывать интерфейс. Все, что нужно знать вашему UnitOfWork, — это как создать DbContext.

Вместо интерфейса вы можете передать ему 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();

Добавлено после комментария

Часть: () => factory.Create(); в последнем выражении называется лямбда-выражением. Это означает: создать функцию без входных параметров (то есть часть ()) и двойное возвращаемое значение, равное factory.Create().

Немного не по теме: объяснение лямбда-выражения

Точно так же, если вам нужно создать лямбда-выражение, представляющее функцию с входным параметром Rectangle и выдающую поверхность прямоугольника:

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

Другими словами: myFunc — это функция, которая имеет Rectangle на входе и double на выходе. Функция такая:

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

Вы называете это так:

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

Точно так же лямбда-выражение, представляющее функцию с двумя входными параметрами и одним выходным параметром:

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

Последний параметр Func‹..., ..., ..., x> всегда является возвращаемым значением.

И для полноты: метод с возвратом void называется Action:

Action(Rectangle) displayRectangle = (r) => this.Form.DrawRectangle(r);
person Harald Coppoolse    schedule 18.02.2019
comment
Большое спасибо @HaraldCoppoolse. Я сделал ваше сказал. Но когда я добавляю UnitOfWork<MyDbContext>.CreateDbContextFunction = factory.Create(); в свой класс Startup.cs, я получаю сообщение об ошибке Невозможно неявно преобразовать тип MyDbContext в System.Func‹MyDbContext› - person realist; 18.02.2019
comment
Большое спасибо @HaraldCoppoolse. Это то, что я искал. Вы сэкономили мне много времени. Это сработало отлично. Единственным недостатком этого подхода является то, что мы создаем много ContextFactory. Например если у нас 8 контекстов, то будет MyDb1ContextFactory, MyDb2ContextFactory,.....,MyDb8ContextFactory. Так что это не универсал. Наконец, добавьте в MyDbContextFactory DbContext IDbContextFactory<HsysDbContext>.Create() { throw new NotImplementedException(); } для людей, которые увидят это сообщение в будущем. Потому что класс требует реализации. - person realist; 18.02.2019
comment
Ну давай, отредактируй вопрос. И, конечно, вы можете создать общую функцию, которая создает экземпляр фабричного объекта, который использует правильную функцию конструктора, но это стоит нового вопроса. - person Harald Coppoolse; 18.02.2019
comment
Я отредактировал ваш ответ. Ты прав. Я создам новый вопрос для этого сейчас. - person realist; 18.02.2019
comment
Я создаю новый вопрос и задаю общий вопрос DbContextFactory @HaraldCoppoolse. Но все выступили против этого подхода UnitOfWork. Они сказали, что вам не следует использовать UnitOfWork. Так что никто не отвечает на мой вопрос. stackoverflow.com/questions/54749019 / - person realist; 19.02.2019

Ограничение new() требует конструктора без параметров; однако, поскольку вам нужно IJwtHelper в вашем DbContext, а это свойство существует только в MyDbContext, вы можете создать свой собственный базовый класс для получения других контекстов вместо DbContext:

public class MyDbContextBase : DbContext
{
    public IJwtHelper JwtHelper { get; set; } 
} 
  • Удалить свойство IJwtHelper из MyDbContext; удалить конструктор; заставить его наследовать MyDbContextBase вместо DbContext

  • Измените ограничение U на интерфейсе IUnitOfWork<U> на MyDbContextBase

  • Измените ограничение TContext с DbContext на MyDbContextBase в классе UnitOfWork<TContext>; добавить IJwtHelper в качестве параметра конструктора

  • После создания экземпляра TContext в конструкторе класса UnitOfWork<TContext> назначьте IJwtHelper через общедоступное свойство.

person CoolBots    schedule 18.02.2019
comment
Я добавил весь свой код в качестве ответа. Как вы сказали, я меняю общедоступный IJwtHelper с частного. И я добавил свой startup.cs. Но я не могу изменить TContext. Потому что он динамичен. Например, через 2 недели я могу создать MyRepos2. Если я изменил TContext, то я могу использовать только один контекст. Можете ли вы отредактировать мой ответ @CoolBots? - person realist; 18.02.2019
comment
Вы можете сделать MyDbContext базовым классом — просто DbContext со свойством IJwtHelper. Затем вы можете создавать подклассы по мере необходимости, например class MyDbContext2 : MyDbContext - person CoolBots; 18.02.2019
comment
Ok. Я изменил свой вопрос и удалил свой ответ. @CoolBots - person realist; 18.02.2019
comment
class MyDbContext2 : MyDbContext может быть неверным. Потому что MyDbContext2 и MyDbContext очень разные по контексту. Таким образом, наследование от MyDbContext может стать проблемой в будущем проекта @CoolBots. - person realist; 18.02.2019
comment
я уточнил свой ответ - person CoolBots; 18.02.2019
comment
Большое спасибо за вашу помощь @CoolBots. Но я не могу добиться этого из-за инъекции JwtHelper. Решение HaraldCoppoolse DbContextFactory работает. - person realist; 18.02.2019