Skip to content

Commit 1069cc8

Browse files
committed
Record associations to the database
Signed-off-by: Victor Chang <[email protected]>
1 parent 0749690 commit 1069cc8

File tree

15 files changed

+512
-3
lines changed

15 files changed

+512
-3
lines changed

src/Api/DicomAssociationInfo.cs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2022 MONAI Consortium
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
using System;
18+
19+
namespace Monai.Deploy.InformaticsGateway.Api
20+
{
21+
public class DicomAssociationInfo : MongoDBEntityBase
22+
{
23+
public DateTime DateTimeDisconnected { get; set; }
24+
public string CorrelationId { get; set; }
25+
public int FileCount { get; private set; }
26+
public string CallingAeTitle { get; set; }
27+
public string CalledAeTitle { get; set; }
28+
public string RemoteHost { get; set; }
29+
public int RemotePort { get; set; }
30+
public string Errors { get; set; }
31+
public TimeSpan Duration { get; private set; }
32+
33+
public DicomAssociationInfo()
34+
{
35+
FileCount = 0;
36+
}
37+
38+
public void FileReceived()
39+
{
40+
FileCount++;
41+
}
42+
43+
public void Disconnect()
44+
{
45+
DateTimeDisconnected = DateTime.UtcNow;
46+
Duration = DateTimeDisconnected.Subtract(DateTimeCreated);
47+
}
48+
}
49+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright 2022 MONAI Consortium
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
using Monai.Deploy.InformaticsGateway.Api;
18+
19+
namespace Monai.Deploy.InformaticsGateway.Database.Api.Repositories
20+
{
21+
public interface IDicomAssociationInfoRepository
22+
{
23+
Task<List<DicomAssociationInfo>> ToListAsync(CancellationToken cancellationToken = default);
24+
25+
Task<DicomAssociationInfo> AddAsync(DicomAssociationInfo item, CancellationToken cancellationToken = default);
26+
27+
}
28+
}

src/Database/DatabaseManager.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ public static IServiceCollection ConfigureDatabase(this IServiceCollection servi
5454
services.AddScoped(typeof(ISourceApplicationEntityRepository), typeof(EntityFramework.Repositories.SourceApplicationEntityRepository));
5555
services.AddScoped(typeof(IStorageMetadataRepository), typeof(EntityFramework.Repositories.StorageMetadataWrapperRepository));
5656
services.AddScoped(typeof(IPayloadRepository), typeof(EntityFramework.Repositories.PayloadRepository));
57+
services.AddScoped(typeof(IDicomAssociationInfoRepository), typeof(EntityFramework.Repositories.DicomAssociationInfoRepository));
5758
return services;
5859
case DbType_MongoDb:
5960
services.AddSingleton<IMongoClient, MongoClient>(s => new MongoClient(connectionStringConfigurationSection[SR.DatabaseConnectionStringKey]));
@@ -65,6 +66,7 @@ public static IServiceCollection ConfigureDatabase(this IServiceCollection servi
6566
services.AddScoped(typeof(ISourceApplicationEntityRepository), typeof(MongoDB.Repositories.SourceApplicationEntityRepository));
6667
services.AddScoped(typeof(IStorageMetadataRepository), typeof(MongoDB.Repositories.StorageMetadataWrapperRepository));
6768
services.AddScoped(typeof(IPayloadRepository), typeof(MongoDB.Repositories.PayloadRepository));
69+
services.AddScoped(typeof(IDicomAssociationInfoRepository), typeof(MongoDB.Repositories.DicomAssociationInfoRepository));
6870

6971
return services;
7072
default:
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2021-2022 MONAI Consortium
3+
* Copyright 2021 NVIDIA Corporation
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
using Microsoft.EntityFrameworkCore;
19+
using Microsoft.EntityFrameworkCore.Metadata.Builders;
20+
using Monai.Deploy.InformaticsGateway.Api;
21+
22+
namespace Monai.Deploy.InformaticsGateway.Database.EntityFramework.Configuration
23+
{
24+
internal class DicomAssociationInfoConfiguration : IEntityTypeConfiguration<DicomAssociationInfo>
25+
{
26+
public void Configure(EntityTypeBuilder<DicomAssociationInfo> builder)
27+
{
28+
builder.HasKey(j => j.Id);
29+
builder.Property(j => j.CalledAeTitle).IsRequired();
30+
builder.Property(j => j.CalledAeTitle).IsRequired();
31+
builder.Property(j => j.DateTimeCreated).IsRequired();
32+
builder.Property(j => j.DateTimeDisconnected).IsRequired();
33+
builder.Property(j => j.CorrelationId).IsRequired();
34+
builder.Property(j => j.FileCount).IsRequired();
35+
builder.Property(j => j.RemoteHost).IsRequired();
36+
builder.Property(j => j.RemotePort).IsRequired();
37+
}
38+
}
39+
}

src/Database/EntityFramework/InformaticsGatewayContext.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public InformaticsGatewayContext(DbContextOptions<InformaticsGatewayContext> opt
4040
public virtual DbSet<InferenceRequest> InferenceRequests { get; set; }
4141
public virtual DbSet<Payload> Payloads { get; set; }
4242
public virtual DbSet<StorageMetadataWrapper> StorageMetadataWrapperEntities { get; set; }
43+
public virtual DbSet<DicomAssociationInfo> DicomAssociationHistories { get; set; }
4344

4445
protected override void OnModelCreating(ModelBuilder modelBuilder)
4546
{
@@ -51,6 +52,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
5152
modelBuilder.ApplyConfiguration(new InferenceRequestConfiguration());
5253
modelBuilder.ApplyConfiguration(new PayloadConfiguration());
5354
modelBuilder.ApplyConfiguration(new StorageMetadataWrapperEntityConfiguration());
55+
modelBuilder.ApplyConfiguration(new DicomAssociationInfoConfiguration());
5456
}
5557

5658
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright 2022 MONAI Consortium
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
using Ardalis.GuardClauses;
18+
using Microsoft.EntityFrameworkCore;
19+
using Microsoft.Extensions.DependencyInjection;
20+
using Microsoft.Extensions.Logging;
21+
using Microsoft.Extensions.Options;
22+
using Monai.Deploy.InformaticsGateway.Api;
23+
using Monai.Deploy.InformaticsGateway.Configuration;
24+
using Monai.Deploy.InformaticsGateway.Database.Api.Logging;
25+
using Monai.Deploy.InformaticsGateway.Database.Api.Repositories;
26+
using Polly;
27+
using Polly.Retry;
28+
29+
namespace Monai.Deploy.InformaticsGateway.Database.EntityFramework.Repositories
30+
{
31+
public class DicomAssociationInfoRepository : IDicomAssociationInfoRepository, IDisposable
32+
{
33+
private readonly ILogger<DicomAssociationInfoRepository> _logger;
34+
private readonly IServiceScope _scope;
35+
private readonly InformaticsGatewayContext _informaticsGatewayContext;
36+
private readonly AsyncRetryPolicy _retryPolicy;
37+
private readonly DbSet<DicomAssociationInfo> _dataset;
38+
private bool _disposedValue;
39+
40+
public DicomAssociationInfoRepository(
41+
IServiceScopeFactory serviceScopeFactory,
42+
ILogger<DicomAssociationInfoRepository> logger,
43+
IOptions<InformaticsGatewayConfiguration> options)
44+
{
45+
Guard.Against.Null(serviceScopeFactory);
46+
Guard.Against.Null(options);
47+
48+
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
49+
50+
_scope = serviceScopeFactory.CreateScope();
51+
_informaticsGatewayContext = _scope.ServiceProvider.GetRequiredService<InformaticsGatewayContext>();
52+
_retryPolicy = Policy.Handle<Exception>().WaitAndRetryAsync(
53+
options.Value.Database.Retries.RetryDelays,
54+
(exception, timespan, count, context) => _logger.DatabaseErrorRetry(timespan, count, exception));
55+
_dataset = _informaticsGatewayContext.Set<DicomAssociationInfo>();
56+
}
57+
58+
public async Task<DicomAssociationInfo> AddAsync(DicomAssociationInfo item, CancellationToken cancellationToken = default)
59+
{
60+
Guard.Against.Null(item);
61+
62+
return await _retryPolicy.ExecuteAsync(async () =>
63+
{
64+
var result = await _dataset.AddAsync(item, cancellationToken).ConfigureAwait(false);
65+
await _informaticsGatewayContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
66+
return result.Entity;
67+
}).ConfigureAwait(false);
68+
}
69+
70+
public async Task<List<DicomAssociationInfo>> ToListAsync(CancellationToken cancellationToken = default)
71+
{
72+
return await _retryPolicy.ExecuteAsync(async () =>
73+
{
74+
return await _dataset.ToListAsync(cancellationToken).ConfigureAwait(false);
75+
}).ConfigureAwait(false);
76+
}
77+
78+
protected virtual void Dispose(bool disposing)
79+
{
80+
if (!_disposedValue)
81+
{
82+
if (disposing)
83+
{
84+
_informaticsGatewayContext.Dispose();
85+
_scope.Dispose();
86+
}
87+
88+
_disposedValue = true;
89+
}
90+
}
91+
92+
public void Dispose()
93+
{
94+
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
95+
Dispose(disposing: true);
96+
GC.SuppressFinalize(this);
97+
}
98+
}
99+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Copyright 2022 MONAI Consortium
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
using Microsoft.EntityFrameworkCore;
18+
using Microsoft.Extensions.DependencyInjection;
19+
using Microsoft.Extensions.Logging;
20+
using Microsoft.Extensions.Options;
21+
using Monai.Deploy.InformaticsGateway.Api;
22+
using Monai.Deploy.InformaticsGateway.Configuration;
23+
using Monai.Deploy.InformaticsGateway.Database.EntityFramework.Repositories;
24+
using Moq;
25+
26+
namespace Monai.Deploy.InformaticsGateway.Database.EntityFramework.Test
27+
{
28+
[Collection("SqliteDatabase")]
29+
public class DicomAssociationInfoRepositoryTest
30+
{
31+
private readonly SqliteDatabaseFixture _databaseFixture;
32+
33+
private readonly Mock<IServiceScopeFactory> _serviceScopeFactory;
34+
private readonly Mock<ILogger<DicomAssociationInfoRepository>> _logger;
35+
private readonly IOptions<InformaticsGatewayConfiguration> _options;
36+
37+
private readonly Mock<IServiceScope> _serviceScope;
38+
private readonly IServiceProvider _serviceProvider;
39+
40+
public DicomAssociationInfoRepositoryTest(SqliteDatabaseFixture databaseFixture)
41+
{
42+
_databaseFixture = databaseFixture ?? throw new ArgumentNullException(nameof(databaseFixture));
43+
_databaseFixture.InitDatabaseWithDicomAssociationInfoEntries();
44+
45+
_serviceScopeFactory = new Mock<IServiceScopeFactory>();
46+
_logger = new Mock<ILogger<DicomAssociationInfoRepository>>();
47+
_options = Options.Create(new InformaticsGatewayConfiguration());
48+
49+
_serviceScope = new Mock<IServiceScope>();
50+
var services = new ServiceCollection();
51+
services.AddScoped(p => _logger.Object);
52+
services.AddScoped(p => databaseFixture.DatabaseContext);
53+
54+
_serviceProvider = services.BuildServiceProvider();
55+
_serviceScopeFactory.Setup(p => p.CreateScope()).Returns(_serviceScope.Object);
56+
_serviceScope.Setup(p => p.ServiceProvider).Returns(_serviceProvider);
57+
58+
_options.Value.Database.Retries.DelaysMilliseconds = new[] { 1, 1, 1 };
59+
_logger.Setup(p => p.IsEnabled(It.IsAny<LogLevel>())).Returns(true);
60+
}
61+
62+
[Fact]
63+
public async Task GivenADicomAssociationInfo_WhenAddingToDatabase_ExpectItToBeSaved()
64+
{
65+
var association = new DicomAssociationInfo { CalledAeTitle = "called", CallingAeTitle = "calling", CorrelationId = Guid.NewGuid().ToString(), DateTimeCreated = DateTime.UtcNow, RemoteHost = "host", RemotePort = 100 };
66+
association.FileReceived();
67+
association.FileReceived();
68+
association.FileReceived();
69+
association.Disconnect();
70+
71+
var store = new DicomAssociationInfoRepository(_serviceScopeFactory.Object, _logger.Object, _options);
72+
await store.AddAsync(association).ConfigureAwait(false);
73+
var actual = await _databaseFixture.DatabaseContext.Set<DicomAssociationInfo>().FirstOrDefaultAsync(p => p.Id.Equals(association.Id)).ConfigureAwait(false);
74+
75+
Assert.NotNull(actual);
76+
Assert.Equal(association.DateTimeCreated, actual!.DateTimeCreated);
77+
Assert.Equal(association.DateTimeDisconnected, actual!.DateTimeDisconnected);
78+
Assert.Equal(association.FileCount, actual!.FileCount);
79+
Assert.Equal(association.Duration, actual!.Duration);
80+
Assert.Equal(association.CalledAeTitle, actual!.CalledAeTitle);
81+
Assert.Equal(association.CallingAeTitle, actual!.CallingAeTitle);
82+
Assert.Equal(association.CorrelationId, actual!.CorrelationId);
83+
}
84+
85+
[Fact]
86+
public async Task GivenDestinationApplicationEntitiesInTheDatabase_WhenToListIsCalled_ExpectAllEntitiesToBeReturned()
87+
{
88+
var store = new DicomAssociationInfoRepository(_serviceScopeFactory.Object, _logger.Object, _options);
89+
90+
var expected = await _databaseFixture.DatabaseContext.Set<DicomAssociationInfo>().ToListAsync().ConfigureAwait(false);
91+
var actual = await store.ToListAsync().ConfigureAwait(false);
92+
93+
Assert.Equal(expected, actual);
94+
}
95+
}
96+
}

src/Database/EntityFramework/Test/SqliteDatabaseFixture.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,25 @@ public void InitDatabaseWithSourceApplicationEntities()
9898
DatabaseContext.SaveChanges();
9999
}
100100

101+
internal void InitDatabaseWithDicomAssociationInfoEntries()
102+
{
103+
var da1 = new DicomAssociationInfo { CalledAeTitle = Guid.NewGuid().ToString(), CallingAeTitle = Guid.NewGuid().ToString(), CorrelationId = Guid.NewGuid().ToString(), RemoteHost = "host", RemotePort = 123 };
104+
var da2 = new DicomAssociationInfo { CalledAeTitle = Guid.NewGuid().ToString(), CallingAeTitle = Guid.NewGuid().ToString(), CorrelationId = Guid.NewGuid().ToString(), RemoteHost = "host", RemotePort = 123 };
105+
var da3 = new DicomAssociationInfo { CalledAeTitle = Guid.NewGuid().ToString(), CallingAeTitle = Guid.NewGuid().ToString(), CorrelationId = Guid.NewGuid().ToString(), RemoteHost = "host", RemotePort = 123 };
106+
var da4 = new DicomAssociationInfo { CalledAeTitle = Guid.NewGuid().ToString(), CallingAeTitle = Guid.NewGuid().ToString(), CorrelationId = Guid.NewGuid().ToString(), RemoteHost = "host", RemotePort = 123 };
107+
var da5 = new DicomAssociationInfo { CalledAeTitle = Guid.NewGuid().ToString(), CallingAeTitle = Guid.NewGuid().ToString(), CorrelationId = Guid.NewGuid().ToString(), RemoteHost = "host", RemotePort = 123 };
108+
109+
var set = DatabaseContext.Set<DicomAssociationInfo>();
110+
set.RemoveRange(set.ToList());
111+
set.Add(da1);
112+
set.Add(da2);
113+
set.Add(da3);
114+
set.Add(da4);
115+
set.Add(da5);
116+
117+
DatabaseContext.SaveChanges();
118+
}
119+
101120
public void Clear<T>() where T : class
102121
{
103122
var set = DatabaseContext.Set<T>();
@@ -109,5 +128,6 @@ public void Dispose()
109128
{
110129
DatabaseContext.Dispose();
111130
}
131+
112132
}
113133
}

0 commit comments

Comments
 (0)