title: DependencyInjection CommandLine API and what I think is late binding
body: <p>I'm running into a wall where it is likely I should refactor.  I recently took a nicely operational Commandline program that didn't do any dependency inject and decided it might be nice to use the SystemCommandline API (<a href="https://github.com/dotnet/command-line-api/blob/main/samples/HostingPlayground/Program.cs" rel="nofollow noreferrer">https://github.com/dotnet/command-line-api/blob/main/samples/HostingPlayground/Program.cs</a>).</p>
  <p>I like the approach (although I also wonder if I should just write this as a PowerShell Command and be done with it).</p>
  <p>I have a strong library. I'm parsing PDFs to pull table data out.  What I am looking for by doing this is to be able to put many different fronts on top of my library.  Drop in into a WPF app, Run it as a service, create a Kestrel based web application.</p>
  <p>I'm really struggling with how to drop the options that I am generating via Commandline into my actual code.</p>
  <p>Here is the Program.cs I've cobbled together:</p>
  <pre><code>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
      {
  
          /// &lt;summary&gt;
          /// 
          /// &lt;/summary&gt;
          /// &lt;param name=&quot;args&quot;&gt;&lt;/param&gt;
          /// &lt;returns&gt;&lt;/returns&gt;
          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 =&gt;
                  {
                      // Read the appsettings.json file as we need it to initialize the file logger later
                      _ = host.ConfigureAppConfiguration((context, config) =&gt;
                      {
                          config.SetBasePath(Directory.GetCurrentDirectory())
                          .AddJsonFile(&quot;appsettings.json&quot;, optional: true)
                          .Build();
                      });
                      // Now that the appsettings has been built / extracted setup the logger
                      _ = host.ConfigureLogging((context, logging) =&gt;
                      {
                          _ = logging.AddFileLogger(options =&gt;
                          {
                              // using the built appsettings above pull the options for the logger via the sections
                              // see appsettings
                              context.Configuration.GetSection(&quot;Logging&quot;)
                              .GetSection(&quot;HMInvoiceParserLogFile&quot;)
                              .GetSection(&quot;Options&quot;)
                              // Bind them back into the file logger
                              .Bind(options);
                          });
                      });
                      _ = host.ConfigureServices((context, services) =&gt;
                      {
                          services.AddSingleton&lt;IExtractRows&lt;TanaRow, TanaReportData&gt;, ExtractTanaRows&gt;();
                          services.AddSingleton&lt;ILineExtractor, LineExtractor&gt;();
                          services.AddSingleton&lt;IReportData, TanaReportData&gt;();
                      });
                  })
                  .UseDefaults()
                  .Build()
                  .InvokeAsync(args);
          }
  
          private static CommandLineBuilder BuildCommandLine()
          {
              // Create a list of Commands arguments that are required
              var root = new RootCommand{
                  new Option&lt;string&gt;(&quot;--FileName&quot;)
                  {
                      IsRequired = true
                  },
                  new Option&lt;string&gt;(&quot;--FileType&quot;)
                  {
                      IsRequired = true
                  }
              };
              // Push the extracted command arguments into the program to run
              root.Handler = CommandHandler.Create&lt;ExtractorOptions, IHost&gt;(Run);
              return new CommandLineBuilder(root);
          }
  
          /// &lt;summary&gt;
          /// The meat of the program that uses the DI container to construct and run
          /// work
          /// &lt;/summary&gt;
          /// &lt;param name=&quot;options&quot;&gt;&lt;/param&gt;
          /// &lt;param name=&quot;host&quot;&gt;&lt;/param&gt;
          private static void Run(ExtractorOptions options, IHost host)
          {
              var serviceProvider = host.Services;
              var lineExtractor = serviceProvider.GetRequiredService&lt;ILineExtractor&gt;();
  
              var loggerFactory = serviceProvider.GetRequiredService&lt;ILoggerFactory&gt;();
              var logger = loggerFactory.CreateLogger(typeof(Program));
  
              var fname = options.Filename;
              var ftype = options.FileType;
  
  
              List&lt;string&gt; lines = lineExtractor.GetLines(fname);
              var reportData = serviceProvider.GetRequiredService&lt;IReportData&gt;(lines =&gt; new TanaReportData(lines));
              logger.LogInformation(1000, &quot;file requested for: {fname} : {ftype}&quot;, fname, ftype);
          }
      }
  
  }
  </code></pre>
  <p>As you can see the BuildCommandLine function is called async and then we get to the meat of the container construction.
  Since its a command line at this point to process a single PFS file after the container is constructed it calls the run program.</p>
  <p>I've tested the code and the container works fine (no I don't care for the syntax personally so will refactor later.</p>
  <p>My issue is with the Run program.</p>
  <p>My singleton instances are converted from what where static classes (I get logger benefit here which made it worthwhile to me)</p>
  <p>And effectively wrap a single operation.</p>
  <ol>
  <li>Extract the lines of the PDF</li>
  <li>Parse each line based on a set of rules / regex (thank you <a href="https://github.com/UglyToad/PdfPig/blob/master/examples/ExtractTextWithNewlines.cs" rel="nofollow noreferrer">PdfPig</a> for doing all the hard work)</li>
  <li>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.</li>
  </ol>
  <p>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.</p>
  <p>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.</p>
  <pre><code>var lineExtractor = serviceProvider.GetRequiredService&lt;ILineExtractor&gt;();
  List&lt;string&gt; lines = lineExtractor.GetLines(fname);
  </code></pre>
  <p>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.</p>
  <p>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</p>
  <pre><code>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(&quot;fr&quot;);
          private static RegexOptions _regexOptions = RegexOptions.Compiled | RegexOptions.ExplicitCapture;
          private Regex _rgxPaymentDate = new(@&quot;Du\s(?&lt;tofind&gt;(\d{2}\/\d{2}\/\d{4}))\sau&quot;, _regexOptions);
          private Regex _rgxReference = new(@&quot;Réference : (?&lt;tofind&gt;(.*))&quot;, _regexOptions);
          private Regex _rgxCreateDate = new(@&quot;Date : (?&lt;tofind&gt;(\d{2}\/\d{2}\/\d{4}))&quot;);
          private Regex _rgxCreateBy = new(@&quot;Rapporteur : (?&lt;tofind&gt;(.*))&quot;);
  
          [Flags]
          private enum Found
          {
              None = 0,
              PaymentDate = 1,
              Reference = 2,
              CreateDate = 4,
              CreateBy = 8
          }
  
          public TanaReportData(List&lt;string&gt; 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[&quot;tofind&quot;].Value;
          }
          private DateTime parseDate(Regex regex, string line)
          {
              var match = regex.Match(line);
              return DateTime.Parse(match.Groups[&quot;tofind&quot;].Value, fr.DateTimeFormat);
          }
          public DateTime Date { get; set; }
          public string Reference { get; set; }
          public DateTime CreatedOn { get; set; }
          public string CreatedBy { get; set; }
      }
  }
  </code></pre>
  <p>(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.</p>
  <p>This line seems to be the crux of my confusion:</p>
  <pre><code>var reportData = serviceProvider.GetRequiredService&lt;TanaReportData&gt;(lines =&gt; new TanaReportData(lines));
  </code></pre>
  <p>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.</p>
  <p>Thanks Geoffrey</p>