Slyv.News (1.0.0)

Published 2026-01-06 19:54:17 +00:00 by logikonline

Installation

dotnet nuget add source --name logikonline --username your_username --password your_token 
dotnet add package --source logikonline --version 1.0.0 Slyv.News

About this package

News aggregation and trend analysis library with RSS feed integration, sentiment tracking, intelligent categorization, and content filtering. Supports multi-source news aggregation, topic extraction, trend detection, and personalized news recommendations. Includes temporal trend analysis for tracking news evolution over time.

Slyv.News

Standalone news aggregation and analysis library for the Slyv platform.

Overview

Slyv.News is a platform-agnostic library that provides news fetching, sentiment analysis, trend tracking, and goal-aligned content filtering. It can be used in:

  • Mobile apps (single user, local SQLite) via .NET MAUI
  • Web APIs (multi-user, SQL Server)
  • Console apps (testing, demos)

Architecture

Following the same pattern as Slyv.Analysis and Slyv.Social, this library:

  • No dependency on Slyv.Core - Completely standalone
  • Repository pattern - Abstract data access via INewsRepository
  • Single-user context - No multi-tenancy complexity
  • Flexible storage - Implement INewsRepository for your data store

Dependencies

Slyv.News
└── Slyv.Analysis (for sentiment analysis, temporal analysis, topic modeling)

Project Structure

Slyv.News/
├── Data/
│   ├── NewsEntities.cs       # News entities (NewsArticle, NewsCategory, NewsSource, etc.)
│   └── TrendEntities.cs      # Trend tracking entities
├── Models/
│   └── NewsModels.cs         # DTOs and API response models
├── Common/
│   └── Enums.cs              # News-related enums
├── Services/
│   ├── NewsService.cs        # Core news fetching and filtering
│   └── NewsTrendAnalysisService.cs # Trend analysis and insights
└── Abstractions/
    └── INewsRepository.cs    # Data access abstraction

Usage

Mobile App (MAUI with SQLite)

// Slyv.Mobile/MauiProgram.cs
public static MauiApp CreateMauiApp()
{
    var builder = MauiApp.CreateBuilder();

    // Register SQLite DbContext for mobile
    builder.Services.AddDbContext<MobileDbContext>(options =>
        options.UseSqlite($"Data Source={dbPath}"));

    // Register repository implementation for mobile
    builder.Services.AddSingleton<INewsRepository, SqliteNewsRepository>();

    // Register news services
    builder.Services.AddHttpClient<INewsService, NewsService>();
    builder.Services.AddScoped<INewsTrendAnalysisService, NewsTrendAnalysisService>();

    return builder.Build();
}

// Implement SqliteNewsRepository in your mobile app
public class SqliteNewsRepository : INewsRepository
{
    private readonly MobileDbContext _context;

    public SqliteNewsRepository(MobileDbContext context)
    {
        _context = context;
    }

    // Implement all INewsRepository methods for SQLite
}

Web API (ASP.NET Core with SQL Server)

// Slyv.Core/Data/EfCoreNewsRepository.cs
public class EfCoreNewsRepository : INewsRepository
{
    private readonly SlyvDbContext _context;

    public EfCoreNewsRepository(SlyvDbContext context)
    {
        _context = context;
    }

    // Implement all INewsRepository methods for EF Core/SQL Server
}

// Slyv.Api/Program.cs
builder.Services.AddDbContext<SlyvDbContext>(options =>
    options.UseSqlServer(connectionString));

builder.Services.AddScoped<INewsRepository, EfCoreNewsRepository>();
builder.Services.AddHttpClient<INewsService, NewsService>();
builder.Services.AddScoped<INewsTrendAnalysisService, NewsTrendAnalysisService>();

Features

News Fetching

  • MediaStack API integration
  • Multi-category support
  • Pagination for large datasets
  • Duplicate detection via content hashing
  • Source and category management

Sentiment Analysis

  • Article sentiment scoring
  • Emotion detection
  • Temporal trend analysis
  • Pattern detection

Goal-Aligned Filtering

  • Filter articles by user goals
  • Relevance scoring
  • Personalized recommendations
  • Audit trail for filtering decisions

Trend Analysis

  • Daily/weekly/monthly snapshots
  • Category trend tracking
  • User progress monitoring
  • Anomaly detection
  • Predictive insights

Entities

Core Entities

  • NewsArticle - News article with sentiment and metadata
  • NewsCategory - Article categories with statistics
  • NewsSource - News sources with credibility scores
  • NewsTag - Article tags for topic tracking

User-Specific Entities

  • UserNewsPreference - User news preferences and filters
  • NewsArticleInteraction - User interactions with articles
  • UserNewsRelevance - Goal alignment scores per article

