DEV Community

Cover image for Building an AI-Powered Recommendation System with .NET Core and ML.NET
hamza zeryouh
hamza zeryouh

Posted on

Building an AI-Powered Recommendation System with .NET Core and ML.NET

πŸ“š Table of Contents

  1. Introduction & Architecture
  2. Project Setup
  3. Core Domain Models
  4. Database Setup & Context
  5. Repository Pattern Implementation
  6. Content-Based Filtering Engine
  7. Collaborative Filtering with ML.NET
  8. Hybrid Recommendation Engine
  9. API Controllers & Endpoints
  10. Configuration & Dependency Injection
  11. Database Seeding & Test Data
  12. Error Handling & Middleware
  13. Testing
  14. Docker & Deployment
  15. Running the Application

1. Introduction & Architecture {#section-1}

What We're Building

A complete movie recommendation system that suggests personalized movies using:

  • βœ… Content-based filtering (based on movie features)
  • βœ… Collaborative filtering (based on user behavior patterns)
  • βœ… Hybrid approach (combining multiple strategies)
  • βœ… ML.NET matrix factorization (production-ready machine learning)

Architecture Overview

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   API Layer                          β”‚
β”‚  Controllers, Middleware, Authentication             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                   β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              Application/Core Layer                  β”‚
β”‚  Services, Entities, Interfaces, Business Logic      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                   β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚           Infrastructure Layer                       β”‚
β”‚  Repositories, DbContext, ML.NET, Caching            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                   β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              Database (SQL Server)                   β”‚
β”‚  Movies, Users, Interactions                         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

Tech Stack

  • .NET 8.0
  • ML.NET (Recommendation)
  • Entity Framework Core
  • SQL Server
  • Swagger/OpenAPI
  • xUnit (Testing)

2. Project Setup {#section-2}

Step 1: Create Solution Structure

# Create solution directory
mkdir MovieRecommendation
cd MovieRecommendation

# Create projects
dotnet new webapi -n MovieRecommendation.API
dotnet new classlib -n MovieRecommendation.Core
dotnet new classlib -n MovieRecommendation.Infrastructure
dotnet new xunit -n MovieRecommendation.Tests

# Create solution
dotnet new sln -n MovieRecommendation

# Add projects to solution
dotnet sln add MovieRecommendation.API
dotnet sln add MovieRecommendation.Core
dotnet sln add MovieRecommendation.Infrastructure
dotnet sln add MovieRecommendation.Tests

# Add project references
cd MovieRecommendation.API
dotnet add reference ../MovieRecommendation.Core
dotnet add reference ../MovieRecommendation.Infrastructure
cd ..

cd MovieRecommendation.Infrastructure
dotnet add reference ../MovieRecommendation.Core
cd ..

cd MovieRecommendation.Tests
dotnet add reference ../MovieRecommendation.API
dotnet add reference ../MovieRecommendation.Core
dotnet add reference ../MovieRecommendation.Infrastructure
cd ..
Enter fullscreen mode Exit fullscreen mode

Step 2: Install Required NuGet Packages

# Core project - ML.NET
cd MovieRecommendation.Core
dotnet add package Microsoft.ML --version 3.0.1
dotnet add package Microsoft.ML.Recommender --version 0.21.1
cd ..

# Infrastructure project - EF Core
cd MovieRecommendation.Infrastructure
dotnet add package Microsoft.EntityFrameworkCore --version 8.0.0
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 8.0.0
dotnet add package Microsoft.EntityFrameworkCore.Tools --version 8.0.0
dotnet add package Microsoft.Extensions.Caching.Memory --version 8.0.0
cd ..

# API project
cd MovieRecommendation.API
dotnet add package Swashbuckle.AspNetCore --version 6.5.0
dotnet add package Microsoft.EntityFrameworkCore.Design --version 8.0.0
cd ..

# Tests project
cd MovieRecommendation.Tests
dotnet add package Moq --version 4.20.70
dotnet add package Microsoft.EntityFrameworkCore.InMemory --version 8.0.0
cd ..
Enter fullscreen mode Exit fullscreen mode

3. Core Domain Models {#section-3}

MovieRecommendation.Core/Entities/Movie.cs

namespace MovieRecommendation.Core.Entities;

public class Movie
{
    public int Id { get; set; }
    public string Title { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;
    public List<string> Genres { get; set; } = new();
    public List<string> Actors { get; set; } = new();
    public string Director { get; set; } = string.Empty;
    public int ReleaseYear { get; set; }
    public int DurationMinutes { get; set; }
    public double AverageRating { get; set; }
    public int TotalRatings { get; set; }
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
Enter fullscreen mode Exit fullscreen mode

MovieRecommendation.Core/Entities/User.cs

namespace MovieRecommendation.Core.Entities;

public class User
{
    public int Id { get; set; }
    public string Username { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
    public List<string> FavoriteGenres { get; set; } = new();
    public List<string> FavoriteActors { get; set; } = new();
    public int PreferredMinYear { get; set; }
    public int PreferredMaxYear { get; set; } = 2024;
    public int PreferredMinDuration { get; set; }
    public int PreferredMaxDuration { get; set; } = 200;
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
Enter fullscreen mode Exit fullscreen mode

MovieRecommendation.Core/Entities/UserInteraction.cs

namespace MovieRecommendation.Core.Entities;

public class UserInteraction
{
    public int Id { get; set; }
    public int UserId { get; set; }
    public int MovieId { get; set; }
    public InteractionType Type { get; set; }
    public double? Rating { get; set; } // 1-5 stars
    public DateTime Timestamp { get; set; } = DateTime.UtcNow;

    // Navigation properties
    public User? User { get; set; }
    public Movie? Movie { get; set; }
}

public enum InteractionType
{
    View = 1,
    Rating = 2,
    Favorite = 3,
    WatchedComplete = 4,
    Dismissed = 5
}
Enter fullscreen mode Exit fullscreen mode

MovieRecommendation.Core/Models/MovieRating.cs (ML.NET Models)

using Microsoft.ML.Data;

namespace MovieRecommendation.Core.Models;

public class MovieRating
{
    [LoadColumn(0)]
    public float UserId { get; set; }

    [LoadColumn(1)]
    public float MovieId { get; set; }

    [LoadColumn(2)]
    public float Rating { get; set; }
}

public class MovieRatingPrediction
{
    public float Score { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

MovieRecommendation.Core/Models/RecommendationResult.cs

namespace MovieRecommendation.Core.Models;

public class RecommendationResult
{
    public int MovieId { get; set; }
    public string Title { get; set; } = string.Empty;
    public double OverallScore { get; set; }
    public double ContentScore { get; set; }
    public double CollaborativeScore { get; set; }
    public double PopularityScore { get; set; }
    public Dictionary<string, double> DetailedScores { get; set; } = new();
    public string ReasonForRecommendation { get; set; } = string.Empty;
    public DateTime GeneratedAt { get; set; } = DateTime.UtcNow;
}

public class RecommendationWeights
{
    public double ContentWeight { get; set; } = 0.4;
    public double CollaborativeWeight { get; set; } = 0.3;
    public double PopularityWeight { get; set; } = 0.2;
    public double RecencyWeight { get; set; } = 0.1;

    // Content-based sub-weights
    public double GenreWeight { get; set; } = 0.35;
    public double ActorWeight { get; set; } = 0.25;
    public double YearWeight { get; set; } = 0.20;
    public double DurationWeight { get; set; } = 0.20;
}
Enter fullscreen mode Exit fullscreen mode

4. Database Setup & Context {#section-4}

MovieRecommendation.Infrastructure/Data/AppDbContext.cs

using Microsoft.EntityFrameworkCore;
using MovieRecommendation.Core.Entities;

namespace MovieRecommendation.Infrastructure.Data;

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
    {
    }

    public DbSet<Movie> Movies => Set<Movie>();
    public DbSet<User> Users => Set<User>();
    public DbSet<UserInteraction> UserInteractions => Set<UserInteraction>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        // Movie configuration
        modelBuilder.Entity<Movie>(entity =>
        {
            entity.ToTable("Movies");
            entity.HasKey(e => e.Id);

            entity.Property(e => e.Title)
                .IsRequired()
                .HasMaxLength(200);

            entity.Property(e => e.Description)
                .HasMaxLength(2000);

            entity.Property(e => e.Director)
                .HasMaxLength(100);

            // Convert List<string> to comma-separated string for storage
            entity.Property(e => e.Genres)
                .HasConversion(
                    v => string.Join(',', v),
                    v => v.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList())
                .HasMaxLength(500);

            entity.Property(e => e.Actors)
                .HasConversion(
                    v => string.Join(',', v),
                    v => v.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList())
                .HasMaxLength(1000);

            // Indexes for better query performance
            entity.HasIndex(e => e.ReleaseYear);
            entity.HasIndex(e => e.AverageRating);
            entity.HasIndex(e => e.CreatedAt);
        });

        // User configuration
        modelBuilder.Entity<User>(entity =>
        {
            entity.ToTable("Users");
            entity.HasKey(e => e.Id);

            entity.Property(e => e.Username)
                .IsRequired()
                .HasMaxLength(100);

            entity.Property(e => e.Email)
                .IsRequired()
                .HasMaxLength(200);

            entity.Property(e => e.FavoriteGenres)
                .HasConversion(
                    v => string.Join(',', v),
                    v => v.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList())
                .HasMaxLength(500);

            entity.Property(e => e.FavoriteActors)
                .HasConversion(
                    v => string.Join(',', v),
                    v => v.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList())
                .HasMaxLength(1000);

            entity.HasIndex(e => e.Username).IsUnique();
            entity.HasIndex(e => e.Email).IsUnique();
        });

        // UserInteraction configuration
        modelBuilder.Entity<UserInteraction>(entity =>
        {
            entity.ToTable("UserInteractions");
            entity.HasKey(e => e.Id);

            entity.Property(e => e.Type)
                .HasConversion<string>()
                .HasMaxLength(50);

            // Relationships
            entity.HasOne(e => e.User)
                .WithMany()
                .HasForeignKey(e => e.UserId)
                .OnDelete(DeleteBehavior.Cascade);

            entity.HasOne(e => e.Movie)
                .WithMany()
                .HasForeignKey(e => e.MovieId)
                .OnDelete(DeleteBehavior.Cascade);

            // Indexes
            entity.HasIndex(e => new { e.UserId, e.MovieId });
            entity.HasIndex(e => e.Timestamp);
            entity.HasIndex(e => e.Type);
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

5. Repository Pattern Implementation {#section-5}

MovieRecommendation.Core/Repositories/IMovieRepository.cs

using MovieRecommendation.Core.Entities;

namespace MovieRecommendation.Core.Repositories;

public interface IMovieRepository
{
    Task<Movie?> GetByIdAsync(int id);
    Task<List<Movie>> GetAvailableMoviesAsync(int limit);
    Task<List<Movie>> GetMoviesByGenreAsync(string genre, int limit);
    Task<List<Movie>> GetMoviesByActorAsync(string actor, int limit);
    Task<Movie> AddAsync(Movie movie);
    Task UpdateAsync(Movie movie);
    Task DeleteAsync(int id);
}
Enter fullscreen mode Exit fullscreen mode

MovieRecommendation.Core/Repositories/IUserRepository.cs

using MovieRecommendation.Core.Entities;

namespace MovieRecommendation.Core.Repositories;

public interface IUserRepository
{
    Task<User?> GetByIdAsync(int id);
    Task<User?> GetByUsernameAsync(string username);
    Task<User> AddAsync(User user);
    Task UpdateAsync(User user);
    Task DeleteAsync(int id);
}
Enter fullscreen mode Exit fullscreen mode

MovieRecommendation.Core/Repositories/IInteractionRepository.cs

using MovieRecommendation.Core.Entities;

namespace MovieRecommendation.Core.Repositories;

public interface IInteractionRepository
{
    Task<List<UserInteraction>> GetUserInteractionsAsync(int userId);
    Task<List<UserInteraction>> GetAllRatingsAsync();
    Task<UserInteraction> AddAsync(UserInteraction interaction);
    Task<int> GetInteractionCountAsync();
}
Enter fullscreen mode Exit fullscreen mode

MovieRecommendation.Infrastructure/Repositories/MovieRepository.cs

using Microsoft.EntityFrameworkCore;
using MovieRecommendation.Core.Entities;
using MovieRecommendation.Core.Repositories;
using MovieRecommendation.Infrastructure.Data;

namespace MovieRecommendation.Infrastructure.Repositories;

public class MovieRepository : IMovieRepository
{
    private readonly AppDbContext _context;

    public MovieRepository(AppDbContext context)
    {
        _context = context;
    }

    public async Task<Movie?> GetByIdAsync(int id)
    {
        return await _context.Movies.FindAsync(id);
    }

    public async Task<List<Movie>> GetAvailableMoviesAsync(int limit)
    {
        return await _context.Movies
            .OrderByDescending(m => m.CreatedAt)
            .Take(limit)
            .ToListAsync();
    }

    public async Task<List<Movie>> GetMoviesByGenreAsync(string genre, int limit)
    {
        return await _context.Movies
            .Where(m => m.Genres.Contains(genre))
            .OrderByDescending(m => m.AverageRating)
            .Take(limit)
            .ToListAsync();
    }

    public async Task<List<Movie>> GetMoviesByActorAsync(string actor, int limit)
    {
        return await _context.Movies
            .Where(m => m.Actors.Contains(actor))
            .OrderByDescending(m => m.AverageRating)
            .Take(limit)
            .ToListAsync();
    }

    public async Task<Movie> AddAsync(Movie movie)
    {
        _context.Movies.Add(movie);
        await _context.SaveChangesAsync();
        return movie;
    }

    public async Task UpdateAsync(Movie movie)
    {
        _context.Movies.Update(movie);
        await _context.SaveChangesAsync();
    }

    public async Task DeleteAsync(int id)
    {
        var movie = await GetByIdAsync(id);
        if (movie != null)
        {
            _context.Movies.Remove(movie);
            await _context.SaveChangesAsync();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

MovieRecommendation.Infrastructure/Repositories/UserRepository.cs

using Microsoft.EntityFrameworkCore;
using MovieRecommendation.Core.Entities;
using MovieRecommendation.Core.Repositories;
using MovieRecommendation.Infrastructure.Data;

namespace MovieRecommendation.Infrastructure.Repositories;

public class UserRepository : IUserRepository
{
    private readonly AppDbContext _context;

    public UserRepository(AppDbContext context)
    {
        _context = context;
    }

    public async Task<User?> GetByIdAsync(int id)
    {
        return await _context.Users.FindAsync(id);
    }

    public async Task<User?> GetByUsernameAsync(string username)
    {
        return await _context.Users
            .FirstOrDefaultAsync(u => u.Username == username);
    }

    public async Task<User> AddAsync(User user)
    {
        _context.Users.Add(user);
        await _context.SaveChangesAsync();
        return user;
    }

    public async Task UpdateAsync(User user)
    {
        _context.Users.Update(user);
        await _context.SaveChangesAsync();
    }

    public async Task DeleteAsync(int id)
    {
        var user = await GetByIdAsync(id);
        if (user != null)
        {
            _context.Users.Remove(user);
            await _context.SaveChangesAsync();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

MovieRecommendation.Infrastructure/Repositories/InteractionRepository.cs

using Microsoft.EntityFrameworkCore;
using MovieRecommendation.Core.Entities;
using MovieRecommendation.Core.Repositories;
using MovieRecommendation.Infrastructure.Data;

namespace MovieRecommendation.Infrastructure.Repositories;

public class InteractionRepository : IInteractionRepository
{
    private readonly AppDbContext _context;

    public InteractionRepository(AppDbContext context)
    {
        _context = context;
    }

    public async Task<List<UserInteraction>> GetUserInteractionsAsync(int userId)
    {
        return await _context.UserInteractions
            .Where(i => i.UserId == userId)
            .OrderByDescending(i => i.Timestamp)
            .ToListAsync();
    }

    public async Task<List<UserInteraction>> GetAllRatingsAsync()
    {
        return await _context.UserInteractions
            .Where(i => i.Type == InteractionType.Rating && i.Rating.HasValue)
            .ToListAsync();
    }

    public async Task<UserInteraction> AddAsync(UserInteraction interaction)
    {
        _context.UserInteractions.Add(interaction);
        await _context.SaveChangesAsync();
        return interaction;
    }

    public async Task<int> GetInteractionCountAsync()
    {
        return await _context.UserInteractions
            .Where(i => i.Type == InteractionType.Rating)
            .CountAsync();
    }
}
Enter fullscreen mode Exit fullscreen mode

6. Content-Based Filtering Engine {#section-6}

MovieRecommendation.Core/Services/IContentBasedEngine.cs

using MovieRecommendation.Core.Entities;
using MovieRecommendation.Core.Models;

namespace MovieRecommendation.Core.Services;

public interface IContentBasedEngine
{
    Task<double> CalculateSimilarityScore(User user, Movie movie);
    Task<List<RecommendationResult>> GetContentBasedRecommendations(int userId, int limit = 10);
}
Enter fullscreen mode Exit fullscreen mode

MovieRecommendation.Infrastructure/Services/ContentBasedEngine.cs

using MovieRecommendation.Core.Entities;
using MovieRecommendation.Core.Models;
using MovieRecommendation.Core.Repositories;
using MovieRecommendation.Core.Services;
using Microsoft.Extensions.Logging;

namespace MovieRecommendation.Infrastructure.Services;

public class ContentBasedEngine : IContentBasedEngine
{
    private readonly IMovieRepository _movieRepository;
    private readonly IUserRepository _userRepository;
    private readonly IInteractionRepository _interactionRepository;
    private readonly ILogger<ContentBasedEngine> _logger;

    public ContentBasedEngine(
        IMovieRepository movieRepository,
        IUserRepository userRepository,
        IInteractionRepository interactionRepository,
        ILogger<ContentBasedEngine> logger)
    {
        _movieRepository = movieRepository;
        _userRepository = userRepository;
        _interactionRepository = interactionRepository;
        _logger = logger;
    }

    public async Task<double> CalculateSimilarityScore(User user, Movie movie)
    {
        var weights = new RecommendationWeights();
        double totalScore = 0.0;

        // Genre similarity (35%)
        if (user.FavoriteGenres.Any())
        {
            var genreScore = CalculateGenreScore(user.FavoriteGenres, movie.Genres);
            totalScore += genreScore * weights.GenreWeight;
        }

        // Actor similarity (25%)
        if (user.FavoriteActors.Any())
        {
            var actorScore = CalculateActorScore(user.FavoriteActors, movie.Actors);
            totalScore += actorScore * weights.ActorWeight;
        }

        // Year preference (20%)
        var yearScore = CalculateYearScore(
            user.PreferredMinYear, user.PreferredMaxYear, movie.ReleaseYear);
        totalScore += yearScore * weights.YearWeight;

        // Duration preference (20%)
        var durationScore = CalculateDurationScore(
            user.PreferredMinDuration, user.PreferredMaxDuration, movie.DurationMinutes);
        totalScore += durationScore * weights.DurationWeight;

        return Math.Max(0.0, Math.Min(1.0, totalScore));
    }

    private double CalculateGenreScore(List<string> userGenres, List<string> movieGenres)
    {
        if (!movieGenres.Any()) return 0.3;

        // Jaccard similarity: intersection / union
        var intersection = userGenres.Intersect(movieGenres, StringComparer.OrdinalIgnoreCase).Count();
        var union = userGenres.Union(movieGenres, StringComparer.OrdinalIgnoreCase).Count();

        if (union == 0) return 0.3;

        double jaccardScore = (double)intersection / union;

        // Bonus if all user preferences are met
        var allPreferencesMet = userGenres.All(g => 
            movieGenres.Contains(g, StringComparer.OrdinalIgnoreCase));

        if (allPreferencesMet && intersection > 0)
            jaccardScore = Math.Min(1.0, jaccardScore + 0.2);

        return jaccardScore;
    }

    private double CalculateActorScore(List<string> userActors, List<string> movieActors)
    {
        if (!movieActors.Any()) return 0.5;

        var matchCount = userActors.Count(ua => 
            movieActors.Contains(ua, StringComparer.OrdinalIgnoreCase));

        if (matchCount == 0) return 0.3;

        return Math.Min(1.0, (double)matchCount / userActors.Count + 0.2);
    }

    private double CalculateYearScore(int minYear, int maxYear, int movieYear)
    {
        if (minYear == 0 && maxYear == 0) return 0.5;

        if (movieYear >= minYear && movieYear <= maxYear)
            return 1.0;

        // Calculate distance from range with tolerance
        int distance;
        if (movieYear < minYear)
            distance = minYear - movieYear;
        else
            distance = movieYear - maxYear;

        var tolerance = 10; // 10 years tolerance
        return Math.Max(0.0, 1.0 - (double)distance / tolerance);
    }

    private double CalculateDurationScore(int minDuration, int maxDuration, int movieDuration)
    {
        if (minDuration == 0 && maxDuration == 0) return 0.5;

        if (movieDuration >= minDuration && movieDuration <= maxDuration)
            return 1.0;

        // Within 20% tolerance
        var midPoint = (minDuration + maxDuration) / 2.0;
        var difference = Math.Abs(movieDuration - midPoint);
        var tolerance = midPoint * 0.2;

        return Math.Max(0.0, 1.0 - difference / tolerance);
    }

    public async Task<List<RecommendationResult>> GetContentBasedRecommendations(
        int userId, int limit = 10)
    {
        var user = await _userRepository.GetByIdAsync(userId);
        if (user == null)
        {
            _logger.LogWarning("User {UserId} not found", userId);
            return new List<RecommendationResult>();
        }

        // Get movies user hasn't interacted with
        var userInteractions = await _interactionRepository.GetUserInteractionsAsync(userId);
        var watchedMovieIds = userInteractions.Select(i => i.MovieId).ToHashSet();

        // Get candidate movies
        var candidateMovies = await _movieRepository.GetAvailableMoviesAsync(limit * 5);
        candidateMovies = candidateMovies
            .Where(m => !watchedMovieIds.Contains(m.Id))
            .ToList();

        var recommendations = new List<RecommendationResult>();

        foreach (var movie in candidateMovies)
        {
            var similarityScore = await CalculateSimilarityScore(user, movie);

            recommendations.Add(new RecommendationResult
            {
                MovieId = movie.Id,
                Title = movie.Title,
                ContentScore = similarityScore,
                DetailedScores = new Dictionary<string, double>
                {
                    ["Genre"] = CalculateGenreScore(user.FavoriteGenres, movie.Genres),
                    ["Actor"] = CalculateActorScore(user.FavoriteActors, movie.Actors),
                    ["Year"] = CalculateYearScore(user.PreferredMinYear, user.PreferredMaxYear, movie.ReleaseYear),
                    ["Duration"] = CalculateDurationScore(user.PreferredMinDuration, user.PreferredMaxDuration, movie.DurationMinutes),
                    ["Rating"] = movie.AverageRating / 5.0
                }
            });
        }

        return recommendations
            .OrderByDescending(r => r.ContentScore)
            .Take(limit)
            .ToList();
    }
}
Enter fullscreen mode Exit fullscreen mode

7. Collaborative Filtering with ML.NET {#section-7}

MovieRecommendation.Core/Services/ICollaborativeEngine.cs

using MovieRecommendation.Core.Models;

namespace MovieRecommendation.Core.Services;

public interface ICollaborativeEngine
{
    Task<bool> TrainModelAsync();
    Task<double> PredictRating(int userId, int movieId);
    Task<List<RecommendationResult>> GetCollaborativeRecommendations(int userId, int limit = 10);
}
Enter fullscreen mode Exit fullscreen mode

MovieRecommendation.Infrastructure/Services/CollaborativeEngine.cs

using Microsoft.ML;
using Microsoft.ML.Trainers;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using MovieRecommendation.Core.Models;
using MovieRecommendation.Core.Repositories;
using MovieRecommendation.Core.Services;

namespace MovieRecommendation.Infrastructure.Services;

public class CollaborativeEngine : ICollaborativeEngine
{
    private readonly IInteractionRepository _interactionRepository;
    private readonly IMovieRepository _movieRepository;
    private readonly IMemoryCache _cache;
    private readonly ILogger<CollaborativeEngine> _logger;
    private readonly MLContext _mlContext;

    private const string ModelCacheKey = "collaborative_model";
    private readonly TimeSpan ModelCacheExpiration = TimeSpan.FromHours(24);

    public CollaborativeEngine(
        IInteractionRepository interactionRepository,
        IMovieRepository movieRepository,
        IMemoryCache cache,
        ILogger<CollaborativeEngine> logger)
    {
        _interactionRepository = interactionRepository;
        _movieRepository = movieRepository;
        _cache = cache;
        _logger = logger;
        _mlContext = new MLContext(seed: 0);
    }

    public async Task<bool> TrainModelAsync()
    {
        try
        {
            _logger.LogInformation("Starting collaborative filtering model training...");

            // Get training data
            var interactions = await _interactionRepository.GetAllRatingsAsync();

            if (interactions.Count < 100)
            {
                _logger.LogWarning(
                    "Not enough data to train model. Need at least 100 ratings, got {Count}", 
                    interactions.Count);
                return false;
            }

            // Convert to ML.NET format
            var trainingData = interactions.Select(i => new MovieRating
            {
                UserId = i.UserId,
                MovieId = i.MovieId,
                Rating = (float)(i.Rating ?? 3.0)
            }).ToList();

            var dataView = _mlContext.Data.LoadFromEnumerable(trainingData);

            // Split data for validation
            var dataSplit = _mlContext.Data.TrainTestSplit(dataView, testFraction: 0.2);

            // Configure matrix factorization
            var options = new MatrixFactorizationTrainer.Options
            {
                MatrixColumnIndexColumnName = nameof(MovieRating.UserId),
                MatrixRowIndexColumnName = nameof(MovieRating.MovieId),
                LabelColumnName = nameof(MovieRating.Rating),
                NumberOfIterations = 20,
                ApproximationRank = 100,
                LearningRate = 0.001,
                Quiet = false
            };

            // Train the model
            _logger.LogInformation("Training matrix factorization model...");
            var trainer = _mlContext.Recommendation().Trainers.MatrixFactorization(options);
            var model = trainer.Fit(dataSplit.TrainSet);

            // Evaluate
            var predictions = model.Transform(dataSplit.TestSet);
            var metrics = _mlContext.Regression.Evaluate(
                predictions, 
                labelColumnName: nameof(MovieRating.Rating));

            _logger.LogInformation(
                "Model trained. RMSE: {RMSE:F3}, RΒ²: {RSquared:F3}", 
                metrics.RootMeanSquaredError, 
                metrics.RSquared);

            // Cache the model
            var cacheOptions = new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = ModelCacheExpiration,
                Size = 100
            };
            _cache.Set(ModelCacheKey, model, cacheOptions);

            return true;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error training collaborative filtering model");
            return false;
        }
    }

    public async Task<double> PredictRating(int userId, int movieId)
    {
        try
        {
            if (!_cache.TryGetValue(ModelCacheKey, out ITransformer? model) || model == null)
            {
                _logger.LogDebug("Model not in cache, returning neutral score");
                return 2.5;
            }

            var predictionEngine = _mlContext.Model
                .CreatePredictionEngine<MovieRating, MovieRatingPrediction>(model);

            var input = new MovieRating
            {
                UserId = userId,
                MovieId = movieId
            };

            var prediction = predictionEngine.Predict(input);

            // Clamp to 0-5 range
            return Math.Max(0, Math.Min(5, prediction.Score));
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error predicting rating for user {UserId}, movie {MovieId}", 
                userId, movieId);
            return 2.5;
        }
    }

    public async Task<List<RecommendationResult>> GetCollaborativeRecommendations(
        int userId, int limit = 10)
    {
        try
        {
            var userInteractions = await _interactionRepository.GetUserInteractionsAsync(userId);
            var watchedMovieIds = userInteractions.Select(i => i.MovieId).ToHashSet();

            var candidateMovies = await _movieRepository.GetAvailableMoviesAsync(limit * 5);
            candidateMovies = candidateMovies
                .Where(m => !watchedMovieIds.Contains(m.Id))
                .ToList();

            var recommendations = new List<RecommendationResult>();

            foreach (var movie in candidateMovies)
            {
                var predictedRating = await PredictRating(userId, movie.Id);
                var normalizedScore = predictedRating / 5.0;

                recommendations.Add(new RecommendationResult
                {
                    MovieId = movie.Id,
                    Title = movie.Title,
                    CollaborativeScore = normalizedScore
                });
            }

            return recommendations
                .OrderByDescending(r => r.CollaborativeScore)
                .Take(limit)
                .ToList();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error getting collaborative recommendations for user {UserId}", userId);
            return new List<RecommendationResult>();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

8. Hybrid Recommendation Engine {#section-8}

MovieRecommendation.Core/Services/IRecommendationEngine.cs

using MovieRecommendation.Core.Entities;
using MovieRecommendation.Core.Models;

namespace MovieRecommendation.Core.Services;

public interface IRecommendationEngine
{
    Task<List<RecommendationResult>> GetRecommendations(
        int userId, 
        int limit = 10, 
        RecommendationWeights? weights = null);

    Task TrackInteraction(int userId, int movieId, InteractionType type, double? rating = null);
}
Enter fullscreen mode Exit fullscreen mode

MovieRecommendation.Infrastructure/Services/RecommendationEngine.cs

using MovieRecommendation.Core.Entities;
using MovieRecommendation.Core.Models;
using MovieRecommendation.Core.Repositories;
using MovieRecommendation.Core.Services;
using Microsoft.Extensions.Logging;

namespace MovieRecommendation.Infrastructure.Services;

public class RecommendationEngine : IRecommendationEngine
{
    private readonly IContentBasedEngine _contentEngine;
    private readonly ICollaborativeEngine _collaborativeEngine;
    private readonly IMovieRepository _movieRepository;
    private readonly IInteractionRepository _interactionRepository;
    private readonly ILogger<RecommendationEngine> _logger;

    public RecommendationEngine(
        IContentBasedEngine contentEngine,
        ICollaborativeEngine collaborativeEngine,
        IMovieRepository movieRepository,
        IInteractionRepository interactionRepository,
        ILogger<RecommendationEngine> logger)
    {
        _contentEngine = contentEngine;
        _collaborativeEngine = collaborativeEngine;
        _movieRepository = movieRepository;
        _interactionRepository = interactionRepository;
        _logger = logger;
    }

    public async Task<List<RecommendationResult>> GetRecommendations(
        int userId, 
        int limit = 10, 
        RecommendationWeights? weights = null)
    {
        weights ??= new RecommendationWeights();

        try
        {
            _logger.LogInformation("Generating recommendations for user {UserId} with limit {Limit}", 
                userId, limit);

            // Get recommendations from both engines in parallel
            var contentTask = _contentEngine.GetContentBasedRecommendations(userId, limit * 3);
            var collaborativeTask = _collaborativeEngine.GetCollaborativeRecommendations(userId, limit * 3);

            await Task.WhenAll(contentTask, collaborativeTask);

            var contentRecommendations = contentTask.Result;
            var collaborativeRecommendations = collaborativeTask.Result;

            // Merge recommendations
            var mergedRecommendations = MergeRecommendations(
                contentRecommendations, 
                collaborativeRecommendations);

            // Calculate overall scores
            foreach (var recommendation in mergedRecommendations)
            {
                var movie = await _movieRepository.GetByIdAsync(recommendation.MovieId);
                if (movie == null) continue;

                // Calculate popularity score
                var popularityScore = CalculatePopularityScore(movie);
                recommendation.PopularityScore = popularityScore;

                // Calculate recency score
                var recencyScore = CalculateRecencyScore(movie);

                // Calculate overall weighted score
                recommendation.OverallScore = 
                    (recommendation.ContentScore * weights.ContentWeight) +
                    (recommendation.CollaborativeScore * weights.CollaborativeWeight) +
                    (popularityScore * weights.PopularityWeight) +
                    (recencyScore * weights.RecencyWeight);

                // Generate human-readable explanation
                recommendation.ReasonForRecommendation = 
                    GenerateRecommendationReason(recommendation);
            }

            var finalRecommendations = mergedRecommendations
                .OrderByDescending(r => r.OverallScore)
                .Take(limit)
                .ToList();

            _logger.LogInformation(
                "Generated {Count} recommendations for user {UserId}", 
                finalRecommendations.Count, userId);

            return finalRecommendations;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error generating recommendations for user {UserId}", userId);
            return new List<RecommendationResult>();
        }
    }

    private List<RecommendationResult> MergeRecommendations(
        List<RecommendationResult> contentRecs,
        List<RecommendationResult> collaborativeRecs)
    {
        var merged = new Dictionary<int, RecommendationResult>();

        // Add content-based recommendations
        foreach (var rec in contentRecs)
        {
            merged[rec.MovieId] = rec;
        }

        // Merge collaborative recommendations
        foreach (var rec in collaborativeRecs)
        {
            if (merged.ContainsKey(rec.MovieId))
            {
                // Update existing recommendation with collaborative score
                merged[rec.MovieId].CollaborativeScore = rec.CollaborativeScore;
            }
            else
            {
                // Add new recommendation
                merged[rec.MovieId] = rec;
            }
        }

        return merged.Values.ToList();
    }

    private double CalculatePopularityScore(Core.Entities.Movie movie)
    {
        // Combine rating quality and number of ratings
        if (movie.TotalRatings == 0) 
            return 0.3; // New movies get neutral score

        // Normalize rating (0-5 scale to 0-1)
        var ratingScore = movie.AverageRating / 5.0;

        // Normalize popularity (use logarithmic scale for diminishing returns)
        var popularityScore = Math.Min(1.0, Math.Log10(movie.TotalRatings + 1) / 5.0);

        // Weighted combination: 70% rating, 30% popularity
        return (ratingScore * 0.7) + (popularityScore * 0.3);
    }

    private double CalculateRecencyScore(Core.Entities.Movie movie)
    {
        var daysSinceCreation = (DateTime.UtcNow - movie.CreatedAt).Days;

        // Recent movies get higher score
        if (daysSinceCreation <= 30) return 1.0;      // New (last month)
        if (daysSinceCreation <= 90) return 0.8;      // Recent (last 3 months)
        if (daysSinceCreation <= 180) return 0.6;     // Semi-recent (last 6 months)
        if (daysSinceCreation <= 365) return 0.4;     // This year
        return 0.2;                                    // Older
    }

    private string GenerateRecommendationReason(RecommendationResult recommendation)
    {
        var reasons = new List<string>();

        // Check content-based factors
        if (recommendation.ContentScore > 0.7)
            reasons.Add("matches your preferences");

        // Check collaborative filtering
        if (recommendation.CollaborativeScore > 0.7)
            reasons.Add("loved by users like you");

        // Check popularity
        if (recommendation.PopularityScore > 0.7)
            reasons.Add("highly rated by many viewers");

        // Check detailed scores
        if (recommendation.DetailedScores.TryGetValue("Genre", out var genreScore) 
            && genreScore > 0.8)
            reasons.Add("in your favorite genre");

        if (recommendation.DetailedScores.TryGetValue("Actor", out var actorScore) 
            && actorScore > 0.8)
            reasons.Add("features your favorite actors");

        // Generate natural language explanation
        if (!reasons.Any())
            return "Recommended for you based on our analysis";

        if (reasons.Count == 1)
            return $"Recommended because it {reasons[0]}";

        if (reasons.Count == 2)
            return $"Recommended because it {reasons[0]} and {reasons[1]}";

        var lastReason = reasons.Last();
        reasons.RemoveAt(reasons.Count - 1);
        return $"Recommended because it {string.Join(", ", reasons)}, and {lastReason}";
    }

    public async Task TrackInteraction(
        int userId, 
        int movieId, 
        InteractionType type, 
        double? rating = null)
    {
        try
        {
            var interaction = new UserInteraction
            {
                UserId = userId,
                MovieId = movieId,
                Type = type,
                Rating = rating,
                Timestamp = DateTime.UtcNow
            };

            await _interactionRepository.AddAsync(interaction);

            _logger.LogInformation(
                "Tracked interaction: User {UserId}, Movie {MovieId}, Type {Type}, Rating {Rating}", 
                userId, movieId, type, rating);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error tracking interaction");
            throw;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

9. API Controllers & Endpoints {#section-9}

MovieRecommendation.API/Controllers/RecommendationsController.cs

using Microsoft.AspNetCore.Mvc;
using MovieRecommendation.Core.Entities;
using MovieRecommendation.Core.Models;
using MovieRecommendation.Core.Services;

namespace MovieRecommendation.API.Controllers;

[ApiController]
[Route("api/[controller]")]
public class RecommendationsController : ControllerBase
{
    private readonly IRecommendationEngine _recommendationEngine;
    private readonly ICollaborativeEngine _collaborativeEngine;
    private readonly ILogger<RecommendationsController> _logger;

    public RecommendationsController(
        IRecommendationEngine recommendationEngine,
        ICollaborativeEngine collaborativeEngine,
        ILogger<RecommendationsController> logger)
    {
        _recommendationEngine = recommendationEngine;
        _collaborativeEngine = collaborativeEngine;
        _logger = logger;
    }

    /// <summary>
    /// Get personalized movie recommendations for a user
    /// </summary>
    /// <param name="userId">User ID</param>
    /// <param name="limit">Number of recommendations (default: 10, max: 50)</param>
    /// <returns>List of recommended movies with scores</returns>
    [HttpGet("user/{userId}")]
    [ProducesResponseType(typeof(List<RecommendationResult>), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
    public async Task<ActionResult<List<RecommendationResult>>> GetRecommendations(
        int userId,
        [FromQuery] int limit = 10)
    {
        if (userId <= 0)
            return BadRequest(new { error = "Invalid user ID" });

        if (limit <= 0 || limit > 50)
            return BadRequest(new { error = "Limit must be between 1 and 50" });

        try
        {
            var recommendations = await _recommendationEngine.GetRecommendations(userId, limit);
            return Ok(recommendations);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error getting recommendations for user {UserId}", userId);
            return StatusCode(500, new { error = "An error occurred while getting recommendations" });
        }
    }

    /// <summary>
    /// Track user interaction with a movie
    /// </summary>
    /// <param name="request">Interaction details</param>
    [HttpPost("track")]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    public async Task<IActionResult> TrackInteraction([FromBody] TrackInteractionRequest request)
    {
        if (request.UserId <= 0 || request.MovieId <= 0)
            return BadRequest(new { error = "Invalid user or movie ID" });

        if (request.Type == InteractionType.Rating && 
            (request.Rating < 1 || request.Rating > 5))
            return BadRequest(new { error = "Rating must be between 1 and 5" });

        try
        {
            await _recommendationEngine.TrackInteraction(
                request.UserId,
                request.MovieId,
                request.Type,
                request.Rating);

            return Ok(new { message = "Interaction tracked successfully" });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error tracking interaction");
            return StatusCode(500, new { error = "An error occurred while tracking interaction" });
        }
    }

    /// <summary>
    /// Train the collaborative filtering ML model
    /// </summary>
    [HttpPost("train")]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    public async Task<IActionResult> TrainModel()
    {
        try
        {
            _logger.LogInformation("Manual model training triggered");
            var success = await _collaborativeEngine.TrainModelAsync();

            if (success)
                return Ok(new { message = "Model trained successfully" });
            else
                return BadRequest(new { error = "Model training failed - not enough data" });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error training model");
            return StatusCode(500, new { error = "An error occurred while training model" });
        }
    }
}

public record TrackInteractionRequest(
    int UserId,
    int MovieId,
    InteractionType Type,
    double? Rating = null);
Enter fullscreen mode Exit fullscreen mode

MovieRecommendation.API/Controllers/MoviesController.cs

using Microsoft.AspNetCore.Mvc;
using MovieRecommendation.Core.Entities;
using MovieRecommendation.Core.Repositories;

namespace MovieRecommendation.API.Controllers;

[ApiController]
[Route("api/[controller]")]
public class MoviesController : ControllerBase
{
    private readonly IMovieRepository _movieRepository;
    private readonly ILogger<MoviesController> _logger;

    public MoviesController(IMovieRepository movieRepository, ILogger<MoviesController> logger)
    {
        _movieRepository = movieRepository;
        _logger = logger;
    }

    /// <summary>
    /// Get all movies with pagination
    /// </summary>
    [HttpGet]
    [ProducesResponseType(typeof(List<Movie>), StatusCodes.Status200OK)]
    public async Task<ActionResult<List<Movie>>> GetMovies([FromQuery] int limit = 20)
    {
        if (limit <= 0 || limit > 100)
            return BadRequest(new { error = "Limit must be between 1 and 100" });

        var movies = await _movieRepository.GetAvailableMoviesAsync(limit);
        return Ok(movies);
    }

    /// <summary>
    /// Get a specific movie by ID
    /// </summary>
    [HttpGet("{id}")]
    [ProducesResponseType(typeof(Movie), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<ActionResult<Movie>> GetMovie(int id)
    {
        var movie = await _movieRepository.GetByIdAsync(id);
        if (movie == null)
            return NotFound(new { error = $"Movie with ID {id} not found" });

        return Ok(movie);
    }

    /// <summary>
    /// Get movies by genre
    /// </summary>
    [HttpGet("genre/{genre}")]
    [ProducesResponseType(typeof(List<Movie>), StatusCodes.Status200OK)]
    public async Task<ActionResult<List<Movie>>> GetMoviesByGenre(string genre, [FromQuery] int limit = 20)
    {
        var movies = await _movieRepository.GetMoviesByGenreAsync(genre, limit);
        return Ok(movies);
    }

    /// <summary>
    /// Create a new movie
    /// </summary>
    [HttpPost]
    [ProducesResponseType(typeof(Movie), StatusCodes.Status201Created)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    public async Task<ActionResult<Movie>> CreateMovie([FromBody] CreateMovieRequest request)
    {
        var movie = new Movie
        {
            Title = request.Title,
            Description = request.Description,
            Genres = request.Genres,
            Actors = request.Actors,
            Director = request.Director,
            ReleaseYear = request.ReleaseYear,
            DurationMinutes = request.DurationMinutes,
            AverageRating = 0,
            TotalRatings = 0
        };

        var created = await _movieRepository.AddAsync(movie);
        return CreatedAtAction(nameof(GetMovie), new { id = created.Id }, created);
    }
}

public record CreateMovieRequest(
    string Title,
    string Description,
    List<string> Genres,
    List<string> Actors,
    string Director,
    int ReleaseYear,
    int DurationMinutes);
Enter fullscreen mode Exit fullscreen mode

MovieRecommendation.API/Controllers/UsersController.cs

using Microsoft.AspNetCore.Mvc;
using MovieRecommendation.Core.Entities;
using MovieRecommendation.Core.Repositories;

namespace MovieRecommendation.API.Controllers;

[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    private readonly IUserRepository _userRepository;
    private readonly ILogger<UsersController> _logger;

    public UsersController(IUserRepository userRepository, ILogger<UsersController> logger)
    {
        _userRepository = userRepository;
        _logger = logger;
    }

    /// <summary>
    /// Get user by ID
    /// </summary>
    [HttpGet("{id}")]
    [ProducesResponseType(typeof(User), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<ActionResult<User>> GetUser(int id)
    {
        var user = await _userRepository.GetByIdAsync(id);
        if (user == null)
            return NotFound(new { error = $"User with ID {id} not found" });

        return Ok(user);
    }

    /// <summary>
    /// Create a new user
    /// </summary>
    [HttpPost]
    [ProducesResponseType(typeof(User), StatusCodes.Status201Created)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    public async Task<ActionResult<User>> CreateUser([FromBody] CreateUserRequest request)
    {
        // Check if username exists
        var existingUser = await _userRepository.GetByUsernameAsync(request.Username);
        if (existingUser != null)
            return BadRequest(new { error = "Username already exists" });

        var user = new User
        {
            Username = request.Username,
            Email = request.Email,
            FavoriteGenres = request.FavoriteGenres ?? new List<string>(),
            FavoriteActors = request.FavoriteActors ?? new List<string>(),
            PreferredMinYear = request.PreferredMinYear,
            PreferredMaxYear = request.PreferredMaxYear,
            PreferredMinDuration = request.PreferredMinDuration,
            PreferredMaxDuration = request.PreferredMaxDuration
        };

        var created = await _userRepository.AddAsync(user);
        return CreatedAtAction(nameof(GetUser), new { id = created.Id }, created);
    }

    /// <summary>
    /// Update user preferences
    /// </summary>
    [HttpPut("{id}/preferences")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> UpdatePreferences(
        int id, 
        [FromBody] UpdatePreferencesRequest request)
    {
        var user = await _userRepository.GetByIdAsync(id);
        if (user == null)
            return NotFound(new { error = $"User with ID {id} not found" });

        user.FavoriteGenres = request.FavoriteGenres;
        user.FavoriteActors = request.FavoriteActors;
        user.PreferredMinYear = request.PreferredMinYear;
        user.PreferredMaxYear = request.PreferredMaxYear;
        user.PreferredMinDuration = request.PreferredMinDuration;
        user.PreferredMaxDuration = request.PreferredMaxDuration;

        await _userRepository.UpdateAsync(user);
        return NoContent();
    }
}

public record CreateUserRequest(
    string Username,
    string Email,
    List<string>? FavoriteGenres,
    List<string>? FavoriteActors,
    int PreferredMinYear,
    int PreferredMaxYear,
    int PreferredMinDuration,
    int PreferredMaxDuration);

public record UpdatePreferencesRequest(
    List<string> FavoriteGenres,
    List<string> FavoriteActors,
    int PreferredMinYear,
    int PreferredMaxYear,
    int PreferredMinDuration,
    int PreferredMaxDuration);
Enter fullscreen mode Exit fullscreen mode

10. Configuration & Dependency Injection {#section-10}

MovieRecommendation.API/appsettings.json

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Database=MovieRecommendation;Trusted_Connection=True;TrustServerCertificate=True;MultipleActiveResultSets=true"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.EntityFrameworkCore": "Warning",
      "MovieRecommendation": "Debug"
    }
  },
  "AllowedHosts": "*",
  "RecommendationSettings": {
    "DefaultLimit": 10,
    "MaxLimit": 50,
    "CacheExpirationHours": 24,
    "MinimumInteractionsForTraining": 100,
    "TrainingIntervalHours": 24
  },
  "Cors": {
    "AllowedOrigins": [
      "http://localhost:3000",
      "http://localhost:4200",
      "http://localhost:5173"
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

MovieRecommendation.API/appsettings.Development.json

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Database=MovieRecommendationDev;Trusted_Connection=True;TrustServerCertificate=True;MultipleActiveResultSets=true"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information",
      "Microsoft.EntityFrameworkCore": "Information"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

MovieRecommendation.API/Program.cs (COMPLETE)

using Microsoft.EntityFrameworkCore;
using MovieRecommendation.Core.Repositories;
using MovieRecommendation.Core.Services;
using MovieRecommendation.Infrastructure.Data;
using MovieRecommendation.Infrastructure.Repositories;
using MovieRecommendation.Infrastructure.Services;
using MovieRecommendation.API.Middleware;

var builder = WebApplication.CreateBuilder(args);

// ===== Add Services =====

// Controllers
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();

// Swagger/OpenAPI
builder.Services.AddSwaggerGen(options =>
{
    options.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo
    {
        Title = "Movie Recommendation API",
        Version = "v1",
        Description = "AI-powered movie recommendation system using ML.NET and collaborative filtering",
        Contact = new Microsoft.OpenApi.Models.OpenApiContact
        {
            Name = "Your Name",
            Email = "your.email@example.com"
        }
    });

    // Include XML comments if available
    var xmlFile = $"{System.Reflection.Assembly.GetExecutingAssembly().GetName().Name}.xml";
    var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
    if (File.Exists(xmlPath))
    {
        options.IncludeXmlComments(xmlPath);
    }
});

// Database Context
builder.Services.AddDbContext<AppDbContext>(options =>
{
    options.UseSqlServer(
        builder.Configuration.GetConnectionString("DefaultConnection"),
        sqlOptions =>
        {
            sqlOptions.EnableRetryOnFailure(
                maxRetryCount: 5,
                maxRetryDelay: TimeSpan.FromSeconds(30),
                errorNumbersToAdd: null);
            sqlOptions.CommandTimeout(60);
        });
});

// Memory Cache
builder.Services.AddMemoryCache(options =>
{
    options.SizeLimit = 1024; // MB
    options.CompactionPercentage = 0.75;
});

// Logging
builder.Services.AddLogging(logging =>
{
    logging.ClearProviders();
    logging.AddConsole();
    logging.AddDebug();
    if (builder.Environment.IsDevelopment())
    {
        logging.SetMinimumLevel(LogLevel.Debug);
    }
});

// Register Repositories
builder.Services.AddScoped<IMovieRepository, MovieRepository>();
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddScoped<IInteractionRepository, InteractionRepository>();

// Register Recommendation Services
builder.Services.AddScoped<IContentBasedEngine, ContentBasedEngine>();
builder.Services.AddScoped<ICollaborativeEngine, CollaborativeEngine>();
builder.Services.AddScoped<IRecommendationEngine, RecommendationEngine>();

// Background Services
builder.Services.AddHostedService<ModelTrainingBackgroundService>();

// CORS
var allowedOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>() 
    ?? new[] { "http://localhost:3000" };

builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(policy =>
    {
        policy.WithOrigins(allowedOrigins)
              .AllowAnyMethod()
              .AllowAnyHeader()
              .AllowCredentials();
    });
});

// Health Checks
builder.Services.AddHealthChecks()
    .AddDbContextCheck<AppDbContext>("database");

// ===== Build App =====

var app = builder.Build();

// ===== Configure Middleware Pipeline =====

// Global Exception Handling
app.UseMiddleware<GlobalExceptionMiddleware>();

// Development-specific middleware
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI(options =>
    {
        options.SwaggerEndpoint("/swagger/v1/swagger.json", "Movie Recommendation API v1");
        options.RoutePrefix = string.Empty; // Swagger at root
    });
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseCors();
app.UseAuthorization();

// Health check endpoint
app.MapHealthChecks("/health");

// Map controllers
app.MapControllers();

// ===== Database Initialization =====

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;
    try
    {
        var context = services.GetRequiredService<AppDbContext>();
        var logger = services.GetRequiredService<ILogger<Program>>();

        logger.LogInformation("Applying database migrations...");
        await context.Database.MigrateAsync();

        if (app.Environment.IsDevelopment())
        {
            logger.LogInformation("Seeding database...");
            await DatabaseSeeder.SeedAsync(context, logger);
        }

        logger.LogInformation("Database initialization completed");
    }
    catch (Exception ex)
    {
        var logger = services.GetRequiredService<ILogger<Program>>();
        logger.LogError(ex, "An error occurred while initializing the database");
        throw;
    }
}

// ===== Run Application =====

app.Run();
Enter fullscreen mode Exit fullscreen mode

11. Database Seeding & Test Data {#section-11}

MovieRecommendation.Infrastructure/Data/DatabaseSeeder.cs

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using MovieRecommendation.Core.Entities;

namespace MovieRecommendation.Infrastructure.Data;

public static class DatabaseSeeder
{
    public static async Task SeedAsync(AppDbContext context, ILogger logger)
    {
        try
        {
            // Check if already seeded
            if (await context.Movies.AnyAsync())
            {
                logger.LogInformation("Database already seeded");
                return;
            }

            logger.LogInformation("Seeding movies...");
            await SeedMoviesAsync(context);

            logger.LogInformation("Seeding users...");
            await SeedUsersAsync(context);

            logger.LogInformation("Seeding interactions...");
            await SeedInteractionsAsync(context);

            logger.LogInformation("Database seeding completed successfully");
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Error seeding database");
            throw;
        }
    }

    private static async Task SeedMoviesAsync(AppDbContext context)
    {
        var movies = new List<Movie>
        {
            new Movie
            {
                Title = "The Shawshank Redemption",
                Description = "Two imprisoned men bond over a number of years, finding solace and eventual redemption through acts of common decency.",
                Genres = new List<string> { "Drama" },
                Actors = new List<string> { "Tim Robbins", "Morgan Freeman", "Bob Gunton" },
                Director = "Frank Darabont",
                ReleaseYear = 1994,
                DurationMinutes = 142,
                AverageRating = 4.9,
                TotalRatings = 2500000,
                CreatedAt = DateTime.UtcNow.AddDays(-365)
            },
            new Movie
            {
                Title = "The Dark Knight",
                Description = "When the menace known as the Joker wreaks havoc and chaos on the people of Gotham, Batman must accept one of the greatest psychological tests.",
                Genres = new List<string> { "Action", "Crime", "Drama" },
                Actors = new List<string> { "Christian Bale", "Heath Ledger", "Aaron Eckhart" },
                Director = "Christopher Nolan",
                ReleaseYear = 2008,
                DurationMinutes = 152,
                AverageRating = 4.8,
                TotalRatings = 2300000,
                CreatedAt = DateTime.UtcNow.AddDays(-300)
            },
            new Movie
            {
                Title = "Inception",
                Description = "A thief who steals corporate secrets through the use of dream-sharing technology is given the inverse task of planting an idea.",
                Genres = new List<string> { "Sci-Fi", "Action", "Thriller" },
                Actors = new List<string> { "Leonardo DiCaprio", "Tom Hardy", "Ellen Page" },
                Director = "Christopher Nolan",
                ReleaseYear = 2010,
                DurationMinutes = 148,
                AverageRating = 4.7,
                TotalRatings = 2100000,
                CreatedAt = DateTime.UtcNow.AddDays(-250)
            },
            new Movie
            {
                Title = "The Godfather",
                Description = "The aging patriarch of an organized crime dynasty transfers control of his clandestine empire to his reluctant son.",
                Genres = new List<string> { "Crime", "Drama" },
                Actors = new List<string> { "Marlon Brando", "Al Pacino", "James Caan" },
                Director = "Francis Ford Coppola",
                ReleaseYear = 1972,
                DurationMinutes = 175,
                AverageRating = 4.9,
                TotalRatings = 1800000,
                CreatedAt = DateTime.UtcNow.AddDays(-400)
            },
            new Movie
            {
                Title = "Pulp Fiction",
                Description = "The lives of two mob hitmen, a boxer, a gangster and his wife intertwine in four tales of violence and redemption.",
                Genres = new List<string> { "Crime", "Drama" },
                Actors = new List<string> { "John Travolta", "Uma Thurman", "Samuel L. Jackson" },
                Director = "Quentin Tarantino",
                ReleaseYear = 1994,
                DurationMinutes = 154,
                AverageRating = 4.7,
                TotalRatings = 1900000,
                CreatedAt = DateTime.UtcNow.AddDays(-350)
            },
            new Movie
            {
                Title = "Forrest Gump",
                Description = "The presidencies of Kennedy and Johnson unfold through the perspective of an Alabama man with an IQ of 75.",
                Genres = new List<string> { "Drama", "Romance" },
                Actors = new List<string> { "Tom Hanks", "Robin Wright", "Gary Sinise" },
                Director = "Robert Zemeckis",
                ReleaseYear = 1994,
                DurationMinutes = 142,
                AverageRating = 4.6,
                TotalRatings = 1950000,
                CreatedAt = DateTime.UtcNow.AddDays(-330)
            },
            new Movie
            {
                Title = "The Matrix",
                Description = "A computer hacker learns from mysterious rebels about the true nature of his reality and his role in the war against its controllers.",
                Genres = new List<string> { "Sci-Fi", "Action" },
                Actors = new List<string> { "Keanu Reeves", "Laurence Fishburne", "Carrie-Anne Moss" },
                Director = "Lana Wachowski",
                ReleaseYear = 1999,
                DurationMinutes = 136,
                AverageRating = 4.6,
                TotalRatings = 1850000,
                CreatedAt = DateTime.UtcNow.AddDays(-280)
            },
            new Movie
            {
                Title = "Interstellar",
                Description = "A team of explorers travel through a wormhole in space in an attempt to ensure humanity's survival.",
                Genres = new List<string> { "Sci-Fi", "Drama", "Adventure" },
                Actors = new List<string> { "Matthew McConaughey", "Anne Hathaway", "Jessica Chastain" },
                Director = "Christopher Nolan",
                ReleaseYear = 2014,
                DurationMinutes = 169,
                AverageRating = 4.6,
                TotalRatings = 1700000,
                CreatedAt = DateTime.UtcNow.AddDays(-200)
            },
            new Movie
            {
                Title = "The Silence of the Lambs",
                Description = "A young FBI cadet must receive the help of an incarcerated cannibal killer to catch another serial killer.",
                Genres = new List<string> { "Thriller", "Crime", "Drama" },
                Actors = new List<string> { "Jodie Foster", "Anthony Hopkins", "Lawrence A. Bonney" },
                Director = "Jonathan Demme",
                ReleaseYear = 1991,
                DurationMinutes = 118,
                AverageRating = 4.6,
                TotalRatings = 1400000,
                CreatedAt = DateTime.UtcNow.AddDays(-380)
            },
            new Movie
            {
                Title = "The Green Mile",
                Description = "The lives of guards on Death Row are affected by one of their charges: a black man accused of child murder.",
                Genres = new List<string> { "Drama", "Fantasy" },
                Actors = new List<string> { "Tom Hanks", "Michael Clarke Duncan", "David Morse" },
                Director = "Frank Darabont",
                ReleaseYear = 1999,
                DurationMinutes = 189,
                AverageRating = 4.6,
                TotalRatings = 1300000,
                CreatedAt = DateTime.UtcNow.AddDays(-290)
            },
            // Add 10 more varied movies
            new Movie
            {
                Title = "The Avengers",
                Description = "Earth's mightiest heroes must come together to stop Loki and his alien army.",
                Genres = new List<string> { "Action", "Adventure", "Sci-Fi" },
                Actors = new List<string> { "Robert Downey Jr.", "Chris Evans", "Scarlett Johansson" },
                Director = "Joss Whedon",
                ReleaseYear = 2012,
                DurationMinutes = 143,
                AverageRating = 4.3,
                TotalRatings = 1600000,
                CreatedAt = DateTime.UtcNow.AddDays(-220)
            },
            new Movie
            {
                Title = "Titanic",
                Description = "A seventeen-year-old aristocrat falls in love with a kind but poor artist aboard the luxurious, ill-fated R.M.S. Titanic.",
                Genres = new List<string> { "Romance", "Drama" },
                Actors = new List<string> { "Leonardo DiCaprio", "Kate Winslet", "Billy Zane" },
                Director = "James Cameron",
                ReleaseYear = 1997,
                DurationMinutes = 194,
                AverageRating = 4.4,
                TotalRatings = 1100000,
                CreatedAt = DateTime.UtcNow.AddDays(-310)
            },
            new Movie
            {
                Title = "Gladiator",
                Description = "A former Roman General sets out to exact vengeance against the corrupt emperor who murdered his family.",
                Genres = new List<string> { "Action", "Drama", "Adventure" },
                Actors = new List<string> { "Russell Crowe", "Joaquin Phoenix", "Connie Nielsen" },
                Director = "Ridley Scott",
                ReleaseYear = 2000,
                DurationMinutes = 155,
                AverageRating = 4.5,
                TotalRatings = 1400000,
                CreatedAt = DateTime.UtcNow.AddDays(-270)
            },
            new Movie
            {
                Title = "The Departed",
                Description = "An undercover cop and a mole in the police attempt to identify each other while infiltrating an Irish gang in Boston.",
                Genres = new List<string> { "Crime", "Drama", "Thriller" },
                Actors = new List<string> { "Leonardo DiCaprio", "Matt Damon", "Jack Nicholson" },
                Director = "Martin Scorsese",
                ReleaseYear = 2006,
                DurationMinutes = 151,
                AverageRating = 4.5,
                TotalRatings = 1300000,
                CreatedAt = DateTime.UtcNow.AddDays(-260)
            },
            new Movie
            {
                Title = "The Prestige",
                Description = "After a tragic accident, two stage magicians engage in a battle to create the ultimate illusion.",
                Genres = new List<string> { "Drama", "Mystery", "Thriller" },
                Actors = new List<string> { "Christian Bale", "Hugh Jackman", "Scarlett Johansson" },
                Director = "Christopher Nolan",
                ReleaseYear = 2006,
                DurationMinutes = 130,
                AverageRating = 4.5,
                TotalRatings = 1200000,
                CreatedAt = DateTime.UtcNow.AddDays(-255)
            },
            new Movie
            {
                Title = "Django Unchained",
                Description = "With the help of a German bounty hunter, a freed slave sets out to rescue his wife from a brutal Mississippi plantation owner.",
                Genres = new List<string> { "Western", "Drama" },
                Actors = new List<string> { "Jamie Foxx", "Christoph Waltz", "Leonardo DiCaprio" },
                Director = "Quentin Tarantino",
                ReleaseYear = 2012,
                DurationMinutes = 165,
                AverageRating = 4.5,
                TotalRatings = 1400000,
                CreatedAt = DateTime.UtcNow.AddDays(-215)
            },
            new Movie
            {
                Title = "The Grand Budapest Hotel",
                Description = "A writer encounters the owner of an aging high-class hotel, who tells him of his early years.",
                Genres = new List<string> { "Comedy", "Drama", "Adventure" },
                Actors = new List<string> { "Ralph Fiennes", "F. Murray Abraham", "Mathieu Amalric" },
                Director = "Wes Anderson",
                ReleaseYear = 2014,
                DurationMinutes = 99,
                AverageRating = 4.4,
                TotalRatings = 850000,
                CreatedAt = DateTime.UtcNow.AddDays(-195)
            },
            new Movie
            {
                Title = "Mad Max: Fury Road",
                Description = "In a post-apocalyptic wasteland, a woman rebels against a tyrannical ruler in search for her homeland.",
                Genres = new List<string> { "Action", "Adventure", "Sci-Fi" },
                Actors = new List<string> { "Tom Hardy", "Charlize Theron", "Nicholas Hoult" },
                Director = "George Miller",
                ReleaseYear = 2015,
                DurationMinutes = 120,
                AverageRating = 4.5,
                TotalRatings = 950000,
                CreatedAt = DateTime.UtcNow.AddDays(-180)
            },
            new Movie
            {
                Title = "The Social Network",
                Description = "As Harvard student Mark Zuckerberg creates the social networking site Facebook.",
                Genres = new List<string> { "Biography", "Drama" },
                Actors = new List<string> { "Jesse Eisenberg", "Andrew Garfield", "Justin Timberlake" },
                Director = "David Fincher",
                ReleaseYear = 2010,
                DurationMinutes = 120,
                AverageRating = 4.4,
                TotalRatings = 680000,
                CreatedAt = DateTime.UtcNow.AddDays(-245)
            },
            new Movie
            {
                Title = "Whiplash",
                Description = "A promising young drummer enrolls at a cut-throat music conservatory where his dreams hang in the balance.",
                Genres = new List<string> { "Drama", "Music" },
                Actors = new List<string> { "Miles Teller", "J.K. Simmons", "Melissa Benoist" },
                Director = "Damien Chazelle",
                ReleaseYear = 2014,
                DurationMinutes = 106,
                AverageRating = 4.6,
                TotalRatings = 830000,
                CreatedAt = DateTime.UtcNow.AddDays(-190)
            }
        };

        await context.Movies.AddRangeAsync(movies);
        await context.SaveChangesAsync();
    }

    private static async Task SeedUsersAsync(AppDbContext context)
    {
        var users = new List<User>
        {
            new User
            {
                Username = "john_doe",
                Email = "john@example.com",
                FavoriteGenres = new List<string> { "Action", "Sci-Fi", "Thriller" },
                FavoriteActors = new List<string> { "Leonardo DiCaprio", "Christian Bale", "Tom Hardy" },
                PreferredMinYear = 2000,
                PreferredMaxYear = 2024,
                PreferredMinDuration = 90,
                PreferredMaxDuration = 180,
                CreatedAt = DateTime.UtcNow.AddDays(-180)
            },
            new User
            {
                Username = "jane_smith",
                Email = "jane@example.com",
                FavoriteGenres = new List<string> { "Drama", "Romance" },
                FavoriteActors = new List<string> { "Tom Hanks", "Morgan Freeman", "Kate Winslet" },
                PreferredMinYear = 1990,
                PreferredMaxYear = 2020,
                PreferredMinDuration = 100,
                PreferredMaxDuration = 200,
                CreatedAt = DateTime.UtcNow.AddDays(-150)
            },
            new User
            {
                Username = "mike_johnson",
                Email = "mike@example.com",
                FavoriteGenres = new List<string> { "Crime", "Thriller", "Drama" },
                FavoriteActors = new List<string> { "Al Pacino", "Robert De Niro", "Matt Damon" },
                PreferredMinYear = 1970,
                PreferredMaxYear = 2015,
                PreferredMinDuration = 120,
                PreferredMaxDuration = 180,
                CreatedAt = DateTime.UtcNow.AddDays(-120)
            },
            new User
            {
                Username = "sarah_williams",
                Email = "sarah@example.com",
                FavoriteGenres = new List<string> { "Comedy", "Adventure", "Drama" },
                FavoriteActors = new List<string> { "Tom Hanks", "Robin Williams", "Bill Murray" },
                PreferredMinYear = 1995,
                PreferredMaxYear = 2024,
                PreferredMinDuration = 90,
                PreferredMaxDuration = 150,
                CreatedAt = DateTime.UtcNow.AddDays(-90)
            },
            new User
            {
                Username = "alex_brown",
                Email = "alex@example.com",
                FavoriteGenres = new List<string> { "Sci-Fi", "Action", "Adventure" },
                FavoriteActors = new List<string> { "Keanu Reeves", "Matthew McConaughey", "Scarlett Johansson" },
                PreferredMinYear = 2000,
                PreferredMaxYear = 2024,
                PreferredMinDuration = 100,
                PreferredMaxDuration = 170,
                CreatedAt = DateTime.UtcNow.AddDays(-60)
            }
        };

        await context.Users.AddRangeAsync(users);
        await context.SaveChangesAsync();
    }

    private static async Task SeedInteractionsAsync(AppDbContext context)
    {
        var random = new Random(42); // Fixed seed for reproducibility
        var interactions = new List<UserInteraction>();

        // User 1 (John): Likes Action, Sci-Fi, Thriller
        interactions.AddRange(new[]
        {
            CreateInteraction(1, 2, InteractionType.Rating, 5.0, -30),  // The Dark Knight
            CreateInteraction(1, 3, InteractionType.Rating, 5.0, -28),  // Inception
            CreateInteraction(1, 7, InteractionType.Rating, 4.5, -25),  // The Matrix
            CreateInteraction(1, 8, InteractionType.Rating, 4.5, -20),  // Interstellar
            CreateInteraction(1, 11, InteractionType.Rating, 4.0, -15), // The Avengers
            CreateInteraction(1, 1, InteractionType.View, null, -10),
            CreateInteraction(1, 4, InteractionType.View, null, -8),
            CreateInteraction(1, 14, InteractionType.Rating, 4.5, -5), // The Departed
        });

        // User 2 (Jane): Likes Drama, Romance
        interactions.AddRange(new[]
        {
            CreateInteraction(2, 1, InteractionType.Rating, 5.0, -25),  // Shawshank
            CreateInteraction(2, 6, InteractionType.Rating, 5.0, -23),  // Forrest Gump
            CreateInteraction(2, 10, InteractionType.Rating, 4.5, -20), // The Green Mile
            CreateInteraction(2, 12, InteractionType.Rating, 4.0, -18), // Titanic
            CreateInteraction(2, 4, InteractionType.Rating, 4.5, -15),  // The Godfather
            CreateInteraction(2, 2, InteractionType.View, null, -10),
            CreateInteraction(2, 5, InteractionType.Rating, 4.0, -5),   // Pulp Fiction
        });

        // User 3 (Mike): Likes Crime, Thriller, Drama
        interactions.AddRange(new[]
        {
            CreateInteraction(3, 4, InteractionType.Rating, 5.0, -30),  // The Godfather
            CreateInteraction(3, 5, InteractionType.Rating, 5.0, -28),  // Pulp Fiction
            CreateInteraction(3, 2, InteractionType.Rating, 4.5, -25),  // The Dark Knight
            CreateInteraction(3, 9, InteractionType.Rating, 4.5, -22),  // Silence of the Lambs
            CreateInteraction(3, 14, InteractionType.Rating, 5.0, -18), // The Departed
            CreateInteraction(3, 1, InteractionType.Rating, 4.5, -15),  // Shawshank
            CreateInteraction(3, 15, InteractionType.Rating, 4.0, -10), // The Prestige
        });

        // User 4 (Sarah): Likes Comedy, Adventure, Drama
        interactions.AddRange(new[]
        {
            CreateInteraction(4, 6, InteractionType.Rating, 5.0, -28),  // Forrest Gump
            CreateInteraction(4, 1, InteractionType.Rating, 4.5, -25),  // Shawshank
            CreateInteraction(4, 17, InteractionType.Rating, 5.0, -20), // Grand Budapest Hotel
            CreateInteraction(4, 13, InteractionType.Rating, 4.0, -18), // Gladiator
            CreateInteraction(4, 10, InteractionType.Rating, 4.5, -15), // The Green Mile
            CreateInteraction(4, 12, InteractionType.View, null, -10),
        });

        // User 5 (Alex): Likes Sci-Fi, Action, Adventure
        interactions.AddRange(new[]
        {
            CreateInteraction(5, 7, InteractionType.Rating, 5.0, -30),  // The Matrix
            CreateInteraction(5, 3, InteractionType.Rating, 5.0, -27),  // Inception
            CreateInteraction(5, 8, InteractionType.Rating, 5.0, -24),  // Interstellar
            CreateInteraction(5, 11, InteractionType.Rating, 4.5, -20), // The Avengers
            CreateInteraction(5, 2, InteractionType.Rating, 4.5, -17),  // The Dark Knight
            CreateInteraction(5, 18, InteractionType.Rating, 4.5, -12), // Mad Max
            CreateInteraction(5, 13, InteractionType.Rating, 4.0, -8),  // Gladiator
        });

        // Add some random interactions for variety
        for (int i = 1; i <= 5; i++)
        {
            for (int j = 0; j < 3; j++)
            {
                var movieId = random.Next(1, 21);
                var rating = 3.0 + (random.NextDouble() * 2.0); // 3.0 to 5.0
                var daysAgo = random.Next(1, 90);

                // Avoid duplicates
                if (!interactions.Any(x => x.UserId == i && x.MovieId == movieId))
                {
                    interactions.Add(CreateInteraction(i, movieId, InteractionType.Rating, rating, -daysAgo));
                }
            }
        }

        await context.UserInteractions.AddRangeAsync(interactions);
        await context.SaveChangesAsync();
    }

    private static UserInteraction CreateInteraction(
        int userId, 
        int movieId, 
        InteractionType type, 
        double? rating, 
        int daysAgo)
    {
        return new UserInteraction
        {
            UserId = userId,
            MovieId = movieId,
            Type = type,
            Rating = rating,
            Timestamp = DateTime.UtcNow.AddDays(daysAgo)
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

12. Error Handling & Middleware {#section-12}

MovieRecommendation.API/Middleware/GlobalExceptionMiddleware.cs

using System.Net;
using System.Text.Json;

namespace MovieRecommendation.API.Middleware;

public class GlobalExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<GlobalExceptionMiddleware> _logger;
    private readonly IHostEnvironment _environment;

    public GlobalExceptionMiddleware(
        RequestDelegate next,
        ILogger<GlobalExceptionMiddleware> _logger,
        IHostEnvironment environment)
    {
        _next = next;
        this._logger = _logger;
        _environment = environment;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "An unhandled exception occurred: {Message}", ex.Message);
            await HandleExceptionAsync(context, ex);
        }
    }

    private Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;

        var response = new
        {
            error = "An error occurred processing your request",
            message = _environment.IsDevelopment() ? exception.Message : "Internal server error",
            stackTrace = _environment.IsDevelopment() ? exception.StackTrace : null,
            timestamp = DateTime.UtcNow
        };

        var options = new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
            WriteIndented = _environment.IsDevelopment()
        };

        var json = JsonSerializer.Serialize(response, options);
        return context.Response.WriteAsync(json);
    }
}
Enter fullscreen mode Exit fullscreen mode

MovieRecommendation.Infrastructure/Services/ModelTrainingBackgroundService.cs

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using MovieRecommendation.Core.Services;

namespace MovieRecommendation.Infrastructure.Services;

public class ModelTrainingBackgroundService : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;
    private readonly ILogger<ModelTrainingBackgroundService> _logger;
    private readonly TimeSpan _trainingInterval = TimeSpan.FromHours(24);

    public ModelTrainingBackgroundService(
        IServiceProvider serviceProvider,
        ILogger<ModelTrainingBackgroundService> logger)
    {
        _serviceProvider = serviceProvider;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Model Training Background Service started");

        // Wait 1 minute before first training attempt
        await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);

        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                _logger.LogInformation("Starting scheduled model training...");

                using var scope = _serviceProvider.CreateScope();
                var collaborativeEngine = scope.ServiceProvider
                    .GetRequiredService<ICollaborativeEngine>();

                var success = await collaborativeEngine.TrainModelAsync();

                if (success)
                {
                    _logger.LogInformation("Scheduled model training completed successfully");
                }
                else
                {
                    _logger.LogWarning("Scheduled model training failed - not enough data");
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error in model training background service");
            }

            // Wait for next training interval
            await Task.Delay(_trainingInterval, stoppingToken);
        }

        _logger.LogInformation("Model Training Background Service stopped");
    }
}
Enter fullscreen mode Exit fullscreen mode

13. Testing {#section-13}

MovieRecommendation.Tests/ContentBasedEngineTests.cs

using Xunit;
using Moq;
using Microsoft.Extensions.Logging;
using MovieRecommendation.Core.Entities;
using MovieRecommendation.Core.Repositories;
using MovieRecommendation.Infrastructure.Services;

namespace MovieRecommendation.Tests;

public class ContentBasedEngineTests
{
    private readonly Mock<IMovieRepository> _mockMovieRepo;
    private readonly Mock<IUserRepository> _mockUserRepo;
    private readonly Mock<IInteractionRepository> _mockInteractionRepo;
    private readonly Mock<ILogger<ContentBasedEngine>> _mockLogger;
    private readonly ContentBasedEngine _engine;

    public ContentBasedEngineTests()
    {
        _mockMovieRepo = new Mock<IMovieRepository>();
        _mockUserRepo = new Mock<IUserRepository>();
        _mockInteractionRepo = new Mock<IInteractionRepository>();
        _mockLogger = new Mock<ILogger<ContentBasedEngine>>();

        _engine = new ContentBasedEngine(
            _mockMovieRepo.Object,
            _mockUserRepo.Object,
            _mockInteractionRepo.Object,
            _mockLogger.Object);
    }

    [Fact]
    public async Task CalculateSimilarityScore_ExactGenreMatch_ReturnsHighScore()
    {
        // Arrange
        var user = new User
        {
            Id = 1,
            FavoriteGenres = new List<string> { "Action", "Sci-Fi" },
            PreferredMinYear = 2000,
            PreferredMaxYear = 2024
        };

        var movie = new Movie
        {
            Id = 1,
            Genres = new List<string> { "Action", "Sci-Fi" },
            ReleaseYear = 2020,
            DurationMinutes = 120,
            AverageRating = 4.5
        };

        // Act
        var score = await _engine.CalculateSimilarityScore(user, movie);

        // Assert
        Assert.True(score > 0.7, $"Expected score > 0.7, got {score}");
    }

    [Fact]
    public async Task CalculateSimilarityScore_NoGenreMatch_ReturnsLowScore()
    {
        // Arrange
        var user = new User
        {
            Id = 1,
            FavoriteGenres = new List<string> { "Romance", "Comedy" },
            PreferredMinYear = 2000,
            PreferredMaxYear = 2024
        };

        var movie = new Movie
        {
            Id = 1,
            Genres = new List<string> { "Horror", "Thriller" },
            ReleaseYear = 2020,
            DurationMinutes = 120,
            AverageRating = 4.5
        };

        // Act
        var score = await _engine.CalculateSimilarityScore(user, movie);

        // Assert
        Assert.True(score < 0.5, $"Expected score < 0.5, got {score}");
    }

    [Fact]
    public async Task GetContentBasedRecommendations_ReturnsLimitedResults()
    {
        // Arrange
        var userId = 1;
        var limit = 5;

        var user = new User
        {
            Id = userId,
            FavoriteGenres = new List<string> { "Action" }
        };

        var movies = Enumerable.Range(1, 10).Select(i => new Movie
        {
            Id = i,
            Title = $"Movie {i}",
            Genres = new List<string> { "Action" },
            ReleaseYear = 2020,
            DurationMinutes = 120,
            AverageRating = 4.0
        }).ToList();

        _mockUserRepo.Setup(r => r.GetByIdAsync(userId))
            .ReturnsAsync(user);
        _mockInteractionRepo.Setup(r => r.GetUserInteractionsAsync(userId))
            .ReturnsAsync(new List<UserInteraction>());
        _mockMovieRepo.Setup(r => r.GetAvailableMoviesAsync(It.IsAny<int>()))
            .ReturnsAsync(movies);

        // Act
        var recommendations = await _engine.GetContentBasedRecommendations(userId, limit);

        // Assert
        Assert.NotNull(recommendations);
        Assert.True(recommendations.Count <= limit);
    }
}
Enter fullscreen mode Exit fullscreen mode

MovieRecommendation.Tests/RecommendationEngineTests.cs

using Xunit;
using Moq;
using Microsoft.Extensions.Logging;
using MovieRecommendation.Core.Models;
using MovieRecommendation.Core.Repositories;
using MovieRecommendation.Core.Services;
using MovieRecommendation.Infrastructure.Services;

namespace MovieRecommendation.Tests;

public class RecommendationEngineTests
{
    [Fact]
    public async Task GetRecommendations_CombinesBothEngines()
    {
        // Arrange
        var mockContentEngine = new Mock<IContentBasedEngine>();
        var mockCollaborativeEngine = new Mock<ICollaborativeEngine>();
        var mockMovieRepo = new Mock<IMovieRepository>();
        var mockInteractionRepo = new Mock<IInteractionRepository>();
        var mockLogger = new Mock<ILogger<RecommendationEngine>>();

        var contentResults = new List<RecommendationResult>
        {
            new RecommendationResult { MovieId = 1, ContentScore = 0.9 },
            new RecommendationResult { MovieId = 2, ContentScore = 0.8 }
        };

        var collaborativeResults = new List<RecommendationResult>
        {
            new RecommendationResult { MovieId = 1, CollaborativeScore = 0.85 },
            new RecommendationResult { MovieId = 3, CollaborativeScore = 0.75 }
        };

        mockContentEngine.Setup(e => e.GetContentBasedRecommendations(It.IsAny<int>(), It.IsAny<int>()))
            .ReturnsAsync(contentResults);
        mockCollaborativeEngine.Setup(e => e.GetCollaborativeRecommendations(It.IsAny<int>(), It.IsAny<int>()))
            .ReturnsAsync(collaborativeResults);

        var engine = new RecommendationEngine(
            mockContentEngine.Object,
            mockCollaborativeEngine.Object,
            mockMovieRepo.Object,
            mockInteractionRepo.Object,
            mockLogger.Object);

        // Act
        var recommendations = await engine.GetRecommendations(1, 10);

        // Assert
        Assert.NotNull(recommendations);
        mockContentEngine.Verify(e => e.GetContentBasedRecommendations(1, 30), Times.Once);
        mockCollaborativeEngine.Verify(e => e.GetCollaborativeRecommendations(1, 30), Times.Once);
    }
}
Enter fullscreen mode Exit fullscreen mode

14. Docker & Deployment {#section-14}

Dockerfile

# Build stage
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src

# Copy project files
COPY ["MovieRecommendation.API/MovieRecommendation.API.csproj", "MovieRecommendation.API/"]
COPY ["MovieRecommendation.Core/MovieRecommendation.Core.csproj", "MovieRecommendation.Core/"]
COPY ["MovieRecommendation.Infrastructure/MovieRecommendation.Infrastructure.csproj", "MovieRecommendation.Infrastructure/"]

# Restore dependencies
RUN dotnet restore "MovieRecommendation.API/MovieRecommendation.API.csproj"

# Copy everything else
COPY . .

# Build
WORKDIR "/src/MovieRecommendation.API"
RUN dotnet build "MovieRecommendation.API.csproj" -c Release -o /app/build

# Publish
FROM build AS publish
RUN dotnet publish "MovieRecommendation.API.csproj" -c Release -o /app/publish /p:UseAppHost=false

# Runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
EXPOSE 80
EXPOSE 443

COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "MovieRecommendation.API.dll"]
Enter fullscreen mode Exit fullscreen mode

docker-compose.yml

version: '3.8'

services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: movie-recommendation-api
    ports:
      - "5000:80"
      - "5001:443"
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ASPNETCORE_URLS=http://+:80
      - ConnectionStrings__DefaultConnection=Server=db;Database=MovieRecommendation;User=sa;Password=YourStrong@Passw0rd123;TrustServerCertificate=True;MultipleActiveResultSets=true
    depends_on:
      - db
    networks:
      - movie-network
    restart: unless-stopped

  db:
    image: mcr.microsoft.com/mssql/server:2022-latest
    container_name: movie-recommendation-db
    environment:
      - ACCEPT_EULA=Y
      - SA_PASSWORD=YourStrong@Passw0rd123
      - MSSQL_PID=Express
    ports:
      - "1433:1433"
    volumes:
      - sqldata:/var/opt/mssql
    networks:
      - movie-network
    restart: unless-stopped
    healthcheck:
      test: /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P YourStrong@Passw0rd123 -Q "SELECT 1" || exit 1
      interval: 10s
      timeout: 3s
      retries: 10
      start_period: 10s

networks:
  movie-network:
    driver: bridge

volumes:
  sqldata:
    driver: local
Enter fullscreen mode Exit fullscreen mode

.dockerignore

**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/bin
**/charts
**/docker-compose*
**/compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
README.md
Enter fullscreen mode Exit fullscreen mode

15. Running the Application {#section-15}

Prerequisites

  • .NET 8.0 SDK installed
  • SQL Server or Docker
  • Visual Studio 2022 / VS Code / Rider

Option 1: Run Locally (Without Docker)

Step 1: Update Connection String

Edit appsettings.Development.json:

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Database=MovieRecommendationDev;Trusted_Connection=True;TrustServerCertificate=True"
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Create Database & Apply Migrations

# Navigate to API project
cd MovieRecommendation.API

# Add migration
dotnet ef migrations add InitialCreate --project ../MovieRecommendation.Infrastructure --startup-project .

# Update database
dotnet ef database update --project ../MovieRecommendation.Infrastructure --startup-project .
Enter fullscreen mode Exit fullscreen mode

Step 3: Run the Application

dotnet run
Enter fullscreen mode Exit fullscreen mode

The API will be available at:

  • HTTP: http://localhost:5000
  • HTTPS: https://localhost:5001
  • Swagger UI: http://localhost:5000 (root)

Option 2: Run with Docker

# Build and start containers
docker-compose up -d

# View logs
docker-compose logs -f api

# Stop containers
docker-compose down
Enter fullscreen mode Exit fullscreen mode

The API will be available at:

  • HTTP: http://localhost:5000

Testing the API

1. Create a User:

curl -X POST "http://localhost:5000/api/users" \
  -H "Content-Type: application/json" \
  -d '{
    "username": "testuser",
    "email": "test@example.com",
    "favoriteGenres": ["Action", "Sci-Fi"],
    "favoriteActors": ["Leonardo DiCaprio"],
    "preferredMinYear": 2000,
    "preferredMaxYear": 2024,
    "preferredMinDuration": 90,
    "preferredMaxDuration": 180
  }'
Enter fullscreen mode Exit fullscreen mode

2. Get Recommendations:

curl "http://localhost:5000/api/recommendations/user/1?limit=10"
Enter fullscreen mode Exit fullscreen mode

3. Track Interaction:

curl -X POST "http://localhost:5000/api/recommendations/track" \
  -H "Content-Type: application/json" \
  -d '{
    "userId": 1,
    "movieId": 2,
    "type": "Rating",
    "rating": 5.0
  }'
Enter fullscreen mode Exit fullscreen mode

4. Train ML Model:

curl -X POST "http://localhost:5000/api/recommendations/train"
Enter fullscreen mode Exit fullscreen mode

5. Get Movies:

curl "http://localhost:5000/api/movies?limit=20"
Enter fullscreen mode Exit fullscreen mode

Sample Response

[
  {
    "movieId": 3,
    "title": "Inception",
    "overallScore": 0.89,
    "contentScore": 0.92,
    "collaborativeScore": 0.85,
    "popularityScore": 0.91,
    "detailedScores": {
      "Genre": 0.95,
      "Actor": 0.88,
      "Year": 1.0,
      "Duration": 0.90,
      "Rating": 0.94
    },
    "reasonForRecommendation": "Recommended because it matches your preferences, in your favorite genre, and features your favorite actors",
    "generatedAt": "2024-01-15T10:30:00Z"
  }
]
Enter fullscreen mode Exit fullscreen mode

πŸŽ‰ COMPLETE! What You've Built

βœ… Features Implemented:

  1. Content-Based Filtering

    • Genre matching with Jaccard similarity
    • Actor preference matching
    • Year and duration compatibility
    • Multi-factor weighted scoring
  2. Collaborative Filtering

    • ML.NET matrix factorization
    • User behavior pattern recognition
    • Rating prediction
    • Automated model training
  3. Hybrid Recommendation

    • Combines multiple strategies
    • Configurable weights
    • Popularity and recency factors
    • Human-readable explanations
  4. Production-Ready Infrastructure

    • Clean architecture (Core/Infrastructure/API)
    • Repository pattern
    • Dependency injection
    • Error handling middleware
    • Background services
    • Database seeding
    • Health checks
  5. API Features

    • RESTful endpoints
    • Swagger documentation
    • Input validation
    • CORS support
    • Logging
  6. DevOps

    • Docker support
    • docker-compose for local development
    • Database migrations
    • Unit tests

πŸ“ˆ Next Steps & Enhancements

Phase 2 Improvements:

  1. Add authentication (JWT/OAuth)
  2. Implement Redis caching
  3. Add rate limiting
  4. Create admin dashboard
  5. Add user feedback collection
  6. Implement A/B testing framework

Phase 3 Advanced Features:

  1. Deep learning models (neural collaborative filtering)
  2. Image-based recommendations
  3. Natural language search
  4. Real-time recommendations with SignalR
  5. Multi-armed bandit algorithms
  6. Contextual recommendations (time, location, device)

πŸ”— Resources


πŸ“ Summary

You now have a complete, production-ready AI recommendation system with:

  • βœ… 2 recommendation strategies (content + collaborative)
  • βœ… ML.NET integration for machine learning
  • βœ… Clean architecture with separation of concerns
  • βœ… Full CRUD API with Swagger docs
  • βœ… Database with EF Core
  • βœ… Docker containerization
  • βœ… Unit tests
  • βœ… Background ML model training
  • βœ… 20+ sample movies and test data

The system is ready to run, test, and extend! πŸš€


πŸ’¬ Questions? Issues? Improvements?

I'd love to hear your thoughts and feedback!

πŸ”— Connect with me on LinkedIn: HAMZA ZERYOUH

Whether you have questions about the implementation, suggestions for improvements, or want to share your own ML.NET experiences – feel free to reach out!

πŸ“§ Let's discuss:

  • How you're using ML.NET in production
  • Challenges you faced with recommendation systems
  • Ideas for Part 2 of this tutorial

Found this helpful? Don't forget to:

  • ⭐ Star the repository (if you create one)
  • πŸ”„ Share this article with your network
  • πŸ’¬ Drop a comment with your experience

#dotnet #machinelearning #mlnet #ai #tutorial #recommendations #cleanarchitecture

Top comments (0)