Skip to main content
OneStream Employee
June 9, 2026

LIM: Example of Register Field Population from a File Explorer File 1

  • June 9, 2026
  • 0 replies
  • 0 views
DISCLAIMER: It should be noted that the focus of this technical guide is to provide general information, considerations, and guidelines for an identified topic. It is NOT to be interpreted as the ONLY approach nor a guarantee that there will not be any issues encountered by using this approach as a customer’s requirements or application configuration may render this guidance as not applicable. In addition, statements that “we believe” and similar statements reflect our beliefs and opinions on the relevant subject. These statements are based upon information available to us as of the date of this article, and while we believe such information forms a reasonable basis for such statements, such information may be limited or incomplete, and our statements should not be read to indicate that we have conducted an exhaustive inquiry into, or review of, all potentially available relevant information. OneStream does not warrant as to the accuracy of this guidance, which is provided on an as-is basis. Any forward-looking statements contained herein are based on information available at the time those statements are made and/or good faith beliefs and assumptions as of that time with respect to future events and are subject to risks and uncertainties that could cause actual performance or results to differ materially from those expressed in or suggested by the forward-looking statements. Considering these risks and uncertainties, the forward-looking events and circumstances discussed in this guide may not occur and actual results could differ materially from those anticipated or implied in the forward-looking statements.

VERSION: PV900 SV200

PLATFORM: 9.2.0

USE CASE: your customer wants a standard repository to store supplementary information to be used to populate certain Register Fields. In this example, a list of job codes and its descriptions is handled as a Spreadsheet component within an OneStream dashboard. Every time a user selects a job code or inputs a job code in the Register, the expectation is that the Job Code Description register field is populated with the description that is from this .csv.

PURPOSE: to provide you with an example that you can leverage as part of your build, whether you decide to incorporate this to existing input options (Register, Spreadsheet, Import File, Connector).

NOTE(S): my example assumes that this file is a .csv and placed in a dedicated folder in File Explorer.

ASSUMPTIONS:

  • The relevant Register Fields have already been created. In my example, I have JobCode and JobCodeDescription register fields.
  • The .csv is uploaded to a dedicated folder in File Explorer
  • A simple dashboard that allows the Administrator or select users to update the file. File Source Type for the Spreadsheet component is set up to reference Application Database File.

I will be splitting this approach into two articles - 1 which will cover WsComponentService, 2 which will cover WorkforcePlanningImportRegisterConnector, WsDynamicGridService and WsTableViewService.

A) WsComponentService: ImportRegisterData (to handle importing files)

Example logic to handle populating the JobCodeDescription for WsComponentService:

NOTE: I am putting in a sample code equivalent below which pertains to SV200 as the table names have changed from PLN to LIM.

private static void populateJobCodeDesc2(SessionInfo si, BRGlobals brGlobals, DashboardWorkspace workspace, DashboardExtenderArgs args, XFSelectionChangedTaskResult taskResult)
		{
		    int rowNumber = 0;
		    var errorList = new List<string>();
		    var invalidJobCodes = new List<string>();
		
		    try
		    {
		        // Load register rows once
		        DataTable registerRows = GetRegisterRows(si);
		
		        // Load lookup file once into memory (cached dictionary)
		        FileSystemLocation location = FileSystemLocation.ApplicationDatabase;
				string fileFullName = "Documents/Public/PLP_Lookups/JobCodeLookup.csv";
				bool includeContentFileBytes = true;
				bool failGracefully = true;
				
				XFFileEx sourceFile = BRApi.FileSystem.GetFile(si, location, fileFullName, includeContentFileBytes, failGracefully);
				
				if (sourceFile == null || sourceFile.XFFile == null || sourceFile.XFFile.ContentFileBytes == null)
				{
				    throw new XFException(si, new Exception($"Source file '{fileFullName}' could not be loaded or is empty."));
				}
	
				// Convert byte[] to string using UTF8
				string textContent = System.Text.Encoding.UTF8.GetString(sourceFile.XFFile.ContentFileBytes);

		        // Split into lines (keep non-empty lines)
		        string[] lines = textContent.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries);
		        char delimiter = DetectDelimiter(lines);
		        Dictionary<string, string=""> jobCodeLookup = BuildJobCodeLookup(lines, delimiter);
		
		        // Batch SQL updates (avoid ExecuteSql per row)
		        //    Chunk updates to prevent excessively large SQL strings
		        const int batchSize = 500;
		        int statementsInBatch = 0;
		
		        var sqlBatch = new System.Text.StringBuilder(capacity: 64 * 1024);
		        var connInfo = BRApi.Database.CreateApplicationDbConnInfo(si);
		
		        foreach (DataRow regRow in registerRows.Rows)
		        {
		            rowNumber++;
		            string jcName = regRow["JobCode"]?.ToString()?.Trim() ?? string.Empty;
		            string empId  = regRow["EmployeeID"]?.ToString()?.Trim() ?? string.Empty;
		            string jcDescription;
		            if (!string.IsNullOrEmpty(jcName) && jobCodeLookup.TryGetValue(jcName, out jcDescription))
		            {
		                regRow["JobCodeDescription"] = jcDescription ?? string.Empty;
		            }
		            else
		            {
		                jcDescription = "INVALID JOB CODE";
		                if (!string.IsNullOrEmpty(jcName))
		                    invalidJobCodes.Add(jcName);
		
		                errorList.Add($"Row {rowNumber}, EmployeeID {empId}, JobCode '{jcName}': INVALID JOB CODE");
		            }
		
		            // Escape for SQL safety
		            string escDesc = SqlStringHelper.EscapeSqlString(jcDescription);
		            string escJc   = SqlStringHelper.EscapeSqlString(jcName);
		            string escEmp  = SqlStringHelper.EscapeSqlString(empId);
		
		            // If your staging table has a unique row key (recommended), use that instead.
		            sqlBatch.AppendLine($@"
					UPDATE [limb2].XFW_LIM_StageRegister
					SET JobCodeDescription = '{escDesc}'
					WHERE EmployeeID = '{escEmp}'
					  AND JobCode    = '{escJc}';");
		
		            statementsInBatch++;
		
		            // Execute in chunks
		            if (statementsInBatch >= batchSize)
		            {
		                BRApi.Database.ExecuteSql(connInfo, sqlBatch.ToString(), false);
		                sqlBatch.Clear();
		                statementsInBatch = 0;
		            }
		        }
		
		        if (statementsInBatch > 0)
		        {
		            BRApi.Database.ExecuteSql(connInfo, sqlBatch.ToString(), false);
		        }
		
		        // If any invalids, throw a consolidated error (after updates are done)
		        if (errorList.Count > 0)
		        {
		            string errorMessage =
		                "Some JobCodes were marked as INVALID:" + Environment.NewLine +
		                string.Join(Environment.NewLine, errorList);
		
		            throw new XFException(si, new Exception(errorMessage));
		        }
		    }
		    catch (XFException)
		    {
		        throw;
		    }
		    catch (Exception ex)
		    {
		        throw new XFException(si, new Exception("Unexpected error occurred: " + ex.Message, ex));
		    }
		}</string,></string></string>

IMPORTANT NOTE: If you have more customizations, then you will need to assess how many of these customizations rely on fetching the register rows. You want to consider fetching these register rows once and reuse it instead of having each method (which corresponds to a different customization) call it; this is to optimize performance and processing. You may want to also consider the use of helpers. Below are some helpers that were referenced in the code above.

GetRegisterRows

private static DataTable GetRegisterRows(SessionInfo si)
		{
		    try
		    {
		        var sql = new System.Text.StringBuilder();
		        sql.AppendLine($@"
		            SELECT
		                [limb2].XFW_LIM_StageRegister.EmployeeID,
				[limb2].XFW_LIM_StageRegister.Instance,
				[limb2].XFW_LIM_StageRegister.LastName,
				[limb2].XFW_LIM_StageRegister.FirstName,
				[limb2].XFW_LIM_StageRegister.Status,
				[limb2].XFW_LIM_StageRegister.HireDate,
				[limb2].XFW_LIM_StageRegister.HirePeriod,
				[limb2].XFW_LIM_StageRegister.TermDate,
				[limb2].XFW_LIM_StageRegister.TermPeriod,
				[limb2].XFW_LIM_StageRegister.Salary,
				[limb2].XFW_LIM_StageRegister.SalaryOverride,
				[limb2].XFW_LIM_StageRegister.Entity,
				[limb2].XFW_LIM_StageRegister.CostCenter,
				[limb2].XFW_LIM_StageRegister.CostCenterDescription,
				[limb2].XFW_LIM_StageRegister.JobCode,
				[limb2].XFW_LIM_StageRegister.JobCodeDescription,
				[limb2].XFW_LIM_StageRegister.FTE,
				[limb2].XFW_LIM_StageRegister.HireYear,
				[limb2].XFW_LIM_StageRegister.TermYear
		            FROM [limb2].XFW_LIM_StageRegister
		        ");
		
		        using (DbConnInfo dbConnApp = BRApi.Database.CreateApplicationDbConnInfo(si))
		        {
		            DataTable dt = BRApi.Database.ExecuteSql(dbConnApp, sql.ToString(), false);
		            return dt;
		        }
		    }
		    catch (Exception ex)
			{
				throw ErrorHandler.LogWrite(si, new XFException(si, ex));
			}
		}

DetectDelimiter

private static char DetectDelimiter(string[] lines)
		{
		    // Default comma; inspect the first non-empty line
		    foreach (var ln in lines)
		    {
		        var line = ln?.Trim();
		        if (string.IsNullOrEmpty(line)) continue;
		
		        // Prefer tab/semicolon/pipe if present
		        if (line.Contains("\t")) return '\t';
		        if (line.Contains(";"))  return ';';
		        if (line.Contains("|"))  return '|';
		        return ','; // default
		    }
		    return ','; // fallback
		}

BuildJobCodeLookup

private static Dictionary<string, string=""> BuildJobCodeLookup(string[] lines, char delimiter)
		{
		    var dict = new Dictionary<string, string="">(StringComparer.OrdinalIgnoreCase);
		
		    bool headerSkipped = false;
		
		    foreach (string raw in lines)
		    {
		        string line = raw?.Trim();
		        if (string.IsNullOrEmpty(line)) continue;
		
		        // Parse respecting quotes
		        var fields = ParseDelimitedLine(line, delimiter);
		        if (fields.Count < 2) continue;
		
		        string code = (fields[0] ?? string.Empty).Trim();
		        string desc = (fields[1] ?? string.Empty).Trim();
		
		        // Skip header row once if it looks like a header
		        // (common: "JobCode,JobCodeDesc" or similar)
		        if (!headerSkipped && LooksLikeHeader(code, desc))
		        {
		            headerSkipped = true;
		            continue;
		        }
		
		        if (string.IsNullOrEmpty(code)) continue;
		
		        // First occurrence wins (avoid overwrite)
		        if (!dict.ContainsKey(code))
		            dict[code] = desc;
		    }
		
		    return dict;
		}</string,></string,>

LooksLikeHeader

private static bool LooksLikeHeader(string first, string second)
		{
		    string a = (first ?? string.Empty).Trim();
		    string b = (second ?? string.Empty).Trim();
		
		    // Minimal header detection: common names
		    return a.Equals("JobCode", StringComparison.OrdinalIgnoreCase)
		        || a.Equals("Job Code", StringComparison.OrdinalIgnoreCase)
		        || b.Equals("JobCodeDesc", StringComparison.OrdinalIgnoreCase)
		        || b.Equals("Job Code Desc", StringComparison.OrdinalIgnoreCase)
		        || b.Equals("JobCodeDescription", StringComparison.OrdinalIgnoreCase);
		}

ParseDelimitedLine

private static List<string> ParseDelimitedLine(string line, char delimiter)
		{
		    var result = new List<string>(8);
		    if (line == null)
		    {
		        result.Add(string.Empty);
		        return result;
		    }
		
		    var sb = new System.Text.StringBuilder();
		    bool inQuotes = false;
		
		    for (int i = 0; i < line.Length; i++)
		    {
		        char c = line[i];
		        if (c == '"')
		        {
		            // If we're in quotes and next char is also quote, it's an escaped quote
		            if (inQuotes && i + 1 < line.Length && line[i + 1] == '"')
		            {
		                sb.Append('"');
		                i++; // skip next quote
		            }
		            else
		            {
		                inQuotes = !inQuotes;
		            }
		            continue;
		        }
		
		        if (c == delimiter && !inQuotes)
		        {
		            result.Add(sb.ToString());
		            sb.Clear();
		            continue;
		        }
		        sb.Append(c);
		    }
		    result.Add(sb.ToString());
		    return result;
		}</string></string>

NOTE: if you plan to use StringBuilder() instead of declaring the variable as a string, you will need to import System.Text into the file. StringBuilder() is a method group so it will also have to be referenced as sql.ToString() as part of the BRApi.Database.ExecuteSql line. Example of what I mean by “import” below.

File Explorer Part 2