Example #1
0
 public BrandDomainEvent(DomainEventType domainEventType = DomainEventType.Create)
 {
     BrandDomainEventId = Guid.NewGuid();
     DomainEventType    = domainEventType;
     Created            = DateTimeOffset.UtcNow;
     Processed          = null;
 }
Example #2
0
 public AuditLogEvent(DomainEventType eventType, string userName, Guid eventStreamId, bool accessDenied = false) : this(accessDenied ? DomainEventType.AccessDeniedAuditEvent : eventType, userName, eventStreamId, 0)
 {
     if (accessDenied)
     {
         this.AccessDeniedEventType = eventType;
     }
 }
Example #3
0
 public DomainEvent(Guid aggregateId, DomainEventType domainEventType)
 {
     AggregateRootId = aggregateId;
     DateCreated     = DateTimeOffset.Now;
     TransactionId   = Guid.NewGuid();
     DomainEventType = domainEventType;
 }
 public DomainEventNotificationBase(DomainEventType domainEventType, string userName, Guid eventStreamId, int eventId)
 {
     this.UserName        = userName;
     this.Timestamp       = DateTimeOffset.UtcNow;
     this.DomainEventType = domainEventType;
     this.EventStreamId   = eventStreamId;
     this.EventId         = eventId;
 }
Example #5
0
 public DomainEvent(string eventData, string username, DomainEventType type, Guid eventStreamId, int?childEntityId = null)
 {
     this.EventData       = eventData;
     this.Username        = username;
     this.DomainEventType = type;
     this.Timestamp       = DateTimeOffset.UtcNow;
     this.EventStreamId   = eventStreamId;
     this.ChildEntityId   = childEntityId;
 }
 public EventWrapper(Guid aggregateId, DateTimeOffset dateCreated, int version, Guid transactionId, Guid userId, DomainEventType type, string data)
 {
     AggregateRootId = aggregateId;
     DateCreated     = dateCreated;
     Version         = version;
     TransactionId   = transactionId;
     CreatedByUserId = userId;
     DomainEventType = type;
     Data            = data;
 }
Example #7
0
 public AuditLogEntry(AuditLogEntryType auditType, DomainEventType eventType, string userName, Guid eventStreamId, int eventId, DataAccessType?dataAccessType)
 {
     this.AuditType       = auditType;
     this.DomainEventType = eventType;
     this.UserName        = userName;
     this.EventStreamId   = eventStreamId;
     this.EventId         = eventId;
     this.DataAccessType  = dataAccessType;
     this.Timestamp       = DateTimeOffset.UtcNow;
 }
Example #8
0
        public List <DomainEvent> GetEventsByType(DomainEventType t)
        {
            var events             = _storage.DomainEvents.Where(e => e.DomainEventType == t).ToList();
            var deserializedEvents = new List <DomainEvent>();

            foreach (var @event in events)
            {
                deserializedEvents.Add(JsonConvert.DeserializeObject <DomainEvent>(@event.Data));
            }

            return(deserializedEvents);
        }
Example #9
0
        /// <summary>
        /// Handle a DELETE command which specifies that an aggregate should be marked as deleted
        /// </summary>
        /// <param name="request"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public async Task <CommandResult <Guid> > Handle(DeleteCommand <T> request, CancellationToken cancellationToken)
        {
            // Validate input
            if (request.EventStreamId == Guid.Empty)
            {
                return(new CommandResult <Guid>(ResultType.BadRequest));
            }

            if (await GetSpecificAggregateRoot(request.EventStreamId) == null)
            {
                return(new CommandResult <Guid>(ResultType.NothingFound));
            }

            // Call extension point to get the action authorised for the user in question, and the specific resource
            var authResult = await this.AuthoriseDelete(request.EventStreamId);

            if (authResult.ResultType == ResultType.AccessDenied)
            {
                // TODO: Move the Access Denied audit log creation to here, from the child classes?
                return(new CommandResult <Guid>(authResult.ResultType));
            }

            // Create a domain event record for this aggregate, and this entity (This is an UPDATE for the aggregate as we're not DELETING the record physically)
            string eventData = new JObject()
            {
                new JProperty("Status", EntityStatus.Cancelled.ToString())
            }.ToString();
            DomainEventType eventType   = (DomainEventType)Enum.Parse(typeof(DomainEventType), ($"Modify{typeof(T).Name}Event"));
            DomainEvent     domainEvent = new DomainEvent(eventData, _currentUser.UserName, eventType, request.EventStreamId);

            // Save the domain event
            _dc.DomainEvents.Add(domainEvent);
            await _dc.SaveChangesAsync();

            // Publish domain event notification - this is published as a MODIFY on the aggregate as we are not physically deleting the record, rather we're changing its status.
            UpdatedEvent <T> eventNotification = new UpdatedEvent <T>(eventData, _currentUser.UserName, domainEvent.EventStreamId, domainEvent.Id);
            await _mediator.Publish(eventNotification);

            // Drop an AuditLogEvent onto the mediator, to dispatch a request to update the system audit log. Again, we're not going to wait for the outcome of this event. Just fire and forget.
            // We are going to audit this as a DELETE even though it's enacted as a modify, for clarity
            AuditLogEvent auditLogNotification = new AuditLogEvent((DomainEventType)Enum.Parse(typeof(DomainEventType), ($"Delete{typeof(T).Name}Event")), _currentUser.UserName, domainEvent.EventStreamId, domainEvent.Id);
            await _mediator.Publish(auditLogNotification);

            // Return new command result, with the PARENT's event stream id.
            return(new CommandResult <Guid>());
        }
Example #10
0
        /// <summary>
        /// Handle a CREATE command which specifies that a new entity should be added to a child collection on an aggregate
        /// </summary>
        /// <param name="request"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public async Task <CommandResult <Guid> > Handle(CreateChildCommand <T> request, CancellationToken cancellationToken)
        {
            // Validate input
            if (string.IsNullOrWhiteSpace(request.EventData) || request.ParentEventStreamId == Guid.Empty)
            {
                return(new CommandResult <Guid>(ResultType.BadRequest));
            }

            if (await GetSpecificAggregateRoot(request.ParentEventStreamId) == null)
            {
                return(new CommandResult <Guid>(ResultType.NothingFound));
            }

            // Call extension point to get the action authorised for the user in question, and the specific resource
            var authResult = await this.AuthoriseModify(request.EventData, request.ParentEventStreamId, request.ChildEntityType);

            if (authResult.ResultType == ResultType.AccessDenied)
            {
                // TODO: Move the Access Denied audit log creation to here, from the child classes?
                return(new CommandResult <Guid>(authResult.ResultType));
            }
            else
            {
                // Create a domain event record for this aggregate, and this new child entity (This is an UPDATE for the aggregate, recorded as a Create of the new child)
                DomainEventType eventType   = (DomainEventType)Enum.Parse(typeof(DomainEventType), string.Format("Create{0}Event", request.ChildEntityType.Name));
                DomainEvent     domainEvent = new DomainEvent(request.EventData, _currentUser.UserName, eventType, request.ParentEventStreamId);

                // Save the domain event
                _dc.DomainEvents.Add(domainEvent);
                await _dc.SaveChangesAsync();

                // Publish domain event notification - this is published as a MODIFY on the aggregate as this will direct it to the correct Aggregate's domain serivce.
                UpdatedEvent <T> eventNotification = new UpdatedEvent <T>(request.EventData, _currentUser.UserName, domainEvent.EventStreamId, domainEvent.Id);
                await _mediator.Publish(eventNotification);

                // Drop an AuditLogEvent onto the mediator, to dispatch a request to update the system audit log. Again, we're not going to wait for the outcome of this event. Just fire and forget.
                AuditLogEvent auditLogNotification = new AuditLogEvent(domainEvent.DomainEventType, _currentUser.UserName, domainEvent.EventStreamId, domainEvent.Id);
                await _mediator.Publish(auditLogNotification);

                // Return new command result, with the PARENT's event stream id.
                return(new CommandResult <Guid>(request.ParentEventStreamId));
            }
        }
Example #11
0
        /// <summary>
        /// Handle CREATE commands
        /// </summary>
        /// <param name="request"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public async Task <CommandResult <Guid> > Handle(CreateCommand <T> request, CancellationToken cancellationToken)
        {
            // Validate input
            if (string.IsNullOrWhiteSpace(request.EventData))
            {
                return(new CommandResult <Guid>(ResultType.BadRequest));
            }

            // Call extension point to get the action authorised for the user in question, and the specific resource
            var authResult = await this.AuthoriseCreate(request.EventData);

            if (authResult.ResultType == ResultType.AccessDenied)
            {
                // TODO: Move the Access Denied audit log creation to here, from the child classes?
                return(new CommandResult <Guid>(authResult.ResultType));
            }
            else
            {
                // This is a Create event (and thus starts a new event stream) so we need to create the Event Stream Id
                Guid eventStreamId = Guid.NewGuid();

                // Save the domain event record to the database
                DomainEventType eventType   = (DomainEventType)Enum.Parse(typeof(DomainEventType), string.Format("Create{0}Event", typeof(T).Name));
                DomainEvent     domainEvent = new DomainEvent(request.EventData, _currentUser.UserName, eventType, eventStreamId);

                _dc.DomainEvents.Add(domainEvent);
                await _dc.SaveChangesAsync();

                // Now publish events to signal the domain model to be updated, and to update the audit log
                CreatedEvent <T> eventNotification = new CreatedEvent <T>(request.EventData, _currentUser.UserName, domainEvent.EventStreamId, domainEvent.Id);
                await _mediator.Publish(eventNotification);

                // Drop an AuditLogEvent onto the mediator, to dispatch a request to update the system audit log. Again, we're not going to wait for the outcome of this event. Just fire and forget.
                AuditLogEvent auditLogNotification = new AuditLogEvent(eventNotification.DomainEventType, _currentUser.UserName, domainEvent.EventStreamId, domainEvent.Id);
                await _mediator.Publish(auditLogNotification);

                // If the response was not 201 CREATED (or it is, and we have errors) then we need to ensure that we pass the correct response code back, along with any "error" messages

                //return new CommandResult<Guid>(eventStreamId);
                return(new CommandResult <Guid>(authResult.ResultType, eventStreamId, authResult.Errors));
            }
        }
Example #12
0
    private void DomainEventCallback(IntPtr conn, IntPtr dom, DomainEventType evt, int detail, IntPtr opaque)
    {
        switch (evt)
        {
        case DomainEventType.VIR_DOMAIN_EVENT_DEFINED:
            _textBuf.Text += "Domain defined :";
            switch ((DomainEventDefinedDetailType)detail)
            {
            case DomainEventDefinedDetailType.VIR_DOMAIN_EVENT_DEFINED_ADDED:
                _textBuf.Text += "domain added\r\n";
                break;

            case DomainEventDefinedDetailType.VIR_DOMAIN_EVENT_DEFINED_UPDATED:
                _textBuf.Text += "domain updated\r\n";
                break;
            }
            break;

        case DomainEventType.VIR_DOMAIN_EVENT_UNDEFINED:
            _textBuf.Text += "Domain undefined :";
            switch ((DomainEventUndefinedDetailType)detail)
            {
            case DomainEventUndefinedDetailType.VIR_DOMAIN_EVENT_UNDEFINED_REMOVED:
                _textBuf.Text += "domain removeed\r\n";
                break;
            }
            break;

        case DomainEventType.VIR_DOMAIN_EVENT_RESUMED:
            _textBuf.Text += "Domain resumed :";
            switch ((DomainEventResumedDetailType)detail)
            {
            case DomainEventResumedDetailType.VIR_DOMAIN_EVENT_RESUMED_MIGRATED:
                _textBuf.Text += "domain migrated\r\n";
                break;

            case DomainEventResumedDetailType.VIR_DOMAIN_EVENT_RESUMED_UNPAUSED:
                _textBuf.Text += "domain unpaused\r\n";
                break;
            }
            break;

        case DomainEventType.VIR_DOMAIN_EVENT_STARTED:
            _textBuf.Text += "Domain started :";
            switch ((DomainEventStartedDetailType)detail)
            {
            case DomainEventStartedDetailType.VIR_DOMAIN_EVENT_STARTED_BOOTED:
                _textBuf.Text += "domain booted\r\n";
                break;

            case DomainEventStartedDetailType.VIR_DOMAIN_EVENT_STARTED_MIGRATED:
                _textBuf.Text += "domain migrated\r\n";
                break;

            case DomainEventStartedDetailType.VIR_DOMAIN_EVENT_STARTED_RESTORED:
                _textBuf.Text += "domain restored\r\n";
                break;
            }
            break;

        case DomainEventType.VIR_DOMAIN_EVENT_STOPPED:
            _textBuf.Text += "Domain stopped :";
            switch ((DomainEventStoppedDetailType)detail)
            {
            case DomainEventStoppedDetailType.VIR_DOMAIN_EVENT_STOPPED_CRASHED:
                _textBuf.Text += "domain crashed\r\n";
                break;

            case DomainEventStoppedDetailType.VIR_DOMAIN_EVENT_STOPPED_DESTROYED:
                _textBuf.Text += "domain destroyed\r\n";
                break;

            case DomainEventStoppedDetailType.VIR_DOMAIN_EVENT_STOPPED_FAILED:
                _textBuf.Text += "domain failed\r\n";
                break;

            case DomainEventStoppedDetailType.VIR_DOMAIN_EVENT_STOPPED_MIGRATED:
                _textBuf.Text += "domain migrated\r\n";
                break;

            case DomainEventStoppedDetailType.VIR_DOMAIN_EVENT_STOPPED_SAVED:
                _textBuf.Text += "domain saved\r\n";
                break;

            case DomainEventStoppedDetailType.VIR_DOMAIN_EVENT_STOPPED_SHUTDOWN:
                _textBuf.Text += "domain shutdown\r\n";
                break;
            }
            break;

        case DomainEventType.VIR_DOMAIN_EVENT_SUSPENDED:
            _textBuf.Text += "Domain suspended :";
            switch ((DomainEventSuspendedDetailType)detail)
            {
            case DomainEventSuspendedDetailType.VIR_DOMAIN_EVENT_SUSPENDED_MIGRATED:
                _textBuf.Text += "domain migrated\r\n";
                break;

            case DomainEventSuspendedDetailType.VIR_DOMAIN_EVENT_SUSPENDED_PAUSED:
                _textBuf.Text += "domain paused\r\n";
                break;
            }
            break;
        }
    }
Example #13
0
 public AuditLogEvent(DomainEventType eventType, String userName, Guid eventStreamId, int eventId) : base(eventType, userName, eventStreamId, eventId)
 {
 }
Example #14
0
 public static bool IsCreateEvent(this DomainEventType eventType)
 {
     return(eventType.ToString().StartsWith("Create"));
 }
Example #15
0
 /// <summary>
 /// Use this constructor only when there is no associated domain event available. This will only be the case where we're auditing an error condition of some kind, such as AccessDenied Audit Events.
 /// </summary>
 /// <param name="eventType"></param>
 /// <param name="userName"></param>
 public AuditLogEvent(DomainEventType eventType, string userName) : this(eventType, userName, Guid.Empty, 0)
 {
 }
 private void DomainEventCallback(IntPtr conn, IntPtr dom, DomainEventType evt, int detail, IntPtr opaque)
 {
     switch (evt)
     {
         case DomainEventType.VIR_DOMAIN_EVENT_DEFINED:
             _textBuf.Text += "Domain defined :";
             switch ((DomainEventDefinedDetailType)detail)
             {
                 case DomainEventDefinedDetailType.VIR_DOMAIN_EVENT_DEFINED_ADDED:
                     _textBuf.Text += "domain added\r\n";
                     break;
                 case DomainEventDefinedDetailType.VIR_DOMAIN_EVENT_DEFINED_UPDATED:
                     _textBuf.Text += "domain updated\r\n";
                     break;
             }
             break;
         case DomainEventType.VIR_DOMAIN_EVENT_UNDEFINED:
             _textBuf.Text += "Domain undefined :";
             switch ((DomainEventUndefinedDetailType)detail)
             {
                 case DomainEventUndefinedDetailType.VIR_DOMAIN_EVENT_UNDEFINED_REMOVED:
                     _textBuf.Text += "domain removeed\r\n";
                     break;
             }
             break;
         case DomainEventType.VIR_DOMAIN_EVENT_RESUMED:
             _textBuf.Text += "Domain resumed :";
             switch ((DomainEventResumedDetailType)detail)
             {
                 case DomainEventResumedDetailType.VIR_DOMAIN_EVENT_RESUMED_MIGRATED:
                     _textBuf.Text += "domain migrated\r\n";
                     break;
                 case DomainEventResumedDetailType.VIR_DOMAIN_EVENT_RESUMED_UNPAUSED:
                     _textBuf.Text += "domain unpaused\r\n";
                     break;
             }
             break;
         case DomainEventType.VIR_DOMAIN_EVENT_STARTED:
             _textBuf.Text += "Domain started :";
             switch ((DomainEventStartedDetailType)detail)
             {
                 case DomainEventStartedDetailType.VIR_DOMAIN_EVENT_STARTED_BOOTED:
                     _textBuf.Text += "domain booted\r\n";
                     break;
                 case DomainEventStartedDetailType.VIR_DOMAIN_EVENT_STARTED_MIGRATED:
                     _textBuf.Text += "domain migrated\r\n";
                     break;
                 case DomainEventStartedDetailType.VIR_DOMAIN_EVENT_STARTED_RESTORED:
                     _textBuf.Text += "domain restored\r\n";
                     break;
             }
             break;
         case DomainEventType.VIR_DOMAIN_EVENT_STOPPED:
             _textBuf.Text += "Domain stopped :";
             switch ((DomainEventStoppedDetailType)detail)
             {
                 case DomainEventStoppedDetailType.VIR_DOMAIN_EVENT_STOPPED_CRASHED:
                     _textBuf.Text += "domain crashed\r\n";
                     break;
                 case DomainEventStoppedDetailType.VIR_DOMAIN_EVENT_STOPPED_DESTROYED:
                     _textBuf.Text += "domain destroyed\r\n";
                     break;
                 case DomainEventStoppedDetailType.VIR_DOMAIN_EVENT_STOPPED_FAILED:
                     _textBuf.Text += "domain failed\r\n";
                     break;
                 case DomainEventStoppedDetailType.VIR_DOMAIN_EVENT_STOPPED_MIGRATED:
                     _textBuf.Text += "domain migrated\r\n";
                     break;
                 case DomainEventStoppedDetailType.VIR_DOMAIN_EVENT_STOPPED_SAVED:
                     _textBuf.Text += "domain saved\r\n";
                     break;
                 case DomainEventStoppedDetailType.VIR_DOMAIN_EVENT_STOPPED_SHUTDOWN:
                     _textBuf.Text += "domain shutdown\r\n";
                     break;
             }
             break;
         case DomainEventType.VIR_DOMAIN_EVENT_SUSPENDED:
             _textBuf.Text += "Domain suspended :";
             switch ((DomainEventSuspendedDetailType)detail)
             {
                 case DomainEventSuspendedDetailType.VIR_DOMAIN_EVENT_SUSPENDED_MIGRATED:
                     _textBuf.Text += "domain migrated\r\n";
                     break;
                 case DomainEventSuspendedDetailType.VIR_DOMAIN_EVENT_SUSPENDED_PAUSED:
                     _textBuf.Text += "domain paused\r\n";
                     break;
             }
             break;
     }
 }
