├── .gitignore ├── AddingCodeRunCsx.PNG ├── CreateFunctionApp.PNG ├── CreateNewFunction.PNG ├── FinalFunction.PNG ├── HttpTriggerWithAuthz.PNG ├── LICENSE ├── README.md └── code ├── FunctionConfig.json ├── Project.json └── run.csx /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # MSTest test Results 33 | [Tt]est[Rr]esult*/ 34 | [Bb]uild[Ll]og.* 35 | 36 | # NUNIT 37 | *.VisualState.xml 38 | TestResult.xml 39 | 40 | # Build Results of an ATL Project 41 | [Dd]ebugPS/ 42 | [Rr]eleasePS/ 43 | dlldata.c 44 | 45 | # .NET Core 46 | project.lock.json 47 | project.fragment.lock.json 48 | artifacts/ 49 | **/Properties/launchSettings.json 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # Visual Studio code coverage results 117 | *.coverage 118 | *.coveragexml 119 | 120 | # NCrunch 121 | _NCrunch_* 122 | .*crunch*.local.xml 123 | nCrunchTemp_* 124 | 125 | # MightyMoose 126 | *.mm.* 127 | AutoTest.Net/ 128 | 129 | # Web workbench (sass) 130 | .sass-cache/ 131 | 132 | # Installshield output folder 133 | [Ee]xpress/ 134 | 135 | # DocProject is a documentation generator add-in 136 | DocProject/buildhelp/ 137 | DocProject/Help/*.HxT 138 | DocProject/Help/*.HxC 139 | DocProject/Help/*.hhc 140 | DocProject/Help/*.hhk 141 | DocProject/Help/*.hhp 142 | DocProject/Help/Html2 143 | DocProject/Help/html 144 | 145 | # Click-Once directory 146 | publish/ 147 | 148 | # Publish Web Output 149 | *.[Pp]ublish.xml 150 | *.azurePubxml 151 | # TODO: Comment the next line if you want to checkin your web deploy settings 152 | # but database connection strings (with potential passwords) will be unencrypted 153 | *.pubxml 154 | *.publishproj 155 | 156 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 157 | # checkin your Azure Web App publish settings, but sensitive information contained 158 | # in these scripts will be unencrypted 159 | PublishScripts/ 160 | 161 | # NuGet Packages 162 | *.nupkg 163 | # The packages folder can be ignored because of Package Restore 164 | **/packages/* 165 | # except build/, which is used as an MSBuild target. 166 | !**/packages/build/ 167 | # Uncomment if necessary however generally it will be regenerated when needed 168 | #!**/packages/repositories.config 169 | # NuGet v3's project.json files produces more ignorable files 170 | *.nuget.props 171 | *.nuget.targets 172 | 173 | # Microsoft Azure Build Output 174 | csx/ 175 | *.build.csdef 176 | 177 | # Microsoft Azure Emulator 178 | ecf/ 179 | rcf/ 180 | 181 | # Windows Store app package directories and files 182 | AppPackages/ 183 | BundleArtifacts/ 184 | Package.StoreAssociation.xml 185 | _pkginfo.txt 186 | 187 | # Visual Studio cache files 188 | # files ending in .cache can be ignored 189 | *.[Cc]ache 190 | # but keep track of directories ending in .cache 191 | !*.[Cc]ache/ 192 | 193 | # Others 194 | ClientBin/ 195 | ~$* 196 | *~ 197 | *.dbmdl 198 | *.dbproj.schemaview 199 | *.jfm 200 | *.pfx 201 | *.publishsettings 202 | orleans.codegen.cs 203 | 204 | # Since there are multiple workflows, uncomment next line to ignore bower_components 205 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 206 | #bower_components/ 207 | 208 | # RIA/Silverlight projects 209 | Generated_Code/ 210 | 211 | # Backup & report files from converting an old project file 212 | # to a newer Visual Studio version. Backup files are not needed, 213 | # because we have git ;-) 214 | _UpgradeReport_Files/ 215 | Backup*/ 216 | UpgradeLog*.XML 217 | UpgradeLog*.htm 218 | 219 | # SQL Server files 220 | *.mdf 221 | *.ldf 222 | *.ndf 223 | 224 | # Business Intelligence projects 225 | *.rdl.data 226 | *.bim.layout 227 | *.bim_*.settings 228 | 229 | # Microsoft Fakes 230 | FakesAssemblies/ 231 | 232 | # GhostDoc plugin setting file 233 | *.GhostDoc.xml 234 | 235 | # Node.js Tools for Visual Studio 236 | .ntvs_analysis.dat 237 | node_modules/ 238 | 239 | # Typescript v1 declaration files 240 | typings/ 241 | 242 | # Visual Studio 6 build log 243 | *.plg 244 | 245 | # Visual Studio 6 workspace options file 246 | *.opt 247 | 248 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 249 | *.vbw 250 | 251 | # Visual Studio LightSwitch build output 252 | **/*.HTMLClient/GeneratedArtifacts 253 | **/*.DesktopClient/GeneratedArtifacts 254 | **/*.DesktopClient/ModelManifest.xml 255 | **/*.Server/GeneratedArtifacts 256 | **/*.Server/ModelManifest.xml 257 | _Pvt_Extensions 258 | 259 | # Paket dependency manager 260 | .paket/paket.exe 261 | paket-files/ 262 | 263 | # FAKE - F# Make 264 | .fake/ 265 | 266 | # JetBrains Rider 267 | .idea/ 268 | *.sln.iml 269 | 270 | # CodeRush 271 | .cr/ 272 | 273 | # Python Tools for Visual Studio (PTVS) 274 | __pycache__/ 275 | *.pyc 276 | 277 | # Cake - Uncomment if you are using it 278 | # tools/** 279 | # !tools/packages.config 280 | 281 | # Telerik's JustMock configuration file 282 | *.jmconfig 283 | 284 | # BizTalk build output 285 | *.btp.cs 286 | *.btm.cs 287 | *.odx.cs 288 | *.xsd.cs 289 | -------------------------------------------------------------------------------- /AddingCodeRunCsx.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/functions-dotnet-migrating-console-apps/8908152fb47fadec47dcb3e84c37daa7fee556ed/AddingCodeRunCsx.PNG -------------------------------------------------------------------------------- /CreateFunctionApp.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/functions-dotnet-migrating-console-apps/8908152fb47fadec47dcb3e84c37daa7fee556ed/CreateFunctionApp.PNG -------------------------------------------------------------------------------- /CreateNewFunction.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/functions-dotnet-migrating-console-apps/8908152fb47fadec47dcb3e84c37daa7fee556ed/CreateNewFunction.PNG -------------------------------------------------------------------------------- /FinalFunction.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/functions-dotnet-migrating-console-apps/8908152fb47fadec47dcb3e84c37daa7fee556ed/FinalFunction.PNG -------------------------------------------------------------------------------- /HttpTriggerWithAuthz.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/functions-dotnet-migrating-console-apps/8908152fb47fadec47dcb3e84c37daa7fee556ed/HttpTriggerWithAuthz.PNG -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | languages: 3 | - csharp 4 | products: 5 | - azure 6 | - azure-functions 7 | page_type: sample 8 | description: "Here is an easy way of getting any .exe running as a web service." 9 | --- 10 | 11 | # Run Console Apps on Azure Functions 12 | 13 | tldr: 14 | Here is an easy way of getting any .exe running as a web service. You just specify the input parameters to your exe in a configuration file. You can use binary files as inputs to the exe by specifying a URL to download it from. 15 | 16 | More details: 17 | This sample is a generic function (.csx file) that can be used to convert **any console application** to an HTTP **webservice** in Azure Functions. All you have to do is edit a configuration file and specify what input parameters will be passed as arguments to the .exe. 18 | 19 | Azure functions is an offering from Microsoft that allows you to create serverless "compute on demand" applications. 20 | 21 | ## Features 22 | - You can use binary files as input (specify the URL and the binary file will be downloaded to a temporary location on the virtual environment hosting your Azure Function) 23 | - You choose which file is sent back to the user as output 24 | - Here's what the configuration file looks like (more details later) 25 | 26 | ```json 27 | { 28 | "name": "consoleAppToFunctions", 29 | "input": { 30 | "command": "ffmpeg.exe -i {inputFile} {output1}", 31 | "arguments": { 32 | "inputFile": { 33 | "url": "https://1drv.ms/v/" 34 | //binary file input 35 | }, 36 | "output1": { 37 | "localfile": "out.mp3" 38 | //stored in a temp folder 39 | } 40 | } 41 | }, 42 | "output": { 43 | "folder": "outputFolder", 44 | "binaryFile": { 45 | "returnFile": "*.mp3", 46 | "returnFileName": "yourFile.mp3" 47 | } 48 | } 49 | } 50 | ``` 51 | 52 | ## Getting Started - Creating a new Function App 53 | 54 | 1. Login to - [Azure Portal](https://portal.azure.com) 55 | 2. Create a function app by specifying an App Name and storage account 56 | 57 | Create a Function App 58 | 59 | 3. Go to the function code editor and Create a New Function 60 | 61 | Creating a New Function 62 | 63 | 4. Select **HTTPTrigger - C#** and name your function **ConsoleAppToFunction** with the right **Authorization Level** 64 | 65 | Create an Http Trigger 66 | 67 | ## Adding Code 68 | > **TL;DR** 69 | If you don't care about the details, just go to your function's "Your Files > Upload Files" (top right) and upload [the 3 files in the code sample](https://github.com/Azure-Samples/functions-dotnet-migrating-console-apps/tree/master/code) along with the .exe (and .dll's it requires) 70 | 71 | The following are steps that you should follow if you want to fimiliarinze yourself with the using Azure functions interface. 72 | 73 | 1. Select the run.csx file under **View files** 74 | 75 | Adding code to run.csx 76 | 77 | 2. Replace the code in **Run.csx** with [ConsoleApp Function Code](https://github.com/Azure-Samples/functions-dotnet-migrating-console-apps/blob/master/code/run.csx?raw=true) 78 | 79 | 3. Since the code uses Json.Net, create a [Project.Json](https://github.com/Azure-Samples/functions-dotnet-migrating-console-apps/blob/master/code/Project.json?raw=true) file. 80 | 81 | ## Configuring the Sample 82 | We have now created a generic function that can run any console application. You can specify which console app to run in a file called **FunctionConfig.json** in the following example config we specify the function to run **FFMpeg.exe** 83 | 84 | ```json 85 | { 86 | "name": "consoleAppToFunctions", 87 | "input": { 88 | "command": "ffmpeg.exe -i {inputFile} {output1}", 89 | "arguments": { 90 | "inputFile": { 91 | "url": "https://1drv.ms/v/" 92 | }, 93 | "output1": { 94 | "localfile": "out.mp3" 95 | } 96 | } 97 | }, 98 | "output": { 99 | "folder": "outputFolder", 100 | "binaryFile": { 101 | "returnFile": "*.mp3", 102 | "returnFileName": "yourFile.mp3" 103 | } 104 | } 105 | } 106 | ``` 107 | 108 | We needed a way to pass binary input files to our function. For this we define an **argument** of type **url**, where we expect the user to upload a file to **Onedrive** (or whatever service that hosts a binary file) and provide the link in the query string. 109 | 110 | Once these changes are done, the function should have the following files as shown below: 111 | 112 | Final configuration 113 | 114 | ## Interacting with the function 115 | 1. Upload your test input file(s) to **OneDrive** 116 | 2. Get a link to the file using the OneDrive **Share** menu - something like https://1drv.ms/v/ 117 | 3. Interact with the function providing the inputs as querystring. Note that `inputFile` here is defined in the **FunctionConfig.json** file above 118 | 119 | Example URL: 120 | 121 | ```http 122 | https://[the URL when you click 'Get Function URL']&inputFile= 123 | ``` 124 | 125 | The function will process this request by invoking the specified console app and provide the output as a file download. 126 | -------------------------------------------------------------------------------- /code/FunctionConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "consoleAppToFunctions", 3 | "input": { 4 | "command": "ffmpeg.exe -i {inputFile} {output1}", 5 | "arguments": { 6 | "inputFile": { 7 | "url": "https://1drv.ms/v/s!AoC1hn1Q1aJCgqtQpZRcIEZvbitNHQ" 8 | }, 9 | "output1": { 10 | "localfile": "out.mp3" 11 | } 12 | } 13 | }, 14 | "output": { 15 | "folder": "outputFolder", 16 | "binaryFile": { 17 | "returnFile": "*.mp3", 18 | "returnFileName": "yourFile.mp3" 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /code/Project.json: -------------------------------------------------------------------------------- 1 | { 2 | "frameworks": { 3 | "net46":{ 4 | "dependencies": { 5 | "Newtonsoft.Json": "8.0.3" 6 | } 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /code/run.csx: -------------------------------------------------------------------------------- 1 | #r "Newtonsoft.Json" 2 | using System.Net; 3 | using Newtonsoft.Json; 4 | using System.IO; 5 | using System.Diagnostics; 6 | 7 | //Code from https://github.com/Azure-Samples/functions-dotnet-migrating-console-apps/edit/master/code/run.csx 8 | //Written by Ambrose http://github.com/efficientHacks and Murali http://github.com/muralivp 9 | 10 | public class ExeArg 11 | { 12 | public string Name { get; set; } 13 | public string Type { get; set; } 14 | public string Value { get; set; } 15 | } 16 | 17 | public static async Task Run(HttpRequestMessage req, TraceWriter log) 18 | { 19 | log.Info("C# HTTP trigger function processed a request."); 20 | 21 | string localPath = req.RequestUri.LocalPath; 22 | string functionName = localPath.Substring(localPath.LastIndexOf('/')+1); 23 | 24 | var json = File.ReadAllText(string.Format(@"D:\home\site\wwwroot\{0}\FunctionConfig.json",functionName)); 25 | 26 | var config = JsonConvert.DeserializeObject(json); 27 | 28 | var functionArguments = config.input.arguments; 29 | var localOutputFolder = Path.Combine(@"d:\home\data", config.output.folder.Value, Path.GetFileNameWithoutExtension(Path.GetTempFileName())); 30 | Directory.CreateDirectory(localOutputFolder); 31 | var workingDirectory = Path.Combine(@"d:\home\site\wwwroot", functionName + "\\bin"); 32 | Directory.SetCurrentDirectory(workingDirectory);//fun fact - the default working directory is d:\windows\system32 33 | 34 | var command = config.input.command.Value; 35 | 36 | var argList = new List(); 37 | 38 | //Parse the config file's arguments 39 | //handle file system, local file etc. and construct the input params for the actual calling of the .exe 40 | foreach (var arg in functionArguments) 41 | { 42 | var exeArg = new ExeArg(); 43 | exeArg.Name = arg.Name; 44 | var value = (Newtonsoft.Json.Linq.JObject)arg.Value; 45 | var property = (Newtonsoft.Json.Linq.JProperty)value.First; 46 | exeArg.Type = property.Name; 47 | exeArg.Value = property.Value.ToString(); 48 | 49 | var valueFromQueryString = await getValueFromQuery(req, exeArg.Name); 50 | 51 | log.Info("valueFromQueryString name=" + exeArg.Name); 52 | log.Info("valueFromQueryString val=" + valueFromQueryString); 53 | if(!string.IsNullOrEmpty(valueFromQueryString)) 54 | { 55 | exeArg.Value = valueFromQueryString; 56 | log.Info(exeArg.Name + " " + valueFromQueryString); 57 | } 58 | 59 | if(exeArg.Type.ToLower() == "localfile" || exeArg.Type.ToLower() == "localfolder") 60 | { 61 | exeArg.Value = Path.Combine(localOutputFolder, exeArg.Value); 62 | exeArg.Type = "string"; 63 | } 64 | if(string.IsNullOrEmpty(exeArg.Value)) 65 | { 66 | //throw exception here 67 | } 68 | argList.Add(exeArg); 69 | } 70 | 71 | //call the exe 72 | Dictionary paramList = ProcessParameters(argList, localOutputFolder); 73 | foreach (string parameter in paramList.Keys) 74 | { 75 | command = command.Replace(parameter, paramList[parameter]); 76 | } 77 | string commandName = command.Split(' ')[0]; 78 | string arguments = command.Split(new char[] { ' ' }, 2)[1]; 79 | log.Info("the command is " + command); 80 | log.Info("the working dir is " + workingDirectory); 81 | Process p = new Process(); 82 | p.StartInfo.UseShellExecute = false; 83 | p.StartInfo.RedirectStandardOutput = true; 84 | p.StartInfo.FileName = commandName; 85 | p.StartInfo.Arguments = arguments; 86 | p.Start(); 87 | string output = p.StandardOutput.ReadToEnd(); 88 | p.WaitForExit(); 89 | File.WriteAllText(localOutputFolder+"\\out.txt",output); 90 | 91 | //handle return file 92 | log.Info("handling return file localOutputFolder=" + localOutputFolder); 93 | string outputFile = config.output.binaryFile.returnFile.Value; 94 | string outputFileName = config.output.binaryFile.returnFileName.Value; 95 | var path = Directory.GetFiles(localOutputFolder, outputFile)[0]; 96 | 97 | log.Info("returning this file " + path); 98 | var result = new FileHttpResponseMessage(localOutputFolder); 99 | var stream = new FileStream(path, FileMode.Open, FileAccess.Read); 100 | result.Content = new StreamContent(stream); 101 | result.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream"); 102 | result.Content.Headers.ContentDisposition = new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment") 103 | { 104 | FileName = outputFileName 105 | }; 106 | 107 | return result; 108 | } 109 | 110 | private static Dictionary ProcessParameters(List arguments, string outputFolder) 111 | { 112 | Dictionary paramList = new Dictionary(); 113 | foreach (var arg in arguments) 114 | { 115 | switch (arg.Type) 116 | { 117 | case "url": 118 | string downloadedFileName = ProcessUrlType((string)arg.Value, outputFolder); 119 | paramList.Add("{" + arg.Name + "}", downloadedFileName); 120 | break; 121 | case "string": 122 | paramList.Add("{" + arg.Name + "}", arg.Value.ToString()); 123 | break; 124 | default: 125 | break; 126 | } 127 | } 128 | return paramList; 129 | } 130 | 131 | //you can modify this method to handle different URLs if necessary 132 | private static string ProcessUrlType(string url, string outputFolder) 133 | { 134 | Directory.CreateDirectory(outputFolder); 135 | string downloadedFile = Path.Combine(outputFolder, Path.GetFileName(Path.GetTempFileName())); 136 | //for oneDrive links 137 | HttpWebRequest webRequest = (HttpWebRequest)HttpWebRequest.Create(url); 138 | webRequest.AllowAutoRedirect = false; 139 | WebResponse webResp = webRequest.GetResponse(); 140 | webRequest = (HttpWebRequest)HttpWebRequest.Create(webResp.Headers["Location"].Replace("redir", "download")); 141 | webResp = webRequest.GetResponse(); 142 | string fileUrl = webResp.Headers["Content-Location"]; 143 | 144 | WebClient webClient = new WebClient(); 145 | webClient.DownloadFile(fileUrl, downloadedFile); 146 | return downloadedFile; 147 | } 148 | 149 | private async static Task getValueFromQuery(HttpRequestMessage req, string name) 150 | { 151 | // parse query parameter 152 | string value = req.GetQueryNameValuePairs() 153 | .FirstOrDefault(q => string.Compare(q.Key, name, true) == 0) 154 | .Value; 155 | 156 | //if not found in query string, look for it in the body (json) 157 | // Get request body 158 | dynamic data = await req.Content.ReadAsAsync(); 159 | 160 | // Set name to query string or body data 161 | value = value ?? data?[name]; 162 | return value; 163 | } 164 | 165 | //this is to delete the folder after the response 166 | //thanks to: https://stackoverflow.com/a/30522890/2205372 167 | public class FileHttpResponseMessage : HttpResponseMessage 168 | { 169 | private string filePath; 170 | 171 | public FileHttpResponseMessage(string filePath) 172 | { 173 | this.filePath = filePath; 174 | } 175 | 176 | protected override void Dispose(bool disposing) 177 | { 178 | base.Dispose(disposing); 179 | Content.Dispose(); 180 | Directory.Delete(filePath,true); 181 | } 182 | } 183 | --------------------------------------------------------------------------------