Skip to content

Commit 449f7e7

Browse files
authored
Merge pull request #12 from AzureCosmosDB/develop
Reorganizing main application to use System.CommandLine hosting
2 parents 5a18e52 + 4fed2e0 commit 449f7e7

File tree

6 files changed

+363
-160
lines changed

6 files changed

+363
-160
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
using Microsoft.DataTransfer.Interfaces;
2+
using Microsoft.Extensions.Configuration;
3+
using Microsoft.Extensions.Logging;
4+
using System;
5+
using System.ComponentModel.Composition.Hosting;
6+
7+
namespace Microsoft.DataTransfer.Core
8+
{
9+
public class ExtensionLoader
10+
{
11+
private readonly IConfiguration _configuration;
12+
private readonly ILogger _logger;
13+
14+
public ExtensionLoader(IConfiguration configuration, ILogger<ExtensionLoader> logger)
15+
{
16+
_configuration = configuration;
17+
_logger = logger;
18+
}
19+
20+
public string GetExtensionFolderPath()
21+
{
22+
return GetExtensionFolderPath(_configuration, _logger);
23+
}
24+
25+
public static string GetExtensionFolderPath(IConfiguration configuration, ILogger logger)
26+
{
27+
var configPath = configuration.GetValue<string>("ExtensionsPath");
28+
if (!string.IsNullOrWhiteSpace(configPath))
29+
{
30+
try
31+
{
32+
var fullPath = Path.GetFullPath(configPath);
33+
if (!Directory.Exists(fullPath))
34+
{
35+
Directory.CreateDirectory(fullPath);
36+
}
37+
38+
return fullPath;
39+
}
40+
catch (Exception ex)
41+
{
42+
logger.LogWarning(ex, "Configured path {ExtensionsPath} is invalid. Using default instead.", configPath);
43+
}
44+
}
45+
46+
var exeFolder = AppContext.BaseDirectory;
47+
var path = Path.Combine(exeFolder, "Extensions");
48+
var di = new DirectoryInfo(path);
49+
if (!di.Exists)
50+
{
51+
di.Create();
52+
}
53+
return di.FullName;
54+
}
55+
56+
public CompositionContainer BuildExtensionCatalog(string extensionsPath)
57+
{
58+
var catalog = new AggregateCatalog();
59+
_logger.LogInformation("Loading extensions from {ExtensionsPath}", extensionsPath);
60+
catalog.Catalogs.Add(new DirectoryCatalog(extensionsPath, "*Extension.dll"));
61+
return new CompositionContainer(catalog);
62+
}
63+
64+
public List<T> LoadExtensions<T>(CompositionContainer container)
65+
where T : class, IDataTransferExtension
66+
{
67+
var sources = new List<T>();
68+
69+
foreach (var exportedExtension in container.GetExports<T>())
70+
{
71+
_logger.LogDebug("Loaded extension {ExtensionName} as {ExtensionType}", exportedExtension.Value.DisplayName, typeof(T).Name);
72+
sources.Add(exportedExtension.Value);
73+
}
74+
75+
_logger.LogInformation("{ExtensionCount} Extensions Loaded for type {ExtensionType}", sources.Count, typeof(T).Name);
76+
77+
return sources;
78+
}
79+
}
80+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using Microsoft.DataTransfer.Interfaces;
2+
using Microsoft.Extensions.Logging;
3+
using System.CommandLine;
4+
using System.CommandLine.Invocation;
5+
using System.ComponentModel.Composition.Hosting;
6+
7+
namespace Microsoft.DataTransfer.Core
8+
{
9+
public class ListCommand : Command
10+
{
11+
public ListCommand()
12+
: base("list", "Loads and lists all available extensions")
13+
{
14+
}
15+
16+
public class CommandHandler : ICommandHandler
17+
{
18+
private readonly ILogger<CommandHandler> _logger;
19+
private readonly ExtensionLoader _extensionLoader;
20+
21+
public CommandHandler(ExtensionLoader extensionLoader, ILogger<CommandHandler> logger)
22+
{
23+
_logger = logger;
24+
_extensionLoader = extensionLoader;
25+
}
26+
27+
public int Invoke(InvocationContext context)
28+
{
29+
string extensionsPath = _extensionLoader.GetExtensionFolderPath();
30+
CompositionContainer container = _extensionLoader.BuildExtensionCatalog(extensionsPath);
31+
32+
var sources = _extensionLoader.LoadExtensions<IDataSourceExtension>(container);
33+
var sinks = _extensionLoader.LoadExtensions<IDataSinkExtension>(container);
34+
35+
Console.WriteLine($"{sources.Count} Source Extensions");
36+
foreach (var extension in sources)
37+
{
38+
Console.WriteLine($"\t{extension.DisplayName}");
39+
}
40+
41+
Console.WriteLine($"{sinks.Count} Sink Extensions");
42+
foreach (var extension in sinks)
43+
{
44+
Console.WriteLine($"\t{extension.DisplayName}");
45+
}
46+
47+
return 0;
48+
}
49+
50+
public Task<int> InvokeAsync(InvocationContext context)
51+
{
52+
return Task.FromResult(Invoke(context));
53+
}
54+
}
55+
}
56+
}

Core/Microsoft.DataTransfer.Core/Microsoft.DataTransfer.Core.csproj

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="6.0.1" />
1717
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
1818
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" />
19+
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
20+
<PackageReference Include="System.CommandLine.Hosting" Version="0.4.0-alpha.22272.1" />
1921
<PackageReference Include="System.ComponentModel.Composition" Version="6.0.0" />
2022
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.0" />
2123
</ItemGroup>
+37-155
Original file line numberDiff line numberDiff line change
@@ -1,178 +1,60 @@
1-
using System.ComponentModel.Composition.Hosting;
2-
using System.Reflection;
3-
using Microsoft.DataTransfer.Interfaces;
1+
using System.CommandLine;
42
using Microsoft.Extensions.Configuration;
53
using Microsoft.Extensions.DependencyInjection;
64
using Microsoft.Extensions.Hosting;
7-
using Microsoft.Extensions.Logging;
5+
using System.CommandLine.Builder;
6+
using System.CommandLine.Hosting;
7+
using System.CommandLine.Parsing;
8+
using System.CommandLine.Help;
89

910
namespace Microsoft.DataTransfer.Core;
1011

1112
class Program
1213
{
13-
public static async Task Main(string[] args)
14+
public static async Task<int> Main(string[] args)
1415
{
15-
using IHost host = Host.CreateDefaultBuilder(args)
16-
.ConfigureAppConfiguration(cfg =>
17-
{
18-
cfg.AddUserSecrets<Program>();
19-
})
20-
.Build();
21-
22-
IConfiguration configuration = host.Services.GetRequiredService<IConfiguration>();
23-
var loggerFactory = host.Services.GetRequiredService<ILoggerFactory>();
24-
var log = loggerFactory.CreateLogger<Program>();
25-
26-
var options = configuration.Get<DataTransferOptions>();
27-
28-
var hostingProcess = host.RunAsync();
29-
30-
var catalog = new AggregateCatalog();
31-
string extensionsPath = GetExtensionFolderPath(configuration, log);
32-
log.LogInformation("Loading extensions from {ExtensionsPath}", extensionsPath);
33-
catalog.Catalogs.Add(new DirectoryCatalog(extensionsPath, "*Extension.dll"));
34-
var container = new CompositionContainer(catalog);
35-
36-
var sources = LoadExtensions<IDataSourceExtension>(container);
37-
var sinks = LoadExtensions<IDataSinkExtension>(container);
38-
39-
log.LogInformation("{TotalExtensionCount} Extensions Loaded", sources.Count + sinks.Count);
40-
41-
var source = GetExtensionSelection(options.Source, sources, "Source");
42-
var sourceConfig = BuildSettingsConfiguration(configuration, options.SourceSettingsPath, $"{source.DisplayName}SourceSettings", options.Source == null);
43-
44-
var sink = GetExtensionSelection(options.Sink, sinks, "Sink");
45-
var sinkConfig = BuildSettingsConfiguration(configuration, options.SinkSettingsPath, $"{sink.DisplayName}SinkSettings", options.Sink == null);
46-
47-
var data = source.ReadAsync(sourceConfig, loggerFactory.CreateLogger(source.GetType().Name));
48-
await sink.WriteAsync(data, sinkConfig, source, loggerFactory.CreateLogger(sink.GetType().Name));
49-
50-
log.LogInformation("Done");
16+
var rootCommand = new RootCommand("Azure data migration tool") { TreatUnmatchedTokensAsErrors = false };
17+
rootCommand.AddCommand(new RunCommand());
18+
rootCommand.AddCommand(new ListCommand());
5119

52-
Console.WriteLine("Enter to Quit...");
53-
Console.ReadLine();
20+
var cmdlineBuilder = new CommandLineBuilder(rootCommand);
5421

55-
await host.StopAsync();
56-
await hostingProcess;
57-
}
58-
59-
private static string GetExtensionFolderPath(IConfiguration configuration, ILogger logger)
60-
{
61-
var configPath = configuration.GetValue<string>("ExtensionsPath");
62-
if (!string.IsNullOrWhiteSpace(configPath))
63-
{
64-
try
22+
var parser = cmdlineBuilder.UseHost(_ => Host.CreateDefaultBuilder(args),
23+
builder =>
6524
{
66-
var fullPath = Path.GetFullPath(configPath);
67-
if (!Directory.Exists(fullPath))
25+
builder.ConfigureAppConfiguration(cfg =>
6826
{
69-
Directory.CreateDirectory(fullPath);
70-
}
71-
72-
return fullPath;
73-
}
74-
catch (Exception ex)
75-
{
76-
logger.LogWarning(ex, "Configured path {ExtensionsPath} is invalid. Using default instead.", configPath);
77-
}
78-
}
79-
80-
var exeFolder = AppContext.BaseDirectory;
81-
var path = Path.Combine(exeFolder, "Extensions");
82-
var di = new DirectoryInfo(path);
83-
if (!di.Exists)
84-
{
85-
di.Create();
86-
}
87-
return di.FullName;
88-
}
89-
90-
private static List<T> LoadExtensions<T>(CompositionContainer container)
91-
where T : class, IDataTransferExtension
92-
{
93-
var sources = new List<T>();
94-
95-
foreach (var exportedExtension in container.GetExports<T>())
96-
{
97-
sources.Add(exportedExtension.Value);
98-
}
99-
100-
return sources;
101-
}
102-
103-
private static T GetExtensionSelection<T>(string? selectionName, List<T> extensions, string inputPrompt)
104-
where T : class, IDataTransferExtension
105-
{
106-
if (!string.IsNullOrWhiteSpace(selectionName))
107-
{
108-
var extension = extensions.FirstOrDefault(s => selectionName.Equals(s.DisplayName, StringComparison.OrdinalIgnoreCase));
109-
if (extension != null)
110-
{
111-
Console.WriteLine($"Using {extension.DisplayName} {inputPrompt}");
112-
return extension;
113-
}
114-
}
115-
116-
Console.WriteLine($"Select {inputPrompt}");
117-
for (var index = 0; index < extensions.Count; index++)
118-
{
119-
var extension = extensions[index];
120-
Console.WriteLine($"{index + 1}:{extension.DisplayName}");
121-
}
122-
123-
string? selection = "";
124-
int input;
125-
while (!int.TryParse(selection, out input) || input > extensions.Count)
126-
{
127-
selection = Console.ReadLine();
128-
}
27+
cfg.AddUserSecrets<Program>();
28+
}).ConfigureServices((hostContext, services) =>
29+
{
30+
services.AddTransient<ExtensionLoader>();
31+
})
32+
.UseCommandHandler<RunCommand, RunCommand.CommandHandler>()
33+
.UseCommandHandler<ListCommand, ListCommand.CommandHandler>();
34+
})
35+
.UseHelp(AddAdditionalArgumentsHelp)
36+
.UseDefaults().Build();
12937

130-
return extensions[input - 1];
38+
return await parser.InvokeAsync(args);
13139
}
13240

