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

loresoft / FluentCommand / 23278216331

19 Mar 2026 03:19AM UTC coverage: 57.398% (+0.7%) from 56.658%
23278216331

push

github

pwelter34
Enable nullable and improve source generators

1403 of 3069 branches covered (45.72%)

Branch coverage included in aggregate %.

527 of 907 new or added lines in 58 files covered. (58.1%)

22 existing lines in 10 files now uncovered.

4288 of 6846 relevant lines covered (62.64%)

330.58 hits per line

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

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

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

6
using Microsoft.Extensions.DependencyInjection;
7

8
namespace FluentCommand.Import;
9

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

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

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

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

53
            var dataTable = CreateTable(context);
×
54
            await PopulateTable(context, dataTable);
×
55

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

59
            var mergeDefinition = CreateMergeDefinition(context);
×
60

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

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

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

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

89
        foreach (var field in importContext.MappedFields)
24✔
90
        {
91
            if (field.Definition.DataType is null)
10✔
92
                continue;
93

94
            var dataType = Nullable.GetUnderlyingType(field.Definition.DataType)
10✔
95
                ?? field.Definition.DataType;
10✔
96

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

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

106
        return dataTable;
2✔
107
    }
108

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

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

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

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

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

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

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

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

149
        return dataTable;
1✔
150
    }
1✔
151

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

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

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

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

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

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

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

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

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

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

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

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

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

233
        return field.DataType is not null
24!
234
            ? ConvertExtensions.SafeConvert(field.DataType, value)
24✔
235
            : value;
24✔
236
    }
24✔
237

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

252
        if (fieldDefault.Value == FieldDefault.CurrentDate)
3!
253
            return DateTimeOffset.UtcNow;
×
254

255
        if (fieldDefault.Value == FieldDefault.Static)
3✔
256
            return fieldDefinition.DefaultValue;
2✔
257

258
        if (fieldDefault.Value == FieldDefault.UserName)
1!
259
            return username;
1✔
260

261
        return null;
×
262
    }
263

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

276
        var mergeDefinition = new DataMergeDefinition();
×
277
        mergeDefinition.TargetTable = importDefinition.TargetTable;
×
278
        mergeDefinition.IncludeInsert = importDefinition.CanInsert;
×
279
        mergeDefinition.IncludeUpdate = importDefinition.CanUpdate;
×
280

281
        // fluent builder
282
        var mergeMapping = new DataMergeMapping(mergeDefinition);
×
283

284
        // map included columns
285
        foreach (var fieldMapping in importContext.MappedFields)
×
286
        {
287
            var fieldDefinition = fieldMapping.Definition;
×
NEW
288
            var nativeType = fieldDefinition.DataType is not null
×
NEW
289
                ? SqlTypeMapping.NativeType(fieldDefinition.DataType)
×
NEW
290
                : "sql_variant";
×
291

292
            mergeMapping
×
293
                .Column(fieldDefinition.Name)
×
294
                .Insert(fieldDefinition.CanInsert)
×
295
                .Update(fieldDefinition.CanUpdate)
×
296
                .Key(fieldDefinition.IsKey)
×
297
                .NativeType(nativeType);
×
298
        }
299

300
        return mergeDefinition;
×
301
    }
302

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

312
            return importValidator;
×
313
        }
314
#pragma warning restore CS0618 // Type or member is obsolete
315

316
        if (importContext.Definition.ValidatorKey != null)
3!
317
        {
318
            var validator = importContext.Services.GetKeyedService<IImportValidator>(importContext.Definition.ValidatorKey);
×
319
            if (validator == null)
×
320
                throw new InvalidOperationException($"Failed to create data row validator with service key '{importContext.Definition.ValidatorKey}'");
×
321

322
            return validator;
×
323
        }
324

325
        return null;
3✔
326
    }
327
}
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