14

I have a method like so:

    public void Encrypt(IFile file)
    {
        if (file == null)
            throw new ArgumentNullException(nameof(file));

        string tempFilename = GetFilename(file);
        using (var stream = new FileStream(tempFilename, FileMode.OpenOrCreate))
        {
            this.EncryptToStream(file, stream);
            file.Write(stream);
        }

        File.Delete(tempFilename);
    }

However, I'm wanting to write another method, very similar but it calls WriteAsync instead, such as:

    public async Task EncryptAsync(IFile file)
    {
        if (file == null)
            throw new ArgumentNullException(nameof(file));

        string tempFilename = GetFilename(file);
        using (var stream = new FileStream(tempFilename, FileMode.OpenOrCreate))
        {
            this.EncryptToStream(file, stream);
            await file.WriteAsync(stream);
        }

        File.Delete(tempFilename);
    }

However I don't like having two methods with practically duplicate code. How can I avoid this? The right approach feels like I should use an Action/Delegate, but the signatures are different....

Thoughts?

hjavaher
  • 2,589
  • 3
  • 30
  • 52
  • ADO.NET is always async internally and simply blocks on the task... That's the sledgehammer solution which I kind of like given the lack of alternatives. – usr Sep 10 '15 at 21:08
  • That's certainly one approach I could take. I've read up on doing it the other way round (i.e. Task.Run to call the synchronous code, though it appears to be frowned upon). – Adrian Luca Thomas Sep 10 '15 at 21:12
  • That's worse because that way you don't get any thread unblocking benefits at all. Completely pointless. – usr Sep 10 '15 at 21:14
  • I know your example is probably simplified, but as it stands it looks fine. You might move a few things to a common location (argument checking, file mode), but you are mostly making calls to other methods. It looks readable and maintainable to me. – Tim M. Sep 10 '15 at 21:15
  • It benefits me in the sense of maintainability / readability, but yes I absolutely agree that there wouldn't be any benefits for the consumer in the Task.Run approach. @TimMedora - it's copy and paste actually! It's not too bad but I'm being a bit of a perfectionist here... I'd like to prevent any duplicate code if at all possible.. – Adrian Luca Thomas Sep 10 '15 at 21:17
  • Yes, there would be no benefits to having such an async API making it pointless. – usr Sep 10 '15 at 21:17
  • I understand the appeal of no duplication, but (speaking from experience) sometimes the solution can be worse than the problem. Ideally, the API only would expose one method (in this case, the async one since file IO can benefit from async), but in the real world a non-async version is still required a lot of the time. – Tim M. Sep 10 '15 at 21:25
  • possible duplicate of [Using async await when implementing a library with both synchronous and asynchronous API for the same functionality](http://stackoverflow.com/questions/27862483/using-async-await-when-implementing-a-library-with-both-synchronous-and-asynchro) – Eris Sep 10 '15 at 21:35

2 Answers2

18

However I don't like having two methods with practically duplicate code. How can I avoid this?

There are a few approaches, as outlined in my MSDN article on brownfield async development.

1) Make naturally-asynchronous APIs asynchronous-only.

This is the most drastic solution (in terms of backwards compatibility), but from a technical perspective, you could argue it's the most correct. With this approach, you would replace Encrypt with EncryptAsync.

While this is the best approach IMO, your end users may not agree. :)

2) Apply the hack of blocking on the asynchronous version.

public void Encrypt(IFile file)
{
  EncryptAsync(file).GetAwaiter().GetResult();
}

Note that (as I describe on my blog), to avoid deadlocks you'll need to use ConfigureAwait(false) everywhere in your asynchronous version.

The disadvantages to this hack:

  • You literally have to use ConfigureAwait(false) for every await in your async version and every method it calls. Forget even one and you've got a deadlock possibility. Note that some libraries do not use ConfigureAwait(false) everywhere for all platforms (notably, HttpClient on mobile platforms).

3) Apply the hack of running the asynchronous version on a thread pool thread and blocking on that.

