Mimetis / Dotmim.Sync

A brand new database synchronization framework, multi platform, multi databases, developed on top of .Net Standard 2.0. https://dotmimsync.readthedocs.io/
MIT License
899 stars 194 forks source link

Multiple Scopes - sync to client? #566

Closed FDEC-KKelley closed 3 years ago

FDEC-KKelley commented 3 years ago

I have a working solution that I need to add the ability to use multiple scopes. The local database server is SQL express and the remote database server is SQL standard all running on Windows with the sync proxy running on IIS. Using my client desktop application I can provision the remote database with multiple scopes. I have been able to successfully provision the server database with multiple scopes with each containing unique tables. The documentation shows a sample of how to sync a database to a client with multiple scopes using SyncAgent but it appears that the option to specify the scope name has been removed. For example "var setupScopeName = new SyncSetup(tables, currentScopeName);" the second parameter for SyncSetup is no longer a valid parameter. I can get the schema to download to the client but it's all in one scope "DefaultScope" and the sync_scope_setup column in the "scope_info" table on the client tends to truncate some of the data. On a side note some of the tables I'm dealing with have a space in the name (old legacy database). They will not provision with a space in the name even when bracketed like this [Action Links] in the table sync list. Removing the space works but breaks the old application using this table.

Server table "scope_info_server" image

Client table "scope_info" image

Mimetis commented 3 years ago

Hey @FDEC-KKelley

Sorry for late answer. The code is out of date for multiscopes. Sorry about that. I need to improve the documentation to fit the last version...

You can set your scope on the SyncAgent itself, instead of SyncSetup:

var agent = new SyncAgent(clientProvider, serverProvider, options, setup, "Scope1");

For the space in the table name, I will do more tests on that, thanks !

FDEC-KKelley commented 3 years ago

Thank you for the reply, I'll code the change next week. It's a great syncing framework you've built.

FDEC-KKelley commented 3 years ago

I've tried your suggestion of putting the scope name on the SyncAgent and have found the folllowing.

var agent = new SyncAgent(clientProvider, serverProvider, options, setup, "Scope1");

If anything other than "DefaultScope" is specified in the above example for the scope name the following two errors are thrown and sync fails:

Exception thrown: 'Dotmim.Sync.Web.Client.HttpSyncWebException' in Dotmim.Sync.Web.Client.dll Exception thrown: 'Dotmim.Sync.Web.Client.HttpSyncWebException' in Dotmim.Sync.Web.Client.dll

Mimetis commented 3 years ago

Can you describe your configuration, from the server side ? Can you get more info from the error raised on the server side ?

FDEC-KKelley commented 3 years ago

An overview of my configuration:

  1. Azure load balancer --> two Web Application Firewall virtual appliances --> two Windows IIS servers running the sync api
  2. Server side database is Microsoft SQL server standard on an Azure Windows VM
  3. Client side database is on SQL Express on Windows 10.

The error on the server side when trying to use a named scope other than DefaultScope: Exception: Dotmim.Sync.HttpScopeNameInvalidException: The scope Scope1 does not exist on the server side. Please provider a correct scope name at Dotmim.Sync.Web.Server.WebServerManager.GetOrchestrator(String scopeName) at Dotmim.Sync.Web.Server.WebServerManager.GetOrchestrator(HttpContext context) at signalr.Hubs.SyncController.Post(String dbn) at signalr.Hubs.SyncController.TryAsync(String dbNameIn) at lambda_method6(Closure , Object ) at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.AwaitableResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.gAwaited|12_0(ControllerActionInvoker invoker, ValueTask`1 actionResultValueTask) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync() --- End of stack trace from previous location --- at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.gAwaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Logged|17_1(ResourceInvoker invoker) at Microsoft.AspNetCore.Routing.EndpointMiddleware.g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger) at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Server.IIS.Core.IISHttpContextOfT`1.ProcessRequestAsync()

A simple database with 7 tables and one scope on the server that I'm trying to sync with named scope "Scope1" image

image

I'll add some more debug logging to my server side code to see if I can get anymore detail. As stated before if I use the "DefaultScope" name no error is thrown. My goal is to sync using multiple scope names with each containing a certain number of tables.

Mimetis commented 3 years ago

On your server side, did you add correctly the provider with the scope ?

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    // [Required]: Handling multiple sessions
    services.AddMemoryCache();

    // [Required]: Get a connection string to your server data source
    var connectionString = Configuration.GetSection("ConnectionStrings")["SqlConnection"];
    // var connectionString = Configuration.GetSection("ConnectionStrings")["MySqlConnection"];

    // [Required] Tables involved in the sync process:
    var tables = new string[] {"ProductCategory", "ProductModel", "Product",
    "Address", "Customer", "CustomerAddress", "SalesOrderHeader", "SalesOrderDetail" };

    // [Required]: Add a SqlSyncProvider acting as the server hub.
    services.AddSyncServer<SqlSyncChangeTrackingProvider>(connectionString, "Scope1", tables);
    //services.AddSyncServer<MySqlSyncProvider>(connectionString, tables);
}
FDEC-KKelley commented 3 years ago

