Informatievlaanderen / registry-documentation

All documentation related to the base registries.
https://informatievlaanderen.github.io/registry-documentation/
1 stars 2 forks source link

Event Name Serialization #6

Open CumpsD opened 3 years ago

CumpsD commented 3 years ago

The case of [EventName] and Syndication Feeds

Context

In our usage of Event Sourcing, we keep the event name as a column in our Events to allow for future deserialisation of the event back into an object. In a very early version, we naively took the class name as the event name. Not long afterwards we introduced an EventNameAttribute to turn this into a fixed value, which would survive refactoring of our code.

For example:

[EventTags(Tags.Sync)]
[EventName("MunicipalityBecameCurrent")]
[EventDescription("De gemeente werd in gebruik genomen.")]
public class MunicipalityBecameCurrent : IHasProvenance, ISetProvenance

If this class ever had to be renamed to MunicipalityBecameCurrentV1, it would not break our deserialisation, because MunicipalityBecameCurrent would be the stored key.

Over time, this attribute has taken on other usages as well, for example in documenting all our events with Structurizr, together with EventDescription.

For the most part of the development, events have remained an implementation detail and were not exposed to the outside world. Until a feature request came along to allow our user to get a feed of events. The use case is to allow them to built anything they want based on the events.

This resulted in the creation of the Feeds endpoints. These endpoints are populated by Syndication projections in each registry. In these projections SetEventData is responsible for populating the XML representation of the event for outside consumption. The feed endpoints simply return these pre-serialised events to the consumer.

Problem

Whereas we used the EventNameAttribute for de/serialisation in the Event Store, we made the mistake of using the following code in the syndication projections:

syndicationItem.EventDataAsXml = message.ToXml(message.GetType().Name).ToString(SaveOptions.DisableFormatting);

This means the class name is used in the feeds instead of the attribute name. In most cases these were identical, causing the late detection of this bug, but in some cases they were different:

[EventName("MunicipalityFacilityLanguageWasAdded")]
[EventDescription("Een faciliteiten taal van de gemeente werd toegevoegd.")]
public class MunicipalityFacilitiesLanguageWasAdded : IHasProvenance, ISetProvenance

This resulted in an XML element having MunicipalityFacilitiesLanguageWasAdded as a root tag instead of MunicipalityFacilityLanguageWasAdded, while also being vulnerable to breaking changes when refactoring class names.

Solution

A solution for this has been implemented in https://github.com/Informatievlaanderen/municipality-registry/pull/107 which does not use the class name anymore, but uses the EventNameAttribute.

=> syndicationItem.EventDataAsXml = message.ToXml(message.GetType().GetCustomAttribute<EventNameAttribute>()!.Value).ToString(SaveOptions.DisableFormatting);

This fix has to be implemented in every registry separately.

Update: While fixing it, we did not have to use reflection to get the attribute, the event name was already present in the event itself.

public static void SetEventData<T>(this MunicipalitySyndicationItem syndicationItem, T message, string eventName)
    => syndicationItem.EventDataAsXml = message.ToXml(eventName).ToString(SaveOptions.DisableFormatting);

Additional remarks

Todo

Determine in which registers this occurs to rebuild the projections. Finding this can be done with a unit test:

[Fact]
public void HasEventNameAttributeEqualToClass()
{
    foreach (var type in _eventTypes)
        type
            .GetCustomAttribute<EventNameAttribute>(true)!.Value
            .Should()
            .Match(x => x == type.Name);
}

Registries to check

Rebuilds Required

CumpsD commented 3 years ago

Fixes

SQL Fixes

Address

UPDATE [address-registry].AddressRegistryLegacy.AddressSyndication
   SET EventDataAsXml = REPLACE(REPLACE(EventDataAsXml, '</AddressPersistentLocalIdWasAssigned>', '</AddressPersistentLocalIdentifierWasAssigned>'), '<AddressPersistentLocalIdWasAssigned>', '<AddressPersistentLocalIdentifierWasAssigned>')
 WHERE [changetype] = 'AddressPersistentLocalIdentifierWasAssigned' AND EventDataAsXml LIKE '<AddressPersistentLocalIdWasAssigned>%'

Building

UPDATE [building-registry].BuildingRegistryLegacy.BuildingSyndication
   SET
EventDataAsXml = 
(CASE
    WHEN [changetype] = 'BuildingPersistentLocalIdentifierWasAssigned' THEN REPLACE(REPLACE(EventDataAsXml, '</BuildingPersistentLocalIdWasAssigned>', '</BuildingPersistentLocalIdentifierWasAssigned>'), '<BuildingPersistentLocalIdWasAssigned>', '<BuildingPersistentLocalIdentifierWasAssigned>')
    WHEN [changetype] = 'BuildingUnitPersistentLocalIdentifierWasAssigned' THEN REPLACE(REPLACE(EventDataAsXml, '</BuildingUnitPersistentLocalIdWasAssigned>', '</BuildingUnitPersistentLocalIdentifierWasAssigned>'), '<BuildingUnitPersistentLocalIdWasAssigned>', '<BuildingUnitPersistentLocalIdentifierWasAssigned>')
    WHEN [changetype] = 'BuildingUnitPersistentLocalIdentifierWasDuplicated' THEN REPLACE(REPLACE(EventDataAsXml, '</BuildingUnitPersistentLocalIdWasDuplicated>', '</BuildingUnitPersistentLocalIdentifierWasDuplicated>'), '<BuildingUnitPersistentLocalIdWasDuplicated>', '<BuildingUnitPersistentLocalIdentifierWasDuplicated>')
    WHEN [changetype] = 'BuildingUnitPersistentLocalIdentifierWasRemoved' THEN REPLACE(REPLACE(EventDataAsXml, '</BuildingUnitPersistentLocalIdWasRemoved>', '</BuildingUnitPersistentLocalIdentifierWasRemoved>'), '<BuildingUnitPersistentLocalIdWasRemoved>', '<BuildingUnitPersistentLocalIdentifierWasRemoved>')
END)
 WHERE ([changetype] = 'BuildingPersistentLocalIdentifierWasAssigned' AND EventDataAsXml LIKE '<BuildingPersistentLocalIdWasAssigned>%')
    OR ([changetype] = 'BuildingUnitPersistentLocalIdentifierWasAssigned' AND EventDataAsXml LIKE '<BuildingUnitPersistentLocalIdWasAssigned>%')
    OR ([changetype] = 'BuildingUnitPersistentLocalIdentifierWasDuplicated' AND EventDataAsXml LIKE '<BuildingUnitPersistentLocalIdWasDuplicated>%')
    OR ([changetype] = 'BuildingUnitPersistentLocalIdentifierWasRemoved' AND EventDataAsXml LIKE '<BuildingUnitPersistentLocalIdWasRemoved>%')
ArneD commented 3 years ago

Address fix: https://github.com/Informatievlaanderen/address-registry/pull/214

AddressPersistentLocalIdWasAssigned => AddressPersistentLocalIdentifierWasAssigned Rebuild or SQL replace needed

ArneD commented 3 years ago

https://github.com/Informatievlaanderen/building-registry/pull/185 Building: rebuild or sql replace needed

BuildingPersistentLocalIdWasAssigned => BuildingPersistentLocalIdentifierWasAssigned
BuildingUnitPersistentLocalIdWasAssigned => BuildingUnitPersistentLocalIdentifierWasAssigned
BuildingUnitPersistentLocalIdWasDuplicated => BuildingUnitPersistentLocalIdentifierWasDuplicated
BuildingUnitPersistentLocalIdWasRemoved => BuildingUnitPersistentLocalIdentifierWasRemoved
CumpsD commented 3 years ago

We need to make sure the receiving ends also use the correct names.

To check: