• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

loresoft / FluentCommand / 16305279797

15 Jul 2025 10:01PM UTC coverage: 54.995% (+0.04%) from 54.951%
16305279797

push

github

pwelter34
Update ImportProcessorTests.cs

1718 of 3630 branches covered (47.33%)

Branch coverage included in aggregate %.

4365 of 7431 relevant lines covered (58.74%)

230.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

47.64
/src/FluentCommand.SqlServer/Import/ImportProcessor.cs
1
using System;
2
using System.Data;
3
using System.Xml.Serialization;
4

5
using FluentCommand.Extensions;
6
using FluentCommand.Merge;
7

8
using Microsoft.Extensions.DependencyInjection;
9

10
namespace FluentCommand.Import;
11

12
/// <summary>
13
/// Processes data imports by transforming, validating, and merging imported data into a target data store.
14
/// </summary>
15
public class ImportProcessor : IImportProcessor
16
{
17
    private readonly IDataSession _dataSession;
18
    private readonly IServiceScopeFactory _serviceScopeFactory;
19

20
    /// <summary>
21
    /// Initializes a new instance of the <see cref="ImportProcessor"/> class.
22
    /// </summary>
23
    /// <param name="dataSession">The data session used for database operations.</param>
24
    /// <param name="serviceScopeFactory">The service provider for resolving dependencies such as translators and validators.</param>
25
    public ImportProcessor(IDataSession dataSession, IServiceScopeFactory serviceScopeFactory)
17✔
26
    {
27
        _dataSession = dataSession;
17✔
28
        _serviceScopeFactory = serviceScopeFactory;
17✔
29
    }
17✔
30

31
    /// <summary>
32
    /// Imports data using the specified import definition and import data.
33
    /// Transforms, validates, and merges the data into the target table as defined by the import configuration.
34
    /// </summary>
35
    /// <param name="importDefinition">The <see cref="ImportDefinition"/> describing the import configuration and rules.</param>
36
    /// <param name="importData">The <see cref="ImportData"/> containing the data and field mappings to be imported.</param>
37
    /// <param name="username">The name of the user performing the import operation.</param>
38
    /// <param name="cancellationToken">A cancellation token to observe while waiting for the task to complete.</param>
39
    /// <returns>
40
    /// An <see cref="ImportResult"/> containing the number of processed rows and any errors encountered.
41
    /// </returns>
42
    /// <exception cref="ArgumentNullException">Thrown if <paramref name="importData"/> or <paramref name="importDefinition"/> is <c>null</c>.</exception>
43
    public virtual async Task<ImportResult> ImportAsync(ImportDefinition importDefinition, ImportData importData, string username, CancellationToken cancellationToken = default)
44
    {
45
        if (importData == null)
×
46
            throw new ArgumentNullException(nameof(importData));
×
47
        if (importDefinition == null)
×
48
            throw new ArgumentNullException(nameof(importDefinition));
×
49

50
        var scopedServices = _serviceScopeFactory.CreateScope();
×
51
        try
52
        {
53
            var context = new ImportProcessContext(scopedServices.ServiceProvider, importDefinition, importData, username);
×
54

55
            var dataTable = CreateTable(context);
×
56
            await PopulateTable(context, dataTable);
×
57

58
            if (dataTable.Rows.Count == 0)
×
59
                return new ImportResult { Processed = 0, Errors = context.Errors?.ConvertAll(e => e.Message) };
×
60

61
            var mergeDefinition = CreateMergeDefinition(context);
×
62

63
            var result = await _dataSession
×
64
                .MergeData(mergeDefinition)
×
65
                .ExecuteAsync(dataTable, cancellationToken);
×
66

67
            return new ImportResult { Processed = result, Errors = context.Errors?.ConvertAll(e => e.Message) };
×
68
        }
69
        finally
70
        {
71
            scopedServices.Dispose();
×
72
        }
73
    }
×
74

75
    /// <summary>
76
    /// Creates a <see cref="DataTable"/> instance based on the mapped fields in the specified import context.
77
    /// The table schema is generated according to the field definitions and their data types.
78
    /// </summary>
79
    /// <param name="importContext">The <see cref="ImportProcessContext"/> containing field mappings and definitions.</param>
80
    /// <returns>
81
    /// A <see cref="DataTable"/> with columns corresponding to the mapped fields.
82
    /// </returns>
83
    /// <exception cref="ArgumentNullException">Thrown if <paramref name="importContext"/> is <c>null</c>.</exception>
84
    protected virtual DataTable CreateTable(ImportProcessContext importContext)
85
    {
86
        if (importContext == null)
2!
87
            throw new ArgumentNullException(nameof(importContext));
×
88

89
        var dataTable = new DataTable("#Import" + DateTime.Now.Ticks);
2✔
90

91
        foreach (var field in importContext.MappedFields)
24✔
92
        {
93
            var dataType = Nullable.GetUnderlyingType(field.Definition.DataType)
10✔
94
                           ?? field.Definition.DataType;
10✔
95

96
            var dataColumn = new DataColumn
10✔
97
            {
10✔
98
                ColumnName = field.Definition.Name,
10✔
99
                DataType = dataType
10✔
100
            };
10✔
101

102
            dataTable.Columns.Add(dataColumn);
10✔
103
        }
104

105
        return dataTable;
2✔
106
    }
107

108
    /// <summary>
109
    /// Populates the specified <see cref="DataTable"/> with data from the import context.
110
    /// Each row is transformed and validated according to the field definitions and mappings.
111
    /// </summary>
112
    /// <param name="importContext">The <see cref="ImportProcessContext"/> containing the import data and mappings.</param>
113
    /// <param name="dataTable">The <see cref="DataTable"/> to populate with imported data.</param>
114
    /// <returns>
115
    /// The populated <see cref="DataTable"/> instance.
116
    /// </returns>
117
    /// <exception cref="ArgumentNullException">Thrown if <paramref name="dataTable"/> or <paramref name="importContext"/> is <c>null</c>.</exception>
118
    protected virtual async Task<DataTable> PopulateTable(ImportProcessContext importContext, DataTable dataTable)
119
    {
120
        if (dataTable == null)
1!
121
            throw new ArgumentNullException(nameof(dataTable));
×
122

123
        if (importContext == null)
1!
124
            throw new ArgumentNullException(nameof(importContext));
×
125

126
        var data = importContext.ImportData.Data;
1✔
127
        if (data == null || data.Length == 0)
1!
128
            return dataTable;
×
129

130
        var rows = data.Length;
1✔
131
        var startIndex = importContext.ImportData.HasHeader ? 1 : 0;
1✔
132

133
        for (var index = startIndex; index < rows; index++)
8✔
134
        {
135
            var row = data[index];
3✔
136

137
            // skip empty row
138
            if (row.All(string.IsNullOrWhiteSpace))
3✔
139
                continue;
140

141
            var dataRow = dataTable.NewRow();
3✔
142

143
            var valid = await PopulateRow(importContext, dataRow, row);
3✔
144
            if (valid)
3✔
145
                dataTable.Rows.Add(dataRow);
3✔
146
        }
3✔
147

148
        return dataTable;
1✔
149
    }
1✔
150

151
    /// <summary>
152
    /// Populates a <see cref="DataRow"/> with values from the specified source row, applying field transformations and validation as needed.
153
    /// </summary>
154
    /// <param name="importContext">The <see cref="ImportProcessContext"/> providing field mappings and validation logic.</param>
155
    /// <param name="dataRow">The <see cref="DataRow"/> to populate.</param>
156
    /// <param name="row">The source data row as an array of strings.</param>
157
    /// <returns>
158
    /// <c>true</c> if the row is valid and should be included; otherwise, <c>false</c>.
159
    /// </returns>
160
    protected virtual async Task<bool> PopulateRow(ImportProcessContext importContext, DataRow dataRow, string[] row)
161
    {
162
        try
163
        {
164
            foreach (var field in importContext.MappedFields)
36✔
165
            {
166
                if (field.Definition.Default.HasValue)
15!
167
                {
168
                    dataRow[field.Definition.Name] = GetDefault(field.Definition, importContext.UserName);
×
169
                    continue;
×
170
                }
171

172
                var index = field.FieldMap.Index;
15✔
173
                if (!index.HasValue)
15✔
174
                    continue;
175

176
                var value = row[index.Value];
15✔
177

178
                var convertValue = await ConvertValue(importContext, field.Definition, value);
15✔
179

180
                dataRow[field.Definition.Name] = convertValue ?? DBNull.Value;
15✔
181
            }
15✔
182

183
            var importValidator = GetValidator(importContext);
3✔
184
            if (importValidator != null)
3!
185
                await importValidator.ValidateRow(importContext.Definition, dataRow);
×
186

187
            return true;
3✔
188
        }
189
        catch (Exception ex)
×
190
        {
191
            importContext.Errors.Add(ex);
×
192

193
            if (importContext.Errors.Count > importContext.Definition.MaxErrors)
×
194
                throw;
×
195

196
            return false;
×
197
        }
198
    }
3✔
199

200
    /// <summary>
201
    /// Converts the source string value into the correct data type for the specified field definition.
202
    /// If a translator is configured, it is used to transform the value; otherwise, a safe conversion is performed.
203
    /// </summary>
204
    /// <param name="importContext">The <see cref="ImportProcessContext"/> for resolving translators.</param>
205
    /// <param name="field">The <see cref="FieldDefinition"/> describing the field and its transformation options.</param>
206
    /// <param name="value">The source value as a string.</param>
207
    /// <returns>
208
    /// The converted value, or the result of the translator if configured.
209
    /// </returns>
210
    /// <exception cref="InvalidOperationException">Thrown if a configured translator cannot be resolved.</exception>
211
    protected virtual async Task<object> ConvertValue(ImportProcessContext importContext, FieldDefinition field, string value)
212
    {
213
#pragma warning disable CS0618 // Type or member is obsolete
214
        if (field.Translator != null)
24!
215
        {
216
            if (importContext.Services.GetService(field.Translator) is not IFieldTranslator translator)
×
217
                throw new InvalidOperationException($"Failed to create translator '{field.Translator}' for field '{field.Name}'");
×
218

219
            return await translator.Translate(value);
×
220
        }
221
#pragma warning restore CS0618 // Type or member is obsolete
222

223
        if (field.TranslatorKey != null)
24!
224
        {
225
            var translator = importContext.Services.GetKeyedService<IFieldTranslator>(field.TranslatorKey);
×
226
            if (translator == null)
×
227
                throw new InvalidOperationException($"Failed to create translator with service key '{field.TranslatorKey}' for field '{field.Name}'");
×
228

229
            return await translator.Translate(value);
×
230
        }
231

232
        return ConvertExtensions.SafeConvert(field.DataType, value);
24✔
233
    }
24✔
234

235
    /// <summary>
236
    /// Gets the default value for the specified field definition, based on the configured <see cref="FieldDefault"/> option.
237
    /// </summary>
238
    /// <param name="fieldDefinition">The <see cref="FieldDefinition"/> for which to get the default value.</param>
239
    /// <param name="username">The username to use if the default is <see cref="FieldDefault.UserName"/>.</param>
240
    /// <returns>
241
    /// The default value for the field, or <c>null</c> if no default is configured.
242
    /// </returns>
243
    protected virtual object GetDefault(FieldDefinition fieldDefinition, string username)
244
    {
245
        var fieldDefault = fieldDefinition?.Default;
3!
246
        if (!fieldDefault.HasValue)
3!
247
            return null;
×
248

249
        if (fieldDefault.Value == FieldDefault.CurrentDate)
3!
250
            return DateTimeOffset.UtcNow;
×
251

252
        if (fieldDefault.Value == FieldDefault.Static)
3✔
253
            return fieldDefinition.DefaultValue;
2✔
254

255
        if (fieldDefault.Value == FieldDefault.UserName)
1!
256
            return username;
1✔
257

258
        return null;
×
259
    }
260

261
    /// <summary>
262
    /// Creates a <see cref="DataMergeDefinition"/> for the import operation, mapping the import fields to the target table columns.
263
    /// </summary>
264
    /// <param name="importContext">The <see cref="ImportProcessContext"/> containing the import definition and mapped fields.</param>
265
    /// <returns>
266
    /// A configured <see cref="DataMergeDefinition"/> instance for the merge operation.
267
    /// </returns>
268
    /// <exception cref="InvalidOperationException">Thrown if a field definition cannot be mapped to a data column.</exception>
269
    protected virtual DataMergeDefinition CreateMergeDefinition(ImportProcessContext importContext)
270
    {
271
        var importDefinition = importContext.Definition;
×
272

273
        var mergeDefinition = new DataMergeDefinition();
×
274
        mergeDefinition.TargetTable = importDefinition.TargetTable;
×
275
        mergeDefinition.IncludeInsert = importDefinition.CanInsert;
×
276
        mergeDefinition.IncludeUpdate = importDefinition.CanUpdate;
×
277

278
        // fluent builder
279
        var mergeMapping = new DataMergeMapping(mergeDefinition);
×
280

281
        // map included columns
282
        foreach (var fieldMapping in importContext.MappedFields)
×
283
        {
284
            var fieldDefinition = fieldMapping.Definition;
×
285
            var nativeType = SqlTypeMapping.NativeType(fieldDefinition.DataType);
×
286

287
            mergeMapping
×
288
                .Column(fieldDefinition.Name)
×
289
                .Insert(fieldDefinition.CanInsert)
×
290
                .Update(fieldDefinition.CanUpdate)
×
291
                .Key(fieldDefinition.IsKey)
×
292
                .NativeType(nativeType);
×
293
        }
294

295
        return mergeDefinition;
×
296
    }
297

298
    protected virtual IImportValidator GetValidator(ImportProcessContext importContext)
299
    {
300
#pragma warning disable CS0618 // Type or member is obsolete
301
        if (importContext.Definition.Validator != null)
3!
302
        {
303
            var validator = importContext.Services.GetService(importContext.Definition.Validator);
×
304
            if (validator is not IImportValidator importValidator)
×
305
                throw new InvalidOperationException($"Failed to create data row validator '{importContext.Definition.Validator}'");
×
306

307
            return importValidator;
×
308
        }
309
#pragma warning restore CS0618 // Type or member is obsolete
310

311
        if (importContext.Definition.ValidatorKey != null)
3!
312
        {
313
            var validator = importContext.Services.GetKeyedService<IImportValidator>(importContext.Definition.ValidatorKey);
×
314
            if (validator == null)
×
315
                throw new InvalidOperationException($"Failed to create data row validator with service key '{importContext.Definition.ValidatorKey}'");
×
316

317
            return validator;
×
318
        }
319

320
        return null;
3✔
321
    }
322
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc