Back

.NET 7 indifference by Grumpy Old Git 

.NET 7 indifference by Grumpy Old Git 

What do Apple and Samsung have to do with .NET? 

The .NET team has been doing great work with unifying various flavours of .NET into .NET 5/6, so it seems that .NET 7 is a performance release. To be fair, .NET is a stable, mature framework which cares about learning curves and not breaking programmers’ brains with breaking changes so performance is the one area where they can give real value to customers.  

But my experience was similar to attending a new flagship mobile phone release. You’re getting all giddy with thoughts on what new, cool, revolutionary feature will you see today, coming up with ridiculous ideas like inbuilt 1080p projectors in phones. You purposefully skip any sneak peeks, rumours and beta versions to not spoil your initial joy. Then the presenter gets on stage, and you get the same phone with a slightly bigger screen, a bit better battery and a higher CPU clock. It’s all good and better than before, but you’re left with a bit of sadness because you expected to have your mind blown.  

I had a similar experience reading through the official release of .NET 7.

Behind the scenes

There are a lot of improvements to speed, consumption, cross-platform support and various other sections of the .NET framework. Seems a lot of it was targeted at ARM64 support (my optimistic self is wondering if Microsoft is working towards an ARM64 version of Windows). This will allow our code to run better without premature optimization, which we know is the root of all evil, right alongside estimations. Think of trees with ground and air roots. One pushes evil from the bottom, the other dripping it from the top.

There’s a minimal .NET docker container image that can be used out of the box in Ubuntu Jammy and built-in Container Support. Just add the Microsoft.NET.Build.Container package, run this command and it should work.

dotnet publish –-os linux –-arch x64 -p:PublicProfile=DefaultContainer

I’m not holding my breath. Docker is such a toddler with a tendency to misbehave given the slightest provocation. 


Better MAUI and Blazor are also included, but I’ll leave the judgement on those improvements to someone who is actually using them for production-level code (is there even someone like that out there?) 

As a warning, .NET 7 is not considered a long-term support version so beware of what might get dropped in .NET 8. Some more performance optimizations might prove erroneous down the road. What we know for now is that for loop has worse performance in .NET 7 than in .NET 6.  The fix will be included in .NET 8.  

Upgrading from 6 to 7 seems to work pretty straightforwardly so far. If some package is locked to .NET 6 you can use DOTNET_ROLL_FORWARD: major as e.g. environment variable, which will most likely fix that. 

Code level changes (Mix of C# 11 and .NET 7) 

There are some improvements, some technical debt refinancing and some new syntax sugar. 

Generic Math 

The built-in number types now implement INumber<TSelf> interface which in turn implements a number of interfaces you might expect from number types, like IAdditionOperators<TSelf, TSelf, TSelf> or IUnaryPlusOperators<TSelf, TSelf>. I’m struggling to come up with a non-academic example where this will come in handy, but I’m not against it, it looks like something I might get some mileage from in the future, but even their example is redundant.  

static T Add<T>(T left, T right) where T : INumber<T> 
{ 
    return left + right; 
} 

I guess it could be used deep in some libraries, weird arithmetics or for code brevity when doing e.g. Matrix operations. We’ll see. 

CLI parser and tab 

This is a nice one. When doing things from CLI you can use a tab to autocomplete command parameters. No more typing –-exclude-launch-settings, just –-exc and hitting a tab key will autocomplete with cycle-through possibilities. 

Nuget 

Another short and sweet one. You can have a solution-level props file that defines the version for packages, and then all the projects with that package will use the version defined in the props file. 

<Project> 
  <PropertyGroup> 
    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> 
  </PropertyGroup> 

  <ItemGroup> 
    <PackageVersion Include="Newtonsoft.Json" Version="13.0.1" /> 
  </ItemGroup> 
</Project> 
<Project Sdk="Microsoft.NET.Sdk"> 
  <PropertyGroup> 
    <TargetFramework>net6.0</TargetFramework> 
  </PropertyGroup> 

  <ItemGroup> 
    <PackageReference Include="Newtonsoft.Json" /> 
  </ItemGroup> 
</Project> 

System.Text.Json 

Polymorphism support. Looks weird and is unintuitive. See it in action 

using System.Text.Json; 
using System.Text.Json.Serialization; 

var c1 = new C1() { P1 = 1 }; 
var c2 = new C2() { P2 = 2 }; 

var s1 = JsonSerializer.Serialize<B>(c1); 
var s2 = JsonSerializer.Serialize<B>(c2); 

var ds1 = (C1)JsonSerializer.Deserialize<B>(s1)!; 
var ds2 = (C2)JsonSerializer.Deserialize<B>(s2)!; 

Console.WriteLine($"s1: {s1}, ds1.P1: {ds1.P1}"); 
Console.WriteLine($"s2: {s2}, ds2.P2: {ds2.P2}"); 

[JsonDerivedType(typeof(C1), "I am C1")] 
[JsonDerivedType(typeof(C2), "I am C2")] 

public abstract class B { } 

public class C1 : B 
{ 
    public int P1 { get; set; } 
} 

public class C2 : B 
{ 
    public int P2 { get; set; } 
} 

With output of

s1: {"$type":"I am C1","P1":1}, ds1.P1: 1
s2: {"$type":"I am C2","P2":2}, ds2.P2: 2

While it works as intended, this code is something I wouldn’t like to read. My main issue is the need to serialize and deserialize through the base class. I’ll probably get used to it, but it’s going to take some active thought.  

Dynamic deserialization 

You can dynamically change the serialization of properties based on other properties. Like excluding them, outputting some other property and various other possibilities. 

using System.Text.Json; 
using System.Text.Json.Serialization; 
using System.Text.Json.Serialization.Metadata; 

var existingPerson = new Person() 
{ 
    Name = "Nietzsche", 
    IsDeleted = false 
}; 

var deletedPerson = new Person() 
{ 
    Name = "God", 
    IsDeleted = true 
}; 

Console.WriteLine($"Existing: {JsonSerializer.Serialize(existingPerson, JsonExtensions.Options)}"); 

Console.WriteLine($"Deleted: {JsonSerializer.Serialize(deletedPerson, JsonExtensions.Options)}"); 


public class Person 
{ 
    public string Name { get; set; } = default!; 
    public bool IsDeleted { get; set; } 
} 

public static class JsonExtensions 
{ 
    public static readonly JsonSerializerOptions Options = new JsonSerializerOptions 
    { 
        TypeInfoResolver = new DefaultJsonTypeInfoResolver 
        { 
            Modifiers = 
            { 
                typeInfo => 
                { 
                    if( typeInfo.Type == typeof(Person)) 
                    { 
                        var namePi = typeInfo.Properties.First(t => t.Name == nameof(Person.Name)); 
                        var deletedPi = typeInfo.Properties.First(t => t.Name == nameof(Person.IsDeleted)); 

                        namePi.Get = (obj) => 
                        { 
                            var person = obj as Person; 
                            if(person!.IsDeleted) 
                                return "Deleted"; 
                            return person.Name; 
                        }; 
                    } 
                } 
            } 
        } 
    }; 
} 

With output of

Existing: {"Name":"Nietzsche","IsDeleted":false} 
Deleted: {"Name":"Deleted","IsDeleted":true}

Static abstract members 

Maybe easiest to explain with a few lines of code, but the point of this feature is that you won’t have to instantiate a class to get to something that is rightfully a static property on the interface. 

public interface IVehicle 
{ 
    static abstract int NoOfWheels(); 
} 

public class Bus : IVehicle 
{ 
    public static int NoOfWheels() 
    { 
        return 6; 
    } 
} 

Random bits and pieces

  1. At least three double quotes (“””) at the start and the same number of quotes at the end enable you to write whatever you like between them. Special characters without escaping, quotes, new lines and similar headache-inducing things. Cool! 
  1. Finally. All Microsoft.Extensions libraries now contain the nullability reference type checks! 
  1. Basically, if you use in-memory caching, which by the way you totally should (e.g. two-level cache, first level in memory and then redis, try it out, thank me later) you can easily get stats on hits, misses, size etc… 
  2. Pattern matching supports strings 
  3. Type converters tor DateOnly, TimeOnly, Int128, UInt128 and Half are now exposed. Seems overly complicated for such a feature. There’ll probably be some syntax sugar in later releases. 

TypeConverter dateOnlyConverter = TypeDescriptor.GetConverter(typeof(DateOnly)); 

DateOnly? date = dateOnlyConverter.ConvertFromString("1940-10-09") as DateOnly?; 

Is it even worth mentioning? 

  1. Microseconds and nanoseconds are added to TimeStamp, DateTime, DateTimeOffset and TimeOnly. I guess there were people that did calculations on ticks to get nanoseconds and I guess whoever needed these, added extension methods a long time ago, so not sure who the audience is for this change, but better to have it and not use it, then need it and have to write 4 lines of code to handle it. 
  1. You can handle Tar libraries using Microsoft provided library. Eh… 
  1. HttpClient now has methods for doing patch requests. No more sending HttpRequestMessage(HttpMethod.Patch, …) Just use _httpClient.PatchAsync
  1. Order and OrderDescending methods to remove the need to write x => x in OrderBy 
  1. Instead of writing static Regex with Compiled flag, you can use the [RegexGenerator] flag. 

Conclusion

The part that dominates this release is speed and cross-platform support. The blow your mind with how easy it is to code some things is lacking, and we hope to see it in .NET 8! 

Back
Do you have a project you need help with?
Get in Touch

By using this website, you agree to our use of cookies. We use cookies to provide you with a great experience and to help our website run effectively.

Accept