📜 ⬆️ ⬇️

Reactive Extensions: client for conditional api with Cache-Aside & Refresh-Ahead strategy

rx-logo

Introduction


In this article I want to consider the development of the client library to the conditional api service. As such a service, I will use Habrahabr’s imaginary Rest-api.

To make such a routine task a bit more interesting, we will complicate the requirements by adding caching and tweak it all with the Reactive Extensions library.

I invite everyone who is interested.

Imagine that we have a nonexisting-api.habrahabr.ru/v1/karma/user_name url, which returns the following json:
')
{ "userName" : "requested user name", "karma" : 123, "lastModified" : "2014-09-01" } 


Appealing to such a service, deserializing the response and displaying the results to the user are all quite trivial. Perhaps a naive implementation could look like this:

  public sealed class NonReactiveHabraClient { private IHttpClient HttpClient { get; set; } public NonReactiveHabraClient(IHttpClient httpClient) { HttpClient = httpClient; } public async Task<KarmaModel> GetKarmaForUser(string userName) { var karmaResponse = await HttpClient.Get(userName); if (!karmaResponse.IsSuccessful) { throw karmaResponse.Exception; } return karmaResponse.Data; } } 


Add caching



Working with a mobile application is very different from working with a desktop or web application. The mobile application is used "on the run", with one hand, often in conditions of poor communication. Of course, the user expects the fastest possible display of information of interest. Obviously, there is a need to cache data.

The key feature of our application is rarely updated data, low criticality of freshness and relevance of data. That is, we can afford to show information from the previous launch. Many weather applications, twitter clients and others have this property.

In applications like ours, the following logic is quite common:
  1. the application should start quickly;
  2. show cached data;
  3. try to get fresh data from the back end;
  4. if successful, save the data to the cache;
  5. display new data to the user or report an error.


Or the same, but in the form of a diagram (I hope I didn’t completely forget how to draw sequence diagrams).

image

There are several basic strategies for working with the application's local cache. I will not consider them all, in this article we are interested in the approach (or pattern) of Cache-Aside.

The basic idea of ​​the pattern is that the cache is only a passive data repository. Thus, the task of updating data falls on the shoulders of the code that uses the cache. For such a cache, you can define a fairly simple interface.

  public interface ICache { bool HasCached(string userName); KarmaModel GetCachedItem(string userName); void Put(KarmaModel updatedKarma); } 


The cache that implements this interface sufficiently meets the requirements of 2 and 4 from the previous list. Items 2, 3 and 4 together are some version of the approach called Refresh-Ahead Caching .

I want to note that the above approach is not a classic implementation of both patterns, but only a combination of basic ideas. However, templates exist for this.

I hope that the standard iterative implementation of this approach will not cause difficulties for the reader, so I will go straight to the variant using Reactive Extensions. In addition, I hope that the reader is already familiar with Rx, at least at the level of general presentation. If you are not familiar with Rx or want to refresh your memory, then I advise you to familiarize yourself with the article from SergeyT “Reactive extensions” and asynchronous operations .

Implementation



And so, for a start we will create the project in the Visual Studio, we will specify project type as Class Library. We will need a Rx-Main NuGet package:

 Install-Package Rx-Main 


Define an abstraction over an http client:

  public interface IHttpClient { Task<KarmaResponse> Get(string userName); } public class KarmaResponse { public bool IsSuccessful { get; set; } public KarmaModel Data { get; set; } public Exception Exception { get; set; } } public class KarmaModel { public string UserName { get; set; } public int Karma { get; set; } public DateTime LastModified { get; set; } } 


The specific implementation of the execution of http requests, parsing and deserialization of the response, error handling is not important to us.

Define the interface of our api-client:

  public interface IHabraClient { IObservable<KarmaModel> GetKarmaForUser(string userName); } 


The key point here: we return IObservable <T> , that is, the “stream” of events to which you can subscribe.

Finally, let's define the implementation of our HabraClient:

 public sealed class ReactiveHabraClient : IHabraClient { private ICache Cache { get; set; } private IHttpClient HttpClient { get; set; } private IScheduler Scheduler { get; set; } public ReactiveHabraClient(ICache cache, IHttpClient httpClient, IScheduler scheduler) { Cache = cache; HttpClient = httpClient; Scheduler = scheduler; } public IObservable<KarmaModel> GetKarmaForUser(string userName) { return Observable.Create<KarmaModel>(observer => Scheduler.Schedule(async () => { KarmaModel karma = null; if (Cache.HasCached(userName)) { karma = Cache.GetCachedItem(userName); observer.OnNext(karma); } var karmaResponse = await HttpClient.Get(userName); if (!karmaResponse.IsSuccessful) { observer.OnError(karmaResponse.Exception); return; } var updatedKarma = karmaResponse.Data; Cache.Put(updatedKarma); if (karma == null || updatedKarma.LastModified > karma.LastModified) { observer.OnNext(updatedKarma); } observer.OnCompleted(); })); } } 


