Skip to content

Commit

Permalink
Merge branch 'main' into performance/create_and_search_dialog
Browse files Browse the repository at this point in the history
  • Loading branch information
dagfinno authored Oct 28, 2024
2 parents afc33c4 + 866eaae commit 091ec36
Show file tree
Hide file tree
Showing 29 changed files with 847 additions and 192 deletions.
140 changes: 70 additions & 70 deletions docs/schema/V1/swagger.verified.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
using Digdir.Domain.Dialogporten.Domain.Common;

namespace Digdir.Domain.Dialogporten.Application.Common.Extensions;

internal static class ReadOnlyCollectionExtensions
{
private const int Cycle = int.MaxValue;

/// <summary>
/// Validates the reference hierarchy in a collection of entities, checking for depth, cyclic references, and width violations.
/// </summary>
/// <typeparam name="TEntity">The type of the entities in the collection.</typeparam>
/// <typeparam name="TKey">The type of the key used to identify entities.</typeparam>
/// <param name="entities">The collection of entities to validate.</param>
/// <param name="keySelector">A function to select the key for each entity.</param>
/// <param name="parentKeySelector">A function to select the parent key for each entity.</param>
/// <param name="propertyName">The name of the property being validated.</param>
/// <param name="maxDepth">The maximum allowed depth of the hierarchy. Default is 100.</param>
/// <param name="maxWidth">The maximum allowed width of the hierarchy. Default is 1.</param>
/// <returns>A list of <see cref="DomainFailure"/> objects representing any validation errors found.</returns>
/// <exception cref="InvalidOperationException">Thrown if an entity's parent key is not found in the collection.</exception>
/// <exception cref="InvalidOperationException">Thrown if an entity's <paramref name="keySelector"/> returns default <typeparamref name="TKey"/>.</exception>
public static List<DomainFailure> ValidateReferenceHierarchy<TEntity, TKey>(
this IReadOnlyCollection<TEntity> entities,
Func<TEntity, TKey> keySelector,
Func<TEntity, TKey?> parentKeySelector,
string propertyName,
int maxDepth = 100,
int maxWidth = 1)
where TKey : struct
{
entities.Select(keySelector).EnsureNonDefaultTKey();

var maxDepthViolation = maxDepth + 1;
var type = typeof(TEntity);
var errors = new List<DomainFailure>();

var invalidReferences = GetInvalidReferences(entities, keySelector, parentKeySelector);
if (invalidReferences.Count > 0)
{
var ids = $"[{string.Join(",", invalidReferences)}]";
errors.Add(new DomainFailure(propertyName,
$"Hierarchy reference violation found. " +
$"{type.Name} with the following referenced ids does not exist: {ids}."));

return errors;
}

var depthByKey = entities
.ToDictionary(keySelector)
.ToDepthByKey(keySelector, parentKeySelector);

var depthErrors = depthByKey
.Where(x => x.Value == maxDepthViolation)
.ToList();

var cycleErrors = depthByKey
.Where(x => x.Value == Cycle)
.ToList();

var widthErrors = entities
.Where(x => parentKeySelector(x) is not null)
.GroupBy(parentKeySelector)
.Where(x => x.Count() > maxWidth)
.ToList();

if (depthErrors.Count > 0)
{
var ids = $"[{string.Join(",", depthErrors.Select(x => x.Key))}]";
errors.Add(new DomainFailure(propertyName,
$"Hierarchy depth violation found. {type.Name} with the following " +
$"ids is at depth {maxDepthViolation}, exceeding the max allowed depth of {maxDepth}. " +
$"It, and all its referencing children is in violation of the depth constraint. {ids}."));
}

if (cycleErrors.Count > 0)
{
var firstTenFailedIds = cycleErrors.Take(10).Select(x => x.Key).ToList();
var cycleCutOffInfo = cycleErrors.Count > 10 ? " (showing first 10)" : string.Empty;

var joinedIds = $"[{string.Join(",", firstTenFailedIds)}]";
errors.Add(new DomainFailure(propertyName,
$"Hierarchy cyclic reference violation found. {type.Name} with the " +
$"following ids is part of a cyclic reference chain{cycleCutOffInfo}: {joinedIds}."));
}

if (widthErrors.Count > 0)
{
var ids = $"[{string.Join(",", widthErrors.Select(x => x.Key))}]";
errors.Add(new DomainFailure(propertyName,
$"Hierarchy width violation found. '{type.Name}' with the following " +
$"ids has to many referring {type.Name}, exceeding the max " +
$"allowed width of {maxWidth}: {ids}."));
}

return errors;
}

private static List<TKey> GetInvalidReferences<TEntity, TKey>(IReadOnlyCollection<TEntity> entities,
Func<TEntity, TKey> keySelector,
Func<TEntity, TKey?> parentKeySelector) where TKey : struct => entities
.Where(x => parentKeySelector(x).HasValue)
.Select(x => parentKeySelector(x)!.Value)
.Except(entities.Select(keySelector))
.ToList();

private static Dictionary<TKey, int> ToDepthByKey<TKey, TEntity>(
this Dictionary<TKey, TEntity> transmissionById,
Func<TEntity, TKey> keySelector,
Func<TEntity, TKey?> parentKeySelector)
where TKey : struct
{
var depthByKey = new Dictionary<TKey, int>();
var breadCrumbs = new HashSet<TKey>();
foreach (var (_, current) in transmissionById)
{
GetDepth(current, transmissionById, keySelector, parentKeySelector, depthByKey, breadCrumbs);
}

return depthByKey;
}

private static int GetDepth<TEntity, TKey>(TEntity current,
Dictionary<TKey, TEntity> entitiesById,
Func<TEntity, TKey> keySelector,
Func<TEntity, TKey?> parentKeySelector,
Dictionary<TKey, int> cachedDepthByVisited,
HashSet<TKey> breadCrumbs)
where TKey : struct
{
var key = keySelector(current);
if (breadCrumbs.Contains(key))
{
return Cycle;
}

if (cachedDepthByVisited.TryGetValue(key, out var cachedDepth))
{
return cachedDepth;
}

breadCrumbs.Add(key);
var parentKey = parentKeySelector(current);
var parentDepth = !parentKey.HasValue ? 0
: entitiesById.TryGetValue(parentKey.Value, out var parent)
? GetDepth(parent, entitiesById, keySelector, parentKeySelector, cachedDepthByVisited, breadCrumbs)
: throw new InvalidOperationException(
$"{nameof(entitiesById)} does not contain expected " +
$"key '{parentKey.Value}'.");

breadCrumbs.Remove(key);
return cachedDepthByVisited[key] = parentDepth == Cycle ? Cycle : ++parentDepth;
}

private static void EnsureNonDefaultTKey<TKey>(this IEnumerable<TKey> keys) where TKey : struct
{
if (keys.Any(key => EqualityComparer<TKey>.Default.Equals(key, default)))
{
throw new InvalidOperationException("All keys must be non-default.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Digdir.Domain.Dialogporten.Application.Features.V1.Common.Content;
public sealed class ContentValueDto
{
/// <summary>
/// A list of localizations for the content
/// A list of localizations for the content.
/// </summary>
public List<LocalizationDto> Value { get; set; } = [];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace Digdir.Domain.Dialogporten.Application.Features.V1.Common.Content;

/// <summary>
/// TODO: Discuss this with the team later. It works for now
/// This class is used to map bewteen the incoming dto object and the internal dialog content structure.
/// This class is used to map between the incoming dto object and the internal dialog content structure.
/// Value needs to be mapped from a list of LocalizationDto in order for merging to work.
/// </summary>
internal sealed class IntermediateDialogContent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace Digdir.Domain.Dialogporten.Application.Features.V1.Common.Content;

/// <summary>
/// TODO: Discuss this with the team later. It works for now
/// This class is used to map bewteen the incoming dto object and the internal transmission content structure.
/// This class is used to map between the incoming dto object and the internal transmission content structure.
/// Value needs to be mapped from a list of LocalizationDto in order for merging to work.
///
/// We might want to consider combining this class with DialogContentInputConverter later.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ public sealed class LocalizationDto
private readonly string _languageCode = null!;

/// <summary>
/// The localized text or URI reference
/// The localized text or URI reference.
/// </summary>
public required string Value { get; init; }

/// <summary>
/// The language code of the localization in ISO 639-1 format
/// The language code of the localization in ISO 639-1 format.
/// </summary>
/// <example>nb</example>
public required string LanguageCode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public sealed class GetDialogTransmissionDto

/// <summary>
/// Flag indicating if the authenticated user is authorized for this transmission. If not, embedded content and
/// the attachments will not be available
/// the attachments will not be available.
/// </summary>
public bool IsAuthorized { get; set; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public sealed class SearchDialogTransmissionDto

/// <summary>
/// Flag indicating if the authenticated user is authorized for this transmission. If not, embedded content and
/// the attachments will not be available
/// the attachments will not be available.
/// </summary>
public bool IsAuthorized { get; set; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ public sealed class GetDialogDto
public string ServiceResource { get; set; } = null!;

/// <summary>
/// The ServiceResource type, as defined in Altinn Resource Registry (see ResourceType)
/// The ServiceResource type, as defined in Altinn Resource Registry (see ResourceType).
/// </summary>
public string ServiceResourceType { get; set; } = null!;

/// <summary>
/// The party code representing the organization or person that the dialog belongs to in URN format
/// The party code representing the organization or person that the dialog belongs to in URN format.
/// </summary>
/// <example>
/// urn:altinn:person:identifier-no:01125512345
Expand All @@ -59,7 +59,7 @@ public sealed class GetDialogDto
public int? Progress { get; set; }

/// <summary>
/// Optional process identifier used to indicate a business process this dialog belongs to
/// Optional process identifier used to indicate a business process this dialog belongs to.
/// </summary>
public string? Process { get; set; }

Expand Down Expand Up @@ -122,23 +122,23 @@ public sealed class GetDialogDto
public SystemLabel.Values SystemLabel { get; set; }

/// <summary>
/// The dialog unstructured text content
/// The dialog unstructured text content.
/// </summary>
public GetDialogContentDto Content { get; set; } = null!;

/// <summary>
/// The dialog token. May be used (if supported) against external URLs referred to in this dialog's apiActions,
/// transmissions or attachments. Should also be used for front-channel embeds.
/// transmissions or attachments. It should also be used for front-channel embeds.
/// </summary>
public string? DialogToken { get; set; }

/// <summary>
/// The attachments associated with the dialog (on an aggregate level)
/// The attachments associated with the dialog (on an aggregate level).
/// </summary>
public List<GetDialogDialogAttachmentDto> Attachments { get; set; } = [];

/// <summary>
/// The immutable list of transmissions associated with the dialog
/// The immutable list of transmissions associated with the dialog.
/// </summary>
public List<GetDialogDialogTransmissionDto> Transmissions { get; set; } = [];

Expand Down Expand Up @@ -193,7 +193,7 @@ public sealed class GetDialogDialogTransmissionDto

/// <summary>
/// Flag indicating if the authenticated user is authorized for this transmission. If not, embedded content and
/// the attachments will not be available
/// the attachments will not be available.
/// </summary>
public bool IsAuthorized { get; set; }

Expand All @@ -220,12 +220,12 @@ public sealed class GetDialogDialogTransmissionDto
public GetDialogDialogTransmissionSenderActorDto Sender { get; set; } = null!;

/// <summary>
/// The transmission unstructured text content
/// The transmission unstructured text content.
/// </summary>
public GetDialogDialogTransmissionContentDto Content { get; set; } = null!;

/// <summary>
/// The transmission-level attachments
/// The transmission-level attachments.
/// </summary>
public List<GetDialogDialogTransmissionAttachmentDto> Attachments { get; set; } = [];
}
Expand All @@ -238,12 +238,12 @@ public sealed class GetDialogDialogSeenLogDto
public Guid Id { get; set; }

/// <summary>
/// The timestamp when the dialog revision was seen
/// The timestamp when the dialog revision was seen.
/// </summary>
public DateTimeOffset SeenAt { get; set; }

/// <summary>
/// The actor that saw the dialog revision
/// The actor that saw the dialog revision.
/// </summary>
public GetDialogDialogSeenLogSeenByActorDto SeenBy { get; set; } = null!;

Expand Down Expand Up @@ -418,7 +418,7 @@ public sealed class GetDialogDialogApiActionDto

/// <summary>
/// String identifier for the action, corresponding to the "action" attributeId used in the XACML service policy,
/// which by default is the policy belonging to the service referred to by "serviceResource" in the dialog
/// which by default is the policy belonging to the service referred to by "serviceResource" in the dialog.
/// </summary>
/// <example>write</example>
public string Action { get; set; } = null!;
Expand Down Expand Up @@ -518,7 +518,7 @@ public sealed class GetDialogDialogGuiActionDto
public Guid Id { get; set; }

/// <summary>
/// The action identifier for the action, corresponding to the "action" attributeId used in the XACML service policy,
/// The action identifier for the action, corresponding to the "action" attributeId used in the XACML service policy.
/// </summary>
public string Action { get; set; } = null!;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace Digdir.Domain.Dialogporten.Application.Features.V1.EndUser.Dialogs.Que
public sealed class SearchDialogDto : SearchDialogDtoBase
{
/// <summary>
/// The content of the dialog in search results
/// The content of the dialog in search results.
/// </summary>
[JsonPropertyOrder(100)] // ILU MAGNUS
public SearchDialogContentDto Content { get; set; } = null!;
Expand Down Expand Up @@ -37,8 +37,8 @@ public sealed class SearchDialogContentDto
}

/// <summary>
/// TOOD: Discuss this with the team later. It works for now
/// This class is used in order to keep using ProjectTo and existing PaginationList code.
/// TODO: Discuss this with the team later. It works for now
/// This class is used to keep using ProjectTo and existing PaginationList code.
/// We first map to this using ProjectTo, then map to the new DialogContent structure
/// in the SearchDialog handlers, after EF core is done loading the data.
/// Then we create a new PaginatedList with the outwards facing dto
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ public class SearchDialogDtoBase
public string ServiceResource { get; set; } = null!;

/// <summary>
/// The ServiceResource type, as defined in Altinn Resource Registry (see ResourceType)
/// The ServiceResource type, as defined in Altinn Resource Registry (see ResourceType).
/// </summary>
public string ServiceResourceType { get; set; } = null!;

/// <summary>
/// The party code representing the organization or person that the dialog belongs to in URN format
/// The party code representing the organization or person that the dialog belongs to in URN format.
/// </summary>
/// <example>
/// urn:altinn:person:identifier-no:01125512345
Expand All @@ -48,12 +48,12 @@ public class SearchDialogDtoBase
public int? Progress { get; set; }

/// <summary>
/// Optional process identifier used to indicate a business process this dialog belongs to
/// Optional process identifier used to indicate a business process this dialog belongs to.
/// </summary>
public string? Process { get; set; }

/// <summary>
/// Optional preceding process identifier to indicate the business process that preceded the process indicated in the "Process" field. Cannot be set without also "Process" being set.
/// Optional preceding process identifier to indicate the business process that preceded the process indicated in the "Process" field. Cannot be set without also "Process" being set.
/// </summary>
public string? PrecedingProcess { get; set; }

Expand Down Expand Up @@ -117,12 +117,12 @@ public sealed class SearchDialogDialogSeenLogDto
public Guid Id { get; set; }

/// <summary>
/// The timestamp when the dialog revision was seen
/// The timestamp when the dialog revision was seen.
/// </summary>
public DateTimeOffset SeenAt { get; set; }

/// <summary>
/// The actor that saw the dialog revision
/// The actor that saw the dialog revision.
/// </summary>
public SearchDialogDialogSeenLogSeenByActorDto SeenBy { get; set; } = null!;

Expand Down
Loading

0 comments on commit 091ec36

Please sign in to comment.