0

I've declared a System.Timers.Timer inside an Api Controller.

Next there is an Action that gets called by a Javascript client and its task is to make every second an HTTP GET request to an external server which sends back a JSON.

Then the JSON gets sent to the Javascript client via WebSocket.

Also I've created another Action that stops the timer when being called.

[Route("api")]
[ApiController]
public class PositionController : ControllerBase
{

    private System.Timers.Timer aTimer = new System.Timers.Timer();

    // ...

    // GET api/position/state
    [HttpGet("[controller]/[action]")]
    public async Task<string> StateAsync()
    {

        try
        {
            Console.WriteLine("In StateAsync (GET)");
            string json = "timer started";

            aTimer.Elapsed += new ElapsedEventHandler(async (sender, args) =>
            {

                json = await Networking.SendGetRequestAsync("www.example.com");

                Console.WriteLine($"Json in response:");
                Console.WriteLine(json);

                await _hubContext.Clients.All.SendAsync("ReceiveMessage", json);


            });
            aTimer.Interval = 1000;
            aTimer.Enabled = true;

            

            Console.WriteLine("----------------------------------");
            return json;

        }
        catch (HttpRequestException error) // Connection problems
        {
            // ...
        }
    }

    // GET api/position/stopstate
    [HttpGet("[controller]/[action]")]
    public async Task<string> StopStateAsync()
    {

        try
        {
            Console.WriteLine("In StopStateAsync (GET)");
            string json = "timer stopped";

            
            aTimer.Enabled = false;


            Console.WriteLine("----------------------------------");
            return json;

        }
        catch (HttpRequestException error) // Connection problems
        {
            // ...
        }
    }

     // ...

}

The problem is, since ASP.NET Controllers (so .Net Core ones?) gets instancieted for every new request, when I call the Stop timer method, the timer doesn't stop because it's not the right Timer instance. So the system continues to make HTTP requests and Websocket transfers...

Is there a way to save and work on the Timer instance I need to stop from a different Controller instance or can I retrieve the original Controller instance?

Thanks in advance guys :)

JackieNBee
  • 173
  • 3
  • 18
  • 2
    Make `Timer` property `static` – Alexander Aug 18 '20 at 12:36
  • You'd probably be better of having a repository (either in memory (single-instance), or in a database) where you can store these timing related items. When starting a timer it should create some sort of unique identifier, which is returned by the StateAsync action and which should be passed to the StopStateAsync action when called. You can then look up the correct instance, based on the reference identifier. Also, i'd rather not use the timer, but just use a POCO object with a 'Start' and 'End' timestamp. You're not un-subscribing from timer events, which is not a good practise. – Thomas Luijken Aug 18 '20 at 12:38

1 Answers1

1

You should really let your controllers do "controller" things. Running a timer in a controller breaks a controller's pattern.

You should look in to implementing an IHostedService that when injected will maintain a timer.

Here is a quick example:

TimerController.cs

[ApiController, Route("api/[controller]")]
public sealed class TimerController : ControllerBase
{
    private readonly ITimedHostedService _timedHostedService;

    public TimerController(ITimedHostedService timedHostedService)
    {
        _timedHostedService = timedHostedService;
    }

    // Just a tip: Use HttpPost. HttpGet should never change the
    // state of your application. You can accidentally hit a GET,
    // while POST takes a little more finesse to execute.
    [HttpPost, Route("startTimer/{milliseconds}")]
    public IActionResult StartTimer(int milliseconds)
    {
        _timedHostedService.StartTimer(milliseconds);
        return Ok();
    }

    [HttpPost, Route("stopTimer")]
    public IActionResult StopTimer()
    {
        _timedHostedService.StopTimer();
        return Ok();
    }

    [HttpGet, Route("isTimerRunning")]
    public IActionResult IsTimerRunning()
    {
        return Ok(new
        {
            result = _timedHostedService.IsTimerRunning()
        });
    }
}

TimedHostedService.cs

public interface ITimedHostedService
{
    void StartTimer(int milliseconds);
    void StopTimer();
    bool IsTimerRunning();
}

public sealed class TimedHostedService : IHostedService, ITimedHostedService
{
    private static Timer _timer;
    private static readonly object _timerLock = new object();

    public void StartTimer(int milliseconds)
    {
        lock(_timerLock)
        {
            _timer ??= new Timer(_ =>
            {
                // TODO: do your timed work here.
            }, null, 0, milliseconds);
        }
    }

    public bool IsTimerRunning()
    {
        lock(_timerLock)
        {
            return _timer != null;
        }
    }

    public void StopTimer()
    {
        lock(_timerLock)
        {
            _timer?.Change(Timeout.Infinite, Timeout.Infinite);
            _timer?.Dispose();
            _timer = null;
        }
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        StopTimer();
        return Task.CompletedTask;
    }
}

Then, inject it as so:

services.AddHostedService<TimedHostedService>();
services.AddTransient<ITimedHostedService, TimedHostedService>();

I haven't tested this, but it should work as-is.

Andy
  • 12,859
  • 5
  • 41
  • 56