Extension methods in .NET part 3: mixture of useful methods
March 13, 2014 Leave a comment
Introduction
So far in this series we’ve looked at the foundations of extension methods in C#. We’ve also seen a number of examples and special cases. We’ll continue our discussion of with a mixture of useful applications of extension methods.
Exceptions
Extending exceptions can be useful if you’d like to get a user-friendly output of wrapped exceptions. You normally don’t want to show the full stacktrace of the top-level and wrapped exceptions to a user. The following extension method collects the exception message from the inner exceptions as long as they are not null:
public static string UserFriendlyFullMessage(this Exception exception) { StringBuilder exceptionBuilder = new StringBuilder(); while (exception != null) { exceptionBuilder.AppendLine(exception.Message); exception = exception.InnerException; } return exceptionBuilder.ToString(); }
Let’s say you have the following method:
private static void TestExceptionExtension(int divideBy) { try { int res = 10 / divideBy; } catch (Exception ex) { InvalidOperationException ioex = new InvalidOperationException("What are you doing???", ex); string errorMessage = string.Concat("Division failed. Tried to divide by ", divideBy); throw new ApplicationException(errorMessage, ioex); } }
Call the method as follows and print the exception messages on the console window:
try { TestExceptionExtension(0); } catch (Exception ex) { String messages = ex.UserFriendlyFullMessage(); Console.WriteLine(messages); }
The output is:
Division failed. Tried to divide by 0
What are you doing???
Attempted to divide by zero.
This is a cleaner presentation of the exceptions to a user who is not familiar with stacktraces and should not even be able to view them.
Enumerations
Consider the following enumeration:
public enum Difficulty { Easy , Medium , Hard , Master }
We can extend enumerations the same way as other objects. The following extension method will hide the ugly Enum.GetName call behind a friendly method signature:
public static class EnumExtensions { public static string ToUserFriendlyString(this Enum value) { return Enum.GetName(value.GetType(), value); } }
You can call this extension method on any enumeration:
string val = Difficulty.Easy.ToUserFriendlyString();
You can add an extended description to a specific enum value as follows:
public enum Difficulty { [Description("This is the easy level")] Easy , Medium , Hard , Master }
…where the Description attribute is found in the System.ComponentModel namespace so you’ll need to reference the correct library.
You can read the detailed description using Reflection. We’ll hide the ugly details behind another extension method:
public static string GetDetailedDescription(this Enum value) { FieldInfo fieldInfo = value.GetType().GetField(value.ToUserFriendlyString()); DescriptionAttribute descriptionAttribute = fieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), false).FirstOrDefault() as DescriptionAttribute; return descriptionAttribute == null ? value.ToUserFriendlyString() : descriptionAttribute.Description; }
We check if there’s a Description attribute on the enumeration value. If that’s the case then we return the description. Otherwise we’ll use the extension method defined above to return the default value.
You can call these methods as follows:
string val = Difficulty.Easy.ToUserFriendlyString(); string desc = Difficulty.Easy.GetDetailedDescription();
Web API
If Web API doesn’t tell you much then check out its home page. It is a great technology to create RESTful web services. According to RFC 2616, i.e. the HTTP protocol specifications, if a new resource is created with a POST or PUT request then the service should respond with a 201 status code – meaning Created – and a Location header specifying the URL of the newly created resource. Example: “Location: http://api.mysite.com/products/10”.
It’s customary for Web API controllers to return a HttpResponseMessage object to the client. It can respond with other objects, such as a string, but the HttpResponseMessage object is very rich. You can set the status code, the content body, the HTTP response headers etc.
The solution will therefore extend the HttpResponseMessage object. It will also accept an HttpRequestMessage object which includes the details about the original incoming request. The request is necessary so that we can find the original URI that was used for the POST/PUT operation. We’ll follow the convention that individual resources can be located by an ID parameter in the URL like in the example above: /products/10.
For the solution to work the Controller that processes the POST/PUT request must also be able to process the GET. This is so that URL in the POST/PUT request can be used to extract the URL for the GET in the Location header. Also, the Controller must have a GET method that returns a resource by its id. The following extension method will do the job:
public static void InsertLocationHeader(this HttpResponseMessage httpResponseMessage, HttpRequestMessage httpRequestMessage, int idOfNewResource) { string uri = string.Concat(httpRequestMessage.RequestUri, "/", idOfNewResource); httpResponseMessage.Headers.Location = new Uri(uri); }
We don’t need to return anything from this method. We just take advantage of the fact that objects are reference types and therefore we can modify the state of the httpResponseMessage object we’re extending.
The method can be called as follows:
HttpResponseMessage httpResp = //build the response message; httpResp.InsertLocationHeader(Request, 20);
The original request can be acquired using the built-in Request property.
Epoch datetime
There’s no built in method in .NET to convert DateTime objects to UNIX timestamps, i.e. to the seconds passed since the midnight of Jan 01 1970. Nothing could be simpler with an extension method:
public static long ToUnixTimeStamp(this DateTime dateTimeUtc) { DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0); TimeSpan timeSpan = dateTimeUtc - epoch; return Convert.ToInt64(timeSpan.TotalSeconds); }
We construct a DateTime with the starting point for the Unix timestamp. We get the time span between ‘now’ and the start date. Finally we extract the seconds passed during that time span.
SecureString
The SecureString object can be used to keep confidential text securely in memory. The following extension method will convert a string to a SecureString:
public static SecureString ToSecureString(this string s) { if (s == null) return null; SecureString psw = new SecureString(); foreach (char c in s.ToCharArray()) { psw.AppendChar(c); } return psw; }
Various
The list of useful extension methods could go on and on. By now you’ve probably got the general idea behind extension methods so you can construct your own. To finish off this short series here’s a couple more methods that you can use in your projects.
Check if integer is within some interval:
public static bool IsBetween(this int theNumber, int lower, int higher) { return (theNumber >= lower) && (theNumber <= higher); }
Remove some prefix or suffix from a string:
public static string TrimPrefix(this string str, string prefix) { if (!String.IsNullOrEmpty(str) && !String.IsNullOrEmpty(prefix) && str.StartsWith(prefix)) { return str.Substring(prefix.Length); } return str; } public static string TrimSuffix(this string str, string suffix) { if (!String.IsNullOrEmpty(str) && !String.IsNullOrEmpty(suffix) && str.EndsWith(suffix)) { return str.Remove(str.Length - suffix.Length); } return str; }
Extract the base64 encoded username and password from the Basic Auth header of a HttpRequestMessage object in Web API:
public static Tuple<string, string> ExtractUsernameAndPasswordFromHttpHeader(this HttpRequestMessage requestMessage) { Dictionary<string, string> headers = requestMessage.ReadHeaders(); if (headers.ContainsKey("Authorization")) { string authHeaderValue = headers["Authorization"]; authHeaderValue = authHeaderValue.Trim(); string encodedCredentials = authHeaderValue.Substring(6); byte[] decodedBytes = Convert.FromBase64String(encodedCredentials); string s = new ASCIIEncoding().GetString(decodedBytes); string[] userPass = s.Split(new char[] { ':' }); string username = userPass[0]; string password = userPass[1]; return new Tuple<string, string>(username, password); } else { throw new ArgumentNullException("Username and password couldn't be extracted from the HttpHeaders collection."); } }
Extract all headers from a HttpRequestMessage into a Dictionary:
public static Dictionary<string, string> ReadHeaders(this HttpRequestMessage requestMessage) { return ExtractHeaders(requestMessage.Headers); } private static Dictionary<string, string> ExtractHeaders(HttpHeaders headers) { Dictionary<string, string> dict = new Dictionary<string, string>(); foreach (var kvp in headers.ToList()) { if (kvp.Value != null) { string header = string.Empty; foreach (var j in kvp.Value) { header += j + " "; } dict.Add(kvp.Key, header); } } return dict; }
Check if an IEnumerable of T is empty:
public static bool IsEmpty<T>(this IEnumerable<T> list) { if (list == null) return true; if (list is ICollection<T>) return ((ICollection<T>)list).Count == 0; else return !list.Any(); }