Monthly Archives: December 2018

Adding a log4Net provider in .net core console app

I recently was developing a console application in .net core, where I had to use log4net logging.

In the standard asp.net core approach we can use:

    public void Configure(IApplicationBuilder app, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory)
    {
        loggerFactory.AddLog4Net();
    }

But this is .net core console application, where I’m creating LoggerFactory on my own, so it would not work.

In order to solve it, I had to implement my own Log4NetProvider, that would implement ILoggerProvider.

    public class Log4NetProvider : ILoggerProvider
    {
        private readonly string _log4NetConfigFile;

        private readonly bool _skipDiagnosticLogs;

        private readonly ConcurrentDictionary<string, ILogger> _loggers =
            new ConcurrentDictionary<string, ILogger>();

        public Log4NetProvider(string log4NetConfigFile, bool skipDiagnosticLogs)
        {
            _log4NetConfigFile = log4NetConfigFile;
            _skipDiagnosticLogs = skipDiagnosticLogs;
        }

        public ILogger CreateLogger(string categoryName)
        {
            return _loggers.GetOrAdd(categoryName, CreateLoggerImplementation);
        }

        public void Dispose()
        {
            _loggers.Clear();
        }

        private ILogger CreateLoggerImplementation(string name)
        {
            return new Log4NetLogger(name, new FileInfo(_log4NetConfigFile), _skipDiagnosticLogs);
        }
    }

And the implementation of an actual logger:

    public class Log4NetLogger : ILogger
    {
        private readonly string _name;

        private readonly ILog _log;

        private readonly bool _skipDiagnosticLogs;

        private ILoggerRepository _loggerRepository;

        public Log4NetLogger(string name, FileInfo fileInfo, bool skipDiagnosticLogs)
        {
            _name = name;
            _loggerRepository = LogManager.GetRepository(Assembly.GetEntryAssembly());
            _log = LogManager.GetLogger(_loggerRepository.Name, name);
            _skipDiagnosticLogs = skipDiagnosticLogs;

            log4net.Config.XmlConfigurator.Configure(_loggerRepository, fileInfo);
        }

        public IDisposable BeginScope<TState>(TState state)
        {
            return null;
        }

        public bool IsEnabled(LogLevel logLevel)
        {
            switch (logLevel)
            {
                case LogLevel.Critical:
                    return _log.IsFatalEnabled;
                case LogLevel.Debug:
                case LogLevel.Trace:
                    return _log.IsDebugEnabled && AllowDiagnostics();
                case LogLevel.Error:
                    return _log.IsErrorEnabled;
                case LogLevel.Information:
                    return _log.IsInfoEnabled && AllowDiagnostics();
                case LogLevel.Warning:
                    return _log.IsWarnEnabled;
                default:
                    throw new ArgumentOutOfRangeException(nameof(logLevel));
            }
        }

        public void Log<TState>(
            LogLevel logLevel,
            EventId eventId,
            TState state,
            Exception exception,
            Func<TState, Exception, string> formatter)
        {
            if (!IsEnabled(logLevel))
            {
                return;
            }

            if (formatter == null)
            {
                throw new ArgumentNullException(nameof(formatter));
            }

            string message = $"{formatter(state, exception)} {exception}";

            if (!string.IsNullOrEmpty(message) || exception != null)
            {
                switch (logLevel)
                {
                    case LogLevel.Critical:
                        _log.Fatal(message);
                        break;
                    case LogLevel.Debug:
                    case LogLevel.Trace:
                        _log.Debug(message);
                        break;
                    case LogLevel.Error:
                        _log.Error(message);
                        break;
                    case LogLevel.Information:
                        _log.Info(message);
                        break;
                    case LogLevel.Warning:
                        _log.Warn(message);
                        break;
                    default:
                        _log.Warn($"Encountered unknown log level {logLevel}, writing out as Info.");
                        _log.Info(message, exception);
                        break;
                }
            }
        }

        private bool AllowDiagnostics()
        {
            if (!_skipDiagnosticLogs)
            {
                return true;
            }

            return !(_name.ToLower().StartsWith("microsoft")
                || _name == "IdentityServer4.AccessTokenValidation.Infrastructure.NopAuthenticationMiddleware");
        }
    }

One last touch is adding an extension for ILoggerFactory to be able to use AddLog4Net.

    public static class Log4netExtensions
    {
        public static ILoggerFactory AddLog4Net(this ILoggerFactory factory, bool skipDiagnosticLogs)
        {
            factory.AddProvider(new Log4NetProvider("log4net.config", skipDiagnosticLogs));
            return factory;
        }
    }

In my DI container registration, I added code:

var loggerFactory = new Microsoft.Extensions.Logging.LoggerFactory();
loggerFactory.AddLog4Net(true);
Container.RegisterInstance<Microsoft.Extensions.Logging.ILoggerFactory>(loggerFactory);

Now it will all works!

To see the whole code, go to my GitHub repository and check this commit: https://github.com/mikuam/console-app-net-core/commit/650ac5348886d3e0238dfec07076b959d62bd4ba

Hope that works for you!

Service Fabric Reliable Actors – is it faster then a regular micro-service approach?

Recently I’m diving into Microsoft actor model implementation – Service Fabric Reliable Actors. Apart from Microsoft Orleans, is another one worth looking into. Let’s start from the beginning.

What is Service Fabric? It is many things and can be compared to Kubernetes:

  • Simplify microservices development and application lifecycle management
  • Reliably scale and orchestrate containers and microservices
  • Data-aware platform for low-latency, high-throughput workloads with stateful containers or microservices
  • Run anything – your choice of languages and programming models
  • Run anywhere – supports Windows/Linux in Azure, on-premises, or other clouds
  • Scales up to thousands of machines

Source: https://azure.microsoft.com/en-us/services/service-fabric/

From my perspective, it is just another way to manage micro-services. It can be set up on Azure or on-premise. Its biggest disadvantage is it’s dashboard, that does not offer much, comparing to IIS or Azure.

What are Reliable Actors? It is a Service Fabric implementation of an actor pattern, that is great for handling many small parallel operations. Actor, in this case, is a small piece of business logic, that can hold state and all actors can work simultaneously and independently, no matter if there is a hundred or hundred thousand of them.

If you’re new to actor model, you can have a look at an introduction to Microsoft Orleans. It covers all the basics: Getting started with Microsoft Orleans

Scenario

Let’s have an example to understand how all of this can be used in practice.

Let’s build price comparer micro-service, that will maintain sellers, products and offers for products. Every seller can have many offers for many products and every product will have many offers from many sellers. Something that in DB will look like this:

The main features of this service are:

  • it is a REST micro-service, so all communication will go through it’s API
  • it needs to persist its state
  • when getting a product, it needs to respond with json, where offers are sorted by seller rating descending

The last requirement forces us to update product offers whenever seller rating changes. Whenever seller rating changes, all its product offers need to be reordered. It sounds complicated, but it’s easier than it seems. API looks like this:

And Json that we would like to get in return, looks like this:

Simple micro-service approach

I already showed you how DB model can look like and this is precisely the way I’m going to implement it. Every operation will go to DB and take data from there. The architecture will be simple:

Of course I might keep my state in memory and update it whenever something changes, but this is rather difficult. In fact, cache invalidation is told to be one of the two hardest problems in software development. Right after naming things.

Let’s have a look how SellerController is built, it’s rather simple:

    [Route("api/[controller]")]
    [ApiController]
    public class SellerController : ControllerBase
    {
        private readonly ISellerRepository _sellerRepository;

        public SellerController(ISellerRepository sellerRepository)
        {
            _sellerRepository = sellerRepository;
        }

        [HttpGet("{id}")]
        public async Task<ActionResult<string>> Get(string id)
        {
            try
            {
                var seller = await _sellerRepository.Get(id);
                return new JsonResult(seller);
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                throw;
            }
        }

        [HttpPost]
        public async Task Post([FromBody] Seller seller)
        {
            try
            {
                await _sellerRepository.Save(seller);
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                throw;
            }
        }

        [HttpPost("{id}/mark/{mark}")]
        public async Task AddMark(string id, decimal mark)
        {
            try
            {
                var seller = await _sellerRepository.Get(id);
                if (seller == null)
                {
                    return;
                }

                seller.MarksCount += 1;
                seller.MarksSum += mark;

                await _sellerRepository.Update(seller);
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                throw;
            }
        }
    }

All the work is done in SellerRepository:

    public class SellerRepository : ISellerRepository
    {
        private const string RemoveSeller = @"DELETE FROM Seller WHERE Id = @Id";

        private const string InsertSeller = @"INSERT INTO Seller (Id, Name, MarksCount, MarksSum) VALUES (@Id, @Name, @MarksCount, @MarksSum)";

        private const string UpdateSellerRating = @"UPDATE Seller SET MarksCount = @MarksCount, MarksSum = @MarksSum WHERE Id = @Id";

        private const string GetSeller = @"SELECT Id, Name, MarksCount, MarksSum FROM Seller WHERE Id = @id";

        private const string GetSellerOffers = @"SELECT ProductId, Price FROM ProductOffer WHERE SellerId = @id";

        private readonly IConfigurationRoot _configuration;

        public SellerRepository(IConfigurationRoot configuration)
        {
            _configuration = configuration;
        }

        public async Task Save(Seller seller)
        {
            using (var connection = new SqlConnection(_configuration.GetConnectionString("DbConnectionString")))
            {
                await connection.ExecuteAsync(RemoveSeller, new { seller.Id });

                await connection.ExecuteAsync(InsertSeller, seller);
            }
        }

        public async Task<Seller> Get(string id)
        {
            using (var connection = new SqlConnection(_configuration.GetConnectionString("DbConnectionString")))
            {
                var sellerOffers = await connection.QueryAsync<Offer>(GetSellerOffers, new { id });
                var seller = await connection.QuerySingleAsync<Seller>(GetSeller, new { id });

                seller.Offers = sellerOffers.ToList();

                return seller;
            }
        }

        public async Task Update(Seller seller)
        {
            using (var connection = new SqlConnection(_configuration.GetConnectionString("DbConnectionString")))
            {
                await connection.ExecuteAsync(UpdateSellerRating, seller);
            }
        }
    }

To be able to use code like this:

connection.QuerySingleAsync<Seller>(GetSeller, new { id })

I used Dapper nuget package – very handy tool that enriches simple IDbConnection with new features.

Service Fabric approach

The functionality of Service Fabric implementation will be exactly the same. Small micro-service that exposes REST API and ensures that state is persistent. And this is where similarities end. First, let’s have a look at the project structure:

From the top:

  • MichalBialecki.com.SF.PriceComparer – have you noticed Service Fabric icon? It contains configuration how to set up SF cluster and what application should be hosted. It also defines how they will be scaled
  • PriceComparer – Business logic for API project, it contains actor implementation
  • PriceComparer.Api – REST API that we expose. Notice that we also have ServiceManifest.xml that is a definition of our service in Service Fabric
  • PriceComparer.Interfaces – the name speaks for itself, just interfaces and dtos

Controller implementation is almost the same as in the previous approach. Instead of using repository it uses actors.

    [Route("api/[controller]")]
    [ApiController]
    public class SellerController : ControllerBase
    {
        [HttpGet("{id}")]
        public async Task<ActionResult<string>> Get(string id)
        {
            try
            {
                var sellerActor = ActorProxy.Create<ISellerActor>(new ActorId(id));
                var seller = await sellerActor.GetState(CancellationToken.None);

                return new JsonResult(seller);
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                throw;
            }
        }

        [HttpPost]
        public async Task Post([FromBody] Seller seller)
        {
            try
            {
                var sellerActor = ActorProxy.Create<ISellerActor>(new ActorId(seller.Id));
                await sellerActor.AddSeller(seller, CancellationToken.None);

            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                throw;
            }
        }

        [HttpPost("{id}/offer")]
        public async Task AddOffer(string id, [FromBody] Offer offer)
        {
            try
            {
                var sellerActor = ActorProxy.Create<ISellerActor>(new ActorId(id));
                await sellerActor.AddOffer(offer, CancellationToken.None);
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                throw;
            }
        }

        [HttpPost("{id}/mark/{mark}")]
        public async Task AddMark(string id, decimal mark)
        {
            try
            {
                var sellerActor = ActorProxy.Create<ISellerActor>(new ActorId(id));
                await sellerActor.Mark(mark, CancellationToken.None);

            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                throw;
            }
        }
    }

ActorProxy.Create<ISellerActor> is the way we instantiate an actor, it is provided by the framework. Implementation of SellerActor needs to inherit Actor class. It also defines on top of the class how the state will be maintained. In our case it will be persisted, that means it will be saved as a file on a disk on the machine where the cluster is located.

    [StatePersistence(StatePersistence.Persisted)]
    internal class SellerActor : Actor, ISellerActor
    {
        private const string StateName = nameof(SellerActor);

        public SellerActor(ActorService actorService, ActorId actorId)
            : base(actorService, actorId)
        {
        }

        public async Task AddSeller(Seller seller, CancellationToken cancellationToken)
        {
            await StateManager.AddOrUpdateStateAsync(StateName, seller, (key, value) => value, cancellationToken);
        }

        public async Task<Seller> GetState(CancellationToken cancellationToken)
        {
            return await StateManager.GetOrAddStateAsync(StateName, new Seller(), cancellationToken);
        }

        public async Task AddOffer(Offer offer, CancellationToken cancellationToken)
        {
            var seller = await StateManager.GetOrAddStateAsync(StateName, new Seller(), cancellationToken);

            var existingOffer = seller.Offers.FirstOrDefault(o => o.ProductId == offer.ProductId);
            if (existingOffer != null)
            {
                seller.Offers.Remove(existingOffer);
            }

            seller.Offers.Add(offer);
            var sellerOffer = new SellerOffer
            {
                ProductId = offer.ProductId,
                Price = offer.Price,
                SellerId = seller.Id,
                SellerRating = seller.Rating,
                SellerName = seller.Name
            };

            var productActor = ActorProxy.Create<IProductActor>(new ActorId(offer.ProductId));
            await productActor.UpdateSellerOffer(sellerOffer, cancellationToken);

            await StateManager.SetStateAsync(StateName, seller, cancellationToken);
        }

        public async Task Mark(decimal value, CancellationToken cancellationToken)
        {
            var seller = await StateManager.GetOrAddStateAsync(StateName, new Seller(), cancellationToken);
            seller.MarksCount += 1;
            seller.MarksSum += value;

            await StateManager.SetStateAsync(StateName, seller, cancellationToken);

            foreach (var offer in seller.Offers)
            {
                var productActor = ActorProxy.Create<IProductActor>(new ActorId(offer.ProductId));
                await productActor.UpdateSellerRating(seller.Id, seller.Rating, cancellationToken);
            }
        }
    }

Notice that in order to use state, we use StateManager, also provided by the framework. The safest way is to either user GetOrAddStateAsync or SetStateAsync. Fun fact – all methods are asynchronous, there are no sync ones. There is a good emphasis on making code async, so that it can be run better in parallel with other jobs.

Take a look at Mark method. In order to mark a seller, we need to get its state, increment counters and save state. Then we need to update all product offers that seller has. Let’s take a look at how updating product looks like:

    public async Task UpdateSellerRating(string sellerId, decimal sellerRating, CancellationToken cancellationToken)
    {
        var product = await StateManager.GetOrAddStateAsync(StateName, new Product(), cancellationToken);

        var existingMatchingOffer = product.Offers.FirstOrDefault(o => o.SellerId == sellerId);
        if (existingMatchingOffer != null)
        {
            existingMatchingOffer.SellerRating = sellerRating;
            product.Offers = product.Offers.OrderByDescending(o => o.SellerRating).ToList();

            await StateManager.SetStateAsync(StateName, product, cancellationToken);
        }
    }

We are updating seller rating in his offer inside a product. That can happen for thousands of products, but since this job is done in different actors, it can be done in parallel. Architecture for this approach is way different when compared to simple micro-service.

Comparison

To compare both approaches I assumed I need a lot of data, so I prepared:

  • 1000 sellers having
  • 10000 products with
  • 100000 offers combined

It sounds a lot, but in a real-life price comparer, this could be just a start. A good starting point for my test, though. The first thing that hit me was loading this data into both services. Since both approaches exposed the same API, I just needed to make 11000 requests to initialize everything. With Service Fabric it all went well, after around 1 minute everything was initialized. However with simple DB approach… it throws SQL timeout exceptions. It turned out, that it couldn’t handle so many requests, even when I extended DB connection timeout. I needed to implement batch init, and after a few tries, I did it. However, the time that I needed to initialize all the data wasn’t so optimistic.

First two columns stand for initializing everything divided by 10, and second two stands for full initialization. Notice that a simple DB approach took around 5 times more than Service Fabric implementation!

To test the performance of my services I needed to send a lot of requests at the same time. In order to do that I used Locust – a performance load tool. It can be easily installed and set up. After preparing a small file that represents a testing scenario, I just run it from the terminal and then I can go to its dashboard, that is accessible via a browser.

Let’s have a look at how the performance of the first approach looks like. In this case, Locust will simulate 200 users, that grows from 0 to 200, 20 users per second. It will handle around 30 requests per second with an average response time 40 ms. When I update that value to 400 users, it will handle around 50 requests per minute, but response time will go to around 3 seconds. That, of course, is not acceptable in micro-service development.

The second video shows the same test, but hitting Service Fabric app. This time I’d like to go bold and start off with 1000 users. It will handle around 140 RPM with an average response time around 30ms, which is even faster than 400 users and first approach. Then I’ll try 2000 users, have a look:

Summary

I showed you two approaches, both written in .Net Core 2.0. The first one is a very simple one using SQL DB, and the second one is Service Fabric with Reliable Actors. From my tests, I could easily see that actors approach is way more performant. Probably around 5 times in this specific case.  Let’s point this out:

Pros:

  • very fast in scenarios, where there are many small pieces of business logic, tightly connected to data
  • trivial to try and implement – there is a Visual Studio project for that

Cons:

  • It’s more complicated to implement than the regular approach
  • Configuring Service Fabric with XML files can be frustrating
  • Since everything handled by the framework, a developer has a bit less control over what’s happening

All in all, in my opinion, it’s worth trying.

 

 All code posted here you can find on my GitHub:

 

Microsoft Orleans – is it fast?

Microsoft Orleans is a developer-friendly framework for building distributed, high-scale computing applications. It does not require from developer to implement concurrency and data storage model. It requires developer to use predefined code blocks and enforces application to be build in a certain way. As a result Microsoft Orleans empowers developer with a framework with an exceptional performance.

Orleans proved its strengths in many scenarios, where the most recognizable ones are cloud services for Halo 4 and 5 games.

You can have a look at full introduction in my previous post: Getting started with Microsoft Orleans

The Scenario

To test the performance of Microsoft Orleans I’ll compare it to simple micro-service implementation. The scenario is about transferring money from one account to another using a persistent storage. Here is the idea:

  • Both services will use .Net Core
  • Data will be saved in Azure CosmosDB database
  • Services will read and send messages from Service Bus
  • One message will trigger transferring money, that will need to get and save data from DB and then service will send two messages with account balance updates

Simple Micro-service approach

This app is really simple. It is a console application, that registers message handler and processes messages. This is how architecture looks like, simple right?