This doesn't seem to allow the scope name to be changed once the service is loaded. When trying to change to the next scope name "Scope2" it throws this error:

System.InvalidOperationException: The response headers cannot be modified because the response has already started.

I can't find anyway to get around this problem.

I can change the server side tables to sync in HttpPost without error using:

var orchestrator = webServerManager.GetOrchestrator(this.HttpContext);
var setup = new SyncSetup(strTableNames );
orchestrator.Setup.Tables.Clear();  //clear table list
orchestrator.Setup.Tables.AddRange(tables); //add table list for current scope name

If there were a similar method for scope name I think it would work.

public static string Scopename { get;set; }

public void ConfigureServices(IServiceCollection services)
{

    // [Required]: Handling multiple sessions
    services.AddMemoryCache();

    services.AddAuthorization();

    services.AddCors(options =>
    {
        options.AddPolicy(MyAllowSpecificOrigins,
        builder =>
        {
            builder.WithOrigins("https://localhost:44369")
            .AllowAnyHeader()
            .AllowAnyMethod()
            .AllowCredentials();
        });
    });

    // SignalR hub service for streaming server side sync messages to client app
    services.AddSignalR().AddHubOptions<SyncHub>(options =>
    {
        options.EnableDetailedErrors = true;
    });
    services.AddControllers();

    // connection string to server data source
    var connectionString = Configuration.GetSection("ConnectionStrings")["SqlConnection"];
    var tables = new string[] { "[tblVersionControl]" }; //inital table to sync

    if(string.IsNullOrEmpty(Scopename) == true)
    {
        Scopename = "Scope1";  //default first scope name
    }

    // SqlSyncProvider acting as the server hub.
    services.AddSyncServer<SqlSyncProvider>(connectionString, Scopename, tables);
}

