23

I currently use the builder pattern to construct my MVC view models.

var viewModel = builder
                  .WithCarousel(),
                  .WithFeaturedItems(3),
                  .Build()

The problem I am coming up against is when I have to make a service call to an async method. This means that my builder method then has to return Task<HomeViewModelBuilder> instead of HomeViewModelBuilder. This prevents me from chaining the build methods as I have to await them.

Example method

public async Task<HomeViewModelBuilder> WithCarousel()
{   
    var carouselItems = await _service.GetAsync();
    _viewModel.Carousel = carouselItems;
    return this;
}

Now I have to use await to call the builder methods.

await builder.WithCarousel();
await builder.WithFeaturedItems(3);

Has anyone used async methods with the builder pattern? If so, is it possible to be able to chain the methods or defer the await to the build method.

i3arnon
  • 113,022
  • 33
  • 324
  • 344
Colin Bacon
  • 15,436
  • 7
  • 52
  • 72

5 Answers5

19

I have not actually done this before, but here's an alternative to Sriram's solution.

The idea is to capture the tasks in the builder object instead of the result of the tasks. The Build method then waits for them to complete and returns the constructed object.

public sealed class HomeViewModelBuilder
{
  // Example async
  private Task<Carousel> _carouselTask = Task.FromResult<Carousel>(null);
  public HomeViewModelBuilder WithCarousel()
  {
    _carouselTask = _service.GetAsync();
    return this;
  }

  // Example sync
  private int _featuredItems;
  public HomeViewModelBuilder WithFeaturedItems(int featuredItems)
  {
    _featuredItems = featuredItems;
    return this;
  }

  public async Task<HomeViewModel> BuildAsync()
  {
    return new HomeViewModel(await _carouselTask, _featuredItems);
  }
}

Usage:

var viewModel = await builder
    .WithCarousel(),
    .WithFeaturedItems(3),
    .BuildAsync();

This builder pattern works with any numbers of asynchronous or synchronous methods, for example:

public sealed class HomeViewModelBuilder
{
  private Task<Carousel> _carouselTask = Task.FromResult<Carousel>(null);
  public HomeViewModelBuilder WithCarousel()
  {
    _carouselTask = _service.GetAsync();
    return this;
  }

  private Task<int> _featuredItemsTask;
  public HomeViewModelBuilder WithFeaturedItems(int featuredItems)
  {
    _featuredItemsTask = _featuredService.GetAsync(featuredItems);
    return this;
  }

  public async Task<HomeViewModel> BuildAsync()
  {
    return new HomeViewModel(await _carouselTask, await _featuredItemsTask);
  }
}

Usage is still the same.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Thanks for the solution. The only downside to this would be if there were two builder methods that had async calls the builder would have to have some sort of checking in it. – Colin Bacon Aug 14 '14 at 16:07
  • 2
    @ColinBacon: I'm not sure what you mean; you can have as many async builder calls as you want with this same pattern. – Stephen Cleary Aug 14 '14 at 17:31
  • sorry I wasn't too clear. If `WithCarousel` & `WithFeaturedItems` both have async calls then how would I handle that in `BuildAsync` as I may not use both methods – Colin Bacon Aug 14 '14 at 19:37
  • 1
    @ColinBacon: Updated with an example where both calls use tasks. – Stephen Cleary Aug 14 '14 at 19:53
  • Wouldn't multiple "With*" methods in the builder will run in parallel? Which could cause issues if the code isn't thread-safe - eg. the injected EF DataContext is the same instance. – Dan Mar 15 '22 at 16:27
  • @Dan: Yes, they would run concurrently. – Stephen Cleary Mar 15 '22 at 16:50
6

As I said in comments, you could write Extension method for HomeViewModelBuilder as well as Task<HomeViewModelBuilder> and chain it.

public static class HomeViewModelBuilderExtension
{
    public static Task<HomeViewModelBuilder> WithCarousel(this HomeViewModelBuilder antecedent)
    {
        return WithCarousel(Task.FromResult(antecedent));
    }

    public static async Task<HomeViewModelBuilder> WithCarousel(this Task<HomeViewModelBuilder> antecedent)
    {
        var builder = await antecedent;
        var carouselItems = await builder.Service.GetAsync();
        builder.ViewModel.Carousel = carouselItems;
        return builder;
    }

    public static Task<HomeViewModelBuilder> WithFeaturedItems(this HomeViewModelBuilder antecedent, int number)
    {
        return WithFeaturedItems(Task.FromResult(antecedent), number);
    }