Code that handles message looks like this:

    public void Run()
    {
        var service = new TableStorageService(_configuration);

        try
        {
            var subscriptionClient = new SubscriptionClient(
                _configuration[ServiceBusKey],
                "accountTransferUpdates",
                "commonSubscription");
            subscriptionClient.PrefetchCount = 1000;

            subscriptionClient.RegisterMessageHandler(
                async (message, token) =>
                {
                    var messageJson = Encoding.UTF8.GetString(message.Body);
                    var updateMessage = JsonConvert.DeserializeObject<AccountTransferMessage>(messageJson);

                    await service.UpdateAccount(updateMessage.From, -updateMessage.Amount);
                    await service.UpdateAccount(updateMessage.To, updateMessage.Amount);

                    Console.WriteLine($"Processed a message from {updateMessage.From} to {updateMessage.To}");
                },
                new MessageHandlerOptions(OnException)
                {
                    MaxAutoRenewDuration = TimeSpan.FromMinutes(60),
                    MaxConcurrentCalls = 1,
                    AutoComplete = true
                });
        }
        catch (Exception e)
        {
            Console.WriteLine("Exception: " + e.Message);
        }
    }

    private Task OnException(ExceptionReceivedEventArgs args)
    {
        Console.WriteLine(args.Exception);

        return Task.CompletedTask;
    }

TableStorageService is used to synchronize state with the database, which in this case it read and update account balance.

    public class TableStorageService
    {
        private const string EndpointUriKey = "CosmosDbEndpointUri";
        private const string PrimaryKeyKey = "CosmosDbPrimaryKey";
        private const string ServiceBusKey = "ServiceBusConnectionString";

        private readonly DocumentClient client;
        private readonly TopicClient topic;

        public TableStorageService(IConfigurationRoot configuration)
        {
            client = new DocumentClient(new Uri(configuration[EndpointUriKey]), configuration[PrimaryKeyKey]);
            topic = new TopicClient(configuration[ServiceBusKey], "balanceUpdates");
        }
        
        public async Task UpdateAccount(int accountNumber, decimal amount)
        {
            Account document;
            try
            {
                var response = await client.ReadDocumentAsync<Account>(accountNumber.ToString());
                document = response.Document;
                document.Balance += amount;
                await client.ReplaceDocumentAsync(accountNumber.ToString(), document);
            }
            catch (DocumentClientException de)
            {
                if (de.StatusCode == HttpStatusCode.NotFound)
                {
                    document = new Account { Id = accountNumber.ToString(), Balance = amount };
                    await client.CreateDocumentAsync(UriFactory.CreateDocumentCollectionUri("bialecki", "accounts"), document);
                }
                else
                {
                    throw;
                }
            }

            await NotifyBalanceUpdate(accountNumber, document.Balance);
        }

        private async Task NotifyBalanceUpdate(int accountNumber, decimal balance)
        {
            var balanceUpdate = new BalanceUpdateMessage
            {
                AccountNumber = accountNumber,
                Balance = balance
            };

            var message = new Message(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(balanceUpdate)));
            await topic.SendAsync(message);
        }
    }

DocumentClient is CosmosDB client provided by the framework. You might be intrigued by try-catch clause. Currently for in CosmosDB package for .Net Core there is no way to check if the document exists and the proposed solution is to handle an exception when the document is not found. In this case, the new document will be created. NotifyBalanceUpdate sends messages to Service Bus.

When we go to Azure portal, we can query the data to check if it is really there:

This is how reading 100 messages looks like:

Microsoft Orleans approach

Microsoft Orleans is an actor framework, where each actor can be understood as a separate service, that does some simple operations and can have its own state. In this case, every account can be an actor, it doesn’t matter if we have few or few hundred thousands of them, the framework will handle that. Another big advantage is that we do not need to care about concurrency and persistence, it is also handled by the framework for us. In Orleans, accounts can perform operations in parallel.  In this case, the architecture looks much different.

