简体   繁体   中英

Using Autofac to resolve multiple concrete classes of IRepository<T>?

I'm new in AutoFac and I'm come across two problems that I need to implement in my WPF project using MVVM. I'm using an interface to implement a repository, but I'm going to implement multiple repositories for SQL, XML, and CSV. So my interface has this:

public interface IRepository<T> : IReadOnlyRepository<T>, IWriteOnlyRepository<T>
{
}

// covariance interface
public interface IReadOnlyRepository<out T> : IDisposable
{
    T FindById(int id);
    IEnumerable<T> GetAllRecords();
}

// contravariance interface
public interface IWriteOnlyRepository<in T> : IDisposable
{
    void Add(T item);
    void Delete(T item);    
    int Save(); 
}

public class SQLRepository<T> : IRepository<T>
{
    // implements the interface using Entity Framework
}

public class XMLRepository<T> : IRepository<T>
{
    // implements the interface using XML Serializer/Deserializer
}

public class CSVRepository<T> : IRepository<T>
{
    // Implements the interface for TextReader/TextWriter for CSV Files (Excel)
}

So here's the rub: I was told by the boss that the customer needs to change repositories while still running the program. So I need to dynamically change the repository at run time. The default will be SQL Server, but the client may want to change to XML... WITHOUT losing the data that is already in the repository. The reason behind it is that if they load a configuration from SQL but they want to save it to a XML file and send it to their client, they can do so

-- OR --

They get an XML file from one of their clients, and they want to save the configuration to SQL, they can do so without worrying about re-entering the data.

I solved one problem by using Generics because I'll be using the same POCO data model class and therefore it preserves the data but then:

  1. How do I implement the 3 different concrete repository classes?
  2. How do I pass in the parameter of T?

I thought about using "named services" to differentiate between the concrete repository classes, and a model base class. I then would use a bootstrapper to look like this:

public class BootStrapper
{

    public IContainer BootStrap()
    {
        var builder = new ContainerBuilder();

        builder.RegisterType<MainWindow>.AsSelf();

        builder.RegisterType<MainViewModel>.As<IMainViewModel>();

        //?? How do I resolve T of IRepository<T>?
        builder.RegisterType<SQLRepository>.Named<IRepository>("SQL") 
        builder.RegisterType<XMLRepository>.Named<IRepository>("XML")
        builder.RegisterType<CSVRepository>.Named<IRepository>("CSV")

        return builder.Build();
    }
}

public partial class App : Application
{
    protected override void OnStartUp(StartUpEventArgs e)
    {
        base.OnStartUp(e);
        var bootsrapper = new BootStrapper();
        var container = bootstrapper.BootStrap();

        // ?? How do I set the SQLRepository as default?
        var mainWindow = container.Resolve<MainWindow>(); 
        mainWindow.Show();
    }
 }

Any suggestions?

EDIT: I forgot to add in there that I'm using Dependency Injection on my ViewModels, so therefore, in my MainViewModel:

 public class MainViewModel
 {
      private IRepository<Model> _repository;

      public MainViewModel(IRepository<Model> repo)
      {
            _repository = _repo;
      }
 }

