16

I'm writing a C# .Net 4.5 library for doing common sql database operations (backup, restore, execute script, etc.). I want to have both synchronous and asynchronous functions for each operation, as this library will be used by both console and GUI apps, but I don't want to duplicate code everywhere. So as I see it, I have two options:

  1. Write the code that does the work in a synchronous function, and then just wrap it in a task for the async function, like so:

    public void BackupDB(string server, string db)  
    {  
        // Do all of the work and long running operation here  
    }
    
    public async Task BackupDBAsync(string server, string db)  
    {  
        await Task.Factory.StartNew(() => BackupDB(server, db)).ConfigureAwait(false);  
    }
    
  2. Write the code that does the work in an asynchronous function, and call it from a synchronous function using .Wait():

    public async Task BackupDBAsync(string server, string db)  
    {  
        // Do all of the work and long running operation here, asynchronously.  
    }
    
    public void BackupDB(string server, string db)  
    {  
        BackupDBAsync(server, db).Wait(); // Execution will wait here until async function finishes completely.  
    }
    

Is one option better than the other? Is one a best practice? Or are there any other (better) alternatives?

I know that one caveat to using .Wait() is that all of the await statements in the async function have to use .ConfigureAwait(false) to avoid deadlocks (as discussed here), but since I'm writing a library that will never need to access the UI or WebContext I am safe to do that.

I'll note too that the SQL library typically also has both synchronous and async functions that can be used, so if doing the work in the sync function, I would call their sync function, and if doing the work in the async function, I would call their async function.

Thoughts/suggestions are appreciated.

-- edit: I've also posted this question on the MSDN forums here to try and get an official MS response --

Community
  • 1
  • 1
deadlydog
  • 22,611
  • 14
  • 112
  • 118
  • The recommended naming is to have async methods end in "Async". Other than that, neither of the two options is better. Although, using ConfigureAwait(false) does limit how your api can be used... – Peter Ritchie Jul 26 '12 at 16:54
  • I'm generally coming something that's already been written. If what's there doesn't support asynchronous at all, then I'd do your example 1. – Peter Ritchie Jul 26 '12 at 17:10

3 Answers3

12

I want to have both synchronous and asynchronous functions for each operation, as this library will be used by both console and GUI apps, but I don't want to duplicate code everywhere.

The best answer is: don't.

Stephen Toub has two excellent blog posts on this topic:

He recommends exposing asynchronous methods as asynchronous, and synchronous methods as synchronous. If you need to expose both, then encapsulate common functionality in private (synchronous) methods, and duplicate the async/sync differences.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Thanks for the links Stephen. I very much like his notion of leave it up to the developer consuming the library to wrap a sync-to-async or async-to-sync function. This way they do not make any incorrect assumptions about the scalability of the functions, and if they need to offload it for a responsive UI (e.g. with Task.Factory.StartNew(() => SomeSynchronousFunction())) or block for a synchronous call they can still do that. So the answer as you said is make async functions async, and sync functions sync. Don't wrap them at all; leave that to the consumer of the library/API. Thanks! – deadlydog Jul 26 '12 at 18:17
  • I also like this answer not only for the problems it helps consumers of the library avoid (as mentioned above), but also it helps prevent bloat in my library, as I was going to create synchronous and asynchronous versions of every public function in my library, which would have also meant duplicate documentation and all of the other headaches that would go along with having 2 functions for every operation. – deadlydog Jul 26 '12 at 19:35
1

I had a similar situation where some applications needed the data to be loaded synchronously and others asyc. I decided to created an interface that I called my dataloader:

public interface IIMViewModelDL {
    void LoadProjects(AssignProjects callback);
}

The AssignProjects callback is just a simple delegate that takes in the returned list of projects:

public delegate void AssignProjects(IEnumerable<Project> results);

Now, the beauty of this is that you can work with the interface without knowing whether you are dealling in sync or async.

Three classes are created: one base, one sync, and one async:

 public abstract class BaseViewModelDL {
    protected IEnumerable<Project> LoadProjects() {
        BaseServiceClient client = new BaseServiceClient();
        return client.Projects();
    }

public class SynchronousViewModelDL : BaseViewModelDL, IIMViewModelDL {
    public void LoadProjects(AssignProjects callback) {
        callback(base.LoadProjects());
    }

public class AsyncIMViewModelDL : BaseViewModelDL, IIMViewModelDL {
    public void LoadProjects(AssignProjects callback) {
        BackgroundWorker loadProjectsAsync = new BackgroundWorker();
        loadProjectsAsync.DoWork += new DoWorkEventHandler(LoadProjectsAsync_DoWork);
        loadProjectsAsync.RunWorkerCompleted += new RunWorkerCompletedEventHandler(LoadProjectsAsync_RunWorkerCompleted);
        loadProjectsAsync.RunWorkerAsync(callback);
    }

void LoadProjectsAsync_DoWork(object sender, DoWorkEventArgs e) {
        var results = new ObservableCollection<Project>(base.LoadProjects());
        e.Result = new object[] { results, e.Argument };
    }

    void LoadProjectsAsync_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) {
        AssignProjects callback = (AssignProjects)((object[])e.Result)[1];
        IEnumerable<Project> results = (IEnumerable<Project>)((object[])e.Result)[0];
        callback(results);
    }

Now, in your application, you can decide how you want to load data...this could be injected through an IoC container, but is hard coded for demo purposes:

private ViewModelDataLoaders.IIMViewModelDL dataLoader = new ViewModelDataLoaders.AsyncIMViewModelDL();

Now, your calling code looks the same and is none the wiser to whether it is async or sync:

private void LoadProjects() {
        dataLoader.LoadProjects(
            delegate(IEnumerable<Project> results) {
                Projects = new ObservableCollection<Project>(results);
            });
    }

I use this regularly for unit testing (sync), WPF applications (async), and console applications (sync).

Josh
  • 10,352
  • 12
  • 58
  • 109
0

There does not seem to be a point to simply mark a method as async without using an await. Marking it as async does not make it asynchronous, it allows you to use awaits (the code executed in the await is what happens asynchronously, and then the rest of the async method will also be done asynchronously) in the body of the method:

Typically, a method modified by the async keyword contains at least one await expression or statement. The method runs synchronously until it reaches the first await expression, at which point it is suspended until the awaited task is complete. In the meantime, control is returned to the caller of the method. If the method does not contain an await expression or statement, then it executes synchronously. A compiler warning alerts you to any async methods that don't contain await because that situation might indicate an error. For more information, see Compiler Warning CS4014.

From: async

Stefan H
  • 6,635
  • 4
  • 24
  • 35
  • I assume the OP would do some sort of "await" in that method. If not, a warning will be emitted. – Peter Ritchie Jul 26 '12 at 17:09
  • Correct Peter, I would be awaiting the SQL server function calls that actually perform the operation (backup, restore, run script, etc.), since that work will be done by the DB server, not my library process (i.e. my PC just waits for an operation on another PC to be complete; it doesn't to the crunching itself). – deadlydog Jul 26 '12 at 17:28