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/01/05 10:17:44 UTC

[isis] branch master updated (be0440a -> 6b04900)

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

danhaywood pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/isis.git.


    from be0440a  ISIS-2158: fixes Demo source links
     new 61e4f55  ISIS-2250: adds config properties to lock down metamodel, or to incrementally validate otherwise.
     new 6b04900  ISIS-2250: sync-adoc

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../examples/services/command/Command.java         |  2 +-
 .../examples/services/iactn/Interaction.java       | 56 +++++++++++++---------
 .../isis/applib/services/command/Command.java      |  2 +-
 .../isis/applib/services/iactn/Interaction.java    | 56 +++++++++++++---------
 .../commons/internal/exceptions/_Exceptions.java   | 12 ++++-
 .../config/examples/generated/isis.reflector.adoc  | 18 ++++++-
 .../config/examples/generated/resteasy.adoc        |  9 ----
 .../org/apache/isis/config/IsisConfiguration.java  | 48 ++++++++++++++++++-
 .../isis/config/beans/IsisBeanTypeRegistry.java    | 11 +++--
 .../isis/metamodel/facets/DomainEventHelper.java   |  2 +-
 ...ctionInvocationFacetForDomainEventAbstract.java |  9 ++--
 ...tySetterOrClearFacetForDomainEventAbstract.java | 13 +++--
 .../isis/metamodel/progmodel/ProgrammingModel.java | 10 ++++
 .../dflt/ProgrammingModelFacetsJava8.java          | 22 +++++++++
 .../isis/metamodel/spec/ObjectSpecification.java   |  1 +
 .../metamodel/specloader/SpecificationLoader.java  |  5 +-
 .../specloader/SpecificationLoaderDefault.java     | 56 +++++++++++++++++-----
 .../specimpl/ObjectSpecificationAbstract.java      | 13 ++++-
 .../specimpl/dflt/ObjectSpecificationDefault.java  |  2 +
 .../validator/MetaModelValidatorVisiting.java      | 46 +++++++++++++++---
 .../SpecificationLoaderTestAbstract.java           |  1 +
 .../metamodel/JdoProgrammingModelPlugin.java       |  2 +
 .../persistence/IsisTransactionJdo.java            |  7 +--
 .../persistence/PersistenceSession5.java           |  2 +-
 24 files changed, 301 insertions(+), 104 deletions(-)


[isis] 02/02: ISIS-2250: sync-adoc

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 6b04900d6d47283d4ab2bc0f89fea56e0f60aeab
Author: danhaywood <da...@haywood-associates.co.uk>
AuthorDate: Sun Jan 5 10:15:45 2020 +0000

    ISIS-2250: sync-adoc
---
 .../examples/services/command/Command.java         |  2 +-
 .../examples/services/iactn/Interaction.java       | 56 +++++++++++++---------
 .../config/examples/generated/isis.reflector.adoc  | 18 ++++++-
 .../config/examples/generated/resteasy.adoc        |  9 ----
 .../SpecificationLoaderTestAbstract.java           |  1 +
 5 files changed, 51 insertions(+), 35 deletions(-)

