Unity est un conteneur léger qui permet de réaliser de l’inversion de controle au niveau de l’architecture des applications
La dernière version téléchargeable est la version 1.2 que l’on peut trouver ici : http://www.codeplex.com/unity, c’est un bloc d’application de la libraire d’entreprise de Microsoft V4.1 et peut etre téléchargé individuellement.
Le gros avantage de cet application block est la façon dont on va pouvoir architecturer de manière beaucoup plus agile une application, pour démontrer ça je vais partir d’un exemple très simple basé sur une application dont le but est la gestion de produits
Aussi pour cela je vais mettre en place une entité du modele :
public class Product
{
public Product()
{
}
public string Name { get; set; }
public decimal SalePrice { get; set; }
public decimal PurchasePrice { get; set; }
}
L’interface du repository pour acceder a cette entité se presente comme ceci :
public interface IProductRepository
{
Product GetProductByName(string name);
void SaveProduct(Product product);
}
Le repository concret est le suivant (il est basé sur SQL Linq), au préalable le datacontext vient d’etre généré :
public class SqlProductRepository : IProductRepository
{
private SqlData.DataContext m_DataContext;
public SqlProductRepository(SqlData.DataContext dataContext)
{
m_DataContext = dataContext;
}
private IQueryable<Product> GetAll()
{
return from productData in m_DataContext.Product
select new Product()
{
Name = productData.Name,
SalePrice = Convert.ToDecimal(productData.SalePrice / 1000000.0),
PurchasePrice = Convert.ToDecimal(productData.PurchasePrice / 1000000.0)
};
}
public Product GetProductByName(string name)
{
return GetAll().SingleOrDefault(i => i.Name.ToLower() == name.ToLower());
}
public void SaveProduct(Product product)
{
SqlData.Product productData = m_DataContext.Product.SingleOrDefault(i => i.Name.ToLower() == product.Name.ToLower());
if (productData == null)
{
productData = new SqlData.Product; m_DataContext.Product.InsertOnSubmit(productData);
}
productData.Name = product.Name;
productData.SalePrice = Convert.ToInt64(product.SalePrice * 1000000);
productData.PurchasePrice = Convert.ToInt64(product.PurchasePrice * 1000000);
m_DataContext.SubmitChanges();
}
}
D’une manière “classique” voici le code qui peut etre mis en place pour inserer un produit :
var connectionString = System.Configuration.ConfigurationManager.ConnectionStrings["CS"];
SqlData.DataContext dataContext = new UnitySample.SqlData.DataContext(connectionString.ConnectionString);
IProductRepository repository = new SqlProductRepository(dataContext);
Product product = new Product();
product.Name = "Test";
product.SalePrice = 10;
product.PurchasePrice = 5;
repository.SaveProduct(product);
Maintenant la meme chose avec Unity :
var container = new UnityContainer();
// Enregistrement du type
container.RegisterType<IProductRepository, SqlProductRepository>(new Microsoft.Practices.Unity.ContainerControlledLifetimeManager());
// injections de la chaine de connexion dans le constructeur du datacontext
var connectionString = System.Configuration.ConfigurationManager.ConnectionStrings["CS"];
InjectionMember injectedConnectionString = new InjectionConstructor(connectionString.ConnectionString);
container.RegisterType<SqlData.DataContext>(new Microsoft.Practices.Unity.ContainerControlledLifetimeManager()
, new InjectionMember[] { injectedConnectionString });
var repository = container.Resolve<IProductRepository>();
Product product = new Product();
product.Name = "Test";
product.SalePrice = 10;
product.PurchasePrice = 5;
repository.SaveProduct(product);
Quelques remarques, dans un premier temps il va falloir indiquer au container comment mapper le repository, pour cela il existe la methode RegisterType<>, elle permet d’indiquer d’une part l’interface et le mapping vers la classe concrete d’autre part, dans le parametre de la methode on indique comment est controlée la durée de vie de l’instance. Unity permet d’indiquer un singleton (le cas ici) mais aussi par thread ou custom.
Ensuite on enregistre le datacontext, ici il n’y a pas d’interface , unity le gere, nous indiquons la durée de vie et nous injectons en parametre du constructeur la chaine de connexion.
pour obtenir une instance du repository il suffit alors d’appeler la methode générique Resolve<>, au passage elle aura retrouvé la classe concrete du repository comme indiqué plus haut, elle aura passé au constructeur une instance du datacontext ayant lui meme été crée en passant en parametre de son constructeur la chaine de connexion. c’est magique ;)
la configuration du container peut se faire via un fichier de configuration de la manière suivante :
<configSections>
<section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Microsoft.Practices.Unity.Configuration, Version=1.2.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
</configSections>
<unity>
<typeAliases>
<typeAlias alias="singleton"
type="Microsoft.Practices.Unity.ContainerControlledLifetimeManager,
Microsoft.Practices.Unity" />
</typeAliases>
<containers>
<container>
<types>
<type type="UnitySample.IProductRepository,UnitySample"
mapTo="UnitySample.SqlProductRepository,UnitySample">
<lifetime type="singleton" />
</type>
<type type="UnitySample.SqlData.DataContext,UnitySample">
<typeConfig>
<constructor>
<param name="connection" parameterType="System.String">
<value value="..."/>
</param>
</constructor>
</typeConfig>
<lifetime type="singleton" />
</type>
</types>
</container>
</containers>
</unity>
La configuration du container devient alors :
var container = new UnityContainer();
UnityConfigurationSection section
= (UnityConfigurationSection)System.Configuration.ConfigurationManager.GetSection("unity");
section.Containers.Default.Configure(container);
var repository = container.Resolve<IProductRepository>();
Product product = new Product();
product.Name = "Test";
product.SalePrice = 10;
product.PurchasePrice = 5;
repository.SaveProduct(product);
L’enorme avantage de cette architecture est sa modularité, je n’ai plus besoin de referencer SqlData, je peux le remplacer par un autre repository en changeant la valeur dans le fichier de configuration.
il est possible aussi de faire de l’injection sur des propriétés, par exemple nous allons mettre en place la possibilité de logger , voici l’interface :
public interface ILogger
{
void Write(string message);
}
Sa classe concrete :
public class ConsoleLogger : ILogger
{
#region ILogger Members
public void Write(string message)
{
Console.WriteLine(message);
}
#endregion
}
Il suffit alors de referencer cette classe dans notre fichier de config :
<type type="UnitySample.ILogger,UnitySample"
mapTo="UnitySample.ConsoleLogger,UnitySample">
<lifetime type="singleton" />
</type>
puis dans le repository sql ajouter la propriété suivante :
[Dependency]
public ILogger Logger { get; set; }
il sera alos possible de logger des information a l’interieur de la classe. l’attribut [Dependency] indique à Unity qu’il faut injecter une instance du logger sur cette propriété , il aurait aussi été possible de definir ceci dans le fichier de configuration.
Logger par la suite dans une base de donnée ou autre devient un jeu d’enfant, il suffit de creer sa propre classe concrète implementant ILogger et de modifier le mapping dans le fichier de configuration.
Donc on l’a bien compris Unity permet toute sorte d’injections, constructeur , propriété, possibilité de tunner finement la durée de vie des instance grace au lifemanager.
Mais ce n’est pas tout, Unity permet aussi l’interception, dans notre exemple nous voulons valider qu’un produit à bien un nom avant de l’inserer, pour cela, il faut referencer l’assembly “Microsoft.Practices.Unity.Interception” puis nous allons creer un attribut “Validator”, il doit heriter de la classe HandlerAttribute.
public class ValidatorAttribute : HandlerAttribute
{
public override ICallHandler CreateHandler(Microsoft.Practices.Unity.IUnityContainer container)
{
var validateCallHandler = container.Resolve<ValidateCallHandler>();
return validateCallHandler;
}
}
Puis nous creons une classe implémentant l’interface ICallHandler, c’est cette classe qui sera appelée lors de l’execution de la methode :
public class ValidateCallHandler : ICallHandler
{
#region ICallHandler Members
public int Order { get; set; }
public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
{
Product product = (Product)input.Arguments[0];
if (string.IsNullOrEmpty(product.Name))
{
throw new ArgumentException("Le nom du produit doit etre renseigné");
}
return getNext()(input, getNext);
}
#endregion
}
le parametre input est la methode invoquée, il suffit alors de recuperer le seule parametre de la methode et de le caster en Product puis verifier le nom
L’interception dans le modele Unity est une extension il va donc falloir configurer cette extension pour le repository de la manière suivante :
container.Configure<Interception>().SetDefaultInterceptorFor<IProductRepository>(new TransparentProxyInterceptor());
il faut aussi indiquer via un attribut quelle methode il faut intercepter :
[Validate]
public void SaveProduct(Product product)
{
...
}
Cela aura pour effet l’appel de la methode Invoke de l’intercepteur ValidateCallHandler avant l’execution du corps de la methode SaveProduct, on peut imaginer des choses comme le chronometrage de l’execution d’une methode, ou la mise en place d’une strategie de securité basée sur ce principe.
Pour conclure, d’une manière générale a l’utilisation on a tendance à oublier les patterns Singleton , Facade, Provider et autres Methodes statiques, il s’agit d’une methode de programmation basée sur du “tissage” qui peut rendre une architecture particulièrement agile, je n’ai pas eu l’occasion de tester cet application block sur de très gros projets, il faudra regarder comment il se comporte avec des centaines de services et repository. Mais c’est très prometteur.
Ci-joint le code source du sample :
Aucun commentaire:
Enregistrer un commentaire