Merge in from minimal into main api.

pull/177/head
James Montemagno 2022-11-08 13:59:59 -08:00
parent ea56325188
commit 5184f01c91
20 changed files with 131 additions and 473 deletions

View File

@ -3,8 +3,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31612.314
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Podcast.API", "src\Services\Podcasts\Podcast.API\Podcast.API.csproj", "{777229ED-F33B-4A49-B872-EB1CF454C329}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Podcast.Infrastructure", "src\Services\Podcasts\Podcast.Infrastructure\Podcast.Infrastructure.csproj", "{0004571F-FFA3-4DB3-BF91-65DF1E774677}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Podcast.Updater.Worker", "src\Services\Podcasts\Podcast.Updater.Worker\Podcast.Updater.Worker.csproj", "{3B324F00-AE02-4FC7-B665-86F87B22BD75}"
@ -36,7 +34,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ListenTogether.Domain", "sr
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ListenTogether.Application", "src\Services\ListenTogether\ListenTogether.Application\ListenTogether.Application.csproj", "{C11EFB24-BF9A-4631-BA96-12A6E1CC9122}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Podcast.MinimalAPI", "src\Services\Podcasts\Podcast.MinimalAPI\Podcast.MinimalAPI.csproj", "{64DB86C6-5A19-44A8-A326-1D3EF7471EED}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Podcast.API", "src\Services\Podcasts\Podcast.API\Podcast.API.csproj", "{58507F58-A2A2-4ACE-9EF6-0FC9C9780DC8}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -44,10 +42,6 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{777229ED-F33B-4A49-B872-EB1CF454C329}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{777229ED-F33B-4A49-B872-EB1CF454C329}.Debug|Any CPU.Build.0 = Debug|Any CPU
{777229ED-F33B-4A49-B872-EB1CF454C329}.Release|Any CPU.ActiveCfg = Release|Any CPU
{777229ED-F33B-4A49-B872-EB1CF454C329}.Release|Any CPU.Build.0 = Release|Any CPU
{0004571F-FFA3-4DB3-BF91-65DF1E774677}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0004571F-FFA3-4DB3-BF91-65DF1E774677}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0004571F-FFA3-4DB3-BF91-65DF1E774677}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -76,16 +70,15 @@ Global
{C11EFB24-BF9A-4631-BA96-12A6E1CC9122}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C11EFB24-BF9A-4631-BA96-12A6E1CC9122}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C11EFB24-BF9A-4631-BA96-12A6E1CC9122}.Release|Any CPU.Build.0 = Release|Any CPU
{64DB86C6-5A19-44A8-A326-1D3EF7471EED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{64DB86C6-5A19-44A8-A326-1D3EF7471EED}.Debug|Any CPU.Build.0 = Debug|Any CPU
{64DB86C6-5A19-44A8-A326-1D3EF7471EED}.Release|Any CPU.ActiveCfg = Release|Any CPU
{64DB86C6-5A19-44A8-A326-1D3EF7471EED}.Release|Any CPU.Build.0 = Release|Any CPU
{58507F58-A2A2-4ACE-9EF6-0FC9C9780DC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{58507F58-A2A2-4ACE-9EF6-0FC9C9780DC8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{58507F58-A2A2-4ACE-9EF6-0FC9C9780DC8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{58507F58-A2A2-4ACE-9EF6-0FC9C9780DC8}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{777229ED-F33B-4A49-B872-EB1CF454C329} = {3577F0EE-7091-46E0-9B25-37559228A896}
{0004571F-FFA3-4DB3-BF91-65DF1E774677} = {3577F0EE-7091-46E0-9B25-37559228A896}
{3B324F00-AE02-4FC7-B665-86F87B22BD75} = {3577F0EE-7091-46E0-9B25-37559228A896}
{03EC558C-665F-4E3A-BDC4-8F56B5ED45C1} = {0DE82D5C-C8CA-40E5-A5FE-3445A7CEE51E}
@ -93,7 +86,7 @@ Global
{F7C33B71-158B-4956-B707-EEE291E6EBF9} = {0DE82D5C-C8CA-40E5-A5FE-3445A7CEE51E}
{5C09F557-3425-4290-9122-56D1EBA8330F} = {0DE82D5C-C8CA-40E5-A5FE-3445A7CEE51E}
{C11EFB24-BF9A-4631-BA96-12A6E1CC9122} = {0DE82D5C-C8CA-40E5-A5FE-3445A7CEE51E}
{64DB86C6-5A19-44A8-A326-1D3EF7471EED} = {3577F0EE-7091-46E0-9B25-37559228A896}
{58507F58-A2A2-4ACE-9EF6-0FC9C9780DC8} = {3577F0EE-7091-46E0-9B25-37559228A896}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {11182059-7938-44A8-9759-7A0BCCFACBE7}

View File

@ -52,8 +52,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Podcast.Pages", "src\Web\Pa
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Podcast.Shared", "src\Web\Shared\Podcast.Shared.csproj", "{5B0ADB1C-FE7B-49B0-92BF-F9CEC44B0F2E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Podcast.MinimalAPI", "src\Services\Podcasts\Podcast.MinimalAPI\Podcast.MinimalAPI.csproj", "{CE938CB6-C3D2-4416-B1D6-C5920ABC2314}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -116,10 +114,6 @@ Global
{5B0ADB1C-FE7B-49B0-92BF-F9CEC44B0F2E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5B0ADB1C-FE7B-49B0-92BF-F9CEC44B0F2E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5B0ADB1C-FE7B-49B0-92BF-F9CEC44B0F2E}.Release|Any CPU.Build.0 = Release|Any CPU
{CE938CB6-C3D2-4416-B1D6-C5920ABC2314}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CE938CB6-C3D2-4416-B1D6-C5920ABC2314}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CE938CB6-C3D2-4416-B1D6-C5920ABC2314}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CE938CB6-C3D2-4416-B1D6-C5920ABC2314}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -140,7 +134,6 @@ Global
{1F672A0B-78AD-4680-A142-9A48E8C64689} = {2AF85B2E-874C-44C4-87F7-098C04BAA2E9}
{64F43FEB-3549-422F-8A09-3043DF4096AE} = {2AF85B2E-874C-44C4-87F7-098C04BAA2E9}
{5B0ADB1C-FE7B-49B0-92BF-F9CEC44B0F2E} = {2AF85B2E-874C-44C4-87F7-098C04BAA2E9}
{CE938CB6-C3D2-4416-B1D6-C5920ABC2314} = {7C8D0BD0-BE07-49C9-B971-F3A8AFF28CEB}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {11182059-7938-44A8-9759-7A0BCCFACBE7}

View File

@ -6,7 +6,7 @@ services:
image: ${DOCKER_REGISTRY-}podcastminapi
build:
context: .
dockerfile: src/Services/Podcasts/Podcast.MinimalAPI/Dockerfile
dockerfile: src/Services/Podcasts/Podcast.API/Dockerfile
depends_on:
- podcast.db
- storage

View File

@ -1,59 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Podcast.API.Models;
using Podcast.Infrastructure.Data;
namespace Podcast.API.Controllers;
[Route("v1/[controller]")]
[ApiController]
public class ShowsController : ControllerBase
{
private readonly PodcastDbContext _podcastDbContext;
public ShowsController(PodcastDbContext podcastDbContext)
{
_podcastDbContext = podcastDbContext;
}
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<ShowDto>))]
public async Task<IEnumerable<ShowDto>> GetAsync(int limit, string? term,
Guid? categoryId,
CancellationToken cancellationToken)
{
var showsQuery = _podcastDbContext.Shows.Include(show => show.Feed!.Categories)
.ThenInclude(x => x.Category)
.AsQueryable();
if (!string.IsNullOrEmpty(term))
showsQuery = showsQuery.Where(show => show.Title.Contains(term));
if (categoryId is not null)
showsQuery = showsQuery.Where(show =>
show.Feed!.Categories.Any(y => y.CategoryId == categoryId));
var shows = await showsQuery.OrderByDescending(show => show.Feed!.IsFeatured)
.ThenBy(x => x.Title)
.Take(limit)
.Select(x => new ShowDto(x))
.ToListAsync(cancellationToken);
return shows;
}
[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ShowDto))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<ShowDto>> GetByIdAsync(Guid id,
CancellationToken cancellationToken)
{
var show = await _podcastDbContext.Shows
.Include(show => show.Episodes.OrderByDescending(episode => episode.Published))
.Include(show => show.Feed!.Categories)
.ThenInclude(feed => feed.Category)
.Where(x => x.Id == id)
.Select(show => new ShowDto(show))
.FirstOrDefaultAsync(cancellationToken);
return show == null ? NotFound() : Ok(show);
}
}

