modules/ai-guide/Write-AiGuide-Endpoints.ps1

param(
    [string]   $ProjectName,
    [string]   $OrmMode,
    [string]   $DbProvider    = "MSSQL",
    [string]   $FrontendMode,
    [string[]] $Entities      = @("User", "Product"),
    [string]   $OutputPath,
    [bool]     $SkipTests     = $false,
    [bool]     $SkipCi        = $false,
    [bool]     $SkipFrontend  = $false,
    [bool]     $Observability = $false,
    [string]   $IdeConfig     = "VSCode"
)

. (Join-Path (Split-Path $MyInvocation.MyCommand.Path) "..\core\Utils.ps1")

$guideDir = Join-Path $OutputPath "ai-guide"
function Write-Guide($fileName, $content) {
    Write-CoreFile (Join-Path $guideDir $fileName) $content
}

$allEntitiesStr = $Entities -join ", "

$ormRepoNote = switch ($OrmMode) {
    "Dapper" { "Dapper ile raw SQL yaz. SQL provider'a gore INSERT syntax farklidir (bkz. orm-guide.md)." }
    "EFCore" { "EF Core: ``_context.Set<T>().AddAsync()`` + ``SaveChangesAsync()`` kullan." }
    "Hybrid" { "Yazma: Dapper (IDbConnection) | Okuma: EF Core (_context.AsNoTracking()). GetPagedAsync EF Core ile yapilir." }
}

$dbNote = switch ($DbProvider) {
    "Postgres" { "PostgreSQL (Npgsql)" }
    "SQLite"   { "SQLite (dosya tabanli)" }
    default    { "MS SQL Server" }
}

$repoImplNote = switch ($OrmMode) {
    "Dapper" { @"
``````csharp
using Dapper;
using System.Data;
using $ProjectName.Domain.Common;
using $ProjectName.Domain.Entities;
using $ProjectName.Domain.Interfaces;

namespace $ProjectName.Persistence.Repositories;

public class CategoryRepository : ICategoryRepository
{
    private readonly IDbConnection _connection;
    public CategoryRepository(IDbConnection connection) => _connection = connection;

    public async Task<IEnumerable<Category>> GetAllAsync()
        => await _connection.QueryAsync<Category>("SELECT * FROM Categories WHERE IsDeleted=0 ORDER BY CreatedAt DESC");

    public async Task<PagedResult<Category>> GetPagedAsync(PagedQuery query) {
        var total = await _connection.ExecuteScalarAsync<int>("SELECT COUNT(*) FROM Categories WHERE IsDeleted=0");
        var items = await _connection.QueryAsync<Category>(
            "SELECT * FROM Categories WHERE IsDeleted=0 ORDER BY CreatedAt DESC OFFSET @Skip ROWS FETCH NEXT @Size ROWS ONLY",
            new { query.Skip, query.Size });
        return new PagedResult<Category> { Items = items, TotalCount = total, Page = query.Page, Size = query.Size };
    }

    public async Task<int> AddAsync(Category c) {
        const string sql = "INSERT INTO Categories (Name, CreatedAt, UpdatedAt, IsDeleted) OUTPUT INSERTED.Id VALUES (@Name, @CreatedAt, @UpdatedAt, 0)";
        return await _connection.ExecuteScalarAsync<int>(sql, c);
    }

    public async Task DeleteAsync(int id)
        => await _connection.ExecuteAsync("UPDATE Categories SET IsDeleted=1, DeletedAt=GETUTCDATE() WHERE Id=@id", new { id });
}
``````
"@
 }
    "EFCore" { @"
``````csharp
using Microsoft.EntityFrameworkCore;
using $ProjectName.Domain.Common;
using $ProjectName.Domain.Entities;
using $ProjectName.Domain.Interfaces;
using $ProjectName.Persistence.Context;

namespace $ProjectName.Persistence.Repositories;

public class CategoryRepository : ICategoryRepository
{
    private readonly AppDbContext _context;
    public CategoryRepository(AppDbContext context) => _context = context;

    public async Task<IEnumerable<Category>> GetAllAsync()
        => await _context.Categories.AsNoTracking().OrderByDescending(c => c.CreatedAt).ToListAsync();

    public async Task<PagedResult<Category>> GetPagedAsync(PagedQuery query) {
        var total = await _context.Categories.CountAsync();
        var items = await _context.Categories.Skip(query.Skip).Take(query.Size).ToListAsync();
        return new PagedResult<Category> { Items = items, TotalCount = total, Page = query.Page, Size = query.Size };
    }

    public async Task<int> AddAsync(Category c) {
        await _context.Categories.AddAsync(c);
        await _context.SaveChangesAsync();
        return c.Id;
    }

    public async Task DeleteAsync(int id) {
        var entity = await _context.Categories.FindAsync(id);
        if (entity != null) { entity.IsDeleted = true; entity.DeletedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); }
    }
}
``````
"@
 }
    "Hybrid" { @"
``````csharp
using Dapper;
using System.Data;
using Microsoft.EntityFrameworkCore;
using $ProjectName.Domain.Common;
using $ProjectName.Domain.Entities;
using $ProjectName.Domain.Interfaces;
using $ProjectName.Persistence.Context;

namespace $ProjectName.Persistence.Repositories;

public class CategoryRepository : ICategoryRepository
{
    private readonly AppDbContext _ctx;
    private readonly IDbConnection _conn;
    public CategoryRepository(AppDbContext ctx, IDbConnection conn) { _ctx = ctx; _conn = conn; }

    // Okuma: EF Core
    public async Task<IEnumerable<Category>> GetAllAsync()
        => await _ctx.Categories.AsNoTracking().OrderByDescending(c => c.CreatedAt).ToListAsync();

    public async Task<PagedResult<Category>> GetPagedAsync(PagedQuery query) {
        var total = await _ctx.Categories.CountAsync();
        var items = await _ctx.Categories.Skip(query.Skip).Take(query.Size).ToListAsync();
        return new PagedResult<Category> { Items = items, TotalCount = total, Page = query.Page, Size = query.Size };
    }

    // Yazma: Dapper
    public async Task<int> AddAsync(Category c) {
        const string sql = "INSERT INTO Categories (Name, CreatedAt, UpdatedAt, IsDeleted) OUTPUT INSERTED.Id VALUES (@Name, @CreatedAt, @UpdatedAt, 0)";
        return await _conn.ExecuteScalarAsync<int>(sql, c);
    }

    public async Task DeleteAsync(int id) {
        var entity = await _ctx.Categories.FindAsync(id);
        if (entity != null) { entity.IsDeleted = true; entity.DeletedAt = DateTime.UtcNow; await _ctx.SaveChangesAsync(); }
    }
}
``````
"@
 }
}

