Рефакторинг прасинга Excel шаблонов

This commit is contained in:
Степанов Дмитрий 2024-02-14 12:13:43 +03:00
parent 0c399f0693
commit 7090daf494
15 changed files with 176 additions and 161 deletions

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using AsbCloudApp.Data;
using AsbCloudApp.Data.DrillTestReport;
using AsbCloudApp.Data.Manuals;
@ -46,6 +47,8 @@ using AsbCloudInfrastructure.Services.ProcessMaps;
using AsbCloudApp.Data.ProcessMapPlan;
using AsbCloudApp.Requests;
using AsbCloudInfrastructure.Services.Parser;
using AsbCloudInfrastructure.Services.ProcessMapPlan.Parser;
using AsbCloudInfrastructure.Services.Trajectory.Parser;
namespace AsbCloudInfrastructure
{
@ -330,7 +333,19 @@ namespace AsbCloudInfrastructure
services.AddTransient<IWellOperationCategoryRepository, WellOperationCategoryRepository>();
services.AddTransient<IDetectedOperationRepository, DetectedOperationRepository>();
services.AddSingleton<ParserServiceFactory>();
services.AddTransient<ParserServiceFactory>(serviceProvider =>
{
var parsers = new Dictionary<int, Func<IParserService>>
{
{ ParserIds.IdTrajectoryPlanParser, () => new TrajectoryPlanParser(serviceProvider) },
{ ParserIds.IdTrajectoryFactManualParser, () => new TrajectoryFactManualParser(serviceProvider) },
{ ParserIds.IdProcessMapPlanDrillingParser, () => new ProcessMapPlanDrillingParser(serviceProvider) }
};
var factory = new ParserServiceFactory(parsers);
return factory;
});
return services;
}

View File

@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using System.IO;
using ClosedXML.Excel;
namespace AsbCloudInfrastructure.Services.Parser;
public class Cell
{
private static IDictionary<Type, Func<IXLCell, object?>> converters = new Dictionary<Type, Func<IXLCell, object?>>()
{
{ typeof(bool), cell => cell.GetBoolean() },
{ typeof(double), cell => cell.GetValue<double>() },
{ typeof(float), cell => cell.GetValue<float>() },
{ typeof(long), cell => cell.GetValue<long>() },
{ typeof(ulong), cell => cell.GetValue<ulong>() },
{ typeof(int), cell => cell.GetValue<int>() },
{ typeof(uint), cell => cell.GetValue<uint>() },
{ typeof(short), cell => cell.GetValue<short>() },
{ typeof(ushort), cell => cell.GetValue<ushort>() },
{ typeof(string), cell => cell.GetString() },
{
typeof(DateTime), cell =>
{
if (cell.DataType == XLDataType.DateTime)
return cell.GetDateTime();
var stringValue = cell.GetString();
return DateTime.Parse(stringValue);
}
},
{
typeof(DateTimeOffset), cell =>
{
var stringValue = cell.GetString();
return DateTimeOffset.Parse(stringValue);
}
},
{
typeof(DateOnly), cell =>
{
var stringValue = cell.GetString();
return DateOnly.Parse(stringValue);
}
},
{
typeof(TimeOnly), cell =>
{
var stringValue = cell.GetString();
return TimeOnly.Parse(stringValue);
}
},
{
typeof(TimeSpan), cell =>
{
if (cell.DataType == XLDataType.TimeSpan)
return cell.GetTimeSpan();
var stringValue = cell.GetString();
return TimeSpan.Parse(stringValue);
}
},
};
private readonly Type type;
public Cell(int columnNumber,
Type type)
{
ColumnNumber = columnNumber;
this.type = type;
}
public int ColumnNumber { get; }
public object? GetValueFromCell(IXLCell cell)
{
try
{
return converters[type].Invoke(cell);
}
catch
{
var message = string.Format(XLExtentions.InvalidValueTemplate, cell.Worksheet.Name, cell.Address.RowNumber,
cell.Address.ColumnNumber);
throw new FileFormatException(message);
}
}
}

View File

@ -1,17 +0,0 @@
using ClosedXML.Excel;
namespace AsbCloudInfrastructure.Services.Parser.Data;
public class Cell
{
public Cell(int columnNumber,
XLDataType type)
{
ColumnNumber = columnNumber;
Type = type;
}
public int ColumnNumber { get; }
public XLDataType Type { get; }
}