View File

@ -1,61 +0,0 @@
using Azure.Storage.Queues;
using Microsoft.EntityFrameworkCore;
using Podcast.API.Models;
using Podcast.Infrastructure.Data;
using Podcast.Infrastructure.Http.Feeds;
namespace Microsoft.AspNetCore.Builder;
public static class FeedEndpointsWebApplicationExtensions
{
public static void MapFeedEndpointRoutes(this WebApplication app)
{
// receive user-submitted feeds
app.MapPost("v1/feeds", async (QueueClient queueClient, UserSubmittedFeedDto feed, CancellationToken cancellationToken) =>
{
await queueClient.CreateIfNotExistsAsync(cancellationToken: cancellationToken);
await queueClient.SendMessageAsync(new BinaryData(feed), cancellationToken: cancellationToken);
})
.WithName("SubmitNewFeed")
.WithTags("Feeds");
// get all the user-submitted feeds
app.MapGet("v1/feeds", (PodcastDbContext podcastDbContext, CancellationToken cancellationToken) =>
{
return podcastDbContext.UserSubmittedFeeds.OrderByDescending(f => f.Timestamp).ToListAsync(cancellationToken);
})
.WithName("GetUserSubmittedFeeds")
.WithTags("Feeds");
// update a user-submitted feed
app.MapPut("v1/feeds/{id}", async (PodcastDbContext podcastDbContext, IFeedClient feedClient, Guid id, CancellationToken cancellationToken) =>
{
var feed = podcastDbContext.UserSubmittedFeeds.Find(id);
if (feed is null)
return;
var categories = feed.Categories.Split(',');
await feedClient.AddFeedAsync(podcastDbContext, feed.Url, categories, cancellationToken);
podcastDbContext.Remove(feed);
await podcastDbContext.SaveChangesAsync(cancellationToken);
})
.WithTags("Feeds");
// delete a specific user-submitted feed
app.MapDelete("v1/feeds/{id}", async (PodcastDbContext podcastDbContext, Guid id, CancellationToken cancellationToken) =>
{
var feed = podcastDbContext.UserSubmittedFeeds.FirstOrDefault(x => x.Id == id);
if (feed is null)
return Results.NotFound();
podcastDbContext.Remove(feed);
await podcastDbContext.SaveChangesAsync(cancellationToken);
return Results.Accepted();
})
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status202Accepted)
.WithName("DeleteFeed")
.WithTags("Feeds");
}
}

View File

@ -1 +1,10 @@

global using Azure.Storage.Queues;
global using Microsoft.AspNetCore.Authentication.JwtBearer;
global using Microsoft.AspNetCore.Http.HttpResults;
global using Microsoft.EntityFrameworkCore;
global using Microsoft.OpenApi.Models;
global using Podcast.API.Models;
global using Podcast.API.Routes;
global using Podcast.Infrastructure.Data;
global using Podcast.Infrastructure.Data.Models;
global using Podcast.Infrastructure.Http.Feeds;

View File

@ -1,27 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<UserSecretsId>764bd3fe-29c2-469f-991b-c5c141d23e3e</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<ImplicitUsings>enable</ImplicitUsings>
<DockerfileContext>..\..\..\..</DockerfileContext>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<UserSecretsId>764bd3fe-29c2-469f-991b-c5c141d23e3e</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<ImplicitUsings>enable</ImplicitUsings>
<DockerfileContext>..\..\..\..</DockerfileContext>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Storage.Queues" Version="12.11.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.1" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.14.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.Storage.Queues" Version="12.11.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.1" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.14.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.0-*" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
<PackageReference Include="Asp.Versioning.Http" Version="6.1.0" />
<PackageReference Include="Microsoft.OpenApi" Version="1.4.3" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.0-*" />
<PackageReference Include="Microsoft.Identity.Web" Version="1.25.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Podcast.Infrastructure\Podcast.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Podcast.Infrastructure\Podcast.Infrastructure.csproj" />
</ItemGroup>
</Project>

