You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by da...@apache.org on 2020/09/20 10:48:41 UTC

[isis] branch ISIS-2222 updated: ISIS-2222: reworking Command and other stuff

This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch ISIS-2222
in repository https://gitbox.apache.org/repos/asf/isis.git


The following commit(s) were added to refs/heads/ISIS-2222 by this push:
     new c2bd0db   ISIS-2222: reworking Command and other stuff
c2bd0db is described below

commit c2bd0db9b4d345fd10656aee4aa57b1084721128
Author: danhaywood <da...@haywood-associates.co.uk>
AuthorDate: Sat Sep 19 18:00:06 2020 +0100

     ISIS-2222: reworking Command and other stuff
---
 .../applib-ant/examples/annotation/Action.java     |   4 +-
 .../applib-ant/examples/annotation/Property.java   |   4 +-
 .../events/domain/AbstractDomainEvent.java         |   3 +-
 .../examples/util/schema/CommandDtoUtils.java      |  13 +-
 .../examples/services/clock/ClockService.java      |  11 +-
 .../examples/services/command/Command.java         | 178 ++++----
 .../examples/services/command/CommandContext.java  |  81 ----
 .../services/command/CommandExecutorService.java   |  16 +-
 .../services/command/CommandOutcomeHandler.java    |  24 +
 .../examples/services/command/CommandService.java  |  24 +-
 .../services/commanddto/HasCommandDto.java         |  12 +
 .../conmap/ContentMappingServiceForCommandDto.java | 106 +++++
 .../ContentMappingServiceForCommandsDto.java       |   2 +-
 .../conmap}/UserDataKeys.java                      |   2 +-
 .../processor}/CommandDtoProcessor.java            |  33 +-
 .../CommandDtoProcessorForActionAbstract.java      |   6 +-
 .../CommandDtoProcessorForPropertyAbstract.java    |   6 +-
 .../processor}/spi/CommandDtoProcessorService.java |  24 +-
 .../spi/CommandDtoProcessorServiceIdentity.java    |  23 +
 .../ContentMappingServiceForCommandDto.java        | 153 -------
 .../examples/services/iactn/Interaction.java       | 152 ++++---
 .../services/iactn/InteractionContext.java         |  33 +-
 .../services/metamodel/MetaModelService.java       |   4 +-
 .../TableColumnOrderForCollectionTypeAbstract.java |   4 +-
 .../services/wrapper/control/AsyncControl.java     |  18 +-
 .../wrapper/control/AsyncControlService.java       |  52 ---
 .../services/wrapper/control/ControlAbstract.java  |  10 +-
 .../org/apache/isis/applib/IsisModuleApplib.java   |   2 +
 .../command/spi/CommandServiceListener.java        |  27 ++
 .../services/wrapper/control/AsyncControl.java     |  15 +-
 .../services/wrapper/control/ControlAbstract.java  |   4 +-
 .../isis/applib/util/schema/CommandDtoUtils.java   |   8 +
 .../wrapper/control/AsyncControl_Test.java         |  13 +-
 .../applib/util/schema/CommandDtoUtils_Test.java   |  13 +-
 .../ActionAnnotationFacetFactoryTest_Command.java  |  49 +-
 .../DomainObjectLayoutFactoryTest.java             |   5 +-
 .../ImageValueSemanticsProviderAbstractTest.java   |   4 -
 .../command/CommandExecutorServiceDefault.java     |  10 +-
 .../wrapper/WrapperFactoryDefault.java             |  79 ++--
 .../WrapperFactoryDefault_wrappedObject_Test.java  | 503 ---------------------
 ...actoryDefault_wrappedObject_transient_Test.java | 299 ------------
 .../Action/command/ActionCommandJdo.java           |  61 +--
 .../Action/command/ActionCommandJdo.layout.xml     |  27 +-
 .../ActionCommandJdo_mixinUpdateProperty.java      |   5 +-
 ...andJdo_mixinUpdatePropertyCommandDisabled.java} |  12 +-
 ...mmandJdo_mixinUpdatePropertyMetaAnnotation.java |  10 +-
 ...ixinUpdatePropertyMetaAnnotationOverridden.java |   9 +-
 .../demoapp/dom/events/DemoEventSubscriber.java    |   2 +-
 .../src/main/java/demoapp/dom/menubars.layout.xml  |  13 +-
 .../java/demoapp/dom/services/ServicesMenu.java    |  48 ++
 .../WrapperFactoryJdo-description.adoc             |   2 +
 .../services/wrapperFactory/WrapperFactoryJdo.java | 127 ++++++
 .../wrapperFactory/WrapperFactoryJdo.layout.xml}   |  30 +-
 .../wrapperFactory/WrapperFactoryJdoEntities.java  |  33 ++
 .../WrapperFactoryJdoSeedService.java              |  39 ++
 ...WrapperFactoryJdo_mixinUpdatePropertyAsync.java |  44 ++
 ...WrapperFactoryJdo_updatePropertyAsyncMixin.java |  40 ++
 .../testdomain/conf/Configuration_headless.java    |  14 +-
 .../impl/CommandServiceListenerForJdo.java         |  39 +-
 .../commandlog/impl/jdo/CommandJdoRepository.java  |  23 +-
 .../commandlog/impl/ui/CommandServiceMenu.java     |  17 +-
 .../secondary/fetch/CommandFetcher_Test.java       |   2 +
 .../ui/auth/VaadinAuthenticationHandler.java       |   2 +-
 .../services/eventbus/ActionDomainEvent.java       |   6 +-
 .../adoc/modules/integtestsupport/pages/about.adoc |   2 +-
 .../applib/IsisIntegrationTestAbstract.java        |  13 +-
 .../applib/IsisInteractionHandler.java             |   2 +-
 tooling/javamodel/pom.xml                          |   1 +
 tooling/projectmodel/pom.xml                       |   1 +
 .../isis/viewer/wicket/ui/errors/JGrowlUtil.java   |   4 +-
 .../wicket/ui/panels/FormExecutorDefault.java      |   9 +-
 71 files changed, 1123 insertions(+), 1543 deletions(-)

diff --git a/api/applib/src/main/adoc/modules/applib-ant/examples/annotation/Action.java b/api/applib/src/main/adoc/modules/applib-ant/examples/annotation/Action.java
index 89bbaa5..f8c0703 100644
--- a/api/applib/src/main/adoc/modules/applib-ant/examples/annotation/Action.java
+++ b/api/applib/src/main/adoc/modules/applib-ant/examples/annotation/Action.java
@@ -28,8 +28,8 @@ import java.lang.annotation.Target;
 import org.apache.isis.applib.events.domain.ActionDomainEvent;
 import org.apache.isis.applib.services.commanddto.processor.CommandDtoProcessor;
 import org.apache.isis.applib.services.command.CommandService;
-import org.apache.isis.applib.services.conmap.command.ContentMappingServiceForCommandDto;
-import org.apache.isis.applib.services.conmap.command.ContentMappingServiceForCommandsDto;
+import org.apache.isis.applib.services.commanddto.conmap.ContentMappingServiceForCommandDto;
+import org.apache.isis.applib.services.commanddto.conmap.ContentMappingServiceForCommandsDto;
 import org.apache.isis.applib.value.Blob;
 import org.apache.isis.applib.value.Clob;
 
diff --git a/api/applib/src/main/adoc/modules/applib-ant/examples/annotation/Property.java b/api/applib/src/main/adoc/modules/applib-ant/examples/annotation/Property.java
index 0600ead..65db374 100644
--- a/api/applib/src/main/adoc/modules/applib-ant/examples/annotation/Property.java
+++ b/api/applib/src/main/adoc/modules/applib-ant/examples/annotation/Property.java
@@ -27,8 +27,8 @@ import java.lang.annotation.Target;
 
 import org.apache.isis.applib.events.domain.PropertyDomainEvent;
 import org.apache.isis.applib.services.commanddto.processor.CommandDtoProcessor;
-import org.apache.isis.applib.services.conmap.command.ContentMappingServiceForCommandDto;
-import org.apache.isis.applib.services.conmap.command.ContentMappingServiceForCommandsDto;
+import org.apache.isis.applib.services.commanddto.conmap.ContentMappingServiceForCommandDto;
+import org.apache.isis.applib.services.commanddto.conmap.ContentMappingServiceForCommandsDto;
 import org.apache.isis.applib.spec.Specification;
 import org.apache.isis.applib.value.Blob;
 import org.apache.isis.applib.value.Clob;
diff --git a/api/applib/src/main/adoc/modules/applib-classes/examples/events/domain/AbstractDomainEvent.java b/api/applib/src/main/adoc/modules/applib-classes/examples/events/domain/AbstractDomainEvent.java
index a61b5ae..9e6d244 100644
--- a/api/applib/src/main/adoc/modules/applib-classes/examples/events/domain/AbstractDomainEvent.java
+++ b/api/applib/src/main/adoc/modules/applib-classes/examples/events/domain/AbstractDomainEvent.java
@@ -22,7 +22,6 @@ import java.util.HashMap;
 import java.util.Map;
 
 import org.apache.isis.applib.Identifier;
-import org.apache.isis.applib.annotation.Programmatic;
 import org.apache.isis.applib.events.EventObjectBase;
 import org.apache.isis.applib.services.i18n.TranslatableString;
 import org.apache.isis.applib.util.ObjectContracts;