[HttpPost("db")]
public async Task TryAsync(string dbNameIn = "")  //get database name from api url and pass to Post method
{
    string dbNameInFormat = dbNameIn.Remove(dbNameIn.Length - 1, 1);
    await Post(dbNameInFormat);
}
public async Task Post(string dbn)
{

    WebSyncProxy.Info("SyncController: Post: Database Name: " + dbn.ToString()); //debug logging 

    connectionPart2 = dbn;
    connectionFinal = string.Concat(connectionPart1, connectionPart2, connectionPart3);  //finalize connection string based on passed in string dbn (database name)

    DataSet scopeNamesDataSet = new DataSet();
    string scopeNameHold = string.Empty;
    string currentScopeName = string.Empty;
    string forqrueryString = string.Empty;
    var connection = new SqlConnection(connectionFinal);

    //Query that gets the scopes to create
    forqrueryString = "Use " + dbn + " select distinct [ScopeName] from qryScopeNames";

    SqlDataAdapter scopeNameAdapter = new SqlDataAdapter();
    scopeNameAdapter.SelectCommand = new SqlCommand(forqrueryString, connection);
    scopeNameAdapter.Fill(scopeNamesDataSet);

    foreach (DataTable scopeNames in scopeNamesDataSet.Tables) //loop through scope names
    {
        foreach (DataRow row in scopeNames.Rows)
        {

            currentScopeName = (@row[0].ToString());

            Startup.Scopename = currentScopeName;  //update service scopename

            GetTables(dbn, currentScopeName, out string tablesList);  //get list of sync tables based on passed in scope name

            string[] strTableNames = new string[] { "" };
            strTableNames = tablesList.Split(',');

            var tables = strTableNames ;

            var serverProvider = new SqlSyncProvider(connection.ToString().Replace("Master", dbn));
            var setup = new SyncSetup(strTableNames );
            var options = new SyncOptions();
            options.UseVerboseErrors = true;
            options.UseBulkOperations = false;
            options.CleanFolder = false;
            options.DisableConstraintsOnApplyChanges = true;
            options.CleanMetadatas = false;

            var orchestrator = webServerManager.GetOrchestrator(this.HttpContext);
            WebSyncProxy.Info("SyncController: Post: currentScopeName: " + currentScopeName.ToString()); //debug logging 
            WebSyncProxy.Info("SyncController: Post: orchestratorScopeName: " + orchestrator.ScopeName.ToString());//debug logging 

            orchestrator.Setup.Tables.Clear();  //clear table list
            orchestrator.Setup.Tables.AddRange(tables); //add table list for current scope name
            WebSyncProxy.Info("SyncController: Post: Tables: " + orchestrator.Setup.Tables.ToString());  //debug logging 

            orchestrator.Provider.ConnectionString = connectionFinal;

            try
            {
                var progress = new SynchronousProgress<ProgressArgs>(pa =>
                syncmessage = ($"<Server> Sync Stage>> " + pa.Context.SyncStage + "\t Sync Message>> " + pa.Message));

                // handle request
                await webServerManager.HandleRequestAsync(this.HttpContext, default, progress);

            }
            catch (SqlException se)
            {
                WebSyncProxy.Error("SyncController: Post: " + se.ToString());
                await WebServerManager.WriteExceptionAsync(this.HttpContext.Response, se);
            }
            catch (SyncException e)
            {
                WebSyncProxy.Error("SyncController: Post: " + e.ToString());
                await WebServerManager.WriteExceptionAsync(this.HttpContext.Response, e);
            }
            catch (Exception ex)
            {
                WebSyncProxy.Error("SyncController: Post: " + ex.ToString());
                await WebServerManager.WriteExceptionAsync(this.HttpContext.Response, ex);
            }

        }

    }
}
Mimetis commented 3 years ago

I dont understand what you are doing;, actually.

The only things I can say about that is:

Add one provider for each scope you have to handle:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddMemoryCache();

    var connectionString = Configuration.GetSection("ConnectionStrings")["SqlConnection"];

    var tables = new string[] {"ProductCategory", "ProductModel", "Product"};
    services.AddSyncServer<SqlSyncChangeTrackingProvider>(connectionString, "Scope_Products", tables);

    var tables2 = new string[] {  "Address", "Customer", "CustomerAddress"};
    services.AddSyncServer<SqlSyncChangeTrackingProvider>(connectionString, "Scope_Customers", tables2);

    var tables3 = new string[] {  "SalesOrderHeader", "SalesOrderDetail" };
    services.AddSyncServer<SqlSyncChangeTrackingProvider>(connectionString, "Scope_Sales", tables3);
}

From the client do not forget to add the scope name you want to reach. Client needs to know which scope he needs to reach.

Do not change your setup tables in your POST

Just don't do that:

var orchestrator = webServerManager.GetOrchestrator(this.HttpContext);
var setup = new SyncSetup(strTableNames );
orchestrator.Setup.Tables.Clear();  //clear table list
orchestrator.Setup.Tables.AddRange(tables); //add table list for current scope name

If you have multiple scopes (as I did in the previous code example) you won't have to do that

FDEC-KKelley commented 3 years ago

If there were only one defined database schema to sync I agree your example is the way to go. However, I am faced with several different databases with varying tables. Sorry I didn't state that in my original post. Hard coding table names won't work for me.

Mimetis commented 3 years ago

I'm afraid your scenario is not supported by the DMS framework. Did you find a way to make it work, finally ?

FDEC-KKelley commented 3 years ago

Yes I was able to get it working. Below is partial code from my desktop application main form, along with my API startup and controller code.


// ------------------------------------------------------------------------------------
// partial mainform.cs  loops through each scope to sync for the user selected database
// ------------------------------------------------------------------------------------

public async Task < bool > SyncThroughWebApiAsync(CancellationToken cancellationToken, string DatabaseToSync) {
  // ------------------------------------------------------------------------------------
  // Get list of scope names for the selected database
  // ------------------------------------------------------------------------------------
  await GetScopesAsync(DatabaseToSync, "%", true);

  string[] strScopes = new string[] {
    ""
  };
  strScopes = tableList.Split(',');
  var scopes = strScopes;

  // ------------------------------------------------------------------------------------
  // loop through all defined scopes for selected database 
  // ------------------------------------------------------------------------------------
  foreach(string strScopeName in scopes) {
    var taskCompletionSource = new TaskCompletionSource < bool > ();

    if (cancellationToken.IsCancellationRequested || Globals.SyncCancel == true) {
      cancellationToken.ThrowIfCancellationRequested();
    }

    if (cancellationToken.IsCancellationRequested) {
      return false;
    }

    AddItemToListBox("Syncing Project Database: >> " + DatabaseToSync);
    var scopeName = strScopeName.Replace("[", "").Replace("]", "").Trim('"');
    var clientProvider = new SqlSyncProvider(ConnectionFinal);
    var handler = new HttpClientHandler {
      AutomaticDecompression = DecompressionMethods.GZip
    };
    var client = new HttpClient(handler) {
      Timeout = TimeSpan.FromMinutes(5)
    };

    // ------------------------------------------------------------------------------------
    // Sync API Proxy, send database name and scope name to sync 
    // ------------------------------------------------------------------------------------
    var proxyClientProvider = new WebClientOrchestrator(SyncDataClass.WebSyncUrl + ":44369/api/Sync/db?dbNameIn=" + DatabaseToSync + "^" + scopeName, null, null, null, 4);

    //batch folder
    System.IO.Directory.CreateDirectory(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @ "\Temp\CACEA_Sync_Folder");

    // ----------------------------------
    // Client side options
    // ----------------------------------
    var clientOptions = new SyncOptions {
      ScopeInfoTableName = "scope_info",
        DisableConstraintsOnApplyChanges = true,
        BatchDirectory = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @ "\Temp\CACEA_Sync_Folder",
        SnapshotsDirectory = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @ "\Temp\CACEA_Sync_Folder",
        BatchSize = 500,
        CleanMetadatas = true,
        UseBulkOperations = false,
        UseVerboseErrors = true,
        ConflictResolutionPolicy = (ConflictResolutionPolicy) ConflictResolution.ServerWins,
    };

    agent = new SyncAgent(clientProvider, proxyClientProvider, clientOptions, scopeName);

    // Intercept a table changes selecting stage.
    if (SyncDataClass.DatabaseType == "PROJECTDB" | SyncDataClass.DatabaseType == "PREFDB")

    {
      agent.LocalOrchestrator.OnTableChangesSelecting(args => {
        if (Globals.SyncCancel == true) {
          args.Cancel = true;
          CancelSync();
          return;
        }

        AddItemToListBox($"[Client]-->Getting_Changes: Getting changes from table: {args.Table.GetFullName()} ...");
      });
    }

    try {
      //Set progress output to write to listbox
      var progressClient = new SynchronousProgress < ProgressArgs > (s => this.Invoke(new BindLstBoxControl(AddItemToListBox), new object[] {
        $ "[Client]-->Progress: {s.Context.SyncStage}:\t{DateTime.Now.ToString("
        T ")}:\t{ s.Message}"
      }));

      // This proxy is for getting sync status updates from the API sync proxy
      // SignalR is used with a streaming hub to send updates to the desktop GUI during a sync, as the API cannot send these messages back realtime
      HubConnection connection = new HubConnectionBuilder()
        .WithUrl(SyncDataClass.WebSyncUrl + ":44369/syncHub", options => {
          options.ClientCertificates.Add(clientCert);
        })
        .WithAutomaticReconnect()
        .Build();

      //create a cancellation token to use for the SignalR stream from the proxy
      var cancellationTokenSource = new CancellationTokenSource();

      await connection.StartAsync(cancellationTokenSource.Token).ContinueWith(t => {
        if (t.IsFaulted)
          // ToDo:  add code for faulted initial connection
          Debug.WriteLine(t.Exception.GetBaseException());
        else

      });

      var s = agent.SynchronizeAsync(SyncType.Normal, cancellationTokenSource.Token, progressClient);

      if (Globals.SyncCancel == true) {
        cancellationTokenSource.Cancel();
        CancelSync();
        break;
      }

      //make sure listbox scrolls
      lstMessages.SelectedIndex = lstMessages.Items.Count - 1;
      lstMessages.SelectedIndex = -1;

      //get the sync agent session state
      string agentsession = agent.SessionState.ToString();

      string receivedold = string.Empty;
      string receivednew = string.Empty;

      //get updates from the SignalR hub while syncing
      while (agent.SessionState == SyncSessionState.Synchronizing) {
        if (Globals.SyncCancel == true) {
          cancellationTokenSource.Cancel();
          CancelSync();
          break;
        }
        Globals.SyncInProgress = true;
        ProgressFormShown = true;
        agentsession = agent.SessionState.ToString(); //check for session state change
        var stream = connection.StreamAsync < SomeData > ("GetSomeDataWithAsyncStreams", 5, 50, cancellationTokenSource.Token);
        await foreach(var d in stream) {
          receivednew = d.Value ?? "";

          if (receivednew.Contains(receivedold)) //keep from displaying duplicate update information
          {
            receivedold = d.Value ?? "";
          } else {
            if (d.Value.Contains("<Server>") == true) {
              lstMessages.Items.Add(d.Value); //add to listbox
              //make sure listbox scrolls
              lstMessages.SelectedIndex = lstMessages.Items.Count - 1;
              lstMessages.SelectedIndex = -1;
            }
          }
          receivedold = d.Value ?? "";
        }
      }
      Globals.SyncInProgress = false;
    } catch (TaskCanceledException) {
      guiDebug.Error("MainForm: SyncThroughWebApiAsync: " + TaskCanceledException.ToString());
    } catch (SyncException e) {
      Globals.SyncInProgress = false;
      string messageerr = "Error During Sync of database " + DatabaseToSync + ": " + e.Message.ToString();
      string title = "Sync Error";
      MessageBoxButtons buttons = MessageBoxButtons.OK;
      DialogResult result = MessageBox.Show(messageerr, title, buttons);
      guiDebug.Error("MainForm: SyncThroughWebApiAsync: " + e.ToString());
      return false;
    } catch (Exception e) {
      Globals.SyncInProgress = false;
      string messageerr = "General Error During Sync of database " + DatabaseToSync + ": " + e.ToString();
      string title = "Sync Error";
      MessageBoxButtons buttons = MessageBoxButtons.OK;
      DialogResult result = MessageBox.Show(messageerr, title, buttons);
      guiDebug.Error("MainForm: SyncThroughWebApiAsync: " + e.ToString());
      return false;
    }
  }

  return true;
}

