What exception should I throw for a failed file parsing? - c#

I have a method parsing a file. However, this parsing could fail anytime, depending on various conditions (not-so-cautious user playing with the file, for example).
public string ParseDatFile(string datFile)
{
string[] deezLines = File.ReadAllLines(datFile);
// We're searching for an essential data inside the file.
bool daEssentialDataFound = false;
foreach (string datLine in deezLines)
{
if (datLine.Contains("daEssentialData"))
{
daEssentialDataFound = true;
break;
}
}
if (!daEssentialDataFound)
throw new WhatShouldIThrowException("yo dood where's da essential data in " + datFile + "?");
DoStuffWith(deezLines);
}
Is there an exception I could use in such a scenario? I thought about:
FormatException: not really clear about the issue,
Custom exception: I do not have any special treatment for the exception I'm throwing, so I'd rather avoid using a custom exception, even though that's always a way to settle things down.

FileFormatException should be fine :
The exception that is thrown when an input file or a data stream that
is supposed to conform to a certain file format specification is
malformed.
You can optionally provide the uri and a descriptive error message.
If you don't want to reference WindowsBase then you may create your own exception specific to your format. Based on the fact there is an XmlException thrown by XmlReader.Read.

I would throw a custom exception since that increases readability and allows to catch that specifc exception:
public class InvalidFileFormatException : System.FormatException
{
public InvalidFileFormatException(string exText) : base(exText) { }
}
// you could even provide an exception if a single line has an invalid format
public class SpecificLineErrorException : InvalidFileFormatException
{
public string Line { get; set; }
public SpecificLineErrorException(string exText, string line) : base(exText)
{
this.Line = line;
}
}
Now your method can look like(also linqified it a little bit:
public string ParseDatFile(string datFile)
{
string[] deezLines = File.ReadAllLines(datFile);
if(!deezLines.Any(l => l.Contains("daEssentialData")))
throw new InvalidFileFormatException("yo dood where's da essential data in " + datFile + "?");
DoStuffWith(deezLines);
}

Related

Prevent error if txt/json file is not in the right format (corrupted) before reading it

I have an app that reads and rights to a txt file in a json format. Everything is working fine except that from time to time the txt/json file for some reason becomes corrupted and the app crashes when trying to read it.
Here is the code...
User Class
public class User
{
public string UserName { get; set; }
}
usersFile.txt (json)
[{"UserName":"someUserName"}]
Reading Class
public static string myUsersFolder = #"c:\myUsersFilder";
string usersFile = Path.Combine(myUsersFolder, "usersFile.txt");
public void readUsersFromFile()
{
try
{
if (!File.Exists(usersFile))
throw new FileNotFoundException();// throws an exception if the file is not found
string jsonContent = File.ReadAllText(Path.Combine(myUsersFolder, usersFile));
List<User> users = JsonConvert.DeserializeObject<List<User>>(jsonContent);
foreach (var u in users)
{
User user = new User();
user.UserName = u.UserName;
UsersObservableCollection.Add(user);
}
}
catch (FileNotFoundException f)
{
Console.WriteLine("Couldn't read users from file: " + f.Message);
}
}
Error
'The invocation of the constructor on type 'MyProgramName.ViewModel.ViewModelLocator' that matches the specified binding constraints threw an exception.'
The issue is that if at some point the file usersFile.txt becomes corrupted, where the format is not right, for instance missing a } curly bracket, the app crashes.
[{"UserName":"someUserName"] // missing a curly bracket, app crashes
How can I prevent the app from crashing if the file is in the wrong format?
You should use the .NET JSON serializer (System.Text.Json). The .NET JSON library has a modern async API (How to serialize and deserialize (marshal and unmarshal) JSON in .NET.
The original exception you are experiencing is the result of the failed deserialization. You could (but shouldn't) wrap the deserialization part alone into a try-catch block. Also don't explicitly throw an exception (FileNotFoundException) just to catch it (even in the same context). Rather show the error message directly (or log it).
Exceptions are very expensive. You would always try to avoid them. Checking if the file exists successfully avoids the FileNotFoundException. Job done.
For this sake, the correct approach would be to validate the JSON before you try to deserialize it: avoid the exception instead of handling it.
The following example shows a fixed version of your code. It also incorporates JSON validation to avoid any malformed input related exceptions.
The example also uses the StreamReader to read from the file asynchronously. this helps to avoid freezing your application during file handling. Generally use async APIs where possible.
public async Task readUsersFromFileAsync()
{
if (!File.Exists(usersFile))
{
Console.WriteLine("Couldn't read users from file: " + f.Message);
}
using var fileReader = new StreamReader(Path.Combine(myUsersFolder, usersFile));
string jsonContent = await fileReader.ReadToEndAsync();
if (!await TryValidateJsonInputAsync(jsonContent, out IList<string> errorMessages))
{
foreach (string errorMessage in errorMessages)
{
Console.WriteLine(errorMessage);
}
return;
}
List<User> users = JsonConvert.DeserializeObject<List<User>>(jsonContent);
foreach (var u in users)
{
User user = new User();
user.UserName = u.UserName;
UsersObservableCollection.Add(user);
}
}
private Task<bool> TryValidateJsonInputAsync(string jsonContentToValidate, out IList<string> messages)
{
var jsonSchemaText = new StreamReader("json_schema.txt");
JSchema jsonSchema = JSchema.Parse(jsonSchemaText);
JObject jsonToValidate = JObject.Parse(jsonContentToValidate);
return jsonToValidate.IsValid(jsonSchema, out messages);
}
The example JSON schema used in the example (json_schema.txt file).
{
'description': 'A person',
'type': 'object',
'properties': {
'name': {'type': 'string'},
'hobbies': {
'type': 'array',
'items': {'type': 'string'}
}
}
}
See the Newtonsoft documentation Validating JSON to get more detailed information on how to validate JSON input.
But again, I recommend the JsonSerializer from the System.Text.Json namespace as it offers an async API.

Logging reserved words/characters

I have a *.resx string that looks like this:
Failed to deserialize an object of type '{0}' from the following string:{1}{2}
This string is being used to log such kinds of errors and currently, the logging statement looks like this:
_logger.LogError(Resources.FailedToDeserialize, typeof(MyType).Name, Environment.NewLine, invalidJsonString);
As you can see - I need to pass Environment.NewLine each time to display my logs correctly for any OS.
I am curious are there any reserved string interpolation words/characters to insert such values?
For example, my string could look like this:
Failed to deserialize an object of type '{0}' from the following string:{NewLine}{2}
And my logging statement would be a bit simpler:
_logger.LogError(Resources.FailedToDeserialize, typeof(MyType).Name, invalidJsonString);
One thing you can do is some form of pre processing on application start up by reading the resource file, replacing your keyword of choice i.e {NewLine} with Environment.NewLine and then use that cached string for the entirety of your application life time.
You can make the fields readonly and do some reflection magic to set the value but this example should give you an idea of how to solve your current problem.
public static class LoggingMessageTemplates
{
//Reference your resource here e.g Resource.FailedToDeserialize
public static string FailedToDeserialize = "Resource.Something {NewLine} Something Else";
public static void FormatMessages()
{
var stringFields = typeof(LoggingMessageTemplates)
.GetFields()
.Where(x => x.FieldType == typeof(string));
foreach(var field in stringFields)
{
if (field.GetValue(null) is not string fieldValue)
{
throw new InvalidCastException($"Failed to cast field {field.Name} to string.");
}
field.SetValue(null, fieldValue.Replace("{NewLine}", Environment.NewLine));
}
}
}
//On application startup, format the resources to use the Environment.NewLine char of the current system.
LoggingMessageTemplates.FormatMessages();
//When logging, reference the LoggingMessageTemplates class rather than the direct resource.
Console.WriteLine(LoggingMessageTemplates.FailedToDeserialize);
//i.e
_logger.LogError(LoggingMessageTemplates.FailedToDeserialize, typeof(MyType).Name, invalidJsonString);

project.Models.tableName:The field pvid must be a string or array type with a maximum length of '20'

I encounter exception error below when i try to add a new entry in the database with :
db.patient_visit.Add(pv);
db.SaveChanges();
An exception of type 'System.InvalidOperationException' occurred in project.dll but was not handled in user code
Additional information: project.Models.tableName:The field pvid must be a string or array type with a maximum length of '20'.
Before this, my model is automatically and originally set like below because i did Code-First migration with existing database.
[Key]
[StringLength(20)]
public string pvid { get; set; }
Logically, it looks like it is already in the range. But for the sake of trying, i tried changing it to code below, and did add-migration. Then, in the Up() method, its empty. Does it mean that the modification is not recognized? Because when i tried running the project again, i encountered the same error as above.
[Key]
[MaxLength(20), MinLength(12)]
public string pvid { get; set; }
Then when i put breakpoints at my query, i see that:
InnerException: null
Message: The data reader is incompatible with the specified 'project.Models.tableName'. A member of the type 'columnName', does not have a corresponding column in the data reader with the same name.
Is there something wrong with my query?
ChargeVM objDetails = new ChargeVM();
var query3 = db.Database.SqlQuery<patient_visit>("SELECT MAX(CAST(SUBSTRING(pvid, 3, 10) AS int)) FROM patient_visit");
if (query3 != null)
{
objDetails.pvid = query3.ToString();
objDetails.pvid += 1;
objDetails.pvid = "PV" + objDetails.pvid;
}
patient_visit pv = new patient_visit()
{
pvid = objDetails.pvid,
paid = paid
};
db.patient_visit.Add(pv);
I put a try catch for the db.SaveChanges() to catch any exceptions, and at line "throw raise", the exception error that i mentioned in the first paragraph pops up.
try
{
db.SaveChanges();
}
catch (System.Data.Entity.Validation.DbEntityValidationException dbEx)
{
Exception raise = dbEx;
foreach (DbEntityValidationResult validationErrors in dbEx.EntityValidationErrors)
{
foreach (DbValidationError validationError in validationErrors.ValidationErrors)
{
string message = string.Format("{0}:{1}", validationErrors.Entry.Entity.ToString(), validationError.ErrorMessage);
// raise a new exception nesting
// the current instance as InnerException
raise = new InvalidOperationException(message, raise);
}
}
throw raise;
}
Actually i just switched to C# recently and i simply converted my codes from vb.net to C#. I am wondering if it was a problem with the syntax or anything because it was running smoothly previously with vb.net. Could anyone help point it out to me please? I'm new to this.

How do I get the current line number?

Here is an example of what I want to do:
MessageBox.Show("Error line number " + CurrentLineNumber);
In the code above the CurrentLineNumber, should be the line number in the source code of this piece of code.
How can I do that?
In .NET 4.5 / C# 5, you can get the compiler to do this work for you, by writing a utility method that uses the new caller attributes:
using System.Runtime.CompilerServices;
static void SomeMethodSomewhere()
{
ShowMessage("Boo");
}
...
static void ShowMessage(string message,
[CallerLineNumber] int lineNumber = 0,
[CallerMemberName] string caller = null)
{
MessageBox.Show(message + " at line " + lineNumber + " (" + caller + ")");
}
This will display, for example:
Boo at line 39 (SomeMethodSomewhere)
There's also [CallerFilePath] which tells you the path of the original code file.
Use the StackFrame.GetFileLineNumber method, for example:
private static void ReportError(string message)
{
StackFrame callStack = new StackFrame(1, true);
MessageBox.Show("Error: " + message + ", File: " + callStack.GetFileName()
+ ", Line: " + callStack.GetFileLineNumber());
}
See Scott Hanselman's Blog entry for more information.
[Edit: Added the following]
For those using .Net 4.5 or later, consider the CallerFilePath, CallerMethodName and CallerLineNumber attributes in the System.Runtime.CompilerServices namespace. For example:
public void TraceMessage(string message,
[CallerMemberName] string callingMethod = "",
[CallerFilePath] string callingFilePath = "",
[CallerLineNumber] int callingFileLineNumber = 0)
{
// Write out message
}
The arguments must be string for CallerMemberName and CallerFilePath and an int for CallerLineNumber and must have a default value. Specifying these attributes on method parameters instructs the compiler to insert the appropriate value in the calling code at compile time, meaning it works through obfuscation. See Caller Information for more information.
I prefer one liners so:
int lineNumber = (new System.Diagnostics.StackFrame(0, true)).GetFileLineNumber();
In .NET 4.5 you can get the line number by creating the function:
static int LineNumber([System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0)
{
return lineNumber;
}
Then each time you call LineNumber() you will have the current line. This has the advantage over any solution using the StackTrace that it should work in both debug and release.
So taking the original request of what is required, it would become:
MessageBox.Show("Error enter code here line number " + LineNumber());
This is building on the excellent answer by Marc Gravell.
For those who need a .NET 4.0+ method solution:
using System;
using System.IO;
using System.Diagnostics;
public static void Log(string message) {
StackFrame stackFrame = new System.Diagnostics.StackTrace(1).GetFrame(1);
string fileName = stackFrame.GetFileName();
string methodName = stackFrame.GetMethod().ToString();
int lineNumber = stackFrame.GetFileLineNumber();
Console.WriteLine("{0}({1}:{2})\n{3}", methodName, Path.GetFileName(fileName), lineNumber, message);
}
How to call:
void Test() {
Log("Look here!");
}
Output:
Void Test()(FILENAME.cs:104)
Look here!
Change the Console.WriteLine format how you like!
If its in a try catch block use this.
try
{
//Do something
}
catch (Exception ex)
{
System.Diagnostics.StackTrace trace = new System.Diagnostics.StackTrace(ex, true);
Console.WriteLine("Line: " + trace.GetFrame(0).GetFileLineNumber());
}
You only asked about the line number and with nullable project type, you then need to use a something like this
internal class Utils
{
public static int Line([CallerLineNumber] int? lineNumber =null)=>lineNumber;
}
in your code, if you like to get a line number you then just call
var line=Utils.Line();
if you are logging and you would like to document the line number in say logging than call the method like this
public void MyMethod(int someValue)
{
switch(someValue)
{
case 1:
if(abc<xyz)
{
logger.LogInformation("case value {someValue} this line {line} was true", someValue ,Utils.Line()-2);
}
break;
case 2:
logger.LogInformation("case value {someValue} this {line} was executed",someValue,Utils.Line());
break;
caste 3:
logger.LogInformation("case value {someValue} this {line} was executed",someValue,Utils.Line());
break;
}
}
You can extend this pattern with any of the other [CallerXXX] methods and not use them where ever, not just in the method parameters.
in the Nuget Package Walter I use a super cool class named ExceptionObject
if you import the NuGet package you have some nice extension methods on the Exception class as well as access to a CallStack showing the call chain including method parameters and parameter values of all methods called.
It's like a stack of an exception only with values showing how you got where you got with what values.
public void MyMethod()
{
try
{
//get me all methods, signatures, parameters line numbers file names etc used as well as assembly info of all assemblies used for documentation of how the code got here
var stack= new CallStack();
foreach( var frame in StackedFrames)
{
logger.LogDebug(frame.ToString());
}
}
catch(SqlException e)
{
var ex = new ExceptionObject(e);
logger.LogException(e,"{this} exception due to {message} {server} {procedure} TSQL-line:{sqlline}\n{TSQL}"
,e.GetType().Name
,e.Message
,ex.SqlServer
,ex.SqlProcedureName
,ex.SqlLineNumber
,ex.Tsql
,ex.CallStack);
}
catch(Exception e)
{
var ex = new ExceptionObject(e);
logger.LogException(e,"{this} exception due to {message} signature: signature}\nCallStack:", e.GetType().Name,e.Message,ex.Signature,ex.CallStack);
}
}

user defined Error collection in Visual C#

I want to write an user defined error collection class which should collect all the Error's. When we validate an entity object if there is no error it should go and save to the Database. if Error there it should display it.
now i have wrote the class it collects the error and displays it successfully but when there is two identical error the class throws an exception.
(i use error-code for the error. the value for the error-code is in resx file from where the display method will take the value and display it. Display works perfectly)
//The code where it collects Error
if (objdepartment.Departmentname == null)
{
ErrorCollection.AddErrors("A1001","Department Name");
}
if (objdepartment.Departmentcode == null)
{
ErrorCollection.AddErrors("A1001","Department code");
}
//In the Errorcollection
public class ErrorCollection
{
static Dictionary<string,List<string>> ErrorCodes;
private ErrorCollection() { }
public static void AddErrors(string eCode,params string[] dataItem)
{
if (ErrorCodes == null)
{
ErrorCodes = new Dictionary<string, List<string>>();
}
List<String> lsDataItem = new List<String>();
foreach (string strD in dataItem)
lsDataItem.Add(strD);
ErrorCodes.Add(eCode, lsDataItem);
}
public static string DisplayErrors()
{
string ErrorMessage;
//string Key;
ErrorMessage = String.Empty;
if (ErrorCodes != null)
{
string Filepath= "D:\\Services\\ErrorCollection\\";
//Read Errors- Language Specsific message from resx file.
ResourceManager rm = ResourceManager.CreateFileBasedResourceManager("ErrorMessages", Filepath, null);
StringBuilder sb = new StringBuilder();
foreach (string error in ErrorCodes.Keys)
{
List<string> list = ErrorCodes[error];
if (error == "A0000")
{
sb.Append("System Exception : " + list[0]);
}
else
{
sb.Append(rm.GetString(error) + "\nBreak\n");
}
for (int counter = 0; counter < list.Count; counter++)
{
sb.Replace("{A}", list[counter]);
}
}
ErrorMessage = sb.ToString();
}
return ErrorMessage;
}
}
now when there is two common error. then the code shows an exception like "datakey already exist" in the line " ErrorCodes.Add(eCode, lsDataItem);" (the italic part where the exception throwed)
Well for one thing, having this statically is a terrible idea. You should create an instance of ErrorCollection to add the errors to IMO, and make the variable an instance variable instead of static.
Then you need to take a different approach within AddErrors, presumably adding all the new items if the key already exists. Something like this:
List<string> currentItems;
if (!ErrorCodes.TryGetValue(eCode, out currentItems))
{
currentItems = new List<string>);
ErrorCodes[eCode] = currentItems;
}
currentItems.AddRange(dataItem);
You are adding "A1001" twice as a key in a dictionary. That simply isn't allowed. However, more urgently - why is that dictionary static? That means that everything, anywhere, shares that error collection.
Suggestions:
make that not static (that is a bad idea - also, it isn't synchronized)
check for existence of the key, and react accordingly:
if(ErrorCodes.ContainsKey(eCode)) ErrorCodes[eCode].AddRange(lsDataItem);
else ErrorCodes.Add(eCode, lsDataItem);
As an aside, you might also consider implementing IDataErrorInfo, which is a built-in standard wrapper for this type of functionality, and will provide support for your error collection to work with a few standard APIs. But don't rush into this until you need it ;p

Categories

Resources