public void Encrypt(IFile file)
{
  Task.Run(() => EncryptAsync(file)).GetAwaiter().GetResult();
}

This approach avoids the deadlock situation entirely, but has its own disadvantages:

  • The asynchronous code has to be able to be run on a thread pool thread.
  • The asynchronous code may be run on multiple thread pool threads throughout its existence. If the asynchronous code implicitly depends on the synchronization of a single-threaded context, or if it uses thread-local state, then this approach won't work without some rewriting.

4) Pass in a flag argument.

This is the approach I like the best, if you're unwilling to take approach (1).

private async Task EncryptAsync(IFile file, bool sync)
{
  if (file == null)
    throw new ArgumentNullException(nameof(file));

  string tempFilename = GetFilename(file);
  using (var stream = new FileStream(tempFilename, FileMode.OpenOrCreate))
  {
    this.EncryptToStream(file, stream);
    if (sync)
      file.Write(stream);
    else
      await file.WriteAsync(stream);
  }

  File.Delete(tempFilename);
}

public void Encrypt(IFile file)
{
  EncryptAsync(file, sync: true).GetAwaiter().GetResult();
}

public Task EncryptAsync(IFile file)
{
  return EncryptAsync(file, sync: false);
}

The boolean flag is certainly ugly - and a red flag to proper OOP design - but it's hidden in a private method, and not as dangerous as the other hacks.

There's a couple other hacks covered in my article (single-threaded thread pool context, and nested message loops), but I generally don't recommend them.


On a side note, if your code really is using FileStream, you need to explicitly open it for asynchronous access to get true asynchronous operations. That is, you have to call the constructor either passing true for the isAsync parameter, or setting the FileOptions.Asynchronous flag in the value for the options parameter.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Wow great response, thanks! I think I will go for solution 1 for now, however if I absolutely need both in the future I'll be inclined to go down route 2 or 3... but not 4 simply due to being a bit of a SOLID purist.. Thanks! – Adrian Luca Thomas Sep 10 '15 at 22:13
-1

Something like this:

public async Task EncryptAsync(IFile file, bool AAsync)
{
    if (file == null)
        throw new ArgumentNullException(nameof(file));

    string tempFilename = GetFilename(file);
    using (var stream = new FileStream(tempFilename, FileMode.OpenOrCreate))
    {
        this.EncryptToStream(file, stream);
        if (AAsync)
            await file.WriteAsync(stream);
        else
            file.Write(stream);
    }

    File.Delete(tempFilename);
}
Rohit Gupta
  • 4,022
  • 20
  • 31
  • 41
  • I see where you're coming from but that would be breaking a SOLID principle (i.e. Single Responsibility). Here we're changing the internal logic of the method by passing in a boolean to determine whether something is async or not. Thanks for the suggestion though! :-) – Adrian Luca Thomas Sep 10 '15 at 21:09
  • Mixing `async` code with non-`async` code is bad practice in C#. – Setsu Sep 10 '15 at 21:19
  • 1
    "please correct any syntax errors" -- Done. Parameters are separated by a comma, not a semicolon. The type comes before the parameter name. And `file.Write` doesn't return anything that can be `await`ed. –  Sep 10 '15 at 21:36
  • 2
    Although having conditionals all over the method for every other possibly-async method call is nasty, in some cases this may still be less bad than the alternative of duplicating the whole method body. There isn't really any third option that I'm aware of. (Note: for the non-`async` version, `EncryptAsync(file, false)` is guaranteed to complete synchronously, and accessing its `Result` property is guaranteed to be non-blocking.) –  Sep 10 '15 at 21:37
  • @Setsu, it is easy to criticise. How about a solution. And practices are practices - just like rules, somethimes they need to be broken. – Rohit Gupta Sep 10 '15 at 21:45
  • 1
    @RohitGupta The main problem here is that you have a method that must be called with `async` syntax, but is actually doing synchronous work. Given the amount of duplication involved I agree with the previous comment of actually just having both versions since the benefit of breaking the rules is not significant. – Setsu Sep 10 '15 at 21:58