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

loresoft / FluentCommand / 16301005024

15 Jul 2025 06:03PM UTC coverage: 54.951%. First build
16301005024

push

github

pwelter34
import data tweaks, xml doc updates

1716 of 3630 branches covered (47.27%)

Branch coverage included in aggregate %.

78 of 143 new or added lines in 11 files covered. (54.55%)

4361 of 7429 relevant lines covered (58.7%)

231.01 hits per line

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

44.97
/src/FluentCommand.SqlServer/Import/ImportProcessor.cs
1
using System;
2
using System.Data;
3

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

7
using Microsoft.Extensions.DependencyInjection;
8

9
namespace FluentCommand.Import;
10

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

19
    private IImportValidator _importValidator;
20

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

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

51
        // validator is shared for entire import process
NEW
52
        _importValidator = GetValidator(importDefinition);
×
53

NEW
54
        var context = new ImportProcessContext(_serviceProvider, importDefinition, importData, username);
×
55

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

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

62
        var mergeDefinition = CreateMergeDefinition(context);
×
63

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

NEW
68
        return new ImportResult { Processed = result, Errors = context.Errors?.ConvertAll(e => e.Message) };
×
69
    }
×
70

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

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

87
        foreach (var field in importContext.MappedFields)
24✔
88
        {
89
            var dataType = Nullable.GetUnderlyingType(field.Definition.DataType)
10✔
90
                           ?? field.Definition.DataType;
10✔
91

92
            var dataColumn = new DataColumn
10✔
93
            {
10✔
94
                ColumnName = field.Definition.Name,
10✔
95
                DataType = dataType
10✔
96
            };
10✔
97

98
            dataTable.Columns.Add(dataColumn);
10✔
99
        }
100

101
        return dataTable;
2✔
102
    }
103

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

119
        if (importContext == null)
1!
120
            throw new ArgumentNullException(nameof(importContext));
×
121

122
        var data = importContext.ImportData.Data;
1✔
123
        if (data == null || data.Length == 0)
1!
124
            return dataTable;
×
125

126
        var rows = data.Length;
1✔
127
        var startIndex = importContext.ImportData.HasHeader ? 1 : 0;
1✔
128

129
        for (var index = startIndex; index < rows; index++)
8✔
130
        {
131
            var row = data[index];
3✔
132

133
            // skip empty row
134
            if (row.All(string.IsNullOrWhiteSpace))
3✔
135
                continue;
136

137
            var dataRow = dataTable.NewRow();
3✔
138

139
            var valid = await PopulateRow(importContext, dataRow, row);
3✔
140
            if (valid)
3✔
141
                dataTable.Rows.Add(dataRow);
3✔
142
        }
3✔
143

144
        return dataTable;
1✔
145
    }
1✔
146

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

168
                var index = field.FieldMap.Index;
15✔
169
                if (!index.HasValue)
15✔
170
                    continue;
171

172
                var value = row[index.Value];
15✔
173

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

176
                dataRow[field.Definition.Name] = convertValue ?? DBNull.Value;
15✔
177
            }
15✔
178

179
            if (_importValidator != null)
3!
NEW
180
                await _importValidator.ValidateRow(importContext.Definition, dataRow);
×
181

182
            return true;
3✔
183
        }
184
        catch (Exception ex)
×
185
        {
186
            importContext.Errors.Add(ex);
×
187

188
            if (importContext.Errors.Count > importContext.Definition.MaxErrors)
×
189
                throw;
×
190

191
            return false;
×
192
        }
193
    }
3✔
194

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

NEW
214
            return await translator.Translate(value);
×
215
        }
216
#pragma warning restore CS0618 // Type or member is obsolete
217

218
        if (field.TranslatorKey != null)
24!
219
        {
NEW
220
            var translator = _serviceProvider.GetKeyedService<IFieldTranslator>(field.TranslatorKey);
×
NEW
221
            if (translator == null)
×
NEW
222
                throw new InvalidOperationException($"Failed to create translator with service key '{field.TranslatorKey}' for field '{field.Name}'");
×
223

NEW
224
            return await translator.Translate(value);
×
225
        }
226

227
        return ConvertExtensions.SafeConvert(field.DataType, value);
24✔
228
    }
24✔
229

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

244
        if (fieldDefault.Value == FieldDefault.CurrentDate)
3!
245
            return DateTimeOffset.UtcNow;
×
246

247
        if (fieldDefault.Value == FieldDefault.Static)
3✔
248
            return fieldDefinition.DefaultValue;
2✔
249

250
        if (fieldDefault.Value == FieldDefault.UserName)
1!
251
            return username;
1✔
252

253
        return null;
×
254
    }
255

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

268
        var mergeDefinition = new DataMergeDefinition();
×
269
        mergeDefinition.TargetTable = importDefinition.TargetTable;
×
270
        mergeDefinition.IncludeInsert = importDefinition.CanInsert;
×
271
        mergeDefinition.IncludeUpdate = importDefinition.CanUpdate;
×
272

273
        // fluent builder
274
        var mergeMapping = new DataMergeMapping(mergeDefinition);
×
275

276
        // map included columns
277
        foreach (var fieldMapping in importContext.MappedFields)
×
278
        {
279
            var fieldDefinition = fieldMapping.Definition;
×
280
            var nativeType = SqlTypeMapping.NativeType(fieldDefinition.DataType);
×
281

282
            mergeMapping
×
283
                .Column(fieldDefinition.Name)
×
284
                .Insert(fieldDefinition.CanInsert)
×
285
                .Update(fieldDefinition.CanUpdate)
×
286
                .Key(fieldDefinition.IsKey)
×
287
                .NativeType(nativeType);
×
288
        }
289

290
        return mergeDefinition;
×
291
    }
292

293
    protected virtual IImportValidator GetValidator(ImportDefinition importDefinition)
294
    {
295
#pragma warning disable CS0618 // Type or member is obsolete
NEW
296
        if (importDefinition.Validator != null)
×
297
        {
NEW
298
            var validator = _serviceProvider.GetService(importDefinition.Validator);
×
NEW
299
            if (validator is not IImportValidator importValidator)
×
NEW
300
                throw new InvalidOperationException($"Failed to create data row validator '{importDefinition.Validator}'");
×
301

NEW
302
            return importValidator;
×
303
        }
304
#pragma warning restore CS0618 // Type or member is obsolete
305

NEW
306
        if (importDefinition.ValidatorKey != null)
×
307
        {
NEW
308
            var validator = _serviceProvider.GetKeyedService<IImportValidator>(importDefinition.ValidatorKey);
×
NEW
309
            if (validator == null)
×
NEW
310
                throw new InvalidOperationException($"Failed to create data row validator with service key '{importDefinition.ValidatorKey}'");
×
311

NEW
312
            return validator;
×
313
        }
314

NEW
315
        return null;
×
316
    }
317
}
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