Store indexes in database instead of configurations.

Answered
Eric Morgan asked on January 11, 2023

Currently all DataStorageOptions (indexes) options are storing in configurations and fetching from configurations that means all data is storing in memory. Is there any way to get DataStorageOptions and related data as soon as the request arrives instead of getting from configurations.
I checked the source code, so I mean instead of getting index options from this: IOptionsMonitor<DatasourceOptions>, there can be a dataSourceOptions provider or someting to get indexes from DB?

Or maybe you can advice more elegant solution?

Here what we are trying to do:
- We will create a UI to allow users to create indexes dynamically (like FDS Admin Panel).
- Each user can have more than 1000 indexes or much more.
- So we want to manage all these thing dynamically.

Custom data server example on GitHub is outdated. There are a lot of changes since it's late update (2 years ago). So it is a bit difficult to implement custom data server by referencing this repo.

5 answers

Public
Maksym Diachenko Maksym Diachenko Flexmonster January 12, 2023

Hi, Patrick!

Thank you for writing to us.

Please note that since your project would require creating many indexes dynamically, we would like to emphasize that Flexmonster Data Server is not the best data source for such a case. FDS loads the entire dataset into the server's RAM to improve the speed of aggregating the data for a pivot view. When the index is created dynamically, the dataset will still be stored in RAM. Even though there aren't any limitations for index quantity or size, we believe that the mentioned amount of indexes (1000 or much more) would require a too large amount of RAM. Instead of the Data Server, we recommend one of the alternatives:

JSON/CSV endpoint with query parameters

If you don't have any datasets containing large volumes of data (100MB or more), this approach would be the simplest solution. You can implement a single server endpoint that would load the data from a database and return JSON/CSV in response. For example, on the client side, you could specify the query parameters in the filename URL (for reference, see the code snippet below). Then on the server side, you could use the query parameters to retrieve the required dataset. Data about the data source could be stored in the SQL table, with a key equal to the name parameter, and the server could use this table for connecting to the matching table and database.

report: {
dataSource: {
type: "json"
    filename: "https://yourserver.com/table?name=table1"
  }
}

It is possible to use headers instead of request params by setting the requestHeaders property: https://www.flexmonster.com/api/data-source-object/#requestheaders
Also, check out our guide for using a server-side script as a data source: https://www.flexmonster.com/doc/connecting-to-other-data-sources/#server-side-script 

Implementing the custom API

As another possible alternative, we suggest implementing your own custom data source API server using our communication protocol to retrieve aggregated data from a server to Flexmonster Pivot. The server would need to be capable of retrieving data from the database, aggregating it, and returning it to the client.  With this approach, you would have full control over the data processing logic on a server, and it would be possible to implement it without loading all the datasets into RAM. You are welcome to look through more information about custom API protocol: https://www.flexmonster.com/doc/introduction-to-custom-data-source-api/

Since the custom data source API is a set of requests and responses, it provides full flexibility over how the server can be structured, as there are many ways how it could be implemented depending on the use case. We have a detailed guide going over the main steps to implement the protocol on your own server: https://www.flexmonster.com/doc/implement-custom-data-source-api/.

Speaking of our GitHub sample implementations, they serve as a general overview/example for guidance purposes. The sample implementations are kept up-to-date with the correct request/response structure, and our team makes any necessary updates if the structure is updated.

Our team is ready to guide you through the implementation process. Please let us know which solution fits better for you.

Best Regards,
Maksym

Public
Eric Morgan January 12, 2023

Hi @Maksym, thanks for your suggestions. I solved it (but there are still workarounds in my solution).
I implemented my custom dataStorge and dataLoader. Here what I did:

CustomDataStorage:

public class FmReportManager : IDataStorage
{
private readonly IDataLoader _dataLoader;

public FmReportManager(IDataLoader dataLoader)
{
_dataLoader = dataLoader;
}

public async Task<IDataStructure> GetOrAddAsync(string cacheKey)
{
return _dataLoader.Load(cacheKey);
}

public void Remove(string key)
{
throw new System.NotImplementedException();
}
}

CustomDataLoader: (not completed)

