/// <summary> /// Unlike read write is called exactly once per each row and not called at all if the row's state does not require actual writing. /// </summary> /// <param name="execContext"></param> /// <param name="configuration"></param> /// <returns></returns> protected override object Write(IDataLoaderWriteContext execContext) //IDataLoaderContext execContext, Configuration configuration) { { Node node = execContext.CurrentNode; // Statement is already selected for the requested operation (While fetching the Configuration if (!string.IsNullOrWhiteSpace(Action(execContext).Query)) { IADOTransactionScope scopedContext = execContext.OwnContextScoped as IADOTransactionScope; // Check if it is valid if (scopedContext == null) { throw new NullReferenceException("Scoped synchronization and transaction context is not available."); } // Settings should be passed to the scopedContext in the ExternalServiceImp DbConnection conn = scopedContext.Connection; DbTransaction trans = scopedContext.StartADOTransaction(); using (DbCommand cmd = conn.CreateCommand()) { cmd.Transaction = trans; cmd.Parameters.Clear(); ProcessCommand(cmd, Action(execContext).Query, execContext); using (DbDataReader reader = cmd.ExecuteReader()) { do { while (reader.Read()) { for (int i = 0; i < reader.FieldCount; i++) { string fname = reader.GetName(i); if (fname == null) { continue; } fname = fname.ToLower().Trim(); // fname = fname.Trim(); // TODO: We have to rethink this - lowercasing seems more inconvenience than a viable protection against human mistakes. if (fname.Length == 0) { throw new Exception("Empty field name in a store context in nodedesfinition: " + node.NodeSet.Name); } object v = reader.GetValue(i); execContext.Row[fname] = (v is DBNull) ? null : v; } } // This is important, we have been doing this for a single result before, but it is better to assume more than one, so that // update of the data being written can be done more freely - using more than one select statement after writing. This is // probably rare, but having the opportunity is better than not having it. } while (reader.NextResult()); if (execContext.Operation != OPERATION_DELETE) { execContext.DataState.SetUnchanged(execContext.Row); } } } } return(null); // if this is not null it should add new results in the data // TODO: Consider if this is possible and useful (for some future version - not urgent). }
protected virtual Dictionary <string, object> LoadLookups( LoadedNodeSet loaderContext, IProcessingContext processingContext, IPluginServiceManager pluginServiceManager, IPluginsSynchronizeContextScoped contextScoped, INode currentNode) { Dictionary <string, object> returnResult = new Dictionary <string, object>(); List <Dictionary <string, object> > results = null; Dictionary <string, object> currentResult = null; // This shouldn't happen to a dog! but I needed a hack for basket consumer. LookupLoaderContext.Instance.PluginServiceManager = pluginServiceManager; LookupLoaderContext.Instance.LoaderContext = loaderContext; LookupLoaderContext.Instance.ProcessingContext = processingContext; // Lookup lookup = (Lookup)currentNode; if (lookup.HasStatement()) { results = new List <Dictionary <string, object> >(); IADOTransactionScope scopedContext = contextScoped as IADOTransactionScope; // Check it is valid if (scopedContext == null) { throw new NullReferenceException("Scoped synchronization and transaction context is not available."); } DbConnection conn = scopedContext.Connection; using (DbCommand cmd = conn.CreateCommand()) { cmd.CommandText = lookup.Query; using (DbDataReader reader = cmd.ExecuteReader()) { if (reader.HasRows) { while (reader.Read()) { currentResult = new Dictionary <string, object>(reader.FieldCount); currentResult = new Dictionary <string, object>(reader.FieldCount); for (int i = 0; i < reader.FieldCount; i++) { string fldname = reader.GetName(i); if (fldname == null) { continue; } fldname = fldname.ToLower().Trim(); if (fldname.Length == 0) { throw new Exception($"Empty name when reading the output of a query. The field index is {i}. The query is: {cmd.CommandText}"); } if (currentResult.ContainsKey(fldname)) { throw new Exception($"Duplicated field name in the output of a query. The field is:{fldname}, the query is: {cmd.CommandText}"); } object o = reader.GetValue(i); currentResult.Add(fldname, (o is DBNull) ? null : o); } results.Add(currentResult); } } } } returnResult.Add(lookup.BindingKey, results); } return(returnResult); }
/* Considerations: * We have to read the configuration in a single turn and consume it from the internal configuration container * in order to avoid direct hard coded dependency on the system configuration structure - most notably dependency on its * structure and interpreatation. * Solution: * We have a nested class in which the configuration values used by this loader are collected (sometimes with some preprocessing) * Lifecycle: * The configuration (part of it at least) has a lifecycle equal to the node execution which is shorter than the life of the dataloader (at least potentially), * so the configuration has to be reread on each node executio and thus is not persisted in the loader's instance, but passed to the main overridable methods. * */ //protected class Configuration { // public string Statement { get; set; } //} /// <summary> /// /// </summary> /// <param name="execContext"></param> /// <returns></returns> //protected virtual Configuration ReadConfiguration(IDataLoaderContext execContext) { // var cfg = new Configuration(); // if (execContext.Action == ACTION_READ) { // cfg.Statement = execContext.CurrentNode.Read.Select.HasStatement?execContext.CurrentNode.Read.Select.Query:null; // } else if (execContext.Action == ACTION_WRITE) { // switch (execContext.Operation) { // case OPERATION_INSERT: // if (execContext.CurrentNode.Write.Insert.HasStatement) { // cfg.Statement = execContext.CurrentNode.Write.Insert.Query; // } // break; // case OPERATION_UPDATE: // if (execContext.CurrentNode.Write.Update.HasStatement) { // cfg.Statement = execContext.CurrentNode.Write.Update.Query; // } // break; // case OPERATION_DELETE: // if (execContext.CurrentNode.Write.Delete.HasStatement) { // cfg.Statement = execContext.CurrentNode.Write.Delete.Query; // } // break; // } // } // return cfg; /////////////////////////////// //} #endregion //public async Task<object> ExecuteAsync(IDataLoaderContext execContext) { // // The procedure is different enough to deserve splitting by read/write // Configuration cfg = ReadConfiguration(execContext); // object result; // if (execContext.Action == ACTION_WRITE) { // result = ExecuteWrite(execContext, cfg); // } else if (execContext.Action == ACTION_READ) { // result = ExecuteRead(execContext, cfg); // } else { // // unknown action // throw new Exception("Unknown action (only read/write) are supported"); // } // return await Task.FromResult(result); //} protected override List <Dictionary <string, object> > Read(IDataLoaderReadContext execContext) { // TODO: What to return if there is no statement: // I think we should have two policies - empty object which enables children extraction if logically possible and // null wich stops the processing here. List <Dictionary <string, object> > results = new List <Dictionary <string, object> >(); Dictionary <string, object> currentResult = null; Node node = execContext.CurrentNode; if (!string.IsNullOrWhiteSpace(Action(execContext).Query)) { // Scope context for the same loader IADOTransactionScope scopedContext = execContext.OwnContextScoped as IADOTransactionScope; // Check it is valid if (scopedContext == null) { throw new NullReferenceException("Scoped synchronization and transaction context is not available."); } // Configuration settings Should be set to the scoped context during its creation/obtainment - see ExternalServiceImp // No tranaction in read mode - lets not forget that closing the transaction also closes the connection - so the ;ifecycle control will do this using the transaction based notation // from ITransactionScope DbConnection conn = scopedContext.Connection; using (DbCommand cmd = conn.CreateCommand()) { cmd.Transaction = scopedContext.CurrentTransaction; // if we decide to open transaction in future this will guarantee we only have to open it and will take effect throughout the code. cmd.Parameters.Clear(); // This will set the resulting command text if everything is Ok. // The processing will make replacements in the SQL and bind parameters by requesting them from the resolver expressions configured on this node. // TODO: Some try...catching is necessary. ProcessCommand(cmd, Action(execContext).Query, execContext); using (DbDataReader reader = cmd.ExecuteReader()) { do { if (reader.HasRows) { // Read a result (many may be contained) row by row while (reader.Read()) { currentResult = new Dictionary <string, object>(reader.FieldCount); for (int i = 0; i < reader.FieldCount; i++) { string fldname = reader.GetName(i); if (fldname == null) { continue; } // TODO: May be configure that or at least create a compile time definition fldname = fldname.ToLower().Trim(); // TODO: lowercase //fldname = fldname.Trim(); if (fldname.Length == 0) { throw new Exception($"Empty name when reading the output of a query. The field index is {i}. The query is: {cmd.CommandText}"); } if (currentResult.ContainsKey(fldname)) { throw new Exception($"Duplicated field name in the output of a query. The field is:{fldname}, the query is: {cmd.CommandText}"); } object v = reader.GetValue(i); currentResult.Add(fldname, (v is DBNull) ? null : v); } // Mark the records unchanged, because they are just picked up from the data store (rdbms in this case). execContext.DataState.SetUnchanged(currentResult); results.Add(currentResult); if (!node.IsList) { break; } } } } while (reader.NextResult()); } } } return(results); // TODO: Decide what behavior we want with empty statements. I for one prefer null result, effectively stopping the operation. }