JobRadar is an automated job scraping and aggregation system built with .NET 10. It scrapes job listings from multiple sources (LinkedIn, HelloJob, Glorri), stores them in PostgreSQL, and exports results to CSV, Excel, or JSON formats.
- Multi-Source Scraping: Scrapes job listings from LinkedIn, HelloJob, and Glorri
- Smart Deduplication: Uses SHA-256 URL hashing to prevent duplicate job entries
- Scheduled Execution: Quartz.NET-based background jobs with configurable intervals
- Multiple Export Formats: Export data to CSV, Excel (XLSX), or JSON
- RESTful API: Query and filter job listings via minimal API endpoints
- Clean Architecture: Organized into SharedKernel, Features, Infrastructure, and API layers
- Fully Tested: Includes unit and integration tests using xUnit
- Structured Logging: Serilog integration for production-ready logging
JobRadar/
├── JobRadar.SharedKernel/ # Domain entities, enums, interfaces
├── JobRadar.Features/ # Scraping & export strategies (business logic)
├── JobRadar.Infrastructure/ # EF Core, repositories, PostgreSQL integration
├── JobRadar.API/ # Minimal API endpoints
├── JobRadar.Worker/ # Quartz background jobs
├── JobRadar.UnitTests/ # Unit tests
└── JobRadar.IntegrationTests/ # Integration tests
- Strategy Pattern: For scrapers and exporters
- Page Object Model: For Playwright scraping logic
- Repository Pattern: For data access abstraction
- Options Pattern: For configuration management
- Result Pattern: Using
ErrorOrfor functional error handling
- .NET 10 SDK
- PostgreSQL 14+
- Docker (optional, for containerized setup)
-
Clone the repository:
git clone https://github.com/NigarOsmanova/JobRadar.git cd JobRadar -
Install Playwright browsers:
pwsh bin/Debug/net10.0/playwright.ps1 install chromium
-
Configure settings:
- Copy
appsettings.example.jsontoappsettings.jsonin bothJobRadar.APIandJobRadar.Worker - Update the PostgreSQL connection string
- Configure scraper keywords, locations, and enabled sources
- Copy
-
Apply database migrations:
cd JobRadar.Infrastructure dotnet ef database update --startup-project ../JobRadar.Worker -
Run with Docker Compose (recommended):
docker-compose up -d
Or run locally:
# Terminal 1: Run the Worker cd JobRadar.Worker dotnet run # Terminal 2: Run the API cd JobRadar.API dotnet run
{
"ConnectionStrings": {
"Postgres": "Host=localhost;Database=JobRadar;Username=postgres;Password=yourpassword"
},
"ScraperSettings": {
"SearchKeyword": ".NET Developer",
"Location": "Baku",
"IntervalMinutes": 120,
"MaxJobsPerRun": 50,
"EnabledSources": ["LinkedIn", "Hellojob", "Glorri"]
},
"ExportSettings": {
"OutputPath": "C:\\JobRadarExports",
"Formats": ["Csv", "Excel", "Json"]
}
}| Method | Endpoint | Description |
|---|---|---|
| GET | /jobs |
Get paginated job listings |
| GET | /jobs/new |
Get only new (unread) jobs |
| GET | /jobs/{id} |
Get job by ID |
| GET | /jobs/source/{source} |
Get jobs from specific source |
curl -X GET "https://localhost:5001/jobs?page=1&pageSize=20"{
"items": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"title": ".NET Developer",
"company": "Acme Corp",
"location": "Baku, Azerbaijan",
"url": "https://linkedin.com/jobs/123",
"source": "LinkedIn",
"scrapedAt": "2025-01-15T10:30:00Z",
"postedAt": "2025-01-14T08:00:00Z",
"isNew": true
}
],
"page": 1,
"pageSize": 20,
"totalCount": 150,
"totalPages": 8,
"hasNextPage": true,
"hasPreviousPage": false
}- .NET 10: Modern C# with minimal APIs
- PostgreSQL + EF Core 10: Relational data storage
- Playwright: Headless browser automation for scraping
- Quartz.NET: Job scheduling and background tasks
- Serilog: Structured logging
- ClosedXML: Excel file generation
- CsvHelper: CSV export
- xUnit: Unit and integration testing
- ErrorOr: Functional error handling
# Run all tests
dotnet test
# Run only unit tests
dotnet test --filter "FullyQualifiedName~UnitTests"
# Run only integration tests
dotnet test --filter "FullyQualifiedName~IntegrationTests"CREATE TABLE JobListings (
Id UUID PRIMARY KEY,
Title VARCHAR(300) NOT NULL,
Company VARCHAR(200) NOT NULL,
Location VARCHAR(500) NOT NULL,
Url VARCHAR(1000) NOT NULL,
UrlHash VARCHAR(64) NOT NULL UNIQUE, -- SHA-256 hash for deduplication
Source INT NOT NULL,
ScrapedAt TIMESTAMP NOT NULL,
PostedAt TIMESTAMP NULL,
Description TEXT NULL,
IsNew BOOLEAN NOT NULL
);
CREATE UNIQUE INDEX IX_JobListings_UrlHash ON JobListings(UrlHash);- Create a new folder in
JobRadar.Features/Scraping/{SourceName}/ - Implement
IJobScraperStrategyby extendingBaseJobScraper - Create Page Objects extending
JobPageandJobCard - Register in
DependencyInjection.cs - Add the source name to
appsettings.json→EnabledSources
- Create a new class in
JobRadar.Features/Export/Strategies/ - Implement
IJobExportStrategy - Register in
ExportExtensions.cs - Add the format to
appsettings.json→Formats
The project includes a docker-compose.yml for easy setup:
docker-compose up -dThis starts:
- PostgreSQL container
- pgAdmin (optional, for database management)
This project is open-source and available under the MIT License.
Nigar Osmanova
- GitHub: @NigarOsmanova
- LinkedIn: Nigar Osmanova
- Playwright for robust browser automation
- Quartz.NET for reliable job scheduling
- ErrorOr for functional error handling patterns
- Web dashboard UI — VakansiyaRadar (Next.js + TypeScript)
- Add more job sources (Indeed, Glassdoor, etc.)
- Email notifications for new jobs
- Dockerize the entire application
- Add CI/CD pipeline (GitHub Actions)
- Implement rate limiting for API
- Add filtering by keywords/salary in API
This project is intended for educational and portfolio purposes only.
It demonstrates how to build an automated job aggregation system using .NET, Playwright, PostgreSQL, and scheduled background processing. Users are responsible for ensuring that their use of this project complies with the Terms of Service, robots.txt policies, and applicable laws of any websites they choose to scrape.
The author does not encourage or endorse unauthorized scraping, excessive request volumes, bypassing access controls, or collecting data in violation of third-party website policies.