Skip to Content
All posts

.NET 8 Updates

8 min read ·  — #dotnet#dotnet-8

.NET 8 Updates

JSON Improvements

Missing member handling

It’s now possible to configure object deserialization behavior, whenever the underlying JSON payload includes properties that cannot be mapped to members of the deserialized POCO type. This can be controlled by setting a JsonUnmappedMemberHandling value, either as an annotation on the POCO type itself, globally on JsonSerializerOptions or programmatically by customizing the JsonTypeInfo contract for the relevant types:

JsonSerializer.Deserialize<MyPoco>("""{"Id" : 42, "AnotherId" : -1 }""");

// JsonException : The JSON property 'AnotherId' could not be mapped
// to any .NET member contained in type 'MyPoco'.

[JsonUnmappedMemberHandling(JsonUnmappedMemberHandling.Disallow)]
public class MyPoco
{
   public int Id { get; set; }
}

Benefits

Overall, JsonUnmappedMemberHandling provides you with a level of control and flexibility when dealing with missing or unexpected members during JSON deserialization, ensuring that your application can handle such scenarios gracefully.

Interface hierarchy support

System.Text.Json now supports serializing properties from interface hierarchies:

IDerived value = new Derived { Base = 0, Derived =1 };

JsonSerializer.Serialize(value); // {"Base":0,"Derived":1}

public interface IBase
{
    public int Base { get; set; }
}

public interface IDerived : IBase
{
    public int Derived { get; set; }
}

public class Derived : IDerived
{
    public int Base { get; set; }
    public int DerivedProp { get; set; }
}

Benefits

The support for serializing properties from interface hierarchies in System.Text.Json provides benefits such as increased flexibility and extensibility in designing and implementing data models, allowing for more modular and reusable code. It also simplifies the serialization process by automatically including properties defined in interfaces, reducing the need for manual mapping or custom serialization logic.

JsonSerializerOptions.MakeReadOnly() and JsonSerializerOptions.IsReadOnly APIs

The JsonSerializerOptions class has always been using freezable semantics, but up until now freezing could only be done implicitly by passing the instance to one of the JsonSerializer methods. The addition of the new APIs makes it possible for users to explicitly control when their JsonSerializerOptions instance should be frozen:

public class MySerializer
{
  private JsonSerializerOptions Options { get; }

  public MySerializer()
  {
        Options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
        {
          Converters =
          {
            new MyCustomConverter()
          }
        };

        // Make read-only before exposing the property.
        Options.MakeReadOnly();
  }
}

Benefits

The JsonSerializerOptions.IsReadOnly property allows you to check whether a JsonSerializerOptions instance is read-only, providing a convenient way to determine if the options can be modified or if they are in a finalized state. These APIs promote code stability, enhance security, and help prevent accidental modifications to serialization settings, ensuring consistent and predictable behavior throughout your application.

System.ComponentModel.DataAnnotations Extensions

RequiredAttribute.DisallowAllDefaultValues

The RequiredAttribute now allows validating that structs do not equal their default values.

For example:

[Required(DisallowAllDefaultValues = true)]
public Guid MyGuidValue { get; set; }

This example will fail validation if its value equals Guid.Empty.

RangeAttribute exclusive bounds

Users can now specify exclusive bounds in their range validation:

[Range(0d, 1d, MinimumIsExclusive = true, MaximumIsExclusive = true)]
public double Sample { get; set; }

This attribute accepts any values in the open interval but rejects the boundary values 0 and 1.

LengthAttribute

The LengthAttribute can now be used to set both lower and upper bounds for strings or collections:

[Length(10, 20)] // Require at least 10 elements and at most 20 elements.
public ICollection<int> Values { get; set; }

AllowedValuesAttribute and DeniedValuesAttribute

These attributes can be used to specify allow lists and deny lists for validating a property:

[AllowedValues("apple", "banana", "mango")]
public string Fruit { get; set; }

[DeniedValues("pineapple", "anchovy", "broccoli")]
public string PizzaTopping { get; set; }

SDK: Enhancements to Metrics APIs

Dependency Injection (DI) Friendly metrics APIs

Introduced in Preview 5 and brings a number of improvements and updates to metrics APIs that covers additional use cases. New IMeterFactory interface, which can be registered in DI containers and used to create Meter objects in an isolated manner.

// service is the DI IServiceCollection
// Register the IMeterFactory to the DI container
// using the default meter factory implementation.
services.AddMetrics();

Consumers can now use the code below to create a meter factory and use it to easily create a new Meter object.

var meterFactory = serviceProvider.GetRequiredService<IMeterFactory>();

MeterOptions options = new MeterOptions("MeterName")
{
   Version = "version",
};

Meter meter = meterFactory.Create(options);

Benefits

Dependency Injection (DI) friendly metrics APIs provide benefits such as improved testability, modularity, and flexibility in monitoring and measuring application performance. By using DI to inject metric dependencies, it becomes easier to replace or mock metrics implementations during testing, decoupling metric collection from the business logic and promoting separation of concerns.

IExceptionHandler

The exception handler middleware is an existing component used to catch un-expected request processing exceptions and return a user-friendly error page without leaking implementation details. IExceptionHandler is a new interface for services that can be resolved and invoked by the exception handler middleware. It gives the developer a callback that allows handling known exceptions in a central location.