// ------------------------------------------------------------------------------------
// startup.cs
// ------------------------------------------------------------------------------------

using Dotmim.Sync;
using Dotmim.Sync.Web.Server;
using Microsoft.AspNetCore.Mvc;
using NLog;
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Threading.Tasks;

namespace signalr.Hubs {
  [Route("api/[controller]")]
  [ApiController]

  public class SyncController: ControllerBase {
    private Logger WebSyncProxy = LogManager.GetLogger("WebSyncProxy");
    private WebServerManager webServerManager;
    string connectionPart1 { get; set; }
    string connectionPart2 { get; set; }
    string connectionPart3 { get; set; }
    string connectionFinal { get; set; }

    public SyncController(WebServerManager webServerManager) {
      this.webServerManager = webServerManager;
      connectionPart1 = @ "Data Source=DBserver\fdec;Initial Catalog=";
      connectionPart3 = ";Trusted_Connection=True;";
    }

    public static string syncmessage { get;  set; }

    [HttpPost("db")]
    // ------------------------------------------------------------------------------------
    // get database name and scope name from api url and pass to Post method
    // ------------------------------------------------------------------------------------
    public async Task TryAsync(string dbNameIn = "") {
      try {
        string scopeNameInFormat = dbNameIn.Substring(dbNameIn.IndexOf("^") + 1, (dbNameIn.Length - dbNameIn.IndexOf("^")) - 2); //scope name
        string dbNameInFormat = dbNameIn.Substring(0, dbNameIn.IndexOf("^")); //database name
        await Post(dbNameInFormat, scopeNameInFormat); //send database name and scope name to POST method
      } catch (SyncException e) {
        WebSyncProxy.Error("Error** SyncController: TryAsync: e: " + e.ToString());
        await WebServerManager.WriteExceptionAsync(this.HttpContext.Response, e);
      }
    }

    // ------------------------------------------------------------------------------------
    // dbn = database name to sync from client, scopeIn = scope name from client
    // ------------------------------------------------------------------------------------
    public async Task Post(string dbn, string scopeIn) {
      connectionPart2 = dbn;
      connectionFinal = string.Concat(connectionPart1, connectionPart2, connectionPart3); //finalize connection string based on passed in string dbn
      var orchestrator = webServerManager.GetOrchestrator(scopeIn); // get scope orchestrator
      orchestrator.Setup.Tables.Clear(); //clear default table list
      orchestrator.Provider.ConnectionString = connectionFinal;

      try {
        WebSyncProxy.Info("Information** SyncController: Post: Orchestrator ScopeName: " + orchestrator.ScopeName.ToString());

        // ------------------------------------------------------------------------------------
        // get list of sync tables based on passed in string dbn
        // ------------------------------------------------------------------------------------
        GetTables(dbn, orchestrator.ScopeName.ToString(), out string tablesList);

        string[] strTables = new string[] {
          ""
        };
        strTables = tablesList.Split(',');
        var tables = strTables;

        // ------------------------------------------------------------------------------------
        // add tables for the current scope
        // ------------------------------------------------------------------------------------
        orchestrator.Setup.Tables.AddRange(tables);

        var progress = new SynchronousProgress < ProgressArgs > (pa =>
        syncmessage = ($"<Server> Sync Stage>> " + pa.Context.SyncStage + "\t Sync Message>> " + pa.Message));

        await orchestrator.HandleRequestAsync(this.HttpContext,
          default, progress); //process the current scope

      } catch (SqlException se) {
        WebSyncProxy.Error("Error** SyncController: Post: se: " + se.ToString());
        await WebServerManager.WriteExceptionAsync(this.HttpContext.Response, se);
      } catch (SyncException e) {
        WebSyncProxy.Error("Error** SyncController: Post: e: " + e.ToString());
        await WebServerManager.WriteExceptionAsync(this.HttpContext.Response, e);
      } catch (Exception ex) {
        WebSyncProxy.Error("Error** SyncController: Post: ex: " + ex.ToString());
        await WebServerManager.WriteExceptionAsync(this.HttpContext.Response, ex);
      }
    }

    [HttpGet]
    [RequireHttps]
    public async Task Get() => await webServerManager.HandleRequestAsync(this.HttpContext);

    public string GetTables(string DbToUse, string scopeNameFilter, out string tableList) {
      DataSet DS = new DataSet();
      string queryString = string.Empty;
      List < string > TableList = new List < string > ();
      string tblList = string.Empty;
      SqlConnection connection = new SqlConnection(connectionFinal);

      // ------------------------------------------------------------------------------------
      // Query that gets all the tables to sync
      // ------------------------------------------------------------------------------------
      queryString = "Use " + DbToUse + " select [TableNames],[ScopeName] from qryScopeNames where ScopeName = '" + scopeNameFilter + "' group by [TableNames],scopename ";

      try {
        SqlDataAdapter adapter = new SqlDataAdapter();
        adapter.SelectCommand = new SqlCommand(queryString, connection);

        adapter.Fill(DS);

        foreach(DataTable table in DS.Tables) {
          foreach(DataRow row in table.Rows)
          TableList.Add(@ "[" + row[0].ToString() + @ "]"); //Surround table names with brackets encase a table name has a space in it
        }
      } catch (System.Data.SqlClient.SqlException exception) {
        WebSyncProxy.Error("Error** SyncController: Post: " + exception.ToString());
        connection.Close();
        Debug.WriteLine(exception.ToString());
      } catch (Exception ex) {
        WebSyncProxy.Error("Error** SyncController: Post: " + ex.ToString());
        connection.Close();
        Debug.WriteLine(ex.ToString());
      }

      tblList = "\"" + string.Join("\", \"", TableList.ToArray()) + "\""; //Surround table names with double quotes
      tableList = tblList;
      return tableList;
    }
  }
}

// ------------------------------------------------------------------------------------
// SyncController.cs
// ------------------------------------------------------------------------------------

using Dotmim.Sync.SqlServer;
using Microsoft.AspNetCore.Authentication.Certificate;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using NLog;
using signalr.Hubs;
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;

namespace WebSyncServer {
  public class Startup {
    public static Microsoft.Extensions.Logging.ILogger _logger;
    private Logger WebSyncProxyStartup = LogManager.GetLogger("WebSyncProxy");

    public Startup(IConfiguration configuration) {
      LogManager.LoadConfiguration(System.String.Concat(Directory.GetCurrentDirectory(), "/nlog.config"));
      Configuration = configuration;
    }

    public IConfiguration Configuration {
      get;
    }
    readonly string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services) {

      // [Required]: Handling multiple sessions
      services.AddMemoryCache();
      services.AddAuthorization();
      services.AddCors(options => {
        options.AddPolicy(MyAllowSpecificOrigins,
          builder => {
            builder.WithOrigins("https://localhost:44369")
              .AllowAnyHeader()
              .WithMethods("GET", "POST")
              .AllowCredentials();
          });
      });

      //SignalR
      services.AddSignalR().AddHubOptions < SyncHub > (options => {
        options.EnableDetailedErrors = true;
      });
      services.AddControllers();

      // Get a connection string to server data source
      var connectionString = Configuration.GetSection("ConnectionStrings")["SqlConnection"];
      var tables1 = new string[] { "" };

      // ------------------------------------------------------------------------------------
      // Add a SqlSyncProvider acting as the server hub for each possible scope name
      // ------------------------------------------------------------------------------------
      services.AddSyncServer < SqlSyncProvider > (connectionString, "Scope1", tables1);
      services.AddSyncServer < SqlSyncProvider > (connectionString, "Scope2", tables1);
      services.AddSyncServer < SqlSyncProvider > (connectionString, "Scope3", tables1);
      services.AddSyncServer < SqlSyncProvider > (connectionString, "Scope4", tables1);
      services.AddSyncServer < SqlSyncProvider > (connectionString, "Scope5", tables1);
    }

    public class RequireHttpsAttribute: AuthorizationFilterAttribute {
      public override void OnAuthorization(HttpActionContext actionContext) {
        if (actionContext.Request.RequestUri.Scheme != Uri.UriSchemeHttps) {
          actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden) {
            ReasonPhrase = "HTTPS Required"
          };
        } else {
          base.OnAuthorization(actionContext);
        }
      }
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger < Startup > logger) {
      if (env.IsDevelopment()) {
        app.UseDeveloperExceptionPage();
      }
      app.UseHttpsRedirection();
      app.UseStaticFiles();
      app.UseRouting();
      app.UseCertificateForwarding();
      app.UseAuthentication();
      app.UseCors(MyAllowSpecificOrigins);
      app.UseAuthorization();
      app.UseEndpoints(endpoints => {
      endpoints.MapControllers();
      endpoints.MapHub < SyncHub > ("/syncHub");
      });
    }
  }
}
Mimetis commented 3 years ago

Thanks for sharing your code, it will help people, for sure !