WorkContext null in background thread

Topics: Core, Writing modules
Developer
Nov 17, 2011 at 5:01 PM

It seems that when something is run in a background thread (I happened to do this with Task), then the WorkContext object is null. I can understand why this is good this way (e. g. there is no valid HttpContext in a background thread since it's not bound to a request), however problems arise if I'd like to use a service that needs WorkContext to run. Are there any standard practices to solve this?

Thanks in advance.

Coordinator
Nov 17, 2011 at 5:10 PM

You mean you can't inject a work contxt accessor?

Developer
Nov 17, 2011 at 5:20 PM

Yes, the object got from the IoC container is null (actually I haven't tried to get IWorkContextAcessor directly, the property of IOrchardServices is null).

Coordinator
Nov 17, 2011 at 5:25 PM

Well, please do try this: inject IWorkContextAccessor and call GetContext on it.

Developer
Nov 17, 2011 at 5:38 PM

Thanks, but this doesn't work either. IWorkContextAccessor is set, but GetContext() returns null.

However the major problem is not with my code that I could change to work somehow else, but with other ones. For example uploading a media file in the background fails since MediaService needs the WorkContext. Translation (method calls on NullLocalizer.Instance) also fails, I think because of the same reason.

The code is as simple as (written in a piece of code that runs in the foreground and can access WorkContext fine):

            var task = new Task(() =>
            {
                _someService.MethodUsingWorkContext();
            });
            task.Start();
This is problematic since the wast majority of modules out there use WorkContext at least for getting own settings (attached to Site) from the DB.

Nov 17, 2011 at 5:44 PM

Ah, this is a System.Threading.Task? The problem is likely that it runs on a separate thread so Orchard can no longer match it up with an appropriate WorkContext.

Orchard has two ways of operating background tasks: IBackgroundTask, and IScheduledTaskManager - if you execute a task with either of them then you'll have access to WorkContext.

Developer
Nov 18, 2011 at 2:48 PM

Thanks! Indeed by Task I've meant System.Threading.Task. I've looked at the usage of both interface. I'm really not sure whether these are appropriate.

I wouldn't like a task that runs at intervals or at a specified time independently, but I'd rather like to fire off a piece of code that runs async (once), just like with Task. Is there any special cause why it wouldn't be possible to access the work context from a background thread? It's also null if the object whose method runs in the background gets instantiated in the foreground (and therefore gets the work context in the ctor properly).

Nov 18, 2011 at 3:40 PM

WorkContext is tied to a specific thread and to a specific Orchard shell. If you try to get one from an unknown thread then Orchard basically can't know which WorkContext to give you. There might be a way to manually register your thread with the correct shell; but a simpler way is just to set up a scheduled task in the very near future.

Nov 18, 2011 at 4:00 PM

By the way, I've literally just been setting up something kind-of similar but in my case I'm firing off an .exe process (using System.Diagnostics.Process). But the whole thing is wrapped up so it doesn't need to access any Orchard services once it starts running. So if there's any way of refactoring what you want to do so you can get the information you need before firing off the Task, it might work that way. But if you need to use the Orchard framework then you have to stay within its bounds. You could always look at how IBackgroundTask and IScheduledTaskManager are implemented and try to implement your own system that works in a similar way, but it gets pretty complicated when you start look at things that low level!

Developer
Nov 20, 2011 at 10:29 AM

Thanks, I've looked into the implementation. The funny thing is, I couldn't find the code, if there is any, where threads or tasks are registered for background tasks.

Anyway, I tried to nest

using (var scope = _workContextAccessor.CreateWorkContextScope())
{
}

and

using (var transactionScope = new TransactionScope(TransactionScopeOption.RequiresNew))
{
}

blocks. In the built-in implementation the calls to all the IBackgroundTasks' Sweep() are nested in the first block (all in one), while each call is in its own transaction scope.

Now if I try to mimic this implementation, it works more or less, but if a new transaction is started, naturally things like WorkContext.CurrentCulture get lost.

I'm now working to figure something out.

Developer
Nov 21, 2011 at 9:48 AM

This approach seems to work:

An auxiliary class:

        class TaskContext
        {
            public string CurrentCulture { get; set; }
            public ISite CurrentSite { get; set; }

            public TaskContext(WorkContext workContext)
            {
                CurrentCulture = workContext.CurrentCulture;
                CurrentSite = workContext.CurrentSite;
            }

            public WorkContext Transcribe(WorkContext workContext)
            {
                workContext.CurrentCulture = CurrentCulture;
                workContext.CurrentSite = CurrentSite;

                return workContext;
            }
        }

 

The task:

            var task = new Task((taskContext) =>
            {
                using (var scope = _workContextAccessor.CreateWorkContextScope())
                {
                    using (var transactionScope = new TransactionScope(TransactionScopeOption.Required))
                    {
                        try
                        {
                            ((TaskContext)taskContext).Transcribe(_workContextAccessor.GetContext());
                             // Background code goes here
                        }
                        catch (Exception e)
                        {
                            // Logging
                            // Background task failed with exception e.Message
                        }
                    }
                }
            }, new TaskContext(_workContextAccessor.GetContext()));
            task.Start();

 
Now this TaskContext is of course not something bullet-proof, and HttpContext, CurrentUser and CurrentTheme are not transferred.
I've seen a class HttpContextPlaceholder in \src\Orchard\Mvc\MvcModule.cs, but its purpose is a bit vague for me. Maybe it should be used here.