Ver Fonte

Add project files.

Robert há 2 anos atrás
pai
commit
5f708f0d53

+ 25 - 0
CsvUploader.sln

@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.32510.428
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CsvUploader", "CsvUploader\CsvUploader.csproj", "{E8D59507-C254-48A7-B4A0-296DF3985C0A}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{E8D59507-C254-48A7-B4A0-296DF3985C0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{E8D59507-C254-48A7-B4A0-296DF3985C0A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{E8D59507-C254-48A7-B4A0-296DF3985C0A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{E8D59507-C254-48A7-B4A0-296DF3985C0A}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(ExtensibilityGlobals) = postSolution
+		SolutionGuid = {DE91569B-C947-448F-9550-D847D10B48EA}
+	EndGlobalSection
+EndGlobal

+ 6 - 0
CsvUploader/App.config

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<configuration>
+    <startup> 
+        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7" />
+    </startup>
+</configuration>

+ 186 - 0
CsvUploader/CsvUploadTask.cs

@@ -0,0 +1,186 @@
+using CsvUploader.Func;
+using CsvUploader.Model;
+using Dapper;
+using System;
+using System.Collections.Generic;
+using System.Data.SqlClient;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace CsvUploader
+{
+    class CsvUploadTask
+    {
+        public CsvUploadTask(Model.UploadTask uploadTaskSetting)
+        {
+            this._uploadTaskSetting = uploadTaskSetting;
+            _uploadRecordService = new UploadRecordService(uploadTaskSetting.Name);
+            _fileSystemWatcher = new FileSystemWatcher(uploadTaskSetting.Folder);
+            _fileSystemWatcher.Changed += _fileSystemWatcher_Changed;
+        }
+
+        public void Start()
+        {
+            _ = StartAsync();
+        }
+
+        public async Task StartAsync()
+        {
+            _fileSystemWatcher.EnableRaisingEvents = true;
+            InitialUploadTask = CheckAndUpload();
+        }
+
+        private readonly UploadTask _uploadTaskSetting;
+        private readonly UploadRecordService _uploadRecordService;
+        private readonly FileSystemWatcher _fileSystemWatcher;
+
+        private Task InitialUploadTask = null;
+        private List<string> unUploadFiles = new List<string>();
+
+        private async Task CheckAndUpload()
+        {
+            var files = Directory.GetFiles(_uploadTaskSetting.Folder, "*", SearchOption.AllDirectories);
+            foreach (var file in files)
+            {
+                await TryUploadFileRecord(file);
+            }
+        }
+
+        private async void _fileSystemWatcher_Changed(object sender, FileSystemEventArgs e)
+        {
+            if (e.ChangeType != WatcherChangeTypes.Changed)
+            {
+                return;
+            }
+
+            if (InitialUploadTask is null ||
+                !InitialUploadTask.IsCompleted)
+            {
+                unUploadFiles.Add(e.Name);
+                return;
+            }
+
+            foreach (var file in unUploadFiles)
+            {
+                await TryUploadFileRecord(e.Name);
+            }
+
+            //check file or folder
+            await TryUploadFileRecord(e.FullPath);
+        }
+
+        private async ValueTask TryUploadFileRecord(string file)
+        {
+            var fileType = Path.GetExtension(file);
+            if (fileType != ".csv")
+            {
+                return;
+            }
+
+            var fileName = Path.GetFileName(file);
+            var updateTime = File.GetLastWriteTime(file);
+            var uploadTime = _uploadRecordService.TryGetUploadDateTime(fileName);
+
+            if (uploadTime == null ||
+                uploadTime < updateTime)
+            {
+                try
+                {
+                    if (await UploadFileRecord(file))
+                    {
+                        _uploadRecordService.SetUploadDateTime(file, updateTime);
+                        Console.WriteLine($"File {fileName} record uploaded");
+                        return;
+                    }
+                    Console.WriteLine($"File {fileName} record upload FAIL");
+                }
+                catch(Exception e)
+                {
+                    //Console.WriteLine(e.StackTrace);
+                    Console.WriteLine(e.Message);
+                }
+            }
+        }
+
+        private async Task<bool> UploadFileRecord(string file)
+        {
+            UploadData data = ParseUploadData(file);
+            if (data is null)
+            {
+                throw new FormatException($"file {file} formate error");
+            }
+
+            try
+            {
+                string cmd = CreateSqlCmd(data);
+                using (var dbConn = new SqlConnection(_uploadTaskSetting.SqlConnectionString))
+                {
+                    dbConn.Open();
+                    var cnt = await dbConn.ExecuteAsync(cmd);
+
+                    return cnt > 0;
+                }
+            }
+            catch (Exception e)
+            {
+                Console.WriteLine(e.Message);
+                throw new Exception($"Task {_uploadTaskSetting.Name} upload fail", e);
+            }
+        }
+
+        private UploadData ParseUploadData(string file)
+        {
+            try
+            {
+                var toReturn = new UploadData();
+                var lines = File.ReadAllLines(file);
+                var csvPairs = new Dictionary<string, string>();
+
+                csvPairs.AddSimpleData(lines)
+                        .AddTablesData(_uploadTaskSetting.Tables)
+                        .CleanDataTail()
+                        .TryValueConvert(_uploadTaskSetting.IsPassToInt);
+
+                var columnNames = _uploadTaskSetting.CsvColumnNames.ToList();
+                columnNames = columnNames.ReplaceByRule(csvPairs).ToList();
+
+                UpdateUploadData(toReturn, csvPairs, columnNames);
+
+                return toReturn;
+            }
+            catch (IOException e)
+            {
+                throw new Exception($"File {file} in use", e);
+            }
+            catch (Exception e)
+            {
+                return null;
+            }
+        }
+
+        private void UpdateUploadData(UploadData toReturn, Dictionary<string, string> csvPairs, List<string> columnNames)
+        {
+            foreach (var pair in csvPairs)
+            {
+                if (columnNames.Contains(pair.Key))
+                {
+                    var index = columnNames.IndexOf(pair.Key);
+                    toReturn.ColumnNames.Add(_uploadTaskSetting.DbColumnNames[index]);
+                    toReturn.Datas.Add(pair.Value);
+                }
+            }
+        }
+
+        private string CreateSqlCmd(UploadData data)
+        {
+            string cmd = $"INSERT INTO {_uploadTaskSetting.TableName} (";
+            cmd += String.Join(", ", data.ColumnNames.Select(x => $"[{x}]"));
+            cmd += ") VALUES (";
+            cmd += String.Join(", ", data.Datas.Select(x => $"'{x}'"));
+            cmd += $")";
+            return cmd;
+        }
+    }
+}

+ 108 - 0
CsvUploader/CsvUploadTaskService.cs

@@ -0,0 +1,108 @@
+using CsvUploader.Model;
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace CsvUploader
+{
+    class CsvUploadTaskService
+    {
+        public const string TaskSettingFileName = "Task.ini";
+
+        public CsvUploadTaskService(string settingFileName = TaskSettingFileName)
+        {
+            if (!File.Exists(settingFileName))
+            {
+                throw new FileNotFoundException("Task setting file Not Found!", settingFileName, null);
+            }
+
+            var taskSetting = JsonConvert.DeserializeObject<UploadTask[]>(File.ReadAllText(settingFileName));
+
+            if (taskSetting is null)
+            {
+                throw new Exception("Task setting file Formate Error");
+            }
+
+            InitBase(taskSetting);
+        }
+
+        public CsvUploadTaskService(UploadTask[] uploadTaskSetting)
+        {
+            InitBase(uploadTaskSetting);
+        }
+
+        private void InitBase(UploadTask[] uploadTaskSetting)
+        {
+            if (uploadTaskSetting is null)
+            {
+                throw new NullReferenceException("Task setting should not be null");
+            }
+
+            CheckNullSetting(uploadTaskSetting);
+            CheckColumeMatch(uploadTaskSetting);
+
+            StartTasks(uploadTaskSetting);
+        }
+
+        private void StartTasks(UploadTask[] uploadTaskSetting)
+        {
+            foreach(var taskSetting in uploadTaskSetting)
+            {
+                var task = new CsvUploadTask(taskSetting);
+                task.Start();
+            }
+        }
+
+        private static void CheckNullSetting(UploadTask[] uploadTaskSetting)
+        {
+            for(int index = 0; index < uploadTaskSetting.Count(); index++)
+            {
+                var task = uploadTaskSetting[index];
+                if (string.IsNullOrEmpty(task.Name))
+                {
+                    throw new Exception($"Task NO,{index} Name should not be empty");
+                }
+
+                if (string.IsNullOrEmpty(task.Folder))
+                {
+                    throw new Exception($"Task {task.Name} file directory should not be empty");
+                }
+
+                if (!Directory.Exists(task.Folder))
+                {
+                    throw new Exception($"Task {task.Name} file directory not exist");
+                }
+
+                if (string.IsNullOrEmpty(task.SqlConnectionString))
+                {
+                    throw new Exception($"Task {task.Name} SqlConnectionString should not be empty");
+                }
+
+                //if (string.IsNullOrEmpty(task.IndexedColumnName))
+                //{
+                //    throw new Exception($"Task {task.Name} IndexedColumnName should not be empty");
+                //}
+
+                if (string.IsNullOrEmpty(task.TableName))
+                {
+                    throw new Exception($"Task {task.Name} TableName should not be empty");
+                }
+            }
+        }
+
+        private static void CheckColumeMatch(UploadTask[] uploadTaskSetting)
+        {
+            foreach(var task in uploadTaskSetting)
+            {
+                if (task.CsvColumnNames.Count() != task.DbColumnNames.Count())
+                {
+                    throw new Exception($"{task.Name} CsvColumnNames count should equal DbColumnNames");
+                }
+            }
+        }
+    }
+}

+ 75 - 0
CsvUploader/CsvUploader.csproj

@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProjectGuid>{E8D59507-C254-48A7-B4A0-296DF3985C0A}</ProjectGuid>
+    <OutputType>Exe</OutputType>
+    <RootNamespace>CsvUploader</RootNamespace>
+    <AssemblyName>CsvUploader</AssemblyName>
+    <TargetFrameworkVersion>v4.7</TargetFrameworkVersion>
+    <FileAlignment>512</FileAlignment>
+    <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
+    <Deterministic>true</Deterministic>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <PlatformTarget>AnyCPU</PlatformTarget>
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug\</OutputPath>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <PlatformTarget>AnyCPU</PlatformTarget>
+    <DebugType>pdbonly</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release\</OutputPath>
+    <DefineConstants>TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="Dapper, Version=2.0.0.0, Culture=neutral, processorArchitecture=MSIL">
+      <HintPath>..\packages\Dapper.2.0.123\lib\net461\Dapper.dll</HintPath>
+    </Reference>
+    <Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
+      <HintPath>..\packages\Newtonsoft.Json.13.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
+    </Reference>
+    <Reference Include="System" />
+    <Reference Include="System.Core" />
+    <Reference Include="System.Runtime.CompilerServices.Unsafe, Version=4.0.4.1, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
+      <HintPath>..\packages\System.Runtime.CompilerServices.Unsafe.4.5.3\lib\net461\System.Runtime.CompilerServices.Unsafe.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Threading.Tasks.Extensions, Version=4.2.0.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
+      <HintPath>..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Xml.Linq" />
+    <Reference Include="System.Data.DataSetExtensions" />
+    <Reference Include="Microsoft.CSharp" />
+    <Reference Include="System.Data" />
+    <Reference Include="System.Net.Http" />
+    <Reference Include="System.Xml" />
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="CsvUploadTask.cs" />
+    <Compile Include="CsvUploadTaskService.cs" />
+    <Compile Include="Func\Extentions.cs" />
+    <Compile Include="Model\CsvTable.cs" />
+    <Compile Include="Model\UploadData.cs" />
+    <Compile Include="Model\UploadedRecord.cs" />
+    <Compile Include="Model\UploadTask.cs" />
+    <Compile Include="Program.cs" />
+    <Compile Include="Properties\AssemblyInfo.cs" />
+    <Compile Include="UploadDataParser.cs" />
+    <Compile Include="UploadRecordService.cs" />
+  </ItemGroup>
+  <ItemGroup>
+    <None Include="App.config" />
+    <None Include="packages.config" />
+  </ItemGroup>
+  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+</Project>

+ 37 - 0
CsvUploader/Func/Extentions.cs

@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace CsvUploader.Func
+{
+    public static class Extentions
+    {
+        public static IEnumerable<string> ReplaceByRule(this IList<string> source,IDictionary<string,string> pairs)
+        {
+            var tmp = source.ToList();
+            foreach (var name in tmp)
+            {
+                //if (name.StartsWith("{") && name.EndsWith("}"))
+                if (name.Contains("{") && name.Contains("}"))
+                {
+                    var startIndex = name.IndexOf("{");
+                    var endIndex = name.IndexOf("}");
+                    var replaceTarget = name.Substring(startIndex, endIndex - startIndex + 1);
+                    var subName = replaceTarget.Substring(1, replaceTarget.Length - 2);
+
+                    if (pairs.Keys.Contains(subName))
+                    {
+                        var index = source.IndexOf(name);
+                        source.Remove(name);
+
+                        var tmpName = name.Replace(replaceTarget, pairs[subName].Trim(','));
+                        source.Insert(index, tmpName);
+                    }
+                }
+            }
+            return source;
+        }
+    }
+}

+ 15 - 0
CsvUploader/Model/CsvTable.cs

@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace CsvUploader.Model
+{
+    public class CsvTable
+    {
+        public string Name { get; set; }
+        public List<string> Keys { get; set; }
+        public List<string> Cols { get; set; }
+    }
+}

+ 16 - 0
CsvUploader/Model/UploadData.cs

@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace CsvUploader.Model
+{
+    class UploadData
+    {
+        //public string IndexedColumnName { get; set; }
+        //public string IndexColumnData { get; set; }
+        public List<string> ColumnNames { get; set; } = new List<string>();
+        public List<string> Datas { get; set; } = new List<string>();
+    }
+}

+ 21 - 0
CsvUploader/Model/UploadTask.cs

@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace CsvUploader.Model
+{
+    public class UploadTask
+    {
+        public string Name { get; set; }
+        public string Folder { get; set; }
+        public string SqlConnectionString { get; set; }
+        public string TableName { get; set; }
+        //public string IndexedColumnName { get; set; }
+        public List<string> CsvColumnNames { get; set; }
+        public List<string> DbColumnNames { get; set; }
+        public List<CsvTable> Tables { get; set; }
+        public bool IsPassToInt { get; set; }
+    }
+}

+ 15 - 0
CsvUploader/Model/UploadedRecord.cs

@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace CsvUploader.Model
+{
+    class UploadedRecord
+    {
+        public string TaskName { get; set; }
+        public List<string> FileNames { get; set; }
+        public List<DateTime> FileUploadDate { get; set; }
+    }
+}

+ 26 - 0
CsvUploader/Program.cs

@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace CsvUploader
+{
+    class Program
+    {
+        static void Main(string[] args)
+        {
+            try
+            {
+                var watchTask = new CsvUploadTaskService();
+                Console.WriteLine("UploadTask Start");
+            }
+            catch(Exception e)
+            {
+                Console.WriteLine(e.Message);
+                Console.WriteLine(e.StackTrace);
+            }
+            Console.Read();
+        }
+    }
+}

+ 36 - 0
CsvUploader/Properties/AssemblyInfo.cs

@@ -0,0 +1,36 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("CsvUploader")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("CsvUploader")]
+[assembly: AssemblyCopyright("Copyright ©  2022")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components.  If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("e8d59507-c254-48a7-b4a0-296df3985c0a")]
+
+// Version information for an assembly consists of the following four values:
+//
+//      Major Version
+//      Minor Version
+//      Build Number
+//      Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]

+ 160 - 0
CsvUploader/UploadDataParser.cs

@@ -0,0 +1,160 @@
+using CsvUploader.Func;
+using CsvUploader.Model;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace CsvUploader
+{
+    static class UploadDataParserExt
+    {
+        public static Dictionary<string, string> AddSimpleData(this Dictionary<string, string> toPairs, IEnumerable<string> dataLines)
+        {
+            foreach (var line in dataLines)
+            {
+                var rowDatas = line.Split(new[] { ',' }, 2);
+                toPairs[rowDatas[0]] = rowDatas[1];
+            }
+            return toPairs;
+        }
+
+        public static Dictionary<string, string> AddTablesData(this Dictionary<string, string> toPairs, List<CsvTable> tables)
+        {
+            if (tables is null || tables.Count == 0)
+            {
+                return toPairs;
+            }
+
+            foreach (var table in tables)
+            {
+                table.Keys = table.Keys.ReplaceByRule(toPairs).ToList();
+                table.Cols = table.Cols.ReplaceByRule(toPairs).ToList();
+            }
+
+            CheckKeysColeExist(toPairs, tables);
+            CheckColsColeExist(toPairs, tables);
+
+            foreach (var table in tables)
+            {
+                toPairs.AddTableData(table);
+            }
+
+            return toPairs;
+        }
+
+        public static Dictionary<string, string> CleanDataTail(this Dictionary<string, string> toPairs)
+        {
+            var keys = toPairs.Keys.ToList();
+            foreach(var key in keys)
+            {
+                toPairs[key] = toPairs[key].Trim(',');
+            }
+            return toPairs;
+        }
+
+        public static Dictionary<string,string> TryValueConvert(this Dictionary<string, string> toPairs, bool isPassToInt)
+        {
+            if (!isPassToInt)
+            {
+                return toPairs;
+            }
+
+            var keys = toPairs.Keys.ToList();
+            foreach (var key in keys)
+            {
+                if (toPairs[key].ToLower() == "pass")
+                {
+                    toPairs[key] = "1";
+                }
+                if (toPairs[key].ToLower() == "fail")
+                {
+                    toPairs[key] = "0";
+                }
+            }
+            return toPairs;
+        }
+
+        private static Dictionary<string, string> AddTableData(this Dictionary<string, string> toPairs, CsvTable table)
+        {
+            var keyCnt = table.Keys.Count;
+            var sampleKeyRow = toPairs[table.Keys[0]];
+            var dataCnt = sampleKeyRow.Count(x => x == ',') + 1;
+
+            string[][] keyTable = new string[dataCnt][];
+            for(int index = 0;index < keyTable.Length; index ++)
+            {
+                keyTable[index] = new string[keyCnt];
+            }
+
+            for (int keyIndex = 0; keyIndex < table.Keys.Count; keyIndex++)
+            {
+                var keyRow = toPairs[table.Keys[keyIndex]];
+                var keyDatas = keyRow.Split(',');
+                for (int keyDataIndex = 0; keyDataIndex < keyDatas.Count(); keyDataIndex++)
+                {
+                    keyTable[keyDataIndex][keyIndex] = keyDatas[keyDataIndex];
+                }
+            }
+
+            var keyList = keyTable.Select(x => string.Join(".", x)).ToList();
+
+            foreach (var col in table.Cols)
+            {
+                toPairs.AddTableColData(keyList, col);
+            }
+            return toPairs;
+        }
+
+        private static Dictionary<string, string> AddTableColData(this Dictionary<string, string> toPairs, List<string> keyList, string colName)
+        {
+            var colRow = toPairs[colName];
+            var colDatas = colRow.Split(',');
+
+            if (keyList.Count != colDatas.Length)
+            {
+                throw new Exception("key and data length not match");
+            }
+
+            for (int index = 0; index < colDatas.Length; index++)
+            {
+                var key = keyList[index];
+                var generatedKey = $"{key}.{colName}";
+                toPairs[generatedKey] = colDatas[index];
+            }
+
+            return toPairs;
+        }
+
+        private static void CheckKeysColeExist(Dictionary<string, string> toPairs, List<CsvTable> tables)
+        {
+            var keys = toPairs.Keys;
+            foreach(var table in tables)
+            {
+                foreach (var key in table.Keys)
+                {
+                    if (!keys.Contains(key))
+                    {
+                        throw new Exception($"{table.Name} with key {key} not found!");
+                    }
+                }
+            }
+        }
+
+        private static void CheckColsColeExist(Dictionary<string, string> toPairs, List<CsvTable> tables)
+        {
+            var keys = toPairs.Keys;
+            foreach(var table in tables)
+            {
+                foreach (var col in table.Cols)
+                {
+                    if (!keys.Contains(col))
+                    {
+                        throw new Exception($"{table.Name} with col {col} not found!");
+                    }
+                }
+            }
+        }
+    }
+}

+ 83 - 0
CsvUploader/UploadRecordService.cs

@@ -0,0 +1,83 @@
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace CsvUploader
+{
+    class UploadRecordService
+    {
+        public const string UploadedRecord = "record";
+        private string _taskName;
+        private Model.UploadedRecord _record;
+        private List<Model.UploadedRecord> _records;
+
+        public UploadRecordService(string taskName)
+        {
+            if (string.IsNullOrEmpty(taskName))
+            {
+                throw new Exception("task Name should not be empty");
+            }
+
+            if (!File.Exists(UploadedRecord))
+            {
+                var stream = File.CreateText(UploadedRecord);
+                stream.Write("[]");
+                stream.Close();
+                stream.Dispose();
+            }
+
+            var records = JsonConvert.DeserializeObject<List<Model.UploadedRecord>>(File.ReadAllText(UploadedRecord));
+
+            if (records is null)
+            {
+                throw new Exception($"File {UploadedRecord} formate error");
+            }
+
+            var record = records.FirstOrDefault(x => x.TaskName == taskName);
+            if (record == null)
+            {
+                record = new Model.UploadedRecord() {
+                    TaskName = taskName,
+                    FileNames = new List<string>(), 
+                    FileUploadDate = new List<DateTime>()
+                };
+                records.Add(record);
+            }
+
+            _taskName = taskName;
+            this._record = record;
+            _records = records;
+        }
+
+        public DateTime? TryGetUploadDateTime(string fileName)
+        {
+            if (_record.FileNames.Contains(fileName))
+            {
+                var index = _record.FileNames.IndexOf(fileName);
+                return _record.FileUploadDate[index];
+            }
+            return null;
+        }
+
+        public void SetUploadDateTime(string fileName,DateTime dateTime)
+        {
+            fileName = Path.GetFileName(fileName);
+            if (_record.FileNames.Contains(fileName))
+            {
+                var index = _record.FileNames.IndexOf(fileName);
+                _record.FileUploadDate[index] = dateTime;
+
+                return;
+            }
+
+            _record.FileNames.Add(fileName);
+            _record.FileUploadDate.Add(dateTime);
+
+            File.WriteAllText(UploadedRecord, JsonConvert.SerializeObject(_records));
+        }
+    }
+}

+ 7 - 0
CsvUploader/packages.config

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+  <package id="Dapper" version="2.0.123" targetFramework="net47" />
+  <package id="Newtonsoft.Json" version="13.0.2" targetFramework="net47" />
+  <package id="System.Runtime.CompilerServices.Unsafe" version="4.5.3" targetFramework="net47" />
+  <package id="System.Threading.Tasks.Extensions" version="4.5.4" targetFramework="net47" />
+</packages>