{
  "steward": "dotnet-bestpractises-steward",
  "project": "Azure-AI-RAG-CSharp-Semantic-Kernel-Functions",
  "runDate": "2026-03-21",
  "runId": "2026-03-21T00-00-00",
  "findings": [
    {
      "id": "DNET-DI-001",
      "title": "Singleton ChatHistory shared across all requests — multi-tenancy data leak",
      "severity": "critical",
      "category": "DI",
      "file": "src/ChatAPI/Program.cs",
      "line": 30,
      "description": "ChatHistory is registered as a singleton, meaning a single mutable conversation history object is shared across every HTTP request and every user session. Messages from one user's session accumulate alongside messages from all other users, causing data leakage and incorrect AI responses.",
      "recommendation": "Remove the singleton registration for ChatHistory. Instantiate ChatHistory per request inside ChatService.GetResponseAsync (loading prior messages from Cosmos for the given sessionId), rather than injecting it as a shared service.",
      "status": "open"
    },
    {
      "id": "DNET-EXCEPT-001",
      "title": "Startup exception swallowed silently in PopulateCosmosAsync",
      "severity": "critical",
      "category": "EXCEPT",
      "file": "src/ChatAPI/Data/sample_data/products/GenerateProductInfo.cs",
      "line": 84,
      "description": "PopulateCosmosAsync wraps all work in a broad try/catch that logs the error and returns normally. If the database population fails (e.g., missing connection string, network error), the application starts in a degraded state with an empty database and serves incorrect results without any indication of failure.",
      "recommendation": "Remove the outer try/catch or re-throw after logging so the host startup fails fast and the error is surfaced immediately. Use IHostApplicationLifetime.StopApplication() as an alternative if graceful shutdown is preferred.",
      "status": "open"
    },
    {
      "id": "DNET-NULL-001",
      "title": "Non-nullable return type on GetProductByNameAsync despite returning null",
      "severity": "notable",
      "category": "NULL",
      "file": "src/ChatAPI/Data/ProductData.cs",
      "line": 60,
      "description": "GetProductByNameAsync is declared as Task<Dictionary<string, object>> (non-nullable) but returns null at line 60 when no product is found. With nullable reference types enabled, this violates the type contract and will generate a CS8603 warning. Callers that do not null-check the result will throw NullReferenceException.",
      "recommendation": "Change the return type to Task<Dictionary<string, object>?> and update all callers to handle the null case explicitly.",
      "status": "open"
    },
    {
      "id": "DNET-NULL-002",
      "title": "ChatMessage model properties uninitialized — nullable annotation mismatch",
      "severity": "notable",
      "category": "NULL",
      "file": "src/ChatAPI/Data/ChatHistoryData.cs",
      "line": 10,
      "description": "The ChatMessage class declares string properties (id, sessionid, message, role) as non-nullable reference types with no initializers and no constructor assignment. With <Nullable>enable</Nullable>, the compiler will emit CS8618 warnings. At runtime these will be null if not explicitly set, causing NullReferenceExceptions in callers.",
      "recommendation": "Either add = default! initializers (to suppress warnings while acknowledging the risk), add proper constructor initialization, mark the properties as nullable (string?), or convert ChatMessage to a record with required init properties.",
      "status": "open"
    },
    {
      "id": "DNET-NULL-003",
      "title": "Product model properties uninitialized — nullable annotation mismatch",
      "severity": "notable",
      "category": "NULL",
      "file": "src/ChatAPI/Data/Product.cs",
      "line": 1,
      "description": "The Product class in Product.cs declares string and List<string> properties as non-nullable with no initialization. Additionally, there are two Product class definitions in the same namespace (Product.cs and GenerateProductInfo.cs) which will cause a duplicate type compile error.",
      "recommendation": "Consolidate the two Product class definitions into one. Add proper nullability annotations or initializers. Consider converting to a record type.",
      "status": "open"
    },
    {
      "id": "DNET-EXCEPT-002",
      "title": "Exceptions logged at LogInformation level in plugin catch blocks",
      "severity": "notable",
      "category": "EXCEPT",
      "file": "src/ChatAPI/Plugins/AISearchDataPlugin.cs",
      "line": 44,
      "description": "Both AISearchDataPlugin.ResourceLookup and ProductDataPlugin.GetAzureProductDetailsById catch exceptions and log them using _logger.LogInformation. Errors logged at Information level are invisible in dashboards and alerting systems configured to filter at Warning or Error. The exception details will be missed in production monitoring.",
      "recommendation": "Replace _logger.LogInformation(ex, ...) with _logger.LogError(ex, ...) in all catch blocks that handle exceptions representing failures.",
      "status": "open"
    },
    {
      "id": "DNET-NULL-004",
      "title": "ChatRequest model properties uninitialized — nullable annotation mismatch",
      "severity": "notable",
      "category": "NULL",
      "file": "src/ChatAPI/Controllers/ChatController.cs",
      "line": 9,
      "description": "ChatRequest.Input and ChatRequest.SessionId are declared as non-nullable string properties with no initializers or required modifier. With nullable enabled this generates CS8618 warnings and leaves the door open for NullReferenceException if the JSON body omits these fields.",
      "recommendation": "Add the required modifier (required public string Input { get; set; }), add nullable annotations (string?), or convert ChatRequest to a record with required init properties.",
      "status": "open"
    },
    {
      "id": "DNET-DISPOSE-001",
      "title": "FeedIterator not disposed in GetMessagesBySessionIdAsync and GetProductByNameAsync",
      "severity": "minor",
      "category": "DISPOSE",
      "file": "src/ChatAPI/Data/ChatHistoryData.cs",
      "line": 92,
      "description": "FeedIterator<T> implements IDisposable. The iterators created in ChatHistoryData.GetMessagesBySessionIdAsync and ProductData.GetProductByNameAsync are not wrapped in using statements, leaving disposal to the GC finalizer rather than deterministic cleanup.",
      "recommendation": "Wrap all FeedIterator<T> instances in using statements: using var query = container.GetItemQueryIterator<...>(...).",
      "status": "open"
    },
    {
      "id": "DNET-DISPOSE-002",
      "title": "MemoryStream not disposed in loop in PopulateCosmosAsync",
      "severity": "minor",
      "category": "DISPOSE",
      "file": "src/ChatAPI/Data/sample_data/products/GenerateProductInfo.cs",
      "line": 70,
      "description": "A new MemoryStream is allocated on each iteration of the foreach loop in PopulateCosmosAsync and is never disposed. MemoryStream implements IDisposable and should be deterministically disposed.",
      "recommendation": "Wrap the MemoryStream in a using statement: using var stream = new MemoryStream(...).",
      "status": "open"
    },
    {
      "id": "DNET-LINQ-001",
      "title": "Count() > 0 used instead of Any() in GetProductByNameAsync",
      "severity": "minor",
      "category": "LINQ",
      "file": "src/ChatAPI/Data/ProductData.cs",
      "line": 52,
      "description": "response.Resource.Count() > 0 enumerates the entire collection to obtain a count when Any() would short-circuit after the first element. Additionally, the pattern Count() > 0 followed immediately by First() causes two enumerations of the same IEnumerable.",
      "recommendation": "Replace with var first = response.Resource.FirstOrDefault(); if (first is not null) { return first; } to enumerate at most once.",
      "status": "open"
    },
    {
      "id": "DNET-ASYNC-001",
      "title": "Dead variable embeddingString computed and discarded in ResourceLookup",
      "severity": "minor",
      "category": "ASYNC",
      "file": "src/ChatAPI/Plugins/AISearchDataPlugin.cs",
      "line": 37,
      "description": "In AISearchDataPlugin.ResourceLookup, the embedding result is serialized to embeddingString but this variable is never used. This is dead code that wastes CPU and memory on every invocation. The intermediate embeddingTask variable is also unnecessary — the result can be awaited directly.",
      "recommendation": "Remove the embeddingString variable entirely. Simplify to: var embedding = await _embedding.GenerateEmbeddingAsync(question);",
      "status": "open"
    },
    {
      "id": "DNET-MODERN-001",
      "title": "AISearchDataPlugin and ProductDataPlugin use explicit constructors inconsistently",
      "severity": "minor",
      "category": "MODERN",
      "file": "src/ChatAPI/Plugins/AISearchDataPlugin.cs",
      "line": 22,
      "description": "Most classes in the codebase use primary constructors (C# 12 feature). AISearchDataPlugin and ProductDataPlugin use explicit constructors with manual field assignment, which is inconsistent with the rest of the codebase style.",
      "recommendation": "Refactor to use primary constructors to align with the codebase convention and reduce boilerplate.",
      "status": "open"
    },
    {
      "id": "DNET-INFO-001",
      "title": "Nullable reference types enabled and ImplicitUsings enabled — good baseline",
      "severity": "info",
      "category": "NULL",
      "file": "src/ChatAPI/ChatAPI.csproj",
      "description": "The project file correctly enables <Nullable>enable</Nullable> and <ImplicitUsings>enable</ImplicitUsings>, providing a solid modern C# baseline. The project targets net8.0 (LTS).",
      "status": "open"
    },
    {
      "id": "DNET-INFO-002",
      "title": "Primary constructors adopted across most classes — modern C# practice applied",
      "severity": "info",
      "category": "MODERN",
      "description": "ProductData, ChatHistoryData, GenerateProductInfo, ChatService, and ChatController all use C# 12 primary constructors, demonstrating adoption of modern language features and reducing constructor boilerplate.",
      "status": "open"
    }
  ],
  "summary": {
    "critical": 2,
    "notable": 5,
    "minor": 6,
    "info": 2,
    "total": 15
  }
}