IExceptionHandler‘s are registered by calling IServiceCollection.AddExceptionHandler<T>. Multiple can be added, and they’ll be called in the order registered. If an exception handler handles a request, it can return true to stop processing. If an exception isn’t handled by any exception handler, then control falls back to the old behavior and options from the middleware. Different metrics and logs will be emitted for handled versus unhandled exceptions.

public interface IExceptionHandler
{
    ValueTask<bool> TryHandleAsync(
    HttpContext httpContext,
    Exception exception,
    CancellationToken cancellationToken);
}

Benefits

The introduction of the new IExceptionHandler interface in .NET 8 provides benefits by allowing developers to have fine-grained control over exception handling and customization in their applications. It enables the implementation of custom exception handling logic, such as logging, error reporting, or recovery strategies, providing a centralized and extensible approach to handle exceptions consistently across the application.

API authoring

Support for generic attributes

Attributes that previously required a System.Type parameter are now available in cleaner generic variants. This is made possible by support for generic attributes in C# 11. For example, the syntax for annotating the response type of an action can be modified as follows:

[ApiController]
[Route("api/[controller]")]
public class TodosController : Controller
{
  [HttpGet("/")]
  // Before: [ProducesResponseType(typeof(Todo), StatusCodes.Status200OK)]
  [ProducesResponseType<Todo>(StatusCodes.Status200OK)]
  public Todo Get() => new Todo(1, "Write a sample", DateTime.Now, false);
}

Generic variants are supported for the following attributes:

  • [ProducesResponseType]
  • [Produces]
  • [MiddlewareFilter]
  • [ModelBinder]
  • [ModelMetadataType]
  • [ServiceFilter]
  • [TypeFilter]

Benefits

The support for generic attributes in API authoring in .NET 8 brings benefits by enabling the creation of highly customizable and type-safe attribute-based metadata annotations for APIs. This allows for more flexible and reusable code, promotes better code organization, and simplifies the implementation of cross-cutting concerns such as validation, authorization, and configuration.

Introducing the options validation source generator

To reduce startup overhead and improve validation feature set in .NET 8 was introduced the source code generator that implements the validation logic.

Options validation usage

public class FirstModelNoNamespace
{
    [Required]
    [MinLength(5)]
    public string P1 { get; set; } = string. Empty;

    [Microsoft.Extensions.Options
    .ValidateObjectMembers(typeof(SecondValidatorNoNamespace))]
    public SecondModelNoNamespace? P2 { get; set; }

    [Microsoft.Extensions.Options.ValidateObjectMembers]
    public ThirdModelNoNamespace? P3 { get; set; }
}

public class SecondModelNoNamespace
{
    [Required]
    [MinLength(5)]
    public string P4 { get; set; } = string. Empty;
}

public class ThirdModelNoNamespace
{
    [Required]
    [MinLength(5)]
    public string P5 { get; set; } = string.Empty;
}

[OptionsValidator]
public partial class FirstValidatorNoNamespace
: IValidateOptions<FirstModelNoNamespace>
{
}

[OptionsValidator]
public partial class SecondValidatorNoNamespace
: IValidateOptions<SecondModelNoNamespace>{}

If the app is using dependency injection, it can inject the validation using the following pattern.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();
builder.Services.Configure<FirstModelNoNamespace>(builder.Configuration
                .GetSection(...));

builder.Services
       .AddSingleton<IValidateOptions<FirstModelNoNamespace>,
        FirstValidatorNoNamespace>();

builder.Services
       .AddSingleton<IValidateOptions<SecondModelNoNamespace>,
        SecondValidatorNoNamespace>();

Benefits

The options validation source generator in .NET 8 provides automated validation of configuration options at compile-time, ensuring early detection of errors and reducing runtime issues caused by misconfigured options. This leads to improved code reliability, faster debugging, and enhanced developer productivity by eliminating manual validation and providing clear error messages.

Things we’ve missed

Missed things from Preview 1 & 2

  • Native AOT; -.NET Container images;
  • Utility methods for working with randomness (GetItems, Shuffle);
  • New performance-focused types in the core libraries (System.Collections.Frozen, IndexOfAny);
  • Blazor updates;
  • HTTP/3 enabled by default;
  • HTTP/2 over TLS (HTTPS) support on macOS;
  • IPNetwork.Parse and TryParse;
  • New analyzer to detect multiple FromBody attributes;
  • New APIs in ProblemDetails to support more resilient integrations;
  • New IResettable interface in ObjectPool

Missed things from Preview 3 & 4

  • ASP.NET Core support for native AOT;
  • Blazor and Razor updates;
  • NuGet: Auditing package dependencies for security vulnerabilities;
  • Template Engine: secure experience with packages from Nuget.org;
  • Libraries: UTF8 improvements;
  • Introduced Time abstraction;
  • API authoring: expanded support for form binding in minimal APIs;
  • Authentication and Authorization: Identity API endpoints & improved support for custom authorization policies with IAuthorizationRequirementData

Missed things from Preview 5 & 6

  • Servers & middleware: IHttpSysRequestTimingFeature;
  • SignalR seamless reconnect;
  • Blazor updates;
  • Authentication and authorization: authentication updates in ASP.NET Core SPA templates;
  • Testing metrics in ASP.NET Core apps (MetricCollector);
  • Stream-based ZipFile CreateFromDirectory and ExtractToDirectory method overloads;
  • Expanding LoggerMessageAttribute Constructor Overloads for Enhanced Functionality.