now I did try as suggested that I change the code to this:

 builder.RegisterGeneric(typeof(SQLRepository<>).As(typeof(IRepository<>));
 builder.RegisterGeneric(typeof(XMLRepository<>).As(typeof(IRepository<>));

I then debug the code by stepping into it, and when I hit at MainViewModel constructor, it's giving me XMLRepository class. From what I've read in the documentation for " default registrations ", it will always be XMLRepository and never SQLRepository. I then tried to " open generic decorator registration " like:

 builder.RegisterGeneric(typeof(SQLRepository<>).Named("SQL", typeof(IRepository<>));
 builder.RegisterGeneric(typeof(XMLRepository<>).Named("XML", typeof(IRepository<>));


 builder.RegisterGenericDecorator(typeof(SQLRepository<>), typeof(IRepository<>), fromKey: "SQL");
 builder.RegisterGenericDecorator(typeof(XMLRepository<>), typeof(IRepository<>), fromKey: "XML");     

But then how do I resolve it when I'm trying to use the MainWindow?

UPDATE EDIT #2

Okay, so I was asked by a legitimate question by tdragon about how I wanted this resolved. The MainWindow.xaml.cs file looks like this:

public partial class MainWindow : Window
{
     private MainViewModel _viewModel;

     public MainWindow(MainViewModel viewModel)
     {
          InitializeComponent();

          _viewModel = viewModel;
          DataContext = _viewModel;
     }
}

But the real problem is with the App.xaml.cs file, which I've already gave the code in the in my original question.

There is a Good article here in autofac documentation.

Use the RegisterGeneric() builder method to register generic components as below.

  var builder = new ContainerBuilder();
  builder.RegisterGeneric(typeof(SQLRepository<>));
  builder.RegisterGeneric(typeof(XMLRepository<>));
  builder.RegisterGeneric(typeof(CSVRepository<>));
  builder.RegisterGeneric(typeof(SQLRepository<>))
               .As(typeof(IRepository<>))
               .InstancePerLifetimeScope();
  builder.RegisterGeneric(typeof(XMLRepository<>))
               .As(typeof(IRepository<>))
               .InstancePerLifetimeScope();
  builder.RegisterGeneric(typeof(CSVRepository<>))
               .As(typeof(IRepository<>))
               .InstancePerLifetimeScope();
  builder.Register(c => new Myclass()).OnActivating(
                e =>
                {
                    e.Instance.SqlTaskRepo = e.Context.Resolve<SQLRepository<Task>>();

                }
                );

UPDATED

You can resolve T by scanning assembly instead that would be better way to resolve kindly take a look below code hope it will help you

    builder.RegisterGeneric(typeof(SQLRepository<>));
    builder.RegisterGeneric(typeof(XMLRepository<>));
    builder.RegisterGeneric(typeof(CSVRepository<>));
    var dataAccess = Assembly.GetExecutingAssembly();
    builder.RegisterAssemblyTypes(dataAccess)
         .Where(t => typeof(SQLRepository<>).IsAssignableFrom(t));
    builder.RegisterAssemblyTypes(dataAccess)
             .Where(t => typeof(XMLRepository<>).IsAssignableFrom(t));
    builder.RegisterAssemblyTypes(dataAccess)
             .Where(t => typeof(CSVRepository<>).IsAssignableFrom(t));
    builder.RegisterType<MainViewModel>();

One of possible solution would be to register your repositories using keys, instead of names:

var builder = new ContainerBuilder();

builder.RegisterGeneric(typeof(SqlRepository<>)).Keyed(RepositoryType.Sql, typeof(IRepository<>));
builder.RegisterGeneric(typeof(XmlRepository<>)).Keyed(RepositoryType.Xml, typeof(IRepository<>));
builder.RegisterGeneric(typeof(CsvRepository<>)).Keyed(RepositoryType.Csv, typeof(IRepository<>));

where keys would be some enum values ( string could be used as well, but imho enum is cleaner and less error-prone), eg

enum RepositoryType { Sql, Xml, Csv }

Then, instead of injecting IRepository<Model> which always gives you latest registered dependency, you can inject IIndex<RepositoryType, IRepository<Model>> . Using index operator you can get proper repository type. In addition, you can implement some kind of ConfigurationProvider , where you can store currently selected type of repository, like:

public interface IConfigurationProvider
{
    RepositoryType SelectedRepositoryType { get; set; }
}

public class ConfigurationProvider : IConfigurationProvider
{
    public RepositoryType SelectedRepositoryType
    {
        get { /* read the value from some configuration file */ }
        set { /* store the new value */ }
    }
}

Of course, it should be also registered in the container. You can store this value wherever you want (app.config, any other custom file).

Then, the constructor of MainViewModel would look like this:

public MainViewModel(
    IIndex<RepositoryType, IRepository<Model>> repoIndex, 
    IConfigurationProvider configurationProvider)
{
    var repository = repoIndex[configurationProvider.SelectedRepositoryType];  // would return the repository of currently selected type
}

You can find more details about IIndex in Autofac documentation .

I have to admit, I was a bit floored when I got this answer. I'm posting this answer for anyone else that might have the same problem I've was facing.

Since the solution that was provided to me didn't work right (until tdragon updated his answer), I went to Googlegroups for Autofac and someone else came up with the answer.

However, I have given the credit to tdragon (thanks dude!) for coming up with the IIndex method which is why I put his post as an answer, but I gotten more feedback about it from other sources which is why I'm posting my answer.

I went and contacted Thomas Claudius Huber, the author of two great Pluralsight courses on WPF and MVVM. One was on doing ModelWrappers, and the other was doing unit testing with ViewModels. I strongly suggest those courses to newbies that are trying to refine their WPF and MVVM skills. It was his courses that got me turned on Autofac and it help out tremendously. Thomas and tdragon's solution using IIndexing did help resolve the problem.

But there is an interesting alternative that someone on the Autofac Googlegroup by Alex Meyer-Gleaves. His first alternative was using a Lambda expression which was:

 builder.Register(c => new MainViewModel(c.ResolveNamed<IRepository<Stock>>("XMLrepository"), c.ResolveNamed<IRepository<Vendor>>("SQLRepository"))).AsSelf();

But he also mentioned that starting with Autofac 4.3.0, there was an attribute filter that will help with the issue. First thing I needed to do was add ".WithAttributeFiltering()" when building the container like this:

 public IContainer BootStrap()
 {
      builder.RegisterType<MainViewModel>().AsSelf().WithAttributeFiltering();
      builder.RegisterType<MainView>().AsSelf();
      builder.RegisterGeneric(typeof(XMLRepository<>)).Keyed("XMLRepository", typeof(IRepository<>));
      builder.RegisterGeneric(typeof(SQLRepository<>)).Keyed("SQLRepository", typeof(IRepository<>));

      return builder.Build();
 }

Then in the constructor, you can do this:

   public MainViewModel([KeyFilter("XMLRepository")]IRepository<Stock> stockRepo, 
                        [KeyFilter("XMLRepository")]IRepository<Vendor> vendorRepo)
  { ... // code here }

Thanks guys for all your help!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM