Migrate from Arc4u 8.x to 9
This guide is for teams that run an application on Arc4u 8.x (the last version is 8.3.2) and want to move it to Arc4u 9. It lists every breaking change with the 8.x code, the Arc4u 9 code and the steps to follow. Work through the sections in order: the first three (packages, target framework, JSON) apply to every application, the others only if you use the feature.
The Arc4u 9 code in this guide is the code on the develop/9.0.0 branch. For the complete list of
changes per version, see the changelog.
Before you start
- [ ] Install the .NET 10 SDK or later. Arc4u 9 targets
net10.0andnet11.0only (see Target frameworks). - [ ] Upgrade to Arc4u 8.3.2 first and fix its build warnings. Several APIs removed in 9 are
already marked
[Obsolete]in 8.x and the warning tells you what to use instead. - [ ] Work on a branch, and back up the configuration files (
appsettings*.json) and the Kubernetes or deployment manifests that hold Arc4u settings. Several configuration keys change. - [ ] Read the changelog entry for 9.0.0.
- [ ] Plan to clear distributed caches (Redis, SQL Server, Dapr state) when you deploy if you used the Protobuf serializer (see ADAL and Protobuf removed).
Warning
Arc4u 9 is in preview: install the packages with --prerelease. The last preview on NuGet,
9.0.0-preview37, was built before part of the changes described here, and it targets net8.0,
net9.0 and net10.0. If you migrate to that preview, check this list:
- Already in preview37:
AddHybridAuthentication;AddOidcAuthenticationregistering OpenID Connect and cookies only, without JWT bearer;AddClientTokensandClientTokensSectionPath; theAuthenticationMethod,NameClaimTypeandRoleClaimTypesettings;DataProtectionCertificate;JwtHttpHandler<T>; the removal of the*EventsTypekeys; theIReadOnlyCollection<IError>parameter ofOnFailed. - Not yet in preview37: the settings are still named
OpenIdandOAuth2, the bearerAuthenticationTypeis stillOAuth2Bearer, theAuthenticationTypeandSettingsKeyskeys are still read,IClaimsFiller.GetAsyncstill has the 8.x signature, and the…Asyncresult extension methods on a plainResultstill return synchronously.
Because AddOidcAuthentication no longer registers JWT bearer, an 8.x application that keeps
calling it stops accepting bearer tokens without any error at startup: switch to
AddHybridAuthentication (see Registration methods).
Package renames
The .Standard part of the package names referred to .NET Standard. Arc4u 9 no longer targets
.NET Standard (#134), so every
Arc4u.Standard.* package is published under a new ID without .Standard. The namespaces do not
change: Arc4u 8.x already used Arc4u.* namespaces, so most using directives stay as they are.
Before (8.x)
<ItemGroup>
<PackageReference Include="Arc4u.Standard.Caching.Memory" Version="8.3.2" />
<PackageReference Include="Arc4u.Standard.OAuth2.AspNetCore.Authentication" Version="8.3.2" />
</ItemGroup>
After (9)
<ItemGroup>
<PackageReference Include="Arc4u.Caching.Memory" Version="9.0.0-preview37" />
<PackageReference Include="Arc4u.OAuth2.AspNetCore.Authentication" Version="9.0.0-preview37" />
</ItemGroup>
Steps:
- Replace each package ID with the new one from the table below. The old IDs stop at 8.3.2 and have no 9 version.
- Keep the same version for every Arc4u package: the packages depend on each other.
- The assembly names change with the package IDs (
Arc4u.Standard.Caching.Memory.dllbecomesArc4u.Caching.Memory.dll). Update every configuration value that names an Arc4u assembly, for example the assembly-qualified type names in theApplication.Dependencysection ("Arc4u.Caching.Memory.MemoryCache, Arc4u.Standard.Caching.Memory"becomes"Arc4u.Caching.Memory.MemoryCache, Arc4u.Caching.Memory"). - In a Blazor application, update the static web asset paths:
_content/Arc4u.Standard.OAuth2.Blazor/becomes_content/Arc4u.OAuth2.Blazor/.
| Old package | New package |
|---|---|
Arc4u.Standard |
Arc4u |
Arc4u.Standard.AspNetCore.gRpc |
Arc4u.AspNetCore.gRpc |
Arc4u.Standard.Authorization |
Arc4u.Authorization |
Arc4u.Standard.Caching |
Arc4u.Caching |
Arc4u.Standard.Caching.Dapr |
Arc4u.Caching.Dapr |
Arc4u.Standard.Caching.Memory |
Arc4u.Caching.Memory |
Arc4u.Standard.Caching.Redis |
Arc4u.Caching.Redis |
Arc4u.Standard.Caching.SqlServer |
Arc4u.Caching.SqlServer |
Arc4u.Standard.Configuration |
Arc4u.Configuration |
Arc4u.Standard.Configuration.Decryptor |
Arc4u.Configuration.Decryptor |
Arc4u.Standard.Configuration.Store |
Arc4u.Configuration.Store |
Arc4u.Standard.Core |
Arc4u.Core |
Arc4u.Standard.Dependency |
Arc4u.Dependency |
Arc4u.Standard.Diagnostics |
Arc4u.Diagnostics |
Arc4u.Standard.Diagnostics.Serilog |
Arc4u.Diagnostics.Serilog |
Arc4u.Standard.Diagnostics.Serilog.Sinks.RealmDb |
Arc4u.Diagnostics.Serilog.Sinks.RealmDb |
Arc4u.Standard.Dispatcher |
Arc4u.Dispatcher |
Arc4u.Standard.EfCore |
Arc4u.EfCore |
Arc4u.Standard.FluentValidation |
Arc4u.FluentValidation |
Arc4u.Standard.gRPC |
Arc4u.gRPC |
Arc4u.Standard.MongoDB |
Arc4u.MongoDB |
Arc4u.Standard.OAuth2 |
Arc4u.OAuth2 |
Arc4u.Standard.OAuth2.AspNetCore |
Arc4u.OAuth2.AspNetCore |
Arc4u.Standard.OAuth2.AspNetCore.Authentication |
Arc4u.OAuth2.AspNetCore.Authentication |
Arc4u.Standard.OAuth2.AspNetCore.Blazor |
Arc4u.OAuth2.AspNetCore.Blazor |
Arc4u.Standard.OAuth2.Blazor |
Arc4u.OAuth2.Blazor |
Arc4u.Standard.OAuth2.Client |
Arc4u.OAuth2.Client |
Arc4u.Standard.OData |
Arc4u.OData |
Arc4u.Standard.Results |
Arc4u.Results |
Arc4u.Standard.Serializer |
Arc4u.Serializer |
Arc4u.Standard.Serializer.JSon |
Arc4u.Serializer.JSon |
Arc4u.Standard.Threading |
Arc4u.Threading |
Arc4u.AspNetCore.Results and Arc4u.Configuration.Store.EfCore keep their names. The
following 8.x packages have no Arc4u 9 version:
| 8.x package | What to do |
|---|---|
Arc4u.Standard.Dependency.ComponentModel |
IContainer replaced by keyed services |
Arc4u.Standard.OAuth2.AspNetCore.Adal, Arc4u.Standard.Serializer.Protobuf, Arc4u.Standard.Serializer.ProtobufV2 |
ADAL and Protobuf removed |
Arc4u.Standard.Diagnostics.TraceListeners |
TraceListeners removed |
Arc4u.Standard.OAuth2.Msal |
MSAL status |
Arc4u.Standard.NServiceBus, Arc4u.Standard.NServiceBus.Core, Arc4u.Standard.NServiceBus.RabbitMQ |
NServiceBus replaced by Dapr pub/sub |
Arc4u.Prism.DI.Wpf |
Prism.DI.Wpf replaced by Prism.DryIoc |
Arc4u.Standard.OAuth2.AspNetCore.Api |
Other removed APIs |
Arc4u.Standard.Data |
Arc4u.Data and change tracking removed |
Arc4u 9 also adds packages that have no 8.x equivalent: Arc4u.Dependency.Tool (source
generators for dependency registration), Arc4u.OAuth2.Client.Authentication (OpenID Connect
for desktop and mobile clients) and Arc4u.AspNetCore.Versioning (not published on NuGet yet).
The package support matrix lists the status of every package.
Target frameworks
Arc4u 8.3.x targeted netstandard2.0, net8.0 and net9.0 (ASP.NET Core packages: net8.0
and net9.0). Arc4u 9 drops .NET Standard and the .NET versions that reach end of support
(#134). It targets:
| Packages | Target frameworks |
|---|---|
| All Arc4u 9 packages, except the ones below | net10.0, net11.0 |
Arc4u.Dependency |
netstandard2.0, net10.0, net11.0 |
Arc4u.Dependency.Tool |
netstandard2.0 (a Roslyn source generator, it runs inside the compiler) |
Arc4u.AspNetCore.Versioning |
net10.0 |
Before (8.x)
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
After (9)
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
Steps:
- Set
TargetFrameworktonet10.0(ornet11.0) in every project that references Arc4u, including class libraries that targetednetstandard2.0to share code with Arc4u 8.x. - Update the other Microsoft packages of the project (
Microsoft.AspNetCore.*,Microsoft.Extensions.*, Entity Framework Core) to the matching major version. - Update the base image of your containers (for example
mcr.microsoft.com/dotnet/aspnet:10.0).
An application that must stay on .NET 8 or .NET 9 stays on Arc4u 8.3.2.
Newtonsoft.Json replaced by System.Text.Json
Arc4u 9 no longer depends on Newtonsoft.Json (#137).
In 8.x, Arc4u.Standard.Dependency referenced Newtonsoft.Json, so every Arc4u application could
use it without referencing it. Arc4u types that were serialized with Newtonsoft.Json attributes,
such as UserProfile, now use System.Text.Json attributes. The string helpers that Arc4u added to
DataContractSerializer, DataContractJsonSerializer and XmlSerializer (WriteObject(graph, out string),
ReadObject(string), Serialize, Deserialize) are removed as well.
Before (8.x)
using Newtonsoft.Json;
public static class OrderJson
{
// Compiles without a Newtonsoft.Json reference: Arc4u.Standard.Dependency brings it.
public static string Serialize(int orderId) => JsonConvert.SerializeObject(new { OrderId = orderId });
}
After (9)
using System.Text.Json;
public static class OrderJson
{
public static string Serialize(int orderId) => JsonSerializer.Serialize(new { OrderId = orderId });
}
Steps:
- Build the solution. Errors
CS0246onNewtonsoft.Jsontypes show the code that relied on the Arc4u reference. - Either port that code to System.Text.Json (see Microsoft's
migration guide),
or add an explicit reference:
dotnet add package Newtonsoft.Json. - If you stored Arc4u objects serialized with Newtonsoft.Json (for example in a cache), check that System.Text.Json reads them, or clear them.
- If you serialized Arc4u types with
DataContractSerializerorDataContractJsonSerializer(for example in a WCF or CoreWCF contract), serialize them with System.Text.Json instead.TokenInfo,ClaimDto,MessageCategory,UserProfile,Authorization,Operation,ScopedRoles,ScopedOperations,Graph<T>,Bound<T>,Interval<T>,Period,BoundTypeandBoundDirectionno longer carry[DataContract],[DataMember]or[EnumMember]attributes (#278). Without them, theDataContractSerializerloses the members that have no public setter, such as the include paths of aGraph<T>and the bounds of anInterval<T>. Data written by theDataContractSerializercannot be read back with System.Text.Json: clear it or convert it.
ADAL and Protobuf removed
Arc4u 9 removes the packages built on deprecated libraries (#130):
Arc4u.Standard.OAuth2.AspNetCore.Adalused ADAL (Microsoft.IdentityModel.Clients.ActiveDirectory), which Microsoft no longer supports. It targetednet6.0only and its last stable version is 8.2.1.Arc4u.Standard.Serializer.ProtobufandArc4u.Standard.Serializer.ProtobufV2providedIObjectSerializationimplementations based on protobuf-net. Their last stable version is 8.2.1 andProtoBufSerializationis marked[Obsolete("Use Arc4u.Serializer.JSon instead.")].
Before (8.x)
using Arc4u.OAuth2.Extensions;
using Arc4u.Serializer;
// Arc4u.Standard.Serializer.ProtobufV2
builder.Services.AddSingleton<IObjectSerialization, ProtoBufSerialization>();
// Arc4u.Standard.OAuth2.AspNetCore.Adal
var adalOptions = new AdalAuthenticationOptions
{
// ...
};
builder.Services.AddOpenIdBearerAuthentication(adalOptions);
After (9)
using Arc4u.OAuth2.Extensions;
using Arc4u.Serializer;
// Arc4u.Serializer.JSon
builder.Services.AddSingleton<IObjectSerialization>(_ => new JsonSerialization());
// Arc4u.OAuth2.AspNetCore.Authentication: OpenID Connect and JWT bearer, read from the Authentication section
builder.Services.AddHybridAuthentication(builder.Configuration);
Steps:
- Replace the ADAL registration with the Arc4u 9 authentication of the
server authentication guide:
AddHybridAuthenticationfor a web application that signs users in and calls APIs,AddJwtAuthenticationfor an API. It works with Microsoft Entra ID, ADFS and other OpenID Connect providers. - Register JsonSerialization wherever you registered
ProtoBufSerialization. To keep a named serializer that a cache selects withSerializerName, register it as a keyed service (AddKeyedSingleton<IObjectSerialization>("<name>", ...)). - Clear the distributed caches that stored Protobuf data, or give the caches new names, before the new version starts: the JSON serializer cannot read entries written by the Protobuf serializer.
TraceListeners removed
Arc4u.Standard.Diagnostics.TraceListeners wrote Arc4u logs to System.Diagnostics trace
listeners (#133). It targeted netstandard2.0,
its last stable version is 8.2.1 and TraceLoggerProvider is marked [Obsolete("Use Serilog")].
Arc4u 9 logs through Microsoft.Extensions.Logging: send the logs to Serilog (with
Arc4u.Diagnostics.Serilog) or OpenTelemetry.
Before (8.x)
using Arc4u.Diagnostics;
builder.Logging.AddProvider(new TraceLoggerProvider());
After (9)
using Serilog;
// Packages: Serilog.Extensions.Hosting, Serilog.Settings.Configuration
builder.Services.AddSerilog((services, loggerConfiguration) =>
loggerConfiguration.ReadFrom.Configuration(builder.Configuration));
Steps:
- Remove the
Arc4u.Standard.Diagnostics.TraceListenerspackage and theTraceLoggerProviderregistration, and the trace listener configuration inapp.configorweb.config. - Configure Serilog or OpenTelemetry as described in the diagnostics guide.
MSAL status
Arc4u 9 does not ship MSAL support (#135).
Arc4u.Standard.OAuth2.Msal (MsalTokenProvider and PublicClientApp, for desktop clients) has
no 9 package: its last version is 8.3.2, and its source code is removed from the repository.
| 8.x client | Arc4u 9 |
|---|---|
Desktop or mobile client with MsalTokenProvider |
Arc4u.OAuth2.Client.Authentication: OpenID Connect with Duende.IdentityModel.OidcClient |
Blazor WebAssembly with BlazorMsalTokenProvider |
BlazorMsalTokenProvider is still in Arc4u.OAuth2.Blazor |
Before (8.x)
using Arc4u.Dependency.ComponentModel;
using Arc4u.OAuth2.Msal.TokenProvider.Client;
using Arc4u.OAuth2.Token;
using Microsoft.Identity.Client;
public static class ClientAuthentication
{
public static void Configure(IServiceCollection services)
{
var publicClient = PublicClientApplicationBuilder.Create("<client-id>")
.WithAuthority("https://login.microsoftonline.com/<tenant-id>")
.WithDefaultRedirectUri()
.Build();
var container = new ComponentModelContainer(services);
container.RegisterInstance(new PublicClientApp { PublicClient = publicClient });
container.Register<ITokenProvider, MsalTokenProvider>(MsalTokenProvider.ProviderName);
}
}
After (9)
using Arc4u.OAuth2.Extensions;
using Duende.IdentityModel.OidcClient.Browser;
public static class ClientAuthentication
{
// browser: your IBrowser implementation that shows the identity provider's sign-in page.
public static void Configure(IServiceCollection services, IConfiguration configuration,
IBrowser browser, ILoggerFactory loggerFactory)
{
services.AddOidcClientAuthentication(browser, loggerFactory, configuration);
}
}
Steps:
- Replace
Arc4u.Standard.OAuth2.MsalwithArc4u.OAuth2.Client.Authentication. - Implement
IBrowserfor your UI technology and move the client settings to theAuthenticationsection, as described in the client authentication guide.
Note
BlazorTokenProvider and BlazorMsalTokenProvider are both registered under the provider name
blazor: register only one of them in an application.
NServiceBus replaced by Dapr pub/sub
Arc4u.Standard.NServiceBus, Arc4u.Standard.NServiceBus.Core and
Arc4u.Standard.NServiceBus.RabbitMQ are not continued in Arc4u 9 and are deprecated on NuGet.org
(#135, #189, #272).
They targeted net8.0 only and depended on NServiceBus 7.8. Arc4u 9 has no messaging package:
use Dapr pub/sub
directly with the Dapr.AspNetCore package. The broker (RabbitMQ, Azure Service Bus, Kafka and
others) is then a Dapr component that you change without touching the code.
| NServiceBus with Arc4u 8.x | Dapr |
|---|---|
HandleMessageBase<T>.Handle(T) |
An endpoint with a [Topic] attribute that receives the event |
MessagesToPublish.Add(event) |
DaprClient.PublishEventAsync(pubsubName, topic, event) |
SenderEndpointConfigBase and ReceiverEndpointConfigurationBase (transport and routing) |
A pub/sub component file (YAML) |
Command sent with MessagesToPublish.Add(command) |
A dedicated topic, or Dapr service invocation |
Before (8.x)
using Arc4u.NServiceBus;
public sealed record OrderPlaced(int OrderId);
public sealed record InvoiceRequested(int OrderId);
public sealed class OrderPlacedHandler : HandleMessageBase<OrderPlaced>
{
public override Task Handle(OrderPlaced message)
{
// ... business work
MessagesToPublish.Add(new InvoiceRequested(message.OrderId));
return Task.CompletedTask;
}
}
After (9)
Publish an event from any service with DaprClient (register it with builder.Services.AddDaprClient()):
using Dapr.Client;
public sealed record OrderPlaced(int OrderId);
public sealed class OrderPublisher(DaprClient daprClient)
{
public Task PublishAsync(OrderPlaced message, CancellationToken cancellationToken)
=> daprClient.PublishEventAsync("pubsub", "order-placed", message, cancellationToken);
}
Subscribe in the receiving service:
// Program.cs
using Dapr;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprClient();
var app = builder.Build();
app.UseCloudEvents();
app.MapSubscribeHandler();
app.MapPost("/orders/placed", [Topic("pubsub", "order-placed")] (OrderPlaced message) =>
{
// ... business work
return Results.Ok();
});
await app.RunAsync();
Declare the broker as a Dapr component named pubsub, for example RabbitMQ:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: pubsub
spec:
type: pubsub.rabbitmq
version: v1
metadata:
- name: connectionString
value: "amqp://localhost:5672"
Steps:
- Run each service with a Dapr sidecar and add the pub/sub component (see the RabbitMQ component reference).
- Add
Dapr.AspNetCoreto the services, and replace eachHandleMessageBase<T>with a subscribed endpoint and eachMessagesToPublish.AddwithPublishEventAsync. MessagesScopepublished the collected messages when the unit of work completed. With Dapr, callPublishEventAsyncafter your data is saved. If the publish must be part of the database transaction, look at the Dapr outbox pattern.- Remove the NServiceBus packages and endpoint configuration classes.
Prism.DI.Wpf replaced by Prism.DryIoc
Arc4u.Prism.DI.Wpf adapted Prism 7.2 to the Arc4u IContainer. It is not
continued in Arc4u 9 and is deprecated on NuGet.org (#189, #272): it
targeted net8.0-windows and net9.0-windows, and IContainer no longer exists in Arc4u 9 (see
IContainer replaced by keyed services). Use Prism's own
DryIoc container, Prism.DryIoc.
Before (8.x)
// App.xaml.cs
using System.Windows;
using Arc4u.Dependency;
using Arc4u.Dependency.ComponentModel;
using Prism.DI;
using Prism.DI.Ioc;
using Prism.Ioc;
public partial class App : PrismApplication
{
protected override IContainerExtension CreateContainerExtension()
=> new AppContainerExtension(new ComponentModelContainer());
protected override Window CreateShell() => Container.Resolve<MainWindow>();
protected override void RegisterTypes(Prism.Ioc.IContainerRegistry containerRegistry)
{
containerRegistry.Register<IOrderService, OrderService>();
}
}
// Your subclass of the abstract DIContainerExtension.
public sealed class AppContainerExtension(IContainer container) : DIContainerExtension(container)
{
public override void FinalizeExtension() => Container.CreateContainer();
}
After (9)
// App.xaml.cs
using System.Windows;
using Arc4u.Dependency;
using DryIoc.Microsoft.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Prism.DryIoc;
using Prism.Ioc;
public partial class App : PrismApplication
{
protected override Window CreateShell() => Container.Resolve<MainWindow>();
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.Register<IOrderService, OrderService>();
// Arc4u 9 registers its services in an IServiceCollection: copy them into DryIoc.
var services = new ServiceCollection();
services.AddILogger();
containerRegistry.GetContainer().Populate(services);
}
}
Steps:
- Replace
Arc4u.Prism.DI.WpfwithPrism.DryIoc, and addDryIoc.Microsoft.DependencyInjectionif you register Arc4u services with theirIServiceCollectionextension methods. - Derive
AppfromPrism.DryIoc.PrismApplicationand delete yourDIContainerExtensionsubclass. - Prism 9 moved some types to new namespaces (for example the regions); follow the Prism upgrade notes for the version you choose.
Important
Prism.DryIoc 9 requires a Prism license: the package asks you to accept either the Prism
Community License (for organizations with an annual gross revenue under USD 1 million, or that
never received more than USD 3 million of outside capital) or the Prism Commercial License. Read the terms on
prismlibrary.com before you upgrade.
Message and Messages replaced by ProblemDetails
Arc4u 8.x reported errors with Message objects collected in Messages and thrown in an
AppException. Arc4u 9 removes Message, Messages, MessageType, LocalizedMessage and
AppException (#142). Methods return a FluentResults
Result instead, and the errors become a
ProblemDetails response (HTTP) or an RpcException (gRPC).
Before (8.x)
using Arc4u.ServiceModel;
using Microsoft.AspNetCore.Mvc;
public sealed record Order(int Id, int Quantity);
public sealed class OrderService(ILogger<OrderService> logger)
{
public Order Create(Order order)
{
var messages = new Messages();
if (order.Quantity <= 0)
{
messages.Add(new Message(MessageCategory.Business, MessageType.Error, "The quantity must be positive."));
}
messages.LogAndThrowIfNecessary(logger); // throws an AppException when an error was added
// ...
return order;
}
}
[ApiController]
[Route("orders")]
public sealed class OrdersController(OrderService orderService) : ControllerBase
{
[HttpPost]
public ActionResult<Order> Create(Order order) => Ok(orderService.Create(order));
}
After (9)
using Arc4u.AspNetCore.Results;
using Arc4u.Results.Validation;
using FluentResults;
using Microsoft.AspNetCore.Mvc;
public sealed record Order(int Id, int Quantity);
public sealed class OrderService
{
public Result<Order> Create(Order order)
{
if (order.Quantity <= 0)
{
return Result.Fail(ValidationError.Create("The quantity must be positive.")
.WithCode("Order.Quantity"));
}
// ...
return Result.Ok(order);
}
}
[ApiController]
[Route("orders")]
public sealed class OrdersController(OrderService orderService) : ControllerBase
{
[HttpPost]
public ActionResult<Order> Create(Order order) => orderService.Create(order).ToActionOkResult();
}
In 8.x, the ManageExceptionsFilter turned an AppException into a response that contained the
business messages. In Arc4u 9 the filter only handles unexpected exceptions (403 for
UnauthorizedAccessException, 500 for any other exception); a ValidationError becomes a 422
validation problem details response through ToActionOkResult.
| 8.x | Arc4u 9 |
|---|---|
Messages, Message with MessageType.Error |
Result.Fail(...) with a ValidationError (expected, 422) or a ProblemDetailError (with your status code) |
throw new AppException(...) |
Return a failed Result; throw only for unexpected errors |
Messages.LogAndThrowIfNecessary(logger) |
result.LogIfFailed() and return the result |
ITokenProvider.GetTokenAsync returning Task<TokenInfo?> |
Returns ValueTask<Result<TokenInfo>>: replace Task.FromResult with ValueTask.FromResult (or async) in your providers, and await the call only once |
ICredentialTokenProvider.GetTokenAsync |
Returns ValueTask<Result<TokenInfo>>, like ITokenProvider. CredentialTokenCacheTokenProvider.CreateBasicTokenInfoAsync (protected) also returns a ValueTask |
IAppPrincipalFactory.CreatePrincipalAsync(Messages messages, ...) |
CreatePrincipalAsync(...) without Messages, returns Result<AppPrincipal> |
Arc4u.ServiceModel.MessageCategory |
Arc4u.Diagnostics.MessageCategory |
Steps:
- Change the business methods to return
ResultorResult<T>and replace eachMessagewith aValidationErroror aProblemDetailError. - In controllers and minimal APIs, convert the result with the
Arc4u.AspNetCore.Resultsextension methods; in gRPC services, useToRpcException()fromArc4u.AspNetCore.gRpc. The results guide shows every method and status code. - Update your own
ITokenProviderandIAppPrincipalFactoryimplementations to return aResult. - Clients that read the list of
Messagefrom a 8.x response must read a ProblemDetails body instead.
IContainer replaced by keyed services
Arc4u 8.x resolved services by name through its own container abstraction: IContainer,
IContainerRegistry, IContainerResolve, ComponentModelContainer (package
Arc4u.Standard.Dependency.ComponentModel), DependencyContext and ContainerContext. Arc4u 9
removes them and uses the keyed services of Microsoft.Extensions.DependencyInjection: the 8.x name
becomes the service key (#140). The [Export],
[Shared] and [Scoped] attributes stay in Arc4u.Dependency.
Before (8.x)
using Arc4u.Dependency;
using Arc4u.Dependency.ComponentModel;
public interface IPaymentGateway
{
Task PayAsync(decimal amount);
}
public sealed class CardPaymentGateway : IPaymentGateway
{
public Task PayAsync(decimal amount) => Task.CompletedTask;
}
public static class Registration
{
public static void Register(WebApplicationBuilder builder)
{
// Registers the types listed in Application.Dependency, then a named service.
var container = new ComponentModelContainer(builder.Services).InitializeFromConfig(builder.Configuration);
container.Register<IPaymentGateway, CardPaymentGateway>("card");
}
}
public sealed class CheckoutService(IContainerResolve container)
{
public Task CheckoutAsync(decimal amount) => container.Resolve<IPaymentGateway>("card")!.PayAsync(amount);
}
After (9)
using Arc4u.Caching.Memory;
public interface IPaymentGateway
{
Task PayAsync(decimal amount);
}
public sealed class CardPaymentGateway : IPaymentGateway
{
public Task PayAsync(decimal amount) => Task.CompletedTask;
}
public static class Registration
{
public static void Register(WebApplicationBuilder builder)
{
builder.Services.AddKeyedTransient<IPaymentGateway, CardPaymentGateway>("card");
// Was "Arc4u.Caching.Memory.MemoryCache, Arc4u.Standard.Caching.Memory" in Application.Dependency.
builder.Services.AddMemoryCacheKind();
}
}
public sealed class CheckoutService([FromKeyedServices("card")] IPaymentGateway gateway)
{
public Task CheckoutAsync(decimal amount) => gateway.PayAsync(amount);
}
| 8.x | Arc4u 9 |
|---|---|
container.Register<IFoo, Foo>(), RegisterScoped, RegisterSingleton |
services.AddTransient<IFoo, Foo>(), AddScoped, AddSingleton |
container.Register<IFoo, Foo>("name") |
services.AddKeyedTransient<IFoo, Foo>("name") (and AddKeyedScoped, AddKeyedSingleton) |
container.RegisterInstance(instance, "name") |
services.AddKeyedSingleton<IFoo>("name", instance) |
IContainerResolve injected in a class |
IServiceProvider, or constructor parameters |
Resolve<T>("name") |
GetRequiredKeyedService<T>("name"), or a [FromKeyedServices("name")] parameter |
TryResolve<T>("name", out var value) |
TryGetService<T>("name", out var value) (Arc4u.Dependency) |
ResolveAll<T>(), ResolveAll<T>("name") |
GetServices<T>(), GetKeyedServices<T>("name") |
CreateScope() on the container |
IServiceProvider.CreateScope() |
InitializeFromConfig(configuration) reading Application.Dependency at run time |
Explicit registrations, or the Arc4u.Dependency.Tool source generator |
GetCacheContext(this IContainerResolve), InitializeTimeZoneContext(this IContainerResolve) |
Same methods on IServiceProvider |
Steps:
- Remove
Arc4u.Standard.Dependency.ComponentModeland everyComponentModelContainer,InitializeFromConfigandDependencyContextcall. - Register each named type as a keyed service. Arc4u types that 8.x found through the
Application.Dependencysection must be registered too:AddCacheContextresolves a cache implementation by the key of itsKind(Memory,Redis,RedisSentinel,Sql,Dapr), so without the keyedICacheregistration the cache is not created. Call the method of each cache package you use:AddMemoryCacheKind(),AddRedisCacheKinds(),AddSqlCacheKind()orAddDaprCacheKind(). - To keep registering types from
[Export]attributes and fromApplication.Dependency, add theArc4u.Dependency.Toolgenerator: it generates the registrations at compile time. The dependency injection guide explains the setup. - Replace
IContainerResolvewithIServiceProvideror with constructor injection.
Authentication settings refactoring
Arc4u 9 changes how the server authentication is registered and configured
(Arc4u.OAuth2.AspNetCore.Authentication). The settings that 8.x let you name or point to by
configuration are now fixed, so fewer keys are needed.
Registration methods
In 8.x, AddOidcAuthentication registered OpenID Connect, cookies and JWT bearer. In Arc4u 9 that
combination is AddHybridAuthentication, and AddOidcAuthentication registers OpenID Connect and
cookies only, without JWT bearer. AddJwtAuthentication (API only) keeps its name.
Before (8.x)
// Program.cs
using Arc4u.OAuth2.Extensions;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOidcAuthentication(builder.Configuration);
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
await app.RunAsync();
After (9)
// Program.cs
using Arc4u.OAuth2.Extensions;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHybridAuthentication(builder.Configuration);
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
await app.RunAsync();
Use AddOidcAuthentication only when the application never receives bearer tokens. When you
configure the options in code instead of in appsettings.json, the OAuth2 settings moved to
HybridAuthenticationOptions.OAuth2SettingsOptions, Certificate is
renamed DataProtectionCertificate, and AuthenticationTicketTTL is renamed AuthenticationTicketTtl
(in appsettings.json the key AuthenticationTicketTTL still works, because configuration keys are
not case-sensitive).
Configuration keys
The configuration binder ignores unknown keys, so the removed keys do not break the startup, but they have no effect any more. Remove them to avoid confusion.
| Section | Key | Change in Arc4u 9 |
|---|---|---|
Authentication |
OpenIdSettingsKey, OAuth2SettingsKey |
Removed: the settings names are fixed (see Settings names). |
Authentication (JWT bearer) |
ValidateAudience |
Removed: it was never read by AddJwtAuthentication. Use Authentication:OAuth2.Settings:ValidateAudience. |
Authentication |
JwtBearerEventsType, CookieAuthenticationEventsType, OpenIdConnectEventsType |
Removed: register your events class in the service collection (see Custom events). |
Authentication |
CookiesConfigureOptionsType |
Removed: register your own IPostConfigureOptions<CookieAuthenticationOptions> before the Add…Authentication call; Arc4u registers its own only if none is registered. |
Authentication:AuthenticationCacheTicketStore |
TicketStore |
Removed: Arc4u registers CacheTicketStore as ITicketStore only if none is registered, so register your own ITicketStore in the service collection. |
Authentication |
NameClaimType |
Added. Default name. |
Authentication |
RoleClaimType |
Added. Default role. |
Authentication |
AuthenticationMethod |
Added. RedirectGet (default) or FormPost. |
Authentication:OpenId.Settings |
AuthenticationType |
Removed: always Cookies. |
Authentication:OAuth2.Settings |
AuthenticationType |
Removed: always OAuth2. |
Authentication:ClaimsMiddleWare:ClaimsFiller |
SettingsKeys |
Removed. |
Authentication (JWT) |
ClientSecretSectionPath (section Authentication:ClientSecrets) |
Replaced by ClientTokensSectionPath (section Authentication:ClientTokens), see Client secrets replaced by client tokens. |
Before (8.x)
{
"Authentication": {
"OpenId.Settings": {
"ClientId": "<client-id>",
"ClientSecret": "<client-secret>",
"AuthenticationType": "Cookies",
"Audiences": [ "<client-id>" ],
"Scopes": [ "openid", "offline_access" ]
},
"OAuth2.Settings": {
"AuthenticationType": "OAuth2Bearer",
"Audiences": [ "<api-audience>" ]
},
"ClaimsMiddleWare": {
"ClaimsFiller": {
"LoadClaimsFromClaimsFillerProvider": true,
"SettingsKeys": [ "OAuth2" ]
}
}
}
}
After (9)
{
"Authentication": {
"OpenId.Settings": {
"ClientId": "<client-id>",
"ClientSecret": "<client-secret>",
"Audiences": [ "<client-id>" ],
"Scopes": [ "openid", "offline_access" ]
},
"OAuth2.Settings": {
"Audiences": [ "<api-audience>" ]
},
"ClaimsMiddleWare": {
"ClaimsFiller": {
"LoadClaimsFromClaimsFillerProvider": true
}
}
}
}
The other keys of the Authentication section (DefaultAuthority, CookieName, DataProtection
and so on) keep their names. One default changed: Authentication:TokenCache:MaxTime is 50 minutes
instead of 20. The server authentication guide documents
all the keys.
Settings names
Arc4u registers the OpenID Connect and OAuth2 settings as named SimpleKeyValueSettings. The names
changed, and so did the AuthenticationType of the identities that JWT bearer authentication creates:
| Settings | 8.x name | Arc4u 9 name |
|---|---|---|
OpenID Connect (OpenId.Settings) |
OpenId |
Cookies (Constants.CookiesAuthenticationType) |
OAuth2 (OAuth2.Settings) |
OAuth2 |
OAuth2 (Constants.BearerAuthenticationType) |
AuthenticationType of a bearer identity |
OAuth2Bearer |
OAuth2 |
JwtHttpHandler also became generic (JwtHttpHandler<T>): the type parameter is the category of its logger.
Before (8.x)
using Arc4u.Configuration;
using Arc4u.OAuth2.Token;
using Microsoft.Extensions.Options;
public sealed class OrdersApiHandler(IServiceProvider serviceProvider,
ILogger<JwtHttpHandler> logger,
IOptionsMonitor<SimpleKeyValueSettings> settings)
: JwtHttpHandler(serviceProvider, logger, settings.Get("OpenId"));
After (9)
using Arc4u.Configuration;
using Arc4u.OAuth2;
using Arc4u.OAuth2.Token;
using Microsoft.Extensions.Options;
public sealed class OrdersApiHandler(IServiceProvider serviceProvider,
ILogger<OrdersApiHandler> logger,
IOptionsMonitor<SimpleKeyValueSettings> settings)
: JwtHttpHandler<OrdersApiHandler>(serviceProvider, logger, settings.Get(Constants.CookiesAuthenticationType));
Search your code for "OpenId", Constants.OpenIdOptionsName, Constants.OAuth2OptionsName and
"OAuth2Bearer" and replace them with the Arc4u 9 names.
Custom claims filler
IClaimsFiller.GetAsync no longer receives the settings and the parameter object.
Before (8.x)
using System.Security.Principal;
using Arc4u;
using Arc4u.IdentityModel.Claims;
using Arc4u.Security.Principal;
public sealed class DatabaseClaimsFiller : IClaimsFiller
{
public Task<IEnumerable<ClaimDto>> GetAsync(IIdentity identity, IEnumerable<IKeyValueSettings> settings, object? parameter)
{
IEnumerable<ClaimDto> claims = [new ClaimDto("department", "sales")];
return Task.FromResult(claims);
}
}
After (9)
using System.Security.Principal;
using Arc4u.IdentityModel.Claims;
using Arc4u.Security.Principal;
public sealed class DatabaseClaimsFiller : IClaimsFiller
{
public Task<IEnumerable<ClaimDto>> GetAsync(IIdentity identity)
{
IEnumerable<ClaimDto> claims = [new ClaimDto("department", "sales")];
return Task.FromResult(claims);
}
}
Custom events
In 8.x you named your events classes in configuration (JwtBearerEventsType and the others). In
Arc4u 9 the registration methods resolve JwtBearerEvents, CookieAuthenticationEvents and
OpenIdConnectEvents from the service collection, and register the Arc4u classes only if you have
not registered your own. Register yours before the Add…Authentication call:
using Arc4u.OAuth2.Events;
using Arc4u.OAuth2.Extensions;
using Microsoft.AspNetCore.Authentication.JwtBearer;
builder.Services.AddTransient<JwtBearerEvents, AuditBearerEvents>();
builder.Services.AddHybridAuthentication(builder.Configuration);
using Arc4u.OAuth2.Events;
using Microsoft.AspNetCore.Authentication.JwtBearer;
public sealed class AuditBearerEvents(ILogger<StandardBearerEvents> logger) : StandardBearerEvents(logger)
{
public override Task TokenValidated(TokenValidatedContext context)
{
// ... your code
return base.TokenValidated(context);
}
}
Steps:
- Replace
AddOidcAuthenticationwithAddHybridAuthenticationif the application also accepts bearer tokens. - Remove the keys listed in the table above and register your events classes in code.
- Replace the
OpenIdsettings name withCookies, andOAuth2BearerwithOAuth2. - Update your
IClaimsFillerimplementations and yourJwtHttpHandlersubclasses.
Note
The configuration overloads of AddOidcAuthentication and AddHybridAuthentication apply
ValidateAudience and ValidateAuthority from the Authentication section
(#220). Check that these keys hold the values you want:
they were ignored before.
AddJwtAuthentication and the bearer scheme of AddHybridAuthentication now validate the token issuer
(Authentication:ValidateAuthority, true by default). The OpenID Connect sign-in uses the same rule
instead of comparing with DefaultAuthority:Url: the iss claim must be DefaultAuthority:Issuer, or the issuer of the provider's metadata.
If your tokens have another issuer (for example Entra ID v1 tokens with a v2 authority), set
DefaultAuthority:Issuer to it, or ValidateAuthority to false.
Client secrets replaced by client tokens
AddJwtAuthentication registered the credentials a service uses to call other services from the
Authentication:ClientSecrets section. Arc4u 9 reads the Authentication:ClientTokens section
instead (ClientTokenSettingsOptions). Each entry names a Scenario, and the credentials move to a Settings dictionary. The
entry name (Backend below) is still the name of the settings to pass to a JwtHttpHandler.
Before (8.x)
{
"Authentication": {
"ClientSecrets": {
"Backend": {
"ClientId": "<client-id>",
"User": "<service-account>",
"Password": "<password>",
"Scopes": [ "openid" ]
}
}
}
}
After (9)
{
"Authentication": {
"ClientTokens": {
"Backend": {
"Scenario": "UserPassword",
"Scopes": [ "openid" ],
"Settings": {
"ClientId": "<client-id>",
"User": "<service-account>",
"Password": "<password>"
}
}
}
}
}
| 8.x entry | Arc4u 9 Scenario |
Keys in Settings |
|---|---|---|
User and Password |
UserPassword |
ClientId, User, Password, optional ClientSecret |
Credential (user:password) |
Basic |
ClientId, Credential, optional ClientSecret |
| (not available) | ClientCredentials |
ClientId, ClientSecret |
Authority, Scopes and AuthenticationType stay at the level of the entry. ProviderId and
BasicProviderId are removed: the scenario selects the token provider.
Fluent logging API
The Arc4u logging extensions were rewritten for performance
(#150). Technical(), Business() and
Monitoring() still exist on ILogger<T>, but they return an ILoggerWrapper<T>, which is an
ILogger<T>: you add properties first and then log with the standard Log… methods. There is no
Log() call at the end any more.
Before (8.x)
using Arc4u.Diagnostics;
public sealed class InvoiceService(ILogger<InvoiceService> logger)
{
public void Send(int invoiceId)
{
logger.Technical().Information("Sending invoice {InvoiceId}", invoiceId).Add("InvoiceId", invoiceId).Log();
logger.Business().Warning("Invoice {InvoiceId} is overdue", invoiceId).Log();
}
}
After (9)
using Arc4u.Diagnostics;
public sealed class InvoiceService(ILogger<InvoiceService> logger)
{
public void Send(int invoiceId)
{
logger.Technical().Add("InvoiceId", invoiceId).LogInformation("Sending invoice {InvoiceId}", invoiceId);
logger.Business().LogWarning("Invoice {InvoiceId} is overdue", invoiceId);
}
}
Steps:
- Call
builder.Services.AddILogger()(AddApplicationContext()calls it too). Without it,Technical(),Business()andMonitoring()throwInvalidOperationException("Bad Arc4u usage."), because the injectedILogger<T>is not the Arc4u wrapper. - Rewrite each call: move
Add…before the log call, replaceInformation,Warning,Error,Fatal,DebugwithLogInformation,LogWarning,LogError,LogCritical,LogDebug, and drop.Log(). - Replace
logger.Technical().From<T>()on a non-genericILoggerwithlogger.Technical<T>(). LoggerContextis removed: useILogger.BeginScopeor anIAddPropertiesToLogimplementation to add properties to every entry. The diagnostics guide covers both.
Note
The properties added with Add (and the message template arguments) are written with that entry
only: Technical(), Business() and Monitoring() return a logger dedicated to the entry.
Request timing middleware removed
AddMonitoringTimeElapsed() (Arc4u.OAuth2.AspNetCore) and AddGrpcMonitoringTimeElapsed()
(Arc4u.AspNetCore.gRpc) wrote a Time to complete method call log entry for every request, with
its elapsed milliseconds, status code and endpoint. OpenTelemetry already records that: ASP.NET Core
starts an activity for each request, gRPC calls included, which becomes a server span with its
duration, route and status code, and it publishes the http.server.request.duration histogram for
every request. Arc4u 9 removes the two middlewares, their extension methods and the
LogTimeToCompleteCall and TimeToCompleteMethodCall log messages.
Before (8.x)
using Arc4u.AspNetCore.Middleware;
using Arc4u.OAuth2.Middleware;
var app = builder.Build();
app.AddMonitoringTimeElapsed((type, elapsed) => RequestDuration.Record(elapsed.TotalMilliseconds));
app.AddGrpcMonitoringTimeElapsed();
After (9)
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing.AddAspNetCoreInstrumentation()
.AddSource("Arc4u")
.AddOtlpExporter())
.WithMetrics(metrics => metrics.AddAspNetCoreInstrumentation()
.AddOtlpExporter());
var app = builder.Build();
Steps:
- Delete the calls to
AddMonitoringTimeElapsedandAddGrpcMonitoringTimeElapsed. - If you do not export traces and metrics yet, add the packages
OpenTelemetry.Extensions.Hosting,OpenTelemetry.Instrumentation.AspNetCoreand an exporter (for exampleOpenTelemetry.Exporter.OpenTelemetryProtocol), and register them as above. - If you passed an
Action<Type, TimeSpan>to feed your own metric, use thehttp.server.request.durationhistogram instead: itshttp.routeandhttp.response.status_codeattributes identify the endpoint. For a measure ASP.NET Core does not take, record it with your ownSystem.Diagnostics.Metrics.Histogram<T>. - Update the queries, dashboards and alerts that read the
Time to complete method callentries (Elapsedproperty; Monitoring category for HTTP, Technical category and event id 9002 for gRPC) to read the span durations or thehttp.server.request.durationmetric.
Note
The CPU and memory monitoring (AddSystemMonitoring(), SystemResources) still works in Arc4u 9
but is obsolete: build warning CS0618. Replace it with the System.Runtime meter, as described in
Log CPU and memory usage.
Result extension methods
The FluentResults extension methods of Arc4u.Results changed in three ways:
OnFailedandOnFailedAsynccallbacks receive anIReadOnlyCollection<IError>instead of aList<IError>.- The
…Asyncmethods called on a plainResultorResult<T>(OnSuccessAsync,OnFailedAsync,OnSuccessNullAsync,OnSuccessNotNullAsync) returned aResultin 8.x and blocked on the callback with.Wait(). In Arc4u 9 they return aTask<Result>orTask<Result<T>>: await them. - Passing an asynchronous lambda to a synchronous method (
OnSuccess,OnFailed,OnSuccessNull,OnSuccessNotNull) is now a compile error (CS0619). In 8.x the lambda ran asasync void: nothing awaited it and its exceptions were lost. Use the…Asyncmethods and await them.
Before (8.x)
using Arc4u.Results;
using FluentResults;
public interface INotifier
{
Task NotifyAsync();
}
public static class OrderNotifications
{
public static Result Notify(Result result, INotifier notifier, ILogger logger)
=> result.OnSuccess(async () => await notifier.NotifyAsync())
.OnFailed(errors => errors.ForEach(error => logger.LogWarning("{Message}", error.Message)));
}
After (9)
using Arc4u.Results;
using FluentResults;
public interface INotifier
{
Task NotifyAsync();
}
public static class OrderNotifications
{
public static Task<Result> NotifyAsync(Result result, INotifier notifier, ILogger logger)
=> result.OnSuccessAsync(() => notifier.NotifyAsync())
.OnFailed(errors =>
{
foreach (var error in errors)
{
logger.LogWarning("{Message}", error.Message);
}
});
}
The …Async methods on a plain result:
Before (8.x)
using Arc4u.Results;
using FluentResults;
public sealed record Invoice(int Id);
public interface IInvoiceStore
{
Task SaveAsync(Invoice invoice);
Task ReportAsync(int errorCount);
}
public static class InvoiceSaving
{
// Returns synchronously: each callback is blocked on with .Wait().
public static Result<Invoice> Save(Result<Invoice> result, IInvoiceStore store)
=> result.OnSuccessAsync(invoice => store.SaveAsync(invoice))
.OnFailedAsync((List<IError> errors) => store.ReportAsync(errors.Count));
}
After (9)
using Arc4u.Results;
using FluentResults;
public sealed record Invoice(int Id);
public interface IInvoiceStore
{
Task SaveAsync(Invoice invoice);
Task ReportAsync(int errorCount);
}
public static class InvoiceSaving
{
public static Task<Result<Invoice>> SaveAsync(Result<Invoice> result, IInvoiceStore store)
=> result.OnSuccessAsync(invoice => store.SaveAsync(invoice))
.OnFailedAsync((IReadOnlyCollection<IError> errors) => store.ReportAsync(errors.Count));
}
The FluentValidation helpers moved as well: ValidateWithResult and ValidateWithResultAsync are
now in Arc4u.FluentValidation (namespace Arc4u.Validation), next to the rules IsDateOnly and
IsUtcDateTime. ToFluentResultErrors is renamed ToResultErrors, and ToMessages and
ToMessageType (which produced Messages) are removed. The IsInsert, IsUpdate, IsDelete and
IsNone rules are removed (see Arc4u.Data and change tracking removed).
A few smaller changes may also need attention:
ToActionCreatedResultAsync,ToHttpCreatedResultAsyncandToTypedCreatedResultAsyncon aResultwithout a value (or a task of it) lost their unused type parameter: writeresult.ToActionCreatedResultAsync(location)instead ofresult.ToActionCreatedResultAsync<Order>(location).ValidationError.WithMetadatano longer throws on a duplicate key: the first value is kept, as withProblemDetailError.WithMetadata.- The title of the generic
500problem details is now "A technical error occurred!" (the typo "occured" is fixed). Update any client or test that matches on that text. ToProblemDetailscalled on a successful result no longer adds an error to that result: it stays successful.
Arc4u.Data and change tracking removed
With a domain-centric approach, the domain model decides what changes in an aggregate, and the data access layer persists the aggregate with the API of Entity Framework Core. The entities no longer carry the change to persist, so Arc4u 9 removes the change-tracking concept (#266):
| 8.x | Replacement |
|---|---|
The Arc4u.Standard.Data package: IdEntity, PersistEntity, the audit entities (CreateAuditEntity, UpdateAuditEntity, DeleteAuditEntity, AuditEntity and their interfaces), EntitySet<TEntity>, EntityItem<TEntity>, PersistChangeActions, RelationAttribute, Validate<T> and ValidateAll |
Define the entities in your domain model with your own base classes, and validate them with FluentValidation and ValidateWithResult from Arc4u.FluentValidation |
Arc4u.Data.IPersistEntity and Arc4u.Data.PersistChange (package Arc4u) |
None: remove the PersistChange property and its entity.Ignore(e => e.PersistChange) mapping |
Arc4u.EfCore: ChangeGraphTracker (Tracker, TrackPersistGraph, SavePersistChangesAsync, SavePersistChanges) and PersisteChangeExtension.Convert |
The Entity Framework Core API: load the aggregate tracked, call its domain methods, then SaveChangesAsync. For a detached aggregate use Add, Update, Remove or ChangeTracker.TrackGraph with your own callback |
Arc4u.FluentValidation: the IsInsert(), IsUpdate(), IsDelete() and IsNone() rules and ValidatorPredicates |
Validate the operation in the domain model, or write a separate validator for each operation |
Graph<T> and the GraphExtension methods of Arc4u.EfCore that apply its include paths to a
query are kept.
Before (8.x)
using Arc4u.Data;
using Arc4u.EfCore;
using Microsoft.EntityFrameworkCore;
public class OrderRepository(ShopContext db)
{
public async Task SaveAsync(Order order, CancellationToken cancellationToken)
{
db.ChangeTracker.TrackGraph(order, ChangeGraphTracker.Tracker);
await db.SaveChangesAsync(cancellationToken);
}
}
After (9)
using Microsoft.EntityFrameworkCore;
public class OrderService(ShopContext db)
{
public async Task AddLineAsync(Guid orderId, string label, CancellationToken cancellationToken)
{
var order = await db.Orders.Include(o => o.Lines)
.SingleAsync(o => o.Id == orderId, cancellationToken);
order.AddLine(label); // the domain model changes the aggregate
await db.SaveChangesAsync(cancellationToken); // EF Core detects the changes
}
}
- Remove the
Arc4u.Standard.Datapackage reference and theusing Arc4u.Data;directives. - Replace the Arc4u entity base classes with your own (for example a base class with a
Guid Id). - Remove the
PersistChangeproperties, theirIgnoremappings, and the code that sets them. - Persist the aggregates with the Entity Framework Core API instead of
ChangeGraphTracker.
Other API changes
Before (8.x)
using Arc4u.Authorization;
using Arc4u.OAuth2.Middleware;
using Arc4u.Security.Principal;
public static class Security
{
public static void Register(WebApplicationBuilder builder, Operation[] operations)
{
builder.Services.AddScopedOperationsPolicy(["Admin"], operations,
options => options.InvokeHandlersAfterFailure = false);
}
public static void Use(WebApplication app)
{
app.UseForceOfOpenId(options => options.ForceAuthenticationForPaths.Add("/swagger*"));
}
}
After (9)
using Arc4u.Authorization;
using Arc4u.OAuth2.Middleware;
using Arc4u.Security.Principal;
public static class Security
{
public static void Register(WebApplicationBuilder builder, Operation[] operations)
{
builder.Services.AddScopedOperationsPolicy(["Admin"], operations)
.ConfigureAuthorization(options => options.InvokeHandlersAfterFailure = false);
builder.Services.AddForceOpenId(options => options.ForceAuthenticationForPaths.Add("/swagger*"));
}
public static void Use(WebApplication app)
{
app.UseForceOpenId();
}
}
| 8.x | Arc4u 9 |
|---|---|
app.UseForceOfOpenId(options) |
services.AddForceOpenId(options) (or AddForceOpenId(configuration), section Authentication:ClaimsMiddleWare:ForceOpenId) and app.UseForceOpenId() |
AddScopedOperationsPolicy(scopes, operations, authorizationOptions) |
AddScopedOperationsPolicy(scopes, operations) returns a PoliciesBuilder; call ConfigureAuthorization(...) on it |
ScopedOperationsRequirement, ScopedOperationsHandler |
AllScopedOperationsRequirement, AllScopedOperationsHandler (and AnyScopedOperation… for "any of") |
AuthorityOptions.SetData(url, tokenEndpoint, metadataAddress) and the AuthorityOptions(url, tokenEndpoint, metadataAddress) constructor |
SetData(url, tokenEndpoint, issuer, metadataAddress) and AuthorityOptions(url, tokenEndpoint, issuer, metadataAddress) |
Arc4u.Caching.SecureCache |
Arc4u.Blazor.Caching.SecureCache |
Arc4u.FluentValidation.DefaultValidatorExtensions |
Arc4u.Validation.DefaultValidatorExtensions |
Arc4u.OAuth2.Configuration.ClaimsIdentifierOption |
Arc4u.OAuth2.Options.ClaimsIdentifierOption |
Arc4u.OAuth2.Middleware.OpenIdBearerInjectorOptions, OpenIdBearerInjectorSettingsOptions |
Namespace Arc4u.OAuth2.Options |
Arc4u.OAuth2.AspNetCore.Options.ValidateResourceRightMiddlewareOptions |
Arc4u.OAuth2.Options.ValidateResourceRightMiddlewareOptions |
Arc4u.OAuth2.Security.Principal.AppServicePrincipalFactory |
Arc4u.OAuth2.AppServicePrincipalFactory |
Arc4u.Utils.Enum<T>, EnumUtil |
The generic methods of System.Enum (Enum.GetValues<T>(), Enum.TryParse<T>()) |
IApplicationContext.Principal as an AppPrincipal, SetPrincipal(AppPrincipal) |
AppPrincipal? Principal and SetPrincipal(AppPrincipal?): check the principal for null before using it (nobody authenticated), and pass null to clear it |
ITokenProvider.SignOutAsync(settings, cancellationToken) |
Moved to ISignOutTokenProvider. Implement it only when your provider holds tokens for a user session; otherwise remove your SignOutAsync method. SignOutUserAsync of AppPrincipalFactory and AppServicePrincipalFactory signs out every registered provider that implements it, with the same settings: ignore the settings that are not yours. OidcTokenProvider and CredentialTokenCacheTokenProvider have no sign-out; sign out of the cookie with HttpContext.SignOutAsync |
Arc4u.Configuration.Store.EfCore: ServiceCollectionExtensions (called as a static class) |
DbContextSectionStoreServiceCollectionExtensions. Calls written as extension methods do not change |
AddNotificationHandlersAsScoped(ServiceLifetime lifetime) (Arc4u.Dispatcher), which registered the handlers as scoped for any value but Transient |
AddNotificationHandlersAsScoped() or AddNotificationHandlersAsTransient() |
Identity log property: UserProfile.Name (Domain\SamAccountName) or the identity name |
The email of the user profile, or the identity name when there is no email. Update the log queries and dashboards that filter on it |
AddApplicationContext() registered DefaultLoggingProperties, so the entries had the Identity property |
AddApplicationContext() registers no provider: add services.AddKeyedScoped<IAddPropertiesToLog, DefaultLoggingProperties>("Scoped"), or list Arc4u.Diagnostics.DefaultLoggingProperties, Arc4u in Application.Dependency:RegisterTypes. See Diagnostics and logging |
MemoryLogDbSink.LogMessages as a List<LogMessage> (indexer, RemoveAt, LINQ on the live list) |
A thread-safe MemoryLogMessages: Add, Clear, Count, GetPage(predicate, skip, take), or enumerate it to read a snapshot. Prefer ILogStore |
Arc4u.OAuth2.Blazor: the login popup of BlazorTokenProvider (WindowInterop.OpenWindowAsync) waited forever when the user closed it, and let a JSException through when CreatePopupWindow.js was not loaded |
A TimeoutException when the login is not completed within 5 minutes, and an InvalidOperationException (the JSException as inner exception) when the popup cannot be opened. Both come from the token request, so from the HTTP calls that need a token: catch them there to show a message or let the user retry |
Other removed APIs
| 8.x | Replacement |
|---|---|
CypherCodec.GenerateKeyAndIV, EncodeClearText, DecodeCypherstring, CypherCodecConfig.IV (AES-CBC with a shared IV) |
CypherCodec.GenerateKey, Encrypt and Decrypt (AES-GCM with a random nonce per value). Remove EncryptionRijndael:IV and encrypt the Decrypt: values again. Long values encrypted with a certificate (<key>.<iv>.<data>) must also be encrypted again |
UserProfile.Name (Domain\SamAccountName) |
UserProfile.Email, PrincipalName, or Domain and SamAccountName |
IApplicationContext.ActivityID (set by the gRPC interceptors and the HTTP handlers) |
Activity.Current?.TraceId (OpenTelemetry). The logger adds it as the ActivityId property when an activity exists |
Arc4u.Standard.OAuth2.AspNetCore.Api: ServiceAspectAttribute, OperationCheckAttribute (already obsolete in 8.x) |
ASP.NET Core authorization policies from AddScopedOperationsPolicy ([Authorize("<operation>")]), ManageExceptionsFilter and SetCultureActionFilter |
SecretBasicExtension.AddSecretAuthentication, CredentialSecretTokenProvider |
AddClientTokens (see Client secrets replaced by client tokens) |
LoggerContext, CommonLoggerProperties, LoggerBase |
See Fluent logging API |
RealmLoggingDbCtx.CreateMapping() (AutoMapper) |
Removed with the AutoMapper dependency |
RealmLoggingDbCtx(RealmConfiguration, ILoggerFactory) and RealmLoggingDbCtx.Realm |
RealmLoggingDbCtx(RealmConfiguration). The store opens a Realm for each call, so it no longer exposes one: open your own with Realm.GetInstance(config) on the thread that uses it |
CustomRootCaExtensions.CertificateOptionKeys (static, shared by every service collection of the process) |
The names registered with AddCustomRootCA in a service collection: serviceProvider.GetRequiredService<IOptions<CARootOptionKeys>>().Value.Keys |
Arc4u.gRPC: ClientErrorInterceptor |
Catch the RpcException and call ToResult() (see ProblemDetails over gRPC). A 403 returned by the service arrives as PermissionDenied with status 403 in its ProblemDetails. The interceptor only converted exceptions raised while a call was started, which almost never happens with Grpc.Net.Client, so removing it barely changes what your code receives |
Arc4u.OAuth2.Client.Authentication: OidcClientAuthenticationOptions and OidcClientAuthenticationSectionOptions NameClaimType, RoleClaimType and ValidateAuthority (Authentication:NameClaimType, Authentication:RoleClaimType, Authentication:ValidateAuthority in a client application) |
None: the client authentication never read them. Delete the assignments; the configuration keys are ignored. The client principal is built by the AppPrincipalFactory |