An Event Driven Approach for Building Website using ASP.NET Core

First of all this post not only related to asp.net core and can apply to previous versions of asp.net MVC. But the sample codes are based on asp.net core RC2.

One of the most challenging part of developing application is to keep the code base healthy as it grows with features. We start with greenfield project. Everything shiny and works fine. The business grows and we need to add lot of new functionalities. One day we found that the application is no longer maintainable. We can’t easily add new feature anymore and that application become legacy again.

That’s why micro services become more popular nowadays. This blog post is not about micro services but how we can write maintainable application with isolated features using CQRS and event driven approach.

As an example take a look at following home page of an imaginary e-commerce site that sell books.

sample-site

 

For home page we want to load following features from server sides.

  1. Category menu
  2. Latest Books
  3. Books of the week
  4. Books saved as favourite or not

The reason why we decide to load them from server side is as below:

  1. We want to load home page quickly without any further delay from client side. So the page is immediately usable rather than displaying a spinner and wait to load all data. If you need further details read some articles on page speed 🙂 The discussion about that is out of this blog post scope.
  2. Other important reason is they have SEO value.
  3. Some features like recently viewed can be loaded later using client script as they most likely position below fold of initial screen. That means not inside viewport of first page load. And this doesn’t have SEO value as the data depends on current users last visited books.
  4. Books saved or not can be loaded using javascript. But here we will load from server side so that the information is immediately visible rather then script load later. We try to load most things from server that will be above view fold. Visible to user screen on first load.

First Approach : One big ViewModel

So how can achieve this? We can load all data of home page in one go. So we can define a viewmodel for page as below and load the viewmodel from homecontroller.

    public class HomeViewModel
    {
        public string CurrentUserFullName { get; set; }
        public int TotalBooksInCart { get; set; }
        public IEnumerable<BookListItemDto> LatestBooks {get; set;}
        public IEnumerable<BookListItemDto> BooksOfTheWeek {get; set;}

        public IEnumerable<CategoryMenuItemDto> CategoryMenu {get; set;}
    }

    public class CategoryMenuItemDto
    {
        public string Slug {get; set;}
        public string Name {get; set;}
        public string TotalBooks {get; set;}
    }

    public class BookListItemDto
    {
        public string Id {get; set;}
        public string Title {get; set;}
        public string Slug {get; set;}
        public string ImageUrl {get; set;}
        public decimal Price {get; set;}
        public bool IsSaved {get; set;}
    }

    public class HomeController : Controller
    {
        private readonly IRequestBus bus;
        public HomeController(IRequestBus bus)
        {
            this.bus = bus;
        }

        public async Task<IActionResult> Index(){
            var vm = await bus.SendAsync<HomePageQuery,HomeViewModel>(new HomePageQuery());
            return View(vm.Value);
        }
    }

    public class HomePageQueryHandler : AsyncRequestHandlerBase<HomePageQuery,HomeViewModel>
    {
        // hide constructor code

        public async Task<HomeViewModel> HandleAsync(HomePageQuery query)
        {
            var taskLoadCategoryMenu = categoryApiProxy.GetAsync();
            var taskLoadLatestBooks = booksApiProxy.GetLatestBooksAsync();
            var taskLoadBooksOfTheWeek = booksApiProxy.GetBooksOfTheWeekAsync();
            var taskLoadCart = cartApiProxy.GetCartAsync();
            // Just to remove complexity assuming user logged in
            var taskLoadProfile = userProfileApiProxy.GetProfileAsync(_userContext.UserId);

            await Task.WhenAll(taskLoadCategoryMenu, taskLoadLatestBooks, taskLoadBooksOfTheWeek, taskLoadCart, taskLoadProfile);

            return new HomeViewModel
            {
                CurrentUserFullName = taskLoadProfile.Result.Name,
                TotalBooksInCart = taskLoadCart.Result.Items.Count(),
                LatestBooks = LoadBooksListWithSavedStatus(taskLoadLatestBooks.Result, taskLoadCart.Result),
                BooksOfTheWeek = LoadBooksListWithSavedStatus(taskLoadBooksOfTheWeek.Result, taskLoadCart.Result),
                CategoryMenu = taskLoadCategoryMenu.Result
            };
        }
    }

The problem with this approach is it loads all data for a page in one big viewmodel. Some of them like total items in cart, category menu we need to do again in other pages. And our query handler getting too much responsibility with lots of code. This code will grow day by day when we add more features in home page. Its going to be a maintenance problem in future as it violates single responsibility principle. One good thing about this approach I can do all api calls in parallel. That means I can load all data from server side with minimal time which is great for performance.

Second Approach: Use of View Component or Render Action

In this approach we will use Render action to load different features and keep them independent, rather than using a big viewmodel for everything on a page.

    public class HomeController : Controller
    {
        public Task<IActionResult> Index()
        {
            return View();
        }
    }

    public class LatestBooksViewComponent : ViewComponent
    {
        private readonly IRequestBus bus;
        public LatestBooksViewComponent(IRequestBus bus)
        {
            this.bus = bus;
        }
        public async Task<IViewComponentResult> InvokeAsync()
        {
            var vm = await bus.SendAsync<LatestBooksQuery,IEnumerable<BookListItemDto>>();
            return View(vm);
        }
    }

    public class LatestBooksQueryHandler : AsyncQueryHandlerBase<LatestBooksQuery, IEnumerable<BookListItemDto>>
    {
        // constructor code removed

        public Task<IEnumerable<BookListItemDto>> HandleAsync(LatestBooksQuery query)
        {
            return booksApiProxy.GetLatestBooksAsync();
        }
    }

Sample html code for index.cshtml for home.

<div class="page-shell">
    <section class="two-col">
        <section class="col-1">
            @await Component.InvokeAsync("CategoryMenu")
        </section>
        <section class="col-2">
            @await Component.InvokeAsync("LatestBooks")
            @await Component.InvokeAsync("BooksOfTheWeek")
            @await Component.InvokeAsync("RecentlyViewed")
        </section>
    </section>
</div>

As you can see this is nice and clean approach. Each feature can be individual component and load required data by itself without any direct dependency on home page. Now home page doesn’t need to load all data for all the feature required in this page. We can load this features in other pages also without changing any code in page itself as they don’t have any dependencies on container page.

But one big problem with this approach is that now we lost the performance of first approach. Because all data e.g latest books, books of the week, categories now loading from api one after another. So total load time of page now should be slower than first approach.

So we need a solution that has all the benefit of second and first approach. Let’s see the third approach.

Third Approach: Events to the rescue

Let’s declare an event and publish the event from homecontroller.

public class HomePageRequestedEvent : IEvent { }

public class HomeController : Controller
{
	private readonly IRequestBus bus;

	public HomeController(IRequestBus bus)
	{
		this.bus = bus;	
	}

	public async Task<IActionResult> Index()
	{
		await bus.PublishAsync<HomePageRequestedEvent>(new HomePageRequestedEvent());
		return View();
	}
}

public class LatestBooksViewComponent : ViewComponent
{
    private readonly ILatestBooksProvider provider;

    public LatestBooksViewComponent(ILatestBooksProvider provider)
    {
        this.provider = provider;
    }
    

    public IViewComponentResult Invoke()
    {
        var vm = provider.Get();

        return View("~/Features/Home/LatestBooks/Views/LatestBooks.cshtml", vm);
    } 
}

Now write a handler that will populate latest books view model and store the value using provider.

public class LoadLatestBooksOnPageLoad : IAsyncEventHandler<HomePageRequestedEvent>
{
    private readonly ILatestBooksProvider provider;
    private readonly IRestClient restClient;
    private readonly ILogger logger;
    private readonly IOptions<ApiSettings> settings;

    public LoadLatestBooksOnPageLoad(ILatestBooksProvider provider,
        IRestClient restClient, 
        ILogger logger,
        IOptions<ApiSettings> settings)
    {
        this.provider = provider;
        this.restClient = restClient;
        this.logger = logger;
        this.settings = settings;
    }
    
    public async Task HandleAsync(HomePageRequestedEvent eEvent)
    {
        var response = await ErrorSafe.WithLogger(logger)
            .ExecuteAsync(() => restClient.For($"{settings.Value.BaseUrl}/books/latest")
                .AcceptJson()
                .Timeout(1000)
                .RetryOnFailure(2)
                .GetAsync<IEnumerable<BookDto>>());

        provider.Set(response.Value?.Output);
    }
}

That’s it, You can write other features of home page like BooksOfTheWeek, CategoryMenu etc using same event handler pattern and all of them will load in parallel. That means performance gain without compromising loosely coupled architecture of your system.

A sample application available to check here.

Happy coding 🙂

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s