ASP.NET Core valide

ASP.NET Core valide


Surtout pour les amateurs de livres de la série "C ++ en 24 heures", j'ai décidé d'écrire un article sur ASP.NET Core.


Si vous n'avez pas développé sous .NET ou sous une plate-forme similaire auparavant, cela n'a aucun sens d'aller sous la coupe pour vous. Mais si vous êtes intéressé à apprendre ce que l'IoC, DI, DIP, Interseptors, Middleware, Filtres (c'est-à-dire tout ce qui diffère du Core du .NET classique), alors vous devez certainement cliquer sur "En savoir plus", pendant que vous développez Sans comprendre tout cela, ce n'est clairement pas correct.


IoC, DI, DIP


Si un théâtre commence par un cintre, ASP.NET Core commence par une injection de dépendance. Pour faire face à l'ID, vous devez comprendre ce qu'est l'IoC.


En parlant d'IoC, on rappelle souvent le principe hollywoodien de «Ne nous appelez pas, nous vous appellerons». Ce qui signifie "Pas besoin de nous appeler, nous vous appellerons nous-mêmes".


Différentes sources donnent différents modèles auxquels l'IoC peut être appliqué. Et très probablement, ils vont bien et se complètent simplement. Voici certains de ces modèles: usine, localisateur de service, méthode de modèle, observateur, stratégie.


Examinons l'IoC en utilisant une application console simple comme exemple.


Supposons que nous ayons deux classes simples qui implémentent une interface avec une seule méthode:


class ConsoleLayer : ILayer { public void Write(string text) { Console.WriteLine(text); } } class DebugLayer : ILayer { public void Write(string text) { Debug.WriteLine(text); } } interface ILayer { void Write(string text); } 

Les deux dépendent de l'abstraction (dans ce cas, l'interface agit comme une abstraction).


Et disons que nous avons un objet de niveau supérieur utilisant ces classes:


  class Logging : ILayer { private ILayer _instance; public Logging(int i) { if (i == 1) { _instance = new ConsoleLayer(); } else { _instance = new DebugLayer(); } } public void Write(string text) { _instance.Write(text); } } 

Selon le paramètre constructeur, la variable _instance est initialisée par une classe spécifique. Eh bien et en outre, lors de l'appel à Write, la sortie vers la console ou vers Debug sera terminée. Tout semble être assez bon et même, semble-t-il, correspond à la première partie du principe d'inversion de dépendance


Les objets de niveau supérieur sont indépendants des objets de niveau inférieur. Celles-ci et celles-ci dépendent d'abstractions.

Dans notre cas, ILayer agit comme une abstraction.


Mais nous devons aussi avoir un objet d'un niveau encore plus élevé. Celui qui utilise la classe Logging


  static void Main(string[] args) { var log = new Logging(1); log.Write("Hello!"); Console.Read(); } 

En initialisant Logging avec 1, nous obtenons dans la classe Logging une instance de la classe qui génère des données vers la console. Si nous initialisons la journalisation avec un autre numéro, log.Write affichera les données dans Debug. Tout, semble-t-il, fonctionne, mais cela fonctionne mal. Notre objet de niveau supérieur Main dépend des détails du code de l'objet de niveau inférieur - la classe Logging. Si nous changeons quelque chose dans cette classe, nous devrons changer le code de la classe Main. Pour éviter que cela ne se produise, nous allons faire une inversion de contrôle - Inversion de contrôle. Faisons en sorte que la classe Main contrôle ce qui se passe dans la classe Logging. La classe Logging recevra, en tant que paramètre constructeur, une instance d'une classe qui implémente l'interface ILayer


  class Logging { private ILayer _instance; public Logging(ILayer instance) { _instance = instance; } public void Write(string text) { _instance.Write(text); } } 

Et maintenant, notre classe principale ressemblera à ceci:


  static void Main(string[] args) { var log = new Logging(new DebugLayer()); log.Write("Hello!"); Console.Read(); } 

En fait, nous décorons notre objet Logging avec l'objet nécessaire pour nous.


Maintenant, notre application est conforme à la deuxième partie du principe d'inversion de dépendance:


Les abstractions sont indépendantes des détails. Les détails dépendent des abstractions. C'est-à-dire nous ne connaissons pas les détails de ce qui se passe dans la classe Logging, nous passons simplement la classe qui implémente l'abstraction nécessaire.

Il existe un tel terme couplage étanche - connexion étanche. Plus la connexion entre les composants de l'application est faible, mieux c'est. Je voudrais noter que cet exemple d'application simple n'atteint pas un peu l'idéal. Pourquoi? Oui, car dans la classe de plus haut niveau de Main, nous utilisons deux fois la création d'instances de classe à l'aide de new. Et il y a une telle phrase mnémotechnique «Nouveau est un indice» - ce qui signifie que moins vous utilisez de nouvelles, moins les connexions des composants sont serrées dans l'application et mieux c'est. Idéalement, nous ne devrions pas utiliser le nouveau DebugLayer, mais devrions obtenir DebugLayer d'une autre manière. Lequel? Par exemple, à partir d'un conteneur IoC ou à l'aide de la réflexion d'un paramètre passé à Main.


Nous avons maintenant compris ce qu'est l'inversion de contrôle (IoC) et ce qu'est l'inversion de dépendance (DIP). Reste à comprendre ce qu'est l'Injection de Dépendance (DI). L'IoC est un paradigme de conception. L'injection de dépendance est un modèle. C'est ce que nous avons maintenant dans le constructeur de la classe Logging. Nous obtenons une instance d'une dépendance spécifique. La classe Logging dépend d'une instance d'une classe qui implémente ILayer. Et cette instance est injectée via le constructeur.


Conteneur IoC


Un conteneur IoC est un tel objet qui contient de nombreuses dépendances spécifiques (dépendance). La dépendance peut être autrement appelée un service - en règle générale, c'est une classe avec une certaine fonctionnalité. Si nécessaire, la dépendance du type requis peut être obtenue à partir du conteneur. L'injection de dépendance dans un conteneur est Inject. Extraire - Résoudre. Voici un exemple du conteneur IoC auto-écrit le plus simple:


  public static class IoCContainer { private static readonly Dictionary<Type, Type> _registeredObjects = new Dictionary<Type, Type>(); public static dynamic Resolve<TKey>() { return Activator.CreateInstance(_registeredObjects[typeof(TKey)]); } public static void Register<TKey, TConcrete>() where TConcrete : TKey { _registeredObjects[typeof(TKey)] = typeof(TConcrete); } } 

Juste une douzaine de lignes de code, mais vous pouvez déjà l'utiliser (pas pour la production, bien sûr, mais à des fins éducatives).


