PEtter
PEtter

Reputation: 836

Call async over sync c# without thread locking

I have made a lightweight mediator kind of library for use internally.

A query can be mapped to a async method without the caller knowing anything about it.

public interface IMediator
    {
        TResult ExecuteQuery<TResult>(Query<TResult> query);
        Task<TResult> ExecuteQueryAsync<TResult>(Query<TResult> query);
    
        Task ExecuteCommandAsync(Command command);
        Task<TResult> ExecuteCommandAsync<TResult>(Command<TResult> command);
        
        void ExecuteCommand(Command command);
        TResult ExecuteCommand<TResult>(Command<TResult> command);

        void EnqueueTask(object task);
    }

 public abstract class Query<TResult> { }

 public interface IQueryHandler<Query, TResult> where Query : Query<TResult>
    {

        TResult Handle(Query param);
    }

    public interface IAsyncQueryHandler<Query, TResult> where Query : Query<TResult>
    {
        Task<TResult> Handle(Query param);
    }

When the caller is in a sync thread e.g factory method in the service container and want to execute a query it can not do that async using the ExecuteQueryAsync but the actual handler of the query, implementing the IAsyncQueryHandler is async.

So I need to call async over sync.

I do some reflection a registration time to actually figure out how to call the handler, I tried the .wait() and .result() but I get thread locks.

current code looks like this.

private object AsyncSync(object service, object param)
{
  dynamic t = Method.Invoke(service, new[] { param });
  return t.GetAwaiter().GetResult();
}

The Method is the method info from reflection, and the service is an instance of the class with that method.

This code still gives me deadlock of threads.

How could I achieve this with, do I need to look into the task factory? Any suggestions?

Upvotes: 3

Views: 1676

Answers (2)

PEtter
PEtter

Reputation: 836

Big thanks to GTHvidsten This is the solution that seems to be working.

private object RunTaskTSync(Func<Task<object>> task)
{
    var cultureUi = CultureInfo.CurrentUICulture;
    var culture = CultureInfo.CurrentCulture;

    return _taskFactory
        .StartNew(()=> {
            Thread.CurrentThread.CurrentCulture = culture;
            Thread.CurrentThread.CurrentUICulture = cultureUi;
            return task(); })
        .Unwrap()
        .GetAwaiter()
        .GetResult();
}

private object AsyncSync(object service, object param)
{
    return RunTaskTSync(async () => await (dynamic)Method.Invoke(service, new[] { param }));
}

I do not like the dynamic thing, but it is an easy way to do it :)

Upvotes: 0

TheHvidsten
TheHvidsten

Reputation: 4418

As as been commented already, the optimal solution would be to have async all the way... but sometimes that's not possible. Even Microsoft has acknowledged this by having a tool in their own frameworks to run async methods synchronously:

public static class AsyncHelper
{
    private static readonly TaskFactory MyTaskFactory = new TaskFactory(CancellationToken.None,
        TaskCreationOptions.None, TaskContinuationOptions.None, TaskScheduler.Default);

    public static TResult RunSync<TResult>(Func<Task<TResult>> func)
    {
        var cultureUi = CultureInfo.CurrentUICulture;
        var culture = CultureInfo.CurrentCulture;
        return MyTaskFactory.StartNew(() =>
        {
            Thread.CurrentThread.CurrentCulture = culture;
            Thread.CurrentThread.CurrentUICulture = cultureUi;
            return func();
        }).Unwrap().GetAwaiter().GetResult();
    }

    public static void RunSync(Func<Task> func)
    {
        var cultureUi = CultureInfo.CurrentUICulture;
        var culture = CultureInfo.CurrentCulture;
        MyTaskFactory.StartNew(() =>
        {
            Thread.CurrentThread.CurrentCulture = culture;
            Thread.CurrentThread.CurrentUICulture = cultureUi;
            return func();
        }).Unwrap().GetAwaiter().GetResult();
    }
}

https://github.com/aspnet/AspNetIdentity/blob/main/src/Microsoft.AspNet.Identity.Core/AsyncHelper.cs

You use it like this:

public async Task<int> DoSomethingAsync()
{
    await DoSomethingElseAsync();
    return 0;
}

int result = AsyncHelper.RunSync(async () => await DoSomethingAsync());

My feeling about this is that if Microsoft does this in their own frameworks, then it's OK for us to do the same.

Upvotes: 3

Related Questions