$dbTableNote = switch ($DbProvider) {
    "Postgres" { @"
``````sql
CREATE TABLE IF NOT EXISTS "Categories" (
    "Id" SERIAL PRIMARY KEY,
    "Name" VARCHAR(200) NOT NULL,
    "IsDeleted" BOOLEAN NOT NULL DEFAULT FALSE,
    "DeletedAt" TIMESTAMP,
    "CreatedAt" TIMESTAMP NOT NULL DEFAULT NOW(),
    "UpdatedAt" TIMESTAMP NOT NULL DEFAULT NOW()
);
``````
"@
 }
    "SQLite" { @"
``````sql
CREATE TABLE IF NOT EXISTS Categories (
    Id INTEGER PRIMARY KEY AUTOINCREMENT,
    Name TEXT NOT NULL,
    IsDeleted INTEGER NOT NULL DEFAULT 0,
    DeletedAt TEXT,
    CreatedAt TEXT NOT NULL DEFAULT (datetime('now')),
    UpdatedAt TEXT NOT NULL DEFAULT (datetime('now'))
);
``````
"@
 }
    default { @"
``````sql
CREATE TABLE Categories (
    Id INT IDENTITY(1,1) PRIMARY KEY,
    Name NVARCHAR(200) NOT NULL,
    IsDeleted BIT NOT NULL DEFAULT 0,
    DeletedAt DATETIME2,
    CreatedAt DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
    UpdatedAt DATETIME2 NOT NULL DEFAULT GETUTCDATE()
);
``````
"@
 }
}

