web
You’re offline. This is a read only version of the page.
close
Skip to main content

Announcements

No record found.

News and Announcements icon
Community site session details

Community site session details

Session Id :
Dynamics 365 Community / Blogs / Living In Technology / Stop Putting Everything in ...

Stop Putting Everything in One Place: A Better Repository Strategy for Power Platform and Azure Projects

jestuder Profile Picture jestuder 158
When working with software development teams — whether as a developer, architect, or technical manager — one pattern tends to surface repeatedly in inherited or long-running projects: the monolithic solution. Everything lives in a single Visual Studio solution file, regardless of whether the code targets different runtimes, serves different purposes, or is deployed to completely different environments. The intent is usually convenience, but the long-term cost is significant.

This post outlines why splitting code into separate, purpose-driven repositories is the better approach — and why mixing .NET versions in a single solution in particular should be avoided.

What Does a Monolithic Solution Look Like?
A monolithic solution in the .NET world typically includes a mix of project types under one solution file (.sln). For example, a Dynamics 365 / Power Platform project might contain:

  • Plugins targeting .NET Framework 4.6.2
  • Azure Functions targeting .NET 8.0 (or even .NET 10.0)
  • Console utilities built on .NET Standard 2.0
  • Web APIs or background workers on modern .NET

All of this is loaded into a single solution, often sharing a single Git repository. At a glance, it seems organized. In practice, it creates layers of compounding problems.

