Núcleo ASP.NET válido

Núcleo ASP.NET válido


Especialmente para os amantes de livros da série "C ++ em 24 horas", decidi escrever um artigo sobre o ASP.NET Core.


Se você não desenvolveu o .NET ou qualquer plataforma semelhante antes, não faz sentido entrar em detalhes. Mas se você estiver interessado em saber o que são IoC, DI, DIP, Interseptores, Middleware, Filtros (ou seja, tudo o que distingue o Core do .NET clássico), então você definitivamente precisa clicar em "Leia mais" à medida que estiver desenvolvendo Sem entender tudo isso, claramente não está correto.


IoC, DI, DIP


Se um teatro começa com um cabide, o ASP.NET Core inicia com uma injeção de dependência. Para lidar com o DI, você precisa entender o que é IoC.


Falando sobre IoC, muitas vezes se lembra do princípio de Hollywood de "Não ligue para nós, nós ligaremos para você". O que significa "Não há necessidade de nos ligar, nós mesmos ligaremos para você".


Fontes diferentes fornecem padrões diferentes aos quais a IoC pode ser aplicada. E provavelmente eles estão bem e apenas se complementam. Aqui estão alguns desses padrões: fábrica, localizador de serviço, método de modelo, observador, estratégia.


Vejamos a IoC usando um aplicativo de console simples como exemplo.


Suponha que tenhamos duas classes simples que implementam uma interface com um método:


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); } 

Ambos dependem da abstração (neste caso, a interface atua como uma abstração).


E digamos que temos um objeto de nível superior usando estas 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); } } 

Dependendo do parâmetro do construtor, a variável _instance é inicializada por uma classe específica. Além disso, ao chamar Write, a saída para o console ou para Debug será concluída. Tudo parece estar muito bom e até parece corresponder à primeira parte do princípio da Inversão da Dependência


Objetos de nível superior são independentes dos objetos de nível inferior. Ambos e aqueles dependem de abstrações.

No nosso caso, o ILayer atua como uma abstração.


Mas também devemos ter um objeto de nível ainda mais alto. Um que usa a classe Logging


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

Ao inicializar o Log com 1, obtemos na classe Logging uma instância da classe que gera dados para o console. Se inicializarmos o Log com qualquer outro número, o log.Write produzirá dados para Debug. Parece que tudo funciona, mas funciona mal. Nosso objeto de nível superior Main depende dos detalhes do código do objeto de nível inferior - a classe Logging. Se mudarmos algo nesta classe, precisaremos alterar o código da classe Main. Para impedir que isso aconteça, faremos uma inversão de controle - Inversão de Controle. Vamos fazer com que a classe Main controle o que acontece na classe Logging. A classe Logging receberá, como parâmetro construtor, uma instância de uma classe que implementa a interface ILayer


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

E agora, nossa classe Principal ficará assim:


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

De fato, decoramos nosso objeto Logging com o objeto necessário para nós.


Agora, nosso aplicativo está em conformidade com a segunda parte do princípio de Inversão de dependência:


As abstrações são independentes dos detalhes. Os detalhes dependem das abstrações. I.e. não sabemos os detalhes do que está acontecendo na classe Logging, apenas passamos a classe para lá que implementa a abstração necessária.

Existe um termo termo acoplamento estanque - conexão estanque. Quanto mais fraca a conexão entre os componentes no aplicativo, melhor. Gostaria de observar que este exemplo de uma aplicação simples não atinge nem um pouco o ideal. Porque Sim, porque na classe de nível mais alto em Main, usamos duas vezes a criação de instâncias de classe usando new. E existe uma frase mnemônica “Novo é uma pista” - o que significa que quanto menos você usar novo, menos conexões estreitas de componentes no aplicativo e melhor. Idealmente, não devemos usar o novo DebugLayer, mas devemos obter o DebugLayer de alguma outra maneira. Qual? Por exemplo, de um contêiner de IoC ou usando a reflexão de um parâmetro passado para Principal.


Agora, descobrimos o que é Inversion of Control (IoC) e o que é Dependency Inversion (DIP). Resta entender o que é injeção de dependência (DI). IoC é um paradigma de design. Injeção de Dependência é um padrão. É isso que temos agora no construtor da classe Logging. Temos uma instância de uma dependência específica. A classe Logging depende de uma instância de uma classe que implementa ILayer. E esta instância é injetada através do construtor.


Container IoC


