You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@causeway.apache.org by ah...@apache.org on 2026/03/27 05:39:28 UTC

(causeway) branch v3 updated: CAUSEWAY-3982: extend auditing to only log on updates, not create or delete (cherrypicked from v4)

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

ahuber pushed a commit to branch v3
in repository https://gitbox.apache.org/repos/asf/causeway.git


The following commit(s) were added to refs/heads/v3 by this push:
     new b832e0a6b31 CAUSEWAY-3982: extend auditing to only log on updates, not create or delete (cherrypicked from v4)
b832e0a6b31 is described below

commit b832e0a6b3188055a2598705f7fcc44f863793dd
Author: andi-huber <ah...@apache.org>
AuthorDate: Fri Mar 27 06:36:07 2026 +0100

    CAUSEWAY-3982: extend auditing to only log on updates, not create or
    delete (cherrypicked from v4)
---
 .../causeway/applib/annotation/Publishing.java     |  21 +-
 .../CommandPublishingFacetForActionAnnotation.java |  55 +-
 ...ommandPublishingFacetForPropertyAnnotation.java |  80 +-
 ...xecutionPublishingFacetForActionAnnotation.java |  46 +-
 ...cutionPublishingFacetForPropertyAnnotation.java |  82 +-
 ...gePublishingFacetForDomainObjectAnnotation.java |  35 +-
 ...FacetForDomainObjectAnnotationAsConfigured.java |   2 +-
 ...tityChangePublishingFacetFromConfiguration.java |   2 +-
 .../entitychange/EntityChangePublishingFacet.java  |  45 +-
 .../EntityChangePublishingFacetAbstract.java       |  12 +-
 .../EntityPropertyChangePublishingPolicyFacet.java |  11 +-
 ...PublishingPolicyFacetForPropertyAnnotation.java |   6 +-
 .../metamodel/facets/FacetFactoryTestAbstract.java | 484 +++++++++++
 .../DomainObjectAnnotationFacetFactoryTest.java    | 768 ++++++++++++++++++
 .../PropertyAnnotationFacetFactoryTest.java        | 885 +++++++++++++++++++++
 .../changetracking/EntityChangeTrackerDefault.java |  44 +-
 16 files changed, 2356 insertions(+), 222 deletions(-)

