I have a workflow; within the workflow I can have many stages. I want to open and close a stage based on some business rules. E.g., stage 7 cannot be opened, because stage 5 + 6 need to be closed first; or, check whether key data x is filled out: if so, you can close stage 4.
How can I optimize or improve my code, below? Are there any guidelines or help?
public interface IHigherLevelWorkflow
{
int InstructionID { get; }
int DashboardID { get; }
string Name { get; }
string Description { get; }
int CurrentStage { get; }
IList<DashboardStageDetailDTO> Stages { get; }
InstructionStageProgress CloseStage(int progressId);
InstructionStageProgress OpenStage(int progressId);
}
public class Workflow : IHigherLevelWorkflow
{
private readonly IReadOnlySession _readOnlySession;
private readonly IUserSession _userSession;
public Workflow (Instruction instruction, IReadOnlySession readOnlySession, IUserSession userSession)
{
Guard.ArgumentNotNull(instruction, "instruction");
InstructionID = instruction.InstructionID;
MyInstruction = instruction;
Guard.ArgumentNotNull(readOnlySession, "readOnlySession");
_readOnlySession = readOnlySession;
_userSession = userSession;
Guard.ArgumentNotNull(userSession, "usersession");
MyWorkflow = ResolveDashboard();
}
public Instruction MyInstruction { get; private set; }
public int InstructionID { get; private set; }
public int DashboardID
{
get { return MyWorkflow.DashboardID; }
}
public string Name
{
get { return MyWorkflow.Name; }
}
public string Description
{
get { return MyWorkflow.Description; }
}
public int CurrentStage
{
get { return MyWorkflow.CurrentStage; }
}
public IList<DashboardStageDetailDTO> Stages
{
get {return MyWorkflow.Stages; }
}
public InstructionStageProgress CloseStage(int progressId)
{
//user wants to close a stage...
//is stage valid?
var stage = Stages.FirstOrDefault(x => x.ProgressID == progressId);
if(stage == null)
throw new NoAccessException(string.Format("stage progress #{0} does not exist",progressId));
//what kind of stage is it? do some rules and checks...
//if ok, save to db - do we require a update session? Should we return the object and allow caller to save?
//where will notifications be generated?
var mystage = _readOnlySession.Single<InstructionStageProgress>(x => x.ProgressID == progressId);
if (mystage == null)
throw new NoAccessException(string.Format("stage progress #{0} does not exist", progressId));
mystage.IsCompleted = true;
mystage.DateCompleted = DateTime.Now;
mystage.CompletedByID = _userSession.UserID;
return mystage;
}
private static void OpenStageRules(DashboardStageDetailDTO stageDetail)
{
//depending on what stage it is, we will allow the opening of the stage...
var rules = new RulesException<DashboardStageDetailDTO>();
switch (stageDetail.ViewHook)
{
case StageViewHook.NoView: //first and last stage can never be opened, this is down to the system
rules.ErrorForModel("this stage cannot be opened");
break;
case StageViewHook.Quotes:
rules.ErrorForModel("this stage cannot be opened");
break;
case StageViewHook.Configure:
break;
case StageViewHook.Report:
rules.ErrorForModel("this stage cannot be opened");
break;
case StageViewHook.Check:
rules.ErrorForModel("this stage cannot be opened");
break;
}
if(rules.Errors.Any())
throw rules;
}
public InstructionStageProgress OpenStage(int progressId)
{
//user wants to open a stage...
//is stage valid?
var stage = Stages.FirstOrDefault(x => x.ProgressID == progressId);
if (stage == null)
throw new NoAccessException(string.Format("stage progress #{0} does not exist", progressId));
//what kind of stage is it? do some rules and checks...
OpenStageRules(stage);
//if ok, save to db - do we require a update session? Should we return the object and allow caller to save?
//where will notifications be generated?
var mystage = _readOnlySession.Single<InstructionStageProgress>(x => x.ProgressID == progressId);
if (mystage == null)
throw new NoAccessException(string.Format("stage progress #{0} does not exist", progressId));
mystage.IsCompleted = false;
mystage.DateCompleted = null;
mystage.CompletedByID = null;
mystage.DateInitiated = DateTime.Now;
mystage.InitiatedByID = _userSession.UserID;
return mystage;
}
private WorkflowDTO MyWorkflow { get; set; }
private WorkflowDTO ResolveDashboard()
{
var wf = _readOnlySession.All<Dashboard>()
.GroupJoin(_readOnlySession.All<DashboardStage>(),
x => x.DashboardID,
y => y.DashboardID,
(dash, ds) =>
new
{
Dashboard = dash,
DashboardStages =
ds.DefaultIfEmpty().Join(_readOnlySession.All<InstructionStageProgress>()
.Where(x => x.InstructionID == InstructionID),
x => x.DashboardStageID,
y => y.StageID,
(d, s) => new
{
InstructionStageProgress = s,
DashboardStage = d,
StageOwnerCompany =
_readOnlySession.All<Company>().FirstOrDefault
(
c => c.CompanyID == s.StageOwnerID),
})
})
.Select(dash =>
new WorkflowDTO
{
DashboardID = dash.Dashboard.DashboardID,
Name = dash.Dashboard.Name,
Description = dash.Dashboard.Description,
CurrentStage = dash.DashboardStages
.OrderByDescending(o => o.DashboardStage.SortOrder)
.First(x => x.InstructionStageProgress.IsCompleted).DashboardStage.
DashboardStageID,
Stages = dash.DashboardStages
.Select(stage =>
new DashboardStageDetailDTO
{
Name = stage.DashboardStage.Name,
Description = stage.DashboardStage.Description,
StageID = stage.DashboardStage.DashboardStageID,
StageNumber = stage.DashboardStage.SortOrder,
ViewHook =
stage.DashboardStage.StageHook.Convert<StageViewHook>(),
ProgressID = stage.InstructionStageProgress.ProgressID,
StageOwnerID = stage.InstructionStageProgress.StageOwnerID,
StageOwnerCompanyName =
stage.StageOwnerCompany != null
? stage.StageOwnerCompany.Identifier
: string.Empty,
Progress =
new ProgressDTO
{
DateInitiated =
stage.InstructionStageProgress.DateInitiated,
DateCompleted =
stage.InstructionStageProgress.DateCompleted,
IsCompleted =
stage.InstructionStageProgress.IsCompleted,
InitiatedByUserID =
_readOnlySession.All<User>().Single(
u =>
u.UserID ==
stage.InstructionStageProgress.InitiatedByID)
.
Login,
SignedOffByUserID =
_readOnlySession.All<User>().Single(
u =>
u.UserID ==
stage.InstructionStageProgress.CompletedByID)
.
Login,
}
})
.OrderBy(s => s.StageNumber)
.ToList()
})
.FirstOrDefault();
if (wf == null)
throw new NoAccessException(string.Format("dashboard not found for instruction #{0}", InstructionID));
return wf;
}
}
Here are my stage and progress DTO classes:
public class DashboardStageDetailDTO
{
public int ProgressID { get; set; }
public int StageID { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public int StageNumber { get; set; }
public StageViewHook ViewHook { get; set; }
public string Url { get; set; }
public bool IsViewAvailable { get; set; }
public ProgressDTO Progress { get; set; }
public int? StageOwnerID { get; set; }
}
public class ProgressDTO
{
public bool IsCompleted { get; set; }
public DateTime? DateInitiated { get; set; }
public string InitiatedByUserID { get; set; }
public DateTime? DateCompleted { get; set; }
public string SignedOffByUserID { get; set; }
}
Update to my code:
public class Workflow : IHigherLevelWorkflow
{
public Workflow(Instruction instruction, Dashboard dashboard, IEnumerable<StageProgress> progress)
{
Guard.ArgumentNotNull(instruction, "instruction");
InstructionID = instruction.InstructionID;
MyInstruction = instruction;
Guard.ArgumentNotNull(dashboard, "dashboard");
Description = dashboard.Description;
Name = dashboard.Name;
DashboardID = dashboard.DashboardID;
Guard.ArgumentNotNull(progress, "progress");
Progress = progress.OrderBy(x => x.Stages.SortOrder)
.ToDictionary(x => x.Progress.ProgressID, y => y);
}
public Instruction MyInstruction { get; private set; }
public int InstructionID { get; private set; }
public int DashboardID { get; private set; }
public string Name { get; private set; }
public string Description { get; private set; }
public IDictionary<int, StageProgress> Progress { get; private set; }
public int CurrentStage
{
get
{
//get the last stage that was completed
return Progress.Last(x => x.Value.Progress.IsCompleted).Value.Progress.ProgressID;
}
}
}
public class WorkflowService
{
private readonly IReadOnlySession _readOnlySession;
private readonly IUserSession _userSession;
private readonly IUpdateSession _updateSession;
private readonly INotificationService _notificationService;
public WorkflowService(IUserSession userSession, IReadOnlySession readOnlySession, IUpdateSession updateSession)
{
_userSession = userSession;
_readOnlySession = readOnlySession;
_updateSession = updateSession;
_notificationService = new NotificationService();
}
private int InstructionID { get; set; }
private int ProgressId { get; set; }
private DashboardInfo GetMyDashboard()
{
var wf = _readOnlySession.All<Dashboard>()
.GroupJoin(_readOnlySession.All<DashboardStage>(),
x => x.DashboardID,
y => y.DashboardID,
(dash, ds) =>
new
{
Dashboard = dash,
DashboardStages = ds.DefaultIfEmpty()
.Join(_readOnlySession.All<InstructionStageProgress>()
.Where(x => x.InstructionID == InstructionID),
x => x.DashboardStageID,
y => y.StageID,
(d, s) => new StageProgress
{
Progress = s,
Stages = d,
})
})
.Select(dash =>
new DashboardInfo
{
Dashboard = dash.Dashboard,
Stages = dash.DashboardStages.ToList()
})
.FirstOrDefault();
if (wf == null)
throw new NoAccessException(string.Format("dashboard not found for instruction #{0}", InstructionID));
return wf;
}
/// <summary>
/// we could determine workflow by instruction source?
/// </summary>
/// <param name="instruction"></param>
/// <returns></returns>
private IHigherLevelWorkflow DetermineWorkflow(Instruction instruction)
{
var dashboard = GetMyDashboard();
var wfhandler = new Workflow (instruction, dashboard.Dashboard, dashboard.Stages);
return wfhandler;
}
/// <summary>
/// these rules will be based on the workflow...
/// </summary>
/// <param name="workflow"></param>
/// <returns></returns>
private static IWorkflowStageHandler DetermineWorkflowRules(IHigherLevelWorkflow workflow)
{
return new WorkflowStageHandler();
}
public void CloseWorkflowStage(int instructionid, int progressId)
{
InstructionID = instructionid;
ProgressId = progressId;
var instruction = _readOnlySession.GetData(instructionid, _userSession);
//set up workflow + rules...
var wfhandler = DetermineWorkflow(instruction);
var stageHandler = DetermineWorkflowRules(wfhandler);
StageProgress mystage;
stageHandler.CloseStage(_readOnlySession, _userSession, wfhandler, progressId, out mystage);
//raise notification
//persist...
_updateSession.Update(mystage.Progress);
_notificationService.SaveNotifications(_updateSession);
_updateSession.CommitChanges();
}
public void OpenWorkflowStage(int instructionid, int progressId)
{
InstructionID = instructionid;
ProgressId = progressId;
var instruction = _readOnlySession.GetData(instructionid, _userSession);
//user wants to close a stage...
//is stage valid?
var wfhandler = DetermineWorkflow(instruction);
var stageHandler = DetermineWorkflowRules(wfhandler);
StageProgress mystage;
stageHandler.OpenStage(_readOnlySession, _userSession, wfhandler, progressId, out mystage);
//raise notification//
//persist...
_updateSession.Update(mystage.Progress);
_notificationService.SaveNotifications(_updateSession);
_updateSession.CommitChanges();
}
}