samus
samus

Reputation: 6202

Running a STA (Single-Threaded Apartment) thread from a Web API (2.1) controller

I'm trying to run a STA (Single-Threaded Apartment) thread from a Web API (2.1) controller.

To do this, I'm using StaTaskScheduler:

/// <summary>Provides a scheduler that uses STA threads.</summary>
public sealed class StaTaskScheduler : TaskScheduler, IDisposable
{
    /// <summary>Stores the queued tasks to be executed by our pool of STA threads.</summary>
    private BlockingCollection<Task> _tasks;
    /// <summary>The STA threads used by the scheduler.</summary>
    private readonly List<Thread> _threads;

    /// <summary>Initializes a new instance of the StaTaskScheduler class with the specified concurrency level.</summary>
    /// <param name="numberOfThreads">The number of threads that should be created and used by this scheduler.</param>
    public StaTaskScheduler(int numberOfThreads)
    {
        // Validate arguments
        if (numberOfThreads < 1) throw new ArgumentOutOfRangeException("concurrencyLevel");

        // Initialize the tasks collection
        _tasks = new BlockingCollection<Task>();

        // Create the threads to be used by this scheduler
        _threads = Enumerable.Range(0, numberOfThreads).Select(i =>
        {
            var thread = new Thread(() =>
            {
                // Continually get the next task and try to execute it.
                // This will continue until the scheduler is disposed and no more tasks remain.
                foreach (var t in _tasks.GetConsumingEnumerable())
                {
                    TryExecuteTask(t);
                }
            });
            thread.IsBackground = true;
            thread.SetApartmentState(ApartmentState.STA);
            return thread;
        }).ToList();

        // Start all of the threads
        _threads.ForEach(t => t.Start());
    }
}

One option is to run the STA thread from a CustomHttpControllerDispatcher:

public static void Register(HttpConfiguration config)
{
    config.Routes.MapHttpRoute(
        name: "DefaultApi",
        routeTemplate: "api/{controller}/{id}",
        defaults: new { id = RouteParameter.Optional }      
        //constraints: null,        
        // - Trying to avoid this
        //handler: new Controllers.CustomHttpControllerDispatcher(config)
    );
}

public class CustomHttpControllerDispatcher : System.Web.Http.Dispatcher.HttpControllerDispatcher
{
    public CustomHttpControllerDispatcher(HttpConfiguration configuration) : base(configuration)
    {
    }
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        // My stuff here
    }
}

However, that would require manual construction of the http response, which involves json serialization, something I would rather avoid.

That leaves me with my current option, which is running the STA thread from within the library that the controller calls into (to generate a custom form).

Everything works OK if the library is called using a "Windows Application" type test application with a Main decorated with [STAThread]. However, when called from the web service controller, there is a "swallowed" exception when the following task returns:

Note: I'm am not rendering any dialogs, so there shouldn't be any "Showing a modal dialog box or form when the application is not running in UserInteractive mode is not a valid operation." exceptions, which do not occur at all when generating the custom form from an STA thread started within a CustomHttpControllerDispatcher.

private Task<AClass> SendAsync1()
{
    var staTaskScheduler = new StaTaskScheduler(1);

    return Task.Factory.StartNew<CustomForm>(() =>
        {
            // - execution causes "swallowed exception"
            return new AClass();
        }, 
        CancellationToken.None,
        TaskCreationOptions.None,
        staTaskScheduler
    );
}

That is, when step debugging, the stack trace disappears after stepping over:

return new AClass(); 

I usually deal with this by narrowing down some breakpoints, but for this case I don't think it is possible since there are no debugging symbols or source for Task.cs (and the multitude of associated files).

Note: I can now step through the disassembly of System.Threading.Tasks, however the debugging process will probably be lengthy since these are not trivial libraries, so any insight would be greatly appreciated.

I'm suspecting that maybe it's b/c I'm trying to schedule an STA thread from an MTA thread (controller), but am not positive?


Usage

GetCustomForm().Wait();


private FixedDocumentSequence _sequence;

private async Task GetCustomForm()
{
    // - will be slapped with a "calling thread cannot access...", next issue
    _sequence = await SendAsync1b();
}


private readonly StaTaskScheduler _staTaskScheduler = new StaTaskScheduler(1);

private Task<FixedDocumentSequence> SendAsync1b()
{
    //var staTaskScheduler = new StaTaskScheduler(100);
    //var staTaskScheduler = new StaTaskScheduler(1);

    return Task.Factory.StartNew<FixedDocumentSequence>(() =>
    {
        FixedDocumentSequence sequence;
        CustomForm view = new CustomForm();

        view.ViewModel = new ComplaintCustomFormViewModel(BuildingEntity.form_id, (int)Record);     
        sequence = view.ViewModel.XpsDocument.GetFixedDocumentSequence();

        return sequence;
    },
    CancellationToken.None,
    TaskCreationOptions.None,
    _staTaskScheduler);
}

References

Upvotes: 1

Views: 1804

Answers (1)

samus
samus

Reputation: 6202

Running the WPF/Entity code directly in an STA thread seems to be working:

Thread thread = GetCustomFormPreviewView4();
thread.Start();
thread.Join();


private Thread GetCustomFormPreviewView4()
{
    var thread = new Thread(() =>
        {
            FixedDocumentSequence sequence;
            CustomForm view = new CustomForm();

            view.ViewModel = new ComplaintCustomFormViewModel(BuildingEntity.form_id, (int)Record);     
            sequence = view.ViewModel.XpsDocument.GetFixedDocumentSequence();

            //view.ShowDialog();
            //...
        }
    );

    thread.IsBackground = true;
    thread.SetApartmentState(ApartmentState.STA);

    return thread;
}

Upvotes: 0

Related Questions