Trend Tracking

  • NewsTrendSnapshot - Global news trends
  • CategoryTrendSnapshot - Category-specific trends
  • UserProgressSnapshot - User reading progress
  • SentimentTrendAnalysis - Sentiment over time
  • TrendAlert - Alerts for significant changes

Code Examples

News Fetching

Basic News Fetching

using Slyv.News.Services;
using Slyv.News.Models;

public class NewsController
{
    private readonly INewsService _newsService;

    public async Task<List<NewsArticle>> GetLatestNews()
    {
        // Fetch latest 100 news articles across all categories
        var (articles, newCount, duplicateCount) = await _newsService.FetchAllNewsAsync(limit: 100);

        Console.WriteLine($"Fetched {newCount} new articles");
        Console.WriteLine($"Skipped {duplicateCount} duplicates");

        foreach (var article in articles)
        {
            Console.WriteLine($"{article.Title}");
            Console.WriteLine($"  Source: {article.Source?.Name}");
            Console.WriteLine($"  Sentiment: {article.SentimentScore:F2}");
            Console.WriteLine($"  Published: {article.PublishedAt:yyyy-MM-dd}");
        }

        return articles;
    }
}

Fetch News by Category

// Fetch news with user preferences
var preferences = new NewsPreferencesDto
{
    Categories = new List<string> { "technology", "science", "business" },
    Countries = new List<string> { "us", "gb" },
    Languages = new List<string> { "en" },
    ExcludeSources = new List<string> { "tabloid-news" }
};

var (articles, newCount, duplicateCount) = await _newsService.FetchNewsAsync(
    preferences,
    limit: 50
);

Console.WriteLine($"Found {articles.Count} articles matching your preferences");

Extended News Fetching (Pagination)

// Fetch large volume of news (500+ articles) with automatic pagination
var (articles, newCount, duplicateCount) = await _newsService.FetchExtendedNewsAsync(totalLimit: 500);

Console.WriteLine($"Fetched {newCount} new articles out of {totalLimit} requested");
Console.WriteLine($"Categories covered: {articles.Select(a => a.Category).Distinct().Count()}");

Goal-Aligned News Filtering

Get News Aligned with User Goals

using Slyv.News.Services;

public class PersonalizedNewsService
{
    private readonly INewsService _newsService;

    public async Task<GoalAlignedNewsFeedDto> GetPersonalizedFeed(Guid userId)
    {
        // Get news feed filtered by user's active goals
        var feed = await _newsService.GetGoalAlignedFeedAsync(userId, includeAudit: true);

        Console.WriteLine($"Total articles: {feed.TotalArticles}");
        Console.WriteLine($"Filtered articles: {feed.FilteredArticles.Count}");
        Console.WriteLine($"Average alignment: {feed.AverageAlignment:P0}");

        foreach (var article in feed.FilteredArticles)
        {
            Console.WriteLine($"\n{article.Article.Title}");
            Console.WriteLine($"  Alignment score: {article.AlignmentScore:P0}");
            Console.WriteLine($"  Matching goals:");

            foreach (var goalMatch in article.MatchingGoals ?? [])
            {
                Console.WriteLine($"    - {goalMatch.GoalTitle}: {goalMatch.Score:P0}");
            }
        }

        // Review audit trail
        if (feed.AuditTrail != null)
        {
            Console.WriteLine($"\nFiltering decisions:");
            Console.WriteLine($"  Applied thresholds: {feed.AuditTrail.AppliedThresholds}");
            Console.WriteLine($"  Rejection reasons: {string.Join(", ", feed.AuditTrail.RejectionReasons)}");
        }

        return feed;
    }
}

Calculate Article Relevance for User

// Calculate how relevant an article is to a user's goals
var articleId = Guid.Parse("...");
var userId = Guid.Parse("...");

var relevance = await _newsService.CalculateUserRelevanceAsync(userId, articleId);

Console.WriteLine($"Article relevance:");
Console.WriteLine($"  Overall score: {relevance.RelevanceScore:P0}");
Console.WriteLine($"  Sentiment alignment: {relevance.SentimentAlignment:F2}");
Console.WriteLine($"  Category match: {relevance.CategoryMatch}");
Console.WriteLine($"  Goal alignments: {relevance.GoalAlignments?.Count ?? 0}");

News Analysis

Analyze Article Sentiment

// Analyze a news article for sentiment and emotion
var article = await _newsService.GetArticleByIdAsync(articleId);
if (article != null)
{
    var analyzed = await _newsService.AnalyzeArticleAsync(article);

    Console.WriteLine($"Title: {analyzed.Title}");
    Console.WriteLine($"Sentiment: {analyzed.SentimentLabel} ({analyzed.SentimentScore:F2})");
    Console.WriteLine($"Emotion: {analyzed.EmotionTone}");
    Console.WriteLine($"Confidence: {analyzed.SentimentConfidence:P0}");
}

