diff --git a/AzureDevOpsStateTracker.Functions/AzureDevOpsStateTracker.Functions.csproj b/AzureDevOpsStateTracker.Functions/AzureDevOpsStateTracker.Functions.csproj
index 6d0b3c8..e7bbcc3 100644
--- a/AzureDevOpsStateTracker.Functions/AzureDevOpsStateTracker.Functions.csproj
+++ b/AzureDevOpsStateTracker.Functions/AzureDevOpsStateTracker.Functions.csproj
@@ -17,9 +17,6 @@
-
-
-
PreserveNewest
diff --git a/AzureDevOpsStateTracker.Functions/AzureDevOpsStateTracker.Functions.sln b/AzureDevOpsStateTracker.Functions/AzureDevOpsStateTracker.Functions.sln
index 3d0754b..ba1c1f3 100644
--- a/AzureDevOpsStateTracker.Functions/AzureDevOpsStateTracker.Functions.sln
+++ b/AzureDevOpsStateTracker.Functions/AzureDevOpsStateTracker.Functions.sln
@@ -5,8 +5,6 @@ VisualStudioVersion = 16.0.31229.75
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureDevOpsStateTracker.Functions", "AzureDevOpsStateTracker.Functions.csproj", "{3C86C085-C8C6-46BB-8315-D4348928034C}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureDevopsStateTracker", "..\..\azure-devops-state-tracker\AzureDevopsStateTracker\AzureDevopsStateTracker.csproj", "{E71EE8C3-7D74-4C91-B244-2394160F7486}"
-EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -17,10 +15,6 @@ Global
{3C86C085-C8C6-46BB-8315-D4348928034C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3C86C085-C8C6-46BB-8315-D4348928034C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3C86C085-C8C6-46BB-8315-D4348928034C}.Release|Any CPU.Build.0 = Release|Any CPU
- {E71EE8C3-7D74-4C91-B244-2394160F7486}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {E71EE8C3-7D74-4C91-B244-2394160F7486}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {E71EE8C3-7D74-4C91-B244-2394160F7486}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {E71EE8C3-7D74-4C91-B244-2394160F7486}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/AzureDevopsStateTracker/Adapters/WorkItemAdapter.cs b/AzureDevopsStateTracker/Adapters/WorkItemAdapter.cs
new file mode 100644
index 0000000..904f362
--- /dev/null
+++ b/AzureDevopsStateTracker/Adapters/WorkItemAdapter.cs
@@ -0,0 +1,109 @@
+using AzureDevopsStateTracker.DTOs;
+using AzureDevopsStateTracker.Entities;
+using AzureDevopsStateTracker.Interfaces;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace AzureDevopsStateTracker.Adapters
+{
+ internal class WorkItemAdapter : IWorkItemAdapter
+ {
+ public WorkItemDTO ToWorkItemDTO(WorkItem workItem)
+ {
+ if (workItem == null) return null;
+
+ return new WorkItemDTO()
+ {
+ Id = workItem.Id,
+ CreatedAt = workItem.CreatedAt,
+ AssignedTo = workItem.AssignedTo,
+ CreatedBy = workItem.CreatedBy,
+ CurrentStatus = workItem.CurrentStatus,
+ TeamProject = workItem.TeamProject,
+ AreaPath = workItem.AreaPath,
+ IterationPath = workItem.IterationPath,
+ Title = workItem.Title,
+ Type = workItem.Type,
+ Effort = workItem.Effort,
+ StoryPoints = workItem.StoryPoints,
+ OriginalEstimate = workItem.OriginalEstimate,
+ WorkItemParentId = workItem.WorkItemParentId,
+ Activity = workItem.Activity,
+ Tags = workItem.Tags == null ? new List() : workItem.Tags.Split(';').ToList(),
+ WorkItemsChangesDTO = ToWorkItemsChangeDTO(workItem.WorkItemsChanges.OrderBy(x => x.CreatedAt).ToList()),
+ TimesByStateDTO = ToTimeByStatesDTO(workItem.CalculateTotalTimeByState().ToList()),
+ };
+ }
+
+ public List ToWorkItemsDTO(List workItems)
+ {
+ var workItemsDTO = new List();
+
+ if (workItems == null) return workItemsDTO;
+
+ workItems.ForEach(
+ workItem =>
+ workItemsDTO.Add(ToWorkItemDTO(workItem)));
+
+ return workItemsDTO
+ .ToList();
+ }
+
+ public WorkItemChangeDTO ToWorkItemChangeDTO(WorkItemChange workIteChange)
+ {
+ if (workIteChange == null) return null;
+
+ return new WorkItemChangeDTO()
+ {
+ NewDate = workIteChange.NewDate,
+ NewState = workIteChange.NewState,
+ OldState = workIteChange.OldState,
+ OldDate = workIteChange.OldDate,
+ ChangedBy = workIteChange.ChangedBy
+ };
+ }
+
+ public List ToWorkItemsChangeDTO(List workItemsChanges)
+ {
+ var workItemsChangeDTO = new List();
+
+ if (workItemsChanges == null) return workItemsChangeDTO;
+
+ workItemsChanges.ForEach(
+ workItemsChange =>
+ workItemsChangeDTO.Add(ToWorkItemChangeDTO(workItemsChange)));
+
+ return workItemsChangeDTO
+ .Where(w => w != null)
+ .ToList();
+ }
+
+ public TimeByStateDTO ToTimeByStateDTO(TimeByState workItemStatusTime)
+ {
+ if (workItemStatusTime == null) return null;
+
+ return new TimeByStateDTO()
+ {
+ CreatedAt = workItemStatusTime.CreatedAt,
+ State = workItemStatusTime.State,
+ //TotalTime = workItemStatusTime.TotalTimeText,
+ //TotalWorkedTime = workItemStatusTime.TotalWorkedTimeText
+ };
+ }
+
+ public List ToTimeByStatesDTO(List workItemStatusTimes)
+ {
+ var workItemStatusTimeDTO = new List();
+
+ if (workItemStatusTimes == null) return workItemStatusTimeDTO;
+
+ workItemStatusTimes.ForEach(
+ workItemStatusTime =>
+ workItemStatusTimeDTO.Add(ToTimeByStateDTO(workItemStatusTime)));
+
+ return workItemStatusTimeDTO
+ .Where(w => w != null)
+ .ToList();
+ }
+ }
+}
\ No newline at end of file
diff --git a/AzureDevopsStateTracker/AzureDevopsStateTracker.csproj b/AzureDevopsStateTracker/AzureDevopsStateTracker.csproj
new file mode 100644
index 0000000..647ec7b
--- /dev/null
+++ b/AzureDevopsStateTracker/AzureDevopsStateTracker.csproj
@@ -0,0 +1,13 @@
+
+
+
+ netcoreapp3.1
+ false
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AzureDevopsStateTracker/AzureDevopsStateTracker.sln b/AzureDevopsStateTracker/AzureDevopsStateTracker.sln
new file mode 100644
index 0000000..1a63535
--- /dev/null
+++ b/AzureDevopsStateTracker/AzureDevopsStateTracker.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.31402.337
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureDevopsStateTracker", "AzureDevopsStateTracker.csproj", "{E00DFF81-08B1-4A71-82C4-7CED951299BE}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {E00DFF81-08B1-4A71-82C4-7CED951299BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E00DFF81-08B1-4A71-82C4-7CED951299BE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E00DFF81-08B1-4A71-82C4-7CED951299BE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E00DFF81-08B1-4A71-82C4-7CED951299BE}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {53133A2B-5357-47B5-A1C4-30953F789623}
+ EndGlobalSection
+EndGlobal
diff --git a/AzureDevopsStateTracker/Configurations/Configuration.cs b/AzureDevopsStateTracker/Configurations/Configuration.cs
new file mode 100644
index 0000000..a20ca36
--- /dev/null
+++ b/AzureDevopsStateTracker/Configurations/Configuration.cs
@@ -0,0 +1,37 @@
+using AzureDevopsStateTracker.Adapters;
+using AzureDevopsStateTracker.Data;
+using AzureDevopsStateTracker.Data.Context;
+using AzureDevopsStateTracker.Interfaces;
+using AzureDevopsStateTracker.Interfaces.Internals;
+using AzureDevopsStateTracker.Services;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using System;
+
+namespace AzureDevopsStateTracker.Configurations
+{
+ public static class Configuration
+ {
+ public static IServiceCollection AddAzureDevopsStateTracker(this IServiceCollection services, DataBaseConfig configurations)
+ {
+ services.AddDbContext(options =>
+ options.UseSqlServer(DataBaseConfig.ConnectionsString));
+
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+
+ return services;
+ }
+
+ public static IApplicationBuilder UseAzureDevopsStateTracker(this IApplicationBuilder app, IServiceProvider serviceProvider)
+ {
+ var context = serviceProvider.GetService();
+ context.Database.Migrate();
+
+ return app;
+ }
+ }
+}
\ No newline at end of file
diff --git a/AzureDevopsStateTracker/DTOs/Create/CreateWorkItemDTO.cs b/AzureDevopsStateTracker/DTOs/Create/CreateWorkItemDTO.cs
new file mode 100644
index 0000000..e6282cd
--- /dev/null
+++ b/AzureDevopsStateTracker/DTOs/Create/CreateWorkItemDTO.cs
@@ -0,0 +1,19 @@
+using Newtonsoft.Json;
+
+namespace AzureDevopsStateTracker.DTOs.Create
+{
+ public class CreateWorkItemDTO
+ {
+ [JsonProperty("resource")]
+ public Resource Resource { get; set; }
+ }
+
+ public class Resource
+ {
+ [JsonProperty("id")]
+ public string Id { get; set; }
+
+ [JsonProperty("fields")]
+ public Fields Fields { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/AzureDevopsStateTracker/DTOs/Fields.cs b/AzureDevopsStateTracker/DTOs/Fields.cs
new file mode 100644
index 0000000..1351315
--- /dev/null
+++ b/AzureDevopsStateTracker/DTOs/Fields.cs
@@ -0,0 +1,59 @@
+using Newtonsoft.Json;
+using System;
+
+namespace AzureDevopsStateTracker.DTOs
+{
+ public class Fields
+ {
+ [JsonProperty("System.AreaPath")]
+ public string AreaPath { get; set; }
+
+ [JsonProperty("System.TeamProject")]
+ public string TeamProject { get; set; }
+
+ [JsonProperty("System.IterationPath")]
+ public string IterationPath { get; set; }
+
+ [JsonProperty("System.AssignedTo")]
+ public string AssignedTo { get; set; }
+
+ [JsonProperty("System.WorkItemType")]
+ public string Type { get; set; }
+
+ [JsonProperty("System.CreatedDate")]
+ public DateTime CreatedDate { get; set; }
+
+ [JsonProperty("System.CreatedBy")]
+ public string CreatedBy { get; set; }
+
+ [JsonProperty("System.ChangedBy")]
+ public string ChangedBy { get; set; }
+
+ [JsonProperty("System.State")]
+ public string State { get; set; }
+
+ [JsonProperty("System.Title")]
+ public string Title { get; set; }
+
+ [JsonProperty("System.Tags")]
+ public string Tags { get; set; }
+
+ [JsonProperty("System.Parent")]
+ public string Parent { get; set; }
+
+ [JsonProperty("Microsoft.VSTS.Scheduling.StoryPoints")]
+ public string StoryPoints { get; set; }
+
+ [JsonProperty("Microsoft.VSTS.Scheduling.OriginalEstimate")]
+ public string OriginalEstimate { get; set; }
+
+ [JsonProperty("Microsoft.VSTS.Scheduling.RemainingWork")]
+ public string RemainingWork { get; set; }
+
+ [JsonProperty("Microsoft.VSTS.Scheduling.Effort")]
+ public string Effort { get; set; }
+
+ [JsonProperty("Microsoft.VSTS.Common.Activity")]
+ public string Activity { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/AzureDevopsStateTracker/DTOs/Update/UpdateWorkItemDTO.cs b/AzureDevopsStateTracker/DTOs/Update/UpdateWorkItemDTO.cs
new file mode 100644
index 0000000..d0ba441
--- /dev/null
+++ b/AzureDevopsStateTracker/DTOs/Update/UpdateWorkItemDTO.cs
@@ -0,0 +1,68 @@
+using Newtonsoft.Json;
+using System;
+
+namespace AzureDevopsStateTracker.DTOs.Update
+{
+ public class UpdatedWorkItemDTO
+ {
+ [JsonProperty("resource")]
+ public Resource Resource { get; set; }
+ }
+
+ public class Resource
+ {
+ [JsonProperty("workItemId")]
+ public string WorkItemId { get; set; }
+
+ [JsonProperty("fields")]
+ public ResourceFields Fields { get; set; }
+
+ [JsonProperty("revision")]
+ public Revision Revision { get; set; }
+ }
+
+ public class Revision
+ {
+ [JsonProperty("fields")]
+ public Fields Fields { get; set; }
+ }
+
+ public class ResourceFields
+ {
+ [JsonProperty("System.State")]
+ public OldNewValues State { get; set; }
+
+ [JsonProperty("Microsoft.VSTS.Common.StateChangeDate")]
+ public DateTimeOldNewValues StateChangeDate { get; set; }
+
+ [JsonProperty("System.ChangedBy")]
+ public ChangedByOldNewValues ChangedBy { get; set; }
+ }
+
+ public class OldNewValues
+ {
+ [JsonProperty("oldValue")]
+ public string OldValue { get; set; }
+
+ [JsonProperty("newValue")]
+ public string NewValue { get; set; }
+ }
+
+ public class DateTimeOldNewValues
+ {
+ [JsonProperty("oldValue")]
+ public DateTime OldValue { get; set; }
+
+ [JsonProperty("newValue")]
+ public DateTime NewValue { get; set; }
+ }
+
+ public class ChangedByOldNewValues
+ {
+ [JsonProperty("oldValue")]
+ public string OldValue { get; set; }
+
+ [JsonProperty("newValue")]
+ public string NewValue { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/AzureDevopsStateTracker/DTOs/WorkItemDTO.cs b/AzureDevopsStateTracker/DTOs/WorkItemDTO.cs
new file mode 100644
index 0000000..d9990a4
--- /dev/null
+++ b/AzureDevopsStateTracker/DTOs/WorkItemDTO.cs
@@ -0,0 +1,100 @@
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+
+namespace AzureDevopsStateTracker.DTOs
+{
+ public class WorkItemDTO
+ {
+ [JsonProperty("id")]
+ public string Id { get; set; }
+
+ [JsonProperty("created_at")]
+ public DateTime CreatedAt { get; set; }
+
+ [JsonProperty("assigned_to")]
+ public string AssignedTo { get; set; }
+
+ [JsonProperty("type")]
+ public string Type { get; set; }
+
+ [JsonProperty("effort")]
+ public string Effort { get; set; }
+
+ [JsonProperty("story_points")]
+ public string StoryPoints { get; set; }
+
+ [JsonProperty("original_estimate")]
+ public string OriginalEstimate { get; set; }
+
+ [JsonProperty("created_by")]
+ public string CreatedBy { get; set; }
+
+ [JsonProperty("title")]
+ public string Title { get; set; }
+
+ [JsonProperty("team_project")]
+ public string TeamProject { get; set; }
+
+ [JsonProperty("iteration_path")]
+ public string IterationPath { get; set; }
+
+ [JsonProperty("area_path")]
+ public string AreaPath { get; set; }
+
+ [JsonProperty("current_status")]
+ public string CurrentStatus { get; set; }
+
+ [JsonProperty("work_item_parent_id")]
+ public string WorkItemParentId { get; set; }
+
+ [JsonProperty("activity")]
+ public string Activity { get; set; }
+
+ [JsonProperty("tags")]
+ public IEnumerable Tags { get; set; }
+
+ [JsonProperty("workItems_changes")]
+ public List WorkItemsChangesDTO { get; set; }
+
+ [JsonProperty("times_by_state")]
+ public List TimesByStateDTO { get; set; }
+ }
+
+ public class WorkItemChangeDTO
+ {
+ [JsonProperty("new_date")]
+ public DateTime NewDate { get; set; }
+
+ [JsonProperty("new_state")]
+ public string NewState { get; set; }
+
+ [JsonProperty("old_state")]
+ public string OldState { get; set; }
+
+ [JsonProperty("old_date")]
+ public DateTime? OldDate { get; set; }
+
+ [JsonProperty("changed_by")]
+ public string ChangedBy { get; set; }
+ }
+
+
+ public class TimeByStateDTO
+ {
+ [JsonProperty("created_at")]
+ public DateTime CreatedAt { get; set; }
+
+ [JsonProperty("updated_at")]
+ public DateTime UpdatedAt { get; set; }
+
+ [JsonProperty("state")]
+ public string State { get; set; }
+
+ [JsonProperty("total_time")]
+ public string TotalTime { get; set; }
+
+ [JsonProperty("total_worked_time")]
+ public string TotalWorkedTime { get; set; }
+ }
+}
diff --git a/AzureDevopsStateTracker/Data/Context/AzureDevopsStateTrackerContext.cs b/AzureDevopsStateTracker/Data/Context/AzureDevopsStateTrackerContext.cs
new file mode 100644
index 0000000..18113a6
--- /dev/null
+++ b/AzureDevopsStateTracker/Data/Context/AzureDevopsStateTrackerContext.cs
@@ -0,0 +1,32 @@
+using AzureDevopsStateTracker.Entities;
+using Microsoft.EntityFrameworkCore;
+using System;
+using System.Linq;
+
+namespace AzureDevopsStateTracker.Data.Context
+{
+ public class AzureDevopsStateTrackerContext : DbContext, IDisposable
+ {
+ public AzureDevopsStateTrackerContext(DbContextOptions options) : base(options)
+ { }
+
+ public DbSet WorkItems { get; set; }
+ public DbSet WorkItemsChange { get; set; }
+ public DbSet TimeByStates { get; set; }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ foreach (var property in modelBuilder.Model.GetEntityTypes().SelectMany(
+ e => e.GetProperties().Where(p => p.ClrType == typeof(string))))
+ property.SetColumnType("varchar(200)");
+
+ modelBuilder.HasDefaultSchema(DataBaseConfig.SchemaName);
+ }
+
+ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+ {
+ if (!optionsBuilder.IsConfigured)
+ optionsBuilder.UseSqlServer(DataBaseConfig.ConnectionsString);
+ }
+ }
+}
\ No newline at end of file
diff --git a/AzureDevopsStateTracker/Data/DataBaseConfig.cs b/AzureDevopsStateTracker/Data/DataBaseConfig.cs
new file mode 100644
index 0000000..fbf1079
--- /dev/null
+++ b/AzureDevopsStateTracker/Data/DataBaseConfig.cs
@@ -0,0 +1,20 @@
+using AzureDevopsStateTracker.Extensions;
+using System;
+
+namespace AzureDevopsStateTracker.Data
+{
+ public class DataBaseConfig
+ {
+ public DataBaseConfig(string connectionsString, string schemaName = "dbo")
+ {
+ if (connectionsString.IsNullOrEmpty())
+ throw new ArgumentException("The ConnectionsString is required");
+
+ ConnectionsString = connectionsString;
+ SchemaName = schemaName;
+ }
+
+ public static string ConnectionsString { get; private set; }
+ public static string SchemaName { get; private set; }
+ }
+}
\ No newline at end of file
diff --git a/AzureDevopsStateTracker/Data/Repository.cs b/AzureDevopsStateTracker/Data/Repository.cs
new file mode 100644
index 0000000..23d4673
--- /dev/null
+++ b/AzureDevopsStateTracker/Data/Repository.cs
@@ -0,0 +1,70 @@
+using AzureDevopsStateTracker.Data.Context;
+using AzureDevopsStateTracker.Entities;
+using AzureDevopsStateTracker.Interfaces.Internals;
+using Microsoft.EntityFrameworkCore;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace AzureDevopsStateTracker.Data
+{
+ internal abstract class Repository : IRepository where TEntity : Entity
+ {
+ protected readonly AzureDevopsStateTrackerContext Db;
+ protected readonly DbSet DbSet;
+
+ public Repository(AzureDevopsStateTrackerContext db)
+ {
+ Db = db;
+ DbSet = db.Set();
+ }
+
+ public virtual void Add(TEntity entity)
+ {
+ DbSet.Add(entity);
+ }
+
+ public virtual void Add(IEnumerable entities)
+ {
+ DbSet.AddRange(entities);
+ }
+
+ public virtual void Update(TEntity entity)
+ {
+ DbSet.Update(entity);
+ }
+
+ public virtual void Update(IEnumerable entities)
+ {
+ DbSet.UpdateRange(entities);
+ }
+
+ public virtual void Delete(TEntity entity)
+ {
+ DbSet.Remove(entity);
+ }
+
+ public virtual TEntity GetById(string id)
+ {
+ return DbSet
+ .Where(x => x.Id == id)
+ .FirstOrDefault();
+ }
+
+ public bool Exist(string id)
+ {
+ return DbSet
+ .Any(x => x.Id == id);
+ }
+
+ public void SaveChanges()
+ {
+ Db.SaveChanges();
+ }
+
+ public void Dispose()
+ {
+ GC.SuppressFinalize(this);
+ }
+ }
+}
\ No newline at end of file
diff --git a/AzureDevopsStateTracker/Data/WorkItemRepository.cs b/AzureDevopsStateTracker/Data/WorkItemRepository.cs
new file mode 100644
index 0000000..853f884
--- /dev/null
+++ b/AzureDevopsStateTracker/Data/WorkItemRepository.cs
@@ -0,0 +1,43 @@
+using AzureDevopsStateTracker.Data.Context;
+using AzureDevopsStateTracker.Entities;
+using AzureDevopsStateTracker.Interfaces.Internals;
+using Microsoft.EntityFrameworkCore;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace AzureDevopsStateTracker.Data
+{
+ internal class WorkItemRepository : Repository, IWorkItemRepository
+ {
+ public WorkItemRepository(AzureDevopsStateTrackerContext context) : base(context) { }
+
+ public WorkItem GetByWorkItemId(string workItemId)
+ {
+ return DbSet
+ .Include(x => x.WorkItemsChanges)
+ .Include(x => x.TimeByStates)
+ .FirstOrDefault(x => x.Id == workItemId);
+ }
+
+ public IEnumerable ListByWorkItemId(IEnumerable workItemsId)
+ {
+ return DbSet
+ .Include(x => x.WorkItemsChanges)
+ .Include(x => x.TimeByStates)
+ .Where(x => workItemsId.Contains(x.Id));
+ }
+
+ public IEnumerable ListByIterationPath(string iterationPath)
+ {
+ return DbSet
+ .Include(x => x.WorkItemsChanges)
+ .Include(x => x.TimeByStates)
+ .Where(x => x.IterationPath == iterationPath);
+ }
+
+ public void RemoveAllTimeByState(List timeByStates)
+ {
+ Db.TimeByStates.RemoveRange(timeByStates);
+ }
+ }
+}
\ No newline at end of file
diff --git a/AzureDevopsStateTracker/Entities/Entity.cs b/AzureDevopsStateTracker/Entities/Entity.cs
new file mode 100644
index 0000000..4f50888
--- /dev/null
+++ b/AzureDevopsStateTracker/Entities/Entity.cs
@@ -0,0 +1,21 @@
+using System;
+
+namespace AzureDevopsStateTracker.Entities
+{
+ public abstract class Entity
+ {
+ public string Id { get; protected set; }
+ public DateTime CreatedAt { get; private set; }
+ public Entity(string id)
+ {
+ Id = id;
+ CreatedAt = DateTime.UtcNow;
+ }
+
+ public Entity()
+ {
+ Id = Guid.NewGuid().ToString();
+ CreatedAt = DateTime.UtcNow;
+ }
+ }
+}
\ No newline at end of file
diff --git a/AzureDevopsStateTracker/Entities/TimeByState.cs b/AzureDevopsStateTracker/Entities/TimeByState.cs
new file mode 100644
index 0000000..4145ebb
--- /dev/null
+++ b/AzureDevopsStateTracker/Entities/TimeByState.cs
@@ -0,0 +1,36 @@
+using AzureDevopsStateTracker.Extensions;
+using System;
+
+namespace AzureDevopsStateTracker.Entities
+{
+ public class TimeByState : Entity
+ {
+ public string WorkItemId { get; private set; }
+ public string State { get; private set; }
+ public double TotalTime { get; private set; }
+ public double TotalWorkedTime { get; private set; }
+
+ public WorkItem WorkItem { get; private set; }
+
+ private TimeByState() { }
+
+ public TimeByState(string workItemId, string state, TimeSpan totalTime, TimeSpan totalWorkedTime)
+ {
+ WorkItemId = workItemId;
+ State = state;
+ TotalTime = totalTime.TotalSeconds;
+ TotalWorkedTime = totalWorkedTime.TotalSeconds;
+
+ Validate();
+ }
+
+ public void Validate()
+ {
+ if (WorkItemId.IsNullOrEmpty() || Convert.ToInt64(WorkItemId) <= 0)
+ throw new Exception("WorkItemId is required");
+
+ if (State.IsNullOrEmpty())
+ throw new Exception("State is required");
+ }
+ }
+}
\ No newline at end of file
diff --git a/AzureDevopsStateTracker/Entities/WorkItem.cs b/AzureDevopsStateTracker/Entities/WorkItem.cs
new file mode 100644
index 0000000..64aa79c
--- /dev/null
+++ b/AzureDevopsStateTracker/Entities/WorkItem.cs
@@ -0,0 +1,128 @@
+using AzureDevopsStateTracker.Extensions;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace AzureDevopsStateTracker.Entities
+{
+ public class WorkItem : Entity
+ {
+ public string AreaPath { get; private set; }
+ public string TeamProject { get; private set; }
+ public string IterationPath { get; private set; }
+ public string AssignedTo { get; private set; }
+ public string Type { get; private set; }
+ public string CreatedBy { get; private set; }
+ public string Title { get; private set; }
+ public string Tags { get; private set; }
+ public string Effort { get; private set; }
+ public string OriginalEstimate { get; private set; }
+ public string StoryPoints { get; private set; }
+ public string WorkItemParentId { get; private set; }
+ public string Activity { get; private set; }
+
+ private readonly List _workItemsChanges;
+ public IReadOnlyCollection WorkItemsChanges => _workItemsChanges;
+
+ private readonly List _timeByState;
+ public IReadOnlyCollection TimeByStates => _timeByState;
+ public string CurrentStatus => _workItemsChanges?.OrderBy(x => x.CreatedAt)?.LastOrDefault()?.NewState;
+
+ private WorkItem()
+ {
+ _workItemsChanges = new List();
+ _timeByState = new List();
+ }
+
+ public WorkItem(string workItemId) : base(workItemId)
+ {
+ _workItemsChanges = new List();
+ _timeByState = new List();
+ Validate();
+ }
+
+ public void Update(string title,
+ string teamProject, string areaPath,
+ string iterationPath, string type,
+ string createdBy, string assignedTo,
+ string tags,
+ string workItemParentId,
+ string effort,
+ string storyPoint,
+ string originalEstimate,
+ string activity)
+ {
+ TeamProject = teamProject;
+ AreaPath = areaPath;
+ IterationPath = iterationPath;
+ Type = type;
+ Title = title;
+ CreatedBy = createdBy;
+ AssignedTo = assignedTo;
+ Tags = tags;
+ WorkItemParentId = workItemParentId;
+ Effort = effort;
+ StoryPoints = storyPoint;
+ OriginalEstimate = originalEstimate;
+ Activity = activity;
+ }
+
+ public void Validate()
+ {
+ if (Id.IsNullOrEmpty())
+ throw new Exception("WorkItemId is required");
+ }
+
+ public void AddWorkItemChange(WorkItemChange workItemChange)
+ {
+ if (workItemChange == null)
+ throw new Exception("WorkItemChange is null");
+
+ _workItemsChanges.Add(workItemChange);
+ }
+
+ public void AddTimeByState(TimeByState timeByState)
+ {
+ if (timeByState == null)
+ throw new Exception("TimeByState is null");
+
+ _timeByState.Add(timeByState);
+ }
+
+ public void AddTimesByState(IEnumerable timesByState)
+ {
+ if (!timesByState.Any())
+ return;
+
+ foreach (var timeByState in timesByState)
+ AddTimeByState(timeByState);
+ }
+
+ public void ClearTimesByState()
+ {
+ _timeByState.Clear();
+ }
+
+ public IEnumerable CalculateTotalTimeByState()
+ {
+ var timesByStateList = new List();
+ if (!_workItemsChanges.Any())
+ return timesByStateList;
+
+ foreach (var workItemChange in _workItemsChanges.OrderBy(x => x.CreatedAt).GroupBy(x => x.OldState).Where(x => x.Key != null))
+ {
+ var totalTime = TimeSpan.Zero;
+ var totalWorkedTime = TimeSpan.Zero;
+ foreach (var data in workItemChange)
+ {
+ totalTime += data.CalculateTotalTime();
+ totalWorkedTime += data.CalculateTotalWorkedTime();
+ }
+
+ timesByStateList.Add(new TimeByState(Id, workItemChange.Key, totalTime, totalWorkedTime));
+ }
+
+ return timesByStateList;
+ }
+ }
+}
\ No newline at end of file
diff --git a/AzureDevopsStateTracker/Entities/WorkItemChange.cs b/AzureDevopsStateTracker/Entities/WorkItemChange.cs
new file mode 100644
index 0000000..b7baac5
--- /dev/null
+++ b/AzureDevopsStateTracker/Entities/WorkItemChange.cs
@@ -0,0 +1,59 @@
+using AzureDevopsStateTracker.Extensions;
+using System;
+
+namespace AzureDevopsStateTracker.Entities
+{
+ public class WorkItemChange : Entity
+ {
+ public string WorkItemId { get; private set; }
+ public DateTime NewDate { get; private set; }
+ public DateTime? OldDate { get; private set; }
+ public string NewState { get; private set; }
+ public string OldState { get; private set; }
+ public string ChangedBy { get; private set; }
+ public WorkItem WorkItem { get; private set; }
+
+ private WorkItemChange() { }
+
+ public WorkItemChange(string workItemId, string changedBy, DateTime newDate, string newState, string oldState, DateTime? oldDate)
+ {
+ WorkItemId = workItemId;
+ NewDate = newDate;
+ OldDate = oldDate;
+ NewState = newState;
+ OldState = oldState;
+ ChangedBy = changedBy;
+
+ Validate();
+ }
+
+ public void Validate()
+ {
+ if (WorkItemId.IsNullOrEmpty() || Convert.ToInt64(WorkItemId) <= 0)
+ throw new Exception("WorkItemId is required");
+ }
+
+ public TimeSpan CalculateTotalTime()
+ {
+ return OldDate == null ? TimeSpan.Zero : NewDate - OldDate.GetValueOrDefault();
+ }
+
+ public TimeSpan CalculateTotalWorkedTime()
+ {
+ if (OldDate.GetValueOrDefault() == DateTime.MinValue)
+ return TimeSpan.Zero;
+
+ TimeSpan hoursWorked = TimeSpan.Zero;
+ for (var i = OldDate.GetValueOrDefault(); i <= NewDate; i = i.AddHours(1))
+ {
+ if (i.DayOfWeek != DayOfWeek.Saturday && i.DayOfWeek != DayOfWeek.Sunday)
+ {
+ if ((i.TimeOfDay.Hours >= 8 && i.TimeOfDay.Hours < 12) || (i.TimeOfDay.Hours >= 14 && i.TimeOfDay.Hours < 18))
+ hoursWorked += (NewDate.TimeOfDay - i.TimeOfDay);
+ }
+ }
+
+ return hoursWorked;
+ }
+ }
+}
\ No newline at end of file
diff --git a/AzureDevopsStateTracker/Extensions/HelperExtenions.cs b/AzureDevopsStateTracker/Extensions/HelperExtenions.cs
new file mode 100644
index 0000000..286b592
--- /dev/null
+++ b/AzureDevopsStateTracker/Extensions/HelperExtenions.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Linq;
+
+namespace AzureDevopsStateTracker.Extensions
+{
+ public static class HelperExtenions
+ {
+ public static bool IsNullOrEmpty(this string text)
+ {
+ return string.IsNullOrEmpty(text?.Trim());
+ }
+
+ public static string ExtractEmail(this string user)
+ {
+ if (user is null)
+ return user;
+
+ if (!user.Contains(" <") && !user.TrimEnd().Contains(">"))
+ return user;
+
+ return user.Split("<").LastOrDefault().Split(">").FirstOrDefault();
+ }
+
+ public static string ToTextTime(this TimeSpan timeSpan)
+ {
+ if (timeSpan.Days > 0)
+ return $@"{timeSpan:%d} Dia(s) {timeSpan:%h} h e {timeSpan:%m} min e {timeSpan:%s} s";
+
+ if (timeSpan.Hours > 0)
+ return $@"{timeSpan:%h} h e {timeSpan:%m} min e {timeSpan:%s} s";
+
+ if (timeSpan.Minutes > 0)
+ return $@"{timeSpan:%m} min e {timeSpan:%s} s";
+
+ if (timeSpan.Seconds > 0)
+ return $@"{timeSpan:%s} s";
+
+ return "-";
+ }
+ }
+}
\ No newline at end of file
diff --git a/AzureDevopsStateTracker/Interfaces/IAzureDevopsStateTrackerService.cs b/AzureDevopsStateTracker/Interfaces/IAzureDevopsStateTrackerService.cs
new file mode 100644
index 0000000..a0ef0f9
--- /dev/null
+++ b/AzureDevopsStateTracker/Interfaces/IAzureDevopsStateTrackerService.cs
@@ -0,0 +1,13 @@
+using AzureDevopsStateTracker.DTOs;
+using AzureDevopsStateTracker.DTOs.Create;
+using AzureDevopsStateTracker.DTOs.Update;
+
+namespace AzureDevopsStateTracker.Interfaces
+{
+ public interface IAzureDevopsStateTrackerService
+ {
+ void Create(CreateWorkItemDTO createDto);
+ void Update(UpdatedWorkItemDTO updateDto);
+ WorkItemDTO GetByWorkItemId(string workItemId);
+ }
+}
\ No newline at end of file
diff --git a/AzureDevopsStateTracker/Interfaces/IWorkItemAdapter.cs b/AzureDevopsStateTracker/Interfaces/IWorkItemAdapter.cs
new file mode 100644
index 0000000..5fb5db8
--- /dev/null
+++ b/AzureDevopsStateTracker/Interfaces/IWorkItemAdapter.cs
@@ -0,0 +1,12 @@
+using AzureDevopsStateTracker.DTOs;
+using AzureDevopsStateTracker.Entities;
+using System.Collections.Generic;
+
+namespace AzureDevopsStateTracker.Interfaces
+{
+ public interface IWorkItemAdapter
+ {
+ WorkItemDTO ToWorkItemDTO(WorkItem workItem);
+ List ToWorkItemsDTO(List workItems);
+ }
+}
\ No newline at end of file
diff --git a/AzureDevopsStateTracker/Interfaces/Internals/IRepository.cs b/AzureDevopsStateTracker/Interfaces/Internals/IRepository.cs
new file mode 100644
index 0000000..adbfb76
--- /dev/null
+++ b/AzureDevopsStateTracker/Interfaces/Internals/IRepository.cs
@@ -0,0 +1,18 @@
+using AzureDevopsStateTracker.Entities;
+using System;
+using System.Collections.Generic;
+
+namespace AzureDevopsStateTracker.Interfaces.Internals
+{
+ public interface IRepository : IDisposable where TEntity : Entity
+ {
+ void Add(TEntity entity);
+ void Add(IEnumerable entities);
+ TEntity GetById(string id);
+ bool Exist(string id);
+ void Update(TEntity entity);
+ void Update(IEnumerable entities);
+ void Delete(TEntity entity);
+ void SaveChanges();
+ }
+}
\ No newline at end of file
diff --git a/AzureDevopsStateTracker/Interfaces/Internals/IWorkItemRepository.cs b/AzureDevopsStateTracker/Interfaces/Internals/IWorkItemRepository.cs
new file mode 100644
index 0000000..82e93b9
--- /dev/null
+++ b/AzureDevopsStateTracker/Interfaces/Internals/IWorkItemRepository.cs
@@ -0,0 +1,13 @@
+using AzureDevopsStateTracker.Entities;
+using System.Collections.Generic;
+
+namespace AzureDevopsStateTracker.Interfaces.Internals
+{
+ public interface IWorkItemRepository : IRepository
+ {
+ WorkItem GetByWorkItemId(string workItemId);
+ IEnumerable ListByWorkItemId(IEnumerable workItemsId);
+ IEnumerable ListByIterationPath(string iterationPath);
+ void RemoveAllTimeByState(List timeByStates);
+ }
+}
\ No newline at end of file
diff --git a/AzureDevopsStateTracker/Migrations/20210719163846_AzureDevopsStateTrackerInitial.Designer.cs b/AzureDevopsStateTracker/Migrations/20210719163846_AzureDevopsStateTrackerInitial.Designer.cs
new file mode 100644
index 0000000..b7fa1cf
--- /dev/null
+++ b/AzureDevopsStateTracker/Migrations/20210719163846_AzureDevopsStateTrackerInitial.Designer.cs
@@ -0,0 +1,154 @@
+//
+using System;
+using AzureDevopsStateTracker.Data;
+using AzureDevopsStateTracker.Data.Context;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+namespace AzureDevopsStateTracker.Migrations
+{
+ [DbContext(typeof(AzureDevopsStateTrackerContext))]
+ [Migration("20210719163846_AzureDevopsStateTrackerInitial")]
+ partial class AzureDevopsStateTrackerInitial
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasDefaultSchema(DataBaseConfig.SchemaName)
+ .HasAnnotation("ProductVersion", "3.1.16")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128)
+ .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
+
+ modelBuilder.Entity("AzureDevopsStateTracker.Entities.TimeByState", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("varchar(200)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("State")
+ .HasColumnType("varchar(200)");
+
+ b.Property("TotalTime")
+ .HasColumnType("float");
+
+ b.Property("TotalWorkedTime")
+ .HasColumnType("float");
+
+ b.Property("WorkItemId")
+ .HasColumnType("varchar(200)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("WorkItemId");
+
+ b.ToTable("TimeByStates");
+ });
+
+ modelBuilder.Entity("AzureDevopsStateTracker.Entities.WorkItem", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("varchar(200)");
+
+ b.Property("Activity")
+ .HasColumnType("varchar(200)");
+
+ b.Property("AreaPath")
+ .HasColumnType("varchar(200)");
+
+ b.Property("AssignedTo")
+ .HasColumnType("varchar(200)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedBy")
+ .HasColumnType("varchar(200)");
+
+ b.Property("Effort")
+ .HasColumnType("varchar(200)");
+
+ b.Property("IterationPath")
+ .HasColumnType("varchar(200)");
+
+ b.Property("OriginalEstimate")
+ .HasColumnType("varchar(200)");
+
+ b.Property("StoryPoints")
+ .HasColumnType("varchar(200)");
+
+ b.Property("Tags")
+ .HasColumnType("varchar(200)");
+
+ b.Property("TeamProject")
+ .HasColumnType("varchar(200)");
+
+ b.Property("Title")
+ .HasColumnType("varchar(200)");
+
+ b.Property("Type")
+ .HasColumnType("varchar(200)");
+
+ b.Property("WorkItemParentId")
+ .HasColumnType("varchar(200)");
+
+ b.HasKey("Id");
+
+ b.ToTable("WorkItems");
+ });
+
+ modelBuilder.Entity("AzureDevopsStateTracker.Entities.WorkItemChange", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("varchar(200)");
+
+ b.Property("ChangedBy")
+ .HasColumnType("varchar(200)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("NewDate")
+ .HasColumnType("datetime2");
+
+ b.Property("NewState")
+ .HasColumnType("varchar(200)");
+
+ b.Property("OldDate")
+ .HasColumnType("datetime2");
+
+ b.Property("OldState")
+ .HasColumnType("varchar(200)");
+
+ b.Property("WorkItemId")
+ .HasColumnType("varchar(200)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("WorkItemId");
+
+ b.ToTable("WorkItemsChange");
+ });
+
+ modelBuilder.Entity("AzureDevopsStateTracker.Entities.TimeByState", b =>
+ {
+ b.HasOne("AzureDevopsStateTracker.Entities.WorkItem", "WorkItem")
+ .WithMany("TimeByStates")
+ .HasForeignKey("WorkItemId");
+ });
+
+ modelBuilder.Entity("AzureDevopsStateTracker.Entities.WorkItemChange", b =>
+ {
+ b.HasOne("AzureDevopsStateTracker.Entities.WorkItem", "WorkItem")
+ .WithMany("WorkItemsChanges")
+ .HasForeignKey("WorkItemId");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/AzureDevopsStateTracker/Migrations/20210719163846_AzureDevopsStateTrackerInitial.cs b/AzureDevopsStateTracker/Migrations/20210719163846_AzureDevopsStateTrackerInitial.cs
new file mode 100644
index 0000000..de24b1a
--- /dev/null
+++ b/AzureDevopsStateTracker/Migrations/20210719163846_AzureDevopsStateTrackerInitial.cs
@@ -0,0 +1,118 @@
+using System;
+using AzureDevopsStateTracker.Data;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace AzureDevopsStateTracker.Migrations
+{
+ public partial class AzureDevopsStateTrackerInitial : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.EnsureSchema(
+ name: DataBaseConfig.SchemaName);
+
+ migrationBuilder.CreateTable(
+ name: "WorkItems",
+ schema: DataBaseConfig.SchemaName,
+ columns: table => new
+ {
+ Id = table.Column(type: "varchar(200)", nullable: false),
+ CreatedAt = table.Column(nullable: false),
+ AreaPath = table.Column(type: "varchar(200)", nullable: true),
+ TeamProject = table.Column(type: "varchar(200)", nullable: true),
+ IterationPath = table.Column(type: "varchar(200)", nullable: true),
+ AssignedTo = table.Column(type: "varchar(200)", nullable: true),
+ Type = table.Column(type: "varchar(200)", nullable: true),
+ CreatedBy = table.Column(type: "varchar(200)", nullable: true),
+ Title = table.Column(type: "varchar(200)", nullable: true),
+ Tags = table.Column(type: "varchar(200)", nullable: true),
+ Effort = table.Column(type: "varchar(200)", nullable: true),
+ OriginalEstimate = table.Column(type: "varchar(200)", nullable: true),
+ StoryPoints = table.Column(type: "varchar(200)", nullable: true),
+ WorkItemParentId = table.Column(type: "varchar(200)", nullable: true),
+ Activity = table.Column(type: "varchar(200)", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_WorkItems", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "TimeByStates",
+ schema: DataBaseConfig.SchemaName,
+ columns: table => new
+ {
+ Id = table.Column(type: "varchar(200)", nullable: false),
+ CreatedAt = table.Column(nullable: false),
+ WorkItemId = table.Column(type: "varchar(200)", nullable: true),
+ State = table.Column(type: "varchar(200)", nullable: true),
+ TotalTime = table.Column(nullable: false),
+ TotalWorkedTime = table.Column(nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_TimeByStates", x => x.Id);
+ table.ForeignKey(
+ name: "FK_TimeByStates_WorkItems_WorkItemId",
+ column: x => x.WorkItemId,
+ principalSchema: DataBaseConfig.SchemaName,
+ principalTable: "WorkItems",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Restrict);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "WorkItemsChange",
+ schema: DataBaseConfig.SchemaName,
+ columns: table => new
+ {
+ Id = table.Column(type: "varchar(200)", nullable: false),
+ CreatedAt = table.Column(nullable: false),
+ WorkItemId = table.Column(type: "varchar(200)", nullable: true),
+ NewDate = table.Column(nullable: false),
+ OldDate = table.Column(nullable: true),
+ NewState = table.Column(type: "varchar(200)", nullable: true),
+ OldState = table.Column(type: "varchar(200)", nullable: true),
+ ChangedBy = table.Column(type: "varchar(200)", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_WorkItemsChange", x => x.Id);
+ table.ForeignKey(
+ name: "FK_WorkItemsChange_WorkItems_WorkItemId",
+ column: x => x.WorkItemId,
+ principalSchema: DataBaseConfig.SchemaName,
+ principalTable: "WorkItems",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Restrict);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_TimeByStates_WorkItemId",
+ schema: DataBaseConfig.SchemaName,
+ table: "TimeByStates",
+ column: "WorkItemId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_WorkItemsChange_WorkItemId",
+ schema: DataBaseConfig.SchemaName,
+ table: "WorkItemsChange",
+ column: "WorkItemId");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "TimeByStates",
+ schema: DataBaseConfig.SchemaName);
+
+ migrationBuilder.DropTable(
+ name: "WorkItemsChange",
+ schema: DataBaseConfig.SchemaName);
+
+ migrationBuilder.DropTable(
+ name: "WorkItems",
+ schema: DataBaseConfig.SchemaName);
+ }
+ }
+}
diff --git a/AzureDevopsStateTracker/Migrations/AzureDevopsStateTrackerContextModelSnapshot.cs b/AzureDevopsStateTracker/Migrations/AzureDevopsStateTrackerContextModelSnapshot.cs
new file mode 100644
index 0000000..7baa9b1
--- /dev/null
+++ b/AzureDevopsStateTracker/Migrations/AzureDevopsStateTrackerContextModelSnapshot.cs
@@ -0,0 +1,152 @@
+//
+using System;
+using AzureDevopsStateTracker.Data;
+using AzureDevopsStateTracker.Data.Context;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+namespace AzureDevopsStateTracker.Migrations
+{
+ [DbContext(typeof(AzureDevopsStateTrackerContext))]
+ partial class AzureDevopsStateTrackerContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasDefaultSchema(DataBaseConfig.SchemaName)
+ .HasAnnotation("ProductVersion", "3.1.16")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128)
+ .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
+
+ modelBuilder.Entity("AzureDevopsStateTracker.Entities.TimeByState", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("varchar(200)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("State")
+ .HasColumnType("varchar(200)");
+
+ b.Property("TotalTime")
+ .HasColumnType("float");
+
+ b.Property("TotalWorkedTime")
+ .HasColumnType("float");
+
+ b.Property("WorkItemId")
+ .HasColumnType("varchar(200)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("WorkItemId");
+
+ b.ToTable("TimeByStates");
+ });
+
+ modelBuilder.Entity("AzureDevopsStateTracker.Entities.WorkItem", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("varchar(200)");
+
+ b.Property("Activity")
+ .HasColumnType("varchar(200)");
+
+ b.Property("AreaPath")
+ .HasColumnType("varchar(200)");
+
+ b.Property("AssignedTo")
+ .HasColumnType("varchar(200)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedBy")
+ .HasColumnType("varchar(200)");
+
+ b.Property("Effort")
+ .HasColumnType("varchar(200)");
+
+ b.Property("IterationPath")
+ .HasColumnType("varchar(200)");
+
+ b.Property("OriginalEstimate")
+ .HasColumnType("varchar(200)");
+
+ b.Property("StoryPoints")
+ .HasColumnType("varchar(200)");
+
+ b.Property("Tags")
+ .HasColumnType("varchar(200)");
+
+ b.Property("TeamProject")
+ .HasColumnType("varchar(200)");
+
+ b.Property("Title")
+ .HasColumnType("varchar(200)");
+
+ b.Property("Type")
+ .HasColumnType("varchar(200)");
+
+ b.Property("WorkItemParentId")
+ .HasColumnType("varchar(200)");
+
+ b.HasKey("Id");
+
+ b.ToTable("WorkItems");
+ });
+
+ modelBuilder.Entity("AzureDevopsStateTracker.Entities.WorkItemChange", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("varchar(200)");
+
+ b.Property("ChangedBy")
+ .HasColumnType("varchar(200)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("NewDate")
+ .HasColumnType("datetime2");
+
+ b.Property("NewState")
+ .HasColumnType("varchar(200)");
+
+ b.Property("OldDate")
+ .HasColumnType("datetime2");
+
+ b.Property("OldState")
+ .HasColumnType("varchar(200)");
+
+ b.Property("WorkItemId")
+ .HasColumnType("varchar(200)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("WorkItemId");
+
+ b.ToTable("WorkItemsChange");
+ });
+
+ modelBuilder.Entity("AzureDevopsStateTracker.Entities.TimeByState", b =>
+ {
+ b.HasOne("AzureDevopsStateTracker.Entities.WorkItem", "WorkItem")
+ .WithMany("TimeByStates")
+ .HasForeignKey("WorkItemId");
+ });
+
+ modelBuilder.Entity("AzureDevopsStateTracker.Entities.WorkItemChange", b =>
+ {
+ b.HasOne("AzureDevopsStateTracker.Entities.WorkItem", "WorkItem")
+ .WithMany("WorkItemsChanges")
+ .HasForeignKey("WorkItemId");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/AzureDevopsStateTracker/Services/AzureDevopsStateTrackerService.cs b/AzureDevopsStateTracker/Services/AzureDevopsStateTrackerService.cs
new file mode 100644
index 0000000..7b1f699
--- /dev/null
+++ b/AzureDevopsStateTracker/Services/AzureDevopsStateTrackerService.cs
@@ -0,0 +1,129 @@
+using AzureDevopsStateTracker.DTOs;
+using AzureDevopsStateTracker.DTOs.Create;
+using AzureDevopsStateTracker.DTOs.Update;
+using AzureDevopsStateTracker.Entities;
+using AzureDevopsStateTracker.Extensions;
+using AzureDevopsStateTracker.Interfaces;
+using AzureDevopsStateTracker.Interfaces.Internals;
+using System;
+using System.Linq;
+
+namespace AzureDevopsStateTracker.Services
+{
+ public class AzureDevopsStateTrackerService : IAzureDevopsStateTrackerService
+ {
+ public readonly IWorkItemRepository _workItemRepository;
+ public readonly IWorkItemAdapter _workItemAdapter;
+
+ public AzureDevopsStateTrackerService(
+ IWorkItemAdapter workItemAdapter, IWorkItemRepository workItemRepository)
+ {
+ _workItemAdapter = workItemAdapter;
+ _workItemRepository = workItemRepository;
+ }
+
+ public void Create(CreateWorkItemDTO create)
+ {
+ var workItem = new WorkItem(create.Resource.Id);
+
+ workItem.Update(create.Resource.Fields.Title,
+ create.Resource.Fields.TeamProject,
+ create.Resource.Fields.AreaPath,
+ create.Resource.Fields.IterationPath,
+ create.Resource.Fields.Type,
+ create.Resource.Fields.CreatedBy.ExtractEmail(),
+ create.Resource.Fields.AssignedTo.ExtractEmail(),
+ create.Resource.Fields.Tags,
+ create.Resource.Fields.Parent,
+ create.Resource.Fields.Effort,
+ create.Resource.Fields.StoryPoints,
+ create.Resource.Fields.OriginalEstimate,
+ create.Resource.Fields.Activity);
+
+ AddWorkItemChange(workItem, create);
+
+ _workItemRepository.Add(workItem);
+ _workItemRepository.SaveChanges();
+ }
+
+ public void Update(UpdatedWorkItemDTO update)
+ {
+ var workItem = _workItemRepository.GetByWorkItemId(update.Resource.WorkItemId);
+ if (workItem is null)
+ return;
+
+ workItem.Update(update.Resource.Revision.Fields.Title,
+ update.Resource.Revision.Fields.TeamProject,
+ update.Resource.Revision.Fields.AreaPath,
+ update.Resource.Revision.Fields.IterationPath,
+ update.Resource.Revision.Fields.Type,
+ update.Resource.Revision.Fields.CreatedBy,
+ update.Resource.Revision.Fields.AssignedTo,
+ update.Resource.Revision.Fields.Tags,
+ update.Resource.Revision.Fields.Parent,
+ update.Resource.Revision.Fields.Effort,
+ update.Resource.Revision.Fields.StoryPoints,
+ update.Resource.Revision.Fields.OriginalEstimate,
+ update.Resource.Revision.Fields.Activity);
+
+ AddWorkItemChange(workItem, update);
+
+ _workItemRepository.Update(workItem);
+ _workItemRepository.SaveChanges();
+ }
+
+ public WorkItemDTO GetByWorkItemId(string workItemId)
+ {
+ var workItem = _workItemRepository.GetByWorkItemId(workItemId);
+ if (workItem is null)
+ return null;
+
+ return _workItemAdapter.ToWorkItemDTO(workItem);
+ }
+
+ #region Support Methods
+ public WorkItemChange ToWorkItemChange(string workItemId, string changedBy, DateTime newDate, string newState, string oldState = null, DateTime? oldDate = null)
+ {
+ return new WorkItemChange(workItemId, changedBy.ExtractEmail(), newDate, newState, oldState, oldDate);
+ }
+
+ public void AddWorkItemChange(WorkItem workItem, CreateWorkItemDTO create)
+ {
+ var workItemChange = ToWorkItemChange(workItem.Id,
+ create.Resource.Fields.ChangedBy,
+ create.Resource.Fields.CreatedDate,
+ create.Resource.Fields.State);
+
+ workItem.AddWorkItemChange(workItemChange);
+ }
+
+ public void AddWorkItemChange(WorkItem workItem, UpdatedWorkItemDTO update)
+ {
+ var changedBy = update.Resource.Revision.Fields.ChangedBy ?? update.Resource.Fields.ChangedBy.NewValue;
+ var workItemChange = ToWorkItemChange(workItem.Id,
+ changedBy,
+ update.Resource.Fields.StateChangeDate.NewValue,
+ update.Resource.Fields.State.NewValue,
+ update.Resource.Fields.State.OldValue,
+ update.Resource.Fields.StateChangeDate.OldValue);
+
+ workItem.AddWorkItemChange(workItemChange);
+
+ UpdateTimeByStates(workItem);
+ }
+
+ public void UpdateTimeByStates(WorkItem workItem)
+ {
+ RemoveTimeByStateFromDataBase(workItem);
+
+ workItem.ClearTimesByState();
+ workItem.AddTimesByState(workItem.CalculateTotalTimeByState());
+ }
+
+ public void RemoveTimeByStateFromDataBase(WorkItem workItem)
+ {
+ _workItemRepository.RemoveAllTimeByState(workItem.TimeByStates.ToList());
+ }
+ #endregion
+ }
+}
\ No newline at end of file