The .NET Runtime Problem
.NET Framework, .NET Standard, and modern .NET (formerly .NET Core) are not the same runtime. They share a name and a language (C#), but they target different execution environments with different APIs, behaviors, and limitations.

  • .NET Framework 4.6.2 runs on the Windows CLR and is required for Dynamics 365 plugins — it cannot be upgraded to modern .NET without a full rewrite of how those plugins are registered and executed.
  • .NET Standard is a compatibility specification, not a runtime. Projects targeting .NET Standard can be consumed by both .NET Framework and modern .NET, but they have API surface restrictions as a result.
  • Modern .NET (8.0, 10.0, etc.) is cross-platform, cloud-optimized, and the direction Microsoft is actively investing in. Azure Functions, web APIs, and console tools fit naturally here.

When these are mixed in a single solution, shared library packages become a negotiation between runtimes. A NuGet package used by a .NET Framework project must also support the .NET Standard or .NET Framework target moniker, even if the consuming Azure Function project could benefit from a more modern, .NET 8-specific version of that same package. Developers end up pinning dependency versions to the lowest common denominator, forfeiting performance improvements, bug fixes, and new APIs available in newer package versions.

Dependency Management Becomes a Liability
In a single repository with mixed runtimes, every NuGet dependency decision carries more weight than it should. Consider the following scenario:

A shared utility library is consumed by both a plugin project (.NET Framework 4.6.2) and an Azure Function (.NET 8). A critical security patch is released for a dependency — but the updated package only supports .NET 6 and above. The Azure Function can take the patch immediately. The plugin project cannot. Now the repository has a version conflict with no clean resolution, and the security patch goes unapplied in part of the codebase.

Separate repositories eliminate this tension entirely. Each codebase manages its own dependencies in isolation, upgrades on its own schedule, and is never held back by the constraints of an unrelated project type.

Build Times and CI/CD Complexity
A large monolithic solution means every build — whether triggered locally by a developer or by a CI/CD pipeline — compiles the entire codebase. A change to a single plugin class triggers a rebuild of Azure Functions, utilities, and everything else in the solution. Over time, this adds up to significant wasted time across a development team.

Separate repositories allow for lean, targeted pipelines:

  • A plugin repository builds, tests, and packages plugin assemblies only when plugin code changes.
  • An Azure Functions repository has its own pipeline tuned for cloud deployment, including environment-specific configuration and slot swapping.
  • Utility libraries can publish NuGet packages independently and be versioned with semantic versioning, consumed by whichever other repositories actually need them.

Build pipelines become faster, easier to reason about, and significantly easier to maintain.

Deployment Cadences Are Different
Not all code deploys at the same frequency or through the same process. Dynamics 365 plugins are registered through the Plugin Registration Tool or a deployment pipeline targeting Dataverse. Azure Functions are deployed to Azure App Service or Function Apps. A console utility might be packaged as a NuGet tool or run on demand.

Mixing these in a single repository means a deployment pipeline must either handle all targets at once or become increasingly conditional and brittle with per-project logic. Separate repositories give each deployment type a clean, purpose-built pipeline with no crossover concerns.

Team Ownership and Access Control
In many organizations, different teams or vendors own different parts of a system. A single monolithic repository creates an all-or-nothing access model: either a developer has access to the entire codebase, or they do not. This is a security and confidentiality concern when working with outside contractors or specialized teams.

Separate repositories allow access to be scoped appropriately. A vendor brought in specifically for Azure integration work has no reason to access plugin source code, and vice versa. Branch protection rules, required reviewers, and deployment permissions can all be configured independently per repository.

Separation of Concerns at the Repository Level
Each repository becomes a single unit of responsibility with a clear purpose. This has practical benefits beyond the technical:

  • Cleaner commit history — Changes are scoped to the project they affect. Pull requests are focused and easy to review.
  • Easier onboarding — A new developer working on Azure Functions does not need to understand the plugin codebase to get started.
  • Simpler versioning — Each repository can be tagged and versioned independently based on its own release history.
  • Better documentation alignment — READMEs, wikis, and runbooks map directly to one focused codebase rather than trying to cover everything under one roof.

A Practical Repository Strategy for Power Platform Projects
When applied to a Power Platform and Azure ecosystem, a clean repository strategy is not just theoretical — it directly maps to how the platform itself is structured, how code is compiled, and how deployments work. Below is a recommended breakdown.

1. Power Platform Solutions
This repository stores exported and unpacked Power Platform solution files. Using the Power Platform CLI (pac solution export with the --unpack flag), solution files can be stored in a human-readable, diff-able format. This makes it possible to track exactly what changed in a solution between commits — a specific form, a cloud flow, a security role — rather than comparing opaque compressed files.

This repository is not application code. It is configuration and customization under source control. Keeping it separate from plugin or web resource code means the solution backup pipeline can run independently, on its own schedule, without triggering unrelated build steps.

2. Power Platform Plugins
Plugin code targets .NET Framework 4.6.2, a hard requirement imposed by the Dataverse plugin execution sandbox. This runtime constraint alone is sufficient reason to isolate plugin code in its own repository. The build pipeline for this repository produces signed plugin assemblies that are registered against Dataverse through the Plugin Registration Tool or an automated deployment pipeline.

Mixing plugin code with projects targeting modern .NET versions causes the dependency management and versioning problems described earlier. A dedicated plugin repository keeps the runtime target locked, the NuGet dependencies appropriate for that runtime, and the deployment process clean.

3. Power Pages
Power Pages portals have their own content model — page templates, content snippets, web files, site settings, and liquid templates — that is separate from both solution customizations and plugin code. The Power Platform CLI supports downloading portal content (pac paportal download), which produces a folder structure that can be committed to source control.

A dedicated Power Pages repository stores this portal content independently. Portal deployments have their own lifecycle: content and layout changes are often made by non-developer roles such as web designers or content authors, and those changes should not be entangled with plugin deployments or solution exports.

4. Power Platform Web Resources
Web resources — JavaScript files, HTML pages, CSS stylesheets, and images used in model-driven app forms and views — are deployed to Dataverse but are developed and maintained as front-end assets. They are not compiled assemblies and have no runtime target dependency. However, they benefit from the same source control discipline: version history, code review, and automated deployment.

A dedicated web resources repository can include tooling for bundling or minifying JavaScript, linting, and automated upload to Dataverse via the Power Platform CLI or the Dataverse Web API. Keeping web resources separate from plugin code also reflects the reality that these are often maintained by developers with different skill sets — front-end versus back-end.

5. Azure Functions
Azure Functions target modern .NET (8.0, 10.0, or whatever the current LTS version is at the time of development). They are cloud-deployed, independently scalable, and managed through Azure infrastructure. Their CI/CD pipeline involves Azure-specific deployment steps — publishing to a Function App, managing App Settings and Key Vault references, handling deployment slots — none of which have any overlap with plugin registration or solution export.

A dedicated Azure Functions repository contains only function code, supporting libraries, and infrastructure configuration (such as Bicep or ARM templates if infrastructure-as-code is used). Dependencies are free to target modern .NET APIs without any concern for .NET Framework compatibility.

6. Tools
Not every piece of code is a production workload. Console applications built to run a one-time data migration, automate a repetitive administrative task, or assist with a business process during a go-live are valuable but do not belong in the same repository as production services.

A dedicated tools repository is the right home for these utilities. They can target modern .NET for maximum flexibility, be documented with their intended use and expected inputs, and be versioned without affecting deployment pipelines for any production system. Keeping tools separate also prevents the temptation to repurpose production shared libraries in ways that were never intended — a common source of unintended side effects in monolithic solutions.

How These Repositories Work Together
Each of these repositories operates independently, but they are part of a coherent ecosystem. Shared code — utility functions, common data models, API clients — can be extracted into a dedicated shared library repository that publishes internal NuGet packages. Each consuming repository references those packages at the appropriate version rather than copying code or linking projects across repository boundaries.

The result is a clean dependency graph: each repository knows exactly what it depends on, and changes to one repository do not unexpectedly break or require rebuilds in another.

Common Objections
The most common pushback against splitting repositories is that it makes things "harder to find" or requires managing more repositories. These are understandable concerns but are largely addressed by good tooling:

  • Most Git platforms (GitHub, Azure DevOps) support organization-level search across repositories, making code discovery straightforward.
  • Naming conventions and documentation standards eliminate confusion about where code lives.
  • The overhead of managing multiple repositories is a one-time setup cost. The cost of maintaining a monolithic solution grows continuously over time.

Summary
A monolithic solution may feel like a simpler starting point, but it accumulates technical debt quickly — especially when mixing .NET Framework, .NET Standard, and modern .NET targets. The right approach is to organize repositories by purpose, runtime target, and deployment destination. Plugins belong in a plugin repository targeting .NET Framework 4.6.2. Azure Functions belong in a functions repository targeting the appropriate modern .NET version. Shared libraries, utilities, and other components each deserve their own space.

The short-term convenience of keeping everything in one place is not worth the long-term cost in build complexity, dependency conflicts, deployment friction, and maintenance burden. Breaking up the monolith is an investment that pays dividends throughout the entire life of a project.

This was originally posted here.

Comments

*This post is locked for comments