$efContextNote = if ($OrmMode -ne "Dapper") { @"
**AppDbContext.cs icine ekle:**
``````csharp
public DbSet<Category> Categories => Set<Category>();

// OnModelCreating icinde - soft delete global filter:
builder.Entity<Category>().HasQueryFilter(e => !e.IsDeleted);
``````

**EF Core Migration calistir:**
``````bash
dotnet ef migrations add AddCategory \
  --project src/$ProjectName.Persistence \
  --startup-project src/$ProjectName.WebApi
dotnet ef database update \
  --project src/$ProjectName.Persistence \
  --startup-project src/$ProjectName.WebApi
``````
"@
 } else { "" }

# ─── add-feature.md ───────────────────────────────────────
Write-Guide "add-feature.md" @"
# Yeni Ozellik (Entity) Ekleme Rehberi

Bu rehber, projeye yeni bir entity eklemek icin gereken
tum adimlarini sirayla aciklar. Ornek entity: **Category**

> Mevcut entityler: $allEntitiesStr
> Bunlar ayni yapiyla uretilmistir - referans olarak kullanabilirsin.

---

## Adim 1 - Domain: Entity

``````csharp
// src/$ProjectName.Domain/Entities/Category.cs
namespace $ProjectName.Domain.Entities;
public class Category : BaseEntity {
    public string Name { get; set; } = string.Empty;
    // BaseEntity'den: Id, CreatedAt, UpdatedAt, IsDeleted, DeletedAt
}
``````

## Adim 2 - Domain: Interface

``````csharp
// src/$ProjectName.Domain/Interfaces/ICategoryRepository.cs
using $ProjectName.Domain.Common;
namespace $ProjectName.Domain.Interfaces;
public interface ICategoryRepository : IRepository<Category> {
    // Ek metotlar gerekiyorsa buraya ekle
}
``````

> ``IRepository<T>`` zaten sunlari icerir:
> ``GetByIdAsync``, ``GetAllAsync``, ``GetPagedAsync``, ``AddAsync``, ``UpdateAsync``, ``DeleteAsync``

## Adim 3 - Application: DTO

``````csharp
// src/$ProjectName.Application/DTOs/CategoryDto.cs
namespace $ProjectName.Application.DTOs;
public record CategoryDto(int Id, string Name, DateTime CreatedAt);
``````

## Adim 4 - Application: AutoMapper

``````csharp
// src/$ProjectName.Application/Mappings/CategoryProfile.cs
using AutoMapper;
using $ProjectName.Domain.Entities;
using $ProjectName.Application.DTOs;
namespace $ProjectName.Application.Mappings;
public class CategoryProfile : Profile {
    public CategoryProfile() { CreateMap<Category, CategoryDto>(); }
}
``````

## Adim 5 - Application: CQRS Queries

``````csharp
// GetAllCategoriesQuery.cs
public record GetAllCategoriesQuery : IRequest<IEnumerable<CategoryDto>>;
public class GetAllCategoriesQueryHandler : IRequestHandler<GetAllCategoriesQuery, IEnumerable<CategoryDto>> {
    private readonly ICategoryRepository _repo;
    private readonly IMapper _mapper;
    public GetAllCategoriesQueryHandler(ICategoryRepository repo, IMapper mapper) { _repo = repo; _mapper = mapper; }
    public async Task<IEnumerable<CategoryDto>> Handle(GetAllCategoriesQuery request, CancellationToken ct)
        => _mapper.Map<IEnumerable<CategoryDto>>(await _repo.GetAllAsync());
}

// GetPagedCategoriesQuery.cs
public record GetPagedCategoriesQuery(int Page, int Size) : IRequest<PagedResult<CategoryDto>>;
public class GetPagedCategoriesQueryHandler : IRequestHandler<GetPagedCategoriesQuery, PagedResult<CategoryDto>> {
    private readonly ICategoryRepository _repo; private readonly IMapper _mapper;
    public GetPagedCategoriesQueryHandler(ICategoryRepository repo, IMapper mapper) { _repo = repo; _mapper = mapper; }
    public async Task<PagedResult<CategoryDto>> Handle(GetPagedCategoriesQuery request, CancellationToken ct) {
        var query = new PagedQuery { Page = request.Page, Size = request.Size };
        var result = await _repo.GetPagedAsync(query);
        return new PagedResult<CategoryDto> { Items = _mapper.Map<IEnumerable<CategoryDto>>(result.Items), TotalCount = result.TotalCount, Page = result.Page, Size = result.Size };
    }
}
``````

