diff --git a/Controllers/AppController.cs b/Controllers/AppController.cs index 93eb044..1127890 100644 --- a/Controllers/AppController.cs +++ b/Controllers/AppController.cs @@ -9,27 +9,32 @@ namespace WebApp_AppService.Controllers public class AppController : ControllerBase { private readonly IConfiguration app; - public AppController(IConfiguration configuration) + private readonly ILogger _logger; + + public AppController(IConfiguration configuration, ILogger logger) { app = configuration; + _logger = logger; } [HttpGet] [Route("appinvoke")] public ActionResult appinvoke() { + _logger.LogWarning("DANGEROUS ENDPOINT: appinvoke called - creates memory leak with 2100 event subscribers"); + try { Subscriber.CreatePublishers(); + _logger.LogError("Memory leak created: 2100 subscribers with 1MB each = ~2.1GB memory leak"); + return "Created multiple subscribers to the publisher! WARNING: This creates a memory leak."; } - catch (Exception ex) { + _logger.LogError(ex, "Exception in appinvoke endpoint"); Debug.WriteLine(ex.ToString()); return "Error: " + ex.Message; } - - return "Created multiple subscribers to the publisher!"; } private static Processor p = new Processor(); @@ -67,13 +72,26 @@ public void ProcessTransaction(Customer customer) [Route("memleak/{kb}")] public ActionResult memleak(int kb) { - int it = (kb * 1000) / 100; - for (int i = 0; i < it; i++) + _logger.LogWarning("DANGEROUS ENDPOINT: memleak called with {KB}KB - will cause memory leak", kb); + + try { - p.ProcessTransaction(new Customer(Guid.NewGuid().ToString())); - } + int it = (kb * 1000) / 100; + _logger.LogWarning("Adding {Iterations} customer objects to static cache (memory leak)", it); + + for (int i = 0; i < it; i++) + { + p.ProcessTransaction(new Customer(Guid.NewGuid().ToString())); + } - return "success:memleak"; + _logger.LogError("Memory leak created: {KB}KB worth of objects added to static cache", kb); + return $"success:memleak - Added {it} objects to cache (WARNING: Memory leak created)"; + } + catch (Exception ex) + { + _logger.LogError(ex, "Exception in memleak endpoint with {KB}KB", kb); + return "Error: " + ex.Message; + } } [HttpGet] @@ -81,77 +99,97 @@ public ActionResult memleak(int kb) public async Task> doWork(int? durationInSeconds) { var seconds = durationInSeconds ?? 10; - var start = DateTime.UtcNow; - var endTime = start.AddSeconds(seconds); + _logger.LogWarning("High CPU work started for {Seconds} seconds", seconds); + + try + { + var start = DateTime.UtcNow; + var endTime = start.AddSeconds(seconds); - double result = 0; - long iterations = 0; - int threadCount = (Environment.ProcessorCount > 2) ? (int)Math.Ceiling((decimal)Environment.ProcessorCount / 2) : 1; - object lockObj = new object(); + double result = 0; + long iterations = 0; + int threadCount = (Environment.ProcessorCount > 2) ? (int)Math.Ceiling((decimal)Environment.ProcessorCount / 2) : 1; + object lockObj = new object(); - var tasks = new List(); + _logger.LogInformation("Starting high CPU task with {ThreadCount} threads for {Seconds}s", threadCount, seconds); - for (int i = 0; i < threadCount; i++) - { - tasks.Add(Task.Run(() => - { - double localResult = 0; - long localIterations = 0; + var tasks = new List(); - while (DateTime.UtcNow < endTime) + for (int i = 0; i < threadCount; i++) + { + tasks.Add(Task.Run(() => { - // More expensive operations - localResult += Math.Pow(Math.Sin(localIterations), 2) + Math.Cos(localIterations); - localResult += Math.Sqrt(Math.Abs(localResult)); - localResult += Math.Log(Math.Abs(localResult) + 1); - - // Prime calculation (expensive) - bool isPrime = IsPrime(localIterations % 10000 + 2); - if (isPrime) localResult += 1; - - localIterations++; - } + double localResult = 0; + long localIterations = 0; + + while (DateTime.UtcNow < endTime) + { + // More expensive operations + localResult += Math.Pow(Math.Sin(localIterations), 2) + Math.Cos(localIterations); + localResult += Math.Sqrt(Math.Abs(localResult)); + localResult += Math.Log(Math.Abs(localResult) + 1); + + // Prime calculation (expensive) + bool isPrime = IsPrime(localIterations % 10000 + 2); + if (isPrime) localResult += 1; + + localIterations++; + } + + lock (lockObj) + { + result += localResult; + iterations += localIterations; + } + })); + } - lock (lockObj) + bool IsPrime(long number) + { + if (number < 2) return false; + for (long i = 2; i <= Math.Sqrt(number); i++) { - result += localResult; - iterations += localIterations; + if (number % i == 0) return false; } - })); - } + return true; + } - bool IsPrime(long number) + await Task.WhenAll(tasks); + + _logger.LogWarning("High CPU work completed: {Iterations:N0} iterations, {Result:F2} result", iterations, result); + return $"High CPU task completed! Iterations: {iterations:N0}, Result: {result:F2} for Duration: {durationInSeconds}"; + } + catch (Exception ex) { - if (number < 2) return false; - for (long i = 2; i <= Math.Sqrt(number); i++) - { - if (number % i == 0) return false; - } - return true; + _logger.LogError(ex, "Exception in work endpoint with duration {Seconds}", seconds); + return "Error: " + ex.Message; } - - await Task.WhenAll(tasks); - return $"High CPU task completed! Iterations: {iterations:N0}, Result: {result:F2} for Duration: {durationInSeconds}"; } [HttpGet] [Route("test")] public ActionResult sayhello() { - var connectionString = app.GetConnectionString("StorageAccount"); - if (string.IsNullOrEmpty(connectionString)) - { - throw new InvalidOperationException("Storage account connection string is not configured."); - } - + _logger.LogInformation("Storage connection test requested"); + try { + var connectionString = app.GetConnectionString("StorageAccount"); + if (string.IsNullOrEmpty(connectionString)) + { + _logger.LogError("Storage account connection string is not configured"); + throw new InvalidOperationException("Storage account connection string is not configured."); + } + var blobServiceClient = new BlobServiceClient(connectionString); var accountInfo = blobServiceClient.GetAccountInfo(); + + _logger.LogInformation("Storage account connection successful"); return "Hello, the storage account is connected successfully! Account Name: "; } catch (Exception ex) { + _logger.LogError(ex, "Failed to connect to storage account"); throw new InvalidOperationException($"Failed to connect to storage account: {ex.Message}", ex); } } @@ -162,14 +200,38 @@ public ActionResult sayhello() [Route("crash")] public ActionResult crash() { - double bytesSize = 0; - while (true || bytesSize < 1_000_000) + _logger.LogCritical("CRITICAL DANGER: Crash endpoint called - will cause OutOfMemory exception and application crash"); + + try { - bytesSize += 10 * 1024 * 1024; // 10MB - memoryHog.Add(new byte[10 * 1024 * 1024]); // Allocate 1MB - } + double bytesSize = 0; + int iterations = 0; + + while (true || bytesSize < 1_000_000) + { + bytesSize += 10 * 1024 * 1024; // Track 10MB + memoryHog.Add(new byte[10 * 1024 * 1024]); // Allocate 10MB + iterations++; + + if (iterations % 10 == 0) // Log every 100MB + { + _logger.LogCritical("Memory allocation: {SizeMB}MB allocated, iteration {Iterations}", + bytesSize / (1024 * 1024), iterations); + } + } - return "success:oomd"; + return "success:oomd"; + } + catch (OutOfMemoryException ex) + { + _logger.LogCritical(ex, "OutOfMemory exception occurred as expected in crash endpoint"); + return "OutOfMemory exception occurred - application may crash"; + } + catch (Exception ex) + { + _logger.LogError(ex, "Unexpected exception in crash endpoint"); + return "Unexpected error: " + ex.Message; + } } } } diff --git a/Controllers/DiagnosticsController.cs b/Controllers/DiagnosticsController.cs new file mode 100644 index 0000000..02cef77 --- /dev/null +++ b/Controllers/DiagnosticsController.cs @@ -0,0 +1,205 @@ +using Microsoft.AspNetCore.Mvc; +using System.Diagnostics; +using System.Reflection; +using System.Runtime; + +namespace WebApp_AppService.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class DiagnosticsController : ControllerBase + { + private readonly ILogger _logger; + + public DiagnosticsController(ILogger logger) + { + _logger = logger; + } + + [HttpGet] + [Route("system-info")] + public ActionResult GetSystemInfo() + { + _logger.LogInformation("System info requested"); + + var process = Process.GetCurrentProcess(); + var gcInfo = GC.GetTotalMemory(false); + + var systemInfo = new + { + Timestamp = DateTime.UtcNow, + Environment = new + { + MachineName = Environment.MachineName, + ProcessorCount = Environment.ProcessorCount, + OSVersion = Environment.OSVersion.ToString(), + RuntimeVersion = Environment.Version.ToString(), + Framework = Assembly.GetEntryAssembly()?.GetCustomAttribute()?.FrameworkName + }, + Memory = new + { + WorkingSet = process.WorkingSet64, + PrivateMemory = process.PrivateMemorySize64, + VirtualMemory = process.VirtualMemorySize64, + GCTotalMemory = gcInfo, + GCGen0Collections = GC.CollectionCount(0), + GCGen1Collections = GC.CollectionCount(1), + GCGen2Collections = GC.CollectionCount(2) + }, + Process = new + { + Id = process.Id, + StartTime = process.StartTime, + TotalProcessorTime = process.TotalProcessorTime, + ThreadCount = process.Threads.Count, + HandleCount = process.HandleCount + } + }; + + return Ok(systemInfo); + } + + [HttpGet] + [Route("memory-pressure")] + public ActionResult GetMemoryPressure() + { + _logger.LogInformation("Memory pressure check requested"); + + var process = Process.GetCurrentProcess(); + var gcBefore = GC.GetTotalMemory(false); + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + var gcAfter = GC.GetTotalMemory(false); + + var memoryInfo = new + { + Timestamp = DateTime.UtcNow, + BeforeGC = gcBefore, + AfterGC = gcAfter, + Collected = gcBefore - gcAfter, + WorkingSet = process.WorkingSet64, + PrivateMemory = process.PrivateMemorySize64, + Gen0Collections = GC.CollectionCount(0), + Gen1Collections = GC.CollectionCount(1), + Gen2Collections = GC.CollectionCount(2), + IsServerGC = GCSettings.IsServerGC, + LatencyMode = GCSettings.LatencyMode.ToString() + }; + + if (gcBefore > 500 * 1024 * 1024) // 500MB + { + _logger.LogWarning("High memory usage detected: {MemoryUsage} bytes", gcBefore); + } + + return Ok(memoryInfo); + } + + [HttpGet] + [Route("incident-logs")] + public ActionResult GetIncidentLogs() + { + _logger.LogInformation("Incident logs requested for contoso-chat-net"); + + var incidentInfo = new + { + Timestamp = DateTime.UtcNow, + ResourceDetails = new + { + Name = "contoso-chat-net", + Type = "Web App", + Location = "westus", + ResourceGroup = "mrsharm-operations-agent-3p-rg", + SubscriptionId = "be8d491e-109c-4ee1-aaee-dc7615af0a42" + }, + KnownIssues = new[] + { + new + { + Endpoint = "/api/app/crash", + Severity = "Critical", + Description = "Intentionally causes OutOfMemory exception - infinite memory allocation", + Recommendation = "This endpoint should be disabled in production" + }, + new + { + Endpoint = "/api/app/memleak/{kb}", + Severity = "High", + Description = "Causes memory leaks by accumulating objects in static cache", + Recommendation = "Monitor memory usage and restart application if necessary" + }, + new + { + Endpoint = "/api/app/appinvoke", + Severity = "High", + Description = "Creates 2100 event subscribers that are never cleaned up", + Recommendation = "Causes gradual memory leak - monitor memory pressure" + }, + new + { + Endpoint = "/api/app/work", + Severity = "Medium", + Description = "Creates high CPU usage with expensive mathematical operations", + Recommendation = "Monitor CPU usage and consider rate limiting" + }, + new + { + Endpoint = "/api/app/test", + Severity = "Medium", + Description = "May throw exceptions if storage connection string not configured", + Recommendation = "Ensure proper configuration and exception handling" + } + }, + SystemHealth = GetQuickHealthCheck() + }; + + return Ok(incidentInfo); + } + + [HttpPost] + [Route("force-gc")] + public ActionResult ForceGarbageCollection() + { + _logger.LogWarning("Manual garbage collection triggered"); + + var beforeGC = GC.GetTotalMemory(false); + var stopwatch = Stopwatch.StartNew(); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + + stopwatch.Stop(); + var afterGC = GC.GetTotalMemory(false); + + var result = new + { + Timestamp = DateTime.UtcNow, + MemoryBeforeGC = beforeGC, + MemoryAfterGC = afterGC, + MemoryFreed = beforeGC - afterGC, + ElapsedMs = stopwatch.ElapsedMilliseconds, + Gen0Collections = GC.CollectionCount(0), + Gen1Collections = GC.CollectionCount(1), + Gen2Collections = GC.CollectionCount(2) + }; + + return Ok(result); + } + + private object GetQuickHealthCheck() + { + var process = Process.GetCurrentProcess(); + var memory = GC.GetTotalMemory(false); + + return new + { + Status = memory > 1024 * 1024 * 1024 ? "Warning" : "Healthy", // 1GB threshold + MemoryUsage = memory, + ProcessorTime = process.TotalProcessorTime, + ThreadCount = process.Threads.Count, + LastChecked = DateTime.UtcNow + }; + } + } +} \ No newline at end of file diff --git a/INCIDENT_RESPONSE.md b/INCIDENT_RESPONSE.md new file mode 100644 index 0000000..e2f5291 --- /dev/null +++ b/INCIDENT_RESPONSE.md @@ -0,0 +1,133 @@ +# Incident Response Guide for contoso-chat-net + +## Resource Details +- **Name**: contoso-chat-net +- **Type**: Azure Web App +- **Location**: westus +- **Resource Group**: mrsharm-operations-agent-3p-rg +- **Subscription ID**: be8d491e-109c-4ee1-aaee-dc7615af0a42 + +## Diagnostic Endpoints + +### Health Checks +- `GET /health` - Basic health check +- `GET /health/ready` - Readiness probe +- `GET /health/live` - Liveness probe + +### System Diagnostics +- `GET /api/diagnostics/system-info` - Comprehensive system information +- `GET /api/diagnostics/memory-pressure` - Memory usage analysis +- `GET /api/diagnostics/incident-logs` - Incident-specific information +- `POST /api/diagnostics/force-gc` - Manual garbage collection (emergency use) + +## Known Problematic Endpoints ⚠️ + +### CRITICAL - Application Crash Risk +- **Endpoint**: `GET /api/app/crash` +- **Risk**: Causes OutOfMemory exception and application crash +- **Description**: Infinite loop allocating 10MB blocks until memory exhausted +- **Response**: Immediately disable endpoint, restart application if crashed + +### HIGH - Memory Leak Risk +- **Endpoint**: `GET /api/app/memleak/{kb}` +- **Risk**: Accumulates objects in static cache causing memory leaks +- **Description**: Adds customer objects to never-cleared static cache +- **Monitoring**: Watch memory usage trends, GC pressure +- **Response**: Restart application, monitor `/api/diagnostics/memory-pressure` + +- **Endpoint**: `GET /api/app/appinvoke` +- **Risk**: Creates 2100 event subscribers with 1MB each (~2.1GB leak) +- **Description**: Event handlers never unsubscribed, objects remain in memory +- **Monitoring**: Memory usage spikes after calls +- **Response**: Monitor memory, restart if excessive memory usage + +### MEDIUM - Performance Impact +- **Endpoint**: `GET /api/app/work?durationInSeconds={n}` +- **Risk**: High CPU usage for specified duration +- **Description**: CPU-intensive mathematical operations across multiple threads +- **Monitoring**: CPU usage, response times +- **Response**: Consider rate limiting, monitor system load + +- **Endpoint**: `GET /api/app/test` +- **Risk**: Configuration-dependent failures +- **Description**: Requires Azure Storage connection string +- **Monitoring**: Exception logs, configuration validation +- **Response**: Verify connection string configuration + +## Incident Investigation Process + +### 1. Initial Assessment +```bash +# Check application health +curl https://contoso-chat-net.azurewebsites.net/health + +# Get system information +curl https://contoso-chat-net.azurewebsites.net/api/diagnostics/system-info + +# Check memory pressure +curl https://contoso-chat-net.azurewebsites.net/api/diagnostics/memory-pressure +``` + +### 2. Memory Issues Investigation +- Monitor memory usage trends in Azure portal +- Check GC collection frequency and pressure +- Use `/api/diagnostics/memory-pressure` to assess current state +- Review recent calls to `/api/app/memleak/*` or `/api/app/appinvoke` + +### 3. Performance Issues Investigation +- Check CPU utilization in Azure portal +- Review calls to `/api/app/work` endpoint +- Monitor response times and concurrent requests + +### 4. Application Crashes +- Check application logs for OutOfMemory exceptions +- Verify if `/api/app/crash` endpoint was called +- Monitor application restart events + +## Emergency Response Actions + +### Memory Pressure +1. Force garbage collection: `POST /api/diagnostics/force-gc` +2. Monitor improvement via `/api/diagnostics/memory-pressure` +3. If no improvement, restart the application +4. Scale up instance size if recurring issue + +### Application Crash +1. Check Azure portal for crash details +2. Review application logs for OutOfMemory exceptions +3. Restart the application +4. Disable problematic endpoints if necessary + +### High CPU Usage +1. Monitor current CPU load +2. Check for active calls to `/api/app/work` +3. Implement rate limiting if needed +4. Scale out with additional instances + +## Monitoring and Alerting + +### Key Metrics to Monitor +- Memory usage (Working Set, Private Memory) +- CPU utilization +- Request response times +- Error rates and exceptions +- GC collection frequency + +### Recommended Alerts +- Memory usage > 80% of available +- CPU usage > 85% for >5 minutes +- Error rate > 5% for >2 minutes +- OutOfMemory exceptions (immediate alert) +- Application restart events + +## Prevention Measures + +1. **Endpoint Safety**: Disable or rate-limit dangerous endpoints in production +2. **Resource Limits**: Implement memory and CPU limits +3. **Health Checks**: Use health check endpoints for monitoring +4. **Logging**: Comprehensive logging for all operations +5. **Monitoring**: Continuous monitoring of key metrics + +## Contact Information +- **Assigned to**: mrsharm (for triage and resolution) +- **SRE Agent Link**: [Azure Portal Link](https://portal.azure.com/?feature.customPortal=false&feature.canmodifystamps=true&feature.fastmanifest=false&nocdn=force&websitesextension_loglevel=verbose&Microsoft_Azure_PaasServerless=betaµsoft_azure_paasserverless_assettypeoptions=%7B%22SreAgentCustomMenu%22%3A%7B%22options%22%3A%22%22%7D%7D#view/Microsoft_Azure_PaasServerless/AgentFrameBlade.ReactView/id/%2Fsubscriptions%2F%2FresourceGroups%2F%2Fproviders%2FMicrosoft.App%2Fagents%2F/sreLink/%2Fviews%2Factivities%2Fthreads%2F22499ac3-4d0d-4bd9-8a0d-d55dca0b0ca7) \ No newline at end of file diff --git a/Program.cs b/Program.cs index c6c3db7..8c0e1f6 100644 --- a/Program.cs +++ b/Program.cs @@ -8,24 +8,42 @@ public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); + // Add services to the container builder.Services.AddControllers(); - // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); + + // Add health checks + builder.Services.AddHealthChecks() + .AddCheck("self", () => Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult.Healthy("API is running")); + + // Add logging + builder.Logging.ClearProviders(); + builder.Logging.AddConsole(); + builder.Logging.AddDebug(); var app = builder.Build(); - // Configure the HTTP request pipeline. + // Configure the HTTP request pipeline if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } + // Add health check endpoints + app.MapHealthChecks("/health"); + app.MapHealthChecks("/health/ready"); + app.MapHealthChecks("/health/live"); + app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); + // Log startup + var logger = app.Services.GetRequiredService>(); + logger.LogInformation("Application starting up - contoso-chat-net"); + app.Run(); } } diff --git a/PublisherSubscriber.cs b/PublisherSubscriber.cs index eba63a5..af03266 100644 --- a/PublisherSubscriber.cs +++ b/PublisherSubscriber.cs @@ -2,7 +2,7 @@ { public class Publisher { - public event EventHandler SomeEvent; + public event EventHandler? SomeEvent; public void RaiseEvent() { @@ -19,7 +19,7 @@ public void Subscribe(Publisher publisher) publisher.SomeEvent += OnEvent; // Never unsubscribed } - private void OnEvent(object sender, EventArgs e) + private void OnEvent(object? sender, EventArgs e) { Console.WriteLine($"Event received, data size: {_largeData.Length}"); } diff --git a/SOLUTION_SUMMARY.md b/SOLUTION_SUMMARY.md new file mode 100644 index 0000000..5d10dfd --- /dev/null +++ b/SOLUTION_SUMMARY.md @@ -0,0 +1,69 @@ +# Incident Response Implementation Summary + +## Overview +This solution addresses the incident report for Azure Web App "contoso-chat-net" by implementing comprehensive monitoring, logging, and diagnostic capabilities to help investigate and resolve incidents. + +## Key Features Implemented + +### 1. Health Check Endpoints +- `/health` - Basic application health +- `/health/ready` - Readiness probe for Azure monitoring +- `/health/live` - Liveness probe for Azure monitoring + +### 2. Comprehensive Diagnostics API +- `GET /api/diagnostics/system-info` - System metrics (CPU, memory, threads, GC) +- `GET /api/diagnostics/memory-pressure` - Memory analysis with garbage collection +- `GET /api/diagnostics/incident-logs` - Incident-specific information with known issues +- `POST /api/diagnostics/force-gc` - Emergency garbage collection + +### 3. Enhanced Logging & Monitoring +- Structured logging with severity levels (INFO, WARN, ERROR, CRITICAL) +- Dangerous endpoint detection and logging +- Memory leak tracking and alerting +- Performance monitoring for CPU-intensive operations +- Exception handling with detailed context + +### 4. Known Issues Documentation +The system now identifies and warns about problematic endpoints: +- **CRITICAL**: `/api/app/crash` - Causes OutOfMemory crash +- **HIGH**: `/api/app/memleak/{kb}` - Memory leak via static cache +- **HIGH**: `/api/app/appinvoke` - Memory leak via event subscribers +- **MEDIUM**: `/api/app/work` - High CPU usage +- **MEDIUM**: `/api/app/test` - Storage connection issues + +## Usage Examples + +```bash +# Check application health +curl https://contoso-chat-net.azurewebsites.net/health + +# Get comprehensive system information +curl https://contoso-chat-net.azurewebsites.net/api/diagnostics/system-info + +# Check memory pressure and perform analysis +curl https://contoso-chat-net.azurewebsites.net/api/diagnostics/memory-pressure + +# Get incident-specific logs and known issues +curl https://contoso-chat-net.azurewebsites.net/api/diagnostics/incident-logs + +# Force garbage collection in emergency +curl -X POST https://contoso-chat-net.azurewebsites.net/api/diagnostics/force-gc +``` + +## Benefits for Incident Response + +1. **Real-time Monitoring**: Health checks and system metrics for proactive monitoring +2. **Issue Detection**: Automatic logging of dangerous operations and memory leaks +3. **Diagnostic Information**: Detailed system information for troubleshooting +4. **Emergency Tools**: Manual garbage collection for memory pressure situations +5. **Documentation**: Complete incident response guide with procedures + +## Azure Integration + +The solution is designed to work with Azure App Service monitoring: +- Health check endpoints for Azure health monitoring +- Structured logging compatible with Azure Log Analytics +- Metrics suitable for Azure Application Insights +- Emergency procedures documented for Azure portal operations + +This implementation provides the necessary tools and information to effectively investigate, troubleshoot, and resolve incidents in the contoso-chat-net Azure Web App. \ No newline at end of file diff --git a/WebApp_AppService.csproj b/WebApp_AppService.csproj index 1b9714c..dc1062a 100644 --- a/WebApp_AppService.csproj +++ b/WebApp_AppService.csproj @@ -9,6 +9,8 @@ + +