2

I am trying to create an instance of a class at runtime and call the method but it's not working for me.

I have a class library where I have a class and method as below :

Class library MyApp.Service:

namespace MyApp.Service.SchedularOperations
{
    public class WeeklyTaskSchedular : ISchedular
    {
        private readonly IDbConnection _dbConnection;
        private readonly IProductService _productService;
        
        public WeeklyTaskSchedular(IDbConnection dbConnection,IProductService productService)
        {
            _dbConnection = dbConnection;
            _productService = productService;
             Frequency = Frequencies.Weekly
        }

        public Frequencies Frequency { get ; set ; }

        public int Process (ScheduleInfo info)
        {
            //process logic 
            return 0; indicate success
        }
    }
}

BackgroundService project:

namespace MyApp.BackgroundSchedularService
{
    public class RunSchedular : BackgroundService
    {
        private readonly ILogger<RunSchedular> _logger;
        private readonly IProductService _productService;

        public RunSchedular(ILogger<RunSchedular> logger, IProductService productService)
        {
            _logger = logger;
            _productService = productService;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                string connectionString = GetThirdPartyConnectionStringFromDatabase();
                string executionClassName = "MyApp.Service.SchedularOperations.WeeklyTaskSchedular"; //coming from database
                IDbConnection connection = new SqlConnection(connectionString);
                object[] constructorArgs = { connection, _productService};
                
                Type type = GetInstance(executionClassName); //getting null
                object instance = Activator.CreateInstance(type,constructorArgs);
                
                object[] methodArgs = { new ScheduleInfo() };
                
                type.GetMethod("Process").Invoke(instance,methodArgs);
                await Task.Delay(1000, stoppingToken);
            }
        }
        
        public Type GetInstance(string strFullyQualifiedName)
        {
            foreach(var asm in AppDomain.CurrentDomain.GetAssemblies())
            {
                Type type = asm.GetType(strFullyQualifiedName);
                if(type !=null)
                    return type;
            }
            return null;
        }
    }
}

public class Program
{
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureServices((hostContext, services) =>
                {
                    services.AddHostedService<RunSchedular>();
                    services.AddTransient<IProductService, ProductService>();
                });
}

Problem is this :

Type type = GetInstance(executionClassName); //getting null

Can someone please help me create an instance of a class at run time with constructor arguments and call the method?

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
I Love Stackoverflow
  • 6,738
  • 20
  • 97
  • 216
  • If your type was in DI, this would be a whole lot easier. – gunr2171 Oct 07 '22 at 16:31
  • @gunr2171 How can I register with DI when the implementation name of class is stored in the database and I need to call different implementation(Weekly,Monthly etc) at run time? – I Love Stackoverflow Oct 07 '22 at 16:33
  • 1
    You can vastly simplify this question. Most of the code you've included has nothing to do with the behavior you're seeing. Clearly the problem is that `GetType("MyApp.Service.SchedularOperations.WeeklyTaskSchedular")` is returning null for all the assemblies in your domain. Maybe try logging the assemblies you're searching to see if the class library assembly hasn't been loaded? If that assembly is loaded, try logging all the types it has declared, and see if your type is in that list? – StriplingWarrior Oct 07 '22 at 16:41
  • FYI : https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.activatorutilities.createinstance?view=dotnet-plat-ext-6.0 – ProgrammingLlama Oct 07 '22 at 16:43
  • @StriplingWarrior AppDomain.CurrentDomain.GetAssemblies() is returning only "System.Private.CoreLib, Version = 6.0.0.0". – I Love Stackoverflow Oct 07 '22 at 16:45
  • 2
    Assemblies won't be included if code referring to them hasn't been accessed IIRC: https://stackoverflow.com/questions/10284861/not-all-assemblies-are-being-loaded-into-appdomain-from-the-bin-folder – ProgrammingLlama Oct 07 '22 at 16:49
  • @ProgrammingLlama Thank you so much for the input but what is IIRC? Can you please guide me little bit? Will really appreciate – I Love Stackoverflow Oct 07 '22 at 18:22
  • IIRC = if I recall correctly. Sorry for the confusion. – ProgrammingLlama Oct 08 '22 at 02:11

6 Answers6

3

The approach seems wrong to me, you should try to use DI to help with this.

Register all the schedulers as services appropriately:

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureServices((hostContext, services) =>
        {
            services.AddTransient<IDbConnection, DbConnection>();

            // Notice here that these schedulars are singletons throughout their execution since they will be created under the singleton hosted service RunSchedular.
            services.AddTransient<ISchedular, WeeklyTaskSchedular>();
            services.AddTransient<ISchedular, DailyTaskSchedular>(); // Made an assumption here this exists also

            services.AddHostedService<RunSchedular>();
        });

Then inject the framework provided IServiceProvider and get the service based on the type matching.