## Adim 6 - Application: CQRS Commands

``````csharp
// AddCategoryCommand.cs
public record AddCategoryCommand(string Name) : IRequest<int>;
public class AddCategoryCommandHandler : IRequestHandler<AddCategoryCommand, int> {
    private readonly ICategoryRepository _repo;
    public AddCategoryCommandHandler(ICategoryRepository repo) => _repo = repo;
    public async Task<int> Handle(AddCategoryCommand request, CancellationToken ct) {
        var category = new Category { Name = request.Name, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow };
        return await _repo.AddAsync(category);
    }
}

// AddCategoryCommandValidator.cs
public class AddCategoryCommandValidator : AbstractValidator<AddCategoryCommand> {
    public AddCategoryCommandValidator() {
        RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
    }
}

// DeleteCategoryCommand.cs
public record DeleteCategoryCommand(int Id) : IRequest<Unit>;
public class DeleteCategoryCommandHandler : IRequestHandler<DeleteCategoryCommand, Unit> {
    private readonly ICategoryRepository _repo;
    public DeleteCategoryCommandHandler(ICategoryRepository repo) => _repo = repo;
    public async Task<Unit> Handle(DeleteCategoryCommand request, CancellationToken ct) {
        await _repo.DeleteAsync(request.Id); // Soft delete - fiziksel silme yok
        return Unit.Value;
    }
}
``````

## Adim 7 - Persistence: Repository ($OrmMode)

Not: $ormRepoNote
$repoImplNote

## Adim 8 - Persistence: DI Kaydı

``````csharp
// ServiceRegistration.cs icine ekle:
services.AddScoped<ICategoryRepository, CategoryRepository>();
``````

## Adim 9 - WebApi: Controller

``````csharp
// src/$ProjectName.WebApi/Controllers/CategoriesController.cs
[ApiController] [Route("api/[controller]")] [Authorize]
public class CategoriesController : ControllerBase {
    private readonly IMediator _mediator;
    public CategoriesController(IMediator mediator) => _mediator = mediator;

    [HttpGet]
    public async Task<IActionResult> GetAll()
        => Ok(ApiResponse<IEnumerable<CategoryDto>>.Ok(await _mediator.Send(new GetAllCategoriesQuery())));

    [HttpGet("paged")]
    public async Task<IActionResult> GetPaged([FromQuery] int page = 1, [FromQuery] int size = 20)
        => Ok(ApiResponse<PagedResult<CategoryDto>>.Ok(await _mediator.Send(new GetPagedCategoriesQuery(page, size))));

    [HttpPost]
    public async Task<IActionResult> Add([FromBody] AddCategoryCommand cmd)
        => Ok(ApiResponse<int>.Ok(await _mediator.Send(cmd), "Category eklendi."));

    [HttpDelete("{id}")]
    [Authorize(Policy = "AdminOnly")]
    public async Task<IActionResult> Delete(int id)
        => Ok(ApiResponse<Unit>.Ok(await _mediator.Send(new DeleteCategoryCommand(id))));
}
``````

## Adim 10 - Veritabani ($dbNote)

$dbTableNote
$efContextNote
---

## Adim 11 - Dev Log Guncelle

Her yeni entity veya mimari karar sonrasinda ``ai-guide/devlog.md`` dosyasina giris ekle:

``````markdown
## [YYYY-AA-GG] <Entity/Ozellik adi> eklendi
**Neden:** <Neden bu entity/ozellik gerekti>
**Nasil:** <Hangi adimlar izlendi, hangi kararlar alindi>
**Karar:** <Dikkat edilmesi gereken onemli tercih veya kisitlama>
**Etkilenen:** <Eklenen/degistirilen dosyalar>
``````

Bu adim isteğe baglidir ama AI asistanin gelecekte daha iyi baglam kurmasini saglar.
"@