Project structure looks like this:

  • SiloHost – sets up and run a silo to host grains, which is just another name for actors
  • OrleansClient – second application. This one connects to the silo and run client code to use grains
  • AccountTransfer.Interfaces – its an abstraction for grains
  • AccountTransfer.Grains – grains implementation, that handles business logic

Let’s have a look at how running a silo looks like:

    public class Program
    {
        private static IConfigurationRoot configuration;

        public static int Main(string[] args)
        {
            return RunMainAsync().Result;
        }

        private static async Task<int> RunMainAsync()
        {
            try
            {
                var builder = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);

                configuration = builder.Build();

                var host = await StartSilo();
                Console.WriteLine("Press Enter to terminate...");
                Console.ReadLine();

                await host.StopAsync();

                return 0;
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
                return 1;
            }
        }

        private static async Task<ISiloHost> StartSilo()
        {
            var builder = new SiloHostBuilder()
                .UseLocalhostClustering()
                .Configure<EndpointOptions>(options => options.AdvertisedIPAddress = IPAddress.Loopback)
                .ConfigureServices(context => ConfigureDI(context))
                .ConfigureLogging(logging => logging.AddConsole())
                .UseInClusterTransactionManager()
                .UseInMemoryTransactionLog()
                .AddAzureTableGrainStorageAsDefault(
                    (options) =>
                    {
                        options.ConnectionString = configuration.GetConnectionString("CosmosBDConnectionString");
                        options.UseJson = true;
                    })
                .UseTransactionalState();

            var host = builder.Build();
            await host.StartAsync();
            return host;
        }

        private static IServiceProvider ConfigureDI(IServiceCollection services)
        {
            services.AddSingleton<IServiceBusClient>((sp) => new ServiceBusClient(configuration.GetConnectionString("ServiceBusConnectionString")));

            return services.BuildServiceProvider();
        }
    }

This is the whole code. Amazingly short comparing to what we are doing here. Notice, that configuring CosmosDB Azure Table storage takes just a few lines. I even configured dependency injection that I will use in account grain.

This is how connecting to silo looks like:

    public class Program
    {
        private static IConfigurationRoot configuration;

        static int Main(string[] args)
        {
            return RunMainAsync().Result;
        }

        private static async Task<int> RunMainAsync()
        {
            try
            {
                var builder = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);

                configuration = builder.Build();

                using (var client = await StartClientWithRetries())
                {
                    DoClientWork(client);
                    Console.ReadKey();
                }

                return 0;
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                return 1;
            }
        }

        private static async Task<IClusterClient> StartClientWithRetries(int initializeAttemptsBeforeFailing = 5)
        {
            int attempt = 0;
            IClusterClient client;
            while (true)
            {
                try
                {
                    client = new ClientBuilder()
                        .UseLocalhostClustering()
                        .Configure<ClusterOptions>(options =>
                        {
                            options.ClusterId = "dev";
                            options.ServiceId = "AccountTransferApp";
                        })
                        .ConfigureApplicationParts(parts => parts.AddApplicationPart(typeof(IAccountGrain).Assembly).WithReferences())
                        .ConfigureLogging(logging => logging.AddConsole())
                        .Build();

                    await client.Connect();
                    Console.WriteLine("Client successfully connect to silo host");
                    break;
                }
                catch (SiloUnavailableException)
                {
                    attempt++;
                    Console.WriteLine($"Attempt {attempt} of {initializeAttemptsBeforeFailing} failed to initialize the Orleans client.");
                    if (attempt > initializeAttemptsBeforeFailing)
                    {
                        throw;
                    }
                    await Task.Delay(TimeSpan.FromSeconds(4));
                }
            }

            return client;
        }

        private static Task HandleException(ExceptionReceivedEventArgs args)
        {
            Console.WriteLine(args.Exception + ", stack trace: " + args.Exception.StackTrace);
            return Task.CompletedTask;
        }
    }