diff --git a/core/applib/src/main/doc/modules/applib-svc/examples/services/command/Command.java b/core/applib/src/main/doc/modules/applib-svc/examples/services/command/Command.java
index fe83936..1eb9ff2 100644
--- a/core/applib/src/main/doc/modules/applib-svc/examples/services/command/Command.java
+++ b/core/applib/src/main/doc/modules/applib-svc/examples/services/command/Command.java
@@ -390,7 +390,7 @@ public interface Command extends HasUniqueId {
          * </p>
          *
          * See also {@link Interaction#getCurrentExecution()} and
-         * {@link Interaction.Execution#setStartedAt(Timestamp)}.
+         * {@link #setStartedAt(org.apache.isis.applib.services.clock.ClockService, org.apache.isis.applib.services.metrics.MetricsService)}.
          */
         void setStartedAt(Timestamp startedAt);
 
diff --git a/core/applib/src/main/doc/modules/applib-svc/examples/services/iactn/Interaction.java b/core/applib/src/main/doc/modules/applib-svc/examples/services/iactn/Interaction.java
index 4cb19a9..d53e822 100644
--- a/core/applib/src/main/doc/modules/applib-svc/examples/services/iactn/Interaction.java
+++ b/core/applib/src/main/doc/modules/applib-svc/examples/services/iactn/Interaction.java
@@ -27,10 +27,7 @@ import java.util.UUID;
 import java.util.concurrent.Callable;
 import java.util.concurrent.atomic.LongAdder;
 
-import javax.inject.Inject;
-
 import org.apache.isis.applib.annotation.Programmatic;
-import org.apache.isis.applib.annotation.Value;
 import org.apache.isis.applib.events.domain.AbstractDomainEvent;
 import org.apache.isis.applib.events.domain.ActionDomainEvent;
 import org.apache.isis.applib.events.domain.PropertyDomainEvent;
@@ -40,7 +37,6 @@ import org.apache.isis.applib.services.command.Command;
 import org.apache.isis.applib.services.eventbus.EventBusService;
 import org.apache.isis.applib.services.metrics.MetricsService;
 import org.apache.isis.applib.services.wrapper.WrapperFactory;
-import org.apache.isis.applib.services.xactn.Transaction;
 import org.apache.isis.applib.util.schema.MemberExecutionDtoUtils;
 import org.apache.isis.commons.internal.collections._Lists;
 import org.apache.isis.commons.internal.collections._Maps;
@@ -54,6 +50,8 @@ import org.apache.isis.schema.ixn.v1.ObjectCountsDto;
 import org.apache.isis.schema.ixn.v1.PropertyEditDto;
 import org.apache.isis.schema.jaxbadapters.JavaSqlTimestampXmlGregorianCalendarAdapter;
 
+import lombok.val;
+
 /**
  * Represents an action invocation or property modification, resulting in some state change of the system.  It captures
  * not only the target object and arguments passed, but also builds up the call-graph, and captures metrics, eg
@@ -79,7 +77,6 @@ import org.apache.isis.schema.jaxbadapters.JavaSqlTimestampXmlGregorianCalendarA
  * </p>
  *
  */
-@Value
 public class Interaction implements HasUniqueId {
 
     // -- transactionId (property)
@@ -135,11 +132,13 @@ public class Interaction implements HasUniqueId {
     @Programmatic
     public Object execute(
             final MemberExecutor<ActionInvocation> memberExecutor,
-            final ActionInvocation actionInvocation) {
+            final ActionInvocation actionInvocation,
+            final ClockService clockService,
+            final MetricsService metricsService) {
 
         push(actionInvocation);
 
-        return executeInternal(memberExecutor, actionInvocation);
+        return executeInternal(memberExecutor, actionInvocation, clockService, metricsService);
     }
 
     /**
@@ -153,16 +152,20 @@ public class Interaction implements HasUniqueId {
     @Programmatic
     public Object execute(
             final MemberExecutor<PropertyEdit> memberExecutor,
-            final PropertyEdit propertyEdit) {
+            final PropertyEdit propertyEdit,
+            final ClockService clockService,
+            final MetricsService metricsService) {
 
         push(propertyEdit);
 
-        return executeInternal(memberExecutor, propertyEdit);
+        return executeInternal(memberExecutor, propertyEdit, clockService, metricsService);
     }
 
     private <T extends Execution<?,?>> Object executeInternal(
             final MemberExecutor<T> memberExecutor,
-            final T execution) {
+            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.
@@ -190,7 +193,7 @@ public class Interaction implements HasUniqueId {
             }
         } finally {
             final Timestamp completedAt = clockService.nowAsJavaSqlTimestamp();
-            pop(completedAt);
+            pop(completedAt, metricsService);
         }
     }
 
@@ -238,12 +241,14 @@ public class Interaction implements HasUniqueId {
      * </p>
      */
     @Programmatic
-    private Execution<?,?> pop(final Timestamp completedAt) {
+    private Execution<?,?> pop(
+            final Timestamp completedAt,
+            final MetricsService metricsService) {
         if(currentExecution == null) {
             throw new IllegalStateException("No current execution to pop");
         }
         final Execution<?,?> popped = currentExecution;
-        popped.setCompletedAt(completedAt);
+        popped.setCompletedAt(completedAt, metricsService);
 
         moveCurrentTo(currentExecution.getParent());
         return popped;
@@ -301,7 +306,7 @@ public class Interaction implements HasUniqueId {
          */
         PUBLISHED_EVENT,
         /**
-         * There may be multiple transactions within a given interaction, as per {@link Transaction#getSequence()}.
+         * There may be multiple transactions within a given interaction.
          */
         TRANSACTION,
         ;
@@ -483,8 +488,12 @@ public class Interaction implements HasUniqueId {
         }
 
         @Programmatic
-        public void setStartedAt(final Timestamp startedAt) {
-            syncMetrics(When.BEFORE, startedAt);
+        public Timestamp start(
+                final ClockService clockService,
+                final MetricsService metricsService) {
+            val startedAt = clockService.nowAsJavaSqlTimestamp();
+            syncMetrics(When.BEFORE, startedAt, metricsService);
+            return startedAt;
         }
 
 
@@ -499,8 +508,10 @@ public class Interaction implements HasUniqueId {
         /**
          * <b>NOT API</b>: intended to be called only by the framework.
          */
-        void setCompletedAt(final Timestamp completedAt) {
-            syncMetrics(When.AFTER, completedAt);
+        void setCompletedAt(
+                final Timestamp completedAt,
+                final MetricsService metricsService) {
+            syncMetrics(When.AFTER, completedAt, metricsService);
         }
 
 
@@ -651,8 +662,10 @@ public class Interaction implements HasUniqueId {
                     final int numberObjectsLoaded,
                     final int numberObjectsDirtied);
         }
-        private void syncMetrics(final When when, final Timestamp timestamp) {
-            final MetricsService metricsService = interaction.metricsService;
+        private void syncMetrics(
+                final When when,
+                final Timestamp timestamp,
+                final MetricsService metricsService) {
 
             final int numberObjectsLoaded = metricsService.numberObjectsLoaded();
             final int numberObjectsDirtied = metricsService.numberObjectsDirtied();
@@ -706,7 +719,4 @@ public class Interaction implements HasUniqueId {
         }
     }
 
-    @Inject MetricsService metricsService;
-    @Inject ClockService clockService;
-
 }
diff --git a/core/config/src/main/doc/modules/config/examples/generated/isis.reflector.adoc b/core/config/src/main/doc/modules/config/examples/generated/isis.reflector.adoc
index 762e19c..dd2c13a 100644
--- a/core/config/src/main/doc/modules/config/examples/generated/isis.reflector.adoc
+++ b/core/config/src/main/doc/modules/config/examples/generated/isis.reflector.adoc
@@ -4,12 +4,26 @@ explicit-annotations.action
 |  Whether or not a public method needs to be annotated with @{@link org.apache.isis.applib.annotation.Action} in order to be picked up as an action in the metamodel.
 
 | isis.reflector.introspector. +
+lock-after-full-introspection
+|  true
+|  If true, then no new specifications will be allowed to be loaded once introspection has been complete.  +
+ Only applies if the introspector is configured to perform full introspection up-front (either because of {@link IntrospectionMode#FULL} or {@link IntrospectionMode#LAZY_UNLESS_PRODUCTION} when in production); otherwise is ignored. 
+
+| isis.reflector.introspector. +
 mode
 | 
-| 
+|  Whether all known types should be fully introspected as part of the bootstrapping, or should only be partially introspected initially.  +
+ Leaving this as lazy means that there's a chance that metamodel validation errors will not be discovered during bootstrap.  That said, metamodel validation is still run incrementally for any classes introspected lazily after initial bootstrapping (unless {@link #isValidateIncrementally()} is disabled. 
 
 | isis.reflector.introspector. +
 parallelize
 |  true
-| 
+|  Whether to perform introspection and metamodel validation in parallel.
+
+| isis.reflector.introspector. +
+validate-incrementally
+|  true
+|  If true, then metamodel validation is performed after any new specification has been loaded (after the initial bootstrapping).  +
+ This does _not</i> apply if the introspector is configured to perform full introspection up-front AND when the metamodel is {@link #isLockAfterFullIntrospection() locked} after initial bootstrapping (because in that case the lock check will simply prevent any new specs from being loaded). But it will apply otherwise.   +
+In particular, this setting _can</i> still apply even if the {@link #getMode() introspection mode} is set to {@link IntrospectionMode#FULL full}, because that in itself does not preclude some code from attempting to load some previously unknown type.  For example, a fixture script could attempt to invoke an action on some new type using the {@link org.apache.isis.applib.services.wrapper.WrapperFactory} - this will cause introspection of that new type to be performed. 
 
diff --git a/core/config/src/main/doc/modules/config/examples/generated/resteasy.adoc b/core/config/src/main/doc/modules/config/examples/generated/resteasy.adoc
index 8a8ac8b..644481d 100644
--- a/core/config/src/main/doc/modules/config/examples/generated/resteasy.adoc
+++ b/core/config/src/main/doc/modules/config/examples/generated/resteasy.adoc
@@ -6,15 +6,6 @@
 | 
 | 
 
-| resteasy.jaxrs.app. +
-registration
-| 
-| 
-
-| resteasy.jaxrs.default-path
-|  /restful
-|  Note that this is used rather than `resteasy.servlet.mapping.prefix</code> because there is _NO</i> implementation of {@link javax.ws.rs.core.Application}, so we rely on it being automatically created.
-
 | resteasy.resteasy-settings
 | 
 | 
diff --git a/core/metamodel/src/test/java/org/apache/isis/metamodel/specloader/SpecificationLoaderTestAbstract.java b/core/metamodel/src/test/java/org/apache/isis/metamodel/specloader/SpecificationLoaderTestAbstract.java
index 4e6dba9..3e71eef 100644
--- a/core/metamodel/src/test/java/org/apache/isis/metamodel/specloader/SpecificationLoaderTestAbstract.java
+++ b/core/metamodel/src/test/java/org/apache/isis/metamodel/specloader/SpecificationLoaderTestAbstract.java
@@ -65,6 +65,7 @@ abstract class SpecificationLoaderTestAbstract {
 
         IsisConfiguration newConfiguration() {
             val config = new IsisConfiguration(); // uses defaults!
+            config.getReflector().getIntrospector().setLockAfterFullIntrospection(false);
             config.setEnvironment(newConfigurableEnvironment());
             return config;
         }


[isis] 01/02: ISIS-2250: adds config properties to lock down metamodel, or to incrementally validate otherwise.

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 61e4f55704028ca1ae777e80af48d1cdf49b7142
Author: danhaywood <da...@haywood-associates.co.uk>
AuthorDate: Sun Jan 5 10:09:37 2020 +0000

    ISIS-2250: adds config properties to lock down metamodel, or to incrementally validate otherwise.
    
    Also:
    - adds validator to ensure that there are no actions except on known types
    - no longer inject into Interaction, instead pass in the required services (ClockService, MetricsService).
---
 .../isis/applib/services/command/Command.java      |  2 +-
 .../isis/applib/services/iactn/Interaction.java    | 56 +++++++++++++---------
 .../commons/internal/exceptions/_Exceptions.java   | 12 ++++-
 .../org/apache/isis/config/IsisConfiguration.java  | 48 ++++++++++++++++++-
 .../isis/config/beans/IsisBeanTypeRegistry.java    | 11 +++--
 .../isis/metamodel/facets/DomainEventHelper.java   |  2 +-
 ...ctionInvocationFacetForDomainEventAbstract.java |  9 ++--
 ...tySetterOrClearFacetForDomainEventAbstract.java | 13 +++--
 .../isis/metamodel/progmodel/ProgrammingModel.java | 10 ++++
 .../dflt/ProgrammingModelFacetsJava8.java          | 22 +++++++++
 .../isis/metamodel/spec/ObjectSpecification.java   |  1 +
 .../metamodel/specloader/SpecificationLoader.java  |  5 +-
 .../specloader/SpecificationLoaderDefault.java     | 56 +++++++++++++++++-----
 .../specimpl/ObjectSpecificationAbstract.java      | 13 ++++-
 .../specimpl/dflt/ObjectSpecificationDefault.java  |  2 +
 .../validator/MetaModelValidatorVisiting.java      | 46 +++++++++++++++---
 .../metamodel/JdoProgrammingModelPlugin.java       |  2 +
 .../persistence/IsisTransactionJdo.java            |  7 +--
 .../persistence/PersistenceSession5.java           |  2 +-
 19 files changed, 250 insertions(+), 69 deletions(-)

diff --git a/core/applib/src/main/java/org/apache/isis/applib/services/command/Command.java b/core/applib/src/main/java/org/apache/isis/applib/services/command/Command.java
index fe83936..1eb9ff2 100644
--- a/core/applib/src/main/java/org/apache/isis/applib/services/command/Command.java
+++ b/core/applib/src/main/java/org/apache/isis/applib/services/command/Command.java
@@ -390,7 +390,7 @@ public interface Command extends HasUniqueId {
          * </p>
          *
          * See also {@link Interaction#getCurrentExecution()} and
-         * {@link Interaction.Execution#setStartedAt(Timestamp)}.
+         * {@link #setStartedAt(org.apache.isis.applib.services.clock.ClockService, org.apache.isis.applib.services.metrics.MetricsService)}.
          */
         void setStartedAt(Timestamp startedAt);
 
diff --git a/core/applib/src/main/java/org/apache/isis/applib/services/iactn/Interaction.java b/core/applib/src/main/java/org/apache/isis/applib/services/iactn/Interaction.java
index 4cb19a9..d53e822 100644
--- a/core/applib/src/main/java/org/apache/isis/applib/services/iactn/Interaction.java
+++ b/core/applib/src/main/java/org/apache/isis/applib/services/iactn/Interaction.java
@@ -27,10 +27,7 @@ import java.util.UUID;
 import java.util.concurrent.Callable;
 import java.util.concurrent.atomic.LongAdder;
 
-import javax.inject.Inject;
-
 import org.apache.isis.applib.annotation.Programmatic;
-import org.apache.isis.applib.annotation.Value;
 import org.apache.isis.applib.events.domain.AbstractDomainEvent;
 import org.apache.isis.applib.events.domain.ActionDomainEvent;
 import org.apache.isis.applib.events.domain.PropertyDomainEvent;
@@ -40,7 +37,6 @@ import org.apache.isis.applib.services.command.Command;
 import org.apache.isis.applib.services.eventbus.EventBusService;
 import org.apache.isis.applib.services.metrics.MetricsService;
 import org.apache.isis.applib.services.wrapper.WrapperFactory;
-import org.apache.isis.applib.services.xactn.Transaction;
 import org.apache.isis.applib.util.schema.MemberExecutionDtoUtils;
 import org.apache.isis.commons.internal.collections._Lists;
 import org.apache.isis.commons.internal.collections._Maps;
@@ -54,6 +50,8 @@ import org.apache.isis.schema.ixn.v1.ObjectCountsDto;
 import org.apache.isis.schema.ixn.v1.PropertyEditDto;
 import org.apache.isis.schema.jaxbadapters.JavaSqlTimestampXmlGregorianCalendarAdapter;
 
+import lombok.val;
+
 /**
  * Represents an action invocation or property modification, resulting in some state change of the system.  It captures
  * not only the target object and arguments passed, but also builds up the call-graph, and captures metrics, eg
@@ -79,7 +77,6 @@ import org.apache.isis.schema.jaxbadapters.JavaSqlTimestampXmlGregorianCalendarA
  * </p>
  *
  */
-@Value
 public class Interaction implements HasUniqueId {
 
     // -- transactionId (property)
@@ -135,11 +132,13 @@ public class Interaction implements HasUniqueId {
     @Programmatic
     public Object execute(
             final MemberExecutor<ActionInvocation> memberExecutor,
-            final ActionInvocation actionInvocation) {
+            final ActionInvocation actionInvocation,
+            final ClockService clockService,
+            final MetricsService metricsService) {
 
         push(actionInvocation);
 
-        return executeInternal(memberExecutor, actionInvocation);
+        return executeInternal(memberExecutor, actionInvocation, clockService, metricsService);
     }
 
     /**
@@ -153,16 +152,20 @@ public class Interaction implements HasUniqueId {
     @Programmatic
     public Object execute(
             final MemberExecutor<PropertyEdit> memberExecutor,
-            final PropertyEdit propertyEdit) {
+            final PropertyEdit propertyEdit,
+            final ClockService clockService,
+            final MetricsService metricsService) {
 
         push(propertyEdit);
 
-        return executeInternal(memberExecutor, propertyEdit);
+        return executeInternal(memberExecutor, propertyEdit, clockService, metricsService);
     }
 
     private <T extends Execution<?,?>> Object executeInternal(
             final MemberExecutor<T> memberExecutor,
-            final T execution) {
+            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.
@@ -190,7 +193,7 @@ public class Interaction implements HasUniqueId {
             }
         } finally {
             final Timestamp completedAt = clockService.nowAsJavaSqlTimestamp();
-            pop(completedAt);
+            pop(completedAt, metricsService);
         }
     }
 
@@ -238,12 +241,14 @@ public class Interaction implements HasUniqueId {
      * </p>
      */
     @Programmatic
-    private Execution<?,?> pop(final Timestamp completedAt) {
+    private Execution<?,?> pop(
+            final Timestamp completedAt,
+            final MetricsService metricsService) {
         if(currentExecution == null) {
             throw new IllegalStateException("No current execution to pop");
         }
         final Execution<?,?> popped = currentExecution;
-        popped.setCompletedAt(completedAt);
+        popped.setCompletedAt(completedAt, metricsService);
 
         moveCurrentTo(currentExecution.getParent());
         return popped;
@@ -301,7 +306,7 @@ public class Interaction implements HasUniqueId {
          */
         PUBLISHED_EVENT,
         /**
-         * There may be multiple transactions within a given interaction, as per {@link Transaction#getSequence()}.
+         * There may be multiple transactions within a given interaction.
          */
         TRANSACTION,
         ;
@@ -483,8 +488,12 @@ public class Interaction implements HasUniqueId {
         }
 
         @Programmatic
-        public void setStartedAt(final Timestamp startedAt) {
-            syncMetrics(When.BEFORE, startedAt);
+        public Timestamp start(
+                final ClockService clockService,
+                final MetricsService metricsService) {
+            val startedAt = clockService.nowAsJavaSqlTimestamp();
+            syncMetrics(When.BEFORE, startedAt, metricsService);
+            return startedAt;
         }
 
 
@@ -499,8 +508,10 @@ public class Interaction implements HasUniqueId {
         /**
          * <b>NOT API</b>: intended to be called only by the framework.
          */
-        void setCompletedAt(final Timestamp completedAt) {
-            syncMetrics(When.AFTER, completedAt);
+        void setCompletedAt(
+                final Timestamp completedAt,
+                final MetricsService metricsService) {
+            syncMetrics(When.AFTER, completedAt, metricsService);
         }
 
 
@@ -651,8 +662,10 @@ public class Interaction implements HasUniqueId {
                     final int numberObjectsLoaded,
                     final int numberObjectsDirtied);
         }
-        private void syncMetrics(final When when, final Timestamp timestamp) {
-            final MetricsService metricsService = interaction.metricsService;
+        private void syncMetrics(
+                final When when,
+                final Timestamp timestamp,
+                final MetricsService metricsService) {
 
             final int numberObjectsLoaded = metricsService.numberObjectsLoaded();
             final int numberObjectsDirtied = metricsService.numberObjectsDirtied();
@@ -706,7 +719,4 @@ public class Interaction implements HasUniqueId {
         }
     }
 
-    @Inject MetricsService metricsService;
-    @Inject ClockService clockService;
-
 }
diff --git a/core/commons/src/main/java/org/apache/isis/commons/internal/exceptions/_Exceptions.java b/core/commons/src/main/java/org/apache/isis/commons/internal/exceptions/_Exceptions.java
index 8598579..974396d 100644
--- a/core/commons/src/main/java/org/apache/isis/commons/internal/exceptions/_Exceptions.java
+++ b/core/commons/src/main/java/org/apache/isis/commons/internal/exceptions/_Exceptions.java
@@ -74,11 +74,21 @@ public final class _Exceptions {
      * @param _case the unmatched case to be reported
      * @return
      */
-    public static final IllegalArgumentException illegalArgument(String format, @Nullable Object ... args) {
+    public static final IllegalArgumentException illegalArgument(
+            final String format,
+            final @Nullable Object ... args) {
         requires(format, "format");
         return new IllegalArgumentException(String.format(format, args));
     }
 
+    public static IllegalStateException illegalState(
+            final String format,
+            final @Nullable Object ... args) {
+        requires(format, "format");
+        return new IllegalStateException(String.format(format, args));
+    }
+
+
     public static final NoSuchElementException noSuchElement(String msg) {
         return new NoSuchElementException(msg);
     }
diff --git a/core/config/src/main/java/org/apache/isis/config/IsisConfiguration.java b/core/config/src/main/java/org/apache/isis/config/IsisConfiguration.java
index 9a66511..78db370 100644
--- a/core/config/src/main/java/org/apache/isis/config/IsisConfiguration.java
+++ b/core/config/src/main/java/org/apache/isis/config/IsisConfiguration.java
@@ -501,8 +501,52 @@ public class IsisConfiguration {
         private final Introspector introspector = new Introspector();
         @Data
         public static class Introspector {
+            /**
+             * Whether to perform introspection and metamodel validation in parallel.
+             */
             private boolean parallelize = true;
+            /**
+             * Whether all known types should be fully introspected as part of the bootstrapping, or should only be
+             * partially introspected initially.
+             *
+             * <p>
+             * Leaving this as lazy means that there's a chance that metamodel validation errors will not be
+             * discovered during bootstrap.  That said, metamodel validation is still run incrementally for any
+             * classes introspected lazily after initial bootstrapping (unless {@link #isValidateIncrementally()} is
+             * disabled.
+             * </p>
+             */
             private IntrospectionMode mode = IntrospectionMode.LAZY_UNLESS_PRODUCTION;
+            /**
+             * If true, then no new specifications will be allowed to be loaded once introspection has been complete.
+             *
+             * <p>
+             * Only applies if the introspector is configured to perform full introspection up-front (either because of
+             * {@link IntrospectionMode#FULL} or {@link IntrospectionMode#LAZY_UNLESS_PRODUCTION} when in production);
+             * otherwise is ignored.
+             * </p>
+             */
+            private boolean lockAfterFullIntrospection = true;
+            /**
+             * If true, then metamodel validation is performed after any new specification has been loaded (after the
+             * initial bootstrapping).
+             *
+             * <p>
+             * This does <i>not</i> apply if the introspector is configured to perform full introspection up-front
+             * AND when the metamodel is {@link #isLockAfterFullIntrospection() locked} after initial bootstrapping
+             * (because in that case the lock check will simply prevent any new specs from being loaded).
+             * But it will apply otherwise.
+             * </p>
+             *
+             * <p>In particular, this setting <i>can</i> still apply even if the {@link #getMode() introspection mode}
+             * is set to {@link IntrospectionMode#FULL full}, because that in itself does not preclude some code
+             * from attempting to load some previously unknown type.  For example, a fixture script could attempt to
+             * invoke an action on some new type using the
+             * {@link org.apache.isis.applib.services.wrapper.WrapperFactory} - this will cause introspection of that
+             * new type to be performed.
+             * </p>
+             */
+            private boolean validateIncrementally = true;
         }
 
         private final Validator validator = new Validator();
@@ -1122,7 +1166,7 @@ public class IsisConfiguration {
              * eg: {@code isis.value.format.datetime=iso}
              * <p>
              * A pre-determined list of values is available, specifically 'iso_encoding', 'iso' and 'medium' (see
-             * {@link org.apache.isis.metamodel.facets.value.datetimejdk8local.Jdk8LocalDateTimeValueSemanticsProvider.NAMED_TITLE_FORMATTERS}).  
+             * <code>org.apache.isis.metamodel.facets.value.datetimejdk8local.Jdk8LocalDateTimeValueSemanticsProvider#NAMED_TITLE_FORMATTERS</code>).
              * Alternatively, can also specify a mask, eg <tt>dd-MMM-yyyy</tt>.
              */
             DATETIME,
@@ -1132,7 +1176,7 @@ public class IsisConfiguration {
              * eg: {@code isis.value.format.date=iso}
              * <p>
              * A pre-determined list of values is available, specifically 'iso_encoding', 'iso' and 'medium' (see
-             * {@link org.apache.isis.metamodel.facets.value.datejdk8local.Jdk8LocalDateValueSemanticsProvider.NAMED_TITLE_FORMATTERS}).  
+             * <code>org.apache.isis.metamodel.facets.value.datejdk8local.Jdk8LocalDateValueSemanticsProvider.NAMED_TITLE_FORMATTERS</code>).
              * Alternatively,  can also specify a mask, eg <tt>dd-MMM-yyyy</tt>.
              */
             DATE, 
diff --git a/core/config/src/main/java/org/apache/isis/config/beans/IsisBeanTypeRegistry.java b/core/config/src/main/java/org/apache/isis/config/beans/IsisBeanTypeRegistry.java
index 1107a2b..073060b 100644
--- a/core/config/src/main/java/org/apache/isis/config/beans/IsisBeanTypeRegistry.java
+++ b/core/config/src/main/java/org/apache/isis/config/beans/IsisBeanTypeRegistry.java
@@ -162,15 +162,16 @@ public final class IsisBeanTypeRegistry implements IsisComponentScanInterceptor,
     /**
      * If given type is part of the meta-model and is available for injection, 
      * returns the <em>Managed Bean's</em> name (id) as
-     * recognized by the IoC container, {@code null} otherwise;
+     * recognized by the IoC container.
+     *
      * @param type
      * @return
      */
-    public String getManagedBeanNameForType(Class<?> type) {
+    public Optional<String> getManagedBeanNameForType(Class<?> type) {
         if(vetoedTypes.contains(type)) { // vetos are coming from the spec-loader during init
-            return null;
+            return Optional.empty();
         }
-        return managedBeanNamesByType.get(type);
+        return Optional.ofNullable(managedBeanNamesByType.get(type));
     }
     
     /**
@@ -179,7 +180,7 @@ public final class IsisBeanTypeRegistry implements IsisComponentScanInterceptor,
      * @param type
      */
     public boolean isManagedBean(Class<?> type) {
-        return getManagedBeanNameForType(type)!=null;
+        return getManagedBeanNameForType(type).isPresent();
     }
     
     // -- HELPER
diff --git a/core/metamodel/src/main/java/org/apache/isis/metamodel/facets/DomainEventHelper.java b/core/metamodel/src/main/java/org/apache/isis/metamodel/facets/DomainEventHelper.java
index 1b736bd..067ecb2 100644
--- a/core/metamodel/src/main/java/org/apache/isis/metamodel/facets/DomainEventHelper.java
+++ b/core/metamodel/src/main/java/org/apache/isis/metamodel/facets/DomainEventHelper.java
@@ -132,7 +132,7 @@ public class DomainEventHelper {
                             .map(ObjectActionParameter::getName)
                             .collect(_Lists.toUnmodifiable());
 
-                    val parameterTypes = parameters.stream()
+                    final List<Class<?>> parameterTypes = parameters.stream()
                             .map(ObjectActionParameter::getSpecification)
                             .map(ObjectSpecification::getCorrespondingClass)
                             .collect(_Lists.toUnmodifiable());
diff --git a/core/metamodel/src/main/java/org/apache/isis/metamodel/facets/actions/action/invocation/ActionInvocationFacetForDomainEventAbstract.java b/core/metamodel/src/main/java/org/apache/isis/metamodel/facets/actions/action/invocation/ActionInvocationFacetForDomainEventAbstract.java
index 6893538..d9e2900 100644
--- a/core/metamodel/src/main/java/org/apache/isis/metamodel/facets/actions/action/invocation/ActionInvocationFacetForDomainEventAbstract.java
+++ b/core/metamodel/src/main/java/org/apache/isis/metamodel/facets/actions/action/invocation/ActionInvocationFacetForDomainEventAbstract.java
@@ -44,6 +44,7 @@ import org.apache.isis.applib.services.iactn.Interaction.ActionInvocation;
 import org.apache.isis.applib.services.iactn.InteractionContext;
 import org.apache.isis.applib.services.metamodel.MetaModelService;
 import org.apache.isis.applib.services.metamodel.MetaModelService.Mode;
+import org.apache.isis.applib.services.metrics.MetricsService;
 import org.apache.isis.applib.services.queryresultscache.QueryResultsCache;
 import org.apache.isis.applib.services.registry.ServiceRegistry;
 import org.apache.isis.commons.collections.Can;
@@ -205,7 +206,7 @@ implements ImperativeFacet {
                             mixinElseRegularAdapter, mixedInAdapter, execution);
 
             // sets up startedAt and completedAt on the execution, also manages the execution call graph
-            interaction.execute(callable, execution);
+            interaction.execute(callable, execution, getClockService(), getMetricsService());
 
             // handle any exceptions
             final Interaction.Execution<ActionInvocationDto, ?> priorExecution =
@@ -407,6 +408,9 @@ implements ImperativeFacet {
     private ClockService getClockService() {
         return serviceRegistry.lookupServiceElseFail(ClockService.class);
     }
+    private MetricsService getMetricsService() {
+        return serviceRegistry.lookupServiceElseFail(MetricsService.class);
+    }
 
     private PublisherDispatchService getPublishingServiceInternal() {
         return serviceRegistry.lookupServiceElseFail(PublisherDispatchService.class);
@@ -444,8 +448,7 @@ implements ImperativeFacet {
                 // 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 = getClockService().nowAsJavaSqlTimestamp();
-                execution.setStartedAt(startedAt);
+                val startedAt = execution.start(getClockService(), getMetricsService());
                 if(command.getStartedAt() == null) {
                     command.internal().setStartedAt(startedAt);
                 }
diff --git a/core/metamodel/src/main/java/org/apache/isis/metamodel/facets/properties/property/modify/PropertySetterOrClearFacetForDomainEventAbstract.java b/core/metamodel/src/main/java/org/apache/isis/metamodel/facets/properties/property/modify/PropertySetterOrClearFacetForDomainEventAbstract.java
index f3ce1e8..b8c1a7f 100644
--- a/core/metamodel/src/main/java/org/apache/isis/metamodel/facets/properties/property/modify/PropertySetterOrClearFacetForDomainEventAbstract.java
+++ b/core/metamodel/src/main/java/org/apache/isis/metamodel/facets/properties/property/modify/PropertySetterOrClearFacetForDomainEventAbstract.java
@@ -19,7 +19,6 @@
 
 package org.apache.isis.metamodel.facets.properties.property.modify;
 
-import java.sql.Timestamp;
 import java.util.Map;
 import java.util.Objects;
 
@@ -31,6 +30,7 @@ import org.apache.isis.applib.services.command.CommandContext;
 import org.apache.isis.applib.services.command.spi.CommandService;
 import org.apache.isis.applib.services.iactn.Interaction;
 import org.apache.isis.applib.services.iactn.InteractionContext;
+import org.apache.isis.applib.services.metrics.MetricsService;
 import org.apache.isis.commons.exceptions.IsisException;
 import org.apache.isis.metamodel.consent.InteractionInitiatedBy;
 import org.apache.isis.metamodel.facetapi.Facet;
@@ -50,6 +50,8 @@ import org.apache.isis.schema.ixn.v1.PropertyEditDto;
 
 import static org.apache.isis.commons.internal.base._Casts.uncheckedCast;
 
+import lombok.val;
+
 public abstract class PropertySetterOrClearFacetForDomainEventAbstract
 extends SingleValueFacetAbstract<Class<? extends PropertyDomainEvent<?,?>>> {
 
@@ -222,8 +224,7 @@ extends SingleValueFacetAbstract<Class<? extends PropertyDomainEvent<?,?>>> {
                         // 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).
-                        final Timestamp startedAt = getClockService().nowAsJavaSqlTimestamp();
-                        execution.setStartedAt(startedAt);
+                        val startedAt = execution.start(getClockService(), getMetricsService());
                         if(command.getStartedAt() == null) {
                             command.internal().setStartedAt(startedAt);
                         }
@@ -274,7 +275,7 @@ extends SingleValueFacetAbstract<Class<? extends PropertyDomainEvent<?,?>>> {
             };
 
             // sets up startedAt and completedAt on the execution, also manages the execution call graph
-            interaction.execute(executor, execution);
+            interaction.execute(executor, execution, getClockService(), getMetricsService());
 
             // handle any exceptions
             final Interaction.Execution<?, ?> priorExecution = interaction.getPriorExecution();
@@ -322,6 +323,10 @@ extends SingleValueFacetAbstract<Class<? extends PropertyDomainEvent<?,?>>> {
         return getServiceRegistry().lookupServiceElseFail(ClockService.class);
     }
 
+    private MetricsService getMetricsService() {
+        return getServiceRegistry().lookupServiceElseFail(MetricsService.class);
+    }
+
     private PublisherDispatchService getPublishingServiceInternal() {
         return getServiceRegistry().lookupServiceElseFail(PublisherDispatchService.class);
     }
diff --git a/core/metamodel/src/main/java/org/apache/isis/metamodel/progmodel/ProgrammingModel.java b/core/metamodel/src/main/java/org/apache/isis/metamodel/progmodel/ProgrammingModel.java
index 9e237b3..8ba832d 100644
--- a/core/metamodel/src/main/java/org/apache/isis/metamodel/progmodel/ProgrammingModel.java
+++ b/core/metamodel/src/main/java/org/apache/isis/metamodel/progmodel/ProgrammingModel.java
@@ -19,14 +19,18 @@
 
 package org.apache.isis.metamodel.progmodel;
 
+import java.util.function.Predicate;
 import java.util.function.Supplier;
 import java.util.stream.Stream;
 
 import org.apache.isis.commons.internal.functions._Functions;
 import org.apache.isis.metamodel.facets.FacetFactory;
+import org.apache.isis.metamodel.spec.ObjectSpecification;
 import org.apache.isis.metamodel.specloader.validator.MetaModelValidator;
 import org.apache.isis.metamodel.specloader.validator.MetaModelValidatorVisiting;
 
+import lombok.NonNull;
+
 public interface ProgrammingModel {
 
     // -- ENUM TYPES
@@ -155,6 +159,12 @@ public interface ProgrammingModel {
         addValidator(MetaModelValidatorVisiting.of(visitor), markers);
     }
     
+    default void addValidator(final MetaModelValidatorVisiting.Visitor visitor,
+                              final @NonNull Predicate<ObjectSpecification> specPredicate,
+                              final Marker ... markers) {
+        addValidator(MetaModelValidatorVisiting.of(visitor, specPredicate), markers);
+    }
+
     /** shortcut for see {@link #addPostProcessor(PostProcessingOrder, Class, Supplier, Marker...)}*/
     default <T extends ObjectSpecificationPostProcessor> void addPostProcessor(
             PostProcessingOrder order, 
diff --git a/core/metamodel/src/main/java/org/apache/isis/metamodel/progmodels/dflt/ProgrammingModelFacetsJava8.java b/core/metamodel/src/main/java/org/apache/isis/metamodel/progmodels/dflt/ProgrammingModelFacetsJava8.java
index bd5eb4b..77fb34d 100644
--- a/core/metamodel/src/main/java/org/apache/isis/metamodel/progmodels/dflt/ProgrammingModelFacetsJava8.java
+++ b/core/metamodel/src/main/java/org/apache/isis/metamodel/progmodels/dflt/ProgrammingModelFacetsJava8.java
@@ -17,8 +17,13 @@
 
 package org.apache.isis.metamodel.progmodels.dflt;
 
+import java.util.function.Predicate;
+
+import org.apache.isis.applib.Identifier;
 import org.apache.isis.applib.services.inject.ServiceInjector;
+import org.apache.isis.commons.internal.ioc.BeanSort;
 import org.apache.isis.metamodel.authorization.standard.AuthorizationFacetFactory;
+import org.apache.isis.metamodel.facetapi.FacetHolder;
 import org.apache.isis.metamodel.facets.OrphanedSupportingMethodValidator;
 import org.apache.isis.metamodel.facets.actions.action.ActionAnnotationFacetFactory;
 import org.apache.isis.metamodel.facets.actions.action.ActionChoicesForCollectionParameterFacetFactory;
@@ -145,6 +150,12 @@ import org.apache.isis.metamodel.facets.value.uuid.UUIDValueFacetUsingSemanticsP
 import org.apache.isis.metamodel.postprocessors.param.DeriveFacetsPostProcessor;
 import org.apache.isis.metamodel.progmodel.ProgrammingModelAbstract;
 import org.apache.isis.metamodel.services.title.TitlesAndTranslationsValidator;
+import org.apache.isis.metamodel.spec.ObjectSpecification;
+import org.apache.isis.metamodel.spec.feature.Contributed;
+import org.apache.isis.metamodel.specloader.validator.MetaModelValidator;
+import org.apache.isis.metamodel.specloader.validator.MetaModelValidatorVisiting;
+
+import lombok.NonNull;
 
 public final class ProgrammingModelFacetsJava8 extends ProgrammingModelAbstract {
 
@@ -356,6 +367,17 @@ public final class ProgrammingModelFacetsJava8 extends ProgrammingModelAbstract
         addPostProcessor(PostProcessingOrder.A1_BUILTIN, DeriveFacetsPostProcessor.class);
         addValidator(new TitlesAndTranslationsValidator());
 
+        addValidator((objectSpec, validator) -> {
+            final long numActions = objectSpec.streamObjectActions(Contributed.INCLUDED).count();
+            if (numActions > 0) {
+                validator.onFailure(objectSpec, objectSpec.getIdentifier(),
+                        "%s: is a (concrete) but UNKNOWN sort, yet has %d actions",
+                        objectSpec.getCorrespondingClass().getName(),
+                        numActions);
+            }
+            return false;
+        }, objectSpec -> objectSpec.getBeanSort() == BeanSort.UNKNOWN && ! objectSpec.isAbstract());
+
     }
 
 
diff --git a/core/metamodel/src/main/java/org/apache/isis/metamodel/spec/ObjectSpecification.java b/core/metamodel/src/main/java/org/apache/isis/metamodel/spec/ObjectSpecification.java
index 3bb1afc..8c3c24b 100644
--- a/core/metamodel/src/main/java/org/apache/isis/metamodel/spec/ObjectSpecification.java
+++ b/core/metamodel/src/main/java/org/apache/isis/metamodel/spec/ObjectSpecification.java
@@ -434,6 +434,7 @@ ObjectAssociationContainer, Hierarchical,  DefaultProvider {
     /**
      * Introspecting up to the level required.
      * @since 2.0
+     * @return whether it's necessary to re-run validations.
      */
     void introspectUpTo(IntrospectionState upTo);
 
diff --git a/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/SpecificationLoader.java b/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/SpecificationLoader.java
index a409bfa..a5b132b 100644
--- a/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/SpecificationLoader.java
+++ b/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/SpecificationLoader.java
@@ -133,8 +133,5 @@ public interface SpecificationLoader {
     }
 
 
-
-
-
-
+    void revalidateIfNecessary();
 }
diff --git a/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/SpecificationLoaderDefault.java b/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/SpecificationLoaderDefault.java
index da510c2..6a743d9 100644
--- a/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/SpecificationLoaderDefault.java
+++ b/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/SpecificationLoaderDefault.java
@@ -43,6 +43,7 @@ import org.apache.isis.commons.internal.base._Lazy;
 import org.apache.isis.commons.internal.base._Timing;
 import org.apache.isis.commons.internal.collections._Lists;
 import org.apache.isis.commons.internal.environment.IsisSystemEnvironment;
+import org.apache.isis.commons.internal.exceptions._Exceptions;
 import org.apache.isis.config.IsisConfiguration;
 import org.apache.isis.config.beans.IsisBeanTypeRegistry;
 import org.apache.isis.config.beans.IsisBeanTypeRegistryHolder;
@@ -110,6 +111,12 @@ public class SpecificationLoaderDefault implements SpecificationLoader {
     private final SpecificationCacheDefault<ObjectSpecification> cache = 
             new SpecificationCacheDefault<>();
 
+    /**
+     * We only ever mark the metamodel as fully introspected if in {@link #isFullIntrospect() full} introspection mode.
+     */
+    @Getter
+    private boolean metamodelFullyIntrospected = false;
+
     /** JUnit Test Support */
     public static SpecificationLoaderDefault getInstance (
             IsisConfiguration isisConfiguration,
@@ -235,6 +242,9 @@ public class SpecificationLoaderDefault implements SpecificationLoader {
         stopWatch.stop();
         log.info("Metamodel created in " + (long)stopWatch.getMillis() + " ms.");
 
+        if(isFullIntrospect()) {
+            metamodelFullyIntrospected = true;
+        }
     }
     
     @Override
@@ -322,9 +332,25 @@ public class SpecificationLoaderDefault implements SpecificationLoader {
         synchronized (cache) {
             cachedSpec = cache.computeIfAbsent(typeName, __->createSpecification(substitutedType));
         }
-        
+
         cachedSpec.introspectUpTo(upTo);
-        return cachedSpec; 
+
+        return cachedSpec;
+    }
+
+    public void revalidateIfNecessary() {
+        if(!this.isisConfiguration.getReflector().getIntrospector().isValidateIncrementally()) {
+            return;
+        }
+
+        if (this.validationResult.isMemoized()) {
+            this.validationResult.clear();
+        }
+        final ValidationFailures validationFailures = this.getValidationResult();
+
+        if(validationFailures.hasFailures()) {
+            throw _Exceptions.illegalState(String.join("\n", validationFailures.getMessages("[%d] %s")));
+        }
     }
 
     // -- LOOKUP
@@ -368,8 +394,17 @@ public class SpecificationLoaderDefault implements SpecificationLoader {
      * Creates the appropriate type of {@link ObjectSpecification}.
      */
     private ObjectSpecification createSpecification(final Class<?> cls) {
-        
-         // ... and create the specs
+
+        if(isMetamodelFullyIntrospected() && isisConfiguration.getReflector().getIntrospector().isLockAfterFullIntrospection()) {
+            throw _Exceptions.illegalState(
+                    "Cannot introspect class '%s' because the metamodel has been fully introspected and is now locked. " +
+                    "One reason this can happen is if you are attempting to invoke an action through the WrapperFactory " +
+                    "on a service class incorrectly annotated with Spring's @Service annotation instead of " +
+                    "@DomainService.",
+                    cls.getName());
+        }
+
+        // ... and create the specs
         final ObjectSpecificationAbstract objectSpec;
         if (FreeStandingList.class.isAssignableFrom(cls)) {
 
@@ -381,14 +416,13 @@ public class SpecificationLoaderDefault implements SpecificationLoader {
             val typeRegistry = isisBeanTypeRegistryHolder.getIsisBeanTypeRegistry();
 
             val managedBeanNameIfAny = typeRegistry.getManagedBeanNameForType(cls);
-
             objectSpec = new ObjectSpecificationDefault(
-                    cls,
-                    metaModelContext,
-                    facetProcessor, 
-                    managedBeanNameIfAny, 
-                    postProcessor, 
-                    classSubstitutor);
+                            cls,
+                            metaModelContext,
+                            facetProcessor,
+                            managedBeanNameIfAny.orElse(null),
+                            postProcessor,
+                            classSubstitutor);
         }
 
         return objectSpec;
diff --git a/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/specimpl/ObjectSpecificationAbstract.java b/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/specimpl/ObjectSpecificationAbstract.java
index c9f8129..bad3057 100644
--- a/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/specimpl/ObjectSpecificationAbstract.java
+++ b/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/specimpl/ObjectSpecificationAbstract.java
@@ -275,7 +275,9 @@ public abstract class ObjectSpecificationAbstract extends FacetHolderImpl implem
         if(log.isDebugEnabled()) {
             log.debug("introspectingUpTo: {}, {}", getFullIdentifier(), upTo);
         }
-        
+
+        boolean revalidate = false;
+
         switch (introspectionState) {
         case NOT_INTROSPECTED:
             if(isLessThan(upTo)) {
@@ -289,6 +291,7 @@ public abstract class ObjectSpecificationAbstract extends FacetHolderImpl implem
                 this.introspectionState = IntrospectionState.MEMBERS_BEING_INTROSPECTED;
                 introspectMembers();
                 this.introspectionState = IntrospectionState.TYPE_AND_MEMBERS_INTROSPECTED;
+                revalidate = true;
             }
             // set to avoid infinite loops
             break;
@@ -301,6 +304,7 @@ public abstract class ObjectSpecificationAbstract extends FacetHolderImpl implem
                 this.introspectionState = IntrospectionState.MEMBERS_BEING_INTROSPECTED;
                 introspectMembers();
                 this.introspectionState = IntrospectionState.TYPE_AND_MEMBERS_INTROSPECTED;
+                revalidate = true;
             }
             break;
         case MEMBERS_BEING_INTROSPECTED:
@@ -308,6 +312,13 @@ public abstract class ObjectSpecificationAbstract extends FacetHolderImpl implem
         case TYPE_AND_MEMBERS_INTROSPECTED:
             // nothing to do
             break;
+
+        default:
+            throw _Exceptions.unexpectedCodeReach();
+        }
+
+        if(revalidate) {
+            getSpecificationLoader().revalidateIfNecessary();
         }
     }
 
diff --git a/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/specimpl/dflt/ObjectSpecificationDefault.java b/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/specimpl/dflt/ObjectSpecificationDefault.java
index 66b4120..14b38aa 100644
--- a/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/specimpl/dflt/ObjectSpecificationDefault.java
+++ b/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/specimpl/dflt/ObjectSpecificationDefault.java
@@ -33,6 +33,8 @@ import org.apache.isis.commons.collections.Can;
 import org.apache.isis.commons.internal.base._Lazy;
 import org.apache.isis.commons.internal.collections._Lists;
 import org.apache.isis.commons.internal.collections._Maps;
+import org.apache.isis.commons.internal.exceptions._Exceptions;
+import org.apache.isis.commons.internal.ioc.BeanSort;
 import org.apache.isis.metamodel.commons.StringExtensions;
 import org.apache.isis.metamodel.commons.ToString;
 import org.apache.isis.metamodel.context.MetaModelContext;
diff --git a/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/validator/MetaModelValidatorVisiting.java b/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/validator/MetaModelValidatorVisiting.java
index e65f7f6..2494719 100644
--- a/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/validator/MetaModelValidatorVisiting.java
+++ b/core/metamodel/src/main/java/org/apache/isis/metamodel/specloader/validator/MetaModelValidatorVisiting.java
@@ -19,14 +19,14 @@
 
 package org.apache.isis.metamodel.specloader.validator;
 
+import java.util.function.Predicate;
+
 import org.apache.isis.metamodel.spec.ObjectSpecification;
 import org.apache.isis.metamodel.specloader.SpecificationLoaderDefault;
 
 import lombok.NonNull;
-import lombok.RequiredArgsConstructor;
 import lombok.val;
 
-@RequiredArgsConstructor(staticName = "of")
 public class MetaModelValidatorVisiting extends MetaModelValidatorAbstract {
 
     // -- INTERFACES
@@ -44,9 +44,37 @@ public class MetaModelValidatorVisiting extends MetaModelValidatorAbstract {
     }
     
     // -- IMPLEMENTATION
-    
+
+    public static MetaModelValidatorVisiting of(
+            final @NonNull Visitor visitor) {
+        return new MetaModelValidatorVisiting(visitor);
+    }
+
+    public static MetaModelValidatorVisiting of(
+            final @NonNull Visitor visitor,
+            final @NonNull Predicate<ObjectSpecification> includeIf) {
+        return new MetaModelValidatorVisiting(visitor, includeIf);
+    }
+
+
     @NonNull private final Visitor visitor;
-    
+    @NonNull private final Predicate<ObjectSpecification> includeIf;
+
+    private MetaModelValidatorVisiting(
+            final @NonNull Visitor visitor,
+            final @NonNull Predicate<ObjectSpecification> includeIf) {
+        this.visitor = visitor;
+        this.includeIf = includeIf;
+    }
+
+    private MetaModelValidatorVisiting(
+            final @NonNull Visitor visitor) {
+        this(visitor,
+                // by default, exclude managed beans from validation
+                spec -> !spec.isManagedBean() && !spec.getBeanSort().isUnknown()
+        );
+    }
+
     @Override
     public void collectFailuresInto(@NonNull ValidationFailures validationFailures) {
         validateAll();
@@ -59,9 +87,9 @@ public class MetaModelValidatorVisiting extends MetaModelValidatorAbstract {
         val specLoader = (SpecificationLoaderDefault)super.getMetaModelContext().getSpecificationLoader();
         
         specLoader.forEach(spec->{
-            
-            if(spec.isManagedBean() || spec.getBeanSort().isUnknown()) {
-                return; // exclude managed beans from validation
+
+            if(! includeIf.test(spec)) {
+                return;
             }
             
             visitor.visit(spec, this);            
@@ -69,6 +97,10 @@ public class MetaModelValidatorVisiting extends MetaModelValidatorAbstract {
         
     }
 
+    private static boolean filter(ObjectSpecification spec) {
+        return spec.isManagedBean() || spec.getBeanSort().isUnknown();
+    }
+
     private void summarize() {
         if(visitor instanceof SummarizingVisitor) {
             SummarizingVisitor summarizingVisitor = (SummarizingVisitor) visitor;
diff --git a/core/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/metamodel/JdoProgrammingModelPlugin.java b/core/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/metamodel/JdoProgrammingModelPlugin.java
index 32dc1d9..0549ee8 100644
--- a/core/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/metamodel/JdoProgrammingModelPlugin.java
+++ b/core/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/metamodel/JdoProgrammingModelPlugin.java
@@ -205,6 +205,8 @@ public class JdoProgrammingModelPlugin implements MetaModelRefiner {
                         
                     }
                 }
+                // so can be revalidated again if necessary.
+                collidingSpecsById.clear();
             }
 
             private String asCsv(final List<ObjectSpecification> specList) {
diff --git a/core/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/persistence/IsisTransactionJdo.java b/core/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/persistence/IsisTransactionJdo.java
index 550cb40..f5de05a 100644
--- a/core/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/persistence/IsisTransactionJdo.java
+++ b/core/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/persistence/IsisTransactionJdo.java
@@ -44,6 +44,7 @@ import org.apache.isis.runtime.persistence.transaction.IsisTransactionFlushExcep
 import org.apache.isis.runtime.persistence.transaction.IsisTransactionManagerException;
 
 import lombok.Getter;
+import lombok.Setter;
 import lombok.val;
 import lombok.extern.log4j.Log4j2;
 
@@ -183,12 +184,8 @@ public class IsisTransactionJdo implements Transaction {
 
     // -- state
 
+    @Getter
     private State state;
-
-    public State getState() {
-        return state;
-    }
-
     private void setState(final State state) {
         this.state = state;
         if(state.isComplete()) {
diff --git a/core/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/persistence/PersistenceSession5.java b/core/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/persistence/PersistenceSession5.java
index 20dfc98..45d0d56 100644
--- a/core/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/persistence/PersistenceSession5.java
+++ b/core/persistence/jdo/datanucleus-5/src/main/java/org/apache/isis/persistence/jdo/datanucleus5/persistence/PersistenceSession5.java
@@ -172,7 +172,7 @@ implements IsisLifecycleListener.PersistenceSessionLifecycleManagement {
         }
 
         final Command command = createCommand();
-        final Interaction interaction = factoryService.instantiate(Interaction.class);
+        final Interaction interaction = new Interaction();
 
         final Timestamp timestamp = clockService.nowAsJavaSqlTimestamp();
         final String userName = userService.getUser().getName();