📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

Review this Directory.Build.props that turns on TreatWarningsAsErrors with a long NoWarn list. What would you change, and how should warnings-as-errors be used in a team?

A code review of warning policy: TreatWarningsAsErrors versus WarningsAsErrors and WarningsNotAsErrors, NoWarn versus severities in .editorconfig, CI-only versus local enforcement, AnalysisLevel pinning, and ratcheting a legacy codebase to zero warnings.

Mid-Level CodeReview Intermediate analyzers build-system code-quality code-review treat-warnings-as-errors

🎯 Key points

  • TreatWarningsAsErrors=true makes every warning an error; WarningsNotAsErrors then carves out IDs that stay visible as warnings (known debt)
  • Without TreatWarningsAsErrors, WarningsAsErrors promotes only the listed IDs or groups (such as nullable), which suits a ratchet in legacy code
  • NoWarn hides warnings completely; prefer .editorconfig severities, scoped to folders, with justification
  • Long NoWarn lists hide real bugs (nullable, obsolete APIs, security rules) and teach the team to stop reading compiler output
  • Pin AnalysisLevel when warnings are errors, so SDK upgrades don't break builds unexpectedly
  • Enforce consistently: local builds and CI should behave the same, or developers discover failures only after pushing
  • Treat NuGet audit warnings (NU1901-NU1904) deliberately: fail on high and critical, track the rest

⏱ 30-second answer

The file under review turns on TreatWarningsAsErrors and then hides 25 warnings with NoWarn. That list includes nullable warnings, obsolete-API warnings and a security rule, so the policy looks strict while silencing the warnings that matter most. I'd remove those suppressions. Rules that genuinely don't apply would get a severity in .editorconfig, scoped to the right folders and commented. Rules we can't fix yet would go in WarningsNotAsErrors, so they stay visible as warnings. I'd also pin AnalysisLevel so an SDK upgrade can't break the build overnight, apply the same settings locally and in CI, and ratchet a legacy codebase towards zero warnings rather than flipping everything at once.

🗣 Full answer (senior)

How I would answer this in an interview

"My approach is to look past the headline TreatWarningsAsErrors to what's being suppressed and why. The mistake I see is exactly this: a long NoWarn list that hides nullable warnings, CS0618 obsolete APIs, the CA2100 SQL rule and NU1902 audit findings, so the build looks strict while the team learns to ignore the compiler. Known debt belongs in WarningsNotAsErrors so it stays visible, a legacy ratchet uses WarningsAsErrors for chosen groups like nullable, and genuine exceptions go in .editorconfig with a scope and a comment; I'd also pin AnalysisLevel instead of latest-all, enforce the same settings locally and in CI, make NU1903 and NU1904 errors, and move the opt-in condition to Directory.Build.targets, because props are evaluated before the project sets it.

For example, our [identity service at company] looked strict, with warnings as errors in CI, but a [40-entry] NoWarn list was hiding [900] warnings. Before a framework upgrade, my task was to find out what we couldn't see. I removed the suppressions on a branch, grouped the warnings, and found an obsolete authentication API and [two] SQL injection risks, which I fixed first. I moved the remaining debt to WarningsNotAsErrors with owners, pinned the analysis level and set up a CI ratchet that fails if a project's warning count goes up. As a result, the upgrade went through without surprises and suppressions dropped from [40] to [5] justified entries.

Warnings as errors is only valuable if the warnings that remain are meaningful, so security, nullable and high-severity audit findings are never suppressed, and the exceptions list has an owner and keeps shrinking."


The file under review

<Project>
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <AnalysisLevel>latest-all</AnalysisLevel>
    <TreatWarningsAsErrors Condition="'$(CI)' == 'true'">true</TreatWarningsAsErrors>
    <NoWarn>$(NoWarn);CS8600;CS8602;CS8603;CS8618;CS0618;CA2100;CA1848;CA2007;CA1062;NU1603;NU1902;...</NoWarn>
  </PropertyGroup>
</Project>

Findings

# Issue Why it matters Change
1 Nullable warnings (CS8600, CS8602, CS8603, CS8618) in NoWarn Nullable=enable is pointless if its warnings are hidden Remove; fix, or use WarningsNotAsErrors temporarily for projects still migrating
2 CS0618 (obsolete API) hidden You won't notice APIs that will be removed in the next version Remove; track and replace usages
3 CA2100 hidden SQL injection risk rule silenced repo-wide Remove; set to error
4 NU1902 hidden Hides known moderate-severity vulnerabilities in packages Remove; decide policy for audit warnings
5 AnalysisLevel=latest-all with warnings as errors Every SDK upgrade can add new rules and break builds Pin 10.0-recommended, upgrade deliberately
6 Warnings as errors only when CI=true Developers find failures only after pushing Same setting locally; use the IDE to see them early
7 CA2007, CA1848, CA1062 in a single global list Might be valid for some projects, not others Move to .editorconfig with a comment per rule and folder scoping

The properties

  • TreatWarningsAsErrors=true: all warnings are errors.
  • WarningsAsErrors=<ids or nullable>: only these are errors (use when you can't turn on everything yet).
  • WarningsNotAsErrors=<ids>: with TreatWarningsAsErrors, these stay warnings, visible but not blocking.
  • NoWarn=<ids>: the warning is not reported at all.
  • .editorconfig dotnet_diagnostic.<id>.severity: per rule, per folder, for analyser diagnostics (and many compiler warnings).

A better version

<Project>
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <AnalysisLevel>10.0-recommended</AnalysisLevel>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <!-- Visible, not blocking, while we pay down these specific items. Owner: platform team, review each quarter -->
    <WarningsNotAsErrors>$(WarningsNotAsErrors);CS0618;NU1902</WarningsNotAsErrors>
  </PropertyGroup>
</Project>

and in .editorconfig:

[*.cs]
dotnet_diagnostic.CA2100.severity = error
dotnet_diagnostic.CA2007.severity = none   # ASP.NET Core: no SynchronizationContext

Ratcheting a legacy codebase

  1. Measure warnings per project.
  2. Turn on TreatWarningsAsErrors for projects already at zero.
  3. For the rest, make the most valuable categories errors first (WarningsAsErrors=nullable, security rules).
  4. Fail CI if a project's warning count goes up (a simple script over the build log or SARIF output).
  5. When everything is clean, set it repo-wide and delete the exceptions.

Production example

A repo had a 40-entry NoWarn list accumulated over years. Removing it in a branch surfaced 900 warnings, including CS0618 on an authentication API scheduled for removal and two CA2100 hits. The team fixed the security items immediately, moved CS0618 to WarningsNotAsErrors with a ticket, and ratcheted the rest over two sprints.

Best practices, performance and security

  • A short, justified exceptions list beats a long silent one.
  • Security rules and NuGet audit for high and critical vulnerabilities should be errors.
  • Review warning policy changes like code; put build files under CODEOWNERS.

🔍 What the interviewer is checking

Tests whether the candidate can review build policy, not just code.

Green flags: spots nullable and security suppressions; knows the difference between NoWarn, WarningsAsErrors and WarningsNotAsErrors; pins AnalysisLevel; proposes ratcheting; notices CI-only enforcement and the evaluation-order bug.

Red flags: "looks strict, approve"; suggests adding more NoWarn entries; enforces everything at once.

📚 Learn in depth

1. Who produces warnings in a .NET build

A single dotnet build collects diagnostics from several producers, and they don't all obey the same switches:

Producer IDs Controlled by
C# compiler CS0xxx, CS8xxx (nullable is CS86xx) TreatWarningsAsErrors, WarningsAsErrors, WarningsNotAsErrors, NoWarn, #pragma, .editorconfig
.NET SDK analysers CAxxxx (quality), IDExxxx (style) The same, plus AnalysisLevel, AnalysisMode, EnforceCodeStyleInBuild
Third-party analysers for example SAxxxx, xUnitxxxx The same, through .editorconfig and the MSBuild properties
NuGet restore NUxxxx (audit is NU1901-NU1904) TreatWarningsAsErrors, WarningsAsErrors, NoWarn, plus NuGetAudit* properties
MSBuild itself MSBxxxx MSBuildTreatWarningsAsErrors, MSBuildWarningsAsErrors, MSBuildWarningsAsMessages, or -warnaserror on the command line

The last row surprises people: TreatWarningsAsErrors doesn't promote MSBuild's own warnings, such as MSB3277 assembly version conflicts. Use MSBuildTreatWarningsAsErrors for those, or dotnet build -warnaserror in CI, which applies to everything.

2. How a diagnostic ends up as an error, a warning or nothing

        diagnostic produced (compiler or analyser), default severity
                              |
   .editorconfig / .globalconfig severity for this file?
          none | silent | suggestion | warning | error
                              |
                              v
                     effective severity
                              |
   #pragma warning disable or [SuppressMessage] here? --yes--> suppressed
                              | no
        NoWarn contains ID? --yes--> dropped (not reported at all)
                              | no
        WarningsAsErrors contains ID (or 'nullable')? --yes--> error
                              | no
        TreatWarningsAsErrors = true?
              | yes                                   | no
              v                                       v
   WarningsNotAsErrors contains ID?               warning
        | yes -> warning   | no -> error

Three consequences:

  • TreatWarningsAsErrors only promotes things that are warnings. A rule set to suggestion in .editorconfig stays a suggestion, so you can keep a rule visible in the IDE without blocking the build.
  • NoWarn is the bluntest tool: the diagnostic disappears from every log, every report and every IDE.
  • WarningsNotAsErrors keeps a diagnostic visible as a warning, which is usually what you want for known debt.

3. The suppression tools, from narrowest to widest

Tool Scope Visible later? Good for
#pragma warning disable CA2100 // reason ... restore A few lines Yes, in the code One justified exception
[SuppressMessage("Security", "CA2100", Justification = "...")] A member or type Yes, with a required justification A reviewed exception on one method
.editorconfig section such as [src/Legacy/**.cs] A folder or file pattern Yes, in a config file A rule that doesn't fit one area (generated code, tests)
WarningsNotAsErrors Project or repo Yes, still reported as a warning Known debt being paid down
NoWarn Project or repo No Rules that genuinely never apply to the codebase

When you use pragmas, turn on IDE0079 ("remove unnecessary suppression") so suppressions that no longer hide anything get cleaned up.

4. Why AnalysisLevel must be pinned with warnings as errors

 AnalysisLevel=latest-all + TreatWarningsAsErrors=true

   SDK 10.0.1xx  -->  rules A, B, C enabled           build green
   SDK 10.0.2xx  -->  rules A, B, C, D enabled        build RED, no code changed

 AnalysisLevel=10.0-recommended

   SDK 10.0.2xx  -->  rule set frozen at 10.0          build green
   later: a pull request changes it to 11.0-recommended and fixes the new warnings

latest follows whichever SDK happens to build the code, so an agent image update or a new developer machine can break the build. A pinned value like 10.0-recommended makes the rule set part of the source, and upgrading it becomes a reviewed change. Pin the SDK in global.json as well, because new compiler warnings can arrive with a new SDK band even when AnalysisLevel is pinned.

5. NuGet audit warnings

NuGet checks restored packages against known vulnerabilities:

ID Severity
NU1901 Low
NU1902 Moderate
NU1903 High
NU1904 Critical

Related properties: NuGetAudit (on by default), NuGetAuditLevel (minimum severity reported) and NuGetAuditMode (direct or all; the .NET 10 SDK audits transitive packages too by default).

A sensible policy is to make NU1903 and NU1904 errors with WarningsAsErrors, keep NU1901 and NU1902 as visible warnings with an owner, and never put any of them in NoWarn. When a specific advisory doesn't apply to you, suppress that advisory with <NuGetAuditSuppress Include="https://github.com/advisories/GHSA-..." /> and a comment, rather than hiding every future vulnerability of that severity.

6. Local versus CI enforcement

If only CI promotes warnings to errors (Condition="'$(CI)' == 'true'"), developers get a green local build and a red pipeline about ten minutes later. The feedback loop gets longer and pull requests pick up "fix warnings" commits.

Use the same settings everywhere. If strictness gets in the way of quick experiments, the IDE's error list shows the problems as you type, and a developer can still build a scratch branch with -p:TreatWarningsAsErrors=false on purpose. The default should be identical.

7. Ratcheting a legacy codebase

 week 0   measure: warnings per project (build log or SARIF)
            Orders.Api 412   Billing 37   Shared 0   Reports 1,980
 week 1   Shared: TreatWarningsAsErrors=true (already at zero)
          everyone: WarningsAsErrors=nullable;CA2100;CA3001;NU1903;NU1904
          CI: fail if any project's count goes UP
 week n   Billing reaches 0 -> strict; Orders.Api 412 -> 120 -> 0 -> strict
 end      TreatWarningsAsErrors in Directory.Build.props, opt-ins deleted

To produce countable output, set <ErrorLog>$(MSBuildProjectName).sarif,version=2.1</ErrorLog> so the compiler writes a SARIF file per project, or build with -bl and inspect the binary log. A small script compares the counts with a baseline file committed in the repo and fails when any number rises. Lower the baseline in the same pull request that fixes warnings.

8. Diagnosis

Symptom Likely cause Check
A warning still appears despite NoWarn It's an MSBuild warning (MSBxxxx), or NoWarn is overwritten later without $(NoWarn); dotnet msbuild -getProperty:NoWarn and look for assignments that drop the existing value
Build red only on CI CI-only conditions, a different SDK, or -warnaserror in the pipeline Compare dotnet --info and the build command
Build red after an SDK update, no code change AnalysisLevel=latest, or new compiler warnings in the new band Pin AnalysisLevel and the SDK
An IDE rule doesn't fail the build EnforceCodeStyleInBuild is off, or the rule is at suggestion Set it to true and the severity to warning
Strict opt-in property seems ignored The condition is in Directory.Build.props, evaluated before the project sets the property Move the condition to Directory.Build.targets

9. Performance and security

  • Promoting warnings to errors has no build-time cost. Enabling more analysers does (see the analyser question), so measure with -p:ReportAnalyzer=true if builds slow down.
  • Security rules (CA2100, CA3001-CA3012 injection rules, CA5xxx cryptography rules) and high and critical NuGet audit findings should be errors and should never appear in NoWarn.
  • Build files decide what the compiler is allowed to tell you, so put Directory.Build.props, Directory.Build.targets and .editorconfig under CODEOWNERS.

10. .NET 11 awareness

.NET 11 (due in November 2026, an STS release with 24 months of support, currently in preview and release candidates) will add analyser rules and compiler warnings, as every release does. With AnalysisLevel and global.json pinned to .NET 10, they reach the build only when a pull request raises the pins and fixes what appears.

11. Try it yourself

  1. Create a project with Nullable enabled and a method that dereferences a string?.
  2. Build with -p:TreatWarningsAsErrors=true and see CS8602 as an error.
  3. Add <WarningsNotAsErrors>CS8602</WarningsNotAsErrors> and build: it's a warning again.
  4. Replace it with <NoWarn>CS8602</NoWarn> and build: the warning is gone entirely, including from the IDE.
  5. Add <ErrorLog>$(MSBuildProjectName).sarif,version=2.1</ErrorLog>, remove NoWarn, build and open the SARIF file to see a countable list.
  6. Reference a package version with a known advisory and watch NU1903 or NU1904 appear during restore; then add it to WarningsAsErrors and see restore fail.

💻 Code

<!-- Directory.Build.props: repo-wide defaults (evaluated BEFORE each project file) -->
<Project>
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <AnalysisLevel>10.0-recommended</AnalysisLevel>
    <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>

    <!-- Always errors, in every project -->
    <WarningsAsErrors>$(WarningsAsErrors);nullable;CA2100;NU1903;NU1904</WarningsAsErrors>
  </PropertyGroup>
</Project>
<!-- Directory.Build.targets: evaluated AFTER each project file, so it can see the project's opt-in -->
<Project>
  <PropertyGroup Condition="'$(StrictWarnings)' == 'true'">
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <WarningsNotAsErrors>$(WarningsNotAsErrors);CS0618</WarningsNotAsErrors> <!-- tracked: TICKET-1234 -->
  </PropertyGroup>
</Project>
<!-- A project that reached zero warnings opts in -->
<PropertyGroup>
  <StrictWarnings>true</StrictWarnings>
</PropertyGroup>

The opt-in condition lives in Directory.Build.targets because a condition in Directory.Build.props would run before the project file sets StrictWarnings, and would never match. Once every project has opted in, move TreatWarningsAsErrors into Directory.Build.props and delete the opt-in.

⚠ Common mistakes

  • Mistake: a long, unexplained NoWarn list. Each entry hides a whole rule in every file, forever. New nullable, obsolete-API and security problems arrive silently, nobody knows whether the original reason still applies, and the team learns that warnings are noise. The list only grows. Instead: remove suppressions that hide real problems, scope genuine exceptions in .editorconfig or with [SuppressMessage(Justification = ...)], and give each one a comment and an owner.

  • Mistake: using NoWarn for known debt. The debt becomes invisible, so nobody pays it down and the number of real occurrences keeps rising. Instead: put it in WarningsNotAsErrors, so it is still reported as a warning but doesn't block the build, and link it to a ticket.

  • Mistake: combining AnalysisLevel=latest with warnings as errors. New rules arrive with every SDK update, so a build can go red on an agent image change with no code change. Instead: pin AnalysisLevel (for example 10.0-recommended) and the SDK in global.json, and raise them in a dedicated pull request that fixes the new warnings.

  • Mistake: enforcing warnings as errors only in CI. Developers get a green local build and learn about failures after pushing, which slows every pull request. Instead: use the same settings locally and in CI, and rely on the IDE error list for fast feedback.

  • Mistake: turning on TreatWarningsAsErrors across a legacy repo in one step. Thousands of errors appear, and the quickest way back to green is mass suppression, which is worse than where you started. Instead: ratchet. Make the most important categories errors first (nullable, security, high and critical NuGet audit), make clean projects strict, and fail CI when a project's warning count rises.

  • Mistake: putting an opt-in condition in Directory.Build.props. The props file is evaluated before the project file, so a property the project sets (such as StrictWarnings) is still empty when the condition runs, and the opt-in never works. Instead: put conditions that depend on project settings in Directory.Build.targets.

  • Mistake: overwriting NoWarn or WarningsAsErrors without the existing value. <NoWarn>CA2007</NoWarn> in a project throws away the SDK's defaults and the repo-wide list. Instead: always append: <NoWarn>$(NoWarn);CA2007</NoWarn>.

  • Mistake: ignoring or blanket-suppressing NuGet audit warnings. NoWarn=NU1902;NU1903 hides every future vulnerability of that severity, not only the one you reviewed. Instead: make NU1903 and NU1904 errors, update the package, and when an advisory truly doesn't apply, suppress that advisory alone with NuGetAuditSuppress and a comment.

  • Mistake: assuming TreatWarningsAsErrors covers everything. MSBuild's own warnings (such as MSB3277 version conflicts) aren't promoted by it, so they pile up unnoticed. Instead: add MSBuildTreatWarningsAsErrors or build with -warnaserror in CI when you want those to fail too.

🏛 Senior insight: trade-offs

Warnings-as-errors is only valuable if the warnings that remain are meaningful. A senior reviewer looks past the headline TreatWarningsAsErrors=true to what's being suppressed and why, and pushes the policy towards: pinned analysis level, security and nullable always errors, known debt visible as warnings with an owner, and the same behaviour locally and in CI.

In a review of a change like this, the questions they ask are:

  1. Does each suppression have a reason and a scope? A rule silenced in one generated folder is fine; a security rule silenced repo-wide is not.
  2. Is it hidden or visible? Debt belongs in WarningsNotAsErrors (still shown) rather than NoWarn (gone).
  3. Which categories can never be suppressed? Typically CA2100/CA5xxx security rules, nullable, and NU1903/NU1904 audit findings.
  4. Will an SDK or package upgrade break the build? If yes, pin the version and upgrade it deliberately.
  5. Is the list shrinking? A suppression list with no owner or date only grows.

In a legacy codebase they ratchet rather than flip, because a policy that forces mass suppressions on day one teaches the team to ignore the compiler.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate mentions AnalysisLevel or CA rules. Why: Checks knowledge of the analysers behind the warnings.
    How to answer

    Analysers are plug-ins for the Roslyn compiler. They report diagnostics in the editor and during the build. CA rules (CA1xxx to CA5xxx) are the SDK's code quality analysers, covering design, performance, reliability and security. The .NET analysers are enabled by default for net5.0+ projects (EnableNETAnalyzers), but only a default subset of rules reports as warnings; AnalysisLevel and AnalysisMode set how many apply. IDE rules (IDE0xxx) are code style rules, such as var usage or file-scoped namespaces. They show in the editor but only run in the build when EnforceCodeStyleInBuild is true, which is why style issues often pass CI. Severities for both are set in .editorconfig (or a .globalconfig) with dotnet_diagnostic.<id>.severity. Third-party analysers come in as NuGet packages. For a team, I put the levels in Directory.Build.props, the severities in one root config file, and tighten them step by step.

  2. When: Candidate removes nullable IDs from NoWarn. Why: Nullable warnings are the most common suppressed category; checks a migration plan.
    How to answer

    <Nullable>enable</Nullable> makes reference types non-nullable by default: string? means a value can be null and string means it can't. The compiler then warns when code might dereference null. It's purely compile-time, so nothing changes at run time. In a big codebase I migrate bottom-up: shared libraries and domain types first, because their annotations flow out to everyone else. I go project by project, or file by file with #nullable enable, and give each step a small pull request so feature work carries on. When a project is clean I make its nullable warnings errors with WarningsAsErrors=nullable, so it can't slip back. I use attributes such as [NotNullWhen] where flow analysis needs help and keep ! for real boundaries. I also watch for two things: EF Core migrations, where nullability decides whether columns are required, and public APIs, where annotations are part of the contract.

  3. When: Candidate spots that the opt-in condition can't work in props. Why: The evaluation-order bug needs Directory.Build.props versus .targets knowledge.
    How to answer

    Directory.Build.props and Directory.Build.targets are ordinary MSBuild files that the SDK imports automatically into every project below them. For each project, MSBuild searches upwards and uses the nearest one it finds. .props is imported early, before the project's own content, so it's for default properties and items: target framework, Nullable, analysers, package metadata. Projects can still override those values. .targets is imported late, after the project, so it's for targets and logic that need final values. Only the nearest file is picked up. To layer them, the inner file imports the outer one with GetPathOfFileAbove. I debug them with dotnet msbuild -preprocess or a binary log.

🛠 Real-world problems: how you handle them

A strict-looking build that hid an authentication API removal
Situation
A .NET 10 identity service had TreatWarningsAsErrors on in CI and a 40-entry NoWarn list accumulated over several years. A framework upgrade was planned for the next quarter.
Signals
CI always green; zero warnings in build output; during upgrade planning, a spike branch removing NoWarn produced 900 warnings, including CS0618 on an authentication API marked for removal and two CA2100 SQL warnings.
Investigation
Removed NoWarn on a branch and grouped warnings by ID and project. Traced each suppression's origin with git blame: most were added to unblock a single PR and never revisited.
Resolution
Fixed the CA2100 issues immediately, replaced the obsolete authentication API, moved remaining known debt to WarningsNotAsErrors with tickets, pinned AnalysisLevel, and enforced the same settings locally and in CI. Ratcheted projects to full strictness as they reached zero warnings.
Prevention
CODEOWNERS on build files, a rule that suppressions need a comment and ticket, a CI check that fails if a project's warning count rises, and quarterly review of the exceptions list.

How to tell it (STAR)

**Situation:** Our [identity service] at [company] looked strict, with warnings as errors in CI, but a [40-entry] NoWarn list was hiding [900] warnings. **Task:** Before a framework upgrade, I reviewed the warning policy to find out what we couldn't see. **Action:** I removed the suppressions on a branch, grouped the warnings, and found an obsolete authentication API and [two] SQL injection risks. I fixed those first, moved the remaining debt to WarningsNotAsErrors with owners, pinned the analysis level and set up a ratchet in CI. **Result:** The upgrade went through without surprises, the security issues were closed [before release], and suppressions dropped from [40] to [5] justified entries.

↩ Usually asked after

More in Build System

Last updated 1 Nov 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me