#c# #dependency-injection #.net-5
Вопрос:
Я натыкаюсь на стену, где, вероятно, мне следует провести рефакторинг. Недавно я взял хорошо работающую программу командной строки, которая не выполняла никаких зависимостей, и решил, что было бы неплохо использовать SystemCommandline API (https://github.com/dotnet/command-line-api/blob/main/samples/HostingPlayground/Program.cs ).
Мне нравится этот подход (хотя я также задаюсь вопросом, должен ли я просто написать это как команду PowerShell и покончить с этим).
У меня сильная библиотека. Я разбираю PDF-файлы, чтобы извлечь данные таблицы. То, что я ищу, делая это, — это возможность размещать множество разных фронтов поверх моей библиотеки. Зайдите в приложение WPF, запустите его как службу, создайте веб-приложение на основе Kestrel.
Я действительно борюсь с тем, как перенести параметры, которые я генерирую с помощью командной строки, в мой фактический код.
Вот Program.cs, который я собрал вместе:
namespace Hephalu.Invoices.Commandline
{
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Configuration;
using System.CommandLine;
using System.CommandLine.Builder;
using System.CommandLine.Hosting;
using System.CommandLine.Invocation;
using System.CommandLine.Parsing;
using System.Threading.Tasks;
using Hephalu.Invoices.CommandLine.Logging;
using System.IO;
using Hephalu.Invoices.Extractors;
using Hephalu.Invoices.ReportData;
using Hephalu.Invoices.Rows;
using System.Collections.Generic;
class Program
{
/// <summary>
///
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
static async Task Main(string[] args)
{
// Wait for the async task to build / check the correct command line parameters
_ = await BuildCommandLine()
// Build the host application and setup the various part pass it back to the
// main after being built and invoked.
.UseHost(host =>
{
// Read the appsettings.json file as we need it to initialize the file logger later
_ = host.ConfigureAppConfiguration((context, config) =>
{
config.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true)
.Build();
});
// Now that the appsettings has been built / extracted setup the logger
_ = host.ConfigureLogging((context, logging) =>
{
_ = logging.AddFileLogger(options =>
{
// using the built appsettings above pull the options for the logger via the sections
// see appsettings
context.Configuration.GetSection("Logging")
.GetSection("HMInvoiceParserLogFile")
.GetSection("Options")
// Bind them back into the file logger
.Bind(options);
});
});
_ = host.ConfigureServices((context, services) =>
{
services.AddSingleton<IExtractRows<TanaRow, TanaReportData>, ExtractTanaRows>();
services.AddSingleton<ILineExtractor, LineExtractor>();
services.AddSingleton<IReportData, TanaReportData>();
});
})
.UseDefaults()
.Build()
.InvokeAsync(args);
}
private static CommandLineBuilder BuildCommandLine()
{
// Create a list of Commands arguments that are required
var root = new RootCommand{
new Option<string>("--FileName")
{
IsRequired = true
},
new Option<string>("--FileType")
{
IsRequired = true
}
};
// Push the extracted command arguments into the program to run
root.Handler = CommandHandler.Create<ExtractorOptions, IHost>(Run);
return new CommandLineBuilder(root);
}
/// <summary>
/// The meat of the program that uses the DI container to construct and run
/// work
/// </summary>
/// <param name="options"></param>
/// <param name="host"></param>
private static void Run(ExtractorOptions options, IHost host)
{
var serviceProvider = host.Services;
var lineExtractor = serviceProvider.GetRequiredService<ILineExtractor>();
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger(typeof(Program));
var fname = options.Filename;
var ftype = options.FileType;
List<string> lines = lineExtractor.GetLines(fname);
var reportData = serviceProvider.GetRequiredService<IReportData>(lines => new TanaReportData(lines));
logger.LogInformation(1000, "file requested for: {fname} : {ftype}", fname, ftype);
}
}
}
Как вы можете видеть, функция BuildCommandLine вызывается асинхронно, а затем мы переходим к сути конструкции контейнера.
Поскольку на данный момент это командная строка для обработки одного файла PFS после создания контейнера, она вызывает программу run.
Я протестировал код, и контейнер работает нормально (нет, лично мне не важен синтаксис, поэтому я проведу рефакторинг позже.
Моя проблема связана с запускаемой программой.
Мои одноэлементные экземпляры преобразуются из статических классов what where (здесь я получаю преимущество logger, что сделало его полезным для меня)
И эффективно завершает одну операцию.
- Извлеките строки PDF
- Разбирайте каждую строку на основе набора правил / регулярных выражений (спасибо PdfPig за всю тяжелую работу)
- Do something with the now POCO faux table table data (e.g. Use a CSV Writer, Use a DB Writer, push to a WPF output. This will be done based on the program that uses the assemblies and abstracted from the above logic.
So everything is looking good. I’m using a singleton right now for all of this because my lifetime is that of the running of the extraction.exe against a supplied file and type. Note type isn’t implemented yet as I am still test the new flavor of this with DI.
Anyway Commandline API passes in my parsed and typed arguments into my Run Program as the options object they have been mapped to and gives me an IHost object.
var lineExtractor = serviceProvider.GetRequiredService<ILineExtractor>();
List<string> lines = lineExtractor.GetLines(fname);
That seems to be fine (Its essentially a unstaticed class that now just relies on a logger, but does work on a list of strings.
My issue is I have another class I need to instantiate a single time during or after my after this my reportdata object which is currently accepting a public constructor argument
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace Hephalu.Invoices.ReportData
{
public class TanaReportData : IReportData
{
private static readonly CultureInfo fr = new CultureInfo("fr");
private static RegexOptions _regexOptions = RegexOptions.Compiled | RegexOptions.ExplicitCapture;
private Regex _rgxPaymentDate = new(@"Dus(?<tofind>(d{2}/d{2}/d{4}))sau", _regexOptions);
private Regex _rgxReference = new(@"Réference : (?<tofind>(.*))", _regexOptions);
private Regex _rgxCreateDate = new(@"Date : (?<tofind>(d{2}/d{2}/d{4}))");
private Regex _rgxCreateBy = new(@"Rapporteur : (?<tofind>(.*))");
[Flags]
private enum Found
{
None = 0,
PaymentDate = 1,
Reference = 2,
CreateDate = 4,
CreateBy = 8
}
public TanaReportData(List<string> report)
{
Found fnd = 0;
foreach(var line in report)
{
if(_rgxPaymentDate.IsMatch(line) amp;amp; !fnd.HasFlag(Found.PaymentDate))
{
Date = parseDate(_rgxPaymentDate, line);
fnd |= Found.PaymentDate;
}
if(_rgxReference.IsMatch(line) amp;amp; !fnd.HasFlag(Found.Reference))
{
Reference = parseString(_rgxReference, line);
fnd |= Found.Reference;
}
if(_rgxCreateDate.IsMatch(line) amp;amp; !fnd.HasFlag(Found.CreateDate))
{
CreatedOn = parseDate(_rgxCreateDate,line);
fnd |= Found.CreateDate;
}
if(_rgxCreateBy.IsMatch(line) amp;amp; !fnd.HasFlag(Found.CreateBy))
{
CreatedBy = parseString(_rgxCreateBy, line);
fnd |= Found.CreateBy;
}
if(fnd.HasFlag(Found.PaymentDate | Found.Reference | Found.CreateDate | Found.CreateBy))
{
break;
}
}
}
private string parseString(Regex regex, string line)
{
var match = regex.Match(line);
return match.Groups["tofind"].Value;
}
private DateTime parseDate(Regex regex, string line)
{
var match = regex.Match(line);
return DateTime.Parse(match.Groups["tofind"].Value, fr.DateTimeFormat);
}
public DateTime Date { get; set; }
public string Reference { get; set; }
public DateTime CreatedOn { get; set; }
public string CreatedBy { get; set; }
}
}
(No I’m not saying any of this is pretty, but at least it works).
This extracts data I need to construct the tabular data I will pull later on.
This line seems to be the crux of my confusion:
var reportData = serviceProvider.GetRequiredService<TanaReportData>(lines => new TanaReportData(lines));
How given the options and Ihost host do I know either late create the singleton (for me I don’t think life time matters. I will exist now for the duration of the parsing of a single file) or modify it to accept the needed list of strings or do I somehow cause the DI container to accept the list of strings that I have just gotten so when the singleton is constructed I have something to use or am I better off just doing another refactor.
Thanks Geoffrey