Recently, a customer engaged with my team because they have started to adopt Viva Connections in their company. They are happy about the experience, but they miss one piece of information that they consider important: the stock price of the company. It is quite a common use case, but Viva Connections doesn't provide this feature as built in. So, I decided: "Why not take advantage of this engagement and build something that can be useful to every customer who is adopting Viva Connections?". In this series of posts, we're going to build a Viva Connections card to display the price of any stock on the market, powered by a set of Azure services like Azure Functions and Azure Cache for Redis. This post will be focused on the backend, while in the next one we're going to build the actual card.
This is the final look of the project we're going to build:
The card displays the basic information (stock name and current price). When you click on it, an Adaptive Card will display more details on the stock.
We're going to leverage a service called Alpha Vantage to get the stock prices. It's a powerful service that offers an API that can return either a JSON or a CSV file and it supports a lot of market data. The company offers a free tier with some limitations (5 API requests per minute and 500 requests per day), so you might want to consider purchasing a paid tier for a production scenario. However, our backend will include a cache based on Azure Cache for Redis, which will help us to reduce the number of calls we must perform against the API.
We're going to use two different APIs offered by Alpha Vantage:
The first step is to head to the official website and register. You will receive an API key: store it somewhere since we'll need to use it later.
As I mentioned in the introduction, we won't call this API directly from the Viva Connections card, but we're going to build a middleware API, which will help us to improve performance and reduce the number of API calls we must perform against the Alpha Vantage APIs. We're going to build this API using Azure Functions: thanks to the serverless model, we can focus only on the code we need to write, without worrying about the infrastructure and the configuration. Additionally, pricing is very low, with one million executions per month included in the free plan.
Since I'm a .NET developer, I'm going to build the Azure Function in C# using Visual Studio 2022. If you're a web developer, you can use Visual Studio Code to create a project based on Node.js as runtime, which enables you to write the function with JavaScript / TypeScript. Start by creating a new project in Visual Studio using the Azure Functions template (you will need the Azure workload enabled), pick .NET 6.0 as runtime and choose Http Trigger with Open API as function type. For testing purposes, choose Anonymous as authentication.
As a first step, it's not required but I suggest you give a more meaningful name to the file and the class name that is generated by Visual Studio. But, most of all, set a meaningful name inside the [FunctionName]
attribute, since it will define the name of the API endpoint:
[FunctionName("StockPricesApi")]
The second change we must make is on the signature of the Azure Function. We need a way to get an input parameter from the caller, which is the stock symbol we want to track. There are multiple ways to do that: the one we're going to use is routing. The final segment of our API's URL will include the stock symbol. Let's change the signature of the Run()
method like this:
public async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "stockPrices/{stockSymbol}")] HttpRequest req, string stockSymbol)
Let's say we want to get the stock price of Microsoft. Thanks to this change, we can call the API with the following URL:
https://<azure-function-url>/api/stockPrices/MSFT
The Azure Functions runtime will automatically inject into the stockSymbol
parameter of the function the final segment of the URL, which includes the stock symbol. Now we have all the information we need to call the Alpha Vantage APIs but, before doing it, let's make our life easier, by adding a NuGet package to our project that will help us to work with the Alpha Vantage APIs without having to manually perform HTTP requets or to parse JSON. Right click on the project, choose Manage NuGet packages, look and install a package called AlphaVantage.Net.Stocks. Now we have everything we need to start calling the Alpha Vantage APIs. However, as mentioned in the introduction, the information we want our Azure Function to return is the combination of two different Alpha Vantage APIs: Intraday and Overview. A such, let's create a new class that will combine the information we want to display in the Viva Connections card in a single entity:
public class StockPrice
{
public decimal OpeningPrice { get; set; }
public decimal ClosingPrice { get; set; }
public decimal HighestPrice { get; set; }
public decimal LowestPrice { get; set; }
public long Volume { get; set; }
public string CompanyName { get; set; }
public string Exchange { get; set; }
public string Symbol { get; set; }
public DateTime Time { get; set; }
}
Now let's write some code using the AlphaVantage.Net.Stocks library which creates a new StockPrice
object pulling data from the two APIs:
[FunctionName("StockPricesApi")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "stockPrices/{stockSymbol}")] HttpRequest req, string stockSymbol)
{
string apiKey = "your-api-key";
var client = new AlphaVantageClient(apiKey);
var stocksClient = client.Stocks();
StockTimeSeries stock = await stocksClient.GetTimeSeriesAsync(stockSymbol, Interval.Min60, OutputSize.Compact, isAdjusted: true);
var query = new Dictionary<string, string>()
{
{"symbol", stockSymbol }
};
JsonDocument company = await client.RequestParsedJsonAsync(ApiFunction.OVERVIEW, query);
var stockQuote = new StockPrice {
OpeningPrice = stock.DataPoints.FirstOrDefault().OpeningPrice,
ClosingPrice = stock.DataPoints.FirstOrDefault().ClosingPrice,
HighestPrice = stock.DataPoints.FirstOrDefault().HighestPrice,
LowestPrice = stock.DataPoints.FirstOrDefault().LowestPrice,
Time = stock.DataPoints.FirstOrDefault().Time,
Volume = stock.DataPoints.FirstOrDefault().Volume,
CompanyName = company.RootElement.GetProperty("Name").GetString(),
Exchange = company.RootElement.GetProperty("Exchange").GetString(),
Symbol = company.RootElement.GetProperty("Symbol").GetString()
};
return new OkObjectResult(stockQuote);
}
The first step is to create an AlphaVantageClient
object, which requires the API key we got from the Alpha Vantage portal as initializer. Then, since we want to work with stock prices, we must get a specific client to call the Stocks()
function. Getting the prices is quite easy: we use the GetTimeSeriesAsync()
method, passing various parameters to customize the output (like the time interval or that we want the most recent prices instead of the whole history). Most importantly, as the first parameter we specify the stock symbol we want to track, which is coming from the URL of the API. In response, we get a StockTimeSeries
object, with a collection of DataPoints
objects. Each of them represents a stock value on a specific date and time, from the most recent to the oldest one. Our API needs to return the most recent one, so we pick the first item in the collection, and we use some of its properties to populate our StockPrice
object.
Getting the information about the company is a bit different, since the AlphaVantage.Net library doesn't include a specific client (and, thus, specific objects) to deal with the Company
entity like it does with stocks. As such, we must use the generic method RequestParsedJsonAsync()
, to which we pass:
ApiFunction
enumerator. In our case, it's OVERVIEW
.Dictionary<string, string>
collection. In our case, we must provide only one parameter called symbol
with, as value, the stock symbol of the company (again, we're getting this from the API URL).We get back a JsonDocument
object, which isn't strongly typed. As such, we must use the GetProperty()
method to get out the values from the JSON. The ones we are interested in are Name
, Exchange
and Symbol
. All of them are strings, so we extract their values by calling GetString()
. Thanks to this code, we can extract the information we need to complete our StockPrice
object, which we can wrap into an OkObjectResult
object and return it. This special ASP.NET object will automatically serialize our StockPrice
object, returning an equivalent JSON to the caller.
We can test our work by pressing F5. Visual Studio will launch the local Azure Functions emulator and our API will be hosted locally. To see if it's working, we can use a tool like Postman (or just open your browser) and hit the URL http://localhost:7071/api/stockPrices/{stockSymbol}
, replacing {stockSymbol}
with the stock symbol of a company, like MSFT or AAPL. If you did everything correctly, you would see a JSON like the following one:
The previous code has a flaw: we're keeping the API key in code, which is anything but good practice. First, because if we're hosting our project on a source control repository, everyone who has access to it will be able to see our key. Second, because if at any point in time we must change the API key, we are forced to build and deploy an updated version of our function. Let's improve our code and move the API key into the application settings. However, we aren't going to use the default ones (the ones stored in the local.settings.json
file) because also this file must be considered public, and it's typically committed as part of the project on a repository. We're going to use a .NET feature called Secret Manager. The way it works is like a regular settings file: it's a JSON file, in which we can store key / value pairs that are available to the web application. However, this file is stored in a location outside the project, so that it doesn't become part of the files that are included into the repository. What makes it powerful is that this file is completely transparent to us as developers since its content gets merged with the default application settings at runtime. As such, we can read settings from this file using the same exact code we use to read the other application settings.
To start, right click on your project and choose Add → Connected service. Under Service Dependencies, click on the + icon and look for a feature called Secrets.json (local).
Confirm the changes described in the next step and click Finish. This will install a NuGet package called Microsoft.Extensions.Configuration.UserSecrets. Now right click on the project and choose Manage user secrets. This step will do two operations:
UserSecretsId
. It's a GUID that univocally identifies the secrets of your application.%APPDATA%\Microsoft\UserSecrets\<userSecretsId>\
.Let's add our API key in the secrets.json file:
{
"AlphaVantageApiKey": "your-api-key"
}
Now we must change the code of our Azure Function so that the API key is read from the configuration. As mentioned, we don't have to make many changes, since the secrets are automatically merged into the Azure Functions configuration. All we need to do is to change the public constructor of our function to add a new parameter of type IConfiguration
:
private readonly ILogger<StockPricesApi> _logger;
private readonly IConfiguration _configuration;
public StockPricesApi(ILogger<StockPricesApi> log, IConfiguration configuration)
{
_logger = log;
_configuration = configuration;
}
Thanks to dependency injection, the runtime will automatically inject into the IConfiguration
object all the settings read from the secrets and the application settings. This means that, to get the API key, we just need to change the following line of code from:
string apiKey = "your-api-key";
var client = new AlphaVantageClient(apiKey);
var stocksClient = client.Stocks();
to:
string apiKey = _configuration.GetValue<string>("AlphaVantageApiKey");
var client = new AlphaVantageClient(apiKey);
var stocksClient = client.Stocks();
If you want to test that everything is still working, just press F5 and try to call again the endpoint http://localhost:7071/api/stockPrices/MSFT
. You should get a JSON again with all the stock information about Microsoft.
The current implementation works fine, but it has a limit. Every time we invoke the Azure Function, we're calling the Alpha Vantage APIs. If we pause and think to the full context of our solution (displaying the stock price in a Viva Connections card), this means that potentially hundreds or thousands of employees might open the Viva Connections dashboard in a very short time and all of them will try to get a fresh copy of the stock data from the API. This introduces two problems:
To reduce the impact of these two problems, we're going to introduce caching, using the Azure Cache for Redis service, which is one of the most popular solutions to support caching scenarios in your applications. By using this service, we're going to store the most recent stock pricing information into a cache. When the Azure Function gets called, it will try to get the price first from the cache; only if it doesn't exist or if the value is too old, we're going to hit the real Alpha Vantage APIs. Redis is powerful because, other than being simple to use and very efficient, it also takes care automatically of many of the challenges that you must face when you implement a caching mechanism. For example, it automatically manages the cache expiration, by deleting an item once the expiration time you have set has passed.
The easiest way to add caching is to right click again on your project, choose Add → Connected service and, under Service Dependencies, click the + symbol. This time, we're going to pick Azure Cache for Redis.
For this project, we're going to leverage the real Azure Cache for Redis service, so that we can simplify the developer environment. However, if you prefer to work on a completely local and offline solution, you can choose the option Redis Cache on container (local), which will create the Redis cache on a local container. This option requires you to install Docker Desktop, since the container is based on a Docker image.
If you haven't already done it, you will be asked to login with an account which is connected to an Azure subscription. Then click on Create new to start the wizard to create a new instance of the service. Feel free to pick the region, resource group and name you prefer. In terms of SKU, a Basic C0 is good enough for our scenario, since we'll need to store only JSON data. Once you have created and selected the service, Visual Studio will help you to configure your application to connect to it. First, it will add a setting in your configuration called CacheConnection
with, as value, the connection string. Since we have already set up the Secrets Manager support, make sure to select Local user secrets file: Secrets.json (local).
Once you complete the wizard, it's time to install a NuGet package that will help us to read and store data into the cache. Right click again on the project, choose Manage NuGet Packages, search for and install a package called StackExchange.Redis. Now we can leverage dependency injection so we can simplify the cache integration in our function. However, to do it, we must customize the initialization process of the Azure Function, which can be achieved by adding a new file to our project called Startup.cs
. This is the implementation you must supply:
[assembly: FunctionsStartup(typeof(StockPricesApi.Startup))]
namespace StockPricesApi
{
public class Startup: FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
}
}
}
The Startup
class must implement a class called FunctionsStartup
and, as header, we must use the assembly
attribute to declare the definition of the current class as the one to use when the Azure Functions starts up. Inside the Configure()
method we have access to the IFunctionsHostBuilder
object, which we can use to customize the Azure Functions initialization. Among the various tasks we can perform, we can use it to register new objects in the dependency injection container other than the default ones we have already used (like ILogger
and IConfiguration
). We can use it to initialize the Redis connection as following:
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using StackExchange.Redis;
[assembly: FunctionsStartup(typeof(StockPricesApi.Startup))]
namespace StockPricesApi
{
public class Startup: FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
var connection = builder.GetContext().Configuration["CacheConnection"];
var muxer = ConnectionMultiplexer.Connect(connection);
builder.Services.AddSingleton<IConnectionMultiplexer>(muxer);
}
}
}
First, we retrieve the connection string from the CacheConnection
entry in the configuration. Then, we create a connection using the ConnectionMultiplexer.Connect()
method, passing as parameter the connection string. Finally, we register the connection object in the dependency injection container, by passing it to the builder.Services.AddSingleton<IConnectionMultiplexer>()
method.
Now, to access to the cache in our function, we just need to add a new IConnectionMultiplexer
parameter to the the initializer of our Function class:
private readonly ILogger<StockPricesApi> _logger;
private readonly IConnectionMultiplexer _redisCache;
private readonly IConfiguration _configuration;
public StockPricesApi(ILogger<StockPricesApi> log, IConnectionMultiplexer redisCache, IConfiguration configuration)
{
_logger = log;
_redisCache = redisCache;
_configuration = configuration;
}
Now we can use the _redisCache
object to read and write data against the Azure Cache for Redis service we have just set up. Redis works similarly to the application settings, so using a key / value pair approach. In our case, the key will be dynamic: since the Viva Connections card can be used to display information about any stock, we need to create a different entry in the cache for each stock; the value will be the JSON returned by the Alpha Vantage API for that stock.
Let's see how our function code changes to use the cache:
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "stockPrices/{stockSymbol}")] HttpRequest req, string stockSymbol)
{
_logger.LogInformation("C# HTTP trigger function processed a request.");
_logger.LogInformation($"Requested stock symbol: {stockSymbol}");
var db = _redisCache.GetDatabase();
var json = await db.StringGetAsync($"StockPrice-{stockSymbol}");
StockPrice stockQuote;
if (string.IsNullOrEmpty(json))
{
_logger.LogInformation($"Stock price for {stockSymbol} not available in cache, getting a fresh value");
string apiKey = _configuration.GetValue<string>("AlphaVantageApiKey");
var client = new AlphaVantageClient(apiKey);
var stocksClient = client.Stocks();
StockTimeSeries stock = await stocksClient.GetTimeSeriesAsync("MSFT", Interval.Min60, OutputSize.Compact, isAdjusted: true);
var query = new Dictionary<string, string>()
{
{"symbol", stockSymbol }
};
JsonDocument company = await client.RequestParsedJsonAsync(ApiFunction.OVERVIEW, query);
stockQuote = new StockPrice {
OpeningPrice = stock.DataPoints.FirstOrDefault().OpeningPrice,
ClosingPrice = stock.DataPoints.FirstOrDefault().ClosingPrice,
HighestPrice = stock.DataPoints.FirstOrDefault().HighestPrice,
LowestPrice = stock.DataPoints.FirstOrDefault().LowestPrice,
Time = stock.DataPoints.FirstOrDefault().Time,
Volume = stock.DataPoints.FirstOrDefault().Volume,
CompanyName = company.RootElement.GetProperty("Name").GetString(),
Exchange = company.RootElement.GetProperty("Exchange").GetString(),
Symbol = company.RootElement.GetProperty("Symbol").GetString()
};
var stockQuoteJson = JsonConvert.SerializeObject(stockQuote);
int cacheExpireInHours = 6;
await db.StringSetAsync($"StockPrice-{stockSymbol}", stockQuoteJson, TimeSpan.FromHours(cacheExpireInHours));
_logger.LogInformation($"Json saved into in Redis: {stockQuoteJson}");
}
else
{
stockQuote = JsonConvert.DeserializeObject<StockPrice>(json);
_logger.LogInformation($"Stock price for {stockSymbol} retrieved from Redis cache");
_logger.LogInformation($"Json read from Redis: {json}");
}
return stockQuote != null ? new OkObjectResult(stockQuote) : new NotFoundResult();
}
First, we get a reference to the Redis database by calling the GetDatabase()
method. Then, from the database, we try to read a value identified by the key StockPrice-{StockSymbol}
, where StockSymbol
is the symbol of the stock that the user is trying to retrieve. For example, if the API has been called with the endpoint /api/stockPrices/MSFT
, the function will look on Redis for an item with key StockPrice-MSFT
. The value we're expecting is a JSON string, so we use the StringGetAsync()
method. If the value is empty, it means that it hasn't cached before or that the value is expired.
In this case, we proceed with the original code we have previously written, which retrieves the data from the Alpha Vantage API to build a StockPrice
object.
The only difference is that, before returning it to the caller, we convert the StockPrice
object into a JSON string (using JsonConvert.SerializeObject()
) and we store it into the Redis cache by calling StringSetAsync()
. As parameters, we pass the key specific for this stock symbol, the JSON string, and the cache expiration. In this case, we're passing a fixed value (6 hours), but you can also turn this parameter into an application setting, so that you can change it at a later stage without redeploying the function. Cache expiration means that Redis will automatically invalidate the value after the time you have specified has passed. In this case, it means that the Azure Function will get a new one from Alpha Vantage the first time the API is called after 6 hours since the data for that stock symbol was originally acquired.
In case, instead, we have a valid value in the cache, we deserialize the JSON back into a StockPrice
object and we return it to the caller, without interacting at all with the Alpha Vantage APIs.
That's it. Now if you press F5 and you try again to hit the URL http://localhost:7071/api/stockPrices/MSFT
, the first time the JSON payload will come from the Alpha Vantage APIs, while every other call will get the value from the cache.
If you want to check that the Redis data is there (and eventually experiment with it), you can use a tool called Redis Insights, which gives you a UI to explore the content of a Redis cache. Once you have installed it, click on Add Redis database and use the Add Database manually option. Before adding it, however, you must temporarily disable SSL-only support for your Redis cache, otherwise the tool won't be able to connect. To do that, open the Azure portal and look for the Azure Cache for Redis you have previously created from Visual Studio. On the left blade, click on Advanced Settings and set to No the option Allow access only via SSL.
For security reasons, you should change the option back to Yes as soon as you have finished using Redis Insight.
Now you can go back to the Redis Insight and add the connection with the following parameters:
your-service.redis.cache.windows.net:6379
).Then click Add Redis database. If all the information is correct, you will see your service listed in the main table and, clicking on it, you will see the available data. If you have implemented caching in your Azure Function correctly, you will see an entry with the name StockPrice-
followed by the stock symbol you have chosen and, if you click on it, you will see the full JSON from your API:
The tool is helpful also to debug and diagnose the cache, since you can edit and delete the stored value. This way, for example, you can trigger a new Alpha Vantage API request from the Azure Function without having to wait for the cache to expire.
In this post, we have focused on the backend of our Viva Connections solution to display stock prices. In the next one, we'll complete the project, and we'll build the actual card, which will use the backend we have just built to get the data to display.
You can find the project written so far on GitHub. The project also includes a Bicep template that you can use to deploy on Azure all the required resources (Azure Functions and Azure Cache for Redis) configured in the correct way. We'll talk more about this in the final post of this series.
Happy coding!
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.