Entity Framework in .NET Aspire

Published on Wednesday, November 29, 2023

A path through the infrastructure

.NET Aspire is an opinionated, cloud ready stack for building observable, production ready, distributed applications.

.NET Aspire is currently in preview and focuses on simplifying the developer experience with orchestration and automatic service discovery features. There's a huge potential for .NET Aspire beyond this initial valuable feature set.

Being in preview, .NET Aspire may not yet support all the scenarios or workloads you may be comfortable with. It's an opinionated framework, which means differences of opinion are natural and expected. Currently, one of those opinions seems to be a focus on containers. The sample solutions that the new dotnet templates provide are a great example of the benefits of containerization. The .NET Aspire starter solution that dotnet new --use-redis-cache --output AspireStarter generates, out of the box, is something that, when debugged, will download, run, and utilize a Docker Redis image. (I've worked with teams where getting each member productive in a development environment has ended up being days of work.) The AppHost component of a .NET Aspire solution codifies abstract aspects of the architectural decisions that automates the generation and deployment of a development environment<(!--and configuration provides the details from future decisions about other environments-->.)

A container focus is empowered by .NET Aspire's orchestration features. An independent orchestration responsibility enables better separation of release and deploy concerns from build and test concerns; shifting right those decisions that release and deploy depend on. (i.e., the ability to develop, execute, and evaluate solutions are discernibly left of release and operation.) Containers are an established method of componentizing a distributed system with independent servers (sometimes called "tiers.") This provides flexibility to deploy and execute in a development environment even before architectural decisions about a production topology have been considered. For example, debugging the .NET Aspire starter app automatically spins up a Redis container in Docker, but it's extremely unlikely that's how it will be deployed in production. In production, will there be one only Redis instance? If you have many instances, what sort of gateway or reverse proxy to that pool of instances will be utilized? Will it be on-prem or cloud? Will it be Azure, AWS, or Google Cloud? The beauty of Aspire's orchestration feature is that it doesn't matter yet; you can configure orchestration to figure it out at run-time, one environment at a time!

But, with every decision comes compromise. Technologies that depend on the physical resources that come from those decisions (that we're now effectively deferring) introduce some challenges with some existing software development idioms. A chicken-and-egg situation: if how to connect to physical resources may only be known at run-time, what happens to design-time technologies that depend on that connection information?

One popular technology in .NET, Entity Framework, suffers one of those challenges in .NET Aspire (Possibly only in code-first scenarios. Many Entity Framework examples detail adding Entity Framework support to an existing component (resource, like console app, ASP.NET Core web API, Razor app, etc.), creating a circular dependency between its project and the existence of an executing database (i.e., a valid database connection string.) In database-first, you have an existing application with existing physical databases and practices to utilize them in a development environment. With .NET Aspire, developers are shifted left from the decisions that provide the resources that things like migrations add <migration-name> and dotnet ef database update require to function properly.

To be clear, the way .NET Aspire works is that the orchestration (AppHost) executes, figures out the various connection strings, and overrides the appsettings by setting environment variables before running the other components. The premise behind this means that at run-time, whatever is in appsettings is ignored. dotnet ef command doesn't execute at run-time; it effectively runs at design-time and gets its configuration from appsettings, so it's out of sync with reality.

The basic guidance is to abstract those types of dependencies as .NET Aspire resources. Nothing new conceptually, but this might be an application of the principles of abstraction at a level less commonly applied. Refining that guidance to using Entity Framework: the database should be an independent resource. Independent resources are modeled in .NET as either separate projects or separate solutions. Luckily, an .NET Aspire sample addresses this. Let's look into the details.

The structure of the eShopLite sample overlaps with the .NET Aspire starter dotnet new template. It has a Blazor web frontend, a web API, an Aspire AppHost, and an Aspire service defaults project. Additionally, there is a shopping cart service (BasketService), and the catalog database (CatalogDb) project is an abstraction of the database resource.

The CatalogDb looks very similar to what you'd end up with following Tutorial: Create a web API with ASP.NET Core: an ASP.NET Core web API that leverages Entity Framework, and is effectively a gateway to a backend database. Although, that tutorial uses Entity Framework in-memory rather than via PostgreSQL. The way eShopLite supports Entity Framework is through the CatalogDb project. CatalogDb is like a stub project to the rest of the solution: Aspire doesn't execute it, but CatalogService depends upon it for the database model classes and DbContext (utilized more like a class library.) Nothing connects to the CatalogDb web API. The CatalogDb project contains all the Entity Framework design-time details and references, allowing you to utilize Entity Framework's features like migrations add <migration-name> and dotnet ef database update. The target of Entity Framework operations like migration add and database update would depend on the configuration in appsettings.json. Initialization/seeding of the data is handled in CatalogDbInitializer within CatalogDb, as well as migrations at run-time (startup). CatalogDb appsettings connection strings must be in sync with the run-time values for ef commands to work.

In summary, if you want to utilize Entity Framework in a basic .NET Aspire application, adding a project to contain the entity models, context, and Entity Framework references and supporting a database engine container is a recommended place to get started. I suspect this guidance may be refined as .NET Aspire evolves.

I'm still wrapping my head around how .NET Aspire can support other non-containerized workloads like Azure SQL. Still, a containerized design melds nicely with the idea of independent resources (or nodes) in .NET Aspire. .NET Aspire also helps to more clearly delineate concerns like design, build, test, release, and deploy. As with .NET Aspire, containerization is an easier starting point for someone interested in distributed applications.

I look forward to how .NET Aspire evolves.

comments powered by Disqus