Public/New-ApiToolsCrudApi.ps1
|
<#
.SYNOPSIS Generates a complete ASP.NET Core Web API with CRUD controllers from an existing database. .DESCRIPTION New-ApiToolsCrudApi connects to SQL Server or PostgreSQL, scaffolds Entity Framework models from the database schema, generates CRUD controllers for each table, and configures a ready-to-run Web API project with Swagger. The command supports interactive mode with connection string examples, automatic PostgreSQL ODBC driver discovery, and requires only the .NET CLI tools (dotnet ef and aspnet-codegenerator). SQL Server uses System.Data.SqlClient and PostgreSQL uses System.Data.Odbc for connection validation before scaffolding. .PARAMETER ConnectionString The database connection string. For SQL Server use Windows authentication like "Server=localhost;Database=mydb;Trusted_Connection=True;" or SQL authentication like "Server=localhost;Database=mydb;User Id=sa;Password=StrongPwd!". For PostgreSQL use native format like "Server=localhost;Port=5432;Database=mydb;User Id=postgres;Password=secret". If omitted, interactive mode prompts with examples. .PARAMETER ProjectName The name for the generated Web API project. If omitted, defaults to "{DatabaseName}_CRUD_API". The function handles naming conflicts automatically by appending numeric suffixes. .PARAMETER OutputPath The directory where the project folder will be created. Defaults to the current directory. .PARAMETER Force If supplied, overwrites an existing project directory with the same name. .PARAMETER DryRun When present, validates prerequisites and connection, shows what would be generated, and performs no changes. .EXAMPLE # Interactive mode with examples PS> New-ApiToolsCrudApi Prompts for connection string with SQL Server and PostgreSQL examples, validates tools and connection. .EXAMPLE # Generate API from SQL Server database PS> New-ApiToolsCrudApi -ConnectionString "Server=localhost;Database=Hospital_db;Trusted_Connection=True;" Scaffolds models and controllers from Hospital_db, creates a complete Web API project named Hospital_db_CRUD_API. .EXAMPLE # Generate API from PostgreSQL with custom project name PS> New-ApiToolsCrudApi -ConnectionString "Server=localhost;Port=5432;Database=hospital_db;User Id=postgres;Password=Secret123!" -ProjectName "HospitalAPI" Creates HospitalAPI project with auto-discovered PostgreSQL ODBC driver, scaffolds all models and CRUD controllers. .EXAMPLE # Dry-run to preview what would be generated PS> New-ApiToolsCrudApi -ConnectionString "Server=localhost;Database=mydb;Trusted_Connection=True;" -DryRun Validates connection and tools, shows project structure that would be created without generating files. .EXAMPLE # Force overwrite existing project PS> New-ApiToolsCrudApi -ConnectionString "Server=localhost;Database=mydb;Trusted_Connection=True;" -ProjectName "MyAPI" -Force Deletes existing MyAPI folder if present and regenerates the complete project. .INPUTS None. You pipe nothing to this command. .OUTPUTS A PSCustomObject summary with Action, Engine, Database, ProjectName, ProjectPath, ModelsGenerated, ControllersGenerated, and Created fields when execution succeeds. In -DryRun mode returns a plan object. .NOTES Author: Your Name Module: apitools This command requires .NET CLI with dotnet-ef and dotnet-aspnet-codegenerator global tools installed. The function validates these prerequisites before execution. PostgreSQL ODBC driver discovery is automatic. .CHANGELOG v1.1 - Fixed PostgreSQL scaffolding issue by replacing array splatting with direct argument passing v1.2 - Fixed PostgreSQL controller generation by adding Microsoft.EntityFrameworkCore.SqlServer package requirement (aspnet-codegenerator bug workaround) #> function New-ApiToolsCrudApi { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] param( [Parameter(Mandatory = $false)] [string]$ConnectionString, [string]$ProjectName, [string]$OutputPath = (Get-Location).Path, [switch]$Force, [switch]$DryRun ) # ========================================================================= # HELPER FUNCTION: Convert PostgreSQL native connection string to ODBC # ========================================================================= function ConvertTo-PostgreSqlOdbc { param([string]$NativeConnectionString) # Already ODBC format - return as-is if ($NativeConnectionString -match '(?i)Driver\s*=') { return $NativeConnectionString } # Auto-discover PostgreSQL ODBC driver Write-Verbose "Auto-discovering PostgreSQL ODBC driver..." $pgDriver = $null try { $pgDriver = (Get-OdbcDriver -Platform '64-bit' -ErrorAction SilentlyContinue | Where-Object { $_.Name -match 'PostgreSQL.*UNICODE' } | Select-Object -First 1).Name } catch { # Fallback: try common driver names $commonDrivers = @('PostgreSQL Unicode(x64)', 'PostgreSQL Unicode') foreach ($driver in $commonDrivers) { try { $testConn = "Driver={$driver};Server=localhost" $null = New-Object System.Data.Odbc.OdbcConnection $testConn $pgDriver = $driver break } catch { } } } if (-not $pgDriver) { throw "No 64-bit PostgreSQL ODBC driver found. Install psqlODBC (x64) from https://www.postgresql.org/ftp/odbc/versions/ and try again." } Write-Verbose "Using PostgreSQL ODBC driver: $pgDriver" # Parse native connection string $params = @{} foreach ($part in ($NativeConnectionString -split ';')) { $p = $part.Trim() if ($p -match '^(?i)([^=]+)=(.+)$') { $key = $Matches[1].Trim() $value = $Matches[2].Trim() $params[$key] = $value } } # Build ODBC connection string $odbcParts = @("Driver={$pgDriver}") if ($params.ContainsKey('Server') -or $params.ContainsKey('Host')) { $server = if ($params.ContainsKey('Server')) { $params['Server'] } else { $params['Host'] } $odbcParts += "Server=$server" } if ($params.ContainsKey('Port')) { $odbcParts += "Port=$($params['Port'])" } if ($params.ContainsKey('Database') -or $params.ContainsKey('Initial Catalog')) { $db = if ($params.ContainsKey('Database')) { $params['Database'] } else { $params['Initial Catalog'] } $odbcParts += "Database=$db" } if ($params.ContainsKey('User Id') -or $params.ContainsKey('Username') -or $params.ContainsKey('Uid')) { $uid = if ($params.ContainsKey('User Id')) { $params['User Id'] } elseif ($params.ContainsKey('Username')) { $params['Username'] } else { $params['Uid'] } $odbcParts += "Uid=$uid" } if ($params.ContainsKey('Password') -or $params.ContainsKey('Pwd')) { $myPwd = if ($params.ContainsKey('Password')) { $params['Password'] } else { $params['Pwd'] } $odbcParts += "Pwd=$myPwd" } if ($params.ContainsKey('SSL Mode') -or $params.ContainsKey('SslMode')) { $ssl = if ($params.ContainsKey('SSL Mode')) { $params['SSL Mode'] } else { $params['SslMode'] } $odbcParts += "SSLMode=$ssl" } return ($odbcParts -join ';') } # ========================================================================= # STEP 1: CHECK REQUIRED .NET CLI TOOLS # ========================================================================= Write-Verbose "Checking required .NET development tools..." $toolErrors = @() # Check for .NET CLI try { $dotnetVersion = dotnet --version 2>$null if ($null -eq $dotnetVersion) { throw "dotnet command not found" } Write-Verbose "✓ .NET CLI found (version: $dotnetVersion)" } catch { $toolErrors += "The .NET CLI is required but not installed. Download from https://dotnet.microsoft.com/download" } # Check for Entity Framework CLI tool try { $efVersion = dotnet ef --version 2>$null if ($null -eq $efVersion) { throw "dotnet-ef tool not found" } Write-Verbose "✓ Entity Framework CLI tool found" } catch { $toolErrors += "The dotnet-ef tool is required. Install with: dotnet tool install --global dotnet-ef" } # Check for ASP.NET Code Generator tool try { $codegenVersion = dotnet aspnet-codegenerator --help 2>$null if ($null -eq $codegenVersion) { throw "dotnet-aspnet-codegenerator tool not found" } Write-Verbose "✓ ASP.NET Code Generator tool found" } catch { $toolErrors += "The dotnet-aspnet-codegenerator tool is required. Install with: dotnet tool install --global dotnet-aspnet-codegenerator" } if ($toolErrors.Count -gt 0) { throw "Missing required tools:`n" + ($toolErrors -join "`n") } # ========================================================================= # STEP 2: INTERACTIVE MODE - Prompt for connection string if not provided # ========================================================================= if ([string]::IsNullOrWhiteSpace($ConnectionString)) { Write-Host "" Write-Host "=== DATABASE CONNECTION SETUP ===" -ForegroundColor Cyan Write-Host "" Write-Host "Enter your database connection string:" -ForegroundColor White Write-Host "" Write-Host "PostgreSQL Examples:" -ForegroundColor Yellow Write-Host " Server=localhost;Port=5432;Database=hospital_db;User Id=postgres;Password=yourpassword" -ForegroundColor Gray Write-Host " Server=hostname;Port=5432;Database=mydb;User Id=postgres;Password=yourpassword;SSL Mode=Require" -ForegroundColor Gray Write-Host "" Write-Host "SQL Server Examples:" -ForegroundColor Yellow Write-Host " Server=localhost;Database=Hospital_db;Trusted_Connection=true" -ForegroundColor Gray Write-Host " Server=localhost;Database=mydb;User Id=sa;Password=YourStrong!Passw0rd" -ForegroundColor Gray Write-Host "" $ConnectionString = Read-Host "Connection String" if ([string]::IsNullOrWhiteSpace($ConnectionString)) { throw "Connection string cannot be empty." } } # ========================================================================= # STEP 3: AUTO-DETECT DATABASE ENGINE AND EXTRACT DATABASE NAME # ========================================================================= Write-Verbose "Auto-detecting database engine..." $engine = $null $providerPackage = $null $scaffoldProvider = $null # PostgreSQL detection if ($ConnectionString -match '(?i)(npgsql|postgres|port\s*=\s*5432)') { $engine = 'PostgreSQL' $providerPackage = 'Npgsql.EntityFrameworkCore.PostgreSQL' $scaffoldProvider = 'Npgsql.EntityFrameworkCore.PostgreSQL' Write-Verbose "Detected engine: PostgreSQL" } # SQL Server detection elseif ($ConnectionString -match '(?i)(server\s*=|data source\s*=|trusted_connection|sqlserver)') { $engine = 'SqlServer' $providerPackage = 'Microsoft.EntityFrameworkCore.SqlServer' $scaffoldProvider = 'Microsoft.EntityFrameworkCore.SqlServer' Write-Verbose "Detected engine: SQL Server" } else { throw "Unable to auto-detect database engine. Ensure connection string contains 'Server=' or 'postgres'." } # Extract database name from connection string $DatabaseName = $null foreach ($part in ($ConnectionString -split ';')) { if ($part -match '(?i)^(Database|Initial Catalog)\s*=\s*(.+)$') { $DatabaseName = $Matches[2].Trim() break } } if ([string]::IsNullOrWhiteSpace($DatabaseName)) { throw "Could not extract database name from connection string. Ensure it includes 'Database=YourDatabaseName'." } Write-Verbose "Database name extracted: $DatabaseName" # Auto-fix common SQL Server connection issues if ($engine -eq 'SqlServer') { if ($ConnectionString -notmatch '(?i)TrustServerCertificate') { $ConnectionString = $ConnectionString + ";TrustServerCertificate=true" } if ($ConnectionString -notmatch '(?i)Encrypt') { $ConnectionString = $ConnectionString + ";Encrypt=false" } Write-Verbose "Modified connection string includes SSL trust settings" } # ========================================================================= # STEP 4: VALIDATE DATABASE CONNECTION # ========================================================================= Write-Verbose "Validating database connection..." try { if ($engine -eq 'SqlServer') { $conn = New-Object System.Data.SqlClient.SqlConnection $ConnectionString $conn.Open() $cmd = $conn.CreateCommand() $cmd.CommandText = "SELECT 1" [void]$cmd.ExecuteScalar() $conn.Close() Write-Verbose "✓ SQL Server connection validated" } else { # Convert to ODBC and test PostgreSQL connection $odbcConn = ConvertTo-PostgreSqlOdbc -NativeConnectionString $ConnectionString $conn = New-Object System.Data.Odbc.OdbcConnection $odbcConn $conn.Open() $cmd = $conn.CreateCommand() $cmd.CommandText = "SELECT 1" [void]$cmd.ExecuteScalar() $conn.Close() Write-Verbose "✓ PostgreSQL connection validated" } } catch { throw "Connection validation failed: $($_.Exception.Message)" } # ========================================================================= # STEP 5: DETERMINE PROJECT NAME AND PATH # ========================================================================= if ([string]::IsNullOrWhiteSpace($ProjectName)) { $ProjectName = "$($DatabaseName)_CRUD_API" } # Handle naming conflicts $finalProjectName = $ProjectName $counter = 1 $projectFullPath = Join-Path $OutputPath $finalProjectName if (-not $Force) { while (Test-Path $projectFullPath) { $finalProjectName = "$ProjectName`_$counter" $projectFullPath = Join-Path $OutputPath $finalProjectName $counter++ } } Write-Verbose "Project will be created at: $projectFullPath" # ========================================================================= # STEP 6: DRY RUN MODE - Show plan without executing # ========================================================================= if ($DryRun) { $plan = [pscustomobject]@{ Action = 'CreateCrudApi' Engine = $engine Database = $DatabaseName ProjectName = $finalProjectName ProjectPath = $projectFullPath Provider = $providerPackage Steps = @( "1. Create Web API project: dotnet new webapi -n $finalProjectName" "2. Install base EF package: dotnet add package Microsoft.EntityFrameworkCore" "3. Install EF design package: dotnet add package Microsoft.EntityFrameworkCore.Design" "4. Install EF tools package: dotnet add package Microsoft.EntityFrameworkCore.Tools" "5. Install provider package: dotnet add package $providerPackage" "6. Install codegen package: dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design" "7. Install Swagger package: dotnet add package Swashbuckle.AspNetCore" "8. Scaffold DbContext and models: dotnet ef dbcontext scaffold" "9. Build project: dotnet build" "10. Generate CRUD controllers for each model" "11. Configure Program.cs with DbContext and Swagger" "12. Configure appsettings.json with connection string" ) WillCreate = $true } Write-Host "" Write-Host "=== DRY RUN - EXECUTION PLAN ===" -ForegroundColor Yellow Write-Host "" Write-Host "Engine: $($plan.Engine)" -ForegroundColor White Write-Host "Database: $($plan.Database)" -ForegroundColor White Write-Host "Project: $($plan.ProjectName)" -ForegroundColor White Write-Host "Path: $($plan.ProjectPath)" -ForegroundColor White Write-Host "" Write-Host "Steps that would be executed:" -ForegroundColor White foreach ($step in $plan.Steps) { Write-Host " $step" -ForegroundColor Gray } Write-Host "" return $plan } # ========================================================================= # STEP 7: CREATE WEB API PROJECT # ========================================================================= if ($PSCmdlet.ShouldProcess("$engine::$DatabaseName", "Create CRUD Web API project")) { # Initialize progress tracking $totalSteps = 7 $currentStep = 0 # Remove existing directory if Force is specified if ($Force -and (Test-Path $projectFullPath)) { Write-Verbose "Removing existing project directory..." Remove-Item -Path $projectFullPath -Recurse -Force } # Create project $currentStep++ Write-Progress -Activity "Creating CRUD API Project" -Status "Creating Web API project..." -PercentComplete (($currentStep / $totalSteps) * 100) Write-Verbose "Creating Web API project..." $createResult = & dotnet new webapi -n $finalProjectName -o $projectFullPath 2>&1 if ($LASTEXITCODE -ne 0) { Write-Progress -Activity "Creating CRUD API Project" -Completed throw "Failed to create Web API project: $createResult" } Write-Verbose "✓ Web API project created" # Change to project directory Push-Location $projectFullPath try { # ========================================================================= # STEP 8: INSTALL REQUIRED NUGET PACKAGES # ========================================================================= $currentStep++ Write-Progress -Activity "Creating CRUD API Project" -Status "Installing NuGet packages..." -PercentComplete (($currentStep / $totalSteps) * 100) Write-Verbose "Installing NuGet packages..." $packages = @( 'Microsoft.EntityFrameworkCore', 'Microsoft.EntityFrameworkCore.Design', 'Microsoft.EntityFrameworkCore.Tools', $providerPackage, 'Microsoft.VisualStudio.Web.CodeGeneration.Design', 'Swashbuckle.AspNetCore' ) # CRITICAL FIX: aspnet-codegenerator has a hardcoded dependency on SQL Server package # even when using PostgreSQL. This is a known bug/limitation in the code generator tool. # The generator checks for Microsoft.EntityFrameworkCore.SqlServer during reflection # and fails silently (with exit code 0) if it's not found, even for non-SQL Server databases. # We must install this package for controller generation to work with PostgreSQL. if ($engine -eq 'PostgreSQL') { $packages += 'Microsoft.EntityFrameworkCore.SqlServer' Write-Verbose "Adding SQL Server package (required by aspnet-codegenerator even for PostgreSQL projects)" } $packageCount = 0 foreach ($package in $packages) { $packageCount++ $packageStatus = "Installing package $packageCount of $($packages.Count): $package" Write-Progress -Activity "Creating CRUD API Project" -Status $packageStatus -PercentComplete (($currentStep / $totalSteps) * 100) -CurrentOperation $package Write-Verbose "Installing $package..." $installResult = & dotnet add package $package 2>&1 if ($LASTEXITCODE -ne 0) { Write-Warning "Failed to install $package : $installResult" } } Write-Verbose "✓ NuGet packages installed" # ========================================================================= # STEP 9: SCAFFOLD DBCONTEXT AND MODELS (FIXED - Direct argument passing) # ========================================================================= $currentStep++ Write-Progress -Activity "Creating CRUD API Project" -Status "Scaffolding DbContext and models from database..." -PercentComplete (($currentStep / $totalSteps) * 100) Write-Verbose "Scaffolding DbContext and models from database..." $dbContextName = "$($DatabaseName)Context" # FIX: Use direct argument passing instead of array splatting (@) # This preserves proper quoting of the connection string when passed to dotnet ef # Previously, array expansion would lose the connection string quoting, causing # Npgsql to fail parsing. Direct argument passing with line continuation (`) # maintains proper shell-level quoting throughout the invocation. $scaffoldResult = & dotnet ef dbcontext scaffold ` "$ConnectionString" ` $scaffoldProvider ` --output-dir Models ` --context $dbContextName ` --force 2>&1 if ($LASTEXITCODE -ne 0) { Write-Progress -Activity "Creating CRUD API Project" -Completed throw "Failed to scaffold DbContext: $scaffoldResult" } Write-Verbose "✓ DbContext and models scaffolded" # Get list of generated model files $modelFiles = Get-ChildItem -Path "Models" -Filter "*.cs" -File | Where-Object { $_.Name -ne "$dbContextName.cs" } $modelsGenerated = $modelFiles.Count Write-Verbose "Generated $modelsGenerated model(s)" # ========================================================================= # STEP 10: BUILD PROJECT (Required before controller generation) # ========================================================================= $currentStep++ Write-Progress -Activity "Creating CRUD API Project" -Status "Building project..." -PercentComplete (($currentStep / $totalSteps) * 100) Write-Verbose "Building project before controller generation..." $buildResult = & dotnet build 2>&1 if ($LASTEXITCODE -ne 0) { Write-Progress -Activity "Creating CRUD API Project" -Completed throw "Failed to build project before controller generation: $buildResult" } Write-Verbose "✓ Project built successfully" # ========================================================================= # STEP 11: GENERATE CRUD CONTROLLERS # ========================================================================= $currentStep++ Write-Progress -Activity "Creating CRUD API Project" -Status "Generating CRUD controllers..." -PercentComplete (($currentStep / $totalSteps) * 100) Write-Verbose "Generating CRUD controllers..." # Ensure Controllers folder exists $controllersPath = Join-Path (Get-Location) "Controllers" if (-not (Test-Path $controllersPath)) { New-Item -ItemType Directory -Path $controllersPath -Force | Out-Null Write-Verbose "✓ Created Controllers folder: $controllersPath" } $controllersGenerated = 0 $controllersSkipped = 0 $controllerFailures = @() $controllerCount = 0 foreach ($modelFile in $modelFiles) { $controllerCount++ $modelName = [System.IO.Path]::GetFileNameWithoutExtension($modelFile.Name) $controllerStatus = "Generating controller $controllerCount of $($modelFiles.Count): $modelName" Write-Progress -Activity "Creating CRUD API Project" -Status $controllerStatus -PercentComplete (($currentStep / $totalSteps) * 100) -CurrentOperation "$modelName`Controller" Write-Verbose "Generating controller for $modelName..." # Build fully qualified type names (required for proper type resolution) $modelNamespace = "$finalProjectName.Models.$modelName" $contextNamespace = "$finalProjectName.Models.$dbContextName" # Generate controller with fully qualified names and absolute path $controllerResult = & dotnet aspnet-codegenerator controller ` -name "$($modelName)Controller" ` -async ` -api ` -m $modelNamespace ` -dc $contextNamespace ` -outDir $controllersPath ` -f 2>&1 $resultString = $controllerResult | Out-String if ($LASTEXITCODE -eq 0) { # Verify the file was actually created (aspnet-codegenerator sometimes reports success incorrectly) $controllerFile = Join-Path $controllersPath "$($modelName)Controller.cs" if (Test-Path $controllerFile) { $controllersGenerated++ Write-Verbose "✓ Controller generated and verified: $($modelName)Controller.cs" } else { $errorMsg = "Controller generation reported success but file not found. This may indicate missing EF provider packages." $controllerFailures += [PSCustomObject]@{ Model = $modelName Error = $errorMsg Reason = "FileNotCreated" } Write-Verbose "⚠ $errorMsg Model: $modelName" } } else { # Check if this is a "Primary key not found" error - these models should be skipped if ($resultString -match "Primary key not found") { $controllersSkipped++ Write-Verbose "⊘ Skipped $modelName (no detectable primary key - common with composite keys or ASP.NET Identity tables)" } else { # Real error - record it $controllerFailures += [PSCustomObject]@{ Model = $modelName Error = $resultString.Trim() Reason = "GenerationError" } Write-Verbose "⚠ Failed to generate controller for $modelName : $resultString" } } } # Show summary of controller generation if ($controllersSkipped -gt 0) { Write-Verbose "Skipped $controllersSkipped model(s) without detectable primary keys (ASP.NET Identity tables, composite keys, etc.)" } if ($controllerFailures.Count -gt 0) { Write-Host "" Write-Host "WARNING: $($controllerFailures.Count) controller(s) could not be generated:" -ForegroundColor Yellow foreach ($failure in $controllerFailures) { if ($failure.Reason -eq "GenerationError") { Write-Host " ✗ $($failure.Model)" -ForegroundColor Yellow } } Write-Host "" Write-Host "Note: Models with composite keys or no detectable primary key were automatically skipped." -ForegroundColor Cyan Write-Host "Controllers folder: $controllersPath" -ForegroundColor Cyan Write-Host "" } # ========================================================================= # STEP 12: CONFIGURE PROGRAM.CS # ========================================================================= $currentStep++ Write-Progress -Activity "Creating CRUD API Project" -Status "Configuring Program.cs..." -PercentComplete (($currentStep / $totalSteps) * 100) Write-Verbose "Configuring Program.cs..." $dbContextMethod = if ($engine -eq 'SqlServer') { 'UseSqlServer' } else { 'UseNpgsql' } $programCsContent = @" using Microsoft.EntityFrameworkCore; using $finalProjectName.Models; var builder = WebApplication.CreateBuilder(args); // Add services to the container var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); builder.Services.AddDbContext<$dbContextName>(options => options.$dbContextMethod(connectionString)); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(options => { // Resolve conflicts when multiple operations have the same HTTP method and path options.ResolveConflictingActions(apiDescriptions => apiDescriptions.First()); }); var app = builder.Build(); // Configure the HTTP request pipeline if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run(); "@ Set-Content -Path "Program.cs" -Value $programCsContent -Encoding UTF8 Write-Verbose "✓ Program.cs configured" # ========================================================================= # STEP 13: CONFIGURE APPSETTINGS.JSON # ========================================================================= $currentStep++ Write-Progress -Activity "Creating CRUD API Project" -Status "Configuring appsettings.json..." -PercentComplete (($currentStep / $totalSteps) * 100) Write-Verbose "Configuring appsettings.json..." # Escape backslashes for JSON $escapedConnectionString = $ConnectionString -replace '\\', '\\\\' $appsettingsContent = @" { "ConnectionStrings": { "DefaultConnection": "$escapedConnectionString" }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*" } "@ Set-Content -Path "appsettings.json" -Value $appsettingsContent -Encoding UTF8 Write-Verbose "✓ appsettings.json configured" # Complete progress bar Write-Progress -Activity "Creating CRUD API Project" -Status "Completed!" -PercentComplete 100 Start-Sleep -Milliseconds 500 Write-Progress -Activity "Creating CRUD API Project" -Completed # ========================================================================= # STEP 14: RETURN SUCCESS SUMMARY # ========================================================================= Write-Host "" if ($controllersGenerated -eq $modelsGenerated) { Write-Host "✓ CRUD API project created successfully!" -ForegroundColor Green } else { Write-Host "⚠ CRUD API project created with some controllers skipped" -ForegroundColor Yellow } Write-Host "" Write-Host "Project: $finalProjectName" -ForegroundColor White Write-Host "Location: $projectFullPath" -ForegroundColor White Write-Host "Models: $modelsGenerated" -ForegroundColor White Write-Host "Controllers: $controllersGenerated" -ForegroundColor $(if ($controllersGenerated -eq $modelsGenerated) { 'White' } else { 'Yellow' }) if ($controllersSkipped -gt 0) { Write-Host "Skipped: $controllersSkipped (models without detectable primary keys)" -ForegroundColor Gray } if ($controllerFailures.Count -gt 0) { Write-Host "Failures: $($controllerFailures.Count)" -ForegroundColor Red } Write-Host "" Write-Host "To run the API:" -ForegroundColor Cyan Write-Host " cd `"$projectFullPath`"" -ForegroundColor Gray Write-Host " dotnet run" -ForegroundColor Gray Write-Host "" Write-Host "Access your API at:" -ForegroundColor Cyan Write-Host " http://localhost:5xxx/swagger (check console output for exact port)" -ForegroundColor Gray Write-Host " https://localhost:7xxx/swagger (if HTTPS is configured)" -ForegroundColor Gray Write-Host "" if ($controllersSkipped -gt 0) { Write-Host "Note: Some models (like ASP.NET Identity tables) were skipped due to composite keys." -ForegroundColor Yellow Write-Host " These can be added manually if needed." -ForegroundColor Yellow Write-Host "" } Write-Host "Note: To enable HTTPS, run: dotnet dev-certs https --trust" -ForegroundColor Yellow Write-Host "" return [pscustomobject]@{ Action = 'CreateCrudApi' Engine = $engine Database = $DatabaseName ProjectName = $finalProjectName ProjectPath = $projectFullPath ModelsGenerated = $modelsGenerated ControllersGenerated = $controllersGenerated ControllersSkipped = $controllersSkipped ControllerFailures = $controllerFailures.Count Created = $true } } finally { Pop-Location } } } |