Um contêiner de IoC é um objeto que contém muitas dependências específicas (dependência). Caso contrário, a dependência pode ser chamada de serviço - como regra, é uma classe com uma certa funcionalidade. Se necessário, a dependência do tipo requerido pode ser obtida no contêiner. Injetar dependência em um contêiner é Injetar. Extrair - resolver. Aqui está um exemplo do contêiner IoC auto-escrito mais simples:


  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); } } 

Apenas uma dúzia de linhas de código, mas você já pode usá-lo (não para produção, é claro, mas para fins educacionais).


Você pode registrar a dependência (por exemplo, ConsoleLayer ou DebugLayer que usamos no exemplo anterior) assim:


  IoCContainer.Register<ILayer, ConsoleLayer>(); 

E extraia-o do contêiner no local necessário do programa, da seguinte maneira:


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

Em contêineres reais, Dispose () também é implementado, o que permite destruir recursos que se tornaram desnecessários.


A propósito, o nome container IoC não transmite exatamente o significado, já que o termo IoC é muito mais amplo em aplicações. Portanto, recentemente, o termo contêiner DI tem sido usado com mais e mais frequência (já que a injeção de dependência ainda é aplicada).


Vida útil do serviço + vários métodos de extensão no Root de composição


Os aplicativos ASP.NET Core contêm o arquivo Startup.cs, que é o ponto de partida do aplicativo para configurar o DI. Configura a DI no método ConfigureServices.


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

Esse código adicionará a classe SomeRepository ao contêiner de DI, que implementa a interface ISomeRepository. O fato de o serviço ser adicionado ao contêiner usando AddScoped significa que uma instância da classe será criada toda vez que uma página for solicitada.
Você pode adicionar um serviço a um contêiner sem especificar uma interface.


  services.AddScoped<SomeRepository>(); 

Mas esse método não é recomendado, pois seu aplicativo perde a flexibilidade e as conexões próximas aparecem. É recomendável que você sempre especifique uma interface, pois, nesse caso, a qualquer momento, você pode substituir uma implementação da interface por outra. E se as implementações suportarem o princípio de substituição de Liskov, alterando o nome da classe de implementação com um "toque do pulso", você alterará a funcionalidade de todo o aplicativo.


Existem mais 2 opções para adicionar um serviço - AddSingleton e AddTransient.
Ao usar AddSingleton, o serviço é criado uma vez e, ao usar o aplicativo, a chamada vai para a mesma instância. Use esse método com cuidado, pois são possíveis vazamentos de memória e problemas de multithreading.


AddSingleton tem um pequeno recurso. Pode ser inicializado no primeiro acesso a ele


  services.AddSingleton<IYourService, YourService>(); 

imediatamente quando adicionado ao construtor


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

Na segunda maneira, você pode até adicionar um parâmetro ao construtor.
Se você deseja adicionar um parâmetro ao construtor de um serviço adicionado não apenas usando AddSingleton, mas também usando AddTransient / AddScoped, você pode usar a expressão lambda:


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

E, finalmente, ao usar o AddTransient, um serviço é criado toda vez que você o acessa. Ótimo para serviços leves que não consomem memória e recursos.


Se com AddSingleton e AddScoped tudo deve ficar mais ou menos claro, o AddTransient precisa de esclarecimentos. A documentação oficial fornece um exemplo no qual um determinado serviço é adicionado ao contêiner de DI, tanto como parâmetro do construtor de outro serviço, como separadamente. E caso seja adicionado separadamente usando AddTransient, ele cria sua instância 2 vezes. Vou dar um exemplo muito, muito simplificado. Na vida real, não é recomendado para uso, porque classes para simplificar não herdam interfaces. Digamos que temos uma classe simples:


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

E há uma segunda classe que contém a primeira como um serviço dependente e recebe essa dependência como um parâmetro construtor:


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

Agora injetamos dois serviços:


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

E em algum controlador em Ação, adicione o recebimento de nossas dependências e exiba os valores na janela Debug.


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

Portanto, como resultado, obtemos 2 valores Guid diferentes. Mas se substituirmos AddTransient por AddScoped, obteremos 2 valores idênticos.


O contêiner de IoC do aplicativo ASP.NET Core contém alguns serviços por padrão. Por exemplo, IConfiguration é um serviço com o qual você pode obter configurações do aplicativo nos arquivos appsettings.json e appsettings.Development.json. IHostingEnvironment e ILoggerFactory com os quais você pode obter a configuração atual e uma classe auxiliar que permite o log.


