Skip to content

Commit

Permalink
Merge branch 'main' into dependabot/nuget/coverlet.collector-6.0.2
Browse files Browse the repository at this point in the history
  • Loading branch information
ardalis authored Jan 3, 2025
2 parents 33177c0 + 2fd5512 commit 1ca0d96
Show file tree
Hide file tree
Showing 23 changed files with 556 additions and 310 deletions.
25 changes: 25 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/.idea
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
LOCAL_SQL_PASSWORD=L0c4l_S3cr3t_P4a5sw0rD
6 changes: 3 additions & 3 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: CI/CD Pipeline
on: [ push, pull_request, workflow_dispatch ]

env:
AZURE_WEBAPP_NAME: devBetter
AZURE_WEBAPP_NAME: devbetter-linux
AZURE_GROUP_NAME: DevBetterGroup
AZURE_WEBAPP_PACKAGE_PATH: '.'

Expand All @@ -12,7 +12,7 @@ jobs:
name: Continuous Integration
strategy:
matrix:
os: [ ubuntu-latest, windows-latest ]
os: [ ubuntu-latest ]
runs-on: ${{ matrix.os }}
outputs:
is_push_to_default_branch: ${{ steps.conditionals_handler.outputs.is_push_to_default_branch }}
Expand Down Expand Up @@ -104,7 +104,7 @@ jobs:
if: needs.ci.outputs.is_push_to_default_branch == 'true'
name: Continuous Deployment
needs: ci
runs-on: windows-latest
runs-on: ubuntu-latest
steps:
- name: Download publish artifacts
id: dl_publish_artifacts
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/dotnetcore.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ on: [pull_request, workflow_dispatch]
jobs:
build:

runs-on: windows-latest
runs-on: ubuntu-latest
env:
VIMEO_TOKEN: ${{ secrets.VIMEO_TOKEN }}

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: publish

env:
AZURE_WEBAPP_NAME: devBetter
AZURE_WEBAPP_NAME: devbetter-linux
AZURE_GROUP_NAME: DevBetterGroup
AZURE_WEBAPP_PACKAGE_PATH: '.' # set this to the path to your web app project, defaults to the repository root

Expand All @@ -12,7 +12,7 @@ on:

jobs:
build-and-deploy:
runs-on: ubuntu-18.04
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
Expand Down
3 changes: 3 additions & 0 deletions DevBetterWeb.sln
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
efscripts.txt = efscripts.txt
global.json = global.json
README.md = README.md
Dockerfile = Dockerfile
docker-compose.yml = docker-compose.yml
.env = .env
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevBetterWeb.UnitTests", "tests\DevBetterWeb.UnitTests\DevBetterWeb.UnitTests.csproj", "{0AB375BC-E7AD-4BD9-8D28-AC07351CE37A}"
Expand Down
5 changes: 5 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<Project>
<PropertyGroup>
<NuGetAuditMode>direct</NuGetAuditMode>
</PropertyGroup>
</Project>
3 changes: 2 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@
<PackageVersion Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="7.0.13" />
<PackageVersion Include="NETStandard.Library" Version="2.0.3" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="NimblePros.Vimeo" Version="1.0.6" />
<PackageVersion Include="NimblePros.ApiClient" Version="1.0.11" />
<PackageVersion Include="NimblePros.Vimeo" Version="1.0.10" />
<PackageVersion Include="NSubstitute" Version="5.1.0" />
<PackageVersion Include="ReportGenerator" Version="5.1.25" />
<PackageVersion Include="Sendgrid" Version="9.28.1" />
Expand Down
23 changes: 23 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
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
ARG BUILD_CONFIGURATION=Debug
COPY *.sln .
COPY . .
RUN dotnet restore
WORKDIR "/src/DevBetterWeb.Web"
RUN dotnet build "DevBetterWeb.Web.csproj" -c $BUILD_CONFIGURATION -o /app/build

FROM build AS publish
ARG BUILD_CONFIGURATION=Debug
RUN dotnet publish "DevBetterWeb.Web.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
# Optional: Set this here if not setting it from docker-compose.yml
ENV ASPNETCORE_ENVIRONMENT=Local
ENTRYPOINT ["dotnet", "DevBetterWeb.Web.dll"]
48 changes: 46 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ A web application for devBetter.com, a developer coaching program web site and a

## What is devBetter?

Head over to [devBetter.com](https://devbetter.com) to see the live site. Scroll through the home page and read the testimonials. Essentially devBetter is a group dedicated to improving professional software developers of all stripes. We have a virtual community (currently using Discord) and we meet for live group Q&A sessions about once a week (currently using Zoom). We challenge and promote one another, answer tough code and software design questions, work through exercises, and more. This site is used as a playground by some members and its owner, Steve, to provide a real, working example of some of the coding techniques and practices we discuss. This is in contrast to labs, katas, and exercises that, while also valuable, are not the same as solving real world problems with real software in a production environment.
Head over to [devBetter.com](https://devbetter.com) to see the live site. Scroll through the home page and read the testimonials. Essentially devBetter is a group dedicated to improving professional software developers of all stripes. We have a virtual community (currently using Discord) and we meet for live group Q&A sessions about once a week (currently using Zoom). We challenge and promote one another, answer tough code and software design questions, work through exercises, and more. This site is used as a playground by some members and its owner, ardalis, to provide a real, working example of some of the coding techniques and practices we discuss. This is in contrast to labs, katas, and exercises that, while also valuable, are not the same as solving real world problems with real software in a production environment.

## Features

Expand Down Expand Up @@ -45,6 +45,8 @@ Head over to [devBetter.com](https://devbetter.com) to see the live site. Scroll

### Building and Running the App Locally

You can both run the app manually by running the SQL migrations, or by using `docker-compose` see [this section](#run-with-docker)

- Clone (or Fork and Clone) the repository locally
- Run migrations for both AppDbContext and IdentityDbContext

Expand All @@ -64,7 +66,7 @@ You should create an **appsettings.development.json** file to hold your other co

For the Discord web hook integration, you can use the `dev-test` channel in devBetter's Discord server. The web hook url is in the channel description on Discord. You can use that in you appsettings.development.json. Alternatively, you can set up your own Discord server - see [here](https://ardalis.com/add-discord-notifications-to-asp-net-core-apps/) for a walkthrough - and add the url to appsettings.development.json in the Discord Webhooks section that you can copy from appsettings.json. You could also create a mock server which will provide you with a url to use - an example is mocky.io

## EF Migrations Commands
### EF Migrations Commands

Add a new migration (from the DevBetter.Web folder):

Expand All @@ -90,6 +92,48 @@ Generate Idempotent Update Script (for production)(from the DevBetter.Web folder
dotnet ef migrations script -c AppDbContext -i -o migrate.sql -p ../DevBetterWeb.Infrastructure/DevBetterWeb.Infrastructure.csproj -s DevBetterWeb.Web.csproj
```

### Run with Docker

Alternatively you can use `docker-compose` to run the app locally. This is specially helpful when launching the app in operative systems that don't have support for SQL Express like MacOS.

To run with docker compose, simply run in the root of the repo:

```bash
docker compose up
```

The multi-container app runs two services:
- The web project: at `http://localhost/`
- The SQL Edge container at `localhost:1433`.

By default the application will run in the `Development` environment, and the SQL migrations will be applied programatically by the web project container during startup.

If you want to access the database outside of the app you will need the local password for the database, which you can find in this [.env file](https://github.com/DevBetterCom/DevBetterWeb/blob/main/.env). You can also find the full connection string as an environment variable for the web project with the name `ConnectionStrings:DefaultConnection` running in bash:

```bash
docker inspect \
--format='{{range .Config.Env}}{{println .}}{{end}}' dev-better-web \
| grep "ConnectionStrings:DefaultConnection=" \
| cut -d '=' -f 2- \
| sed 's/database/localhost/g'
```

Or in PowerShell:

```powershell
docker inspect `
--format='{{range .Config.Env}}{{println .}}{{end}}' dev-better-web `
| Select-String "ConnectionStrings:DefaultConnection=" `
| ForEach-Object { $_.Line -replace "database", "localhost" } `
| ForEach-Object { ($_ -split "Connection=")[1] }
```

To stop the services run:

```bash
docker compose down
```

## Video Upload Instructions (admin only)

Put the video files and their associated markdown files in a folder you wish to upload from. Specify the Vimeo token and devBetter API key.
Expand Down
38 changes: 38 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
services:
web:
image: dev-better-web
container_name: dev-better-web
depends_on:
- database
ports:
- 80:80
build:
context: .
dockerfile: Dockerfile
networks:
- my-network
environment:
ASPNETCORE_ENVIRONMENT: Development
ConnectionStrings:DefaultConnection: Server=database,1433;MultipleActiveResultSets=true;User Id=sa;Password=${LOCAL_SQL_PASSWORD};Encrypt=false

database:
image: mcr.microsoft.com/azure-sql-edge
cap_add:
- SYS_PTRACE
environment:
- ACCEPT_EULA=1
- MSSQL_SA_PASSWORD=${LOCAL_SQL_PASSWORD}
ports:
- 1433:1433
container_name: ms-sql
command:
- "/bin/sh"
- "-c"
- "/opt/mssql/bin/launchpadd -usens=false -enableOutboundAccess=true -usesameuser=true -sqlGroup root -- -reparentOrphanedDescendants=true -useDefaultLaunchers=false & /app/asdepackage/AsdePackage & /opt/mssql/bin/sqlservr"
privileged: true
networks:
- my-network

networks:
my-network:

2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "8.0.0",
"version": "8.*",
"rollForward": "latestFeature"
}
}
16 changes: 13 additions & 3 deletions src/DevBetterWeb.Core/Services/CreateVideoService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
using NimblePros.Vimeo.Models;
using NimblePros.Vimeo.VideoServices;
using NimblePros.Vimeo.VideoTusService;
using static DevBetterWeb.Core.Entities.Member;
using static Microsoft.ApplicationInsights.MetricDimensionNames.TelemetryContext;

namespace DevBetterWeb.Core.Services;
public class CreateVideoService : ICreateVideoService
Expand All @@ -20,13 +22,15 @@ public class CreateVideoService : ICreateVideoService
private readonly IUploadVideoTusService _uploadVideoTusService;
private readonly IRepository<ArchiveVideo> _repositoryArchiveVideo;
private readonly IAddCreatedVideoToFolderService _addCreatedVideoToFolderService;
private readonly IAppLogger<CreateVideoService> _logger;

public CreateVideoService(GetVideoService getVideoService, IUploadVideoTusService uploadVideoTusService, IRepository<ArchiveVideo> repositoryArchiveVideo, IAddCreatedVideoToFolderService addCreatedVideoToFolderService)
public CreateVideoService(IAppLogger<CreateVideoService> logger, GetVideoService getVideoService, IUploadVideoTusService uploadVideoTusService, IRepository<ArchiveVideo> repositoryArchiveVideo, IAddCreatedVideoToFolderService addCreatedVideoToFolderService)
{
_getVideoService = getVideoService;
_uploadVideoTusService = uploadVideoTusService;
_repositoryArchiveVideo = repositoryArchiveVideo;
_addCreatedVideoToFolderService = addCreatedVideoToFolderService;
_logger = logger;
}

public async Task<string> StartAsync(string videoName, long videoSize, string domain, CancellationToken cancellationToken = default)
Expand All @@ -40,9 +44,15 @@ public async Task<string> StartAsync(string videoName, long videoSize, string do
EmbedDomains = new List<string> { domain },
HideFromVimeo = true
};
var sessionId = await _uploadVideoTusService.StartAsync(uploadVideoRequest, cancellationToken);
var responseSessionId = await _uploadVideoTusService.StartAsync(uploadVideoRequest, cancellationToken);
//TODO: Remove this
_logger.LogInformation($"Error Vimeo: {responseSessionId.Json}");
if (!responseSessionId.IsSuccess || string.IsNullOrEmpty(responseSessionId.Data))
{
_logger.LogError(new Exception(responseSessionId.Exception?.Message), responseSessionId.Json);
}

return sessionId;
return responseSessionId.Data;
}

public async Task<UploadChunkStatus> UploadChunkAsync(bool isBaseFolder, string sessionId, string chunk, string? description, long? folderId, CancellationToken cancellationToken = default)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public MemberByUserIdWithBooksReadAndMemberSubscriptionsSpec(string userId)
.Include(member => member.MemberSubscriptions);

Query
.Include(member => member.UploadedBooks);
.Include(member => member.UploadedBooks)
.AsSplitQuery()
.AsNoTracking();
}
}
1 change: 1 addition & 0 deletions src/DevBetterWeb.Web/DevBetterWeb.Web.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
</PackageReference>
<PackageReference Include="Microsoft.Web.LibraryManager.Build" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="NimblePros.ApiClient" />
<PackageReference Include="NimblePros.Vimeo" />
<PackageReference Include="Sendgrid" />
<PackageReference Include="Serilog" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public UploadChunkEndpoint(ICreateVideoService createVideo, VimeoSettings vimeoS
_vimeoSettings = vimeoSettings;
}

[HttpPost("videos/upload")]
[HttpPost("api/videos/upload")]
public override async Task<ActionResult<UploadChunkStatus>> HandleAsync([FromBody] UploadChunkRequest uploadChunkRequest, CancellationToken cancellationToken = default)
{
if (uploadChunkRequest.Chunk.Length <= 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,19 @@ public class UploadVideoStartEndpoint : EndpointBaseAsync
.WithResult<ActionResult<UploadVideoStartResponse>>
{
private readonly ICreateVideoService _createVideo;
private readonly IAppLogger<UploadVideoStartEndpoint> _logger;

public UploadVideoStartEndpoint(ICreateVideoService createVideo)
public UploadVideoStartEndpoint(ICreateVideoService createVideo,
IAppLogger<UploadVideoStartEndpoint> logger)
{
_createVideo = createVideo;
_logger = logger;
}

[HttpPost("videos/start")]
[HttpPost("api/videos/start")]
public override async Task<ActionResult<UploadVideoStartResponse>> HandleAsync([FromBody] UploadVideoStartRequest request, CancellationToken cancellationToken = default)
{
_logger.LogWarning("HandleAsync called for videos/start");
string domain = HttpContext.Request.Host.Value;
var sessionId = await _createVideo.StartAsync(request.VideoName, request.VideoSize, domain, cancellationToken);

Expand Down
43 changes: 43 additions & 0 deletions src/DevBetterWeb.Web/MigrationService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace DevBetterWeb.Web;

public interface ILocalMigrationService<TContext> where TContext : DbContext
{
Task ApplyDatabaseMigrationsAsync();
}

public class MigrationService<TContext> : ILocalMigrationService<TContext>
where TContext : DbContext
{
private readonly ILogger<MigrationService<TContext>> _logger;

private readonly TContext _dbContext;

public MigrationService(
TContext context,
ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<MigrationService<TContext>>();
_dbContext = context;
}

public async Task ApplyDatabaseMigrationsAsync()
{
if (await NoPendingMigrationsAsync(_dbContext))
{
_logger.LogInformation($"No pending migrations to apply for context: {typeof(TContext).Name}");
return;
}

_logger.LogInformation($"Applying pending migrations for context: {typeof(TContext).Name}...");
await _dbContext.Database.MigrateAsync();
_logger.LogInformation($"Migrations applied successfully for context: {typeof(TContext).Name}");
}

private static async Task<bool> NoPendingMigrationsAsync(TContext dbContext) =>
!(await dbContext.Database.GetPendingMigrationsAsync()).Any();
}
Loading

0 comments on commit 1ca0d96

Please sign in to comment.