diff --git a/api/applib/src/main/java/org/apache/causeway/applib/annotation/Publishing.java b/api/applib/src/main/java/org/apache/causeway/applib/annotation/Publishing.java
index b0fcc085537..c6125513cdb 100644
--- a/api/applib/src/main/java/org/apache/causeway/applib/annotation/Publishing.java
+++ b/api/applib/src/main/java/org/apache/causeway/applib/annotation/Publishing.java
@@ -45,8 +45,8 @@ public enum Publishing {
      * Publishing of data triggered by interaction with this object
      * should be handled as per the default publishing policy
      * configured in <tt>application.properties</tt>.
-     * <p>
-     * If no publishing policy is configured, then publishing is disabled.
+     *
+     * <p>If no publishing policy is configured, then publishing is disabled.
      */
     AS_CONFIGURED,
 
@@ -55,6 +55,23 @@ public enum Publishing {
      */
     ENABLED,
 
+    /**
+     * Applies only to {@link EntityPropertyChangeSubscriber}, whereby events are published for modifications to the
+     * object, but no events are published for the initial creation of an object.
+     *
+     * <p>In the case of audit trail extension,
+     * this effectively suppresses all of the "[NEW] -> value" entries that are created for every property of the
+     * entity when it is being created, and also all of the "value -> [DELETED]" entries that are created for every property of the
+     * entity when it is being deleted.
+     *
+     * <p>This variant is intended only where the application code has enough traceability built into the domain
+     * (perhaps to provide visibility to the end-users) that the technical auditing is overkill.  It will also
+     * of course reduce the volume of auditing, so improves performance (likely both response times and throughput).
+     *
+     * <p>For other subscribers, behaviour is the same as {@link #ENABLED}.
+     */
+    ENABLED_FOR_UPDATES_ONLY,
+
     /**
      * Do <b>not</b> publish data triggered by interaction with this object
      * (even if otherwise configured to enable publishing).
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/command/CommandPublishingFacetForActionAnnotation.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/command/CommandPublishingFacetForActionAnnotation.java
index c4ec4abb732..b7f35a5f8ae 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/command/CommandPublishingFacetForActionAnnotation.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/command/CommandPublishingFacetForActionAnnotation.java
@@ -62,40 +62,33 @@ public static Optional<CommandPublishingFacet> create(
         var publishingPolicy = ActionConfigOptions.actionCommandPublishingPolicy(configuration);
 
         return actionsIfAny
-                .filter(action -> action.commandPublishing() != Publishing.NOT_SPECIFIED)
-                .map(action -> {
-                    Publishing publishing = action.commandPublishing();
+            .filter(action -> action.commandPublishing() != Publishing.NOT_SPECIFIED)
+            .map(action -> {
+                Publishing publishing = action.commandPublishing();
 
-                    final Class<? extends CommandDtoProcessor> processorClass = action.commandDtoProcessor();
-                    final CommandDtoProcessor processor = newProcessorElseNull(processorClass);
+                final Class<? extends CommandDtoProcessor> processorClass = action.commandDtoProcessor();
+                final CommandDtoProcessor processor = newProcessorElseNull(processorClass);
 
-                    if(processor != null) {
-                        publishing = Publishing.ENABLED;
-                    }
+                if(processor != null) {
+                    publishing = Publishing.ENABLED;
+                }
 
-                    switch (publishing) {
-                        case AS_CONFIGURED:
-                            switch (publishingPolicy) {
-                                case NONE:
-                                    return new CommandPublishingFacetForActionAnnotationAsConfigured.None(holder, servicesInjector);
-                                case IGNORE_QUERY_ONLY:
-                                case IGNORE_SAFE:
-                                    return Facets.hasSafeSemantics(holder)
-                                            ? new CommandPublishingFacetForActionAnnotationAsConfigured.IgnoreSafe(holder, servicesInjector)
-                                            : new CommandPublishingFacetForActionAnnotationAsConfigured.IgnoreSafeYetNot(holder, servicesInjector);
-                                case ALL:
-                                    return new CommandPublishingFacetForActionAnnotationAsConfigured.All(holder, servicesInjector);
-                                default:
-                                    throw new IllegalStateException(String.format("configured action.commandPublishing policy '%s' not recognised", publishingPolicy));
-                            }
-                        case DISABLED:
-                            return new CommandPublishingFacetForActionAnnotation.Disabled(processor, holder, servicesInjector);
-                        case ENABLED:
-                            return new CommandPublishingFacetForActionAnnotation.Enabled(processor, holder, servicesInjector);
-                        default:
-                            throw new IllegalStateException(String.format("@Action#commandPublishing '%s' not recognised", publishing));
-                    }
-                });
+                return switch (publishing) {
+                    case AS_CONFIGURED -> switch (publishingPolicy) {
+                        case NONE -> new CommandPublishingFacetForActionAnnotationAsConfigured.None(holder, servicesInjector);
+                        case IGNORE_QUERY_ONLY, IGNORE_SAFE -> Facets.hasSafeSemantics(holder)
+                            ? new CommandPublishingFacetForActionAnnotationAsConfigured.IgnoreSafe(holder, servicesInjector)
+                            : new CommandPublishingFacetForActionAnnotationAsConfigured.IgnoreSafeYetNot(holder, servicesInjector);
+                        case ALL -> new CommandPublishingFacetForActionAnnotationAsConfigured.All(holder, servicesInjector);
+                        default -> throw new IllegalStateException(
+                                String.format("configured action.commandPublishing policy '%s' not recognised", publishingPolicy));
+                    };
+                    case DISABLED -> new CommandPublishingFacetForActionAnnotation.Disabled(processor, holder, servicesInjector);
+                    case ENABLED, ENABLED_FOR_UPDATES_ONLY -> new CommandPublishingFacetForActionAnnotation.Enabled(processor, holder, servicesInjector);
+                    default -> throw new IllegalStateException(
+                            String.format("@Action#commandPublishing '%s' not recognised", publishing));
+                };
+            });
     }
 
     CommandPublishingFacetForActionAnnotation(
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/command/CommandPublishingFacetForPropertyAnnotation.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/command/CommandPublishingFacetForPropertyAnnotation.java
index 873470d9746..642a9edd462 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/command/CommandPublishingFacetForPropertyAnnotation.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/command/CommandPublishingFacetForPropertyAnnotation.java
@@ -68,7 +68,7 @@ public static CommandPublishingFacet create(
 
         return propertyIfAny
             .filter(property -> property.commandPublishing() != Publishing.NOT_SPECIFIED)
-            .map(property -> {
+            .<CommandPublishingFacet>map(property -> {
                 Publishing publishing = property.commandPublishing();
 
                 var processorClass = property.commandDtoProcessor();
@@ -78,64 +78,54 @@ public static CommandPublishingFacet create(
                     publishing = Publishing.ENABLED;
                 }
 
-                switch (publishing) {
-                    case AS_CONFIGURED:
-                        switch (publishingPolicy) {
-                            case NONE:
-                                return (CommandPublishingFacet)new CommandPublishingFacetForPropertyAnnotationAsConfigured.None(holder, servicesInjector);
-                            case ALL:
-                                return new CommandPublishingFacetForPropertyAnnotationAsConfigured.All(holder, servicesInjector);
-                            default:
-                                throw new IllegalStateException(String.format("configured property.commandpublishing policy '%s' not recognised", publishingPolicy));
-                        }
-                    case DISABLED:
-                        return new CommandPublishingFacetForPropertyAnnotation.Disabled(processor, holder, servicesInjector);
-                    case ENABLED:
-                        return new CommandPublishingFacetForPropertyAnnotation.Enabled(processor, holder, servicesInjector);
-                    default:
-                        throw new IllegalStateException(String.format("@Property#commandPublishing '%s' not recognised", publishing));
-                }
+                return switch (publishing) {
+                    case AS_CONFIGURED -> switch (publishingPolicy) {
+                        case NONE -> new CommandPublishingFacetForPropertyAnnotationAsConfigured.None(holder, servicesInjector);
+                        case ALL -> new CommandPublishingFacetForPropertyAnnotationAsConfigured.All(holder, servicesInjector);
+                        default -> throw new IllegalStateException(
+                                String.format("configured property.commandpublishing policy '%s' not recognised", publishingPolicy));
+                    };
+                    case DISABLED -> new CommandPublishingFacetForPropertyAnnotation.Disabled(processor, holder, servicesInjector);
+                    case ENABLED, ENABLED_FOR_UPDATES_ONLY -> new CommandPublishingFacetForPropertyAnnotation.Enabled(processor, holder, servicesInjector);
+                    default -> throw new IllegalStateException(
+                            String.format("@Property#commandPublishing '%s' not recognised", publishing));
+                };
             })
             .orElseGet(() -> {
                 // there is no publishing facet from either @Action or @Property, so use the appropriate configuration to install a default
-                if (representsProperty(holder)) {
+                if (representsProperty(holder))
                     // we are dealing with a property
-                    switch (publishingPolicy) {
-                        case NONE:
-                            return new CommandPublishingFacetForPropertyFromConfiguration.None(holder, servicesInjector);
-                        case ALL:
-                            return new CommandPublishingFacetForPropertyFromConfiguration.All(holder, servicesInjector);
-                        default:
-                            throw new IllegalStateException(String.format("configured property.commandPublishing policy '%s' not recognised", publishingPolicy));
-                    }
-                } else {
+                    return switch (publishingPolicy) {
+                        case NONE -> new CommandPublishingFacetForPropertyFromConfiguration.None(holder, servicesInjector);
+                        case ALL -> new CommandPublishingFacetForPropertyFromConfiguration.All(holder, servicesInjector);
+                        default -> throw new IllegalStateException(String.format("configured property.commandPublishing policy '%s' not recognised", publishingPolicy));
+                    };
+                else {
                     // we are dealing with an action
                     var actionPublishingPolicy = ActionConfigOptions.actionCommandPublishingPolicy(configuration);
-                    switch (actionPublishingPolicy) {
-                        case NONE:
-                            return new CommandPublishingFacetForActionFromConfiguration.None(holder, servicesInjector);
-                        case IGNORE_QUERY_ONLY:
-                        case IGNORE_SAFE:
-                            return Facets.hasSafeSemantics(holder)
-                                    ? new CommandPublishingFacetForActionFromConfiguration.IgnoreSafe(holder, servicesInjector)
-                                    : new CommandPublishingFacetForActionFromConfiguration.IgnoreSafeYetNot(holder, servicesInjector);
-                        case ALL:
-                            return new CommandPublishingFacetForActionFromConfiguration.All(holder, servicesInjector);
-                        default:
-                            throw new IllegalStateException(String.format("configured action.commandPublishing policy '%s' not recognised", actionPublishingPolicy));
-                    }
+                    return switch (actionPublishingPolicy) {
+                        case NONE -> new CommandPublishingFacetForActionFromConfiguration.None(holder, servicesInjector);
+                        case IGNORE_QUERY_ONLY, IGNORE_SAFE -> Facets.hasSafeSemantics(holder)
+                                                            ? new CommandPublishingFacetForActionFromConfiguration.IgnoreSafe(holder, servicesInjector)
+                                                            : new CommandPublishingFacetForActionFromConfiguration.IgnoreSafeYetNot(holder, servicesInjector);
+                        case ALL -> new CommandPublishingFacetForActionFromConfiguration.All(holder, servicesInjector);
+                        default -> throw new IllegalStateException(String.format("configured action.commandPublishing policy '%s' not recognised", actionPublishingPolicy));
+                    };
                 }
             });
+
     }
 
     private static boolean representsProperty(final FacetHolder holder) {
         // a property
-        if (holder instanceof TypedFacetHolder && ((TypedFacetHolder)holder).featureType() == FeatureType.PROPERTY) {
+        if (holder instanceof TypedFacetHolder typedFacetHolder
+                && typedFacetHolder.featureType() == FeatureType.PROPERTY)
             return true;
-        }
         // or a mixin
-        return  holder.containsFacet(ContributingFacet.class) &&
-                holder.getFacet(ContributingFacet.class).contributed() == MixinFacet.Contributing.AS_PROPERTY;
+        return holder.lookupFacet(ContributingFacet.class)
+                .map(ContributingFacet::contributed)
+                .map(MixinFacet.Contributing.AS_PROPERTY::equals)
+                .orElse(false);
     }
 
     CommandPublishingFacetForPropertyAnnotation(
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/execution/ExecutionPublishingFacetForActionAnnotation.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/execution/ExecutionPublishingFacetForActionAnnotation.java
index 14b780a8d5f..67b52d6bdb8 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/execution/ExecutionPublishingFacetForActionAnnotation.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/execution/ExecutionPublishingFacetForActionAnnotation.java
@@ -58,34 +58,24 @@ public static Optional<ExecutionPublishingFacet> create(
 
         var publishingPolicy = ActionConfigOptions.actionExecutionPublishingPolicy(configuration);
 
-        return
-        actionsIfAny
-                .filter(action -> action.executionPublishing() != Publishing.NOT_SPECIFIED)
-                .map(Action::executionPublishing)
-                .<ExecutionPublishingFacet>map(publishing -> {
-                    switch (publishing) {
-                        case AS_CONFIGURED:
-                            switch (publishingPolicy) {
-                                case NONE:
-                                    return new ExecutionPublishingFacetForActionAnnotationAsConfigured.None(holder);
-                                case IGNORE_QUERY_ONLY:
-                                case IGNORE_SAFE:
-                                    return Facets.hasSafeSemantics(holder)
-                                            ? new ExecutionPublishingFacetForActionAnnotationAsConfigured.IgnoreSafe(holder)
-                                            : new ExecutionPublishingFacetForActionAnnotationAsConfigured.IgnoreSafeYetNot(holder);
-                                case ALL:
-                                    return new ExecutionPublishingFacetForActionAnnotationAsConfigured.All(holder);
-                                default:
-                                    throw new IllegalStateException(String.format("configured action.executionPublishing policy '%s' not recognised", publishingPolicy));
-                                }
-                        case DISABLED:
-                            return new ExecutionPublishingFacetForActionAnnotation.Disabled(holder);
-                        case ENABLED:
-                            return new ExecutionPublishingFacetForActionAnnotation.Enabled(holder);
-                        default:
-                            throw new IllegalStateException(String.format("@Action#executionPublishing '%s' not recognised", publishing));
-                    }
-                });
+        return actionsIfAny
+            .filter(action -> action.executionPublishing() != Publishing.NOT_SPECIFIED)
+            .map(Action::executionPublishing)
+            .<ExecutionPublishingFacet>map(publishing -> (switch (publishing) {
+                case AS_CONFIGURED -> switch (publishingPolicy) {
+                    case NONE -> new ExecutionPublishingFacetForActionAnnotationAsConfigured.None(holder);
+                    case IGNORE_QUERY_ONLY, IGNORE_SAFE -> Facets.hasSafeSemantics(holder)
+                        ? new ExecutionPublishingFacetForActionAnnotationAsConfigured.IgnoreSafe(holder)
+                        : new ExecutionPublishingFacetForActionAnnotationAsConfigured.IgnoreSafeYetNot(holder);
+                    case ALL -> new ExecutionPublishingFacetForActionAnnotationAsConfigured.All(holder);
+                    default -> throw new IllegalStateException(
+                            String.format("configured action.executionPublishing policy '%s' not recognised", publishingPolicy));
+                };
+                case DISABLED -> new ExecutionPublishingFacetForActionAnnotation.Disabled(holder);
+                case ENABLED, ENABLED_FOR_UPDATES_ONLY -> new ExecutionPublishingFacetForActionAnnotation.Enabled(holder);
+                default -> throw new IllegalStateException(
+                        String.format("@Action#executionPublishing '%s' not recognised", publishing));
+            }));
     }
 
     ExecutionPublishingFacetForActionAnnotation(final FacetHolder holder) {
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/execution/ExecutionPublishingFacetForPropertyAnnotation.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/execution/ExecutionPublishingFacetForPropertyAnnotation.java
index ec9fcc705df..f08b54d6e34 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/execution/ExecutionPublishingFacetForPropertyAnnotation.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/members/publish/execution/ExecutionPublishingFacetForPropertyAnnotation.java
@@ -67,66 +67,54 @@ public static ExecutionPublishingFacet create(
         return propertyIfAny
             .map(Property::executionPublishing)
             .filter(publishing -> publishing != Publishing.NOT_SPECIFIED)
-            .map(publishing -> {
-
-                switch (publishing) {
-                    case AS_CONFIGURED:
-                        switch (publishingPolicy) {
-                            case NONE:
-                                return (ExecutionPublishingFacet)new ExecutionPublishingFacetForPropertyAnnotationAsConfigured.None(holder);
-                            case ALL:
-                                return new ExecutionPublishingFacetForPropertyAnnotationAsConfigured.All(holder);
-                            default:
-                                throw new IllegalStateException(String.format("configured property.executionPublishing policy '%s' not recognised", publishingPolicy));
-                        }
-                    case DISABLED:
-                        return new ExecutionPublishingFacetForPropertyAnnotation.Disabled(holder);
-                    case ENABLED:
-                        return new ExecutionPublishingFacetForPropertyAnnotation.Enabled(holder);
-                    default:
-                        throw new IllegalStateException(String.format("@Property#executionPublishing '%s' not recognised", publishing));
-                }
-            })
+            .<ExecutionPublishingFacet>map(publishing -> (switch (publishing) {
+            case AS_CONFIGURED -> switch (publishingPolicy) {
+                case NONE -> new ExecutionPublishingFacetForPropertyAnnotationAsConfigured.None(holder);
+                case ALL -> new ExecutionPublishingFacetForPropertyAnnotationAsConfigured.All(holder);
+                default -> throw new IllegalStateException(
+                        String.format("configured property.executionPublishing policy '%s' not recognised", publishingPolicy));
+            };
+            case DISABLED -> new ExecutionPublishingFacetForPropertyAnnotation.Disabled(holder);
+            case ENABLED, ENABLED_FOR_UPDATES_ONLY -> new ExecutionPublishingFacetForPropertyAnnotation.Enabled(holder);
+            default -> throw new IllegalStateException(
+                    String.format("@Property#executionPublishing '%s' not recognised", publishing));
+            }))
             .orElseGet(() -> {
                 // there is no publishing facet from either @Action or @Property, so use the appropriate configuration to install a default
-                if (representsProperty(holder)) {
+                if (representsProperty(holder))
                     // we are dealing with a property
-                    switch (publishingPolicy) {
-                        case NONE:
-                            return new ExecutionPublishingFacetForPropertyFromConfiguration.None(holder);
-                        case ALL:
-                            return new ExecutionPublishingFacetForPropertyFromConfiguration.All(holder);
-                        default:
-                            throw new IllegalStateException(String.format("configured property.executionPublishing policy '%s' not recognised", publishingPolicy));
-                    }
-                } else {
+                    return switch (publishingPolicy) {
+                        case NONE -> new ExecutionPublishingFacetForPropertyFromConfiguration.None(holder);
+                        case ALL -> new ExecutionPublishingFacetForPropertyFromConfiguration.All(holder);
+                        default -> throw new IllegalStateException(
+                                String.format("configured property.executionPublishing policy '%s' not recognised", publishingPolicy));
+                    };
+                else {
                     // we are dealing with an action
                     var actionPublishingPolicy = ActionConfigOptions.actionExecutionPublishingPolicy(configuration);
-                    switch (actionPublishingPolicy) {
-                        case NONE:
-                            return new ExecutionPublishingFacetForActionFromConfiguration.None(holder);
-                        case IGNORE_QUERY_ONLY:
-                        case IGNORE_SAFE:
-                            return Facets.hasSafeSemantics(holder)
-                                    ? new ExecutionPublishingFacetForActionFromConfiguration.IgnoreSafe(holder)
-                                    : new ExecutionPublishingFacetForActionFromConfiguration.IgnoreSafeYetNot(holder);
-                        case ALL:
-                            return new ExecutionPublishingFacetForActionFromConfiguration.All(holder);
-                        default:
-                            throw new IllegalStateException(String.format("configured action.executionPublishing policy '%s' not recognised", actionPublishingPolicy));
-                    }
+                    return switch (actionPublishingPolicy) {
+                    case NONE -> new ExecutionPublishingFacetForActionFromConfiguration.None(holder);
+                    case IGNORE_QUERY_ONLY, IGNORE_SAFE -> Facets.hasSafeSemantics(holder)
+                                                        ? new ExecutionPublishingFacetForActionFromConfiguration.IgnoreSafe(holder)
+                                                        : new ExecutionPublishingFacetForActionFromConfiguration.IgnoreSafeYetNot(holder);
+                    case ALL -> new ExecutionPublishingFacetForActionFromConfiguration.All(holder);
+                    default -> throw new IllegalStateException(
+                            String.format("configured action.executionPublishing policy '%s' not recognised", actionPublishingPolicy));
+                    };
                 }
             });
     }
 
     private static boolean representsProperty(final FacetHolder holder) {
         // a property
-        if (holder instanceof TypedFacetHolder && ((TypedFacetHolder)holder).featureType() == FeatureType.PROPERTY) {
+        if (holder instanceof TypedFacetHolder typedFacetHolder
+                && typedFacetHolder.featureType() == FeatureType.PROPERTY)
             return true;
-        }
         // or a mixin
-        return  holder.containsFacet(ContributingFacet.class) &&
-                holder.getFacet(ContributingFacet.class).contributed() == MixinFacet.Contributing.AS_PROPERTY;
+        return holder.lookupFacet(ContributingFacet.class)
+                .map(ContributingFacet::contributed)
+                .map(MixinFacet.Contributing.AS_PROPERTY::equals)
+                .orElse(false);
     }
 
     public ExecutionPublishingFacetForPropertyAnnotation(final FacetHolder holder) {
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/entitychangepublishing/EntityChangePublishingFacetForDomainObjectAnnotation.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/entitychangepublishing/EntityChangePublishingFacetForDomainObjectAnnotation.java
index 9956501d41c..56a686b2250 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/entitychangepublishing/EntityChangePublishingFacetForDomainObjectAnnotation.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/entitychangepublishing/EntityChangePublishingFacetForDomainObjectAnnotation.java
@@ -38,32 +38,27 @@ public static Optional<EntityChangePublishingFacet> create(
 
         var publish = entityChangePublishingIfAny.orElse(Publishing.AS_CONFIGURED);
 
-        switch (publish) {
-        case NOT_SPECIFIED:
-        case AS_CONFIGURED:
-
-            var publishingPolicy = DomainObjectConfigOptions.entityChangePublishingPolicy(configuration);
-            switch (publishingPolicy) {
-            case NONE:
-                return Optional.of(entityChangePublishingIfAny.isPresent()
+        return switch (publish) {
+            case NOT_SPECIFIED, AS_CONFIGURED -> {
+                var publishingPolicy = DomainObjectConfigOptions.entityChangePublishingPolicy(configuration);
+                yield switch (publishingPolicy) {
+                    case NONE -> Optional.of(entityChangePublishingIfAny.isPresent()
                         ? new EntityChangePublishingFacetForDomainObjectAnnotationAsConfigured(holder, false)
                         : new EntityChangePublishingFacetFromConfiguration(holder, false));
-            default:
-                return Optional.of(entityChangePublishingIfAny.isPresent()
+                    default -> Optional.of(entityChangePublishingIfAny.isPresent()
                         ? new EntityChangePublishingFacetForDomainObjectAnnotationAsConfigured(holder, true)
                         : new EntityChangePublishingFacetFromConfiguration(holder, true));
+                };
             }
-        case DISABLED:
-            return Optional.of(new EntityChangePublishingFacetForDomainObjectAnnotation(holder, false));
-        case ENABLED:
-            return Optional.of(new EntityChangePublishingFacetForDomainObjectAnnotation(holder, true));
-
-        default:
-            throw _Exceptions.unmatchedCase(publish);
-        }
+            case DISABLED -> Optional.of(new EntityChangePublishingFacetForDomainObjectAnnotation(holder, false, false, false, false));
+            case ENABLED -> Optional.of(new EntityChangePublishingFacetForDomainObjectAnnotation(holder, true, true, true, true));
+            case ENABLED_FOR_UPDATES_ONLY -> Optional.of(new EntityChangePublishingFacetForDomainObjectAnnotation(holder, true, false, true, false));
+            default -> throw _Exceptions.unmatchedCase(publish);
+        };
     }
 
-    protected EntityChangePublishingFacetForDomainObjectAnnotation(final FacetHolder holder, boolean enabled) {
-        super(holder, enabled);
+    protected EntityChangePublishingFacetForDomainObjectAnnotation(final FacetHolder holder, final boolean enabled,
+            final boolean enabledForCreate, final boolean enabledForUpdate, final boolean enabledForDelete) {
+        super(holder, enabled, enabledForCreate, enabledForUpdate, enabledForDelete);
     }
 }
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/entitychangepublishing/EntityChangePublishingFacetForDomainObjectAnnotationAsConfigured.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/entitychangepublishing/EntityChangePublishingFacetForDomainObjectAnnotationAsConfigured.java
index df5890d9df7..e724d79135d 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/entitychangepublishing/EntityChangePublishingFacetForDomainObjectAnnotationAsConfigured.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/entitychangepublishing/EntityChangePublishingFacetForDomainObjectAnnotationAsConfigured.java
@@ -23,7 +23,7 @@
 public class EntityChangePublishingFacetForDomainObjectAnnotationAsConfigured extends EntityChangePublishingFacetForDomainObjectAnnotation {
 
     public EntityChangePublishingFacetForDomainObjectAnnotationAsConfigured(final FacetHolder facetHolder, final boolean enabled) {
-        super(facetHolder, enabled);
+        super(facetHolder, enabled, enabled, enabled, enabled);
     }
 
 }
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/entitychangepublishing/EntityChangePublishingFacetFromConfiguration.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/entitychangepublishing/EntityChangePublishingFacetFromConfiguration.java
index cdeebda521c..0ef4aef74cf 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/entitychangepublishing/EntityChangePublishingFacetFromConfiguration.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/domainobject/entitychangepublishing/EntityChangePublishingFacetFromConfiguration.java
@@ -30,7 +30,7 @@ public class EntityChangePublishingFacetFromConfiguration
 extends EntityChangePublishingFacetAbstract {
 
     public EntityChangePublishingFacetFromConfiguration(final FacetHolder facetHolder, final boolean enabled) {
-        super(facetHolder, enabled);
+        super(facetHolder, enabled, enabled, enabled, enabled);
     }
 
 }
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/publish/entitychange/EntityChangePublishingFacet.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/publish/entitychange/EntityChangePublishingFacet.java
index 91513f3bee0..c8b869cdc12 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/publish/entitychange/EntityChangePublishingFacet.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/publish/entitychange/EntityChangePublishingFacet.java
@@ -18,6 +18,8 @@
  */
 package org.apache.causeway.core.metamodel.facets.object.publish.entitychange;
 
+import java.util.Optional;
+
 import org.apache.causeway.core.metamodel.facetapi.Facet;
 import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
@@ -30,22 +32,41 @@
  */
 public interface EntityChangePublishingFacet extends Facet {
 
-    public static boolean isPublishingEnabled(final FacetHolder facetHolder) {
-        if(facetHolder==null) {
-            return false;
-        }
+    static boolean isPublishingEnabled(final FacetHolder facetHolder) {
+        return entityChangePublishingFacet(facetHolder)
+                .map(EntityChangePublishingFacet::isEnabled)
+                .orElse(false);
+    }
+
+    static boolean isPublishingEnabledForCreate(final FacetHolder facetHolder) {
+        return entityChangePublishingFacet(facetHolder)
+                .map(EntityChangePublishingFacet::isEnabledForCreate)
+                .orElse(false);
+    }
 
-        if(facetHolder instanceof ObjectSpecification) {
-            if(!((ObjectSpecification)facetHolder).isEntity()) {
-                return false;
-            }
-        }
+    static boolean isPublishingEnabledForUpdate(final FacetHolder facetHolder) {
+        return entityChangePublishingFacet(facetHolder)
+                .map(EntityChangePublishingFacet::isEnabledForUpdate)
+                .orElse(false);
+    }
 
-        var entityChangePublishingFacet = facetHolder.getFacet(EntityChangePublishingFacet.class);
-        return entityChangePublishingFacet != null
-                && entityChangePublishingFacet.isEnabled();
+    static boolean isPublishingEnabledForDelete(final FacetHolder facetHolder) {
+        return entityChangePublishingFacet(facetHolder)
+                .map(EntityChangePublishingFacet::isEnabledForDelete)
+                .orElse(false);
     }
 
     boolean isEnabled();
+    boolean isEnabledForCreate();
+    boolean isEnabledForUpdate();
+    boolean isEnabledForDelete();
 
+    private static Optional<EntityChangePublishingFacet> entityChangePublishingFacet(final FacetHolder facetHolder) {
+        if(facetHolder==null)
+            return Optional.empty();
+        if(facetHolder instanceof ObjectSpecification objSpepc
+                && !objSpepc.isEntity())
+            return Optional.empty(); // optimization
+        return facetHolder.lookupFacet(EntityChangePublishingFacet.class);
+    }
 }
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/publish/entitychange/EntityChangePublishingFacetAbstract.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/publish/entitychange/EntityChangePublishingFacetAbstract.java
index af60b3f0544..20ce78bf62f 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/publish/entitychange/EntityChangePublishingFacetAbstract.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/publish/entitychange/EntityChangePublishingFacetAbstract.java
@@ -32,12 +32,18 @@ private static final Class<? extends Facet> type() {
         return EntityChangePublishingFacet.class;
     }
 
-    @Getter
-    private final boolean enabled;
+    @Getter private final boolean enabled;
+    @Getter private final boolean enabledForCreate;
+    @Getter private final boolean enabledForUpdate;
+    @Getter private final boolean enabledForDelete;
 
-    public EntityChangePublishingFacetAbstract(final FacetHolder facetHolder, boolean enabled) {
+    public EntityChangePublishingFacetAbstract(final FacetHolder facetHolder, final boolean enabled,
+            final boolean enabledForCreate, final boolean enabledForUpdate, final boolean enabledForDelete) {
         super(EntityChangePublishingFacetAbstract.type(), facetHolder);
         this.enabled = enabled;
+        this.enabledForCreate = enabledForCreate;
+        this.enabledForUpdate = enabledForUpdate;
+        this.enabledForDelete = enabledForDelete;
     }
 
 }
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/properties/property/entitychangepublishing/EntityPropertyChangePublishingPolicyFacet.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/properties/property/entitychangepublishing/EntityPropertyChangePublishingPolicyFacet.java
index 6eca0d5a86a..4ea8c167ac0 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/properties/property/entitychangepublishing/EntityPropertyChangePublishingPolicyFacet.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/properties/property/entitychangepublishing/EntityPropertyChangePublishingPolicyFacet.java
@@ -18,14 +18,14 @@
  */
 package org.apache.causeway.core.metamodel.facets.properties.property.entitychangepublishing;
 
+import org.jspecify.annotations.NonNull;
+
 import org.apache.causeway.applib.annotation.Publishing;
 import org.apache.causeway.applib.value.Blob;
 import org.apache.causeway.applib.value.Clob;
 import org.apache.causeway.core.metamodel.facetapi.Facet;
 import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
 
-import org.jspecify.annotations.NonNull;
-
 /**
  * Indicates whether a property should be excluded from entity change publishing (auditing).
  * @since 2.0
@@ -33,7 +33,7 @@
 public interface EntityPropertyChangePublishingPolicyFacet extends Facet {
 
     /**
-     * Must be one of Publishing.ENABLED or Publishing.DISABLED.
+     * Must be one of {@link Publishing#ENABLED}, {@link Publishing#ENABLED_FOR_UPDATES_ONLY} or {@link Publishing#DISABLED}.
      */
     @NonNull Publishing getEntityChangePublishing();
 
@@ -42,7 +42,8 @@ default boolean isPublishingVetoed() {
     }
 
     default boolean isPublishingAllowed() {
-        return getEntityChangePublishing() == Publishing.ENABLED;
+        return getEntityChangePublishing() == Publishing.ENABLED
+                || getEntityChangePublishing() == Publishing.ENABLED_FOR_UPDATES_ONLY;
     }
 
     static boolean isExcludedFromPublishing(final @NonNull OneToOneAssociation property) {
@@ -58,7 +59,7 @@ static boolean isExcludedFromPublishing(final @NonNull OneToOneAssociation prope
                     .map(EntityPropertyChangePublishingPolicyFacet::isPublishingAllowed)
                     .orElse(false);
 
-            //XXX CAUSEWAY-1488, exclude Bob/Clob from property change publishing unless explicitly allowed
+            //XXX CAUSEWAY-1488, exclude Blob/Clob from property change publishing unless explicitly allowed
             return !isExplictlyAllowed;
         }
 
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/properties/property/entitychangepublishing/EntityPropertyChangePublishingPolicyFacetForPropertyAnnotation.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/properties/property/entitychangepublishing/EntityPropertyChangePublishingPolicyFacetForPropertyAnnotation.java
index dbf4cc19abc..bcba7479eb2 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/properties/property/entitychangepublishing/EntityPropertyChangePublishingPolicyFacetForPropertyAnnotation.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/properties/property/entitychangepublishing/EntityPropertyChangePublishingPolicyFacetForPropertyAnnotation.java
@@ -34,9 +34,9 @@ public static Optional<EntityPropertyChangePublishingPolicyFacet> create(
         return propertyIfAny
                 .map(Property::entityChangePublishing)
                 // only install facet if policy is explicit ('enabled' or 'disabled')
-                .filter(entityChangePublishing ->
-                    entityChangePublishing == Publishing.ENABLED
-                        || entityChangePublishing == Publishing.DISABLED)
+                .filter(entityChangePublishing -> entityChangePublishing == Publishing.ENABLED
+                    || entityChangePublishing == Publishing.ENABLED_FOR_UPDATES_ONLY
+                    || entityChangePublishing == Publishing.DISABLED)
                 .map(entityChangePublishing ->
                     new EntityPropertyChangePublishingPolicyFacetForPropertyAnnotation(entityChangePublishing, holder));
     }
diff --git a/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/facets/FacetFactoryTestAbstract.java b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/facets/FacetFactoryTestAbstract.java
new file mode 100644
index 00000000000..a286ff984aa
--- /dev/null
+++ b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/facets/FacetFactoryTestAbstract.java
@@ -0,0 +1,484 @@
+/*
+ *  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.causeway.core.metamodel.facets;
+
+import java.util.Optional;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+import org.jspecify.annotations.NonNull;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.mockito.Mockito;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.causeway.applib.Identifier;
+import org.apache.causeway.applib.annotation.Introspection.IntrospectionPolicy;
+import org.apache.causeway.applib.id.LogicalType;
+import org.apache.causeway.applib.services.i18n.TranslationService;
+import org.apache.causeway.applib.services.iactnlayer.InteractionService;
+import org.apache.causeway.commons.internal.assertions._Assert;
+import org.apache.causeway.commons.internal.reflection._GenericResolver.ResolvedMethod;
+import org.apache.causeway.commons.internal.reflection._MethodFacades;
+import org.apache.causeway.core.metamodel.context.HasMetaModelContext;
+import org.apache.causeway.core.metamodel.context.MetaModelContext;
+import org.apache.causeway.core.metamodel.execution.MemberExecutorService;
+import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
+import org.apache.causeway.core.metamodel.facetapi.FeatureType;
+import org.apache.causeway.core.metamodel.facets.FacetFactory.ProcessClassContext;
+import org.apache.causeway.core.metamodel.facets.FacetFactory.ProcessMethodContext;
+import org.apache.causeway.core.metamodel.facets.FacetFactory.ProcessParameterContext;
+import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
+import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
+import org.apache.causeway.core.metamodel.spec.feature.OneToManyAssociation;
+import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
+import org.apache.causeway.core.metamodel.spec.impl._JUnitSupport;
+import org.apache.causeway.core.metamodel.valuesemantics.IntValueSemantics;
+import org.apache.causeway.core.mmtestsupport.MetaModelContext_forTesting;
+import org.apache.causeway.core.mmtestsupport.MethodRemover_forTesting;
+import org.apache.causeway.core.security.authentication.InteractionContextFactory;
+
+import lombok.Builder;
+import lombok.Getter;
+
+public abstract class FacetFactoryTestAbstract
+implements HasMetaModelContext {
+
+    // -- SCENARIO BUILDER
+
+    @Builder
+    public record ActionScenario(
+            Class<?> declaringClass,
+            String actionName,
+            Optional<Class<?>> mixinClass) {
+        public static ActionScenarioBuilder builder(final Class<?> declaringClass, final String actionName) {
+            return new ActionScenario.ActionScenarioBuilder()
+                    .mixinClass(Optional.empty())
+                    .declaringClass(declaringClass)
+                    .actionName(actionName);
+        }
+    }
+
+    @Builder
+    public record ParameterScenario(
+            Class<?> declaringClass,
+            String actionName,
+            int paramIndex,
+            Optional<Class<?>> mixinClass) {
+        public static ParameterScenarioBuilder builder(final Class<?> declaringClass, final String actionName, final int paramIndex) {
+            return new ParameterScenario.ParameterScenarioBuilder()
+                    .mixinClass(Optional.empty())
+                    .declaringClass(declaringClass)
+                    .actionName(actionName)
+                    .paramIndex(paramIndex);
+        }
+    }
+
+    @Builder
+    public record PropertyScenario(
+            Class<?> declaringClass,
+            String propertyName,
+            Optional<Class<?>> mixinClass) {
+        public static PropertyScenarioBuilder builder(final Class<?> declaringClass, final String propertyName) {
+            return new PropertyScenario.PropertyScenarioBuilder()
+                    .mixinClass(Optional.empty())
+                    .declaringClass(declaringClass)
+                    .propertyName(propertyName);
+        }
+    }
+
+    @Builder
+    public record CollectionScenario(
+            Class<?> declaringClass,
+            String collectionName,
+            Optional<Class<?>> mixinClass) {
+        public static CollectionScenarioBuilder builder(final Class<?> declaringClass, final String collectionName) {
+            return new CollectionScenario.CollectionScenarioBuilder()
+                    .mixinClass(Optional.empty())
+                    .declaringClass(declaringClass)
+                    .collectionName(collectionName);
+        }
+    }
+
+    // -- SETUP
+
+    @Getter(onMethod_ = {@Override}) protected MetaModelContext metaModelContext;
+
+    private MethodRemover_forTesting methodRemover;
+
+    @BeforeEach
+    protected void setup() {
+        setup(__->{});
+    }
+
+    protected final void setup(
+        final Consumer<MetaModelContext_forTesting.MetaModelContext_forTestingBuilder> customizer) {
+
+        var mockTranslationService = Mockito.mock(TranslationService.class);
+        var mockInteractionService = Mockito.mock(InteractionService.class);
+        var mockMemberExecutorService = Mockito.mock(MemberExecutorService.class);
+
+        var iaContext = InteractionContextFactory.testing();
+
+        this.methodRemover = new MethodRemover_forTesting();
+
+        var builder = MetaModelContext_forTesting.builder()
+            .translationService(mockTranslationService)
+            .interactionService(mockInteractionService)
+            .memberExecutor(mockMemberExecutorService)
+            .valueSemantic(new IntValueSemantics());
+
+        customizer.accept(builder);
+
+        this.metaModelContext = builder.build();
+
+        Mockito.when(mockInteractionService.currentInteractionContext()).thenReturn(Optional.of(iaContext));
+    }
+
+    // -- TEAR DOWN
+
+    @AfterEach
+    protected void tearDownAll() {
+        methodRemover = null;
+    }
+
+    @FunctionalInterface
+    public static interface MemberScenarioConsumer {
+        void accept(
+                ProcessMethodContext processMethodContext,
+                FacetHolder facetHolder,
+                FacetedMethod facetedMethod);
+    }
+    @FunctionalInterface
+    protected static interface MixedInActionScenarioConsumer {
+        void accept(
+                ProcessMethodContext processMethodContext,
+                ObjectSpecification mixeeSpec,
+                FacetedMethod facetedMethod,
+                ObjectAction mixedInAct);
+    }
+    @FunctionalInterface
+    protected static interface MixedInPropertyScenarioConsumer {
+        void accept(
+                ProcessMethodContext processMethodContext,
+                ObjectSpecification mixeeSpec,
+                FacetedMethod facetedMethod,
+                OneToOneAssociation mixedInProp);
+    }
+    @FunctionalInterface
+    protected static interface MixedInCollectionScenarioConsumer {
+        void accept(
+                ProcessMethodContext processMethodContext,
+                ObjectSpecification mixeeSpec,
+                FacetedMethod facetedMethod,
+                OneToManyAssociation mixedInColl);
+    }
+    @FunctionalInterface
+    protected static interface ParameterScenarioConsumer {
+        void accept(
+                ProcessParameterContext processParameterContext,
+                FacetHolder facetHolder,
+                FacetedMethod facetedMethod,
+                FacetedMethodParameter facetedMethodParameter);
+    }
+
+    /**
+     * Action scenario.
+     */
+    protected void actionScenario(
+            final Class<?> declaringClass, final String actionName, final MemberScenarioConsumer consumer) {
+        actionScenario(ActionScenario.builder(declaringClass, actionName).build(), consumer);
+    }
+    /**
+     * Custom Action scenario.
+     */
+    protected void actionScenario(
+            final ActionScenario scenario, final MemberScenarioConsumer consumer) {
+
+        var declaringClass = scenario.declaringClass();
+        var memberId = scenario.actionName();
+        var actionMethod = _Utils.findMethodByNameOrFail(declaringClass, memberId);
+        var paramTypes = actionMethod.paramTypes();
+        var facetHolder = actionFacetHolder(declaringClass, memberId, paramTypes);
+        var facetedMethod = FacetedMethod.testing.createForAction(getMetaModelContext(), declaringClass, memberId, paramTypes);
+        var processMethodContext = ProcessMethodContext
+                .forTesting(declaringClass, FeatureType.ACTION, actionMethod, methodRemover, facetedMethod);
+
+        consumer.accept(processMethodContext, facetHolder, facetedMethod);
+    }
+    /**
+     * MixedIn Action scenario.
+     */
+    protected void actionScenarioMixedIn(
+            final Class<?> mixeeClass, final Class<?> mixinClass, final MixedInActionScenarioConsumer consumer) {
+        var scenario = ActionScenario.builder(mixeeClass, "unused")
+                .mixinClass(Optional.of(mixinClass))
+                .build();
+        actionScenarioMixedIn(scenario, consumer);
+    }
+    protected void actionScenarioMixedIn(
+            final ActionScenario scenario, final MixedInActionScenarioConsumer consumer) {
+
+        var declaringClass = scenario.declaringClass();
+
+        // get mixin main, assuming 'act'
+        var mixinClass = scenario.mixinClass().orElseThrow();
+        var mixedInMethod = _Utils.findMethodByNameOrFail(mixinClass, "act");
+
+        var annotatedMethod = mixedInMethod;
+        var facetedMethod = FacetedMethod.createForAction(getMetaModelContext(), mixinClass,
+                _MethodFacades.regular(annotatedMethod));
+
+        var id = facetedMethod.getFeatureIdentifier();
+        assertNotNull(id.className());
+
+        var processMethodContext = new ProcessMethodContext(
+                mixinClass, IntrospectionPolicy.ENCAPSULATION_ENABLED, FeatureType.ACTION,
+                _MethodFacades.regular(annotatedMethod),
+                methodRemover, facetedMethod, true);
+
+        final ObjectSpecification mixeeSpec = getSpecificationLoader().loadSpecification(declaringClass);
+        final ObjectSpecification mixinSpec = getSpecificationLoader().loadSpecification(mixinClass);
+        final ObjectAction mixedInAct =
+                _JUnitSupport.mixedInActionforMixinMain(mixeeSpec, mixinSpec, "act", facetedMethod);
+
+        consumer.accept(processMethodContext, mixeeSpec, facetedMethod, mixedInAct);
+    }
+
+    /**
+     * Parameter scenario.
+     */
+    protected void parameterScenario(
+            final Class<?> declaringClass, final String actionName, final int paramIndex, final ParameterScenarioConsumer consumer) {
+        parameterScenario(ParameterScenario.builder(declaringClass, actionName, paramIndex).build(), consumer);
+    }
+    /**
+     * Custom Parameter scenario.
+     */
+    protected void parameterScenario(
+            final ParameterScenario scenario, final ParameterScenarioConsumer consumer) {
+        _Assert.assertEquals(0, scenario.paramIndex(), ()->"not yet implemented otherwise");
+
+        var declaringClass = scenario.declaringClass();
+        var memberId = scenario.actionName();
+        var actionMethod = _Utils.findMethodByNameOrFail(declaringClass, memberId);
+        var paramTypes = actionMethod.paramTypes();
+        var facetHolder = actionFacetHolder(declaringClass, memberId, paramTypes);
+        var facetedMethod = FacetedMethod.testing.createForAction(getMetaModelContext(), declaringClass, memberId, paramTypes);
+        var facetedMethodParameter =
+                actionMethod.isNoArg()
+                ? (FacetedMethodParameter)null
+                : new FacetedMethodParameter(getMetaModelContext(),
+                    FeatureType.ACTION_PARAMETER_SINGULAR, facetedMethod.owningType(),
+                    facetedMethod.methodFacade(), 0);
+
+        var processParameterContext =
+                FacetFactory.ProcessParameterContext.forTesting(
+                        declaringClass, IntrospectionPolicy.ANNOTATION_OPTIONAL, actionMethod, null, facetedMethodParameter);
+
+        consumer.accept(processParameterContext, facetHolder, facetedMethod, facetedMethodParameter);
+    }
+
+    /**
+     * Property scenario.
+     */
+    protected void propertyScenario(
+            final Class<?> declaringClass, final String propertyName, final MemberScenarioConsumer consumer) {
+        propertyScenario(PropertyScenario.builder(declaringClass, propertyName).build(), consumer);
+    }
+    /**
+     * Custom Property scenario.
+     */
+    protected void propertyScenario(
+            final PropertyScenario scenario, final MemberScenarioConsumer consumer) {
+        var declaringClass = scenario.declaringClass();
+        var memberId = scenario.propertyName();
+
+        var facetHolder = propertyFacetHolder(declaringClass, memberId);
+        var annotatedMethod = _Utils.findGetterOrFail(declaringClass, memberId);
+        var facetedMethod = FacetedMethod.createForProperty(getMetaModelContext(), declaringClass, annotatedMethod);
+
+        var processMethodContext = ProcessMethodContext
+                .forTesting(declaringClass, FeatureType.PROPERTY, annotatedMethod, methodRemover, facetedMethod);
+
+        consumer.accept(processMethodContext, facetHolder, facetedMethod);
+    }
+    /**
+     * MixedIn Property scenario.
+     */
+    protected void propertyScenarioMixedIn(
+            final Class<?> mixeeClass, final Class<?> mixinClass, final MixedInPropertyScenarioConsumer consumer) {
+        var scenario = PropertyScenario.builder(mixeeClass, "unused")
+                .mixinClass(Optional.of(mixinClass))
+                .build();
+        propertyScenarioMixedIn(scenario, consumer);
+    }
+    protected void propertyScenarioMixedIn(
+            final PropertyScenario scenario, final MixedInPropertyScenarioConsumer consumer) {
+
+        var declaringClass = scenario.declaringClass();
+
+        // get mixin main, assuming 'prop'
+        var mixinClass = scenario.mixinClass().orElseThrow();
+        var annotatedMethod = _Utils.findMethodByNameOrFail(mixinClass, "prop");
+
+        var facetedMethod = FacetedMethod.createForProperty(getMetaModelContext(), mixinClass, annotatedMethod);
+
+        var id = facetedMethod.getFeatureIdentifier();
+        assertNotNull(id.className());
+
+        var processMethodContext = new ProcessMethodContext(
+                mixinClass, IntrospectionPolicy.ENCAPSULATION_ENABLED, FeatureType.PROPERTY,
+                _MethodFacades.regular(annotatedMethod),
+                methodRemover, facetedMethod, true);
+
+        final ObjectSpecification mixeeSpec = getSpecificationLoader().loadSpecification(declaringClass);
+        final ObjectSpecification mixinSpec = getSpecificationLoader().loadSpecification(mixinClass);
+        final OneToOneAssociation mixedInProp =
+                _JUnitSupport.mixedInProp(mixeeSpec, mixinSpec, "prop", facetedMethod);
+
+        consumer.accept(processMethodContext, mixeeSpec, facetedMethod, mixedInProp);
+    }
+
+    /**
+     * Collection scenario.
+     */
+    protected void collectionScenario(
+            final Class<?> declaringClass, final String collectionName, final MemberScenarioConsumer consumer) {
+        collectionScenario(CollectionScenario.builder(declaringClass, collectionName).build(), consumer);
+    }
+    /**
+     * Custom Collection scenario.
+     */
+    protected void collectionScenario(
+            final CollectionScenario scenario, final MemberScenarioConsumer consumer) {
+
+        var declaringClass = scenario.declaringClass();
+        var memberId = scenario.collectionName();
+        var facetHolder = collectionFacetHolder(declaringClass, memberId);
+        var annotatedMethod = _Utils.findGetterOrFail(declaringClass, memberId);
+        var facetedMethod = FacetedMethod.createForProperty(getMetaModelContext(), declaringClass, annotatedMethod);
+
+        var processMethodContext = ProcessMethodContext
+                .forTesting(declaringClass, FeatureType.COLLECTION, annotatedMethod, methodRemover, facetedMethod);
+
+        consumer.accept(processMethodContext, facetHolder, facetedMethod);
+    }
+    /**
+     * MixedIn Collection scenario.
+     */
+    protected void collectionScenarioMixedIn(
+            final Class<?> mixeeClass, final Class<?> mixinClass, final MixedInCollectionScenarioConsumer consumer) {
+        var scenario = CollectionScenario.builder(mixeeClass, "unused")
+                .mixinClass(Optional.of(mixinClass))
+                .build();
+        collectionScenarioMixedIn(scenario, consumer);
+    }
+    protected void collectionScenarioMixedIn(
+            final CollectionScenario scenario, final MixedInCollectionScenarioConsumer consumer) {
+
+        var declaringClass = scenario.declaringClass();
+
+        // get mixin main, assuming 'coll'
+        var mixinClass = scenario.mixinClass().orElseThrow();
+        var annotatedMethod = _Utils.findMethodByNameOrFail(mixinClass, "coll");
+
+        var facetedMethod = FacetedMethod.createForCollection(getMetaModelContext(), mixinClass, annotatedMethod);
+
+        var id = facetedMethod.getFeatureIdentifier();
+        assertNotNull(id.className());
+
+        var processMethodContext = new ProcessMethodContext(
+                mixinClass, IntrospectionPolicy.ENCAPSULATION_ENABLED, FeatureType.COLLECTION,
+                _MethodFacades.regular(annotatedMethod),
+                methodRemover, facetedMethod, true);
+
+        final ObjectSpecification mixeeSpec = getSpecificationLoader().loadSpecification(declaringClass);
+        final ObjectSpecification mixinSpec = getSpecificationLoader().loadSpecification(mixinClass);
+        final OneToManyAssociation mixedInColl =
+                _JUnitSupport.mixedInColl(mixeeSpec, mixinSpec, "coll", facetedMethod);
+
+        consumer.accept(processMethodContext, mixeeSpec, facetedMethod, mixedInColl);
+    }
+
+    /**
+     * DomainObject scenario.
+     */
+    protected void objectScenario(final Class<?> declaringClass, final BiConsumer<ProcessClassContext, FacetHolder> consumer) {
+        var facetHolder = FacetHolder.simple(getMetaModelContext(),
+                Identifier.classIdentifier(LogicalType.fqcn(declaringClass)));
+        var processClassContext = ProcessClassContext
+                .forTesting(declaringClass, methodRemover, facetHolder);
+        consumer.accept(processClassContext, facetHolder);
+    }
+
+    // -- UTILITY
+
+    protected static ResolvedMethod findMethodExactOrFail(final Class<?> type, final String methodName, final Class<?>[] paramTypes) {
+        return _Utils.findMethodExactOrFail(type, methodName, paramTypes);
+    }
+
+    protected static ResolvedMethod findMethodExactOrFail(final Class<?> type, final String methodName) {
+        return _Utils.findMethodExactOrFail(type, methodName);
+    }
+
+    protected static Optional<ResolvedMethod> findMethodExact(final Class<?> type, final String methodName) {
+        return _Utils.findMethodExact(type, methodName);
+    }
+
+    private FacetHolder actionFacetHolder(final Class<?> declaringClass, final String memberId, final Class<?>[] paramTypes) {
+        return FacetHolder.simple(getMetaModelContext(),
+                Identifier.actionIdentifier(LogicalType.fqcn(declaringClass), memberId, paramTypes));
+    }
+
+    private FacetHolder propertyFacetHolder(final Class<?> declaringClass, final String memberId) {
+        return FacetHolder.simple(getMetaModelContext(),
+                Identifier.propertyIdentifier(LogicalType.fqcn(declaringClass), memberId));
+    }
+
+    private FacetHolder collectionFacetHolder(final Class<?> declaringClass, final String memberId) {
+        return FacetHolder.simple(getMetaModelContext(),
+                Identifier.collectionIdentifier(LogicalType.fqcn(declaringClass), memberId));
+    }
+
+    // -- EXPECTATIONS
+
+    protected final void assertNoMethodsRemoved() {
+        assertTrue(methodRemover.removedMethodMethodCalls().isEmpty());
+        assertTrue(methodRemover.removeMethodArgsCalls().isEmpty());
+    }
+
+    protected final void assertMethodWasRemoved(final ResolvedMethod method) {
+        assertTrue(methodRemover.removedMethodMethodCalls().contains(method),
+                ()->String.format("method was not removed in test scenario: %s", method));
+    }
+
+    protected final void assertMethodWasRemoved(final Class<?> type, final String methodName) {
+        assertMethodWasRemoved(findMethodExactOrFail(type, methodName));
+    }
+
+    protected final void assertMethodEqualsFirstIn(
+            final @NonNull ResolvedMethod method,
+            final @NonNull ImperativeFacet imperativeFacet) {
+        _Utils.assertMethodEquals(method, imperativeFacet.getMethods().getFirstElseFail().asMethodElseFail());
+    }
+
+}
diff --git a/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/facets/object/domainobject/DomainObjectAnnotationFacetFactoryTest.java b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/facets/object/domainobject/DomainObjectAnnotationFacetFactoryTest.java
new file mode 100644
index 00000000000..deb67cba2de
--- /dev/null
+++ b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/facets/object/domainobject/DomainObjectAnnotationFacetFactoryTest.java
@@ -0,0 +1,768 @@
+/*
+ *  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.causeway.core.metamodel.facets.object.domainobject;
+
+import java.util.UUID;
+
+import jakarta.inject.Named;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+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.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.springframework.boot.test.util.TestPropertyValues;
+
+import org.apache.causeway.applib.annotation.Bounding;
+import org.apache.causeway.applib.annotation.DomainObject;
+import org.apache.causeway.applib.annotation.DomainService;
+import org.apache.causeway.applib.annotation.Publishing;
+import org.apache.causeway.applib.id.LogicalType;
+import org.apache.causeway.applib.mixins.system.HasInteractionId;
+import org.apache.causeway.commons.collections.Can;
+import org.apache.causeway.core.config.metamodel.facets.DomainObjectConfigOptions;
+import org.apache.causeway.core.metamodel.facetapi.Facet;
+import org.apache.causeway.core.metamodel.facets.FacetFactoryTestAbstract;
+import org.apache.causeway.core.metamodel.facets.object.autocomplete.AutoCompleteFacet;
+import org.apache.causeway.core.metamodel.facets.object.domainobject.autocomplete.AutoCompleteFacetForDomainObjectAnnotation;
+import org.apache.causeway.core.metamodel.facets.object.domainobject.choices.ChoicesFacetForDomainObjectAnnotation;
+import org.apache.causeway.core.metamodel.facets.object.domainobject.editing.ImmutableFacetForDomainObjectAnnotation;
+import org.apache.causeway.core.metamodel.facets.object.domainobject.editing.ImmutableFacetForDomainObjectAnnotationAsConfigured;
+import org.apache.causeway.core.metamodel.facets.object.domainobject.editing.ImmutableFacetFromConfiguration;
+import org.apache.causeway.core.metamodel.facets.object.domainobject.entitychangepublishing.EntityChangePublishingFacetForDomainObjectAnnotation;
+import org.apache.causeway.core.metamodel.facets.object.domainobject.entitychangepublishing.EntityChangePublishingFacetForDomainObjectAnnotationAsConfigured;
+import org.apache.causeway.core.metamodel.facets.object.domainobject.entitychangepublishing.EntityChangePublishingFacetFromConfiguration;
+import org.apache.causeway.core.metamodel.facets.object.immutable.ImmutableFacet;
+import org.apache.causeway.core.metamodel.facets.object.logicaltype.AliasedFacet;
+import org.apache.causeway.core.metamodel.facets.object.publish.entitychange.EntityChangePublishingFacet;
+import org.apache.causeway.core.metamodel.facets.object.viewmodel.ViewModelFacet;
+import org.apache.causeway.core.metamodel.facets.object.viewmodel.ViewModelFacetForDomainObjectAnnotation;
+import org.apache.causeway.core.metamodel.facets.objectvalue.choices.ChoicesFacet;
+import org.apache.causeway.core.metamodel.specloader.validator.ValidationFailures;
+import org.apache.causeway.core.mmtestsupport.MetaModelContext_forTesting;
+
+abstract class DomainObjectAnnotationFacetFactoryTest
+extends FacetFactoryTestAbstract {
+
+    DomainObjectAnnotationFacetFactory facetFactory;
+
+    @AfterEach
+    protected void tearDown() throws Exception {
+        facetFactory = null;
+    }
+
+    static class Customer {
+    }
+
+    class SomeHasInteractionId implements HasInteractionId {
+
+        @Override
+        public UUID getInteractionId() {
+            return null;
+        }
+
+    }
+
+    @Override
+    protected void setup() {
+        //overrides default setup method to do nothing
+    }
+
+    void allowingEntityChangePublishingToReturn(final DomainObjectConfigOptions.EntityChangePublishingPolicy value) {
+        var testPropertyValues = value!=null
+            ? TestPropertyValues.of("causeway.applib.annotation.domainObject.entityChangePublishing=" + value.name())
+            : TestPropertyValues.empty();
+        super.setup(builder->builder.testPropertyValues(testPropertyValues));
+        facetFactory = new DomainObjectAnnotationFacetFactory(getMetaModelContext());
+    }
+    void allowingObjectsEditingToReturn(final DomainObjectConfigOptions.EditingObjectsConfiguration value) {
+        var testPropertyValues = value!=null
+            ? TestPropertyValues.of("causeway.applib.annotation.domainObject.editing=" + value.name())
+            : TestPropertyValues.empty();
+        super.setup(builder->builder.testPropertyValues(testPropertyValues));
+        facetFactory = new DomainObjectAnnotationFacetFactory(getMetaModelContext());
+    }
+    protected void ignoringConfiguration() {
+        super.setup();
+        facetFactory = new DomainObjectAnnotationFacetFactory(getMetaModelContext());
+    }
+
+    @Nested
+    public class EntityChangePublishing  {
+
+        @DomainObject(entityChangePublishing = org.apache.causeway.applib.annotation.Publishing.AS_CONFIGURED)
+        class CustomerWithDomainObjectAndAuditingSetToAsConfigured {
+        }
+
+        @DomainObject(entityChangePublishing = org.apache.causeway.applib.annotation.Publishing.DISABLED)
+        class CustomerWithDomainObjectAndAuditingSetToDisabled {
+        }
+
+        @DomainObject(entityChangePublishing = org.apache.causeway.applib.annotation.Publishing.ENABLED)
+        class CustomerWithDomainObjectAndAuditingSetToEnabled {
+        }
+
+        @DomainObject(entityChangePublishing = Publishing.ENABLED_FOR_UPDATES_ONLY)
+        class CustomerWithDomainObjectAndEntityChangePublishingSetToEnabledForUpdatesOnly {
+        }
+
+        @Nested
+        public class WhenNotAnnotatedAndDefaultsFromConfiguration {
+
+            @Test
+            void configured_value_set_to_all() {
+                allowingEntityChangePublishingToReturn(DomainObjectConfigOptions.EntityChangePublishingPolicy.ALL);
+                objectScenario(DomainObjectAnnotationFacetFactoryTest.Customer.class, (processClassContext, facetHolder)->{
+                    facetFactory.processEntityChangePublishing(
+                            processClassContext.synthesizeOnType(DomainObject.class), processClassContext);
+
+                    final EntityChangePublishingFacet facet = facetHolder.getFacet(EntityChangePublishingFacet.class);
+                    assertThat(facet, is(notNullValue()));
+                    assertTrue(facet instanceof EntityChangePublishingFacetFromConfiguration);
+                    assertThat(facet.isEnabled(), is(true));
+
+                    assertNoMethodsRemoved();
+                });
+            }
+
+            @Test
+            void configured_value_set_to_none() {
+                allowingEntityChangePublishingToReturn(DomainObjectConfigOptions.EntityChangePublishingPolicy.NONE);
+                objectScenario(DomainObjectAnnotationFacetFactoryTest.Customer.class, (processClassContext, facetHolder)->{
+                    facetFactory.process(processClassContext);
+
+                    final EntityChangePublishingFacet facet = facetHolder.getFacet(EntityChangePublishingFacet.class);
+                    assertNotNull(facet);
+                    assertThat(facet.isEnabled(), is(false));
+
+                    assertNoMethodsRemoved();
+                });
+            }
+
+        }
+
+        @Nested
+        public class WithDomainObjectAnnotationWithAuditingSetToAsConfigured {
+
+            @Test
+            public void configured_value_set_to_all() {
+                allowingEntityChangePublishingToReturn(DomainObjectConfigOptions.EntityChangePublishingPolicy.ALL);
+                objectScenario(CustomerWithDomainObjectAndAuditingSetToAsConfigured.class, (processClassContext, facetHolder)->{
+                    facetFactory.process(processClassContext);
+
+                    final EntityChangePublishingFacet facet = facetHolder.getFacet(EntityChangePublishingFacet.class);
+                    assertNotNull(facet);
+                    assertTrue(facet instanceof EntityChangePublishingFacetForDomainObjectAnnotationAsConfigured);
+                    assertThat(facet.isEnabled(), is(true));
+
+                    assertNoMethodsRemoved();
+                });
+            }
+
+            @Test
+            public void configured_value_set_to_none() {
+                allowingEntityChangePublishingToReturn(DomainObjectConfigOptions.EntityChangePublishingPolicy.NONE);
+                objectScenario(CustomerWithDomainObjectAndAuditingSetToAsConfigured.class, (processClassContext, facetHolder)->{
+                    facetFactory.process(processClassContext);
+
+                    final EntityChangePublishingFacet facet = facetHolder.getFacet(EntityChangePublishingFacet.class);
+                    assertNotNull(facet);
+                    assertThat(facet.isEnabled(), is(false));
+
+                    assertNoMethodsRemoved();
+                });
+            }
+
+        }
+
+        @Nested
+        public class WithDomainObjectAnnotationWithAuditingSetToEnabled {
+
+            @Test
+            public void irrespective_of_configured_value() {
+                allowingEntityChangePublishingToReturn(null);
+                objectScenario(CustomerWithDomainObjectAndAuditingSetToEnabled.class, (processClassContext, facetHolder)->{
+                    facetFactory.process(processClassContext);
+
+                    final Facet facet = facetHolder.getFacet(EntityChangePublishingFacet.class);
+                    assertNotNull(facet);
+                    assertTrue(facet instanceof EntityChangePublishingFacetForDomainObjectAnnotation);
+
+                    assertNoMethodsRemoved();
+                });
+            }
+
+        }
+
+        @Nested
+        public class WithDomainObjectAnnotationWithEntityChangePublishingSetToEnabledForUpdatesOnly {
+
+            @Test
+            public void irrespective_of_configured_value() {
+                allowingEntityChangePublishingToReturn(null);
+                objectScenario(CustomerWithDomainObjectAndEntityChangePublishingSetToEnabledForUpdatesOnly.class, (processClassContext, facetHolder)->{
+                    facetFactory.process(processClassContext);
+
+                    final Facet facet = facetHolder.getFacet(EntityChangePublishingFacet.class);
+                    assertNotNull(facet);
+                    assertTrue(facet instanceof EntityChangePublishingFacetForDomainObjectAnnotation);
+
+                    assertFalse(EntityChangePublishingFacet.isPublishingEnabledForCreate(facetHolder));
+                    assertTrue(EntityChangePublishingFacet.isPublishingEnabledForUpdate(facetHolder));
+                    assertFalse(EntityChangePublishingFacet.isPublishingEnabledForDelete(facetHolder));
+
+                    assertNoMethodsRemoved();
+                });
+            }
+
+        }
+
+        @Nested
+        public class WithDomainObjectAnnotationWithAuditingSetToDisabled {
+
+            @Test
+            public void irrespective_of_configured_value() {
+                allowingEntityChangePublishingToReturn(DomainObjectConfigOptions.EntityChangePublishingPolicy.ALL);
+                objectScenario(CustomerWithDomainObjectAndAuditingSetToDisabled.class, (processClassContext, facetHolder)->{
+                    facetFactory.process(processClassContext);
+
+                    assertFalse(EntityChangePublishingFacet.isPublishingEnabled(facetHolder));
+
+                    assertNoMethodsRemoved();
+                });
+            }
+
+        }
+    }
+
+    @Nested
+    public class AutoComplete {
+
+        class CustomerRepository {
+            public String lookup(final String x) { return null; }
+        }
+
+        class CustomerRepositoryWithDefaultMethodName {
+            public String autoComplete(final String x) { return null; }
+        }
+
+        @DomainObject(autoCompleteRepository = CustomerRepository.class, autoCompleteMethod = "lookup")
+        class CustomerWithDomainObjectAndAutoCompleteRepositoryAndAction {
+        }
+
+        @DomainObject(autoCompleteRepository = CustomerRepositoryWithDefaultMethodName.class)
+        class CustomerWithDomainObjectAndAutoCompleteRepository {
+        }
+
+        @DomainObject
+        class CustomerWithDomainObjectButNoAutoCompleteRepository {
+        }
+
+        @BeforeEach
+        public void setUp() {
+            ignoringConfiguration();
+        }
+
+        @Test
+        public void whenDomainObjectAndAutoCompleteRepositoryAndAction() {
+
+            objectScenario(CustomerWithDomainObjectAndAutoCompleteRepositoryAndAction.class, (processClassContext, facetHolder)->{
+                facetFactory.process(processClassContext);
+
+                final Facet facet = facetHolder.getFacet(AutoCompleteFacet.class);
+                assertNotNull(facet);
+
+                assertTrue(facet instanceof AutoCompleteFacetForDomainObjectAnnotation);
+
+                final AutoCompleteFacetForDomainObjectAnnotation autoCompleteFacet = (AutoCompleteFacetForDomainObjectAnnotation) facet;
+
+                assertThat(CustomerRepository.class.isAssignableFrom(autoCompleteFacet.getRepositoryClass()), is(true));
+                assertThat(autoCompleteFacet.getActionName(), is("lookup"));
+
+                assertNoMethodsRemoved();
+            });
+
+        }
+
+        @Test
+        public void whenDomainObjectAndAutoCompleteRepository() {
+
+            objectScenario(CustomerWithDomainObjectAndAutoCompleteRepository.class, (processClassContext, facetHolder)->{
+                facetFactory.process(processClassContext);
+
+                final Facet facet = facetHolder.getFacet(AutoCompleteFacet.class);
+                assertNotNull(facet);
+
+                assertTrue(facet instanceof AutoCompleteFacetForDomainObjectAnnotation);
+
+                final AutoCompleteFacetForDomainObjectAnnotation autoCompleteFacet = (AutoCompleteFacetForDomainObjectAnnotation) facet;
+
+                assertThat(CustomerRepositoryWithDefaultMethodName.class.isAssignableFrom(autoCompleteFacet.getRepositoryClass()), is(true));
+                assertThat(autoCompleteFacet.getActionName(), is("autoComplete"));
+
+                assertNoMethodsRemoved();
+            });
+        }
+
+        @Test
+        public void whenDomainObjectAnnotationButNoAutoComplete() {
+
+            objectScenario(CustomerWithDomainObjectButNoAutoCompleteRepository.class, (processClassContext, facetHolder)->{
+                facetFactory.process(processClassContext);
+
+                final Facet facet = facetHolder.getFacet(AutoCompleteFacet.class);
+                assertNull(facet);
+
+                assertNoMethodsRemoved();
+            });
+        }
+
+        @Test
+        public void whenNoDomainObjectAnnotation() {
+
+            objectScenario(DomainObjectAnnotationFacetFactoryTest.Customer.class, (processClassContext, facetHolder)->{
+                facetFactory.process(processClassContext);
+
+                final Facet facet = facetHolder.getFacet(AutoCompleteFacet.class);
+                assertNull(facet);
+
+                assertNoMethodsRemoved();
+            });
+        }
+
+    }
+
+    @Nested
+    public class Bounded {
+
+        @DomainObject(bounding = Bounding.BOUNDED)
+        class CustomerWithDomainObjectAndBoundedSetToTrue {
+        }
+
+        @DomainObject(bounding = Bounding.UNBOUNDED)
+        class CustomerWithDomainObjectAndBoundedSetToFalse {
+        }
+
+        @DomainObject
+        class CustomerWithDomainObjectButNoBounded {
+        }
+
+        @BeforeEach
+        public void setUp() {
+            ignoringConfiguration();
+        }
+
+        @Test
+        public void whenDomainObjectAndBoundedSetToTrue() {
+
+            objectScenario(CustomerWithDomainObjectAndBoundedSetToTrue.class, (processClassContext, facetHolder)->{
+                facetFactory.process(processClassContext);
+
+                final Facet facet = facetHolder.getFacet(ChoicesFacet.class);
+                assertNotNull(facet);
+
+                assertTrue(facet instanceof ChoicesFacetForDomainObjectAnnotation);
+
+                assertNoMethodsRemoved();
+
+            });
+        }
+
+        @Test
+        public void whenDomainObjectAndAutoCompleteRepository() {
+
+            objectScenario(CustomerWithDomainObjectAndBoundedSetToFalse.class, (processClassContext, facetHolder)->{
+                facetFactory.process(processClassContext);
+
+                final Facet facet = facetHolder.getFacet(ChoicesFacet.class);
+                assertNull(facet);
+
+                assertNoMethodsRemoved();
+            });
+        }
+
+        @Test
+        public void whenNoDomainObjectAnnotation() {
+
+            objectScenario(DomainObjectAnnotationFacetFactoryTest.Customer.class, (processClassContext, facetHolder)->{
+                facetFactory.process(processClassContext);
+
+                final Facet facet = facetHolder.getFacet(ChoicesFacet.class);
+                assertNull(facet);
+
+                assertNoMethodsRemoved();
+            });
+        }
+    }
+
+
+    @DomainObject(editing = org.apache.causeway.applib.annotation.Editing.AS_CONFIGURED)
+    static class CustomerWithDomainObjectAndEditingSetToAsConfigured {
+    }
+
+    @DomainObject(editing = org.apache.causeway.applib.annotation.Editing.DISABLED)
+    static class CustomerWithDomainObjectAndEditingSetToDisabled {
+    }
+
+    @DomainObject(editing = org.apache.causeway.applib.annotation.Editing.ENABLED)
+    static class CustomerWithDomainObjectAndEditingSetToEnabled {
+    }
+
+    @Nested
+    public class Editing {
+
+        @Nested
+        public class WhenNotAnnotatedAndDefaultsFromConfiguration {
+
+            @Test
+            public void configured_value_set_to_true() {
+                allowingObjectsEditingToReturn(DomainObjectConfigOptions.EditingObjectsConfiguration.TRUE);
+
+                objectScenario(DomainObjectAnnotationFacetFactoryTest.Customer.class, (processClassContext, facetHolder)->{
+                    facetFactory.process(processClassContext);
+
+                    final Facet facet = facetHolder.getFacet(ImmutableFacet.class);
+                    assertNull(facet);
+
+                    assertNoMethodsRemoved();
+
+                });
+            }
+
+            @Test
+            public void configured_value_set_to_false() {
+                allowingObjectsEditingToReturn(DomainObjectConfigOptions.EditingObjectsConfiguration.FALSE);
+
+                objectScenario(DomainObjectAnnotationFacetFactoryTest.Customer.class, (processClassContext, facetHolder)->{
+                    facetFactory.process(processClassContext);
+
+                    final Facet facet = facetHolder.getFacet(ImmutableFacet.class);
+                    assertNotNull(facet);
+                    assertTrue(facet instanceof ImmutableFacetFromConfiguration);
+
+                    assertNoMethodsRemoved();
+                });
+            }
+
+            @Test
+            public void configured_value_set_to_defaults() {
+                ignoringConfiguration();
+                objectScenario(DomainObjectAnnotationFacetFactoryTest.Customer.class, (processClassContext, facetHolder)->{
+                    facetFactory.process(processClassContext);
+
+                    final Facet facet = facetHolder.getFacet(ImmutableFacet.class);
+                    assertNotNull(facet); // default is now non-editable
+                    assertTrue(facet instanceof ImmutableFacetFromConfiguration);
+
+                    assertNoMethodsRemoved();
+                });
+            }
+        }
+
+        @Nested
+        public class WithDomainObjectAnnotationWithEditingSetToAsConfigured {
+
+            @Test
+            public void configured_value_set_to_true() {
+                allowingObjectsEditingToReturn(DomainObjectConfigOptions.EditingObjectsConfiguration.TRUE);
+                objectScenario(CustomerWithDomainObjectAndEditingSetToAsConfigured.class, (processClassContext, facetHolder)->{
+                    facetFactory.process(processClassContext);
+
+                    final Facet facet = facetHolder.getFacet(ImmutableFacet.class);
+                    assertNull(facet);
+
+                    assertNoMethodsRemoved();
+                });
+            }
+
+            @Test
+            public void configured_value_set_to_false() {
+                allowingObjectsEditingToReturn(DomainObjectConfigOptions.EditingObjectsConfiguration.FALSE);
+                objectScenario(CustomerWithDomainObjectAndEditingSetToAsConfigured.class, (processClassContext, facetHolder)->{
+                    facetFactory.process(processClassContext);
+
+                    final Facet facet = facetHolder.getFacet(ImmutableFacet.class);
+                    assertNotNull(facet);
+                    assertTrue(facet instanceof ImmutableFacetForDomainObjectAnnotation);
+
+                    assertNoMethodsRemoved();
+                });
+            }
+
+            @Test
+            public void configured_value_set_to_defaults() {
+                ignoringConfiguration();
+                objectScenario(CustomerWithDomainObjectAndEditingSetToAsConfigured.class, (processClassContext, facetHolder)->{
+                    facetFactory.process(processClassContext);
+
+                    final Facet facet = facetHolder.getFacet(ImmutableFacet.class);
+                    assertNotNull(facet); // default is now non-editable
+                    assertTrue(facet instanceof ImmutableFacetForDomainObjectAnnotationAsConfigured);
+
+                    assertNoMethodsRemoved();
+                });
+            }
+        }
+
+        @Nested
+        public class WithDomainObjectAnnotationWithEditingSetToEnabled {
+
+            @Test
+            public void irrespective_of_configured_value() {
+                allowingObjectsEditingToReturn(DomainObjectConfigOptions.EditingObjectsConfiguration.FALSE);
+                objectScenario(CustomerWithDomainObjectAndEditingSetToEnabled.class, (processClassContext, facetHolder)->{
+                    facetFactory.process(processClassContext);
+
+                    final ImmutableFacet facet = facetHolder.getFacet(ImmutableFacet.class);
+                    assertNull(facet);
+
+                    assertNoMethodsRemoved();
+                });
+            }
+        }
+
+        @Nested
+        public class WithDomainObjectAnnotationWithEditingSetToDisabled {
+
+            @Test
+            public void irrespective_of_configured_value() {
+                allowingObjectsEditingToReturn(DomainObjectConfigOptions.EditingObjectsConfiguration.TRUE);
+                objectScenario(CustomerWithDomainObjectAndEditingSetToDisabled.class, (processClassContext, facetHolder)->{
+                    facetFactory.process(processClassContext);
+
+                    final Facet facet = facetHolder.getFacet(ImmutableFacet.class);
+                    assertNotNull(facet);
+                    assertTrue(facet instanceof ImmutableFacetForDomainObjectAnnotation);
+
+                    assertNoMethodsRemoved();
+                });
+            }
+        }
+    }
+
+    @Named("CUS")
+    @DomainObject
+    static class LogicalTypeNameCustomerWithDomainObjectAndObjectTypeSet {
+    }
+
+    @DomainObject
+    static class LogicalTypeNameCustomerWithDomainObjectButNoObjectType {
+    }
+
+    @Nested
+    public class LogicalTypeName {
+
+        @BeforeEach
+        public void setUp() {
+            ignoringConfiguration();
+        }
+
+        @Test
+        public void whenDomainObjectAndObjectTypeSetToTrue() {
+            assertThat(LogicalType.infer(LogicalTypeNameCustomerWithDomainObjectAndObjectTypeSet.class).logicalName(),
+                    is("CUS"));
+            assertNoMethodsRemoved();
+        }
+
+        @Test
+        public void whenDomainObjectAndObjectTypeNotSet() {
+
+            objectScenario(LogicalTypeNameCustomerWithDomainObjectButNoObjectType.class, (processClassContext, facetHolder)->{
+                facetFactory.process(processClassContext);
+
+                final Facet facet = facetHolder.getFacet(AliasedFacet.class);
+                assertNull(facet);
+
+                assertNoMethodsRemoved();
+            });
+        }
+
+        @Test
+        public void whenNoDomainObjectAnnotation() {
+
+            objectScenario(DomainObjectAnnotationFacetFactoryTest.Customer.class, (processClassContext, facetHolder)->{
+                facetFactory.process(processClassContext);
+
+                final Facet facet = facetHolder.getFacet(AliasedFacet.class);
+                assertNull(facet);
+
+                assertNoMethodsRemoved();
+            });
+        }
+
+    }
+
+    @DomainObject(nature = org.apache.causeway.applib.annotation.Nature.ENTITY)
+    static class CustomerWithDomainObjectAndNatureSetToJdoEntity {
+    }
+
+    @DomainObject(nature = org.apache.causeway.applib.annotation.Nature.NOT_SPECIFIED)
+    static class CustomerWithDomainObjectAndNatureSetToNotSpecified {
+    }
+
+    @DomainObject(nature = org.apache.causeway.applib.annotation.Nature.VIEW_MODEL)
+    static class CustomerWithDomainObjectAndNatureSetToViewModel {
+    }
+
+    @DomainObject
+    static class CustomerWithDomainObjectButNoNature {
+    }
+
+    @Nested
+    public class Nature  {
+
+        @BeforeEach
+        public void setUp() {
+            ignoringConfiguration();
+        }
+
+        @Test
+        public void whenDomainObjectAndNatureSetToJdoEntity() {
+
+            objectScenario(CustomerWithDomainObjectAndNatureSetToJdoEntity.class, (processClassContext, facetHolder)->{
+                facetFactory.process(processClassContext);
+
+                final Facet facet = facetHolder.getFacet(ViewModelFacet.class);
+                assertNull(facet);
+
+                assertNoMethodsRemoved();
+            });
+        }
+
+        @Test
+        public void whenDomainObjectAndNatureSetToNotSpecified() {
+
+            objectScenario(CustomerWithDomainObjectAndNatureSetToNotSpecified.class, (processClassContext, facetHolder)->{
+                facetFactory.process(processClassContext);
+
+                final Facet facet = facetHolder.getFacet(ViewModelFacet.class);
+                assertNull(facet);
+
+                assertNoMethodsRemoved();
+            });
+        }
+
+        @Test
+        public void whenDomainObjectAndNatureSetToViewModel() {
+
+            objectScenario(CustomerWithDomainObjectAndNatureSetToViewModel.class, (processClassContext, facetHolder)->{
+                facetFactory.process(processClassContext);
+
+                final Facet facet = facetHolder.getFacet(ViewModelFacet.class);
+                assertNotNull(facet);
+
+                assertTrue(facet instanceof ViewModelFacetForDomainObjectAnnotation);
+
+                assertNoMethodsRemoved();
+            });
+
+        }
+
+        @Test
+        public void whenNoDomainObjectAnnotation() {
+
+            objectScenario(DomainObjectAnnotationFacetFactoryTest.Customer.class, (processClassContext, facetHolder)->{
+                facetFactory.process(processClassContext);
+
+                final Facet facet = facetHolder.getFacet(ViewModelFacet.class);
+                assertNull(facet);
+
+                assertNoMethodsRemoved();
+            });
+
+        }
+
+    }
+
+    @Named("object.name")
+    @DomainObject(aliased = {"object.name", "object.alias"})
+    static class AliasDomainObjectWithAliases {
+    }
+
+    @Named("service.name")
+    @DomainService(aliased = {"service.name", "service.alias"})
+    static class AliasDomainServiceWithAliases {
+    }
+
+    @Nested
+    public class Alias {
+        DomainObjectAnnotationFacetFactory facetFactory;
+
+        @Test
+        public void testValidationDomainObjectWithAliasesConfigured() {
+            metaModelContext = MetaModelContext_forTesting.builder()
+                .testPropertyValues(TestPropertyValues.of("causeway.core.metaModel.validator.allowLogicalTypeNameAsAlias=true"))
+                .refiners(Can.of(__->facetFactory))
+                .build();
+            facetFactory = new DomainObjectAnnotationFacetFactory(getMetaModelContext());
+            ((MetaModelContext_forTesting) getMetaModelContext()).getProgrammingModel();//kicks off the programming model factory
+
+            getMetaModelContext().getSpecificationLoader().specForTypeElseFail(AliasDomainObjectWithAliases.class);
+            ValidationFailures validationFailures = getMetaModelContext().getSpecificationLoader().getOrAssessValidationResult();
+            assertFalse(validationFailures.hasFailures());
+        }
+
+        @Test
+        public void testValidationDomainServiceWithAliasesConfigured() {
+            metaModelContext = MetaModelContext_forTesting.builder()
+                .testPropertyValues(TestPropertyValues.of("causeway.core.metaModel.validator.allowLogicalTypeNameAsAlias=true"))
+                .refiners(Can.of(__->facetFactory))
+                .build();
+            facetFactory = new DomainObjectAnnotationFacetFactory(getMetaModelContext());
+            ((MetaModelContext_forTesting) getMetaModelContext()).getProgrammingModel();//kicks off the programming model factory
+
+            getMetaModelContext().getSpecificationLoader().specForTypeElseFail(AliasDomainServiceWithAliases.class);
+            ValidationFailures validationFailures = getMetaModelContext().getSpecificationLoader().getOrAssessValidationResult();
+            assertFalse(validationFailures.hasFailures());
+        }
+        @Test
+        public void testValidationDomainObjectWithAliasesDefault() {
+            metaModelContext = MetaModelContext_forTesting.builder()
+                .refiners(Can.of(__->facetFactory))
+                .build();
+            facetFactory = new DomainObjectAnnotationFacetFactory(getMetaModelContext());
+            ((MetaModelContext_forTesting) getMetaModelContext()).getProgrammingModel();//kicks off the programming model factory
+
+            getMetaModelContext().getSpecificationLoader().specForTypeElseFail(AliasDomainObjectWithAliases.class);
+            ValidationFailures validationFailures = getMetaModelContext().getSpecificationLoader().getOrAssessValidationResult();
+            assertTrue(validationFailures.hasFailures());
+        }
+
+        @Test
+        public void testValidationDomainServiceWithAliasesDefault() {
+            metaModelContext = MetaModelContext_forTesting.builder()
+                .refiners(Can.of(__->facetFactory))
+                .build();
+            facetFactory = new DomainObjectAnnotationFacetFactory(getMetaModelContext());
+            ((MetaModelContext_forTesting) getMetaModelContext()).getProgrammingModel();//kicks off the programming model factory
+
+            getMetaModelContext().getSpecificationLoader().specForTypeElseFail(AliasDomainServiceWithAliases.class);
+            ValidationFailures validationFailures = getMetaModelContext().getSpecificationLoader().getOrAssessValidationResult();
+            assertTrue(validationFailures.hasFailures());
+        }
+    }
+}
diff --git a/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/facets/properties/property/PropertyAnnotationFacetFactoryTest.java b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/facets/properties/property/PropertyAnnotationFacetFactoryTest.java
new file mode 100644
index 00000000000..17bbf6dab49
--- /dev/null
+++ b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/facets/properties/property/PropertyAnnotationFacetFactoryTest.java
@@ -0,0 +1,885 @@
+/*
+ *  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.causeway.core.metamodel.facets.properties.property;
+
+import java.util.regex.Pattern;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.TestInstance.Lifecycle;
+import org.mockito.Mockito;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.causeway.applib.annotation.DomainObject;
+import org.apache.causeway.applib.annotation.MemberSupport;
+import org.apache.causeway.applib.annotation.Nature;
+import org.apache.causeway.applib.annotation.Optionality;
+import org.apache.causeway.applib.annotation.Property;
+import org.apache.causeway.applib.annotation.Publishing;
+import org.apache.causeway.applib.annotation.Snapshot;
+import org.apache.causeway.applib.annotation.Where;
+import org.apache.causeway.applib.events.domain.PropertyDomainEvent;
+import org.apache.causeway.applib.spec.Specification;
+import org.apache.causeway.core.metamodel.commons.matchers.CausewayMatchers;
+import org.apache.causeway.core.metamodel.consent.Consent.VetoReason;
+import org.apache.causeway.core.metamodel.consent.InteractionInitiatedBy;
+import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
+import org.apache.causeway.core.metamodel.facetapi.FacetUtil;
+import org.apache.causeway.core.metamodel.facets.DomainEventFacetAbstract.EventTypeOrigin;
+import org.apache.causeway.core.metamodel.facets.FacetFactory;
+import org.apache.causeway.core.metamodel.facets.FacetFactoryTestAbstract;
+import org.apache.causeway.core.metamodel.facets.FacetedMethod;
+import org.apache.causeway.core.metamodel.facets.members.disabled.DisabledFacet;
+import org.apache.causeway.core.metamodel.facets.objectvalue.mandatory.MandatoryFacet;
+import org.apache.causeway.core.metamodel.facets.objectvalue.maxlen.MaxLengthFacet;
+import org.apache.causeway.core.metamodel.facets.objectvalue.mustsatisfyspec.MustSatisfySpecificationFacet;
+import org.apache.causeway.core.metamodel.facets.objectvalue.regex.RegExFacet;
+import org.apache.causeway.core.metamodel.facets.propcoll.accessor.PropertyOrCollectionAccessorFacetAbstract;
+import org.apache.causeway.core.metamodel.facets.propcoll.memserexcl.SnapshotExcludeFacet;
+import org.apache.causeway.core.metamodel.facets.properties.property.disabled.DisabledFacetForPropertyAnnotation;
+import org.apache.causeway.core.metamodel.facets.properties.property.entitychangepublishing.EntityPropertyChangePublishingPolicyFacet;
+import org.apache.causeway.core.metamodel.facets.properties.property.mandatory.MandatoryFacetForPropertyAnnotation;
+import org.apache.causeway.core.metamodel.facets.properties.property.maxlength.MaxLengthFacetForPropertyAnnotation;
+import org.apache.causeway.core.metamodel.facets.properties.property.modify.PropertyDomainEventFacet;
+import org.apache.causeway.core.metamodel.facets.properties.property.modify.PropertyModifyFacetAbstract;
+import org.apache.causeway.core.metamodel.facets.properties.property.mustsatisfy.MustSatisfySpecificationFacetForPropertyAnnotation;
+import org.apache.causeway.core.metamodel.facets.properties.property.regex.RegExFacetForPropertyAnnotation;
+import org.apache.causeway.core.metamodel.facets.properties.property.snapshot.SnapshotExcludeFacetForPropertyAnnotation;
+import org.apache.causeway.core.metamodel.facets.properties.update.clear.PropertyClearFacet;
+import org.apache.causeway.core.metamodel.facets.properties.update.clear.PropertyClearFacetAbstract;
+import org.apache.causeway.core.metamodel.facets.properties.update.modify.PropertySetterFacet;
+import org.apache.causeway.core.metamodel.facets.properties.update.modify.PropertySetterFacetAbstract;
+import org.apache.causeway.core.metamodel.object.ManagedObject;
+import org.apache.causeway.core.metamodel.postprocessors.members.SynthesizeDomainEventsForMixinPostProcessor;
+import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
+import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+
+class PropertyAnnotationFacetFactoryTest extends FacetFactoryTestAbstract {
+
+    PropertyAnnotationFacetFactory facetFactory;
+
+    private static void processDomainEvent(
+            final PropertyAnnotationFacetFactory facetFactory, final FacetFactory.ProcessMethodContext processMethodContext) {
+        var propertyIfAny = facetFactory.propertyIfAny(processMethodContext);
+        facetFactory.processDomainEvent(processMethodContext, propertyIfAny);
+    }
+
+    private static void processOptional(
+            final PropertyAnnotationFacetFactory facetFactory, final FacetFactory.ProcessMethodContext processMethodContext) {
+        var propertyIfAny = facetFactory.propertyIfAny(processMethodContext);
+        facetFactory.processOptional(processMethodContext, propertyIfAny);
+    }
+
+    private static void processRegEx(
+            final PropertyAnnotationFacetFactory facetFactory, final FacetFactory.ProcessMethodContext processMethodContext) {
+        var propertyIfAny = facetFactory.propertyIfAny(processMethodContext);
+        facetFactory.processRegEx(processMethodContext, propertyIfAny);
+    }
+
+    private static void processEditing(
+            final PropertyAnnotationFacetFactory facetFactory, final FacetFactory.ProcessMethodContext processMethodContext) {
+        var propertyIfAny = facetFactory.propertyIfAny(processMethodContext);
+        facetFactory.processEditing(processMethodContext, propertyIfAny);
+    }
+
+    private static void processMaxLength(
+            final PropertyAnnotationFacetFactory facetFactory, final FacetFactory.ProcessMethodContext processMethodContext) {
+        var propertyIfAny = facetFactory.propertyIfAny(processMethodContext);
+        facetFactory.processMaxLength(processMethodContext, propertyIfAny);
+    }
+
+    private static void processMustSatisfy(
+            final PropertyAnnotationFacetFactory facetFactory, final FacetFactory.ProcessMethodContext processMethodContext) {
+        var propertyIfAny = facetFactory.propertyIfAny(processMethodContext);
+        facetFactory.processMustSatisfy(processMethodContext, propertyIfAny);
+    }
+
+    private static void processSnapshot(
+            final PropertyAnnotationFacetFactory facetFactory, final FacetFactory.ProcessMethodContext processMethodContext) {
+        var propertyIfAny = facetFactory.propertyIfAny(processMethodContext);
+        facetFactory.processSnapshot(processMethodContext, propertyIfAny);
+    }
+
+    private static void processEntityPropertyChangePublishing(
+            final PropertyAnnotationFacetFactory facetFactory, final FacetFactory.ProcessMethodContext processMethodContext) {
+        var propertyIfAny = facetFactory.propertyIfAny(processMethodContext);
+        facetFactory.processEntityPropertyChangePublishing(processMethodContext, propertyIfAny);
+    }
+
+    @BeforeEach
+    final void setUp() throws Exception {
+        facetFactory = new PropertyAnnotationFacetFactory(getMetaModelContext());
+    }
+
+    @AfterEach
+    final void tearDown() throws Exception {
+        facetFactory = null;
+    }
+
+    @TestInstance(Lifecycle.PER_CLASS)
+    static class Modify extends PropertyAnnotationFacetFactoryTest {
+
+        private void addGetterFacet(final FacetHolder holder) {
+            var mockOnType = Mockito.mock(ObjectSpecification.class);
+            FacetUtil.addFacet(new PropertyOrCollectionAccessorFacetAbstract(mockOnType, holder) {
+                @Override
+                public Object getAssociationValueAsPojo(
+                        final ManagedObject inObject,
+                        final InteractionInitiatedBy interactionInitiatedBy) {
+                    return null;
+                }
+            });
+        }
+
+        private void addSetterFacet(final FacetHolder holder) {
+            FacetUtil.addFacet(new PropertySetterFacetAbstract(holder) {
+                @Override
+                public ManagedObject setProperty(
+                        final OneToOneAssociation owningAssociation,
+                        final ManagedObject inObject,
+                        final ManagedObject value,
+                        final InteractionInitiatedBy interactionInitiatedBy) {
+                    return inObject;
+                }
+            });
+        }
+
+        private void addClearFacet(final FacetHolder holder) {
+            FacetUtil.addFacet(new PropertyClearFacetAbstract(holder) {
+                @Override
+                public ManagedObject clearProperty(
+                        final OneToOneAssociation owningProperty,
+                        final ManagedObject targetAdapter,
+                        final InteractionInitiatedBy interactionInitiatedBy) {
+                    return targetAdapter;
+                }
+            });
+        }
+
+        private void assertHasPropertyDomainEventFacet(
+                final FacetedMethod facetedMethod,
+                final EventTypeOrigin eventTypeOrigin,
+                final Class<? extends PropertyDomainEvent<?,?>> eventType) {
+            var domainEventFacet = facetedMethod.lookupFacet(PropertyDomainEventFacet.class).orElseThrow();
+            assertEquals(eventTypeOrigin, domainEventFacet.getEventTypeOrigin());
+            assertThat(domainEventFacet.getEventType(), CausewayMatchers.classEqualTo(eventType));
+
+            if(facetedMethod.methodFacade().getName().equals("prop"))
+                return; // skip further checks, when in a mixed-in scenario
+
+            // then
+            var setterFacet = facetedMethod.getFacet(PropertySetterFacet.class);
+            assertNotNull(setterFacet);
+            assertTrue(setterFacet instanceof PropertyModifyFacetAbstract, "unexpected facet: " + setterFacet);
+            final PropertyModifyFacetAbstract setterFacetImpl = (PropertyModifyFacetAbstract) setterFacet;
+            assertEquals(eventTypeOrigin, setterFacetImpl.getEventTypeOrigin());
+            assertThat(setterFacetImpl.getEventType(), CausewayMatchers.classEqualTo(eventType));
+
+            // then
+            var clearFacet = facetedMethod.getFacet(PropertyClearFacet.class);
+            assertNotNull(clearFacet);
+            assertTrue(clearFacet instanceof PropertyModifyFacetAbstract);
+            final PropertyModifyFacetAbstract clearFacetImpl = (PropertyModifyFacetAbstract) clearFacet;
+            assertEquals(eventTypeOrigin, setterFacetImpl.getEventTypeOrigin());
+            assertThat(clearFacetImpl.getEventType(), CausewayMatchers.classEqualTo(eventType));
+        }
+
+        @Test
+        void withPropertyDomainEvent_fallingBackToDefault() {
+
+            class Customer {
+                @Getter @Setter private String name;
+            }
+
+            // given
+            propertyScenario(Customer.class, "name", (processMethodContext, facetHolder, facetedMethod)->{
+                addGetterFacet(facetedMethod);
+                addSetterFacet(facetedMethod);
+                addClearFacet(facetedMethod);
+
+                // when
+                processDomainEvent(facetFactory, processMethodContext);
+
+                // then
+                assertHasPropertyDomainEventFacet(facetedMethod,
+                        EventTypeOrigin.DEFAULT, PropertyDomainEvent.Default.class);
+            });
+        }
+
+        @Test
+        void withPropertyDomainEvent_annotatedOnMethod() {
+
+            class Customer {
+                class NamedChangedDomainEvent extends PropertyDomainEvent<Customer, String> {}
+                @Property(domainEvent = NamedChangedDomainEvent.class)
+                @Getter @Setter private String name;
+            }
+
+            // given
+            propertyScenario(Customer.class, "name", (processMethodContext, facetHolder, facetedMethod)->{
+                addGetterFacet(facetedMethod);
+                addSetterFacet(facetedMethod);
+                addClearFacet(facetedMethod);
+
+                // when
+                processDomainEvent(facetFactory, processMethodContext);
+
+                // then
+                assertHasPropertyDomainEventFacet(facetedMethod,
+                        EventTypeOrigin.ANNOTATED_MEMBER, Customer.NamedChangedDomainEvent.class);
+            });
+        }
+
+        @Test
+        void withPropertyDomainEvent_annotatedOnType() {
+
+            @DomainObject(propertyDomainEvent = Customer.NamedChangedDomainEvent.class)
+            class Customer {
+                class NamedChangedDomainEvent extends PropertyDomainEvent<Customer, String> {}
+                @Property
+                @Getter @Setter private String name;
+            }
+
+            // given
+            propertyScenario(Customer.class, "name", (processMethodContext, facetHolder, facetedMethod)->{
+                addGetterFacet(facetedMethod);
+                addSetterFacet(facetedMethod);
+                addClearFacet(facetedMethod);
+
+                // when
+                processDomainEvent(facetFactory, processMethodContext);
+
+                // then
+                assertHasPropertyDomainEventFacet(facetedMethod,
+                        EventTypeOrigin.ANNOTATED_OBJECT, Customer.NamedChangedDomainEvent.class);
+            });
+        }
+
+        @Test
+        void withPropertyDomainEvent_annotatedOnTypeAndMethod() {
+
+            @DomainObject(propertyDomainEvent = Customer.NamedChangedDomainEvent1.class)
+            class Customer {
+                class NamedChangedDomainEvent1 extends PropertyDomainEvent<Customer, String> {}
+                class NamedChangedDomainEvent2 extends PropertyDomainEvent<Customer, String> {}
+                @Property(domainEvent = NamedChangedDomainEvent2.class)
+                @Getter @Setter private String name;
+            }
+
+            // given
+            propertyScenario(Customer.class, "name", (processMethodContext, facetHolder, facetedMethod)->{
+                addGetterFacet(facetedMethod);
+                addSetterFacet(facetedMethod);
+                addClearFacet(facetedMethod);
+
+                // when
+                processDomainEvent(facetFactory, processMethodContext);
+
+                // then - the property annotation should win
+                assertHasPropertyDomainEventFacet(facetedMethod,
+                        EventTypeOrigin.ANNOTATED_MEMBER, Customer.NamedChangedDomainEvent2.class);
+            });
+        }
+
+        @Test
+        void withPropertyDomainEvent_mixedIn_annotatedOnMethod() {
+            var postProcessor = new SynthesizeDomainEventsForMixinPostProcessor(getMetaModelContext());
+
+            // given
+            class Customer {
+                class NamedChangedDomainEvent extends PropertyDomainEvent<Customer, String> {}
+            }
+            @DomainObject(nature=Nature.MIXIN, mixinMethod = "prop")
+            @RequiredArgsConstructor
+            @SuppressWarnings("unused")
+            class Customer_name {
+                final Customer mixee;
+                @Property(domainEvent = Customer.NamedChangedDomainEvent.class)
+                public String prop() { return "mixed-in name"; }
+            }
+
+            propertyScenarioMixedIn(Customer.class, Customer_name.class,
+                    (processMethodContext, mixeeSpec, facetedMethod, mixedInProp)->{
+
+                // when
+                processDomainEvent(facetFactory, processMethodContext);
+                postProcessor.postProcessProperty(mixeeSpec, mixedInProp);
+
+                // then
+                assertHasPropertyDomainEventFacet(facetedMethod,
+                        EventTypeOrigin.ANNOTATED_MEMBER, Customer.NamedChangedDomainEvent.class);
+
+            });
+        }
+
+        @Test
+        void withPropertyDomainEvent_mixedIn_annotatedOnMixedInType() {
+            var postProcessor = new SynthesizeDomainEventsForMixinPostProcessor(getMetaModelContext());
+
+            // given
+            class Customer {
+                class NamedChangedDomainEvent extends PropertyDomainEvent<Customer, String> {}
+            }
+            @Property(domainEvent = Customer.NamedChangedDomainEvent.class)
+            @RequiredArgsConstructor
+            @SuppressWarnings("unused")
+            class Customer_name {
+                final Customer mixee;
+                @MemberSupport
+                public String prop() { return "mixed-in name"; }
+            }
+
+            propertyScenarioMixedIn(Customer.class, Customer_name.class,
+                    (processMethodContext, mixeeSpec, facetedMethod, mixedInProp)->{
+
+                // when
+                processDomainEvent(facetFactory, processMethodContext);
+                postProcessor.postProcessProperty(mixeeSpec, mixedInProp);
+
+                // then
+                assertHasPropertyDomainEventFacet(facetedMethod,
+                        EventTypeOrigin.ANNOTATED_MEMBER, Customer.NamedChangedDomainEvent.class);
+
+            });
+        }
+
+        @Test
+        void withPropertyDomainEvent_mixedIn_annotatedOnMixeeType() {
+            var postProcessor = new SynthesizeDomainEventsForMixinPostProcessor(getMetaModelContext());
+
+            // given
+            @DomainObject(propertyDomainEvent = Customer.NamedChangedDomainEvent.class)
+            class Customer {
+                class NamedChangedDomainEvent extends PropertyDomainEvent<Customer, String> {}
+            }
+            @Property
+            @RequiredArgsConstructor
+            @SuppressWarnings("unused")
+            class Customer_name {
+                final Customer mixee;
+                @MemberSupport
+                public String prop() { return "mixed-in name"; }
+            }
+
+            propertyScenarioMixedIn(Customer.class, Customer_name.class,
+                    (processMethodContext, mixeeSpec, facetedMethod, mixedInProp)->{
+
+                // when
+                processDomainEvent(facetFactory, processMethodContext);
+                postProcessor.postProcessProperty(mixeeSpec, mixedInProp);
+
+                // then
+                assertHasPropertyDomainEventFacet(facetedMethod,
+                        EventTypeOrigin.ANNOTATED_OBJECT, Customer.NamedChangedDomainEvent.class);
+
+            });
+        }
+
+        @Test
+        void withPropertyDomainEvent_mixedIn_annotatedOnMixeeAndMixedInType() {
+            var postProcessor = new SynthesizeDomainEventsForMixinPostProcessor(getMetaModelContext());
+
+            // given
+            @DomainObject(propertyDomainEvent = Customer.NamedChangedDomainEvent1.class)
+            class Customer {
+                class NamedChangedDomainEvent1 extends PropertyDomainEvent<Customer, String> {}
+                class NamedChangedDomainEvent2 extends PropertyDomainEvent<Customer, String> {}
+            }
+            @Property(domainEvent = Customer.NamedChangedDomainEvent2.class)
+            @RequiredArgsConstructor
+            @SuppressWarnings("unused")
+            class Customer_name {
+                final Customer mixee;
+                @MemberSupport
+                public String prop() { return "mixed-in name"; }
+            }
+
+            propertyScenarioMixedIn(Customer.class, Customer_name.class,
+                    (processMethodContext, mixeeSpec, facetedMethod, mixedInProp)->{
+
+                // when
+                processDomainEvent(facetFactory, processMethodContext);
+                postProcessor.postProcessProperty(mixeeSpec, mixedInProp);
+
+                // then - the mixed-in annotation should win
+                assertHasPropertyDomainEventFacet(facetedMethod,
+                        EventTypeOrigin.ANNOTATED_MEMBER, Customer.NamedChangedDomainEvent2.class);
+
+            });
+        }
+
+        @Test
+        void withPropertyDomainEvent_mixedIn_annotatedOnMixeeTypeAndMixedInMethod() {
+            var postProcessor = new SynthesizeDomainEventsForMixinPostProcessor(getMetaModelContext());
+
+            // given
+            @DomainObject(propertyDomainEvent = Customer.NamedChangedDomainEvent1.class)
+            class Customer {
+                class NamedChangedDomainEvent1 extends PropertyDomainEvent<Customer, String> {}
+                class NamedChangedDomainEvent2 extends PropertyDomainEvent<Customer, String> {}
+            }
+            @DomainObject(nature=Nature.MIXIN, mixinMethod = "prop")
+            @RequiredArgsConstructor
+            @SuppressWarnings("unused")
+            class Customer_name {
+                final Customer mixee;
+                @Property(domainEvent = Customer.NamedChangedDomainEvent2.class)
+                public String prop() { return "mixed-in name"; }
+            }
+
+            propertyScenarioMixedIn(Customer.class, Customer_name.class,
+                    (processMethodContext, mixeeSpec, facetedMethod, mixedInProp)->{
+
+                // when
+                processDomainEvent(facetFactory, processMethodContext);
+                postProcessor.postProcessProperty(mixeeSpec, mixedInProp);
+
+                // then - the mixed-in annotation should win
+                assertHasPropertyDomainEventFacet(facetedMethod,
+                        EventTypeOrigin.ANNOTATED_MEMBER, Customer.NamedChangedDomainEvent2.class);
+
+            });
+        }
+
+    }
+
+    @TestInstance(Lifecycle.PER_CLASS)
+    static class Editing extends PropertyAnnotationFacetFactoryTest {
+
+        @Test
+        void withAnnotationOnGetter() {
+            @SuppressWarnings("unused")
+            class Customer {
+                @Property(
+                        editing = org.apache.causeway.applib.annotation.Editing.DISABLED,
+                        editingDisabledReason = "you cannot edit the name property")
+                public String getName() { return null; }
+                public void setName(final String name) {}
+            }
+
+            assertDisabledFacetOn(Customer.class, "name",
+                    "you cannot edit the name property");
+        }
+
+        @Test
+        void withAnnotationOnField() {
+
+            class Customer {
+                @Property(
+                        editing = org.apache.causeway.applib.annotation.Editing.DISABLED,
+                        editingDisabledReason = "you cannot edit the name property")
+                @Getter @Setter
+                private String name;
+            }
+
+            assertDisabledFacetOn(Customer.class, "name",
+                    "you cannot edit the name property");
+        }
+
+        @Test
+        void withAnnotationOnBooleanGetter() {
+            @SuppressWarnings("unused")
+            class Customer {
+                @Property(
+                        editing = org.apache.causeway.applib.annotation.Editing.DISABLED,
+                        editingDisabledReason = "you cannot edit the subscribed property"
+                        )
+                public boolean isSubscribed() { return true; }
+                public void setSubscribed(final boolean b) {}
+            }
+
+            assertDisabledFacetOn(Customer.class, "subscribed",
+                    "you cannot edit the subscribed property");
+        }
+
+        @Test
+        void withAnnotationOnBooleanField() {
+
+            class Customer {
+                @Property(
+                        editing = org.apache.causeway.applib.annotation.Editing.DISABLED,
+                        editingDisabledReason = "you cannot edit the subscribed property"
+                        )
+                @Getter @Setter
+                private boolean subscribed;
+            }
+
+            assertDisabledFacetOn(Customer.class, "subscribed",
+                    "you cannot edit the subscribed property");
+        }
+
+        // -- SPECIAL SCENARIO CAUSEWAY-2963
+
+        static interface PrimitiveBooleanHolder {
+            @Property(
+                    editing = org.apache.causeway.applib.annotation.Editing.DISABLED,
+                    editingDisabledReason = "a")
+            boolean isReadWriteProperty();
+            void setReadWriteProperty(boolean c);
+        }
+
+        static class PrimitiveBooleanEntity implements PrimitiveBooleanHolder {
+            @Property(
+                    editing = org.apache.causeway.applib.annotation.Editing.DISABLED,
+                    editingDisabledReason = "b")
+            @Getter @Setter
+            private boolean readWriteProperty;
+        }
+
+        @Test
+        void recognizeAnnotationOnPrimitiveBoolean() {
+            assertDisabledFacetOn(PrimitiveBooleanEntity.class, "readWriteProperty", "b");
+        }
+
+        // -- HELPER
+
+        private void assertDisabledFacetOn(final Class<?> declaringClass, final String propertyName, final String expectedDisabledReason) {
+
+            // given
+            propertyScenario(declaringClass, propertyName, (processMethodContext, facetHolder, facetedMethod)->{
+                // when
+                processEditing(facetFactory, processMethodContext);
+                // then
+                var disabledFacet = facetedMethod.getFacet(DisabledFacet.class);
+                assertNotNull(disabledFacet);
+                assertTrue(disabledFacet instanceof DisabledFacetForPropertyAnnotation);
+                var disabledFacet2 = (DisabledFacetForPropertyAnnotation) disabledFacet;
+                assertThat(disabledFacet.where(), is(Where.EVERYWHERE));
+                assertThat(disabledFacet2.disabledReason(null).map(VetoReason::string).orElse(null), is(expectedDisabledReason));
+            });
+        }
+
+    }
+
+    @TestInstance(Lifecycle.PER_CLASS)
+    static class MaxLength extends PropertyAnnotationFacetFactoryTest {
+
+        @Test
+        void withAnnotation() {
+
+            class Customer {
+                @Property(maxLength = 30)
+                @Getter @Setter private String name;
+            }
+
+            // given
+            propertyScenario(Customer.class, "name", (processMethodContext, facetHolder, facetedMethod)->{
+                // when
+                processMaxLength(facetFactory, processMethodContext);
+
+                // then
+                final MaxLengthFacet maxLengthFacet = facetedMethod.getFacet(MaxLengthFacet.class);
+                assertNotNull(maxLengthFacet);
+                assertTrue(maxLengthFacet instanceof MaxLengthFacetForPropertyAnnotation);
+                assertThat(maxLengthFacet.value(), is(30));
+            });
+        }
+    }
+
+    @TestInstance(Lifecycle.PER_CLASS)
+    static class MustSatisfy extends PropertyAnnotationFacetFactoryTest {
+
+        public static class NotTooHot implements Specification {
+            @Override
+            public String satisfies(final Object obj) {
+                return null;
+            }
+        }
+
+        public static class NotTooCold implements Specification {
+            @Override
+            public String satisfies(final Object obj) {
+                return null;
+            }
+        }
+
+        @Test
+        void withAnnotation() {
+
+            class Customer {
+                @Property(
+                        mustSatisfy = {NotTooHot.class, NotTooCold.class}
+                        )
+                @Getter @Setter private String name;
+            }
+
+            // given
+            propertyScenario(Customer.class, "name", (processMethodContext, facetHolder, facetedMethod)->{
+                // when
+                processMustSatisfy(facetFactory, processMethodContext);
+
+                // then
+                final MustSatisfySpecificationFacet mustSatisfySpecificationFacet = facetedMethod.getFacet(MustSatisfySpecificationFacet.class);
+                assertNotNull(mustSatisfySpecificationFacet);
+                assertTrue(mustSatisfySpecificationFacet instanceof MustSatisfySpecificationFacetForPropertyAnnotation);
+                final MustSatisfySpecificationFacetForPropertyAnnotation mustSatisfySpecificationFacetImpl = (MustSatisfySpecificationFacetForPropertyAnnotation) mustSatisfySpecificationFacet;
+                var specifications = mustSatisfySpecificationFacetImpl.getSpecifications();
+                assertThat(specifications.size(), is(2));
+
+                assertTrue(specifications.getElseFail(0) instanceof NotTooHot);
+                assertTrue(specifications.getElseFail(1) instanceof NotTooCold);
+            });
+        }
+
+    }
+
+    @TestInstance(Lifecycle.PER_CLASS)
+    static class EntityPropertyChangePublishingPolicy extends PropertyAnnotationFacetFactoryTest {
+
+        @Test
+        void exclusion() {
+
+            class Customer {
+                @Property(entityChangePublishing = Publishing.DISABLED)
+                @Getter @Setter private String name;
+            }
+
+            // given
+            propertyScenario(Customer.class, "name", (processMethodContext, facetHolder, facetedMethod)->{
+                // when
+                processEntityPropertyChangePublishing(facetFactory, processMethodContext);
+                // then
+                var changePolicyFacet = facetedMethod.lookupFacet(EntityPropertyChangePublishingPolicyFacet.class).orElse(null);
+                assertNotNull(changePolicyFacet);
+                assertTrue(changePolicyFacet.isPublishingVetoed());
+                assertFalse(changePolicyFacet.isPublishingAllowed());
+            });
+        }
+
+        @Test
+        void whenDefault() {
+
+            class Customer {
+                @Property
+                @Getter @Setter private String name;
+            }
+
+            // given
+            propertyScenario(Customer.class, "name", (processMethodContext, facetHolder, facetedMethod)->{
+                // when
+                processEntityPropertyChangePublishing(facetFactory, processMethodContext);
+                // then
+                var changePolicyFacet = facetedMethod.getFacet(EntityPropertyChangePublishingPolicyFacet.class);
+                assertNull(changePolicyFacet);
+            });
+        }
+
+    }
+
+    @TestInstance(Lifecycle.PER_CLASS)
+    static class SnapshotExcluded extends PropertyAnnotationFacetFactoryTest {
+
+        @Test
+        void withAnnotation() {
+
+            class Customer {
+                @Property(snapshot = Snapshot.EXCLUDED)
+                @Getter @Setter private String name;
+            }
+
+            // given
+            propertyScenario(Customer.class, "name", (processMethodContext, facetHolder, facetedMethod)->{
+                // when
+                processSnapshot(facetFactory, processMethodContext);
+                // then
+                final SnapshotExcludeFacet snapshotExcludeFacet = facetedMethod.getFacet(SnapshotExcludeFacet.class);
+                assertNotNull(snapshotExcludeFacet);
+                assertTrue(snapshotExcludeFacet instanceof SnapshotExcludeFacetForPropertyAnnotation);
+            });
+        }
+    }
+
+    @TestInstance(Lifecycle.PER_CLASS)
+    static class Mandatory extends PropertyAnnotationFacetFactoryTest {
+
+        @Test
+        void whenOptionalityIsTrue() {
+
+            class Customer {
+                @Property(optionality = Optionality.OPTIONAL)
+                @Getter @Setter private String name;
+            }
+
+            // given
+            propertyScenario(Customer.class, "name", (processMethodContext, facetHolder, facetedMethod)->{
+                // when
+                processOptional(facetFactory, processMethodContext);
+                // then
+                final MandatoryFacet mandatoryFacet = facetedMethod.getFacet(MandatoryFacet.class);
+                assertNotNull(mandatoryFacet);
+                assertTrue(mandatoryFacet instanceof MandatoryFacetForPropertyAnnotation.Optional);
+            });
+        }
+
+        @Test
+        void whenOptionalityIsFalse() {
+
+            class Customer {
+                @Property(optionality = Optionality.MANDATORY)
+                @Getter @Setter private String name;
+            }
+
+            // given
+            propertyScenario(Customer.class, "name", (processMethodContext, facetHolder, facetedMethod)->{
+                // when
+                processOptional(facetFactory, processMethodContext);
+                // then
+                final MandatoryFacet mandatoryFacet = facetedMethod.getFacet(MandatoryFacet.class);
+                assertNotNull(mandatoryFacet);
+                assertTrue(mandatoryFacet instanceof MandatoryFacetForPropertyAnnotation.Required);
+            });
+        }
+
+        @Test
+        void whenOptionalityIsDefault() {
+
+            class Customer {
+                @Property(optionality = Optionality.DEFAULT)
+                @Getter @Setter private String name;
+            }
+
+            // given
+            propertyScenario(Customer.class, "name", (processMethodContext, facetHolder, facetedMethod)->{
+                // when
+                processOptional(facetFactory, processMethodContext);
+                // then
+                final MandatoryFacet mandatoryFacet = facetedMethod.getFacet(MandatoryFacet.class);
+                assertNull(mandatoryFacet);
+            });
+        }
+
+        @Test
+        void whenNone() {
+
+            class Customer {
+                @Property()
+                @Getter @Setter private String name;
+            }
+
+            // given
+            propertyScenario(Customer.class, "name", (processMethodContext, facetHolder, facetedMethod)->{
+                // when
+                processOptional(facetFactory, processMethodContext);
+                // then
+                final MandatoryFacet mandatoryFacet = facetedMethod.getFacet(MandatoryFacet.class);
+                assertNull(mandatoryFacet);
+            });
+        }
+
+    }
+
+    @TestInstance(Lifecycle.PER_CLASS)
+    static class RegEx extends PropertyAnnotationFacetFactoryTest {
+
+        @Test
+        void whenHasAnnotation() {
+
+            class Customer {
+                @Property(
+                        regexPattern = "[123].*",
+                        regexPatternFlags = Pattern.CASE_INSENSITIVE | Pattern.MULTILINE)
+                @Getter @Setter private String name;
+            }
+
+            // given
+            propertyScenario(Customer.class, "name", (processMethodContext, facetHolder, facetedMethod)->{
+                // when
+                processRegEx(facetFactory, processMethodContext);
+                // then
+                final RegExFacet regExFacet = facetedMethod.getFacet(RegExFacet.class);
+                assertNotNull(regExFacet);
+                assertTrue(regExFacet instanceof RegExFacetForPropertyAnnotation);
+                assertThat(regExFacet.patternFlags(), is(10));
+                assertThat(regExFacet.regexp(), is("[123].*"));
+            });
+        }
+
+        @Test
+        void whenNone() {
+
+            class Customer {
+                @Property()
+                @Getter @Setter private String name;
+            }
+
+            // given
+            propertyScenario(Customer.class, "name", (processMethodContext, facetHolder, facetedMethod)->{
+                // when
+                processRegEx(facetFactory, processMethodContext);
+                // then
+                final RegExFacet regExFacet = facetedMethod.getFacet(RegExFacet.class);
+                assertNull(regExFacet);
+            });
+        }
+
+        @Test
+        void whenEmptyString() {
+
+            class Customer {
+                @Property(regexPattern = "")
+                @Getter @Setter private String name;
+            }
+
+            // given
+            propertyScenario(Customer.class, "name", (processMethodContext, facetHolder, facetedMethod)->{
+                // when
+                processRegEx(facetFactory, processMethodContext);
+
+                // then
+                final RegExFacet regExFacet = facetedMethod.getFacet(RegExFacet.class);
+                assertNull(regExFacet);
+            });
+        }
+
+        @Test
+        void whenNotAnnotatedOnStringProperty() {
+
+            class Customer {
+                @Property(regexPattern = "[abc].*")
+                public int getName() {return 0; }
+                @SuppressWarnings("unused") public void setName(final int name) { }
+            }
+
+            // given
+            propertyScenario(Customer.class, "name", (processMethodContext, facetHolder, facetedMethod)->{
+                // when
+                processRegEx(facetFactory, processMethodContext);
+
+                // then
+                final RegExFacet regExFacet = facetedMethod.getFacet(RegExFacet.class);
+                assertNull(regExFacet);
+            });
+        }
+
+    }
+
+}
diff --git a/persistence/commons/src/main/java/org/apache/causeway/persistence/commons/integration/changetracking/EntityChangeTrackerDefault.java b/persistence/commons/src/main/java/org/apache/causeway/persistence/commons/integration/changetracking/EntityChangeTrackerDefault.java
index e2baafec544..fce90ce2432 100644
--- a/persistence/commons/src/main/java/org/apache/causeway/persistence/commons/integration/changetracking/EntityChangeTrackerDefault.java
+++ b/persistence/commons/src/main/java/org/apache/causeway/persistence/commons/integration/changetracking/EntityChangeTrackerDefault.java
@@ -39,18 +39,12 @@
 import jakarta.inject.Named;
 import jakarta.inject.Provider;
 
-import org.apache.causeway.applib.jaxb.JavaSqlXMLGregorianCalendarMarshalling;
-import org.apache.causeway.core.metamodel.execution.InteractionInternal;
-
-import org.apache.causeway.core.metamodel.services.deadlock.DeadlockRecognizer;
-import org.apache.causeway.schema.chg.v2.ChangesDto;
-import org.apache.causeway.schema.chg.v2.ObjectsDto;
-import org.apache.causeway.schema.common.v2.OidsDto;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
 
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.core.Ordered;
-import org.jspecify.annotations.Nullable;
 import org.springframework.stereotype.Component;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.support.TransactionSynchronization;
@@ -61,6 +55,7 @@
 import org.apache.causeway.applib.annotation.PriorityPrecedence;
 import org.apache.causeway.applib.annotation.Programmatic;
 import org.apache.causeway.applib.annotation.TransactionScope;
+import org.apache.causeway.applib.jaxb.JavaSqlXMLGregorianCalendarMarshalling;
 import org.apache.causeway.applib.services.bookmark.Bookmark;
 import org.apache.causeway.applib.services.iactn.Interaction;
 import org.apache.causeway.applib.services.iactn.InteractionProvider;
@@ -74,10 +69,12 @@
 import org.apache.causeway.commons.internal.collections._Sets;
 import org.apache.causeway.commons.internal.exceptions._Exceptions;
 import org.apache.causeway.core.config.CausewayConfiguration;
+import org.apache.causeway.core.metamodel.execution.InteractionInternal;
 import org.apache.causeway.core.metamodel.facets.object.publish.entitychange.EntityChangePublishingFacet;
 import org.apache.causeway.core.metamodel.object.ManagedObject;
 import org.apache.causeway.core.metamodel.object.ManagedObjects;
 import org.apache.causeway.core.metamodel.object.MmEntityUtils;
+import org.apache.causeway.core.metamodel.services.deadlock.DeadlockRecognizer;
 import org.apache.causeway.core.metamodel.services.objectlifecycle.HasEnlistedEntityPropertyChanges;
 import org.apache.causeway.core.metamodel.services.objectlifecycle.PreAndPostValue;
 import org.apache.causeway.core.metamodel.services.objectlifecycle.PropertyChangeRecord;
@@ -88,9 +85,11 @@
 import org.apache.causeway.core.transaction.changetracking.EntityPropertyChangePublisher;
 import org.apache.causeway.core.transaction.changetracking.HasEnlistedEntityChanges;
 import org.apache.causeway.persistence.commons.CausewayModulePersistenceCommons;
+import org.apache.causeway.schema.chg.v2.ChangesDto;
+import org.apache.causeway.schema.chg.v2.ObjectsDto;
+import org.apache.causeway.schema.common.v2.OidsDto;
 
 import lombok.Getter;
-import org.jspecify.annotations.NonNull;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.log4j.Log4j2;
 
@@ -305,21 +304,18 @@ private boolean shouldPublish(final PreAndPostValue preAndPostValue) {
 
     private boolean isEntityExcludedForChangePublishing(final ManagedObject entity) {
 
-        if (!configuration.isEnabled()) {
+        if (!configuration.isEnabled())
             return true;
-        }
 
-        if(!EntityChangePublishingFacet.isPublishingEnabled(entity.objSpec())) {
+        if(!EntityChangePublishingFacet.isPublishingEnabled(entity.objSpec()))
             return true; // ignore entities that are not enabled for entity change publishing
-        }
 
         // guard against transient
         if(ManagedObjects.bookmark(entity).isEmpty()) return true;
 
-        if(changes.isMemoized()) {
+        if(changes.isMemoized())
             throw _Exceptions.illegalState("Cannot enlist additional changes for auditing, "
                     + "since changedObjectPropertiesRef was already prepared (memoized) for auditing.");
-        }
 
         return false;
     }
@@ -373,9 +369,8 @@ public Optional<EntityChanges> getEntityChanges(
 
         // a defensive copy of
         var changeKindByEnlistedAdapter = new HashMap<>(this.changeKindByEnlistedAdapter);
-        if(changeKindByEnlistedAdapter.isEmpty()) {
+        if(changeKindByEnlistedAdapter.isEmpty())
             return Optional.empty();
-        }
 
         final Interaction interaction = currentInteraction();
         final int numberEntitiesLoaded1 = numberEntitiesLoaded();
@@ -418,9 +413,8 @@ private static ChangesDto newDto(
 
         changeKindByEnlistedEntity.forEach((bookmark, kind)->{
             var oidDto = bookmark.toOidDto();
-            if(oidDto==null) {
+            if(oidDto==null)
                 return;
-            }
             switch(kind) {
                 case CREATE:
                     objectsDto.getCreated().getOid().add(oidDto);
@@ -530,9 +524,9 @@ public void enlistCreated(final ManagedObject entity) {
 
         _Xray.enlistCreated(entity, interactionProviderProvider);
 
-        if (isEntityExcludedForChangePublishing(entity)) {
+        if (isEntityExcludedForChangePublishing(entity)
+                || !EntityChangePublishingFacet.isPublishingEnabledForCreate(entity.objSpec()))
             return;
-        }
 
         log.debug("enlist entity's property changes for publishing {}", entity);
 
@@ -551,9 +545,9 @@ public void enlistUpdating(
 
         _Xray.enlistUpdating(entity, interactionProviderProvider);
 
-        if (isEntityExcludedForChangePublishing(entity)) {
+        if (isEntityExcludedForChangePublishing(entity)
+                || !EntityChangePublishingFacet.isPublishingEnabledForUpdate(entity.objSpec()))
             return;
-        }
 
         if(log.isDebugEnabled()) {
             log.debug("enlist entity's property changes for publishing {}", entity);
@@ -588,7 +582,9 @@ public void enlistDeleting(final ManagedObject entity) {
 
         _Xray.enlistDeleting(entity, interactionProviderProvider);
 
-        if (isEntityExcludedForChangePublishing(entity)) return;
+        if (isEntityExcludedForChangePublishing(entity)
+                || !EntityChangePublishingFacet.isPublishingEnabledForDelete(entity.objSpec()))
+            return;
 
         suppressAutoFlushIfRequired(() -> {
             final boolean enlisted = enlistForChangeKindPublishing(entity, EntityChangeKind.DELETE);