As classes são recuperadas do contêiner usando a seguinte construção típica (o exemplo mais comum):


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

Uma variável com modificadores de acesso somente leitura privados é criada no escopo do controlador. A dependência é obtida do contêiner no construtor da classe e atribuída a uma variável privada. Além disso, essa variável pode ser usada em qualquer método ou controlador de ação.
Às vezes, você não deseja criar uma variável para usá-la em apenas uma ação. Então você pode usar o atributo [FromServices]. Um exemplo:


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

Parece estranho, mas para não chamar o método da classe estática DateTime.Now () no código, às vezes isso é feito para que o valor de tempo seja obtido do serviço como parâmetro. Assim, torna-se possível passar a qualquer momento como parâmetro, o que significa que fica mais fácil escrever testes e, como regra, fica mais fácil fazer alterações no aplicativo.
Isso não quer dizer que a estática seja má. Métodos estáticos são mais rápidos. E provavelmente a estática pode ser usada em algum lugar do próprio contêiner de IoC. Mas se salvarmos nosso aplicativo de tudo estático e novo, obteremos mais flexibilidade.


Contêineres DI de terceiros


O que examinamos e o que o contêiner de DI do ASP.NET Core realmente implementa por padrão é a injeção de construtor. Ainda há a oportunidade de injetar dependência na propriedade usando a chamada injeção de propriedade, mas esse recurso não está disponível no contêiner incorporado ao ASP.NET Core. Por exemplo, podemos ter alguma classe que você implementa como dependência, e essa classe tem algum tipo de propriedade pública. Agora imagine que durante ou depois de injetarmos a dependência, precisamos definir o valor da propriedade. Vamos voltar a um exemplo semelhante ao exemplo que examinamos recentemente.
Se tivermos essa classe:


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

que podemos introduzir como vício,


  services.AddTransient<Operation>(); 

então, usando o contêiner padrão, não podemos definir o valor da propriedade.
Se você quiser usar esta oportunidade para definir um valor para a propriedade OperationId, poderá usar algum tipo de contêiner de DI de terceiros que ofereça suporte à injeção de propriedade. A propósito, a injeção de propriedades não é particularmente recomendada. No entanto, ainda existem Injeção de método e Injeção de método de incubação, que podem ser úteis para você e que também não são suportadas pelo contêiner padrão.


Contêineres de terceiros podem ter outros recursos muito úteis. Por exemplo, usando um contêiner de terceiros, você só pode adicionar dependência aos controladores que possuem uma palavra específica no nome. E frequentemente usado - recipientes DI, otimizados para desempenho.
Aqui está uma lista de alguns contêineres DI de terceiros suportados pelo ASP.NET Core: Autofac, Castle Windsor, LightInject, DryIoC, StructureMap, Unity


Embora esteja usando um contêiner DI padrão, não é possível usar injeção de propriedade / método, mas é possível implementar um serviço dependente como parâmetro construtor implementando o padrão Factory da seguinte maneira:


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

Nesse caso, GetService retornará nulo se o serviço dependente não for encontrado. Há uma variação de GetRequiredService que lançará uma exceção se o serviço dependente não for encontrado.
O processo de obtenção de um serviço dependente usando GetService realmente aplica o padrão do localizador de serviço.


Autofac


Vamos dar uma olhada no Autofac com um exemplo prático. Convenientemente, os serviços do contêiner podem ser registrados e recebidos, da maneira padrão e usando o Autofac.


Instale o pacote NuGet Autofac.Extensions.DependencyInjection.
Altere o valor retornado pelo método ConfigureServices de void para IServiceProvider. E adicione propriedade


  public IContainer ApplicationContainer { get; private set; } 

Depois disso, será possível adicionar código como o seguinte ao final do método ConfigureServices da classe Startup (esta é apenas uma das opções para registrar serviços):


  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); 

Aqui builder.Populate (services); Adiciona serviços do IServiceCollection ao contêiner. Além disso, já é possível registrar serviços no builder.RegisterType. Ah sim. Eu quase esqueci. Você deve alterar de void para IServiceProvider o valor de retorno do método ConfigureServices.


AOP com ASP.NET Core - Autofac Interseptors


