diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 00000000..fb6905d0
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,35 @@
+name: Azure Bicep
+
+on:
+ workflow_dispatch
+
+env:
+ targetEnv: dev
+
+jobs:
+ build-and-deploy:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pages: write
+ id-token: write
+ steps:
+ # Checkout code
+ - uses: actions/checkout@main
+
+ # Log into Azure
+ - uses: azure/login@v2.1.1
+ with:
+ client-id: ${{ secrets.AZURE_CLIENT_ID }}
+ tenant-id: ${{ secrets.AZURE_TENANT_ID }}
+ subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ enable-AzPSSession: true
+
+ # Deploy ARM template
+ - name: Run ARM deploy
+ uses: azure/arm-deploy@v1
+ with:
+ subscriptionId: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ resourceGroupName: ${{ secrets.AZURE_RG }}
+ template: ./src/InfrastructureAsCode/main.bicep
+ parameters: environment=${{ env.targetEnv }}
\ No newline at end of file
diff --git a/.github/workflows/dotnet-deploy-1.yml b/.github/workflows/dotnet-deploy-1.yml
new file mode 100644
index 00000000..f976499f
--- /dev/null
+++ b/.github/workflows/dotnet-deploy-1.yml
@@ -0,0 +1,31 @@
+name: .NET CI
+
+on:
+ push:
+ branches: [ main ]
+ paths:
+ - src/Application/**
+ pull_request:
+ branches: [ main ]
+ paths:
+ - src/Application/**
+ # Allows you to run this workflow manually from the Actions tab
+ workflow_dispatch:
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: '8.0.x'
+
+ - name: Restore dependencies
+ run: dotnet restore ./src/Application/src/RazorPagesTestSample/RazorPagesTestSample.csproj
+ - name: Build
+ run: dotnet build --no-restore ./src/Application/src/RazorPagesTestSample/RazorPagesTestSample.csproj
+ - name: Test
+ run: dotnet test --no-build --verbosity normal ./src/Application/tests/RazorPagesTestSample.Tests/RazorPagesTestSample.Tests.csproj
\ No newline at end of file
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 00000000..cb588375
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,22 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "Save All Files",
+ "command": "${command:workbench.action.files.saveAll}",
+ "type": "shell",
+ "problemMatcher": []
+ },
+ {
+ "label": "Build Project",
+ "dependsOn": "Save All Files",
+ "command": "dotnet build",
+ "type": "shell",
+ "group": {
+ "kind": "build",
+ "isDefault": true
+ },
+ "problemMatcher": "$msCompile"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/docs/03_improve_deploy_app/0301.md b/docs/03_improve_deploy_app/0301.md
index 2c32af80..1ee87072 100644
--- a/docs/03_improve_deploy_app/0301.md
+++ b/docs/03_improve_deploy_app/0301.md
@@ -13,7 +13,7 @@ We have a work machine configured, so now it is time to ensure that everything w
## Description
-Developers at Munson's Pickles and Preserves like the Team Messaging System in general, but they consistently bring up issues with one aspect of the application: it limits messages to 200 characters or fewer. Developers have made a case that the app should support messages of up to 250 characters in length instead.
+Developers at Munson's Pickles and Preserves like the Team Messaging System in general, but they consistently bring up issues with one aspect of the application: it limits messages to 250 characters or fewer. Developers have made a case that the app should support messages of up to 250 characters in length instead.
In this task, you will modify the application to support 250 characters instead of 200. You will also follow a feature branching strategy and use a pull request to bring your change into the `main` branch. If you wish to complete this task using a Test-Driven Design approach, please read the **Advanced Challenges (optional)** section below before making any changes.
diff --git a/src/Application/src/RazorPagesTestSample/Data/Message.cs b/src/Application/src/RazorPagesTestSample/Data/Message.cs
index ea99cbd6..7e8955d2 100644
--- a/src/Application/src/RazorPagesTestSample/Data/Message.cs
+++ b/src/Application/src/RazorPagesTestSample/Data/Message.cs
@@ -9,8 +9,8 @@ public class Message
[Required]
[DataType(DataType.Text)]
- [StringLength(200, ErrorMessage = "There's a 200 character limit on messages. Please shorten your message.")]
+ [StringLength(250, ErrorMessage = "There's a 250 character limit on messages. Please shorten your message.")]
public string Text { get; set; }
}
#endregion
-}
+}
\ No newline at end of file
diff --git a/src/Application/src/RazorPagesTestSample/Dockerfile b/src/Application/src/RazorPagesTestSample/Dockerfile
new file mode 100644
index 00000000..c4f208e6
--- /dev/null
+++ b/src/Application/src/RazorPagesTestSample/Dockerfile
@@ -0,0 +1,24 @@
+# Use the official .NET 8 SDK image as a build stage
+FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
+WORKDIR /app
+
+# Copy the project file and restore dependencies
+COPY *.csproj .
+RUN dotnet restore
+
+# Copy the rest of the application code
+COPY . .
+
+# Build the application
+RUN dotnet publish -c Release -o out
+
+# Use the official .NET 8 runtime image as a runtime stage
+FROM mcr.microsoft.com/dotnet/aspnet:8.0
+WORKDIR /app
+COPY --from=build /app/out .
+
+# Set the environment variable for ASP.NET Core HTTP ports
+ENV ASPNETCORE_HTTP_PORTS=80
+
+# Set the entry point for the application
+ENTRYPOINT ["dotnet", "RazorPagesTestSample.dll"]
\ No newline at end of file
diff --git a/src/Application/src/RazorPagesTestSample/Pages/Error.cshtml b/src/Application/src/RazorPagesTestSample/Pages/Error.cshtml
index 6f92b956..b34f2566 100644
--- a/src/Application/src/RazorPagesTestSample/Pages/Error.cshtml
+++ b/src/Application/src/RazorPagesTestSample/Pages/Error.cshtml
@@ -5,7 +5,7 @@
}
Error.
-An error occurred while processing your request.
+An error occurred while processing your request.
@if (Model.ShowRequestId)
{
diff --git a/src/Application/tests/RazorPagesTestSample.Tests/RazorPagesTestSample.Tests.csproj b/src/Application/tests/RazorPagesTestSample.Tests/RazorPagesTestSample.Tests.csproj
index a66e0a92..51174545 100644
--- a/src/Application/tests/RazorPagesTestSample.Tests/RazorPagesTestSample.Tests.csproj
+++ b/src/Application/tests/RazorPagesTestSample.Tests/RazorPagesTestSample.Tests.csproj
@@ -1,19 +1,15 @@
-
net8.0
-
-
-
-
+
@@ -22,12 +18,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
- ..\..\src\RazorPagesTestSample\bin\Debug\net8.0\RazorPagesTestSample.dll
-
+
-
-
+
\ No newline at end of file
diff --git a/src/Application/tests/RazorPagesTestSample.Tests/UnitTests/DataAccessLayerTest.cs b/src/Application/tests/RazorPagesTestSample.Tests/UnitTests/DataAccessLayerTest.cs
index 91a91aaa..0cb535b8 100644
--- a/src/Application/tests/RazorPagesTestSample.Tests/UnitTests/DataAccessLayerTest.cs
+++ b/src/Application/tests/RazorPagesTestSample.Tests/UnitTests/DataAccessLayerTest.cs
@@ -4,6 +4,7 @@
using Microsoft.EntityFrameworkCore;
using Xunit;
using RazorPagesTestSample.Data;
+using System.ComponentModel.DataAnnotations;
namespace RazorPagesTestSample.Tests.UnitTests
{
@@ -25,7 +26,7 @@ public async Task GetMessagesAsync_MessagesAreReturned()
// Assert
var actualMessages = Assert.IsAssignableFrom>(result);
Assert.Equal(
- expectedMessages.OrderBy(m => m.Id).Select(m => m.Text),
+ expectedMessages.OrderBy(m => m.Id).Select(m => m.Text),
actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
}
}
@@ -77,7 +78,7 @@ public async Task DeleteMessageAsync_MessageIsDeleted_WhenMessageIsFound()
await db.AddRangeAsync(seedMessages);
await db.SaveChangesAsync();
var recId = 1;
- var expectedMessages =
+ var expectedMessages =
seedMessages.Where(message => message.Id != recId).ToList();
#endregion
@@ -90,7 +91,7 @@ public async Task DeleteMessageAsync_MessageIsDeleted_WhenMessageIsFound()
// Assert
var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
Assert.Equal(
- expectedMessages.OrderBy(m => m.Id).Select(m => m.Text),
+ expectedMessages.OrderBy(m => m.Id).Select(m => m.Text),
actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
#endregion
}
@@ -121,10 +122,42 @@ public async Task DeleteMessageAsync_NoMessageIsDeleted_WhenMessageIsNotFound()
// Assert
var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
Assert.Equal(
- expectedMessages.OrderBy(m => m.Id).Select(m => m.Text),
+ expectedMessages.OrderBy(m => m.Id).Select(m => m.Text),
actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
}
}
+
+
+
+ //Generate a unit test theory to generate messages of various lengths including 250 and try to validate the message object.
+ [Theory]
+ [InlineData(150, true)]
+ [InlineData(199, true)]
+ [InlineData(200, true)]
+ [InlineData(201, true)]
+ [InlineData(249, true)]
+ [InlineData(250, true)]
+ [InlineData(251, false)]
+ [InlineData(300, false)]
+ public async Task AddMessageAsync_TestMessageLength(int messageLength, bool expectedValidMessage)
+ {
+ using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
+ {
+ // Arrange
+ var recId = 10;
+ var expectedMessage = new Message() { Id = recId, Text = new string('X', messageLength) };
+
+ // Act
+ var isValidMessage = Validator.TryValidateObject(expectedMessage, new ValidationContext(expectedMessage), null, validateAllProperties: true);
+
+ // Simulate an asynchronous operation
+ await Task.Delay(1);
+
+ // Assert
+ Assert.Equal(expectedValidMessage, isValidMessage);
+ }
+ }
+
#endregion
}
}
diff --git a/src/Application/tests/RazorPagesTestSample.Tests/UnitTests/MessageTests.cs b/src/Application/tests/RazorPagesTestSample.Tests/UnitTests/MessageTests.cs
new file mode 100644
index 00000000..5b91f3ae
--- /dev/null
+++ b/src/Application/tests/RazorPagesTestSample.Tests/UnitTests/MessageTests.cs
@@ -0,0 +1,85 @@
+using System.ComponentModel.DataAnnotations;
+using RazorPagesTestSample.Data;
+using Xunit;
+using System.Collections.Generic;
+
+namespace RazorPagesTestSample.Tests.UnitTests
+{
+ public class MessageTests
+ {
+ [Fact]
+ public void MessageText_ShouldNotExceed250Characters()
+ {
+ // Arrange
+ var message = new Message
+ {
+ Text = new string('a', 251) // 251 characters
+ };
+ var validationContext = new ValidationContext(message);
+ var validationResults = new List();
+
+ // Act
+ var isValid = Validator.TryValidateObject(message, validationContext, validationResults, true);
+
+ // Assert
+ Assert.False(isValid);
+ Assert.Contains(validationResults, v => v.ErrorMessage.Contains("250 character limit"));
+ }
+
+ [Fact]
+ public void MessageText_ShouldBeValid_WhenWithin250Characters()
+ {
+ // Arrange
+ var message = new Message
+ {
+ Text = new string('a', 250) // 250 characters
+ };
+ var validationContext = new ValidationContext(message);
+ var validationResults = new List();
+
+ // Act
+ var isValid = Validator.TryValidateObject(message, validationContext, validationResults, true);
+
+ // Assert
+ Assert.True(isValid);
+ }
+
+ //test the insertion of a message of length 150
+ [Fact]
+ public void MessageText_ShouldBeValid_WhenWithin150Characters()
+ {
+ // Arrange
+ var message = new Message
+ {
+ Text = new string('a', 150) // 150 characters
+ };
+ var validationContext = new ValidationContext(message);
+ var validationResults = new List();
+
+ // Act
+ var isValid = Validator.TryValidateObject(message, validationContext, validationResults, true);
+
+ // Assert
+ Assert.True(isValid);
+ }
+
+ //test a message of length 249
+ [Fact]
+ public void MessageText_ShouldBeValid_WhenWithin249Characters()
+ {
+ // Arrange
+ var message = new Message
+ {
+ Text = new string('a', 249) // 249 characters
+ };
+ var validationContext = new ValidationContext(message);
+ var validationResults = new List();
+
+ // Act
+ var isValid = Validator.TryValidateObject(message, validationContext, validationResults, true);
+
+ // Assert
+ Assert.True(isValid);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/InfrastructureAsCode/main.bicep b/src/InfrastructureAsCode/main.bicep
index 6dc69618..d8e740d2 100644
--- a/src/InfrastructureAsCode/main.bicep
+++ b/src/InfrastructureAsCode/main.bicep
@@ -8,10 +8,98 @@ var webAppName = '${uniqueString(resourceGroup().id)}-${environment}'
var appServicePlanName = '${uniqueString(resourceGroup().id)}-mpnp-asp'
var logAnalyticsName = '${uniqueString(resourceGroup().id)}-mpnp-la'
var appInsightsName = '${uniqueString(resourceGroup().id)}-mpnp-ai'
-var sku = 'S1'
+var sku = 'P0V3'
var registryName = '${uniqueString(resourceGroup().id)}mpnpreg'
var registrySku = 'Standard'
var imageName = 'techexcel/dotnetcoreapp'
var startupCommand = ''
-// TODO: complete this script
+
+resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = {
+ name: logAnalyticsName
+ location: location
+ properties: {
+ sku: {
+ name: 'PerGB2018'
+ }
+ retentionInDays: 90
+ workspaceCapping: {
+ dailyQuotaGb: 1
+ }
+ }
+}
+
+resource appInsights 'Microsoft.Insights/components@2020-02-02-preview' = {
+ name: appInsightsName
+ location: location
+ kind: 'web'
+ properties: {
+ Application_Type: 'web'
+ WorkspaceResourceId: logAnalyticsWorkspace.id
+ }
+}
+
+resource containerRegistry 'Microsoft.ContainerRegistry/registries@2020-11-01-preview' = {
+ name: registryName
+ location: location
+ sku: {
+ name: registrySku
+ }
+ properties: {
+ adminUserEnabled: true
+ }
+}
+
+resource appServicePlan 'Microsoft.Web/serverFarms@2022-09-01' = {
+ name: appServicePlanName
+ location: location
+ kind: 'linux'
+ properties: {
+ reserved: true
+ }
+ sku: {
+ name: sku
+ }
+}
+
+resource appServiceApp 'Microsoft.Web/sites@2020-12-01' = {
+ name: webAppName
+ location: location
+ properties: {
+ serverFarmId: appServicePlan.id
+ httpsOnly: true
+ clientAffinityEnabled: false
+ siteConfig: {
+ linuxFxVersion: 'DOCKER|${containerRegistry.name}.azurecr.io/${uniqueString(resourceGroup().id)}/${imageName}'
+ http20Enabled: true
+ minTlsVersion: '1.2'
+ appCommandLine: startupCommand
+ appSettings: [
+ {
+ name: 'WEBSITES_ENABLE_APP_SERVICE_STORAGE'
+ value: 'false'
+ }
+ {
+ name: 'DOCKER_REGISTRY_SERVER_URL'
+ value: 'https://${containerRegistry.name}.azurecr.io'
+ }
+ {
+ name: 'DOCKER_REGISTRY_SERVER_USERNAME'
+ value: containerRegistry.name
+ }
+ {
+ name: 'DOCKER_REGISTRY_SERVER_PASSWORD'
+ value: containerRegistry.listCredentials().passwords[0].value
+ }
+ {
+ name: 'APPINSIGHTS_INSTRUMENTATIONKEY'
+ value: appInsights.properties.InstrumentationKey
+ }
+ ]
+ }
+ }
+}
+
+output application_name string = appServiceApp.name
+output application_url string = appServiceApp.properties.hostNames[0]
+output container_registry_name string = containerRegistry.name