Architecting LINQ To SQL Applications, part 6

Previous: Architecting LINQ To SQL Applications, part 5


Mapping with XML files instead of Attributes


Greg Young pointed out in the comments to the last post that using attributes can clutter your domain objects. Although it is simpler to show attributes first, so that you can relate rolling your own mappings to the designer generated code, I did not want to leave the story incomplete without showing you how to move those mappings into a file in order to keep your domain objects clean.


The correspondence between the mapping file and the attributes is straightforward. Instead of attributes we just have XML elements and instead of properties on those attributes, we have attributes on our XML elements.


First of all we need to create a text file to hold our mappings. We call it Keysafe.map. Next we need to indicate the xml encoding:


<?xml version=”1.0″ encoding=”utf-8″?>


Now we need to open up a Database element, which will form the root of our mapping.


<Database Name=”northwind” xmlns=”http://schemas.microsoft.com/linqtosql/mapping/2007“>
</Database>


Within the Database element we need to add a Table element for each entity we wish to map (equivalent to our [Table] attribute).


<?xml version=”1.0″ encoding=”utf-8″?>
<Database Name=”northwind” xmlns=”
http://schemas.microsoft.com/linqtosql/mapping/2007“>
 <Table Name=”dbo.Category”>
 </Table>
</Database>


Because we are not using an attribute we have to tell LINQ To SQL what type our table maps to explicitly:


<?xml version=”1.0″ encoding=”utf-8″?>
<Database Name=”northwind” xmlns=”
http://schemas.microsoft.com/linqtosql/mapping/2007“>
 <Table Name=”dbo.Category”>
  <Type Name=”KeySafeDomain.Category”>
  </Type>
 </Table>
</Database>


Then we need to map out our Columns. Again because we are not associating our attribute with a member, we have to explicitly indicate the member.


<?xml version=”1.0″ encoding=”utf-8″?>
<Database Name=”northwind” xmlns=”
http://schemas.microsoft.com/linqtosql/mapping/2007“>
 <Table Name=”dbo.Category”>
  <Type Name=”KeySafeDomain.Category”>
   <Column Name=”Id” Member=”Id” DbType=”Int NOT NULL IDENTITY” IsPrimaryKey=”true” IsDbGenerated=”true” UpdateCheck=”Never” AutoSync=”OnInsert” />
   <Column Name=”Name” Member=”Name” DbType=”NVarChar(50) NOT NULL” CanBeNull=”false” UpdateCheck=”Never” />
   <Column Name=”ParentId” Member=”ParentId” DbType=”Int” UpdateCheck=”Never” />
   <Column Name=”Version” Member=”Version” DbType=”rowversion NOT NULL” CanBeNull=”false” IsDbGenerated=”true” IsVersion=”true” AutoSync=”Always” />
  </Type>
 </Table>
</Database>


As before the DbType information is there to help us generate the Db from our domain model.


We also need to map out the associations between our classes. Again the conversion between the attribute based model and our XML model is straightforward.

<Association Member=Children Storage=children ThisKey=ParentId OtherKey=Id/>

<Association Member=Parent Storage=parent ThisKey=ParentId/>

<Association Member=Systems Storage=systems OtherKey=CategoryId/>

At this point it is just grunt work to translate our previous attribute based mappings into an XML mapping file.


In the end our mapping looks like this:

<?xml version=1.0 encoding=utf-8?>

<Database Name=northwind xmlns=http://schemas.microsoft.com/linqtosql/mapping/2007>

<Table Name=dbo.Category>

<Type Name=KeySafeDomain.Category>

<Column Name=Id Member=Id DbType=Int NOT NULL IDENTITY IsPrimaryKey=true IsDbGenerated=true UpdateCheck=Never AutoSync=OnInsert />

<Column Name=Name Member=Name DbType=NVarChar(50) NOT NULL CanBeNull=false UpdateCheck=Never />

<Column Name=ParentId Member=ParentId DbType=Int UpdateCheck=Never />

<Column Name=Version Member=Version DbType=rowversion NOT NULL CanBeNull=false IsDbGenerated=true IsVersion=true AutoSync=Always />

<Association Member=Children Storage=children ThisKey=ParentId OtherKey=Id/>

<Association Member=Parent Storage=parent ThisKey=ParentId/>

<Association Member=Systems Storage=systems OtherKey=CategoryId/>

</Type>

</Table>

<Table Name=dbo.ITSystem>

<Type Name=KeySafeDomain.ITSystem>

<Column Name=CategoryId Member=CategoryId DbType=Int NOT NULL UpdateCheck=Never />

<Column Name=Comments Member=Comments DbType=NVarChar(4000) UpdateCheck=Never />

<Column Name=Name Member=Name DbType=NVarChar(50) NOT NULL CanBeNull=false UpdateCheck=Never />

<Column Name=Id Member=Id DbType=Int NOT NULL IDENTITY IsPrimaryKey=true IsDbGenerated=true UpdateCheck=Never AutoSync=OnInsert />

<Column Name=Version Member=Version DbType=rowversion NOT NULL CanBeNull=false IsDbGenerated=true IsVersion=true AutoSync=Always />

<Association Member=Keys Storage=keys OtherKey=SystemId/>

<Association Member=Category Storage=category OtherKey=Id ThisKey=CategoryId/>

</Type>

</Table>

<Table Name=dbo.Key>

<Type Name=KeySafeDomain.Key>

<Column Name=Id Member=Id DbType=Int NOT NULL IDENTITY IsPrimaryKey=true IsDbGenerated=true UpdateCheck=Never AutoSync=OnInsert />

<Column Name=Password Member=Password DbType=NVarChar(50) NOT NULL CanBeNull=false UpdateCheck=Never />

<Column Name=SystemId Member=SystemId DbType=Int NOT NULL UpdateCheck=Never />

<Column Name=UserName Member=UserName DbType=NVarChar(50) NOT NULL CanBeNull=false UpdateCheck=Never />

<Column Name=Version Member=Version DbType=rowversion NOT NULL CanBeNull=false IsDbGenerated=true IsVersion=true AutoSync=Always />

<Association Member=System Storage=system OtherKey=Id ThisKey=SystemId/>

</Type>

</Table>

</Database>

We can then delete all of our mappings from our domain model so that our model is clean.


    public class Category
    {
        private EntitySet<Category> children = new EntitySet<Category>();
        public int Id { get; set; }
        public string Name {get;set; }
        public int? ParentId { get; set; }
        private EntityRef<Category> parent;
        private EntitySet<ITSystem> systems = new EntitySet<ITSystem>();
        public byte[] Version {get;set;}
        …
    }
   
    public class Key
    {
        public int Id { get; set; }
        public string Password { get; set; }
        public int SystemId { get; set; }
        private EntityRef<ITSystem> system = default(EntityRef<ITSystem>);
        public string UserName { get; set; }
        public byte[] Version {get;set;}
        …
    }
   
    public class ITSystem
    {
        public int CategoryId { get; set; }
  private EntityRef<Category> category = default(EntityRef<Category>);
        public string Comments {get;set;}
        public string Name {get;set; }
        public int Id { get; set; }
        private EntitySet<Key> keys = new EntitySet<Key>();
        public byte[] Version {get;set;}
        …
    }


Managing the Mapping File 


I often embed the file into the dll that contains the model. The upside here is that you don’t need to worry about deployment, but the downside is that you cannot change the mapping without re-issuing the DLL. If you embed the mapping file, you will need some code to read it, so that you can pass it into the DataContext. I use a helper class like this:


    public static class Mapping
    {
        public static XmlMappingSource GetMapping()
        {
            XmlMappingSource mapping;
            using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(“KeySafeDomain.Mappings.Keysafe.map”))
            {
                mapping = XmlMappingSource.FromStream(stream);
            }


            return mapping;
        }
    }


Then we just grab the mapping when we construct our typesafe DataContext:


        public KeySafeContext() : base(ConfigurationManager.ConnectionStrings[DbName].ConnectionString, KeySafeDomain.Mappings.Mapping.GetMapping()) 
        {
            Systems = GetTable<ITSystem>();
            Keys = GetTable<Key>();
            Categories = GetTable<Category>();


            DataLoadOptions dataLoadOptions = new DataLoadOptions();
            dataLoadOptions.LoadWith<ITSystem>(s=>s.Keys);
        }



With that done we can re-run our tests to check that everything passes.


 

About Ian Cooper

Ian Cooper has over 18 years of experience delivering Microsoft platform solutions in government, healthcare, and finance. During that time he has worked for the DTi, Reuters, Sungard, Misys and Beazley delivering everything from bespoke enterpise solutions to 'shrink-wrapped' products to thousands of customers. Ian is a passionate exponent of the benefits of OO and Agile. He is test-infected and contagious. When he is not writing C# code he is also the and founder of the London .NET user group. http://www.dnug.org.uk
This entry was posted in LINQ. Bookmark the permalink. Follow any comments here with the RSS feed for this post.
  • Russell East

    Fantastic series. I use similar approaches to the ones use are promoting, SoC, TDD/BDD and DDD etc. I have been mainly using nhibernate and a little bit of Castle ActiveRecord over the last year or so. I did dismiss Linq for SQL a while a ago when i played with it. The designer and the generated code is not a good sales point for developers that like to write great code and value their domain model. From reading this series, i am going to late in bed tonight as i have to cut some code and getting something running with Linq for SQL ;-).

    I do have a question that i hope you can give me guidence on. Though its probably simple. You are putting all your mappings into one file. As the domain grows so will this file and i assume that over time this will slow things down a bit. Can the mapping files be split up so you have one mapping file per type like nhibernate does?

    Again, great series.

  • Jeremy Holt

    How would you map a complex property to the database? Say your entity has a property of type PrimaryKey:

    struct PrimaryKey{
    int Value {get; set:}
    bool IsValidKey {get;}
    }

    and you want to map PrimaryKey.Value to the database’s

    Is there any way to do this?

  • Ian Cooper

    @Jim

    For sure, but I would suggest that people opt for working from the domain and generating the Db over generating the mappings from the Db schema. This approach supports domain and test-driven development better than schema first approaches (you can’t drive your schema with tests based on user stories in quite the same way).

  • http://www.ThinqLinq.com Jim Wooley

    If you don’t want to hand craft the XML, you can use SQL Metal to generate the XML structures from your database schema. Once you have that in place, you can the maintain the XML file. This has the advantage not only of keeping the classes de-cluttered as Greg pointed out, but also keeps all of the mapping consolidated to a centralized location which may make maintenance easiler. An additional benefit of the XML mapping is that you can change the mappings dynamically at run-time without requiring a re-compilation. We discuss the pros and cons of these various mapping options in our LINQ In Action book.

    BTW, nice series. Keep them coming.