In the
last post, we created the basic outline of a project that started to decouple LINQ enabled objects from the data layer. We have an ILoader interface that defines what a loader looks like, and a LinqLoader class that uses LINQ To SQL to pull data from a database.
The biggest problem with the code is that it is rather messy. Strings are hard-coded, and worse, the UI layer needs to know the data layer to create the proper ILoader. So before we dig any deeper with LINQ, let's improve the code quality in our app.
First of all, let's remove that ugly hard-coding. This is our LinqLoader constructor:
public LinqLoader() {
XmlMappingSource source = XmlMappingSource.FromUrl(@"C:\somepath\map.xml");
this.context = new DataContext(@"Data Source=localhost\SQLEXPRESS;Initial Catalog=LinqTest;Integrated Security=True", source);
}
Since the path to the map.xml file is first, let's tackle that. Looking at the API for XmlMappingSource, we can see that it has a FromStream
method. To take advantage of this, we can move the xml directly into our project and make it an embedded resource. To retrieve the file and perform other miscellaneous functions, I created a Util class.
Once that is done, we can move on to the connection string. The .NET framework provides some nice utilities for managing configuration files, so let's use one here. You could simply dump the info into the <AppSettings> tag, but let's be pragmatic and use the <connectionStrings> tag instead:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<connectionStrings>
<add name="MainDB" connectionString="Copy and paste your string here"/>
</connectionStrings>
</configuration>
The code to retrieve the connection string from the config is stored in the System.Configuration assembly, so that to your project. I also added a bit of code to our new Util class to call the appropriate methods.
Now, our constructor looks like this:
private const string MapFilename = "mappy.xml";
public LinqLoader() {
XmlMappingSource source = XmlMappingSource.FromStream(Util.LoadResource(MapFilename));
this.context = new DataContext(Util.ConnectionString, source);
}
It should be noted that I added the constant, since we still need some semblance of what the file is called. You could move that into the config file as well, but since this is now an
embedded resource, it's not going anywhere. All the hard-coded paths are now gone; we can move this assembly anywhere, and it will still work as desired.
Next up is removing the coupling between the data layer and the UI. The goal here is to completely remove any reference of the data layer and ILoader from our UI code. The UI should not ever care about the what/where/how of data.
Since we are already using a config file, let's make the data layer configurable. Again, we could dump everything into <appSettings>, but let's create a custom config section, instead. All of this code will be going into our Core library. Eventually, as we start to add more and more classes to the root, we can create a more logical folder structure, but for now there is no need for the extra work. This too will need a reference to System.Configuration.
A custom configuration requires a couple of things: A class representing the config section, and a handler that states the section to deserialize into the config object. I have a code snippet that gives me the handler without having to type it all out again. For a very robust implementation of a handler, see this blog post.
As of right now, our config class will only need one property, the typename of our ILoader implementation.
[Serializable]
[System.Xml.Serialization.XmlRoot(ConfigSectionName)]
public class Config {
internal const string ConfigSectionName = "LinqExampleConfig";
private ILoader _loader;
public string DataLoaderType { get; set; }
public static Config Settings {
get { return System.Configuration.ConfigurationManager.GetSection(ConfigSectionName) as Config; }
}
}
As a convention, I like to use a getter called "Settings." A future refactoring could be to convert the class to a true Singleton. Regardless, the property allows quick access to config values:
string s = Config.Settings.SomeProperty;
The two attributes on the class help the ConfigurationManager handle multiple custom config sections.
Now we need to add the appropriate fields to our app.config file. First is the section declaration:
<configSections>
<section name="LinqExampleConfig" type="LinqExample.Core.ConfigHandler, LinqExample.Core" requirePermission="false" />
</configSections>
Next are the values for that section:
<LinqExampleConfig type="LinqExample.Core.Config, LinqExample.Core">
<DataLoaderType>LinqExample.Data.LinqLoader, LinqExample.Data</DataLoaderType>
</LinqExampleConfig>
The slightly more verbose type names are important; they say not only which class to use, but the assembly that contains them.
This enables us to retrieve the type of ILoader we want, now we just need to instantiate it. Again, in the Config class, I used this code snippet:
private static T CreateObject<T>(string typeName) {
Type t = Type.GetType(typeName);
ConstructorInfo ctor = t.GetConstructor(new Type[] { });
return (T)ctor.Invoke(new object[] { });
}
internal ILoader DataLoader {
get {
if (this._loader == null) {
this._loader = CreateObject<ILoader>(this.DataLoaderType);
}
return _loader;
}
}
It's reasonably likely that I'll need to instantiate some kind of class again, so I abstracted the CreateObject method out. Now, any calls to Config.Settings.DataLoader will give us whichever loader we specified in our config. Note: if the type you specified in your config cannot be found by the executing program, a NullReferenceException will be thrown. I included some code to avoid this, but omitted it here for brevity's sake.
The only other thing to do is remove the UI's reference's to the loader. Let's revisit the Person class again. It only has some private variables and getters and setters. That's really not any different than an old-school C struct. To make this more object oriented, and we can add loading methods to the class and hide the implementation from the UI.
public partial class Person {
/* Hiding properties and constructor */
public static Person LoadSingle(int personId) {
return Config.Settings.DataLoader.LoadSinglePerson(personId);
}
public static List LoadAll() {
return Config.Settings.DataLoader.LoadAllPeople();
}
}
With these changes, the UI doesn't know what an ILoader is anymore. We can even mark the interface as internal and then just expose the internals to the data layer for maximum encapsulation. To enable this, add this in your Core's AssemblyInfo.cs:
[assembly: InternalsVisibleTo("LinqExample.Data")]
Note: The data library will be able to see all your internals. If this poses a problem for you, skip this step, and keep the interface public.
All that's left is to remove the Console's reference to the Data assembly, and rework our "amazing" UI code:
foreach (Person p in Person.LoadAll()) {
System.Console.WriteLine("{0} {1} is a person here.", p.FirstName, p.LastName);
}
Another Note: if you run this at this moment, you will probably be hit by the type not found error discussed earlier. Since the UI doesn't have a reference to the Data code, it will not copy it to the bin directory. This can be remedied with a post-build event:
copy /Y "$(SolutionDir)LinqExample.Data\$(OutDir)LinqExample.Data.dll" "$(TargetDir)LinqExample.Data.dll"
This script copies the Data dll to your output directory, which will then be found when the app runs. For extra fancy points, you can wrap everything up into a build script.
That looks familiar! Ok, let's review what we have here: our code is entirely decoupled, meaning that it is easier to interchange the various parts. We removed hard-coded values, improved the quality of our code via configuration, and made some classes more object oriented.
Next time we'll be making things messy again by integrating foreign-key relationships and collections (A person can have some reports that they have created).
Source code for this post is located here.
Currently rated 3.0 by 1 people
- Currently 3/5 Stars.
- 1
- 2
- 3
- 4
- 5