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.
Posted
03-09-2008 7:01 PM
by
Ian Cooper