0

I am creating a scheduled Task in ASP.NET Core WebAPI. I am trying to get 5 URLs from appsettings.json which are not in a section, just in the root of appsettings.json. If I hardcode the strings in the Final class (mentioned in the last of this question), it works fine, but

This is my Program.cs:

    public static void Main(string[] args)
    {
        var configuration = GetConfiguration();
        var host = BuildWebHost(configuration, args);
        host.Run();
        //CreateWebHostBuilder(configuration,args).Build().Run();
    }

    public static IWebHostBuilder CreateWebHostBuilder(IConfiguration _configuration,string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>()
            .UseConfiguration(_configuration);

    private static IConfiguration GetConfiguration()
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
            .AddEnvironmentVariables();

        return builder.Build();
    }

Following is my Startup.cs:

    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.Configure<ScheduledTaskSettings>(Configuration);
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        services.AddSingleton<IOrderSchedulerTask, OrderSchedulerTask>();
        services.AddScheduler((sender, args) =>
        {
            Console.Write(args.Exception.Message);
            args.SetObserved();
        });
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.UseMvc();
    }

I have also created ScheduledTaskSettings.cs with the following class. The string names are identical to the names in appsettings.json.

public class ScheduledTaskSettings
{
    public string ConnectionString { get; set; }

    public string CustomersApiEndpointUrl { get; set; }

    public string SubscriptionsApiEndpointUrl { get; set; }

    public string ProductsApiEndpointUrl { get; set; }

    public string SubscribersApiEndpointUrl { get; set; }

    public string CronScheduleTime { get; set; }
}

I have an interface IOrderSchedulerTask.cs:

public interface IOrderSchedulerTask
{
    string Schedule { get; }
    Task ExecuteAsync(CancellationToken cancellationToken);
}

And the final class OrderSchedulerTask.cs

public class OrderSchedulerTask : IOrderSchedulerTask
{

    // Implementing IOrderScheduler 
    // Scheduled for every one minute
    public string Schedule => "*/1 * * * *";

    public async Task ExecuteAsync(CancellationToken cancellationToken)
    {
        var httpClient = new HttpClient();
        OrderScheduler.GenerateOrders();
    }
}

public class OrderScheduler
{
    public static ScheduledTaskSettings _scheduledTaskSettngs;
    public OrderScheduler(ScheduledTaskSettings scheduledTaskSettings)
    {
        _scheduledTaskSettngs = scheduledTaskSettings;
    }

private static string CustomerApiUrl = _scheduledTaskSettngs.CustomersApiEndpointUrl;
    private static string SubscriptionApiUrl = _scheduledTaskSettngs.SubscriptionsApiEndpointUrl;
    private static string CatalogApiUrl = _scheduledTaskSettngs.ProductsApiEndpointUrl;
    private static string SubscriberUrl = _scheduledTaskSettngs.SubscribersApiEndpointUrl;
    private static string _connectionString = _scheduledTaskSettngs.ConnectionString;

    private static List<Customer> customers = null;
    private static List<Subscription> subscriptions = null;
    private static List<CatalogItem> products = null;
    private static List<Subscriber> subscribers = null;
    private static List<Order> orders = new List<Order>();

    /// <summary>
    /// Generate the orders for next day
    /// </summary>
    public static async void GenerateOrders()
    {
        // Get Customers Data
        GetCustomers();

        //Get Subscription Data
        GetSubscriptions();

        //Get Products Data
        GetProducts();

        //Get Subscribers Data
        GetSubscribers();

        var order = new Order();
        using (var connection = new SqlConnection(_connectionString))
        {
            connection.Open();
            foreach (var item in subscriptions)
            {
                //
            }
        }

    }
    private static async void GetCustomers()
    {
        //Uses CustomerApiUrl String
    }
    private static async void GetSubscriptions()
    {
         //Uses SubscriptionApiUrl String
    }
    private static async void GetProducts()
    {
        //Uses CatalogApiUrl String
    }

    private static async void GetSubscribers()
    {
    //Uses SubscriberUrl String
    }

}

If I hardcode the strings, it works fine, but it throws the following exception when I use the configuration method:

Unhandled Exception:
Unhandled Exception: 
Unhandled Exception:
Unhandled Exception: System.TypeInitializationException: The type initializer for 
'Ordering.ScheduledTask.Tasks.OrderScheduler' threw an exception. ---> System.NullReferenceException: Object reference not set to an instance of an object.
   at Ordering.ScheduledTask.Tasks.OrderScheduler..cctor() in ... OrderSchedulerTask.cs  Line 22

Can someone please guide me through about what should I be doing to correct it.

1 Answer 1

3

There are a few issues I see with your code:

  1. Why are you creating a custom ConfigurationBuilder in your Program.cs? The default builder that you create with WebHost.CreateDefaultBuilder will already set up a configuration that uses both appsettings.json and appsettings.<Environment>.json, as well as other sources like the environment variables or command line arguments.

    So there is no need to create a custom configuration and I would advise you against creating one unless you actually have a good reason to do so.

  2. services.Configure<ScheduledTaskSettings>(Configuration) is a method of the Options framework. You configure an IOptions<ScheduledTaskSettings> that way. So in order to consume that, you will have to inject IOptions<ScheduledTaskSettings> into your OrderScheduler; not just ScheduledTaskSettings.

  3. In OrderScheduler, you inject the task settings (again, this should be options), but you use that to statically set _scheduledTaskSettngs which would be shared between all instances. That is generally a bad idea in applications that make use of DI: You explicitly want this to stay an instance member so that it belongs to the lifetime of the object.

    What’s ultimately causing your errors is the following part:

    private static string CustomerApiUrl = _scheduledTaskSettngs.CustomersApiEndpointUrl;
    private static string SubscriptionApiUrl = _scheduledTaskSettngs.SubscriptionsApiEndpointUrl;
    private static string CatalogApiUrl = _scheduledTaskSettngs.ProductsApiEndpointUrl;
    private static string SubscriberUrl = _scheduledTaskSettngs.SubscribersApiEndpointUrl;
    private static string _connectionString = _scheduledTaskSettngs.ConnectionString;
    

    Since these are all static fields, they will be initialized statically when the type is first used. At that point, no instance will have been created yet, so the static _scheduledTaskSettngs will not be initialized yet. So it’s null, explaining the NullReferenceExceptions you get.

    This approach also prevents you from reacting to changes of that configuration since the values are only read once and then stored statically.

You should make your OrderScheduler a singleton and properly inject it into other things, so that you can use it as an instance that has a well-defined lifetime:

public class OrderScheduler
{
    public readonly ScheduledTaskSettings _scheduledTaskSettings;
    public OrderScheduler(IOptions<ScheduledTaskSettings> scheduledTaskSettings)
    {
        _scheduledTaskSettings = scheduledTaskSettings.Value;
    }

    // using read-only properties instead
    private string CustomerApiUrl => _scheduledTaskSettngs.CustomersApiEndpointUrl;
    private string SubscriptionApiUrl => _scheduledTaskSettngs.SubscriptionsApiEndpointUrl;
    private string CatalogApiUrl => _scheduledTaskSettngs.ProductsApiEndpointUrl;
    private string SubscriberUrl => _scheduledTaskSettngs.SubscribersApiEndpointUrl;
    private string _connectionString => _scheduledTaskSettngs.ConnectionString;

    // …
    // instance members only, no “static”
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.