Explore the Evolution of .NET: Key Features from .NET 7 to 9 Preview
.NET and C# have evolved rapidly in recent years, bringing features that improve performance, simplify syntax, and enhance productivity. In this article, we’ll explore some of the most impactful features introduced from .NET 6 to .NET 9 (Preview), along with their matching C# versions.
Here’s what we’ll cover at a glance:
.NET 7 & C# 11
Required members
Minimal API improvements
Rate limiting middleware
.NET 8 & C# 12
Primary constructors
Collection expressions ([..items])
FrozenDictionary
Keyed services
.NET 9 (Preview) & C# 13
Native OpenAPI Support
LINQ: CountBy, AggregateBy
params Span<T>
Cloud-native support (OpenTelemetry, profiling)
Let’s take a closer look at how these features can help us build better applications, with less effort and more clarity.
Required Members - No More Missing Init Properties (.NET 7)
I once created a User object, forgot to set Name, and boom!! A surprise crash at runtime. Meet "required" members, a simple keyword that saves us from forgetting to initialize essential properties. No more runtime surprises. The compiler’s got our back.
Minimal APIs - Lightweight and Fast, But Not Always the Best Fit (.NET 7)
We're building a simple dashboard for our HR team. It doesn’t need to be fancy. Just something small that can get employee info, handle leave requests, and show the holiday calendar. Since we want to finish it fast, we decide to use Minimal APIs.
We didn’t have to set up controllers, make separate folders for DTOs, or register a ton of services. It just worked and it worked fast.
When to Use It:
When building quick prototypes or MVPs where finishing fast matters more than perfect structure
For small microservices that only need a few specific endpoints
In serverless setups like Azure Functions or AWS Lambda
For internal tools or admin dashboards where just a handful of endpoints are enough
Where It Starts to Struggle:
Now imagine our HR tool gets bigger. Suddenly, we need:
Role-based access for different endpoints
Proper model validation using data annotations
Logging or error handling through filters
More routes, rules, and custom behaviors
Large teams thrive on separation of concerns: Controllers, Services, Validators, DTOs, etc.
Scenarios needing clear separation (Controller + Service + Model). We might start clean with a few lines. However with 100s of endpoints, dependency injection, validation, exception handling, auth, and custom logic - Program.cs becomes a kitchen sink. This leads to teams re-inventing structure manually, introducing inconsistency across endpoints.
Final Verdict
Minimal APIs are a great way to move fast when we don’t need much structure. Perfect for small tools, quick tests, or focused services. But as the app grows and the team grows with it, the lack of clear separation can slow us down. If we find ourselves adding more and more features, roles, validations, and custom logic, it’s a sign that we might need to shift to a more structured approach. Minimal APIs help us start simple, but they’re not always the best place to stay.
Built-in Rate Limit Middleware - No More Third-Party Packages (.NET 7)
"The API crashed again!” - That was our morning. Some overenthusiastic client had fired 1000 requests in seconds. We needed a guard fast. Enter .NET 7’s built-in Rate Limiting. No third-party packages. No hacks. Just clean, powerful protection baked right into the framework.
Wait… What’s Rate Limiting?
Imagine our API is like a coffee shop. If 500 people walk in at once, chaos happens. The barista (our server) crashes.
Rate limiting is like the shop's bouncer. It lets only a certain number of people in per minute, everyone else waits. This way, our barista can work smoothly, and nobody gets hurt (especially our app).
In tech terms: It controls how many requests users can send to our API in a given time which protecting it from abuse, accidents, or overload. There are multiple strategies to support rate limit. Those are Fixed Window, Sliding Window, Token Bucket, Leaky Bucket.
When To Use:
Public APIs (e.g., REST endpoints).
Preventing brute-force attacks.
API fair usage enforcement.
When to Avoid It:
Internal microservices with trusted clients.
Low-traffic apps where overhead isn’t justified.
Primary Constructors in Classes - Cleaner DI or Hidden Trap? (.NET 8)
We're building a small service or a minimal API endpoint handler. Instead of boilerplate constructor injection, we want something clean and readable.
When It Shines:
Perfect for lightweight service classes, handlers, DTOs
Cleaner constructor injection
Reduces ceremony in dependency-injected classes
Pitfalls of Primary Constructors:
Problem: Mutable Primary Constructor Parameter:
When using primary constructors in classes, parameters are not automatically immutable. This can lead to unintended mutations if not properly encapsulated.
The client parameter is implicitly a mutable field. If modified (e.g., in ReplaceClient), dependent code (like SendRequest) might break or behave unpredictably.
Solution: Enforce Immutability:
To ensure immutability, explicitly assign the parameter to a readonly field or a property with a private set.
Problem: Dependency Injection Conflicts:
Dependency injection (DI) containers might mis resolve dependencies when multiple constructors exist.
DI containers like ASP.net Core might select the parameter less constructor by default, bypassing the primary constructor.
Solution: Prioritize Primary Constructors:
Avoid mixing primary and traditional constructors. Use a single primary constructor for DI:
Use explicit constructor injection and avoid ambiguous overloads.
Final Verdict
We should treat parameters like leftovers. Always store them safely in the fridge (fields/properties) so they don’t vanish :). And let’s keep just one door (constructor) for dependency injection containers. Extra doors confuse them, and they’re terrible at guessing which one to pick.
Collection Expressions ([..items]) (.NET 8)
A simplified syntax to create collections (arrays, lists, spans) using a unified [] syntax with spread operator [...]. The following are the use cases.
Merging collections: Combine arrays or lists without needing AddRange or Concat.
Inline initialization: Declare and initialize collections with less boilerplate.
When to Avoid:
Performance-critical code The spread operator (..) introduces minor overhead. In tight loops or high-frequency code paths, prefer manual array operations.
Older projects Collection expressions require .NET 8 or later. If we’re maintaining legacy code, this feature won’t be available.
FrozenDictionary - Immutability & Performance (.NET 8)
Dictionaries are fast, but they’re mutable. We want immutability with speed. FrozenDictionary offers optimized, read-only lookup with low memory overhead.
Where It Shines:
Read-heavy configs
Enum-like mappings
Lookup Tables (e.g., country codes, product catalogs).
Where It Doesn’t Fit:
Dynamic or frequently updated data
Keyed Services - Multiple Strategies, One Interface (.NET 8)
We're building a notification service. Depending on user preferences, we send email, SMS, or push notifications. We want to register all implementations in a clean and organized way.
When It Shines:
Strategy-based injection (e.g., payment processors, exporters, notifiers)
Clean separation between implementations
When It Doesn’t Fit:
Overused in simple projects where an enum switch or factory would suffice
Makes debugging harder if overused
Final Verdict
This pattern works really well when we need to switch between different strategies like sending notifications or handling different payment methods without mixing everything together. It helps keep things clean and separate. But if the project is small, using this everywhere can feel like overkill. Sometimes a simple switch or a basic factory does the job just fine. Overdoing it might just make things harder to read and debug later. So, it’s great when we really need flexibility but not something we need to reach for every time.
Built-in Native OpenAPI Support (.NET 9)
We’re building a new minimal API. We want OpenAPI documentation so frontend developers or Postman can explore the endpoints. Previously, we had to install and configure Swashbuckle. With .NET 9, OpenAPI support is native
When It Shines:
Rapid prototyping
Small, focused APIs
Teams that want Swagger with minimal setup
When It Doesn't Fit
Complex APIs with security schemes, polymorphic responses, and custom docs
APIs that already use NSwag/Swashbuckle heavily
Final Verdict
The new built-in OpenAPI support in .NET 9 feels like a shortcut we’ve always wanted. No extra packages, no complicated setup. Just two lines and Swagger is ready. It’s great when we’re building small APIs or trying things out quickly, especially when frontend teams need something to test against right away. But if we’re dealing with more complex APIs that need deep customization, or if we’ve already invested in tools like Swashbuckle or NSwag, this might not be the right fit. For quick wins, it’s a gem. For big jobs, we might still need the full toolbox.
CountBy and AggregateBy - LINQ Gets a Boost (.NET 9)
We have logs of user activity and want to count actions per user.
When It Shines:
Tiny Reporting like application where summary is needed
Quick statistical breakdowns
When It Doesn’t Fit:
When we need advanced grouping like multi-key (user + day)
When aggregations need full control (e.g., sum, average, max)
params Span<T> (.NET 9)
Imagine we're managing a pizza delivery service. Some days we get single orders, other days we get bulk orders from offices. We need a way to handle both efficiently without wasting boxes or making multiple trips. This is exactly what params Span<T> helps with in .NET. Handling variable numbers of inputs efficiently.
When It Fits:
When we need to pass a variable number of similar items to a method
When we want to avoid heap allocations for temporary arrays
When It Doesn't Fit:
When we need to store the collection long-term (Span is stack-only)
When working with async methods (Span can't cross async boundaries)
Cloud-native support (OpenTelemetry, Profiling) (.NET 9)
Imagine we're an astronomer managing a network of telescopes (our microservices) scattered across mountains (cloud regions). We need to track each telescope's performance, see how they communicate, and get alerts when something goes wrong. .NET's cloud-native tools like OpenTelemetry are our control room monitors!
When It Fits:
When building distributed systems with multiple services
When we need end-to-end tracing across service boundaries
When monitoring performance and diagnosing issues in production
When It Doesn't Fit:
For very simple, single-process applications with no need for monitoring
The Raw Truth About OpenTelemetry in .NET 9
OpenTelemetry has existed since .NET 5 (via NuGet)
Core tracing APIs (Activity, DiagnosticSource) existed even before OTel
Microsoft's own Application Insights used these same APIs
Microsoft wants to push Azure Monitor adoption
Still Needs NuGets: Absolutely zero OTel code ships in .NET runtime
Real Advancements: Slightly simpler Azure setup, Better AOT support (but instrumentation still needs reflection)
Full Stack Software Engineer | Agile | AWS | .Net core, Angular, React, Java -Spring Boot | kubernetes, MicroService, Docker, Solution Architect | ERP and Voice technologies like: CUCM, UCCX, Asterisk, Sip
4moSimple and short summary. Great effort.
Full Stack Engineer | C# | .NET Core | JavaScript | Angular | React | Microservices | SQL | Azure
4moGreat summary! It's exciting to see how .NET and C# continue to evolve—features like Primary Constructors and Native OpenAPI support are real game-changers. Looking forward to diving deeper into .NET 9 and seeing how these updates enhance real-world development. 🚀
.NET | .NET Core | Sql Server | Remote-Ready | Azure | Microservice | Docker | Kubernetes | Angular | React | React Native | JavaScript | DevOps | WordPress
4moGood to know
SQA Lead at IT Magnet | ISTQB | CEH | CHFI | Azure | AI | QA Automation | API Test | Talent Acquisition | Teacher | Mentor
4moNice