public class RunSchedular : BackgroundService
{
    private readonly IDbConnection _dbConnection;
    private readonly IServiceProvider _serviceProvider;

    public RunSchedular(IDbConnection dbConnection, IServiceProvider provider)
    {
        _dbConnection = dbConnection;
        _serviceProvider = provider;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            string executionClassName = _dbConnection.GetSchedularType();
            ISchedular schedular = _serviceProvider.GetServices<ISchedular>().First(x => x.GetType() == Type.GetType(executionClassName));
            schedular.Process(new ScheduleInfo());
            await Task.Delay(1000, stoppingToken);
        }
    }

DBConnection.cs

public string GetSchedularType()
{
    // Imagine data access stuff
    return "MyApp.Service.SchedularOperations.WeeklyTaskSchedular";
}
Jimenemex
  • 3,104
  • 3
  • 24
  • 56
  • I like the approach of using DI to create the instance. But the shown code creates instances of all implementing types, chooses one and discards the others. Check this answer for factory implementation example when only the required type is created: https://stackoverflow.com/a/73640633/2557855 – Artur Oct 14 '22 at 07:56
3

I assume that the MyApp.Service class library is referenced in your BackgroundService project (since you are able to create an instance of the ScheduleInfo class). In this case I would suggest you use the Type.GetType() instead of walking through loaded assemblies which need some additional steps to ensure that it is loaded before you need it.

Here is the modified RunScheduler:

namespace MyApp.BackgroundSchedularService
{
    public class RunSchedular : BackgroundService
    {
        private readonly ILogger<RunSchedular> _logger;
        private readonly IProductService _productService;

        public RunSchedular(ILogger<RunSchedular> logger, IProductService productService)
        {
            _logger = logger;
            _productService = productService;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                string connectionString = GetThirdPartyConnectionStringFromDatabase();
                string executionClassName = "MyApp.Service.SchedularOperations.WeeklyTaskSchedular, MyApp.Service"; //coming from database
                IDbConnection connection = new SqlConnection(connectionString);
                object[] constructorArgs = { connection, _productService };

                Type type = Type.GetType(executionClassName);
                object instance = Activator.CreateInstance(type, constructorArgs);

                object[] methodArgs = { new ScheduleInfo() };

                type.GetMethod("Process").Invoke(instance, methodArgs);
                await Task.Delay(1000, stoppingToken);
            }
        }
    }
}

Two key changes:

  1. The class name is "MyApp.Service.SchedularOperations.WeeklyTaskSchedular, MyApp.Service" which specifies the assembly (dll) name. Although a fully qualified name shall include version, culture and public key token, this should do for your case.
  2. Type.GetType() is used and you might remove the public Type GetInstance() method.

Other codes remain the same as yours.

victor6510
  • 1,191
  • 8
  • 14
  • Would like to get the instance from DI and not use Activator.CreateInstance. Still upvoted for your kind efforts towards helping me – I Love Stackoverflow Oct 15 '22 at 16:40
  • @ILoveStackoverflow Your example code also uses `Activator.CreateInstance` and you only ask for a solution to get the type, since your code returns null at that point. Thus I think this answer should be marked as correct, if the solution works. If you want to solve it with DI that should be another question. – AlexS Oct 16 '22 at 09:55
2

Fully qualified class name is case sensitive. Your class name "MyApp.Service.SchedularOperations.WeeklyTaskSchedular"; is not match with physical namespaces.

Check fully qualified class name:

string fullyQualifiedName = typeof(WeeklyTaskSchedular).AssemblyQualifiedName;
MD. RAKIB HASAN
  • 3,670
  • 4
  • 22
  • 35
2

I think the problem is on AppDomain.CurrentDomain.GetAssemblies() you could try for the following options -

I think the above links might solve your problem.

akshay
  • 5,811
  • 5
  • 39
  • 58
jo_Veera
  • 293
  • 3
  • 12
2

Inject ISchedularFactory into your RunSchedular class.
Then that can read the chosen schedule from db and return the concrete implementation, e.g. WeeklySchedular/MonthlySchedular

public class SchedularFactory
{
    ISchedular GetSchedular()
    {
        var dbData = "monthly";
        return dbData switch
        {
            "monthly" => new MonthlySchedular(),
            "daily" => new DailySchedular(),
        };
    }
}

public class MonthlySchedular : ISchedular
{
    public void Execute()
    {
        // do monthly tasks
    }
}

public class DailySchedular : ISchedular
{
    public void Execute()
    {
        // do daily tasks
    }
}

public interface ISchedular
{
    public void Execute();
}
ColinM
  • 74
  • 2
0

Your methos is correct. I'm also using that logic types kept in database and creating them at runtime with reflection because of some specific requirements. I think you don't have the necessary assembly.In our design we also keep path data in database to necessary assemblies and search/load from those paths.