View File

@ -1,17 +0,0 @@
using System.Collections.Generic;
namespace AsbCloudInfrastructure.Services.Parser.Data;
public class Row
{
public Row(int number,
IDictionary<string, (Cell, object?)> cells)
{
Number = number;
Cells = cells;
}
public int Number { get; }
public IDictionary<string, (Cell CellInfo, object? CellValue)> Cells { get; }
}

View File

@ -7,7 +7,6 @@ using System.Reflection;
using AsbCloudApp.Data;
using AsbCloudApp.Requests.ParserOptions;
using AsbCloudApp.Services;
using AsbCloudInfrastructure.Services.Parser.Data;
using ClosedXML.Excel;
using Mapster;
@ -45,27 +44,23 @@ public abstract class ParserExcelService<TDto, TOptions> : IParserService<TDto,
?? throw new ArgumentNullException($"Файл '{TemplateFileName}' не найден");
protected virtual Row ParseRow(IXLRow xlRow)
protected virtual IDictionary<string, object?> ParseRow(IXLRow xlRow)
{
var cells = Cells.ToDictionary(x => x.Key, x =>
{
var columnNumber = x.Value.ColumnNumber;
var type = x.Value.Type;
var cellValue = xlRow.Cell(columnNumber).GetCellValue(type);
return (x.Value, cellValue);
var xlCell = xlRow.Cell(columnNumber);
var cellValue = x.Value.GetValueFromCell(xlCell);
return cellValue;
});
var row = new Row(xlRow.RowNumber(), cells);
return row;
return cells;
}
protected virtual TDto BuildDto(Row row)
protected virtual TDto BuildDto(IDictionary<string, object?> row, int rowNumber)
{
var propertiesDict = row.Cells.ToDictionary(x => x.Key, x => x.Value.CellValue);
return propertiesDict.Adapt<TDto>();
var dto = row.Adapt<TDto>();
return dto;
}
private ValidationResultDto<TDto> Validate(TDto dto, int rowNumber)
@ -118,12 +113,13 @@ public abstract class ParserExcelService<TDto, TOptions> : IParserService<TDto,
for (var i = 0; i < count; i++)
{
var xlRow = sheet.Row(1 + i + HeaderRowsCount);
var rowNumber = xlRow.RowNumber();
try
{
var row = ParseRow(xlRow);
var dto = BuildDto(row);
var validationResult = Validate(dto, xlRow.RowNumber());
var dto = BuildDto(row, rowNumber);
var validationResult = Validate(dto, rowNumber);
valiationResults.Add(validationResult);
}
catch (FileFormatException ex)

View File

@ -0,0 +1,8 @@
namespace AsbCloudInfrastructure.Services.Parser;
public static class ParserIds
{
public const int IdTrajectoryFactManualParser = 1;
public const int IdTrajectoryPlanParser = 2;
public const int IdProcessMapPlanDrillingParser = 3;
}

View File

@ -3,31 +3,16 @@ using System.Collections.Generic;
using AsbCloudApp.Data;
using AsbCloudApp.Requests.ParserOptions;
using AsbCloudApp.Services;
using AsbCloudInfrastructure.Services.ProcessMapPlan.Parser;
using AsbCloudInfrastructure.Services.Trajectory.Parser;
using Microsoft.Extensions.DependencyInjection;
namespace AsbCloudInfrastructure.Services.Parser;
public class ParserServiceFactory : IDisposable
public class ParserServiceFactory
{
public const int IdTrajectoryFactManualParser = 1;
public const int IdTrajectoryPlanParser = 2;
public const int IdProcessMapPlanDrillingParser = 3;
private readonly IDictionary<int, Func<IParserService>> parsers;
private readonly IServiceScope serviceScope;
public ParserServiceFactory(IServiceProvider serviceProvider)
public ParserServiceFactory(IDictionary<int, Func<IParserService>> parsers)
{
serviceScope = serviceProvider.CreateScope();
parsers = new Dictionary<int, Func<IParserService>>
{
{ IdTrajectoryPlanParser, () => new TrajectoryPlanParser(serviceScope.ServiceProvider) },
{ IdTrajectoryFactManualParser, () => new TrajectoryFactManualParser(serviceScope.ServiceProvider) },
{ IdProcessMapPlanDrillingParser, () => new ProcessMapPlanDrillingParser(serviceScope.ServiceProvider) }
};
this.parsers = parsers;
}
public IParserService<TDto, TOptions> Create<TDto, TOptions>(int idParserService)
@ -40,9 +25,4 @@ public class ParserServiceFactory : IDisposable
return parserService.Invoke() as IParserService<TDto, TOptions>
?? throw new ArgumentNullException(nameof(idParserService), "Ошибка приведения типа");
}
public void Dispose()
{
serviceScope.Dispose();
}
}

