Adding bulk migration tool
Adding readme
This commit is contained in:
Kyle Swartz 2022-03-17 16:44:42 -04:00
Родитель b1625c9f54
Коммит 7cfd899fe4
73 изменённых файлов: 2320 добавлений и 0 удалений

Просмотреть файл

@ -0,0 +1,30 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.32112.339
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NCEBulkMigrationTool", "NCEBulkMigrationTool\NCEBulkMigrationTool.csproj", "{E38DE1C2-FBDC-45A9-9260-E8A84D4AA944}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{FA662682-F134-4839-B591-495F741EB4E5}"
ProjectSection(SolutionItems) = preProject
nuget.config = nuget.config
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{E38DE1C2-FBDC-45A9-9260-E8A84D4AA944}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E38DE1C2-FBDC-45A9-9260-E8A84D4AA944}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E38DE1C2-FBDC-45A9-9260-E8A84D4AA944}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E38DE1C2-FBDC-45A9-9260-E8A84D4AA944}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A4D8510A-2D70-40AC-BA22-2C4B6DBADA1B}
EndGlobalSection
EndGlobal

Просмотреть файл

@ -0,0 +1,18 @@
namespace NCEBulkMigrationTool;
internal record AppSettings
{
public string AppId { get; init; } = string.Empty;
public string Upn { get; init; } = string.Empty;
public string Domain
{
get
{
var index = this.Upn.LastIndexOf("@");
if (index == -1) { return string.Empty; }
return this.Upn[++index..];
}
}
}

Просмотреть файл

@ -0,0 +1,15 @@
namespace NCEBulkMigrationTool;
internal class CsvProvider
{
public async Task ExportCsv<T>(IEnumerable<T> data, string fileName)
{
int index = fileName.LastIndexOf('/');
var directory = fileName[..index];
Directory.CreateDirectory(directory);
using var subscriptionsWriter = new StreamWriter(fileName);
using var subscriptionsCsvWriter = new CsvWriter(subscriptionsWriter, CultureInfo.InvariantCulture);
await subscriptionsCsvWriter.WriteRecordsAsync(data);
}
}

Просмотреть файл

@ -0,0 +1,88 @@
namespace NCEBulkMigrationTool;
internal class CustomerProvider : ICustomerProvider
{
private readonly ITokenProvider tokenProvider;
public CustomerProvider(ITokenProvider tokenProvider)
{
this.tokenProvider = tokenProvider;
}
public async Task<bool> ExportCustomersAsync()
{
Console.WriteLine("Generating token...");
var authenticationResult = await this.tokenProvider.GetTokenAsync();
Console.WriteLine("Token generated...");
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authenticationResult.AccessToken);
httpClient.DefaultRequestHeaders.Add(Constants.PartnerCenterClientHeader, Constants.ClientName);
Console.WriteLine("Getting customers");
var allCustomers = new List<CompanyProfile>();
string continuationToken = string.Empty;
string route = Routes.GetCustomers;
do
{
var request = new HttpRequestMessage(HttpMethod.Get, route);
request.Headers.Add("MS-CorrelationId", Guid.NewGuid().ToString());
if (!string.IsNullOrWhiteSpace(continuationToken))
{
request.Headers.Add("MS-ContinuationToken", continuationToken);
route = $"{Routes.GetCustomers}&seekOperation=next";
}
request.RequestUri = new Uri(route);
var response = await httpClient.SendAsync(request).ConfigureAwait(false);
if (response.StatusCode == HttpStatusCode.BadRequest)
{
var customerErrorResponse = await response.Content.ReadFromJsonAsync<CustomerErrorResponse>();
//// If we get error code 2000 then it means we fetched all the records.
if (customerErrorResponse?.Code == 2000)
{
break;
}
}
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
authenticationResult = await this.tokenProvider.GetTokenAsync();
httpClient.DefaultRequestHeaders.Clear();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authenticationResult.AccessToken);
httpClient.DefaultRequestHeaders.Add(Constants.PartnerCenterClientHeader, Constants.ClientName);
request = new HttpRequestMessage(HttpMethod.Get, route);
request.Headers.Add("MS-CorrelationId", Guid.NewGuid().ToString());
request.Headers.Add("MS-ContinuationToken", continuationToken);
response = await httpClient.SendAsync(request).ConfigureAwait(false);
}
response.EnsureSuccessStatusCode();
var customerCollection = await response.Content.ReadFromJsonAsync<ResourceCollection<Customer>>();
if (customerCollection?.Items.Any() == false)
{
Console.WriteLine("No customers found");
Console.ReadLine();
return true;
}
continuationToken = customerCollection!.ContinuationToken;
var customers = customerCollection!.Items.Select(customer => customer.CompanyProfile);
allCustomers.AddRange(customers);
} while (!string.IsNullOrWhiteSpace(continuationToken));
var csvProvider = new CsvProvider();
Console.WriteLine("Exporting customers");
await csvProvider.ExportCsv(allCustomers, $"{Constants.OutputFolderPath}/customers.csv");
Console.WriteLine($"Exported customers at {Environment.CurrentDirectory}/{Constants.OutputFolderPath}/customers.csv");
return true;
}
}

Просмотреть файл

@ -0,0 +1,16 @@
global using System.Net.Http.Headers;
global using System.Net.Http.Json;
global using System.Text;
global using System.Text.Json;
global using System.Text.Json.Serialization;
global using NCEBulkMigrationTool;
global using CsvHelper;
global using System.Collections.Concurrent;
global using System.Globalization;
global using Microsoft.Identity.Client;
global using System.Net;
global using System.Configuration;
global using Microsoft.Extensions.Configuration;
global using System.Diagnostics;
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.Hosting;

Просмотреть файл

@ -0,0 +1,6 @@
namespace NCEBulkMigrationTool;
internal interface ICustomerProvider
{
Task<bool> ExportCustomersAsync();
}

Просмотреть файл

@ -0,0 +1,8 @@
namespace NCEBulkMigrationTool;
internal interface INewCommerceMigrationProvider
{
Task<bool> UploadNewCommerceMigrationsAsync();
Task<bool> ExportNewCommerceMigrationStatusAsync();
}

Просмотреть файл

@ -0,0 +1,8 @@
namespace NCEBulkMigrationTool;
internal interface ISubscriptionProvider
{
Task<bool> ExportLegacySubscriptionsAsync();
Task<bool> ExportModernSubscriptionsAsync();
}

Просмотреть файл

@ -0,0 +1,6 @@
namespace NCEBulkMigrationTool;
internal interface ITokenProvider
{
Task<AuthenticationResult> GetTokenAsync();
}

Просмотреть файл