public class FmReportRepository : IDataLoader
{
private readonly IDataSourcesRepository _dataSourcesRepository;
private readonly IParser _parser;

public FmReportRepository(IDataSourcesRepository dataSourcesRepository, IParser parser)
{
_dataSourcesRepository = dataSourcesRepository;
_parser = parser;
}

public IDataStructure Load(string index)
{
var entity = _dataSourcesRepository.GetAsync(x => x.Index == index).Result;

try
{
entity.LoadingStatus = DataSourceLoadingStatusTypes.Pending;
// TODO: update entity in database

// TODO: match entity to IndexOptions
_parser.IndexOptions = new IndexOptions();

var parsedData = _parser.Parse();
var listDataStructure = new ColumnListDataStructure();
var innerTypes = new Dictionary<int, ColumnType>();
foreach (var rowsAndColumnsMatrix in parsedData)
{
if (listDataStructure.GetNameAndTypes().Count == 0)
{
foreach (var keyValuePair in _parser.ColumnInfoByIndex)
{
var columnType = GetColumnType(keyValuePair.Value.Type);
innerTypes.Add(keyValuePair.Key, columnType);
listDataStructure.AddEmptyColumn(keyValuePair.Value.Name, columnType);
}
}

for (var columnIndex = 0; columnIndex < rowsAndColumnsMatrix.GetLength(1); ++columnIndex)
{
if (innerTypes[columnIndex] == ColumnType.StringType)
{
var column = listDataStructure.GetColumn<string>(_parser.ColumnInfoByIndex[columnIndex].Name);
for (var rowIndex = 0; rowIndex < rowsAndColumnsMatrix.GetLength(0); ++rowIndex)
{
var columnValue = rowsAndColumnsMatrix[rowIndex, columnIndex];
column.Add(columnValue.ToString());
}
}
else
{
var column = listDataStructure.GetColumn<double?>(_parser.ColumnInfoByIndex[columnIndex].Name);
for (var rowIndex = 0; rowIndex < rowsAndColumnsMatrix.GetLength(0); ++rowIndex)
{
var columnValue = rowsAndColumnsMatrix[rowIndex, columnIndex];
column.Add(columnValue is DBNull ? new double?() : Convert.ToDouble(columnValue));
}
}
}
}

entity.LoadingStatus = DataSourceLoadingStatusTypes.Loaded;
// TODO: update entity in database

return listDataStructure;
}
catch (Exception)
{
entity.LoadingStatus = DataSourceLoadingStatusTypes.Failed;
// TODO: update entity in database

throw;
}
}

private static ColumnType GetColumnType(Type columnType)
{
switch (Type.GetTypeCode(columnType))
{
case TypeCode.SByte:
case TypeCode.Byte:
case TypeCode.Int16:
case TypeCode.UInt16:
case TypeCode.Int32:
case TypeCode.UInt32:
case TypeCode.Int64:
case TypeCode.UInt64:
case TypeCode.Single:
case TypeCode.Double:
case TypeCode.Decimal:
return ColumnType.DoubleType;
case TypeCode.DateTime:
return ColumnType.DateType;
default:
return ColumnType.StringType;
}
}
}

All necessary requests:

public class FmReportsController : AnalyticServiceController, IFmReportsAppService
{
private readonly IFmReportsAppService _fmReportsAppService;
private readonly IApiService _fmApiService;
private readonly IOptionsMonitor<DatasourceOptions> _datasourceOptions;

public FmReportsController(IFmReportsAppService fmReportsAppService, IApiService fmApiService, IOptionsMonitor<DatasourceOptions> datasourceOptions)
{
_fmReportsAppService = fmReportsAppService;
_fmApiService = fmApiService;
_datasourceOptions = datasourceOptions;
}

[HttpPost("handshake")]
public Task<HandshakeResponse> HandshakeAsync()
{
return _fmReportsAppService.HandshakeAsync();
}

[HttpPost("fields")]
public Task<Schema> PostFieldsAsync(FieldsRequest request)
{
// TODO: This is a workaround for the issue with the Flexmonster Data Server. The private method in fmApiService still uses optionsMonitor pattern.
// TODO: To override that private method, we need to implement whole IFmReportsAppService interface.
_datasourceOptions.CurrentValue.Indexes = new Dictionary<string, IndexOptions> {{request.Index, new IndexOptions {Type = "json"}}};

return _fmApiService.GetFieldsAsync(request.Index);
}

[HttpPost("members")]
public Task<MembersResponse> PostMembersAsync(MembersRequest request)
{
// TODO: This is a workaround for the issue with the Flexmonster Data Server. The private method in fmApiService still uses optionsMonitor pattern.
// TODO: To override that private method, we need to implement whole IFmReportsAppService interface.
_datasourceOptions.CurrentValue.Indexes = new Dictionary<string, IndexOptions> {{request.Index, new IndexOptions {Type = "json"}}};

return _fmApiService.GetMembersAsync(request);
}

[HttpPost("select")]
public Task<SelectResponse> PostSelectAsync(SelectRequest request)
{
// TODO: This is a workaround for the issue with the Flexmonster Data Server. The private method in fmApiService still uses optionsMonitor pattern.
// TODO: To override that private method, we need to implement whole IFmReportsAppService interface.
_datasourceOptions.CurrentValue.Indexes = new Dictionary<string, IndexOptions> {{request.Index, new IndexOptions {Type = "json"}}};

return _fmApiService.GetAggregatedDataAsync(request);
}
}

and service replacement:

...
context.Services.AddFlexmonsterApi();
context.Services.ConfigureFlexmonsterOptions(configuration);
context.Services.Replace(ServiceDescriptor.Singleton<IDataStorage, FmReportManager>());
context.Services.Replace(ServiceDescriptor.Transient<IDataLoader, FmReportRepository>());

So instead of implement whole FDS, I just override DataStorage and DataLoader . But still I need a workaround like following:

[HttpPost("fields")]
public Task<Schema> PostFieldsAsync(FieldsRequest request)
{
// workaround:
_datasourceOptions.CurrentValue.Indexes = new Dictionary<string, IndexOptions> {{request.Index, new IndexOptions {Type = "json"}}};

return _fmApiService.GetFieldsAsync(request.Index);
}

because of following code in IApiService . it is using _dataSourceOptions.

private async Task<IDataStructure> LoadData(string index)
{
if (!this._dataSourceOptions.CurrentValue.Indexes.ContainsKey(index))
throw new FlexmonsterApiException("Index with name \"" + index + "\" was not found.");
if (this._dataSourceOptions.CurrentValue.Indexes[index].LoadingStatus == LoadingStatus.pending)
throw new FlexmonsterApiException("Index with name \"" + index + "\" is currently loading. Please try again later.");
if (this._dataSourceOptions.CurrentValue.Indexes[index].LoadingStatus == LoadingStatus.failed)
throw new FlexmonsterApiException("Index with name \"" + index + "\" is unavailable.");
return await this._dataStorage.GetOrAddAsync(index);
}
Public
Eric Morgan January 12, 2023

BTW, you said: "Our team is ready to guide you through the implementation process. Please let us know which solution fits better for you."

If they can help or any suggestion on my solution, it would be great 🙂 It would be nice if i could get rid of that workaround.

Public
Maksym Diachenko Maksym Diachenko Flexmonster January 16, 2023

Hello, Patrick!

Thank you for your reply.

Before advising on the provided code, we would like to point out that the Data Server may not be the best solution for your case. As you initially mentioned, each user will be able to create indexes dynamically (adding up to 1000 or more indexes per user). With Flexmonster Data Server's architecture, all the indexes are stored in the server's RAM. This may not cause any issues during development since there may be a far smaller amount of indexes for testing. However, maintaining the server with enough RAM for keeping thousands of indexes per user would become too difficult and costly in production. Hence, we strongly recommend using other approaches described in our previous answer.
With this in mind, please let us know if you still want to proceed with Flexmonster Data Server.

Looking forward to hearing your thoughts.

Best Regards,
Maksym

Public
Maksym Diachenko Maksym Diachenko Flexmonster January 24, 2023

Hi, Patrick!

Hope you are doing well.
We are wondering if you had time to check our previous reply. Are you still considering FDS DLL as a solution for your case, or would you like to use the previously suggested alternatives with the server-side script or custom API?
Looking forward to hearing from you.

Best Regards,
Maksym

Please login or Register to Submit Answer