Trend Analysis

using Slyv.News.Services;

public class TrendAnalysisService
{
    private readonly INewsTrendAnalysisService _trendService;

    public async Task AnalyzeTrends(Guid userId)
    {
        // Analyze trends for the user
        await _trendService.AnalyzeTrendsAsync(userId);

        // Get daily trend snapshot
        var dailyTrend = await _trendService.GetDailyTrendSnapshotAsync(DateTime.Today, userId);
        if (dailyTrend != null)
        {
            Console.WriteLine($"Daily News Trends for {dailyTrend.SnapshotDate:yyyy-MM-dd}:");
            Console.WriteLine($"  Articles analyzed: {dailyTrend.ArticlesAnalyzed}");
            Console.WriteLine($"  Average sentiment: {dailyTrend.AverageSentiment:F2}");
            Console.WriteLine($"  Top categories: {string.Join(", ", dailyTrend.TopCategories ?? [])}");
            Console.WriteLine($"  Trending topics: {string.Join(", ", dailyTrend.TrendingTopics ?? [])}");
        }

        // Get sentiment trend over time
        var sentimentTrends = await _trendService.GetSentimentTrendAsync(
            userId,
            startDate: DateTime.Today.AddDays(-30),
            endDate: DateTime.Today
        );

        Console.WriteLine($"\n30-Day Sentiment Trend:");
        foreach (var trend in sentimentTrends)
        {
            Console.WriteLine($"  {trend.AnalysisDate:yyyy-MM-dd}: {trend.AverageSentiment:F2}");
            Console.WriteLine($"    Positive: {trend.PositivePercentage:P0}");
            Console.WriteLine($"    Negative: {trend.NegativePercentage:P0}");
        }
    }

    public async Task MonitorTrendAlerts(Guid userId)
    {
        // Get active trend alerts
        var alerts = await _trendService.GetActiveTrendAlertsAsync(userId);

        Console.WriteLine($"Active Trend Alerts ({alerts.Count}):");
        foreach (var alert in alerts)
        {
            Console.WriteLine($"  {alert.AlertType}: {alert.Message}");
            Console.WriteLine($"    Severity: {alert.Severity}");
            Console.WriteLine($"    Detected: {alert.DetectedAt:g}");
        }
    }
}

User Preferences

Update News Preferences

// Update user's news preferences
var preferences = new NewsPreferencesDto
{
    Categories = new List<string> { "technology", "science", "health" },
    Countries = new List<string> { "us" },
    Languages = new List<string> { "en" },
    MinSentimentScore = 0.3,  // Prefer positive news
    MaxArticlesPerDay = 50,
    ExcludeSources = new List<string> { "low-quality-source" }
};

var success = await _newsService.UpdateUserPreferencesAsync(userId, preferences);

if (success)
{
    Console.WriteLine("News preferences updated successfully");
}

Repository Implementation

EF Core News Repository

using Microsoft.EntityFrameworkCore;
using Slyv.News.Abstractions;
using Slyv.News.Data;

public class EfCoreNewsRepository : INewsRepository
{
    private readonly SlyvDbContext _context;

    public EfCoreNewsRepository(SlyvDbContext context)
    {
        _context = context;
    }

    public async Task<NewsArticle?> GetArticleByIdAsync(Guid id, CancellationToken ct = default)
    {
        return await _context.NewsArticles
            .Include(a => a.Source)
            .Include(a => a.Categories)
            .Include(a => a.Tags)
            .AsNoTracking()
            .FirstOrDefaultAsync(a => a.Id == id, ct);
    }

    public async Task<List<NewsArticle>> GetArticlesByCategoryAsync(
        int categoryId,
        int limit = 50,
        CancellationToken ct = default)
    {
        return await _context.NewsArticles
            .Include(a => a.Source)
            .Where(a => a.Categories.Any(c => c.CategoryId == categoryId))
            .OrderByDescending(a => a.PublishedAt)
            .Take(limit)
            .AsNoTracking()
            .ToListAsync(ct);
    }

    public async Task<NewsArticle> SaveArticleAsync(NewsArticle article, CancellationToken ct = default)
    {
        _context.NewsArticles.Add(article);
        await _context.SaveChangesAsync(ct);
        return article;
    }

    public async Task<NewsArticle?> GetArticleByUrlAsync(string url, CancellationToken ct = default)
    {
        return await _context.NewsArticles
            .FirstOrDefaultAsync(a => a.Url == url, ct);
    }

    public async Task<List<NewsArticle>> GetRecentArticlesAsync(
        DateTime since,
        int limit = 100,
        CancellationToken ct = default)
    {
        return await _context.NewsArticles
            .Include(a => a.Source)
            .Where(a => a.PublishedAt >= since)
            .OrderByDescending(a => a.PublishedAt)
            .Take(limit)
            .AsNoTracking()
            .ToListAsync(ct);
    }