133-
private static IConfiguration BuildSettingsConfiguration(IConfiguration configuration, string? settingsPath, string configSection, bool promptForFile)
41+
private static void AddAdditionalArgumentsHelp(HelpContext helpContext)
13442
{
135-
IConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
136-
if (!string.IsNullOrEmpty(settingsPath))
137-
{
138-
configurationBuilder = configurationBuilder.AddJsonFile(settingsPath);
139-
}
140-
else if (promptForFile)
43+
helpContext.HelpBuilder.CustomizeLayout(_ =>
14144
{
142-
Console.Write($"Load settings from a file? (y/n):");
143-
var response = Console.ReadLine();
144-
if (IsYesResponse(response))
145-
{
146-
Console.Write("Path to file: ");
147-
var path = Console.ReadLine();
148-
if (!string.IsNullOrWhiteSpace(path))
149-
{
150-
configurationBuilder = configurationBuilder.AddJsonFile(path);
151-
}
152-
}
153-
else
45+
var layout = HelpBuilder.Default.GetLayout().ToList();
46+
layout.Remove(HelpBuilder.Default.AdditionalArgumentsSection());
47+
if (helpContext.Command.GetType() == typeof(RunCommand))
15448
{
155-
Console.Write($"Configuration section to read settings? (default={configSection}):");
156-
response = Console.ReadLine();
157-
if (!string.IsNullOrWhiteSpace(response))
49+
layout.Add(ctx =>
15850
{
159-
configSection = response;
160-
}
51+
ctx.Output.WriteLine("Additional Arguments:");
52+
ctx.Output.WriteLine(" Extension specific settings can be provided as additional arguments in the form:");
53+
ctx.HelpBuilder.WriteColumns(new List<TwoColumnHelpRow> { new("--<extension><Source|Sink>Settings:<name> <value>", "ex: --JsonSourceSettings:FilePath MyDataFile.json") }.AsReadOnly(), ctx);
54+
});
16155
}
162-
}
163-
164-
return configurationBuilder
165-
.AddConfiguration(configuration.GetSection(configSection))
166-
.Build();
167-
}
168-
169-
private static bool IsYesResponse(string? response)
170-
{
171-
if (response?.Equals("y", StringComparison.CurrentCultureIgnoreCase) == true)
172-
return true;
173-
if (response?.Equals("yes", StringComparison.CurrentCultureIgnoreCase) == true)
174-
return true;
17556

176-
return false;
57+
return layout;
58+
});
17759
}
178-
}
60+
}