Example #17
0
 public UserEvent(DomainEventType type) : base(type)
 {
 }
Example #18
0
        /// <summary>
        /// Process the stream of events identified by <paramref name="eventStreamId"/> against the aggregate provided in <paramref name="aggregateRecord"/>
        /// </summary>
        /// <param name="aggregateRecord"></param>
        /// <param name="eventType"></param>
        /// <param name="eventStreamId"></param>
        /// <returns></returns>
        private async Task <UpdateServiceResult <Guid> > ProcessEventStream(T aggregateRecord, DomainEventType eventType, Guid eventStreamId)
        {
            try
            {
                // Get the set of events in the event stream that are NEWER than the current snapshot
                List <DomainEvent> eventStream = _dc.DomainEvents.Where(e => e.EventStreamId == eventStreamId && e.Timestamp.CompareTo(aggregateRecord.EventVersionTimestamp) > 0).ToList();

                if (eventStream.Any() == false)
                {
                    return(new UpdateServiceResult <Guid>(ResultType.NothingFound, new List <string>()
                    {
                        "Error - an attempt was made to process an event stream using an EventStreamId that was not present in the event data store, or where every event is older than the current aggregate snapshot view."
                    }));
                }

                // Loop through the event stream applying the changes in chronological order.
                foreach (DomainEvent e in eventStream)
                {
                    try
                    {
                        aggregateRecord = await this.ProcessEvent(e, aggregateRecord);
                    }
                    catch (Exception ex)
                    {
                        // TODO: Decide whether to explictly roll back the transaction - might be best
                        List <string> errors = new List <string>();
                        errors.Add(ex.Message);
                        return(new UpdateServiceResult <Guid>(ResultType.InternalServerError, errors));
                    }
                }

                // Ensure the aggregate is updated with the timestamp of the latest event
                aggregateRecord.EventVersionTimestamp = eventStream.OrderBy(e => e.Timestamp).Last().Timestamp;

                // If we've created a NEW domain object, then add it to the DC, otherwise mark it as modified
                if (_dc.Entry <T>(aggregateRecord).State == EntityState.Detached)
                {
                    _dc.Add <T>(aggregateRecord);
                }

                // Save to the data store
                await _dc.SaveChangesAsync();

                // Return a Good result containing the Id of the new record
                return(eventType.IsCreateEvent() ? new UpdateServiceResult <Guid>(aggregateRecord.EventStreamId) : new UpdateServiceResult <Guid>());
            }
            catch (Exception ex)
            {
                List <string> errors = new List <string>();
                errors.Add(ex.Message);

                return(new UpdateServiceResult <Guid>(ResultType.InternalServerError, errors));
            }
        }
