From cb9c84a7aa87bcd320dcfc81513990beddf25655 Mon Sep 17 00:00:00 2001 From: jvandaal Date: Mon, 16 Sep 2024 16:48:57 +0200 Subject: [PATCH] feat: calculate for which buildings to invalidate the cache upon parcel changes --- .../Building/Count/BuildingCountHandlerV2.cs | 9 ++- .../Detail/BuildingDetailHandlerV2.cs | 1 - .../Building/List/BuildingListHandlerV2.cs | 9 ++- .../Building/Query/BuildingListOsloQueryV2.cs | 12 ++-- .../Infrastructure/Modules/ApiModule.cs | 2 +- ...ule.cs => ParcelBuildingMatchingModule.cs} | 9 ++- .../ParcelMatching/IParcelMatching.cs | 11 ---- ...ildingRegistry.Consumer.Read.Parcel.csproj | 1 + .../BuildingToInvalidate.cs | 28 ++++++++ .../ConsumerParcelContext.cs | 6 +- .../IParcelMatching.cs | 9 +++ .../Infrastructure/Program.cs | 16 +++++ .../ParcelMatching.cs | 31 +-------- .../ParcelWithCount/ConsumerParcel.cs | 6 +- .../ParcelWithCount/ParcelKafkaProjection.cs | 66 +++++++++++++++++-- .../appsettings.json | 3 +- .../BuildingMatching.cs | 60 +++++++++++++++++ .../IBuildingMatching.cs | 10 +++ .../GetUnderlyingBuildingsTests.cs | 24 +++---- .../GetUnderlyingParcelsTests.cs | 17 ++--- .../ParcelConsumerKafkaProjectionTests.cs | 2 +- 21 files changed, 241 insertions(+), 91 deletions(-) rename src/BuildingRegistry.Api.Oslo/Infrastructure/Modules/{ParcelMatchingModule.cs => ParcelBuildingMatchingModule.cs} (55%) delete mode 100644 src/BuildingRegistry.Api.Oslo/Infrastructure/ParcelMatching/IParcelMatching.cs create mode 100644 src/BuildingRegistry.Consumer.Read.Parcel/BuildingToInvalidate.cs create mode 100644 src/BuildingRegistry.Consumer.Read.Parcel/IParcelMatching.cs rename src/{BuildingRegistry.Api.Oslo/Infrastructure/ParcelMatching => BuildingRegistry.Consumer.Read.Parcel}/ParcelMatching.cs (64%) create mode 100644 src/BuildingRegistry.Projections.Legacy/BuildingMatching.cs create mode 100644 src/BuildingRegistry.Projections.Legacy/IBuildingMatching.cs diff --git a/src/BuildingRegistry.Api.Oslo/Building/Count/BuildingCountHandlerV2.cs b/src/BuildingRegistry.Api.Oslo/Building/Count/BuildingCountHandlerV2.cs index a3b23a5a0..5762b2859 100644 --- a/src/BuildingRegistry.Api.Oslo/Building/Count/BuildingCountHandlerV2.cs +++ b/src/BuildingRegistry.Api.Oslo/Building/Count/BuildingCountHandlerV2.cs @@ -7,7 +7,6 @@ namespace BuildingRegistry.Api.Oslo.Building.Count using Be.Vlaanderen.Basisregisters.Api.Search.Pagination; using Be.Vlaanderen.Basisregisters.GrAr.Legacy; using Consumer.Read.Parcel; - using Infrastructure.ParcelMatching; using MediatR; using Microsoft.EntityFrameworkCore; using Projections.Legacy; @@ -17,14 +16,14 @@ public class BuildingCountHandlerV2 : IRequestHandler Handle(BuildingCountRequest request, CancellationToken cancellationToken) @@ -32,7 +31,7 @@ public async Task Handle(BuildingCountRequest request, Can return new TotaalAantalResponse { Aantal = request.FilteringHeader.ShouldFilter - ? await new BuildingListOsloQueryV2(_legacyContext, _consumerParcelContext, _parcelMatching) + ? await new BuildingListOsloQueryV2(_legacyContext, _consumerParcelContext, _buildingMatching) .Fetch(request.FilteringHeader, request.SortingHeader, new NoPaginationRequest()) .Items .CountAsync(cancellationToken) diff --git a/src/BuildingRegistry.Api.Oslo/Building/Detail/BuildingDetailHandlerV2.cs b/src/BuildingRegistry.Api.Oslo/Building/Detail/BuildingDetailHandlerV2.cs index 8da98bc19..9e43a1928 100644 --- a/src/BuildingRegistry.Api.Oslo/Building/Detail/BuildingDetailHandlerV2.cs +++ b/src/BuildingRegistry.Api.Oslo/Building/Detail/BuildingDetailHandlerV2.cs @@ -15,7 +15,6 @@ namespace BuildingRegistry.Api.Oslo.Building.Detail using Consumer.Read.Parcel; using Converters; using Infrastructure.Options; - using Infrastructure.ParcelMatching; using MediatR; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; diff --git a/src/BuildingRegistry.Api.Oslo/Building/List/BuildingListHandlerV2.cs b/src/BuildingRegistry.Api.Oslo/Building/List/BuildingListHandlerV2.cs index e133f50a2..27ef0405c 100644 --- a/src/BuildingRegistry.Api.Oslo/Building/List/BuildingListHandlerV2.cs +++ b/src/BuildingRegistry.Api.Oslo/Building/List/BuildingListHandlerV2.cs @@ -8,7 +8,6 @@ namespace BuildingRegistry.Api.Oslo.Building.List using Converters; using Infrastructure; using Infrastructure.Options; - using Infrastructure.ParcelMatching; using MediatR; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; @@ -19,24 +18,24 @@ public class BuildingListHandlerV2 : IRequestHandler _responseOptions; public BuildingListHandlerV2( LegacyContext legacyContext, IOptions responseOptions, ConsumerParcelContext consumerParcelContext, - IParcelMatching parcelMatching) + IBuildingMatching buildingMatching) { _legacyContext = legacyContext; _responseOptions = responseOptions; _consumerParcelContext = consumerParcelContext; - _parcelMatching = parcelMatching; + _buildingMatching = buildingMatching; } public async Task Handle(BuildingListRequest request, CancellationToken cancellationToken) { - var pagedBuildings = new BuildingListOsloQueryV2(_legacyContext, _consumerParcelContext, _parcelMatching) + var pagedBuildings = new BuildingListOsloQueryV2(_legacyContext, _consumerParcelContext, _buildingMatching) .Fetch(request.FilteringHeader, request.SortingHeader, request.PaginationRequest); var buildings = await pagedBuildings.Items.ToListAsync(cancellationToken); diff --git a/src/BuildingRegistry.Api.Oslo/Building/Query/BuildingListOsloQueryV2.cs b/src/BuildingRegistry.Api.Oslo/Building/Query/BuildingListOsloQueryV2.cs index 4ec590e61..c984dedf3 100644 --- a/src/BuildingRegistry.Api.Oslo/Building/Query/BuildingListOsloQueryV2.cs +++ b/src/BuildingRegistry.Api.Oslo/Building/Query/BuildingListOsloQueryV2.cs @@ -10,7 +10,6 @@ namespace BuildingRegistry.Api.Oslo.Building.Query using Consumer.Read.Parcel; using Consumer.Read.Parcel.ParcelWithCount; using Converters; - using Infrastructure.ParcelMatching; using Microsoft.EntityFrameworkCore; using Projections.Legacy; using Projections.Legacy.BuildingDetailV2; @@ -19,14 +18,17 @@ public class BuildingListOsloQueryV2 : Query { private readonly LegacyContext _context; private readonly ConsumerParcelContext _consumerParcelContext; - private readonly IParcelMatching _parcelMatching; + private readonly IBuildingMatching _buildingMatching; protected override ISorting Sorting => new BuildingSorting(); - public BuildingListOsloQueryV2(LegacyContext context, ConsumerParcelContext consumerParcelContext, IParcelMatching parcelMatching) + public BuildingListOsloQueryV2( + LegacyContext context, + ConsumerParcelContext consumerParcelContext, + IBuildingMatching buildingMatching) { _context = context; _consumerParcelContext = consumerParcelContext; - _parcelMatching = parcelMatching; + _buildingMatching = buildingMatching; } protected override IQueryable Filter(FilteringHeader filtering) @@ -63,7 +65,7 @@ protected override IQueryable Filter(FilteringHeader x.CaPaKey == filtering.Filter.CaPaKey); if (parcel is not null && parcel.Status == ParcelStatus.Realized) { - var underlyingBuildings = _parcelMatching.GetUnderlyingBuildings(parcel.Geometry); + var underlyingBuildings = _buildingMatching.GetUnderlyingBuildings(parcel.Geometry); buildings = buildings.Where(x => underlyingBuildings.Contains(x.PersistentLocalId)); } else diff --git a/src/BuildingRegistry.Api.Oslo/Infrastructure/Modules/ApiModule.cs b/src/BuildingRegistry.Api.Oslo/Infrastructure/Modules/ApiModule.cs index 4d58a8026..5c66ff193 100644 --- a/src/BuildingRegistry.Api.Oslo/Infrastructure/Modules/ApiModule.cs +++ b/src/BuildingRegistry.Api.Oslo/Infrastructure/Modules/ApiModule.cs @@ -29,7 +29,7 @@ protected override void Load(ContainerBuilder builder) { builder .RegisterModule(new MediatRModule()) - .RegisterModule(new ParcelMatchingModule()) + .RegisterModule(new ParcelBuildingMatchingModule()) .RegisterModule(new LegacyModule(_configuration, _services, _loggerFactory)) .RegisterModule(new ConsumerParcelModule(_configuration, _services, _loggerFactory)); diff --git a/src/BuildingRegistry.Api.Oslo/Infrastructure/Modules/ParcelMatchingModule.cs b/src/BuildingRegistry.Api.Oslo/Infrastructure/Modules/ParcelBuildingMatchingModule.cs similarity index 55% rename from src/BuildingRegistry.Api.Oslo/Infrastructure/Modules/ParcelMatchingModule.cs rename to src/BuildingRegistry.Api.Oslo/Infrastructure/Modules/ParcelBuildingMatchingModule.cs index 8caa3dcfe..89d2f7d95 100644 --- a/src/BuildingRegistry.Api.Oslo/Infrastructure/Modules/ParcelMatchingModule.cs +++ b/src/BuildingRegistry.Api.Oslo/Infrastructure/Modules/ParcelBuildingMatchingModule.cs @@ -1,15 +1,20 @@ namespace BuildingRegistry.Api.Oslo.Infrastructure.Modules { using Autofac; - using ParcelMatching; + using Consumer.Read.Parcel; + using Projections.Legacy; - public class ParcelMatchingModule : Module + public class ParcelBuildingMatchingModule : Module { protected override void Load(ContainerBuilder builder) { builder .RegisterType() .AsImplementedInterfaces(); + + builder + .RegisterType() + .AsImplementedInterfaces(); } } } diff --git a/src/BuildingRegistry.Api.Oslo/Infrastructure/ParcelMatching/IParcelMatching.cs b/src/BuildingRegistry.Api.Oslo/Infrastructure/ParcelMatching/IParcelMatching.cs deleted file mode 100644 index 9b668d285..000000000 --- a/src/BuildingRegistry.Api.Oslo/Infrastructure/ParcelMatching/IParcelMatching.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace BuildingRegistry.Api.Oslo.Infrastructure.ParcelMatching -{ - using System.Collections.Generic; - using NetTopologySuite.Geometries; - - public interface IParcelMatching - { - IEnumerable GetUnderlyingParcels(byte[] buildingGeometryBytes); - IEnumerable GetUnderlyingBuildings(Geometry parcelGeometry); - } -} diff --git a/src/BuildingRegistry.Consumer.Read.Parcel/BuildingRegistry.Consumer.Read.Parcel.csproj b/src/BuildingRegistry.Consumer.Read.Parcel/BuildingRegistry.Consumer.Read.Parcel.csproj index 772a4f99d..14569a86a 100644 --- a/src/BuildingRegistry.Consumer.Read.Parcel/BuildingRegistry.Consumer.Read.Parcel.csproj +++ b/src/BuildingRegistry.Consumer.Read.Parcel/BuildingRegistry.Consumer.Read.Parcel.csproj @@ -18,6 +18,7 @@ + diff --git a/src/BuildingRegistry.Consumer.Read.Parcel/BuildingToInvalidate.cs b/src/BuildingRegistry.Consumer.Read.Parcel/BuildingToInvalidate.cs new file mode 100644 index 000000000..842bd5baf --- /dev/null +++ b/src/BuildingRegistry.Consumer.Read.Parcel/BuildingToInvalidate.cs @@ -0,0 +1,28 @@ +namespace BuildingRegistry.Consumer.Read.Parcel +{ + using BuildingRegistry.Infrastructure; + using Microsoft.EntityFrameworkCore; + using Microsoft.EntityFrameworkCore.Metadata.Builders; + + public class BuildingToInvalidate + { + public int Id { get; set; } + public int BuildingPersistentLocalId { get; set; } + } + + public class BuildingToInvalidateConfiguration : IEntityTypeConfiguration + { + public const string TableName = "BuildingsToInvalidate"; + + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(TableName, Schema.ConsumerReadParcel) + .HasKey(x => x.Id) + .IsClustered(); + + builder.Property(x => x.Id).UseIdentityColumn(1).ValueGeneratedOnAdd(); + + builder.Property(x => x.BuildingPersistentLocalId); + } + } +} diff --git a/src/BuildingRegistry.Consumer.Read.Parcel/ConsumerParcelContext.cs b/src/BuildingRegistry.Consumer.Read.Parcel/ConsumerParcelContext.cs index 4c6b4099e..2f23347d8 100644 --- a/src/BuildingRegistry.Consumer.Read.Parcel/ConsumerParcelContext.cs +++ b/src/BuildingRegistry.Consumer.Read.Parcel/ConsumerParcelContext.cs @@ -18,8 +18,10 @@ namespace BuildingRegistry.Consumer.Read.Parcel public class ConsumerParcelContext : RunnerDbContext, IParcels { - public DbSet ParcelConsumerItemsWithCount { get; set; } - public DbSet ParcelAddressItemsWithCount { get; set; } + public DbSet ParcelConsumerItemsWithCount => Set(); + public DbSet ParcelAddressItemsWithCount => Set(); + + public DbSet BuildingsToInvalidate => Set(); // This needs to be here to please EF public ConsumerParcelContext() diff --git a/src/BuildingRegistry.Consumer.Read.Parcel/IParcelMatching.cs b/src/BuildingRegistry.Consumer.Read.Parcel/IParcelMatching.cs new file mode 100644 index 000000000..d986ce09b --- /dev/null +++ b/src/BuildingRegistry.Consumer.Read.Parcel/IParcelMatching.cs @@ -0,0 +1,9 @@ +namespace BuildingRegistry.Consumer.Read.Parcel +{ + using System.Collections.Generic; + + public interface IParcelMatching + { + IEnumerable GetUnderlyingParcels(byte[] buildingGeometryBytes); + } +} diff --git a/src/BuildingRegistry.Consumer.Read.Parcel/Infrastructure/Program.cs b/src/BuildingRegistry.Consumer.Read.Parcel/Infrastructure/Program.cs index 1ff21d8c5..81b6b81ad 100644 --- a/src/BuildingRegistry.Consumer.Read.Parcel/Infrastructure/Program.cs +++ b/src/BuildingRegistry.Consumer.Read.Parcel/Infrastructure/Program.cs @@ -19,6 +19,7 @@ namespace BuildingRegistry.Consumer.Read.Parcel.Infrastructure using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using ParcelWithCount; + using Projections.Legacy; using Serilog; using Serilog.Debugging; using Serilog.Extensions.Logging; @@ -80,6 +81,16 @@ public static async Task Main(string[] args) sqlServerOptions.MigrationsHistoryTable(MigrationTables.ConsumerReadParcel, Schema.ConsumerReadParcel); sqlServerOptions.UseNetTopologySuite(); })); + + services + .AddDbContext((_, options) => options + .UseLoggerFactory(loggerFactory) + .UseSqlServer(hostContext.Configuration.GetConnectionString("LegacyProjections"), sqlServerOptions => + { + sqlServerOptions.EnableRetryOnFailure(); + sqlServerOptions.MigrationsHistoryTable(MigrationTables.Legacy, Schema.Legacy); + sqlServerOptions.UseNetTopologySuite(); + })); }) .UseServiceProviderFactory(new AutofacServiceProviderFactory()) .ConfigureContainer((hostContext, builder) => @@ -127,6 +138,11 @@ public static async Task Main(string[] args) .As() .SingleInstance(); + builder + .RegisterType() + .AsImplementedInterfaces() + .InstancePerLifetimeScope(); + builder .RegisterType() .As() diff --git a/src/BuildingRegistry.Api.Oslo/Infrastructure/ParcelMatching/ParcelMatching.cs b/src/BuildingRegistry.Consumer.Read.Parcel/ParcelMatching.cs similarity index 64% rename from src/BuildingRegistry.Api.Oslo/Infrastructure/ParcelMatching/ParcelMatching.cs rename to src/BuildingRegistry.Consumer.Read.Parcel/ParcelMatching.cs index e2c089722..e97e5f26c 100644 --- a/src/BuildingRegistry.Api.Oslo/Infrastructure/ParcelMatching/ParcelMatching.cs +++ b/src/BuildingRegistry.Consumer.Read.Parcel/ParcelMatching.cs @@ -1,22 +1,18 @@ -namespace BuildingRegistry.Api.Oslo.Infrastructure.ParcelMatching +namespace BuildingRegistry.Consumer.Read.Parcel { using System; using System.Collections.Generic; using System.Linq; - using Consumer.Read.Parcel; - using Consumer.Read.Parcel.ParcelWithCount; using NetTopologySuite.Geometries; - using Projections.Legacy; + using ParcelWithCount; public class ParcelMatching : IParcelMatching { private readonly ConsumerParcelContext _consumerParcelContext; - private readonly LegacyContext _legacyContext; - public ParcelMatching(ConsumerParcelContext consumerParcelContext, LegacyContext legacyContext) + public ParcelMatching(ConsumerParcelContext consumerParcelContext) { _consumerParcelContext = consumerParcelContext; - _legacyContext = legacyContext; } public IEnumerable GetUnderlyingParcels(byte[] buildingGeometryBytes) @@ -41,27 +37,6 @@ public IEnumerable GetUnderlyingParcels(byte[] buildingGeometryBytes) .Select(parcel => parcel.CaPaKey); } - public IEnumerable GetUnderlyingBuildings(Geometry parcelGeometry) - { - var boundingBox = parcelGeometry.Factory.ToGeometry(parcelGeometry.EnvelopeInternal); - - var underlyingBuildings = _legacyContext - .BuildingDetailsV2 - .Where(building => boundingBox.Intersects(building.SysGeometry)) - .ToList() - .Where(building => parcelGeometry.Intersects(building.SysGeometry)) - .Select(building => - new { - building.PersistentLocalId, - Overlap = CalculateOverlap(building.SysGeometry, parcelGeometry) - }) - .ToList(); - - return underlyingBuildings - .Where(building => building.Overlap >= 0.8 / underlyingBuildings.Count) - .Select(building => building.PersistentLocalId); - } - private static double CalculateOverlap(Geometry? buildingGeometry, Geometry parcel) { if (buildingGeometry is null) diff --git a/src/BuildingRegistry.Consumer.Read.Parcel/ParcelWithCount/ConsumerParcel.cs b/src/BuildingRegistry.Consumer.Read.Parcel/ParcelWithCount/ConsumerParcel.cs index 30fa6ee3f..d29595a5c 100644 --- a/src/BuildingRegistry.Consumer.Read.Parcel/ParcelWithCount/ConsumerParcel.cs +++ b/src/BuildingRegistry.Consumer.Read.Parcel/ParcelWithCount/ConsumerParcel.cs @@ -3,6 +3,7 @@ namespace BuildingRegistry.Consumer.Read.Parcel.ParcelWithCount using System; using System.Threading; using System.Threading.Tasks; + using Autofac; using Be.Vlaanderen.Basisregisters.MessageHandling.Kafka.Consumer; using Be.Vlaanderen.Basisregisters.ProjectionHandling.Connector; using Microsoft.EntityFrameworkCore; @@ -12,17 +13,20 @@ namespace BuildingRegistry.Consumer.Read.Parcel.ParcelWithCount public class ConsumerParcel : BackgroundService { private readonly IHostApplicationLifetime _hostApplicationLifetime; + private readonly ILifetimeScope _lifetimeScope; private readonly IDbContextFactory _consumerParcelDbContextFactory; private readonly IConsumer _consumer; private readonly ILogger _logger; public ConsumerParcel( IHostApplicationLifetime hostApplicationLifetime, + ILifetimeScope lifetimeScope, IDbContextFactory consumerParcelDbContextFactory, IConsumer consumer, ILoggerFactory loggerFactory) { _hostApplicationLifetime = hostApplicationLifetime; + _lifetimeScope = lifetimeScope; _consumerParcelDbContextFactory = consumerParcelDbContextFactory; _consumer = consumer; _logger = loggerFactory.CreateLogger(); @@ -32,7 +36,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) { var projector = new ConnectedProjector( - Resolve.WhenEqualToHandlerMessageType(new ParcelKafkaProjection().Handlers)); + Resolve.WhenEqualToHandlerMessageType(new ParcelKafkaProjection(_lifetimeScope).Handlers)); try { diff --git a/src/BuildingRegistry.Consumer.Read.Parcel/ParcelWithCount/ParcelKafkaProjection.cs b/src/BuildingRegistry.Consumer.Read.Parcel/ParcelWithCount/ParcelKafkaProjection.cs index 7e3ac73fc..a11ee3ade 100644 --- a/src/BuildingRegistry.Consumer.Read.Parcel/ParcelWithCount/ParcelKafkaProjection.cs +++ b/src/BuildingRegistry.Consumer.Read.Parcel/ParcelWithCount/ParcelKafkaProjection.cs @@ -1,13 +1,16 @@ namespace BuildingRegistry.Consumer.Read.Parcel.ParcelWithCount { using System; + using System.Linq; + using Autofac; using Be.Vlaanderen.Basisregisters.GrAr.Contracts.ParcelRegistry; using Be.Vlaanderen.Basisregisters.ProjectionHandling.Connector; using Be.Vlaanderen.Basisregisters.Utilities.HexByteConvertor; + using Projections.Legacy; public class ParcelKafkaProjection : ConnectedProjection { - public ParcelKafkaProjection() + public ParcelKafkaProjection(ILifetimeScope lifetimeScope) { var wkbReader = WKBReaderFactory.Create(); @@ -20,6 +23,8 @@ public ParcelKafkaProjection() if (parcel is null) { var extendedWkbGeometry = message.ExtendedWkbGeometry.ToByteArray(); + var geometry = wkbReader.Read(extendedWkbGeometry); + await context .ParcelConsumerItemsWithCount .AddAsync(new ParcelConsumerItem( @@ -27,7 +32,7 @@ await context message.CaPaKey, ParcelStatus.Parse(message.ParcelStatus), extendedWkbGeometry, - wkbReader.Read(extendedWkbGeometry), + geometry, message.IsRemoved) , ct); @@ -35,6 +40,15 @@ await context { await context.AddIdempotentParcelAddress(parcelId, addressPersistentLocalId, ct); } + + await using var scope = lifetimeScope.BeginLifetimeScope(); + var buildingMatching = scope.Resolve(); + + var buildingPersistentLocalIds = buildingMatching.GetUnderlyingBuildings(geometry); + context.BuildingsToInvalidate.AddRange(buildingPersistentLocalIds.Select(x => new BuildingToInvalidate + { + BuildingPersistentLocalId = x + })); } }); @@ -44,6 +58,15 @@ await context .ParcelConsumerItemsWithCount.FindAsync([Guid.Parse(message.ParcelId)], cancellationToken: ct); parcel!.Status = ParcelStatus.Retired; + + await using var scope = lifetimeScope.BeginLifetimeScope(); + var buildingMatching = scope.Resolve(); + + var buildingPersistentLocalIds = buildingMatching.GetUnderlyingBuildings(parcel.Geometry); + context.BuildingsToInvalidate.AddRange(buildingPersistentLocalIds.Select(x => new BuildingToInvalidate + { + BuildingPersistentLocalId = x + })); }); When(async (context, message, ct) => @@ -52,16 +75,41 @@ await context .ParcelConsumerItemsWithCount.FindAsync([Guid.Parse(message.ParcelId)], cancellationToken: ct); parcel!.Status = ParcelStatus.Realized; + + await using var scope = lifetimeScope.BeginLifetimeScope(); + var buildingMatching = scope.Resolve(); + + var buildingPersistentLocalIds = buildingMatching.GetUnderlyingBuildings(parcel.Geometry); + context.BuildingsToInvalidate.AddRange(buildingPersistentLocalIds.Select(x => new BuildingToInvalidate + { + BuildingPersistentLocalId = x + })); }); When(async (context, message, ct) => { + await using var scope = lifetimeScope.BeginLifetimeScope(); + var buildingMatching = scope.Resolve(); + var parcel = await context .ParcelConsumerItemsWithCount.FindAsync([Guid.Parse(message.ParcelId)], cancellationToken: ct); + var previousBuildingPersistentLocalIds = buildingMatching.GetUnderlyingBuildings(parcel!.Geometry).ToArray(); + var extendedWkbGeometry = message.ExtendedWkbGeometry.ToByteArray(); - parcel!.ExtendedWkbGeometry = extendedWkbGeometry; + parcel.ExtendedWkbGeometry = extendedWkbGeometry; parcel.SetGeometry(wkbReader.Read(extendedWkbGeometry)); + + var currentBuildingPersistentLocalIds = buildingMatching.GetUnderlyingBuildings(parcel.Geometry).ToArray(); + + var buildingPersistentLocalIds = previousBuildingPersistentLocalIds + .Except(currentBuildingPersistentLocalIds) + .Concat(currentBuildingPersistentLocalIds.Except(previousBuildingPersistentLocalIds)); + + context.BuildingsToInvalidate.AddRange(buildingPersistentLocalIds.Select(x => new BuildingToInvalidate + { + BuildingPersistentLocalId = x + })); }); When(async (context, message, ct) => @@ -72,6 +120,7 @@ await context if (parcel is null) { var extendedWkbGeometry = message.ExtendedWkbGeometry.ToByteArray(); + var geometry = wkbReader.Read(extendedWkbGeometry); await context .ParcelConsumerItemsWithCount @@ -80,8 +129,17 @@ await context message.CaPaKey, ParcelStatus.Realized, extendedWkbGeometry, - wkbReader.Read(extendedWkbGeometry)) + geometry) , ct); + + await using var scope = lifetimeScope.BeginLifetimeScope(); + var buildingMatching = scope.Resolve(); + + var buildingPersistentLocalIds = buildingMatching.GetUnderlyingBuildings(geometry); + context.BuildingsToInvalidate.AddRange(buildingPersistentLocalIds.Select(x => new BuildingToInvalidate + { + BuildingPersistentLocalId = x + })); } }); diff --git a/src/BuildingRegistry.Consumer.Read.Parcel/appsettings.json b/src/BuildingRegistry.Consumer.Read.Parcel/appsettings.json index 47bd0f6f8..75d9e1fba 100644 --- a/src/BuildingRegistry.Consumer.Read.Parcel/appsettings.json +++ b/src/BuildingRegistry.Consumer.Read.Parcel/appsettings.json @@ -1,7 +1,8 @@ { "ConnectionStrings": { "ConsumerParcel": "Server=(localdb)\\mssqllocaldb;Database=EFProviders.InMemory.BuildingRegistry;Trusted_Connection=True;TrustServerCertificate=True;", - "ConsumerParcelAdmin": "Server=(localdb)\\mssqllocaldb;Database=EFProviders.InMemory.BuildingRegistry;Trusted_Connection=True;TrustServerCertificate=True;" + "ConsumerParcelAdmin": "Server=(localdb)\\mssqllocaldb;Database=EFProviders.InMemory.BuildingRegistry;Trusted_Connection=True;TrustServerCertificate=True;", + "LegacyProjections": "Server=(localdb)\\mssqllocaldb;Database=EFProviders.InMemory.BuildingRegistry;Trusted_Connection=True;TrustServerCertificate=True;" }, "Kafka": { diff --git a/src/BuildingRegistry.Projections.Legacy/BuildingMatching.cs b/src/BuildingRegistry.Projections.Legacy/BuildingMatching.cs new file mode 100644 index 000000000..11660a9b0 --- /dev/null +++ b/src/BuildingRegistry.Projections.Legacy/BuildingMatching.cs @@ -0,0 +1,60 @@ +namespace BuildingRegistry.Projections.Legacy +{ + using System; + using System.Collections.Generic; + using System.Linq; + using NetTopologySuite.Geometries; + + public class BuildingMatching : IBuildingMatching + { + private readonly LegacyContext _legacyContext; + + public BuildingMatching(LegacyContext legacyContext) + { + _legacyContext = legacyContext; + } + + public IEnumerable GetUnderlyingBuildings(Geometry parcelGeometry) + { + var boundingBox = parcelGeometry.Factory.ToGeometry(parcelGeometry.EnvelopeInternal); + + var underlyingBuildings = _legacyContext + .BuildingDetailsV2 + .Where(building => boundingBox.Intersects(building.SysGeometry)) + .ToList() + .Where(building => parcelGeometry.Intersects(building.SysGeometry)) + .Select(building => + new { + building.PersistentLocalId, + Overlap = CalculateOverlap(building.SysGeometry, parcelGeometry) + }) + .ToList(); + + return underlyingBuildings + .Where(building => building.Overlap >= 0.8 / underlyingBuildings.Count) + .Select(building => building.PersistentLocalId); + } + + private static double CalculateOverlap(Geometry? buildingGeometry, Geometry parcel) + { + if (buildingGeometry is null) + { + return 0; + } + + try + { + return buildingGeometry.Intersection(parcel).Area / buildingGeometry.Area; + } + catch (TopologyException topologyException) + { + // Consider parcels that Intersect, but fail with "found non-noded intersection" on calculating, to have an overlap value of 0 + if (topologyException.Message.Contains("found non-noded intersection", StringComparison.InvariantCultureIgnoreCase)) + return 0; + + // any other TopologyException should be treated normally + throw; + } + } + } +} diff --git a/src/BuildingRegistry.Projections.Legacy/IBuildingMatching.cs b/src/BuildingRegistry.Projections.Legacy/IBuildingMatching.cs new file mode 100644 index 000000000..ddd98a7f7 --- /dev/null +++ b/src/BuildingRegistry.Projections.Legacy/IBuildingMatching.cs @@ -0,0 +1,10 @@ +namespace BuildingRegistry.Projections.Legacy +{ + using System.Collections.Generic; + using NetTopologySuite.Geometries; + + public interface IBuildingMatching + { + IEnumerable GetUnderlyingBuildings(Geometry parcelGeometry); + } +} diff --git a/test/BuildingRegistry.Tests/Oslo/ParcelMatchingTests/GetUnderlyingBuildingsTests.cs b/test/BuildingRegistry.Tests/Oslo/ParcelMatchingTests/GetUnderlyingBuildingsTests.cs index bf3111ab7..395360314 100644 --- a/test/BuildingRegistry.Tests/Oslo/ParcelMatchingTests/GetUnderlyingBuildingsTests.cs +++ b/test/BuildingRegistry.Tests/Oslo/ParcelMatchingTests/GetUnderlyingBuildingsTests.cs @@ -1,28 +1,24 @@ namespace BuildingRegistry.Tests.Oslo.ParcelMatchingTests { - using System; using System.Linq; using Api.BackOffice.Abstractions.Building; - using Api.Oslo.Infrastructure.ParcelMatching; using BackOffice; using Building; using FluentAssertions; using NetTopologySuite.Geometries; using NodaTime; + using Projections.Legacy; using Projections.Legacy.BuildingDetailV2; using Xunit; public class GetUnderlyingBuildingsTests { - private readonly FakeConsumerParcelContext _consumerParcelContext; private readonly FakeLegacyContext _legacyContext; public GetUnderlyingBuildingsTests() { - _consumerParcelContext = new FakeConsumerParcelContextFactory() - .CreateDbContext(Array.Empty()); _legacyContext = new FakeLegacyContextFactory() - .CreateDbContext(Array.Empty()); + .CreateDbContext([]); } [Fact] @@ -42,9 +38,9 @@ public void WithBuildingOverlapping100Percent_ThenReturnsTheUnderlyingBuilding() new Instant())); _legacyContext.SaveChanges(); - var parcelMatching = new ParcelMatching(_consumerParcelContext, _legacyContext); + var buildingMatching = new BuildingMatching(_legacyContext); - var result = parcelMatching.GetUnderlyingBuildings(parcelGeometry100PercentOverlap); + var result = buildingMatching.GetUnderlyingBuildings(parcelGeometry100PercentOverlap); result.Should().ContainSingle(); } @@ -66,9 +62,9 @@ public void WithBuildingLessThan80PercentOverlap_ThenReturnsNothing() new Instant())); _legacyContext.SaveChanges(); - var parcelMatching = new ParcelMatching(_consumerParcelContext, _legacyContext); + var buildingMatching = new BuildingMatching(_legacyContext); - var result = parcelMatching.GetUnderlyingBuildings(parcelGeometry); + var result = buildingMatching.GetUnderlyingBuildings(parcelGeometry); result.Should().BeEmpty(); } @@ -99,9 +95,9 @@ public void With2BuildingsAbove40PercentOverlap_ThenReturnsThe2Buildings() new Instant())); _legacyContext.SaveChanges(); - var parcelMatching = new ParcelMatching(_consumerParcelContext, _legacyContext); + var buildingMatching = new BuildingMatching(_legacyContext); - var result = parcelMatching.GetUnderlyingBuildings(parcelGeometry); + var result = buildingMatching.GetUnderlyingBuildings(parcelGeometry); result.Count().Should().Be(2); } @@ -134,9 +130,9 @@ public void With2Buildings_1Above40Percent_1Under40Percent_ThenReturns1Building( new Instant())); _legacyContext.SaveChanges(); - var parcelMatching = new ParcelMatching(_consumerParcelContext, _legacyContext); + var buildingMatching = new BuildingMatching(_legacyContext); - var result = parcelMatching.GetUnderlyingBuildings(parcelGeometry).ToList(); + var result = buildingMatching.GetUnderlyingBuildings(parcelGeometry).ToList(); result.Count().Should().Be(1); result.First().Should().Be(buildingAbove40PercentPersistentLocalId); diff --git a/test/BuildingRegistry.Tests/Oslo/ParcelMatchingTests/GetUnderlyingParcelsTests.cs b/test/BuildingRegistry.Tests/Oslo/ParcelMatchingTests/GetUnderlyingParcelsTests.cs index 0f8874fd0..3c8c87977 100644 --- a/test/BuildingRegistry.Tests/Oslo/ParcelMatchingTests/GetUnderlyingParcelsTests.cs +++ b/test/BuildingRegistry.Tests/Oslo/ParcelMatchingTests/GetUnderlyingParcelsTests.cs @@ -3,8 +3,8 @@ namespace BuildingRegistry.Tests.Oslo.ParcelMatchingTests using System; using System.Linq; using Api.BackOffice.Abstractions.Building; - using Api.Oslo.Infrastructure.ParcelMatching; using BackOffice; + using Consumer.Read.Parcel; using Consumer.Read.Parcel.ParcelWithCount; using FluentAssertions; using NetTopologySuite.Geometries; @@ -13,14 +13,11 @@ namespace BuildingRegistry.Tests.Oslo.ParcelMatchingTests public class GetUnderlyingParcelsTests { private readonly FakeConsumerParcelContext _consumerParcelContext; - private readonly FakeLegacyContext _legacyContext; public GetUnderlyingParcelsTests() { _consumerParcelContext = new FakeConsumerParcelContextFactory() - .CreateDbContext(Array.Empty()); - _legacyContext = new FakeLegacyContextFactory() - .CreateDbContext(Array.Empty()); + .CreateDbContext([]); } [Fact] @@ -39,7 +36,7 @@ public void WithParcelOverlapping100Percent_ThenReturnsTheUnderlyingParcel() _consumerParcelContext.SaveChanges(); - var parcelMatching = new ParcelMatching(_consumerParcelContext, _legacyContext); + var parcelMatching = new ParcelMatching(_consumerParcelContext); var result = parcelMatching.GetUnderlyingParcels(buildingGeometry100PercentOverlap.AsBinary()); @@ -62,7 +59,7 @@ public void WithRetiredParcelOverlapping100Percent_ThenReturnsNothing() _consumerParcelContext.SaveChanges(); - var parcelMatching = new ParcelMatching(_consumerParcelContext, _legacyContext); + var parcelMatching = new ParcelMatching(_consumerParcelContext); var result = parcelMatching.GetUnderlyingParcels(buildingGeometry100PercentOverlap.AsBinary()); @@ -85,7 +82,7 @@ public void WithParcelLessThan80PercentOverlap_ThenReturnsNothing() _consumerParcelContext.SaveChanges(); - var parcelMatching = new ParcelMatching(_consumerParcelContext, _legacyContext); + var parcelMatching = new ParcelMatching(_consumerParcelContext); var result = parcelMatching.GetUnderlyingParcels(buildingGeometry.AsBinary()); @@ -116,7 +113,7 @@ public void With2ParcelsAbove40PercentOverlap_ThenReturnsThe2Parcels() _consumerParcelContext.SaveChanges(); - var parcelMatching = new ParcelMatching(_consumerParcelContext, _legacyContext); + var parcelMatching = new ParcelMatching(_consumerParcelContext); var result = parcelMatching.GetUnderlyingParcels(buildingGeometry50PercentOverlap.AsBinary()); @@ -149,7 +146,7 @@ public void With2Parcels_1Above40Percent_1Under40Percent_ThenReturns1Parcel() _consumerParcelContext.SaveChanges(); - var parcelMatching = new ParcelMatching(_consumerParcelContext, _legacyContext); + var parcelMatching = new ParcelMatching(_consumerParcelContext); var result = parcelMatching.GetUnderlyingParcels(building.AsBinary()).ToList(); diff --git a/test/BuildingRegistry.Tests/ProjectionTests/Consumer.Parcel/ParcelConsumerKafkaProjectionTests.cs b/test/BuildingRegistry.Tests/ProjectionTests/Consumer.Parcel/ParcelConsumerKafkaProjectionTests.cs index d9c8a5864..c6ce2fd69 100644 --- a/test/BuildingRegistry.Tests/ProjectionTests/Consumer.Parcel/ParcelConsumerKafkaProjectionTests.cs +++ b/test/BuildingRegistry.Tests/ProjectionTests/Consumer.Parcel/ParcelConsumerKafkaProjectionTests.cs @@ -448,6 +448,6 @@ protected override ConsumerParcelContext CreateContext() return new ConsumerParcelContext(options); } - protected override ParcelKafkaProjection CreateProjection() => new ParcelKafkaProjection(); + protected override ParcelKafkaProjection CreateProjection() => new ParcelKafkaProjection(Container); } }