Core/Microsoft.DataTransfer.Core/Properties/launchSettings.json

+5-5
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,23 @@
66
},
77
"JSON->Cosmos": {
88
"commandName": "Project",
9-
"commandLineArgs": "--source json --sink cosmos-nosql --SourceSettingsPath=c:\\temp\\JsonSourceSettings.json --SinkSettingsPath=c:\\temp\\CosmosSinkSettings.json"
9+
"commandLineArgs": "run -from json --sink cosmos-nosql --source-settings c:\\temp\\JsonSourceSettings.json --sink-settings c:\\temp\\CosmosSinkSettings.json"
1010
},
1111
"Cosmos->JSON": {
1212
"commandName": "Project",
13-
"commandLineArgs": "--source cosmos-nosql --sink json --SourceSettingsPath=c:\\temp\\CosmosSourceSettings.json --SinkSettingsPath=c:\\temp\\JsonSinkSettings.json"
13+
"commandLineArgs": "run --source cosmos-nosql --sink json --source-settings=c:\\temp\\CosmosSourceSettings.json --sink-settings=c:\\temp\\JsonSinkSettings.json"
1414
},
1515
"SqlServer->Cosmos": {
1616
"commandName": "Project",
17-
"commandLineArgs": "--source sqlServer --sink cosmos-nosql --SourceSettingsPath=c:\\temp\\SqlSourceSettings.json --SinkSettingsPath=c:\\temp\\CosmosSinkSettings.json"
17+
"commandLineArgs": "run --source sqlServer --sink cosmos-nosql --source-settings=c:\\temp\\SqlSourceSettings.json --sink-settings=c:\\temp\\CosmosSinkSettings.json"
1818
},
1919
"JSON->SqlServer": {
2020
"commandName": "Project",
21-
"commandLineArgs": "--source json --sink sqlServer --JsonSourceSettings:FilePath=c:\\temp\\test-json-sql-in.json --SinkSettingsPath=c:\\temp\\SqlSinkSettings.json"
21+
"commandLineArgs": "run --source json --sink sqlServer --JsonSourceSettings:FilePath=c:\\temp\\test-json-sql-in.json --SinkSettingsPath=c:\\temp\\SqlSinkSettings.json"
2222
},
2323
"JSON URI->Cosmos": {
2424
"commandName": "Project",
25-
"commandLineArgs": "--source json --sink cosmos-nosql --JsonSourceSettings:FilePath=https://raw.githubusercontent.com/AzureCosmosDB/data-migration-desktop-tool/feature/cosmos-configuration/Extensions/Json/Microsoft.DataTransfer.JsonExtension.UnitTests/Data/ArraysTypesNesting.json --SinkSettingsPath=c:\\temp\\CosmosSinkSettings.json"
25+
"commandLineArgs": "run --source json --sink cosmos-nosql --JsonSourceSettings:FilePath=https://raw.githubusercontent.com/AzureCosmosDB/data-migration-desktop-tool/feature/cosmos-configuration/Extensions/Json/Microsoft.DataTransfer.JsonExtension.UnitTests/Data/ArraysTypesNesting.json --SinkSettingsPath=c:\\temp\\CosmosSinkSettings.json"
2626
}
2727
}
2828
}

0 commit comments

Comments
 (0)