Puresharp est un ensemble de fonctionnalités pour .NET 4.5.2+ / .NET Core 2.1 visant à améliorer la productivité en produisant des applications flexibles et efficaces.
Puresharp fournit principalement des outils architecturaux pour construire les bases des applications professionnelles :
Ce cadre est divisé en 2 parties :
IPuresharp est un package nuget dédié à la réécriture des assemblys (à l'aide de Mono.Cecil ) pour leur permettre d'être hautement personnalisables au moment de l'exécution. IPuresharp n'ajoutera pas de nouvelle référence de bibliothèque aux assemblages, mais inclura uniquement un processus de post-construction pour réécrire automatiquement les assemblages juste après la réussite de la construction.
Install-Package IPuresharp -Version 5.0.5
Il peut être utilisé manuellement avec la ligne de commande pour gérer les assemblys tiers
IPuresharp.exe "FullnameToAssembly.dll"
Puresharp est un package nuget offrant diverses fonctionnalités utiles pour concevoir une architecture saine et productive. Ce package comprend également toute l'artillerie permettant de gérer facilement les éléments apportés par l'écrivain IL IPuresharp. Le package nuget ajoute une bibliothèque (Puresharp.dll) sans aucune autre dépendance.
Install-Package Puresharp -Version 5.0.5
remarque : Il est recommandé d'installer le package nuget IPuresharp dans tous les projets et d'installer le package nuget Puresharp uniquement dans le projet où vous en avez explicitement besoin.
Le workflow global du DI Container est similaire aux autres : configurer une composition, créer un conteneur et instancier à partir du conteneur certains composants.
Exemple d'interfaces à configurer
public interface IA
{
}
public interface IB
{
}
public interface IC
{
}
Exemple d'implémentations à lier aux interfaces
public class A : IA
{
public A(IB b, IC c)
{
}
}
public class B : IB
{
public B(IC c, int constant)
{
}
}
public class C : IC
{
}
Créer une composition
var _composition = new Composition();
Composition de configuration pour IA, IB, IC avec respectivement A, B, C
_composition.Setup<IA>(() => new A(Metadata<IB>.Value, Metadata<IC>.Value), Instantiation.Multiton);
_composition.Setup<IB>(() => new B(Metadata<IC>.Value, 28), Instantiation.Multiton);
_composition.Setup<IC>(() => new C(), Instantiation.Multiton);
Créer un conteneur à partir de la configuration de la composition
var _container = _composition.Materialize();
Instancier un module d'IA à partir d'un conteneur
using (var _module = _container.Module<IA>())
{
var _ia = _module.Value;
}
remarque : le module est IDisposable et contrôle le cycle de vie pour toutes les dépendances.
Comment est géré le cycle de vie des dépendances ? Lorsqu'un module est configuré en composition, le mode d'instanciation est requis et peut être Singleton (une instance unique avec un cycle de vie lié au conteneur), Multiton (une nouvelle instance pour chaque module avec un cycle de vie lié au module lui-même) ou Volatile (toujours une nouvelle instance avec cycle de vie lié au module propriétaire). Le conteneur et le module sont tous deux IDisposable pour publier les composants créés.
Mes interfaces devraient-elles implémenter IDisposable pour correspondre à la gestion du cycle de vie ? Au contraire, l'interface d'un composant ne doit jamais implémenter l'interface IDisposable qui est un souci purement d'infrastructure. Seules les implémentations pourraient l’être. Le conteneur s'assure de disposer correctement des implémentations lorsqu'il implémente l'interface IDisposable.
Pourquoi utiliser une expression lambda pour configurer des composants au lieu d'un paramètre générique classique ? L'expression Lambda offre un moyen de cibler le constructeur à utiliser, de spécifier quand utiliser les dépendances et de capturer la constante.
Comment la dépendance est-elle configurée ? Utilisez simplement Metadata<T>.Value dans l'expression lorsque vous devez récupérer la dépendance du conteneur.
L'injection du constructeur empêche-t-elle la référence cyclique entre les composants ? Non, les références cycliques sont une fonctionnalité. Lorsqu'une instance est créée, ce n'est pas vraiment le cas, une instance proxy paresseuse est préparée pour minimiser la rétention des ressources inutilisées et autoriser les références cycliques.
En préversion, seuls les constructeurs sont utilisés pour configurer le composant, est-ce limité à l'injection de constructeur ? Non, les expressions sont totalement ouvertes. Vous pouvez injecter des méthodes statiques, des constructeurs, des membres et même mélanger différents styles.
Flux de travail :
Exemple d'interface
[AttributeUsage(AttributeTargets.Method)]
public class Read : Attribute
{
}
[AttributeUsage(AttributeTargets.Method)]
public class Operation : Attribute
{
}
public interface IService
{
[Operation]
void SaveComment(int id, string text);
[Read]
[Operation]
string GetComment(int id);
}
Exemple de mise en œuvre
public class Service : IService
{
public void SaveComment(int id, string text)
{
}
public string GetComment(int id)
{
return null;
}
}
Supposons que nous souhaitions enregistrer toutes les opérations en lecture seule. Pour cela, nous devons définir un Pointcut qui représente toutes les méthodes qui sont des opérations en lecture seule (où sont placés les attributs Read et Operation)
public class ReadonlyOperation : Pointcut.And<Pointcut<Operation>, Pointcut<Read>>
{
}
Définir un conseil à enregistrer avant avec Trace.WriteLine par exemple lors de l'appel de méthodes
public class Log : IAdvice
{
private MethodBase m_Method;
public Log(MethodBase method)
{
this.m_Method = method;
}
public void Instance<T>(T instance)
{
}
public void Argument<T>(ref T value)
{
}
public void Begin()
{
Trace.WriteLine(this.m_Method);
}
public void Await(MethodInfo method, Task task)
{
}
public void Await<T>(MethodInfo method, Task<T> task)
{
}
public void Continue()
{
}
public void Throw(ref Exception exception)
{
}
public void Throw<T>(ref Exception exception, ref T value)
{
}
public void Return()
{
}
public void Return<T>(ref T value)
{
}
public void Dispose()
{
}
}
Définir un aspect qui utilise les conseils du journal
public class Logging : Aspect
{
override public IEnumerable<Advisor> Manage(MethodBase method)
{
yield return Advice
.For(method)
.Around(() => new Log(method));
}
}
Instancier Aspect et l'intégrer à notre ReadonlyOperation Pointcut
var _logging = new Logging();
_logging.Weave<ReadonlyOperation>();
Félicitation, l'aspect de journalisation est désormais injecté dans tous les contrats d'opération en lecture seule.
Voici un ensemble d'échantillons pour montrer les différentes manières de créer et de conseiller.
public class Logging : Aspect
{
override public IEnumerable<Advisor> Manage(MethodBase method)
{
//Use classic interceptor to create an 'Around' advisor (place break points in interceptor methods to test interception).
yield return Advice
.For(method)
.Around(() => new Interceptor());
//Use linq expression to generate a 'Before' advisor.
yield return Advice
.For(method)
.Before(invocation =>
{
return Expression.Call
(
Metadata.Method(() => Console.WriteLine(Metadata<string>.Value)),
Expression.Constant($"Expression : { method.Name }")
);
});
//Use linq expression to generate a 'Before' advisor.
yield return Advice
.For(method)
.Before
(
Expression.Call
(
Metadata.Method(() => Console.WriteLine(Metadata<string>.Value)),
Expression.Constant($"Expression2 : { method.Name }")
)
);
//Use ILGeneration from reflection emit API to generate a 'Before' advisor.
yield return Advice
.For(method)
.Before(advice =>
{
advice.Emit(OpCodes.Ldstr, $"ILGenerator : { method.Name }");
advice.Emit(OpCodes.Call, Metadata.Method(() => Console.WriteLine(Metadata<string>.Value)));
});
//Use simple Action to generate a 'Before' advisor.
yield return Advice
.For(method)
.Before(() => Console.WriteLine($"Action : { method.Name }"));
//Use an expression to generate an 'After-Returning-Value' Advisor
yield return Advice
.For(method)
.After()
.Returning()
.Value(_Execution =>
{
return Expression.Call
(
Metadata.Method(() => Console.WriteLine(Metadata<string>.Value)),
Expression.Call
(
Metadata.Method(() => string.Concat(Metadata<string>.Value, Metadata<string>.Value)),
Expression.Constant("Returned Value : "),
Expression.Call(_Execution.Return, Metadata<object>.Method(_Object => _Object.ToString()))
)
);
});
//Validate an email parameter value.
yield return Advice
.For(method)
.Parameter<EmailAddressAttribute>()
.Validate((_Parameter, _Attribute, _Value) =>
{
if (_Value == null) { throw new ArgumentNullException(_Parameter.Name); }
try { new MailAddress(_Value.ToString()); }
catch (Exception exception) { throw new ArgumentException(_Parameter.Name, exception); }
});
}
}
Puis-je intégrer plusieurs aspects dans le même Pointcut ? Oui, faites juste attention à l’ordre du tissage.
Comment puis-je supprimer un aspect d’un Pointcut ? Il existe une méthode Release définie dans Aspect pour supprimer Aspect de Pointcut .
Des attributs sont-ils requis pour définir Pointcut ? Non, Pointcut peut être défini en héritant directement de Pointcut et en implémentant la méthode abstraite Match qui prend un MethodBase comme argument unique et renvoie un booléen pour indiquer si une méthode est dans la portée de Pointcut .
Pourquoi dois-je utiliser IPuresharp ? L'interception est basée sur IPuresharp. En effet, IPuresharp ajoute une action de construction pour réécrire CIL afin de rendre l'assemblage « convivial pour les architectes » en injectant des fonctionnalités transparentes et cachées pour accorder un contrôle total de l'exécution au moment de l'exécution.
Puis-je intercepter le constructeur ? Si oui, comment puis-je le mettre en œuvre ? L'interception du constructeur est prise en charge et est traitée comme une autre méthode avec la déclaration du type comme premier argument et void pour le type de retour.
Les types et méthodes génériques sont-ils pris en charge ? Les types et méthodes génériques sont entièrement pris en charge par injection.
Puis-je intercepter les méthodes asynchrones ? Les méthodes asynchrones sont entièrement prises en charge par injection et offrent un moyen d'intercepter chaque étape asynchrone.