1

I am running a timed background task to send out emails, and in the email I want to include a generated link.

When I send out other emails via user interactions in the controller, I'm using this little method to generate the link:

public string BuildUrl(string controller, string action, int id)
{
    Uri domain = new Uri(Request.GetDisplayUrl());
    return domain.Host + (domain.IsDefaultPort ? "" : ":" + domain.Port) +
        $@"/{controller}/{action}/{id}";
}

Of course, a background task does not know anything about the Http context, so I would need to replace the domain-part of the link, like this:

public string BuildUrl(string controller, string action, int id)
{
    return aStringPassedInFromSomewhere + $@"/{controller}/{action}/{id}";
}

I'm starting the background task in startup.cs ConfigureServices like this:

services.AddHostedService<ProjectTaskNotifications>();

I was thinking to maybe get the domainname from a resource file, but then I might as well just hard code it into the task method.

Is there some way to pass this information dynamically to the background task?

MORE INFO

Here is the entire background task:

internal class ProjectTaskNotifications : IHostedService, IDisposable
{
    private readonly ILogger _logger;
    private Timer _timer;
    private readonly IServiceScopeFactory scopeFactory;
    private readonly IMapper auto;

    public ProjectTaskNotifications(
        ILogger<ProjectTaskNotifications> logger, 
        IServiceScopeFactory scopeFactory, 
        IMapper mapper)
    {
        _logger = logger;
        this.scopeFactory = scopeFactory;
        auto = mapper;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Timed Background Service is starting.");

        _timer = new Timer(DoWork, null, TimeSpan.Zero,
            TimeSpan.FromSeconds(30));

        return Task.CompletedTask;
    }

    private void DoWork(object state)
    {
        _logger.LogInformation("Timed Background Service is working.");

        // Connect to the database and cycle through all unsent
        // notifications, checking if some of them are due to be sent:
        using (var scope = scopeFactory.CreateScope())
        {
            var db = scope.ServiceProvider.GetRequiredService<MyDbContext>();

            List<ProjectTaskNotification> notifications = db.ProjectTaskNotifications
                .Include(t => t.Task)
                    .ThenInclude(o => o.TaskOwner)
                .Include(t => t.Task)
                    .ThenInclude(p => p.Project)
                        .ThenInclude(o => o.ProjectOwner)
                .Where(s => !s.IsSent).ToList();

            foreach (var notification in notifications)
            {
                if (DateTime.UtcNow > notification.Task.DueDate
                   .AddMinutes(-notification.TimeBefore.TotalMinutes))
                {
                    SendEmail(notification);
                    notification.Sent = DateTime.UtcNow;
                    notification.IsSent = true;
                }
            }
            db.UpdateRange(notifications);
            db.SaveChanges();
        }
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Timed Background Service is stopping.");
        _timer?.Change(Timeout.Infinite, 0);
        return Task.CompletedTask;
    }

    public void Dispose()
    {
        _timer?.Dispose();
    }

    public void SendEmail(ProjectTaskNotification notification)
    {   // Trimmed down for brevity

        // Key parts
        string toAddr = notification.Task.TaskOwner.Email1;
        BodyBuilder bodyBuilder = new BodyBuilder
        {
            HtmlBody = TaskInfo(auto.Map<ProjectTaskViewModel>(notification.Task))
        };

        return;
    }

    public string TaskInfo(ProjectTaskViewModel task)
    {   // Trimmed down for brevity
        return $@"<p>{BuildUrl("ProjectTasks", "Edit", task.Id)}</p>";
    }

    public string BuildUrl(string controller, string action, int id)
    {   
        // This is where I need the domain name sent in from somewhere:
        return "domain:port" + $@"/{controller}/{action}/{id}";
    }
}
Stian
  • 1,522
  • 2
  • 22
  • 52
  • 1
    This answer could help you: https://stackoverflow.com/questions/49813628/run-a-background-task-from-a-controller-action-in-asp-net-core-2/49814520 – Roxana Sh Aug 17 '19 at 09:38
  • How your background task works, from where it get email which should be sent? – Fabio Aug 17 '19 at 23:00
  • @Fabio See updated question. I added the background task class to show how I figure out which e-mails to send. – Stian Aug 18 '19 at 09:16
  • Why you can not save domain url within notification in database. then you will be able to send email with any domain url. – Fabio Aug 18 '19 at 09:19
  • @Fabio Hm... Yes, I can do that. The redundant information which is the string "domain.name:port#" does not weigh down the database too much, I guess... :) – Stian Aug 18 '19 at 09:25
  • 1
    Consider "notification" record as an event with information about email to be sent. Background task (or event handler) should not care about how to build required data, all data is prepared and saved in the event. Event handler(Background task) will just process it. – Fabio Aug 18 '19 at 09:46
  • On other side, if domain information are known during application start up, then you can provide this information during start up to registered background task. – Fabio Aug 18 '19 at 09:48
  • @Fabio I like the idea of storing all the information in each `Notification`. This simplifies things a lot! :) – Stian Aug 18 '19 at 12:24

1 Answers1

-1

You can pass in any object to the IHostedService provider via the constructor.

public ProjectTaskNotifications(IUrlPrefixProvider provider)
{
    _urlPrefixProvider = urlPrefixProvider
}

private string BuildUrl(<Your args>)
{
    var prefix = _urlPrefixProvider.GetPrefix(<args>);
    ....
}

In startup.cs you can have

services.AddSingleton<IUrlPrefixProvider, MyUrlPrefixProvider>()
services.AddHostedService<ProjectTaskNotifications>();

and let dependency injection take care of the rest.

Sanjid
  • 133
  • 1
  • 6
  • What is IUrlPrefixProvider? Visual Studio asks me if I might be missing a using directive or an assembly reference, but when I press CTRL+. there are no suggestions of any using directive. – Stian Aug 17 '19 at 09:17
  • Should have clarified. I don't know where you are getting the `aStringPassedInFromSomewhere` from, so I assumed you would have some custom object (which I named `MyUrlPrefixProvider`) which your background task can query to get the prefix. I might not be understanding your use case correctly. – Sanjid Aug 17 '19 at 18:43
  • This will not work, because as OP mentioned, url can be generated based on request context. Provider will be instantiated only once and will not have access to the current context. – Fabio Aug 17 '19 at 22:57