Showing posts with label C#. Show all posts
Showing posts with label C#. Show all posts

Friday, January 19, 2018

CommonAssemblyInfo

I'm writing this post since I posted this on Twitter, and within half an hour, was asked to share my knowledge:


In Visual Studio, there's a little known feature that lets you add files as links to a project. Using this trick, we can also ease maintenance slightly by having a CommonAssemblyInfo.cs file where common attributes across the assemblies in a solution can be applied, but maintained in a single file.

Here is the sample demo application to show the steps one normally goes through. We have an existing command line utility to calculate tax based on an entered amount and tax rate. Quite simple.


In the Properties folder there is an AssemblyInfo.cs file listing the various attributes applied to the assembly.


This isn't always set up as expected, because most people forget to look at this file. One thing that stands out for example is the Company and Copyright information is defaulted sometimes to Microsoft or some other vendor based on the system settings. This may be undesirable, and so we can update these settings.


See some links at the bottom of this page for more info on these attributes.



You can see these are shown in the properties on the executable that is built.

Maybe we go back to look at our application and want to separate out the domain aspects of the console application, perhaps because in future we may want to share that with a web application frontend. The first step one would do is creating a new project and moving the classes around, fixing references, etc.


But you'll notice that most of these attributes are the same. The only two that maybe would be different are actually just the title of the assembly and the Guid it uses. If we rename things someday, we'd have to update at least two files (maybe we rename CommonAssemblyInfoDemo to something more useful, for example).

The first thing I usually do when creating a solution with more than one project, is first creating a file called CommonAssemblyInfo.cs that lives in the same directory as the solution file (CommonAssemblyInfoDemo.sln). This is usually done outside of Visual Studio, and then using Visual Studio to add the existing item to the solution, to make it easier to edit. I also setup solution folders that are numbered to keep things nicely organised.


You'll notice that only the common attributes are left in this file. But we still need to incorporate it into the other projects.

To do this, we also need to add it as an existing item to each project, however, there is a trick - we need to add it as a link.




You'll notice in the Solution Explorer that the icon of the file has a link symbol in it, to indicate it's just a reference to an existing file, so that all links refer to the same file.

Now we just update each projects AssemblyInfo to just include the information related specifically to that project.


If we need to update the AssemblyVersion across the entire solution, it's now a simple matter of just updating the CommonAssemblyInfo file to do this, instead of going through each project and remembering to update each one.

Hope this helped someone, the extra effort when setting up a new project can sometimes save time in the future.

For example, if we had a continuous integration build, and we just wanted to update the version number across all these artifacts based on some number set in our CI system, we could just update the CommonAssemblyInfo file before we run the solution through the build.

Links




Wednesday, February 15, 2017

C# - Testing that different cultures won't affect formatting

Ever worked on a system where you write code and test it and it all works perfect, but then maybe a unit test starts failing on a build server, or maybe a report looks wrong to the consumers of the report, all because it formatted a number to "12345,67" instead of "12345.67"?

This tip will help you.

First, lets assume we have this code:

public class ReportFormatter
{
  public string Format(decimal value)
  {
    return value.ToString();
  }
}

And a nice little unit test for it:

[TestClass]
public sealed class ReportFormatterTest
{
  [TestMethod]
  public void Format()
  {
    var sut = new ReportFormatter();
    
    var result = sut.Format(12345.67M);
    
    Assert.AreEqual("12345.67", result);
  }
}

This works perfectly fine. Lets even imagine that all our machines all have the same setup, and all are set to use the same regional settings. Great, nothing should ever break.

Until maybe Microsoft releases a patch to Windows that changes our regional settings to be "correct" - in fact, South Africa should be using a comma as a separator... even though none of us use this standard :D

So then it breaks our code, and our business rules that disagree with it.

Well, the good news is we can change the regional settings of the running thread, by changing its CultureInfo details. Here is a little utility class to do so:

public class TemporaryCultureSwitch : IDisposable
{
  private readonly CultureInfo _originalCulture;
  private readonly CultureInfo _originalUICulture;
  
  public TemporaryCultureSwitch(CultureInfo cultureInfo)
  {
    _originalCulture = Thread.CurrentThread.CurrentCulture;
    _originalUICulture = Thread.CurrentThread.CurrentUICulture;
    
    Thread.CurrentThread.CurrentCulture = cultureInfo;
    Thread.CurrentThread.CurrentUICulture = cultureInfo;
  }
  
  public TemporaryCultureSwitch(string cultureName) : this(new CultureInfo(cultureName)) { }
  
  public void Dispose()
  {
    Thread.CurrentThread.CurrentCulture = _originalCulture;
    Thread.CurrentThread.CurrentUICulture = _originalUICulture;
  }
}

We can now update our test to be a bit more specific:

[TestMethod]
public void FormatShouldNotBeAffectedByCultureChanges()
{
  var culture = new CultureInfo("en-ZA");
  culture.NumberFormat.NumberDecimalSeparator = ",";
  using (new TemporaryCultureSwitch(culture))
  {
    var sut = new ReportFormatter();
    
    var result = sut.Format(12345.67M);
    
    Assert.AreEqual("12345.67", result);
  }
}

Now we have a test that will fail consistently! Time to fix the code. One way of doing this is realizing that there is an overload of Decimal.ToString that takes in a CultureInfo object. We actually can use the InvariantCulture as below:

public string Format(decimal value)
{
  return value.ToString(CultureInfo.InvariantCulture);
}

The test passes and we now know for sure that regional settings won't affect our code.

Wednesday, February 22, 2012

A look at ASP.NET MVC 4

I just watched the talk by Scott Guthrie at Techdays 2012 in the Netherlands entitled "A look at ASP.NET MVC 4", see the video at the bottom of this post if you want.

In the talk, Scott talks about some of the new features in ASP.NET 4, as well as touching on some new features of Entity Framework Code First. The highlights of the features are:

  • Database Migrations
  • Bundling/Minification Support
  • Web APIs
  • Mobile Web
  • Real Time Communication (SignalR)
  • Asynchronous Support using language features (async and await)

An extremely useful addition to EF Code First is that of database migrations, allowing you to progressively develop your code and database. Migrations allow you to deploy/rollback different versions of your database. Each migration can add/remove its parts to the database (e.g. adding a column when migrating up, or removing the column when migrating down, even perhaps extracting data to a temp table and processing it during a possibly destructive migration). The one project I was on had a whole custom written database patching/versioning framework which enabled true integration testing, as well as generation of deployment scripts, which EF Code First with migrations can probably provide out of the box now. Very nice.

The bundling and minification support is a welcome addition too. By convention, instead of referencing specific scripts or CSS, if you reference a folder, all the relevant resources in that folder will be bundled and processed together. An HTML helper is also available which also provides versioning of the bundles by appending a hash of the resources to the query string. Custom bundles can be defined and custom processors can be implemented as well, for example, in the talk Scott illustrates that you could use CoffeeScript and LESS processors in a bundle, greatly improving a web developers life.

The WCF Web API, is now part of ASP.NET and is now known as the ASP.NET Web API. It provides the power of WCF with the ease of ASP.NET MVC, while respecting the HTTP protocol a lot more. It provides built in support for writing code once and supporting multiple response types (JSON/XML), OData for querying, filtering and sorting data by just adding to the query string (no code change... as long as your code supports returning an IQueryable), and also provides a nicer programming model for HTTP responses.

The default MVC project templates come with CSS that uses media queries for a more adaptive feel to the application. And for scenarios where media queries aren't enough, there's also support for detecting a mobile client, and returning a completely different template or view for a request. This allows for creating a single web application, but catering for a wider range of clients.

The Real Time Communication with SignalR allows for the server to push through to the clients. SignalR can detect if the client supports WebSockets, or if not falls back to various methods that enable the expected behaviour, e.g. long polling.

The Async support just leverages existing asynchronous controller functionality, but allows you to do so using the async and await keywords that are part of the next version of .NET.

I highly suggest that you watch the whole video, as always, the Gu's talk is full of useful information:

Tuesday, May 10, 2011

Visual Studio Clipboard Ring

Today I found out about the Visual Studio Clipboard Ring, which keeps the last 15 entries that you have entered into the clipboard using copy and cut (CTRL+C, CTRL+V, SHIFT+DEL), and lets you paste into the Visual Studio Editor an item from that list.

You just press CTRL+SHIFT+V when pasting, and each successive press will cycle through the entries. Very useful if you kept something on the clipboard, but then pressed SHIFT+DEL to delete a line, forgetting about what you wanted to paste! Which I tend to do a lot!

Wednesday, February 16, 2011

Xml and XPath queries in .NET with C#

Querying Xml should be easy using XPath. But for some reason I had an issue where for the life of me I was sure the XPath should have been returning something, but was returning nothing. The issue turned out to be because I wasn't specifying namespaces.

The below Unit Tests illustrate the ways that work and don't work (you will need a reference to System.Xml in the project).

Note: I must update this to use a nice code formatter, used Wordpress before, think Blogger doesn't have it out the box.


using System.Xml;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace XmlTestProject
{
    [TestClass]
    public class XmlTests
    {
        /// 
        /// A test to see if we can query using XPath correctly when there is no namespace defined.
        /// This test passes.
        /// 
        [TestMethod]
        [TestCategory("Unit")]
        public void CanQueryWhenNoNamespace()
        {
            string xml = @"Jamesj@miebarrow.comjames@work.co.za";

            string emailsXpath = "//email";

            XmlDocument doc = new XmlDocument();
            doc.LoadXml(xml);

            XmlNodeList emails = doc.SelectNodes(emailsXpath);

            Assert.AreEqual(2, emails.Count, "There should be exactly two nodes retrieved");
            XmlNode emailNode = emails.Item(0);
            Assert.AreEqual("email", emailNode.Name, "The first node should be an email element");
            Assert.AreEqual("j@miebarrow.com", emailNode.InnerText, "The inner text of the email element should be the email address");
        }

        /// 
        /// A test to see if we can query using XPath correctly when there is a namespace defined,
        /// using the same method as when no namespace is defined.
        /// This test fails.
        /// 
        [TestMethod]
        public void CanQueryWhenDefaultNamespace()
        {
            string xml = @"Jamesj@miebarrow.comjames@work.co.za";

            string emailsXpath = "//email";

            XmlDocument doc = new XmlDocument();
            doc.LoadXml(xml);

            XmlNodeList emails = doc.SelectNodes(emailsXpath);

            Assert.AreEqual(2, emails.Count, "There should be exactly two nodes retrieved");
            XmlNode emailNode = emails.Item(0);
            Assert.AreEqual("email", emailNode.Name, "The first node should be an email element");
            Assert.AreEqual("j@miebarrow.com", emailNode.InnerText, "The inner text of the email element should be the email address");
        }

        /// 
        /// A test to see if we can query using XPath correctly when there is a namespace defined,
        /// by providing the namespaces to the SelectNodes function, and explicitly using the
        /// namepsace prefix in our XPath expression.
        /// This test passes, and is the correct way to achieve our goal.
        /// 
        [TestMethod]
        public void CanQueryWhenDefaultNamespaceAndUsingNamespaceManager()
        {
            string xml = @"Jamesj@miebarrow.comjames@work.co.za";

            string emailsXpath = "//jb:email";

            XmlDocument doc = new XmlDocument();
            doc.LoadXml(xml);

            XmlNamespaceManager namespaceManager = new XmlNamespaceManager(doc.NameTable);
            namespaceManager.AddNamespace("jb", "http://jamiebarrow.com/2011/02/16");

            XmlNodeList emails = doc.SelectNodes(emailsXpath, namespaceManager);

            Assert.AreEqual(2, emails.Count, "There should be exactly two nodes retrieved");
            XmlNode emailNode = emails.Item(0);
            Assert.AreEqual("email", emailNode.Name, "The first node should be an email element");
            Assert.AreEqual("j@miebarrow.com", emailNode.InnerText, "The inner text of the email element should be the email address");
        }

        /// 
        /// A test to see what happens when we specify a default namespace, and also a different
        /// namespace, and perform a query using the non-default namespace.
        /// This test passes.
        /// Note that the element we retrieve is fully qualified with a namespace prefix, whereas
        /// in previous examples, the elements were part of the default namespace and had were
        /// not qualified with a prefix.
        /// 
        [TestMethod]
        public void CanQueryWhenDefaultNamespaceAndUsingNamespaceManagerWithMixedNamespaces()
        {
            string xml = @"Jamesj@miebarrow.comjames@work.co.zaanother@example.com";

            string emailsXpath = "//test:email";

            XmlDocument doc = new XmlDocument();
            doc.LoadXml(xml);

            XmlNamespaceManager namespaceManager = new XmlNamespaceManager(doc.NameTable);
            namespaceManager.AddNamespace("jb", "http://jamiebarrow.com/2011/02/16");
            namespaceManager.AddNamespace("test", "http://test.org/");

            XmlNodeList emails = doc.SelectNodes(emailsXpath, namespaceManager);

            Assert.AreEqual(1, emails.Count, "There should be exactly two nodes retrieved");
            XmlNode emailNode = emails.Item(0);
            Assert.AreEqual("test:email", emailNode.Name, "The first node should be an email element, qualified by a namespace prefix since we are querying the non-default namespace");
            Assert.AreEqual("another@example.com", emailNode.InnerText, "The inner text of the email element should be the email address");
        }
    }
}