Build an activity feed using .NET
You need .NET and Visual Studio Code installed on your machine, along with the Visual Studio Code C# extension.
Getting a notification for an important event hours after the event is over is really annoying.
To be responsive, you need to be in sync with what is happening and as soon as it happens. Activity feeds allow you to visualize activities in your application and see what is happening in realtime.
In this tutorial, I’ll show you how to integrate an activity feed into your project using ASP.NET Core and Pusher’s Channels. We’ll start by building an application that will allow a user to add a product, view, change status, and delete a product. Then, we’ll create a new page that displays in realtime what is happening in the app.
Here is what the final app will look like:
Prerequisites
This tutorial uses the following technologies:
- JavaScript (jQuery)
- ASP.NET Core
- Visual Studio Code
- .NET Core SDK >= 2.0 (Download and install it here if you don’t have it)
- Visual Studio Code C# extension (Install C# extension from the Visual Studio Code Marketplace if you don’t have it installed)
- A Pusher account
Before we move on, verify that your installation is complete by typing the below command:
dotnet --version
If everything worked correctly, the above command will have an output like below:
Setting up a Pusher Channels application
To start utilizing Pusher’s technology, you need to create a Pusher app and get the app keys. Log in or sign up (if you don’t have an account already) for a free account.
Once you are logged in, create a new app then note down your app_id
, key
, secret
and cluster
. We’ll need it later.
Creating an ASP.NET Core MVC project
We’ll set up an ASP.NET Core project using the dotnet
command.
Open up a command line, and enter the below commands:
mkdir ProdFeed
cd ProdFeed
dotnet new mvc
In the above command,
- The first command will create a new folder named
ProdFeed
- The second command will change your current directory to the folder you just created
- And the last command will create a new ASP.NET Core MVC project in your current folder
Next,
- Open the
ProdFeed
folder in Visual Studio Code editor and select theStartup.cs
file.
💡 If your Visual Studio Code has been added to your system path, you can open the project by typing
code .
in your command prompt.
- Select Yes to the Warn message “Required assets to build and debug are missing from
ProdFeed
. Add them?” - Select Restore to the Info message “There are unresolved dependencies” if you got the message.
Now, press Debug (F5) to build and run the program. The address in which the project is running will open automatically if there is no error. In case it does not open automatically, navigate to http://localhost:5000/ from your browser. You should see a default page.
Next, update ProdFeed.csproj
with the following code:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.3" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.0" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.1" />
</ItemGroup>
</Project>
Save the file and select Restore to the Info message “There are unresolved dependencies”. This will prepare the project for scaffolding and enable entity framework tooling. Now we are ready to start building our application.
Adding models
A model is an object that represents the data in our application. For this project, we’ll create a model - Product
- which will hold our business logic for products.
Now, create a new file named Product.cs
in the Models
folder and add the below code to it:
using System;
namespace ProdFeed.Models
{
public class Product
{
public int ID { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public bool Status { get; set; }
public decimal Price { get; set; }
}
}
Creating the database context
The database context is the main class that coordinates Entity Framework functionality for a given data model. We’ll derive from the Microsoft.EntityFrameworkCore.DbContext
to create this class. When we run our migration, a table named Products
will be created which we’ll use to save products.
Create a new file called ProdFeedContext.cs
in the Models
folder and add the following code to it:
using Microsoft.EntityFrameworkCore;
namespace ProdFeed.Models
{
public class ProdFeedContext : DbContext
{
public ProdFeedContext (DbContextOptions<ProdFeedContext> options)
: base(options)
{
}
public DbSet<Product> Products { get; set; }
}
}
Setting up the database and running migrations
Now that we have created our models, we can easily generate a migration file that will contain code for creating and updating our table schema.
In this tutorial, we’ll make use of SQLite for our database.
We’ll register the database context with the dependency injection container. Services (such as the DB context) that are registered with the dependency injection container are available to the controllers.
Update the code in ConfigureServices
method of /Startup.cs
file with the following code:
[...]
public void ConfigureServices(IServiceCollection services)
{
[...]
services.AddDbContext<ProdFeedContext>(options =>
options.UseSqlite("Data Source=ProdFeed.db"));
[...]
}
[...]
This tells Entity Framework which model classes are included in the data model.
Finally, add the following usings to the header of Startup.cs
file:
using ProdFeed.Models;
using Microsoft.EntityFrameworkCore;
You can see the database context as a database connection and a set of tables, and the Dbset as a representation of the tables themselves.
The database context allows us to link our model properties to our database with a connection string (in our case, we are using SQLite)
Running the migration
From your command line, run the following command:
dotnet ef migrations add ProdFeed
dotnet ef database update
- The first command will create a migration script that will be used for managing our database tables
- The second command will execute the migration script, thereby applying the migration to the database to create the schema
💡 If you got an error while running the command, stop the debugging or the server and try again.
Adding our controllers
We’ll need two controllers - ProdctController
and FeedController
. The ProductController will be responsible for all product-related logic while the FeedController will be responsible for feeds related logic.
The Product controller
Now let’s create the ProductController. Create a new file called ProductController.cs
in the Controllers
folder and add the below code to it:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using ProdFeed.Models;
namespace ProdFeed.Controllers
{
public class ProductController : Controller
{
private readonly ProdFeedContext _context;
public ProductController(ProdFeedContext context)
{
_context = context;
}
}
}
Here, we have injected ProdFeedContext
class into the ProductController
class.
Next, let’s add a method for listing all the products to the views. Add the following code to ProductController.cs
:
[...]
public async Task<IActionResult> Index()
{
// get all products..
return View(await _context.Products.ToListAsync());
}
[...]
This will fetch all the products on the Products
table and pass it down to the view.
Next, add the following code to add the Create
method in ProductController.cs
:
[...]
[HttpPost]
public async Task<IActionResult> Create([Bind("ID,Name,Description,Status,Price")] Product product)
{
if (ModelState.IsValid)
{
_context.Products.Add(product);
await _context.SaveChangesAsync();
}
return RedirectToAction(nameof(Index));
}
[...]
This method will add a new product to the database.
Next, add the following code to add the Delete
method in ProductController.cs
:
[...]
[HttpGet]
public async Task<IActionResult> Delete(int id)
{
var product = new Product { ID = id };
_context.Products.Remove(product);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
[...]
This method will delete a product from the database using the product ID.
Next, add the following code to add the ChangeStatus
method in ProductController.cs
:
[...]
[HttpGet]
public async Task<IActionResult> ChangeStatus(int id)
{
var product = await _context.Products.SingleOrDefaultAsync(m => m.ID == id);
product.Status = !product.Status;
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
[...]
In this method, we’ll change the product status, either from “In stock” to “Out of stock” or vice versa.
With this, we now have four routes available:
- http://localhost:5000/Product/Index - for listing products
- http://localhost:5000/Product/Create - for creating new product
- http://localhost:5000/Product/Delete/{id} - for deleting a product
- http://localhost:5000/Product/ChangeStatus/{id} - for changing a product status
Although, if you visit any of the routes, you’ll get an error because we are yet to create their respective views.
The Feedback controller
Next, let’s create the controller for feeds.
Create a new file named FeedController.cs
in the Controllers
folder and add the following code to it:
using Microsoft.AspNetCore.Mvc;
namespace ProdFeed.Controllers
{
public class FeedController : Controller
{
public IActionResult Index()
{
return View();
}
}
}
Adding the views
Now, let’s craft out our views. The layout view allows us to define a common site template, which can be inherited in multiple views to provide a consistent look and feel across multiple pages of our application.
Replace the content in Views/Shared/_Layout.cshtml
with the below:
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css">
<title>Hello, world!</title>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<a class="navbar-brand" href="#">Product</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarText">
<ul class="navbar-nav mr-auto">
<li class="nav-item active">
<a class="nav-link" href="#">Home <span class="sr-only">(current)</span></a>
</li>
</ul>
</div>
</nav>
<div class="container-fluid">
@RenderBody()
</div>
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"></script>
</body>
</html>
For the product page, create a new folder named Product
in the Views
folder then create a new file called Index.cshtml
to the Product
folder.
Now, add the below code to Views``/Product/Index.cshtml
:
@model IEnumerable<ProdFeed.Models.Product>
<div class="row">
<div class="col">
<div style="padding: 40px;">
<h4 class="text-center">Add product</h4>
<form method="POST" action="/product/Create">
<div class="form-group">
<label for="name">Name</label>
<input
type="text"
class="form-control"
name="Name"
id="name"
placeholder="Gala"
>
</div>
<div class="form-group">
<label for="product_name">($)Price</label>
<input
type="text"
class="form-control"
name="Price"
id="Price"
placeholder="10"
>
</div>
<div class="form-group">
<label for="status">Availability</label>
<select class="form-control" id="Status" name="Status">
<option value="true">In stock</option>
<option value="false">Out of Stock</option>
</select>
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea
class="form-control"
id="description"
name="Description"
rows="3"
></textarea>
</div>
<button
type="submit"
role="submit"
class="btn btn-secondary btn-lg btn-block"
>
Add Product
</button>
</form>
</div>
</div>
<div class="col">
<div class="products" style="padding: 40px;">
<h4 class="text-center">Products</h4>
@foreach (var product in Model) {
<div class="product">
<div class="card" style="margin-bottom: 5px;">
<img
class="card-img-top"
height="250"
src="https://www.africalinked.com/images/product-default.png"
alt="Product image"
>
<div class="card-body">
<h5 class="card-title">@product.Name</h5>
<p class="card-text">@product.Description</p>
<p class="card-text">$@product.Price</p>
@if (product.Status) {
<span style="color: green">In Stock</span>
} else {
<span style="color: red"> Out of Stock </span>
}
</div>
<div class="card-footer">
<div class="row">
<div class="col">
<a
type="link"
asp-controller="Product"
asp-route-id="@product.ID"
asp-action="Delete"
role="button"
class="btn btn-secondary btn-lg btn-block"
>
Delete
</a>
</div>
<div class="col">
<a
type="link"
asp-controller="Product"
asp-route-id="@product.ID"
asp-action="ChangeStatus"
role="button"
class="btn btn-secondary btn-lg btn-block"
>
@if (!product.Status) {
<span>In stock</span>
}
else
{
<span>Out of Stock</span>
}
</a>
</div>
</div>
</div>
</div>
</div>
}
</div>
</div>
</div>
Now, we have our product page where users can add, delete or change the status of a product. The page will have two columns. The first column is for adding a new product while the second column will be used to display products.
Creating the feed page
Create a new folder named Feed
in the Views
folder then add a new file called Index.cshtml
to the Feed
folder.
Now, add the below code to Views``/Feed/Index.cshtml
:
<style>
.feed {
padding: 2px 10px;
background-color:#6c757d;
margin: 4px;
color:aliceblue;
border-radius: 3px;
}
</style>
<div class="row">
<div class="col">
<div class="container" style="padding: 40px;">
<h4 class="text-center">Feeds</h4>
<div id="feeds">
<!-- feeds -->
</div>
</div>
</div>
</div>
<script src="https://js.pusher.com/4.2/pusher.min.js"></script>
Subscribing to a channel, triggering and listening for events
We’ll subscribe to a channel called feed
on the feed page. Then we’ll continuously listen for new_feed
events. When there is any activity, we’ll trigger an event to Pusher’s server so that Pusher will broadcast the event to the client (our feed page). Then we’ll act on the event to display the feed for that activity.
Installing the Pusher library
Pusher has a .NET library that makes it easy to interact with its API. We need to add this to the project.
From your command line, install the library by running the below command:
dotnet add package PusherServer
Channel helper class and events
Let’s create a helper class that we’ll use to trigger event to Pusher.
Create a new folder called Helpers
in the root folder of the project then create a new file named ChannelHelper.cs
in the folder you just created.
Then, add the following code to ChannelHelper.cs
:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using PusherServer;
namespace ProdFeed.Helpers
{
public class Channel
{
public static async Task<IActionResult> Trigger(object data, string channelName, string eventName)
{
var options = new PusherOptions
{
Cluster = "<PUSHER_APP_CLUSTER>",
Encrypted = true
};
var pusher = new Pusher(
"<PUSHER_APP_ID>",
"<PUSHER_APP_KEY>",
"<PUSHER_APP_SECRET>",
options
);
var result = await pusher.TriggerAsync(
channelName,
eventName,
data
);
return new OkObjectResult(data);
}
}
}
In the preceding code,
- We created a method called
Trigger
which acceptsdata
,channelName
andeventName
as parameters. We’ll use this method to trigger events to Pusher - Then, we included the Pusher library. Although we’ve not yet installed the library, we’ll do so in the next step
- Next, we initialized the .NET library
- Finally, we triggered an event to Pusher using the parameters passed to the method
Make sure to update the code with your correct Pusher keys.
Import the ChannelHelper class to ProductController.cs
Add the bellow using to ProductController.cs
:
using ProdFeed.Helpers;
Trigger an event when a new product is added
Update the Create
method in the ProductController.cs
with the folowing code:
[HttpPost]
public async Task<IActionResult> Create([Bind("ID,Name,Description,Status,Price")] Product product)
{
if (ModelState.IsValid)
{
_context.Products.Add(product);
await _context.SaveChangesAsync();
var data = new {
message = System.String.Format("New product with ID of #{0} added", product.ID)
};
await Channel.Trigger(data, "feed", "new_feed");
}
return RedirectToAction(nameof(Index));
}
Here we added code for triggering an event to Pusher once a new product has been created using await Channel.Trigger(data, "feed", "new_feed");
.
Trigger an event when a product is deleted
Next, update the Delete
method in the ProductController.cs
class with the following code:
[HttpGet]
public async Task<IActionResult> Delete(int id)
{
var product = new Product { ID = id };
_context.Products.Remove(product);
await _context.SaveChangesAsync();
var data = new {
message = System.String.Format("Product with ID of #{0} deleted", product.ID)
};
await Channel.Trigger(data, "feed", "new_feed");
return RedirectToAction(nameof(Index));
}
When we delete a product, we’ll trigger an event to Pusher.
Trigger an event when a product status is changed
Finally, update the ChangeStatus
method in the ProductController.cs
class with the following code:
[HttpGet]
public async Task<IActionResult> ChangeStatus(int id)
{
var product = await _context.Products.SingleOrDefaultAsync(m => m.ID == id);
product.Status = !product.Status;
await _context.SaveChangesAsync();
var status = product.Status ? "In stock" : "Out of Stock";
var data = new {
message = System.String.Format("Status of product with ID #{0} status changed to '{1}'", product.ID, status)
};
await Channel.Trigger(data, "feed", "new_feed");
return RedirectToAction(nameof(Index));
}
When the status of a product changes, we’ll trigger an event to Pusher using await Channel.Trigger(data, "feed", "new_feed");
Listening and responding to events
Now we can trigger events on the server side when there is an activity going on. Next, we’ll respond to those events on the client side. We’ll do this using the Pusher JavaScript library we’ve included earlier.
Initiate the Pusher JavaScript library by adding the below code to Views/Feed/Index.cshtml
file:
[...]
<script type="text/javascript">
const pusher = new Pusher('<PUSHER_APP_KEY>', {
cluster: '<PUSHER_APP_CLUSTER>'
});
</script>
Make sure to update the code with your correct Pusher keys.
Next, subscribe to a channel. Add the below code to Views/Feed/Index.cshtml
between the <script>
tag:
[...]
const channel = pusher.subscribe('feed');
[...]
Next, listen for new_feed
events and respond to them when they happen. Add the below code to Views/Feed/Index.cshtml
between the <script>
tag:
[...]
channel.bind('new_feed', function(data) {
$("#feeds").append(`
<div class="feed">
<div class="feed" style="margin-bottom: 5px;">
${data.message}
</div>
</div>
`);
});
[...]
And that’s it! There you have your working activity feed. Load up the pages (the product page - http://localhost:5000/Product and the feed page - http://localhost:5000/Feed) in a different tab in your browser then add or delete a product.
Conclusion
In this tutorial, we built a simple app to demonstrate how you can add an activity feed to your apps. Feeds will be visible to every user using the app. But at times, this might not be what you want. You may want to send the notification to some targeted user. This means you need to subscribe to a private channel instead of a public channel. You can read more about the private channels here.
Also, you can get the complete code of this app on Github.
20 May 2018
by Gideon Onwuka