The other entity is related to Article. Named ArticleHit, it creates a new record every time someone visits a particular article. This will help us generate a query of "most viewed" articles.
// ArticleHit.cs
public class ArticleHit : Base
{
public int ArticleId { get; set; }
public Article Article { get; set; }
public static void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<ArticleHit>()
.HasOne(prop => prop.Article)
.WithMany()
.HasPrincipalKey(article => article.Id)
.HasForeignKey(articleHit => articleHit.ArticleId);
}
}
Now, you may have noticed that both of these entities inherit a class called "Base". This basically stores properties such as a unique ID, when it was created, last updated or whether it's deleted.
We used the "Base" class a lot in my article entitled "Create CRUD API Endpoints with ASP.NET Core & Entity Framework". You can read more about it and how we use it to set up the entities in our DbContext.
The Services
With the project set up, we can go ahead and implement from services. The first thing we want to do is to set up a class that will store the data for our "Most Viewed" results. We want to store information about the article including the ID and the title. In addition, we want to store how many hits that article has had.
// MostViewedArticleView.cs
public class MostViewedArticleView
{
public int ArticleId { get; set; }
public int Hits { get; set; }
public string Title { get; set; }
}
Next, we are going to set up a service. This service will be responsible for storing our "most viewed" articles list and will be a singleton. As a result, the ASP.NET Core API can be used to retrieve the "most viewed" articles list and display it in an API endpoint.
// IMostViewedArticleService.cs
public interface IMostViewedArticleService
{
IEnumerable<MostViewedArticleView> MostViewedArticles { get; set; }
}
// MostViewedArticleService.cs
public class MostViewedArticleService : IMostViewedArticleService
{
public IEnumerable<MostViewedArticleView> MostViewedArticles { get; set; }
}
Now it's time to set up our background service.
Hosted Service
For our background service, we are going to create a class which will inherit the BackgroundService class. The BackgroundService class has an ExecuteAsync abstract method that we need to integrate.
As you can probably guess, the ExecuteAsync method contains the code that is executed when the background service is ran.
Executing The Task
The ExecuteAsync method will generate a query in Entity Framework to get the most viewed articles in the last 60 seconds. Once it's complete, it will delay the task by a minute before it is ran again.
But, how do we get Entity Framework to work in a service like this? What we can do is inject the IServiceProvider interface into our hosted service. Thereafter, we can create a new scoped instance. This scoped instance is specific to our ExecuteAsync method. It's from there that we can get a reference to our DbContext from our localised scope and execute the relevant queries.
Once we have the results, we store them in the MostViewedArticles property in the MostViewedArticleService.
// MostViewedArticleHostedService.cs
public class MostViewedArticleHostedService : BackgroundService
{
protected IServiceProvider _serviceProvider;
protected IMostViewedArticleService _mostViewedArticleService;
public MostViewedArticleHostedService([NotNull] IServiceProvider serviceProvider, [NotNull] IMostViewedArticleService mostViewedArticleService)
{
_serviceProvider = serviceProvider;
_mostViewedArticleService = mostViewedArticleService;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
using (var scope = _serviceProvider.CreateScope())
{
var hostedServicesDbContext = (HostedServicesDbContext)scope.ServiceProvider.GetRequiredService(typeof(HostedServicesDbContext));
var timeFrom = DateTimeOffset.Now.AddSeconds(-60);
_mostViewedArticleService.MostViewedArticles = hostedServicesDbContext.Set<ArticleHit>()
.Join(
hostedServicesDbContext.Set<Article>(),
articleHit => articleHit.ArticleId,
article => article.Id,
(articleHit, article) => new { ArticleHit = articleHit, Article = article }
)
.Where(g => g.ArticleHit.Created >= timeFrom)
.GroupBy(g => g.Article.Id)
.Select(g => new MostViewedArticleView { ArticleId = g.Key, Title = g.Min(t => t.Article.Title), Hits = g.Count() })
.OrderByDescending(g => g.Hits)
.ToList();
}
await Task.Delay(new TimeSpan(0, 1, 0));
}
}
}
In this method, you can see that we have built up a query where each result gets stored into a MostViewedArticleView class. But why are we explicitly defining a join by using the .Join method? Why not just the .Include method?
Because we are using the .GroupBy method, it tends to lose any reference to any includes when using this. So we need to explicitly define the join in our query to get a reference to Article.
Adding the Services to Startup
At the moment, neither the singleton or the hosted service will create an instance in our ASP.NET Core API. We need to configure the services in our Startup class. In-order to do this, we need to use the AddSingleton and AddHostedService functions respectively.
// Startup.cs
public class Startup
{
...
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
...
services.AddSingleton<IMostViewedArticleService, MostViewedArticleService>();
services.AddHostedService<MostViewedArticleHostedService>();
}
...
}
Testing our Service
We created an controller in our ASP.NET Core API project. This has two endpoints. The first creates a new entity in the ArticleHit table when called. The other gets a reference to our MostViewedArticleService singleton class which displays the results of our "most viewed" articles.
// ArticleHitController.cs
[ApiController]
[Route("api/article-hit")]
public class ArticleHitController : Controller
{
protected readonly HostedServicesDbContext _hostedServicesDbContext;
protected readonly IMostViewedArticleService _mostViewedArticleService;
public ArticleHitController([NotNull] HostedServicesDbContext hostedServicesDbContext, [NotNull] IMostViewedArticleService mostViewedArticleService)
{
_hostedServicesDbContext = hostedServicesDbContext;
_mostViewedArticleService = mostViewedArticleService;
}
[HttpPost]
public async Task<IActionResult> CreateAsync(ArticleHit entity)
{
await _hostedServicesDbContext.Set<ArticleHit>().AddAsync(entity);
await _hostedServicesDbContext.SaveChangesAsync();
return Ok(entity);
}
[HttpGet("most-viewed")]
public IActionResult GetMostViewed()
{
return Ok(_mostViewedArticleService.MostViewedArticles);
}
}
To ensure this works, we used Postman to test out the API endpoints. We created a number of hits for several of our articles. Next, we put a breakpoint at the end of our ExecuteAsync method in MostViewedArticleHosted Service. This is so we know when the background service is ran. Once it's ran, we call our GetMostViewed API endpoint to check that the results have been updated. You can watch back our live stream of us doing it.