Falando sobre programação orientada a aspectos, eles mencionam outro termo - preocupações transversais. Preocupação é alguma informação que afeta o código. Na versão russa, eles usam a palavra responsabilidade. Bem, preocupações transversais são responsabilidades que afetam outras responsabilidades. Mas, idealmente, eles não devem se influenciar, certo? Quando eles se influenciam, fica mais difícil mudar o programa. É mais conveniente quando temos todas as operações separadamente. Log, transações, armazenamento em cache e muito mais podem ser feitos usando o AOP sem alterar o código das próprias classes e métodos.


No mundo .NET, um método é frequentemente usado quando o código AOP é incorporado usando um pós-processador em um código de aplicativo já compilado ( PostSharp ) ou, como alternativa, você pode usar interceptores - esses são interceptores de eventos que podem ser adicionados ao código do aplicativo. Esses interceptadores, em regra, usam o decorador que já examinamos para o trabalho deles.


Vamos criar seu próprio interceptador. O exemplo mais simples e mais típico que é mais fácil de reproduzir é o log.
Além do pacote Autofac.Extensions.DependencyInjection, também instalaremos o pacote Autofac.Extras.DynamicProxy
Instalado? Adicione uma classe de log simples que será chamada ao acessar determinados serviços.


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

Adicione ao nosso registro Registro automático do interceptador:


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

E agora, a cada chamada para a classe, o método Intercept da classe Logger será chamado.
Assim, podemos simplificar nossa vida e não escrever uma entrada de log no início de cada método. Nós o teremos automaticamente. E, se desejado, será fácil alterá-lo ou desativá-lo para todo o aplicativo.


Também podemos remover .InterceptedBy (typeof (Logger)); e inclua interceptação de chamada apenas para serviços de aplicativos específicos usando o atributo [Interceptar (typeof (Logger))] - você deve especificá-lo antes do cabeçalho da classe


Middleware


O ASP.NET possui uma cadeia específica de chamadas de código que ocorrem em todas as solicitações. Mesmo antes do carregamento da UI / MVC, determinadas ações são executadas.


Ou seja, por exemplo, se adicionarmos no início do método Configure da classe Startup.cs o código


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

então, podemos ver no console de depuração quais arquivos nossos aplicativos solicitam. De fato, obtemos os recursos da AOP "prontos para uso"
Um exemplo um pouco inútil, mas claro e informativo do uso de middleware, mostrarei agora:


  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 cada solicitação, uma cadeia de chamadas é iniciada. De cada app.Use, depois de chamar next.invoke (), a transição para a próxima chamada é feita. E tudo termina após o app.Run funcionar.
Você pode executar algum código apenas ao acessar uma rota específica.
Você pode fazer isso usando o app.


  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!"); }); } 

Agora, se você for apenas para a página do site, poderá ver o texto “Olá!”. Se adicionar / Adeus à barra de endereços, verá Adeus.


Além de Use and Map, você pode usar UseWhen ou MapWhen para adicionar código à cadeia de middleware apenas sob determinadas condições específicas.


Até agora, ainda existem exemplos inúteis, certo? Aqui está um exemplo 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(); }); 

Aqui, adicionamos cabeçalhos a cada solicitação para ajudar a proteger a página contra ataques de hackers.


Ou aqui está um exemplo de localização:


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

Agora, se você adicionar o parâmetro? Culture = fr ao endereço da página, poderá mudar o idioma do aplicativo para francês (se a localização for adicionada ao seu aplicativo, tudo funcionará)


Filtros


Se a cadeia de middleware se referir a processos antes do MVC, os filtros trabalharão juntos com o MVC.
O diagrama esquemático a seguir mostra como os filtros funcionam.


Filtros


Primeiro, os filtros de autorização são elaborados. I.e. você pode criar algum tipo de filtro ou vários filtros e inserir neles algum tipo de código de autorização que funcionará mediante solicitações.


Então eles cumprem os filtros de recursos. Usando esses filtros, você pode, por exemplo, retornar algumas informações do cache.


Em seguida, ocorre a ligação de dados e os filtros de ação são executados. Com a ajuda deles, você pode manipular os parâmetros passados ​​para Action e o resultado retornado.


Os filtros de exceção, como o nome sugere, permitem adicionar algum tipo de tratamento geral de erros para o aplicativo. Deve ser bastante conveniente lidar com erros em todos os lugares da mesma forma. Uma espécie de AOP-shny plus.


Os filtros de resultados permitem executar alguma ação antes de executar o controlador de ação ou depois. Eles são bastante semelhantes aos filtros de ação, mas são executados apenas se não houver erros. Adequado para lógica vinculada ao 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/pt437002/


All Articles