Vous pouvez enregistrer la dépendance (par exemple, ConsoleLayer ou DebugLayer que nous avons utilisé dans l'exemple précédent) comme ceci:


  IoCContainer.Register<ILayer, ConsoleLayer>(); 

Et extrayez-le du conteneur à la place nécessaire du programme comme ceci:


  ILayer layer = IoCContainer.Resolve<ILayer>(); layer.Write("Hello from IoC!"); 

Dans les vrais conteneurs, Dispose () est également implémenté, ce qui vous permet de détruire les ressources devenues inutiles.


Soit dit en passant, le nom IoC container ne donne pas exactement le sens, car le terme IoC est beaucoup plus large dans son application. Par conséquent, récemment, le terme conteneur DI a été utilisé de plus en plus souvent (car l'injection de dépendance est toujours appliquée).


Durée de vie des services + diverses méthodes d'extension dans Composition Root


Les applications ASP.NET Core contiennent le fichier Startup.cs, qui est le point de départ de l'application pour configurer DI. Configure DI dans la méthode ConfigureServices.


  public void ConfigureServices(IServiceCollection services) { services.AddScoped<ISomeRepository, SomeRepository>(); } 

Ce code ajoutera la classe SomeRepository au conteneur DI, qui implémente l'interface ISomeRepository. Le fait que le service soit ajouté au conteneur à l'aide d'AddScoped signifie qu'une instance de la classe sera créée chaque fois qu'une page est demandée.
Vous pouvez ajouter un service à un conteneur sans spécifier d'interface.


  services.AddScoped<SomeRepository>(); 

Mais cette méthode n'est pas recommandée, car votre application perd sa flexibilité et des connexions étroites apparaissent. Il est recommandé de toujours spécifier une interface, car dans ce cas, à tout moment, vous pouvez remplacer une implémentation de l'interface par une autre. Et si les implémentations prennent en charge le principe de substitution Liskov, alors en changeant le nom de la classe d'implémentation d'un simple coup de pouce, vous changerez la fonctionnalité de l'application entière.


Il existe 2 autres options pour ajouter un service - AddSingleton et AddTransient.
Lorsque vous utilisez AddSingleton, le service est créé une fois et lorsque vous utilisez l'application, l'appel est dirigé vers la même instance. Utilisez cette méthode particulièrement soigneusement, car des fuites de mémoire et des problèmes de multithreading sont possibles.


AddSingleton a une petite fonctionnalité. Il peut être initialisé soit au premier accès


  services.AddSingleton<IYourService, YourService>(); 

soit immédiatement après l'ajout au constructeur


  services.AddSingleton<IYourService>(new YourService(param)); 

Dans la deuxième manière, vous pouvez même ajouter un paramètre au constructeur.
Si vous souhaitez ajouter un paramètre au constructeur d'un service ajouté non seulement à l'aide d'AddSingleton, mais également à l'aide d'AddTransient / AddScoped, vous pouvez utiliser l'expression lambda:


  services.AddTransient<IYourService>(o => new YourService(param)); 

Et enfin, lorsque vous utilisez AddTransient, un service est créé chaque fois que vous y accédez. Idéal pour les services légers qui ne consomment pas de mémoire et de ressources.


Si avec AddSingleton et AddScoped tout devrait être plus ou moins clair, alors AddTransient a besoin de clarification. La documentation officielle donne un exemple dans lequel un certain service est ajouté au conteneur DI à la fois comme paramètre du constructeur d'un autre service et séparément de manière indépendante. Et dans le cas où il est ajouté séparément à l'aide d'AddTransient, il crée son instance 2 fois. Je vais donner un exemple très, très simplifié. Dans la vraie vie, son utilisation n'est pas recommandée, car les classes de simplicité n'héritent pas des interfaces. Disons que nous avons une classe simple:


  public class Operation { public Guid OperationId { get; private set; } public Operation() { OperationId = Guid.NewGuid(); } } 

Et il existe une deuxième classe qui contient la première en tant que service dépendant et reçoit cette dépendance en tant que paramètre constructeur:


  public class OperationService { public Operation Operation { get; } public OperationService (Operation operation) { Operation = operation; } } 

Maintenant, nous injectons deux services:


  services.AddTransient<Operation>(); services.AddScoped<OperationService>(); 

Et dans certains contrôleurs en action, ajoutez la réception de nos dépendances et affichez les valeurs dans la fenêtre de débogage.


  public IActionResult Index([FromServices] Operation operation, [FromServices] OperationService operationService) { Debug.WriteLine(operation.OperationId); Debug.WriteLine(operationService.Operation.OperationId); return View(); } 

Par conséquent, nous obtenons 2 valeurs Guid différentes. Mais si nous remplaçons AddTransient par AddScoped, alors nous obtenons 2 valeurs identiques.


Le conteneur IoC de l'application ASP.NET Core contient certains services par défaut. Par exemple, IConfiguration est un service avec lequel vous pouvez obtenir les paramètres d'application à partir des fichiers appsettings.json et appsettings.Development.json. IHostingEnvironment et ILoggerFactory avec lesquels vous pouvez obtenir la configuration actuelle et une classe d'assistance qui permet la journalisation.


Les classes sont extraites du conteneur en utilisant la construction typique suivante (l'exemple le plus courant):


  private readonly IConfiguration _configuration; public SomePageController(IConfiguration configuration) { _configuration = configuration; } public async Task<IActionResult> Index() { string connectionString = _configuration["connectionString"]; } 

Une variable avec des modificateurs d'accès en lecture seule privés est créée dans la portée du contrôleur. La dépendance est obtenue à partir du conteneur dans le constructeur de la classe et affectée à une variable privée. De plus, cette variable peut être utilisée dans n'importe quelle méthode ou contrôleur d'action.
Parfois, vous ne voulez pas créer de variable pour l’utiliser dans une seule action. Ensuite, vous pouvez utiliser l'attribut [FromServices]. Un exemple:


  public IActionResult About([FromServices] IDateTime dateTime) { ViewData["Message"] = «  " + dateTime.Now; return View(); } 

Cela semble étrange, mais afin de ne pas appeler la méthode de la classe statique DateTime.Now () dans le code, il est parfois fait pour que la valeur de temps soit obtenue du service en tant que paramètre. Ainsi, il devient possible de passer à tout moment en tant que paramètre, ce qui signifie qu'il devient plus facile d'écrire des tests et, en règle générale, il devient plus facile d'apporter des modifications à l'application.
Cela ne veut pas dire que l'électricité statique est mauvaise. Les méthodes statiques sont plus rapides. Et le plus probablement statique peut être utilisé quelque part dans le conteneur IoC lui-même. Mais si nous sauvegardons notre application de tout ce qui est statique et nouveau, nous obtiendrons plus de flexibilité.


Conteneurs DI tiers


Ce que nous avons examiné et ce que le conteneur DI ASP.NET Core implémente réellement par défaut est l'injection de constructeur. Il existe toujours la possibilité d'injecter une dépendance dans une propriété à l'aide de l'injection de propriété, mais cette fonctionnalité n'est pas disponible dans le conteneur intégré à ASP.NET Core. Par exemple, nous pouvons avoir une classe que vous implémentez en tant que dépendance, et cette classe a une sorte de propriété publique. Imaginez maintenant que pendant ou après avoir injecté la dépendance, nous devons définir la valeur de la propriété. Revenons à un exemple similaire à l'exemple que nous avons récemment examiné.
Si nous avons une telle classe:


  public class Operation { public Guid OperationId { get; set; } public Operation() {} } 

que nous pouvons introduire comme addiction,


  services.AddTransient<Operation>(); 

puis en utilisant le conteneur standard, nous ne pouvons pas définir la valeur de la propriété.
Si vous souhaitez utiliser cette opportunité pour définir une valeur pour la propriété OperationId, vous pouvez utiliser une sorte de conteneur DI tiers qui prend en charge l'injection de propriété. Soit dit en passant, l'injection de propriété n'est pas particulièrement recommandée. Cependant, il existe toujours des méthodes d'injection de méthode et d'injection de méthode Setter, qui pourraient bien vous être utiles et qui ne sont pas non plus prises en charge par le conteneur standard.


Les conteneurs tiers peuvent avoir d'autres fonctionnalités très utiles. Par exemple, en utilisant un conteneur tiers, vous pouvez uniquement ajouter une dépendance aux contrôleurs qui ont un mot spécifique dans le nom. Et cas assez souvent utilisé - conteneurs DI, optimisés pour les performances.
Voici une liste de certains conteneurs DI tiers pris en charge par ASP.NET Core: Autofac, Castle Windsor, LightInject, DryIoC, StructureMap, Unity


Bien que vous utilisiez un conteneur DI standard, vous ne pouvez pas utiliser l'injection de propriété / méthode, mais vous pouvez implémenter un service dépendant en tant que paramètre constructeur en implémentant le modèle Factory comme suit:


  services.AddTransient<IDataService, DataService>((dsvc) => { IOtherService svc = dsvc.GetService<IOtherService>(); return new DataService(svc); }); 

Dans ce cas, GetService renverra null si le service dépendant n'est pas trouvé. Il existe une variante de GetRequiredService qui lèvera une exception si le service dépendant n'est pas trouvé.
Le processus d'obtention d'un service dépendant à l'aide de GetService applique en fait le modèle de localisateur de service.


Autofac


Jetons un œil à Autofac avec un exemple pratique. De manière pratique, les services du conteneur peuvent être enregistrés et reçus, à la fois par défaut et en utilisant Autofac.


Installez le package NuGet Autofac.Extensions.DependencyInjection.
Modifiez la valeur renvoyée par la méthode ConfigureServices de void à IServiceProvider. Et ajouter une propriété


  public IContainer ApplicationContainer { get; private set; } 

Après cela, il deviendra possible d'ajouter du code comme le suivant à la fin de la méthode ConfigureServices de la classe Startup (ce n'est qu'une des options d'enregistrement des services):


  services.AddTransient<ISomeRepository, SomeRepository>(); var builder = new ContainerBuilder(); builder.Populate(services); builder.RegisterType<AnotherRepository>().As<IAnotherRepository>(); this.ApplicationContainer = builder.Build(); return new AutofacServiceProvider(this.ApplicationContainer); 

Ici builder.Populate (services); Ajoute des services de IServiceCollection au conteneur. Eh bien, il est déjà possible d'enregistrer des services auprès de builder.RegisterType. Ah oui. J'ai presque oublié. Vous devez remplacer void par IServiceProvider la valeur de retour de la méthode ConfigureServices.


AOP avec ASP.NET Core - Autofac Interseptors


Parlant de programmation orientée vers les aspects, ils mentionnent un autre terme - des préoccupations transversales. La préoccupation est une information qui affecte le code. Dans la version russe, ils utilisent le mot responsabilité. Eh bien, les préoccupations transversales sont des responsabilités qui affectent d'autres responsabilités. Mais idéalement, ils ne devraient pas s'influencer mutuellement, non? Lorsqu'ils s'influencent mutuellement, il devient plus difficile de changer de programme. C'est plus pratique lorsque nous avons toutes les opérations séparément. La journalisation, les transactions, la mise en cache et bien plus peuvent être effectuées à l'aide d'AOP sans changer le code des classes et des méthodes elles-mêmes.


Dans le monde .NET, une méthode est souvent utilisée lorsque le code AOP est incorporé à l'aide d'un post-processeur dans un code d'application déjà compilé ( PostSharp ). Ou bien, vous pouvez utiliser des intercepteurs - ce sont des hooks d'événements qui peuvent être ajoutés au code d'application. Ces intercepteurs utilisent généralement le décorateur que nous avons déjà examiné pour leur travail.


Créons votre propre intercepteur. L'exemple le plus simple et le plus typique qui est le plus facile à reproduire est la journalisation.
En plus du package Autofac.Extensions.DependencyInjection, nous installerons également le package Autofac.Extras.DynamicProxy
Installé? Ajoutez une classe de journal simple qui sera appelée lors de l'accès à certains services.


  public class Logger : IInterceptor { public void Intercept(IInvocation invocation) { Debug.WriteLine($"Calling {invocation.Method.Name} from Proxy"); invocation.Proceed(); } } 

Ajouter à notre inscription Enregistrement Autofac de l'intercepteur:


  builder.Register(i => new Logger()); builder.RegisterType<SomeRepository >() .As<ISomeRepository >() .EnableInterfaceInterceptors() .InterceptedBy(typeof(Logger)); 

Et maintenant, à chaque appel à la classe, la méthode Intercept de la classe Logger sera appelée.
Ainsi, nous pouvons simplifier notre vie et ne pas écrire une entrée de journal au début de chaque méthode. Nous l'aurons automatiquement. Et si vous le souhaitez, il nous sera facile de le modifier ou de le désactiver pour l'ensemble de l'application.


Nous pouvons également supprimer .InterceptedBy (typeof (Logger)); et ajouter l'interception d'appels uniquement pour des services d'application spécifiques en utilisant l'attribut [Intercept (typeof (Logger))] - vous devez le spécifier avant l'en-tête de classe.


Middleware


ASP.NET possède une chaîne spécifique d'appels de code qui se produit à chaque demande. Même avant le chargement de l'interface utilisateur / MVC, certaines actions sont effectuées.


C'est, par exemple, si nous ajoutons au début de la méthode Configure de la classe Startup.cs le code


  app.Use(async (context, next) => { Debug.WriteLine(context.Request.Path); await next.Invoke(); }); 

alors nous pouvons voir dans la console de débogage quels fichiers nos demandes d'application. En fait, nous obtenons les capacités de l'AOP «out of box»
Un petit exemple inutile, mais clair et informatif d'utilisation de middleware, je vais vous montrer maintenant:


  public void Configure(IApplicationBuilder app) { app.Use(async (context, next) => { await context.Response.WriteAsync("Hello!" + Environment.NewLine); await next.Invoke(); }); app.Run(async context => { await context.Response.WriteAsync("Hello again."); }); } 

A chaque demande, une chaîne d'appels démarre. Depuis chaque application, après avoir appelé next.invoke (), la transition vers l'appel suivant est effectuée. Et tout se termine après que l'application fonctionne.
Vous ne pouvez exécuter du code que lors de l'accès à une route spécifique.
Vous pouvez le faire en utilisant app.Map:


  private static void Goodbye(IApplicationBuilder app) { app.Run(async context => { await context.Response.WriteAsync("Goodbye!"); }); } public void Configure(IApplicationBuilder app) { app.Map("/goodbye", Goodbye); app.Run(async context => { await context.Response.WriteAsync("Hello!"); }); } 

Maintenant, si vous allez simplement sur la page du site, vous pouvez voir le texte "Bonjour!", Et si vous ajoutez / Au revoir à la barre d'adresse, vous verrez Au revoir.


Outre Use and Map, vous pouvez utiliser UseWhen ou MapWhen pour ajouter du code à la chaîne du middleware uniquement dans certaines conditions spécifiques.


Jusqu'à présent, il y a encore des exemples inutiles, non? Voici un exemple normal:


  app.Use(async (context, next) => { context.Response.Headers.Add("X-Frame-Options", "DENY"); context.Response.Headers.Add("X-Content-Type-Options", "nosniff"); context.Response.Headers.Add("X-Xss-Protection", "1"); await next(); }); 

Ici, nous ajoutons des en-têtes à chaque demande pour aider à protéger la page contre les attaques de pirates.


Ou voici un exemple de localisation:


  var supportedCultures = new[] { new CultureInfo("ru"), new CultureInfo("fr") }; app.UseRequestLocalization(new RequestLocalizationOptions { DefaultRequestCulture = new RequestCulture("ru"), SupportedCultures = supportedCultures, SupportedUICultures = supportedCultures }); 

Maintenant, si vous ajoutez le paramètre? Culture = fr à l'adresse de la page, vous pouvez basculer la langue de l'application sur le français (si la localisation est ajoutée à votre application, alors tout fonctionnera)


Filtres


Si la chaîne du middleware fait référence aux processus avant MVC, les filtres fonctionnent avec MVC.
Le diagramme schématique suivant montre le fonctionnement des filtres.


Filtres


Tout d'abord, les filtres d'autorisation sont élaborés. C'est-à-dire vous pouvez créer une sorte de filtre ou plusieurs filtres et y entrer une sorte de code d'autorisation qui fonctionnera sur les demandes.


Ils remplissent ensuite les filtres de ressources. À l'aide de ces filtres, vous pouvez, par exemple, renvoyer certaines informations du cache.


Ensuite, la liaison de données se produit et des filtres d'action sont exécutés. Avec leur aide, vous pouvez manipuler les paramètres passés à Action et le résultat renvoyé.


Les filtres d'exception comme l'indique le nom vous permettent d'ajouter une sorte de gestion générale des erreurs pour l'application. Il devrait être assez pratique de gérer les erreurs partout de la même manière. Une sorte de AOP-shny plus.


Les filtres de résultats vous permettent d'effectuer une action avant d'exécuter le contrôleur d'action ou après. Ils sont assez similaires aux filtres d'action, mais ne sont exécutés qu'en l'absence d'erreurs. Convient pour la logique liée à View.


. :


  public class YourCustomFilter : Attribute, IAuthorizationFilter { public async void OnAuthorization(AuthorizationFilterContext context) { // -    ,     ,    context.Result = new ContentResult() { Content = "        " }; } } 

DI ( Startup.cs)


  services.AddScoped<YourCustomFilter>(); 

- Action


  [ServiceFilter(typeof(YourCustomFilter))] 

– middleware - action . Configure


  public class MyMiddlewareFilter { public void Configure(IApplicationBuilder applicationBuilder) { applicationBuilder.Use(async (context, next) => { Debug.WriteLine("  middleware!"); await next.Invoke(); }); } } 

Action-


  [MiddlewareFilter(typeof(MyMiddlewareFilter))] 

Source: https://habr.com/ru/post/fr437002/


All Articles