This is also a simple console application. Both apps need to be run together, cause client is connecting to the silo and if fails, tries again after few seconds. The only part missing here is DoClientWork method:

    private static void DoClientWork(IClusterClient client)
    {
        var subscriptionClient = new SubscriptionClient(
            configuration.GetConnectionString("ServiceBusConnectionString"),
            "accountTransferUpdates",
            "orleansSubscription",
            ReceiveMode.ReceiveAndDelete);
        subscriptionClient.PrefetchCount = 1000;

        try
        {
            subscriptionClient.RegisterMessageHandler(
                async (message, token) =>
                {
                    var messageJson = Encoding.UTF8.GetString(message.Body);
                    var updateMessage = JsonConvert.DeserializeObject<AccountTransferMessage>(messageJson);

                    await client.GetGrain<IAccountGrain>(updateMessage.From).Withdraw(updateMessage.Amount);
                    await client.GetGrain<IAccountGrain>(updateMessage.To).Deposit(updateMessage.Amount);
                        
                    Console.WriteLine($"Processed a message from {updateMessage.From} to {updateMessage.To}");
                    await Task.CompletedTask;
                },
                new MessageHandlerOptions(HandleException)
                {
                    MaxAutoRenewDuration = TimeSpan.FromMinutes(60),
                    MaxConcurrentCalls = 20,
                    AutoComplete = true
                });
        }
        catch (Exception e)
        {
            Console.WriteLine("Exception: " + e.Message);
        }
    }

This is almost the same code that we had in micro-service approach. We are reading Service Bus messages and deserialize them, but then we use actors. From this point execution will be handled by them. AccountGrain looks like this:

    [Serializable]
    public class Balance
    {
        public decimal Value { get; set; } = 1000;
    }

    public class AccountGrain : Grain<Balance>, IAccountGrain
    {
        private readonly IServiceBusClient serviceBusClient;

        public AccountGrain(
            IServiceBusClient serviceBusClient)
        {
            this.serviceBusClient = serviceBusClient;
        }

        async Task IAccountGrain.Deposit(decimal amount)
        {
            try
            {
                this.State.Value += amount;
                await this.WriteStateAsync();

                await NotifyBalanceUpdate();
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }
        }

        async Task IAccountGrain.Withdraw(decimal amount)
        {
            this.State.Value -= amount;
            await this.WriteStateAsync();

            await NotifyBalanceUpdate();
        }

        Task<decimal> IAccountGrain.GetBalance()
        {
            return Task.FromResult(this.State.Value);
        }

        private async Task NotifyBalanceUpdate()
        {
            var balanceUpdate = new BalanceUpdateMessage
            {
                AccountNumber = (int)this.GetPrimaryKeyLong(),
                Balance = this.State.Value
            };

            var message = new Message(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(balanceUpdate)));
            await serviceBusClient.SendMessageAsync(message);
        }
    }

Notice that on top we have serializable Balance class. When defining actor like this: AccountGrain : Grain<Balance>, it means that Balance will be our state, that we can later refer to as this.State. Getting and updating state is trivial, and both Withdraw and Deposit causes sending Service Bus message by calling NotifyBalanceUpdate.

In Azure portal we can have a look how data is saved. I choose to serialize it to json, so we can see account state easily:

Let’s have a look at reading 1000 messages by a single thread with Microsoft Orleans looks like:

It runs noticeably faster, but what’s more interesting is that we can read messages with even 20 concurrent threads at a time:

Comparsion

As you could see, I used two approaches to read and process 100 and 1000 Service Bus messages, written in .net core with a persistant state in remote CosmosDB database. Results can be seen here:

Blue color represents reading 100 messages, red represents reading 1000 messages. As you can see Microsoft Orleans is a few times faster.

To sum up, using Microsoft Orleans:

Pros:

  • Microsoft actor framework could give you outstanding performance
  • It requires minimal knowledge to write your first app
  • Documentation is great
  • The code is open source, you can post issues

Cons:

  • It doesn’t fit every scenario
  • Maintenance and deployment is a bit more difficult than a simple IIS app

 

If you’re interested in the code, have a look at my GitHub: