Skip to content

Commit a1452b2

Browse files
author
MHD\nialan
committed
More refactoring to enable mocking of objects for unit testing. Return JSON generation is not done with string manipulation any more :)
1 parent b417e56 commit a1452b2

File tree

8 files changed

+102
-222
lines changed

8 files changed

+102
-222
lines changed

ADF.Functions/DataLakeConfigFactory.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ public DataLakeConfig GetDataLakeConfig (HttpRequest req)
3535
//_logger.LogInformation($"req.GetQueryParameterDictionary(): {JsonConvert.SerializeObject(req.GetQueryParameterDictionary(), Formatting.Indented)}");
3636

3737
var data = GetRequestData(req);
38-
config.AccountUri = req.Query["accountUri"] != StringValues.Empty ? (string)req.Query["accountUri"] : data?.accountUri;
39-
config.Container = req.Query["container"] != StringValues.Empty ? (string)req.Query["container"] : data?.container;
38+
config.AccountUri = req.Query[AccountUriParam] != StringValues.Empty ? (string)req.Query[AccountUriParam] : data?.accountUri;
39+
config.Container = req.Query[ContainerParam] != StringValues.Empty ? (string)req.Query[ContainerParam] : data?.container;
4040

4141
return config;
4242
}

ADF.Functions/DataLakeFunctions.cs

Lines changed: 21 additions & 191 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,31 @@
11
using System;
2-
using System.IO;
32
using System.Threading.Tasks;
43
using Microsoft.AspNetCore.Mvc;
54
using Microsoft.Azure.WebJobs;
65
using Microsoft.Azure.WebJobs.Extensions.Http;
76
using Microsoft.AspNetCore.Http;
87
using Microsoft.Extensions.Logging;
9-
using Newtonsoft.Json;
10-
using Azure.Identity;
11-
using Azure.Storage.Files.DataLake;
12-
using System.Collections.Generic;
13-
using Newtonsoft.Json.Linq;
14-
using Flurl;
158
using System.Linq;
16-
using System.Reflection;
17-
using System.Linq.Dynamic.Core;
18-
using Azure.Storage.Files.DataLake.Models;
199
using Azure.Datafactory.Extensions.DataLake;
20-
using Azure.Datafactory.Extensions.DataLake.Model;
2110

2211
namespace Azure.Datafactory.Extensions.Functions
2312
{
24-
public partial class DataLakeFunctions
13+
public partial class DataLakeFunctions: FunctionsBase
2514
{
26-
private readonly ILogger<DataLakeFunctions> _logger;
2715
private readonly DataLakeConfigFactory _configFactory;
2816
private readonly IDataLakeClientFactory _clientFactory;
29-
public DataLakeFunctions(ILogger<DataLakeFunctions> logger, DataLakeConfigFactory configFactory, IDataLakeClientFactory clientFactory)
17+
private readonly DataLakeControllerFactory _controllerFactory;
18+
public DataLakeFunctions(ILogger<DataLakeFunctions> logger, DataLakeConfigFactory configFactory, IDataLakeClientFactory clientFactory, DataLakeControllerFactory controllerFactory):
19+
base(logger)
3020
{
31-
_logger = logger;
3221
_configFactory = configFactory;
3322
_clientFactory = clientFactory;
23+
_controllerFactory = controllerFactory;
3424
}
3525

3626

3727

28+
3829
[FunctionName("DataLakeGetItems")]
3930
public async Task<IActionResult> DataLakeGetItems(
4031
[HttpTrigger(AuthorizationLevel.Function, "get" /*, "post"*/, Route = null)] HttpRequest req)
@@ -53,7 +44,14 @@ public async Task<IActionResult> DataLakeGetItems(
5344
throw new ArgumentException($"Account Uri '{dataLakeConfig.AccountUri}' not found. Check the URI is correct.");
5445

5546
var client = _clientFactory.GetDataLakeClient(dataLakeConfig);
56-
return await GetItemsAsync(client, dataLakeConfig, getItemsConfig);
47+
var controller = _controllerFactory.CreateDataLakeController(client);
48+
49+
var responseJson = GetBaseResponse(dataLakeConfig, getItemsConfig);
50+
var items = await controller.GetItemsAsync(dataLakeConfig, getItemsConfig);
51+
foreach (var item in items)
52+
responseJson.Add(item.Key, item.Value);
53+
54+
return (IActionResult)new OkObjectResult(responseJson);
5755
}
5856
catch (ArgumentException ex)
5957
{
@@ -85,21 +83,20 @@ public async Task<IActionResult> DataLakeCheckPathCase(
8583
throw new ArgumentException($"Account Uri '{dataLakeConfig.AccountUri}' not found. Check the URI is correct.");
8684

8785
var client = _clientFactory.GetDataLakeClient(dataLakeConfig);
86+
var controller = _controllerFactory.CreateDataLakeController(client);
8887

89-
var paramsJsonFragment = GetParamsJsonFragment(dataLakeConfig, getItemsConfig);
90-
var validatedPath = await CheckPathAsync(client, getItemsConfig.Path, true);
88+
var validatedPath = await controller.CheckPathAsync(getItemsConfig.Path, true);
9189

9290
// If multiple files match, the function will throw and the catch block will return a BadRequestObjectResult
9391
// If the path could not be found as a directory, try for a file...
94-
validatedPath ??= await CheckPathAsync(client, getItemsConfig.Path, false);
92+
validatedPath ??= await controller.CheckPathAsync(getItemsConfig.Path, false);
9593

96-
var resultJson = "{" +
97-
$"{paramsJsonFragment}, \"validatedPath\":\"{validatedPath}\" " +
98-
"}";
94+
var responseJson = GetBaseResponse(dataLakeConfig, getItemsConfig);
95+
responseJson.Add("validatedPath", validatedPath);
9996

10097
return validatedPath != null ?
101-
(IActionResult)new OkObjectResult(JObject.Parse(resultJson)) :
102-
(IActionResult)new NotFoundObjectResult(JObject.Parse(resultJson));
98+
(IActionResult)new OkObjectResult(responseJson):
99+
(IActionResult)new NotFoundObjectResult(responseJson);
103100
}
104101
catch (ArgumentException ex)
105102
{
@@ -112,173 +109,6 @@ public async Task<IActionResult> DataLakeCheckPathCase(
112109
return new BadRequestObjectResult("An error occurred, see the Azure Function logs for more details");
113110
}
114111
}
115-
116-
117-
118-
119-
120-
121-
122-
private string GetParamsJsonFragment(DataLakeConfig dataLakeConfig, object parameters)
123-
{
124-
return $"\"debugInfo\": {AssemblyHelpers.GetAssemblyVersionInfoJson()}," +
125-
$"\"storageContainerUrl\": {dataLakeConfig.BaseUrl}," +
126-
parameters == null ?
127-
string.Empty :
128-
$"\"parameters\": {JsonConvert.SerializeObject(parameters, Formatting.Indented, new JsonSerializerSettings { NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore })}";
129-
}
130-
131-
132-
133-
134-
135-
136-
137-
138-
139-
140-
141-
142-
143-
144-
145-
146-
147-
148-
149-
150-
151-
private async Task<string> CheckPathAsync(DataLakeFileSystemClient client, string path, bool isDirectory)
152-
{
153-
if (path == null || path.Trim() == "/")
154-
return null;
155-
156-
// Check if the path exists with the casing as is...
157-
var pathExists = isDirectory ?
158-
client.GetDirectoryClient(path).Exists() :
159-
client.GetFileClient(path).Exists();
160-
if (pathExists)
161-
return path;
162-
163-
_logger.LogInformation($"${(isDirectory ? "Directory" : "File")} '${path}' not found, checking paths case using case insensitive compare...");
164-
165-
// Split the paths so we can test them seperately
166-
var directoryPath = isDirectory ? path : Path.GetDirectoryName(path).Replace(Path.DirectorySeparatorChar, '/');
167-
var filename = isDirectory ? null : Path.GetFileName(path);
168-
169-
// If the directory does not exist, we find it
170-
string validDirectory = null;
171-
if (! await client.GetDirectoryClient(path).ExistsAsync())
172-
{
173-
var directoryParts = directoryPath.Split('/');
174-
foreach (var directoryPart in directoryParts)
175-
{
176-
var searchItem = directoryPart;
177-
var validPaths = MatchPathItemsCaseInsensitive(client, validDirectory, searchItem, true);
178-
179-
if (validPaths.Count == 0)
180-
return null;
181-
else if (validPaths.Count > 1)
182-
throw new Exception("Multiple paths matched with case insensitive compare.");
183-
184-
validDirectory = validPaths[0];
185-
}
186-
}
187-
188-
if (isDirectory)
189-
return validDirectory;
190-
191-
// Now check if the file exists using the corrected directory, and if not find a match...
192-
var testFilePath = $"{validDirectory ?? ""}/{filename}".TrimStart('/');
193-
if (client.GetFileClient(testFilePath).Exists())
194-
return testFilePath;
195-
196-
var files = MatchPathItemsCaseInsensitive(client, validDirectory, filename, false);
197-
if (files.Count > 1)
198-
throw new Exception("Multiple paths matched with case insensitive compare.");
199-
return files.FirstOrDefault();
200-
}
201-
202-
private IList<string> MatchPathItemsCaseInsensitive(DataLakeFileSystemClient client, string basePath, string searchItem, bool isDirectory)
203-
{
204-
var paths = client.GetPaths(basePath).ToList();
205-
return paths.Where(p => p.IsDirectory == isDirectory && Path.GetFileName(p.Name).Equals(searchItem, StringComparison.CurrentCultureIgnoreCase))
206-
.Select(p => p.Name)
207-
.ToList();
208-
209-
}
210-
211-
212-
private async Task<IActionResult> GetItemsAsync(DataLakeFileSystemClient client, DataLakeConfig dataLakeConfig, DataLakeGetItemsConfig getItemsConfig)
213-
{
214-
var directory = getItemsConfig.IgnoreDirectoryCase ?
215-
await CheckPathAsync(client, getItemsConfig.Directory, true) :
216-
getItemsConfig.Directory;
217-
218-
var paramsJsonFragment = GetParamsJsonFragment(dataLakeConfig, getItemsConfig);
219-
220-
if (!client.GetDirectoryClient(directory).Exists())
221-
return new BadRequestObjectResult(JObject.Parse($"{{ {paramsJsonFragment}, \"error\": \"Directory '{directory} could not be found'\" }}"));
222-
223-
var paths = client
224-
.GetPaths(path: directory ?? string.Empty, recursive: getItemsConfig.Recursive)
225-
.Select(p => new DataLakeFile
226-
{
227-
Name = Path.GetFileName(p.Name),
228-
Directory = p.IsDirectory.GetValueOrDefault(false) ?
229-
p.Name :
230-
Path.GetDirectoryName(p.Name).Replace(Path.DirectorySeparatorChar, '/'),
231-
FullPath = p.Name,
232-
Url = Url.Combine(dataLakeConfig.BaseUrl, p.Name),
233-
IsDirectory = p.IsDirectory.GetValueOrDefault(false),
234-
ContentLength = p.ContentLength.GetValueOrDefault(0),
235-
LastModified = p.LastModified.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
236-
})
237-
.ToList();
238-
239-
// 1: Filter the results using dynamic LINQ
240-
foreach (var filter in getItemsConfig.Filters.Where(f => f.IsValid))
241-
{
242-
var dynamicLinqQuery = filter.GetDynamicLinqString();
243-
string dynamicLinqQueryValue = filter.GetDynamicLinqValue();
244-
_logger.LogInformation($"Applying filter: paths.AsQueryable().Where(\"{dynamicLinqQuery}\", \"{filter.Value}\").ToList()");
245-
paths = paths.AsQueryable().Where(dynamicLinqQuery, dynamicLinqQueryValue).ToList();
246-
}
247-
248-
// 2: Sort the results
249-
if (!string.IsNullOrWhiteSpace(getItemsConfig.OrderByColumn))
250-
{
251-
paths = paths.AsQueryable()
252-
.OrderBy(getItemsConfig.OrderByColumn + (getItemsConfig.OrderByDescending ? " descending" : string.Empty))
253-
.ToList();
254-
}
255-
256-
// 3: Do a top N if required
257-
if (getItemsConfig.Limit > 0 && getItemsConfig.Limit < paths.Count)
258-
paths = paths.Take(getItemsConfig.Limit).ToList();
259-
260-
261-
262-
// Output the results
263-
var versionAttribute = Attribute.GetCustomAttribute(Assembly.GetExecutingAssembly(), typeof(AssemblyInformationalVersionAttribute)) as AssemblyInformationalVersionAttribute;
264-
265-
var IsEveryFilterValid = getItemsConfig.Filters.All(f => f.IsValid);
266-
var filesListJson = IsEveryFilterValid ?
267-
$"\"fileCount\": {paths.Count}," +
268-
$"\"files\": {JsonConvert.SerializeObject(paths, Formatting.Indented)}" :
269-
string.Empty;
270-
271-
var resultJson = $"{{ {paramsJsonFragment}, {(getItemsConfig.IgnoreDirectoryCase && directory != getItemsConfig.Directory ? $"\"correctedFilePath\": \"{directory}\"," : string.Empty)} {filesListJson} }}";
272-
273-
return IsEveryFilterValid ?
274-
(IActionResult)new OkObjectResult(JObject.Parse(resultJson)) :
275-
(IActionResult)new BadRequestObjectResult(JObject.Parse(resultJson));
276-
}
277-
278112

279113
}
280-
281-
282-
283-
284114
}

ADF.Functions/FunctionsBase.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using Azure.Datafactory.Extensions.DataLake.Model;
2+
using Microsoft.Extensions.Logging;
3+
using Newtonsoft.Json.Linq;
4+
5+
namespace Azure.Datafactory.Extensions.Functions
6+
{
7+
public abstract class FunctionsBase
8+
{
9+
protected readonly ILogger _logger;
10+
11+
public FunctionsBase(ILogger<FunctionsBase> logger)
12+
{
13+
_logger = logger;
14+
}
15+
16+
protected JObject GetBaseResponse(DataLakeConfig dataLakeConfig, object parameters)
17+
{
18+
var assemblyInfo = AssemblyHelpers.GetAssemblyVersionInfoJson();
19+
20+
var responseJson = new JObject();
21+
if (assemblyInfo.HasValues)
22+
responseJson.Add("debugInfo", assemblyInfo);
23+
24+
if (dataLakeConfig.BaseUrl != null)
25+
responseJson.Add("storageContainerUrl", dataLakeConfig.BaseUrl);
26+
27+
var paramatersJson = JObject.FromObject(parameters);
28+
responseJson.Add("parameters", paramatersJson);
29+
30+
return responseJson;
31+
}
32+
}
33+
}

ADF.Functions/Startup.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ public override void Configure(IFunctionsHostBuilder builder)
1313
builder.Services.AddLogging();
1414

1515
builder.Services.AddTransient(typeof(DataLakeConfigFactory));
16-
//builder.Services.AddSingleton<DataLakeConfigFactory>();
1716
builder.Services.AddTransient<IDataLakeClientFactory, DataLakeClientFactory>();
17+
builder.Services.AddTransient(typeof(DataLakeControllerFactory));
1818
}
1919
}
2020
}

ADF.Functions/host.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@
77
"isEnabled": true
88
}
99
},
10-
"logLevel": {
11-
"Azure.Datafactory.Extensions.Functions.DataLakeFunctions": "Information"
12-
}
10+
"logLevel": {
11+
"Azure.Datafactory.Extensions.Functions.DataLakeFunctions": "Information",
12+
"Azure.Datafactory.Extensions.Functions.FunctionsBase": "Information",
13+
"Azure.Datafactory.Extensions.Functions.DataLakeConfigFactory": "Information",
14+
"Azure.Datafactory.Extensions.DataLake.DataLakeClientFactory": "Information",
15+
"Azure.Datafactory.Extensions.DataLake.DataLakeControllerFactory": "Information"
16+
}
1317
}
1418
}

Azure.DataFactory.Extensions/AssemblyHelpers.cs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,30 @@
11
using System.Linq;
22
using System.Reflection;
3+
using Newtonsoft.Json.Linq;
34

45
namespace Azure.Datafactory.Extensions.Functions
56
{
67
public class AssemblyHelpers
78
{
8-
public static string GetAssemblyVersionInfoJson()
9+
public static JObject GetAssemblyVersionInfoJson()
910
{
1011
var callingAssembly = Assembly.GetCallingAssembly();
1112
return GetAssemblyVersionInfoJson(callingAssembly);
1213
}
1314

14-
public static string GetAssemblyVersionInfoJson(Assembly assembly)
15+
public static JObject GetAssemblyVersionInfoJson(Assembly assembly)
1516
{
1617
string buildDate = assembly.GetCustomAttributes().OfType<AssemblyMetadataAttribute>().FirstOrDefault(a => a.Key == "BuildDate")?.Value;
1718
string informationalVersion = assembly.GetCustomAttributes().OfType<AssemblyInformationalVersionAttribute>().FirstOrDefault()?.InformationalVersion;
1819

19-
return "{" +
20-
$" \"buildDate\": \"{buildDate}\"," +
21-
$" \"informationalVersion\": \"{informationalVersion}\"" +
22-
"}";
20+
var jobj = new JObject();
21+
if (buildDate != null)
22+
jobj.Add("buildDate", buildDate);
23+
24+
if (informationalVersion != null)
25+
jobj.Add("informationalVersion", informationalVersion);
26+
27+
return jobj;
2328
}
2429

2530
}

0 commit comments

Comments
 (0)