The code is fairly straightforward: we create and return a new Observable object, which immediately returns the cached data (if any) and then quietly asynchronously requests the updated values. If the data is updated (the LastModified field has changed) we notify the subscribers again, save the data to the cache and end the sequence.

Thus, the code of the View model using our ReactiveHabraClient will be compact and declarative:

  public class MainViewModel { private IHabraClient HabraClient { get; set; } public MainViewModel(IHabraClient habraClient, string userName) { HabraClient = habraClient; Initialize(userName); } private void Initialize(string userName) { IsLoading = true; HabraClient.GetKarmaForUser(userName) .Subscribe(onNext: HandleData, onError: HandleException, onCompleted: () => IsLoading = false); } private void HandleException(Exception exception) { ErrorMessage = exception.Message; IsLoading = false; } private void HandleData(KarmaModel data) { Karma = data.Karma; } public bool IsLoading { get; set; } public int? Karma { get; set; } public string ErrorMessage { get; set; } } 


Of course, the attentive reader has already noticed that there is no implementation of INotifyPropertyChanged and dispatching ( OnNext , OnError and OnCompleted are not executed in the UI stream). Imagine that your favorite MVVM framework took over these tasks.

Perhaps this article could have been completed, but we didn’t open up the question of testing at all. Indeed, writing unit tests to asynchronous code is often not very convenient. What can we say about asynchronous code using Rx?

Testing



Let's try to write a few unit tests for our ReactiveHabraClient and MainViewModel.

To do this, we will create a new project of the Class Library type, add a link to the main project and install several NuGet packages.
Namely: Rx-Main , Rx-Testing , Nunit and Moq .

 Install-Package Rx-Main Install-Package Rx-Testing Install-Package NUnit Install-Package Moq 


Create a ReactiveHabraClientTest class inherited from ReactiveTest .
ReactiveTest is the base class that comes with the Rx-Testing package. It defines several methods that will be useful to us when writing tests.

I will not litter the article with large listings, and here I will cite only one test for each of the classes. The rest of the tests will be available on GitHub. The link to the repository is at the end of the article.

Let's test the following scenario: With an empty cache, HabraClient should download the data, put it into the cache, call OnNext and OnCompleted .

For this we need a mock-and on IHttpClient , ICache . We also need the TestScheduler class from the Rx-Test package.
TestScheduler implements the IScheduler interface and can be substituted for the platform-specific implementation of the scheduler. The class allows us to literally manage time and execute the asynchronous code step by step. For those who wish, I highly recommend the excellent article Testing Rx Queries using Virtual Time Scheduling .

 [SetUp] public void SetUp() { Model = new KarmaModel {Karma = 10, LastModified = new DateTime(2014, 09, 10, 1, 1, 1, 0), UserName = USER_NAME}; Cache = new Mock<ICache>(); Scheduler = new TestScheduler(); HttpClient = new Mock<IHttpClient>(); } 


And let's start writing the test itself.
Arrange

Adjust Mock behavior: the cache will be empty, the data will be loaded successfully.
  Cache.Setup(c => c.HasCached(It.IsAny<string>())).Returns(false); HttpClient.Setup(http => http.Get(USER_NAME)).ReturnsAsync(new KarmaResponse { Data = Model, IsSuccessful = true }); var client = new ReactiveHabraClient(Cache.Object, HttpClient.Object, Scheduler); 


In the test case, we expect a sequence of one OnNext call and one OnCompleted call.
Create the following sequence:

  var expected = Scheduler.CreateHotObservable(OnNext(2, Model), OnCompleted<KarmaModel>(2)); 

This will require clarification. The OnNext(2, Model) method is a method defined in ReactiveTest .
His signature is as follows:
 public static Recorded<Notification<T>> OnNext<T>(long ticks, T value) 


In essence, it creates a record that the OnNext method was called with the Model parameter. The magic number 2 is the time in ticks for our TestScheduler . Not a very beautiful solution, but quite understandable. In the “tick” number zero we create TestScheduler , in the “tick” number one we subscribe to events, and in the “tick” number two the sequence of messages should go.

Act


 var results = Scheduler.Start(() => client.GetKarmaForUser(USER_NAME), 0, 1, 10); 


Here we run TestScheduler , which creates a zero tick, and subscribes to client.GetKarmaForUser(USER_NAME) in the first "tick". The last parameter is a “tick” on which Dispose will be called, but in this case this value is not important to us.

Finally, the last step.

Assert


  ReactiveAssert.AreElementsEqual(expected.Messages, results.Messages); Cache.Verify(cache => cache.Put(Model), Times.Once); 


We are convinced that the sequence of messages that we received coincides with the intended sequence. And also check that the updated model is stored in the cache.

Ideally, such a test would be worth breaking into two, but I would like to demonstrate that the techniques we are accustomed to continue to work in the rx-world.

The test for MainViewModel will be slightly different.

Create a Mock for the IHabraClient and declare a KarmaStream type
  Subject: 
[SetUp] public void SetUp() { Client = new Mock<IHabraClient>(); KarmaStream = new Subject<KarmaModel>(); }


Subject<T> IObservable IObserver . KarmaStream GetKarmaForUser OnNext , OnCompleted OnError . "" c TestScheduler .

:
[Test] public void KarmaValueSetToPropertyWhenOnNextCalled() { Client.Setup(client => client.GetKarmaForUser(USER_NAME)).Returns(KarmaStream); var viewModel = new MainViewModel(Client.Object, USER_NAME); KarmaStream.OnNext(new KarmaModel {Karma = 10}); Assert.AreEqual(10, viewModel.Karma); }



, GitHub .

Windows Phone, .Net 4.5. , WP SDK . , Class Library WP8 . , Rx PCL-.



. . , Windows Phone.

.



:
http://reactivex.io/ The Reactive Extensions (Rx)... EN " " RU Read-Through, Write-Through, Write-Behind, and Refresh-Ahead Caching EN Cache-Aside Pattern EN Testing Rx EN Testing Rx Queries using Virtual Time Scheduling EN Testing and Debugging Observable Sequences EN UDP C# Reactive Extensions RU https://www.websequencediagrams.com/
Subject:
[SetUp] public void SetUp() { Client = new Mock<IHabraClient>(); KarmaStream = new Subject<KarmaModel>(); }


Subject<T> IObservable IObserver . KarmaStream GetKarmaForUser OnNext , OnCompleted OnError . "" c TestScheduler .

:
[Test] public void KarmaValueSetToPropertyWhenOnNextCalled() { Client.Setup(client => client.GetKarmaForUser(USER_NAME)).Returns(KarmaStream); var viewModel = new MainViewModel(Client.Object, USER_NAME); KarmaStream.OnNext(new KarmaModel {Karma = 10}); Assert.AreEqual(10, viewModel.Karma); }



, GitHub .

Windows Phone, .Net 4.5. , WP SDK . , Class Library WP8 . , Rx PCL-.



. . , Windows Phone.

.



:
http://reactivex.io/ The Reactive Extensions (Rx)... EN " " RU Read-Through, Write-Through, Write-Behind, and Refresh-Ahead Caching EN Cache-Aside Pattern EN Testing Rx EN Testing Rx Queries using Virtual Time Scheduling EN Testing and Debugging Observable Sequences EN UDP C# Reactive Extensions RU https://www.websequencediagrams.com/
  Subject: 
[SetUp] public void SetUp() { Client = new Mock<IHabraClient>(); KarmaStream = new Subject<KarmaModel>(); }


Subject<T> IObservable IObserver . KarmaStream GetKarmaForUser OnNext , OnCompleted OnError . "" c TestScheduler .

:
[Test] public void KarmaValueSetToPropertyWhenOnNextCalled() { Client.Setup(client => client.GetKarmaForUser(USER_NAME)).Returns(KarmaStream); var viewModel = new MainViewModel(Client.Object, USER_NAME); KarmaStream.OnNext(new KarmaModel {Karma = 10}); Assert.AreEqual(10, viewModel.Karma); }



, GitHub .

Windows Phone, .Net 4.5. , WP SDK . , Class Library WP8 . , Rx PCL-.



. . , Windows Phone.

.



