Is it better to use IEnumerable as read only list? - c#

I am downloading .tgz file from the remote server to a folder locally and then unzipping it out. After that I read all those json/txt files in memory. Below is my code which does that:
public IEnumerable<DataHolder> GetFiles(string fileName)
{
// this will download files to a directory
var isDownloadSuccess = DownloadFiles(_url, fileName, _directoryToDownload);
if (!isDownloadSuccess.Result) { yield return default; }
// this will unzip files in same directory
var isUnzipSuccess = UnzipTgzFile(_directoryToDownload, fileName);
if (!isUnzipSuccess) { yield return default; }
// this will get list of all files in same directory
IList<string> files = GetListOfFiles(_directoryToDownload);
if (files == null || files.Count == 0) { yield return default; }
// total files will be 500 max
for (int i = 0; i < files.Count; i++)
{
var cfgPath = files[i];
if (!File.Exists(cfgPath)) { continue; }
var fileDate = File.GetLastWriteTimeUtc(cfgPath);
var fileContent = File.ReadAllText(cfgPath);
var pathPieces = cfgPath.Split(System.IO.Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries);
var fileName = pathPieces[pathPieces.Length - 1];
var md5Hash = CheckMD5(cfgPath);
yield return new DataHolder
{
FileName = fileName,
FileDate = fileDate,
FileContent = fileContent,
FileMD5HashValue = md5Hash
};
}
}
Use Case:
If I am not able to download files successfully (isDownloadSuccess is false) then I want to return empty IEnumerable back.
If I am not able to unzip files successfully (isUnzipSuccess is false) then I want to return empty IEnumerable back as well.
If I am not able to get list of files successfully (files list is empty) then I want to return empty IEnumerable back as well.
If I had some processing issues in the for loop then I want to return empty IEnumerable back as well.
Otherwise just return readonly IEnumerable back to the caller with data in it.
Problem I am having with above approach is - I cannot do empty check in the cases when it returns yield return default and also I am confuse on what happens if processing fails in for loop, will it return empty IEnumerable as well back to the caller?
IEnumerable<DataHolder> dataHolders = GetFiles(fileName);
// below check doesn't work on negative cases
if (dataHolders == null || !dataHolders.Any())
return false;
//....
So is this the right way to use IEnumerable here or I can use any other data structure which can provide read only list to the caller along with empty list (for negative cases) which I can easily check for null or empty.
Question:
My goal is just to return read only list back to the user with data in it (for positive cases). And for all negative cases, I need to return back empty read only list to the user.

We talked in chat, but will reiterate.
Yield doesn't really work here as we don't really need those semantics for any specific reason. You want to get a list of files to use later for comparing against other lists of files (they all have to be read in to memory eventually, may as well do it now):
public IReadOnlyList<DataHolder> GetFiles(string fileName)
{
// this will download files to a directory
var isDownloadSuccess = DownloadFiles(_url, fileName, _directoryToDownload);
if (!isDownloadSuccess.Result) { return Array.Empty<DataHolder>(); }
// this will unzip files in same directory
var isUnzipSuccess = UnzipTgzFile(_directoryToDownload, fileName);
if (!isUnzipSuccess) { return Array.Empty<DataHolder>(); }
// this will get list of all files in same directory
IList<string> files = GetListOfFiles(_directoryToDownload);
if (files == null || files.Count == 0) { return Array.Empty<DataHolder>(); }
var lst = new List<DataHolder>(files.Count);
for (int i = 0; i < files.Count; i++)
{
var cfgPath = files[i];
if (!File.Exists(cfgPath)) { continue; }
var fileDate = File.GetLastWriteTimeUtc(cfgPath);
var fileContent = File.ReadAllText(cfgPath);
var pathPieces = cfgPath.Split(System.IO.Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries);
var fileName = pathPieces[pathPieces.Length - 1];
var md5Hash = CheckMD5(cfgPath);
lst.Add(new DataHolder
{
FileName = fileName,
FileDate = fileDate,
FileContent = fileContent,
FileMD5HashValue = md5Hash
});
}
return lst.AsReadOnly();
}
We are now just returning a read-only list of all your items, which allows you to do checks if any items exist, such as:
if(lst?.Count > 0){ /* There are items to process */ }
Also, this doesn't break your pattern as IReadOnlyList implements IEnumerable, so it will fit in quite nicely.

Since you download and unzip all files at once, I understand that you're not concerned about this implementation being an actual iteratable (as a foreach would wait until everything is done before being able to iterate).
Keeping that in mind, the easiest you can do is to get rid of yields and return arrays.
Sample implementation (might need some spell check):
public IEnumerable<DataHolder> GetFiles(string fileName)
{
// this will download files to a directory
var isDownloadSuccess = DownloadFiles(_url, fileName, _directoryToDownload);
if (!isDownloadSuccess.Result) { return Array.Empty<DataHolder>(); }
// this will unzip files in same directory
var isUnzipSuccess = UnzipTgzFile(_directoryToDownload, fileName);
if (!isUnzipSuccess) { return Array.Empty<DataHolder>(); }
// this will get list of all files in same directory
IList<string> files = GetListOfFiles(_directoryToDownload);
if (files == null || files.Count == 0) { return Array.Empty<DataHolder>(); }
var data = new DataHolder[files.Count];
try
{
for (int i = 0; i < files.Count; i++)
{
var cfgPath = files[i];
if (!File.Exists(cfgPath)) { continue; }
var fileDate = File.GetLastWriteTimeUtc(cfgPath);
var fileContent = File.ReadAllText(cfgPath);
var pathPieces = cfgPath.Split(System.IO.Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries);
var fileName = pathPieces[pathPieces.Length - 1];
var md5Hash = CheckMD5(cfgPath);
data[i] = new DataHolder
{
FileName = fileName,
FileDate = fileDate,
FileContent = fileContent,
FileMD5HashValue = md5Hash
};
}
return data;
}
catch (Exception ex)
{
return Array.Empty<DataHolder>();
}
}
For consuming this, you would, for example:
var files = GetFiles("somename.txt");
if (!files.Any()) // do not check for files being null
{
return;
}
Side note, I would change the first few lines into this, so you don't do sync-over-async which can cause deadlocks:
public async Task<IEnumerable<DataHolder>> GetFiles(string fileName)
{
// this will download files to a directory
var isDownloadSuccess = await DownloadFiles(_url, fileName, _directoryToDownload);
if (!isDownloadSuccess) { return Array.Empty<DataHolder>(); }
...
}

Related

Is it possible to download a .txt file and manipulate said file in one endpoint using ASP.NET MVC controller?

My task is to download a .txt file, remove some of the data and save it as .json. Currently I use one interface to download a file as .txt file and another interface to read all lines of said file, make it into an object and remove what I don't need.
Currently what happens is, that I have to go to download endpoint to get my .txt then to edit endpoint to parse and save as .json.
This is the filter method:
public void FilterOutInvalidRates(string path)
{
try
{
var targetLocation = #"TargetLocation/";
var name = File.ReadAllLines(path).First();
var fileName = name.Substring(0,3);
var lines = File.ReadAllLines(path).Skip(2);
var model = lines.Select(p => new Rates
{
a = p.Split("|")[0],
b = p.Split("|")[1],
c = p.Split("|")[2],
d = p.Split("|")[3],
e = p.Split("|")[4],
});
List<Rates> rates = model.Where(model => IsTheCountryValid(model.Country)).ToList();
var jsonString = Newtonsoft.Json.JsonConvert.SerializeObject(rates.ToArray(), Formatting.Indented);
System.IO.File.WriteAllText(targetLocation + fileName + ".json", jsonString);
System.IO.File.Delete(path);
}
catch (FileNotFoundException)
{
Console.WriteLine(#"Unable to read file:" + path.ToString());
}
}
And this is the download service:
public async void DownloadRatesTxtFile(string uri, string outputPath, int number)
{
var policy = BuildRetryPolicy();
var path = await policy.ExecuteAsync(() => uri
.DownloadFileAsync(outputPath, #"CurrencyRate" + number + ".txt"));
}
This is the controller implementation - I know it's not ideal, if I could do it all in one endpoint or a different architecture I could get rid of a ton of code. The issue is, that before the endpoint returns OK I am not able to manipulate the files in any way or read them.
[HttpGet("download")]
public async Task<IActionResult> Download()
{
var fileCounter = 1;
var outputPath2 = AppDomain.CurrentDomain.BaseDirectory + #"Data/";
var outputPath = #"TargetLocation/";
DateTime begindate = Convert.ToDateTime("01/07/2022");
DateTime enddate = Convert.ToDateTime("5/07/2022");
while (begindate < enddate)
{
if (begindate.DayOfWeek == DayOfWeek.Saturday)
{
begindate = begindate.AddDays(2);
}
_exchangeRateConnector.DownloadRatesTxtFile(_exchangeRateConnector.GenerateRatesUrl(begindate), outputPath2, fileCounter);
begindate = begindate.AddDays(1);
fileCounter++;
}
return Ok();
}
[HttpGet("edit")]
public async Task<IActionResult> Edit()
{
var outputPath2 = AppDomain.CurrentDomain.BaseDirectory + #"Data/";
var outputPath = #"TargetLocation/";
var fileCount = (from file in Directory.EnumerateFiles(outputPath2, "*.txt", SearchOption.TopDirectoryOnly)
select file).Count();
for (int i = 1; i <= fileCount; i++)
{
_exchangeRateRepository.FilterOutInvalidRates(outputPath2 + "CurrencyRate" + i + ".txt");
}
return Ok();
}
Thank you so much for any sort of advice.

How to convert from large csv file into json without using split (Out Of Memory issue) C#

I am trying to parse a 300MB csv file and save it on mongodb. In order to do that I will need to convert this csv file into a list of BsonDocument which include key value pairs which create a document. each row in the csv file is a new BsonDocument.
Every couple of minutes of parallel testing, I am getting OOM exception on the split operation.
I've read this article which is very interesting. but I couldn't find any practical solution which I can implement on those huge files.
I was looking into different csv helpers, but couldn't find anything which solve this issue.
Any help will be much appreciated.
You should be able to read it line by line like this:
public static void Main()
{
using (StreamReader sr = new StreamReader(path))
{
string[] headers = null;
string[] curLine;
while ((curLine = sr.ReadLine().Split(',')) != null)
{
if (firstLine == null)
{
headers = curLine;
}
else
{
processLine(headers, curLine);
}
}
}
}
public static void processLine(string[] headers, string[] line)
{
for (int i = 0; i < headers.Length)
{
string header = headers[i];
string line = line[i];
//Now you have individual header/line pairs that you can put into mongodb
}
}
I've never used mongodb and I don't know the structure of your csv or your mongo, so I won't be able to help much there. Hopefully you can get it from here though. If not, edit your post with some more details about how you need to structure your mongodb and hopefully somebody will post a more helpful answer.
Thank you #dbc That worked!
#ashbygeek, I needed to add this to your code,
while (!sr.EndOfStream && (curLine = sr.ReadLine().Split('\t')) != null)
{
//do process
}
So I am uploading my code which I get my big CSV file from Azure blob, and insert in Batch to mongoDB instead of each document.
I also created my own primary key hash, and index, in order to identify duplicates documents, and if I found one, I'll start insert them one by one in order to identify the duplicate.
I hope it will help for someone in the future.
using (TextFieldParser parser = new TextFieldParser(blockBlob2.OpenRead()))
{
parser.TextFieldType = FieldType.Delimited;
parser.SetDelimiters("\t");
bool headerWritten = false;
List<BsonDocument> listToInsert = new List<BsonDocument>();
int chunkSize = 50;
int counter = 0;
var headers = new string[0];
while (!parser.EndOfData)
{
//Processing row
var fields = parser.ReadFields();
if (!headerWritten)
{
headers = fields;
headerWritten = true;
continue;
}
listToInsert.Add(new BsonDocument(headers.Zip(fields, (k, v) => new { k, v }).ToDictionary(x => x.k, x => x.v)));
counter++;
if (counter != chunkSize) continue;
AdditionalInformation(listToInsert, dataCollectionQueueMessage);
CalculateHashForPrimaryKey(listToInsert);
await InsertDataIntoDB(listToInsert, dataCollectionQueueMessage);
counter = 0;
listToInsert.Clear();
}
if (listToInsert.Count > 0)
{
AdditionalInformation(listToInsert, dataCollectionQueueMessage);
CalculateHashForPrimaryKey(listToInsert);
await InsertDataIntoDB(listToInsert, dataCollectionQueueMessage);
}
}
private async Task InsertDataIntoDB(List<BsonDocument>listToInsert, DataCollectionQueueMessage dataCollectionQueueMessage)
{
const string connectionString = "mongodb://127.0.0.1/localdb";
var client = new MongoClient(connectionString);
_database = client.GetDatabase("localdb");
var collection = _database.GetCollection<BsonDocument>(dataCollectionQueueMessage.CollectionTypeEnum.ToString());
await collection.Indexes.CreateOneAsync(new BsonDocument("HashMultipleKey", 1), new CreateIndexOptions() { Unique = true, Sparse = true, });
try
{
await collection.InsertManyAsync(listToInsert);
}
catch (Exception ex)
{
ApplicationInsights.Instance.TrackException(ex);
await InsertSingleDocuments(listToInsert, collection, dataCollectionQueueMessage);
}
}
private async Task InsertSingleDocuments(List<BsonDocument> dataCollectionDict, IMongoCollection<BsonDocument> collection
,DataCollectionQueueMessage dataCollectionQueueMessage)
{
ApplicationInsights.Instance.TrackEvent("About to start insert individual documents and to find the duplicate one");
foreach (var data in dataCollectionDict)
{
try
{
await collection.InsertOneAsync(data);
}
catch (Exception ex)
{
ApplicationInsights.Instance.TrackException(ex,new Dictionary<string, string>() {
{
"Error Message","Duplicate document was detected, therefore ignoring this document and continuing to insert the next docuemnt"
}, {
"FilePath",dataCollectionQueueMessage.FilePath
}}
);
}
}
}

Web API Upload Files

I have some data to save into a database.
I have created a web api post method to save data. Following is my post method:
[Route("PostRequirementTypeProcessing")]
public IEnumerable<NPAAddRequirementTypeProcessing> PostRequirementTypeProcessing(mdlAddAddRequirementTypeProcessing requTypeProcess)
{
mdlAddAddRequirementTypeProcessing rTyeProcessing = new mdlAddAddRequirementTypeProcessing();
rTyeProcessing.szDescription = requTypeProcess.szDescription;
rTyeProcessing.iRequirementTypeId = requTypeProcess.iRequirementTypeId;
rTyeProcessing.szRequirementNumber = requTypeProcess.szRequirementNumber;
rTyeProcessing.szRequirementIssuer = requTypeProcess.szRequirementIssuer;
rTyeProcessing.szOrganization = requTypeProcess.szOrganization;
rTyeProcessing.dIssuedate = requTypeProcess.dIssuedate;
rTyeProcessing.dExpirydate = requTypeProcess.dExpirydate;
rTyeProcessing.szSignedBy = requTypeProcess.szSignedBy;
rTyeProcessing.szAttachedDocumentNo = requTypeProcess.szAttachedDocumentNo;
if (String.IsNullOrEmpty(rTyeProcessing.szAttachedDocumentNo))
{
}
else
{
UploadFile();
}
rTyeProcessing.szSubject = requTypeProcess.szSubject;
rTyeProcessing.iApplicationDetailsId = requTypeProcess.iApplicationDetailsId;
rTyeProcessing.iEmpId = requTypeProcess.iEmpId;
NPAEntities context = new NPAEntities();
Log.Debug("PostRequirementTypeProcessing Request traced");
var newRTP = context.NPAAddRequirementTypeProcessing(requTypeProcess.szDescription, requTypeProcess.iRequirementTypeId,
requTypeProcess.szRequirementNumber, requTypeProcess.szRequirementIssuer, requTypeProcess.szOrganization,
requTypeProcess.dIssuedate, requTypeProcess.dExpirydate, requTypeProcess.szSignedBy,
requTypeProcess.szAttachedDocumentNo, requTypeProcess.szSubject, requTypeProcess.iApplicationDetailsId,
requTypeProcess.iEmpId);
return newRTP.ToList();
}
There is a field called 'szAttachedDocumentNo' which is a document that's being saved in the database as well.
After saving all data, I want the physical file of the 'szAttachedDocumentNo' to be saved on the server. So i created a method called "UploadFile" as follows:
[HttpPost]
public void UploadFile()
{
if (HttpContext.Current.Request.Files.AllKeys.Any())
{
// Get the uploaded file from the Files collection
var httpPostedFile = HttpContext.Current.Request.Files["UploadedFile"];
if (httpPostedFile != null)
{
// Validate the uploaded image(optional)
string folderPath = HttpContext.Current.Server.MapPath("~/UploadedFiles");
//string folderPath1 = Convert.ToString(ConfigurationManager.AppSettings["DocPath"]);
//Directory not exists then create new directory
if (!Directory.Exists(folderPath))
{
Directory.CreateDirectory(folderPath);
}
// Get the complete file path
var fileSavePath = Path.Combine(folderPath, httpPostedFile.FileName);
// Save the uploaded file to "UploadedFiles" folder
httpPostedFile.SaveAs(fileSavePath);
}
}
}
Before running the project, i debbugged the post method, so when it comes to "UploadFile" line, it takes me to its method.
From the file line, it skipped the remaining lines and went to the last line; what means it didn't see any file.
I am able to save everything to the database, just that i didn't see the physical file in the specified location.
Any help would be much appreciated.
Regards,
Somad
Makes sure the request "content-type": "multipart/form-data" is set
[HttpPost()]
public async Task<IHttpActionResult> UploadFile()
{
if (!Request.Content.IsMimeMultipartContent())
throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
try
{
MultipartMemoryStreamProvider provider = new MultipartMemoryStreamProvider();
await Request.Content.ReadAsMultipartAsync(provider);
if (provider.Contents != null && provider.Contents.Count == 0)
{
return BadRequest("No files provided.");
}
foreach (HttpContent file in provider.Contents)
{
string filename = file.Headers.ContentDisposition.FileName.Trim('\"');
byte[] buffer = await file.ReadAsByteArrayAsync();
using (MemoryStream stream = new MemoryStream(buffer))
{
// save the file whereever you want
}
}
return Ok("files Uploded");
}
catch (Exception ex)
{
return InternalServerError(ex);
}
}

Append (n) to file names (not using system.io.file) to ensure unique names

I have a class that holds the name of a file, and the data of a file:
public class FileMeta
{
public string FileName { get; set; }
public byte[] FileData { get; set; }
}
I have a method that populates a collection of this class through an async download operation (Files are not coming from a local file system):
async Task<List<FileMeta>> ReturnFileData(IEnumerable<string> urls)
{
using (var client = new HttpClient())
{
var results = await Task.WhenAll(urls.Select(async url => new
{
FileName = Path.GetFileName(url),
FileData = await client.GetByteArrayAsync(url),
}));
return results.Select(result =>
new FileMeta
{
FileName = result.FileName,
FileData = result.FileData
}).ToList();
}
}
I am going to feed this List<FileMeta> into a ZipFile creator, and the ZipFile like all File containers needs unique file names.
Readability is important, and I would like to be able to do the following:
file.txt => file.txt
file.txt => file(1).txt
file.txt => file(2).txt
There are a number of examples on how to do this within the file system, but not with a simple object collection. (Using System.IO.File.Exists for example)
What's the best way to loop through this collection of objects and return a unique set of file names?
How far I've gotten
private List<FileMeta> EnsureUniqueFileNames(IEnumerable<FileMeta> fileMetas)
{
var returnList = new List<FileMeta>();
foreach (var file in fileMetas)
{
while (DoesFileNameExist(file.FileName, returnList))
{
//Append (n) in sequence until match is not found?
}
}
return returnList;
}
private bool DoesFileNameExist(string fileName, IEnumerable<FileMeta> fileMeta)
{
var fileNames = fileMeta.Select(file => file.FileName).ToList();
return fileNames.Contains(fileName);
}
You can try the following to increment the filenames:
private List<FileMeta> EnsureUniqueFileNames(IEnumerable<FileMeta> fileMetas)
{
var returnedList = new List<FileMeta>();
foreach (var file in fileMetas)
{
int count = 0;
string originalFileName = file.FileName;
while (returnedList.Any(fileMeta => fileMeta.FileName.Equals(file.FileName,
StringComparison.OrdinalIgnoreCase))
{
string fileNameOnly = Path.GetFileNameWithoutExtension(originalFileName);
string extension = Path.GetExtension(file.FileName);
file.FileName = string.Format("{0}({1}){2}", fileNameOnly, count, extension);
count++;
}
returnList.Add(file);
}
return returnList;
}
As a side note, in your ReturnFileData, you're generating two lists, one of anonymous type and one of your actual FileMeta type. You can reduce the creation of the intermediate list. Actually, you don't need to await inside the method at all:
private Task<FileMeta[]> ReturnFileDataAsync(IEnumerable<string> urls)
{
var client = new HttpClient();
return Task.WhenAll(urls.Select(async url => new FileMeta
{
FileName = Path.GetFileName(url),
FileData = await client.GetByteArrayAsync(url),
}));
}
I made the return type a FileMeta[] instead of a List<FileMeta>, as it is a fixed sized returning anyway, and reduces the need to call ToList on the returned array. I also added the Async postfix, to follow the TAP guidelines.

Get to an Exchange folder by path using EWS

I need to retrieve items from the 'Inbox\test\final' Exchange folder using EWS. The folder is provided by a literal path as written above. I know I can split this string into folder names and recursively search for the necessary folder, but is there a more optimal way that can translate a string path into a folder instance or folder ID?
I'm using the latest EWS 2.0 assemblies. Do these assemblies provide any help, or am I stuck with manual recursion?
You could use an extended property as in this example
private string GetFolderPath(ExchangeService service, FolderId folderId)
{
var folderPathExtendedProp = new ExtendedPropertyDefinition(26293, MapiPropertyType.String);
var folderPropSet = new PropertySet(BasePropertySet.FirstClassProperties) { folderPathExtendedProp };
var folder = Folder.Bind(service, folderId, folderPropSet);
string path = null;
folder.TryGetProperty(folderPathExtendedProp, out path);
return path?.Replace("\ufffe", "\\");
}
Source: https://social.msdn.microsoft.com/Forums/en-US/e5d07492-f8a3-4db5-b137-46e920ab3dde/exchange-ews-managed-getting-full-path-for-a-folder?forum=exchangesvrdevelopment
Since Exchange Server likes to map everything together with Folder.Id, the only way to find the path you're looking for is by looking at folder names.
You'll need to create a recursive function to go through all folders in a folder collection, and track the path as it moves through the tree of email folders.
Another parameter is needed to track the path that you're looking for.
public static Folder GetPathFolder(ExchangeService service, FindFoldersResults results,
string lookupPath, string currentPath)
{
foreach (Folder folder in results)
{
string path = currentPath + #"\" + folder.DisplayName;
if (folder.DisplayName == "Calendar")
{
continue;
}
Console.WriteLine(path);
FolderView view = new FolderView(50);
SearchFilter filter = new SearchFilter.IsEqualTo(FolderSchema.Id, folder.Id);
FindFoldersResults folderResults = service.FindFolders(folder.Id, view);
Folder result = GetPathFolder(service, folderResults, lookupPath, path);
if (result != null)
{
return result;
}
string[] pathSplitForward = path.Split(new[] { "/" }, StringSplitOptions.RemoveEmptyEntries);
string[] pathSplitBack = path.Split(new[] { #"\" }, StringSplitOptions.RemoveEmptyEntries);
string[] lookupPathSplitForward = lookupPath.Split(new[] { "/" }, StringSplitOptions.RemoveEmptyEntries);
string[] lookupPathSplitBack = lookupPath.Split(new[] { #"\" }, StringSplitOptions.RemoveEmptyEntries);
if (ArraysEqual(pathSplitForward, lookupPathSplitForward) ||
ArraysEqual(pathSplitBack, lookupPathSplitBack) ||
ArraysEqual(pathSplitForward, lookupPathSplitBack) ||
ArraysEqual(pathSplitBack, lookupPathSplitForward))
{
return folder;
}
}
return null;
}
"ArraysEqual":
public static bool ArraysEqual<T>(T[] a1, T[] a2)
{
if (ReferenceEquals(a1, a2))
return true;
if (a1 == null || a2 == null)
return false;
if (a1.Length != a2.Length)
return false;
EqualityComparer<T> comparer = EqualityComparer<T>.Default;
for (int i = 0; i < a1.Length; i++)
{
if (!comparer.Equals(a1[i], a2[i])) return false;
}
return true;
}
I do all the extra array checking since sometimes my clients enter paths with forward slashes, back slashes, starting with a slash, etc. They're not tech savvy so let's make sure the program works every time!
As you go through each directory, compare the desired path to the iterated path. Once it's found, bubble up the Folder object that it's currently on. You'll need to create a search filter for that folder's id:
FindItemsResults<item> results = service.FindItems(foundFolder.Id, searchFilter, view);
Loop through the emails in results!
foreach (Item item in results)
{
// do something with item (email)
}
Here's my recursive descent implementation, which attempts to fetch as little information as possible on the way to the target folder:
private readonly FolderView _folderTraversalView = new FolderView(1) { PropertySet = PropertySet.IdOnly };
private Folder TraceFolderPathRec(string[] pathTokens, FolderId rootId)
{
var token = pathTokens.FirstOrDefault();
var matchingSubFolder = _exchangeService.FindFolders(
rootId,
new SearchFilter.IsEqualTo(FolderSchema.DisplayName, token),
_folderTraversalView)
.FirstOrDefault();
if (matchingSubFolder != null && pathTokens.Length == 1) return matchingSubFolder;
return matchingSubFolder == null ? null : TraceFolderPathRec(pathTokens.Skip(1).ToArray(), matchingSubFolder.Id);
}
For a '/'-delimited path, it can be called as follows:
public Folder TraceFolderPath(string folderPath)
{ // Handle folder names with '/' in them
var tokens = folderPath
.Replace("\\/", "<slash>")
.Split('/')
.Select(t => t.Replace("<slash>", "/"))
.ToArray();
return TraceFolderPathRec(tokens, WellKnownFolderName.MsgFolderRoot);
}
No, you don't need recursion and you efficiently go straight to the folder. This uses the same extended property as Tom, and uses it to apply a search filter:
using Microsoft.Exchange.WebServices.Data; // from nuget package "Microsoft.Exchange.WebServices"
...
private static Folder GetOneFolder(ExchangeService service, string folderPath)
{
var propertySet = new PropertySet(BasePropertySet.IdOnly);
propertySet.AddRange(new List<PropertyDefinitionBase> {
FolderSchema.DisplayName,
FolderSchema.TotalCount
});
var pageSize = 100;
var folderView = new FolderView(pageSize)
{
Offset = 0,
OffsetBasePoint = OffsetBasePoint.Beginning,
PropertySet = propertySet
};
folderView.Traversal = FolderTraversal.Deep;
var searchFilter = new SearchFilter.IsEqualTo(ExchangeExtendedProperty.FolderPathname, folderPath);
FindFoldersResults findFoldersResults;
var baseFolder = new FolderId(WellKnownFolderName.MsgFolderRoot);
var localFolderList = new List<Folder>();
do
{
findFoldersResults = service.FindFolders(baseFolder, searchFilter, folderView);
localFolderList.AddRange(findFoldersResults.Folders);
folderView.Offset += pageSize;
} while (findFoldersResults.MoreAvailable);
return localFolderList.SingleOrDefault();
}
...
public static class ExchangeExtendedProperty
{
/// <summary>PR_FOLDER_PATHNAME String</summary>
public static ExtendedPropertyDefinition FolderPathname { get => new ExtendedPropertyDefinition(0x66B5, MapiPropertyType.String); }
}
The path will need to be prefixed with a backslash, ie. \Inbox\test\final.

Categories