# ─── orm-guide.md ─────────────────────────────────────────
$dbProviderNote = switch ($DbProvider) {
    "Postgres" { "**PostgreSQL (Npgsql):** Tablo/sutun icin cift tirnak veya snake_case. ``RETURNING Id`` ile yeni ID. ``OFFSET/LIMIT`` ile sayfalama." }
    "SQLite"   { "**SQLite:** ``last_insert_rowid()`` ile yeni ID. ``LIMIT/OFFSET`` ile sayfalama. Boolean icin INTEGER (0/1) kullanilir." }
    default    { "**MS SQL Server:** ``OUTPUT INSERTED.Id`` ile yeni ID. ``OFFSET/FETCH NEXT`` ile sayfalama. Bit ile Boolean." }
}

$insertSqlSnippet = if ($DbProvider -eq "Postgres") { @"
``````csharp
const string sql = @"
    INSERT INTO ""Products"" (""Name"", ""Price"", ""CreatedAt"", ""UpdatedAt"", ""IsDeleted"")
    VALUES (@Name, @Price, @CreatedAt, @UpdatedAt, false)
    RETURNING ""Id"" ";
product.Id = await _connection.ExecuteScalarAsync<int>(sql, product);
``````
"@
 } elseif ($DbProvider -eq "SQLite") { @"
``````csharp
const string sql = @"
    INSERT INTO Products (Name, Price, CreatedAt, UpdatedAt, IsDeleted)
    VALUES (@Name, @Price, @CreatedAt, @UpdatedAt, 0);
    SELECT last_insert_rowid();";
product.Id = await _connection.ExecuteScalarAsync<int>(sql, product);
``````
"@
 } else { @"
``````csharp
const string sql = @"
    INSERT INTO Products (Name, Price, CreatedAt, UpdatedAt, IsDeleted)
    OUTPUT INSERTED.Id
    VALUES (@Name, @Price, @CreatedAt, @UpdatedAt, 0)";
product.Id = await _connection.ExecuteScalarAsync<int>(sql, product);
``````
"@
 }

$pagedSqlSnippet = if ($DbProvider -eq "Postgres") { @"
``````csharp
var total = await _conn.ExecuteScalarAsync<int>("SELECT COUNT(*) FROM ""Products"" WHERE ""IsDeleted""=false");
var items = await _conn.QueryAsync<Product>(
    @"SELECT * FROM ""Products"" WHERE ""IsDeleted""=false ORDER BY ""CreatedAt"" DESC LIMIT @Size OFFSET @Skip",
    new { query.Size, query.Skip });
``````
"@
 } elseif ($DbProvider -eq "SQLite") { @"
``````csharp
var total = await _conn.ExecuteScalarAsync<int>("SELECT COUNT(*) FROM Products WHERE IsDeleted=0");
var items = await _conn.QueryAsync<Product>(
    "SELECT * FROM Products WHERE IsDeleted=0 ORDER BY CreatedAt DESC LIMIT @Size OFFSET @Skip",
    new { query.Size, query.Skip });
``````
"@
 } else { @"
``````csharp
var total = await _conn.ExecuteScalarAsync<int>("SELECT COUNT(*) FROM Products WHERE IsDeleted=0");
var items = await _conn.QueryAsync<Product>(
    "SELECT * FROM Products WHERE IsDeleted=0 ORDER BY CreatedAt DESC OFFSET @Skip ROWS FETCH NEXT @Size ROWS ONLY",
    new { query.Skip, query.Size });
``````
"@
 }

$ormGuide = switch ($OrmMode) {
    "Dapper" { @"
# ORM Kilavuzu - Dapper

## Veritabani: $dbNote
$dbProviderNote

## INSERT (Yeni ID Alma)
$insertSqlSnippet

## SELECT (Tum Kayitlar - Soft Delete Filtreli)
$(if ($DbProvider -eq "Postgres") { @"
``````csharp
const string sql = @"SELECT * FROM ""Products"" WHERE ""IsDeleted""=false ORDER BY ""CreatedAt"" DESC";
return (await _connection.QueryAsync<Product>(sql)).ToList();
``````
"@ } else { @"
``````csharp
const string sql = "SELECT * FROM Products WHERE IsDeleted=0 ORDER BY CreatedAt DESC";
return (await _connection.QueryAsync<Product>(sql)).ToList();
``````
"@ })
## GetPagedAsync
$pagedSqlSnippet
``````csharp
return new PagedResult<Product> { Items = items, TotalCount = total, Page = query.Page, Size = query.Size };
``````

## Soft Delete (UPDATE - fiziksel silme yok)
$(if ($DbProvider -eq "Postgres") { @"
``````csharp
await _connection.ExecuteAsync(
    @"UPDATE ""Products"" SET ""IsDeleted""=true, ""DeletedAt""=NOW() WHERE ""Id""=@id",
    new { id });
``````
"@ } elseif ($DbProvider -eq "SQLite") { @"
``````csharp
await _connection.ExecuteAsync(
    "UPDATE Products SET IsDeleted=1, DeletedAt=datetime('now') WHERE Id=@id",
    new { id });
``````
"@ } else { @"
``````csharp
await _connection.ExecuteAsync(
    "UPDATE Products SET IsDeleted=1, DeletedAt=GETUTCDATE() WHERE Id=@id",
    new { id });
``````
"@ })

## UPDATE
``````csharp
await _connection.ExecuteAsync(
    "UPDATE Products SET Name=@Name, Price=@Price, UpdatedAt=@UpdatedAt WHERE Id=@Id AND IsDeleted=0",
    product);
``````
"@
 }
    "EFCore" { @"
# ORM Kilavuzu - EF Core 8

## Veritabani: $dbNote
$dbProviderNote

## SELECT (Global Query Filter - IsDeleted=true olanlari otomatik gizler)
``````csharp
// Normal sorgu - silinmisleri getirmez (HasQueryFilter devrede)
return await _context.Products.AsNoTracking().OrderByDescending(p => p.CreatedAt).ToListAsync();

// Silinmisleri de getirmek icin:
return await _context.Products.IgnoreQueryFilters().ToListAsync();
``````

## GetPagedAsync
``````csharp
public async Task<PagedResult<Product>> GetPagedAsync(PagedQuery query) {
    var total = await _context.Products.CountAsync();
    var items = await _context.Products.Skip(query.Skip).Take(query.Size).ToListAsync();
    return new PagedResult<Product> { Items = items, TotalCount = total, Page = query.Page, Size = query.Size };
}
``````

## INSERT
``````csharp
await _context.Products.AddAsync(product);
await _context.SaveChangesAsync();
return product.Id;
``````

## Soft Delete
``````csharp
var entity = await _context.Products.FindAsync(id);
if (entity != null) { entity.IsDeleted = true; entity.DeletedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); }
``````

## LINQ Ornekleri
``````csharp
// Filtreleme
await _context.Products.Where(p => p.Name.Contains(search)).ToListAsync();

// Include (iliskili entity)
await _context.Orders.Include(o => o.Product).ToListAsync();

// Projeksiyon (DTO'ya direkt map)
await _context.Products.Select(p => new ProductDto(p.Id, p.Name, p.CreatedAt)).ToListAsync();
``````
"@
 }
    "Hybrid" { @"
# ORM Kilavuzu - Hybrid (Dapper + EF Core)

## Veritabani: $dbNote
$dbProviderNote

## Hangi ORM Ne Zaman?

| Operasyon | ORM | Neden |
|--------------------|----------|-----------------------------------------------------|
| GetAllAsync | EF Core | LINQ, AsNoTracking, global soft delete filter |
| GetByIdAsync | EF Core | FindAsync cache avantaji |
| GetPagedAsync | EF Core | Skip/Take kolayligi, global filter otomatik calisir |
| AddAsync | Dapper | Anlik INSERT + ID donusu |
| UpdateAsync | Dapper | Performansli bulk update |
| DeleteAsync | EF Core | Soft delete: entity yukle, flag set et, kaydet |

## Okuma: EF Core
``````csharp
// Soft delete global filter otomatik devrede
var all = await _ctx.Products.AsNoTracking().ToListAsync();

// Sayfalama
var paged = await _ctx.Products.Skip(query.Skip).Take(query.Size).ToListAsync();
``````

## Yazma: Dapper
$insertSqlSnippet

## Soft Delete: EF Core
``````csharp
var entity = await _ctx.Products.FindAsync(id);
if (entity != null) { entity.IsDeleted = true; entity.DeletedAt = DateTime.UtcNow; await _ctx.SaveChangesAsync(); }
``````
"@
 }
}
Write-Guide "orm-guide.md" $ormGuide