    public async Task<int> GetSourceCountAsync(CancellationToken ct = default)
    {
        return await _context.NewsSources.CountAsync(ct);
    }

    public async Task<List<NewsSource>> GetAllSourcesAsync(CancellationToken ct = default)
    {
        return await _context.NewsSources
            .AsNoTracking()
            .ToListAsync(ct);
    }

    public async Task<int> SaveChangesAsync(CancellationToken ct = default)
    {
        return await _context.SaveChangesAsync(ct);
    }
}

Configuration

appsettings.json

{
  "MediaStack": {
    "ApiKey": "your-mediastack-api-key",
    "BaseUrl": "http://api.mediastack.com/v1/"
  },
  "NewsSettings": {
    "DefaultLimit": 50,
    "MaxArticlesPerRequest": 100,
    "CacheDuration": "01:00:00",
    "MinSentimentThreshold": 0.0,
    "EnableGoalFiltering": true
  }
}

Service Registration

// Program.cs
builder.Services.AddHttpClient<INewsService, NewsService>();
builder.Services.AddScoped<INewsTrendAnalysisService, NewsTrendAnalysisService>();
builder.Services.AddScoped<INewsRepository, EfCoreNewsRepository>();

// Register Slyv.Analysis dependencies (required)
builder.Services.AddSingleton<AdvancedSentimentAnalyzer>();
builder.Services.AddSingleton<TopicModeling>();

// Register Slyv.Goals dependencies (for goal-aligned filtering)
builder.Services.AddScoped<IGoalRepository, EfCoreGoalRepository>();
builder.Services.AddScoped<IGoalAlignmentService, GoalAlignmentService>();

Best Practices

1. Handle Duplicate Detection

// ✅ Good - NewsService automatically detects duplicates
var (articles, newCount, duplicateCount) = await _newsService.FetchAllNewsAsync(100);
Console.WriteLine($"New: {newCount}, Duplicates: {duplicateCount}");

// Articles are deduplicated by content hash

2. Use Goal-Aligned Filtering

// ✅ Good - Filter by user goals for personalized feed
var feed = await _newsService.GetGoalAlignedFeedAsync(userId);

// ❌ Bad - Showing all news without filtering
var allArticles = await _newsService.FetchAllNewsAsync(1000);
// ✅ Good - Track trends to identify patterns
await _trendService.AnalyzeTrendsAsync(userId);
var trends = await _trendService.GetSentimentTrendAsync(userId, startDate, endDate);

// Detect anomalies and send alerts
var alerts = await _trendService.GetActiveTrendAlertsAsync(userId);

4. Cache News Articles

// ✅ Good - Query recent articles from database
var recentArticles = await _newsRepository.GetRecentArticlesAsync(
    DateTime.Today.AddHours(-6),
    limit: 50
);

// ❌ Bad - Fetching from API every time
// This costs API credits and is slower

Troubleshooting

Issue: No news articles returned

Solution: Check MediaStack API key configuration:

{
  "MediaStack": {
    "ApiKey": "your-api-key-here"
  }
}

Issue: Duplicate articles appearing

Solution: NewsService automatically deduplicates by content hash. Check repository implementation saves content hash:

article.ContentHash = ComputeContentHash(article.Title + article.Description);

Issue: Goal-aligned filtering not working

Solution: Ensure GoalRepository and GoalAlignmentService are registered:

builder.Services.AddScoped<IGoalRepository, EfCoreGoalRepository>();
builder.Services.AddScoped<IGoalAlignmentService, GoalAlignmentService>();

License

Part of the Slyv project. See main repository for license information.


Built with ❤️ by the Slyv Team

Stay informed with personalized, goal-aligned news

Initial release of Slyv.News with news aggregation and trend analysis capabilities.

Dependencies

ID Version Target Framework
Slyv.Analysis 1.0.0 net9.0
Slyv.Goals 1.0.0 net9.0
Microsoft.Extensions.Configuration.Abstractions 9.0.10 net9.0
Microsoft.Extensions.DependencyInjection 9.0.10 net9.0
Microsoft.Extensions.DependencyInjection.Abstractions 9.0.10 net9.0
Microsoft.Extensions.Http 9.0.10 net9.0
Microsoft.Extensions.Logging.Abstractions 9.0.10 net9.0
Microsoft.Extensions.Options 9.0.10 net9.0
System.Text.Json 9.0.10 net9.0
Details
NuGet
2026-01-06 19:54:17 +00:00
0
David H. Friedel Jr.
178 KiB
Assets (4)
Versions (1) View all
1.0.0 2026-01-06