View File

@ -1,70 +1,104 @@
using Azure.Storage.Queues;
using Microsoft.EntityFrameworkCore;
using Microsoft.OpenApi.Models;
using Podcast.API.Controllers;
using Podcast.API.Models;
using Podcast.Infrastructure.Data;
using Podcast.Infrastructure.Http.Feeds;
using Asp.Versioning;
using Asp.Versioning.Conventions;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Identity.Web;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.Threading.RateLimiting;
var builder = WebApplication.CreateBuilder(args);
// Database and storage related-services
var connectionString = builder.Configuration.GetConnectionString("PodcastDb");
builder.Services.AddSqlServer<PodcastDbContext>(connectionString);
var queueConnectionString = builder.Configuration.GetConnectionString("FeedQueue");
builder.Services.AddSingleton(new QueueClient(queueConnectionString, "feed-queue"));
builder.Services.AddHttpClient<IFeedClient, FeedClient>();
builder.Services.AddSwaggerGen(setup =>
{
setup.SwaggerDoc("v1",
new OpenApiInfo { Description = "NetPodcast API", Title = ".NetConf2021", Version = "v1" });
// Authentication and authorization-related services
// Comment back in if testing authentication
// builder.Services.AddMicrosoftIdentityWebApiAuthentication(builder.Configuration);
builder.Services.AddAuthorizationBuilder().AddPolicy("modify_feeds", policy => policy.RequireScope("API.Access"));
// OpenAPI and versioning-related services
builder.Services.AddSwaggerGen();
builder.Services.Configure<SwaggerGeneratorOptions>(opts => {
opts.InferSecuritySchemes = true;
});
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(2, 0);
options.ReportApiVersions = true;
options.AssumeDefaultVersionWhenUnspecified = true;
options.ApiVersionReader = new HeaderApiVersionReader("api-version");
});
builder.Services.AddCors(setup =>
{
setup.AddDefaultPolicy(policy => policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod());
});
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddControllers();
// Rate-limiting and output caching-related services
builder.Services.AddRateLimiter(options => options.AddFixedWindowLimiter("feeds", options =>
{
options.PermitLimit = 5;
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = 0;
options.Window = TimeSpan.FromSeconds(2);
options.AutoReplenishment = false;
}));
builder.Services.AddOutputCache();
var app = builder.Build();
await EnsureDbAsync(app.Services);
// Register required middlewares
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "NetPodcast Api v1");
c.SwaggerEndpoint("/swagger/v1/swagger.json", ".NET Podcasts API");
});
app.UseCors();
app.UseRateLimiter();
app.UseOutputCache();
app.MapGet("v1/categories",
async (PodcastDbContext podcastDbContext, CancellationToken cancellationToken) =>
{
var categories = await podcastDbContext.Categories.Select(x => new CategoryDto(x.Id, x.Genre))
.ToListAsync(cancellationToken);
return categories;
});
var versionSet = app.NewApiVersionSet()
.HasApiVersion(1.0)
.HasApiVersion(2.0)
.ReportApiVersions()
.Build();
app.MapGet("v1/episodes/{id}", async (PodcastDbContext podcastDbContext, Guid id,
CancellationToken cancellationToken) =>
{
var episode = await podcastDbContext.Episodes.Include(episode => episode.Show)
.Where(episode => episode.Id == id)
.Select(episode => new EpisodeDto(episode))
.FirstAsync(cancellationToken);
return episode;
});
var shows = app.MapGroup("/shows");
var categories = app.MapGroup("/categories");
var episodes = app.MapGroup("/episodes");
shows
.MapShowsApi()
.WithApiVersionSet(versionSet)
.MapToApiVersion(1.0)
.MapToApiVersion(2.0)
.CacheOutput();
categories
.MapCategoriesApi()
.WithApiVersionSet(versionSet)
.MapToApiVersion(1.0);
episodes
.MapEpisodesApi()
.WithApiVersionSet(versionSet)
.MapToApiVersion(1.0);
var feedIngestionEnabled = app.Configuration.GetValue<bool>("Features:FeedIngestion");
if (feedIngestionEnabled)
{
app.MapFeedEndpointRoutes();
var feeds = app.MapGroup("/feeds");
feeds.MapFeedsApi().WithApiVersionSet(versionSet).MapToApiVersion(2.0).RequireRateLimiting("feeds");
}
app.MapControllers();
app.Run();
static async Task EnsureDbAsync(IServiceProvider sp)