@@ -107,7 +106,7 @@ public abstract class AbstractDomainEvent<S> extends EventObjectBase<S> {
 
         /**
          * When the {@link org.apache.isis.applib.services.command.Command} is made available on the
-         * {@link org.apache.isis.applib.events.domain.ActionDomainEvent} via {@link Interaction#getCommand()}.
+         * {@link org.apache.isis.applib.events.domain.ActionDomainEvent} via {@link org.apache.isis.applib.services.iactn.Interaction#getCommand()}.
          */
         public boolean isExecutingOrLater() {
             return isExecuting() || isExecuted();
diff --git a/api/applib/src/main/adoc/modules/applib-classes/examples/util/schema/CommandDtoUtils.java b/api/applib/src/main/adoc/modules/applib-classes/examples/util/schema/CommandDtoUtils.java
index 9b6c93b0..75512e5 100644
--- a/api/applib/src/main/adoc/modules/applib-classes/examples/util/schema/CommandDtoUtils.java
+++ b/api/applib/src/main/adoc/modules/applib-classes/examples/util/schema/CommandDtoUtils.java
@@ -30,7 +30,10 @@ import javax.xml.bind.JAXBException;
 import javax.xml.bind.Marshaller;
 import javax.xml.bind.Unmarshaller;
 
+import org.apache.isis.applib.services.bookmark.Bookmark;
 import org.apache.isis.applib.util.JaxbUtil;
+import org.apache.isis.core.commons.internal.base._Strings;
+import org.apache.isis.core.commons.internal.exceptions._Exceptions;
 import org.apache.isis.core.commons.internal.resources._Resources;
 import org.apache.isis.schema.cmd.v2.ActionDto;
 import org.apache.isis.schema.cmd.v2.CommandDto;
@@ -134,13 +137,21 @@ public final class CommandDtoUtils {
 
     public static void setUserData(
             final CommandDto dto, final String key, final String value) {
-        if(dto == null || key == null) {
+        if(dto == null || key == null || _Strings.isNullOrEmpty(value)) {
             return;
         }
         final MapDto userData = userDataFor(dto);
         CommonDtoUtils.putMapKeyValue(userData, key, value);
     }
 
+    public static void setUserData(
+            final CommandDto dto, final String key, final Bookmark bookmark) {
+        if(dto == null || key == null || bookmark == null) {
+            return;
+        }
+        setUserData(dto, key, bookmark.toString());
+    }
+
     private static MapDto userDataFor(final CommandDto commandDto) {
         MapDto userData = commandDto.getUserData();
         if(userData == null) {
diff --git a/api/applib/src/main/adoc/modules/applib-svc/examples/services/clock/ClockService.java b/api/applib/src/main/adoc/modules/applib-svc/examples/services/clock/ClockService.java
index 597a56b..4583a4c 100644
--- a/api/applib/src/main/adoc/modules/applib-svc/examples/services/clock/ClockService.java
+++ b/api/applib/src/main/adoc/modules/applib-svc/examples/services/clock/ClockService.java
@@ -18,15 +18,11 @@
  */
 package org.apache.isis.applib.services.clock;
 
-import java.sql.Timestamp;
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-import java.time.OffsetDateTime;
 import java.util.TimeZone;
 
 import javax.inject.Named;
+import javax.xml.datatype.XMLGregorianCalendar;
 
-import org.joda.time.DateTime;
 import org.joda.time.DateTimeZone;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.context.annotation.Primary;
@@ -35,6 +31,7 @@ import org.springframework.stereotype.Service;
 
 import org.apache.isis.applib.annotation.OrderPrecedence;
 import org.apache.isis.applib.clock.Clock;
+import org.apache.isis.applib.jaxb.JavaSqlXMLGregorianCalendarMarshalling;
 
 /**
  * This service allows an application to be decoupled from the system time.  The most common use case is in support of
@@ -66,6 +63,10 @@ public class ClockService {
         return Clock.getTimeAsJavaSqlTimestamp();
     }
 
+    public XMLGregorianCalendar nowAsXMLGregorianCalendar() {
+        return JavaSqlXMLGregorianCalendarMarshalling.toXMLGregorianCalendar(nowAsJavaSqlTimestamp());
+    }
+
     public long nowAsMillis() {
         return Clock.getEpochMillis();
     }
diff --git a/api/applib/src/main/adoc/modules/applib-svc/examples/services/command/Command.java b/api/applib/src/main/adoc/modules/applib-svc/examples/services/command/Command.java
index 62df89d..dc55a40 100644
--- a/api/applib/src/main/adoc/modules/applib-svc/examples/services/command/Command.java
+++ b/api/applib/src/main/adoc/modules/applib-svc/examples/services/command/Command.java
@@ -22,16 +22,16 @@ import java.sql.Timestamp;
 import java.util.UUID;
 
 import org.apache.isis.applib.events.domain.ActionDomainEvent;
+import org.apache.isis.applib.jaxb.JavaSqlXMLGregorianCalendarMarshalling;
 import org.apache.isis.applib.services.HasUniqueId;
 import org.apache.isis.applib.services.HasUsername;
 import org.apache.isis.applib.services.bookmark.Bookmark;
+import org.apache.isis.applib.services.commanddto.HasCommandDto;
 import org.apache.isis.applib.services.iactn.Interaction;
 import org.apache.isis.applib.services.wrapper.WrapperFactory;
 import org.apache.isis.applib.services.wrapper.control.AsyncControl;
 import org.apache.isis.schema.cmd.v2.CommandDto;
 
-import static org.apache.isis.applib.jaxb.JavaSqlXMLGregorianCalendarMarshalling.toTimestamp;
-
 import lombok.Getter;
 
 /**
@@ -67,46 +67,70 @@ import lombok.Getter;
  * </p>
  */
 // tag::refguide[]
-public class Command implements HasUniqueId, HasUsername {
-
-    public Command() {
-        this(UUID.randomUUID());
-    }
-    public Command(final Command parent, final CommandDto commandDto) {
-        this(UUID.fromString(commandDto.getTransactionId()));
-        outcomeHandler().setCommandDto(commandDto);
-        outcomeHandler().setParent(parent);
-        outcomeHandler().setUsername(commandDto.getUser());
-        outcomeHandler().setTimestamp(toTimestamp(commandDto.getTimestamp()));
-    }
-
-    private Command(final UUID uuid) {
-        this.uniqueId = uuid;
-    }
-
-
-    @Getter
-    private UUID uniqueId;
+public class Command implements HasUniqueId, HasUsername, HasCommandDto {
 
     // end::refguide[]
     /**
+     * Unique identifier for the command.
+     *
+     * <p>
+     *     Derived from {@link #getCommandDto()}'s {@link CommandDto#getTransactionId()}
+     * </p>
+     */
+    @Override
+    // tag::refguide[]
+    public UUID getUniqueId() {                 // <.>
+        // ...
+        // end::refguide[]
+        return commandDto != null
+                ? UUID.fromString(commandDto.getTransactionId())
+                : null;
+    }
+    /**
      * The user that created the command.
+     *
+     * <p>
+     *     Derived from {@link #getCommandDto()}'s {@link CommandDto#getUser()}
+     * </p>
      */
+    @Override
     // tag::refguide[]
-    @Getter
-    private String username;                    // <.>
-    // end::refguide[]
+    public String getUsername() {               // <.>
+        // ...
+        // end::refguide[]
+        return commandDto != null
+                ? commandDto.getUser()
+                : null;
+    }
 
     /**
      * The date/time at which this command was created.
+     *
+     * <p>
+     *     Derived from {@link #getCommandDto()}'s {@link CommandDto#getTimestamp()}.
+     * </p>
      */
     // tag::refguide[]
-    @Getter
-    private Timestamp timestamp;                // <.>
-    // end::refguide[]
+    public Timestamp getTimestamp() {           // <.>
+        // ...
+        // end::refguide[]
+        return commandDto != null
+                ? JavaSqlXMLGregorianCalendarMarshalling.toTimestamp(commandDto.getTimestamp())
+                : null;
+    }
 
     /**
      * Serializable representation of the action invocation/property edit.
+     *
+     * <p>
+     *     When the framework sets this (through an internal API), it is
+     *     expected to have {@link CommandDto#getTransactionId()},
+     *     {@link CommandDto#getUser()}, {@link CommandDto#getTimestamp()},
+     *     {@link CommandDto#getTargets()} and {@link CommandDto#getMember()}
+     *     to be populated.  The {@link #getUniqueId()}, {@link #getUsername()},
+     *     {@link #getTimestamp()} and {@link #getTarget()} are all derived
+     *     from the provided {@link CommandDto}.
+     * </p>
      */
     // tag::refguide[]
     @Getter
@@ -114,32 +138,40 @@ public class Command implements HasUniqueId, HasUsername {
     // end::refguide[]
 
     /**
-     * Also available in {@link #getCommandDto()}, is the {@link Bookmark} of
+     * Derived from {@link #getCommandDto()}, is the {@link Bookmark} of
      * the target object (entity or service) on which this action/edit was performed.
      */
     // tag::refguide[]
-    @Getter
-    private Bookmark target;                    // <.>
+    public Bookmark getTarget() {               // <.>
+        return commandDto != null
+                ? Bookmark.from(commandDto.getTargets().getOid().get(0))
+                : null;
+    }
     // end::refguide[]
 
     /**
-     * Also available in {@link #getCommandDto()}, holds a string
+     * Derived from {@link #getCommandDto()}, holds a string
      * representation of the invoked action, or the edited property.
      */
     // tag::refguide[]
-    @Getter
-    private String logicalMemberIdentifier;     // <.>
+    public String getLogicalMemberIdentifier() {    // <.>
+        return commandDto != null
+                    ? commandDto.getMember().getLogicalMemberIdentifier()
+                    : null;
+    }
     // end::refguide[]
 
     /**
-     * For commands created through the {@link WrapperFactory} (using
-     * {@link WrapperFactory#asyncWrap(Object, AsyncControl)} or
-     * {@link WrapperFactory#asyncWrapMixin(Class, Object, AsyncControl)}),
+     * For async commands created through the {@link WrapperFactory},
      * captures the parent command.
      *
      * <p>
      *     Will return <code>null</code> if there is no parent.
      * </p>
+     *
+     * @see WrapperFactory#asyncWrap(Object, AsyncControl)
+     * @see WrapperFactory#asyncWrapMixin(Class, Object, AsyncControl)
+     *
      */
     // tag::refguide[]
     @Getter
@@ -147,17 +179,11 @@ public class Command implements HasUniqueId, HasUsername {
     // end::refguide[]
 
     /**
-     * For an command that has actually been executed, holds the date/time at which the {@link Interaction} that
-     * executed the command started.
+     * For an command that has actually been executed, holds the date/time at
+     * which the {@link Interaction} that executed the command started.
      *
-     * <p>
-     *     Previously this field was deprecated (on the basis that the startedAt is also held in
-     *     {@link Interaction.Execution#getStartedAt()}). However, this property is now used in master/slave
-     *     replay scenarios which may query a persisted Command.
-     * </p>
-     *
-     * See also {@link Interaction#getCurrentExecution()} and
-     * {@link Interaction.Execution#getStartedAt()}.
+     * @see Interaction#getCurrentExecution()
+     * @see Interaction.Execution#getStartedAt()
      */
     // tag::refguide[]
     @Getter
@@ -234,26 +260,18 @@ public class Command implements HasUniqueId, HasUsername {
     // end::refguide[]
 
 
-    private final Command.Internal INTERNAL = new Internal();
+    private final Updater UPDATER = new Updater();
 
-    public class Internal {
-        /**
-         * <b>NOT API</b>: intended to be called only by the framework.
-         *
-         * <p>
-         * Implementation notes: set when the Isis PersistenceSession is opened.
-         */
-        public void setUsername(String username) {
-            Command.this.username = username;
-        }
+    public class Updater implements CommandOutcomeHandler {
         /**
          * <b>NOT API</b>: intended to be called only by the framework.
          *
          * <p>
-         * Implementation notes: set when the Isis PersistenceSession is opened.
+         * Implementation notes: set when the action is invoked (in the <tt>ActionInvocationFacet</tt>).
+         * @param commandDto
          */
-        public void setTimestamp(Timestamp timestamp) {
-            Command.this.timestamp = timestamp;
+        public void setCommandDto(final CommandDto commandDto, final int targetIdx) {
+            Command.this.commandDto = commandDto;
         }
         /**
          * <b>NOT API</b>: intended to be called only by the framework.
@@ -268,55 +286,37 @@ public class Command implements HasUniqueId, HasUsername {
         }
         /**
          * <b>NOT API</b>: intended to be called only by the framework.
-         *
-         * <p>
-         * Implementation notes: set when the action is invoked (in the <tt>ActionInvocationFacet</tt>).
-         * @param commandDto
          */
-        public void setCommandDto(CommandDto commandDto) {
-            Command.this.commandDto = commandDto;
-        }
-        /**
-         * <b>NOT API</b>: intended to be called only by the framework.
-         *
-         * <p>
-         * Implementation notes: set when the action is invoked (in the ActionInvocationFacet).
-         */
-        public void setTarget(Bookmark target) {
-            Command.this.target = target;
-        }
-        /**
-         * <b>NOT API</b>: intended to be called only by the framework.
-         *
-         * <p>
-         * Implementation notes: set when the action is invoked (in
-         * <tt>ActionInvocationFacet</tt>) or property is edited (in
-         * <tt>PropertySetterFacet</tt>).
-         */
-        public void setLogicalMemberIdentifier(String logicalMemberIdentifier) {
-            Command.this.logicalMemberIdentifier = logicalMemberIdentifier;
+        @Override
+        public Timestamp getStartedAt() {
+            return Command.this.getStartedAt();
         }
         /**
          * <b>NOT API</b>: intended to be called only by the framework.
          */
+        @Override
         public void setStartedAt(Timestamp startedAt) {
             Command.this.startedAt = startedAt;
         }
         /**
          * <b>NOT API</b>: intended to be called only by the framework.
          */
+        @Override
         public void setCompletedAt(final Timestamp completed) {
             Command.this.completedAt = completed;
         }
         /**
          * <b>NOT API</b>: intended to be called only by the framework.
          */
+        @Override
         public void setResult(final Bookmark result) {
             Command.this.result = result;
         }
+
         /**
          * <b>NOT API</b>: intended to be called only by the framework.
          */
+        @Override
         public void setException(final Throwable exception) {
             Command.this.exception = exception;
         }
@@ -336,8 +336,8 @@ public class Command implements HasUniqueId, HasUsername {
     /**
      * <b>NOT API</b>: intended to be called only by the framework.
      */
-    public Command.Internal internal() {
-        return INTERNAL;
+    public Updater updater() {
+        return UPDATER;
     }
 
 
diff --git a/api/applib/src/main/adoc/modules/applib-svc/examples/services/command/CommandContext.java b/api/applib/src/main/adoc/modules/applib-svc/examples/services/command/CommandContext.java
deleted file mode 100644
index df73176..0000000
--- a/api/applib/src/main/adoc/modules/applib-svc/examples/services/command/CommandContext.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- */
-package org.apache.isis.applib.services.command;
-
-import java.util.Optional;
-
-import javax.inject.Inject;
-import javax.inject.Named;
-
-import org.springframework.beans.factory.DisposableBean;
-import org.springframework.beans.factory.annotation.Qualifier;
-import org.springframework.context.annotation.Primary;
-import org.springframework.core.annotation.Order;
-import org.springframework.stereotype.Service;
-
-import org.apache.isis.applib.annotation.IsisInteractionScope;
-import org.apache.isis.applib.annotation.OrderPrecedence;
-import org.apache.isis.applib.services.TransactionScopeListener;
-import org.apache.isis.applib.services.inject.ServiceInjector;
-import org.apache.isis.applib.services.metrics.MetricsService;
-import org.apache.isis.applib.services.registry.ServiceRegistry;
-
-import lombok.Getter;
-import lombok.RequiredArgsConstructor;
-
-/**
- * This service (API and implementation) provides access to context information about any {@link Command}.
- *
- * This implementation has no UI and there is only one implementation (this class) in applib, so it is annotated with
- * {@link org.apache.isis.applib.annotation.DomainService}.  This means that it is automatically registered and
- * available for use; no further configuration is required.
- */
-// tag::refguide[]
-@Service
-@Named("isisApplib.CommandContext")
-@Order(OrderPrecedence.EARLY - 10) // before ChangedObjectService
-@Primary
-@Qualifier("Default")
-@IsisInteractionScope
-@RequiredArgsConstructor(onConstructor_ = {@Inject})
-//@Log4j2
-public class CommandContext implements DisposableBean {
-
-    private final MetricsService metricsService;
-
-    @Getter
-    private Command command;
-
-    // end::refguide[]
-    /**
-     * <b>NOT API</b>: intended to be called only by the framework.
-     */
-    public void setCommand(final Command command) {
-        this.command = command;
-    }
-
-    @Override
-    public void destroy() throws Exception {
-        setCommand(null);
-    }
-
-
-    // tag::refguide[]
-}
-// end::refguide[]
diff --git a/api/applib/src/main/adoc/modules/applib-svc/examples/services/command/CommandExecutorService.java b/api/applib/src/main/adoc/modules/applib-svc/examples/services/command/CommandExecutorService.java
index 4634dc5..4fce410 100644
--- a/api/applib/src/main/adoc/modules/applib-svc/examples/services/command/CommandExecutorService.java
+++ b/api/applib/src/main/adoc/modules/applib-svc/examples/services/command/CommandExecutorService.java
@@ -53,22 +53,22 @@ public interface CommandExecutorService {
      */
     // tag::refguide[]
     Bookmark executeCommand(
-            SudoPolicy sudoPolicy,          // <.>
-            Command command                 // <.>
+            SudoPolicy sudoPolicy,                  // <.>
+            Command command                         // <.>
     );
 
     Bookmark executeCommand(
-            SudoPolicy sudoPolicy,          // <.>
-            CommandDto commandDto           // <.>
-    );
+            SudoPolicy sudoPolicy,                  // <.>
+            CommandDto commandDto,                  // <.>
+            CommandOutcomeHandler outcomeHandler);  // <.>
 
     Bookmark executeCommand(
-            Command command                 // <.>
+            Command command                         // <.>
     );
 
     Bookmark executeCommand(
-            CommandDto commandDto           // <.>
-    );
+            CommandDto commandDto,                  // <.>
+            CommandOutcomeHandler outcomeHandler);  // <.>
 
 }
 // end::refguide[]
diff --git a/api/applib/src/main/adoc/modules/applib-svc/examples/services/command/CommandOutcomeHandler.java b/api/applib/src/main/adoc/modules/applib-svc/examples/services/command/CommandOutcomeHandler.java
new file mode 100644
index 0000000..ccd9530
--- /dev/null
+++ b/api/applib/src/main/adoc/modules/applib-svc/examples/services/command/CommandOutcomeHandler.java
@@ -0,0 +1,24 @@
+package org.apache.isis.applib.services.command;
+
+import java.sql.Timestamp;
+
+import org.apache.isis.applib.services.bookmark.Bookmark;
+
+public interface CommandOutcomeHandler {
+
+    CommandOutcomeHandler NULL = new CommandOutcomeHandler() {
+        @Override public Timestamp getStartedAt() { return null; }
+        @Override public void setStartedAt(Timestamp startedAt) { }
+        @Override public void setCompletedAt(Timestamp completedAt) { }
+        @Override public void setResult(Bookmark resultBookmark) { }
+        @Override public void setException(Throwable throwable) { }
+    };
+
+    Timestamp getStartedAt();
+    void setStartedAt(Timestamp startedAt);
+
+    void setCompletedAt(Timestamp completedAt);
+
+    void setResult(Bookmark resultBookmark);
+    void setException(Throwable throwable);
+}
diff --git a/api/applib/src/main/adoc/modules/applib-svc/examples/services/command/CommandService.java b/api/applib/src/main/adoc/modules/applib-svc/examples/services/command/CommandService.java
index ce19893..eae10f0 100644
--- a/api/applib/src/main/adoc/modules/applib-svc/examples/services/command/CommandService.java
+++ b/api/applib/src/main/adoc/modules/applib-svc/examples/services/command/CommandService.java
@@ -18,6 +18,7 @@
  */
 package org.apache.isis.applib.services.command;
 
+import java.sql.Timestamp;
 import java.util.List;
 
 import javax.inject.Inject;
@@ -30,9 +31,12 @@ import org.springframework.stereotype.Service;
 
 import org.apache.isis.applib.annotation.IsisInteractionScope;
 import org.apache.isis.applib.annotation.OrderPrecedence;
+import org.apache.isis.applib.services.clock.ClockService;
 import org.apache.isis.applib.services.command.Command;
 import org.apache.isis.applib.services.command.spi.CommandServiceListener;
+import org.apache.isis.applib.services.user.UserService;
 
+import lombok.val;
 import lombok.extern.log4j.Log4j2;
 
 @Service
@@ -46,22 +50,6 @@ public class CommandService {
 
     // end::refguide[]
     /**
-     * Simply instantiates the appropriate instance of the {@link Command}.
-     *
-     * <p>
-     * Its members will be populated automatically by the framework (the
-     * {@link Command}'s {@link Command#getTimestamp()},
-     * {@link Command#getUsername()} and {@link Command#getUniqueId()}).
-     * </p>
-     */
-    // tag::refguide[]
-    public Command create() {                   // <.>
-        return new Command();
-    }
-
-    // end::refguide[]
-
-    /**
      * &quot;Complete&quot; the command, providing an opportunity ot persist
      * a memento of the command if the
      * {@link Command#isSystemStateChanged() system state has changed}.
@@ -80,7 +68,9 @@ public class CommandService {
             return;
         }
 
-        log.debug("complete: {}, systemStateChanged {}", command.getLogicalMemberIdentifier(), command.isSystemStateChanged());
+        log.debug("complete: {}, systemStateChanged {}",
+                command.getLogicalMemberIdentifier(),
+                command.isSystemStateChanged());
 
     // tag::refguide[]
         commandServiceListeners.forEach(x -> x.onComplete(command));
diff --git a/api/applib/src/main/adoc/modules/applib-svc/examples/services/commanddto/HasCommandDto.java b/api/applib/src/main/adoc/modules/applib-svc/examples/services/commanddto/HasCommandDto.java
new file mode 100644
index 0000000..6c9d631
--- /dev/null
+++ b/api/applib/src/main/adoc/modules/applib-svc/examples/services/commanddto/HasCommandDto.java
@@ -0,0 +1,12 @@
+package org.apache.isis.applib.services.commanddto;
+
+import org.apache.isis.schema.cmd.v2.CommandDto;
+
+/**
+ * Objects implementing this interface will be processed automatically by
+ * {@link org.apache.isis.applib.services.commanddto.conmap.ContentMappingServiceForCommandDto}.
+ */
+public interface HasCommandDto {
+
+    CommandDto getCommandDto();
+}
diff --git a/api/applib/src/main/adoc/modules/applib-svc/examples/services/commanddto/conmap/ContentMappingServiceForCommandDto.java b/api/applib/src/main/adoc/modules/applib-svc/examples/services/commanddto/conmap/ContentMappingServiceForCommandDto.java
new file mode 100644
index 0000000..6e1edfb
--- /dev/null
+++ b/api/applib/src/main/adoc/modules/applib-svc/examples/services/commanddto/conmap/ContentMappingServiceForCommandDto.java
@@ -0,0 +1,106 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.isis.applib.services.commanddto.conmap;
+
+import java.sql.Timestamp;
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.ws.rs.core.MediaType;
+
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.annotation.Primary;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Service;
+
+import org.apache.isis.applib.annotation.OrderPrecedence;
+import org.apache.isis.applib.jaxb.JavaSqlXMLGregorianCalendarMarshalling;
+import org.apache.isis.applib.services.bookmark.Bookmark;
+import org.apache.isis.applib.services.command.Command;
+import org.apache.isis.applib.services.commanddto.HasCommandDto;
+import org.apache.isis.applib.services.commanddto.processor.CommandDtoProcessor;
+import org.apache.isis.applib.services.conmap.ContentMappingService;
+import org.apache.isis.applib.services.commanddto.processor.spi.CommandDtoProcessorService;
+import org.apache.isis.applib.services.metamodel.MetaModelService;
+import org.apache.isis.applib.util.schema.CommandDtoUtils;
+import org.apache.isis.core.commons.internal.exceptions._Exceptions;
+import org.apache.isis.schema.cmd.v2.CommandDto;
+import org.apache.isis.schema.common.v2.PeriodDto;
+
+import lombok.val;
+
+@Service
+@Named("isisApplib.ContentMappingServiceForCommandDto")
+@Order(OrderPrecedence.EARLY)
+@Primary
+@Qualifier("CommandDto")
+public class ContentMappingServiceForCommandDto implements ContentMappingService {
+
+    @Override
+    public Object map(Object object, final List<MediaType> acceptableMediaTypes) {
+        final boolean supported = Util.isSupported(CommandDto.class, acceptableMediaTypes);
+        if(!supported) {
+            return null;
+        }
+
+        return asProcessedDto(object);
+    }
+
+    CommandDto asProcessedDto(final Object object) {
+        val commandDto = asCommandDto(object);
+        return asProcessedDto(object, commandDto);
+    }
+
+    private CommandDto asCommandDto(Object object) {
+        if(object instanceof CommandDto) {
+            return (CommandDto) object;
+        }
+        if(object instanceof HasCommandDto) {
+            return ((HasCommandDto) object).getCommandDto();
+        }
+        return null;
+    }
+
+    private CommandDto asProcessedDto(final Object domainObject, CommandDto commandDto) {
+
+        // global processors
+        for (final CommandDtoProcessorService commandDtoProcessorService : commandDtoProcessorServices) {
+            commandDto = commandDtoProcessorService.process(domainObject, commandDto);
+            if(commandDto == null) {
+                // any processor could return null, effectively breaking the chain.
+                return null;
+            }
+        }
+
+        // specific processor for this specific member (action or property)
+        val logicalMemberId = commandDto.getMember().getLogicalMemberIdentifier();
+        final CommandDtoProcessor commandDtoProcessor =
+                metaModelService.commandDtoProcessorFor(logicalMemberId);
+        if (commandDtoProcessor == null) {
+            return commandDto;
+        }
+        return commandDtoProcessor.process(commandDto);
+    }
+
+
+    @Inject MetaModelService metaModelService;
+    @Inject List<CommandDtoProcessorService> commandDtoProcessorServices;
+
+}
diff --git a/api/applib/src/main/adoc/modules/applib-svc/examples/services/conmap/command/ContentMappingServiceForCommandsDto.java b/api/applib/src/main/adoc/modules/applib-svc/examples/services/commanddto/conmap/ContentMappingServiceForCommandsDto.java
similarity index 98%
rename from api/applib/src/main/adoc/modules/applib-svc/examples/services/conmap/command/ContentMappingServiceForCommandsDto.java
rename to api/applib/src/main/adoc/modules/applib-svc/examples/services/commanddto/conmap/ContentMappingServiceForCommandsDto.java
index ade5070..4d491b2 100644
--- a/api/applib/src/main/adoc/modules/applib-svc/examples/services/conmap/command/ContentMappingServiceForCommandsDto.java
+++ b/api/applib/src/main/adoc/modules/applib-svc/examples/services/commanddto/conmap/ContentMappingServiceForCommandsDto.java
@@ -16,7 +16,7 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-package org.apache.isis.applib.services.conmap.command;
+package org.apache.isis.applib.services.commanddto.conmap;
 
 import java.util.List;
 
diff --git a/api/applib/src/main/adoc/modules/applib-svc/examples/services/conmap/command/UserDataKeys.java b/api/applib/src/main/adoc/modules/applib-svc/examples/services/commanddto/conmap/UserDataKeys.java
similarity index 95%
rename from api/applib/src/main/adoc/modules/applib-svc/examples/services/conmap/command/UserDataKeys.java
rename to api/applib/src/main/adoc/modules/applib-svc/examples/services/commanddto/conmap/UserDataKeys.java
index bc1bb2f..4fc4e59 100644
--- a/api/applib/src/main/adoc/modules/applib-svc/examples/services/conmap/command/UserDataKeys.java
+++ b/api/applib/src/main/adoc/modules/applib-svc/examples/services/commanddto/conmap/UserDataKeys.java
@@ -16,7 +16,7 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-package org.apache.isis.applib.services.conmap.command;
+package org.apache.isis.applib.services.commanddto.conmap;
 
 import org.apache.isis.schema.cmd.v2.CommandDto;
 
diff --git a/api/applib/src/main/adoc/modules/applib-svc/examples/services/command/CommandDtoProcessor.java b/api/applib/src/main/adoc/modules/applib-svc/examples/services/commanddto/processor/CommandDtoProcessor.java
similarity index 57%
rename from api/applib/src/main/adoc/modules/applib-svc/examples/services/command/CommandDtoProcessor.java
rename to api/applib/src/main/adoc/modules/applib-svc/examples/services/commanddto/processor/CommandDtoProcessor.java
index 60edc81..a0706ad 100644
--- a/api/applib/src/main/adoc/modules/applib-svc/examples/services/command/CommandDtoProcessor.java
+++ b/api/applib/src/main/adoc/modules/applib-svc/examples/services/commanddto/processor/CommandDtoProcessor.java
@@ -16,33 +16,42 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-package org.apache.isis.applib.services.command;
+package org.apache.isis.applib.services.commanddto.processor;
 
 import org.apache.isis.schema.cmd.v2.CommandDto;
 
+/**
+ * Refine (or possibly ignore) a command when replicating from primary
+ * to secondary.
+ */
 // tag::refguide[]
 public interface CommandDtoProcessor {
 
     // end::refguide[]
     /**
-     * Returning <tt>null</tt> means that the command's DTO is effectively excluded from any list.
-     * If replicating from master to slave, this allows commands that can't be replicated to be ignored.
-     * @param command
-     * @param commandDto
+     * The implementation can if necessary refine or alter the
+     * {@link CommandDto} to be replicated from primary to secondary.
+     *
+     * <p>
+     *     That said, the most common use case is to return <code>null</code>,
+     *     which results in the command effectively being ignore.
+     * </p>
+     *
+     * @param commandDto - to be processed
+     * @return <tt>null</tt> means that the command's DTO is effectively
+     *         excluded.
      */
     // tag::refguide[]
-    CommandDto process(final Command command, CommandDto commandDto);   // <.>
+    CommandDto process(CommandDto commandDto);   // <.>
 
     // end::refguide[]
     /**
-     * Convenience implementation to simply indicate that no DTO should be returned for a command,
-     * effectively ignoring it for replay purposes.
+     * Convenience implementation to simply indicate that no DTO should be
+     * returned for a command, effectively ignoring it for replication purposes.
      */
-    public static class Null implements CommandDtoProcessor {
+    class Null implements CommandDtoProcessor {
         @Override
-        public CommandDto process(
-                final Command command,
-                final CommandDto commandDto) {
+        public CommandDto process(final CommandDto commandDto) {
             return null;
         }
     }
diff --git a/api/applib/src/main/adoc/modules/applib-svc/examples/services/command/CommandDtoProcessorForActionAbstract.java b/api/applib/src/main/adoc/modules/applib-svc/examples/services/commanddto/processor/CommandDtoProcessorForActionAbstract.java
similarity index 91%
rename from api/applib/src/main/adoc/modules/applib-svc/examples/services/command/CommandDtoProcessorForActionAbstract.java
rename to api/applib/src/main/adoc/modules/applib-svc/examples/services/commanddto/processor/CommandDtoProcessorForActionAbstract.java
index 97c9780..b887323 100644
--- a/api/applib/src/main/adoc/modules/applib-svc/examples/services/command/CommandDtoProcessorForActionAbstract.java
+++ b/api/applib/src/main/adoc/modules/applib-svc/examples/services/commanddto/processor/CommandDtoProcessorForActionAbstract.java
@@ -16,8 +16,9 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-package org.apache.isis.applib.services.command;
+package org.apache.isis.applib.services.commanddto.processor;
 
+import org.apache.isis.applib.services.command.Command;
 import org.apache.isis.schema.cmd.v2.ActionDto;
 import org.apache.isis.schema.cmd.v2.CommandDto;
 import org.apache.isis.schema.cmd.v2.ParamDto;
@@ -27,9 +28,6 @@ import org.apache.isis.schema.cmd.v2.ParamsDto;
  * Convenience adapter for command processors for action invocations.
  */
 public abstract class CommandDtoProcessorForActionAbstract implements CommandDtoProcessor {
-    protected CommandDto asDto(final Command command) {
-        return command.getCommandDto();
-    }
     protected ActionDto getActionDto(final CommandDto commandDto) {
         return (ActionDto) commandDto.getMember();
     }
diff --git a/api/applib/src/main/adoc/modules/applib-svc/examples/services/command/CommandDtoProcessorForPropertyAbstract.java b/api/applib/src/main/adoc/modules/applib-svc/examples/services/commanddto/processor/CommandDtoProcessorForPropertyAbstract.java
similarity index 88%
rename from api/applib/src/main/adoc/modules/applib-svc/examples/services/command/CommandDtoProcessorForPropertyAbstract.java
rename to api/applib/src/main/adoc/modules/applib-svc/examples/services/commanddto/processor/CommandDtoProcessorForPropertyAbstract.java
index b8e2dd3..e833809 100644
--- a/api/applib/src/main/adoc/modules/applib-svc/examples/services/command/CommandDtoProcessorForPropertyAbstract.java
+++ b/api/applib/src/main/adoc/modules/applib-svc/examples/services/commanddto/processor/CommandDtoProcessorForPropertyAbstract.java
@@ -16,8 +16,9 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-package org.apache.isis.applib.services.command;
+package org.apache.isis.applib.services.commanddto.processor;
 
+import org.apache.isis.applib.services.command.Command;
 import org.apache.isis.schema.cmd.v2.CommandDto;
 import org.apache.isis.schema.cmd.v2.PropertyDto;
 
@@ -26,9 +27,6 @@ import org.apache.isis.schema.cmd.v2.PropertyDto;
  */
 public abstract class CommandDtoProcessorForPropertyAbstract
 implements CommandDtoProcessor {
-    protected CommandDto asDto(final Command commandWithDto) {
-        return commandWithDto.getCommandDto();
-    }
     protected PropertyDto getPropertyDto(final CommandDto commandDto) {
         return (PropertyDto) commandDto.getMember();
     }
diff --git a/api/applib/src/main/adoc/modules/applib-svc/examples/services/conmap/command/spi/CommandDtoProcessorService.java b/api/applib/src/main/adoc/modules/applib-svc/examples/services/commanddto/processor/spi/CommandDtoProcessorService.java
similarity index 61%
rename from api/applib/src/main/adoc/modules/applib-svc/examples/services/conmap/command/spi/CommandDtoProcessorService.java
rename to api/applib/src/main/adoc/modules/applib-svc/examples/services/commanddto/processor/spi/CommandDtoProcessorService.java
index 2d0de3d..c645d04 100644
--- a/api/applib/src/main/adoc/modules/applib-svc/examples/services/conmap/command/spi/CommandDtoProcessorService.java
+++ b/api/applib/src/main/adoc/modules/applib-svc/examples/services/commanddto/processor/spi/CommandDtoProcessorService.java
@@ -16,12 +16,17 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-package org.apache.isis.applib.services.conmap.command.spi;
+package org.apache.isis.applib.services.commanddto.processor.spi;
 
-import org.apache.isis.applib.annotation.Programmatic;
-import org.apache.isis.applib.services.command.Command;
+import javax.annotation.Nullable;
+import javax.inject.Named;
+
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Service;
+
+import org.apache.isis.applib.annotation.OrderPrecedence;
 import org.apache.isis.applib.services.commanddto.processor.CommandDtoProcessor;
-import org.apache.isis.applib.services.conmap.command.ContentMappingServiceForCommandDto;
+import org.apache.isis.applib.services.commanddto.conmap.ContentMappingServiceForCommandDto;
 import org.apache.isis.schema.cmd.v2.CommandDto;
 
 /**
@@ -32,7 +37,16 @@ import org.apache.isis.schema.cmd.v2.CommandDto;
 // tag::refguide[]
 public interface CommandDtoProcessorService {
 
-    CommandDto process(final Command command, CommandDto commandDto);
+    /**
+     * @param domainObject - is the target that acts as the source of the
+     *                       {@link CommandDto}.
+     * @param commandDto - is either <code>null</code>, or is passed from a
+     *                     previous implementation for further refinement.
+     * @return
+     */
+    CommandDto process(final Object domainObject, @Nullable final CommandDto commandDto);
+
 
 }
 // end::refguide[]
+
diff --git a/api/applib/src/main/adoc/modules/applib-svc/examples/services/commanddto/processor/spi/CommandDtoProcessorServiceIdentity.java b/api/applib/src/main/adoc/modules/applib-svc/examples/services/commanddto/processor/spi/CommandDtoProcessorServiceIdentity.java
new file mode 100644
index 0000000..aee41b0
--- /dev/null
+++ b/api/applib/src/main/adoc/modules/applib-svc/examples/services/commanddto/processor/spi/CommandDtoProcessorServiceIdentity.java
@@ -0,0 +1,23 @@
+package org.apache.isis.applib.services.commanddto.processor.spi;
+
+import javax.inject.Named;
+
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Service;
+
+import org.apache.isis.applib.annotation.OrderPrecedence;
+import org.apache.isis.schema.cmd.v2.CommandDto;
+
+/**
+ * At least one implementation is required.
+ */
+@Service
+@Named("isisApplib.CommandDtoProcessorServiceIdentity")
+@Order(OrderPrecedence.LAST)
+public class CommandDtoProcessorServiceIdentity implements CommandDtoProcessorService {
+
+    @Override
+    public CommandDto process(final Object domainObject, final CommandDto commandDto) {
+        return commandDto;
+    }
+}
diff --git a/api/applib/src/main/adoc/modules/applib-svc/examples/services/conmap/command/ContentMappingServiceForCommandDto.java b/api/applib/src/main/adoc/modules/applib-svc/examples/services/conmap/command/ContentMappingServiceForCommandDto.java
deleted file mode 100644
index 2a6f488..0000000
--- a/api/applib/src/main/adoc/modules/applib-svc/examples/services/conmap/command/ContentMappingServiceForCommandDto.java
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- */
-package org.apache.isis.applib.services.conmap.command;
-
-import java.sql.Timestamp;
-import java.util.List;
-import java.util.stream.Collectors;
-
-import javax.inject.Inject;
-import javax.inject.Named;
-import javax.ws.rs.core.MediaType;
-
-import org.springframework.beans.factory.annotation.Qualifier;
-import org.springframework.context.annotation.Primary;
-import org.springframework.core.annotation.Order;
-import org.springframework.stereotype.Service;
-
-import org.apache.isis.applib.annotation.OrderPrecedence;
-import org.apache.isis.applib.jaxb.JavaSqlXMLGregorianCalendarMarshalling;
-import org.apache.isis.applib.services.bookmark.Bookmark;
-import org.apache.isis.applib.services.command.Command;
-import org.apache.isis.applib.services.commanddto.processor.CommandDtoProcessor;
-import org.apache.isis.applib.services.conmap.ContentMappingService;
-import org.apache.isis.applib.services.conmap.command.spi.CommandDtoProcessorService;
-import org.apache.isis.applib.services.metamodel.MetaModelService;
-import org.apache.isis.applib.util.schema.CommandDtoUtils;
-import org.apache.isis.core.commons.internal.exceptions._Exceptions;
-import org.apache.isis.schema.cmd.v2.CommandDto;
-import org.apache.isis.schema.common.v2.PeriodDto;
-
-@Service
-@Named("isisApplib.ContentMappingServiceForCommandDto")
-@Order(OrderPrecedence.EARLY)
-@Primary
-@Qualifier("CommandDto")
-public class ContentMappingServiceForCommandDto implements ContentMappingService {
-
-    @Override
-    public Object map(Object object, final List<MediaType> acceptableMediaTypes) {
-        final boolean supported = Util.isSupported(CommandDto.class, acceptableMediaTypes);
-        if(!supported) {
-            return null;
-        }
-
-        return asProcessedDto(object);
-    }
-
-    /**
-     * Not part of the {@link ContentMappingService} API.
-     */
-    public CommandDto map(final Command command) {
-        return asProcessedDto(command);
-    }
-
-    CommandDto asProcessedDto(final Object object) {
-        if (!(object instanceof Command)) {
-            return null;
-        }
-        final Command command = (Command) object;
-        return asProcessedDto(command);
-    }
-
-    private CommandDto asProcessedDto(final Command command) {
-        if(command == null) {
-            return null;
-        }
-        CommandDto commandDto = command.getCommandDto();
-
-        // global processors
-        for (final CommandDtoProcessorService commandDtoProcessorService : commandDtoProcessorServices) {
-            commandDto = commandDtoProcessorService.process(command, commandDto);
-            if(commandDto == null) {
-                // any processor could return null, effectively breaking the chain.
-                return null;
-            }
-        }
-
-        // specific processors for this specific member (action or property)
-        final CommandDtoProcessor commandDtoProcessor =
-                metaModelService.commandDtoProcessorFor(commandDto.getMember().getLogicalMemberIdentifier());
-        if (commandDtoProcessor == null) {
-            return commandDto;
-        }
-        return commandDtoProcessor.process(command, commandDto);
-    }
-
-
-    /**
-     * Uses the SPI infrastructure to copy over standard properties from {@link Command} to {@link CommandDto}.
-     */
-    @Service
-    @Named("isisApplib.ContentMappingServiceForCommandDto.CopyOverFromCommand")
-    // specify quite a high priority since custom processors will probably want to run after this one
-    // (but can choose to run before if they wish)
-    @Order(OrderPrecedence.EARLY)
-    @Qualifier("Command")
-    public static class CopyOverFromCommand implements CommandDtoProcessorService {
-
-        @Override
-        public CommandDto process(final Command command, CommandDto commandDto) {
-
-            // for some reason this isn't being persisted initially, so patch it in.  TODO: should fix this
-            commandDto.setUser(command.getUsername());
-
-            // the timestamp field was only introduced in v1.4 of cmd.xsd, so there's no guarantee
-            // it will have been populated.  We therefore copy the value in from CommandWithDto entity.
-            if(commandDto.getTimestamp() == null) {
-                final Timestamp timestamp = command.getTimestamp();
-                commandDto.setTimestamp(JavaSqlXMLGregorianCalendarMarshalling.toXMLGregorianCalendar(timestamp));
-            }
-
-            final Bookmark result = command.getResult();
-            CommandDtoUtils.setUserData(commandDto,
-                    UserDataKeys.RESULT, result != null ? result.toString() : null);
-            // knowing whether there was an exception is on the master is used to determine whether to
-            // continue when replayed on the slave if an exception occurs there also
-            Throwable exception = command.getException();
-            CommandDtoUtils.setUserData(commandDto,
-                    UserDataKeys.EXCEPTION,
-                        _Exceptions.asStacktrace(exception));
-
-            PeriodDto timings = CommandDtoUtils.timingsFor(commandDto);
-            timings.setStartedAt(JavaSqlXMLGregorianCalendarMarshalling.toXMLGregorianCalendar(command.getStartedAt()));
-            timings.setCompletedAt(JavaSqlXMLGregorianCalendarMarshalling.toXMLGregorianCalendar(command.getCompletedAt()));
-
-            return commandDto;
-        }
-    }
-
-
-    @Inject
-    MetaModelService metaModelService;
-
-    @Inject
-    List<CommandDtoProcessorService> commandDtoProcessorServices;
-
-}
diff --git a/api/applib/src/main/adoc/modules/applib-svc/examples/services/iactn/Interaction.java b/api/applib/src/main/adoc/modules/applib-svc/examples/services/iactn/Interaction.java
index 5cfb218..aba8f10 100644
--- a/api/applib/src/main/adoc/modules/applib-svc/examples/services/iactn/Interaction.java
+++ b/api/applib/src/main/adoc/modules/applib-svc/examples/services/iactn/Interaction.java
@@ -50,7 +50,6 @@ import org.apache.isis.schema.ixn.v2.ObjectCountsDto;
 import org.apache.isis.schema.ixn.v2.PropertyEditDto;
 
 import lombok.Getter;
-import lombok.Setter;
 import lombok.val;
 import lombok.extern.log4j.Log4j2;
 
@@ -83,8 +82,17 @@ import lombok.extern.log4j.Log4j2;
 @Log4j2
 public class Interaction implements HasUniqueId {
 
-    @Getter @Setter
-    private UUID uniqueId;                          // <.>
+    public Interaction(final Command command) {
+        this.command = command;
+    }
+
+    @Getter
+    private Command command;                        // <.>
+
+    @Override
+    public UUID getUniqueId() {                     // <.>
+        return command.getUniqueId();
+    }
 
     // end::refguide[]
     private final List<Execution<?,?>> executionGraphs = _Lists.newArrayList();
@@ -159,16 +167,31 @@ public class Interaction implements HasUniqueId {
      * Use the provided {@link MemberExecutor} to invoke an action, with the provided
      * {@link ActionInvocation} capturing the details of said action.
      * </p>
+     *
+     * <p>
+     *     Because this both pushes an {@link Interaction.Execution} to
+     *     represent the action invocation and then pops it, that completed
+     *     execution is accessible at {@link Interaction#getPriorExecution()}.
+     * </p>
      */
     public Object execute(
             final MemberExecutor<ActionInvocation> memberExecutor,
             final ActionInvocation actionInvocation,
             final ClockService clockService,
-            final MetricsService metricsService) {
+            final MetricsService metricsService,
+            final Command command) {
 
-        push(actionInvocation);
+        pushAndStart(actionInvocation, clockService, metricsService, command);
+        try {
+            return executeInternal(memberExecutor, actionInvocation);
+        } finally {
+            popAndComplete(clockService, metricsService);
+        }
+    }
 
-        return executeInternal(memberExecutor, actionInvocation, clockService, metricsService);
+    private void pushAndStart(ActionInvocation actionInvocation, ClockService clockService, MetricsService metricsService, Command command) {
+        push(actionInvocation);
+        start(actionInvocation, clockService, metricsService, command);
     }
 
     /**
@@ -178,51 +201,50 @@ public class Interaction implements HasUniqueId {
      * Use the provided {@link MemberExecutor} to edit a property, with the provided
      * {@link PropertyEdit} capturing the details of said property edit.
      * </p>
+     *
+     * <p>
+     *     Because this both pushes an {@link Interaction.Execution} to
+     *     represent the property edit and then pops it, that completed
+     *     execution is accessible at {@link Interaction#getPriorExecution()}.
+     * </p>
      */
     public Object execute(
             final MemberExecutor<PropertyEdit> memberExecutor,
             final PropertyEdit propertyEdit,
             final ClockService clockService,
-            final MetricsService metricsService) {
+            final MetricsService metricsService,
+            final Command command) {
 
         push(propertyEdit);
-
-        return executeInternal(memberExecutor, propertyEdit, clockService, metricsService);
+        start(propertyEdit, clockService, metricsService, command);
+        try {
+            return executeInternal(memberExecutor, propertyEdit);
+        } finally {
+            popAndComplete(clockService, metricsService);
+        }
     }
 
-    private <T extends Execution<?,?>> Object executeInternal(
-            final MemberExecutor<T> memberExecutor,
-            final T execution,
-            final ClockService clockService,
-            final MetricsService metricsService) {
-
-        // as a convenience, since in all cases we want the command to start when the first 
-        // interaction executes, we populate the command here.
+    private <T extends Execution<?,?>> Object executeInternal(MemberExecutor<T> memberExecutor, T execution) {
 
         try {
-            try {
-                Object result = memberExecutor.execute(execution);
-                execution.setReturned(result);
-                return result;
-            } catch (Exception ex) {
-
-                //TODO there is an issue with exceptions getting swallowed, unless this is fixed,
-                // we rather print all of them, no matter whether recognized or not later on
-                // examples are IllegalArgument- or NullPointer- exceptions being swallowed when using the
-                // WrapperFactory utilizing async calls
-                log.error("failed to execute an interaction", ex);
-
-                // just because an exception has thrown, does not mean it is that significant;
-                // it could be that it is recognized by an ExceptionRecognizer and is not severe
-                // eg. unique index violation in the DB
-                getCurrentExecution().setThrew(ex);
-
-                // propagate (as in previous design); caller will need to trap and decide
-                throw ex;
-            }
-        } finally {
-            final Timestamp completedAt = clockService.nowAsJavaSqlTimestamp();
-            pop(completedAt, metricsService);
+            Object result = memberExecutor.execute(execution);
+            execution.setReturned(result);
+            return result;
+        } catch (Exception ex) {
+
+            //TODO there is an issue with exceptions getting swallowed, unless this is fixed,
+            // we rather print all of them, no matter whether recognized or not later on
+            // examples are IllegalArgument- or NullPointer- exceptions being swallowed when using the
+            // WrapperFactory utilizing async calls
+            log.error("failed to execute an interaction", ex);
+
+            // just because an exception has thrown, does not mean it is that significant;
+            // it could be that it is recognized by an ExceptionRecognizer and is not severe
+            // eg. unique index violation in the DB
+            getCurrentExecution().setThrew(ex);
+
+            // propagate (as in previous design); caller will need to trap and decide
+            throw ex;
         }
     }
 
@@ -253,6 +275,20 @@ public class Interaction implements HasUniqueId {
         return execution;
     }
 
+    private void start(
+            final Interaction.Execution<?,?> execution,
+            final ClockService clockService,
+            final MetricsService metricsService,
+            final Command command) {
+        // set the startedAt (and update command if this is the top-most member execution)
+        // (this isn't done within Interaction#execute(...) because it requires the DTO
+        // to have been set on the current execution).
+        val startedAt = execution.start(clockService, metricsService);
+        if(command.getStartedAt() == null) {
+            command.updater().setStartedAt(startedAt);
+        }
+    }
+
     /**
      * <b>NOT API</b>: intended to be called only by the framework.
      *
@@ -261,13 +297,16 @@ public class Interaction implements HasUniqueId {
      * from the stack of events held by the command.
      * </p>
      */
-    private Execution<?,?> pop(
-            final Timestamp completedAt,
+    private Execution<?,?> popAndComplete(
+            final ClockService clockService,
             final MetricsService metricsService) {
+
         if(currentExecution == null) {
             throw new IllegalStateException("No current execution to pop");
         }
         final Execution<?,?> popped = currentExecution;
+
+        final Timestamp completedAt = clockService.nowAsJavaSqlTimestamp();
         popped.setCompletedAt(completedAt, metricsService);
 
         moveCurrentTo(currentExecution.getParent());
@@ -386,8 +425,18 @@ public class Interaction implements HasUniqueId {
         // tag::refguide-2[]
         @Getter
         private final String targetMember;
-
         // end::refguide-2[]
+
+
+        /**
+         * Captures metrics before the Execution Dto is present.
+         */
+        private int numberObjectsLoadedBefore;
+        /**
+         * Captures metrics before the Execution Dto is present.
+         */
+        private int numberObjectsDirtiedBefore;
+
         protected Execution(
                 final Interaction interaction,
                 final InteractionType interactionType,
@@ -575,15 +624,8 @@ public class Interaction implements HasUniqueId {
                         final int numberObjectsDirtied) {
 
                     execution.startedAt = timestamp;
-
-                    final MetricsDto metricsDto = metricsFor(execution);
-
-                    final PeriodDto periodDto = timingsFor(metricsDto);
-                    periodDto.setStartedAt(JavaSqlXMLGregorianCalendarMarshalling.toXMLGregorianCalendar(timestamp));
-
-                    final ObjectCountsDto objectCountsDto = objectCountsFor(metricsDto);
-                    numberObjectsLoadedFor(objectCountsDto).setBefore(numberObjectsLoaded);
-                    numberObjectsDirtiedFor(objectCountsDto).setBefore(numberObjectsDirtied);
+                    execution.numberObjectsLoadedBefore = numberObjectsLoaded;
+                    execution.numberObjectsDirtiedBefore = numberObjectsLoaded;
                 }
 
                 // tag::refguide-2a[]
@@ -603,9 +645,13 @@ public class Interaction implements HasUniqueId {
                     final MetricsDto metricsDto = metricsFor(execution);
 
                     final PeriodDto periodDto = timingsFor(metricsDto);
-                    periodDto.setCompletedAt(JavaSqlXMLGregorianCalendarMarshalling.toXMLGregorianCalendar(timestamp));
+                    periodDto.setStartedAt(JavaSqlXMLGregorianCalendarMarshalling.toXMLGregorianCalendar(execution.startedAt));
+                    periodDto.setCompletedAt(JavaSqlXMLGregorianCalendarMarshalling.toXMLGregorianCalendar(execution.completedAt));
 
                     final ObjectCountsDto objectCountsDto = objectCountsFor(metricsDto);
+                    numberObjectsLoadedFor(objectCountsDto).setBefore(execution.numberObjectsLoadedBefore);
+                    numberObjectsDirtiedFor(objectCountsDto).setBefore(execution.numberObjectsDirtiedBefore);
+
                     numberObjectsLoadedFor(objectCountsDto).setAfter(numberObjectsLoaded);
                     numberObjectsDirtiedFor(objectCountsDto).setAfter(numberObjectsDirtied);
                 }
diff --git a/api/applib/src/main/adoc/modules/applib-svc/examples/services/iactn/InteractionContext.java b/api/applib/src/main/adoc/modules/applib-svc/examples/services/iactn/InteractionContext.java
index 86cb93c..de41f1e 100644
--- a/api/applib/src/main/adoc/modules/applib-svc/examples/services/iactn/InteractionContext.java
+++ b/api/applib/src/main/adoc/modules/applib-svc/examples/services/iactn/InteractionContext.java
@@ -18,8 +18,10 @@
  */
 package org.apache.isis.applib.services.iactn;
 
+import javax.inject.Inject;
 import javax.inject.Named;
 
+import org.springframework.beans.factory.DisposableBean;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.context.annotation.Primary;
 import org.springframework.core.annotation.Order;
@@ -28,8 +30,15 @@ import org.springframework.stereotype.Service;
 import org.apache.isis.applib.annotation.DomainService;
 import org.apache.isis.applib.annotation.IsisInteractionScope;
 import org.apache.isis.applib.annotation.OrderPrecedence;
+import org.apache.isis.applib.services.TransactionScopeListener;
+import org.apache.isis.applib.services.command.Command;
+import org.apache.isis.applib.services.inject.ServiceInjector;
+import org.apache.isis.applib.services.metrics.MetricsService;
 
 import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.val;
+import lombok.extern.log4j.Log4j2;
 
 /**
  * This service (API and implementation) provides access to context information about any {@link Interaction}.
@@ -41,14 +50,18 @@ import lombok.Getter;
 // tag::refguide[]
 @Service
 @Named("isisApplib.InteractionContext")
-@Order(OrderPrecedence.MIDPOINT)
+@Order(OrderPrecedence.EARLY - 10) // before ChangedObjectService
 @Primary
 @Qualifier("Default")
 @IsisInteractionScope
-//@Log4j2
-public class InteractionContext {
+@RequiredArgsConstructor(onConstructor_ = {@Inject})
+@Log4j2
+public class InteractionContext implements TransactionScopeListener, DisposableBean {
 
     // end::refguide[]
+
+    private final MetricsService metricsService;
+
     /**
      * The currently active {@link Interaction} for this thread.
      */
@@ -65,5 +78,19 @@ public class InteractionContext {
     }
 
     // tag::refguide[]
+
+    @Override
+    public void onTransactionEnded() {
+        val command = getInteraction().getCommand();
+        command.updater().setSystemStateChanged(
+                command.isSystemStateChanged() ||
+                        metricsService.numberObjectsDirtied() > 0);
+    }
+
+    @Override
+    public void destroy() throws Exception {
+        setInteraction(null);
+    }
+
 }
 // end::refguide[]
diff --git a/api/applib/src/main/adoc/modules/applib-svc/examples/services/metamodel/MetaModelService.java b/api/applib/src/main/adoc/modules/applib-svc/examples/services/metamodel/MetaModelService.java
index feac932..03dd90d 100644
--- a/api/applib/src/main/adoc/modules/applib-svc/examples/services/metamodel/MetaModelService.java
+++ b/api/applib/src/main/adoc/modules/applib-svc/examples/services/metamodel/MetaModelService.java
@@ -79,7 +79,7 @@ public interface MetaModelService {
     BeanSort sortOf(Bookmark bookmark, Mode mode);      // <.>
 
     CommandDtoProcessor commandDtoProcessorFor(         // <.>
-                            String memberIdentifier);
+                            String logicalMemberIdentifier);
 
     // end::refguide[]
     // tag::refguide-1[]
@@ -92,7 +92,7 @@ public interface MetaModelService {
         STRICT,
         // end::refguide-1[]
         /**
-         * If the {@link #sortOf(Class, Mode) sort of} object type is unknown, then return {@link Sort#UNKNOWN}.
+         * If the {@link #sortOf(Class, Mode) sort of} object type is unknown, then return {@link BeanSort#UNKNOWN}.
          */
         // tag::refguide-1[]
         RELAXED
diff --git a/api/applib/src/main/adoc/modules/applib-svc/examples/services/tablecol/TableColumnOrderForCollectionTypeAbstract.java b/api/applib/src/main/adoc/modules/applib-svc/examples/services/tablecol/TableColumnOrderForCollectionTypeAbstract.java
index 4d604e3..33bc35d 100644
--- a/api/applib/src/main/adoc/modules/applib-svc/examples/services/tablecol/TableColumnOrderForCollectionTypeAbstract.java
+++ b/api/applib/src/main/adoc/modules/applib-svc/examples/services/tablecol/TableColumnOrderForCollectionTypeAbstract.java
@@ -34,7 +34,7 @@ public abstract class TableColumnOrderForCollectionTypeAbstract<T>
             final String collectionId,
             final Class<?> collectionType,
             final List<String> propertyIds) {
-        if (!this.collectionType.isAssignableFrom(collectionType)) {
+        if (! this.collectionType.isAssignableFrom(collectionType)) {
             return propertyIds;
         }
         return orderParented(parent, collectionId, propertyIds);
@@ -50,7 +50,7 @@ public abstract class TableColumnOrderForCollectionTypeAbstract<T>
     public final List<String> orderStandalone(
             final Class<?> collectionType,
             final List<String> propertyIds) {
-        if (this.collectionType.isAssignableFrom(collectionType)) {
+        if (! this.collectionType.isAssignableFrom(collectionType)) {
             return propertyIds;
         }
         return orderStandalone(propertyIds);
diff --git a/api/applib/src/main/adoc/modules/applib-svc/examples/services/wrapper/control/AsyncControl.java b/api/applib/src/main/adoc/modules/applib-svc/examples/services/wrapper/control/AsyncControl.java
index d5fccec..fa6ab86 100644
--- a/api/applib/src/main/adoc/modules/applib-svc/examples/services/wrapper/control/AsyncControl.java
+++ b/api/applib/src/main/adoc/modules/applib-svc/examples/services/wrapper/control/AsyncControl.java
@@ -39,14 +39,18 @@ import lombok.extern.log4j.Log4j2;
 @Log4j2
 public class AsyncControl<R> extends ControlAbstract<AsyncControl<R>> {
 
-    public static AsyncControl<Void> control() {                        // <.>
-        return new AsyncControl<>();
+    public static AsyncControl<Void> returningVoid() {                        // <.>
+        return new AsyncControl<>(Void.class);
     }
-    public static <X> AsyncControl<X> control(final Class<X> clazz) {   // <.>
-        return new AsyncControl<>();
+    public static <X> AsyncControl<X> returning(final Class<X> cls) {     // <.>
+        return new AsyncControl<X>(cls);
     }
 
-    private AsyncControl() {
+    @Getter
+    private final Class<R> returnType;                                  // <.>
+
+    private AsyncControl(final Class<R> returnType) {
+        this.returnType = returnType;
         with(exception -> {                                             // <.>
             log.error(logMessage(), exception);
             return null;
@@ -107,9 +111,8 @@ public class AsyncControl<R> extends ControlAbstract<AsyncControl<R>> {
      * Contains the result of the invocation.  However, if an entity is returned, then the object is automatically
      * detached because the persistence session within which it was obtained will have been closed already.
      */
-    @Setter(AccessLevel.PACKAGE)
     // tag::refguide[]
-    @Getter
+    @Getter @Setter
     private Future<R> future;                                           // <.>
 
     // end::refguide[]
@@ -127,6 +130,7 @@ public class AsyncControl<R> extends ControlAbstract<AsyncControl<R>> {
         }
         return buf.toString();
     }
+
     // tag::refguide[]
     // ...
 }
diff --git a/api/applib/src/main/adoc/modules/applib-svc/examples/services/wrapper/control/AsyncControlService.java b/api/applib/src/main/adoc/modules/applib-svc/examples/services/wrapper/control/AsyncControlService.java
deleted file mode 100644
index c112868..0000000
--- a/api/applib/src/main/adoc/modules/applib-svc/examples/services/wrapper/control/AsyncControlService.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- */
-package org.apache.isis.applib.services.wrapper.control;
-
-import java.lang.reflect.Method;
-import java.util.concurrent.Future;
-
-import org.springframework.stereotype.Component;
-
-import org.apache.isis.applib.services.bookmark.Bookmark;
-
-import lombok.val;
-
-/**
- * For framework internal use.
- */
-@Component
-public class AsyncControlService {
-
-    public <R> AsyncControl<R> init(AsyncControl<R> asyncControl, Method method, Bookmark bookmark) {
-        asyncControl.setMethod(method);
-        asyncControl.setBookmark(bookmark);
-        return asyncControl;
-    }
-
-    public <R> AsyncControl<R> update(AsyncControl<R> asyncControl, Future<R> future) {
-        asyncControl.setFuture(future);
-        return asyncControl;
-    }
-
-    public <R> boolean shouldCheckRules(AsyncControl<R> asyncControl) {
-        val executionModes = asyncControl.getExecutionModes();
-        val skipRules = executionModes.contains(ExecutionMode.SKIP_RULE_VALIDATION);
-        return !skipRules;
-    }
-}
diff --git a/api/applib/src/main/adoc/modules/applib-svc/examples/services/wrapper/control/ControlAbstract.java b/api/applib/src/main/adoc/modules/applib-svc/examples/services/wrapper/control/ControlAbstract.java
index 0716096..2ed93d6 100644
--- a/api/applib/src/main/adoc/modules/applib-svc/examples/services/wrapper/control/ControlAbstract.java
+++ b/api/applib/src/main/adoc/modules/applib-svc/examples/services/wrapper/control/ControlAbstract.java
@@ -38,17 +38,15 @@ public class ControlAbstract<T extends ControlAbstract<T>> {
     }
 
     /**
-     * Set by framework.
+     * Set by framework; simply used for logging purposes.
      */
-    @Setter(AccessLevel.PACKAGE)
-    @Getter(AccessLevel.PACKAGE)
+    @Getter(AccessLevel.PACKAGE) @Setter
     private Method method;
 
     /**
-     * Set by framework.
+     * Set by framework; simply used for logging purposes.
      */
-    @Setter(AccessLevel.PACKAGE)
-    @Getter(AccessLevel.PACKAGE)
+    @Getter(AccessLevel.PACKAGE) @Setter
     private Bookmark bookmark;
 
     // tag::refguide[]
diff --git a/api/applib/src/main/java/org/apache/isis/applib/IsisModuleApplib.java b/api/applib/src/main/java/org/apache/isis/applib/IsisModuleApplib.java
index 4f3fdca..ada8693 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/IsisModuleApplib.java
+++ b/api/applib/src/main/java/org/apache/isis/applib/IsisModuleApplib.java
@@ -34,6 +34,7 @@ import org.apache.isis.applib.services.bookmark.BookmarkHolder_lookup;
 import org.apache.isis.applib.services.bookmark.BookmarkHolder_object;
 import org.apache.isis.applib.services.clock.ClockService;
 import org.apache.isis.applib.services.command.CommandService;
+import org.apache.isis.applib.services.command.spi.CommandServiceListener;
 import org.apache.isis.applib.services.commanddto.conmap.ContentMappingServiceForCommandDto;
 import org.apache.isis.applib.services.commanddto.conmap.ContentMappingServiceForCommandsDto;
 import org.apache.isis.applib.services.commanddto.processor.spi.CommandDtoProcessorServiceIdentity;
@@ -73,6 +74,7 @@ import org.apache.isis.schema.IsisModuleSchema;
         ClockService.class,
         CommandDtoProcessorServiceIdentity.class,
         CommandService.class,
+        CommandServiceListener.Null.class,
         ContentMappingServiceForCommandDto.class,
         ContentMappingServiceForCommandsDto.class,
         InteractionContext.class,
diff --git a/api/applib/src/main/java/org/apache/isis/applib/services/command/spi/CommandServiceListener.java b/api/applib/src/main/java/org/apache/isis/applib/services/command/spi/CommandServiceListener.java
index 5d10793..ba6b727 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/services/command/spi/CommandServiceListener.java
+++ b/api/applib/src/main/java/org/apache/isis/applib/services/command/spi/CommandServiceListener.java
@@ -18,8 +18,18 @@
  */
 package org.apache.isis.applib.services.command.spi;
 
+import javax.inject.Named;
+
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.annotation.Primary;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Service;
+
+import org.apache.isis.applib.annotation.OrderPrecedence;
 import org.apache.isis.applib.services.command.Command;
 
+import lombok.extern.log4j.Log4j2;
+
 /**
  * SPI
  */
@@ -36,5 +46,22 @@ public interface CommandServiceListener {
      */
     // tag::refguide[]
     void onComplete(final Command command);           // <.>
+
+    /**
+     * At least one implementation is required to satisfy injection point into
+     * {@link org.apache.isis.applib.services.command.CommandService}.
+     */
+    @Service
+    @Named("isisApplib.CommandServiceListenerNull")
+    @Order(OrderPrecedence.LATE)
+    @Qualifier("Null")
+    @Log4j2
+    public static class Null implements CommandServiceListener {
+
+        @Override
+        public void onComplete(Command command) {
+
+        }
+    }
 }
 // end::refguide[]
diff --git a/api/applib/src/main/java/org/apache/isis/applib/services/wrapper/control/AsyncControl.java b/api/applib/src/main/java/org/apache/isis/applib/services/wrapper/control/AsyncControl.java
index 3019281..fa6ab86 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/services/wrapper/control/AsyncControl.java
+++ b/api/applib/src/main/java/org/apache/isis/applib/services/wrapper/control/AsyncControl.java
@@ -39,14 +39,18 @@ import lombok.extern.log4j.Log4j2;
 @Log4j2
 public class AsyncControl<R> extends ControlAbstract<AsyncControl<R>> {
 
-    public static AsyncControl<Void> control() {                        // <.>
-        return new AsyncControl<>();
+    public static AsyncControl<Void> returningVoid() {                        // <.>
+        return new AsyncControl<>(Void.class);
     }
-    public static <X> AsyncControl<X> control(final Class<X> clazz) {   // <.>
-        return new AsyncControl<>();
+    public static <X> AsyncControl<X> returning(final Class<X> cls) {     // <.>
+        return new AsyncControl<X>(cls);
     }
 
-    private AsyncControl() {
+    @Getter
+    private final Class<R> returnType;                                  // <.>
+
+    private AsyncControl(final Class<R> returnType) {
+        this.returnType = returnType;
         with(exception -> {                                             // <.>
             log.error(logMessage(), exception);
             return null;
@@ -126,6 +130,7 @@ public class AsyncControl<R> extends ControlAbstract<AsyncControl<R>> {
         }
         return buf.toString();
     }
+
     // tag::refguide[]
     // ...
 }
diff --git a/api/applib/src/main/java/org/apache/isis/applib/services/wrapper/control/ControlAbstract.java b/api/applib/src/main/java/org/apache/isis/applib/services/wrapper/control/ControlAbstract.java
index 6088154..2ed93d6 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/services/wrapper/control/ControlAbstract.java
+++ b/api/applib/src/main/java/org/apache/isis/applib/services/wrapper/control/ControlAbstract.java
@@ -38,13 +38,13 @@ public class ControlAbstract<T extends ControlAbstract<T>> {
     }
 
     /**
-     * Set by framework.
+     * Set by framework; simply used for logging purposes.
      */
     @Getter(AccessLevel.PACKAGE) @Setter
     private Method method;
 
     /**
-     * Set by framework.
+     * Set by framework; simply used for logging purposes.
      */
     @Getter(AccessLevel.PACKAGE) @Setter
     private Bookmark bookmark;
diff --git a/api/applib/src/main/java/org/apache/isis/applib/util/schema/CommandDtoUtils.java b/api/applib/src/main/java/org/apache/isis/applib/util/schema/CommandDtoUtils.java
index 75512e5..8747371 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/util/schema/CommandDtoUtils.java
+++ b/api/applib/src/main/java/org/apache/isis/applib/util/schema/CommandDtoUtils.java
@@ -152,6 +152,14 @@ public final class CommandDtoUtils {
         setUserData(dto, key, bookmark.toString());
     }
 
+    public static void clearUserData(
+            final CommandDto dto, final String key) {
+        if(dto == null || key == null) {
+            return;
+        }
+        userDataFor(dto).getEntry().removeIf(x -> x.getKey().equals(key));
+    }
+
     private static MapDto userDataFor(final CommandDto commandDto) {
         MapDto userData = commandDto.getUserData();
         if(userData == null) {
diff --git a/api/applib/src/test/java/org/apache/isis/applib/services/wrapper/control/AsyncControl_Test.java b/api/applib/src/test/java/org/apache/isis/applib/services/wrapper/control/AsyncControl_Test.java
index 3846167..52e9175 100644
--- a/api/applib/src/test/java/org/apache/isis/applib/services/wrapper/control/AsyncControl_Test.java
+++ b/api/applib/src/test/java/org/apache/isis/applib/services/wrapper/control/AsyncControl_Test.java
@@ -19,7 +19,6 @@
 package org.apache.isis.applib.services.wrapper.control;
 
 import java.util.concurrent.Executor;
-import java.util.concurrent.ExecutorService;
 
 import org.assertj.core.api.Assertions;
 import org.junit.Test;
@@ -34,7 +33,7 @@ public class AsyncControl_Test {
     public void defaults() throws Exception {
 
         // given
-        val control = AsyncControl.control();
+        val control = AsyncControl.returningVoid();
 
         // then
         Assertions.assertThat(control.getExecutionModes()).isEmpty();
@@ -43,7 +42,7 @@ public class AsyncControl_Test {
     @Test
     public void check_rules() throws Exception {
         // given
-        val control = AsyncControl.control();
+        val control = AsyncControl.returningVoid();
 
         // when
         control.withCheckRules();
@@ -56,7 +55,7 @@ public class AsyncControl_Test {
     public void skip_rules() throws Exception {
 
         // given
-        val control = AsyncControl.control();
+        val control = AsyncControl.returningVoid();
 
         // when
         control.withSkipRules();
@@ -69,7 +68,7 @@ public class AsyncControl_Test {
     public void user() throws Exception {
 
         // given
-        val control = AsyncControl.control();
+        val control = AsyncControl.returningVoid();
 
         // when
         control.withUser("fred");
@@ -82,7 +81,7 @@ public class AsyncControl_Test {
     public void roles() throws Exception {
 
         // given
-        val control = AsyncControl.control();
+        val control = AsyncControl.returningVoid();
 
         // when
         control.withRoles("role-1", "role-2");
@@ -101,7 +100,7 @@ public class AsyncControl_Test {
         }));
         ExceptionHandler exceptionHandler = ex -> null;
 
-        val control = AsyncControl.control(String.class)
+        val control = AsyncControl.returning(String.class)
                 .withSkipRules()
                 .withUser("fred")
                 .withRoles("role-1", "role-2")
diff --git a/api/applib/src/test/java/org/apache/isis/applib/util/schema/CommandDtoUtils_Test.java b/api/applib/src/test/java/org/apache/isis/applib/util/schema/CommandDtoUtils_Test.java
index eb9dae5..d4e84c3 100644
--- a/api/applib/src/test/java/org/apache/isis/applib/util/schema/CommandDtoUtils_Test.java
+++ b/api/applib/src/test/java/org/apache/isis/applib/util/schema/CommandDtoUtils_Test.java
@@ -62,7 +62,18 @@ public class CommandDtoUtils_Test {
         CommandDtoUtils.setUserData(dto, "someKey", "someOtherValue");
         assertThat(CommandDtoUtils.getUserData(dto, "someKey"), is("someOtherValue"));
 
-        CommandDtoUtils.setUserData(dto, "someKey", (String)null);
+    }
+
+    @Test
+    public void clearUserData() {
+        // given
+        CommandDtoUtils.setUserData(dto, "someKey", "someOtherValue");
+        assertThat(CommandDtoUtils.getUserData(dto, "someKey"), is("someOtherValue"));
+
+        // when
+        CommandDtoUtils.clearUserData(dto, "someKey");
+
+        // then
         assertThat(CommandDtoUtils.getUserData(dto, "someKey"), is(nullValue()));
     }
 }
\ No newline at end of file
diff --git a/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/actions/action/ActionAnnotationFacetFactoryTest_Command.java b/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/actions/action/ActionAnnotationFacetFactoryTest_Command.java
index 950e122..13cc600 100644
--- a/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/actions/action/ActionAnnotationFacetFactoryTest_Command.java
+++ b/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/actions/action/ActionAnnotationFacetFactoryTest_Command.java
@@ -22,22 +22,16 @@ import java.lang.reflect.Method;
 
 import org.junit.Test;
 
-import static org.hamcrest.CoreMatchers.is;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
-import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertTrue;
 
 import org.apache.isis.applib.annotation.Action;
-import org.apache.isis.applib.annotation.SemanticsOf;
+import org.apache.isis.applib.annotation.CommandReification;
 import org.apache.isis.core.metamodel.facetapi.Facet;
 import org.apache.isis.core.metamodel.facets.FacetFactory.ProcessMethodContext;
 import org.apache.isis.core.metamodel.facets.actions.action.command.CommandFacetForActionAnnotation;
-import org.apache.isis.core.metamodel.facets.actions.action.command.CommandFacetForActionAnnotationAsConfigured;
-import org.apache.isis.core.metamodel.facets.actions.action.command.CommandFacetFromConfiguration;
 import org.apache.isis.core.metamodel.facets.actions.command.CommandFacet;
-import org.apache.isis.core.metamodel.facets.actions.publish.PublishedActionFacet;
-import org.apache.isis.core.metamodel.facets.actions.semantics.ActionSemanticsFacetAbstract;
 
 import lombok.val;
 
@@ -63,7 +57,7 @@ public class ActionAnnotationFacetFactoryTest_Command extends ActionAnnotationFa
     }
 
     @Test
-    public void given_annotation_then_facet_added() {
+    public void given_annotation_but_command_not_specified_then_facet_not_added() {
 
         // given
         class Customer {
@@ -78,10 +72,49 @@ public class ActionAnnotationFacetFactoryTest_Command extends ActionAnnotationFa
 
         // then
         final Facet facet = facetedMethod.getFacet(CommandFacet.class);
+        assertNull(facet);
+    }
+
+    @Test
+    public void given_annotation_with_command_enabled_then_facet_added() {
+
+        // given
+        class Customer {
+            @Action(command = CommandReification.ENABLED)
+            public void someAction() {
+            }
+        }
+        final Method actionMethod = findMethod(Customer.class, "someAction");
+
+        // when
+        processCommand(facetFactory, new ProcessMethodContext(Customer.class, null, actionMethod, mockMethodRemover, facetedMethod));
+
+        // then
+        final Facet facet = facetedMethod.getFacet(CommandFacet.class);
         assertNotNull(facet);
         assertTrue(facet instanceof CommandFacetForActionAnnotation);
     }
 
 
+    @Test
+    public void given_annotation_with_command_disabled_then_facet_not_added() {
+
+        // given
+        class Customer {
+            @Action(command = CommandReification.DISABLED)
+            public void someAction() {
+            }
+        }
+        final Method actionMethod = findMethod(Customer.class, "someAction");
+
+        // when
+        processCommand(facetFactory, new ProcessMethodContext(Customer.class, null, actionMethod, mockMethodRemover, facetedMethod));
+
+        // then
+        final Facet facet = facetedMethod.getFacet(CommandFacet.class);
+        assertNull(facet);
+    }
+
+
 
 }
\ No newline at end of file
diff --git a/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/object/domainobjectlayout/DomainObjectLayoutFactoryTest.java b/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/object/domainobjectlayout/DomainObjectLayoutFactoryTest.java
index 4ff0732..6c77491 100644
--- a/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/object/domainobjectlayout/DomainObjectLayoutFactoryTest.java
+++ b/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/object/domainobjectlayout/DomainObjectLayoutFactoryTest.java
@@ -19,6 +19,7 @@
 
 package org.apache.isis.core.metamodel.facets.object.domainobjectlayout;
 
+import org.assertj.core.api.Assertions;
 import org.jmock.auto.Mock;
 import org.junit.After;
 import org.junit.Assert;
@@ -215,7 +216,7 @@ public class DomainObjectLayoutFactoryTest extends AbstractFacetFactoryJUnit4Tes
                 assertTrue(facet instanceof CssClassFacetForDomainObjectLayoutAnnotation);
 
                 final CssClassFacetForDomainObjectLayoutAnnotation facetImpl = (CssClassFacetForDomainObjectLayoutAnnotation) facet;
-                Assert.assertThat(facetImpl.cssClass(mockAdapter), is("foobar"));
+                Assertions.assertThat(facetImpl.cssClass(mockAdapter)).isEqualTo("foobar");
 
                 expectNoMethodsRemoved();
             }
@@ -228,7 +229,7 @@ public class DomainObjectLayoutFactoryTest extends AbstractFacetFactoryJUnit4Tes
                 facetFactory.process(new FacetFactory.ProcessClassContext(cls, mockMethodRemover, facetHolder));
 
                 final Facet facet = facetHolder.getFacet(CssClassFacet.class);
-                assertNotNull(facet);
+                assertNull(facet);
 
                 expectNoMethodsRemoved();
             }
diff --git a/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/value/ImageValueSemanticsProviderAbstractTest.java b/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/value/ImageValueSemanticsProviderAbstractTest.java
index 386b272..c9ed1a3 100644
--- a/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/value/ImageValueSemanticsProviderAbstractTest.java
+++ b/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/value/ImageValueSemanticsProviderAbstractTest.java
@@ -32,7 +32,6 @@ import org.apache.isis.applib.services.inject.ServiceInjector;
 import org.apache.isis.core.metamodel.facetapi.FacetHolder;
 import org.apache.isis.core.metamodel.facets.value.image.ImageValueSemanticsProviderAbstract;
 import org.apache.isis.core.metamodel.spec.ManagedObject;
-import org.apache.isis.core.internaltestsupport.config.IsisConfigurationLegacy;
 import org.apache.isis.core.internaltestsupport.jmocking.JUnitRuleMockery2;
 import org.apache.isis.core.internaltestsupport.jmocking.JUnitRuleMockery2.Mode;
 
@@ -47,9 +46,6 @@ public class ImageValueSemanticsProviderAbstractTest {
     @Mock
     private ServiceInjector mockServicesInjector;
 
-    @Mock
-    private IsisConfigurationLegacy mockConfiguration;
-
     private TestImageSemanticsProvider adapter;
 
     @Before
diff --git a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/command/CommandExecutorServiceDefault.java b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/command/CommandExecutorServiceDefault.java
index f3c857a..74d7f0d 100644
--- a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/command/CommandExecutorServiceDefault.java
+++ b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/command/CommandExecutorServiceDefault.java
@@ -62,6 +62,7 @@ import org.apache.isis.core.metamodel.spec.feature.ObjectAction;
 import org.apache.isis.core.metamodel.spec.feature.ObjectAssociation;
 import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation;
 import org.apache.isis.core.metamodel.specloader.SpecificationLoader;
+import org.apache.isis.core.metamodel.specloader.specimpl.ObjectActionMixedIn;
 import org.apache.isis.core.runtime.iactn.IsisInteraction;
 import org.apache.isis.core.runtime.iactn.IsisInteractionFactory;
 import org.apache.isis.core.runtime.iactn.IsisInteractionTracker;
@@ -204,7 +205,14 @@ public class CommandExecutorServiceDefault implements CommandExecutorService {
                 // it will switch the targetAdapter to be the mixedInAdapter transparently
                 val argAdapters = argAdaptersFor(actionDto);
 
-                val resultAdapter = objectAction.execute(InteractionHead.simple(targetAdapter)
+                InteractionHead head;
+                if(objectAction instanceof ObjectActionMixedIn) {
+                    ObjectActionMixedIn actionMixedIn = (ObjectActionMixedIn) objectAction;
+                    head = actionMixedIn.interactionHead(targetAdapter);
+                } else {
+                    head = InteractionHead.simple(targetAdapter);
+                }
+                val resultAdapter = objectAction.execute(head
                         , argAdapters, InteractionInitiatedBy.FRAMEWORK);
 
                 // flush any Isis PersistenceCommands pending
diff --git a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/wrapper/WrapperFactoryDefault.java b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/wrapper/WrapperFactoryDefault.java
index 1676007..de01e1e 100644
--- a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/wrapper/WrapperFactoryDefault.java
+++ b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/wrapper/WrapperFactoryDefault.java
@@ -27,6 +27,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.concurrent.Callable;
 import java.util.concurrent.Future;
 import java.util.function.BiConsumer;
 
@@ -43,10 +44,12 @@ import org.springframework.stereotype.Service;
 import org.apache.isis.applib.annotation.OrderPrecedence;
 import org.apache.isis.applib.services.bookmark.Bookmark;
 import org.apache.isis.applib.services.bookmark.BookmarkService;
+import org.apache.isis.applib.services.command.Command;
 import org.apache.isis.applib.services.command.CommandExecutorService;
 import org.apache.isis.applib.services.command.CommandOutcomeHandler;
 import org.apache.isis.applib.services.factory.FactoryService;
 import org.apache.isis.applib.services.iactn.InteractionContext;
+import org.apache.isis.applib.services.inject.ServiceInjector;
 import org.apache.isis.applib.services.metamodel.MetaModelService;
 import org.apache.isis.applib.services.repository.RepositoryService;
 import org.apache.isis.applib.services.wrapper.WrapperFactory;
@@ -106,6 +109,7 @@ import static org.apache.isis.applib.services.metamodel.MetaModelService.Mode.RE
 import static org.apache.isis.applib.services.wrapper.control.SyncControl.control;
 
 import lombok.Data;
+import lombok.RequiredArgsConstructor;
 import lombok.val;
 
 /**
@@ -121,17 +125,15 @@ import lombok.val;
 //@Log4j2
 public class WrapperFactoryDefault implements WrapperFactory {
     
+    @Inject IsisInteractionTracker isisInteractionTracker;
+
     @Inject FactoryService factoryService;
     @Inject MetaModelContext metaModelContext;
     @Inject SpecificationLoader specificationLoader;
-    @Inject IsisInteractionTracker isisInteractionTracker;
-    @Inject IsisInteractionFactory isisInteractionFactory;
     @Inject Provider<InteractionContext> interactionContextProvider;
-    @Inject TransactionService transactionService;
-    @Inject CommandExecutorService commandExecutorService;
+    @Inject ServiceInjector serviceInjector;
     @Inject ProxyFactoryService proxyFactoryService; // protected to allow JUnit test
     @Inject CommandDtoServiceInternal commandDtoServiceInternal;
-    @Inject BookmarkService bookmarkService;
 
     private final List<InteractionListener> listeners = new ArrayList<>();
     private final Map<Class<? extends InteractionEvent>, InteractionEventDispatcher>
@@ -306,6 +308,7 @@ public class WrapperFactoryDefault implements WrapperFactory {
 
         val isisInteraction = currentIsisInteraction();
         val asyncAuthSession = authSessionFrom(asyncControl, isisInteraction.getAuthenticationSession());
+        val command = interactionContextProvider.get().getInteraction().getCommand();
 
         val targetAdapter = memberAndTarget.getTarget();
         val method = memberAndTarget.getMethod();
@@ -333,35 +336,15 @@ public class WrapperFactoryDefault implements WrapperFactory {
         asyncControl.setBookmark(Bookmark.from(oidDto));
 
         val executorService = asyncControl.getExecutorService();
-        val future = executorService.submit(() ->
-                isisInteractionFactory.callAuthenticated(asyncAuthSession, () ->
-                    transactionService.executeWithinTransaction(() -> {
-                        val bookmark = commandExecutorService.executeCommand(commandDto, CommandOutcomeHandler.NULL);
-                        if (bookmark != null) {
-                            Object entity = bookmarkService.lookup(bookmark);
-                            val metaModelService = WrapperFactoryDefault.this.getMetaModelService();
-                            if (metaModelService.sortOf(bookmark, RELAXED).isEntity()) {
-                                entity = WrapperFactoryDefault.this.getRepositoryService().detach(entity);
-                            }
-                            return entity;
-                        }
-                        return null;
-                    })
-        ));
+        val future = executorService.submit(
+                new ExecCommand(asyncAuthSession, commandDto, asyncControl.getReturnType(), command, serviceInjector)
+        );
 
-        asyncControl.setFuture((Future<R>) future);
+        asyncControl.setFuture(future);
 
         return null;
     }
 
-    private RepositoryService getRepositoryService() {
-        return metaModelContext.getRepositoryService();
-    }
-
-    private MetaModelService getMetaModelService() {
-        return metaModelContext.getServiceRegistry().lookupServiceElseFail(MetaModelService.class);
-    }
-
     private <T> MemberAndTarget forRegular(Method method, T domainObject) {
 
         val targetObjSpec = (ObjectSpecificationDefault) specificationLoader.loadSpecification(method.getDeclaringClass());
@@ -513,4 +496,42 @@ public class WrapperFactoryDefault implements WrapperFactory {
         return currentIsisInteraction().getObjectManager();
     }
 
+    @RequiredArgsConstructor
+    private static class ExecCommand<R> implements Callable<R> {
+
+        private final AuthenticationSession authenticationSession;
+        private final CommandDto commandDto;
+        private final Class<R> returnType;
+        private final Command parentCommand;
+        private final ServiceInjector serviceInjector;
+
+        @Inject IsisInteractionFactory isisInteractionFactory;
+        @Inject TransactionService transactionService;
+        @Inject CommandExecutorService commandExecutorService;
+        @Inject Provider<InteractionContext> interactionContextProvider;
+        @Inject BookmarkService bookmarkService;
+        @Inject RepositoryService repositoryService;
+        @Inject MetaModelService metaModelService;
+
+        @Override
+        public R call() {
+            serviceInjector.injectServicesInto(this);
+
+            return isisInteractionFactory.callAuthenticated(authenticationSession, () -> {
+                val childCommand = interactionContextProvider.get().getInteraction().getCommand();
+                childCommand.updater().setParent(parentCommand);
+                return transactionService.executeWithinTransaction(() -> {
+                        val bookmark = commandExecutorService.executeCommand(commandDto, CommandOutcomeHandler.NULL);
+                        if (bookmark == null) {
+                            return null;
+                        }
+                        R entity = bookmarkService.lookup(bookmark, returnType);
+                        if (metaModelService.sortOf(bookmark, RELAXED).isEntity()) {
+                            entity = repositoryService.detach(entity);
+                        }
+                        return entity;
+                    });
+            });
+        }
+    }
 }
diff --git a/core/runtimeservices/src/test/java/org/apache/isis/core/runtimeservices/wrapper/WrapperFactoryDefault_wrappedObject_Test.java b/core/runtimeservices/src/test/java/org/apache/isis/core/runtimeservices/wrapper/WrapperFactoryDefault_wrappedObject_Test.java
deleted file mode 100644
index 2df1b2d..0000000
--- a/core/runtimeservices/src/test/java/org/apache/isis/core/runtimeservices/wrapper/WrapperFactoryDefault_wrappedObject_Test.java
+++ /dev/null
@@ -1,503 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- */
-
-package org.apache.isis.core.runtimeservices.wrapper;
-
-import java.lang.reflect.Method;
-import java.util.Collections;
-import java.util.List;
-import java.util.Optional;
-import java.util.stream.Stream;
-
-import org.jmock.Expectations;
-import org.jmock.auto.Mock;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.CoreMatchers.notNullValue;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import org.apache.isis.applib.Identifier;
-import org.apache.isis.applib.services.bookmark.BookmarkService;
-import org.apache.isis.applib.services.command.Command;
-import org.apache.isis.applib.services.command.CommandExecutorService;
-import org.apache.isis.applib.services.factory.FactoryService;
-import org.apache.isis.applib.services.iactn.Interaction;
-import org.apache.isis.applib.services.iactn.InteractionContext;
-import org.apache.isis.applib.services.message.MessageService;
-import org.apache.isis.applib.services.metamodel.BeanSort;
-import org.apache.isis.applib.services.wrapper.DisabledException;
-import org.apache.isis.applib.services.wrapper.HiddenException;
-import org.apache.isis.applib.services.wrapper.InvalidException;
-import org.apache.isis.applib.services.xactn.TransactionService;
-import org.apache.isis.core.codegen.bytebuddy.services.ProxyFactoryServiceByteBuddy;
-import org.apache.isis.core.commons.internal.plugins.codegen.ProxyFactoryService;
-import org.apache.isis.core.internaltestsupport.jmocking.JUnitRuleMockery2;
-import org.apache.isis.core.internaltestsupport.jmocking.JUnitRuleMockery2.Mode;
-import org.apache.isis.core.metamodel._testing.MetaModelContext_forTesting;
-import org.apache.isis.core.metamodel.context.MetaModelContext;
-import org.apache.isis.core.metamodel.facetapi.FacetUtil;
-import org.apache.isis.core.metamodel.facets.FacetedMethod;
-import org.apache.isis.core.metamodel.facets.all.named.NamedFacetInferred;
-import org.apache.isis.core.metamodel.facets.members.disabled.method.DisableForContextFacetViaMethod;
-import org.apache.isis.core.metamodel.facets.members.hidden.method.HideForContextFacetViaMethod;
-import org.apache.isis.core.metamodel.facets.object.entity.EntityFacet;
-import org.apache.isis.core.metamodel.facets.properties.accessor.PropertyAccessorFacetViaAccessor;
-import org.apache.isis.core.metamodel.facets.properties.update.clear.PropertyClearFacetViaClearMethod;
-import org.apache.isis.core.metamodel.facets.properties.update.init.PropertyInitializationFacetViaSetterMethod;
-import org.apache.isis.core.metamodel.facets.properties.update.modify.PropertySetterFacetViaModifyMethod;
-import org.apache.isis.core.metamodel.facets.properties.validating.method.PropertyValidateFacetViaMethod;
-import org.apache.isis.core.metamodel.interactions.HidingInteractionAdvisor;
-import org.apache.isis.core.metamodel.objectmanager.ObjectManager;
-import org.apache.isis.core.metamodel.services.command.CommandDtoServiceInternal;
-import org.apache.isis.core.metamodel.spec.ManagedObject;
-import org.apache.isis.core.metamodel.spec.ObjectSpecification;
-import org.apache.isis.core.metamodel.spec.feature.ObjectMember;
-import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation;
-import org.apache.isis.core.metamodel.specloader.SpecificationLoader;
-import org.apache.isis.core.metamodel.specloader.specimpl.OneToOneAssociationDefault;
-import org.apache.isis.core.metamodel.specloader.specimpl.dflt.ObjectSpecificationDefault;
-import org.apache.isis.core.runtime.iactn.IsisInteractionFactory;
-import org.apache.isis.core.runtime.iactn.IsisInteractionTracker;
-import org.apache.isis.core.runtimeservices.wrapper.dom.employees.Employee;
-import org.apache.isis.core.runtimeservices.wrapper.dom.employees.EmployeeRepository;
-import org.apache.isis.core.runtimeservices.wrapper.dom.employees.EmployeeRepositoryImpl;
-import org.apache.isis.core.security.authentication.AuthenticationSessionTracker;
-import org.apache.isis.core.security.authentication.standard.SimpleSession;
-import org.apache.isis.schema.cmd.v2.CommandDto;
-
-import lombok.val;
-
-public class WrapperFactoryDefault_wrappedObject_Test {
-
-    @Rule
-    public JUnitRuleMockery2 context = JUnitRuleMockery2.createFor(Mode.INTERFACES_AND_CLASSES);
-
-    @Rule
-    public ExpectedException expectedException = ExpectedException.none();
-
-    @Mock private AuthenticationSessionTracker mockAuthenticationSessionTracker;
-    @Mock private MessageService mockMessageService;
-    @Mock private InteractionContext mockInteractionContext;
-    @Mock private Interaction mockInteraction;
-    @Mock private Command mockCommand;
-    @Mock private CommandDtoServiceInternal mockCommandDtoServiceInternal;
-    @Mock private ObjectSpecification mockOnType;
-    @Mock private SpecificationLoader mockSpecificationLoader;
-    @Mock private IsisInteractionFactory mockIsisInteractionFactory;
-    @Mock private IsisInteractionTracker mockIsisInteractionTracker;
-    @Mock private CommandExecutorService mockCommandExecutorService;
-    @Mock private ObjectSpecificationDefault mockEmployeeSpec;
-    @Mock private FactoryService mockFactoryService;
-    @Mock private TransactionService mockTransactionService;
-    @Mock private BookmarkService mockBookmarkService;
-    @Mock protected ObjectManager mockObjectManager;
-    
-    private ObjectMember employeeNameMember;
-
-    @Mock private ObjectSpecificationDefault mockStringSpec;
-    @Mock private ManagedObject mockEmployeeAdapter;
-    @Mock private ManagedObject mockAdapterForStringSmith;
-    @Mock private ManagedObject mockAdapterForStringJones;
-    @Mock private Identifier mockId;
-
-    private final SimpleSession session = new SimpleSession("tester", Collections.<String>emptyList());
-
-    private EmployeeRepository employeeRepository;
-
-    private Employee employeeDO;
-    private Employee employeeWO;
-
-    private WrapperFactoryDefault wrapperFactory;
-    
-    protected MetaModelContext metaModelContext;
-
-    @SuppressWarnings("unchecked")
-    @Before
-    public void setUp() {
-
-        // PRODUCTION
-        
-        val proxyFactoryService = (ProxyFactoryService) new ProxyFactoryServiceByteBuddy();
-
-        metaModelContext = MetaModelContext_forTesting.builder()
-                .specificationLoader(mockSpecificationLoader)
-                .objectManager(mockObjectManager)
-                .authenticationSessionTracker(mockAuthenticationSessionTracker)
-                .singleton(proxyFactoryService)
-                .singleton(wrapperFactory = createWrapperFactory(proxyFactoryService))
-                .singleton(mockCommandDtoServiceInternal)
-                .singleton(mockFactoryService)
-                .singleton(mockIsisInteractionFactory)
-                .singleton(mockIsisInteractionTracker)
-                .singleton(mockTransactionService)
-                .singleton(mockCommandExecutorService)
-                .singleton(mockCommandDtoServiceInternal)
-                .singleton(mockBookmarkService)
-                .build();
-        
-        metaModelContext.getServiceInjector().injectServicesInto(wrapperFactory);
-
-        employeeRepository = new EmployeeRepositoryImpl();
-
-        employeeDO = new Employee();
-        employeeDO.setName("Smith");
-        employeeDO.setEmployeeRepository(employeeRepository);
-
-        context.checking(new Expectations() {
-            {
-                
-                allowing(mockObjectManager).adapt(employeeDO);
-                will(returnValue(mockEmployeeAdapter));
-
-                allowing(mockEmployeeSpec).isManagedBean();
-                will(returnValue(true));
-                
-                allowing(mockEmployeeSpec).getBeanSort();
-                will(returnValue(BeanSort.ENTITY));
-                
-                allowing(mockEmployeeSpec).isIdentifiable();
-                will(returnValue(true));
-                
-                allowing(mockEmployeeSpec).getCorrespondingClass();
-                will(returnValue(Employee.class));
-
-                allowing(mockStringSpec).getCorrespondingClass();
-                will(returnValue(String.class));
-
-                allowing(mockCommandDtoServiceInternal).asCommandDto(with(any(List.class)), with(any(OneToOneAssociation.class)), with(any(ManagedObject.class)));
-                will(returnValue(new CommandDto()));
-                
-                allowing(mockInteractionContext).getInteraction();
-                will(returnValue(mockInteraction));
-                
-                allowing(mockInteraction).getCommand();
-                will(returnValue(mockCommand));
-
-                allowing(mockSpecificationLoader).loadSpecification(String.class);
-                will(returnValue(mockStringSpec));
-
-                allowing(mockStringSpec).getShortIdentifier();
-                will(returnValue(String.class.getName()));
-
-                allowing(mockAuthenticationSessionTracker).currentAuthenticationSession();
-                will(returnValue(Optional.of(session)));
-
-                allowing(mockEmployeeAdapter).titleString(null);
-                will(returnValue("titleOf[mockEmployeeAdapter]"));
-                
-                allowing(mockEmployeeAdapter).getSpecification();
-                will(returnValue(mockEmployeeSpec));
-
-                allowing(mockSpecificationLoader).loadSpecification(Employee.class);
-                will(returnValue(mockEmployeeSpec));
-
-                allowing(mockEmployeeSpec).getMember(methodOf(Employee.class, "getEmployeeRepository"));
-                will(returnValue(null));
-                
-                allowing(mockEmployeeSpec).getFacet(EntityFacet.class);
-                will(returnValue(null));
-
-
-//                allowing(mockAdapterForStringJones).isDestroyed();
-//                will(returnValue(false));
-
-                allowing(mockAdapterForStringJones).getSpecification();
-                will(returnValue(mockStringSpec));
-
-                allowing(mockObjectManager).adapt("Jones");
-                will(returnValue(mockAdapterForStringJones));
-
-            }
-        });
-
-
-        final Method employeeGetNameMethod = methodOf(Employee.class, "getName");
-        final Method employeeSetNameMethod = methodOf(Employee.class, "setName", String.class);
-        final Method employeeModifyNameMethod = methodOf(Employee.class, "modifyName", String.class);
-        final Method employeeHideNameMethod = methodOf(Employee.class, "hideName");
-        final Method employeeDisableNameMethod = methodOf(Employee.class, "disableName");
-        final Method employeeValidateNameMethod = methodOf(Employee.class, "validateName", String.class);
-        final Method employeeClearNameMethod = methodOf(Employee.class, "clearName");
-        employeeNameMember = new OneToOneAssociationDefault(
-                facetedMethodForProperty(
-                        metaModelContext,
-                        employeeSetNameMethod, employeeGetNameMethod, employeeModifyNameMethod, employeeClearNameMethod, employeeHideNameMethod, employeeDisableNameMethod, employeeValidateNameMethod));
-
-        context.checking(new Expectations() {
-            {
-                //                allowing(mockServicesInjector).lookupServiceElseFail(WrapperFactory.class);
-                //                will(returnValue(wrapperFactory));
-
-                allowing(mockEmployeeSpec).getMember(employeeGetNameMethod);
-                will(returnValue(employeeNameMember));
-
-                allowing(mockEmployeeSpec).getMember(employeeSetNameMethod);
-                will(returnValue(employeeNameMember));
-
-                allowing(mockEmployeeSpec).getMember(employeeModifyNameMethod);
-                will(returnValue(employeeNameMember));
-
-                allowing(mockEmployeeSpec).getMember(employeeClearNameMethod);
-                will(returnValue(employeeNameMember));
-
-                allowing(mockEmployeeAdapter).getPojo();
-                will(returnValue(employeeDO));
-
-//                allowing(mockEmployeeAdapter).isRepresentingPersistent();
-//                will(returnValue(true));
-            }
-        });
-
-
-        employeeWO = wrapperFactory.wrap(employeeDO);
-    }
-
-    protected WrapperFactoryDefault createWrapperFactory(ProxyFactoryService proxyFactoryService) {
-        val wrapperFactory = new WrapperFactoryDefault();
-        wrapperFactory.proxyFactoryService = proxyFactoryService;
-        wrapperFactory.init();
-        return wrapperFactory;
-    }
-
-    @Test
-    public void shouldWrapDomainObject() {
-        // then
-        assertThat(employeeWO, is(notNullValue()));
-    }
-
-    @Test
-    public void shouldBeAbleToInjectIntoDomainObjects() {
-        assertThat(employeeDO.getEmployeeRepository(), is(notNullValue()));
-    }
-
-    @Test
-    public void cannotAccessMethodNotCorrespondingToMember() {
-
-        expectedException.expectMessage(
-                "Method 'getEmployeeRepository' being invoked does not correspond to any of the object's fields or actions.");
-
-        // then
-        assertThat(employeeWO.getEmployeeRepository(), is(notNullValue()));
-    }
-
-    @Test
-    public void shouldBeAbleToReadVisibleProperty() {
-
-        allowingEmployeeHasSmithAdapter();
-
-        assertTrue(
-                metaModelContext.getConfiguration().getCore().getMetaModel().isFilterVisibility());
-
-        context.checking(new Expectations() {{
-
-            allowing(mockAdapterForStringSmith).getSpecification();
-            will(returnValue(mockStringSpec));
-
-            allowing(mockStringSpec).isEntity();
-            will(returnValue(false));
-            
-            allowing(mockStringSpec).getIdentifier();
-            will(returnValue(mockId));
-            
-            allowing(mockStringSpec).getBeanSort();
-            will(returnValue(BeanSort.VIEW_MODEL));
-            
-            allowing(mockStringSpec).streamFacets(HidingInteractionAdvisor.class);
-            will(returnValue(Stream.empty()));
-            
-            allowing(mockObjectManager).adapt("Smith");
-            will(returnValue(mockAdapterForStringSmith));
-        }});
-
-        // then
-        assertThat(employeeWO.getName(), is(employeeDO.getName()));
-    }
-
-    @Test
-    public void shouldNotBeAbleToViewHiddenProperty() {
-
-        expectedException.expect(HiddenException.class);
-
-        allowingEmployeeHasSmithAdapter();
-
-        // given
-        employeeDO.whetherHideName = true;
-        // when
-        employeeWO.getName();
-        // then should throw exception
-    }
-
-    @Test
-    public void shouldBeAbleToModifyEnabledPropertyUsingSetter() {
-
-        allowingJonesStringValueAdapter();
-
-        assertTrue(
-                metaModelContext.getConfiguration().getCore().getMetaModel().isFilterVisibility());
-
-        context.checking(new Expectations() {
-            {
-                allowing(mockAdapterForStringJones).titleString(null);
-
-                ignoring(mockCommand);
-
-                allowing(mockStringSpec).isParented();
-                will(returnValue(false));
-                
-                allowing(mockStringSpec).isEntity();
-                will(returnValue(false));
-                
-                allowing(mockStringSpec).getIdentifier();
-                will(returnValue(mockId));
-                
-                allowing(mockStringSpec).getBeanSort();
-                will(returnValue(BeanSort.VIEW_MODEL));
-                
-                allowing(mockEmployeeSpec).isViewModelCloneable(mockEmployeeAdapter);
-                will(returnValue(false));
-
-                allowing(mockStringSpec).streamFacets(HidingInteractionAdvisor.class);
-                will(returnValue(Stream.empty()));
-                
-            }
-        });
-
-        // when
-        employeeWO.setName("Jones");
-        // then
-        assertThat(employeeDO.getName(), is("Jones"));
-        assertThat(employeeWO.getName(), is(employeeDO.getName()));
-    }
-
-    @Test
-    public void shouldNotBeAbleToModifyDisabledProperty() {
-
-        expectedException.expect(DisabledException.class);
-
-        // given
-        employeeDO.reasonDisableName = "sorry, no change allowed";
-        // when
-        employeeWO.setName("Jones");
-        // then should throw exception
-    }
-
-    @Test
-    public void shouldNotBeAbleToModifyPropertyUsingModify() {
-
-        allowingJonesStringValueAdapter();
-
-        expectedException.expect(UnsupportedOperationException.class);
-
-        // when
-        employeeWO.modifyName("Jones");
-        // then should throw exception
-    }
-
-    @Test
-    public void shouldNotBeAbleToModifyPropertyUsingClear() {
-
-        expectedException.expect(UnsupportedOperationException.class);
-
-        // when
-        employeeWO.clearName();
-        // then should throw exception
-    }
-
-    @Test
-    public void shouldNotBeAbleToModifyPropertyIfInvalid() {
-
-        allowingJonesStringValueAdapter();
-
-        expectedException.expect(InvalidException.class);
-
-        // given
-        employeeDO.reasonValidateName = "sorry, invalid data";
-        // when
-        employeeWO.setName("Jones");
-        // then should throw exception
-    }
-
-
-    // //////////////////////////////////////
-
-    private FacetedMethod facetedMethodForProperty(
-            MetaModelContext mmc,
-            Method init, Method accessor, Method modify, Method clear, Method hide, Method disable, Method validate) {
-        FacetedMethod facetedMethod = FacetedMethod.createForProperty(accessor.getDeclaringClass(), accessor);
-        facetedMethod.setMetaModelContext(mmc);
-        FacetUtil.addFacet(new PropertyAccessorFacetViaAccessor(mockOnType, accessor, facetedMethod));
-        FacetUtil.addFacet(new PropertyInitializationFacetViaSetterMethod(init, facetedMethod));
-        FacetUtil.addFacet(new PropertySetterFacetViaModifyMethod(modify, facetedMethod));
-        FacetUtil.addFacet(new PropertyClearFacetViaClearMethod(clear, facetedMethod));
-        FacetUtil.addFacet(new HideForContextFacetViaMethod(hide, facetedMethod));
-        FacetUtil.addFacet(new DisableForContextFacetViaMethod(disable, null, null, facetedMethod));
-        FacetUtil.addFacet(new PropertyValidateFacetViaMethod(validate, null, null, facetedMethod));
-        FacetUtil.addFacet(new NamedFacetInferred(accessor.getName(), facetedMethod));
-        return facetedMethod;
-    }
-
-    private static Method methodOf(Class<?> cls, String methodName) {
-        return methodOf(cls, methodName, new Class<?>[]{});
-    }
-
-    private static Method methodOf(Class<?> cls, String methodName, Class<?>... args) {
-        try {
-            return cls.getMethod(methodName, args);
-        } catch (Exception e) {
-            throw new RuntimeException(e);
-        }
-    }
-
-    // //////////////////////////////////////
-
-    private void allowingEmployeeHasSmithAdapter() {
-        context.checking(new Expectations() {
-            {
-//                allowing(mockAdapterManager).adapterFor("Smith");
-//                will(returnValue(mockAdapterForStringSmith));
-
-                allowing(mockAdapterForStringSmith).getPojo();
-                will(returnValue("Smith"));
-            }
-        });
-    }
-
-    private void allowingJonesStringValueAdapter() {
-        context.checking(new Expectations() {
-            {
-//                allowing(mockAdapterManager).adapterFor("Jones");
-//                will(returnValue(mockAdapterForStringJones));
-
-                allowing(mockAdapterForStringJones).getPojo();
-                will(returnValue("Jones"));
-
-//                allowing(mockAdapterForStringJones).isTransient();
-//                will(returnValue(false));
-            }
-        });
-    }
-
-
-
-}
diff --git a/core/runtimeservices/src/test/java/org/apache/isis/core/runtimeservices/wrapper/WrapperFactoryDefault_wrappedObject_transient_Test.java b/core/runtimeservices/src/test/java/org/apache/isis/core/runtimeservices/wrapper/WrapperFactoryDefault_wrappedObject_transient_Test.java
deleted file mode 100644
index e1a94c2..0000000
--- a/core/runtimeservices/src/test/java/org/apache/isis/core/runtimeservices/wrapper/WrapperFactoryDefault_wrappedObject_transient_Test.java
+++ /dev/null
@@ -1,299 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- */
-
-package org.apache.isis.core.runtimeservices.wrapper;
-
-import java.lang.reflect.Method;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Optional;
-
-import org.jmock.Expectations;
-import org.jmock.auto.Mock;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.MatcherAssert.assertThat;
-
-import org.apache.isis.applib.Identifier;
-import org.apache.isis.applib.annotation.Where;
-import org.apache.isis.applib.services.bookmark.BookmarkService;
-import org.apache.isis.applib.services.command.CommandExecutorService;
-import org.apache.isis.applib.services.factory.FactoryService;
-import org.apache.isis.applib.services.inject.ServiceInjector;
-import org.apache.isis.applib.services.message.MessageService;
-import org.apache.isis.applib.services.registry.ServiceRegistry;
-import org.apache.isis.applib.services.wrapper.DisabledException;
-import org.apache.isis.applib.services.wrapper.events.PropertyModifyEvent;
-import org.apache.isis.applib.services.wrapper.events.PropertyUsabilityEvent;
-import org.apache.isis.applib.services.wrapper.events.PropertyVisibilityEvent;
-import org.apache.isis.applib.services.xactn.TransactionService;
-import org.apache.isis.core.codegen.bytebuddy.services.ProxyFactoryServiceByteBuddy;
-import org.apache.isis.core.commons.internal.plugins.codegen.ProxyFactoryService;
-import org.apache.isis.core.internaltestsupport.jmocking.JUnitRuleMockery2;
-import org.apache.isis.core.internaltestsupport.jmocking.JUnitRuleMockery2.Mode;
-import org.apache.isis.core.metamodel._testing.MetaModelContext_forTesting;
-import org.apache.isis.core.metamodel.consent.Allow;
-import org.apache.isis.core.metamodel.consent.Consent;
-import org.apache.isis.core.metamodel.consent.InteractionInitiatedBy;
-import org.apache.isis.core.metamodel.consent.InteractionResult;
-import org.apache.isis.core.metamodel.consent.Veto;
-import org.apache.isis.core.metamodel.context.MetaModelContext;
-import org.apache.isis.core.metamodel.facetapi.Facet;
-import org.apache.isis.core.metamodel.facets.members.disabled.DisabledFacet;
-import org.apache.isis.core.metamodel.facets.members.disabled.DisabledFacetAbstractAlwaysEverywhere;
-import org.apache.isis.core.metamodel.facets.object.entity.EntityFacet;
-import org.apache.isis.core.metamodel.facets.properties.accessor.PropertyAccessorFacetViaAccessor;
-import org.apache.isis.core.metamodel.facets.properties.update.modify.PropertySetterFacetViaSetterMethod;
-import org.apache.isis.core.metamodel.objectmanager.ObjectManager;
-import org.apache.isis.core.metamodel.services.command.CommandDtoServiceInternal;
-import org.apache.isis.core.metamodel.spec.ManagedObject;
-import org.apache.isis.core.metamodel.spec.ObjectSpecification;
-import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation;
-import org.apache.isis.core.metamodel.specloader.SpecificationLoader;
-import org.apache.isis.core.metamodel.specloader.specimpl.dflt.ObjectSpecificationDefault;
-import org.apache.isis.core.runtime.iactn.IsisInteractionFactory;
-import org.apache.isis.core.runtime.iactn.IsisInteractionTracker;
-import org.apache.isis.core.runtimeservices.wrapper.dom.employees.Employee;
-import org.apache.isis.core.security.authentication.AuthenticationSessionTracker;
-import org.apache.isis.core.security.authentication.standard.SimpleSession;
-
-import static org.apache.isis.core.internaltestsupport.jmocking.PostponedAction.returnValuePostponed;
-
-import lombok.val;
-
-/**
- * Contract test.
- */
-public class WrapperFactoryDefault_wrappedObject_transient_Test {
-
-    @Rule
-    public final JUnitRuleMockery2 context = JUnitRuleMockery2.createFor(Mode.INTERFACES_AND_CLASSES);
-
-    @Mock private AuthenticationSessionTracker mockAuthenticationSessionTracker;
-    @Mock private MessageService mockMessageService;
-    @Mock private CommandDtoServiceInternal mockCommandDtoServiceInternal;
-    @Mock private ObjectSpecification mockOnType;
-    @Mock private SpecificationLoader mockSpecificationLoader;
-    @Mock private IsisInteractionFactory mockIsisInteractionFactory;
-    @Mock private IsisInteractionTracker mockIsisInteractionTracker;
-    @Mock private CommandExecutorService mockCommandExecutorService;
-    @Mock private FactoryService mockFactoryService;
-    @Mock private TransactionService mockTransactionService;
-
-    @Mock private ManagedObject mockEmployeeAdapter;
-    @Mock private ObjectSpecificationDefault mockEmployeeSpec;
-    @Mock private OneToOneAssociation mockPasswordMember;
-    @Mock private Identifier mockPasswordIdentifier;
-    @Mock private ServiceInjector mockServiceInjector;
-    @Mock private ServiceRegistry mockServiceRegistry;
-    @Mock private BookmarkService mockBookmarkService;
-    @Mock protected ManagedObject mockPasswordAdapter;
-    @Mock protected ObjectManager mockObjectManager;
-
-    private Employee employeeDO;
-
-    private final String passwordValue = "12345678";
-
-    private final SimpleSession session = new SimpleSession("tester", Collections.<String>emptyList());
-
-    private Method getPasswordMethod;
-    private Method setPasswordMethod;
-
-    private WrapperFactoryDefault wrapperFactory;
-    private Employee employeeWO;
-    private List<Facet> facets;
-    
-    protected MetaModelContext metaModelContext;
-
-    @Before
-    public void setUp() throws Exception {
-
-        // PRODUCTION
-        
-        val proxyFactoryService = (ProxyFactoryService) new ProxyFactoryServiceByteBuddy();
-        
-        metaModelContext = MetaModelContext_forTesting.builder()
-                .specificationLoader(mockSpecificationLoader)
-                .objectManager(mockObjectManager)
-                .authenticationSessionTracker(mockAuthenticationSessionTracker)
-                .singleton(proxyFactoryService)
-                .singleton(wrapperFactory = createWrapperFactory(proxyFactoryService))
-                .singleton(mockFactoryService)
-                .singleton(mockIsisInteractionFactory)
-                .singleton(mockIsisInteractionTracker)
-                .singleton(mockTransactionService)
-                .singleton(mockCommandExecutorService)
-                .singleton(mockCommandDtoServiceInternal)
-                .singleton(mockBookmarkService)
-                .build();
-        
-        metaModelContext.getServiceInjector().injectServicesInto(wrapperFactory);
-
-        employeeDO = new Employee();
-        employeeDO.setName("Smith");
-
-        getPasswordMethod = Employee.class.getMethod("getPassword");
-        setPasswordMethod = Employee.class.getMethod("setPassword", String.class);
-
-        context.checking(new Expectations() {
-            {
-
-                allowing(mockObjectManager).adapt(employeeDO);
-                will(returnValue(mockEmployeeAdapter));
-
-                allowing(mockObjectManager).adapt(passwordValue);
-                will(returnValue(mockPasswordAdapter));
-
-                allowing(mockEmployeeAdapter).getSpecification();
-                will(returnValue(mockEmployeeSpec));
-
-                allowing(mockEmployeeAdapter).getPojo();
-                will(returnValue(employeeDO));
-
-                allowing(mockPasswordAdapter).getPojo();
-                will(returnValue(passwordValue));
-
-                allowing(mockPasswordMember).getIdentifier();
-                will(returnValue(mockPasswordIdentifier));
-
-                allowing(mockPasswordIdentifier).toClassAndNameIdentityString();
-                will(returnValue("mocked-class#member"));
-                
-                allowing(mockSpecificationLoader).loadSpecification(Employee.class);
-                will(returnValue(mockEmployeeSpec));
-
-                allowing(mockEmployeeSpec).getMember(with(setPasswordMethod));
-                will(returnValue(mockPasswordMember));
-
-                allowing(mockEmployeeSpec).getMember(with(getPasswordMethod));
-                will(returnValue(mockPasswordMember));
-                
-                allowing(mockEmployeeSpec).getFacet(EntityFacet.class);
-                will(returnValue(null));
-
-                allowing(mockPasswordMember).getName();
-                will(returnValue("password"));
-
-                allowing(mockAuthenticationSessionTracker).currentAuthenticationSession();
-                will(returnValue(Optional.of(session)));
-
-                allowing(mockPasswordMember).isOneToOneAssociation();
-                will(returnValue(true));
-
-                allowing(mockPasswordMember).isOneToManyAssociation();
-                will(returnValue(false));
-
-            }
-        });
-
-        employeeWO = wrapperFactory.wrap(employeeDO);
-    }
-
-    protected WrapperFactoryDefault createWrapperFactory(ProxyFactoryService proxyFactoryService) {
-        val wrapperFactory = new WrapperFactoryDefault();
-        wrapperFactory.proxyFactoryService = proxyFactoryService;
-        wrapperFactory.init();
-        return wrapperFactory;
-    }
-
-    @Test(expected = DisabledException.class)
-    public void shouldNotBeAbleToModifyProperty() {
-
-        // given
-        final DisabledFacet disabledFacet = new DisabledFacetAbstractAlwaysEverywhere(mockPasswordMember){};
-        facets = Arrays.asList(disabledFacet, new PropertySetterFacetViaSetterMethod(setPasswordMethod, mockPasswordMember));
-
-        final Consent visibilityConsent = new Allow(new InteractionResult(new PropertyVisibilityEvent(employeeDO, null)));
-
-        final InteractionResult usabilityInteractionResult = new InteractionResult(new PropertyUsabilityEvent(employeeDO, null));
-        usabilityInteractionResult.advise("disabled", disabledFacet);
-        final Consent usabilityConsent = new Veto(usabilityInteractionResult);
-
-        context.checking(new Expectations() {
-            {
-                allowing(mockPasswordMember).streamFacets();
-                will(returnValuePostponed(facets::stream));
-
-                allowing(mockPasswordMember).isVisible(mockEmployeeAdapter, InteractionInitiatedBy.USER, Where.ANYWHERE);
-                will(returnValue(visibilityConsent));
-
-                allowing(mockPasswordMember).isUsable(mockEmployeeAdapter, InteractionInitiatedBy.USER, Where.ANYWHERE);
-                will(returnValue(usabilityConsent));
-            }
-        });
-
-        // when
-        employeeWO.setPassword(passwordValue);
-
-        // then should throw exception
-    }
-
-    @Test
-    public void canModifyProperty() {
-        // given
-
-        final Consent visibilityConsent = new Allow(new InteractionResult(new PropertyVisibilityEvent(employeeDO, mockPasswordIdentifier)));
-        final Consent usabilityConsent = new Allow(new InteractionResult(new PropertyUsabilityEvent(employeeDO, mockPasswordIdentifier)));
-        final Consent validityConsent = new Allow(new InteractionResult(new PropertyModifyEvent(employeeDO, mockPasswordIdentifier, passwordValue)));
-
-        context.checking(new Expectations() {
-            {
-                allowing(mockPasswordMember).isVisible(mockEmployeeAdapter, InteractionInitiatedBy.USER, Where.ANYWHERE);
-                will(returnValue(visibilityConsent));
-
-                allowing(mockPasswordMember).isUsable(mockEmployeeAdapter, InteractionInitiatedBy.USER, Where.ANYWHERE);
-                will(returnValue(usabilityConsent));
-
-                allowing(mockPasswordMember).isAssociationValid(mockEmployeeAdapter, mockPasswordAdapter,
-                        InteractionInitiatedBy.USER);
-                will(returnValue(validityConsent));
-            }
-        });
-
-        facets = Arrays.asList((Facet)new PropertySetterFacetViaSetterMethod(
-                setPasswordMethod, mockPasswordMember));
-        
-        context.checking(new Expectations() {
-            {
-                allowing(mockPasswordMember).streamFacets();
-                will(returnValuePostponed(()->facets.stream()));
-
-                oneOf(mockPasswordMember)
-                .set(mockEmployeeAdapter, mockPasswordAdapter, InteractionInitiatedBy.USER);
-                
-                oneOf(mockPasswordMember).get(mockEmployeeAdapter, InteractionInitiatedBy.USER);
-                will(returnValue(mockPasswordAdapter));
-            }
-        });
-
-        // when
-        employeeWO.setPassword(passwordValue);
-
-        // and given
-        facets = Arrays.asList((Facet)new PropertyAccessorFacetViaAccessor(
-                mockOnType, getPasswordMethod, mockPasswordMember));
-
-        // then be allowed
-        assertThat(employeeWO.getPassword(), is(passwordValue));
-    }
-}
diff --git a/examples/demo/domain/src/main/java/demoapp/dom/annotDomain/Action/command/ActionCommandJdo.java b/examples/demo/domain/src/main/java/demoapp/dom/annotDomain/Action/command/ActionCommandJdo.java
index 3857514..5e93c0f 100644
--- a/examples/demo/domain/src/main/java/demoapp/dom/annotDomain/Action/command/ActionCommandJdo.java
+++ b/examples/demo/domain/src/main/java/demoapp/dom/annotDomain/Action/command/ActionCommandJdo.java
@@ -26,6 +26,7 @@ import javax.jdo.annotations.PersistenceCapable;
 
 import org.apache.isis.applib.annotation.Action;
 import org.apache.isis.applib.annotation.ActionLayout;
+import org.apache.isis.applib.annotation.CommandReification;
 import org.apache.isis.applib.annotation.DomainObject;
 import org.apache.isis.applib.annotation.Editing;
 import org.apache.isis.applib.annotation.MemberOrder;
@@ -56,9 +57,9 @@ public class ActionCommandJdo
 
     public ActionCommandJdo(String initialValue) {
         this.property = initialValue;
+        this.propertyCommandDisabled = initialValue;
         this.propertyMetaAnnotated = initialValue;
         this.propertyMetaAnnotatedOverridden = initialValue;
-        this.propertyUpdateAsync = initialValue;
     }
 
     public String title() {
@@ -72,6 +73,11 @@ public class ActionCommandJdo
     private String property;
 
     @Property()
+    @MemberOrder(name = "annotation", sequence = "2")
+    @Getter @Setter
+    private String propertyCommandDisabled;
+
+    @Property()
     @MemberOrder(name = "meta-annotated", sequence = "1")
     @Getter @Setter
     private String propertyMetaAnnotated;
@@ -80,16 +86,12 @@ public class ActionCommandJdo
     @MemberOrder(name = "meta-annotated-overridden", sequence = "1")
     @Getter @Setter
     private String propertyMetaAnnotatedOverridden;
-
-    @Property()
-    @MemberOrder(name = "async", sequence = "1")
-    @Getter @Setter
-    private String propertyUpdateAsync;
 //end::property[]
 
 //tag::annotation[]
     @Action(
-        semantics = SemanticsOf.IDEMPOTENT
+        command = CommandReification.ENABLED
+        , semantics = SemanticsOf.IDEMPOTENT
         , associateWith = "property"
         , associateWithSequence = "1"
     )
@@ -106,6 +108,26 @@ public class ActionCommandJdo
     }
 //end::annotation[]
 
+//tag::annotation-2[]
+    @Action(
+        command = CommandReification.DISABLED
+        , semantics = SemanticsOf.IDEMPOTENT
+        , associateWith = "propertyCommandDisabled"
+        , associateWithSequence = "1"
+    )
+    @ActionLayout(
+        named = "Update Property"
+        , describedAs = "@Action(command = ENABLED)"
+    )
+    public ActionCommandJdo updatePropertyCommandDisabledUsingAnnotation(final String value) {
+        setPropertyCommandDisabled(value);
+        return this;
+    }
+    public String default0UpdatePropertyCommandDisabledUsingAnnotation() {
+        return getPropertyCommandDisabled();
+    }
+//end::annotation[]
+
 //tag::meta-annotation[]
     @ActionCommandEnabledMetaAnnotation      // <.>
     @Action(
@@ -130,13 +152,14 @@ public class ActionCommandJdo
     @ActionCommandDisabledMetaAnnotation        // <.>
     @Action(
         semantics = SemanticsOf.IDEMPOTENT
+        , command = CommandReification.ENABLED
         , associateWith = "propertyMetaAnnotatedOverridden"
         , associateWithSequence = "1"
     )
     @ActionLayout(
         named = "Update Property"
         , describedAs =
-            "@ActionCommandDisabledMetaAnnotation @Action(publishing = ENABLED)"
+            "@ActionCommandDisabledMetaAnnotation @Action(command = ENABLED)"
     )
     public ActionCommandJdo updatePropertyUsingMetaAnnotationButOverridden(final String value) {
         setPropertyMetaAnnotatedOverridden(value);
@@ -147,28 +170,6 @@ public class ActionCommandJdo
     }
 //end::meta-annotation-overridden[]
 
-//tag::async[]
-    @Action(
-        semantics = SemanticsOf.IDEMPOTENT
-        , associateWith = "propertyUpdateAsync"
-        , associateWithSequence = "1"
-    )
-    @ActionLayout(
-        describedAs = "@Action()"
-    )
-    public ActionCommandJdo updatePropertyAsync(final String value) {
-        AsyncControl<Void> control = AsyncControl.control();
-        ActionCommandJdo actionCommandJdo = this.wrapperFactory.asyncWrap(this, control);
-        actionCommandJdo.setPropertyUpdateAsync(value);
-        return this;
-    }
-    public String default0UpdatePropertyAsync() {
-        return getPropertyUpdateAsync();
-    }
-
-    @Inject WrapperFactory wrapperFactory;
-//end::async[]
-
 
 //tag::class[]
 
diff --git a/examples/demo/domain/src/main/java/demoapp/dom/annotDomain/Action/command/ActionCommandJdo.layout.xml b/examples/demo/domain/src/main/java/demoapp/dom/annotDomain/Action/command/ActionCommandJdo.layout.xml
index 757b050..9207772 100644
--- a/examples/demo/domain/src/main/java/demoapp/dom/annotDomain/Action/command/ActionCommandJdo.layout.xml
+++ b/examples/demo/domain/src/main/java/demoapp/dom/annotDomain/Action/command/ActionCommandJdo.layout.xml
@@ -17,29 +17,10 @@
 
 	<bs3:row>
 		<bs3:col span="6">
-			<bs3:row>
-				<bs3:col span="6">
-					<bs3:row>
-						<bs3:col span="12">
-							<cpt:fieldSet name="Annotated" id="annotation"/>
-							<cpt:fieldSet name="Async" id="async"/>
-						</bs3:col>
-					</bs3:row>
-				</bs3:col>
-				<bs3:col span="6">
-					<bs3:row>
-						<bs3:col span="12">
-							<cpt:fieldSet name="Meta-annotated" id="meta-annotated"/>
-							<cpt:fieldSet name="Meta-annotated Overridden" id="meta-annotated-overridden"/>
-						</bs3:col>
-					</bs3:row>
-				</bs3:col>
-			</bs3:row>
-			<bs3:row>
-				<bs3:col span="12">
-					<cpt:fieldSet name="Other" id="other" unreferencedProperties="true"/>
-				</bs3:col>
-			</bs3:row>
+			<cpt:fieldSet name="Annotated" id="annotation"/>
+			<cpt:fieldSet name="Meta-annotated" id="meta-annotated"/>
+			<cpt:fieldSet name="Meta-annotated Overridden" id="meta-annotated-overridden"/>
+			<cpt:fieldSet name="Other" id="other" unreferencedProperties="true"/>
 		</bs3:col>
 		<bs3:col span="6">
 			<cpt:fieldSet name="Description" id="description" >
diff --git a/examples/demo/domain/src/main/java/demoapp/dom/annotDomain/Action/command/ActionCommandJdo_mixinUpdateProperty.java b/examples/demo/domain/src/main/java/demoapp/dom/annotDomain/Action/command/ActionCommandJdo_mixinUpdateProperty.java
index 349b825..806add4 100644
--- a/examples/demo/domain/src/main/java/demoapp/dom/annotDomain/Action/command/ActionCommandJdo_mixinUpdateProperty.java
+++ b/examples/demo/domain/src/main/java/demoapp/dom/annotDomain/Action/command/ActionCommandJdo_mixinUpdateProperty.java
@@ -2,19 +2,20 @@ package demoapp.dom.annotDomain.Action.command;
 
 import org.apache.isis.applib.annotation.Action;
 import org.apache.isis.applib.annotation.ActionLayout;
+import org.apache.isis.applib.annotation.CommandReification;
 import org.apache.isis.applib.annotation.Publishing;
 import org.apache.isis.applib.annotation.SemanticsOf;
 
 //tag::class[]
 @Action(
-    publishing = Publishing.ENABLED         // <.>
+    command = CommandReification.ENABLED
     , semantics = SemanticsOf.IDEMPOTENT
     , associateWith = "property"
     , associateWithSequence = "2"
 )
 @ActionLayout(
     named = "Mixin Update Property"
-    , describedAs = "@Action(publishing = ENABLED)"
+    , describedAs = "@Action(command = ENABLED)"
 )
 public class ActionCommandJdo_mixinUpdateProperty {
     // ...
diff --git a/examples/demo/domain/src/main/java/demoapp/dom/annotDomain/Action/command/ActionCommandJdo_mixinUpdateProperty.java b/examples/demo/domain/src/main/java/demoapp/dom/annotDomain/Action/command/ActionCommandJdo_mixinUpdatePropertyCommandDisabled.java
similarity index 67%
copy from examples/demo/domain/src/main/java/demoapp/dom/annotDomain/Action/command/ActionCommandJdo_mixinUpdateProperty.java
copy to examples/demo/domain/src/main/java/demoapp/dom/annotDomain/Action/command/ActionCommandJdo_mixinUpdatePropertyCommandDisabled.java
index 349b825..3d84891 100644
--- a/examples/demo/domain/src/main/java/demoapp/dom/annotDomain/Action/command/ActionCommandJdo_mixinUpdateProperty.java
+++ b/examples/demo/domain/src/main/java/demoapp/dom/annotDomain/Action/command/ActionCommandJdo_mixinUpdatePropertyCommandDisabled.java
@@ -2,27 +2,27 @@ package demoapp.dom.annotDomain.Action.command;
 
 import org.apache.isis.applib.annotation.Action;
 import org.apache.isis.applib.annotation.ActionLayout;
-import org.apache.isis.applib.annotation.Publishing;
+import org.apache.isis.applib.annotation.CommandReification;
 import org.apache.isis.applib.annotation.SemanticsOf;
 
 //tag::class[]
 @Action(
-    publishing = Publishing.ENABLED         // <.>
+    command = CommandReification.DISABLED
     , semantics = SemanticsOf.IDEMPOTENT
-    , associateWith = "property"
+    , associateWith = "propertyCommandDisabled"
     , associateWithSequence = "2"
 )
 @ActionLayout(
     named = "Mixin Update Property"
-    , describedAs = "@Action(publishing = ENABLED)"
+    , describedAs = "@Action(command = DISABLED)"
 )
-public class ActionCommandJdo_mixinUpdateProperty {
+public class ActionCommandJdo_mixinUpdatePropertyCommandDisabled {
     // ...
 //end::class[]
 
     private final ActionCommandJdo actionCommandJdo;
 
-    public ActionCommandJdo_mixinUpdateProperty(ActionCommandJdo actionCommandJdo) {
+    public ActionCommandJdo_mixinUpdatePropertyCommandDisabled(ActionCommandJdo actionCommandJdo) {
         this.actionCommandJdo = actionCommandJdo;
     }
 
diff --git a/examples/demo/domain/src/main/java/demoapp/dom/annotDomain/Action/command/ActionCommandJdo_mixinUpdatePropertyMetaAnnotation.java b/examples/demo/domain/src/main/java/demoapp/dom/annotDomain/Action/command/ActionCommandJdo_mixinUpdatePropertyMetaAnnotation.java
index 5d3e7e7..5adeda7 100644
--- a/examples/demo/domain/src/main/java/demoapp/dom/annotDomain/Action/command/ActionCommandJdo_mixinUpdatePropertyMetaAnnotation.java
+++ b/examples/demo/domain/src/main/java/demoapp/dom/annotDomain/Action/command/ActionCommandJdo_mixinUpdatePropertyMetaAnnotation.java
@@ -4,6 +4,8 @@ import org.apache.isis.applib.annotation.Action;
 import org.apache.isis.applib.annotation.ActionLayout;
 import org.apache.isis.applib.annotation.SemanticsOf;
 
+import lombok.RequiredArgsConstructor;
+
 //tag::class[]
 @ActionCommandEnabledMetaAnnotation     // <.>
 @Action(
@@ -15,17 +17,11 @@ import org.apache.isis.applib.annotation.SemanticsOf;
     named = "Mixin Update Property"
     , describedAs = "@ActionPublishingEnabledMetaAnnotation"
 )
+@RequiredArgsConstructor
 public class ActionCommandJdo_mixinUpdatePropertyMetaAnnotation {
-    // ...
-//end::class[]
 
     private final ActionCommandJdo actionCommandJdo;
 
-    public ActionCommandJdo_mixinUpdatePropertyMetaAnnotation(ActionCommandJdo actionCommandJdo) {
-        this.actionCommandJdo = actionCommandJdo;
-    }
-
-//tag::class[]
     public ActionCommandJdo act(final String value) {
         actionCommandJdo.setPropertyMetaAnnotated(value);
         return actionCommandJdo;
diff --git a/examples/demo/domain/src/main/java/demoapp/dom/annotDomain/Action/command/ActionCommandJdo_mixinUpdatePropertyMetaAnnotationOverridden.java b/examples/demo/domain/src/main/java/demoapp/dom/annotDomain/Action/command/ActionCommandJdo_mixinUpdatePropertyMetaAnnotationOverridden.java
index 52f6ac7..6f42ebf 100644
--- a/examples/demo/domain/src/main/java/demoapp/dom/annotDomain/Action/command/ActionCommandJdo_mixinUpdatePropertyMetaAnnotationOverridden.java
+++ b/examples/demo/domain/src/main/java/demoapp/dom/annotDomain/Action/command/ActionCommandJdo_mixinUpdatePropertyMetaAnnotationOverridden.java
@@ -2,13 +2,14 @@ package demoapp.dom.annotDomain.Action.command;
 
 import org.apache.isis.applib.annotation.Action;
 import org.apache.isis.applib.annotation.ActionLayout;
+import org.apache.isis.applib.annotation.CommandReification;
 import org.apache.isis.applib.annotation.Publishing;
 import org.apache.isis.applib.annotation.SemanticsOf;
 
 //tag::class[]
-@ActionCommandDisabledMetaAnnotation     // <.>
+@ActionCommandDisabledMetaAnnotation        // <.>
 @Action(
-    publishing = Publishing.ENABLED         // <.>
+    command = CommandReification.ENABLED    // <.>
     , semantics = SemanticsOf.IDEMPOTENT
     , associateWith = "propertyMetaAnnotatedOverridden"
     , associateWithSequence = "2"
@@ -16,8 +17,8 @@ import org.apache.isis.applib.annotation.SemanticsOf;
 @ActionLayout(
     named = "Mixin Update Property"
     , describedAs =
-        "@ActionPublishingDisabledMetaAnnotation " +
-        "@Action(publishing = ENABLED)"
+        "@ActionCommandDisabledMetaAnnotation " +
+        "@Action(command = ENABLED)"
 )
 public class ActionCommandJdo_mixinUpdatePropertyMetaAnnotationOverridden {
     // ...
diff --git a/examples/demo/domain/src/main/java/demoapp/dom/events/DemoEventSubscriber.java b/examples/demo/domain/src/main/java/demoapp/dom/events/DemoEventSubscriber.java
index 0a7223d..7b84a69 100644
--- a/examples/demo/domain/src/main/java/demoapp/dom/events/DemoEventSubscriber.java
+++ b/examples/demo/domain/src/main/java/demoapp/dom/events/DemoEventSubscriber.java
@@ -55,7 +55,7 @@ public class DemoEventSubscriber {
         
         val eventLogWriter = factoryService.get(EventLogWriter.class); // <-- get a new writer
         
-        wrapper.asyncWrap(eventLogWriter, AsyncControl.control()).storeEvent(event);
+        wrapper.asyncWrap(eventLogWriter, AsyncControl.returningVoid()).storeEvent(event);
 
     }
 
diff --git a/examples/demo/domain/src/main/java/demoapp/dom/menubars.layout.xml b/examples/demo/domain/src/main/java/demoapp/dom/menubars.layout.xml
index b37889b..2f08098 100644
--- a/examples/demo/domain/src/main/java/demoapp/dom/menubars.layout.xml
+++ b/examples/demo/domain/src/main/java/demoapp/dom/menubars.layout.xml
@@ -10,6 +10,7 @@ as a replacement for
         xmlns:cpt="http://isis.apache.org/applib/layout/component"
         xmlns:mb3="http://isis.apache.org/applib/layout/menubars/bootstrap3"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+
     <mb3:primary>
         <mb3:menu>
             <mb3:named>Basic Types</mb3:named>
@@ -105,7 +106,7 @@ as a replacement for
         </mb3:menu>
 
         <mb3:menu>
-            <mb3:named>Domain Annotations</mb3:named>
+            <mb3:named>Domain Annot</mb3:named>
             <mb3:section>
                 <mb3:named>@DomainObject</mb3:named>
                 <mb3:serviceAction objectType="demo.DomainObjectMenu" id="publishing"/>
@@ -134,7 +135,7 @@ as a replacement for
         </mb3:menu>
 
         <mb3:menu>
-            <mb3:named>Layout Annotations</mb3:named>
+            <mb3:named>Layout Annot</mb3:named>
             <mb3:section>
                 <mb3:named>@ActionLayout</mb3:named>
                 <mb3:serviceAction objectType="demo.ActionLayoutMenu" id="position"/>
@@ -156,6 +157,13 @@ as a replacement for
         </mb3:menu>
 
         <mb3:menu>
+            <mb3:named>Services</mb3:named>
+            <mb3:section>
+                <mb3:serviceAction objectType="demo.ServicesMenu" id="wrapperFactory"/>
+            </mb3:section>
+        </mb3:menu>
+
+        <mb3:menu>
             <mb3:named>View Models</mb3:named>
             <mb3:section>
                 <mb3:serviceAction objectType="demo.ViewModelMenu" id="stateful"/>
@@ -388,6 +396,7 @@ as a replacement for
         </mb3:menu>
         
     </mb3:secondary>
+
     <mb3:tertiary>
         <mb3:menu>
         	<mb3:named/>
diff --git a/examples/demo/domain/src/main/java/demoapp/dom/services/ServicesMenu.java b/examples/demo/domain/src/main/java/demoapp/dom/services/ServicesMenu.java
new file mode 100644
index 0000000..7163f41
--- /dev/null
+++ b/examples/demo/domain/src/main/java/demoapp/dom/services/ServicesMenu.java
@@ -0,0 +1,48 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package demoapp.dom.services;
+
+import javax.inject.Inject;
+
+import org.apache.isis.applib.annotation.Action;
+import org.apache.isis.applib.annotation.ActionLayout;
+import org.apache.isis.applib.annotation.DomainService;
+import org.apache.isis.applib.annotation.NatureOfService;
+import org.apache.isis.applib.annotation.SemanticsOf;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+
+import demoapp.dom.services.wrapperFactory.WrapperFactoryJdo;
+import demoapp.dom.services.wrapperFactory.WrapperFactoryJdoEntities;
+
+@DomainService(nature=NatureOfService.VIEW, objectType = "demo.ServicesMenu")
+@Log4j2
+@RequiredArgsConstructor(onConstructor_ = {@Inject})
+public class ServicesMenu {
+
+    final WrapperFactoryJdoEntities wrapperFactoryJdoEntities;
+
+    @Action(semantics = SemanticsOf.SAFE)
+    @ActionLayout(cssClassFa="fa-gift", describedAs = "Formal object interactions + async")
+    public WrapperFactoryJdo wrapperFactory(){
+        return wrapperFactoryJdoEntities.first();
+    }
+
+}
diff --git a/examples/demo/domain/src/main/java/demoapp/dom/services/wrapperFactory/WrapperFactoryJdo-description.adoc b/examples/demo/domain/src/main/java/demoapp/dom/services/wrapperFactory/WrapperFactoryJdo-description.adoc
new file mode 100644
index 0000000..7c13ad0
--- /dev/null
+++ b/examples/demo/domain/src/main/java/demoapp/dom/services/wrapperFactory/WrapperFactoryJdo-description.adoc
@@ -0,0 +1,2 @@
+CAUTION: TODO - to document
+
diff --git a/examples/demo/domain/src/main/java/demoapp/dom/services/wrapperFactory/WrapperFactoryJdo.java b/examples/demo/domain/src/main/java/demoapp/dom/services/wrapperFactory/WrapperFactoryJdo.java
new file mode 100644
index 0000000..63d1181
--- /dev/null
+++ b/examples/demo/domain/src/main/java/demoapp/dom/services/wrapperFactory/WrapperFactoryJdo.java
@@ -0,0 +1,127 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package demoapp.dom.services.wrapperFactory;
+
+import javax.inject.Inject;
+import javax.jdo.annotations.DatastoreIdentity;
+import javax.jdo.annotations.IdGeneratorStrategy;
+import javax.jdo.annotations.IdentityType;
+import javax.jdo.annotations.PersistenceCapable;
+
+import org.apache.isis.applib.annotation.Action;
+import org.apache.isis.applib.annotation.ActionLayout;
+import org.apache.isis.applib.annotation.DomainObject;
+import org.apache.isis.applib.annotation.Editing;
+import org.apache.isis.applib.annotation.MemberOrder;
+import org.apache.isis.applib.annotation.Nature;
+import org.apache.isis.applib.annotation.Property;
+import org.apache.isis.applib.annotation.SemanticsOf;
+import org.apache.isis.applib.services.factory.FactoryService;
+import org.apache.isis.applib.services.wrapper.WrapperFactory;
+import org.apache.isis.applib.services.wrapper.control.AsyncControl;
+
+import lombok.Getter;
+import lombok.Setter;
+import lombok.val;
+
+import demoapp.dom._infra.asciidocdesc.HasAsciiDocDescription;
+import demoapp.dom.annotDomain._commands.ExposePersistedCommands;
+
+//tag::class[]
+@PersistenceCapable(identityType = IdentityType.DATASTORE, schema = "demo")
+@DatastoreIdentity(strategy = IdGeneratorStrategy.IDENTITY, column = "id")
+@DomainObject(
+        nature=Nature.JDO_ENTITY
+        , objectType = "demo.WrapperFactoryJdo"
+        , editing = Editing.DISABLED
+)
+public class WrapperFactoryJdo
+        implements HasAsciiDocDescription, ExposePersistedCommands {
+
+    @Inject WrapperFactory wrapperFactory;
+    @Inject FactoryService factoryService;
+
+    // ...
+//end::class[]
+
+    public WrapperFactoryJdo(String initialValue) {
+        this.propertyAsync = initialValue;
+        this.propertyAsyncMixin = initialValue;
+    }
+
+    public String title() {
+        return "WrapperFactory";
+    }
+
+//tag::property[]
+    @Property()
+    @MemberOrder(name = "async", sequence = "1")
+    @Getter @Setter
+    private String propertyAsync;
+
+    @Property()
+    @MemberOrder(name = "async", sequence = "2")
+    @Getter @Setter
+    private String propertyAsyncMixin;
+//end::property[]
+
+//tag::async[]
+    @Action(
+        semantics = SemanticsOf.IDEMPOTENT
+        , associateWith = "propertyAsync"
+        , associateWithSequence = "1"
+    )
+    @ActionLayout(
+        describedAs = "@Action()"
+    )
+    public WrapperFactoryJdo updatePropertyAsync(final String value) {
+        val control = AsyncControl.returningVoid().withSkipRules();
+        val wrapperFactoryJdo = this.wrapperFactory.asyncWrap(this, control);
+        wrapperFactoryJdo.setPropertyAsync(value);
+        return this;
+    }
+    public String default0UpdatePropertyAsync() {
+        return getPropertyAsync();
+    }
+//end::async[]
+
+//tag::async[]
+    @Action(
+        semantics = SemanticsOf.IDEMPOTENT
+        , associateWith = "propertyAsyncMixin"
+        , associateWithSequence = "1"
+    )
+    @ActionLayout(
+        describedAs = "Calls the 'updatePropertyAsync' (mixin) action asynchronously"
+    )
+    public WrapperFactoryJdo updatePropertyUsingAsyncWrapMixin(final String value) {
+        val control = AsyncControl.returning(WrapperFactoryJdo.class).withSkipRules();
+        val mixin = this.wrapperFactory.asyncWrapMixin(WrapperFactoryJdo_updatePropertyAsyncMixin.class, this, control);
+        WrapperFactoryJdo act = mixin.act(value);
+        return this;
+    }
+    public String default0UpdatePropertyUsingAsyncWrapMixin() {
+        return new WrapperFactoryJdo_updatePropertyAsyncMixin(this).default0Act();
+    }
+//end::async[]
+
+//tag::class[]
+
+}
+//end::class[]
diff --git a/examples/demo/domain/src/main/java/demoapp/dom/annotDomain/Action/command/ActionCommandJdo.layout.xml b/examples/demo/domain/src/main/java/demoapp/dom/services/wrapperFactory/WrapperFactoryJdo.layout.xml
similarity index 66%
copy from examples/demo/domain/src/main/java/demoapp/dom/annotDomain/Action/command/ActionCommandJdo.layout.xml
copy to examples/demo/domain/src/main/java/demoapp/dom/services/wrapperFactory/WrapperFactoryJdo.layout.xml
index 757b050..f83f3d4 100644
--- a/examples/demo/domain/src/main/java/demoapp/dom/annotDomain/Action/command/ActionCommandJdo.layout.xml
+++ b/examples/demo/domain/src/main/java/demoapp/dom/services/wrapperFactory/WrapperFactoryJdo.layout.xml
@@ -17,29 +17,8 @@
 
 	<bs3:row>
 		<bs3:col span="6">
-			<bs3:row>
-				<bs3:col span="6">
-					<bs3:row>
-						<bs3:col span="12">
-							<cpt:fieldSet name="Annotated" id="annotation"/>
-							<cpt:fieldSet name="Async" id="async"/>
-						</bs3:col>
-					</bs3:row>
-				</bs3:col>
-				<bs3:col span="6">
-					<bs3:row>
-						<bs3:col span="12">
-							<cpt:fieldSet name="Meta-annotated" id="meta-annotated"/>
-							<cpt:fieldSet name="Meta-annotated Overridden" id="meta-annotated-overridden"/>
-						</bs3:col>
-					</bs3:row>
-				</bs3:col>
-			</bs3:row>
-			<bs3:row>
-				<bs3:col span="12">
-					<cpt:fieldSet name="Other" id="other" unreferencedProperties="true"/>
-				</bs3:col>
-			</bs3:row>
+			<cpt:fieldSet name="Async" id="async"/>
+			<cpt:fieldSet name="Other" id="other" unreferencedProperties="true"/>
 		</bs3:col>
 		<bs3:col span="6">
 			<cpt:fieldSet name="Description" id="description" >
@@ -55,11 +34,6 @@
 		</bs3:col>
 	</bs3:row>
 	<bs3:row>
-		<bs3:col span="12">
-			<cpt:collection id="commands"/>
-		</bs3:col>
-	</bs3:row>
-	<bs3:row>
 		<bs3:col span="12" unreferencedCollections="true"/>
 	</bs3:row>
 
diff --git a/examples/demo/domain/src/main/java/demoapp/dom/services/wrapperFactory/WrapperFactoryJdoEntities.java b/examples/demo/domain/src/main/java/demoapp/dom/services/wrapperFactory/WrapperFactoryJdoEntities.java
new file mode 100644
index 0000000..ae414e3
--- /dev/null
+++ b/examples/demo/domain/src/main/java/demoapp/dom/services/wrapperFactory/WrapperFactoryJdoEntities.java
@@ -0,0 +1,33 @@
+package demoapp.dom.services.wrapperFactory;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+import javax.inject.Inject;
+
+import org.springframework.stereotype.Service;
+
+import org.apache.isis.applib.services.repository.RepositoryService;
+
+import lombok.RequiredArgsConstructor;
+
+@Service
+@RequiredArgsConstructor(onConstructor_ = {@Inject})
+public class WrapperFactoryJdoEntities {
+
+    final RepositoryService repositoryService;
+
+    public Optional<WrapperFactoryJdo> find(final String value) {
+        return repositoryService.firstMatch(WrapperFactoryJdo.class, x -> Objects.equals(x.getPropertyAsync(), value));
+    }
+
+    public List<WrapperFactoryJdo> all() {
+        return repositoryService.allInstances(WrapperFactoryJdo.class);
+    }
+
+    public WrapperFactoryJdo first() {
+        return all().stream().findFirst().get();
+    }
+
+}
diff --git a/examples/demo/domain/src/main/java/demoapp/dom/services/wrapperFactory/WrapperFactoryJdoSeedService.java b/examples/demo/domain/src/main/java/demoapp/dom/services/wrapperFactory/WrapperFactoryJdoSeedService.java
new file mode 100644
index 0000000..3389e0e
--- /dev/null
+++ b/examples/demo/domain/src/main/java/demoapp/dom/services/wrapperFactory/WrapperFactoryJdoSeedService.java
@@ -0,0 +1,39 @@
+package demoapp.dom.services.wrapperFactory;
+
+import javax.inject.Inject;
+
+import org.springframework.stereotype.Service;
+
+import org.apache.isis.applib.services.repository.RepositoryService;
+import org.apache.isis.testing.fixtures.applib.fixturescripts.FixtureScript;
+
+import demoapp.dom._infra.seed.SeedServiceAbstract;
+import demoapp.dom.types.Samples;
+
+@Service
+public class WrapperFactoryJdoSeedService extends SeedServiceAbstract {
+
+    public WrapperFactoryJdoSeedService() {
+        super(PropertyPublishingJdoEntityFixture::new);
+    }
+
+    static class PropertyPublishingJdoEntityFixture extends FixtureScript {
+
+        @Override
+        protected void execute(ExecutionContext executionContext) {
+            samples.stream()
+                    .map(WrapperFactoryJdo::new)
+                    .forEach(domainObject -> {
+                        repositoryService.persist(domainObject);
+                        executionContext.addResult(this, domainObject);
+                    });
+
+        }
+
+        @Inject
+        RepositoryService repositoryService;
+
+        @Inject
+        Samples<String> samples;
+    }
+}
diff --git a/examples/demo/domain/src/main/java/demoapp/dom/services/wrapperFactory/WrapperFactoryJdo_mixinUpdatePropertyAsync.java b/examples/demo/domain/src/main/java/demoapp/dom/services/wrapperFactory/WrapperFactoryJdo_mixinUpdatePropertyAsync.java
new file mode 100644
index 0000000..626ddcf
--- /dev/null
+++ b/examples/demo/domain/src/main/java/demoapp/dom/services/wrapperFactory/WrapperFactoryJdo_mixinUpdatePropertyAsync.java
@@ -0,0 +1,44 @@
+package demoapp.dom.services.wrapperFactory;
+
+import javax.inject.Inject;
+
+import org.apache.isis.applib.annotation.Action;
+import org.apache.isis.applib.annotation.ActionLayout;
+import org.apache.isis.applib.annotation.SemanticsOf;
+import org.apache.isis.applib.services.wrapper.WrapperFactory;
+import org.apache.isis.applib.services.wrapper.control.AsyncControl;
+
+import lombok.RequiredArgsConstructor;
+import lombok.val;
+
+//tag::class[]
+@Action(
+    semantics = SemanticsOf.IDEMPOTENT
+    , associateWith = "propertyAsync"
+    , associateWithSequence = "2"
+)
+@ActionLayout(
+    named = "Mixin Update Property"
+)
+@RequiredArgsConstructor
+public class WrapperFactoryJdo_mixinUpdatePropertyAsync {
+
+    @Inject WrapperFactory wrapperFactory;
+
+    // ...
+//end::class[]
+
+    private final WrapperFactoryJdo wrapperFactoryJdo;
+
+//tag::class[]
+    public WrapperFactoryJdo act(final String value) {
+        val control = AsyncControl.returningVoid().withSkipRules();
+        val wrapped = this.wrapperFactory.asyncWrap(this.wrapperFactoryJdo, control);
+        wrapped.setPropertyAsync(value);
+        return this.wrapperFactoryJdo;
+    }
+    public String default0Act() {
+        return wrapperFactoryJdo.getPropertyAsync();
+    }
+}
+//end::class[]
diff --git a/examples/demo/domain/src/main/java/demoapp/dom/services/wrapperFactory/WrapperFactoryJdo_updatePropertyAsyncMixin.java b/examples/demo/domain/src/main/java/demoapp/dom/services/wrapperFactory/WrapperFactoryJdo_updatePropertyAsyncMixin.java
new file mode 100644
index 0000000..472b5d2
--- /dev/null
+++ b/examples/demo/domain/src/main/java/demoapp/dom/services/wrapperFactory/WrapperFactoryJdo_updatePropertyAsyncMixin.java
@@ -0,0 +1,40 @@
+package demoapp.dom.services.wrapperFactory;
+
+import javax.inject.Inject;
+
+import org.apache.isis.applib.annotation.Action;
+import org.apache.isis.applib.annotation.ActionLayout;
+import org.apache.isis.applib.annotation.SemanticsOf;
+import org.apache.isis.applib.services.wrapper.WrapperFactory;
+import org.apache.isis.applib.services.wrapper.control.AsyncControl;
+
+import lombok.RequiredArgsConstructor;
+import lombok.val;
+
+//tag::class[]
+@Action(
+    semantics = SemanticsOf.IDEMPOTENT
+    , associateWith = "propertyAsyncMixin"
+    , associateWithSequence = "2"
+)
+@ActionLayout(
+    named = "Update Property Async"
+    , describedAs = "Mixin that Updates 'property async mixin' directly"
+)
+@RequiredArgsConstructor
+public class WrapperFactoryJdo_updatePropertyAsyncMixin {
+    // ...
+//end::class[]
+
+    private final WrapperFactoryJdo wrapperFactoryJdo;
+
+//tag::class[]
+    public WrapperFactoryJdo act(final String value) {
+        wrapperFactoryJdo.setPropertyAsyncMixin(value);
+        return wrapperFactoryJdo;
+    }
+    public String default0Act() {
+        return wrapperFactoryJdo.getPropertyAsyncMixin();
+    }
+}
+//end::class[]
diff --git a/examples/smoketests/stable/src/main/java/org/apache/isis/testdomain/conf/Configuration_headless.java b/examples/smoketests/stable/src/main/java/org/apache/isis/testdomain/conf/Configuration_headless.java
index 9f3c21b..f48a11c 100644
--- a/examples/smoketests/stable/src/main/java/org/apache/isis/testdomain/conf/Configuration_headless.java
+++ b/examples/smoketests/stable/src/main/java/org/apache/isis/testdomain/conf/Configuration_headless.java
@@ -39,7 +39,6 @@ import org.springframework.transaction.TransactionStatus;
 
 import org.apache.isis.applib.annotation.OrderPrecedence;
 import org.apache.isis.applib.services.command.Command;
-import org.apache.isis.applib.services.command.CommandContext;
 
 import org.apache.isis.applib.services.command.CommandService;
 import org.apache.isis.applib.services.iactn.Interaction;
@@ -112,7 +111,6 @@ public class Configuration_headless {
     public static class HeadlessCommandSupport {
 
         private final Provider<InteractionContext> interactionContextProvider;
-        private final Provider<CommandContext> commandContextProvider;
         private final CommandService commandService;
 
 
@@ -133,21 +131,13 @@ public class Configuration_headless {
         
         public void setupCommandCreateIfMissing() {
             
-            val commandContext = commandContextProvider.get();
-            final Command command = Optional.ofNullable(commandContext.getCommand())
-                    .orElseGet(()->{
-                        val newCommand = commandService.create();
-                        commandContext.setCommand(newCommand);
-                        return newCommand;
-                    });
-
             val interactionContext = interactionContextProvider.get();
             @SuppressWarnings("unused")
             final Interaction interaction = Optional.ofNullable(interactionContext.getInteraction())
                     .orElseGet(()->{
-                        val newInteraction = new Interaction();
+                        val newCommand = new Command();
+                        val newInteraction = new Interaction(newCommand);
                         interactionContext.setInteraction(newInteraction);
-                        newInteraction.setUniqueId(command.getUniqueId());
                         return newInteraction;
                     });
         }
diff --git a/extensions/core/command-log/impl/src/main/java/org/apache/isis/extensions/commandlog/impl/CommandServiceListenerForJdo.java b/extensions/core/command-log/impl/src/main/java/org/apache/isis/extensions/commandlog/impl/CommandServiceListenerForJdo.java
index f3a4d81..a6b3d8c 100644
--- a/extensions/core/command-log/impl/src/main/java/org/apache/isis/extensions/commandlog/impl/CommandServiceListenerForJdo.java
+++ b/extensions/core/command-log/impl/src/main/java/org/apache/isis/extensions/commandlog/impl/CommandServiceListenerForJdo.java
@@ -18,6 +18,8 @@
  */
 package org.apache.isis.extensions.commandlog.impl;
 
+import java.util.Optional;
+
 import javax.inject.Inject;
 import javax.inject.Named;
 
@@ -28,8 +30,10 @@ import org.springframework.stereotype.Service;
 import org.apache.isis.applib.annotation.OrderPrecedence;
 import org.apache.isis.applib.services.command.Command;
 import org.apache.isis.applib.services.command.spi.CommandServiceListener;
+import org.apache.isis.applib.util.JaxbUtil;
 import org.apache.isis.extensions.commandlog.impl.jdo.CommandJdo;
 import org.apache.isis.extensions.commandlog.impl.jdo.CommandJdoRepository;
+import org.apache.isis.schema.cmd.v2.CommandDto;
 
 import lombok.RequiredArgsConstructor;
 import lombok.val;
@@ -52,17 +56,32 @@ public class CommandServiceListenerForJdo implements CommandServiceListener {
             return;
         }
 
-        val commandJdo = new CommandJdo(command);
-        val parent = command.getParent();
-        val parentJdo =
-            parent != null
-                ? commandJdoRepository
-                    .findByUniqueId(parent.getUniqueId())
-                    .orElse(null)
-                : null;
-        commandJdo.setParent(parentJdo);
+        val existingCommandJdoIfAny =
+                commandJdoRepository.findByUniqueId(command.getUniqueId());
+        if(existingCommandJdoIfAny.isPresent()) {
+            if(true || log.isDebugEnabled()) {
+                val existingCommandDto = existingCommandJdoIfAny.get().getCommandDto();
+
+                String existingCommandDtoXml = JaxbUtil.toXml(existingCommandDto);
+                String commandDtoXml = JaxbUtil.toXml(command.getCommandDto());
+
+                log.debug("existing: \n" + existingCommandDtoXml);
+                log.debug("proposed: \n" + commandDtoXml);
+            }
+        } else {
+            val commandJdo = new CommandJdo(command);
+            val parent = command.getParent();
+            val parentJdo =
+                parent != null
+                    ? commandJdoRepository
+                        .findByUniqueId(parent.getUniqueId())
+                        .orElse(null)
+                    : null;
+            commandJdo.setParent(parentJdo);
+            commandJdoRepository.persist(commandJdo);
+        }
+
 
-        commandJdoRepository.persist(commandJdo);
     }
 
 }
diff --git a/extensions/core/command-log/impl/src/main/java/org/apache/isis/extensions/commandlog/impl/jdo/CommandJdoRepository.java b/extensions/core/command-log/impl/src/main/java/org/apache/isis/extensions/commandlog/impl/jdo/CommandJdoRepository.java
index cee771a..2b3450f 100644
--- a/extensions/core/command-log/impl/src/main/java/org/apache/isis/extensions/commandlog/impl/jdo/CommandJdoRepository.java
+++ b/extensions/core/command-log/impl/src/main/java/org/apache/isis/extensions/commandlog/impl/jdo/CommandJdoRepository.java
@@ -104,7 +104,6 @@ public class CommandJdoRepository {
 
 
     public Optional<CommandJdo> findByUniqueId(final UUID uniqueId) {
-        persistCurrentCommandIfRequired();
         return repositoryService.firstMatch(
                 new QueryDefault<>(CommandJdo.class,
                         "findByUniqueIdStr",
@@ -120,35 +119,17 @@ public class CommandJdoRepository {
 
 
     public List<CommandJdo> findCurrent() {
-        persistCurrentCommandIfRequired();
         return repositoryService.allMatches(
                 new QueryDefault<>(CommandJdo.class, "findCurrent"));
     }
 
 
     public List<CommandJdo> findCompleted() {
-        persistCurrentCommandIfRequired();
         return repositoryService.allMatches(
                 new QueryDefault<>(CommandJdo.class, "findCompleted"));
     }
 
 
-    private void persistCurrentCommandIfRequired() {
-        if(interactionContextProvider == null) {
-            // hmm, this shouldn't happen..
-            return;
-        }
-
-        val command = interactionContextProvider.get().getInteraction().getCommand();
-        val systemStateChanged = command.isSystemStateChanged();
-        if(!systemStateChanged) {
-            return;
-        }
-        final CommandJdo commandJdo = new CommandJdo(command);
-        repositoryService.persist(commandJdo);
-    }
-
-
     public List<CommandJdo> findByTargetAndFromAndTo(
             final Bookmark target
             , final LocalDate from
@@ -381,6 +362,8 @@ public class CommandJdoRepository {
         repositoryService.persist(commandJdo);
     }
 
-
+    public void truncateLog() {
+        repositoryService.removeAll(CommandJdo.class);
+    }
 
 }
diff --git a/extensions/core/command-log/impl/src/main/java/org/apache/isis/extensions/commandlog/impl/ui/CommandServiceMenu.java b/extensions/core/command-log/impl/src/main/java/org/apache/isis/extensions/commandlog/impl/ui/CommandServiceMenu.java
index ce48ad7..4e36848 100644
--- a/extensions/core/command-log/impl/src/main/java/org/apache/isis/extensions/commandlog/impl/ui/CommandServiceMenu.java
+++ b/extensions/core/command-log/impl/src/main/java/org/apache/isis/extensions/commandlog/impl/ui/CommandServiceMenu.java
@@ -22,12 +22,15 @@ import org.apache.isis.applib.annotation.Optionality;
 import org.apache.isis.applib.annotation.OrderPrecedence;
 import org.apache.isis.applib.annotation.Parameter;
 import org.apache.isis.applib.annotation.ParameterLayout;
+import org.apache.isis.applib.annotation.RestrictTo;
 import org.apache.isis.applib.annotation.SemanticsOf;
 import org.apache.isis.applib.services.clock.ClockService;
 import org.apache.isis.extensions.commandlog.impl.IsisModuleExtCommandLogImpl;
 import org.apache.isis.extensions.commandlog.impl.jdo.CommandJdo;
 import org.apache.isis.extensions.commandlog.impl.jdo.CommandJdoRepository;
 
+import lombok.RequiredArgsConstructor;
+
 @DomainService(
     nature = NatureOfService.VIEW,
     objectType = "isisExtensionsCommandLog.CommandServiceMenu"
@@ -40,6 +43,7 @@ import org.apache.isis.extensions.commandlog.impl.jdo.CommandJdoRepository;
 @Named("isisExtensionsCommandLog.CommandServiceMenu")
 @Order(OrderPrecedence.MIDPOINT)
 @Qualifier("Jdo")
+@RequiredArgsConstructor(onConstructor_ = { @Inject })
 public class CommandServiceMenu {
 
     public static abstract class PropertyDomainEvent<T>
@@ -50,6 +54,8 @@ public class CommandServiceMenu {
             extends IsisModuleExtCommandLogImpl.ActionDomainEvent<CommandServiceMenu> {
     }
 
+    final CommandJdoRepository commandServiceRepository;
+    final ClockService clockService;
 
     public static class ActiveCommandsDomainEvent extends ActionDomainEvent { }
     @Action(domainEvent = ActiveCommandsDomainEvent.class, semantics = SemanticsOf.SAFE)
@@ -101,7 +107,14 @@ public class CommandServiceMenu {
     }
 
 
-    @Inject CommandJdoRepository commandServiceRepository;
-    @Inject ClockService clockService;
+    public static class TruncateLogDomainEvent extends ActionDomainEvent { }
+    @Action(domainEvent = TruncateLogDomainEvent.class, semantics = SemanticsOf.IDEMPOTENT_ARE_YOU_SURE, restrictTo = RestrictTo.PROTOTYPING)
+    @ActionLayout(cssClassFa = "fa-trash")
+    @MemberOrder(sequence="40")
+    public void truncateLog() {
+        commandServiceRepository.truncateLog();
+    }
+
+
 }
 
diff --git a/extensions/core/command-replay/secondary/src/test/java/org/apache/isis/extensions/commandreplay/secondary/fetch/CommandFetcher_Test.java b/extensions/core/command-replay/secondary/src/test/java/org/apache/isis/extensions/commandreplay/secondary/fetch/CommandFetcher_Test.java
index 4fbc55b..eae6546 100644
--- a/extensions/core/command-replay/secondary/src/test/java/org/apache/isis/extensions/commandreplay/secondary/fetch/CommandFetcher_Test.java
+++ b/extensions/core/command-replay/secondary/src/test/java/org/apache/isis/extensions/commandreplay/secondary/fetch/CommandFetcher_Test.java
@@ -4,6 +4,7 @@ import java.net.URI;
 
 import javax.ws.rs.core.UriBuilder;
 
+import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 
 import org.apache.isis.applib.services.jaxb.JaxbService;
@@ -17,6 +18,7 @@ import lombok.val;
 
 public class CommandFetcher_Test {
 
+    @Disabled // intended only for manual verification.
     @Test
     public void testing_the_unmarshalling() {
         val jaxRsClient = new JaxRsClientDefault();
diff --git a/incubator/viewers/vaadin/ui/src/main/java/org/apache/isis/incubator/viewer/vaadin/ui/auth/VaadinAuthenticationHandler.java b/incubator/viewers/vaadin/ui/src/main/java/org/apache/isis/incubator/viewer/vaadin/ui/auth/VaadinAuthenticationHandler.java
index d35a3d5..f07ae72 100644
--- a/incubator/viewers/vaadin/ui/src/main/java/org/apache/isis/incubator/viewer/vaadin/ui/auth/VaadinAuthenticationHandler.java
+++ b/incubator/viewers/vaadin/ui/src/main/java/org/apache/isis/incubator/viewer/vaadin/ui/auth/VaadinAuthenticationHandler.java
@@ -111,7 +111,7 @@ public class VaadinAuthenticationHandler implements VaadinServiceInitListener {
         
         val authSession = AuthSessionStoreUtil.get().orElse(null);
         if(authSession!=null) {
-            isisInteractionFactory.openSession(authSession);
+            isisInteractionFactory.openInteraction(authSession);
             return; // access granted
         }
         // otherwise redirect to login page
diff --git a/legacy/extensions/core/applib/src/main/java/org/apache/isis/legacy/applib/services/eventbus/ActionDomainEvent.java b/legacy/extensions/core/applib/src/main/java/org/apache/isis/legacy/applib/services/eventbus/ActionDomainEvent.java
index 3714f9e..47ff80c 100644
--- a/legacy/extensions/core/applib/src/main/java/org/apache/isis/legacy/applib/services/eventbus/ActionDomainEvent.java
+++ b/legacy/extensions/core/applib/src/main/java/org/apache/isis/legacy/applib/services/eventbus/ActionDomainEvent.java
@@ -24,7 +24,7 @@ import java.util.List;
 import org.apache.isis.applib.annotation.SemanticsOf;
 import org.apache.isis.applib.events.domain.AbstractDomainEvent;
 import org.apache.isis.applib.services.command.Command;
-import org.apache.isis.applib.services.command.CommandContext;
+import org.apache.isis.applib.services.iactn.Interaction;
 import org.apache.isis.applib.util.ObjectContracts;
 import org.apache.isis.applib.util.ToString;
 
@@ -77,7 +77,7 @@ public abstract class ActionDomainEvent<S> extends AbstractDomainEvent<S> {
     private Command command;
 
     /**
-     * @deprecated - use {@link CommandContext#getCommand()} to obtain the current {@link Command}.
+     * @deprecated - use {@link Interaction#getCommand()} to obtain the current {@link Command}.
      */
     @Deprecated
     public Command getCommand() {
@@ -87,7 +87,7 @@ public abstract class ActionDomainEvent<S> extends AbstractDomainEvent<S> {
     /**
      * Not API - set by the framework.
      *
-     * @deprecated - the corresponding {@link #getCommand()} should not be called, instead use {@link CommandContext#getCommand()} to obtain the current {@link Command}.
+     * @deprecated - the corresponding {@link #getCommand()} should not be called, instead use {@link Interaction#getCommand()} to obtain the current {@link Command}.
      */
     @Deprecated
     public void setCommand(Command command) {
diff --git a/testing/integtestsupport/adoc/modules/integtestsupport/pages/about.adoc b/testing/integtestsupport/adoc/modules/integtestsupport/pages/about.adoc
index 49a49a8..cd400d4 100644
--- a/testing/integtestsupport/adoc/modules/integtestsupport/pages/about.adoc
+++ b/testing/integtestsupport/adoc/modules/integtestsupport/pages/about.adoc
@@ -380,7 +380,7 @@ This is discussed further xref:about.adoc#wrapper-factory[below].
 for more control over transactions
 
 
-The class also defines (as a nested static class) the `CommandSupport` as a domain service.
+The class also defines (as a nested static class) the `InteractionSupport` as a domain service.
 If ``@Import``ed into the integration test's "app manifest", this ensures that any ``Command``s that are raised as the result of interactions through the wrapper factory are setup correctly.
 
 
diff --git a/testing/integtestsupport/applib/src/main/java/org/apache/isis/testing/integtestsupport/applib/IsisIntegrationTestAbstract.java b/testing/integtestsupport/applib/src/main/java/org/apache/isis/testing/integtestsupport/applib/IsisIntegrationTestAbstract.java
index 33c8535..3ae6cda 100644
--- a/testing/integtestsupport/applib/src/main/java/org/apache/isis/testing/integtestsupport/applib/IsisIntegrationTestAbstract.java
+++ b/testing/integtestsupport/applib/src/main/java/org/apache/isis/testing/integtestsupport/applib/IsisIntegrationTestAbstract.java
@@ -26,12 +26,10 @@ import org.springframework.context.event.EventListener;
 import org.springframework.core.annotation.Order;
 import org.springframework.stereotype.Service;
 
-import org.apache.isis.applib.annotation.Action;
-import org.apache.isis.applib.annotation.CommandReification;
 import org.apache.isis.applib.annotation.OrderPrecedence;
 import org.apache.isis.applib.services.command.Command;
-import org.apache.isis.applib.services.command.CommandContext;
 import org.apache.isis.applib.services.factory.FactoryService;
+import org.apache.isis.applib.services.iactn.InteractionContext;
 import org.apache.isis.applib.services.metamodel.MetaModelService;
 import org.apache.isis.applib.services.registry.ServiceRegistry;
 import org.apache.isis.applib.services.repository.RepositoryService;
@@ -43,7 +41,6 @@ import org.apache.isis.core.runtime.persistence.transaction.events.TransactionAf
 import lombok.AccessLevel;
 import lombok.Getter;
 import lombok.RequiredArgsConstructor;
-import lombok.val;
 
 /**
  * Convenient base class to extend for integration tests. 
@@ -54,14 +51,16 @@ import lombok.val;
 public abstract class IsisIntegrationTestAbstract {
 
     /**
-     * Hook to interact with {@link Command}s (currently unused).
+     * Hook to interact with
+     * {@link org.apache.isis.applib.services.iactn.Interaction}s and
+     * therefore also {@link Command}s (currently unused).
      */
     @Service
     @Order(OrderPrecedence.MIDPOINT)
     @RequiredArgsConstructor(onConstructor_ = {@Inject})
-    public static class CommandSupport {
+    public static class InteractionSupport {
 
-        private final Provider<CommandContext> commandContextProvider;
+        private final Provider<InteractionContext> interactionContextProvider;
 
         @EventListener
         public void on(final TransactionAfterBeginEvent event) {
diff --git a/testing/integtestsupport/applib/src/main/java/org/apache/isis/testing/integtestsupport/applib/IsisInteractionHandler.java b/testing/integtestsupport/applib/src/main/java/org/apache/isis/testing/integtestsupport/applib/IsisInteractionHandler.java
index 02f3a4b..17fa8e8 100644
--- a/testing/integtestsupport/applib/src/main/java/org/apache/isis/testing/integtestsupport/applib/IsisInteractionHandler.java
+++ b/testing/integtestsupport/applib/src/main/java/org/apache/isis/testing/integtestsupport/applib/IsisInteractionHandler.java
@@ -32,7 +32,7 @@ public class IsisInteractionHandler implements BeforeEachCallback, AfterEachCall
     @Override
     public void beforeEach(ExtensionContext extensionContext) throws Exception {
         isisInteractionFactory(extensionContext)
-        .ifPresent(isisInteractionFactory->isisInteractionFactory.openSession(new InitialisationSession()));
+        .ifPresent(isisInteractionFactory->isisInteractionFactory.openInteraction(new InitialisationSession()));
     }
     
     @Override
diff --git a/tooling/javamodel/pom.xml b/tooling/javamodel/pom.xml
index 7e8ee26..d542f61 100644
--- a/tooling/javamodel/pom.xml
+++ b/tooling/javamodel/pom.xml
@@ -29,6 +29,7 @@
 
 	<properties>
 		<code-assert.version>0.9.13</code-assert.version>
+		<skipTests>true</skipTests>
 	</properties>
 
 	<dependencies>
diff --git a/tooling/projectmodel/pom.xml b/tooling/projectmodel/pom.xml
index 14b0f4c..efcbb0e 100644
--- a/tooling/projectmodel/pom.xml
+++ b/tooling/projectmodel/pom.xml
@@ -28,6 +28,7 @@
     </description>
 
 	<properties>
+		<skipTests>true</skipTests>
 	</properties>
 
 	<dependencies>
diff --git a/viewers/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/errors/JGrowlUtil.java b/viewers/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/errors/JGrowlUtil.java
index b358bbf..04c3fec 100644
--- a/viewers/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/errors/JGrowlUtil.java
+++ b/viewers/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/errors/JGrowlUtil.java
@@ -47,10 +47,10 @@ public class JGrowlUtil {
         final StringBuilder buf = new StringBuilder();
 
         for (String info : messageBroker.getMessages()) {
-            addJGrowlCall(info, MessageSeverity.INFO, buf);
+            addJGrowlCall(info, JGrowlUtil.MessageSeverity.INFO, buf);
         }
         for (String warning : messageBroker.getWarnings()) {
-            addJGrowlCall(warning, MessageSeverity.WARNING, buf);
+            addJGrowlCall(warning, JGrowlUtil.MessageSeverity.WARNING, buf);
         }
 
         final String error =  messageBroker.getApplicationError();
diff --git a/viewers/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/panels/FormExecutorDefault.java b/viewers/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/panels/FormExecutorDefault.java
index fcb048b..58e38ce 100644
--- a/viewers/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/panels/FormExecutorDefault.java
+++ b/viewers/wicket/ui/src/main/java/org/apache/isis/viewer/wicket/ui/panels/FormExecutorDefault.java
@@ -36,7 +36,6 @@ import org.apache.wicket.util.visit.IVisitor;
 
 import org.apache.isis.applib.RecoverableException;
 import org.apache.isis.applib.services.command.Command;
-import org.apache.isis.applib.services.command.CommandContext;
 import org.apache.isis.applib.services.exceprecog.ExceptionRecognizer;
 import org.apache.isis.applib.services.exceprecog.ExceptionRecognizer.Category;
 import org.apache.isis.applib.services.exceprecog.ExceptionRecognizer.Recognition;
@@ -45,7 +44,6 @@ import org.apache.isis.applib.services.i18n.TranslationService;
 import org.apache.isis.applib.services.message.MessageService;
 import org.apache.isis.applib.services.registry.ServiceRegistry;
 import org.apache.isis.core.commons.internal.collections._Sets;
-import org.apache.isis.core.commons.internal.exceptions._Exceptions;
 import org.apache.isis.core.metamodel.facets.actions.redirect.RedirectFacet;
 import org.apache.isis.core.metamodel.facets.properties.renderunchanged.UnchangingFacet;
 import org.apache.isis.core.metamodel.spec.ManagedObject;
@@ -242,7 +240,7 @@ implements FormExecutor {
             // irrespective, capture error in the Command, and propagate
             if (command != null) {
                 
-                command.internal().setException(ex);
+                command.updater().setException(ex);
                 
                 //XXX legacy of
                 //command.internal().setException(Throwables.getStackTraceAsString(ex));
@@ -487,11 +485,6 @@ implements FormExecutor {
         return getCommonContext().lookupServiceElseFail(WicketViewerSettings.class);
     }
 
-    // request-scoped
-    private Optional<CommandContext> currentCommandContext() {
-        return getServiceRegistry().lookupService(CommandContext.class);
-    }
-
     ///////////////////////////////////////////////////////////////////////////////
 
     private ManagedObject obtainTargetAdapter() {