@ -0,0 +1,327 @@
namespace NCEBulkMigrationTool;
internal record ResourceCollection<T>(IEnumerable<T> Items, int TotalCount, string ContinuationToken);
internal record Customer(Guid Id, CompanyProfile CompanyProfile);
internal record CompanyProfile(Guid TenantId, string Domain, string CompanyName);
internal record Subscription
{
public string Id { get; init; } = string.Empty;
public string FriendlyName { get; init; } = string.Empty;
public string OfferId { get; init; } = string.Empty;
public string OfferName { get; init; } = string.Empty;
public int Quantity { get; set; }
public string ParentSubscriptionId { get; init; } = string.Empty;
public DateTime EffectiveStartDate { get; init; }
public DateTime CommitmentEndDate { get; init; }
public SubscriptionStatus Status { get; init; }
public BillingCycleType BillingCycle { get; set; }
public string TermDuration { get; set; } = string.Empty;
public string PartnerId { get; set; } = string.Empty;
public ItemType ProductType { get; init; } = new ItemType();
public NewCommerceEligibility MigrationEligibility { get; set; } = new NewCommerceEligibility();
public string MigratedFromSubscriptionId { get; init; } = string.Empty;
}
internal record MigrationRequest
{
public string PartnerTenantId { get; init; } = string.Empty;
public string IndirectResellerMpnId { get; init; } = string.Empty;
public string CustomerName { get; init; } = string.Empty;
public Guid CustomerTenantId { get; init; }
public string LegacySubscriptionId { get; init; } = string.Empty;
public string LegacySubscriptionName { get; init; } = string.Empty;
public string LegacyProductName { get; init; } = string.Empty;
public DateTime ExpirationDate { get; init; }
public bool MigrationEligible { get; set; }
public string NcePsa { get; set; } = string.Empty;
public string CurrentTerm { get; init; } = string.Empty;
public string CurrentBillingPlan { get; init; } = string.Empty;
public int CurrentSeatCount { get; set; }
public bool StartNewTermInNce { get; init; }
public string Term { get; init; } = string.Empty;
public string BillingPlan { get; init; } = string.Empty;
public int SeatCount { get; set; }
public bool AddOn { get; init; }
public string BaseSubscriptionId { get; init; } = string.Empty;
public string MigrationIneligibilityReason { get; set; } = string.Empty;
}
internal record ModernSubscription
{
public string PartnerTenantId { get; init; } = string.Empty;
public string IndirectResellerMpnId { get; init; } = string.Empty;
public string CustomerName { get; init; } = string.Empty;
public Guid CustomerTenantId { get; init; }
public string SubscriptionId { get; init; } = string.Empty;
public string SubscriptionName { get; init; } = string.Empty;
public string ProductName { get; init; } = string.Empty;
public DateTime ExpirationDate { get; init; }
public string Psa { get; set; } = string.Empty;
public string Term { get; init; } = string.Empty;
public string BillingPlan { get; init; } = string.Empty;
public int SeatCount { get; set; }
public string MigratedFromSubscriptionId { get; init; } = string.Empty;
}
public record ItemType
{
public string Id { get; init; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
}
public record NewCommerceMigration
{
public string Id { get; set; } = string.Empty;
public DateTime? StartedTime { get; set; }
public DateTime? CompletedTime { get; set; }
public string CurrentSubscriptionId { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;
public string CustomerTenantId { get; set; } = string.Empty;
public string CatalogItemId { get; set; } = string.Empty;
public string NewCommerceSubscriptionId { get; set; } = string.Empty;
public string NewCommerceOrderId { get; set; } = string.Empty;
public DateTime? SubscriptionEndDate { get; set; }
public int Quantity { get; set; }
public string TermDuration { get; set; } = string.Empty;
public string BillingCycle { get; set; } = string.Empty;
public bool PurchaseFullTerm { get; set; }
public string ExternalReferenceId { get; set; } = string.Empty;
public IEnumerable<NewCommerceMigration> AddOnMigrations { get; set; } = Enumerable.Empty<NewCommerceMigration>();
}
public record NewCommerceMigrationError
{
public int Code { get; set; } = 0;
public string Description { get; set; } = string.Empty;
}
public record MigrationResult
{
public string PartnerTenantId { get; init; } = string.Empty;
public string IndirectResellerMpnId { get; init; } = string.Empty;
public string CustomerName { get; init; } = string.Empty;
public Guid CustomerTenantId { get; init; }
public string LegacySubscriptionId { get; init; } = string.Empty;
public string LegacySubscriptionName { get; init; } = string.Empty;
public string LegacyProductName { get; init; } = string.Empty;
public DateTime ExpirationDate { get; init; }
public bool AddOn { get; init; }
public string MigrationStatus { get; init; } = string.Empty;
public bool StartedNewTermInNce { get; init; }
public string NCETermDuration { get; init; } = string.Empty;
public string NCEBillingPlan { get; init; } = string.Empty;
public int NCESeatCount { get; set; }
public int? ErrorCode { get; set; } = null;
public string ErrorReason { get; init; } = string.Empty;
public string NCESubscriptionId { get; init; } = string.Empty;
public string BatchId { get; init; } = string.Empty;
public string MigrationId { get; init; } = string.Empty;
}
public record NewCommerceEligibility
{
public string CurrentSubscriptionId { get; set; } = string.Empty;
public bool IsEligible { get; set; }
public string TermDuration { get; set; } = string.Empty;
public string BillingCycle { get; set; } = string.Empty;
public string CatalogItemId { get; set; } = string.Empty;
public IEnumerable<NewCommerceEligibilityError> Errors { get; set; } = Enumerable.Empty<NewCommerceEligibilityError>();
public IEnumerable<NewCommerceEligibility> AddOnMigrations { get; set; } = Enumerable.Empty<NewCommerceEligibility>();
}
public record NewCommerceEligibilityError
{
public NewCommerceEligibilityErrorCode Code { get; set; }
public string Description { get; set; } = string.Empty;
}
public record CustomerErrorResponse
{
public int Code { get; set; }
public string Message { get; set; } = string.Empty;
}
public enum NewCommerceEligibilityErrorCode
{
SubscriptionStatusNotActive = 0,
SubscriptionBeingProcessingInOms = 1,
SubscriptionTooCloseToTermEnd = 2,
SubscriptionPromotionsPresent = 3,
SubscriptionAddOnsPresent = 4,
NewCommerceProductUnavailable = 5,
TermDurationBillingCycleCombinationNotSupported = 6,
SubscriptionActiveForLessThanOneMonth = 7,
TradeStatusNotAllowed = 8,
}
[JsonConverter(typeof(JsonStringEnumConverter))]
internal enum SubscriptionStatus
{
None = 0,
Active = 1,
Suspended = 2,
Deleted = 3,
Expired = 4,
Pending = 5,
}
[JsonConverter(typeof(JsonStringEnumConverter))]
internal enum BillingCycleType
{
Unknown = 0,
Monthly = 1,
Annual = 2,
None = 3,
OneTime = 4,
Triennial = 5,
Biennial = 6,
}
internal record Constants
{
public const string InputFolderPath = "ncebulkmigration/input";
public const string OutputFolderPath = "ncebulkmigration/output";
public const string PartnerCenterClientHeader = "MS-PartnerCenter-Client";
public const string ClientName = "BulkMigrationTool";
}
internal record Routes
{
/// <summary>
/// Get customers route.
/// </summary>
public const string GetCustomers = "https://api.partnercenter.microsoft.com/v1/customers?size=500";
/// <summary>
/// Get subscriptions route, 0 represents customerId.
/// </summary>
public const string GetSubscriptions = "https://api.partnercenter.microsoft.com/v1/customers/{0}/subscriptions";
/// <summary>
/// Get new commerce migrations route, 0 represents customerId, 1 represents newCommerceMigrationId.
/// </summary>
public const string GetNewCommerceMigration = "https://api.partnercenter.microsoft.com/v1/customers/{0}/migrations/newcommerce/{1}";
/// <summary>
/// Validate migration eligibility route, 0 represents customerId.
/// </summary>
public const string ValidateMigrationEligibility = "https://api.partnercenter.microsoft.com/v1/customers/{0}/migrations/newcommerce/validate";
/// <summary>
/// Post new commerce migration, 0 represents customerId.
/// </summary>
public const string PostNewCommerceMigration = "https://api.partnercenter.microsoft.com/v1/customers/{0}/migrations/newcommerce";
}

Просмотреть файл

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PackAsTool>true</PackAsTool>
<ToolCommandName>ncebulkmigration</ToolCommandName>
<PackageOutputPath>./nupkg</PackageOutputPath>
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
<Version>0.5.0</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CsvHelper" Version="27.1.1" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.40.0" />
</ItemGroup>
<ItemGroup>
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,444 @@
using System.Threading;
namespace NCEBulkMigrationTool;
internal class NewCommerceMigrationProvider : INewCommerceMigrationProvider
{
private readonly ITokenProvider tokenProvider;
public NewCommerceMigrationProvider(ITokenProvider tokenProvider)
{
this.tokenProvider = tokenProvider;
}
public async Task<bool> UploadNewCommerceMigrationsAsync()
{
var csvProvider = new CsvProvider();
var inputFileNames = Directory.EnumerateFiles($"{Constants.InputFolderPath}/subscriptions");
var authenticationResult = await this.tokenProvider.GetTokenAsync();
foreach (var fileName in inputFileNames)
{
Console.WriteLine($"Processing file {fileName}");
using TextReader fileReader = File.OpenText(fileName);
using var csvReader = new CsvReader(fileReader, CultureInfo.InvariantCulture, leaveOpen: true);
var inputMigrationRequests = csvReader.GetRecords<MigrationRequest>().ToList();
if (inputMigrationRequests.Count > 100)
{
Console.WriteLine($"There are too many migration requests in the file: {fileName}. The maximum limit for migration uploads per file is 100. Please fix the input file to continue...");
continue;
}
var migrations = new ConcurrentBag<IEnumerable<MigrationResult>>();
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authenticationResult.AccessToken);
httpClient.DefaultRequestHeaders.Add(Constants.PartnerCenterClientHeader, Constants.ClientName);
var options = new ParallelOptions()
{
MaxDegreeOfParallelism = 5
};
long subscriptionsCntr = 0; //The counter to track
var batchId = Guid.NewGuid().ToString();
var inputBaseMigrationRequests = inputMigrationRequests.Where(m => !m.AddOn && m.MigrationEligible);
var inputAddOnMigrationRequests = inputMigrationRequests.Where(m => m.AddOn && m.MigrationEligible);
await Parallel.ForEachAsync(inputBaseMigrationRequests, options, async (migrationRequest, cancellationToken) =>
{
try
{
var newCommerceMigration = await this.PostNewCommerceMigrationAsync(httpClient, migrationRequest, inputAddOnMigrationRequests, batchId, cancellationToken);
migrations.Add(newCommerceMigration);
}
catch (Exception)
{
Console.WriteLine($"Migration for subscription: {migrationRequest.LegacySubscriptionId} failed.");
}
finally
{
Interlocked.Increment(ref subscriptionsCntr);
Console.WriteLine($"Processed {subscriptionsCntr} subscription migration requests.", subscriptionsCntr);
}
});
csvReader.Dispose();
fileReader.Close();
var index = fileName.LastIndexOf('\\');
var processedFileName = fileName[++index..];
Console.WriteLine("Exporting migrations");
await csvProvider.ExportCsv(migrations.SelectMany(m => m), $"{Constants.OutputFolderPath}/migrations/{processedFileName}_{batchId}.csv");
File.Move(fileName, $"{Constants.InputFolderPath}/subscriptions/processed/{processedFileName}", true);
await Task.Delay(1000 * 60);
Console.WriteLine($"Exported migrations at {Environment.CurrentDirectory}/{Constants.OutputFolderPath}/migrations/{processedFileName}_{batchId}.csv");
}
return true;
}
public async Task<bool> ExportNewCommerceMigrationStatusAsync()
{
var csvProvider = new CsvProvider();
var inputFileNames = Directory.EnumerateFiles($"{Constants.InputFolderPath}/migrations");
var authenticationResult = await this.tokenProvider.GetTokenAsync();
foreach (var fileName in inputFileNames)
{
Console.WriteLine($"Processing file {fileName}");
using TextReader fileReader = File.OpenText(fileName);
using var csvReader = new CsvReader(fileReader, CultureInfo.InvariantCulture, leaveOpen: true);
var inputMigrations = csvReader.GetRecords<MigrationResult>().ToList();
var migrations = new ConcurrentBag<MigrationResult>();
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authenticationResult.AccessToken);
var options = new ParallelOptions()
{
MaxDegreeOfParallelism = 5
};
long migrationsCntr = 0; //The counter to track
var batchId = Guid.NewGuid().ToString();
var inputBaseMigrationRequests = inputMigrations.Where(m => !m.AddOn);
var inputAddOnMigrationRequests = inputMigrations.Where(m => m.AddOn);
await Parallel.ForEachAsync(inputBaseMigrationRequests, options, async (migration, cancellationToken) =>
{
try
{
var newCommerceMigration = await this.GetNewCommerceMigrationByMigrationIdAsync(httpClient, migration, cancellationToken);
migrations.Add(newCommerceMigration.BaseMigrationResult);
if (newCommerceMigration.AddOnMigrationsResult?.Any() == true)
{
foreach (var addOnMigrationResult in newCommerceMigration.AddOnMigrationsResult)
{
var addOnMigration = inputAddOnMigrationRequests.Single(a => a.LegacySubscriptionId.Equals(addOnMigrationResult.CurrentSubscriptionId, StringComparison.OrdinalIgnoreCase));
addOnMigration = addOnMigration with
{
NCESubscriptionId = addOnMigrationResult.NewCommerceSubscriptionId,
MigrationId = newCommerceMigration.BaseMigrationResult.MigrationId,
MigrationStatus = newCommerceMigration.BaseMigrationResult.MigrationStatus,
};
migrations.Add(addOnMigration);
}
}
}
catch (Exception)
{
Console.WriteLine($"Couldn't retrieve status for migrationId: {migration.MigrationId}.");
}
finally
{
Interlocked.Increment(ref migrationsCntr);
Console.WriteLine($"Processed {migrationsCntr} migration status lookups.", migrationsCntr);
}
});
csvReader.Dispose();
fileReader.Close();
var index = fileName.LastIndexOf('\\');
var processedFileName = fileName[++index..];
Console.WriteLine("Exporting migration status.");
await csvProvider.ExportCsv(migrations, $"{Constants.OutputFolderPath}/migrationstatus/{processedFileName}.csv");
File.Move(fileName, $"{Constants.InputFolderPath}/migrations/processed/{processedFileName}", true);
await Task.Delay(1000 * 60);
Console.WriteLine($"Exported migration status at {Environment.CurrentDirectory}/{Constants.OutputFolderPath}/migrationstatus/{processedFileName}.csv");
}
return true;
}
private async Task<(MigrationResult BaseMigrationResult, IEnumerable<NewCommerceMigration> AddOnMigrationsResult)> GetNewCommerceMigrationByMigrationIdAsync(HttpClient httpClient, MigrationResult migrationResult, CancellationToken cancellationToken)
{
// Validate that the migration result has a migrationId, if a migration didn't initiate the migrationId will be empty.
if (string.IsNullOrWhiteSpace(migrationResult.MigrationId))
{
// We cannot determine the status, we should return this migration result.
return (migrationResult, Enumerable.Empty<NewCommerceMigration>());
}
var getNewCommerceMigration = new HttpRequestMessage(HttpMethod.Get, string.Format(Routes.GetNewCommerceMigration, migrationResult.CustomerTenantId, migrationResult.MigrationId));
getNewCommerceMigration.Headers.Add("MS-CorrelationId", Guid.NewGuid().ToString());
var migrationResponse = await httpClient.SendAsync(getNewCommerceMigration, cancellationToken).ConfigureAwait(false);
if (migrationResponse.StatusCode == HttpStatusCode.Unauthorized)
{
var authenticationResult = await this.tokenProvider.GetTokenAsync();
httpClient.DefaultRequestHeaders.Clear();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authenticationResult.AccessToken);
httpClient.DefaultRequestHeaders.Add(Constants.PartnerCenterClientHeader, Constants.ClientName);
getNewCommerceMigration = new HttpRequestMessage(HttpMethod.Get, string.Format(Routes.GetNewCommerceMigration, migrationResult.CustomerTenantId, migrationResult.MigrationId));
getNewCommerceMigration.Headers.Add("MS-CorrelationId", Guid.NewGuid().ToString());
migrationResponse = await httpClient.SendAsync(getNewCommerceMigration).ConfigureAwait(false);
}
NewCommerceMigrationError? migrationError = null;
NewCommerceMigration? migration = null;
if (migrationResponse.IsSuccessStatusCode)
{
migration = await migrationResponse.Content.ReadFromJsonAsync<NewCommerceMigration>().ConfigureAwait(false);
}
else
{
migrationError = await migrationResponse.Content.ReadFromJsonAsync<NewCommerceMigrationError>().ConfigureAwait(false);
}
var result = this.PrepareMigrationResult(migrationResult, migrationResult.BatchId, migration, migrationError);
return (result, migration?.AddOnMigrations);
}
private async Task<List<MigrationResult>> PostNewCommerceMigrationAsync(HttpClient httpClient, MigrationRequest migrationRequest, IEnumerable<MigrationRequest> addOnMigrationRequests, string batchId, CancellationToken cancellationToken)
{
var newCommerceMigrationRequest = new HttpRequestMessage(HttpMethod.Post, string.Format(Routes.PostNewCommerceMigration, migrationRequest.CustomerTenantId));
newCommerceMigrationRequest.Headers.Add("MS-CorrelationId", Guid.NewGuid().ToString());
NewCommerceMigration newCommerceMigration = new NewCommerceMigration
{
CurrentSubscriptionId = migrationRequest.LegacySubscriptionId,
Quantity = migrationRequest.SeatCount,
BillingCycle = migrationRequest.BillingPlan,
TermDuration = migrationRequest.Term,
ExternalReferenceId = batchId,
};
// If they want to start a new term, then we should take the input from the file.
if (migrationRequest.StartNewTermInNce)
{
newCommerceMigration.PurchaseFullTerm = true;
}
newCommerceMigration.AddOnMigrations = GetAddOnMigrations(migrationRequest.LegacySubscriptionId, addOnMigrationRequests);
newCommerceMigrationRequest.Content = new StringContent(JsonSerializer.Serialize(newCommerceMigration), Encoding.UTF8, "application/json");
var migrationResponse = await httpClient.SendAsync(newCommerceMigrationRequest, cancellationToken).ConfigureAwait(false);
if (migrationResponse.StatusCode == HttpStatusCode.Unauthorized)
{
var authenticationResult = await this.tokenProvider.GetTokenAsync();
httpClient.DefaultRequestHeaders.Clear();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authenticationResult.AccessToken);
httpClient.DefaultRequestHeaders.Add(Constants.PartnerCenterClientHeader, Constants.ClientName);
newCommerceMigrationRequest = new HttpRequestMessage(HttpMethod.Post, string.Format(Routes.PostNewCommerceMigration, migrationRequest.CustomerTenantId));
newCommerceMigrationRequest.Headers.Add("MS-CorrelationId", Guid.NewGuid().ToString());
newCommerceMigrationRequest.Content = new StringContent(JsonSerializer.Serialize(newCommerceMigration), Encoding.UTF8, "application/json");
migrationResponse = await httpClient.SendAsync(newCommerceMigrationRequest).ConfigureAwait(false);
}
NewCommerceMigrationError? migrationError = null;
NewCommerceMigration? migration = null;
if (migrationResponse.IsSuccessStatusCode)
{
migration = await migrationResponse.Content.ReadFromJsonAsync<NewCommerceMigration>().ConfigureAwait(false);
}
else
{
migrationError = await migrationResponse.Content.ReadFromJsonAsync<NewCommerceMigrationError>().ConfigureAwait(false);
}
return this.PrepareMigrationResult(migrationRequest, addOnMigrationRequests, batchId, migration, migrationError);
}
private static IEnumerable<NewCommerceMigration> GetAddOnMigrations(string currentSubscriptionId, IEnumerable<MigrationRequest> addOnMigrationRequests)
{
if (!addOnMigrationRequests.Any())
{
return Enumerable.Empty<NewCommerceMigration>();
}
var allAddOnMigrations = new List<NewCommerceMigration>();
var childAddOns = addOnMigrationRequests.Where(a => a.BaseSubscriptionId.Equals(currentSubscriptionId, StringComparison.OrdinalIgnoreCase));
if (childAddOns.Any())
{
var addOnNewCommerceMigrations = childAddOns.Select(request => new NewCommerceMigration
{
CurrentSubscriptionId = request.LegacySubscriptionId,
Quantity = request.SeatCount,
BillingCycle = request.BillingPlan,
TermDuration = request.Term,
PurchaseFullTerm = request.StartNewTermInNce,
});
allAddOnMigrations.AddRange(addOnNewCommerceMigrations);
foreach (var item in childAddOns)
{
var multiLevelAddons = GetAddOnMigrations(item.LegacySubscriptionId, addOnMigrationRequests);
allAddOnMigrations.AddRange(multiLevelAddons);
}
}
return allAddOnMigrations;
}
/// <summary>
/// Prepares the MigrationResult for the CSV output. This takes the request and transforms it into a migration result.
/// </summary>
/// <param name="migrationRequest">The migration request.</param>
/// <param name="batchId">The batchId.</param>
/// <param name="newCommerceMigration">The new commerce migration response.</param>
/// <param name="newCommerceMigrationError">The new commerce migration error.</param>
/// <returns>The MigrationResult mapping.</returns>
private List<MigrationResult> PrepareMigrationResult(MigrationRequest migrationRequest, IEnumerable<MigrationRequest> addOnMigrationRequests, string batchId, NewCommerceMigration? newCommerceMigration = null, NewCommerceMigrationError? newCommerceMigrationError = null)
{
var migrationResults = new List<MigrationResult>();
PrepareMigrationResult(migrationRequest, batchId, newCommerceMigration, newCommerceMigrationError, migrationResults);
if (newCommerceMigration?.AddOnMigrations.Any() == true)
{
PrepareAddOnMigrationResult(addOnMigrationRequests, batchId, newCommerceMigration, newCommerceMigrationError, migrationResults);
}
return migrationResults;
}
private List<MigrationResult> PrepareAddOnMigrationResult(IEnumerable<MigrationRequest> addOnMigrationRequests, string batchId, NewCommerceMigration? newCommerceMigration, NewCommerceMigrationError? newCommerceMigrationError, List<MigrationResult> migrationResults)
{
foreach (var addOnMigrationResponse in newCommerceMigration.AddOnMigrations)
{
var addOnMigrationRequest = addOnMigrationRequests.SingleOrDefault(n => n.LegacySubscriptionId.Equals(addOnMigrationResponse.CurrentSubscriptionId, StringComparison.OrdinalIgnoreCase));
addOnMigrationResponse.Status = newCommerceMigration.Status;
addOnMigrationResponse.Id = newCommerceMigration.Id;
PrepareMigrationResult(addOnMigrationRequest, batchId, addOnMigrationResponse, newCommerceMigrationError, migrationResults);
}
return migrationResults;
}
private static void PrepareMigrationResult(MigrationRequest migrationRequest, string batchId, NewCommerceMigration? newCommerceMigration, NewCommerceMigrationError? newCommerceMigrationError, List<MigrationResult> migrationResults)
{
if (newCommerceMigrationError != null)
{
var migrationResult = new MigrationResult
{
PartnerTenantId = migrationRequest.PartnerTenantId,
IndirectResellerMpnId = migrationRequest.IndirectResellerMpnId,
CustomerName = migrationRequest.CustomerName,
CustomerTenantId = migrationRequest.CustomerTenantId,
LegacySubscriptionId = migrationRequest.LegacySubscriptionId,
LegacySubscriptionName = migrationRequest.LegacySubscriptionName,
LegacyProductName = migrationRequest.LegacyProductName,
ExpirationDate = migrationRequest.ExpirationDate,
AddOn = migrationRequest.AddOn,
StartedNewTermInNce = migrationRequest.StartNewTermInNce,
NCETermDuration = migrationRequest.Term,
NCEBillingPlan = migrationRequest.BillingPlan,
NCESeatCount = migrationRequest.SeatCount,
ErrorCode = newCommerceMigrationError.Code,
ErrorReason = newCommerceMigrationError.Description,
};
migrationResults.Add(migrationResult);
}
if (newCommerceMigration != null)
{
var migrationResult = new MigrationResult
{
PartnerTenantId = migrationRequest.PartnerTenantId,
IndirectResellerMpnId = migrationRequest.IndirectResellerMpnId,
CustomerName = migrationRequest.CustomerName,
CustomerTenantId = migrationRequest.CustomerTenantId,
LegacySubscriptionId = migrationRequest.LegacySubscriptionId,
LegacySubscriptionName = migrationRequest.LegacySubscriptionName,
LegacyProductName = migrationRequest.LegacyProductName,
ExpirationDate = migrationRequest.ExpirationDate,
AddOn = migrationRequest.AddOn,
MigrationStatus = newCommerceMigration.Status,
StartedNewTermInNce = migrationRequest.StartNewTermInNce,
NCETermDuration = newCommerceMigration.TermDuration,
NCEBillingPlan = newCommerceMigration.BillingCycle,
NCESeatCount = newCommerceMigration.Quantity,
NCESubscriptionId = newCommerceMigration.NewCommerceSubscriptionId,
BatchId = batchId,
MigrationId = newCommerceMigration.Id,
};
migrationResults.Add(migrationResult);
}
}
/// <summary>
/// Prepares the MigrationResult for the CSV output. This function takes a migration result and generates a new migration result based on the data the API returned.
/// </summary>
/// <param name="migrationRequest">The migration request.</param>
/// <param name="batchId">The batchId.</param>
/// <param name="newCommerceMigration">The new commerce migration response.</param>
/// <param name="newCommerceMigrationError">The new commerce migration error.</param>
/// <returns>The MigrationResult mapping.</returns>
private MigrationResult PrepareMigrationResult(MigrationResult migrationResult, string batchId, NewCommerceMigration? newCommerceMigration = null, NewCommerceMigrationError? newCommerceMigrationError = null)
{
MigrationResult result = new MigrationResult();
if (newCommerceMigrationError != null)
{
result = new MigrationResult
{
PartnerTenantId = migrationResult.PartnerTenantId,
IndirectResellerMpnId = migrationResult.IndirectResellerMpnId,
CustomerName = migrationResult.CustomerName,
CustomerTenantId = migrationResult.CustomerTenantId,
LegacySubscriptionId = migrationResult.LegacySubscriptionId,
LegacySubscriptionName = migrationResult.LegacySubscriptionName,
LegacyProductName = migrationResult.LegacyProductName,
ExpirationDate = migrationResult.ExpirationDate,
StartedNewTermInNce = migrationResult.StartedNewTermInNce,
NCETermDuration = migrationResult.NCETermDuration,
NCEBillingPlan = migrationResult.NCEBillingPlan,
NCESeatCount = migrationResult.NCESeatCount,
ErrorCode = newCommerceMigrationError.Code,
ErrorReason = newCommerceMigrationError.Description,
};
}
if (newCommerceMigration != null)
{
result = new MigrationResult
{
PartnerTenantId = migrationResult.PartnerTenantId,
IndirectResellerMpnId = migrationResult.IndirectResellerMpnId,
CustomerName = migrationResult.CustomerName,
CustomerTenantId = migrationResult.CustomerTenantId,
LegacySubscriptionId = migrationResult.LegacySubscriptionId,
LegacySubscriptionName = migrationResult.LegacySubscriptionName,
LegacyProductName = migrationResult.LegacyProductName,
ExpirationDate = migrationResult.ExpirationDate,
MigrationStatus = newCommerceMigration.Status,
StartedNewTermInNce = migrationResult.StartedNewTermInNce,
NCETermDuration = newCommerceMigration.TermDuration,
NCEBillingPlan = newCommerceMigration.BillingCycle,
NCESeatCount = newCommerceMigration.Quantity,
NCESubscriptionId = newCommerceMigration.NewCommerceSubscriptionId,
BatchId = batchId,
MigrationId = newCommerceMigration.Id,
};
}
return result;
}
}

Просмотреть файл

@ -0,0 +1,90 @@

Console.WriteLine("Welcome to NCE Bulk Migration Tool!");
string? appId;
string? upn;
if (args.Length == 2)
{
appId = args[0];
upn = args[1];
}
else
{
AppId:
Console.WriteLine("Enter AppId");
appId = Console.ReadLine();
if (string.IsNullOrWhiteSpace(appId) || !Guid.TryParse(appId, out _))
{
Console.WriteLine("Invalid input, Please try again!");
goto AppId;
}
Upn:
Console.WriteLine("Enter Upn");
upn = Console.ReadLine();
if (string.IsNullOrWhiteSpace(upn))
{
Console.WriteLine("Invalid input, Please try again!");
goto Upn;
}
}
var appSettings = new AppSettings()
{
AppId = appId,
Upn = upn,
};
using IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices((services) =>
{
services.AddSingleton(appSettings);
services.AddSingleton<ITokenProvider, TokenProvider>();
services.AddSingleton<ICustomerProvider, CustomerProvider>();
services.AddSingleton<ISubscriptionProvider, SubscriptionProvider>();
services.AddSingleton<INewCommerceMigrationProvider, NewCommerceMigrationProvider>();
}).Build();
await RunAsync(host.Services);
await host.RunAsync();
static async Task RunAsync(IServiceProvider serviceProvider)
{
Directory.CreateDirectory($"{Constants.InputFolderPath}/subscriptions/processed");
Directory.CreateDirectory($"{Constants.InputFolderPath}/migrations/processed");
Directory.CreateDirectory(Constants.OutputFolderPath);
Console.WriteLine("Please choose an option");
Console.WriteLine("1. Export customers");
Console.WriteLine("2. Export subscriptions with migration eligibility");
Console.WriteLine("3. Upload migrations");
Console.WriteLine("4. Export migration status");
Console.WriteLine("5. Export NCE subscriptions");
SelectOption:
var option = Console.ReadLine();
if (!short.TryParse(option, out short input) || !(input >= 1 && input <= 5))
{
Console.WriteLine("Invalid input, Please try again! Possible values are {1, 2, 3, 4, 5}");
goto SelectOption;
}
Stopwatch stopwatch = Stopwatch.StartNew();
var result = input switch
{
1 => await serviceProvider.GetRequiredService<ICustomerProvider>().ExportCustomersAsync(),
2 => await serviceProvider.GetRequiredService<ISubscriptionProvider>().ExportLegacySubscriptionsAsync(),
3 => await serviceProvider.GetRequiredService<INewCommerceMigrationProvider>().UploadNewCommerceMigrationsAsync(),
4 => await serviceProvider.GetRequiredService<INewCommerceMigrationProvider>().ExportNewCommerceMigrationStatusAsync(),
5 => await serviceProvider.GetRequiredService<ISubscriptionProvider>().ExportModernSubscriptionsAsync(),
_ => throw new InvalidOperationException("Invalid input")
};
stopwatch.Stop();
Console.WriteLine($"Completed the operation in {stopwatch.Elapsed}");
}

