Introduction
Welcome to the C# API Reference for the Migration SDK.
Examples to get started
The following code samples are for writing a simple migration app using the Migration SDK. For details on configuring and customizing the Migration SDK to your specific needs, see Articles and Code Samples.
namespace Csharp.ExampleApplication
{
public static class Program
{
public static async Task Main(string[] args)
{
// Set the DOTNET_ENVIRONMENT environment variable to the name of the environment.
// This loads the appsettings.<DOTNET_ENVIRONMENT>.json config file.
// If no DOTNET_ENVIRONMENT is set, appsettings.json will be used
using var host = Host.CreateDefaultBuilder(args)
.ConfigureServices((ctx, services) =>
{
services
.Configure<MyMigrationApplicationOptions>(ctx.Configuration)
.Configure<EmailDomainMappingOptions>(ctx.Configuration.GetSection("tableau:emailDomainMapping"))
.Configure<UnlicensedUsersMappingOptions>(ctx.Configuration.GetSection("tableau:unlicensedUsersMapping"))
.AddTableauMigrationSdk(ctx.Configuration.GetSection("tableau:migrationSdk"))
.AddCustomizations()
.AddHostedService<MyMigrationApplication>();
})
.Build();
await host.RunAsync();
}
/// <summary>
/// Registers services required for using the Tableau Migration SDK customizations.
/// </summary>
/// <param name="services">The service collection to register services with.</param>
/// <returns>The same service collection as the <paramref name="services"/> parameter.</returns>
public static IServiceCollection AddCustomizations(this IServiceCollection services)
{
services.AddScoped<CustomContext>();
services.AddScoped<SetMigrationContextHook>();
services.AddScoped<EmailDomainMapping>();
services.AddScoped<UnlicensedUsersMapping>();
services.AddScoped<ProjectRenameMapping>();
services.AddScoped<ChangeProjectMapping<IWorkbook>>();
services.AddScoped<ChangeProjectMapping<IDataSource>>();
services.AddScoped<DefaultProjectsFilter>();
services.AddScoped<UnlicensedUsersFilter>();
services.AddScoped<SharedCustomViewFilter>();
services.AddScoped(typeof(UpdatePermissionsHook<,>));
services.AddScoped(typeof(BulkLoggingHook<>));
services.AddScoped<MigratedTagTransformer<IPublishableDataSource>>();
services.AddScoped<MigratedTagTransformer<IPublishableWorkbook>>();
services.AddScoped<EncryptExtractsTransformer<IPublishableDataSource>>();
services.AddScoped<EncryptExtractsTransformer<IPublishableWorkbook>>();
services.AddScoped(typeof(SimpleScheduleStartAtTransformer<>));
services.AddScoped<CustomViewExcludeDefaultUserTransformer>();
services.AddScoped<LogMigrationActionsHook>();
services.AddScoped(typeof(LogMigrationBatchesHook<>));
return services;
}
}
}
namespace Csharp.ExampleApplication
{
internal sealed class MyMigrationApplication : IHostedService
{
private readonly Stopwatch _timer;
private readonly IHostApplicationLifetime _appLifetime;
private IMigrationPlanBuilder _planBuilder;
private readonly IMigrator _migrator;
private readonly MyMigrationApplicationOptions _options;
private readonly ILogger<MyMigrationApplication> _logger;
private readonly MigrationManifestSerializer _manifestSerializer;
public MyMigrationApplication(
IHostApplicationLifetime appLifetime,
IMigrationPlanBuilder planBuilder,
IMigrator migrator,
IOptions<MyMigrationApplicationOptions> options,
ILogger<MyMigrationApplication> logger,
MigrationManifestSerializer manifestSerializer)
{
_timer = new Stopwatch();
_appLifetime = appLifetime;
// You can choose to assign an instance of the ServerToCloudMigrationPlanBuilder to help you
// add your own filters, mappings, transformers or hooks.
// Refer to the Articles section of this documentation for more details.
_planBuilder = planBuilder;
_migrator = migrator;
_options = options.Value;
_logger = logger;
_manifestSerializer = manifestSerializer;
}
public async Task StartAsync(CancellationToken cancel)
{
var executablePath = Assembly.GetExecutingAssembly().Location;
var currentFolder = Path.GetDirectoryName(executablePath);
if (currentFolder is null)
{
throw new Exception("Could not get the current folder path.");
}
var manifestPath = $"{currentFolder}/manifest.json";
var startTime = DateTime.UtcNow;
_timer.Start();
// Use the methods on your plan builder to add configuration and make customizations.
_planBuilder = _planBuilder
.FromSourceTableauServer(_options.Source.ServerUrl, _options.Source.SiteContentUrl, _options.Source.AccessTokenName, Environment.GetEnvironmentVariable("TABLEAU_MIGRATION_SOURCE_TOKEN") ?? string.Empty)
.ToDestinationTableauCloud(_options.Destination.ServerUrl, _options.Destination.SiteContentUrl, _options.Destination.AccessTokenName, Environment.GetEnvironmentVariable("TABLEAU_MIGRATION_DESTINATION_TOKEN") ?? string.Empty)
.ForServerToCloud()
.WithTableauIdAuthenticationType()
// You can add authentication type mappings here
.WithTableauCloudUsernames<EmailDomainMapping>();
var validationResult = _planBuilder.Validate();
if (!validationResult.Success)
{
_logger.LogError("Migration plan validation failed. {Errors}", validationResult.Errors);
Console.WriteLine("Press any key to exit");
Console.ReadKey();
_appLifetime.StopApplication();
}
// Add mappings
_planBuilder.Mappings.Add<UnlicensedUsersMapping, IUser>();
_planBuilder.Mappings.Add<ProjectRenameMapping, IProject>();
_planBuilder.Mappings.Add<ChangeProjectMapping<IDataSource>, IDataSource>();
_planBuilder.Mappings.Add<ChangeProjectMapping<IWorkbook>, IWorkbook>();
// Add filters
_planBuilder.Filters.Add<DefaultProjectsFilter, IProject>();
_planBuilder.Filters.Add<UnlicensedUsersFilter, IUser>();
_planBuilder.Filters.Add<SharedCustomViewFilter, ICustomView>();
// Add post-publish hooks
_planBuilder.Hooks.Add<UpdatePermissionsHook<IPublishableDataSource, IDataSourceDetails>>();
_planBuilder.Hooks.Add<UpdatePermissionsHook<IPublishableWorkbook, IWorkbookDetails>>();
_planBuilder.Hooks.Add<BulkLoggingHook<IUser>>();
// Add transformers
_planBuilder.Transformers.Add<MigratedTagTransformer<IPublishableDataSource>, IPublishableDataSource>();
_planBuilder.Transformers.Add<MigratedTagTransformer<IPublishableWorkbook>, IPublishableWorkbook>();
_planBuilder.Transformers.Add<EncryptExtractsTransformer<IPublishableDataSource>, IPublishableDataSource>();
_planBuilder.Transformers.Add<EncryptExtractsTransformer<IPublishableWorkbook>, IPublishableWorkbook>();
_planBuilder.Transformers.Add<SimpleScheduleStartAtTransformer<ICloudExtractRefreshTask>, ICloudExtractRefreshTask>();
_planBuilder.Transformers.Add<CustomViewExcludeDefaultUserTransformer, IPublishableCustomView>();
// Add initialize migration hooks
_planBuilder.Hooks.Add<SetMigrationContextHook>();
// Add migration action completed hooks
_planBuilder.Hooks.Add<LogMigrationActionsHook>();
// Add batch migration completed hooks
_planBuilder.Hooks.Add<LogMigrationBatchesHook<IUser>>();
_planBuilder.Hooks.Add<LogMigrationBatchesHook<IProject>>();
_planBuilder.Hooks.Add<LogMigrationBatchesHook<IDataSource>>();
_planBuilder.Hooks.Add<LogMigrationBatchesHook<IWorkbook>>();
_planBuilder.Hooks.Add<LogMigrationBatchesHook<ICloudExtractRefreshTask>>();
// Load the previous manifest if possible
var prevManifest = await LoadManifest(manifestPath, cancel);
// Build the plan
var plan = _planBuilder.Build();
// Execute the migration
var result = await _migrator.ExecuteAsync(plan, prevManifest, cancel);
_timer.Stop();
// Save the manifest
await _manifestSerializer.SaveAsync(result.Manifest, manifestPath);
PrintResult(result);
_logger.LogInformation($"Migration Started: {startTime}");
_logger.LogInformation($"Migration Finished: {DateTime.UtcNow}");
_logger.LogInformation($"Elapsed: {_timer.Elapsed}");
Console.WriteLine("Press any key to exit");
Console.ReadKey();
_appLifetime.StopApplication();
}
public Task StopAsync(CancellationToken cancel) => Task.CompletedTask;
/// <summary>
/// Prints the result to console.
/// You can replace this with a logging based method of your choice.
/// </summary>
/// <param name="result">The migration result.</param>
private void PrintResult(MigrationResult result)
{
_logger.LogInformation($"Result: {result.Status}");
// Logging any errors from the manifest.
if (result.Manifest.Errors.Any())
{
_logger.LogError("## Errors detected! ##");
foreach (var error in result.Manifest.Errors)
{
_logger.LogError(error, "Processing Error.");
}
}
foreach (var type in ServerToCloudMigrationPipeline.ContentTypes)
{
var contentType = type.ContentType;
_logger.LogInformation($"## {contentType.Name} ##");
// Manifest entries can be grouped based on content type.
foreach (var entry in result.Manifest.Entries.ForContentType(contentType))
{
_logger.LogInformation($"{contentType.Name} {entry.Source.Location} Migration Status: {entry.Status}");
if (entry.Errors.Any())
{
_logger.LogError($"## {contentType.Name} Errors detected! ##");
foreach (var error in entry.Errors)
{
_logger.LogError(error, "Processing Error.");
}
}
if (entry.Destination is not null)
{
_logger.LogInformation($"{contentType.Name} {entry.Source.Location} migrated to {entry.Destination.Location}");
}
}
}
}
private async Task<MigrationManifest?> LoadManifest(string manifestFilepath, CancellationToken cancel)
{
var manifest = await _manifestSerializer.LoadAsync(manifestFilepath, cancel);
if (manifest is not null)
{
ConsoleKey key;
do
{
Console.Write($"Existing Manifest found at {manifestFilepath}. Should it be used? [Y/n] ");
key = Console.ReadKey().Key;
Console.WriteLine(); // make Console logs prettier
} while (key is not ConsoleKey.Enter && key is not ConsoleKey.Y && key is not ConsoleKey.N);
if (key is ConsoleKey.N)
{
return null;
}
_logger.LogInformation($"Using previous manifest from {manifestFilepath}");
return manifest;
}
return null;
}
}
}
namespace Csharp.ExampleApplication.Config
{
public sealed class MyMigrationApplicationOptions
{
public EndpointOptions Source { get; set; } = new();
public EndpointOptions Destination { get; set; } = new();
}
}
namespace Csharp.ExampleApplication.Config
{
public class EndpointOptions
{
public Uri ServerUrl { get; set; } = TableauSiteConnectionConfiguration.Empty.ServerUrl;
public string SiteContentUrl { get; set; } = string.Empty;
public string AccessTokenName { get; set; } = string.Empty;
// Access token configuration should use a secure configuration system.
public string AccessToken { get; set; } = string.Empty;
}
}
{
"source": {
"serverUrl": "http://server",
"siteContentUrl": "",
"accessTokenName": "my-server-token-name",
"accessToken": "my-secret-server-pat"
},
"destination": {
"serverUrl": "https://pod.online.tableau.com",
"siteContentUrl": "site-name",
"accessTokenName": "my-cloud-token-name",
"accessToken": "my-secret-cloud-pat"
}
}
Suggested Reading