Migrating to DAF 2.0
# Migrating to DAF 2.0
This release is a major: some changes break compilation, and others change behaviour at run time where the compiler says nothing. The list below covers both; step 6 of “How to do it” names the run-time ones.
You upgrade from 1.6.0. There was no published 1.7.0 — its content ships here —, so 1.6 to 2.0 is a single step.
What to change, most likely first
1. Application modules are registered by name
AddModules(Package) — the RTTI discovery of IAppModule — is gone. Each module is applied by name, in whatever order the application wants:
// before
ServiceCollection.AddModules(_T.PackageOf<TSomething>);
// now
ServiceCollection
.AddModule<TEnvironmentModule>
.AddModule<TEvaluationModule>
.AddModule<TSuiteModule>;The uses marked // force-link: go with it — they only existed so the RTTI would see the module, and naming the type in AddModule<T> already links its unit.
IAppModule and TAppModule do not change, and each module’s body — its AddServices — is untouched. What disappears is how they were found, not what they are.
⚠️ If your application derives from TDafApplication, this affects you even if you never wrote AddModules. That builder used to discover the modules of the application class’s package on its own. TDafApplication is retired in 2.0 — your entry point is your variant’s builder, further down — and no variant registers a module for you. Declare yours where you compose services:
Builder.Services
.AddModule<TEnvironmentModule>
.AddModule<TEvaluationModule>;What changes when it runs. Modules register in the order of the AddModule<T> calls, no longer in the order the reflection walk found them, and each module’s own constructor runs.
2. Daf.Commons has been split
Daf.Commons now holds only what extends the standard library. What left has its own package, and it must be added to the requires of whichever package uses it:
| What you used | Now lives in |
|---|---|
Daf.CmdLn.Parser |
nowhere: withdrawn — see the next section |
Daf.SystemProcess |
package Daf.SystemProcess |
KillAfterTimeout is gone from ISystemProcess and from its builder: the deadline always kills the process. Drop the call.
A stream with no destination declared now inherits the parent’s: what the child writes appears where your process writes. To keep it quiet, as in 1.6.0, discard both:
.StandardOutput(TProcessOutput.Discard)
.StandardError(TProcessOutput.Discard)A stream takes one declaration: OnStdOut twice, or OnStdOut and then StandardOutput, raises EInvalidOpException. Keep one per stream.
HideWindow(False), the default, no longer opens a console window of its own: the child runs in your console. There is no option that brings the separate window back.
Sharing your console, the child also receives its Ctrl+C: the user’s Ctrl+C now stops the child together with your application, where in 1.6.0 the child’s own console kept it out. A child that has to outlive a Ctrl+C is launched with HideWindow(True).
ExecuteAsync now launches the child before it returns, on the thread that calls it, and only the wait runs in the task. A launch that fails calls OnFailed on that same thread, before ExecuteAsync returns: a handler that counted on running in the background no longer does.
Kill with no child launched —before running, or after a launch that failed— now raises EInvalidOpException; it used to do nothing visible and cut the next run short. A call that can arrive before the launch or after the end, such as one from a shutdown hook, asks IsRunning first:
if Proc.IsRunning then
Proc.Kill;With a deadline, a descendant that inherits a captured stream —a grandchild left running in the background— no longer holds Execute past it: when the deadline expires with the stream still open, the result is Timeout, where it used to wait for the descendant and come back Completed. A cancellation token bounds that wait the same way. With neither, Execute still waits for the stream to close.
3. Daf.CmdLn.Parser is gone
The unit 1.6.0 shipped inside Daf.Commons is no longer distributed. Its replacement is Daf.CommandLine, a port of .NET’s System.CommandLine — the library the dotnet CLI itself is built on. There is no adapter and no unit alias: they are different APIs, and existing uses clauses do not keep compiling.
The same grammar, declared both ways:
// before
uses Daf.CmdLn.Parser;
var Parser := TCmdLnParserBuilder.Create
.Arg<string>('file')
.Arg<Integer>('port')
.Arg<Boolean>('verbose')
.Build;
var Params := Parser.Parse(GetCommandLine);
if Params = nil then Exit; // nil was the whole report of a bad invocation
var FilePath := Params['file'].AsString; // 'file' written a second time, unchecked
var Port := Params['port'].AsInteger; // and the type chosen again, at run time
var IsVerbose := Params['verbose'].AsBoolean;// now
uses Daf.CommandLine;
var FileOpt := CommandLine.Option<string>('--file').Description('File to compile');
var PortOpt := CommandLine.Option<Integer>('--port').Description('Port to listen on');
var VerboseOpt := CommandLine.Option<Boolean>('--verbose').Description('Say more');
var Root := CommandLine.Root('mytool', 'A tool');
Root.Add([FileOpt, PortOpt, VerboseOpt]);
var Parsed := Root.Parse; // no argument: reads ParamStr
if Parsed.IsHelpRequested or Parsed.IsVersionRequested or not Parsed.Succeeded then
begin
ExitCode := Parsed.Invoke; // the help, the version, or each error naming the
Exit; // option and the text it received
end;
var FilePath := FileOpt.ValueIn(Parsed); // a string, statically
var Port := PortOpt.ValueIn(Parsed); // an Integer, statically
var IsVerbose := VerboseOpt.ValueIn(Parsed);Where the repeated name goes. On the left, 'file' is written once to declare and again to read, and nothing checks that the two agree: a typo gives back an empty value rather than an error. On the right the option object is the key, so the name exists once and asking for one that was never declared is not representable. Reading an IOption<Integer> into a string is a compile error, where AsInteger was a decision taken again at every read.
The mechanical part, term by term:
Daf.CmdLn |
Daf.CommandLine |
|---|---|
TCmdLnParserBuilder.Create … .Build |
CommandLine.Root(name, description) |
.Arg<T>('name') |
CommandLine.Option<T>('--name'), added with Root.Add — the dashes are written now |
.Arg<string>('output\|o') |
CommandLine.Option<string>('--output', ['-o']) |
A.Default(V) |
.DefaultValue(V), and .DefaultValueFactory(…) for a default that has to be worked out |
A.Required(True) |
.Required — on a Boolean it never fails: a Boolean has false for its default |
A.Help('…') |
.Description('…') — and this one is shown: help is generated |
.Command('build') … .EndCommand |
CommandLine.Command('build', description), hung with Parent.Add(Child) |
Params['x'].AsString / .AsInteger / .AsBoolean |
XOpt.ValueIn(Parsed), typed by the option itself |
Params.GetSection('build')['output'] |
OutputOpt.ValueIn(Parsed): there is no section to navigate |
Params.Command = 'build' |
Parsed.Command, an ICommand — compare it against the object you declared |
Parser.Parse(…) = nil |
Parsed.Succeeded false. Parsed.Errors holds each error with the result of its symbol, and Parsed.ErrorText their text |
What does not carry over unchanged:
Parse(string)is notParser.Parse(GetCommandLine). For the command line of the process,Parsewith no arguments readsParamStr.Parse(string)splits a line withCommandLine.SplitCommandLine, and takes its first token for the root only when it is the root’s name or the path of the running executable: a line fromGetCommandLinethat starts withapp.exe, for a root calledapp, readsapp.exeas a value.- Names tell cases apart.
Daf.CmdLntook--Verbosefor--verbose;Daf.CommandLinedoes not, as in .NET. A parse configured withTParserConfiguration.EnableCaseInsensitiveNamescompares them as before. - Order stopped mattering, so some invocations that used to fail now work. In
Daf.CmdLna command’s arguments were consumed before its subcommand, soapp build -o dir --flagdid not parse. ARecursiveoption is read from the leaf, and that invocation now parses. Nothing that worked stops working; a test that asserted the failure will. - Two limitations of the old value engine are gone, and their outcome changed. A negative number could not be passed at all, and
--port 80xyzwas accepted as80. Now-5is a value, and80xyzis refused naming the option, and reading the option raises that error. If anything relied on that truncation, it gets an error where it used to get a number. - Two values for an option that takes one are an error.
Daf.CmdLnkept the last one:--port 1 --port 2read 2. Now the parse fails withOption '--port' expects a single argument but 2 were provided., and reading the option raises it. Naming it again with no value works as before:--port 1 --portreads 1. - Dispatching can move into the grammar.
Daf.CmdLnhanded back values and left dispatching to the application. InDaf.CommandLineeach command can declare its action withSetAction, andInvokeon the parse result runs the one of the command reached and answers the exit code. Code that dispatches by hand onParsed.Commandkeeps working.
Daf.Configuration.CommandLine is untouched. Reading the command line as a configuration source — --Logging:LogLevel=Debug overriding appsettings.json — is a different job and shares no engine with either module.
4. Types renamed or moved out of Daf.Commons
| Before | Now |
|---|---|
TExpresion<T> |
TDeferred<T>, in Daf.Types |
TFilterSpec<T>, in Daf.Expression |
same class, in Daf.FilterSpec |
unit Daf.Expression |
dissolved into Daf.FilterSpec and Daf.Types |
TBooleanExpresion |
removed |
&If.Exp<T> |
removed — on Delphi 13, the ternary operator (if … then … else as an expression) replaces it and evaluates only the branch it takes; on Delphi 12, an if statement |
TGenericRTTI.GetGenericArguments(TRttiType) |
_T.GenericArgs(PTypeInfo), in Daf.Rtti — returns type information rather than reflected types, and counts an argument that is itself generic as one |
TGenericRTTI.MakeGenericType |
removed with no replacement — see below |
TSmartArray<T>.Select(TDeferred<TMapper<T, TTarget>>) |
Select(TFunc<T, TTarget>) |
Daf.Linq.Where / Filter take TFilterSpec<T>, the type that already lived in Commons: Daf.Linq does not introduce a filtering vocabulary of its own, it evaluates the one already named.
MakeGenericType has no replacement because it promised something Delphi does not do: build a generic instantiation at run time. It failed with an access violation even in the case it was written for. If you were calling it to obtain a closed generic the compiler never saw, there is nothing to migrate to — but if what you actually needed was one registration serving every closure of a contract, that is now registering a generic contract once.
5. Daf.SQLCute.Compilers is dissolved into Daf.SQLCute
The six dialect compilers live in Daf.SQLCute, next to the ANSI one, and keep their unit names: uses clauses compile unchanged. Remove Daf.SQLCute.Compilers from requires, and from any DCCReference.
6. Logging: the mute mode is gone
DAFInternalLoggerremoved. It was a globalILoggerunder theDAFcategory that wrote warnings and errors to the debugger.BootstrapLog.LoggerFor(<category>)replaces it: anILoggerunder the category you give it, which reports through the bootstrap channel — to the debugger and to the standard error output, by default.Composing the pipeline with no provider at all now fails when the host is built, with
ELoggingCompositionError, instead of handing back anILoggerthat accepts everything and writes nothing. If your startup composed logging without providers and you had not noticed, this is what will tell you.Startup reports go out under the category of the class reporting, through the
ILoggerpipeline. That is not API, it is output: if you filtered on theDAFcategory, revisit the filter.ILoggergainedIsEnabled(Level), and this one breaks at compile time. A logger now answers whether an entry of a given level would reach any destination, so a caller can skip building arguments that cost more than the line itself — a state dump, a query. ATLoggerdescendant inherits a default that answersTrue, so a logging provider written outside DAF keeps compiling untouched; only a type that implementsILoggerby hand has to add it. AnswerTrueif in doubt:// add to any hand-written ILogger implementer function TMyLogger.IsEnabled(const Level: TLogLevel): Boolean; begin Result := True; // or your own level test, matching what Log actually writes end;
7. A closed container no longer answers
- Resolving from a closed container raises
EContainerShutDownError, and the message names the service that was asked for. It used to do two different things and neither was that: anEAccessViolationif the service was a Singleton, and serving it silently if it was Scoped — an instance nobody was going to collect.EContainerShutDownErrorderives fromEServiceProviderError, so whoever already catches the parent still catches it. - Creating a scope from a closed container fails the same way, instead of handing out a scope that cannot resolve.
- Asking a closed container whether it can resolve fails the same way.
CanResolveused to answer as if the container were still open. - Closing twice no longer breaks. If your code closed in two places — the application’s explicit shutdown and the teardown of whoever owns the provider — and you avoided the second one with a flag, that flag is no longer needed.
The only thing to revisit is the last resolution before the shutdown:
// Before: worked by accident if the service was Scoped
Provider.ShutDown;
var Report := Provider.GetRequiredService<IReport>; // now: EContainerShutDownError
// Now: resolve while the container is still alive
var Report := Provider.GetRequiredService<IReport>;
Provider.ShutDown;8. The application family, and one builder style
This is the largest front of 2.0 for anything that had a .dpr.
IHostBuilderis gone, and with it the deferred style. Registering throughConfigureServices/ConfigureAppConfiguration/ConfigureHostConfigurationcallbacks no longer exists. There is one style, the immediate one: the builder is usable the moment it is created, and what you register happens where it is written.// Before var Builder: IHostBuilder := THostBuilder.Create; Builder.ConfigureServices( procedure(Context: IHostBuilderContext; Services: IServiceCollection) begin Services.AddSingleton<IMyService, TMyService>; end); var Host := Builder.Build; // Now var B := TCliApplication.CreateBuilder(CommandLineConfig.ProcessArgs); B.Services.AddSingleton<IMyService, TMyService>; B.ParseArgs(CommandLineConfig.ProcessArgs); ExitCode := B.Build.Run;The machinery survives whole —
THostBuilderdrops to implementation detail. What changed is who calls it and when.Your entry point is now your variant.
TCliApplication(Daf.Cli),TWebApplication(Daf.Web),TVclApplication(Daf.VCL) andTFmxApplication(Daf.FMX). They shareServices,Configuration,Environment,Logging,PropertiesandServiceProviderOptionsthroughIHostApplicationBuilder.TDafApplicationandTDafApplicationBuilderare retired, and so is theDaf.Applicationpackage. Their replacement is the variant from the entry above, and each member goes somewhere:CreateHostBuilderbecomes your variant’sCreateBuilder;IDafApplicationbecomes theI…ApplicationthatBuildreturns;RunAsyncisRununder its own name;VersionRequestedisIParseResult.IsVersionRequested, answered by the grammar; andAppbecomesDafApp, which exists only in VCL and FMX — in CLI and Web there is no application global on purpose.The template-method side of
TDafApplication—AppClass, the virtualCreate(Host),ExecuteandVersionInfo— has no counterpart in the variants. In CLI the work is the root’s action, declared withB.SetAction, which receives theIServiceProvider; what ran around it is a startup task (AddStartupTask<T>) or a hosted service; and the version is a[Version]attribute on any class.The
try/exceptaroundRunbecomes a policy. Catching what escapes, reporting it and choosing the exit code isB.OnException(Policy)in the CLI and Web variants, declared before the rest of the composition. Theexceptof the.dpronly hands what escapes beforeRuntoB.HandleException(E), which passes it to that same policy and returns the exit code.Daf.Cli’sGreetToolsample shows the whole.dpr.Daf.Application.Versionmoved toDaf.Commons. AddDaf.Commonstorequiresif you usedTVersionInfoand did not already. Two things changed in it:TVersionInfo.Debugis read-only, and the binary declares it: a fourth argument to the attribute or toTVersionInfo.Create. Without it,DebugisFalse— in 1.6 it reported how DAF itself was compiled, whatever the configuration of your binary.VersionTagtakes a template instead of theWithCompiledMetaflag.
// before Info.Debug := {$IFDEF DEBUG}True{$ELSE}False{$ENDIF}; Text := Info.VersionTag(True); // now [Version('2.9.0', 'MyApp', 'Acme', {$IFDEF DEBUG}True{$ELSE}False{$ENDIF})] TMyApp = class end; Text := TVersionInfo.GetFrom(TMyApp).VersionTag(VersionTemplateWithBuild);ILoggingBuildermoved toDaf.Logging.Abstractions, where .NET has it. Composing logging no longer requires the implementation package. And the builder exposes it as a live property — the pipeline is only composed if you touch it, so an application that never configures logging still starts.The environment is now derived from the host configuration, so it can be set on the command line as well as through a variable. Its keys have been renamed too: see section 20.
IConfigurationgained two members, so a key that is absent can be told from one whose value is empty. This breaks implementors of the interface, not callers: the indexer still returns astring, and every existing expression through it still compiles.if Config.Exists('Features:Beta') then ; // presence var Read := Config.Find('Features:Beta'); // presence AND value, one lookup if Read.HasValue and (Read.Value = 'on') then ; var Value := Config['Features:Beta']; // '' when absent AND when emptyA failure reading a configuration file now surfaces when the source is added, not inside
Build, because that is when the file is opened. It still propagates, and it is also reported on the bootstrap channel.
9. MiniSpec: the injection mark is renamed, and the injector holds the three contexts
[Inject]is now[MiniInject].Injecton its own is a name several tools claim, and two units exporting the same one in the sameusesis not an error — the last one wins, in silence. The attribute travels in the facade now, so the extrausesgoes away:// Before uses Daf.MiniSpec, Daf.MiniSpec.Injection; ... [Inject] property Shared: TSharedContext read FShared write FShared; // Now uses Daf.MiniSpec; ... [MiniInject] property Shared: TSharedContext read FShared write FShared;Leaving
[Inject]in place does not fail the build: the compiler emitsW1074 Unknown custom attributeand at run time the member has no attribute, so nothing is injected and nothing says so. Search your specs for[Inject].The mark now works on a field too, which is what the guide always showed and what never worked. A field is reached whatever its visibility; a property only when it is public or published.
TInjectorServiceis no longer a class-wide registry of loose services. It is an instance owned by the run, and what it holds are the three contexts alive while a step runs.Registertakes the scope, so a suite that registered mocks of its own does not compile:// Before: any object, keyed by its class TInjectorService.Register(TDatabaseMock.Create); // Now: there is no loose registry. Build the collaborator in the World, or share it // through the feature or suite context, which is what gets injected. MiniSpec.UseSuiteContext<TTestResources>; // TTestResources owns the mocks
10. A scope now destroys what it created
- Releasing a scope destroys the Scoped services it built. The container used to retain every scope it handed out and release nothing until it was closed, so a process that opens a scope per request — the pattern the guide recommends — accumulated one instance per request for its whole life.
- What breaks: keeping a reference to a Scoped service without holding its scope. Until now it survived the scope it came from; now it does not.
- The provider a service receives does not hold its scope, so a service that keeps it does not keep the scope alive. Using it once the scope is released raises
EContainerShutDownError, and it is not the same object asScope.ServiceProvider. A singleton receives the container’s provider, not that of the scope that asked for it first.
// Before: the cache outlived its scope by accident
var Scope := Provider.CreateScope;
FCache := Scope.ServiceProvider.GetRequiredService<ICache>;
Scope := nil;
FCache.Get('k'); // now: the instance is already gone
// Now: hold the scope for as long as you hold what it built
FScope := Provider.CreateScope;
FCache := FScope.ServiceProvider.GetRequiredService<ICache>;11. A duplicate registration now yields two instances
Registering the same pair twice and then asking for all of them returned the first one twice: the second registration was declared, was counted, and was never built.
- What breaks: a composition that registers a default and then registers it again instead of replacing it.
TryAdd*registers only if absent, andReplacesubstitutes. - Resolving a single service does not change — the last registration still wins — and neither does the order in which they are closed.
12. SQLCute: a query declares its kind
IQuery gains Kind: TQueryKind. It defaults to Select and is set by AsInsert, AsUpdate and their siblings, instead of being deduced from the clauses present.
- What breaks: implementing
IQueryyourself. The new member has to be provided. - Declaring two kinds on the same query now keeps the last one, where the first used to win. A query with a single kind emits exactly what it emitted before.
13. MiniSpec: which context satisfies a [MiniInject] is now declared
- The precedence is declared, and it was the internal order of a dictionary before. Two passes: first an exact type match, walking scenario → feature → suite; and only if there is none, a descendant, in the same order. So an exact match in the suite beats a nearer descendant in the scenario — asking for the type you wrote is not a guess.
- What breaks: a step that injects a base type while two scopes hold compatible objects. It compiles, and it gets a different instance than before — the old winner depended on hash order, so it was never a decision. Name the exact type you want, or keep one of the two.
14. csoReloadOnChange is gone
TConfigurationSourceOption is left with csoOptional. The withdrawn value was never read by any provider, so nothing that a source did changes.
- What breaks: naming it. Drop it from the set —
[csoOptional, csoReloadOnChange]becomes[csoOptional], and a set that only carried it becomes[].
15. NNLog: a target that cannot write no longer raises
A write that fails inside a target used to travel back to whoever logged. TTarget.Write now catches it and reports it on the bootstrap channel, under the target’s class name and naming the target, so a line that never reaches its destination stops aborting the code that was only trying to log.
- What breaks: a spec that asserts the propagation. Rewrite it against the bootstrap channel, below.
- Where the failure shows now:
BootstrapLog, as aLogErrorcarrying the exception and the target’s name. A target whose failures have to be seen is watched there. - A custom target overrides
DoWrite, notWrite, and this half the compiler does tell you:Writeis no longer virtual. Move the body across and drop your own locking —TTarget.Writetakes a critical section held by that target instance before callingDoWrite, so two threads writing through one target no longer overlap.
// before
procedure TMyTarget.Write(const Entry: TLogEntry);
begin
Send(RenderEvent(Entry)); // a failure here reached the caller
end;
// now
procedure TMyTarget.DoWrite(const Entry: TLogEntry);
begin
Send(RenderEvent(Entry)); // a failure here is reported on the bootstrap channel
end;16. NNLog: a rule’s name filters, and an unknown layout token stays visible
In 1.6 a rule’s name was ignored: every rule received every category. It now filters, as a regular expression anchored to the whole category. *, and an absent or empty name, still match every category.
- What breaks: a rule whose
nameis not*stops receiving the categories its pattern does not match. An NLog glob mostly keeps working —Daf.*does —, but a?wildcard is written.and a literal dot\., and anamethat is not a valid regular expression makes the configuration fail to load, naming the rule. enabledtakes the literalstrueandfalse, in any case. A rule that does not declare it is enabled, and any other value disables it.- A layout token with no renderer — a misspelled
${mesage}— is now written as it is, where 1.6 wrote nothing.
17. Web: TDafHttpServer.Start on a port in use raises
When the port is already in use, Start raises an exception that names the port, after the OnError event. In 1.6 it ended with Abort.
- What breaks: an
except on EAbortaroundStartno longer catches it. CatchExceptioninstead.
18. Hosted services stop in reverse order
The IHostedServices stop in the reverse order of their registration — the order the container releases in —, so a service started later stops before the one it may be using. In 1.6 they stopped in registration order.
- What breaks: a service whose
Stopcounted on one registered after it having stopped already. Register the service that is used before the one that uses it.
19. The host loads appsettings.json and the environment by default
The builder loads appsettings.json, appsettings.{Environment}.json, {ApplicationName}.settings.json and {ApplicationName}.settings.{Environment}.json from the content root, all optional, and every environment variable, with __ as the section separator, before the command line: the sources of Host.CreateApplicationBuilder. In 1.6 the host loaded no source of its own.
- What breaks: an environment variable whose name matches a configuration key now overrides it —
Db__ServeroverridesDb:Server—, and anappsettings.jsonin the content root is read without being asked for. Remove the calls that loaded those two files by hand.
20. The host settings take .NET’s names, and the content root is the current directory
The host reads its own settings under the keys of .NET’s HostDefaults — environment, applicationName and contentRoot —, as an argument or as a variable with the DAF_ prefix, which is stripped: --environment Staging and DAF_ENVIRONMENT=Staging set the same key. THostDefaults replaces TDafEnvVars. And with no contentRoot, the content root is the current directory, as in .NET, except in the system directory — where a Windows service starts —, which keeps the folder of the executable.
// before
TDafEnvVars.APP_ENV // 'DAF_APP_ENV'
// now
THostDefaults.EnvironmentKey // 'environment', and DAF_ENVIRONMENT as a variable- What breaks:
DAF_APP_ENV,DAF_APP_NAMEandDAF_CONTENT_ROOTare no longer read, and nothing says so — an application started withDAF_APP_ENV=Developmentruns asProduction. Rename them toDAF_ENVIRONMENT,DAF_APPLICATIONNAMEandDAF_CONTENTROOT. An application started from another directory no longer finds theappsettings.jsonnext to its executable: setcontentRoot, or start it from its folder. And a log configuration that writes under${contentRootPath}now follows the current directory; write${binPath}to keep the log next to the executable.
21. Logging: an entry’s message no longer carries the scope
TLogEntry.Message is the formatted message and nothing else; the open scopes travel only in TLogEntry.Scope, innermost first, each prefixed with |. In 1.6 the message carried them appended, so every line showed them without asking.
- What breaks, and the compiler does not say it: an NNLog layout with
${message}and no${scope}stops showing the scopes — the default layout among them —, and one with both stops writing them twice.${message}${scope}gives the line of 1.6; a target that relied on the default layout needs one of its own.
before ${level} [${category}] ${message}
now ${level} [${category}] ${message}${scope}
- A provider of your own that writes
Entry.Messagestops writing the scopes. AppendEntry.Scope, which already brings its separator:
// before
procedure TMyLogger.Log(const Entry: TLogEntry);
begin
Send(Entry.Message); // the scopes came inside the message
end;
// now
procedure TMyLogger.Log(const Entry: TLogEntry);
begin
Send(Entry.Message + Entry.Scope); // empty outside any scope
end;22. SQLCute: the in operator by name binds its values
Where(col, 'in', V) — and 'not in', OrWhere, WhereNot and OrWhereNot — binds its values like WhereIn: one parameter per element when V is an array, and one for the whole value otherwise. In 1.6 the value was written into the SQL text.
- What breaks: a string holding a list,
Where('id', 'in', '1,2,3'), is now one value and matches nothing. Pass the values withWhereIn.
// before
Q.Where('id', 'in', '1,2,3');
// now
Q.WhereIn('id', [1, 2, 3]);23. MiniSpec: what a suite written for 1.6 sees differently
How MiniSpec runs and counts a suite changes in these cases, and none of them is a compiler error. A suite that was green can come out with scenarios Pending or Undefined, or red.
- A Pending or Undefined step of a Background decides the scenario. The rest of the Background and the steps of the scenario do not run, and the scenario counts as Pending or Undefined. In 1.6 the scenario ran, and could count as passed. What breaks: an Undefined step in a Background now makes the run exit with 1.
- Each Background runs once per scenario, the feature’s before the rule’s. In 1.6 the feature’s Background ran twice for every scenario, and the Background of a rule replaced the feature’s in every scenario of the feature. What breaks: a Background step that is not idempotent ran twice; and a feature with its own Background and rules with theirs now prepares the scenarios of those rules with both.
- A step declared
.Pendingin a Scenario Outline is pending in every row. In 1.6 the rows ran it. What breaks: an outline that passed with a pending step now counts its rows as Pending. - A Scenario Outline without
.Examplesruns once, as a scenario. In 1.6 it was left out of the run and of the report. What breaks: an outline written without its table now runs, and a step of it with neither lambda nor binding is Undefined. - A hook of the suite that raises counts as a failure. Every
Beforeruns, and when one raised no feature runs; everyAfterruns too. In 1.6 the error was lost and the run exited with 0. What breaks: a suite whose set-up or clean-up hook raises now comes out red. - The JUnit report counts like the run. An Undefined scenario or row is a
<failure>and a Pending one a<skipped>; a scenario that did not run is not listed; and each hook that raised is a failed case. In 1.6 Undefined and Pending came out as passed, and a filtered scenario as skipped. What breaks: a CI that read the report as green now sees the Undefined scenarios as failures. - The JSON report gives each element the state it ended in. A scenario or a row is
pendingorundefinedwhen it ends so; each step carries astatus;successis true only for a step that passed; and the root addsundefinedCount. What breaks: a reader that took every status other thanpassasfail, or that readsuccessas “did not fail”.
24. A constructor the container cannot satisfy fails, and the container ignores the mark
- A class whose constructors the container cannot satisfy is no longer built. Resolving it raises
EServiceProviderErrornaming the parameter, its type and the class. In 1.6 the container fell back to the constructor of an ancestor, such asTInterfacedObject’s, and handed the instance over with the dependency atnil. A class that declares no public constructor still uses its ancestor’s. - The container no longer chooses the constructor marked with
[ActivatorUtilitiesConstructor]. The mark only counts inTActivatorUtilities.CreateInstance, which takes the marked constructor as its only candidate and fails, naming its parameter, when it cannot satisfy it. In 1.6 the container chose the marked one whenever it could satisfy it. - What breaks: a registration that only worked thanks to the fallback, and one that relied on the container choosing the marked constructor.
25. Two constructors that compete make the choice fail
- In the container, when a constructor it can satisfy asks for a service the longest one does not ask for, resolving fails with
EServiceProviderErrornaming the class and both constructors. InTActivatorUtilities.CreateInstance, when the two longest it can satisfy tie. In 1.6 the first of the longest won.ValidateOnBuildreports it when the provider is built. - What breaks: a class with two constructors the container can satisfy that do not contain one another. Leave it a single public constructor, or make the shorter one ask only for what the longer one asks for.
26. A Singleton that resolves a Scoped
- Without
ValidateScopesit is no longer rejected. A Singleton that resolves a Scoped — by constructor, by factory, through anILazyor through the provider it kept — receives the root provider’s instance, the same oneProvidergives, which lives until the container shuts down. In 1.6 the resolution raised anExceptionwhile the Singleton was being built. - With
ValidateScopesit fails withEServiceProviderError, naming the Scoped and, while it is being built, the Singleton — also when a Singleton already built resolves it through the provider it kept. A Scoped resolved in a scope the Singleton opens itself is not a capture. - What breaks: code that caught that
Exception. A Singleton that needs a Scoped opens a scope throughIServiceScopeFactoryand resolves it there.
27. SQLCute: a derived compiler receives the compilation
- The protected methods of
TAnsiSqlCompilerthat emit SQL take the compilation as their first parameter, aTSqlCompilation, and add their bindings to it withC.AddBinding. They write every placeholder as?:ParamPlaceholderis gone, and a dialect that numbers its placeholders overridesFormatPlaceholder(Index), which receives the position counted from 0. - A
?in a raw expression is a placeholder in every dialect, counted with the others. A literal question mark, such as the?operator of Postgres onjsonb, is written\?. - What breaks: a compiler derived from
TAnsiSqlCompilerthat overrides an emitting method orParamPlaceholder, at compile time and in that method; and a raw expression with a literal?on Postgres, SQL Server or Oracle.
// Before
function TMyCompiler.CompileDateWhere(const Clause: TDateWhereClause): string;
begin
AddBinding(Clause.Value);
Result := 'CAST(' + WrapColumn(Clause.Column) + ' AS DATE) ' + Clause.Op + ' ' + ParamPlaceholder;
end;
// Now
function TMyCompiler.CompileDateWhere(const C: TSqlCompilation; const Clause: TDateWhereClause): string;
begin
C.AddBinding(Clause.Value);
Result := 'CAST(' + WrapColumn(Clause.Column) + ' AS DATE) ' + OperatorSymbol(Clause.Op) + ' ?';
end;28. SQLCute: an operator written as text is validated
Having,WhereColumnsandOrWhereColumnsrejectinandnot inwithEArgumentException; they used to emit=. A set of values goes throughWhereIn, and anINon an aggregate throughHavingRaw.- The operator of
WhereTime,OrWhereTimeand of a join by columns is validated: one of=,<>,!=,<,<=,>,>=,likeandnot like, and any other raisesEArgumentException. It used to be written into the SQL as it came. - In the AST, the
OpofTJoinClauseandTDateWhereClauseis aTWhereOp, not a string; a compiler emits it withOperatorSymbol. - What breaks: a call that passed
into those methods or an operator outside the list, and code that reads thatOpas a string.
29. SQLCute: comparing with Null asks about NULL
Where(col, Null)emitscol IS NULL, with no binding, and with any operator other than=it emitscol IS NOT NULL; the same inOrWhere,WhereNot,OrWhereNotandHaving.WhereNotkeeps its negation:NOT (col IS NULL). In 1.6 they boundNulltocol = ?, which matches no row on any engine.- What breaks: code that counted that binding, or relied on the condition matching nothing.
30. SQLCute: a logical value is compared with the dialect’s literal
WhereTrue('active')andWhere('active', True)emitactive = true, with no binding. The literal istrueandfalsein ANSI, Postgres, MySQL, Oracle and Firebird,1and0in SQLite, andcast(1 as bit)andcast(0 as bit)in SQL Server. With an operator other than=the comparison is negated:Where('active', '<>', False)emitsNOT (active = false). In 1.6 the value was bound toactive = ?.- Oracle understands
truefrom Oracle 23: before it, write the condition withWhereRaw. - A Firebird
SMALLINTflag column rejects= truewith a conversion error: compare it with a number,Where('flag', 1). - What breaks: code that counted the bindings, or relied on the binding being typed as a Boolean.
31. SQLCute: the column of Having is an identifier
Havingquotes its column like any other column of the dialect: it names a grouped column or the alias of an output column.Having('count(*)', '>', 5), the form the guide showed in 1.6, emits"count(*)" > ?in Postgres, and no such column exists.- An aggregate goes through
HavingRaw:HavingRaw('count(*) > 5'), which every engine accepts. An output alias inHAVINGis rejected by Postgres, SQL Server and Firebird. - What breaks: a
Havingwhose column is an aggregate expression, in a dialect that quotes.
32. Commons: cancelling runs every callback, last registered first, and the linked source is gone
Cancelruns the callbacks last registered first, as in .NET, and runs all of them even when one raises. What they raise comes out together in anEAggregateException(System.Threading) once all have run, also when only one raised: itsInnerExceptions[I], from0toCount - 1, are the callbacks’ exceptions. Each callback runs once, so cancelling again runs nothing.CreateLinkedCancellationTokenSourceis gone. A source that is cancelled when any of several tokens is cancelled is built by hand, registering on each token a callback that cancels it. The callback stays on each token until that token is cancelled, and keeps the source alive until then.- What breaks: code that relied on registration order; an
except on E: EMyErroraroundCancel, which now receives theEAggregateException; and every call toCreateLinkedCancellationTokenSource.
var Linked := CreateCancellationTokenSource;
for var Token in Tokens do
Token.Register(procedure begin Linked.Cancel; end);33. Hosting: the environment name is kept as given
EnvironmentNameis the name as it is written, trimmed.IsDevelopment,IsStaging,IsProductionandIsTestingcompare it without regard to case, sodevelopmentstill answersIsDevelopment.deve,stag,prodandtestare names like any other.DAF_ENVIRONMENT=prodis an environment calledprod:IsProductionanswersFalse, and the host loadsappsettings.prod.jsonand notappsettings.Production.json.- A name of your own is one more environment. In 1.6 a name that was not one of the four known ones, nor one of their abbreviations, ran as
Production. - What breaks: an abbreviation in
DAF_ENVIRONMENTor--environment, and a name of your own that relied on loadingappsettings.Production.json. Write the full name.
34. Commons: a program without a host ends with Ctrl+C again
- The console handler is installed while there is a listener of
TShutdownHook.OnShutdownRequested, which the host assigns when it starts and releases when it stops. In 1.6 it was installed when the unit loaded, so every process that linked Commons took every console event. - Without a listener, Ctrl+C and Ctrl+Break are left to the system, which ends the process. In 1.6 a process without a host survived them.
- With a host, every Ctrl+C and every Ctrl+Break ask for the shutdown. In 1.6 only the first Ctrl+C did.
- Closing the console window is left to the system, with a host or without one: the process ends without stopping the host, as in .NET.
- What breaks: a program without a host that relied on surviving a Ctrl+C. It assigns a listener to
TShutdownHook.OnShutdownRequestedfor as long as it has to hold the signal.
35. Web: a [FromBody] object lives as long as the request
- It is built with its parameterless constructor and filled from the body. In 1.6 the constructor did not run.
- The router frees it after executing the action’s result, as it frees the controller. In 1.6 it leaked, so an action that kept it worked.
- A body that is not a JSON object answers
400 Bad Request, naming the parameter, without invoking the action. - What breaks: an action that keeps the object past the request, in a field, a list or a service. It copies what it needs.
36. Web: requests are served at once
- The router no longer serializes requests. In 1.6 it served them one at a time behind a lock, so state shared between requests never saw two threads at once.
- What breaks: a Singleton, or a field a controller reaches outside its request, that is not safe across threads. It becomes thread-safe, or it is registered Scoped so each request has its own.
37. Web: the server’s error event receives the exception as an argument
IDafHttpServer.OnErroris aTServerErrorEvent:Senderis the server, and the exception comes inError. In 1.6 it was aTNotifyEventwhoseSenderwas the exception.- The exception stays its raiser’s:
Startraises it right after the event, so the handler reads it and neither frees nor keeps it. - What breaks: every
OnErrorhandler, which stops compiling until it takes the new signature.
// before
procedure TMyForm.ServerError(Sender: TObject);
// now
procedure TMyForm.ServerError(Sender: TObject; const Error: Exception);38. Configuration: a value that does not convert stops the binding
- Binding converts with the invariant culture: a decimal carries a point. In 1.6 it followed the process format, and with a decimal comma
1.5was bound as 15. - A present value that does not convert raises
EConfigurationBindingError, naming the value, the key and the type. In 1.6 the member was left at zero, or at the first value of an enumeration, and the application started. - An enumeration binds by the name of a value, in any case, or by its number.
- What breaks: a configuration with a value that does not convert, which now stops at binding, and a decimal written with a comma.
39. SQLCute: every compiler writes ?
- Every compiler writes
?for each binding, in every dialect: the placeholder FireDAC binds on any engine and translates into the engine’s own. In 1.6 the Postgres, SQL Server and Oracle compilers wrote$1,@p0and:p1. - A
\?comes out as??, FireDAC’s literal?, outside a string between single quotes, and as?inside one. - The native style brings the engine’s placeholders back, and writes
\?as?. Set it before the instance compiles;TPlaceholderStylelives inDaf.SQLCute.Compiler:
var Postgres := TPostgresCompiler.Create;
Postgres.Placeholders := TPlaceholderStyle.Native;
var Compiler: IQueryCompiler := Postgres;- What breaks: the SQL of those three compilers run through something that expects
$1,@p0or:p1.
How to do it
- Compile. Whatever is missing comes out as a compiler error, not as odd behaviour at run time.
- Replace each
AddModuleswith one call per module, and drop the force-linkuses. - Fix
requires: add the packages you now use — your variant’s (Daf.Cli,Daf.Web,Daf.VCLorDaf.FMX),Daf.SystemProcessif you launch processes, andDaf.CommandLineif you parse a command line — and removeDaf.ApplicationandDaf.SQLCute.Compilers. - Rename the types and units from the tables above.
- Compile again and run your tests. Then start the application once with
DAF_ENVIRONMENT=Development: the host validates the container, and a registration that went missing stops the build naming it. A Scoped service asked of the root provider is rejected too, when it is resolved: resolve it from a scope the work opens,Services.CreateScope. So is a Scoped a Singleton resolves: the Singleton resolves it in a scope it opens throughIServiceScopeFactory. - Sections 2, 6 to 13 and 15 to 39 carry run-time changes: the compiler says nothing about them. Read them against your own composition — scope lifetimes, duplicate registrations, logging rules and host settings.