Просмотреть файл

@ -0,0 +1,274 @@
namespace NCEBulkMigrationTool;
internal class SubscriptionProvider : ISubscriptionProvider
{
private static string partnerTenantId = string.Empty;
private readonly ITokenProvider tokenProvider;
private long subscriptionsCntr = 0;
public SubscriptionProvider(ITokenProvider tokenProvider)
{
this.tokenProvider = tokenProvider;
}
public async Task<bool> ExportLegacySubscriptionsAsync()
{
var csvProvider = new CsvProvider();
using TextReader fileReader = File.OpenText($"{Constants.InputFolderPath}/customers.csv");
using var csvReader = new CsvReader(fileReader, CultureInfo.InvariantCulture, leaveOpen: true);
var inputCustomers = csvReader.GetRecordsAsync<CompanyProfile>();
ConcurrentBag<IEnumerable<MigrationRequest>> allMigrationRequests = new ConcurrentBag<IEnumerable<MigrationRequest>>();
var failedCustomersBag = new ConcurrentBag<CompanyProfile>();
var authenticationResult = await this.tokenProvider.GetTokenAsync();
partnerTenantId = authenticationResult.TenantId;
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authenticationResult.AccessToken);
httpClient.DefaultRequestHeaders.Add(Constants.PartnerCenterClientHeader, Constants.ClientName);
var options = new ParallelOptions()
{
MaxDegreeOfParallelism = 5
};
await Parallel.ForEachAsync(inputCustomers, options, async (customer, cancellationToken) =>
{
try
{
var subscriptions = await GetLegacySubscriptionsAsync(httpClient, customer, cancellationToken);
Console.WriteLine($"Validating subscriptions eligibility for customer {customer.CompanyName}");
var migrationEligibilities = await ValidateMigrationEligibility(customer, httpClient, options, subscriptions);
allMigrationRequests.Add(migrationEligibilities);
}
catch (Exception ex)
{
Console.WriteLine($"Failed to verify migration eligibility for customer {customer.CompanyName} {ex}");
failedCustomersBag.Add(customer);
}
});
csvReader.Dispose();
fileReader.Close();
Console.WriteLine("Exporting subscriptions");
await csvProvider.ExportCsv(allMigrationRequests.SelectMany(m => m), $"{Constants.OutputFolderPath}/subscriptions.csv");
Console.WriteLine($"Exported subscriptions at {Environment.CurrentDirectory}/{Constants.OutputFolderPath}/subscriptions.csv");
if (failedCustomersBag.Count > 0)
{
Console.WriteLine("Exporting failed customers");
await csvProvider.ExportCsv(failedCustomersBag, "failedCustomers.csv");
Console.WriteLine($"Exported failed customers at {Environment.CurrentDirectory}/failedCustomers.csv");
}
return true;
}
public async Task<bool> ExportModernSubscriptionsAsync()
{
var csvProvider = new CsvProvider();
using TextReader fileReader = File.OpenText($"{Constants.InputFolderPath}/customers.csv");
using var csvReader = new CsvReader(fileReader, CultureInfo.InvariantCulture, leaveOpen: true);
var inputCustomers = csvReader.GetRecordsAsync<CompanyProfile>();
ConcurrentBag<IEnumerable<ModernSubscription>> allSubscriptions = new ConcurrentBag<IEnumerable<ModernSubscription>>();
var failedCustomersBag = new ConcurrentBag<CompanyProfile>();
var authenticationResult = await this.tokenProvider.GetTokenAsync();
partnerTenantId = authenticationResult.TenantId;
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authenticationResult.AccessToken);
httpClient.DefaultRequestHeaders.Add(Constants.PartnerCenterClientHeader, Constants.ClientName);
var options = new ParallelOptions()
{
MaxDegreeOfParallelism = 5
};
await Parallel.ForEachAsync(inputCustomers, options, async (customer, cancellationToken) =>
{
try
{
var subscriptions = await GetModernSubscriptionsAsync(httpClient, customer, cancellationToken);
allSubscriptions.Add(subscriptions);
}
catch (Exception ex)
{
Console.WriteLine($"Failed to fetch subscriptions for customer {customer.CompanyName} {ex}");
failedCustomersBag.Add(customer);
}
});
csvReader.Dispose();
fileReader.Close();
Console.WriteLine("Exporting subscriptions");
await csvProvider.ExportCsv(allSubscriptions.SelectMany(m => m), $"{Constants.OutputFolderPath}/ncesubscriptions.csv");
Console.WriteLine($"Exported subscriptions at {Environment.CurrentDirectory}/{Constants.OutputFolderPath}/ncesubscriptions.csv");
if (failedCustomersBag.Count > 0)
{
Console.WriteLine("Exporting failed customers");
await csvProvider.ExportCsv(failedCustomersBag, "failedCustomers.csv");
Console.WriteLine($"Exported failed customers at {Environment.CurrentDirectory}/failedCustomers.csv");
}
return true;
}
private async Task<IEnumerable<Subscription>> GetLegacySubscriptionsAsync(HttpClient httpClient, CompanyProfile customer, CancellationToken cancellationToken)
{
var allSubscriptions = await this.GetSubscriptionsAsync(httpClient, customer, cancellationToken).ConfigureAwait(false);
var subscriptions = allSubscriptions.Where(s => Guid.TryParse(s.OfferId, out _));
return subscriptions;
}
private async Task<IEnumerable<ModernSubscription>> GetModernSubscriptionsAsync(HttpClient httpClient, CompanyProfile customer, CancellationToken cancellationToken)
{
var allSubscriptions = await this.GetSubscriptionsAsync(httpClient, customer, cancellationToken).ConfigureAwait(false);
var subscriptions = allSubscriptions!.Where(s => s.ProductType.DisplayName.Equals("OnlineServicesNCE", StringComparison.Ordinal) && s.OfferId.Contains(':'));
var modernSubscriptions = subscriptions.Select(s => PrepareModernSubscription(customer, s));
return modernSubscriptions;
}
private async Task<IEnumerable<Subscription>> GetSubscriptionsAsync(HttpClient httpClient, CompanyProfile customer, CancellationToken cancellationToken)
{
var subscriptionRequest = new HttpRequestMessage(HttpMethod.Get, string.Format(Routes.GetSubscriptions, customer.TenantId));
subscriptionRequest.Headers.Add("MS-CorrelationId", Guid.NewGuid().ToString());
var subscriptionResponse = await httpClient.SendAsync(subscriptionRequest, cancellationToken).ConfigureAwait(false);
if (subscriptionResponse.StatusCode == HttpStatusCode.Unauthorized)
{
var authenticationResult = await this.tokenProvider.GetTokenAsync();
httpClient.DefaultRequestHeaders.Clear();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authenticationResult.AccessToken);
httpClient.DefaultRequestHeaders.Add(Constants.PartnerCenterClientHeader, Constants.ClientName);
subscriptionRequest = new HttpRequestMessage(HttpMethod.Get, Routes.GetCustomers);
subscriptionRequest.Headers.Add("MS-CorrelationId", Guid.NewGuid().ToString());
subscriptionResponse = await httpClient.SendAsync(subscriptionRequest).ConfigureAwait(false);
}
subscriptionResponse.EnsureSuccessStatusCode();
var subscriptionCollection = await subscriptionResponse.Content.ReadFromJsonAsync<ResourceCollection<Subscription>>().ConfigureAwait(false);
return subscriptionCollection!.Items;
}
private async Task<ConcurrentBag<MigrationRequest>> ValidateMigrationEligibility(CompanyProfile customer, HttpClient httpClient, ParallelOptions options, IEnumerable<Subscription> subscriptions)
{
var baseSubscriptions = subscriptions.Where(s => string.IsNullOrWhiteSpace(s.ParentSubscriptionId));
var addOns = subscriptions.Where(s => !string.IsNullOrWhiteSpace(s.ParentSubscriptionId));
var migrationRequests = new ConcurrentBag<MigrationRequest>();
var addOnEligibilityList = new ConcurrentBag<IEnumerable<NewCommerceEligibility>>();
await Parallel.ForEachAsync(baseSubscriptions, options, async (subscription, cancellationToken) =>
{
var payload = new NewCommerceMigration
{
CurrentSubscriptionId = subscription.Id,
};
var migrationRequest = new HttpRequestMessage(HttpMethod.Post, string.Format(Routes.ValidateMigrationEligibility, customer.TenantId))
{
Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json")
};
migrationRequest.Headers.Add("MS-CorrelationId", Guid.NewGuid().ToString());
var migrationResponse = await httpClient.SendAsync(migrationRequest, cancellationToken).ConfigureAwait(false);
if (migrationResponse.StatusCode == HttpStatusCode.Unauthorized)
{
var authenticationResult = await this.tokenProvider.GetTokenAsync();
httpClient.DefaultRequestHeaders.Clear();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authenticationResult.AccessToken);
httpClient.DefaultRequestHeaders.Add(Constants.PartnerCenterClientHeader, Constants.ClientName);
migrationRequest = new HttpRequestMessage(HttpMethod.Get, Routes.GetCustomers);
migrationRequest.Headers.Add("MS-CorrelationId", Guid.NewGuid().ToString());
migrationResponse = await httpClient.SendAsync(migrationRequest).ConfigureAwait(false);
}
migrationResponse.EnsureSuccessStatusCode();
var newCommerceEligibility = await migrationResponse.Content.ReadFromJsonAsync<NewCommerceEligibility>().ConfigureAwait(false);
if (newCommerceEligibility.AddOnMigrations.Any())
{
addOnEligibilityList.Add(newCommerceEligibility.AddOnMigrations);
}
migrationRequests.Add(PrepareMigrationRequest(customer, subscription, newCommerceEligibility!));
Interlocked.Increment(ref subscriptionsCntr);
Console.WriteLine($"Validated migration eligibility for {subscriptionsCntr} subscriptions.");
});
foreach(var addOn in addOns)
{
var addOnEligibility = addOnEligibilityList.SelectMany(a => a).SingleOrDefault(m => m.CurrentSubscriptionId.Equals(addOn.Id, StringComparison.OrdinalIgnoreCase));
if (addOnEligibility != null)
{
migrationRequests.Add(PrepareMigrationRequest(customer, addOn, addOnEligibility));
}
}
return migrationRequests;
}
private static MigrationRequest PrepareMigrationRequest(CompanyProfile companyProfile, Subscription subscription, NewCommerceEligibility newCommerceEligibility)
{
return new MigrationRequest
{
PartnerTenantId = partnerTenantId,
IndirectResellerMpnId = subscription.PartnerId,
CustomerName = companyProfile.CompanyName,
CustomerTenantId = companyProfile.TenantId,
LegacySubscriptionId = subscription.Id,
LegacySubscriptionName = subscription.FriendlyName,
LegacyProductName = subscription.OfferName,
ExpirationDate = subscription.CommitmentEndDate,
MigrationEligible = newCommerceEligibility.IsEligible,
NcePsa = newCommerceEligibility.CatalogItemId,
CurrentTerm = subscription.TermDuration,
CurrentBillingPlan = subscription.BillingCycle.ToString(),
CurrentSeatCount = subscription.Quantity,
StartNewTermInNce = false,
Term = subscription.TermDuration,
BillingPlan = subscription.BillingCycle.ToString(),
SeatCount = subscription.Quantity,
AddOn = !string.IsNullOrWhiteSpace(subscription.ParentSubscriptionId),
BaseSubscriptionId = subscription.ParentSubscriptionId,
MigrationIneligibilityReason = newCommerceEligibility.Errors.Any() ?
string.Join(";", newCommerceEligibility.Errors.Select(e => e.Description)) :
string.Empty
};
}
private static ModernSubscription PrepareModernSubscription(CompanyProfile companyProfile, Subscription subscription)
{
return new ModernSubscription
{
PartnerTenantId = partnerTenantId,
IndirectResellerMpnId = subscription.PartnerId,
CustomerName = companyProfile.CompanyName,
CustomerTenantId = companyProfile.TenantId,
SubscriptionId = subscription.Id,
SubscriptionName = subscription.FriendlyName,
ProductName = subscription.OfferName,
ExpirationDate = subscription.CommitmentEndDate,
Psa = subscription.OfferId,
Term = subscription.TermDuration,
BillingPlan = subscription.BillingCycle.ToString(),
SeatCount = subscription.Quantity,
MigratedFromSubscriptionId = subscription.MigratedFromSubscriptionId,
};
}
}

Просмотреть файл

@ -0,0 +1,58 @@
namespace NCEBulkMigrationTool;
internal class TokenProvider : ITokenProvider
{
public TokenProvider(AppSettings appSettings)
{
this.appSettings = appSettings;
}
private readonly AppSettings appSettings;
private AuthenticationResult? authenticationResult;
public async Task<AuthenticationResult> GetTokenAsync()
{
if (authenticationResult != null && authenticationResult.ExpiresOn > DateTimeOffset.UtcNow.AddMinutes(5))
{
return authenticationResult;
}
var scopes = new string[] { $"https://api.partnercenter.microsoft.com/.default" };
var app = PublicClientApplicationBuilder.Create(this.appSettings.AppId)
.WithAuthority("https://login.microsoftonline.com", this.appSettings.Domain, true)
.WithRedirectUri("http://localhost")
.Build();
var accounts = await app.GetAccountsAsync();
AuthenticationResult result;
try
{
result = await app.AcquireTokenSilent(scopes, accounts.FirstOrDefault())
.ExecuteAsync();
}
catch (MsalUiRequiredException)
{
try
{
result = await app.AcquireTokenInteractive(scopes)
.WithLoginHint(this.appSettings.Upn)
.WithPrompt(Prompt.NoPrompt)
.ExecuteAsync();
}
catch (MsalException msalex)
{
throw msalex;
}
}
catch (Exception ex)
{
Console.WriteLine($"Exception while generating token {ex}");
throw;
}
authenticationResult = result;
return result;
}
}

Просмотреть файл

@ -0,0 +1,2 @@
dotnet new tool-manifest --force
dotnet tool install -g --add-source . NCEBulkMigrationTool

Просмотреть файл

@ -0,0 +1 @@
dotnet tool update -g ncebulkmigrationtool --add-source .

Просмотреть файл

