Merge in from minimal into main api.
parent
ea56325188
commit
5184f01c91
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
{
|
|
@ -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
|
||||
{
|
|
@ -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
|
||||
{
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"]
|
|
@ -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>
|
|
@ -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();
|
||||
}
|
|
@ -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": {}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"Features": {
|
||||
"FeedIngestion": true
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"PodcastDb": "",
|
||||
"FeedQueue": "UseDevelopmentStorage=true"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue