Skip to content

Commit b8a0434

Browse files
authored
Add criteria to split name into first, middle, and last. (#302)
* Add criteria to split name into first, middle, and last. * Create unit test to retrieve person data * Move the name components to its own type. * Improve the party lookup and cache mechanism * Use a type instead of object * Code refactoring * Use HasFlag instead of equality checking * Make PersonNameComponents immutable. * Use simple data type to build test data for party lookup with name component * Cache the entire result * Move the includeComponents to be a query parameter * Code refactoring * Rebuild a unit test to test name components * Use IsNullOrEmpty to simulate the actual logic * Use the array initializer syntax * Remove single spaces * Use descriptive values for the query parameter * Implement a new unit test to validate the handling of invalid query parameter values. * Enhance the logic to enable users to retrieve a person's name using a parameter with meaningful semantics * Delegate the actual filtering logic to the Apply method * Remove unnecessary "Organisation" property * Rename PartyComponentOption to PartyComponentOptions * Use the fully qualified name * Remove a test case * Improve the XML documentation * Initialize the non-nullable field * Code refactoring * Code refactoring
1 parent a293d79 commit b8a0434

21 files changed

+944
-22
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
namespace Altinn.Platform.Register.Models;
2+
3+
/// <summary>
4+
/// Specifies the components that should be included when retrieving party's information.
5+
/// </summary>
6+
[Flags]
7+
public enum PartyComponentOptions : uint
8+
{
9+
/// <summary>
10+
/// No additional components are included.
11+
/// </summary>
12+
None = 0,
13+
14+
/// <summary>
15+
/// Includes the party's first name, middle name, and last name.
16+
/// </summary>
17+
NameComponents = 1 << 0,
18+
}

src/Altinn.Platform.Models/src/Altinn.Platform.Models/Register/PartyLookup.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace Altinn.Platform.Register.Models;
77
/// Represents a lookup criteria when looking for a Party. Only one of the properties can be used at a time.
88
/// If none or more than one property have a value the lookup operation will respond with bad request.
99
/// </summary>
10-
public record PartyLookup
10+
public record PartyLookup
1111
: IValidatableObject
1212
{
1313
/// <summary>

src/Altinn.Platform.Models/src/Altinn.Platform.Models/Register/PartyName.cs

+8-1
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,15 @@ public record PartyName
2020
public string? OrgNo { get; set; }
2121

2222
/// <summary>
23-
/// Gets or sets the party name for this result
23+
/// Gets or sets the party name for this result.
2424
/// </summary>
2525
[JsonPropertyName("name")]
2626
public string? Name { get; set; }
27+
28+
/// <summary>
29+
/// Gets or sets the components of a person's name for this result.
30+
/// </summary>
31+
[JsonPropertyName("personName")]
32+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
33+
public PersonNameComponents? PersonName { get; set; }
2734
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace Altinn.Platform.Register.Models;
4+
5+
/// <summary>
6+
/// Represents the components of a person's name.
7+
/// </summary>
8+
public record PersonNameComponents
9+
{
10+
/// <summary>
11+
/// Gets or sets the first name.
12+
/// </summary>
13+
[JsonPropertyName("firstName")]
14+
public string? FirstName { get; init; }
15+
16+
/// <summary>
17+
/// Gets or sets the middle name.
18+
/// </summary>
19+
[JsonPropertyName("middleName")]
20+
public string? MiddleName { get; init; }
21+
22+
/// <summary>
23+
/// Gets or sets the sure name.
24+
/// </summary>
25+
[JsonPropertyName("lastName")]
26+
public string? LastName { get; init; }
27+
}

src/Altinn.Register/src/Altinn.Register.Core/Parties/IV1PartyService.cs

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
using V1Models = Altinn.Platform.Register.Models;
1+
using Altinn.Platform.Register.Models;
2+
3+
using V1Models = Altinn.Platform.Register.Models;
24

35
namespace Altinn.Register.Core.Parties;
46

@@ -40,12 +42,13 @@ public interface IV1PartyService
4042
IAsyncEnumerable<V1Models.Party> LookupPartiesBySSNOrOrgNos(IEnumerable<string> lookupValues, CancellationToken cancellationToken = default);
4143

4244
/// <summary>
43-
/// Lookup party names by social security number organization number.
45+
/// Lookup party names by social security number or organization number.
4446
/// </summary>
4547
/// <param name="lookupValues">The set of ssn/org.nr.</param>
48+
/// <param name="partyComponentOption">Specifies the components that should be included when retrieving party's information.</param>
4649
/// <param name="cancellationToken">The cancellation token.</param>
4750
/// <returns>An async enumerable of <see cref="V1Models.PartyName"/>.</returns>
48-
IAsyncEnumerable<V1Models.PartyName> LookupPartyNames(IEnumerable<V1Models.PartyLookup> lookupValues, CancellationToken cancellationToken = default);
51+
IAsyncEnumerable<V1Models.PartyName> LookupPartyNames(IEnumerable<PartyLookup> lookupValues, PartyComponentOptions partyComponentOption, CancellationToken cancellationToken = default);
4952

5053
/// <summary>
5154
/// Get parties by party ids.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using Altinn.Platform.Register.Models;
2+
using Altinn.Register.ModelBinding;
3+
4+
using Microsoft.OpenApi.Any;
5+
using Microsoft.OpenApi.Models;
6+
7+
using Swashbuckle.AspNetCore.SwaggerGen;
8+
9+
namespace Altinn.Register.ApiDescriptions;
10+
11+
/// <summary>
12+
/// Schema filter for <see cref="PartyComponentOptions"/>.
13+
/// </summary>
14+
public sealed class PartyComponentOptionSchemaFilter
15+
: SchemaFilter<PartyComponentOptions>
16+
{
17+
/// <inheritdoc/>
18+
protected override void Apply(OpenApiSchema schema, SchemaFilterContext context)
19+
{
20+
schema.Enum = null;
21+
schema.Format = null;
22+
schema.Type = "array";
23+
schema.Items = new OpenApiSchema
24+
{
25+
Type = "string",
26+
Enum = PartyComponentOptionModelBinder.AllowedValues.Select(v => (IOpenApiAny)new OpenApiString(v)).ToList(),
27+
};
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using CommunityToolkit.Diagnostics;
2+
3+
using Microsoft.OpenApi.Models;
4+
5+
using Swashbuckle.AspNetCore.SwaggerGen;
6+
7+
namespace Altinn.Register.ApiDescriptions;
8+
9+
/// <summary>
10+
/// Base class for schema filters that affect a specific type.
11+
/// </summary>
12+
/// <typeparam name="T">The type this schema filter affects.</typeparam>
13+
public abstract class SchemaFilter<T> : ISchemaFilter
14+
{
15+
/// <inheritdoc/>
16+
void ISchemaFilter.Apply(OpenApiSchema schema, SchemaFilterContext context)
17+
{
18+
Guard.IsNotNull(schema);
19+
Guard.IsNotNull(context);
20+
21+
if (context.Type != typeof(T))
22+
{
23+
return;
24+
}
25+
26+
Apply(schema, context);
27+
}
28+
29+
/// <summary>
30+
/// Applies the schema filter to the given schema if the context type matches the specified type <typeparamref name="T"/>.
31+
/// </summary>
32+
/// <param name="schema">The schema to apply the filter to.</param>
33+
/// <param name="context">The context in which the schema is being applied.</param>
34+
protected abstract void Apply(OpenApiSchema schema, SchemaFilterContext context);
35+
}

src/Altinn.Register/src/Altinn.Register/Clients/PartiesClient.cs

+35-11
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@
44
using System.Runtime.CompilerServices;
55
using System.Text;
66
using System.Text.Json;
7+
78
using Altinn.Platform.Register.Models;
89
using Altinn.Register.Configuration;
910
using Altinn.Register.Core.Parties;
1011
using Altinn.Register.Core.Utils;
1112
using Altinn.Register.Extensions;
13+
1214
using Microsoft.Extensions.Caching.Memory;
1315
using Microsoft.Extensions.Options;
16+
1417
using V1Models = Altinn.Platform.Register.Models;
1518

1619
namespace Altinn.Register.Clients;
@@ -199,9 +202,9 @@ public PartiesClient(HttpClient httpClient, IOptions<GeneralSettings> generalSet
199202
=> GetPartiesById(partyIds, fetchSubUnits: false, cancellationToken);
200203

201204
/// <inheritdoc />
202-
public IAsyncEnumerable<PartyName> LookupPartyNames(IEnumerable<PartyLookup> lookupValues, CancellationToken cancellationToken = default)
205+
public IAsyncEnumerable<PartyName> LookupPartyNames(IEnumerable<PartyLookup> lookupValues, PartyComponentOptions partyComponentOption, CancellationToken cancellationToken = default)
203206
{
204-
return RunInParallel(lookupValues, ProcessPartyLookupAsync, cancellationToken);
207+
return RunInParallel(lookupValues, partyComponentOption, ProcessPartyLookupAsync, cancellationToken);
205208
}
206209

207210
/// <inheritdoc />
@@ -230,24 +233,30 @@ public IAsyncEnumerable<PartyName> LookupPartyNames(IEnumerable<PartyLookup> loo
230233
_logger.LogError("Getting parties information from bridge failed with {StatusCode}", response.StatusCode);
231234
}
232235

233-
private async Task<PartyName> ProcessPartyLookupAsync(PartyLookup partyLookup, CancellationToken cancellationToken)
236+
private async Task<PartyName> ProcessPartyLookupAsync(PartyLookup partyLookup, PartyComponentOptions partyComponentOption, CancellationToken cancellationToken)
234237
{
235238
Debug.Assert(!string.IsNullOrEmpty(partyLookup.Ssn) || !string.IsNullOrEmpty(partyLookup.OrgNo));
239+
236240
string lookupValue = !string.IsNullOrEmpty(partyLookup.Ssn) ? partyLookup.Ssn : partyLookup.OrgNo!;
241+
237242
string cacheKey = $"n:{lookupValue}";
238-
string? partyName = await GetOrAddPartyNameToCacheAsync(lookupValue, cacheKey, cancellationToken);
243+
244+
PartyName? partyName = await GetOrAddPartyNameToCacheAsync(lookupValue, cacheKey, cancellationToken);
245+
246+
bool includePersonName = partyComponentOption.HasFlag(PartyComponentOptions.NameComponents);
239247

240248
return new PartyName
241249
{
242250
Ssn = partyLookup.Ssn,
243251
OrgNo = partyLookup.OrgNo,
244-
Name = partyName,
252+
Name = partyName?.Name,
253+
PersonName = includePersonName ? partyName?.PersonName : null
245254
};
246255
}
247256

248-
private async Task<string?> GetOrAddPartyNameToCacheAsync(string lookupValue, string cacheKey, CancellationToken cancellationToken)
257+
private async Task<PartyName?> GetOrAddPartyNameToCacheAsync(string lookupValue, string cacheKey, CancellationToken cancellationToken)
249258
{
250-
if (_memoryCache.TryGetValue(cacheKey, out string? partyName)
259+
if (_memoryCache.TryGetValue(cacheKey, out PartyName? partyName)
251260
&& partyName is not null)
252261
{
253262
return partyName;
@@ -268,19 +277,34 @@ private async Task<PartyName> ProcessPartyLookupAsync(PartyLookup partyLookup, C
268277

269278
if (party != null)
270279
{
271-
partyName = party.Name;
272-
_memoryCache.Set(cacheKey, party.Name, new TimeSpan(0, _cacheTimeoutForPartyNames, 0));
280+
partyName = new PartyName()
281+
{
282+
Name = party.Name
283+
};
284+
285+
if (party.Person != null)
286+
{
287+
partyName.PersonName = new()
288+
{
289+
LastName = party.Person.LastName,
290+
FirstName = party.Person.FirstName,
291+
MiddleName = party.Person.MiddleName
292+
};
293+
}
294+
295+
_memoryCache.Set(cacheKey, partyName, new TimeSpan(0, _cacheTimeoutForPartyNames, 0));
273296
}
274297

275298
return partyName;
276299
}
277300

278301
private static async IAsyncEnumerable<TResult> RunInParallel<TIn, TResult>(
279302
IEnumerable<TIn> input,
280-
Func<TIn, CancellationToken, Task<TResult>> func,
303+
PartyComponentOptions partyComponentOption,
304+
Func<TIn, PartyComponentOptions, CancellationToken, Task<TResult>> func,
281305
[EnumeratorCancellation] CancellationToken cancellationToken)
282306
{
283-
var tasks = input.Select(i => func(i, cancellationToken)).ToList();
307+
var tasks = input.Select(i => func(i, partyComponentOption, cancellationToken)).ToList();
284308

285309
while (tasks.Count > 0)
286310
{

src/Altinn.Register/src/Altinn.Register/Controllers/PartiesController.cs

+10-2
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@
22

33
using System.Diagnostics;
44
using System.Security.Claims;
5+
56
using Altinn.Platform.Register.Models;
67
using Altinn.Register.Core.Parties;
78
using Altinn.Register.Extensions;
89
using Altinn.Register.Models;
910
using Altinn.Register.Services.Interfaces;
11+
1012
using AltinnCore.Authentication.Constants;
13+
1114
using Microsoft.AspNetCore.Authorization;
1215
using Microsoft.AspNetCore.Mvc;
16+
1317
using V1Models = Altinn.Platform.Register.Models;
1418

1519
namespace Altinn.Register.Controllers
@@ -155,6 +159,7 @@ public PartiesController(IV1PartyService partyClient, IAuthorizationClient autho
155159
/// Perform a name lookup for the list of parties for the provided ids.
156160
/// </summary>
157161
/// <param name="partyNamesLookup">A list of lookup criteria. For each criteria, one and only one of the properties must be a valid value.</param>
162+
/// <param name="partyComponentOption">Specifies the components that should be included when retrieving party's information.</param>
158163
/// <param name="cancellationToken">The cancellation token.</param>
159164
/// <returns>The identified party names for the corresponding identifiers.</returns>
160165
[HttpPost("nameslookup")]
@@ -163,7 +168,10 @@ public PartiesController(IV1PartyService partyClient, IAuthorizationClient autho
163168
[ProducesResponseType(404)]
164169
[ProducesResponseType(200)]
165170
[Produces("application/json")]
166-
public async Task<ActionResult<PartyNamesLookupResult>> PostPartyNamesLookup([FromBody] PartyNamesLookup partyNamesLookup, CancellationToken cancellationToken = default)
171+
public async Task<ActionResult<PartyNamesLookupResult>> PostPartyNamesLookup(
172+
[FromBody] PartyNamesLookup partyNamesLookup,
173+
[FromQuery] PartyComponentOptions partyComponentOption = PartyComponentOptions.None,
174+
CancellationToken cancellationToken = default)
167175
{
168176
if (partyNamesLookup.Parties is null or { Count: 0 })
169177
{
@@ -173,7 +181,7 @@ public async Task<ActionResult<PartyNamesLookupResult>> PostPartyNamesLookup([Fr
173181
});
174182
}
175183

176-
List<PartyName> items = await _partyClient.LookupPartyNames(partyNamesLookup.Parties, cancellationToken).ToListAsync(cancellationToken);
184+
List<PartyName> items = await _partyClient.LookupPartyNames(partyNamesLookup.Parties, partyComponentOption, cancellationToken).ToListAsync(cancellationToken);
177185
var partyNamesLookupResult = new PartyNamesLookupResult
178186
{
179187
PartyNames = items
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using Altinn.Register.ModelBinding;
2+
3+
using Microsoft.AspNetCore.Mvc.ModelBinding;
4+
5+
namespace Altinn.Register.Extensions;
6+
7+
/// <summary>
8+
/// Extension methods for <see cref="IList{T}"/> of <see cref="IModelBinderProvider"/>.
9+
/// </summary>
10+
internal static class ModelBinderProviderListExtensions
11+
{
12+
/// <summary>
13+
/// Inserts a <see cref="ISingleton{TSelf}"/> binder provider into the list at the specified index.
14+
/// </summary>
15+
/// <typeparam name="T">The binder type.</typeparam>
16+
/// <param name="list">The list of <see cref="IModelBinderProvider"/>.</param>
17+
/// <param name="index">The zero-based index at which the binder provider should be inserted.</param>
18+
/// <returns>The modified list, for chaining.</returns>
19+
/// <exception cref="ArgumentNullException">Thrown if <paramref name="list"/> is null.</exception>
20+
public static IList<IModelBinderProvider> InsertSingleton<T>(this IList<IModelBinderProvider> list, int index)
21+
where T : IModelBinderProvider, ISingleton<T>
22+
{
23+
ArgumentNullException.ThrowIfNull(list);
24+
25+
var instance = T.Instance;
26+
list.Insert(index, instance);
27+
return list;
28+
}
29+
}

0 commit comments

Comments
 (0)