Example #19
0
 public DomainEvent(DomainEventType type)
 {
     EventId = Guid.NewGuid();
     Time    = DateTime.Now.Ticks;
     Type    = type;
 }
Example #20
0
        /// <summary>
        /// Handle a PATCh command which specifies updates on a child entity of the aggregate root
        /// </summary>
        /// <param name="request"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public async Task <CommandResult <Guid> > Handle(ModifyChildCommand <T> request, CancellationToken cancellationToken)
        {
            // Validate input
            if (request.ParentEventStreamId == Guid.Empty || request.ChildEntityId < 1 || request.ChildEntityType == null || string.IsNullOrWhiteSpace(request.EventData))
            {
                return(new CommandResult <Guid>(ResultType.BadRequest));
            }

            T aggregateRoot = await GetSpecificAggregateRoot(request.ParentEventStreamId);

            // find the child collection
            var         childCollectionProperty = aggregateRoot?.GetType().GetProperties().Where(p => p.PropertyType.GenericTypeArguments.Contains(request.ChildEntityType)).First();
            IEnumerable childCollection         = (IEnumerable)childCollectionProperty?.GetMethod.Invoke(aggregateRoot, null);
            int         childCollectionCount    = childCollectionProperty == null || childCollection == null ? 0 : (int)childCollectionProperty.PropertyType.GetProperty("Count").GetValue(childCollection);

            bool childIdFound = false;

            if (childCollection != null)
            {
                foreach (dynamic child in childCollection)
                {
                    if (child.Id == request.ChildEntityId)
                    {
                        childIdFound = true;
                        break;
                    }
                }
            }

            // TODO: Return nothing found if the childentityId is not in the collection
            if (aggregateRoot == null || childCollection == null || childCollectionCount == 0 || childIdFound == false)
            {
                return(new CommandResult <Guid>(ResultType.NothingFound));
            }

            // Call extension point to get the action authorised for the user in question, and the specific resource.
            // This is a modify on a child entity, we're going to rely on the permission for modification of the parent entity for this.
            var authResult = await this.AuthoriseModify(request.EventData, request.ParentEventStreamId, request.ChildEntityType);

            if (authResult.ResultType == ResultType.AccessDenied)
            {
                // TODO: Move the Access Denied audit log creation to here, from the child classes?
                return(new CommandResult <Guid>(authResult.ResultType));
            }

            // Create a domain event record for this aggregate, and this entity (This is an UPDATE for the aggregate, since it's updating child data owned by this aggregate root)
            DomainEventType eventType   = (DomainEventType)Enum.Parse(typeof(DomainEventType), ($"Modify{request.ChildEntityType.Name}Event"));
            DomainEvent     domainEvent = new DomainEvent(request.EventData, _currentUser.UserName, eventType, request.ParentEventStreamId, request.ChildEntityId);

            // Save the domain event
            _dc.DomainEvents.Add(domainEvent);
            await _dc.SaveChangesAsync();

            // Publish domain event notification - this is published as a MODIFY on the aggregate as we are not physically deleting the record, rather we're changing its status.
            UpdatedEvent <T> eventNotification = new UpdatedEvent <T>(request.EventData, _currentUser.UserName, domainEvent.EventStreamId, domainEvent.Id);
            await _mediator.Publish(eventNotification);

            // Drop an AuditLogEvent onto the mediator, to dispatch a request to update the system audit log. Again, we're not going to wait for the outcome of this event. Just fire and forget.
            // We are going to audit this as a DELETE even though it's enacted as a modify, for clarity
            AuditLogEvent auditLogNotification = new AuditLogEvent((DomainEventType)Enum.Parse(typeof(DomainEventType), ($"Modify{request.ChildEntityType.Name}Event")), _currentUser.UserName, domainEvent.EventStreamId, domainEvent.Id);
            await _mediator.Publish(auditLogNotification);

            // Return new command result, with the PARENT's event stream id.
            return(new CommandResult <Guid>());
        }