{"slug": "your-net-application-compiles-does-that-mean-your-architecture-is-correct", "title": "Your .NET Application Compiles. Does That Mean Your Architecture Is Correct?", "summary": "A developer argues that compiling .NET solutions and passing unit tests do not guarantee architectural integrity, illustrating how a Clean Architecture order-management application can silently violate the Dependency Rule when the Application layer injects a concrete SqlOrderRepository from Infrastructure. The proposed fix replaces the concrete dependency with an IOrderRepository abstraction defined in the Domain layer, so the Application layer depends inward rather than outward.", "body_md": "When we start developing a new application, one of the first things we usually discuss is its architecture.\n\nShould we use Clean Architecture? A modular monolith? A traditional layered architecture?\n\nWe define our projects, organize dependencies, establish some rules, and everything looks good.\n\nBut what happens after six months? Or after several developers have worked on the same solution?\n\nMore importantly, **who makes sure that the architecture we initially designed is still respected?**\n\nThe compiler? Not really.\n\nOur unit tests? Not necessarily.\n\nCode reviews? Maybe, if someone notices the problem.\n\nThis is something I've been thinking about for a while, especially with the increasing adoption of AI coding agents.\n\nLet's take a practical example to understand the problem, and then see how we can address it.\n\nImagine we're developing an order management application using Clean Architecture.\n\nOur solution contains the following projects:\n\n```\nShop.slnx\n│\n├── Shop.Domain\n│   ├── Order.cs\n│   └── IOrderRepository.cs\n│\n├── Shop.Application\n│   └── OrderService.cs\n│\n├── Shop.Infrastructure\n│   └── SqlOrderRepository.cs\n│\n└── Shop.Api\n    └── Program.cs\n```\n\nNothing complicated here.\n\nThe Domain layer contains our business model and abstractions.\n\nApplication contains the use cases.\n\nInfrastructure contains the technical implementations.\n\nAnd API is our entry point and composition root.\n\nAccording to the Dependency Rule, dependencies should point inward. Domain should not know about Infrastructure, and Application should not directly depend on Infrastructure implementations.\n\nNow, imagine that we're implementing a use case to confirm an order.\n\nA developer writes the following code:\n\n```\nusing Shop.Domain;\nusing Shop.Infrastructure;\n\nnamespace Shop.Application;\n\npublic sealed class OrderService(\n    SqlOrderRepository repository)\n{\n    public async Task ConfirmAsync(\n        Guid orderId,\n        CancellationToken cancellationToken)\n    {\n        var order = await repository.GetAsync(\n            orderId,\n            cancellationToken);\n\n        order.Confirm();\n\n        await repository.SaveAsync(\n            order,\n            cancellationToken);\n    }\n}\n```\n\nAssuming the project reference exists and the repository exposes these methods, this code is perfectly valid from the compiler's perspective.\n\nIt can compile successfully.\n\nThe unit tests can pass.\n\nThe feature can work exactly as expected.\n\nSo, what's the problem?\n\nWell, our Application layer now directly depends on Infrastructure.\n\nWe have introduced an outward dependency that violates the architecture we initially agreed on.\n\nAnd this is where things become interesting.\n\n**The compiler checks whether our code is valid C#. It doesn't know whether the dependencies respect our architectural decisions.**\n\nOf course, we could catch this during code review. But should we really depend entirely on code reviews to detect something that can be checked automatically?\n\nI don't think so.\n\nThe problem is not the functionality itself. The problem is the dependency.\n\nInstead of injecting `SqlOrderRepository`, our Application layer should depend on an abstraction.\n\nLet's start with our Domain project.\n\n```\nnamespace Shop.Domain;\n\npublic interface IOrderRepository\n{\n    Task<Order> GetAsync(\n        Guid id,\n        CancellationToken cancellationToken);\n\n    Task SaveAsync(\n        Order order,\n        CancellationToken cancellationToken);\n}\n```\n\nWe can now change our Application service:\n\n```\nusing Shop.Domain;\n\nnamespace Shop.Application;\n\npublic sealed class OrderService(\n    IOrderRepository repository)\n{\n    public async Task ConfirmAsync(\n        Guid orderId,\n        CancellationToken cancellationToken)\n    {\n        var order = await repository.GetAsync(\n            orderId,\n            cancellationToken);\n\n        order.Confirm();\n\n        await repository.SaveAsync(\n            order,\n            cancellationToken);\n    }\n}\n```\n\nAnd let Infrastructure implement the abstraction:\n\n```\nusing Shop.Domain;\n\nnamespace Shop.Infrastructure;\n\npublic sealed class SqlOrderRepository\n    : IOrderRepository\n{\n    // Persistence implementation\n}\n```\n\nFinally, we register the implementation in our API project:\n\n```\nbuilder.Services.AddScoped<\n    IOrderRepository,\n    SqlOrderRepository>();\n```\n\nThere it is!\n\nOur Application layer no longer depends on Infrastructure.\n\nWe have restored the intended dependency direction.\n\nBut we haven't solved the original problem yet.\n\nWhat prevents another developer from introducing the same violation tomorrow?\n\nNothing, unless we introduce a mechanism to enforce that boundary.\n\nWe already automate a lot of things in software development.\n\nWe use analyzers to enforce coding conventions, tests to validate behavior, and CI pipelines to verify our changes before merging.\n\nWhy shouldn't we do the same with architecture?\n\nImagine being able to declare the following rules:\n\nAnd then automatically validate those rules against our solution.\n\nThis is the idea behind **Architecture Guardian**, an open-source .NET tool I've been working on.\n\nIt allows us to describe architectural boundaries using a YAML policy and validate them through Roslyn analyzers, a CLI, and MCP tools for AI coding agents.\n\nLet's see how it works.\n\nInstead of keeping our architecture rules only in documentation, we'll describe them in a file called `architecture.guardian.yaml`.\n\nFor our Shop solution, the policy can look like this:\n\n```\nversion: 1\n\nmodules:\n  - id: Shop\n    projects: [\"Shop.*/*.csproj\"]\n\nlayers:\n  - id: Domain\n    projects: [\"Shop.Domain/*.csproj\"]\n\n  - id: Application\n    projects: [\"Shop.Application/*.csproj\"]\n    allowedDependencies: [Domain]\n\n  - id: Infrastructure\n    projects: [\"Shop.Infrastructure/*.csproj\"]\n    allowedDependencies: [Domain]\n\n  - id: Api\n    projects: [\"Shop.Api/*.csproj\"]\n    allowedDependencies:\n      - Domain\n      - Application\n      - Infrastructure\n\nrules:\n  AG002: error\n  AG003: error\n\ngate:\n  failOn: error\n```\n\nHere, each project belongs to a layer, and each layer has an explicit list of allowed dependencies.\n\nNotice that Application can only depend on Domain.\n\nSo, if someone introduces a dependency from Application to Infrastructure, it will violate our policy.\n\nThe important point is that Architecture Guardian doesn't impose Clean Architecture on your application.\n\n**You define your architecture. The tool verifies whether your code respects it.**\n\nFor example, if we were using a different architectural style, we could define different boundaries and permissions.\n\nThe policy should describe the architecture we've chosen, not an architecture selected by the tool.\n\nRemember our first implementation of `OrderService`?\n\nLet's introduce the same problem again.\n\n```\nusing Shop.Infrastructure;\n\nnamespace Shop.Application;\n\npublic sealed class OrderService(\n    SqlOrderRepository repository)\n{\n    // ...\n}\n```\n\nAnd suppose we add the corresponding project reference.\n\nOur application can still compile.\n\nBut now we can validate the architecture.\n\nFirst, install the Architecture Guardian CLI:\n\n```\ndotnet tool install --global \\\n  ArchitectureGuardian.Cli \\\n  --version 0.3.0\n```\n\nAfter restoring the solution, we can run:\n\n```\narchguard scan Shop.slnx \\\n  --policy architecture.guardian.yaml \\\n  --configuration Release \\\n  --trusted\n```\n\nFor a repository we've authorized for MSBuild evaluation, `--trusted` allows the scan to proceed. It doesn't isolate untrusted build logic.\n\nThe scan can now detect our forbidden dependency and report an AG002 error.\n\nAn example diagnostic looks like this:\n\n```\nShop.Application/OrderService.cs:\nerror AG002:\nLayer 'Application' must not depend on\nlayer 'Infrastructure'.\n```\n\nOur code still compiles.\n\nBut our architecture validation fails.\n\nThat's exactly what we wanted.\n\nInstead of depending on someone noticing an architectural violation during a pull request, we now have an explicit rule that can be checked automatically.\n\nSo far, we've looked at a fairly simple layered architecture.\n\nLet's take a slightly more interesting case.\n\nImagine we're developing a modular monolith containing three business modules:\n\n```\nsrc/\n├── Catalog/\n│   ├── Catalog.Domain/\n│   ├── Catalog.Application/\n│   ├── Catalog.Contracts/\n│   └── Catalog.Infrastructure/\n│\n├── Orders/\n│   ├── Orders.Domain/\n│   ├── Orders.Application/\n│   └── Orders.Infrastructure/\n│\n└── Billing/\n    ├── Billing.Contracts/\n    ├── Billing.Application/\n    └── Billing.Infrastructure/\n```\n\nOrders needs to retrieve some product information from Catalog.\n\nA developer finds the following class:\n\n```\nnamespace Catalog.Infrastructure;\n\npublic sealed class ProductRepository\n{\n    public Task<Product> GetAsync(Guid id)\n    {\n        // ...\n    }\n}\n```\n\nSince the class is public, the developer decides to inject it into Orders:\n\n```\nusing Catalog.Infrastructure;\n\nnamespace Orders.Application;\n\npublic sealed class CreateOrderHandler(\n    ProductRepository products)\n{\n    // ...\n}\n```\n\nAgain, the code can compile if the required references exist.\n\nBut we've introduced another problem.\n\nOrders now knows about Catalog's internal persistence implementation.\n\nWhat happens if Catalog changes its storage technology?\n\nWhat if we replace SQL Server with another persistence mechanism?\n\nWhy should Orders care about that?\n\nIt shouldn't.\n\nThis is an example of an implementation detail leaking across module boundaries.\n\nLet's expose an abstraction through `Catalog.Contracts`.\n\n```\nnamespace Catalog.Contracts;\n\npublic interface IProductReader\n{\n    Task<ProductDto?> GetAsync(\n        Guid productId,\n        CancellationToken cancellationToken);\n}\n```\n\nNow, Orders only depends on the contract:\n\n```\nusing Catalog.Contracts;\n\nnamespace Orders.Application;\n\npublic sealed class CreateOrderHandler(\n    IProductReader products)\n{\n    // ...\n}\n```\n\nCatalog can implement this interface internally.\n\nOrders doesn't need to know whether the data comes from Entity Framework Core, an HTTP service, or something else.\n\nWe've created an explicit module boundary.\n\nArchitecture Guardian provides two complementary checks for this scenario:\n\n**AG001** validates whether a dependency between modules is allowed.\n\n**AG004** detects cross-module access to implementation projects instead of declared public contracts.\n\nOne important detail: a class being marked `public` in C# doesn't automatically make it a public architectural contract.\n\nThat distinction is particularly important in modular monoliths.\n\nNow let's look at another interesting case.\n\nImagine we have two modules, Orders and Billing.\n\nOrders needs some information from Billing, and Billing also needs some information from Orders.\n\nEach module exposes its own Contracts project.\n\nThe dependencies look like this:\n\n```\nOrders.Application\n    depends on Billing.Contracts\n\nBilling.Application\n    depends on Orders.Contracts\n```\n\nThere is no circular project reference here.\n\nThe solution can build successfully because the contract projects don't reference each other.\n\nHowever, at the logical module level, we have created a cycle.\n\nOrders depends on Billing, and Billing depends on Orders.\n\nIs this always a catastrophic problem?\n\nNot necessarily. But if our architectural goal is to keep modules independent and prevent cycles, we need to detect it.\n\nThis is what **AG003** is designed for.\n\nIt identifies cycles between logical modules using a complete solution analysis.\n\nAnd this brings us to an important distinction between Roslyn analyzers and solution-level architecture checks.\n\nRoslyn analyzers are extremely useful because they give us feedback directly while coding.\n\nArchitecture Guardian includes a Roslyn analyzer that can detect supported architectural violations within the compilation being analyzed.\n\nThis is valuable for developers because they don't have to run a separate command every time they modify a dependency.\n\nBut a Roslyn analyzer doesn't have the complete view of every relationship in a solution.\n\nFor example, the logical module cycle we just discussed requires examining dependencies across multiple projects.\n\nThat's why Architecture Guardian also provides a solution scanner.\n\nThe responsibilities are different:\n\nThe analyzer currently covers AG001, AG002, and AG004 through AG006 within its compilation scope.\n\nAG003 requires the solution scan.\n\nSo, having no diagnostics in the editor is useful, but it doesn't mean the entire solution is architecturally compliant.\n\nNow that we have defined our architecture, there's one more thing we should do.\n\nMake sure that no change can be merged without validating it.\n\nSince Architecture Guardian provides a CLI, we can execute the scan in an Azure DevOps pipeline, GitHub Actions workflow, or another CI environment.\n\nFor example:\n\n```\nsteps:\n  - script: |\n      dotnet restore Shop.slnx \\\n        -p:Configuration=Release\n    displayName: Restore solution\n\n  - script: |\n      archguard scan Shop.slnx \\\n        --policy architecture.guardian.yaml \\\n        --configuration Release \\\n        --trusted\n    displayName: Validate architecture\n```\n\nThis example assumes the agent already has the CLI and a compatible .NET SDK installed, and that the repository is trusted.\n\nThe scanner returns different exit codes depending on the result.\n\nA successful scan passes the gate.\n\nAn architectural violation can fail the pipeline.\n\nAnd if analysis cannot establish compliance, that is reported separately rather than being considered a successful architecture check.\n\nThis distinction is important.\n\n**A tool that couldn't analyze your solution shouldn't tell you that your architecture is valid.**\n\nFor existing applications, Architecture Guardian also supports baselines and change-oriented analysis, which can help teams introduce enforcement progressively instead of fixing every historical violation immediately.\n\nThis is probably one of the most interesting parts of the project for me.\n\nWe're increasingly using AI coding agents to implement features, refactor code, and work across entire repositories.\n\nThey can generate quite impressive code.\n\nBut they can also introduce architectural violations.\n\nLet's return to our modular monolith.\n\nImagine giving an agent the following task:\n\n\"Implement a feature in Orders that retrieves product information from Catalog.\"\n\nThe agent explores the codebase and discovers `Catalog.Infrastructure.ProductRepository`.\n\nThe class is public.\n\nThe method already does what the agent needs.\n\nSo, it adds the reference and implements the feature.\n\nThe result can be functional.\n\nBut the agent has completely bypassed the architectural contract we wanted to preserve.\n\nWas the agent wrong?\n\nFrom a functional perspective, perhaps not.\n\nFrom an architectural perspective, yes.\n\nThe problem is that the agent may not know our architectural decisions, especially when those decisions are only documented in a README or an architecture diagram.\n\nArchitecture Guardian provides an MCP server that exposes architectural information to compatible AI coding agents.\n\nInstead of simply asking an agent to respect Clean Architecture, we can give it tools to inspect the actual architectural policy.\n\nThe MCP integration exposes operations such as:\n\n`get_architecture` to discover modules, layers, and dependency permissions.`list_rules` to understand available rules.`analyze_solution` to validate changes against the solution.`explain_violation` to understand a reported problem and its possible remediation.`analyze_changes` to inspect findings relative to Git changes.\nLet's imagine a different workflow.\n\nBefore implementing the feature, the agent asks Architecture Guardian for the solution's architectural context.\n\nIt discovers that Orders is allowed to access Catalog through its contract project, but not through Infrastructure.\n\nThe agent implements the feature using `IProductReader`.\n\nThen it runs the architecture analysis.\n\nIf a violation is reported, it can inspect the diagnostic and correct its implementation.\n\nThe difference is that the agent no longer has to rely only on natural-language instructions to understand our architectural boundaries.\n\nIt can query them.\n\nOf course, MCP doesn't guarantee that an agent will always make the right architectural decision.\n\nAnd it doesn't replace the CI gate.\n\nBut it gives agents access to the same architecture policy that developers and pipelines use.\n\nThis is the direction I wanted to explore with Architecture Guardian.\n\nNo, and I think it's important to clarify this.\n\nWe already have libraries and approaches for writing architecture tests in .NET.\n\nThey're useful, and Architecture Guardian isn't intended to make them irrelevant.\n\nThe goal is to make architectural rules available throughout the development workflow, instead of limiting validation to tests that someone must remember to run.\n\nIt also doesn't replace code reviews.\n\nAn architectural rule can verify that Application doesn't depend on Infrastructure.\n\nBut it cannot decide whether your abstraction is well designed, whether your aggregates make sense, or whether you've selected the appropriate architecture for your business requirements.\n\nThose decisions still require engineering judgment.\n\nThe tool is there to enforce the boundaries we've explicitly declared.\n\nAnd we should be careful not to create unnecessary restrictions.\n\n**The goal is to protect our architecture, not to make development more complicated.**\n\nArchitecture Guardian is available on GitHub and distributed through NuGet.\n\nYou can start by installing the CLI:\n\n```\ndotnet tool install --global \\\n  ArchitectureGuardian.Cli \\\n  --version 0.3.0\n```\n\nCreate your `architecture.guardian.yaml` policy, restore the solution, and run the scanner.\n\nThere is also a runnable Clean Architecture example in the documentation, containing both compliant and intentionally violating configurations.\n\nYou can experiment with different dependency violations and observe the diagnostics without modifying your own application.\n\nIf you want to integrate the checks into your IDE, the Roslyn analyzer package is also available.\n\nAnd if you're experimenting with AI coding agents, you can explore the MCP integration.\n\nThe full setup instructions and examples are available here:\n\n**Documentation:** [https://kbourzayq.github.io/arch-guard/](https://kbourzayq.github.io/arch-guard/)\n\n**GitHub:** [https://github.com/kbourzayq/arch-guard](https://github.com/kbourzayq/arch-guard)\n\nTo conclude, I think we sometimes spend a lot of effort designing software architectures without putting the same effort into preserving them.\n\nWe discuss dependency rules, module boundaries, separation of concerns, and all the principles that should help us build maintainable software.\n\nBut as the application grows, it's surprisingly easy to introduce dependencies that contradict those decisions.\n\nAnd most of the time, the compiler won't complain.\n\nMy intention with Architecture Guardian is not to introduce another architectural framework or tell developers how they should design their applications.\n\nIt's to provide a way to **define architectural decisions once and continuously verify them**.\n\nA last piece of advice: don't try to enforce every possible architectural rule from the first day, especially if you're working on a large existing application.\n\nStart with the boundaries that matter most, understand your existing dependencies, and progressively introduce stricter validation.\n\nOtherwise, the tool intended to protect your architecture might become an unnecessary obstacle for your team.\n\nThat's it!\n\nArchitecture Guardian is open source, and feedback is more than welcome.\n\nI'm particularly interested in hearing how other teams manage architectural boundaries in large .NET solutions, modular monoliths, and AI-assisted development workflows.\n\nFeel free to try it, challenge the approach, open an issue, or contribute.\n\nSee you soon!\n\n**DEV.to tags:** `#dotnet` `#csharp` `#architecture` `#opensource`", "url": "https://wpnews.pro/news/your-net-application-compiles-does-that-mean-your-architecture-is-correct", "canonical_source": "https://dev.to/bourzayq_khalid/your-net-application-compiles-does-that-mean-your-architecture-is-correct-5b1i", "published_at": "2026-10-30 19:47:22+00:00", "updated_at": "2026-10-30 19:50:38.070113+00:00", "lang": "en", "topics": ["developer-tools"], "entities": [".NET", "Clean Architecture", "Shop.Domain", "Shop.Application", "Shop.Infrastructure", "SqlOrderRepository", "IOrderRepository"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/your-net-application-compiles-does-that-mean-your-architecture-is-correct", "markdown": "https://wpnews.pro/news/your-net-application-compiles-does-that-mean-your-architecture-is-correct.md", "text": "https://wpnews.pro/news/your-net-application-compiles-does-that-mean-your-architecture-is-correct.txt", "jsonld": "https://wpnews.pro/news/your-net-application-compiles-does-that-mean-your-architecture-is-correct.jsonld"}}