Commit: 43a390c

Initial commit

Mårten Åsberg committed on 2026-02-24 at 19:41
.editorconfig +35 -0
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..aefb382
@@ -0,0 +1,35 @@
root = true
indent_style = space
encoding = utf-8
end_of_line = lf
insert_final_newline = true
# Xml files
[*.xml]
indent_size = 2
# Xml project files
[*.{csproj,fsproj,vbproj,proj,slnx}]
indent_size = 2
# Xml config files
[*.{props,targets,config,nuspec}]
indent_size = 2
[*.json]
indent_size = 2
[*.{razor,html}]
indent_size = 2
[*.css]
indent_size = 2
# C# files
[*.cs]
indent_size = 4
tab_width = 4
max_line_length = 120
dotnet_diagnostic.IDE0005.severity = warning
.gitignore +482 -0
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0808c4a
@@ -0,0 +1,482 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from `dotnet new gitignore`
# dotenv files
.env
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET
project.lock.json
project.fragment.lock.json
artifacts/
# Tye
.tye/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
# but not Directory.Build.rsp, as it configures directory-level build defaults
!Directory.Build.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
.idea/
##
## Visual studio for Mac
##
# globs
Makefile.in
*.userprefs
*.usertasks
config.make
config.status
aclocal.m4
install-sh
autom4te.cache/
*.tar.gz
tarballs/
test-results/
# content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore
# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# Vim temporary swap files
*.swp
Console/Console.csproj +25 -0
diff --git a/Console/Console.csproj b/Console/Console.csproj
new file mode 100644
index 0000000..dd018a3
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>MSearch.Console</RootNamespace>
<AssemblyName>MSearch.Console</AssemblyName>
<OutputType>Exe</OutputType>
<UserSecretsId>5563a99c-e892-4bec-9178-015a71fd9a57</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Domain\Domain.csproj" />
<ProjectReference Include="..\SearchProviders\GitHub\GitHub.csproj" />
<ProjectReference Include="..\SearchProviders\HackerNews\HackerNews.csproj" />
<ProjectReference Include="..\SearchProviders\MicrosoftLearn\MicrosoftLearn.csproj" />
<ProjectReference Include="..\SearchProviders\MozillaDeveloperNetwork\MozillaDeveloperNetwork.csproj" />
<ProjectReference Include="..\SearchProviders\OpenStreetMap\OpenStreetMap.csproj" />
<ProjectReference Include="..\SearchProviders\Reddit\Reddit.csproj" />
<ProjectReference Include="..\SearchProviders\Spotify\Spotify.csproj" />
<ProjectReference Include="..\SearchProviders\StackExchange\StackExchange.csproj" />
<ProjectReference Include="..\SearchProviders\TheMovieDb\TheMovieDb.csproj" />
<ProjectReference Include="..\SearchProviders\Wikipedia\Wikipedia.csproj" />
<ProjectReference Include="..\SearchProviders\YouTube\YouTube.csproj" />
</ItemGroup>
</Project>
Console/Program.cs +49 -0
diff --git a/Console/Program.cs b/Console/Program.cs
new file mode 100644
index 0000000..3ca2b6b
@@ -0,0 +1,49 @@
using System.Threading;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using MSearch.Domain;
using MSearch.SearchProviders.GitHub;
using MSearch.SearchProviders.HackerNews;
using MSearch.SearchProviders.MicrosoftLearn;
using MSearch.SearchProviders.MozillaDeveloperNetwork;
using MSearch.SearchProviders.OpenStreetMap;
using MSearch.SearchProviders.Reddit;
using MSearch.SearchProviders.Spotify;
using MSearch.SearchProviders.StackExchange;
using MSearch.SearchProviders.TheMovieDb;
using MSearch.SearchProviders.Wikipedia;
using MSearch.SearchProviders.YouTube;
var builder = Host.CreateApplicationBuilder(args);
builder.Configuration.AddUserSecrets<Program>();
builder
.Services.AddSearchService()
.AddGitHubSearchProvider()
.AddHackerNewsSearchProvider()
.AddMicrosoftLearnSearchProvider()
.AddMozillaDeveloperNetworkSearchProvider()
.AddOpenStreetMapSearchProvider()
.AddRedditSearchProvider()
.AddSpotifySearchProvider()
.AddStackExchangeSearchProvider("stackoverflow")
.AddTheMovieDbSearchProvider()
.AddWikipediaSearchProvider("en")
.AddYouTubeSearchProvider();
using var app = builder.Build();
await app.StartAsync();
var logger = app.Services.GetRequiredService<ILogger<Program>>();
var searchService = app.Services.GetRequiredService<SearchService>();
await foreach (var result in searchService.Search(args[0], CancellationToken.None))
{
logger.LogInformation("{Title}: {Url}\n{Summary}", result.Title, result.Url, result.Summary);
}
await app.StopAsync();
Console/packages.lock.json +460 -0
diff --git a/Console/packages.lock.json b/Console/packages.lock.json
new file mode 100644
index 0000000..2913bf6
@@ -0,0 +1,460 @@
{
"version": 2,
"dependencies": {
"net10.0": {
"CSharpier.MsBuild": {
"type": "Direct",
"requested": "[1.0.2, )",
"resolved": "1.0.2",
"contentHash": "Y98ba4yeefqrJY+MbCXhvb8x4/mCy8zHeuu/dYKwaM4p0Yu2LToI5ffTg/ErcpPuLnwJBNE229STdhf/dlikIw=="
},
"Microsoft.Extensions.Hosting": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "ssbRCALcbBXfQPXLr4bZ3FVlbnDzeR0F6hPKDUCZLPQul7oBeSGaJq+XLUjwYaptOs4TN5cSy1q6LVLPWFgjKQ==",
"dependencies": {
"Microsoft.Extensions.Configuration": "10.0.3",
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.Configuration.Binder": "10.0.3",
"Microsoft.Extensions.Configuration.CommandLine": "10.0.3",
"Microsoft.Extensions.Configuration.EnvironmentVariables": "10.0.3",
"Microsoft.Extensions.Configuration.FileExtensions": "10.0.3",
"Microsoft.Extensions.Configuration.Json": "10.0.3",
"Microsoft.Extensions.Configuration.UserSecrets": "10.0.3",
"Microsoft.Extensions.DependencyInjection": "10.0.3",
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Diagnostics": "10.0.3",
"Microsoft.Extensions.FileProviders.Abstractions": "10.0.3",
"Microsoft.Extensions.FileProviders.Physical": "10.0.3",
"Microsoft.Extensions.Hosting.Abstractions": "10.0.3",
"Microsoft.Extensions.Logging": "10.0.3",
"Microsoft.Extensions.Logging.Abstractions": "10.0.3",
"Microsoft.Extensions.Logging.Configuration": "10.0.3",
"Microsoft.Extensions.Logging.Console": "10.0.3",
"Microsoft.Extensions.Logging.Debug": "10.0.3",
"Microsoft.Extensions.Logging.EventLog": "10.0.3",
"Microsoft.Extensions.Logging.EventSource": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3"
}
},
"Microsoft.Extensions.Configuration": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "H1Cjv2xmm7O3iAGmFTcnSM0ZhLQ/7SqefmAvSJoT1PbXoxeYc2fo0mCLn2JlVbr9E6YpoU9q/o0fI9neDJB0xQ==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "xVDHL0+SIgemfh95fTO9cGLe17TWv/ZP0n7m01z8X6pzt2DmQpucioWR/mYZA1sRlkWnkXzfl0JweLNWmE9WMg==",
"dependencies": {
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Configuration.Binder": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "759UhpKaR5Jsll9kXpkft4z/7tpeF7Dw2rTY/9f9JchaSQTpRFNIPkZFZvoo7fFpbjUaqtDlO5aiGpmQrp/EUA==",
"dependencies": {
"Microsoft.Extensions.Configuration": "10.0.3",
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3"
}
},
"Microsoft.Extensions.Configuration.CommandLine": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "qVytXuCHQCIJcOsJJnp+1mNCAtiWuLqI0qhCcByFuyxDifthefEWX3fVAXbaxn1lDP2iR1KH44Oq7tvmT7dBHg==",
"dependencies": {
"Microsoft.Extensions.Configuration": "10.0.3",
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3"
}
},
"Microsoft.Extensions.Configuration.EnvironmentVariables": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "jBm6bpc5OM2VHM/QYVUyD78xweFzble6UsIt7GUnQAwCm07hktFaUBtRfO7viLGg5qPbc4ByteNB7DeVAYNSfA==",
"dependencies": {
"Microsoft.Extensions.Configuration": "10.0.3",
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3"
}
},
"Microsoft.Extensions.Configuration.FileExtensions": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "/MLsBbLpwDxsU+7DDNwasf2mKrpMSOWEL377gNZTy5waFkCYvS3GVaLIz6bvikH4rAwHrCOxHw0t/5iCoImYCA==",
"dependencies": {
"Microsoft.Extensions.Configuration": "10.0.3",
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.FileProviders.Abstractions": "10.0.3",
"Microsoft.Extensions.FileProviders.Physical": "10.0.3",
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Configuration.Json": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "mGGMOA9nkET8OVsQfS41o66eWkckBzNHJK6+5VbLQ2YdyqKphcv27uDZxLf4exSl+5QxLnHkN+W/4qEDgyvCPA==",
"dependencies": {
"Microsoft.Extensions.Configuration": "10.0.3",
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.Configuration.FileExtensions": "10.0.3",
"Microsoft.Extensions.FileProviders.Abstractions": "10.0.3"
}
},
"Microsoft.Extensions.Configuration.UserSecrets": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "f6FiscfqlLrNUR2x7XcMqMGz5ZFRwTvezZuebIn4v2ste0nL/sEZ7pdveDXqDyonVv/QTKT8vvIEqTQCczzsOg==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.Configuration.Json": "10.0.3",
"Microsoft.Extensions.FileProviders.Abstractions": "10.0.3",
"Microsoft.Extensions.FileProviders.Physical": "10.0.3"
}
},
"Microsoft.Extensions.DependencyInjection": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "2DLOmC0EkB2smVK8lPP1PIKEgL1arE3CMp9XSIQB/Y7ev5nnnyuM/PizKJ6QfLD08QCYoopSC9SFdbYglDomYg==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3"
}
},
"Microsoft.Extensions.Diagnostics": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "tc0R6i2T+138taoxFPQXb7Sy/4rtq4ytoJrAt4fNGs6k89mHpEhZnXUNgaFKwwb5Ud5rIUeLC6tfpsuHNwiWqg==",
"dependencies": {
"Microsoft.Extensions.Configuration": "10.0.3",
"Microsoft.Extensions.Diagnostics.Abstractions": "10.0.3",
"Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.3"
}
},
"Microsoft.Extensions.Diagnostics.Abstractions": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "mQiTzAj7PIJ2A9YXR5QhgulS1fTWhmQc3ckd1Mrf3hKW07d03fBDqx8vVaFw+cRTebDOeB6pNqdWdnRxsi1hBA==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3"
}
},
"Microsoft.Extensions.FileProviders.Abstractions": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "4TD9AXDRsipTmaemwnjt/DM5Ri0de2JzHQhvZ4woBTjUtL4XrPNsMrOk5oiLJAx1gTrE6pOIhxv+lEde5F6CZA==",
"dependencies": {
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.FileProviders.Physical": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "8qLl5LXtcj6Z8yPbHAA/a57fvvl9nUCdi59AJFuixcWM4wSuENZ8jjoRATOKs/I4vOi/bDe0d5LqGSSLE634eA==",
"dependencies": {
"Microsoft.Extensions.FileProviders.Abstractions": "10.0.3",
"Microsoft.Extensions.FileSystemGlobbing": "10.0.3",
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.FileSystemGlobbing": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "oM7pl8uJz8WRPRlh4AGQS61aeV9GOfTu89yqTiRSYyyMuCNVkbNra9zEk7ApyJ/sZrUpbjOZCRHuitCEsTWghg=="
},
"Microsoft.Extensions.Hosting.Abstractions": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "GdMpC10Jf6poxSvUJ4lgYpJ5F/kJeaAoJmrPufjBoPYyCTKKY5Dyl0rZA+LBNvFqTq1cZa/lhlptlUhNvU6xrg==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Diagnostics.Abstractions": "10.0.3",
"Microsoft.Extensions.FileProviders.Abstractions": "10.0.3",
"Microsoft.Extensions.Logging.Abstractions": "10.0.3"
}
},
"Microsoft.Extensions.Logging": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "8D9Er1cGXNjNDIB+VLBNHn386L5ls2FoiG9a6o12gyn+GG3w6jdfUhzT8dtBnKcevE7/fsVA8MS3FBgFfClFtQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "10.0.3",
"Microsoft.Extensions.Logging.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3"
}
},
"Microsoft.Extensions.Logging.Configuration": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "PBlaoYeusaxNYyN4WFjzcXWlUDSvLUPxy/e6oP1SONOOYA/oBWT2uBmFGJMV9VTtXiXXxCB39LqlYWbsWE4UKA==",
"dependencies": {
"Microsoft.Extensions.Configuration": "10.0.3",
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.Configuration.Binder": "10.0.3",
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Logging": "10.0.3",
"Microsoft.Extensions.Logging.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3",
"Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.3"
}
},
"Microsoft.Extensions.Logging.Console": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "7sRvbBH3icaV9qil8fyBKmR+yEZ0yDU6Bq/KgBwswS36164yGaxbf7Kd4hD1iHZ2GfvyoJWWqBUBm9QX/IASAQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Logging": "10.0.3",
"Microsoft.Extensions.Logging.Abstractions": "10.0.3",
"Microsoft.Extensions.Logging.Configuration": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3"
}
},
"Microsoft.Extensions.Logging.Debug": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "OoH8AcYCq74ab5XUIQc84CZk54G/cU+JztiMXgNKGkomJOeuistTMg0PWPC4VXXMSVBEGWJuMDEBttOrHyXe8w==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Logging": "10.0.3",
"Microsoft.Extensions.Logging.Abstractions": "10.0.3"
}
},
"Microsoft.Extensions.Logging.EventLog": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "1V1oRR+0DKyetPKI4POn7+jXH4kI1D69R/Rjje/4/BSkTM2iUCsRkr7Q0gDyXayhCXgPEf/P19kgwN5t0s/p8w==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Logging": "10.0.3",
"Microsoft.Extensions.Logging.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3",
"System.Diagnostics.EventLog": "10.0.3"
}
},
"Microsoft.Extensions.Logging.EventSource": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "r2hIVkSrb+f8FqcguHqlzyQ8lNGCtWsOPY9+OzJinrqdzQfszS8fXkHVcNHW4uK6WFxI2tPSiGdms2SeRJq8hg==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Logging": "10.0.3",
"Microsoft.Extensions.Logging.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3",
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "GEcpTwo7sUoLGGNTqV1FZEuL+tTD9m81NX/mh099dqGNna07/UGZShKQNZRw4hv6nlliSUwYQgSYc7OR99Jufg=="
},
"Newtonsoft.Json": {
"type": "Transitive",
"resolved": "13.0.3",
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
},
"System.Diagnostics.EventLog": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "+bZnyzt0/vt4g3QSllhsRNGTpa09p7Juy5K8spcK73cOTOefu4+HoY89hZOgIOmzB5A4hqPyEDKnzra7KKnhZw=="
},
"MSearch.Domain": {
"type": "Project",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.3, )",
"Microsoft.Extensions.Logging.Abstractions": "[10.0.3, )"
}
},
"MSearch.SearchProviders.GitHub": {
"type": "Project",
"dependencies": {
"MSearch.Domain": "[1.0.0, )",
"Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.3, )",
"Microsoft.Extensions.Http": "[10.0.3, )",
"Microsoft.Extensions.Logging.Abstractions": "[10.0.3, )",
"Microsoft.Extensions.Options": "[10.0.3, )",
"Microsoft.Extensions.Options.ConfigurationExtensions": "[10.0.3, )"
}
},
"MSearch.SearchProviders.HackerNews": {
"type": "Project",
"dependencies": {
"MSearch.Domain": "[1.0.0, )",
"Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.3, )",
"Microsoft.Extensions.Http": "[10.0.3, )"
}
},
"MSearch.SearchProviders.MicrosoftLearn": {
"type": "Project",
"dependencies": {
"MSearch.Domain": "[1.0.0, )",
"Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.3, )",
"Microsoft.Extensions.Http": "[10.0.3, )"
}
},
"MSearch.SearchProviders.MozillaDeveloperNetwork": {
"type": "Project",
"dependencies": {
"MSearch.Domain": "[1.0.0, )",
"Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.3, )"
}
},
"MSearch.SearchProviders.OpenStreetMap": {
"type": "Project",
"dependencies": {
"MSearch.Domain": "[1.0.0, )",
"Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.3, )",
"Microsoft.Extensions.Http": "[10.0.3, )"
}
},
"MSearch.SearchProviders.Reddit": {
"type": "Project",
"dependencies": {
"MSearch.Domain": "[1.0.0, )",
"Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.3, )",
"Microsoft.Extensions.Http": "[10.0.3, )"
}
},
"MSearch.SearchProviders.Spotify": {
"type": "Project",
"dependencies": {
"MSearch.Domain": "[1.0.0, )",
"Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.3, )",
"Microsoft.Extensions.Http": "[10.0.3, )",
"Microsoft.Extensions.Logging.Abstractions": "[10.0.3, )",
"Microsoft.Extensions.Options": "[10.0.3, )",
"Microsoft.Extensions.Options.ConfigurationExtensions": "[10.0.3, )",
"SpotifyAPI.Web": "[7.4.0, )"
}
},
"MSearch.SearchProviders.StackExchange": {
"type": "Project",
"dependencies": {
"MSearch.Domain": "[1.0.0, )",
"Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.3, )",
"Microsoft.Extensions.Http": "[10.0.3, )",
"Microsoft.Extensions.Options": "[10.0.3, )",
"Microsoft.Extensions.Options.ConfigurationExtensions": "[10.0.3, )"
}
},
"MSearch.SearchProviders.TheMovieDb": {
"type": "Project",
"dependencies": {
"MSearch.Domain": "[1.0.0, )",
"Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.3, )",
"Microsoft.Extensions.Http": "[10.0.3, )",
"Microsoft.Extensions.Options": "[10.0.3, )",
"Microsoft.Extensions.Options.ConfigurationExtensions": "[10.0.3, )"
}
},
"MSearch.SearchProviders.Wikipedia": {
"type": "Project",
"dependencies": {
"HtmlAgilityPack": "[1.12.4, )",
"MSearch.Domain": "[1.0.0, )",
"Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.3, )",
"Microsoft.Extensions.Http": "[10.0.3, )",
"Microsoft.Extensions.Options": "[10.0.3, )",
"Microsoft.Extensions.Options.ConfigurationExtensions": "[10.0.3, )"
}
},
"MSearch.SearchProviders.YouTube": {
"type": "Project",
"dependencies": {
"MSearch.Domain": "[1.0.0, )",
"Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.3, )",
"Microsoft.Extensions.Http": "[10.0.3, )",
"Microsoft.Extensions.Options": "[10.0.3, )",
"Microsoft.Extensions.Options.ConfigurationExtensions": "[10.0.3, )"
}
},
"HtmlAgilityPack": {
"type": "CentralTransitive",
"requested": "[1.12.4, )",
"resolved": "1.12.4",
"contentHash": "ljqvBabvFwKoLniuoQKO8b5bJfJweKLs4fUNS/V5dsvpo0A8MlJqxxn9XVmP2DaskbUXty6IYaWAi1SArGIMeQ=="
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "CentralTransitive",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "bwGMrRcAMWx2s/RDgja97p27rxSz2pEQW0+rX5cWAUWVETVJ/eyxGfjAl8vuG5a+lckWmPIE+vcuaZNVB5YDdw=="
},
"Microsoft.Extensions.Http": {
"type": "CentralTransitive",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "M5gWob3dtzlF14oto1lR1ZuSJrR0gGc+obv7zY9LGmX5y3Ndpve29MrrjqJW/m4CFud4TE/KFUuHjjtwxhCO8g==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Diagnostics": "10.0.3",
"Microsoft.Extensions.Logging": "10.0.3",
"Microsoft.Extensions.Logging.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3"
}
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "CentralTransitive",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "lxl0WLk7ROgBFAsjcOYjQ8/DVK+VMszxGBzUhgtQmAsTNldLL5pk9NG/cWTsXHq0lUhUEAtZkEE7jOGOA8bGKQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3"
}
},
"Microsoft.Extensions.Options": {
"type": "CentralTransitive",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "hU6WzGTPvPoLA2ng1ILvWQb3g0qORdlHNsxI8IcPLumJb3suimYUl+bbDzdo1V4KFsvVhnMWzysHpKbZaoDQPQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Options.ConfigurationExtensions": {
"type": "CentralTransitive",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "bn6QoBbbvwmzLIFyxrnL2/e+sqoNUOGbHyfWK9DPONMv1mDCYHm/C7MusYASM31b2lUx6OiDmonb3v+dv5t0nA==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.Configuration.Binder": "10.0.3",
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3",
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"SpotifyAPI.Web": {
"type": "CentralTransitive",
"requested": "[7.4.0, )",
"resolved": "7.4.0",
"contentHash": "1xewe5k+dy3cQJ9iIOmw4CUmBUnVZflSjcFL33636h/XuTLUnBKkKiB9dCeHJeVVtN0+on+t2Wy3haJBJMp3Vg==",
"dependencies": {
"Newtonsoft.Json": "13.0.3"
}
}
},
"net10.0/linux-x64": {
"System.Diagnostics.EventLog": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "+bZnyzt0/vt4g3QSllhsRNGTpa09p7Juy5K8spcK73cOTOefu4+HoY89hZOgIOmzB5A4hqPyEDKnzra7KKnhZw=="
}
},
"net10.0/win-arm64": {
"System.Diagnostics.EventLog": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "+bZnyzt0/vt4g3QSllhsRNGTpa09p7Juy5K8spcK73cOTOefu4+HoY89hZOgIOmzB5A4hqPyEDKnzra7KKnhZw=="
}
}
}
}
\ No newline at end of file
Directory.Build.props +12 -0
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..9089dde
@@ -0,0 +1,12 @@
<Project>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<RuntimeIdentifiers>win-arm64;linux-x64</RuntimeIdentifiers>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
<EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
</PropertyGroup>
</Project>
Directory.Packages.props +29 -0
diff --git a/Directory.Packages.props b/Directory.Packages.props
new file mode 100644
index 0000000..9f7826d
@@ -0,0 +1,29 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<GlobalPackageReference Include="CSharpier.MsBuild" Version="1.0.2" />
<PackageVersion Include="HtmlAgilityPack" Version="1.12.4" />
<PackageVersion
Include="Microsoft.Extensions.DependencyInjection.Abstractions"
Version="10.0.3"
/>
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.3" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.3" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.3.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.3" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.3" />
<PackageVersion
Include="Microsoft.Extensions.Options.ConfigurationExtensions"
Version="10.0.3"
/>
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="10.3.0" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.0" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.15.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.15.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.15.0" />
<PackageVersion Include="SpotifyAPI.Web" Version="7.4.0" />
</ItemGroup>
</Project>
Domain/Domain.csproj +10 -0
diff --git a/Domain/Domain.csproj b/Domain/Domain.csproj
new file mode 100644
index 0000000..449e286
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>MSearch.Domain</RootNamespace>
<AssemblyName>MSearch.Domain</AssemblyName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
</ItemGroup>
</Project>
Domain/ISearchProvider.cs +9 -0
diff --git a/Domain/ISearchProvider.cs b/Domain/ISearchProvider.cs
new file mode 100644
index 0000000..4db90e4
@@ -0,0 +1,9 @@
using System.Collections.Generic;
using System.Threading;
namespace MSearch.Domain;
public interface ISearchProvider
{
IAsyncEnumerable<SearchResult> Search(SearchQuery query, CancellationToken cancellationToken);
}
Domain/SearchQuery.cs +3 -0
diff --git a/Domain/SearchQuery.cs b/Domain/SearchQuery.cs
new file mode 100644
index 0000000..b0554b1
@@ -0,0 +1,3 @@
namespace MSearch.Domain;
public sealed record SearchQuery(string Term);
Domain/SearchResult.cs +5 -0
diff --git a/Domain/SearchResult.cs b/Domain/SearchResult.cs
new file mode 100644
index 0000000..ae38927
@@ -0,0 +1,5 @@
using System;
namespace MSearch.Domain;
public sealed record SearchResult(string Title, string? Summary, Uri Url);
Domain/SearchService.cs +70 -0
diff --git a/Domain/SearchService.cs b/Domain/SearchService.cs
new file mode 100644
index 0000000..fc8fb66
@@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace MSearch.Domain;
public sealed partial class SearchService(ILogger<SearchService> logger, IEnumerable<ISearchProvider> searchProviders)
{
private readonly IReadOnlyCollection<ISearchProvider> searchProviders = [.. searchProviders];
public IAsyncEnumerable<SearchResult> Search(string term, CancellationToken cancellationToken)
{
var query = new SearchQuery(term);
var channel = Channel.CreateUnbounded<SearchResult>(new() { SingleReader = true });
StartProviders(query, channel.Writer, cancellationToken);
return channel.Reader.ReadAllAsync(cancellationToken);
}
private async void StartProviders(
SearchQuery query,
ChannelWriter<SearchResult> writer,
CancellationToken cancellationToken
)
{
try
{
var searchTasks = searchProviders.Select(sp => StartProvider(query, sp, writer, cancellationToken));
await Task.WhenAll(searchTasks);
}
finally
{
var completed = writer.TryComplete();
if (!completed)
{
LogUncompletedChannel();
}
}
}
private async Task StartProvider(
SearchQuery query,
ISearchProvider provider,
ChannelWriter<SearchResult> writer,
CancellationToken cancellationToken
)
{
try
{
await foreach (var result in provider.Search(query, cancellationToken))
{
await writer.WriteAsync(result, cancellationToken);
}
}
catch (Exception ex)
{
LogSearchProviderException(ex);
}
}
[LoggerMessage(LogLevel.Error, "Failed when processing search provider results.")]
private partial void LogSearchProviderException(Exception exception);
[LoggerMessage(LogLevel.Warning, "Could not close channel after writing all search results. Leaving channel open.")]
private partial void LogUncompletedChannel();
}
Domain/ServiceCollectionExtensions.cs +9 -0
diff --git a/Domain/ServiceCollectionExtensions.cs b/Domain/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..3f4a0c8
@@ -0,0 +1,9 @@
using Microsoft.Extensions.DependencyInjection;
namespace MSearch.Domain;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddSearchService(this IServiceCollection services) =>
services.AddTransient<SearchService>();
}
Domain/packages.lock.json +30 -0
diff --git a/Domain/packages.lock.json b/Domain/packages.lock.json
new file mode 100644
index 0000000..4455173
@@ -0,0 +1,30 @@
{
"version": 2,
"dependencies": {
"net10.0": {
"CSharpier.MsBuild": {
"type": "Direct",
"requested": "[1.0.2, )",
"resolved": "1.0.2",
"contentHash": "Y98ba4yeefqrJY+MbCXhvb8x4/mCy8zHeuu/dYKwaM4p0Yu2LToI5ffTg/ErcpPuLnwJBNE229STdhf/dlikIw=="
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "bwGMrRcAMWx2s/RDgja97p27rxSz2pEQW0+rX5cWAUWVETVJ/eyxGfjAl8vuG5a+lckWmPIE+vcuaZNVB5YDdw=="
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "lxl0WLk7ROgBFAsjcOYjQ8/DVK+VMszxGBzUhgtQmAsTNldLL5pk9NG/cWTsXHq0lUhUEAtZkEE7jOGOA8bGKQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3"
}
}
},
"net10.0/linux-x64": {},
"net10.0/win-arm64": {}
}
}
\ No newline at end of file
MSearch.slnx +19 -0
diff --git a/MSearch.slnx b/MSearch.slnx
new file mode 100644
index 0000000..86d1c40
@@ -0,0 +1,19 @@
<Solution>
<Project Path="Console/Console.csproj" />
<Project Path="Domain/Domain.csproj" />
<Project Path="ServiceDefaults/ServiceDefaults.csproj" />
<Project Path="Web/Web.csproj" />
<Folder Name="/SearchProviders/">
<Project Path="SearchProviders/GitHub/GitHub.csproj" />
<Project Path="SearchProviders/HackerNews/HackerNews.csproj" />
<Project Path="SearchProviders/MicrosoftLearn/MicrosoftLearn.csproj" />
<Project Path="SearchProviders/MozillaDeveloperNetwork/MozillaDeveloperNetwork.csproj" />
<Project Path="SearchProviders/OpenStreetMap/OpenStreetMap.csproj" />
<Project Path="SearchProviders/Reddit/Reddit.csproj" />
<Project Path="SearchProviders/Spotify/Spotify.csproj" />
<Project Path="SearchProviders/StackExchange/StackExchange.csproj" />
<Project Path="SearchProviders/TheMovieDb/TheMovieDb.csproj" />
<Project Path="SearchProviders/Wikipedia/Wikipedia.csproj" />
<Project Path="SearchProviders/YouTube/YouTube.csproj" />
</Folder>
</Solution>
SearchProviders/GitHub/GitHub.csproj +16 -0
diff --git a/SearchProviders/GitHub/GitHub.csproj b/SearchProviders/GitHub/GitHub.csproj
new file mode 100644
index 0000000..504baf3
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>MSearch.SearchProviders.GitHub</RootNamespace>
<AssemblyName>MSearch.SearchProviders.GitHub</AssemblyName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Domain\Domain.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Http" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Options" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" />
</ItemGroup>
</Project>
SearchProviders/GitHub/GitHubItem.cs +11 -0
diff --git a/SearchProviders/GitHub/GitHubItem.cs b/SearchProviders/GitHub/GitHubItem.cs
new file mode 100644
index 0000000..138a2c5
@@ -0,0 +1,11 @@
using System.Text.Json.Serialization;
namespace MSearch.SearchProviders.GitHub;
internal record GitHubItem(
[property: JsonPropertyName("name")] string? Name,
[property: JsonPropertyName("description")] string? Description,
[property: JsonPropertyName("title")] string? Title,
[property: JsonPropertyName("body")] string? Body,
[property: JsonPropertyName("html_url")] string Url
);
SearchProviders/GitHub/GitHubJsonSerializerContext.cs +6 -0
diff --git a/SearchProviders/GitHub/GitHubJsonSerializerContext.cs b/SearchProviders/GitHub/GitHubJsonSerializerContext.cs
new file mode 100644
index 0000000..c22a474
@@ -0,0 +1,6 @@
using System.Text.Json.Serialization;
namespace MSearch.SearchProviders.GitHub;
[JsonSerializable(typeof(GithubResponse))]
internal sealed partial class GitHubJsonSerializerContext : JsonSerializerContext;
SearchProviders/GitHub/GitHubSearchProvider.cs +48 -0
diff --git a/SearchProviders/GitHub/GitHubSearchProvider.cs b/SearchProviders/GitHub/GitHubSearchProvider.cs
new file mode 100644
index 0000000..f61af90
@@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Runtime.CompilerServices;
using System.Threading;
using MSearch.Domain;
namespace MSearch.SearchProviders.GitHub;
internal sealed class GitHubSearchProvider(HttpClient httpClient, string type) : ISearchProvider
{
public async IAsyncEnumerable<SearchResult> Search(
SearchQuery query,
[EnumeratorCancellation] CancellationToken cancellationToken
)
{
var response = await httpClient.GetAsync(
$"search/{type}?q={Uri.EscapeDataString(query.Term)}",
cancellationToken
);
if (response.StatusCode is HttpStatusCode.NotFound)
{
yield break;
}
var data = await response.Content.ReadFromJsonAsync(
GitHubJsonSerializerContext.Default.GithubResponse,
cancellationToken
);
if (data is null)
{
yield break;
}
foreach (var item in data.Items)
{
if (Map(item) is { } result)
{
yield return result;
}
}
}
private static SearchResult? Map(GitHubItem item) =>
(item.Name ?? item.Title) is string title && (item.Description ?? item.Body) is string summary
? new(title, summary, new(item.Url))
: null;
}
SearchProviders/GitHub/GithubResponse.cs +6 -0
diff --git a/SearchProviders/GitHub/GithubResponse.cs b/SearchProviders/GitHub/GithubResponse.cs
new file mode 100644
index 0000000..dd76a6c
@@ -0,0 +1,6 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace MSearch.SearchProviders.GitHub;
internal sealed record GithubResponse([property: JsonPropertyName("items")] IReadOnlyCollection<GitHubItem> Items);
SearchProviders/GitHub/ServiceCollectionExtensions.cs +33 -0
diff --git a/SearchProviders/GitHub/ServiceCollectionExtensions.cs b/SearchProviders/GitHub/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..6692429
@@ -0,0 +1,33 @@
using System.Net.Http;
using Microsoft.Extensions.DependencyInjection;
using MSearch.Domain;
namespace MSearch.SearchProviders.GitHub;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddGitHubSearchProvider(this IServiceCollection services)
{
services.AddHttpClient(
nameof(GitHubSearchProvider),
httpClient =>
{
httpClient.BaseAddress = new("https://api.github.com/");
httpClient.DefaultRequestHeaders.Accept.ParseAdd("application/vnd.github+json");
httpClient.DefaultRequestHeaders.Add("X-GitHub-Api-Version", "2022-11-28");
}
);
services.AddTransient<ISearchProvider>(sp => new GitHubSearchProvider(
sp.GetRequiredService<IHttpClientFactory>().CreateClient(nameof(GitHubSearchProvider)),
"issues"
));
services.AddTransient<ISearchProvider>(sp => new GitHubSearchProvider(
sp.GetRequiredService<IHttpClientFactory>().CreateClient(nameof(GitHubSearchProvider)),
"repositories"
));
return services;
}
}
SearchProviders/GitHub/packages.lock.json +142 -0
diff --git a/SearchProviders/GitHub/packages.lock.json b/SearchProviders/GitHub/packages.lock.json
new file mode 100644
index 0000000..c886c2d
@@ -0,0 +1,142 @@
{
"version": 2,
"dependencies": {
"net10.0": {
"CSharpier.MsBuild": {
"type": "Direct",
"requested": "[1.0.2, )",
"resolved": "1.0.2",
"contentHash": "Y98ba4yeefqrJY+MbCXhvb8x4/mCy8zHeuu/dYKwaM4p0Yu2LToI5ffTg/ErcpPuLnwJBNE229STdhf/dlikIw=="
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "bwGMrRcAMWx2s/RDgja97p27rxSz2pEQW0+rX5cWAUWVETVJ/eyxGfjAl8vuG5a+lckWmPIE+vcuaZNVB5YDdw=="
},
"Microsoft.Extensions.Http": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "M5gWob3dtzlF14oto1lR1ZuSJrR0gGc+obv7zY9LGmX5y3Ndpve29MrrjqJW/m4CFud4TE/KFUuHjjtwxhCO8g==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Diagnostics": "10.0.3",
"Microsoft.Extensions.Logging": "10.0.3",
"Microsoft.Extensions.Logging.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3"
}
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "lxl0WLk7ROgBFAsjcOYjQ8/DVK+VMszxGBzUhgtQmAsTNldLL5pk9NG/cWTsXHq0lUhUEAtZkEE7jOGOA8bGKQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3"
}
},
"Microsoft.Extensions.Options": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "hU6WzGTPvPoLA2ng1ILvWQb3g0qORdlHNsxI8IcPLumJb3suimYUl+bbDzdo1V4KFsvVhnMWzysHpKbZaoDQPQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Options.ConfigurationExtensions": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "bn6QoBbbvwmzLIFyxrnL2/e+sqoNUOGbHyfWK9DPONMv1mDCYHm/C7MusYASM31b2lUx6OiDmonb3v+dv5t0nA==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.Configuration.Binder": "10.0.3",
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3",
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Configuration": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "H1Cjv2xmm7O3iAGmFTcnSM0ZhLQ/7SqefmAvSJoT1PbXoxeYc2fo0mCLn2JlVbr9E6YpoU9q/o0fI9neDJB0xQ==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "xVDHL0+SIgemfh95fTO9cGLe17TWv/ZP0n7m01z8X6pzt2DmQpucioWR/mYZA1sRlkWnkXzfl0JweLNWmE9WMg==",
"dependencies": {
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Configuration.Binder": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "759UhpKaR5Jsll9kXpkft4z/7tpeF7Dw2rTY/9f9JchaSQTpRFNIPkZFZvoo7fFpbjUaqtDlO5aiGpmQrp/EUA==",
"dependencies": {
"Microsoft.Extensions.Configuration": "10.0.3",
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3"
}
},
"Microsoft.Extensions.DependencyInjection": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "2DLOmC0EkB2smVK8lPP1PIKEgL1arE3CMp9XSIQB/Y7ev5nnnyuM/PizKJ6QfLD08QCYoopSC9SFdbYglDomYg==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3"
}
},
"Microsoft.Extensions.Diagnostics": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "tc0R6i2T+138taoxFPQXb7Sy/4rtq4ytoJrAt4fNGs6k89mHpEhZnXUNgaFKwwb5Ud5rIUeLC6tfpsuHNwiWqg==",
"dependencies": {
"Microsoft.Extensions.Configuration": "10.0.3",
"Microsoft.Extensions.Diagnostics.Abstractions": "10.0.3",
"Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.3"
}
},
"Microsoft.Extensions.Diagnostics.Abstractions": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "mQiTzAj7PIJ2A9YXR5QhgulS1fTWhmQc3ckd1Mrf3hKW07d03fBDqx8vVaFw+cRTebDOeB6pNqdWdnRxsi1hBA==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3"
}
},
"Microsoft.Extensions.Logging": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "8D9Er1cGXNjNDIB+VLBNHn386L5ls2FoiG9a6o12gyn+GG3w6jdfUhzT8dtBnKcevE7/fsVA8MS3FBgFfClFtQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "10.0.3",
"Microsoft.Extensions.Logging.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3"
}
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "GEcpTwo7sUoLGGNTqV1FZEuL+tTD9m81NX/mh099dqGNna07/UGZShKQNZRw4hv6nlliSUwYQgSYc7OR99Jufg=="
},
"MSearch.Domain": {
"type": "Project",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.3, )",
"Microsoft.Extensions.Logging.Abstractions": "[10.0.3, )"
}
}
},
"net10.0/linux-x64": {},
"net10.0/win-arm64": {}
}
}
\ No newline at end of file
SearchProviders/HackerNews/HackerNews.csproj +13 -0
diff --git a/SearchProviders/HackerNews/HackerNews.csproj b/SearchProviders/HackerNews/HackerNews.csproj
new file mode 100644
index 0000000..9544914
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>MSearch.SearchProviders.HackerNews</RootNamespace>
<AssemblyName>MSearch.SearchProviders.HackerNews</AssemblyName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Domain\Domain.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Http" />
</ItemGroup>
</Project>
SearchProviders/HackerNews/HackerNewsHit.cs +8 -0
diff --git a/SearchProviders/HackerNews/HackerNewsHit.cs b/SearchProviders/HackerNews/HackerNewsHit.cs
new file mode 100644
index 0000000..d2a2698
@@ -0,0 +1,8 @@
using System.Text.Json.Serialization;
namespace MSearch.SearchProviders.HackerNews;
internal sealed record HackerNewsHit(
[property: JsonPropertyName("title")] string Title,
[property: JsonPropertyName("objectID")] string Id
);
SearchProviders/HackerNews/HackerNewsJsonSerializerContext.cs +6 -0
diff --git a/SearchProviders/HackerNews/HackerNewsJsonSerializerContext.cs b/SearchProviders/HackerNews/HackerNewsJsonSerializerContext.cs
new file mode 100644
index 0000000..95b10a1
@@ -0,0 +1,6 @@
using System.Text.Json.Serialization;
namespace MSearch.SearchProviders.HackerNews;
[JsonSerializable(typeof(HackerNewsSearchResponse))]
internal sealed partial class HackerNewsJsonSerializerContext : JsonSerializerContext;
SearchProviders/HackerNews/HackerNewsSearchProvider.cs +35 -0
diff --git a/SearchProviders/HackerNews/HackerNewsSearchProvider.cs b/SearchProviders/HackerNews/HackerNewsSearchProvider.cs
new file mode 100644
index 0000000..49cac68
@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Runtime.CompilerServices;
using System.Threading;
using MSearch.Domain;
namespace MSearch.SearchProviders.HackerNews;
internal sealed class HackerNewsSearchProvider(HttpClient httpClient) : ISearchProvider
{
public async IAsyncEnumerable<SearchResult> Search(
SearchQuery query,
[EnumeratorCancellation] CancellationToken cancellationToken
)
{
var response = await httpClient.GetFromJsonAsync(
$"https://hn.algolia.com/api/v1/search?query={Uri.EscapeDataString(query.Term)}",
HackerNewsJsonSerializerContext.Default.HackerNewsSearchResponse,
cancellationToken
);
if (response is null)
{
yield break;
}
foreach (var result in response.Hits)
{
yield return Map(result);
}
}
private static SearchResult Map(HackerNewsHit hit) =>
new(hit.Title, Summary: null, new($"https://news.ycombinator.com/item?id={hit.Id}"));
}
SearchProviders/HackerNews/HackerNewsSearchResponse.cs +8 -0
diff --git a/SearchProviders/HackerNews/HackerNewsSearchResponse.cs b/SearchProviders/HackerNews/HackerNewsSearchResponse.cs
new file mode 100644
index 0000000..a3a6bee
@@ -0,0 +1,8 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace MSearch.SearchProviders.HackerNews;
internal sealed record HackerNewsSearchResponse(
[property: JsonPropertyName("hits")] IReadOnlyCollection<HackerNewsHit> Hits
);
SearchProviders/HackerNews/ServiceCollectionExtensions.cs +13 -0
diff --git a/SearchProviders/HackerNews/ServiceCollectionExtensions.cs b/SearchProviders/HackerNews/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..e8fe654
@@ -0,0 +1,13 @@
using Microsoft.Extensions.DependencyInjection;
using MSearch.Domain;
namespace MSearch.SearchProviders.HackerNews;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddHackerNewsSearchProvider(this IServiceCollection services)
{
services.AddHttpClient<ISearchProvider, HackerNewsSearchProvider>(nameof(HackerNewsSearchProvider));
return services;
}
}
SearchProviders/HackerNews/packages.lock.json +142 -0
diff --git a/SearchProviders/HackerNews/packages.lock.json b/SearchProviders/HackerNews/packages.lock.json
new file mode 100644
index 0000000..1b5804a
@@ -0,0 +1,142 @@
{
"version": 2,
"dependencies": {
"net10.0": {
"CSharpier.MsBuild": {
"type": "Direct",
"requested": "[1.0.2, )",
"resolved": "1.0.2",
"contentHash": "Y98ba4yeefqrJY+MbCXhvb8x4/mCy8zHeuu/dYKwaM4p0Yu2LToI5ffTg/ErcpPuLnwJBNE229STdhf/dlikIw=="
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "bwGMrRcAMWx2s/RDgja97p27rxSz2pEQW0+rX5cWAUWVETVJ/eyxGfjAl8vuG5a+lckWmPIE+vcuaZNVB5YDdw=="
},
"Microsoft.Extensions.Http": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "M5gWob3dtzlF14oto1lR1ZuSJrR0gGc+obv7zY9LGmX5y3Ndpve29MrrjqJW/m4CFud4TE/KFUuHjjtwxhCO8g==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Diagnostics": "10.0.3",
"Microsoft.Extensions.Logging": "10.0.3",
"Microsoft.Extensions.Logging.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3"
}
},
"Microsoft.Extensions.Configuration": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "H1Cjv2xmm7O3iAGmFTcnSM0ZhLQ/7SqefmAvSJoT1PbXoxeYc2fo0mCLn2JlVbr9E6YpoU9q/o0fI9neDJB0xQ==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "xVDHL0+SIgemfh95fTO9cGLe17TWv/ZP0n7m01z8X6pzt2DmQpucioWR/mYZA1sRlkWnkXzfl0JweLNWmE9WMg==",
"dependencies": {
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Configuration.Binder": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "759UhpKaR5Jsll9kXpkft4z/7tpeF7Dw2rTY/9f9JchaSQTpRFNIPkZFZvoo7fFpbjUaqtDlO5aiGpmQrp/EUA==",
"dependencies": {
"Microsoft.Extensions.Configuration": "10.0.3",
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3"
}
},
"Microsoft.Extensions.DependencyInjection": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "2DLOmC0EkB2smVK8lPP1PIKEgL1arE3CMp9XSIQB/Y7ev5nnnyuM/PizKJ6QfLD08QCYoopSC9SFdbYglDomYg==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3"
}
},
"Microsoft.Extensions.Diagnostics": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "tc0R6i2T+138taoxFPQXb7Sy/4rtq4ytoJrAt4fNGs6k89mHpEhZnXUNgaFKwwb5Ud5rIUeLC6tfpsuHNwiWqg==",
"dependencies": {
"Microsoft.Extensions.Configuration": "10.0.3",
"Microsoft.Extensions.Diagnostics.Abstractions": "10.0.3",
"Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.3"
}
},
"Microsoft.Extensions.Diagnostics.Abstractions": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "mQiTzAj7PIJ2A9YXR5QhgulS1fTWhmQc3ckd1Mrf3hKW07d03fBDqx8vVaFw+cRTebDOeB6pNqdWdnRxsi1hBA==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3"
}
},
"Microsoft.Extensions.Logging": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "8D9Er1cGXNjNDIB+VLBNHn386L5ls2FoiG9a6o12gyn+GG3w6jdfUhzT8dtBnKcevE7/fsVA8MS3FBgFfClFtQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "10.0.3",
"Microsoft.Extensions.Logging.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3"
}
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "GEcpTwo7sUoLGGNTqV1FZEuL+tTD9m81NX/mh099dqGNna07/UGZShKQNZRw4hv6nlliSUwYQgSYc7OR99Jufg=="
},
"MSearch.Domain": {
"type": "Project",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.3, )",
"Microsoft.Extensions.Logging.Abstractions": "[10.0.3, )"
}
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "CentralTransitive",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "lxl0WLk7ROgBFAsjcOYjQ8/DVK+VMszxGBzUhgtQmAsTNldLL5pk9NG/cWTsXHq0lUhUEAtZkEE7jOGOA8bGKQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3"
}
},
"Microsoft.Extensions.Options": {
"type": "CentralTransitive",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "hU6WzGTPvPoLA2ng1ILvWQb3g0qORdlHNsxI8IcPLumJb3suimYUl+bbDzdo1V4KFsvVhnMWzysHpKbZaoDQPQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Options.ConfigurationExtensions": {
"type": "CentralTransitive",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "bn6QoBbbvwmzLIFyxrnL2/e+sqoNUOGbHyfWK9DPONMv1mDCYHm/C7MusYASM31b2lUx6OiDmonb3v+dv5t0nA==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.Configuration.Binder": "10.0.3",
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3",
"Microsoft.Extensions.Primitives": "10.0.3"
}
}
},
"net10.0/linux-x64": {},
"net10.0/win-arm64": {}
}
}
\ No newline at end of file
SearchProviders/MicrosoftLearn/MicrosoftLearn.csproj +13 -0
diff --git a/SearchProviders/MicrosoftLearn/MicrosoftLearn.csproj b/SearchProviders/MicrosoftLearn/MicrosoftLearn.csproj
new file mode 100644
index 0000000..81fd8e7
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>MSearch.SearchProviders.MicrosoftLearn</RootNamespace>
<AssemblyName>MSearch.SearchProviders.MicrosoftLearn</AssemblyName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Domain\Domain.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Http" />
</ItemGroup>
</Project>
SearchProviders/MicrosoftLearn/MicrosoftLearnJsonSerializerContext.cs +6 -0
diff --git a/SearchProviders/MicrosoftLearn/MicrosoftLearnJsonSerializerContext.cs b/SearchProviders/MicrosoftLearn/MicrosoftLearnJsonSerializerContext.cs
new file mode 100644
index 0000000..2c7d38f
@@ -0,0 +1,6 @@
using System.Text.Json.Serialization;
namespace MSearch.SearchProviders.MicrosoftLearn;
[JsonSerializable(typeof(MicrosoftLearnSearchResponse))]
internal sealed partial class MicrosoftLearnJsonSerializerContext : JsonSerializerContext;
SearchProviders/MicrosoftLearn/MicrosoftLearnSearchProvider.cs +35 -0
diff --git a/SearchProviders/MicrosoftLearn/MicrosoftLearnSearchProvider.cs b/SearchProviders/MicrosoftLearn/MicrosoftLearnSearchProvider.cs
new file mode 100644
index 0000000..94af12c
@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Runtime.CompilerServices;
using System.Threading;
using MSearch.Domain;
namespace MSearch.SearchProviders.MicrosoftLearn;
internal sealed class MicrosoftLearnSearchProvider(HttpClient httpClient) : ISearchProvider
{
public async IAsyncEnumerable<SearchResult> Search(
SearchQuery query,
[EnumeratorCancellation] CancellationToken cancellationToken
)
{
var response = await httpClient.GetFromJsonAsync(
$"https://learn.microsoft.com/api/search?locale=en-us%24top=10&%24filter=category%20eq%20%27Reference%27%20or%20category%20eq%20%27Documentation%27&search={Uri.EscapeDataString(query.Term)}",
MicrosoftLearnJsonSerializerContext.Default.MicrosoftLearnSearchResponse,
cancellationToken
);
if (response is null)
{
yield break;
}
foreach (var result in response.Results)
{
yield return Map(result);
}
}
private static SearchResult Map(MicrosoftLearnSearchResult result) =>
new(result.Title, result.Description, new(result.Url));
}
SearchProviders/MicrosoftLearn/MicrosoftLearnSearchResponse.cs +8 -0
diff --git a/SearchProviders/MicrosoftLearn/MicrosoftLearnSearchResponse.cs b/SearchProviders/MicrosoftLearn/MicrosoftLearnSearchResponse.cs
new file mode 100644
index 0000000..132458d
@@ -0,0 +1,8 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace MSearch.SearchProviders.MicrosoftLearn;
internal sealed record MicrosoftLearnSearchResponse(
[property: JsonPropertyName("results")] IReadOnlyCollection<MicrosoftLearnSearchResult> Results
);
SearchProviders/MicrosoftLearn/MicrosoftLearnSearchResult.cs +9 -0
diff --git a/SearchProviders/MicrosoftLearn/MicrosoftLearnSearchResult.cs b/SearchProviders/MicrosoftLearn/MicrosoftLearnSearchResult.cs
new file mode 100644
index 0000000..f261a57
@@ -0,0 +1,9 @@
using System.Text.Json.Serialization;
namespace MSearch.SearchProviders.MicrosoftLearn;
internal sealed record MicrosoftLearnSearchResult(
[property: JsonPropertyName("title")] string Title,
[property: JsonPropertyName("description")] string Description,
[property: JsonPropertyName("url")] string Url
);
SearchProviders/MicrosoftLearn/ServiceCollectionExtensions.cs +13 -0
diff --git a/SearchProviders/MicrosoftLearn/ServiceCollectionExtensions.cs b/SearchProviders/MicrosoftLearn/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..0c10c3b
@@ -0,0 +1,13 @@
using Microsoft.Extensions.DependencyInjection;
using MSearch.Domain;
namespace MSearch.SearchProviders.MicrosoftLearn;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddMicrosoftLearnSearchProvider(this IServiceCollection services)
{
services.AddHttpClient<ISearchProvider, MicrosoftLearnSearchProvider>(nameof(MicrosoftLearnSearchProvider));
return services;
}
}
SearchProviders/MicrosoftLearn/packages.lock.json +142 -0
diff --git a/SearchProviders/MicrosoftLearn/packages.lock.json b/SearchProviders/MicrosoftLearn/packages.lock.json
new file mode 100644
index 0000000..1b5804a
@@ -0,0 +1,142 @@
{
"version": 2,
"dependencies": {
"net10.0": {
"CSharpier.MsBuild": {
"type": "Direct",
"requested": "[1.0.2, )",
"resolved": "1.0.2",
"contentHash": "Y98ba4yeefqrJY+MbCXhvb8x4/mCy8zHeuu/dYKwaM4p0Yu2LToI5ffTg/ErcpPuLnwJBNE229STdhf/dlikIw=="
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "bwGMrRcAMWx2s/RDgja97p27rxSz2pEQW0+rX5cWAUWVETVJ/eyxGfjAl8vuG5a+lckWmPIE+vcuaZNVB5YDdw=="
},
"Microsoft.Extensions.Http": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "M5gWob3dtzlF14oto1lR1ZuSJrR0gGc+obv7zY9LGmX5y3Ndpve29MrrjqJW/m4CFud4TE/KFUuHjjtwxhCO8g==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Diagnostics": "10.0.3",
"Microsoft.Extensions.Logging": "10.0.3",
"Microsoft.Extensions.Logging.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3"
}
},
"Microsoft.Extensions.Configuration": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "H1Cjv2xmm7O3iAGmFTcnSM0ZhLQ/7SqefmAvSJoT1PbXoxeYc2fo0mCLn2JlVbr9E6YpoU9q/o0fI9neDJB0xQ==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "xVDHL0+SIgemfh95fTO9cGLe17TWv/ZP0n7m01z8X6pzt2DmQpucioWR/mYZA1sRlkWnkXzfl0JweLNWmE9WMg==",
"dependencies": {
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Configuration.Binder": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "759UhpKaR5Jsll9kXpkft4z/7tpeF7Dw2rTY/9f9JchaSQTpRFNIPkZFZvoo7fFpbjUaqtDlO5aiGpmQrp/EUA==",
"dependencies": {
"Microsoft.Extensions.Configuration": "10.0.3",
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3"
}
},
"Microsoft.Extensions.DependencyInjection": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "2DLOmC0EkB2smVK8lPP1PIKEgL1arE3CMp9XSIQB/Y7ev5nnnyuM/PizKJ6QfLD08QCYoopSC9SFdbYglDomYg==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3"
}
},
"Microsoft.Extensions.Diagnostics": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "tc0R6i2T+138taoxFPQXb7Sy/4rtq4ytoJrAt4fNGs6k89mHpEhZnXUNgaFKwwb5Ud5rIUeLC6tfpsuHNwiWqg==",
"dependencies": {
"Microsoft.Extensions.Configuration": "10.0.3",
"Microsoft.Extensions.Diagnostics.Abstractions": "10.0.3",
"Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.3"
}
},
"Microsoft.Extensions.Diagnostics.Abstractions": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "mQiTzAj7PIJ2A9YXR5QhgulS1fTWhmQc3ckd1Mrf3hKW07d03fBDqx8vVaFw+cRTebDOeB6pNqdWdnRxsi1hBA==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3"
}
},
"Microsoft.Extensions.Logging": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "8D9Er1cGXNjNDIB+VLBNHn386L5ls2FoiG9a6o12gyn+GG3w6jdfUhzT8dtBnKcevE7/fsVA8MS3FBgFfClFtQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "10.0.3",
"Microsoft.Extensions.Logging.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3"
}
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "GEcpTwo7sUoLGGNTqV1FZEuL+tTD9m81NX/mh099dqGNna07/UGZShKQNZRw4hv6nlliSUwYQgSYc7OR99Jufg=="
},
"MSearch.Domain": {
"type": "Project",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.3, )",
"Microsoft.Extensions.Logging.Abstractions": "[10.0.3, )"
}
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "CentralTransitive",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "lxl0WLk7ROgBFAsjcOYjQ8/DVK+VMszxGBzUhgtQmAsTNldLL5pk9NG/cWTsXHq0lUhUEAtZkEE7jOGOA8bGKQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3"
}
},
"Microsoft.Extensions.Options": {
"type": "CentralTransitive",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "hU6WzGTPvPoLA2ng1ILvWQb3g0qORdlHNsxI8IcPLumJb3suimYUl+bbDzdo1V4KFsvVhnMWzysHpKbZaoDQPQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Options.ConfigurationExtensions": {
"type": "CentralTransitive",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "bn6QoBbbvwmzLIFyxrnL2/e+sqoNUOGbHyfWK9DPONMv1mDCYHm/C7MusYASM31b2lUx6OiDmonb3v+dv5t0nA==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.Configuration.Binder": "10.0.3",
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3",
"Microsoft.Extensions.Primitives": "10.0.3"
}
}
},
"net10.0/linux-x64": {},
"net10.0/win-arm64": {}
}
}
\ No newline at end of file
SearchProviders/MozillaDeveloperNetwork/MozillaDeveloperNetwork.csproj +12 -0
diff --git a/SearchProviders/MozillaDeveloperNetwork/MozillaDeveloperNetwork.csproj b/SearchProviders/MozillaDeveloperNetwork/MozillaDeveloperNetwork.csproj
new file mode 100644
index 0000000..19a4781
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>MSearch.SearchProviders.MozillaDeveloperNetwork</RootNamespace>
<AssemblyName>MSearch.SearchProviders.MozillaDeveloperNetwork</AssemblyName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Domain\Domain.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
</ItemGroup>
</Project>
SearchProviders/MozillaDeveloperNetwork/MozillaDeveloperNetworkJsonSerializerContext.cs +6 -0
diff --git a/SearchProviders/MozillaDeveloperNetwork/MozillaDeveloperNetworkJsonSerializerContext.cs b/SearchProviders/MozillaDeveloperNetwork/MozillaDeveloperNetworkJsonSerializerContext.cs
new file mode 100644
index 0000000..2c4dd92
@@ -0,0 +1,6 @@
using System.Text.Json.Serialization;
namespace MSearch.SearchProviders.MozillaDeveloperNetwork;
[JsonSerializable(typeof(MozillaDeveloperNetworkSearchResponse))]
internal sealed partial class MozillaDeveloperNetworkJsonSerializerContext : JsonSerializerContext;
SearchProviders/MozillaDeveloperNetwork/MozillaDeveloperNetworkSearchProvider.cs +35 -0
diff --git a/SearchProviders/MozillaDeveloperNetwork/MozillaDeveloperNetworkSearchProvider.cs b/SearchProviders/MozillaDeveloperNetwork/MozillaDeveloperNetworkSearchProvider.cs
new file mode 100644
index 0000000..59211c7
@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Runtime.CompilerServices;
using System.Threading;
using MSearch.Domain;
namespace MSearch.SearchProviders.MozillaDeveloperNetwork;
internal sealed class MozillaDeveloperNetworkSearchProvider(HttpClient httpClient) : ISearchProvider
{
public async IAsyncEnumerable<SearchResult> Search(
SearchQuery query,
[EnumeratorCancellation] CancellationToken cancellationToken
)
{
var response = await httpClient.GetFromJsonAsync(
$"https://developer.mozilla.org/api/v1/search?q={Uri.EscapeDataString(query.Term)}",
MozillaDeveloperNetworkJsonSerializerContext.Default.MozillaDeveloperNetworkSearchResponse,
cancellationToken
);
if (response is null)
{
yield break;
}
foreach (var result in response.Documents)
{
yield return Map(result);
}
}
private static SearchResult Map(MozillaDeveloperNetworkSearchResult result) =>
new(result.Title, result.Title, new(result.Url));
}
SearchProviders/MozillaDeveloperNetwork/MozillaDeveloperNetworkSearchResponse.cs +8 -0
diff --git a/SearchProviders/MozillaDeveloperNetwork/MozillaDeveloperNetworkSearchResponse.cs b/SearchProviders/MozillaDeveloperNetwork/MozillaDeveloperNetworkSearchResponse.cs
new file mode 100644
index 0000000..628caa8
@@ -0,0 +1,8 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace MSearch.SearchProviders.MozillaDeveloperNetwork;
internal sealed record MozillaDeveloperNetworkSearchResponse(
[property: JsonPropertyName("documents")] IReadOnlyCollection<MozillaDeveloperNetworkSearchResult> Documents
);
SearchProviders/MozillaDeveloperNetwork/MozillaDeveloperNetworkSearchResult.cs +9 -0
diff --git a/SearchProviders/MozillaDeveloperNetwork/MozillaDeveloperNetworkSearchResult.cs b/SearchProviders/MozillaDeveloperNetwork/MozillaDeveloperNetworkSearchResult.cs
new file mode 100644
index 0000000..003ae9f
@@ -0,0 +1,9 @@
using System.Text.Json.Serialization;
namespace MSearch.SearchProviders.MozillaDeveloperNetwork;
internal sealed record MozillaDeveloperNetworkSearchResult(
[property: JsonPropertyName("title")] string Title,
[property: JsonPropertyName("summary")] string Summary,
[property: JsonPropertyName("mdn_url")] string Url
);
SearchProviders/MozillaDeveloperNetwork/ServiceCollectionExtensions.cs +10 -0
diff --git a/SearchProviders/MozillaDeveloperNetwork/ServiceCollectionExtensions.cs b/SearchProviders/MozillaDeveloperNetwork/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..1a6a0ce
@@ -0,0 +1,10 @@
using Microsoft.Extensions.DependencyInjection;
using MSearch.Domain;
namespace MSearch.SearchProviders.MozillaDeveloperNetwork;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddMozillaDeveloperNetworkSearchProvider(this IServiceCollection services) =>
services.AddTransient<ISearchProvider, MozillaDeveloperNetworkSearchProvider>();
}
SearchProviders/MozillaDeveloperNetwork/packages.lock.json +37 -0
diff --git a/SearchProviders/MozillaDeveloperNetwork/packages.lock.json b/SearchProviders/MozillaDeveloperNetwork/packages.lock.json
new file mode 100644
index 0000000..62538d4
@@ -0,0 +1,37 @@
{
"version": 2,
"dependencies": {
"net10.0": {
"CSharpier.MsBuild": {
"type": "Direct",
"requested": "[1.0.2, )",
"resolved": "1.0.2",
"contentHash": "Y98ba4yeefqrJY+MbCXhvb8x4/mCy8zHeuu/dYKwaM4p0Yu2LToI5ffTg/ErcpPuLnwJBNE229STdhf/dlikIw=="
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "bwGMrRcAMWx2s/RDgja97p27rxSz2pEQW0+rX5cWAUWVETVJ/eyxGfjAl8vuG5a+lckWmPIE+vcuaZNVB5YDdw=="
},
"MSearch.Domain": {
"type": "Project",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.3, )",
"Microsoft.Extensions.Logging.Abstractions": "[10.0.3, )"
}
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "CentralTransitive",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "lxl0WLk7ROgBFAsjcOYjQ8/DVK+VMszxGBzUhgtQmAsTNldLL5pk9NG/cWTsXHq0lUhUEAtZkEE7jOGOA8bGKQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3"
}
}
},
"net10.0/linux-x64": {},
"net10.0/win-arm64": {}
}
}
\ No newline at end of file
SearchProviders/OpenStreetMap/OpenStreetMap.csproj +13 -0
diff --git a/SearchProviders/OpenStreetMap/OpenStreetMap.csproj b/SearchProviders/OpenStreetMap/OpenStreetMap.csproj
new file mode 100644
index 0000000..8a8d60a
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>MSearch.SearchProviders.OpenStreetMap</RootNamespace>
<AssemblyName>MSearch.SearchProviders.OpenStreetMap</AssemblyName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Domain\Domain.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Http" />
</ItemGroup>
</Project>
SearchProviders/OpenStreetMap/OpenStreetMapItem.cs +10 -0
diff --git a/SearchProviders/OpenStreetMap/OpenStreetMapItem.cs b/SearchProviders/OpenStreetMap/OpenStreetMapItem.cs
new file mode 100644
index 0000000..ade456e
@@ -0,0 +1,10 @@
using System.Text.Json.Serialization;
namespace MSearch.SearchProviders.OpenStreetMap;
internal sealed record OpenStreetMapItem(
[property: JsonPropertyName("name")] string Name,
[property: JsonPropertyName("display_name")] string DisplayName,
[property: JsonPropertyName("osm_type")] string OmsType,
[property: JsonPropertyName("osm_id")] long OmsId
);
SearchProviders/OpenStreetMap/OpenStreetMapJsonSerializerContext.cs +6 -0
diff --git a/SearchProviders/OpenStreetMap/OpenStreetMapJsonSerializerContext.cs b/SearchProviders/OpenStreetMap/OpenStreetMapJsonSerializerContext.cs
new file mode 100644
index 0000000..314f120
@@ -0,0 +1,6 @@
using System.Text.Json.Serialization;
namespace MSearch.SearchProviders.OpenStreetMap;
[JsonSerializable(typeof(OpenStreetMapItem))]
internal sealed partial class OpenStreetMapJsonSerializerContext : JsonSerializerContext;
SearchProviders/OpenStreetMap/OpenStreetMapSearchProvider.cs +25 -0
diff --git a/SearchProviders/OpenStreetMap/OpenStreetMapSearchProvider.cs b/SearchProviders/OpenStreetMap/OpenStreetMapSearchProvider.cs
new file mode 100644
index 0000000..d22e7a1
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading;
using MSearch.Domain;
namespace MSearch.SearchProviders.OpenStreetMap;
internal sealed class OpenStreetMapSearchProvider(HttpClient httpClient) : ISearchProvider
{
public IAsyncEnumerable<SearchResult> Search(SearchQuery query, CancellationToken cancellationToken) =>
httpClient
.GetFromJsonAsAsyncEnumerable(
$"https://nominatim.openstreetmap.org/search?format=json&q={Uri.EscapeDataString(query.Term)}",
OpenStreetMapJsonSerializerContext.Default.OpenStreetMapItem,
cancellationToken
)
.Where(item => item is not null)
.Select(Map!);
private static SearchResult Map(OpenStreetMapItem item) =>
new(item.Name, item.DisplayName, new($"https://www.openstreetmap.org/{item.OmsType}/{item.OmsId}"));
}
SearchProviders/OpenStreetMap/ServiceCollectionExtensions.cs +14 -0
diff --git a/SearchProviders/OpenStreetMap/ServiceCollectionExtensions.cs b/SearchProviders/OpenStreetMap/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..041cfc5
@@ -0,0 +1,14 @@
using Microsoft.Extensions.DependencyInjection;
using MSearch.Domain;
namespace MSearch.SearchProviders.OpenStreetMap;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddOpenStreetMapSearchProvider(this IServiceCollection services)
{
services.AddHttpClient<ISearchProvider, OpenStreetMapSearchProvider>(nameof(OpenStreetMapSearchProvider));
return services;
}
}
SearchProviders/OpenStreetMap/packages.lock.json +142 -0
diff --git a/SearchProviders/OpenStreetMap/packages.lock.json b/SearchProviders/OpenStreetMap/packages.lock.json
new file mode 100644
index 0000000..1b5804a
@@ -0,0 +1,142 @@
{
"version": 2,
"dependencies": {
"net10.0": {
"CSharpier.MsBuild": {
"type": "Direct",
"requested": "[1.0.2, )",
"resolved": "1.0.2",
"contentHash": "Y98ba4yeefqrJY+MbCXhvb8x4/mCy8zHeuu/dYKwaM4p0Yu2LToI5ffTg/ErcpPuLnwJBNE229STdhf/dlikIw=="
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "bwGMrRcAMWx2s/RDgja97p27rxSz2pEQW0+rX5cWAUWVETVJ/eyxGfjAl8vuG5a+lckWmPIE+vcuaZNVB5YDdw=="
},
"Microsoft.Extensions.Http": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "M5gWob3dtzlF14oto1lR1ZuSJrR0gGc+obv7zY9LGmX5y3Ndpve29MrrjqJW/m4CFud4TE/KFUuHjjtwxhCO8g==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Diagnostics": "10.0.3",
"Microsoft.Extensions.Logging": "10.0.3",
"Microsoft.Extensions.Logging.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3"
}
},
"Microsoft.Extensions.Configuration": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "H1Cjv2xmm7O3iAGmFTcnSM0ZhLQ/7SqefmAvSJoT1PbXoxeYc2fo0mCLn2JlVbr9E6YpoU9q/o0fI9neDJB0xQ==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "xVDHL0+SIgemfh95fTO9cGLe17TWv/ZP0n7m01z8X6pzt2DmQpucioWR/mYZA1sRlkWnkXzfl0JweLNWmE9WMg==",
"dependencies": {
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Configuration.Binder": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "759UhpKaR5Jsll9kXpkft4z/7tpeF7Dw2rTY/9f9JchaSQTpRFNIPkZFZvoo7fFpbjUaqtDlO5aiGpmQrp/EUA==",
"dependencies": {
"Microsoft.Extensions.Configuration": "10.0.3",
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3"
}
},
"Microsoft.Extensions.DependencyInjection": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "2DLOmC0EkB2smVK8lPP1PIKEgL1arE3CMp9XSIQB/Y7ev5nnnyuM/PizKJ6QfLD08QCYoopSC9SFdbYglDomYg==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3"
}
},
"Microsoft.Extensions.Diagnostics": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "tc0R6i2T+138taoxFPQXb7Sy/4rtq4ytoJrAt4fNGs6k89mHpEhZnXUNgaFKwwb5Ud5rIUeLC6tfpsuHNwiWqg==",
"dependencies": {
"Microsoft.Extensions.Configuration": "10.0.3",
"Microsoft.Extensions.Diagnostics.Abstractions": "10.0.3",
"Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.3"
}
},
"Microsoft.Extensions.Diagnostics.Abstractions": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "mQiTzAj7PIJ2A9YXR5QhgulS1fTWhmQc3ckd1Mrf3hKW07d03fBDqx8vVaFw+cRTebDOeB6pNqdWdnRxsi1hBA==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3"
}
},
"Microsoft.Extensions.Logging": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "8D9Er1cGXNjNDIB+VLBNHn386L5ls2FoiG9a6o12gyn+GG3w6jdfUhzT8dtBnKcevE7/fsVA8MS3FBgFfClFtQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "10.0.3",
"Microsoft.Extensions.Logging.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3"
}
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "GEcpTwo7sUoLGGNTqV1FZEuL+tTD9m81NX/mh099dqGNna07/UGZShKQNZRw4hv6nlliSUwYQgSYc7OR99Jufg=="
},
"MSearch.Domain": {
"type": "Project",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.3, )",
"Microsoft.Extensions.Logging.Abstractions": "[10.0.3, )"
}
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "CentralTransitive",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "lxl0WLk7ROgBFAsjcOYjQ8/DVK+VMszxGBzUhgtQmAsTNldLL5pk9NG/cWTsXHq0lUhUEAtZkEE7jOGOA8bGKQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3"
}
},
"Microsoft.Extensions.Options": {
"type": "CentralTransitive",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "hU6WzGTPvPoLA2ng1ILvWQb3g0qORdlHNsxI8IcPLumJb3suimYUl+bbDzdo1V4KFsvVhnMWzysHpKbZaoDQPQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Options.ConfigurationExtensions": {
"type": "CentralTransitive",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "bn6QoBbbvwmzLIFyxrnL2/e+sqoNUOGbHyfWK9DPONMv1mDCYHm/C7MusYASM31b2lUx6OiDmonb3v+dv5t0nA==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.Configuration.Binder": "10.0.3",
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3",
"Microsoft.Extensions.Primitives": "10.0.3"
}
}
},
"net10.0/linux-x64": {},
"net10.0/win-arm64": {}
}
}
\ No newline at end of file
SearchProviders/Reddit/Reddit.csproj +13 -0
diff --git a/SearchProviders/Reddit/Reddit.csproj b/SearchProviders/Reddit/Reddit.csproj
new file mode 100644
index 0000000..f486b90
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>MSearch.SearchProviders.Reddit</RootNamespace>
<AssemblyName>MSearch.SearchProviders.Reddit</AssemblyName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Domain\Domain.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Http" />
</ItemGroup>
</Project>
SearchProviders/Reddit/RedditJsonSerializerContext.cs +6 -0
diff --git a/SearchProviders/Reddit/RedditJsonSerializerContext.cs b/SearchProviders/Reddit/RedditJsonSerializerContext.cs
new file mode 100644
index 0000000..c97454f
@@ -0,0 +1,6 @@
using System.Text.Json.Serialization;
namespace MSearch.SearchProviders.Reddit;
[JsonSerializable(typeof(RedditSearchResponse))]
internal sealed partial class RedditJsonSerializerContext : JsonSerializerContext;
SearchProviders/Reddit/RedditSearchData.cs +8 -0
diff --git a/SearchProviders/Reddit/RedditSearchData.cs b/SearchProviders/Reddit/RedditSearchData.cs
new file mode 100644
index 0000000..ef11936
@@ -0,0 +1,8 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace MSearch.SearchProviders.Reddit;
internal sealed record RedditSearchData(
[property: JsonPropertyName("children")] IReadOnlyCollection<RedditSearchItem> Children
);
SearchProviders/Reddit/RedditSearchItem.cs +5 -0
diff --git a/SearchProviders/Reddit/RedditSearchItem.cs b/SearchProviders/Reddit/RedditSearchItem.cs
new file mode 100644
index 0000000..8dec4b6
@@ -0,0 +1,5 @@
using System.Text.Json.Serialization;
namespace MSearch.SearchProviders.Reddit;
internal sealed record RedditSearchItem([property: JsonPropertyName("data")] RedditSearchResult Data);
SearchProviders/Reddit/RedditSearchProvider.cs +35 -0
diff --git a/SearchProviders/Reddit/RedditSearchProvider.cs b/SearchProviders/Reddit/RedditSearchProvider.cs
new file mode 100644
index 0000000..4e310e6
@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Runtime.CompilerServices;
using System.Threading;
using MSearch.Domain;
namespace MSearch.SearchProviders.Reddit;
internal sealed class RedditSearchProvider(HttpClient httpClient) : ISearchProvider
{
public async IAsyncEnumerable<SearchResult> Search(
SearchQuery query,
[EnumeratorCancellation] CancellationToken cancellationToken
)
{
var response = await httpClient.GetFromJsonAsync(
$"https://www.reddit.com/search.json?q={Uri.EscapeDataString(query.Term)}",
RedditJsonSerializerContext.Default.RedditSearchResponse,
cancellationToken
);
if (response is null)
{
yield break;
}
foreach (var item in response.Data.Children)
{
yield return Map(item.Data);
}
}
private static SearchResult Map(RedditSearchResult result) =>
new(result.Title, result.SelfText, new("https://reddit.com" + result.Permalink));
}
SearchProviders/Reddit/RedditSearchResponse.cs +5 -0
diff --git a/SearchProviders/Reddit/RedditSearchResponse.cs b/SearchProviders/Reddit/RedditSearchResponse.cs
new file mode 100644
index 0000000..5592c9e
@@ -0,0 +1,5 @@
using System.Text.Json.Serialization;
namespace MSearch.SearchProviders.Reddit;
internal sealed record RedditSearchResponse([property: JsonPropertyName("data")] RedditSearchData Data);
SearchProviders/Reddit/RedditSearchResult.cs +9 -0
diff --git a/SearchProviders/Reddit/RedditSearchResult.cs b/SearchProviders/Reddit/RedditSearchResult.cs
new file mode 100644
index 0000000..5163d41
@@ -0,0 +1,9 @@
using System.Text.Json.Serialization;
namespace MSearch.SearchProviders.Reddit;
internal sealed record RedditSearchResult(
[property: JsonPropertyName("title")] string Title,
[property: JsonPropertyName("selftext")] string SelfText,
[property: JsonPropertyName("permalink")] string Permalink
);
SearchProviders/Reddit/ServiceCollectionExtensions.cs +14 -0
diff --git a/SearchProviders/Reddit/ServiceCollectionExtensions.cs b/SearchProviders/Reddit/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..76cf421
@@ -0,0 +1,14 @@
using Microsoft.Extensions.DependencyInjection;
using MSearch.Domain;
namespace MSearch.SearchProviders.Reddit;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddRedditSearchProvider(this IServiceCollection services)
{
services.AddHttpClient<ISearchProvider, RedditSearchProvider>(nameof(RedditSearchProvider));
return services;
}
}
SearchProviders/Reddit/packages.lock.json +142 -0
diff --git a/SearchProviders/Reddit/packages.lock.json b/SearchProviders/Reddit/packages.lock.json
new file mode 100644
index 0000000..1b5804a
@@ -0,0 +1,142 @@
{
"version": 2,
"dependencies": {
"net10.0": {
"CSharpier.MsBuild": {
"type": "Direct",
"requested": "[1.0.2, )",
"resolved": "1.0.2",
"contentHash": "Y98ba4yeefqrJY+MbCXhvb8x4/mCy8zHeuu/dYKwaM4p0Yu2LToI5ffTg/ErcpPuLnwJBNE229STdhf/dlikIw=="
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "bwGMrRcAMWx2s/RDgja97p27rxSz2pEQW0+rX5cWAUWVETVJ/eyxGfjAl8vuG5a+lckWmPIE+vcuaZNVB5YDdw=="
},
"Microsoft.Extensions.Http": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "M5gWob3dtzlF14oto1lR1ZuSJrR0gGc+obv7zY9LGmX5y3Ndpve29MrrjqJW/m4CFud4TE/KFUuHjjtwxhCO8g==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Diagnostics": "10.0.3",
"Microsoft.Extensions.Logging": "10.0.3",
"Microsoft.Extensions.Logging.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3"
}
},
"Microsoft.Extensions.Configuration": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "H1Cjv2xmm7O3iAGmFTcnSM0ZhLQ/7SqefmAvSJoT1PbXoxeYc2fo0mCLn2JlVbr9E6YpoU9q/o0fI9neDJB0xQ==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "xVDHL0+SIgemfh95fTO9cGLe17TWv/ZP0n7m01z8X6pzt2DmQpucioWR/mYZA1sRlkWnkXzfl0JweLNWmE9WMg==",
"dependencies": {
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Configuration.Binder": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "759UhpKaR5Jsll9kXpkft4z/7tpeF7Dw2rTY/9f9JchaSQTpRFNIPkZFZvoo7fFpbjUaqtDlO5aiGpmQrp/EUA==",
"dependencies": {
"Microsoft.Extensions.Configuration": "10.0.3",
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3"
}
},
"Microsoft.Extensions.DependencyInjection": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "2DLOmC0EkB2smVK8lPP1PIKEgL1arE3CMp9XSIQB/Y7ev5nnnyuM/PizKJ6QfLD08QCYoopSC9SFdbYglDomYg==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3"
}
},
"Microsoft.Extensions.Diagnostics": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "tc0R6i2T+138taoxFPQXb7Sy/4rtq4ytoJrAt4fNGs6k89mHpEhZnXUNgaFKwwb5Ud5rIUeLC6tfpsuHNwiWqg==",
"dependencies": {
"Microsoft.Extensions.Configuration": "10.0.3",
"Microsoft.Extensions.Diagnostics.Abstractions": "10.0.3",
"Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.3"
}
},
"Microsoft.Extensions.Diagnostics.Abstractions": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "mQiTzAj7PIJ2A9YXR5QhgulS1fTWhmQc3ckd1Mrf3hKW07d03fBDqx8vVaFw+cRTebDOeB6pNqdWdnRxsi1hBA==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3"
}
},
"Microsoft.Extensions.Logging": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "8D9Er1cGXNjNDIB+VLBNHn386L5ls2FoiG9a6o12gyn+GG3w6jdfUhzT8dtBnKcevE7/fsVA8MS3FBgFfClFtQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "10.0.3",
"Microsoft.Extensions.Logging.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3"
}
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "GEcpTwo7sUoLGGNTqV1FZEuL+tTD9m81NX/mh099dqGNna07/UGZShKQNZRw4hv6nlliSUwYQgSYc7OR99Jufg=="
},
"MSearch.Domain": {
"type": "Project",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.3, )",
"Microsoft.Extensions.Logging.Abstractions": "[10.0.3, )"
}
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "CentralTransitive",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "lxl0WLk7ROgBFAsjcOYjQ8/DVK+VMszxGBzUhgtQmAsTNldLL5pk9NG/cWTsXHq0lUhUEAtZkEE7jOGOA8bGKQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3"
}
},
"Microsoft.Extensions.Options": {
"type": "CentralTransitive",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "hU6WzGTPvPoLA2ng1ILvWQb3g0qORdlHNsxI8IcPLumJb3suimYUl+bbDzdo1V4KFsvVhnMWzysHpKbZaoDQPQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Options.ConfigurationExtensions": {
"type": "CentralTransitive",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "bn6QoBbbvwmzLIFyxrnL2/e+sqoNUOGbHyfWK9DPONMv1mDCYHm/C7MusYASM31b2lUx6OiDmonb3v+dv5t0nA==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.Configuration.Binder": "10.0.3",
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3",
"Microsoft.Extensions.Primitives": "10.0.3"
}
}
},
"net10.0/linux-x64": {},
"net10.0/win-arm64": {}
}
}
\ No newline at end of file
SearchProviders/Spotify/ServiceCollectionExtensions.cs +35 -0
diff --git a/SearchProviders/Spotify/ServiceCollectionExtensions.cs b/SearchProviders/Spotify/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..517bfa8
@@ -0,0 +1,35 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using MSearch.Domain;
using SpotifyAPI.Web;
using SpotifyAPI.Web.Http;
namespace MSearch.SearchProviders.Spotify;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddSpotifySearchProvider(this IServiceCollection services)
{
services.AddOptions<SpotifyOptions>().BindConfiguration("SearchProviders:Spotify").ValidateOnStart();
services.AddTransient<IValidateOptions<SpotifyOptions>, SpotifyOptionsValidator>();
services.AddHttpClient<ISpotifyClient, SpotifyClient>(
nameof(SpotifyClient),
(httpClient, sp) =>
{
var options = sp.GetRequiredService<IOptions<SpotifyOptions>>();
var config = SpotifyClientConfig
.CreateDefault()
.WithHTTPClient(new NetHttpClient(httpClient))
.WithAuthenticator(
new ClientCredentialsAuthenticator(options.Value.ClientId, options.Value.ClientSecret)
);
return new SpotifyClient(config);
}
);
services.AddTransient<ISearchProvider, SpotifySearchProvider>();
return services;
}
}
SearchProviders/Spotify/Spotify.csproj +17 -0
diff --git a/SearchProviders/Spotify/Spotify.csproj b/SearchProviders/Spotify/Spotify.csproj
new file mode 100644
index 0000000..17c23d7
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>MSearch.SearchProviders.Spotify</RootNamespace>
<AssemblyName>MSearch.SearchProviders.Spotify</AssemblyName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Domain\Domain.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Http" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Options" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" />
<PackageReference Include="SpotifyAPI.Web" />
</ItemGroup>
</Project>
SearchProviders/Spotify/SpotifyOptions.cs +16 -0
diff --git a/SearchProviders/Spotify/SpotifyOptions.cs b/SearchProviders/Spotify/SpotifyOptions.cs
new file mode 100644
index 0000000..b6f344d
@@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.Options;
namespace MSearch.SearchProviders.Spotify;
internal sealed class SpotifyOptions
{
[Required]
public required string ClientId { get; set; }
[Required]
public required string ClientSecret { get; set; }
}
[OptionsValidator]
internal sealed partial class SpotifyOptionsValidator : IValidateOptions<SpotifyOptions>;
SearchProviders/Spotify/SpotifySearchProvider.cs +87 -0
diff --git a/SearchProviders/Spotify/SpotifySearchProvider.cs b/SearchProviders/Spotify/SpotifySearchProvider.cs
new file mode 100644
index 0000000..9ff1f70
@@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using Microsoft.Extensions.Logging;
using MSearch.Domain;
using SpotifyAPI.Web;
namespace MSearch.SearchProviders.Spotify;
internal sealed class SpotifySearchProvider(ILogger<SpotifySearchProvider> logger, ISpotifyClient spotifyClient)
: ISearchProvider
{
public async IAsyncEnumerable<SearchResult> Search(
SearchQuery query,
[EnumeratorCancellation] CancellationToken cancellationToken
)
{
var response = await spotifyClient.Search.Item(
new(SearchRequest.Types.All, query.Term) { Limit = 3 },
cancellationToken
);
if (response.Tracks.Items is not null)
{
foreach (var track in response.Tracks.Items)
{
if (TryMap(track) is not { } result)
{
logger.FailedMappingTrack(track);
continue;
}
yield return result;
}
}
if (response.Artists.Items is not null)
{
foreach (var artist in response.Artists.Items)
{
if (TryMap(artist) is not { } result)
{
logger.FailedMappingArtist(artist);
continue;
}
yield return result;
}
}
if (response.Albums.Items is not null)
{
foreach (var album in response.Albums.Items)
{
if (TryMap(album) is not { } result)
{
logger.FailedMappingAlbum(album);
continue;
}
yield return result;
}
}
}
private static SearchResult? TryMap(FullTrack track) =>
GetSpotifyUrl(track.ExternalUrls) is Uri spotifyUrl ? new(track.Name, Summary: null, spotifyUrl) : null;
private static SearchResult? TryMap(FullArtist artist) =>
GetSpotifyUrl(artist.ExternalUrls) is Uri spotifyUrl ? new(artist.Name, Summary: null, spotifyUrl) : null;
private static SearchResult? TryMap(SimpleAlbum album) =>
GetSpotifyUrl(album.ExternalUrls) is Uri spotifyUrl ? new(album.Name, Summary: null, spotifyUrl) : null;
private static Uri? GetSpotifyUrl(IReadOnlyDictionary<string, string> externalUrls) =>
externalUrls.GetValueOrDefault("spotify") is string spotifyUrl ? new(spotifyUrl) : null;
}
internal static partial class LoggerExtensions
{
[LoggerMessage(LogLevel.Warning, "Failed mapping track {Track}")]
public static partial void FailedMappingTrack(this ILogger logger, FullTrack track);
[LoggerMessage(LogLevel.Warning, "Failed mapping artist {Artist}")]
public static partial void FailedMappingArtist(this ILogger logger, FullArtist artist);
[LoggerMessage(LogLevel.Warning, "Failed mapping album {Album}")]
public static partial void FailedMappingAlbum(this ILogger logger, SimpleAlbum album);
}
SearchProviders/Spotify/packages.lock.json +156 -0
diff --git a/SearchProviders/Spotify/packages.lock.json b/SearchProviders/Spotify/packages.lock.json
new file mode 100644
index 0000000..1f02901
@@ -0,0 +1,156 @@
{
"version": 2,
"dependencies": {
"net10.0": {
"CSharpier.MsBuild": {
"type": "Direct",
"requested": "[1.0.2, )",
"resolved": "1.0.2",
"contentHash": "Y98ba4yeefqrJY+MbCXhvb8x4/mCy8zHeuu/dYKwaM4p0Yu2LToI5ffTg/ErcpPuLnwJBNE229STdhf/dlikIw=="
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "bwGMrRcAMWx2s/RDgja97p27rxSz2pEQW0+rX5cWAUWVETVJ/eyxGfjAl8vuG5a+lckWmPIE+vcuaZNVB5YDdw=="
},
"Microsoft.Extensions.Http": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "M5gWob3dtzlF14oto1lR1ZuSJrR0gGc+obv7zY9LGmX5y3Ndpve29MrrjqJW/m4CFud4TE/KFUuHjjtwxhCO8g==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Diagnostics": "10.0.3",
"Microsoft.Extensions.Logging": "10.0.3",
"Microsoft.Extensions.Logging.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3"
}
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "lxl0WLk7ROgBFAsjcOYjQ8/DVK+VMszxGBzUhgtQmAsTNldLL5pk9NG/cWTsXHq0lUhUEAtZkEE7jOGOA8bGKQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3"
}
},
"Microsoft.Extensions.Options": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "hU6WzGTPvPoLA2ng1ILvWQb3g0qORdlHNsxI8IcPLumJb3suimYUl+bbDzdo1V4KFsvVhnMWzysHpKbZaoDQPQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Options.ConfigurationExtensions": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "bn6QoBbbvwmzLIFyxrnL2/e+sqoNUOGbHyfWK9DPONMv1mDCYHm/C7MusYASM31b2lUx6OiDmonb3v+dv5t0nA==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.Configuration.Binder": "10.0.3",
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3",
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"SpotifyAPI.Web": {
"type": "Direct",
"requested": "[7.4.0, )",
"resolved": "7.4.0",
"contentHash": "1xewe5k+dy3cQJ9iIOmw4CUmBUnVZflSjcFL33636h/XuTLUnBKkKiB9dCeHJeVVtN0+on+t2Wy3haJBJMp3Vg==",
"dependencies": {
"Newtonsoft.Json": "13.0.3"
}
},
"Microsoft.Extensions.Configuration": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "H1Cjv2xmm7O3iAGmFTcnSM0ZhLQ/7SqefmAvSJoT1PbXoxeYc2fo0mCLn2JlVbr9E6YpoU9q/o0fI9neDJB0xQ==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "xVDHL0+SIgemfh95fTO9cGLe17TWv/ZP0n7m01z8X6pzt2DmQpucioWR/mYZA1sRlkWnkXzfl0JweLNWmE9WMg==",
"dependencies": {
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Configuration.Binder": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "759UhpKaR5Jsll9kXpkft4z/7tpeF7Dw2rTY/9f9JchaSQTpRFNIPkZFZvoo7fFpbjUaqtDlO5aiGpmQrp/EUA==",
"dependencies": {
"Microsoft.Extensions.Configuration": "10.0.3",
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3"
}
},
"Microsoft.Extensions.DependencyInjection": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "2DLOmC0EkB2smVK8lPP1PIKEgL1arE3CMp9XSIQB/Y7ev5nnnyuM/PizKJ6QfLD08QCYoopSC9SFdbYglDomYg==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3"
}
},
"Microsoft.Extensions.Diagnostics": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "tc0R6i2T+138taoxFPQXb7Sy/4rtq4ytoJrAt4fNGs6k89mHpEhZnXUNgaFKwwb5Ud5rIUeLC6tfpsuHNwiWqg==",
"dependencies": {
"Microsoft.Extensions.Configuration": "10.0.3",
"Microsoft.Extensions.Diagnostics.Abstractions": "10.0.3",
"Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.3"
}
},
"Microsoft.Extensions.Diagnostics.Abstractions": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "mQiTzAj7PIJ2A9YXR5QhgulS1fTWhmQc3ckd1Mrf3hKW07d03fBDqx8vVaFw+cRTebDOeB6pNqdWdnRxsi1hBA==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3"
}
},
"Microsoft.Extensions.Logging": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "8D9Er1cGXNjNDIB+VLBNHn386L5ls2FoiG9a6o12gyn+GG3w6jdfUhzT8dtBnKcevE7/fsVA8MS3FBgFfClFtQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "10.0.3",
"Microsoft.Extensions.Logging.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3"
}
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "GEcpTwo7sUoLGGNTqV1FZEuL+tTD9m81NX/mh099dqGNna07/UGZShKQNZRw4hv6nlliSUwYQgSYc7OR99Jufg=="
},
"Newtonsoft.Json": {
"type": "Transitive",
"resolved": "13.0.3",
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
},
"MSearch.Domain": {
"type": "Project",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.3, )",
"Microsoft.Extensions.Logging.Abstractions": "[10.0.3, )"
}
}
},
"net10.0/linux-x64": {},
"net10.0/win-arm64": {}
}
}
\ No newline at end of file
SearchProviders/StackExchange/ServiceCollectionExtensions.cs +33 -0
diff --git a/SearchProviders/StackExchange/ServiceCollectionExtensions.cs b/SearchProviders/StackExchange/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..6e4ac89
@@ -0,0 +1,33 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using MSearch.Domain;
namespace MSearch.SearchProviders.StackExchange;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddStackExchangeSearchProvider(this IServiceCollection services, string site)
{
services
.AddOptions<StackExchangeOptions>(site)
.BindConfiguration("SearchProviders:StackExchange")
.Configure(o => o.Site = site);
services.TryAddTransient<IValidateOptions<StackExchangeOptions>, StackExchangeOptionsValidator>();
services.AddHttpClient<ISearchProvider, StackExchangeSearchProvider>(
nameof(StackExchangeSearchProvider),
(httpClient, sp) =>
{
httpClient.BaseAddress = new("https://api.stackexchange.com/2.3/");
return new StackExchangeSearchProvider(
Options.Create(sp.GetRequiredService<IOptionsMonitor<StackExchangeOptions>>().Get(site)),
httpClient
);
}
);
return services;
}
}
SearchProviders/StackExchange/StackExchange.csproj +15 -0
diff --git a/SearchProviders/StackExchange/StackExchange.csproj b/SearchProviders/StackExchange/StackExchange.csproj
new file mode 100644
index 0000000..d4d07c4
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>MSearch.SearchProviders.StackExchange</RootNamespace>
<AssemblyName>MSearch.SearchProviders.StackExchange</AssemblyName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Domain\Domain.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Http" />
<PackageReference Include="Microsoft.Extensions.Options" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" />
</ItemGroup>
</Project>
SearchProviders/StackExchange/StackExchangeItem.cs +8 -0
diff --git a/SearchProviders/StackExchange/StackExchangeItem.cs b/SearchProviders/StackExchange/StackExchangeItem.cs
new file mode 100644
index 0000000..cff2937
@@ -0,0 +1,8 @@
using System.Text.Json.Serialization;
namespace MSearch.SearchProviders.StackExchange;
internal sealed record StackExchangeItem(
[property: JsonPropertyName("title")] string Title,
[property: JsonPropertyName("link")] string Link
);
SearchProviders/StackExchange/StackExchangeJsonSerializerContext.cs +6 -0
diff --git a/SearchProviders/StackExchange/StackExchangeJsonSerializerContext.cs b/SearchProviders/StackExchange/StackExchangeJsonSerializerContext.cs
new file mode 100644
index 0000000..fb27632
@@ -0,0 +1,6 @@
using System.Text.Json.Serialization;
namespace MSearch.SearchProviders.StackExchange;
[JsonSerializable(typeof(StackExchangeResponse))]
internal sealed partial class StackExchangeJsonSerializerContext : JsonSerializerContext;
SearchProviders/StackExchange/StackExchangeOptions.cs +16 -0
diff --git a/SearchProviders/StackExchange/StackExchangeOptions.cs b/SearchProviders/StackExchange/StackExchangeOptions.cs
new file mode 100644
index 0000000..8c96779
@@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.Options;
namespace MSearch.SearchProviders.StackExchange;
internal sealed class StackExchangeOptions
{
[Required]
public required string ApiKey { get; set; }
[Required]
public required string Site { get; set; }
}
[OptionsValidator]
internal sealed partial class StackExchangeOptionsValidator : IValidateOptions<StackExchangeOptions>;
SearchProviders/StackExchange/StackExchangeResponse.cs +8 -0
diff --git a/SearchProviders/StackExchange/StackExchangeResponse.cs b/SearchProviders/StackExchange/StackExchangeResponse.cs
new file mode 100644
index 0000000..177330b
@@ -0,0 +1,8 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace MSearch.SearchProviders.StackExchange;
internal sealed record StackExchangeResponse(
[property: JsonPropertyName("items")] IReadOnlyCollection<StackExchangeItem> Items
);
SearchProviders/StackExchange/StackExchangeSearchProvider.cs +39 -0
diff --git a/SearchProviders/StackExchange/StackExchangeSearchProvider.cs b/SearchProviders/StackExchange/StackExchangeSearchProvider.cs
new file mode 100644
index 0000000..c02ae65
@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Runtime.CompilerServices;
using System.Threading;
using Microsoft.Extensions.Options;
using MSearch.Domain;
namespace MSearch.SearchProviders.StackExchange;
internal sealed class StackExchangeSearchProvider(IOptions<StackExchangeOptions> options, HttpClient httpClient)
: ISearchProvider
{
private readonly string apiKey = options.Value.ApiKey;
private readonly string site = options.Value.Site;
public async IAsyncEnumerable<SearchResult> Search(
SearchQuery query,
[EnumeratorCancellation] CancellationToken cancellationToken
)
{
var response = await httpClient.GetFromJsonAsync(
$"search/advanced?order=desc&sort=relevance&pagesize=10&site={site}&q={Uri.EscapeDataString(query.Term)}&key={apiKey}",
StackExchangeJsonSerializerContext.Default.StackExchangeResponse,
cancellationToken
);
if (response is null)
{
yield break;
}
foreach (var item in response.Items)
{
yield return Map(item);
}
}
private static SearchResult Map(StackExchangeItem item) => new(item.Title, Summary: null, new(item.Link));
}
SearchProviders/StackExchange/packages.lock.json +142 -0
diff --git a/SearchProviders/StackExchange/packages.lock.json b/SearchProviders/StackExchange/packages.lock.json
new file mode 100644
index 0000000..26ae089
@@ -0,0 +1,142 @@
{
"version": 2,
"dependencies": {
"net10.0": {
"CSharpier.MsBuild": {
"type": "Direct",
"requested": "[1.0.2, )",
"resolved": "1.0.2",
"contentHash": "Y98ba4yeefqrJY+MbCXhvb8x4/mCy8zHeuu/dYKwaM4p0Yu2LToI5ffTg/ErcpPuLnwJBNE229STdhf/dlikIw=="
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "bwGMrRcAMWx2s/RDgja97p27rxSz2pEQW0+rX5cWAUWVETVJ/eyxGfjAl8vuG5a+lckWmPIE+vcuaZNVB5YDdw=="
},
"Microsoft.Extensions.Http": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "M5gWob3dtzlF14oto1lR1ZuSJrR0gGc+obv7zY9LGmX5y3Ndpve29MrrjqJW/m4CFud4TE/KFUuHjjtwxhCO8g==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Diagnostics": "10.0.3",
"Microsoft.Extensions.Logging": "10.0.3",
"Microsoft.Extensions.Logging.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3"
}
},
"Microsoft.Extensions.Options": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "hU6WzGTPvPoLA2ng1ILvWQb3g0qORdlHNsxI8IcPLumJb3suimYUl+bbDzdo1V4KFsvVhnMWzysHpKbZaoDQPQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Options.ConfigurationExtensions": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "bn6QoBbbvwmzLIFyxrnL2/e+sqoNUOGbHyfWK9DPONMv1mDCYHm/C7MusYASM31b2lUx6OiDmonb3v+dv5t0nA==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.Configuration.Binder": "10.0.3",
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3",
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Configuration": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "H1Cjv2xmm7O3iAGmFTcnSM0ZhLQ/7SqefmAvSJoT1PbXoxeYc2fo0mCLn2JlVbr9E6YpoU9q/o0fI9neDJB0xQ==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "xVDHL0+SIgemfh95fTO9cGLe17TWv/ZP0n7m01z8X6pzt2DmQpucioWR/mYZA1sRlkWnkXzfl0JweLNWmE9WMg==",
"dependencies": {
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Configuration.Binder": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "759UhpKaR5Jsll9kXpkft4z/7tpeF7Dw2rTY/9f9JchaSQTpRFNIPkZFZvoo7fFpbjUaqtDlO5aiGpmQrp/EUA==",
"dependencies": {
"Microsoft.Extensions.Configuration": "10.0.3",
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3"
}
},
"Microsoft.Extensions.DependencyInjection": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "2DLOmC0EkB2smVK8lPP1PIKEgL1arE3CMp9XSIQB/Y7ev5nnnyuM/PizKJ6QfLD08QCYoopSC9SFdbYglDomYg==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3"
}
},
"Microsoft.Extensions.Diagnostics": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "tc0R6i2T+138taoxFPQXb7Sy/4rtq4ytoJrAt4fNGs6k89mHpEhZnXUNgaFKwwb5Ud5rIUeLC6tfpsuHNwiWqg==",
"dependencies": {
"Microsoft.Extensions.Configuration": "10.0.3",
"Microsoft.Extensions.Diagnostics.Abstractions": "10.0.3",
"Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.3"
}
},
"Microsoft.Extensions.Diagnostics.Abstractions": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "mQiTzAj7PIJ2A9YXR5QhgulS1fTWhmQc3ckd1Mrf3hKW07d03fBDqx8vVaFw+cRTebDOeB6pNqdWdnRxsi1hBA==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3"
}
},
"Microsoft.Extensions.Logging": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "8D9Er1cGXNjNDIB+VLBNHn386L5ls2FoiG9a6o12gyn+GG3w6jdfUhzT8dtBnKcevE7/fsVA8MS3FBgFfClFtQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "10.0.3",
"Microsoft.Extensions.Logging.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3"
}
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "GEcpTwo7sUoLGGNTqV1FZEuL+tTD9m81NX/mh099dqGNna07/UGZShKQNZRw4hv6nlliSUwYQgSYc7OR99Jufg=="
},
"MSearch.Domain": {
"type": "Project",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.3, )",
"Microsoft.Extensions.Logging.Abstractions": "[10.0.3, )"
}
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "CentralTransitive",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "lxl0WLk7ROgBFAsjcOYjQ8/DVK+VMszxGBzUhgtQmAsTNldLL5pk9NG/cWTsXHq0lUhUEAtZkEE7jOGOA8bGKQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3"
}
}
},
"net10.0/linux-x64": {},
"net10.0/win-arm64": {}
}
}
\ No newline at end of file
SearchProviders/TheMovieDb/ServiceCollectionExtensions.cs +30 -0
diff --git a/SearchProviders/TheMovieDb/ServiceCollectionExtensions.cs b/SearchProviders/TheMovieDb/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..62f72d0
@@ -0,0 +1,30 @@
using System.Net.Http.Headers;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using MSearch.Domain;
namespace MSearch.SearchProviders.TheMovieDb;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddTheMovieDbSearchProvider(this IServiceCollection services)
{
services.AddOptions<TheMovieDbOptions>().BindConfiguration("SearchProviders:TheMovieDb").ValidateOnStart();
services.AddTransient<IValidateOptions<TheMovieDbOptions>, TheMovieDbOptionsValidator>();
services.AddHttpClient<ISearchProvider, TheMovieDbSearchProvider>(
nameof(TheMovieDbSearchProvider),
(sp, httpClient) =>
{
var options = sp.GetRequiredService<IOptions<TheMovieDbOptions>>();
httpClient.BaseAddress = new("https://api.themoviedb.org/");
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Bearer",
options.Value.ApiKey
);
}
);
return services;
}
}
SearchProviders/TheMovieDb/TheMovieDb.csproj +15 -0
diff --git a/SearchProviders/TheMovieDb/TheMovieDb.csproj b/SearchProviders/TheMovieDb/TheMovieDb.csproj
new file mode 100644
index 0000000..89f61eb
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>MSearch.SearchProviders.TheMovieDb</RootNamespace>
<AssemblyName>MSearch.SearchProviders.TheMovieDb</AssemblyName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Domain\Domain.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Http" />
<PackageReference Include="Microsoft.Extensions.Options" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" />
</ItemGroup>
</Project>
SearchProviders/TheMovieDb/TheMovieDbJsonSerializerContext.cs +6 -0
diff --git a/SearchProviders/TheMovieDb/TheMovieDbJsonSerializerContext.cs b/SearchProviders/TheMovieDb/TheMovieDbJsonSerializerContext.cs
new file mode 100644
index 0000000..4b0cc3f
@@ -0,0 +1,6 @@
using System.Text.Json.Serialization;
namespace MSearch.SearchProviders.TheMovieDb;
[JsonSerializable(typeof(TheMovieDbResponse))]
internal sealed partial class TheMovieDbJsonSerializerContext : JsonSerializerContext;
SearchProviders/TheMovieDb/TheMovieDbOptions.cs +13 -0
diff --git a/SearchProviders/TheMovieDb/TheMovieDbOptions.cs b/SearchProviders/TheMovieDb/TheMovieDbOptions.cs
new file mode 100644
index 0000000..f742e2d
@@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.Options;
namespace MSearch.SearchProviders.TheMovieDb;
internal sealed class TheMovieDbOptions
{
[Required]
public required string ApiKey { get; set; }
}
[OptionsValidator]
internal sealed partial class TheMovieDbOptionsValidator : IValidateOptions<TheMovieDbOptions>;
SearchProviders/TheMovieDb/TheMovieDbResponse.cs +8 -0
diff --git a/SearchProviders/TheMovieDb/TheMovieDbResponse.cs b/SearchProviders/TheMovieDb/TheMovieDbResponse.cs
new file mode 100644
index 0000000..842e4d2
@@ -0,0 +1,8 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace MSearch.SearchProviders.TheMovieDb;
internal sealed record TheMovieDbResponse(
[property: JsonPropertyName("results")] IReadOnlyCollection<TheMovieDbResult> Results
);
SearchProviders/TheMovieDb/TheMovieDbResult.cs +27 -0
diff --git a/SearchProviders/TheMovieDb/TheMovieDbResult.cs b/SearchProviders/TheMovieDb/TheMovieDbResult.cs
new file mode 100644
index 0000000..20ff07f
@@ -0,0 +1,27 @@
using System.ComponentModel;
using System.Text.Json.Serialization;
namespace MSearch.SearchProviders.TheMovieDb;
internal sealed record TheMovieDbResult
{
[JsonPropertyName("title")]
public string Title { get; set; } = "";
[JsonPropertyName("overview")]
public string? Overview { get; set; }
[JsonPropertyName("id")]
public required int Id { get; set; }
[JsonPropertyName("media_type")]
public required string MediaType { get; set; }
[JsonPropertyName("name")]
[EditorBrowsable(EditorBrowsableState.Never)]
public string Name
{
get => Title;
set => Title = value;
}
}
SearchProviders/TheMovieDb/TheMovieDbSearchProvider.cs +35 -0
diff --git a/SearchProviders/TheMovieDb/TheMovieDbSearchProvider.cs b/SearchProviders/TheMovieDb/TheMovieDbSearchProvider.cs
new file mode 100644
index 0000000..5588532
@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Runtime.CompilerServices;
using System.Threading;
using MSearch.Domain;
namespace MSearch.SearchProviders.TheMovieDb;
internal sealed class TheMovieDbSearchProvider(HttpClient httpClient) : ISearchProvider
{
public async IAsyncEnumerable<SearchResult> Search(
SearchQuery query,
[EnumeratorCancellation] CancellationToken cancellationToken
)
{
var response = await httpClient.GetFromJsonAsync(
$"3/search/multi?query={Uri.EscapeDataString(query.Term)}",
TheMovieDbJsonSerializerContext.Default.TheMovieDbResponse,
cancellationToken
);
if (response is null)
{
yield break;
}
foreach (var result in response.Results)
{
yield return Map(result);
}
}
private static SearchResult Map(TheMovieDbResult result) =>
new(result.Title, result.Overview, new($"https://www.themoviedb.org/{result.MediaType}/{result.Id}"));
}
SearchProviders/TheMovieDb/packages.lock.json +142 -0
diff --git a/SearchProviders/TheMovieDb/packages.lock.json b/SearchProviders/TheMovieDb/packages.lock.json
new file mode 100644
index 0000000..26ae089
@@ -0,0 +1,142 @@
{
"version": 2,
"dependencies": {
"net10.0": {
"CSharpier.MsBuild": {
"type": "Direct",
"requested": "[1.0.2, )",
"resolved": "1.0.2",
"contentHash": "Y98ba4yeefqrJY+MbCXhvb8x4/mCy8zHeuu/dYKwaM4p0Yu2LToI5ffTg/ErcpPuLnwJBNE229STdhf/dlikIw=="
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "bwGMrRcAMWx2s/RDgja97p27rxSz2pEQW0+rX5cWAUWVETVJ/eyxGfjAl8vuG5a+lckWmPIE+vcuaZNVB5YDdw=="
},
"Microsoft.Extensions.Http": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "M5gWob3dtzlF14oto1lR1ZuSJrR0gGc+obv7zY9LGmX5y3Ndpve29MrrjqJW/m4CFud4TE/KFUuHjjtwxhCO8g==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Diagnostics": "10.0.3",
"Microsoft.Extensions.Logging": "10.0.3",
"Microsoft.Extensions.Logging.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3"
}
},
"Microsoft.Extensions.Options": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "hU6WzGTPvPoLA2ng1ILvWQb3g0qORdlHNsxI8IcPLumJb3suimYUl+bbDzdo1V4KFsvVhnMWzysHpKbZaoDQPQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Options.ConfigurationExtensions": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "bn6QoBbbvwmzLIFyxrnL2/e+sqoNUOGbHyfWK9DPONMv1mDCYHm/C7MusYASM31b2lUx6OiDmonb3v+dv5t0nA==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.Configuration.Binder": "10.0.3",
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3",
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Configuration": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "H1Cjv2xmm7O3iAGmFTcnSM0ZhLQ/7SqefmAvSJoT1PbXoxeYc2fo0mCLn2JlVbr9E6YpoU9q/o0fI9neDJB0xQ==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "xVDHL0+SIgemfh95fTO9cGLe17TWv/ZP0n7m01z8X6pzt2DmQpucioWR/mYZA1sRlkWnkXzfl0JweLNWmE9WMg==",
"dependencies": {
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Configuration.Binder": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "759UhpKaR5Jsll9kXpkft4z/7tpeF7Dw2rTY/9f9JchaSQTpRFNIPkZFZvoo7fFpbjUaqtDlO5aiGpmQrp/EUA==",
"dependencies": {
"Microsoft.Extensions.Configuration": "10.0.3",
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3"
}
},
"Microsoft.Extensions.DependencyInjection": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "2DLOmC0EkB2smVK8lPP1PIKEgL1arE3CMp9XSIQB/Y7ev5nnnyuM/PizKJ6QfLD08QCYoopSC9SFdbYglDomYg==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3"
}
},
"Microsoft.Extensions.Diagnostics": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "tc0R6i2T+138taoxFPQXb7Sy/4rtq4ytoJrAt4fNGs6k89mHpEhZnXUNgaFKwwb5Ud5rIUeLC6tfpsuHNwiWqg==",
"dependencies": {
"Microsoft.Extensions.Configuration": "10.0.3",
"Microsoft.Extensions.Diagnostics.Abstractions": "10.0.3",
"Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.3"
}
},
"Microsoft.Extensions.Diagnostics.Abstractions": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "mQiTzAj7PIJ2A9YXR5QhgulS1fTWhmQc3ckd1Mrf3hKW07d03fBDqx8vVaFw+cRTebDOeB6pNqdWdnRxsi1hBA==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3"
}
},
"Microsoft.Extensions.Logging": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "8D9Er1cGXNjNDIB+VLBNHn386L5ls2FoiG9a6o12gyn+GG3w6jdfUhzT8dtBnKcevE7/fsVA8MS3FBgFfClFtQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "10.0.3",
"Microsoft.Extensions.Logging.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3"
}
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "GEcpTwo7sUoLGGNTqV1FZEuL+tTD9m81NX/mh099dqGNna07/UGZShKQNZRw4hv6nlliSUwYQgSYc7OR99Jufg=="
},
"MSearch.Domain": {
"type": "Project",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.3, )",
"Microsoft.Extensions.Logging.Abstractions": "[10.0.3, )"
}
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "CentralTransitive",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "lxl0WLk7ROgBFAsjcOYjQ8/DVK+VMszxGBzUhgtQmAsTNldLL5pk9NG/cWTsXHq0lUhUEAtZkEE7jOGOA8bGKQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3"
}
}
},
"net10.0/linux-x64": {},
"net10.0/win-arm64": {}
}
}
\ No newline at end of file
SearchProviders/Wikipedia/ServiceCollectionExtensions.cs +26 -0
diff --git a/SearchProviders/Wikipedia/ServiceCollectionExtensions.cs b/SearchProviders/Wikipedia/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..d99987b
@@ -0,0 +1,26 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using MSearch.Domain;
namespace MSearch.SearchProviders.Wikipedia;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddWikipediaSearchProvider(this IServiceCollection services, string site)
{
services.AddOptions<WikipediaOptions>(site).Configure(o => o.Site = site);
services.TryAddTransient<IValidateOptions<WikipediaOptions>, WikipediaOptionsValidator>();
services.AddHttpClient<ISearchProvider, WikipediaSearchProvider>(
nameof(WikipediaSearchProvider),
(httpClient, sp) =>
new WikipediaSearchProvider(
Options.Create(sp.GetRequiredService<IOptionsMonitor<WikipediaOptions>>().Get(site)),
httpClient
)
);
return services;
}
}
SearchProviders/Wikipedia/Wikipedia.csproj +16 -0
diff --git a/SearchProviders/Wikipedia/Wikipedia.csproj b/SearchProviders/Wikipedia/Wikipedia.csproj
new file mode 100644
index 0000000..7ed8531
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>MSearch.SearchProviders.Wikipedia</RootNamespace>
<AssemblyName>MSearch.SearchProviders.Wikipedia</AssemblyName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Domain\Domain.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="HtmlAgilityPack" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Http" />
<PackageReference Include="Microsoft.Extensions.Options" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" />
</ItemGroup>
</Project>
SearchProviders/Wikipedia/WikipediaItem.cs +8 -0
diff --git a/SearchProviders/Wikipedia/WikipediaItem.cs b/SearchProviders/Wikipedia/WikipediaItem.cs
new file mode 100644
index 0000000..8f67bea
@@ -0,0 +1,8 @@
using System.Text.Json.Serialization;
namespace MSearch.SearchProviders.Wikipedia;
internal sealed record WikipediaItem(
[property: JsonPropertyName("title")] string Title,
[property: JsonPropertyName("snippet")] string Snippet
);
SearchProviders/Wikipedia/WikipediaJsonSerializerContext.cs +6 -0
diff --git a/SearchProviders/Wikipedia/WikipediaJsonSerializerContext.cs b/SearchProviders/Wikipedia/WikipediaJsonSerializerContext.cs
new file mode 100644
index 0000000..26ad3ca
@@ -0,0 +1,6 @@
using System.Text.Json.Serialization;
namespace MSearch.SearchProviders.Wikipedia;
[JsonSerializable(typeof(WikipediaResponse))]
internal sealed partial class WikipediaJsonSerializerContext : JsonSerializerContext;
SearchProviders/Wikipedia/WikipediaOptions.cs +13 -0
diff --git a/SearchProviders/Wikipedia/WikipediaOptions.cs b/SearchProviders/Wikipedia/WikipediaOptions.cs
new file mode 100644
index 0000000..e857a83
@@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.Options;
namespace MSearch.SearchProviders.Wikipedia;
internal sealed class WikipediaOptions
{
[Required]
public required string Site { get; set; }
}
[OptionsValidator]
internal sealed partial class WikipediaOptionsValidator : IValidateOptions<WikipediaOptions>;
SearchProviders/Wikipedia/WikipediaQueryResult.cs +8 -0
diff --git a/SearchProviders/Wikipedia/WikipediaQueryResult.cs b/SearchProviders/Wikipedia/WikipediaQueryResult.cs
new file mode 100644
index 0000000..4570e19
@@ -0,0 +1,8 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace MSearch.SearchProviders.Wikipedia;
internal sealed record WikipediaQueryResult(
[property: JsonPropertyName("search")] IReadOnlyCollection<WikipediaItem> Search
);
SearchProviders/Wikipedia/WikipediaResponse.cs +5 -0
diff --git a/SearchProviders/Wikipedia/WikipediaResponse.cs b/SearchProviders/Wikipedia/WikipediaResponse.cs
new file mode 100644
index 0000000..b11093b
@@ -0,0 +1,5 @@
using System.Text.Json.Serialization;
namespace MSearch.SearchProviders.Wikipedia;
internal sealed record WikipediaResponse([property: JsonPropertyName("query")] WikipediaQueryResult Query);
SearchProviders/Wikipedia/WikipediaSearchProvider.cs +47 -0
diff --git a/SearchProviders/Wikipedia/WikipediaSearchProvider.cs b/SearchProviders/Wikipedia/WikipediaSearchProvider.cs
new file mode 100644
index 0000000..daab115
@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Runtime.CompilerServices;
using System.Threading;
using HtmlAgilityPack;
using Microsoft.Extensions.Options;
using MSearch.Domain;
namespace MSearch.SearchProviders.Wikipedia;
internal sealed class WikipediaSearchProvider(IOptions<WikipediaOptions> options, HttpClient httpClient)
: ISearchProvider
{
private readonly string site = options.Value.Site;
public async IAsyncEnumerable<SearchResult> Search(
SearchQuery query,
[EnumeratorCancellation] CancellationToken cancellationToken
)
{
var response = await httpClient.GetFromJsonAsync(
$"https://{site}.wikipedia.org/w/api.php?action=query&list=search&format=json&formatversion=2&srsearch={Uri.EscapeDataString(query.Term)}",
WikipediaJsonSerializerContext.Default.WikipediaResponse,
cancellationToken
);
if (response is null)
{
yield break;
}
foreach (var item in response.Query.Search)
{
yield return Map(item);
}
}
private SearchResult Map(WikipediaItem item) =>
new(item.Title, SanitizeSnippet(item.Snippet), new($"https://{site}.wikipedia.org/wiki/{item.Title}"));
private static string SanitizeSnippet(string snippet)
{
var doc = new HtmlDocument();
doc.LoadHtml(snippet);
return doc.DocumentNode.InnerText;
}
}
SearchProviders/Wikipedia/packages.lock.json +148 -0
diff --git a/SearchProviders/Wikipedia/packages.lock.json b/SearchProviders/Wikipedia/packages.lock.json
new file mode 100644
index 0000000..3f0e88b
@@ -0,0 +1,148 @@
{
"version": 2,
"dependencies": {
"net10.0": {
"CSharpier.MsBuild": {
"type": "Direct",
"requested": "[1.0.2, )",
"resolved": "1.0.2",
"contentHash": "Y98ba4yeefqrJY+MbCXhvb8x4/mCy8zHeuu/dYKwaM4p0Yu2LToI5ffTg/ErcpPuLnwJBNE229STdhf/dlikIw=="
},
"HtmlAgilityPack": {
"type": "Direct",
"requested": "[1.12.4, )",
"resolved": "1.12.4",
"contentHash": "ljqvBabvFwKoLniuoQKO8b5bJfJweKLs4fUNS/V5dsvpo0A8MlJqxxn9XVmP2DaskbUXty6IYaWAi1SArGIMeQ=="
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "bwGMrRcAMWx2s/RDgja97p27rxSz2pEQW0+rX5cWAUWVETVJ/eyxGfjAl8vuG5a+lckWmPIE+vcuaZNVB5YDdw=="
},
"Microsoft.Extensions.Http": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "M5gWob3dtzlF14oto1lR1ZuSJrR0gGc+obv7zY9LGmX5y3Ndpve29MrrjqJW/m4CFud4TE/KFUuHjjtwxhCO8g==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Diagnostics": "10.0.3",
"Microsoft.Extensions.Logging": "10.0.3",
"Microsoft.Extensions.Logging.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3"
}
},
"Microsoft.Extensions.Options": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "hU6WzGTPvPoLA2ng1ILvWQb3g0qORdlHNsxI8IcPLumJb3suimYUl+bbDzdo1V4KFsvVhnMWzysHpKbZaoDQPQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Options.ConfigurationExtensions": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "bn6QoBbbvwmzLIFyxrnL2/e+sqoNUOGbHyfWK9DPONMv1mDCYHm/C7MusYASM31b2lUx6OiDmonb3v+dv5t0nA==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.Configuration.Binder": "10.0.3",
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3",
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Configuration": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "H1Cjv2xmm7O3iAGmFTcnSM0ZhLQ/7SqefmAvSJoT1PbXoxeYc2fo0mCLn2JlVbr9E6YpoU9q/o0fI9neDJB0xQ==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "xVDHL0+SIgemfh95fTO9cGLe17TWv/ZP0n7m01z8X6pzt2DmQpucioWR/mYZA1sRlkWnkXzfl0JweLNWmE9WMg==",
"dependencies": {
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Configuration.Binder": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "759UhpKaR5Jsll9kXpkft4z/7tpeF7Dw2rTY/9f9JchaSQTpRFNIPkZFZvoo7fFpbjUaqtDlO5aiGpmQrp/EUA==",
"dependencies": {
"Microsoft.Extensions.Configuration": "10.0.3",
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3"
}
},
"Microsoft.Extensions.DependencyInjection": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "2DLOmC0EkB2smVK8lPP1PIKEgL1arE3CMp9XSIQB/Y7ev5nnnyuM/PizKJ6QfLD08QCYoopSC9SFdbYglDomYg==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3"
}
},
"Microsoft.Extensions.Diagnostics": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "tc0R6i2T+138taoxFPQXb7Sy/4rtq4ytoJrAt4fNGs6k89mHpEhZnXUNgaFKwwb5Ud5rIUeLC6tfpsuHNwiWqg==",
"dependencies": {
"Microsoft.Extensions.Configuration": "10.0.3",
"Microsoft.Extensions.Diagnostics.Abstractions": "10.0.3",
"Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.3"
}
},
"Microsoft.Extensions.Diagnostics.Abstractions": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "mQiTzAj7PIJ2A9YXR5QhgulS1fTWhmQc3ckd1Mrf3hKW07d03fBDqx8vVaFw+cRTebDOeB6pNqdWdnRxsi1hBA==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3"
}
},
"Microsoft.Extensions.Logging": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "8D9Er1cGXNjNDIB+VLBNHn386L5ls2FoiG9a6o12gyn+GG3w6jdfUhzT8dtBnKcevE7/fsVA8MS3FBgFfClFtQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "10.0.3",
"Microsoft.Extensions.Logging.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3"
}
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "GEcpTwo7sUoLGGNTqV1FZEuL+tTD9m81NX/mh099dqGNna07/UGZShKQNZRw4hv6nlliSUwYQgSYc7OR99Jufg=="
},
"MSearch.Domain": {
"type": "Project",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.3, )",
"Microsoft.Extensions.Logging.Abstractions": "[10.0.3, )"
}
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "CentralTransitive",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "lxl0WLk7ROgBFAsjcOYjQ8/DVK+VMszxGBzUhgtQmAsTNldLL5pk9NG/cWTsXHq0lUhUEAtZkEE7jOGOA8bGKQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3"
}
}
},
"net10.0/linux-x64": {},
"net10.0/win-arm64": {}
}
}
\ No newline at end of file
SearchProviders/YouTube/ServiceCollectionExtensions.cs +18 -0
diff --git a/SearchProviders/YouTube/ServiceCollectionExtensions.cs b/SearchProviders/YouTube/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..ce74e3d
@@ -0,0 +1,18 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using MSearch.Domain;
namespace MSearch.SearchProviders.YouTube;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddYouTubeSearchProvider(this IServiceCollection services)
{
services.AddOptions<YouTubeOptions>().BindConfiguration("SearchProviders:YouTube").ValidateOnStart();
services.AddTransient<IValidateOptions<YouTubeOptions>, YouTubeOptionsValidator>();
services.AddHttpClient<ISearchProvider, YouTubeSearchProvider>(nameof(YouTubeSearchProvider));
return services;
}
}
SearchProviders/YouTube/YouTube.csproj +15 -0
diff --git a/SearchProviders/YouTube/YouTube.csproj b/SearchProviders/YouTube/YouTube.csproj
new file mode 100644
index 0000000..c0e00af
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>MSearch.SearchProviders.YouTube</RootNamespace>
<AssemblyName>MSearch.SearchProviders.YouTube</AssemblyName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Domain\Domain.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Http" />
<PackageReference Include="Microsoft.Extensions.Options" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" />
</ItemGroup>
</Project>
SearchProviders/YouTube/YouTubeId.cs +19 -0
diff --git a/SearchProviders/YouTube/YouTubeId.cs b/SearchProviders/YouTube/YouTubeId.cs
new file mode 100644
index 0000000..1188f7a
@@ -0,0 +1,19 @@
using System.Text.Json.Serialization;
namespace MSearch.SearchProviders.YouTube;
[JsonPolymorphic(
TypeDiscriminatorPropertyName = "kind",
IgnoreUnrecognizedTypeDiscriminators = true,
UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType
)]
[JsonDerivedType(typeof(Channel), "youtube#channel")]
[JsonDerivedType(typeof(Video), "youtube#video")]
internal record YouTubeId
{
private YouTubeId() { }
public sealed record Channel([property: JsonPropertyName("channelId")] string Id) : YouTubeId();
public sealed record Video([property: JsonPropertyName("videoId")] string Id) : YouTubeId();
}
SearchProviders/YouTube/YouTubeItem.cs +8 -0
diff --git a/SearchProviders/YouTube/YouTubeItem.cs b/SearchProviders/YouTube/YouTubeItem.cs
new file mode 100644
index 0000000..338521b
@@ -0,0 +1,8 @@
using System.Text.Json.Serialization;
namespace MSearch.SearchProviders.YouTube;
internal sealed record YouTubeItem(
[property: JsonPropertyName("id")] YouTubeId Id,
[property: JsonPropertyName("snippet")] YouTubeSnippet Snippet
);
SearchProviders/YouTube/YouTubeJsonSerializerContext.cs +6 -0
diff --git a/SearchProviders/YouTube/YouTubeJsonSerializerContext.cs b/SearchProviders/YouTube/YouTubeJsonSerializerContext.cs
new file mode 100644
index 0000000..222c880
@@ -0,0 +1,6 @@
using System.Text.Json.Serialization;
namespace MSearch.SearchProviders.YouTube;
[JsonSerializable(typeof(YouTubeSearchResponse))]
internal sealed partial class YouTubeJsonSerializerContext : JsonSerializerContext;
SearchProviders/YouTube/YouTubeOptions.cs +13 -0
diff --git a/SearchProviders/YouTube/YouTubeOptions.cs b/SearchProviders/YouTube/YouTubeOptions.cs
new file mode 100644
index 0000000..9b9a5be
@@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.Options;
namespace MSearch.SearchProviders.YouTube;
internal sealed class YouTubeOptions
{
[Required]
public required string ApiKey { get; set; }
}
[OptionsValidator]
internal sealed partial class YouTubeOptionsValidator : IValidateOptions<YouTubeOptions>;
SearchProviders/YouTube/YouTubeSearchProvider.cs +49 -0
diff --git a/SearchProviders/YouTube/YouTubeSearchProvider.cs b/SearchProviders/YouTube/YouTubeSearchProvider.cs
new file mode 100644
index 0000000..6d49cc4
@@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Runtime.CompilerServices;
using System.Threading;
using Microsoft.Extensions.Options;
using MSearch.Domain;
namespace MSearch.SearchProviders.YouTube;
internal sealed class YouTubeSearchProvider(IOptions<YouTubeOptions> options, HttpClient httpClient) : ISearchProvider
{
private readonly string apiKey = options.Value.ApiKey;
public async IAsyncEnumerable<SearchResult> Search(
SearchQuery query,
[EnumeratorCancellation] CancellationToken cancellationToken
)
{
var response = await httpClient.GetFromJsonAsync(
$"https://www.googleapis.com/youtube/v3/search?key={apiKey}&part=snippet&q={Uri.EscapeDataString(query.Term)}",
YouTubeJsonSerializerContext.Default.YouTubeSearchResponse,
cancellationToken
);
if (response is null)
{
yield break;
}
foreach (var item in response.Items)
{
if (Map(item) is { } result)
{
yield return result;
}
}
}
private static SearchResult? Map(YouTubeItem item) =>
Map(item.Id) is Uri url ? new(item.Snippet.Title, item.Snippet.Description, url) : null;
private static Uri? Map(YouTubeId id) =>
id switch
{
YouTubeId.Channel(var channelId) => new($"https://www.youtube.com/channel/{channelId}"),
YouTubeId.Video(var videoId) => new($"https://www.youtube.com/video/{videoId}"),
YouTubeId => null,
};
}
SearchProviders/YouTube/YouTubeSearchResponse.cs +8 -0
diff --git a/SearchProviders/YouTube/YouTubeSearchResponse.cs b/SearchProviders/YouTube/YouTubeSearchResponse.cs
new file mode 100644
index 0000000..6c25b51
@@ -0,0 +1,8 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace MSearch.SearchProviders.YouTube;
internal sealed record YouTubeSearchResponse(
[property: JsonPropertyName("items")] IReadOnlyCollection<YouTubeItem> Items
);
SearchProviders/YouTube/YouTubeSnippet.cs +8 -0
diff --git a/SearchProviders/YouTube/YouTubeSnippet.cs b/SearchProviders/YouTube/YouTubeSnippet.cs
new file mode 100644
index 0000000..bec14cb
@@ -0,0 +1,8 @@
using System.Text.Json.Serialization;
namespace MSearch.SearchProviders.YouTube;
internal sealed record YouTubeSnippet(
[property: JsonPropertyName("title")] string Title,
[property: JsonPropertyName("description")] string Description
);
SearchProviders/YouTube/packages.lock.json +142 -0
diff --git a/SearchProviders/YouTube/packages.lock.json b/SearchProviders/YouTube/packages.lock.json
new file mode 100644
index 0000000..26ae089
@@ -0,0 +1,142 @@
{
"version": 2,
"dependencies": {
"net10.0": {
"CSharpier.MsBuild": {
"type": "Direct",
"requested": "[1.0.2, )",
"resolved": "1.0.2",
"contentHash": "Y98ba4yeefqrJY+MbCXhvb8x4/mCy8zHeuu/dYKwaM4p0Yu2LToI5ffTg/ErcpPuLnwJBNE229STdhf/dlikIw=="
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "bwGMrRcAMWx2s/RDgja97p27rxSz2pEQW0+rX5cWAUWVETVJ/eyxGfjAl8vuG5a+lckWmPIE+vcuaZNVB5YDdw=="
},
"Microsoft.Extensions.Http": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "M5gWob3dtzlF14oto1lR1ZuSJrR0gGc+obv7zY9LGmX5y3Ndpve29MrrjqJW/m4CFud4TE/KFUuHjjtwxhCO8g==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Diagnostics": "10.0.3",
"Microsoft.Extensions.Logging": "10.0.3",
"Microsoft.Extensions.Logging.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3"
}
},
"Microsoft.Extensions.Options": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "hU6WzGTPvPoLA2ng1ILvWQb3g0qORdlHNsxI8IcPLumJb3suimYUl+bbDzdo1V4KFsvVhnMWzysHpKbZaoDQPQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Options.ConfigurationExtensions": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "bn6QoBbbvwmzLIFyxrnL2/e+sqoNUOGbHyfWK9DPONMv1mDCYHm/C7MusYASM31b2lUx6OiDmonb3v+dv5t0nA==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.Configuration.Binder": "10.0.3",
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3",
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Configuration": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "H1Cjv2xmm7O3iAGmFTcnSM0ZhLQ/7SqefmAvSJoT1PbXoxeYc2fo0mCLn2JlVbr9E6YpoU9q/o0fI9neDJB0xQ==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3",
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "xVDHL0+SIgemfh95fTO9cGLe17TWv/ZP0n7m01z8X6pzt2DmQpucioWR/mYZA1sRlkWnkXzfl0JweLNWmE9WMg==",
"dependencies": {
"Microsoft.Extensions.Primitives": "10.0.3"
}
},
"Microsoft.Extensions.Configuration.Binder": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "759UhpKaR5Jsll9kXpkft4z/7tpeF7Dw2rTY/9f9JchaSQTpRFNIPkZFZvoo7fFpbjUaqtDlO5aiGpmQrp/EUA==",
"dependencies": {
"Microsoft.Extensions.Configuration": "10.0.3",
"Microsoft.Extensions.Configuration.Abstractions": "10.0.3"
}
},
"Microsoft.Extensions.DependencyInjection": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "2DLOmC0EkB2smVK8lPP1PIKEgL1arE3CMp9XSIQB/Y7ev5nnnyuM/PizKJ6QfLD08QCYoopSC9SFdbYglDomYg==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3"
}
},
"Microsoft.Extensions.Diagnostics": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "tc0R6i2T+138taoxFPQXb7Sy/4rtq4ytoJrAt4fNGs6k89mHpEhZnXUNgaFKwwb5Ud5rIUeLC6tfpsuHNwiWqg==",
"dependencies": {
"Microsoft.Extensions.Configuration": "10.0.3",
"Microsoft.Extensions.Diagnostics.Abstractions": "10.0.3",
"Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.3"
}
},
"Microsoft.Extensions.Diagnostics.Abstractions": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "mQiTzAj7PIJ2A9YXR5QhgulS1fTWhmQc3ckd1Mrf3hKW07d03fBDqx8vVaFw+cRTebDOeB6pNqdWdnRxsi1hBA==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3"
}
},
"Microsoft.Extensions.Logging": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "8D9Er1cGXNjNDIB+VLBNHn386L5ls2FoiG9a6o12gyn+GG3w6jdfUhzT8dtBnKcevE7/fsVA8MS3FBgFfClFtQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "10.0.3",
"Microsoft.Extensions.Logging.Abstractions": "10.0.3",
"Microsoft.Extensions.Options": "10.0.3"
}
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "GEcpTwo7sUoLGGNTqV1FZEuL+tTD9m81NX/mh099dqGNna07/UGZShKQNZRw4hv6nlliSUwYQgSYc7OR99Jufg=="
},
"MSearch.Domain": {
"type": "Project",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.3, )",
"Microsoft.Extensions.Logging.Abstractions": "[10.0.3, )"
}
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "CentralTransitive",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "lxl0WLk7ROgBFAsjcOYjQ8/DVK+VMszxGBzUhgtQmAsTNldLL5pk9NG/cWTsXHq0lUhUEAtZkEE7jOGOA8bGKQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3"
}
}
},
"net10.0/linux-x64": {},
"net10.0/win-arm64": {}
}
}
\ No newline at end of file
ServiceDefaults/Extensions.cs +97 -0
diff --git a/ServiceDefaults/Extensions.cs b/ServiceDefaults/Extensions.cs
new file mode 100644
index 0000000..c97df5a
@@ -0,0 +1,97 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;
using OpenTelemetry;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
namespace Microsoft.Extensions.Hosting;
public static class Extensions
{
private const string HealthEndpointPath = "/health";
private const string AlivenessEndpointPath = "/alive";
public static TBuilder AddServiceDefaults<TBuilder>(this TBuilder builder)
where TBuilder : IHostApplicationBuilder
{
builder.ConfigureOpenTelemetry();
builder.AddDefaultHealthChecks();
builder.Services.AddServiceDiscovery();
builder.Services.ConfigureHttpClientDefaults(http =>
{
http.AddStandardResilienceHandler();
http.ConfigureHttpClient(httpClient =>
{
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("MSearch");
});
});
return builder;
}
public static TBuilder ConfigureOpenTelemetry<TBuilder>(this TBuilder builder)
where TBuilder : IHostApplicationBuilder
{
builder.Logging.AddOpenTelemetry(logging =>
{
logging.IncludeFormattedMessage = true;
logging.IncludeScopes = true;
});
builder
.Services.AddOpenTelemetry()
.WithMetrics(metrics =>
metrics.AddAspNetCoreInstrumentation().AddHttpClientInstrumentation().AddRuntimeInstrumentation()
)
.WithTracing(tracing =>
tracing
.AddSource(builder.Environment.ApplicationName)
.AddAspNetCoreInstrumentation(tracing =>
tracing.Filter = context =>
!context.Request.Path.StartsWithSegments(HealthEndpointPath)
&& !context.Request.Path.StartsWithSegments(AlivenessEndpointPath)
)
.AddHttpClientInstrumentation()
);
builder.AddOpenTelemetryExporters();
return builder;
}
private static TBuilder AddOpenTelemetryExporters<TBuilder>(this TBuilder builder)
where TBuilder : IHostApplicationBuilder
{
var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
if (useOtlpExporter)
{
builder.Services.AddOpenTelemetry().UseOtlpExporter();
}
return builder;
}
public static TBuilder AddDefaultHealthChecks<TBuilder>(this TBuilder builder)
where TBuilder : IHostApplicationBuilder
{
builder.Services.AddHealthChecks().AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);
return builder;
}
public static WebApplication MapDefaultEndpoints(this WebApplication app)
{
app.MapHealthChecks(HealthEndpointPath);
app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions { Predicate = r => r.Tags.Contains("live") });
return app;
}
}
ServiceDefaults/ServiceDefaults.csproj +17 -0
diff --git a/ServiceDefaults/ServiceDefaults.csproj b/ServiceDefaults/ServiceDefaults.csproj
new file mode 100644
index 0000000..f14bdf1
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>Microsoft.Extensions.Hosting</RootNamespace>
<AssemblyName>MSearch.ServiceDefaults</AssemblyName>
<IsAspireSharedProject>true</IsAspireSharedProject>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" />
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" />
</ItemGroup>
</Project>
ServiceDefaults/packages.lock.json +183 -0
diff --git a/ServiceDefaults/packages.lock.json b/ServiceDefaults/packages.lock.json
new file mode 100644
index 0000000..0373bfa
@@ -0,0 +1,183 @@
{
"version": 2,
"dependencies": {
"net10.0": {
"CSharpier.MsBuild": {
"type": "Direct",
"requested": "[1.0.2, )",
"resolved": "1.0.2",
"contentHash": "Y98ba4yeefqrJY+MbCXhvb8x4/mCy8zHeuu/dYKwaM4p0Yu2LToI5ffTg/ErcpPuLnwJBNE229STdhf/dlikIw=="
},
"Microsoft.Extensions.Http.Resilience": {
"type": "Direct",
"requested": "[10.3.0, )",
"resolved": "10.3.0",
"contentHash": "P4+s/eUH3dZdn1HnivSL2dh6/Jb0ndLt2l88oQPZ9BYdyb4tSRAsnz4QkJHGfPA9lS/XblI5QYsxEdfkurPvIg==",
"dependencies": {
"Microsoft.Extensions.Http.Diagnostics": "10.3.0",
"Microsoft.Extensions.Resilience": "10.3.0"
}
},
"Microsoft.Extensions.ServiceDiscovery": {
"type": "Direct",
"requested": "[10.3.0, )",
"resolved": "10.3.0",
"contentHash": "C7onh6YDQKbZjKmAWEef1RDosjxPxA3PZdLob5lhS1AQuKgw0vTHnCKUA1KAhNlzhyfOPVP6tc0cLIPCDoBvoA==",
"dependencies": {
"Microsoft.Extensions.ServiceDiscovery.Abstractions": "10.3.0"
}
},
"OpenTelemetry.Exporter.OpenTelemetryProtocol": {
"type": "Direct",
"requested": "[1.15.0, )",
"resolved": "1.15.0",
"contentHash": "VH8ANc/js9IRvfYt0Q2UaAxNCOWm+IU+vWrtoH7pfx4oWPVdISUt+9uWfBCFMWZg5WzQip5dhslyDjeyZXXfSQ==",
"dependencies": {
"OpenTelemetry": "1.15.0"
}
},
"OpenTelemetry.Extensions.Hosting": {
"type": "Direct",
"requested": "[1.15.0, )",
"resolved": "1.15.0",
"contentHash": "RixjKyB1pbYGhWdvPto4KJs+exdQknJsnjUO9WszdLles5Vcd0EYzxPNJdwmLjYfP+Jfbr4B5nktM4ZgeHSWtg==",
"dependencies": {
"OpenTelemetry": "1.15.0"
}
},
"OpenTelemetry.Instrumentation.AspNetCore": {
"type": "Direct",
"requested": "[1.15.0, )",
"resolved": "1.15.0",
"contentHash": "mte1nRYefxjed2syXgVWq3UCfMKO7MkebvTZmf0O1aLgVgCktLsVjQ6mftyjIbWGBBCHN0wg+Glxj8BSFS70pQ==",
"dependencies": {
"OpenTelemetry.Api.ProviderBuilderExtensions": "[1.15.0, 2.0.0)"
}
},
"OpenTelemetry.Instrumentation.Http": {
"type": "Direct",
"requested": "[1.15.0, )",
"resolved": "1.15.0",
"contentHash": "uToc7bUp8IEdb0ny9mKsL6FrrYelINPzxxiSShJgOf4XmQc4Azww6S5RjRj24YhsOn2a1MABOrxfVTZXtDk4Eg==",
"dependencies": {
"OpenTelemetry.Api.ProviderBuilderExtensions": "[1.15.0, 2.0.0)"
}
},
"OpenTelemetry.Instrumentation.Runtime": {
"type": "Direct",
"requested": "[1.15.0, )",
"resolved": "1.15.0",
"contentHash": "OOvpqR/j2Pb6+tWhHNODIbSJ53Or/MDtTiXEyrsWI02K2lLAgvBFcxUOrHggS/8015cYR3AdSaXv6NZrkz5yQA==",
"dependencies": {
"OpenTelemetry.Api": "[1.15.0, 2.0.0)"
}
},
"Microsoft.Extensions.AmbientMetadata.Application": {
"type": "Transitive",
"resolved": "10.3.0",
"contentHash": "Oh/FQJrTZqiqZuFktqDCwLFgxIUnNATZx46AwUTf5A/+FmK/TzPf/iwSqMK85QlioLD9ehOxWe0NBfsCSkp5pw=="
},
"Microsoft.Extensions.Compliance.Abstractions": {
"type": "Transitive",
"resolved": "10.3.0",
"contentHash": "b3gmxtyX0n8bQFuZ679f8QiOWqjeZKFc6XXPUsYNFzMcWi0Q44Ej3pB41N/QQ0maX380kmOMF36bLSBHNThXAg=="
},
"Microsoft.Extensions.DependencyInjection.AutoActivation": {
"type": "Transitive",
"resolved": "10.3.0",
"contentHash": "g6/S5rhP1XNBqeDa9zKXri/uDKe6gjzSqGsocuySb1OdVm8fz/M9YcWTJsCw5RHF8xIb/B3NgsfBPQF9Emjk3Q=="
},
"Microsoft.Extensions.Diagnostics.ExceptionSummarization": {
"type": "Transitive",
"resolved": "10.3.0",
"contentHash": "TZAZsAFThNQDqCnWaGxV/X7OPSA7b+kY7wNTPNvPvSWa0jcTbmGVyzIY/feF4OgyAUUdzSnnLW6ri2Q+4keM4A=="
},
"Microsoft.Extensions.Http.Diagnostics": {
"type": "Transitive",
"resolved": "10.3.0",
"contentHash": "/xuNWNxI4WLVatiTvaqfLd5ijFhQ/qvE14bOyWxeEWmXJkjh/g2G/5TdzMfoe0afq16OdWLGbrD9gWHo178hbg==",
"dependencies": {
"Microsoft.Extensions.Telemetry": "10.3.0"
}
},
"Microsoft.Extensions.Resilience": {
"type": "Transitive",
"resolved": "10.3.0",
"contentHash": "xc0dZuPkBaVIdMlODDppmNY/dxE27wIQ46gTzStoFXO4/yVcOMKlPmtr9vTP4edyXBRizGxPtcAFmqxZ5gPTkQ==",
"dependencies": {
"Microsoft.Extensions.Diagnostics.ExceptionSummarization": "10.3.0",
"Microsoft.Extensions.Telemetry.Abstractions": "10.3.0",
"Polly.Extensions": "8.4.2",
"Polly.RateLimiting": "8.4.2"
}
},
"Microsoft.Extensions.ServiceDiscovery.Abstractions": {
"type": "Transitive",
"resolved": "10.3.0",
"contentHash": "2j3SdFGgqFQFP6xQpkbqxt9SdzsT4/FK47RQ7MoCAZnKZiZEDuLDq9PfKLqON8HbGc09jHZ02x0tHIjleVab2A=="
},
"Microsoft.Extensions.Telemetry": {
"type": "Transitive",
"resolved": "10.3.0",
"contentHash": "M1esrIGmwU2JBY0JpwdlUTXTNBXSBFEs+41bYBd59+9/vCaXw+vGhtYcCL+JXeGmxTLUHmuXcKbX/uVCSFcuzA==",
"dependencies": {
"Microsoft.Extensions.AmbientMetadata.Application": "10.3.0",
"Microsoft.Extensions.DependencyInjection.AutoActivation": "10.3.0",
"Microsoft.Extensions.Telemetry.Abstractions": "10.3.0"
}
},
"Microsoft.Extensions.Telemetry.Abstractions": {
"type": "Transitive",
"resolved": "10.3.0",
"contentHash": "aKxH6ZsGAewGF8uSXyx1WkjqItwZA+hd1hhQ/4i7o5injCWSdr9vIZ3R3djJfy8OG3xaWK+LZY/+slVvlnwEHw==",
"dependencies": {
"Microsoft.Extensions.Compliance.Abstractions": "10.3.0"
}
},
"OpenTelemetry": {
"type": "Transitive",
"resolved": "1.15.0",
"contentHash": "7mS/oZFF8S6xyqGQfMU1btp0nXJQUPWV535Vp/XMLYwRAUv36xQN+U4vufWBF1+z4HnRTOwuFHtUSGnHbyN6FQ==",
"dependencies": {
"OpenTelemetry.Api.ProviderBuilderExtensions": "1.15.0"
}
},
"OpenTelemetry.Api": {
"type": "Transitive",
"resolved": "1.15.0",
"contentHash": "vk5OGdf6K9kQScCWo3bRjhDWCv6Pqw92IpX4dlARZ8B1WL7/2NGTDtCkkw42eQf7UdwyoHKzVvMH/PtL8d6z7w=="
},
"OpenTelemetry.Api.ProviderBuilderExtensions": {
"type": "Transitive",
"resolved": "1.15.0",
"contentHash": "OnuSUlRpGvowkOzGFQfy+KZFu0cITfKfh2IYJJiZskxVJiOuexwOOuvfDAgpJdmTzVWAHjYdz2shcHZaJ06UjQ==",
"dependencies": {
"OpenTelemetry.Api": "1.15.0"
}
},
"Polly.Core": {
"type": "Transitive",
"resolved": "8.4.2",
"contentHash": "BpE2I6HBYYA5tF0Vn4eoQOGYTYIK1BlF5EXVgkWGn3mqUUjbXAr13J6fZVbp7Q3epRR8yshacBMlsHMhpOiV3g=="
},
"Polly.Extensions": {
"type": "Transitive",
"resolved": "8.4.2",
"contentHash": "GZ9vRVmR0jV2JtZavt+pGUsQ1O1cuRKG7R7VOZI6ZDy9y6RNPvRvXK1tuS4ffUrv8L0FTea59oEuQzgS0R7zSA==",
"dependencies": {
"Polly.Core": "8.4.2"
}
},
"Polly.RateLimiting": {
"type": "Transitive",
"resolved": "8.4.2",
"contentHash": "ehTImQ/eUyO07VYW2WvwSmU9rRH200SKJ/3jku9rOkyWE0A2JxNFmAVms8dSn49QLSjmjFRRSgfNyOgr/2PSmA==",
"dependencies": {
"Polly.Core": "8.4.2"
}
}
},
"net10.0/linux-x64": {},
"net10.0/win-arm64": {}
}
}
\ No newline at end of file
Web/Components/LayoutComponent.razor +11 -0
diff --git a/Web/Components/LayoutComponent.razor b/Web/Components/LayoutComponent.razor
new file mode 100644
index 0000000..53615ed
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link rel="stylesheet" href="@Assets["app.css"]" />
</head>
<body>
</body>
</html>
Web/Components/OpenSearchDescriptor.razor +19 -0
diff --git a/Web/Components/OpenSearchDescriptor.razor b/Web/Components/OpenSearchDescriptor.razor
new file mode 100644
index 0000000..e66ea60
@@ -0,0 +1,19 @@
@using Microsoft.AspNetCore.Http
@inject IHttpContextAccessor httpContextAccessor
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
<ShortName>MSearch</ShortName>
<Description>A search aggregator</Description>
<InputEncoding>utf-8</InputEncoding>
<Url type="text/html"
template="@httpContextAccessor.HttpContext?.Request.Scheme://@httpContextAccessor.HttpContext?.Request.Host/?q={searchTerms}" />
</OpenSearchDescription>
@code {
protected override void OnInitialized()
{
httpContextAccessor.HttpContext?.Response.Headers.ContentType = "application/opensearchdescription+xml";
base.OnInitialized();
}
}
\ No newline at end of file
Web/Components/SearchResultComponent.razor +16 -0
diff --git a/Web/Components/SearchResultComponent.razor b/Web/Components/SearchResultComponent.razor
new file mode 100644
index 0000000..d042aed
@@ -0,0 +1,16 @@
<div>
<h2><a href="@result.Url">@result.Title</a></h2>
@if (!string.IsNullOrEmpty(result.Summary))
{
<p>@result.Summary</p>
}
</div>
@code {
private SearchResult result;
public SearchResultComponent(SearchResult result)
{
this.result = result;
}
}
Web/Components/_Imports.razor +11 -0
diff --git a/Web/Components/_Imports.razor b/Web/Components/_Imports.razor
new file mode 100644
index 0000000..e8ecf38
@@ -0,0 +1,11 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using MSearch.Domain
@using MSearch.Web
@using MSearch.Web.Components
Web/Program.cs +117 -0
diff --git a/Web/Program.cs b/Web/Program.cs
new file mode 100644
index 0000000..c478eee
@@ -0,0 +1,117 @@
using System;
using System.IO;
using System.Net.Mime;
using System.Threading;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using MSearch.Domain;
using MSearch.SearchProviders.GitHub;
using MSearch.SearchProviders.HackerNews;
using MSearch.SearchProviders.MicrosoftLearn;
using MSearch.SearchProviders.MozillaDeveloperNetwork;
using MSearch.SearchProviders.OpenStreetMap;
using MSearch.SearchProviders.Reddit;
using MSearch.SearchProviders.Spotify;
using MSearch.SearchProviders.StackExchange;
using MSearch.SearchProviders.TheMovieDb;
using MSearch.SearchProviders.Wikipedia;
using MSearch.SearchProviders.YouTube;
using MSearch.Web.Components;
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder
.Services.AddSearchService()
.AddGitHubSearchProvider()
.AddHackerNewsSearchProvider()
.AddMicrosoftLearnSearchProvider()
.AddMozillaDeveloperNetworkSearchProvider()
.AddOpenStreetMapSearchProvider()
.AddRedditSearchProvider()
.AddSpotifySearchProvider()
.AddStackExchangeSearchProvider("stackoverflow")
.AddTheMovieDbSearchProvider()
.AddWikipediaSearchProvider("en")
.AddYouTubeSearchProvider();
builder.Services.AddHttpContextAccessor();
builder.Services.AddRazorComponents();
var app = builder.Build();
app.UseStaticFiles();
app.MapGet(
"/",
async (
[FromServices] IServiceProvider serviceProvider,
[FromServices] ILoggerFactory loggerFactory,
[FromServices] SearchService searchService,
HttpResponse response,
[FromQuery(Name = "q")] string? query = null,
CancellationToken cancellationToken = default
) =>
{
response.Headers.ContentType = MediaTypeNames.Text.Html;
await using var bodyWriter = new StreamWriter(response.Body);
await bodyWriter.WriteAsync(
"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<title>MSearch</title>
<link rel="stylesheet" href="app.css" />
<link rel="search" type="application/opensearchdescription+xml" href="opensearch" title="MSearch">
</head>
<body>
<main>
""".AsMemory(),
cancellationToken
);
await using var htmlRenderer = new StaticHtmlRenderer(serviceProvider, loggerFactory);
await htmlRenderer.Dispatcher.InvokeAsync(async () =>
{
{
var output = htmlRenderer.BeginRenderingComponent(new LayoutComponent(), ParameterView.Empty);
await bodyWriter.WriteAsync(output.ToHtmlString().AsMemory(), cancellationToken);
}
if (string.IsNullOrWhiteSpace(query))
{
return;
}
await foreach (var result in searchService.Search(query, cancellationToken))
{
var output = htmlRenderer.BeginRenderingComponent(
new SearchResultComponent(result),
ParameterView.Empty
);
await bodyWriter.WriteAsync(output.ToHtmlString().AsMemory(), cancellationToken);
}
});
}
);
app.MapGet("/opensearch", () => new RazorComponentResult<OpenSearchDescriptor>());
app.MapDefaultEndpoints();
app.Run();
Web/Properties/launchSettings.json +23 -0
diff --git a/Web/Properties/launchSettings.json b/Web/Properties/launchSettings.json
new file mode 100644
index 0000000..d11d8bf
@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5121",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7221;http://localhost:5121",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
Web/Web.csproj +24 -0
diff --git a/Web/Web.csproj b/Web/Web.csproj
new file mode 100644
index 0000000..e9237e5
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<RootNamespace>MSearch.Web</RootNamespace>
<AssemblyName>MSearch.Web</AssemblyName>
<UserSecretsId>d37af2f6-6d31-48a0-8010-bfb4a733a353</UserSecretsId>
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
<NoWarn>$(NoWarn);RZ10012</NoWarn>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Domain\Domain.csproj" />
<ProjectReference Include="..\SearchProviders\GitHub\GitHub.csproj" />
<ProjectReference Include="..\SearchProviders\HackerNews\HackerNews.csproj" />
<ProjectReference Include="..\SearchProviders\MicrosoftLearn\MicrosoftLearn.csproj" />
<ProjectReference Include="..\SearchProviders\MozillaDeveloperNetwork\MozillaDeveloperNetwork.csproj" />
<ProjectReference Include="..\SearchProviders\OpenStreetMap\OpenStreetMap.csproj" />
<ProjectReference Include="..\SearchProviders\Reddit\Reddit.csproj" />
<ProjectReference Include="..\SearchProviders\Spotify\Spotify.csproj" />
<ProjectReference Include="..\SearchProviders\StackExchange\StackExchange.csproj" />
<ProjectReference Include="..\SearchProviders\TheMovieDb\TheMovieDb.csproj" />
<ProjectReference Include="..\SearchProviders\Wikipedia\Wikipedia.csproj" />
<ProjectReference Include="..\SearchProviders\YouTube\YouTube.csproj" />
<ProjectReference Include="..\ServiceDefaults\ServiceDefaults.csproj" />
</ItemGroup>
</Project>
Web/appsettings.Development.json +8 -0
diff --git a/Web/appsettings.Development.json b/Web/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
Web/appsettings.json +9 -0
diff --git a/Web/appsettings.json b/Web/appsettings.json
new file mode 100644
index 0000000..10f68b8
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
Web/packages.lock.json +292 -0
diff --git a/Web/packages.lock.json b/Web/packages.lock.json
new file mode 100644
index 0000000..0bda184
@@ -0,0 +1,292 @@
{
"version": 2,
"dependencies": {
"net10.0": {
"CSharpier.MsBuild": {
"type": "Direct",
"requested": "[1.0.2, )",
"resolved": "1.0.2",
"contentHash": "Y98ba4yeefqrJY+MbCXhvb8x4/mCy8zHeuu/dYKwaM4p0Yu2LToI5ffTg/ErcpPuLnwJBNE229STdhf/dlikIw=="
},
"Microsoft.AspNetCore.App.Internal.Assets": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "mr3Zn+ht8lijYvlMIasftw9opU9hsLKDdnOgQMmYI3RjWPJLOF9l8+YHDseRkTs97wOrULmJgo/NDCmzL/EGDg=="
},
"Microsoft.Extensions.AmbientMetadata.Application": {
"type": "Transitive",
"resolved": "10.3.0",
"contentHash": "Oh/FQJrTZqiqZuFktqDCwLFgxIUnNATZx46AwUTf5A/+FmK/TzPf/iwSqMK85QlioLD9ehOxWe0NBfsCSkp5pw=="
},
"Microsoft.Extensions.Compliance.Abstractions": {
"type": "Transitive",
"resolved": "10.3.0",
"contentHash": "b3gmxtyX0n8bQFuZ679f8QiOWqjeZKFc6XXPUsYNFzMcWi0Q44Ej3pB41N/QQ0maX380kmOMF36bLSBHNThXAg=="
},
"Microsoft.Extensions.DependencyInjection.AutoActivation": {
"type": "Transitive",
"resolved": "10.3.0",
"contentHash": "g6/S5rhP1XNBqeDa9zKXri/uDKe6gjzSqGsocuySb1OdVm8fz/M9YcWTJsCw5RHF8xIb/B3NgsfBPQF9Emjk3Q=="
},
"Microsoft.Extensions.Diagnostics.ExceptionSummarization": {
"type": "Transitive",
"resolved": "10.3.0",
"contentHash": "TZAZsAFThNQDqCnWaGxV/X7OPSA7b+kY7wNTPNvPvSWa0jcTbmGVyzIY/feF4OgyAUUdzSnnLW6ri2Q+4keM4A=="
},
"Microsoft.Extensions.Http.Diagnostics": {
"type": "Transitive",
"resolved": "10.3.0",
"contentHash": "/xuNWNxI4WLVatiTvaqfLd5ijFhQ/qvE14bOyWxeEWmXJkjh/g2G/5TdzMfoe0afq16OdWLGbrD9gWHo178hbg==",
"dependencies": {
"Microsoft.Extensions.Telemetry": "10.3.0"
}
},
"Microsoft.Extensions.Resilience": {
"type": "Transitive",
"resolved": "10.3.0",
"contentHash": "xc0dZuPkBaVIdMlODDppmNY/dxE27wIQ46gTzStoFXO4/yVcOMKlPmtr9vTP4edyXBRizGxPtcAFmqxZ5gPTkQ==",
"dependencies": {
"Microsoft.Extensions.Diagnostics.ExceptionSummarization": "10.3.0",
"Microsoft.Extensions.Telemetry.Abstractions": "10.3.0",
"Polly.Extensions": "8.4.2",
"Polly.RateLimiting": "8.4.2"
}
},
"Microsoft.Extensions.ServiceDiscovery.Abstractions": {
"type": "Transitive",
"resolved": "10.3.0",
"contentHash": "2j3SdFGgqFQFP6xQpkbqxt9SdzsT4/FK47RQ7MoCAZnKZiZEDuLDq9PfKLqON8HbGc09jHZ02x0tHIjleVab2A=="
},
"Microsoft.Extensions.Telemetry": {
"type": "Transitive",
"resolved": "10.3.0",
"contentHash": "M1esrIGmwU2JBY0JpwdlUTXTNBXSBFEs+41bYBd59+9/vCaXw+vGhtYcCL+JXeGmxTLUHmuXcKbX/uVCSFcuzA==",
"dependencies": {
"Microsoft.Extensions.AmbientMetadata.Application": "10.3.0",
"Microsoft.Extensions.DependencyInjection.AutoActivation": "10.3.0",
"Microsoft.Extensions.Telemetry.Abstractions": "10.3.0"
}
},
"Microsoft.Extensions.Telemetry.Abstractions": {
"type": "Transitive",
"resolved": "10.3.0",
"contentHash": "aKxH6ZsGAewGF8uSXyx1WkjqItwZA+hd1hhQ/4i7o5injCWSdr9vIZ3R3djJfy8OG3xaWK+LZY/+slVvlnwEHw==",
"dependencies": {
"Microsoft.Extensions.Compliance.Abstractions": "10.3.0"
}
},
"Newtonsoft.Json": {
"type": "Transitive",
"resolved": "13.0.3",
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
},
"OpenTelemetry": {
"type": "Transitive",
"resolved": "1.15.0",
"contentHash": "7mS/oZFF8S6xyqGQfMU1btp0nXJQUPWV535Vp/XMLYwRAUv36xQN+U4vufWBF1+z4HnRTOwuFHtUSGnHbyN6FQ==",
"dependencies": {
"OpenTelemetry.Api.ProviderBuilderExtensions": "1.15.0"
}
},
"OpenTelemetry.Api": {
"type": "Transitive",
"resolved": "1.15.0",
"contentHash": "vk5OGdf6K9kQScCWo3bRjhDWCv6Pqw92IpX4dlARZ8B1WL7/2NGTDtCkkw42eQf7UdwyoHKzVvMH/PtL8d6z7w=="
},
"OpenTelemetry.Api.ProviderBuilderExtensions": {
"type": "Transitive",
"resolved": "1.15.0",
"contentHash": "OnuSUlRpGvowkOzGFQfy+KZFu0cITfKfh2IYJJiZskxVJiOuexwOOuvfDAgpJdmTzVWAHjYdz2shcHZaJ06UjQ==",
"dependencies": {
"OpenTelemetry.Api": "1.15.0"
}
},
"Polly.Core": {
"type": "Transitive",
"resolved": "8.4.2",
"contentHash": "BpE2I6HBYYA5tF0Vn4eoQOGYTYIK1BlF5EXVgkWGn3mqUUjbXAr13J6fZVbp7Q3epRR8yshacBMlsHMhpOiV3g=="
},
"Polly.Extensions": {
"type": "Transitive",
"resolved": "8.4.2",
"contentHash": "GZ9vRVmR0jV2JtZavt+pGUsQ1O1cuRKG7R7VOZI6ZDy9y6RNPvRvXK1tuS4ffUrv8L0FTea59oEuQzgS0R7zSA==",
"dependencies": {
"Polly.Core": "8.4.2"
}
},
"Polly.RateLimiting": {
"type": "Transitive",
"resolved": "8.4.2",
"contentHash": "ehTImQ/eUyO07VYW2WvwSmU9rRH200SKJ/3jku9rOkyWE0A2JxNFmAVms8dSn49QLSjmjFRRSgfNyOgr/2PSmA==",
"dependencies": {
"Polly.Core": "8.4.2"
}
},
"MSearch.Domain": {
"type": "Project"
},
"MSearch.SearchProviders.GitHub": {
"type": "Project",
"dependencies": {
"MSearch.Domain": "[1.0.0, )"
}
},
"MSearch.SearchProviders.HackerNews": {
"type": "Project",
"dependencies": {
"MSearch.Domain": "[1.0.0, )"
}
},
"MSearch.SearchProviders.MicrosoftLearn": {
"type": "Project",
"dependencies": {
"MSearch.Domain": "[1.0.0, )"
}
},
"MSearch.SearchProviders.MozillaDeveloperNetwork": {
"type": "Project",
"dependencies": {
"MSearch.Domain": "[1.0.0, )"
}
},
"MSearch.SearchProviders.OpenStreetMap": {
"type": "Project",
"dependencies": {
"MSearch.Domain": "[1.0.0, )"
}
},
"MSearch.SearchProviders.Reddit": {
"type": "Project",
"dependencies": {
"MSearch.Domain": "[1.0.0, )"
}
},
"MSearch.SearchProviders.Spotify": {
"type": "Project",
"dependencies": {
"MSearch.Domain": "[1.0.0, )",
"SpotifyAPI.Web": "[7.4.0, )"
}
},
"MSearch.SearchProviders.StackExchange": {
"type": "Project",
"dependencies": {
"MSearch.Domain": "[1.0.0, )"
}
},
"MSearch.SearchProviders.TheMovieDb": {
"type": "Project",
"dependencies": {
"MSearch.Domain": "[1.0.0, )"
}
},
"MSearch.SearchProviders.Wikipedia": {
"type": "Project",
"dependencies": {
"HtmlAgilityPack": "[1.12.4, )",
"MSearch.Domain": "[1.0.0, )"
}
},
"MSearch.SearchProviders.YouTube": {
"type": "Project",
"dependencies": {
"MSearch.Domain": "[1.0.0, )"
}
},
"MSearch.ServiceDefaults": {
"type": "Project",
"dependencies": {
"Microsoft.Extensions.Http.Resilience": "[10.3.0, )",
"Microsoft.Extensions.ServiceDiscovery": "[10.3.0, )",
"OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.0, )",
"OpenTelemetry.Extensions.Hosting": "[1.15.0, )",
"OpenTelemetry.Instrumentation.AspNetCore": "[1.15.0, )",
"OpenTelemetry.Instrumentation.Http": "[1.15.0, )",
"OpenTelemetry.Instrumentation.Runtime": "[1.15.0, )"
}
},
"HtmlAgilityPack": {
"type": "CentralTransitive",
"requested": "[1.12.4, )",
"resolved": "1.12.4",
"contentHash": "ljqvBabvFwKoLniuoQKO8b5bJfJweKLs4fUNS/V5dsvpo0A8MlJqxxn9XVmP2DaskbUXty6IYaWAi1SArGIMeQ=="
},
"Microsoft.Extensions.Http.Resilience": {
"type": "CentralTransitive",
"requested": "[10.3.0, )",
"resolved": "10.3.0",
"contentHash": "P4+s/eUH3dZdn1HnivSL2dh6/Jb0ndLt2l88oQPZ9BYdyb4tSRAsnz4QkJHGfPA9lS/XblI5QYsxEdfkurPvIg==",
"dependencies": {
"Microsoft.Extensions.Http.Diagnostics": "10.3.0",
"Microsoft.Extensions.Resilience": "10.3.0"
}
},
"Microsoft.Extensions.ServiceDiscovery": {
"type": "CentralTransitive",
"requested": "[10.3.0, )",
"resolved": "10.3.0",
"contentHash": "C7onh6YDQKbZjKmAWEef1RDosjxPxA3PZdLob5lhS1AQuKgw0vTHnCKUA1KAhNlzhyfOPVP6tc0cLIPCDoBvoA==",
"dependencies": {
"Microsoft.Extensions.ServiceDiscovery.Abstractions": "10.3.0"
}
},
"OpenTelemetry.Exporter.OpenTelemetryProtocol": {
"type": "CentralTransitive",
"requested": "[1.15.0, )",
"resolved": "1.15.0",
"contentHash": "VH8ANc/js9IRvfYt0Q2UaAxNCOWm+IU+vWrtoH7pfx4oWPVdISUt+9uWfBCFMWZg5WzQip5dhslyDjeyZXXfSQ==",
"dependencies": {
"OpenTelemetry": "1.15.0"
}
},
"OpenTelemetry.Extensions.Hosting": {
"type": "CentralTransitive",
"requested": "[1.15.0, )",
"resolved": "1.15.0",
"contentHash": "RixjKyB1pbYGhWdvPto4KJs+exdQknJsnjUO9WszdLles5Vcd0EYzxPNJdwmLjYfP+Jfbr4B5nktM4ZgeHSWtg==",
"dependencies": {
"OpenTelemetry": "1.15.0"
}
},
"OpenTelemetry.Instrumentation.AspNetCore": {
"type": "CentralTransitive",
"requested": "[1.15.0, )",
"resolved": "1.15.0",
"contentHash": "mte1nRYefxjed2syXgVWq3UCfMKO7MkebvTZmf0O1aLgVgCktLsVjQ6mftyjIbWGBBCHN0wg+Glxj8BSFS70pQ==",
"dependencies": {
"OpenTelemetry.Api.ProviderBuilderExtensions": "[1.15.0, 2.0.0)"
}
},
"OpenTelemetry.Instrumentation.Http": {
"type": "CentralTransitive",
"requested": "[1.15.0, )",
"resolved": "1.15.0",
"contentHash": "uToc7bUp8IEdb0ny9mKsL6FrrYelINPzxxiSShJgOf4XmQc4Azww6S5RjRj24YhsOn2a1MABOrxfVTZXtDk4Eg==",
"dependencies": {
"OpenTelemetry.Api.ProviderBuilderExtensions": "[1.15.0, 2.0.0)"
}
},
"OpenTelemetry.Instrumentation.Runtime": {
"type": "CentralTransitive",
"requested": "[1.15.0, )",
"resolved": "1.15.0",
"contentHash": "OOvpqR/j2Pb6+tWhHNODIbSJ53Or/MDtTiXEyrsWI02K2lLAgvBFcxUOrHggS/8015cYR3AdSaXv6NZrkz5yQA==",
"dependencies": {
"OpenTelemetry.Api": "[1.15.0, 2.0.0)"
}
},
"SpotifyAPI.Web": {
"type": "CentralTransitive",
"requested": "[7.4.0, )",
"resolved": "7.4.0",
"contentHash": "1xewe5k+dy3cQJ9iIOmw4CUmBUnVZflSjcFL33636h/XuTLUnBKkKiB9dCeHJeVVtN0+on+t2Wy3haJBJMp3Vg==",
"dependencies": {
"Newtonsoft.Json": "13.0.3"
}
}
},
"net10.0/linux-x64": {},
"net10.0/win-arm64": {}
}
}
\ No newline at end of file
Web/wwwroot/app.css +77 -0
diff --git a/Web/wwwroot/app.css b/Web/wwwroot/app.css
new file mode 100644
index 0000000..8de0d24
@@ -0,0 +1,77 @@
html,
body {
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 0;
}
* {
box-sizing: border-box;
}
:root {
color-scheme: light dark;
--background-color: light-dark(#ffffff, #1b1a19);
--text-color: light-dark(#70757a, #d2d0ce);
--link-color: light-dark(#4007a2, #82c7ff);
background-color: var(--background-color);
color: var(--text-color);
}
a {
color: var(--link-color);
}
main {
display: flex;
flex-direction: column;
gap: 2rem;
width: 100%;
padding-block: 2rem;
padding-inline: 1rem;
@media (min-width: 40rem) {
max-width: 50rem;
padding-inline: 8rem 2rem;
}
div {
h2 {
font-weight: normal;
margin: 0;
margin-block-end: 0.5rem;
a {
text-decoration: none;
&:hover {
text-decoration: underline;
}
&::after {
content: attr(href);
display: block;
font-size: 50%;
color: var(--text-color);
}
}
}
p {
margin: 0;
}
}
}
.blazor-error-boundary {
background:
url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=)
no-repeat 1rem/1.8rem,
#b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
&::after {
content: "An error has occurred.";
}
}
dotnet-tools.json +13 -0
diff --git a/dotnet-tools.json b/dotnet-tools.json
new file mode 100644
index 0000000..97f37dc
@@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"csharpier": {
"version": "1.2.6",
"commands": [
"csharpier"
],
"rollForward": false
}
}
}
\ No newline at end of file
global.json +10 -0
diff --git a/global.json b/global.json
new file mode 100644
index 0000000..2e206e8
@@ -0,0 +1,10 @@
{
"sdk": {
"version": "10.0.0",
"rollForward": "latestMinor",
"allowPrerelease": false
},
"test": {
"runner": "Microsoft.Testing.Platform"
}
}