:
http://reactivex.io/ The Reactive Extensions (Rx)... EN " " RU Read-Through, Write-Through, Write-Behind, and Refresh-Ahead Caching EN Cache-Aside Pattern EN Testing Rx EN Testing Rx Queries using Virtual Time Scheduling EN Testing and Debugging Observable Sequences EN UDP C# Reactive Extensions RU https://www.websequencediagrams.com/
Subject:
[SetUp] public void SetUp() { Client = new Mock<IHabraClient>(); KarmaStream = new Subject<KarmaModel>(); }


Subject<T> IObservable IObserver . KarmaStream GetKarmaForUser OnNext , OnCompleted OnError . "" c TestScheduler .

:
[Test] public void KarmaValueSetToPropertyWhenOnNextCalled() { Client.Setup(client => client.GetKarmaForUser(USER_NAME)).Returns(KarmaStream); var viewModel = new MainViewModel(Client.Object, USER_NAME); KarmaStream.OnNext(new KarmaModel {Karma = 10}); Assert.AreEqual(10, viewModel.Karma); }



, GitHub .

Windows Phone, .Net 4.5. , WP SDK . , Class Library WP8 . , Rx PCL-.



. . , Windows Phone.

.



:
http://reactivex.io/ The Reactive Extensions (Rx)... EN " " RU Read-Through, Write-Through, Write-Behind, and Refresh-Ahead Caching EN Cache-Aside Pattern EN Testing Rx EN Testing Rx Queries using Virtual Time Scheduling EN Testing and Debugging Observable Sequences EN UDP C# Reactive Extensions RU https://www.websequencediagrams.com/

Subject:
[SetUp] public void SetUp() { Client = new Mock<IHabraClient>(); KarmaStream = new Subject<KarmaModel>(); }


Subject<T> IObservable IObserver . KarmaStream GetKarmaForUser OnNext , OnCompleted OnError . "" c TestScheduler .

:
[Test] public void KarmaValueSetToPropertyWhenOnNextCalled() { Client.Setup(client => client.GetKarmaForUser(USER_NAME)).Returns(KarmaStream); var viewModel = new MainViewModel(Client.Object, USER_NAME); KarmaStream.OnNext(new KarmaModel {Karma = 10}); Assert.AreEqual(10, viewModel.Karma); }



, GitHub .

Windows Phone, .Net 4.5. , WP SDK . , Class Library WP8 . , Rx PCL-.



. . , Windows Phone.

.



:
http://reactivex.io/ The Reactive Extensions (Rx)... EN " " RU Read-Through, Write-Through, Write-Behind, and Refresh-Ahead Caching EN Cache-Aside Pattern EN Testing Rx EN Testing Rx Queries using Virtual Time Scheduling EN Testing and Debugging Observable Sequences EN UDP C# Reactive Extensions RU https://www.websequencediagrams.com/

Subject:
[SetUp] public void SetUp() { Client = new Mock<IHabraClient>(); KarmaStream = new Subject<KarmaModel>(); }


Subject<T> IObservable IObserver . KarmaStream GetKarmaForUser OnNext , OnCompleted OnError . "" c TestScheduler .

:
[Test] public void KarmaValueSetToPropertyWhenOnNextCalled() { Client.Setup(client => client.GetKarmaForUser(USER_NAME)).Returns(KarmaStream); var viewModel = new MainViewModel(Client.Object, USER_NAME); KarmaStream.OnNext(new KarmaModel {Karma = 10}); Assert.AreEqual(10, viewModel.Karma); }



, GitHub .

Windows Phone, .Net 4.5. , WP SDK . , Class Library WP8 . , Rx PCL-.



. . , Windows Phone.

.



:
http://reactivex.io/ The Reactive Extensions (Rx)... EN " " RU Read-Through, Write-Through, Write-Behind, and Refresh-Ahead Caching EN Cache-Aside Pattern EN Testing Rx EN Testing Rx Queries using Virtual Time Scheduling EN Testing and Debugging Observable Sequences EN UDP C# Reactive Extensions RU https://www.websequencediagrams.com/

Subject:
[SetUp] public void SetUp() { Client = new Mock<IHabraClient>(); KarmaStream = new Subject<KarmaModel>(); }


Subject<T> IObservable IObserver . KarmaStream GetKarmaForUser OnNext , OnCompleted OnError . "" c TestScheduler .

:
[Test] public void KarmaValueSetToPropertyWhenOnNextCalled() { Client.Setup(client => client.GetKarmaForUser(USER_NAME)).Returns(KarmaStream); var viewModel = new MainViewModel(Client.Object, USER_NAME); KarmaStream.OnNext(new KarmaModel {Karma = 10}); Assert.AreEqual(10, viewModel.Karma); }



, GitHub .

Windows Phone, .Net 4.5. , WP SDK . , Class Library WP8 . , Rx PCL-.



. . , Windows Phone.

.



:
http://reactivex.io/ The Reactive Extensions (Rx)... EN " " RU Read-Through, Write-Through, Write-Behind, and Refresh-Ahead Caching EN Cache-Aside Pattern EN Testing Rx EN Testing Rx Queries using Virtual Time Scheduling EN Testing and Debugging Observable Sequences EN UDP C# Reactive Extensions RU https://www.websequencediagrams.com/

Subject:
[SetUp] public void SetUp() { Client = new Mock<IHabraClient>(); KarmaStream = new Subject<KarmaModel>(); }


Subject<T> IObservable IObserver . KarmaStream GetKarmaForUser OnNext , OnCompleted OnError . "" c TestScheduler .

:
[Test] public void KarmaValueSetToPropertyWhenOnNextCalled() { Client.Setup(client => client.GetKarmaForUser(USER_NAME)).Returns(KarmaStream); var viewModel = new MainViewModel(Client.Object, USER_NAME); KarmaStream.OnNext(new KarmaModel {Karma = 10}); Assert.AreEqual(10, viewModel.Karma); }



, GitHub .

Windows Phone, .Net 4.5. , WP SDK . , Class Library WP8 . , Rx PCL-.



. . , Windows Phone.

.



:
http://reactivex.io/ The Reactive Extensions (Rx)... EN " " RU Read-Through, Write-Through, Write-Behind, and Refresh-Ahead Caching EN Cache-Aside Pattern EN Testing Rx EN Testing Rx Queries using Virtual Time Scheduling EN Testing and Debugging Observable Sequences EN UDP C# Reactive Extensions RU https://www.websequencediagrams.com/

Subject:
[SetUp] public void SetUp() { Client = new Mock<IHabraClient>(); KarmaStream = new Subject<KarmaModel>(); }


Subject<T> IObservable IObserver . KarmaStream GetKarmaForUser OnNext , OnCompleted OnError . "" c TestScheduler .

:
[Test] public void KarmaValueSetToPropertyWhenOnNextCalled() { Client.Setup(client => client.GetKarmaForUser(USER_NAME)).Returns(KarmaStream); var viewModel = new MainViewModel(Client.Object, USER_NAME); KarmaStream.OnNext(new KarmaModel {Karma = 10}); Assert.AreEqual(10, viewModel.Karma); }



, GitHub .

Windows Phone, .Net 4.5. , WP SDK . , Class Library WP8 . , Rx PCL-.



. . , Windows Phone.

.



:
http://reactivex.io/ The Reactive Extensions (Rx)... EN " " RU Read-Through, Write-Through, Write-Behind, and Refresh-Ahead Caching EN Cache-Aside Pattern EN Testing Rx EN Testing Rx Queries using Virtual Time Scheduling EN Testing and Debugging Observable Sequences EN UDP C# Reactive Extensions RU https://www.websequencediagrams.com/

Subject:
[SetUp] public void SetUp() { Client = new Mock<IHabraClient>(); KarmaStream = new Subject<KarmaModel>(); }


Subject<T> IObservable IObserver . KarmaStream GetKarmaForUser OnNext , OnCompleted OnError . "" c TestScheduler .

:
[Test] public void KarmaValueSetToPropertyWhenOnNextCalled() { Client.Setup(client => client.GetKarmaForUser(USER_NAME)).Returns(KarmaStream); var viewModel = new MainViewModel(Client.Object, USER_NAME); KarmaStream.OnNext(new KarmaModel {Karma = 10}); Assert.AreEqual(10, viewModel.Karma); }



, GitHub .

Windows Phone, .Net 4.5. , WP SDK . , Class Library WP8 . , Rx PCL-.



. . , Windows Phone.

.



:
http://reactivex.io/ The Reactive Extensions (Rx)... EN " " RU Read-Through, Write-Through, Write-Behind, and Refresh-Ahead Caching EN Cache-Aside Pattern EN Testing Rx EN Testing Rx Queries using Virtual Time Scheduling EN Testing and Debugging Observable Sequences EN UDP C# Reactive Extensions RU https://www.websequencediagrams.com/

Source: https://habr.com/ru/post/237559/


All Articles