@ -0,0 +1,232 @@
---
page_type: sample
languages:
- csharp
name: New Commerce Experience Batch Migration Tool (BAM)
description: Learn how to use the New Commerce Experience Batch Migration Tool to migrate your customer's subscription's to NCE.
products:
- azure
- ms-graph
---
# New Commerce Experience Batch Migration Tool (BAM)
## Table of Contents
1. [Introduction](#Introduction)
2. [Experience Summary](#Experience-Summary)
3. [Software Pre-requisites](#Software-Pre-Requisites)
4. [Begin running the Tool and Authenticate Your Account](#Begin-running-the-Tool-and-Authenticate-your-Account)
5. [Step-by-step flow for migrating a batch](#Step-by-step-flow-of-migrating-a-batch)
6. [Load and run Application](#Load-and-run-Application)
7. [Filter by customer](#Filter-by-customer)
8. [Access subscriptions for select customers](#Access-subscriptions-for-select-customers)
9. [Determine subscription migration eligibility](#Determine-subscription-migration-eligibility)
10. [Submitting a batch of subscriptions for migration](#Submitting-a-batch-of-subscriptions-for-migration)
11. [Checking migration status](#Checking-migration-status)
12. [Exporting a list of New Commerce Experience subscriptions](#Exporting-a-list-of-New-Commerce-Experience-subscriptions)
13. [Additional notes and resources](#Additional-notes-and-resources)
## Introduction
To facilitate partner needs of efficiently migrating large quantities of subscriptions before upcoming New Commerce Experience milestones, Microsoft has enabled a Batch Migration (BAM) tool. The BAM tool allows partners to migrate subscriptions into NCE with the following approach:
* Streamlined open source .NET SDK sample app experience
* Leverages Excel to manage migration edits
* Simple tool supporting high quality, repeatable, and customizable migration scenarios in batches
* No code required
## Experience Summary
Below is a high-level workflow of the console app experience and batch migration flow.
![Batch Migration Workflow](assets/images/BatchMigrationWorkflow.png "Batch Migration Workflow")
## Software Pre-Requisites
In order to build and run the BAM tool, .NET 6.0 SDK software is required.
## Step-by-step flow of migrating a batch
### Begin Running the Tool and Authenticate your Account
Steps for tool setup:
* Open command prompt and navigate to the folder where NCEBulkMigrationTool.sln is located.
* Run command:
```
dotnet build -c Release
```
* Once the application is done building navigate to the folder (NCEBulkMigrationTool\bin\Release\net6.0)
* Find the file NCEBulkMigrationTool.exe this is the executable which runs the tool.
* You can either run the tool where you found the original .exe file or you can copy all of the contents of the folder (NCEBulkMigrationTool\bin\Release\net6.0) into a new folder to begin executing the tool.
Steps to run the tool:
* Using Command Prompt navigate to the folder where is CEBulkMigrationTool.exe located (Use above steps to locate file after building)
* In command prompt run the following command:
```
.\NCEBulkMigrationTool.exe <AppId> <Upn>
```
* If a previous version of the tool was installed, enter the following command in the Command Prompt to uninstall the older version:
```
dotnet tool uninstall ncebulkmigrationtool -g
```
* NOTE: If multiple users are running at the same time from the same folder then files can be overwritten or access can be denied. It is better to copy the tool to multiple folders and each user can operate on a separate instance of the tool
### Load .NET console app
Follow the above section to get the tool running:
![NCE Batch Migration Tool Running](assets/images/NceBulkMigrationToolLoaded.png "NCE Batch Migration Tool Running")
Once the tool is running and the account is authenticated a Partner can:
1. Export customers
2. Export subscriptions with migration eligibility
3. Upload subscriptions to be migrated
4. export the migration status of batches that have already been uploaded for migration.
## Export customers
To export a list of customers, enter command “1”. This will produce a CSV similar to the below example:
![Exported customers CSV](assets/images/ExportedCustomersExample.png "Exported customers CSV")
The exported list of customers will be available in the “output” file of the tools folders.
![Input output folder example](assets/images/InputOutputFolderExample.png "Input output folder example")
View exported customers in the file “customers.csv”. For each customer under a partner tenant ID, users can view customer tenant ID, customer domain, and customer company name.
![Exported customer CSV example](assets/images/ExportedCustomersExample.png "Exported customer CSV example")
## Export subscriptions with migration eligibility
In the downloaded “customers.csv” file, the user can remove rows for the customers whose subscriptions they do not want to export in the next file download. The remaining customers on the file represent customers whose subscriptions will be validated for migration eligibility during the next step in the BAM tools flow.
Please save the updated “customers.csv” in the “input” folder in order to successfully execute the next step of receiving subscriptions for the specified customers. The “input” folder contains 2 other nested folders labelled “migrations” and “subscriptions”. Do not place the “customers.csv” file in the nested folders; simply keep it in the “input” folder.
Run the BAM tool and enter command “2” to export subscriptions with migration eligibility.
![Export customer subscriptions](assets/images/ExportSubscriptionsExample.png "Export customer subscriptions")
The console app will indicate subscriptions are being validated for eligibility, as seen in the screenshot above.
![Export customer subscriptions in progress](assets/images/ExportSubscriptionsToolOutput.png "Export customer subscriptions in progress")
Once export is complete, the list of subscriptions for the specified customers will be available in the output folder as “subscriptions.csv”.
![Export customer subscriptions result file](assets/images/SubscriptionsOutput.png "Export customer subscriptions result file")
The “subscriptions.csv” file will provide a list of all legacy subscriptions (both active and suspended) under the customers previously specified.
![Export customer subscriptions result CSV content](assets/images/OutputSubscriptionsResult.png "Export customer subscriptions result CSV content")
The following fields can be viewed for each subscription:
* Partner tenant ID
* Indirect Reseller MPN ID
* Customer Name
* Customer Tenant ID
* Legacy Subscription ID
* Legacy Subscription Name
* Legacy Product Name
* Expiration Date
* Migration Eligible (True or False)
* Current Term
* Current Billing Plan
* Current Seat Count
* Start New Term (post migration in NCE)
* Term (post migration in NCE)
* Billing Plan (post migration in NCE)
* Seat Count (post migration in NCE)
* Add On (True or False)
* Base Subscription (if an add-on)
* Migration Ineligibility Reason (if subscription is not eligible for migration)
## Determine which subscriptions will be migrated and how
With the fields above, users can filter through the exported list of subscriptions to determine which subscriptions they would like to migrate to NCE in a batch. Supported cases may include filtering to migrate subscriptions of a specific product type or subscriptions under a particular indirect reseller in a batch.
Once subscriptions have been filtered and selected, please delete subscriptions that are not selected for the batch from the CSV file. This will prevent any unintended migrations.
**Our recommendation is a limit of 100 subscriptions per batch.**
The next step is determining how they would like the subscriptions to be migrated (e.g. like-to-like or with updated start new term, billing frequency, term duration or seat count attributes).
Users may overwrite the following fields in rows for subscriptions they would like to migrate:
* Start New Term
* Term
* Billing Plan
* Seat Count
These fields represent the instructions or attributes that the NCE subscription will adhere to post-migration. The default values for these fields are the values held by the Legacy subscription being migrated. If no changes are made to a field, the corresponding NCE subscription will have the same value held by the Legacy subscription it migrated from. For example, if a Legacy subscription being migrated has a Current Seat Count of 2 and no changes are made to the Seat Count field, the NCE subscription will have a seat count of 2 post-migration.
For a subscription to start a new term in NCE, please change the Start New Term flag from FALSE to TRUE.
**Please do not change values outside of the Start New Term, Term, Billing Plan, and Seat Count columns.**
## Submitting a batch for migration
Once a batch has been determined (subscriptions for migration have been filtered through and have updated NCE values if desired), save your updated “subscriptions.csv” file in the “subscriptions” folder nested in the “input” folder. Each file saved in the ”subscriptions” folder represents a batch to migrate.
Once all batch files have been added to the folder run the console app and select option 3, upload migrations, for the app to begin reading in batch files in the “subscriptions” folder and executing migration requests.
![Upload migrations selection in console](assets/images/UploadMigrationsExample.png "Upload migrations selection in console")
The console will indicate that the migration requests are being processed:
![Upload migrations tool output](assets/images/UploadMigrationsToolOutput.png "Upload migrations tool output")
Once a file from the “subscriptions” folder has been processed for migration, the tool will move that file into the nested “processed” folder, indicating that migration requests for that batch have been executed. Partners do not need to manually move files into the “processed” folder themselves; files in the “processed” folder will not be read by the app to execute migration on (as they have already been handled).
![Migrations output folder example](assets/images/MigrationsFolderOutputExample.png "Migrations output folder example")
A file for each batch containing the migration IDs will be exported (available in the “migrations” folder nested in the “output” folder). The exported files will be labelled “[batchId].csv”.
![Uploaded migrations CSV output file example](assets/images/UploadedMigrationsCsvOutput.png "Uploaded migrations CSV output file example")
This file will possess the same fields as the input “subscriptions.csv” file with 2 additional columns labeled Batch ID and Migration ID. The Batch ID will be the same for all subscriptions in the file, indicating these subscriptions belong to the same batch or set of migration requests that were processed together. The Batch ID is also reflected in the name of this csv file.
## Checking migration status
The Migration ID is unique to each subscription being migrated. Migration ID can be used to track the migration status. The screenshot above indicates that for all subscriptions, the migration status is still processing (column I).
Once a migration has finished being executed, the status of the migration will be deemed Completed if migration was successful. The NCE Subscription Id will also be populated upon successful migration. If migration was unsuccessful, migration status will be denoted as Failed and the user will be able to view the error reason.
To be able to retrieve a refreshed status file for a batch, the exported “[batchId].csv” file (exported to the “migrations” folder nested in “output”) must be copied or saved into the “migrations” folder nested in the “input” folder. This will allow the tool to read in which batches status has been requested for and prepare reports to export.
Then, a partner must run the console app and select to check migration status. Status files will not be automatically updated. To retrieve updated statuses, a new request must be made each time (see below).
![Check migration status console example](assets/images/CheckMigrationStatusExample.png "Check migration status console example")
To retrieve updated migration statuses, run the console app and enter command “4”.
![Check migration status console output](assets/images/CheckMigrationStatusToolOutput.png "Check migration status console output")
The console app will indicate migration status is being looked up and that a file has been exported to the "migrationstatus” folder. The names of the exported migration status files represent the batch ID of subscriptions contained in the CSVs.
![Check migration status file output example](assets/images/CheckMigrationStatusFileOutput.png "Check migration status file output example")
Select the “[batchID].csv” file in the "migrationstatus” folder.
![Check migration status CSV file example](assets/images/CheckMigrationStatusCsvOutput.png "Check migration status CSV file example")
This file will provide updated statuses for migration requests that have been processed. If more than one batch is represented in the file, use the Batch Id column to filter to access statuses of requests in a particular batch.
## Exporting a list of New Commerce Experience subscriptions
To export NCE subscriptions, enter command 5. The exported list will show up in your “output” folder and will include the fields displayed in the example file below.
![Export NCE subscriptions console example](assets/images/NceBulkMigrationToolLoaded.png "Export NCE subscriptions console example")
![Export NCE subscriptions CSV file output](assets/images/ExportNceSubscriptionsCsvOutput.png "Export NCE subscriptions CSV file output")
## Additional notes and resources
Please migrate only one batch of subscriptions at a time to avoid API throttling and rate limits.
Documentation for migrating subscriptions and additional guidelines is available here: [Migrate subscriptions to new commerce - Partner Center | Microsoft Docs](https://docs.microsoft.com/en-us/partner-center/migrate-subscriptions-to-new-commerce)

Двоичные данные
nce-bulk-migration-tool/assets/images/BatchMigrationWorkflow.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 38 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 351 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 13 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 35 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 21 KiB

Двоичные данные
nce-bulk-migration-tool/assets/images/ConsoleAppLoaded.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 15 KiB

Двоичные данные
nce-bulk-migration-tool/assets/images/CustomerInputExample.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 93 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 406 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 14 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 89 KiB

Двоичные данные
nce-bulk-migration-tool/assets/images/ExportedCustomersExample.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 226 KiB

Двоичные данные
nce-bulk-migration-tool/assets/images/InputOutputFolderExample.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 20 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 45 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 14 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.2 MiB

Двоичные данные
nce-bulk-migration-tool/assets/images/SubscriptionsOutput.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 26 KiB

Двоичные данные
nce-bulk-migration-tool/assets/images/UploadMigrationsExample.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 15 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 28 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 82 KiB

Двоичные данные
nce-bulk-migration-tool/bib/CsvHelper.dll Normal file

Двоичный файл не отображается.

Двоичный файл не отображается.

Двоичный файл не отображается.

Двоичный файл не отображается.

Двоичный файл не отображается.

Двоичный файл не отображается.

Двоичный файл не отображается.

Двоичный файл не отображается.

Двоичные данные
nce-bulk-migration-tool/bib/Microsoft.Extensions.Configuration.dll Normal file

Двоичный файл не отображается.

Двоичный файл не отображается.

Двоичный файл не отображается.

Двоичный файл не отображается.

Двоичный файл не отображается.

Двоичный файл не отображается.

Двоичный файл не отображается.

Двоичные данные
nce-bulk-migration-tool/bib/Microsoft.Extensions.Hosting.dll Normal file

Двоичный файл не отображается.

Двоичный файл не отображается.

Двоичный файл не отображается.

Двоичный файл не отображается.

Двоичные данные
nce-bulk-migration-tool/bib/Microsoft.Extensions.Logging.Debug.dll Normal file

Двоичный файл не отображается.

Двоичный файл не отображается.

Двоичный файл не отображается.

Двоичные данные
nce-bulk-migration-tool/bib/Microsoft.Extensions.Logging.dll Normal file

Двоичный файл не отображается.

Двоичный файл не отображается.

Двоичные данные
nce-bulk-migration-tool/bib/Microsoft.Extensions.Options.dll Normal file

Двоичный файл не отображается.

Двоичные данные
nce-bulk-migration-tool/bib/Microsoft.Extensions.Primitives.dll Normal file

Двоичный файл не отображается.

Двоичные данные
nce-bulk-migration-tool/bib/Microsoft.Identity.Client.dll Normal file

Двоичный файл не отображается.

Просмотреть файл

@ -0,0 +1,648 @@
{
"runtimeTarget": {
"name": ".NETCoreApp,Version=v6.0",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETCoreApp,Version=v6.0": {
"NCEBulkMigrationTool/0.5.0": {
"dependencies": {
"CsvHelper": "27.1.1",
"Microsoft.Extensions.Configuration": "6.0.0",
"Microsoft.Extensions.Configuration.Json": "6.0.0",
"Microsoft.Extensions.DependencyInjection": "6.0.0",
"Microsoft.Extensions.Hosting": "6.0.1",
"Microsoft.Identity.Client": "4.40.0"
},
"runtime": {
"NCEBulkMigrationTool.dll": {}
}
},
"CsvHelper/27.1.1": {
"runtime": {
"lib/net5.0/CsvHelper.dll": {
"assemblyVersion": "27.0.0.0",
"fileVersion": "27.1.1.0"
}
}
},
"Microsoft.Extensions.Configuration/6.0.0": {
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "6.0.0",
"Microsoft.Extensions.Primitives": "6.0.0"
},
"runtime": {
"lib/netstandard2.0/Microsoft.Extensions.Configuration.dll": {
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.21.52210"
}
}
},
"Microsoft.Extensions.Configuration.Abstractions/6.0.0": {
"dependencies": {
"Microsoft.Extensions.Primitives": "6.0.0"
},
"runtime": {
"lib/netstandard2.0/Microsoft.Extensions.Configuration.Abstractions.dll": {
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.21.52210"
}
}
},
"Microsoft.Extensions.Configuration.Binder/6.0.0": {
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "6.0.0"
},
"runtime": {
"lib/netstandard2.0/Microsoft.Extensions.Configuration.Binder.dll": {
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.21.52210"
}
}
},
"Microsoft.Extensions.Configuration.CommandLine/6.0.0": {
"dependencies": {
"Microsoft.Extensions.Configuration": "6.0.0",
"Microsoft.Extensions.Configuration.Abstractions": "6.0.0"
},
"runtime": {
"lib/netstandard2.0/Microsoft.Extensions.Configuration.CommandLine.dll": {
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.21.52210"
}
}
},
"Microsoft.Extensions.Configuration.EnvironmentVariables/6.0.1": {
"dependencies": {
"Microsoft.Extensions.Configuration": "6.0.0",
"Microsoft.Extensions.Configuration.Abstractions": "6.0.0"
},
"runtime": {
"lib/netstandard2.0/Microsoft.Extensions.Configuration.EnvironmentVariables.dll": {
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.222.6406"
}
}
},
"Microsoft.Extensions.Configuration.FileExtensions/6.0.0": {
"dependencies": {
"Microsoft.Extensions.Configuration": "6.0.0",
"Microsoft.Extensions.Configuration.Abstractions": "6.0.0",
"Microsoft.Extensions.FileProviders.Abstractions": "6.0.0",
"Microsoft.Extensions.FileProviders.Physical": "6.0.0",
"Microsoft.Extensions.Primitives": "6.0.0"
},
"runtime": {
"lib/netstandard2.0/Microsoft.Extensions.Configuration.FileExtensions.dll": {
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.21.52210"
}
}
},
"Microsoft.Extensions.Configuration.Json/6.0.0": {
"dependencies": {
"Microsoft.Extensions.Configuration": "6.0.0",
"Microsoft.Extensions.Configuration.Abstractions": "6.0.0",
"Microsoft.Extensions.Configuration.FileExtensions": "6.0.0",
"Microsoft.Extensions.FileProviders.Abstractions": "6.0.0",
"System.Text.Json": "6.0.0"
},
"runtime": {
"lib/netstandard2.1/Microsoft.Extensions.Configuration.Json.dll": {
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.21.52210"
}
}
},
"Microsoft.Extensions.Configuration.UserSecrets/6.0.1": {
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "6.0.0",
"Microsoft.Extensions.Configuration.Json": "6.0.0",
"Microsoft.Extensions.FileProviders.Abstractions": "6.0.0",
"Microsoft.Extensions.FileProviders.Physical": "6.0.0"
},
"runtime": {
"lib/netstandard2.0/Microsoft.Extensions.Configuration.UserSecrets.dll": {
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.222.6406"
}
}
},
"Microsoft.Extensions.DependencyInjection/6.0.0": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0",
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
},
"runtime": {
"lib/net6.0/Microsoft.Extensions.DependencyInjection.dll": {
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.21.52210"
}
}
},
"Microsoft.Extensions.DependencyInjection.Abstractions/6.0.0": {
"runtime": {
"lib/net6.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll": {
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.21.52210"
}
}
},
"Microsoft.Extensions.FileProviders.Abstractions/6.0.0": {
"dependencies": {
"Microsoft.Extensions.Primitives": "6.0.0"
},
"runtime": {
"lib/net6.0/Microsoft.Extensions.FileProviders.Abstractions.dll": {
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.21.52210"
}
}
},
"Microsoft.Extensions.FileProviders.Physical/6.0.0": {
"dependencies": {
"Microsoft.Extensions.FileProviders.Abstractions": "6.0.0",
"Microsoft.Extensions.FileSystemGlobbing": "6.0.0",
"Microsoft.Extensions.Primitives": "6.0.0"
},
"runtime": {
"lib/net6.0/Microsoft.Extensions.FileProviders.Physical.dll": {
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.21.52210"
}
}
},
"Microsoft.Extensions.FileSystemGlobbing/6.0.0": {
"runtime": {
"lib/net6.0/Microsoft.Extensions.FileSystemGlobbing.dll": {
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.21.52210"
}
}
},
"Microsoft.Extensions.Hosting/6.0.1": {
"dependencies": {
"Microsoft.Extensions.Configuration": "6.0.0",
"Microsoft.Extensions.Configuration.Abstractions": "6.0.0",
"Microsoft.Extensions.Configuration.Binder": "6.0.0",
"Microsoft.Extensions.Configuration.CommandLine": "6.0.0",
"Microsoft.Extensions.Configuration.EnvironmentVariables": "6.0.1",
"Microsoft.Extensions.Configuration.FileExtensions": "6.0.0",
"Microsoft.Extensions.Configuration.Json": "6.0.0",
"Microsoft.Extensions.Configuration.UserSecrets": "6.0.1",
"Microsoft.Extensions.DependencyInjection": "6.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0",
"Microsoft.Extensions.FileProviders.Abstractions": "6.0.0",
"Microsoft.Extensions.FileProviders.Physical": "6.0.0",
"Microsoft.Extensions.Hosting.Abstractions": "6.0.0",
"Microsoft.Extensions.Logging": "6.0.0",
"Microsoft.Extensions.Logging.Abstractions": "6.0.0",
"Microsoft.Extensions.Logging.Configuration": "6.0.0",
"Microsoft.Extensions.Logging.Console": "6.0.0",
"Microsoft.Extensions.Logging.Debug": "6.0.0",
"Microsoft.Extensions.Logging.EventLog": "6.0.0",
"Microsoft.Extensions.Logging.EventSource": "6.0.0",
"Microsoft.Extensions.Options": "6.0.0"
},
"runtime": {
"lib/net6.0/Microsoft.Extensions.Hosting.dll": {
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.222.6406"
}
}
},
"Microsoft.Extensions.Hosting.Abstractions/6.0.0": {
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "6.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0",
"Microsoft.Extensions.FileProviders.Abstractions": "6.0.0"
},
"runtime": {
"lib/netstandard2.1/Microsoft.Extensions.Hosting.Abstractions.dll": {
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.21.52210"
}
}
},
"Microsoft.Extensions.Logging/6.0.0": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "6.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0",
"Microsoft.Extensions.Logging.Abstractions": "6.0.0",
"Microsoft.Extensions.Options": "6.0.0",
"System.Diagnostics.DiagnosticSource": "6.0.0"
},
"runtime": {
"lib/netstandard2.1/Microsoft.Extensions.Logging.dll": {
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.21.52210"
}
}
},
"Microsoft.Extensions.Logging.Abstractions/6.0.0": {
"runtime": {
"lib/net6.0/Microsoft.Extensions.Logging.Abstractions.dll": {
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.21.52210"
}
}
},
"Microsoft.Extensions.Logging.Configuration/6.0.0": {
"dependencies": {
"Microsoft.Extensions.Configuration": "6.0.0",
"Microsoft.Extensions.Configuration.Abstractions": "6.0.0",
"Microsoft.Extensions.Configuration.Binder": "6.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0",
"Microsoft.Extensions.Logging": "6.0.0",
"Microsoft.Extensions.Logging.Abstractions": "6.0.0",
"Microsoft.Extensions.Options": "6.0.0",
"Microsoft.Extensions.Options.ConfigurationExtensions": "6.0.0"
},
"runtime": {
"lib/netstandard2.0/Microsoft.Extensions.Logging.Configuration.dll": {
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.21.52210"
}
}
},
"Microsoft.Extensions.Logging.Console/6.0.0": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0",
"Microsoft.Extensions.Logging": "6.0.0",
"Microsoft.Extensions.Logging.Abstractions": "6.0.0",
"Microsoft.Extensions.Logging.Configuration": "6.0.0",
"Microsoft.Extensions.Options": "6.0.0",
"System.Text.Json": "6.0.0"
},
"runtime": {
"lib/net6.0/Microsoft.Extensions.Logging.Console.dll": {
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.21.52210"
}
}
},
"Microsoft.Extensions.Logging.Debug/6.0.0": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0",
"Microsoft.Extensions.Logging": "6.0.0",
"Microsoft.Extensions.Logging.Abstractions": "6.0.0"
},
"runtime": {
"lib/netstandard2.0/Microsoft.Extensions.Logging.Debug.dll": {
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.21.52210"
}
}
},
"Microsoft.Extensions.Logging.EventLog/6.0.0": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0",
"Microsoft.Extensions.Logging": "6.0.0",
"Microsoft.Extensions.Logging.Abstractions": "6.0.0",
"Microsoft.Extensions.Options": "6.0.0",
"System.Diagnostics.EventLog": "6.0.0"
},
"runtime": {
"lib/netstandard2.0/Microsoft.Extensions.Logging.EventLog.dll": {
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.21.52210"
}
}
},
"Microsoft.Extensions.Logging.EventSource/6.0.0": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0",
"Microsoft.Extensions.Logging": "6.0.0",
"Microsoft.Extensions.Logging.Abstractions": "6.0.0",
"Microsoft.Extensions.Options": "6.0.0",
"Microsoft.Extensions.Primitives": "6.0.0",
"System.Runtime.CompilerServices.Unsafe": "6.0.0",
"System.Text.Json": "6.0.0"
},
"runtime": {
"lib/net6.0/Microsoft.Extensions.Logging.EventSource.dll": {
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.21.52210"
}
}
},
"Microsoft.Extensions.Options/6.0.0": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0",
"Microsoft.Extensions.Primitives": "6.0.0"
},
"runtime": {
"lib/netstandard2.1/Microsoft.Extensions.Options.dll": {
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.21.52210"
}
}
},
"Microsoft.Extensions.Options.ConfigurationExtensions/6.0.0": {
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "6.0.0",
"Microsoft.Extensions.Configuration.Binder": "6.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0",
"Microsoft.Extensions.Options": "6.0.0",
"Microsoft.Extensions.Primitives": "6.0.0"
},
"runtime": {
"lib/netstandard2.0/Microsoft.Extensions.Options.ConfigurationExtensions.dll": {
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.21.52210"
}
}
},
"Microsoft.Extensions.Primitives/6.0.0": {
"dependencies": {
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
},
"runtime": {
"lib/net6.0/Microsoft.Extensions.Primitives.dll": {
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.21.52210"
}
}
},
"Microsoft.Identity.Client/4.40.0": {
"runtime": {
"lib/netcoreapp2.1/Microsoft.Identity.Client.dll": {
"assemblyVersion": "4.40.0.0",
"fileVersion": "4.40.0.0"
}
}
},
"System.Diagnostics.DiagnosticSource/6.0.0": {
"dependencies": {
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
}
},
"System.Diagnostics.EventLog/6.0.0": {
"runtime": {
"lib/net6.0/System.Diagnostics.EventLog.dll": {
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.21.52210"
}
},
"runtimeTargets": {
"runtimes/win/lib/net6.0/System.Diagnostics.EventLog.Messages.dll": {
"rid": "win",
"assetType": "runtime",
"assemblyVersion": "6.0.0.0",
"fileVersion": "0.0.0.0"
},
"runtimes/win/lib/net6.0/System.Diagnostics.EventLog.dll": {
"rid": "win",
"assetType": "runtime",
"assemblyVersion": "6.0.0.0",
"fileVersion": "6.0.21.52210"
}
}
},
"System.Runtime.CompilerServices.Unsafe/6.0.0": {},
"System.Text.Encodings.Web/6.0.0": {
"dependencies": {
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
}
},
"System.Text.Json/6.0.0": {
"dependencies": {
"System.Runtime.CompilerServices.Unsafe": "6.0.0",
"System.Text.Encodings.Web": "6.0.0"
}
}
}
},
"libraries": {
"NCEBulkMigrationTool/0.5.0": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"CsvHelper/27.1.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-lo5yypClBORR2XiWPRxQApaA4C2R4GVCIPVRt9HfaLxLmVQtg/KZiOYsd4luLgyOxkUatF9R0QdFQYk7Y2l0cw==",
"path": "csvhelper/27.1.1",
"hashPath": "csvhelper.27.1.1.nupkg.sha512"
},
"Microsoft.Extensions.Configuration/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-tq2wXyh3fL17EMF2bXgRhU7JrbO3on93MRKYxzz4JzzvuGSA1l0W3GI9/tl8EO89TH+KWEymP7bcFway6z9fXg==",
"path": "microsoft.extensions.configuration/6.0.0",
"hashPath": "microsoft.extensions.configuration.6.0.0.nupkg.sha512"
},
"Microsoft.Extensions.Configuration.Abstractions/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-qWzV9o+ZRWq+pGm+1dF+R7qTgTYoXvbyowRoBxQJGfqTpqDun2eteerjRQhq5PQ/14S+lqto3Ft4gYaRyl4rdQ==",
"path": "microsoft.extensions.configuration.abstractions/6.0.0",
"hashPath": "microsoft.extensions.configuration.abstractions.6.0.0.nupkg.sha512"
},
"Microsoft.Extensions.Configuration.Binder/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-b3ErKzND8LIC7o08QAVlKfaEIYEvLJbtmVbFZVBRXeu9YkKfSSzLZfR1SUfQPBIy9mKLhEtJgGYImkcMNaKE0A==",
"path": "microsoft.extensions.configuration.binder/6.0.0",
"hashPath": "microsoft.extensions.configuration.binder.6.0.0.nupkg.sha512"
},
"Microsoft.Extensions.Configuration.CommandLine/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-3nL1qCkZ1Oxx14ZTzgo4MmlO7tso7F+TtMZAY2jUAtTLyAcDp+EDjk3RqafoKiNaePyPvvlleEcBxh3b2Hzl1g==",
"path": "microsoft.extensions.configuration.commandline/6.0.0",
"hashPath": "microsoft.extensions.configuration.commandline.6.0.0.nupkg.sha512"
},
"Microsoft.Extensions.Configuration.EnvironmentVariables/6.0.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-pnyXV1LFOsYjGveuC07xp0YHIyGq7jRq5Ncb5zrrIieMLWVwgMyYxcOH0jTnBedDT4Gh1QinSqsjqzcieHk1og==",
"path": "microsoft.extensions.configuration.environmentvariables/6.0.1",
"hashPath": "microsoft.extensions.configuration.environmentvariables.6.0.1.nupkg.sha512"
},
"Microsoft.Extensions.Configuration.FileExtensions/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-V4Dth2cYMZpw3HhGw9XUDIijpI6gN+22LDt0AhufIgOppCUfpWX4483OmN+dFXRJkJLc8Tv0Q8QK+1ingT2+KQ==",
"path": "microsoft.extensions.configuration.fileextensions/6.0.0",
"hashPath": "microsoft.extensions.configuration.fileextensions.6.0.0.nupkg.sha512"
},
"Microsoft.Extensions.Configuration.Json/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-GJGery6QytCzS/BxJ96klgG9in3uH26KcUBbiVG/coNDXCRq6LGVVlUT4vXq34KPuM+R2av+LeYdX9h4IZOCUg==",
"path": "microsoft.extensions.configuration.json/6.0.0",
"hashPath": "microsoft.extensions.configuration.json.6.0.0.nupkg.sha512"
},
"Microsoft.Extensions.Configuration.UserSecrets/6.0.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-Fy8yr4V6obi7ZxvKYI1i85jqtwMq8tqyxQVZpRSkgeA8enqy/KvBIMdcuNdznlxQMZa72mvbHqb7vbg4Pyx95w==",
"path": "microsoft.extensions.configuration.usersecrets/6.0.1",
"hashPath": "microsoft.extensions.configuration.usersecrets.6.0.1.nupkg.sha512"
},
"Microsoft.Extensions.DependencyInjection/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-k6PWQMuoBDGGHOQTtyois2u4AwyVcIwL2LaSLlTZQm2CYcJ1pxbt6jfAnpWmzENA/wfrYRI/X9DTLoUkE4AsLw==",
"path": "microsoft.extensions.dependencyinjection/6.0.0",
"hashPath": "microsoft.extensions.dependencyinjection.6.0.0.nupkg.sha512"
},
"Microsoft.Extensions.DependencyInjection.Abstractions/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-xlzi2IYREJH3/m6+lUrQlujzX8wDitm4QGnUu6kUXTQAWPuZY8i+ticFJbzfqaetLA6KR/rO6Ew/HuYD+bxifg==",
"path": "microsoft.extensions.dependencyinjection.abstractions/6.0.0",
"hashPath": "microsoft.extensions.dependencyinjection.abstractions.6.0.0.nupkg.sha512"
},
"Microsoft.Extensions.FileProviders.Abstractions/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-0pd4/fho0gC12rQswaGQxbU34jOS1TPS8lZPpkFCH68ppQjHNHYle9iRuHeev1LhrJ94YPvzcRd8UmIuFk23Qw==",
"path": "microsoft.extensions.fileproviders.abstractions/6.0.0",
"hashPath": "microsoft.extensions.fileproviders.abstractions.6.0.0.nupkg.sha512"
},
"Microsoft.Extensions.FileProviders.Physical/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-QvkL7l0nM8udt3gfyu0Vw8bbCXblxaKOl7c2oBfgGy4LCURRaL9XWZX1FWJrQc43oMokVneVxH38iz+bY1sbhg==",
"path": "microsoft.extensions.fileproviders.physical/6.0.0",
"hashPath": "microsoft.extensions.fileproviders.physical.6.0.0.nupkg.sha512"
},
"Microsoft.Extensions.FileSystemGlobbing/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-ip8jnL1aPiaPeKINCqaTEbvBFDmVx9dXQEBZ2HOBRXPD1eabGNqP/bKlsIcp7U2lGxiXd5xIhoFcmY8nM4Hdiw==",
"path": "microsoft.extensions.filesystemglobbing/6.0.0",
"hashPath": "microsoft.extensions.filesystemglobbing.6.0.0.nupkg.sha512"
},
"Microsoft.Extensions.Hosting/6.0.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-hbmizc9KPWOacLU8Z8YMaBG6KWdZFppczYV/KwnPGU/8xebWxQxdDeJmLOgg968prb7g2oQgnp6JVLX6lgby8g==",
"path": "microsoft.extensions.hosting/6.0.1",
"hashPath": "microsoft.extensions.hosting.6.0.1.nupkg.sha512"
},
"Microsoft.Extensions.Hosting.Abstractions/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-GcT5l2CYXL6Sa27KCSh0TixsRfADUgth+ojQSD5EkzisZxmGFh7CwzkcYuGwvmXLjr27uWRNrJ2vuuEjMhU05Q==",
"path": "microsoft.extensions.hosting.abstractions/6.0.0",
"hashPath": "microsoft.extensions.hosting.abstractions.6.0.0.nupkg.sha512"
},
"Microsoft.Extensions.Logging/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-eIbyj40QDg1NDz0HBW0S5f3wrLVnKWnDJ/JtZ+yJDFnDj90VoPuoPmFkeaXrtu+0cKm5GRAwoDf+dBWXK0TUdg==",
"path": "microsoft.extensions.logging/6.0.0",
"hashPath": "microsoft.extensions.logging.6.0.0.nupkg.sha512"
},
"Microsoft.Extensions.Logging.Abstractions/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-/HggWBbTwy8TgebGSX5DBZ24ndhzi93sHUBDvP1IxbZD7FDokYzdAr6+vbWGjw2XAfR2EJ1sfKUotpjHnFWPxA==",
"path": "microsoft.extensions.logging.abstractions/6.0.0",
"hashPath": "microsoft.extensions.logging.abstractions.6.0.0.nupkg.sha512"
},
"Microsoft.Extensions.Logging.Configuration/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-ZDskjagmBAbv+K8rYW9VhjPplhbOE63xUD0DiuydZJwt15dRyoqicYklLd86zzeintUc7AptDkHn+YhhYkYo8A==",
"path": "microsoft.extensions.logging.configuration/6.0.0",
"hashPath": "microsoft.extensions.logging.configuration.6.0.0.nupkg.sha512"
},
"Microsoft.Extensions.Logging.Console/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-gsqKzOEdsvq28QiXFxagmn1oRB9GeI5GgYCkoybZtQA0IUb7QPwf1WmN3AwJeNIsadTvIFQCiVK0OVIgKfOBGg==",
"path": "microsoft.extensions.logging.console/6.0.0",
"hashPath": "microsoft.extensions.logging.console.6.0.0.nupkg.sha512"
},
"Microsoft.Extensions.Logging.Debug/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-M9g/JixseSZATJE9tcMn9uzoD4+DbSglivFqVx8YkRJ7VVPmnvCEbOZ0AAaxsL1EKyI4cz07DXOOJExxNsUOHw==",
"path": "microsoft.extensions.logging.debug/6.0.0",
"hashPath": "microsoft.extensions.logging.debug.6.0.0.nupkg.sha512"
},
"Microsoft.Extensions.Logging.EventLog/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-rlo0RxlMd0WtLG3CHI0qOTp6fFn7MvQjlrCjucA31RqmiMFCZkF8CHNbe8O7tbBIyyoLGWB1he9CbaA5iyHthg==",
"path": "microsoft.extensions.logging.eventlog/6.0.0",
"hashPath": "microsoft.extensions.logging.eventlog.6.0.0.nupkg.sha512"
},
"Microsoft.Extensions.Logging.EventSource/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-BeDyyqt7nkm/nr+Gdk+L8n1tUT/u33VkbXAOesgYSNsxDM9hJ1NOBGoZfj9rCbeD2+9myElI6JOVVFmnzgeWQA==",
"path": "microsoft.extensions.logging.eventsource/6.0.0",
"hashPath": "microsoft.extensions.logging.eventsource.6.0.0.nupkg.sha512"
},
"Microsoft.Extensions.Options/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-dzXN0+V1AyjOe2xcJ86Qbo233KHuLEY0njf/P2Kw8SfJU+d45HNS2ctJdnEnrWbM9Ye2eFgaC5Mj9otRMU6IsQ==",
"path": "microsoft.extensions.options/6.0.0",
"hashPath": "microsoft.extensions.options.6.0.0.nupkg.sha512"
},
"Microsoft.Extensions.Options.ConfigurationExtensions/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-bXWINbTn0vC0FYc9GaQTISbxhQLAMrvtbuvD9N6JelEaIS/Pr62wUCinrq5bf1WRBGczt1v4wDhxFtVFNcMdUQ==",
"path": "microsoft.extensions.options.configurationextensions/6.0.0",
"hashPath": "microsoft.extensions.options.configurationextensions.6.0.0.nupkg.sha512"
},
"Microsoft.Extensions.Primitives/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-9+PnzmQFfEFNR9J2aDTfJGGupShHjOuGw4VUv+JB044biSHrnmCIMD+mJHmb2H7YryrfBEXDurxQ47gJZdCKNQ==",
"path": "microsoft.extensions.primitives/6.0.0",
"hashPath": "microsoft.extensions.primitives.6.0.0.nupkg.sha512"
},
"Microsoft.Identity.Client/4.40.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-MoVyFayLFI/NPzTENJtS3X8mdZyyTkihzQElGimOpiX0+O4jE8RftNLbPGEzn/xDWGqz9julxEbHuOV1tD7+6w==",
"path": "microsoft.identity.client/4.40.0",
"hashPath": "microsoft.identity.client.4.40.0.nupkg.sha512"
},
"System.Diagnostics.DiagnosticSource/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-frQDfv0rl209cKm1lnwTgFPzNigy2EKk1BS3uAvHvlBVKe5cymGyHO+Sj+NLv5VF/AhHsqPIUUwya5oV4CHMUw==",
"path": "system.diagnostics.diagnosticsource/6.0.0",
"hashPath": "system.diagnostics.diagnosticsource.6.0.0.nupkg.sha512"
},
"System.Diagnostics.EventLog/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==",
"path": "system.diagnostics.eventlog/6.0.0",
"hashPath": "system.diagnostics.eventlog.6.0.0.nupkg.sha512"
},
"System.Runtime.CompilerServices.Unsafe/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==",
"path": "system.runtime.compilerservices.unsafe/6.0.0",
"hashPath": "system.runtime.compilerservices.unsafe.6.0.0.nupkg.sha512"
},
"System.Text.Encodings.Web/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-Vg8eB5Tawm1IFqj4TVK1czJX89rhFxJo9ELqc/Eiq0eXy13RK00eubyU6TJE6y+GQXjyV5gSfiewDUZjQgSE0w==",
"path": "system.text.encodings.web/6.0.0",
"hashPath": "system.text.encodings.web.6.0.0.nupkg.sha512"
},
"System.Text.Json/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-zaJsHfESQvJ11vbXnNlkrR46IaMULk/gHxYsJphzSF+07kTjPHv+Oc14w6QEOfo3Q4hqLJgStUaYB9DBl0TmWg==",
"path": "system.text.json/6.0.0",
"hashPath": "system.text.json.6.0.0.nupkg.sha512"
}
}
}

Двоичные данные
nce-bulk-migration-tool/bib/NCEBulkMigrationTool.dll Normal file

Двоичный файл не отображается.

Двоичные данные
nce-bulk-migration-tool/bib/NCEBulkMigrationTool.exe Normal file

Двоичный файл не отображается.

Просмотреть файл

@ -0,0 +1,12 @@
{
"runtimeOptions": {
"tfm": "net6.0",
"framework": {
"name": "Microsoft.NETCore.App",
"version": "6.0.0"
},
"configProperties": {
"System.Reflection.Metadata.MetadataUpdater.IsSupported": false
}
}
}

Двоичные данные
nce-bulk-migration-tool/bib/System.Diagnostics.EventLog.dll Normal file

Двоичный файл не отображается.

Двоичные данные
nce-bulk-migration-tool/bib/ref/NCEBulkMigrationTool.dll Normal file

Двоичный файл не отображается.

Двоичный файл не отображается.

Двоичный файл не отображается.

Просмотреть файл

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget" value="https://api.nuget.org/v3/index.json" />
</packageSources>
<activePackageSource>
<add key="All" value="(Aggregate source)" />
</activePackageSource>
</configuration>