    public static async Task<HomeViewModelBuilder> WithFeaturedItems(this Task<HomeViewModelBuilder> antecedent, int number)
    {
        var builder = await antecedent;
        builder.ViewModel.FeaturedItems = number;
        return builder;
    }
}

We're adding couple of methods for single operation so that you can chain it with HomeViewModelBuilder or Task<HomeViewModelBuilder>. Otherwise you'll not be able to call builder.WithCarousel()

Then use it like

private static void Main()
{
    HomeViewModelBuilder builder = new HomeViewModelBuilder();
    var task = builder
        .WithCarousel()
        .WithFeaturedItems(3);        
}
Sriram Sakthivel
  • 72,067
  • 7
  • 111
  • 189
3

With the builder pattern you can create a strategy of building the object. It does not construct the object until the build method is called. If the logic to populate the object is in the build method then you can call all of the async methods together.

See example code for your builder below. Its just a demonstration of the concept so you may want to improve on it.

    public class Builder
    {
        private bool hasCarousel = false;
        private int featuredItems = 0;

        public Builder WithCarousel()
        {
            hasCarousel = true;
            return this;
        }

        public Builder WithFeaturedItems(int featured)
        {
            featuredItems = featured;
            return this;
        }

        public BuiltObject Build()
        {
            if (hasCarousel)
            {
                // do carousel related calls
            }
            if (featuredItems > 0)
            {
                // do featured items related calls.
            }

            // build and return the actual object.
        }
    }
  • Thanks, the problem with that is depending on what build methods I choose I may not need the asynchronous methods – Colin Bacon Aug 14 '14 at 07:38
  • See the example code in the edit above. You can then call the async methods first in the build method and then anything else that needs doing after. – CarlHembrough Aug 14 '14 at 07:45
  • 3
    This code doesn't make much sense to me. Was the `Builder` constructor supposed to be an async `Build` method? – svick Aug 14 '14 at 10:53
  • @ColinBacon Why is that a problem? You can make `Build()` return a `Task`, except that when you don't need to do anything asynchronous, that `Task` will be already completed (you can achieve this easily using `async`-`await`). – svick Aug 14 '14 at 10:56
  • Thanks svick, you found a mistake in the code example. The build method was declared as a constructor of the class when it was supposed to be a build method on the class. I have updated it. – CarlHembrough Aug 14 '14 at 10:58
2

The deal with async, is that it has the ripple effect. It tends to spread through your code to keep it async "async all the way".

If you want to allow for the builder pattern (or any other fluent pattern, like LINQ) while keeping it async you need to have an async overload for each of the possible calls you want to make. Otherwise you can't use them, or will use them wrong (with "sync over async" for example).

async-await is fairly new, but I'm sure over time you will have an async option for almost anything.

Community
  • 1
  • 1
i3arnon
  • 113,022
  • 33
  • 324
  • 344
  • 1
    Thanks. Could you elaborate on `you need to have an async overload for each of the possible calls` – Colin Bacon Aug 14 '14 at 08:05
  • Using the select answer, I feel this answer has no relevance. "Async all the way" only means that it should be async from request to response, and even that is debatable. In my experience, what this really means is to choose your approach and stick to it. From point a to point z, try not to mix synch and asych approaches. This is in no way means that all of your build methods on the builder need to be asyc; that is absurd. As the selected answer shows, all you're doing is using sync methods to start tasks, then the build is what unwraps the resuls and this is still the in the spirit of async – Sinaesthetic Jan 21 '16 at 19:08
1

The accepted answer was pretty helpful, but I think in some cases (at least in mine) it is more helpful to wait on the builder itself instead of the built objects:

public sealed class HomeViewModelBuilder
{
  
 private List<Task<HomeViewModelBuilder>> taskList= new List<Task<HomeViewModelBuilder>>();

 public HomeViewModelBuilder WithCarousel(){
   taskList.add(WithCarouselInternal());
   return this;
 }

 private async Task<HomeViewModelBuilder> WithCarouselInternal()
  {
    var result = await _service.GetAsync();
    // do something with the result
    return this;
  }

 public HomeViewModelBuilder WithSomthingElse(){
   taskList.add(WithSomethingElseInternal());
   return this;
 }

 (...)


  public async Task<HomeViewModel> BuildAsync()
  {
   await Task.WhenAll(taskList);
   // On that point we can now be sure that all builds are finished
   return new HomeViewModel(...);
  }
}

I hope this is a little more generic, because you just have to keep the taskList up to date and also can call the same build-method more than one time in a simple way (you can't overwrite a previous call to a build method because all will Task will be present in the list and so the build will definitely wait till the Task finishes)

gratinierer
  • 1,748
  • 1
  • 10
  • 10