View File

@ -11,11 +11,10 @@
"Podcast.API": {
"commandName": "Project",
"launchBrowser": true,
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"dotnetRunMessages": true
}
},
"IIS Express": {
"commandName": "IISExpress",
@ -28,8 +27,7 @@
"commandName": "Docker",
"launchBrowser": true,
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
"publishAllPorts": true,
"useSSL": true
"environmentVariables": {}
}
}
}

View File

@ -1,10 +1,4 @@
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.EntityFrameworkCore;
using Podcast.API.Models;
using Podcast.Infrastructure.Data;
using System.Threading;
namespace Podcast.API.Routes;
namespace Podcast.API.Routes;
public static class CategoriesApi
{

View File

@ -1,10 +1,4 @@
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.EntityFrameworkCore;
using Podcast.API.Models;
using Podcast.Infrastructure.Data;
using System.Threading;
namespace Podcast.API.Routes;
namespace Podcast.API.Routes;
public static class EpisodesApi
{

View File

@ -1,14 +1,4 @@
using Azure.Storage.Queues;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.EntityFrameworkCore;
using Podcast.API.Models;
using Podcast.Infrastructure.Data;
using Podcast.Infrastructure.Data.Models;
using Podcast.Infrastructure.Http.Feeds;
using Microsoft.OpenApi.Models;
using Microsoft.AspNetCore.Authentication.JwtBearer;
namespace Podcast.API.Routes;
namespace Podcast.API.Routes;
public static class FeedsApi
{

View File

@ -9,5 +9,19 @@
"ConnectionStrings": {
"PodcastDb": "Server=localhost, 5433;Database=Podcast;User Id=sa;Password=Pass@word",
"FeedQueue": "UseDevelopmentStorage=true"
},
"Authentication": {
"Schemes": {
"Bearer": {
"ValidAudiences": [
"http://localhost:56906",
"https://localhost:44385",
"https://localhost:5001",
"http://localhost:5000",
"1ba2c41d-3a54-414a-9700-1f9393cfafca"
],
"ValidIssuer": "dotnet-user-jwts"
}
}
}
}

View File

@ -1,23 +0,0 @@
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
WORKDIR /src
COPY ["src/Services/Podcasts/Podcast.MinimalAPI/Podcast.MinimalAPI.csproj", "src/Services/Podcasts/Podcast.MinimalAPI/"]
COPY ["src/Services/Podcasts/Podcast.Infrastructure/Podcast.Infrastructure.csproj", "src/Services/Podcasts/Podcast.Infrastructure/"]
RUN dotnet restore "src/Services/Podcasts/Podcast.MinimalAPI/Podcast.MinimalAPI.csproj"
COPY . .
WORKDIR "/src/src/Services/Podcasts/Podcast.MinimalAPI"
RUN dotnet build "Podcast.MinimalAPI.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "Podcast.MinimalAPI.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Podcast.MinimalAPI.dll"]

View File

@ -1,32 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<UserSecretsId>764bd3fe-29c2-469f-991b-c5c141d23e3e</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<ImplicitUsings>enable</ImplicitUsings>
<DockerfileContext>..\..\..\..</DockerfileContext>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Storage.Queues" Version="12.11.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.1" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.14.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.0-*" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
<PackageReference Include="Asp.Versioning.Http" Version="6.1.0" />
<PackageReference Include="Microsoft.OpenApi" Version="1.4.3" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.0-*" />
<PackageReference Include="Microsoft.Identity.Web" Version="1.25.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Podcast.Infrastructure\Podcast.Infrastructure.csproj" />
</ItemGroup>
</Project>

View File

@ -1,114 +0,0 @@
using Azure.Storage.Queues;
using Microsoft.EntityFrameworkCore;
using Podcast.Infrastructure.Data;
using Podcast.Infrastructure.Http.Feeds;
using Asp.Versioning;
using Asp.Versioning.Conventions;
using Podcast.API.Routes;
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Swashbuckle.AspNetCore.SwaggerGen;
using Microsoft.Identity.Web;
var builder = WebApplication.CreateBuilder(args);
// Database and storage related-services
var connectionString = builder.Configuration.GetConnectionString("PodcastDb");
builder.Services.AddSqlServer<PodcastDbContext>(connectionString);
var queueConnectionString = builder.Configuration.GetConnectionString("FeedQueue");
builder.Services.AddSingleton(new QueueClient(queueConnectionString, "feed-queue"));
builder.Services.AddHttpClient<IFeedClient, FeedClient>();
// Authentication and authorization-related services
// Comment back in if testing authentication
// builder.Services.AddMicrosoftIdentityWebApiAuthentication(builder.Configuration);
builder.Services.AddAuthorizationBuilder().AddPolicy("modify_feeds", policy => policy.RequireScope("API.Access"));
// OpenAPI and versioning-related services
builder.Services.AddSwaggerGen();
builder.Services.Configure<SwaggerGeneratorOptions>(opts => {
opts.InferSecuritySchemes = true;
});
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(2, 0);
options.ReportApiVersions = true;
options.AssumeDefaultVersionWhenUnspecified = true;
options.ApiVersionReader = new HeaderApiVersionReader("api-version");
});
builder.Services.AddCors(setup =>
{
setup.AddDefaultPolicy(policy => policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod());
});
// Rate-limiting and output caching-related services
builder.Services.AddRateLimiter(options => options.AddFixedWindowLimiter("feeds", options =>
{
options.PermitLimit = 5;
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = 0;
options.Window = TimeSpan.FromSeconds(2);
options.AutoReplenishment = false;
}));
builder.Services.AddOutputCache();
var app = builder.Build();
await EnsureDbAsync(app.Services);
// Register required middlewares
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", ".NET Podcasts Minimal API");
});
app.UseCors();
app.UseRateLimiter();
app.UseOutputCache();
var versionSet = app.NewApiVersionSet()
.HasApiVersion(1.0)
.HasApiVersion(2.0)
.ReportApiVersions()
.Build();
var shows = app.MapGroup("/shows");
var categories = app.MapGroup("/categories");
var episodes = app.MapGroup("/episodes");
shows
.MapShowsApi()
.WithApiVersionSet(versionSet)
.MapToApiVersion(1.0)
.MapToApiVersion(2.0)
.CacheOutput();
categories
.MapCategoriesApi()
.WithApiVersionSet(versionSet)
.MapToApiVersion(1.0);
episodes
.MapEpisodesApi()
.WithApiVersionSet(versionSet)
.MapToApiVersion(1.0);
var feedIngestionEnabled = app.Configuration.GetValue<bool>("Features:FeedIngestion");
if (feedIngestionEnabled)
{
var feeds = app.MapGroup("/feeds");
feeds.MapFeedsApi().WithApiVersionSet(versionSet).MapToApiVersion(2.0).RequireRateLimiting("feeds");
}
app.Run();
static async Task EnsureDbAsync(IServiceProvider sp)
{
await using var db = sp.CreateScope().ServiceProvider.GetRequiredService<PodcastDbContext>();
await db.Database.MigrateAsync();
}

View File

@ -1,33 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:56906",
"sslPort": 44385
}
},
"profiles": {
"Podcast.MinimalAPI": {
"commandName": "Project",
"launchBrowser": true,
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Docker": {
"commandName": "Docker",
"launchBrowser": true,
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
"environmentVariables": {}
}
}
}

View File

@ -1,27 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"ConnectionStrings": {
"PodcastDb": "Server=localhost, 5433;Database=Podcast;User Id=sa;Password=Pass@word",
"FeedQueue": "UseDevelopmentStorage=true"
},
"Authentication": {
"Schemes": {
"Bearer": {
"ValidAudiences": [
"http://localhost:56906",
"https://localhost:44385",
"https://localhost:5001",
"http://localhost:5000",
"1ba2c41d-3a54-414a-9700-1f9393cfafca"
],
"ValidIssuer": "dotnet-user-jwts"
}
}
}
}

View File

@ -1,17 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"Features": {
"FeedIngestion": true
},
"ConnectionStrings": {
"PodcastDb": "",
"FeedQueue": "UseDevelopmentStorage=true"
}
}