View File

@ -5,8 +5,7 @@ using System.Linq;
using AsbCloudApp.Data;
using AsbCloudApp.Data.ProcessMapPlan;
using AsbCloudApp.Repositories;
using AsbCloudInfrastructure.Services.Parser.Data;
using ClosedXML.Excel;
using AsbCloudInfrastructure.Services.Parser;
using Microsoft.Extensions.DependencyInjection;
namespace AsbCloudInfrastructure.Services.ProcessMapPlan.Parser;
@ -31,36 +30,36 @@ public class ProcessMapPlanDrillingParser : ProcessMapPlanParser<ProcessMapPlanD
protected override IDictionary<string, Cell> Cells => new Dictionary<string, Cell>
{
{ nameof(ProcessMapPlanDrillingDto.Section), new Cell(ColumnSection, XLDataType.Text) },
{ nameof(ProcessMapPlanDrillingDto.Mode), new Cell(ColumnMode, XLDataType.Text) },
{ nameof(ProcessMapPlanDrillingDto.DepthStart), new Cell(3, XLDataType.Number) },
{ nameof(ProcessMapPlanDrillingDto.DepthEnd), new Cell(4, XLDataType.Number) },
{ nameof(ProcessMapPlanDrillingDto.DeltaPressurePlan), new Cell(5, XLDataType.Number) },
{ nameof(ProcessMapPlanDrillingDto.DeltaPressureLimitMax), new Cell(6, XLDataType.Number) },
{ nameof(ProcessMapPlanDrillingDto.AxialLoadPlan), new Cell(7, XLDataType.Number) },
{ nameof(ProcessMapPlanDrillingDto.AxialLoadLimitMax), new Cell(8, XLDataType.Number) },
{ nameof(ProcessMapPlanDrillingDto.TopDriveTorquePlan), new Cell(9, XLDataType.Number) },
{ nameof(ProcessMapPlanDrillingDto.TopDriveTorqueLimitMax), new Cell(10, XLDataType.Number) },
{ nameof(ProcessMapPlanDrillingDto.TopDriveSpeedPlan), new Cell(11, XLDataType.Number) },
{ nameof(ProcessMapPlanDrillingDto.TopDriveSpeedLimitMax), new Cell(12, XLDataType.Number) },
{ nameof(ProcessMapPlanDrillingDto.FlowPlan), new Cell(13, XLDataType.Number) },
{ nameof(ProcessMapPlanDrillingDto.FlowLimitMax), new Cell(14, XLDataType.Number) },
{ nameof(ProcessMapPlanDrillingDto.RopPlan), new Cell(15, XLDataType.Number) },
{ nameof(ProcessMapPlanDrillingDto.UsageSaub), new Cell(16, XLDataType.Number) },
{ nameof(ProcessMapPlanDrillingDto.UsageSpin), new Cell(17, XLDataType.Number) },
{ nameof(ProcessMapPlanDrillingDto.Comment), new Cell(18, XLDataType.Text) }
{ nameof(ProcessMapPlanDrillingDto.Section), new Cell(ColumnSection, typeof(string)) },
{ nameof(ProcessMapPlanDrillingDto.Mode), new Cell(ColumnMode, typeof(string)) },
{ nameof(ProcessMapPlanDrillingDto.DepthStart), new Cell(3, typeof(double)) },
{ nameof(ProcessMapPlanDrillingDto.DepthEnd), new Cell(4, typeof(double)) },
{ nameof(ProcessMapPlanDrillingDto.DeltaPressurePlan), new Cell(5, typeof(double)) },
{ nameof(ProcessMapPlanDrillingDto.DeltaPressureLimitMax), new Cell(6, typeof(double)) },
{ nameof(ProcessMapPlanDrillingDto.AxialLoadPlan), new Cell(7, typeof(double)) },
{ nameof(ProcessMapPlanDrillingDto.AxialLoadLimitMax), new Cell(8, typeof(double)) },
{ nameof(ProcessMapPlanDrillingDto.TopDriveTorquePlan), new Cell(9, typeof(double)) },
{ nameof(ProcessMapPlanDrillingDto.TopDriveTorqueLimitMax), new Cell(10, typeof(double)) },
{ nameof(ProcessMapPlanDrillingDto.TopDriveSpeedPlan), new Cell(11, typeof(double)) },
{ nameof(ProcessMapPlanDrillingDto.TopDriveSpeedLimitMax), new Cell(12, typeof(double)) },
{ nameof(ProcessMapPlanDrillingDto.FlowPlan), new Cell(13, typeof(double)) },
{ nameof(ProcessMapPlanDrillingDto.FlowLimitMax), new Cell(14, typeof(double)) },
{ nameof(ProcessMapPlanDrillingDto.RopPlan), new Cell(15, typeof(double)) },
{ nameof(ProcessMapPlanDrillingDto.UsageSaub), new Cell(16, typeof(double)) },
{ nameof(ProcessMapPlanDrillingDto.UsageSpin), new Cell(17, typeof(double)) },
{ nameof(ProcessMapPlanDrillingDto.Comment), new Cell(18, typeof(string)) }
};
protected override ProcessMapPlanDrillingDto BuildDto(Row row)
protected override ProcessMapPlanDrillingDto BuildDto(IDictionary<string, object?> row, int rowNumber)
{
var dto = base.BuildDto(row);
var dto = base.BuildDto(row, rowNumber);
var section = sections.FirstOrDefault(s =>
string.Equals(s.Caption.Trim(), dto.Section?.Trim(), StringComparison.CurrentCultureIgnoreCase));
if (section is null)
{
var message = string.Format(XLExtentions.ProblemDetailsTemplate, SheetName, row.Number, ColumnSection,
var message = string.Format(XLExtentions.ProblemDetailsTemplate, SheetName, rowNumber, ColumnSection,
"Указана некорректная секция");
throw new FileFormatException(message);
}
@ -69,7 +68,7 @@ public class ProcessMapPlanDrillingParser : ProcessMapPlanParser<ProcessMapPlanD
if (idMode is null)
{
var message = string.Format(XLExtentions.ProblemDetailsTemplate, SheetName, row.Number, ColumnSection,
var message = string.Format(XLExtentions.ProblemDetailsTemplate, SheetName, rowNumber, ColumnSection,
"Указан некорректный режим бурения");
throw new FileFormatException(message);
}

View File

@ -3,8 +3,6 @@ using System.Collections.Generic;
using AsbCloudApp.Data.Trajectory;
using AsbCloudApp.Requests.ParserOptions;
using AsbCloudInfrastructure.Services.Parser;
using AsbCloudInfrastructure.Services.Parser.Data;
using ClosedXML.Excel;
namespace AsbCloudInfrastructure.Services.Trajectory.Parser;
@ -21,13 +19,13 @@ public class TrajectoryFactManualParser : ParserExcelService<TrajectoryGeoFactDt
protected override string TemplateFileName => "TrajectoryFactManualTemplate.xlsx";
protected override IDictionary<string, Cell> Cells => new Dictionary<string, Cell>()
protected override IDictionary<string, Cell> Cells => new Dictionary<string, Cell>
{
{ nameof(TrajectoryGeoFactDto.WellboreDepth), new Cell(1, XLDataType.Number) },
{ nameof(TrajectoryGeoFactDto.ZenithAngle), new Cell(2, XLDataType.Number) },
{ nameof(TrajectoryGeoFactDto.AzimuthGeo), new Cell(3, XLDataType.Number) },
{ nameof(TrajectoryGeoFactDto.AzimuthMagnetic), new Cell(4, XLDataType.Number) },
{ nameof(TrajectoryGeoFactDto.VerticalDepth), new Cell(5, XLDataType.Number) },
{ nameof(TrajectoryGeoFactDto.Comment), new Cell(6, XLDataType.Text) }
{ nameof(TrajectoryGeoFactDto.WellboreDepth), new Cell(1, typeof(double)) },
{ nameof(TrajectoryGeoFactDto.ZenithAngle), new Cell(2, typeof(double)) },
{ nameof(TrajectoryGeoFactDto.AzimuthGeo), new Cell(3, typeof(double)) },
{ nameof(TrajectoryGeoFactDto.AzimuthMagnetic), new Cell(4, typeof(double)) },
{ nameof(TrajectoryGeoFactDto.VerticalDepth), new Cell(5, typeof(double)) },
{ nameof(TrajectoryGeoFactDto.Comment), new Cell(6, typeof(string)) }
};
}

View File

@ -3,8 +3,6 @@ using System.Collections.Generic;
using AsbCloudApp.Data.Trajectory;
using AsbCloudApp.Requests.ParserOptions;
using AsbCloudInfrastructure.Services.Parser;
using AsbCloudInfrastructure.Services.Parser.Data;
using ClosedXML.Excel;
namespace AsbCloudInfrastructure.Services.Trajectory.Parser;
@ -23,12 +21,12 @@ public class TrajectoryPlanParser : ParserExcelService<TrajectoryGeoPlanDto, IPa
protected override IDictionary<string, Cell> Cells => new Dictionary<string, Cell>
{
{ nameof(TrajectoryGeoPlanDto.WellboreDepth), new Cell(1, XLDataType.Number) },
{ nameof(TrajectoryGeoPlanDto.ZenithAngle), new Cell(2, XLDataType.Number) },
{ nameof(TrajectoryGeoPlanDto.AzimuthGeo), new Cell(3, XLDataType.Number) },
{ nameof(TrajectoryGeoPlanDto.AzimuthMagnetic), new Cell(4, XLDataType.Number) },
{ nameof(TrajectoryGeoPlanDto.VerticalDepth), new Cell(5, XLDataType.Number) },
{ nameof(TrajectoryGeoPlanDto.Radius), new Cell(6, XLDataType.Number) },
{ nameof(TrajectoryGeoPlanDto.Comment), new Cell(7, XLDataType.Text) }
{ nameof(TrajectoryGeoPlanDto.WellboreDepth), new Cell(1, typeof(double)) },
{ nameof(TrajectoryGeoPlanDto.ZenithAngle), new Cell(2, typeof(double)) },
{ nameof(TrajectoryGeoPlanDto.AzimuthGeo), new Cell(3, typeof(double)) },
{ nameof(TrajectoryGeoPlanDto.AzimuthMagnetic), new Cell(4, typeof(double)) },
{ nameof(TrajectoryGeoPlanDto.VerticalDepth), new Cell(5, typeof(double)) },
{ nameof(TrajectoryGeoPlanDto.Radius), new Cell(6, typeof(double)) },
{ nameof(TrajectoryGeoPlanDto.Comment), new Cell(7, typeof(string)) }
};
}

View File

@ -8,9 +8,7 @@ namespace AsbCloudInfrastructure;
public static class XLExtentions
{
public const string ProblemDetailsTemplate = "Лист: {0}, Строка: {1}, Столбец: {2}. {3}";
public const string NotFoundSheetTemplate = "Книга excel не содержит листа {0}";
public const string InvalidValueTemplate = "Лист: {0}, Строка: {1}, Столбец: {2}. Содержит некорректное значение";
public static IXLWorksheet GetWorksheet(this IXLWorkbook workbook, string sheetName) =>
@ -34,29 +32,6 @@ public static class XLExtentions
return cell;
}
public static object? GetCellValue(this IXLCell cell, XLDataType type)
{
try
{
return type switch
{
XLDataType.Blank => null,
XLDataType.Boolean => cell.Value.GetBoolean(),
XLDataType.Number => cell.Value.GetNumber(),
XLDataType.Text => cell.Value.GetText(),
XLDataType.Error => cell.Value.GetError(),
XLDataType.DateTime => cell.Value.GetDateTime(),
XLDataType.TimeSpan => cell.Value.GetTimeSpan(),
_ => throw new InvalidCastException()
};
}
catch
{
var message = string.Format(InvalidValueTemplate, cell.Worksheet.Name, cell.Address.RowNumber, cell.Address.ColumnNumber);
throw new FileFormatException(message);
}
}
public static T? GetCellValue<T>(this IXLCell cell)
{
try

View File

@ -1,10 +1,7 @@
using System;
using System.Linq;
using System.Linq;
using AsbCloudApp.Data.Trajectory;
using AsbCloudApp.Requests.ParserOptions;
using AsbCloudInfrastructure.Services.Parser;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using Xunit;
namespace AsbCloudWebApi.Tests.Services.Trajectory;
@ -13,20 +10,8 @@ public class TrajectoryParserTest
{
private const string UsingTemplateFile = "AsbCloudWebApi.Tests.Services.Trajectory.Templates";
private readonly IServiceProvider serviceProviderMock = Substitute.For<IServiceProvider, ISupportRequiredService>();
private readonly IServiceScope serviceScopeMock = Substitute.For<IServiceScope>();
private readonly IServiceScopeFactory serviceScopeFactoryMock = Substitute.For<IServiceScopeFactory>();
private readonly ParserServiceFactory parserServiceFactory;
public TrajectoryParserTest()
{
serviceScopeFactoryMock.CreateScope().Returns(serviceScopeMock);
((ISupportRequiredService)serviceProviderMock).GetRequiredService(typeof(IServiceScopeFactory)).Returns(serviceScopeFactoryMock);
parserServiceFactory = new ParserServiceFactory(serviceProviderMock);
}
[Fact]
public void Parse_trajectory_plan()
{
@ -37,7 +22,7 @@ public class TrajectoryParserTest
Assert.Fail("Файла для импорта не существует");
var parserService = parserServiceFactory.Create<TrajectoryGeoPlanDto, IParserOptionsRequest>(
ParserServiceFactory.IdTrajectoryPlanParser);
ParserIds.IdTrajectoryPlanParser);
var trajectoryRows = parserService.Parse(stream, IParserOptionsRequest.Empty());
@ -54,7 +39,7 @@ public class TrajectoryParserTest
Assert.Fail("Файла для импорта не существует");
var parserService = parserServiceFactory.Create<TrajectoryGeoFactDto, IParserOptionsRequest>(
ParserServiceFactory.IdTrajectoryFactManualParser);
ParserIds.IdTrajectoryFactManualParser);
var trajectoryRows = parserService.Parse(stream, IParserOptionsRequest.Empty());

View File

@ -11,7 +11,7 @@ public class ProcessMapPlanDrillingController : ProcessMapPlanBaseController<Pro
public ProcessMapPlanDrillingController(IChangeLogRepository<ProcessMapPlanDrillingDto, ProcessMapPlanBaseRequestWithWell> repository,
IWellService wellService,
ParserServiceFactory parserFactory)
: base(repository, wellService, parserFactory, ParserServiceFactory.IdProcessMapPlanDrillingParser)
: base(repository, wellService, parserFactory, ParserIds.IdProcessMapPlanDrillingParser)
{
}

View File

@ -1,6 +1,7 @@
using AsbCloudApp.Data.Trajectory;
using AsbCloudApp.Repositories;
using AsbCloudApp.Services;
using AsbCloudInfrastructure;
using AsbCloudInfrastructure.Services.Parser;
using AsbCloudInfrastructure.Services.Trajectory.Export;
using Microsoft.AspNetCore.Mvc;
@ -24,7 +25,7 @@ public class TrajectoryFactManualController : TrajectoryEditableController<Traje
parserServiceFactory,
trajectoryExportService,
trajectoryRepository,
ParserServiceFactory.IdTrajectoryFactManualParser)
ParserIds.IdTrajectoryFactManualParser)
{
}
}

View File

@ -31,7 +31,7 @@ namespace AsbCloudWebApi.Controllers.Trajectory
parserServiceFactory,
trajectoryExportService,
trajectoryRepository,
ParserServiceFactory.IdTrajectoryPlanParser)
ParserIds.IdTrajectoryPlanParser)
{
this.trajectoryVisualizationService = trajectoryVisualizationService;
}