Faheem
Faheem

Reputation: 509

Call to WebAPI does not return correctly with Async

I have created a Service which runs on a Client. This Service inserts a Log Entry in a Database on Server. The Log entry is inserted via API which is running on Server. Below is the simplified code inside Service1 class. The Service is Timer based and runs repeatedly when needed. Hence, it needs to insert the Log Entry when needed. i.e. SendToServer() function is executed repeatedly. I have remove the Timer code because it is not relevant here.

public class Logs
{
 public string param1 { get; set; }
 public string param2 { get; set; }
}

static HttpClient client = new HttpClient();
System.Timers.Timer timer = new System.Timers.Timer(); //New Edit

protected override void OnStart(string[] args)
{
 SendToServer();
 timer.Elapsed += new ElapsedEventHandler(OnElapsedTime);//New Edit
 timer.Interval = 60000; //New Edit
 timer.Enabled = true; //NewEdit
}

private void OnElapsedTime(object source, ElapsedEventArgs e)//New Edit
{
  SendToServer();
}

public void SendToServer()
{
 RunAsync().GetAwaiter().GetResult();
}

static async Task RunAsync(EventLogEntry Log)
{
 client.BaseAddress = new Uri("https://<IP>:<Port>/");
 client.DefaultRequestHeaders.Accept.Clear();
 client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

 Logs log = new Logs
 {
  param1 = "value1";
  param2 = "value2";
 };
 var url = await CreateLogsAsync(log);
}

static async Task<Uri> CreateLogsAsync(Logs log)
{
 ServicePointManager.ServerCertificateValidationCallback = delegate (object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
 {
  return true;
 };
 HttpResponseMessage response = await client.PostAsync("api/Logs", new StringContent(new JavaScriptSerializer().Serialize(log), Encoding.UTF8, "application/json"));
 response.EnsureSuccessStatusCode();
 return response.Headers.Location;
}

When I install the service or power up the Client Machine. The first database insert works fine. However, the data is not inserted in the database the second time. When I restart the system it again inserts the first log. I sense that there is problem with ASync Functions and the first call to API never returns correctly after inserting the data.

The similar code works perfectly in Console Application.

Upvotes: 1

Views: 372

Answers (1)

Matt Davis
Matt Davis

Reputation: 46044

The problem is that your Windows Service is shutting down after executing this line:

RunAsync().GetAwaiter().GetResult();

The OnStart() method needs to do something that keeps the service running, or it will shutdown when OnStart() returns. However, you can't put a perpetual loop, like while (true), in the OnStart() method because the system calls OnStart() as a callback to your code, and it expects OnStart() to return in a timely manner. Likewise, starting a Task or initiating anything on the ThreadPool won't work (as you've seen) because these run as background threads, and background threads are stopped automatically when the application stops. From the link:

A thread is either a background thread or a foreground thread. Background threads are identical to foreground threads, except that background threads do not prevent a process from terminating. Once all foreground threads belonging to a process have terminated, the common language runtime ends the process. Any remaining background threads are stopped and do not complete.

To solve the problem, you need to start a foreground thread. Here's a very rough example that should solve the immediate problem. You'll need tweak it to do what you want it to do.

using System.Threading;

public sealed class MyService : ServiceBase
{
    private Thread           _thread;
    private ManualResetEvent _shutdownEvent = new ManualResetEvent(false);

    protected override void OnStart(string[] args)
    {
        // Create the thread object and tell it to execute the Run method
        _thread = new Thread(Run);

        // Name the thread so it is identifyable
        _thread.Name = "MyService Thread";

        // Set the thread to be a foreground thread (keeps the service running)
        _thread.IsBackground = false;

        // Start the thread
        _thread.Start();
    }

    protected override void OnStop()
    {
        // Set the shutdown event to tell the foreground thread to exit
        // the while loop and return out of the Run() method
        _shutdownEvent.Set();

        // Wait for the foreground thread to exit
        _thread.Join();
    }

    private void Run()
    {
        // The while loop keeps the foreground thread active by executing
        // over and over again until Stop() sets the shutdown event, which
        // triggers the while loop to exit.
        while (!_shutdownEvent.WaitOne(1000))
        {
            // Put your logic here.
            RunAsync().GetAwaiter().GetResult();
        }
    }
}

Note that the while loop in the Run() method will run over and over again until the shutdown event is set (see the Stop() method). The argument to the WaitOne() call is in milliseconds. As coded, the WaitOne() call will block for 1 second before executing your code again. You'll need to tweak this so that your code runs when you want it to.

HTH

Upvotes: 1

Related Questions