Crie agora seu próprio Framework modular para ser utilizado em suas aplicações ASP.NET CORE e Blazor.
Quando eu escrevo meus artigos, sempre crio uma nova aplicação a partir do template padrão do Visual Studio para você conseguir acompanhar o assunto com mais clareza.
E nesses artigos acabo duplicando muito código e deixando algumas coisas hardcoded, impossibilitando o reuso em outros projetos.
Eu quero transformar esses códigos fontes desses artigos em um único Framework que possa ser reutilizado em outros projetos ASP.NET CORE e Blazor.
Quando falo em Framework, eu digo que nesse contexto são uma ou mais DLLs C# .NET Standard com diversas funcionalidades genéricas que poderão ser utilizadas em outros projetos Web.
Algumas informações importantes para quem está chegando agora. Se você é um desenvolvedor .NET mas nunca viu nada de .NET Core, sugiro você ler antes o artigo .NET Core para DesenvolvedoreS .NET.
Com os conceitos desse artigo você poderá montar o seu próprio Framework C#.
#Framework: Os artigos
Selecionei alguns artigos para serem utilizados como base deste, pois possuem diversas funcionalidades e Design Patterns como Repository e Dependency Injection.
Como estamos falando de um Framework, precisamos de funcionalidades genéricas, então cabe muito também usarmos algo de Cache, Extensions e Autenticação.
Fique livre para consultar os códigos fontes desses artigos:
#1 – A Arquitetura
Não é simplesmente juntar os códigos fontes desses artigos em uma ou mais DLL C# criando assim um Framework.
Precisamos de uma arquitetura de software que siga um padrão do começo ao fim.
Existem várias formas e padrões a serem utilizados como 3 camadas, DDD, TDD, XPTO, Ronaldo etc.
O mais importante não se trata de acertar ou errar, mas sim ser coerente.
O diagrama abaixo mostra como será a estrutura da solução:
Padrões de nomenclatura:
– Padrões de nomenclatura C#.
– Sufixo Provider para integração de uma library de terceiros.
– Sufixo Service para classe de regras de negócios.
– Sufixo Extension para Extention Methods.
– 1 DLL WEB que contem métodos e funcionalidades para API e MVC.
– 1 DLL CORE que contem métodos e funcionalidades genéricas e de Negócios.
– 1 DLL API que contem métodos e funconalidadaes específicas para API.
Para essa solução, criei duas aplicações “MyApp“, uma API e outra Blazor, ambas em ASP.NET CORE 3.0.
A aplicação FSL.MyApp.Blazor irá consumir a API de FSL.MyApp.Api.
As classes que implentam as interfaces desse framework começam com a palavra Blazor com intuito de facilitar, sabendo assim onde elas estão localizadas fisicamente na solution.
Seguindo a mesma coerência, as classes da API iniciam com a palavra API.
#2 – FSL.Framework.Core
Como definido na arquitetura desse framework, essa DLL Core conterá métodos e funcionalidades genéricos para todas as aplicações API e Blazor.
O Core foi devivido em módulos, são eles:
– Address: Módulo de endereço;
– ApiClient: Módulo para acesso a API;
– Authentication: Módulo de autenticação;
– Authorization: Módulo de autorização;
– Caching: Módulo de cache;
– Cryptography: Módulo de criptografias;
– Factory: Módulo para criação de instâncias de classes;
– Repository: Módulo de Repositórios de dados;
Seguindo os artigos previamente mencionados, além de todos esses módulos, algumas Models de apoio e Extension Methods também foram colocadas na DLL Core.
Destaque para a classe genérica (e básica) SqlRepository para conexão assíncrona com banco de dados SQL Server.
public class SqlRepository { private string _connectionStringId; private readonly IDefaultConfiguration _configuration; public SqlRepository( IDefaultConfiguration configuration) { _configuration = configuration; UseConnectionStringId(configuration?.ConnectionStringId ?? "Default"); } protected async Task<T> WithConnectionAsync<T>( Func<SqlConnection, Task<T>> getData) { using (var connection = CreateConnection()) { await connection.OpenAsync(); var data = await getData(connection); connection.Close(); return data; }; } protected SqlRepository UseConnectionStringId( string connectionStringId) { _connectionStringId = connectionStringId; return this; } private SqlConnection CreateConnection() { return new SqlConnection(_configuration.GetConnectionString(_connectionStringId)); } }
#3 – FSL.Framework.Web
A DLL Web poderá ser utilizada tanto por API como aplicações Blazor em ASP.NET CORE 3.0 ou superior.
Nessa DLL constam as autenticações JSON Web Token e Cookies usadas respetivamente nos artigos JWT: Customizando o Identity no ASP.NET CORE 3.0 e Cookies: Identity no Blazor e ASP.NET CORE 3.0.
Também, contem versões melhoradas (e adaptadas) para acionamento de Cache e leitura de APIs externas, usadas respectivamente em Como Chamar Cache Provider em Uma Linha de Código no C# e Como Consumir API Restful no Xamarin Forms.
Destaque para classe de acionamento de APIs externas HttpClientApiClientProvider:
public sealed class HttpClientApiClientProvider : BaseApiClientProvider { private readonly HttpClient _httpClient; public HttpClientApiClientProvider() { _httpClient = new HttpClient(); } public override async Task<ApiClientResult<T>> GetAsync<T>( string apiRoute) { _apiRoute = apiRoute; try { AddReponseHeaders(); var response = await _httpClient.GetAsync($"{_apiUrlBase}{apiRoute}"); return await ConvertResponseToApiClientResultAsync<T>(response: response); } catch (Exception ex) { return await ConvertResponseToApiClientResultAsync<T>(ex: ex); } } public override async Task<ApiClientResult<T>> PostAsync<T>( string apiRoute, object body) { _apiRoute = apiRoute; try { AddReponseHeaders(); UseJsonContentType(); var response = await _httpClient.PostAsync( $"{_apiUrlBase}{apiRoute}", new StringContent( body.ToJson(), Encoding.UTF8, _contentType)); return await ConvertResponseToApiClientResultAsync<T>(response: response); } catch (Exception ex) { return await ConvertResponseToApiClientResultAsync<T>(ex: ex); } } private void AddReponseHeaders() { AddBearerTokenHeader(); foreach (var header in _headers) { _httpClient.DefaultRequestHeaders.Add( header.Key, header.Value); } } }
Essa implementação genérica de IApiClientProvider, usa a classe HttpClient para comunicação entre APIs externas.
Futuramente poderíamos implementar esse provider com o RestSharp por exemplo.
O importante é usar sempre a interface IApiClientProvider e não a implementação (classe) em si.
Essa DLL tem apenas uma referência: a FSL.Framework.Core.
#4 – FSL.Framework.Api
Inicialmente a DLL de API possui apenas 2 Controllers: AddressController do módulo de endereços; e LoginController, para autorização e autenticação de usuário na API.
[Route("api/address")] [ApiController] public sealed class AddressController : ControllerBase { private readonly IAddressRepository _addressRepository; public AddressController( IAddressRepository addressRepository) { _addressRepository = addressRepository; } [HttpGet("{id}")] public async Task<BaseResult<Address>> GetAsync( string id) { var data = await _addressRepository.GetAddressAsync(id); return data.ToResult(); } [HttpGet("")] public async Task<BaseResult<IEnumerable<Address>>> GetRangeAsync( string start, string end) { var data = await _addressRepository.GetAddressRangeAsync( start, end); return data.ToResult(); } }
[Route("api/login")] [ApiController] public sealed class LoginController : ControllerBase { private readonly IAuthenticationService _authenticationService; private readonly Core.Authorization.Service.IAuthorizationService _authorizationService; public LoginController( IAuthenticationService authenticationService, Core.Authorization.Service.IAuthorizationService authorizationService) { _authenticationService = authenticationService; _authorizationService = authorizationService; } [AllowAnonymous] [HttpPost] public async Task<IActionResult> PostAsync( [FromBody] LoginUser loginUser) { var authorization = await _authorizationService.AuthorizeAsync(loginUser); if (!authorization.Success) { return Ok(authorization); } var authentication = await _authenticationService.AuthenticateAsync(authorization.Data); if (!authentication.Success) { return Ok(authentication); } return Ok(authentication); } }
Repare que ambas as APIs são totalmente genéricas e podem ser reutilizadas em outros projetos API em ASP.NET CORE.
Essa DLL tem referências a FSL.Framework.Web e FSL.Framework.Core.
#5 – FSL.MyApp.Api
Essa DLL simula uso do framework dentro uma aplicação API em ASP.NET CORE.
Ela possui a implementação ApiAuthorizationService da interface IAuthorizationService responsável por autorizar um usuário a partir de login/email e senha.
public sealed class ApiAuthorizationService : IAuthorizationService { public async Task<BaseResult<IUser>> AuthorizeAsync( LoginUser loginUser) { var loginOrEmail = loginUser?.LoginOrEmail ?? ""; var password = loginUser?.Password ?? ""; var result = new BaseResult<IUser>(); if (loginOrEmail == "fsl" && password == "1234") { result.Success = true; result.Message = "User authorized!"; result.Data = new MyLoggedUser { Id = Guid.NewGuid().ToString(), Name = "Name test", Credentials = "01|02|09", IsAdmin = false }; } else { result.Success = false; result.Message = "Not authorized!"; } return await Task.FromResult(result); } }
Obviamente, como pode ver, ela é uma classe Fake, sem uma implementação real, justamente porque isso dependerá de aplicação para aplicação.
Ao consumir esse framework, e se você quiser usar autorização/autenticação, será necessário implementar a interface IAuthorizationService.
Além dos Controllers de usuários e repositórios de Endereço, é interessante destacar que essa DLL foi preparada para ser publicada no IIS.
Por fim, eu criei alguns Extension Methods para facilitar a configuração desse framework no arquivo Startup.cs, vide:
public void ConfigureServices( IServiceCollection services) { services .AddApiFslFramework(Configuration) // here .Config(opt => { opt.AddDefaultConfiguration(); opt.AddJwtAuthentication(); opt.AddAuthorizationService<ApiAuthorizationService>(); opt.AddAddressRepository<ApiAddressSqlRepository>(); }); } public void Configure( IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseApiFslFramework(); // here }
No caso, a MyApp.Api foi configurada para API (AddApiFslFramework), configurando (Config) leitura padrão de arquivo AppSettings.JSON (AddDefaultConfiguration) e autenticação por JWT (AddJwtAuthentication).
O código fonte do Extension AddApiFslFramework usado acima ficou assim:
public static class FslFrameworkExtension { public static FslFrameworkOptions AddApiFslFramework( this IServiceCollection services, IConfiguration configuration) { services.AddCors(); services.AddControllers(); services.AddFslFramework(configuration); var options = new FslFrameworkOptions( services, configuration); services .AddMvc() .SetCompatibilityVersion(CompatibilityVersion.Version_3_0) .AddJsonOptions(opt => { opt.JsonSerializerOptions.IgnoreNullValues = true; }); return options; } public static IApplicationBuilder UseApiFslFramework( this IApplicationBuilder app) { app.UseCors(option => option.AllowAnyOrigin()); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); return app; } private static IServiceCollection AddFslFramework( this IServiceCollection services, IConfiguration configuration) { services.UseCaching(); services.AddConfiguration<CryptographyConfiguration>(configuration); services.UseCryptography(); services.AddApiClient(); return services; } }
O arquivo AppSettings.JSON da aplicação API ficou assim:
{ "ConnectionStrings": { "Default": "Data Source=.\\SQLEXPRESS2008R2;Initial Catalog=consulta_cep;User ID=sa;Password=1234567890;Persist Security Info=False;Connect Timeout=200000" }, "TokenConfiguration": { "ValidAudience": "FSL", "ValidIssuer": "FSL", "ValidateIssuerSigningKey": true, "ValidateLifetime": true, "ExpirationInSeconds": 60000 }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*" }
Não vou discutir cada uma das configurações porque elas já foram explicadas no artigos que deram base a esse.
#6 – FSL.MyApp.Blazor
O arquivo de configuração Startup.cs na aplicação Blazor ficou assim:
public void ConfigureServices( IServiceCollection services) { services .AddBlazorFslFramework(Configuration) // here .Config(opt => { opt.AddDefaultConfiguration(); opt.AddConfiguration<MyBlazorConfiguration>(); opt.AddCookiesAuthentication(); opt.AddAuthorizationService<BlazorAuthorizationService>(); opt.AddAddressRepository<BlazorAddressRepository>(); opt.AddFactoryService<BlazorFactoryService>(); opt.AddApiClientService<BlazorApiClientService>(); }); services.AddSingleton<WeatherForecastService>(); } public void Configure( IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Error"); } app.UseBlazorFslFramework(); // here }
Como pode ver, é a mesma lógica usada na API, só que nesse caso a configuração de AddBlazorFslFramework usa autenticação Cookies, customiza a leitura do arquivo AppSettings.JSON através de MyBlazorConfiguration e customiza alguns módulos como ApiClient, Factory e Address.
O código fonte do Extension AddBlazorFslFramework usado acima ficou assim:
public static class FslFrameworkExtension { public static FslFrameworkOptions AddBlazorFslFramework( this IServiceCollection services, IConfiguration configuration) { services.AddRazorPages(); services.AddServerSideBlazor(); services.AddFslFramework(configuration); var options = new FslFrameworkOptions( services, configuration); return options; } public static IApplicationBuilder UseBlazorFslFramework( this IApplicationBuilder app) { app.UseStaticFiles(); app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapBlazorHub(); endpoints.MapFallbackToPage("/_Host"); }); return app; } }
Destaque para a implementação BlazorApiClientService da interface IApiClientService do módulo de ApiClient.
public sealed class BlazorApiClientService : IApiClientService { private readonly MyBlazorConfiguration _configuration; private readonly IFactoryService _factoryService; private readonly ILoggedUserService _loggedUserService; public BlazorApiClientService( MyBlazorConfiguration configuration, IFactoryService factoryService, ILoggedUserService loggedUserService) { _configuration = configuration; _factoryService = factoryService; _loggedUserService = loggedUserService; } public async Task<IApiClientProvider> CreateInstanceAsync() { var instance = _factoryService .InstanceOf<IApiClientProvider>() .UseJsonContentType() .UseBaseUrl(_configuration.ApiUrl); var user = await _loggedUserService.GetLoggedUserAsync<MyLoggedUser>(); instance.UseAuthenticationBearer(user?.Data?.AccessToken); return instance; } }
Essa classe cria uma instância para IApiClientProvider e pré-configura alguns parâmetros como URL BASE de API, AccessToken caso o usuário esteja logado e JSON ContentType.
O arquivo AppSettings.JSON da aplicação Blazor ficou assim:
{ "CookiesConfiguration": { "ExpirationInSeconds": 60000, "CookieName": "FSLMyAppBlazor" }, "CryptographyConfiguration": { "CryptographicKey": "FSLSampleKey" }, "MyBlazorConfiguration": { "ApiUrl": "http://localhost/fsl-myapp-api/api/" }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*" }
Obs.: a propriedade ApiUrl de MyBlazorConfiguration guarda o endereço publicado da API FSL.MyApp.Api.
Outro destaque usado nessa aplicação Blazor é o Repository do módulo Address.
Ao invés de ir no banco de dados SQL trazer os endereços, ele aciona a API de FSL.MyApp.Api justamente através da interface IApiClientService.
public sealed class BlazorAddressRepository : IAddressRepository { private readonly IApiClientService _apiClientService; public BlazorAddressRepository( IApiClientService apiClientService) { _apiClientService = apiClientService; } public async Task<Address> GetAddressAsync( string zipCode) { var apiClient = await _apiClientService.CreateInstanceAsync(); var result = await apiClient.GetAsync<Address>($"address/{zipCode}"); return result.Data; } public async Task<IEnumerable<Address>> GetAddressRangeAsync( string start, string end) { var apiClient = await _apiClientService.CreateInstanceAsync(); var result = await apiClient.GetAsync<List<Address>>($"address?start={start}&end={end}"); return result.Data; } }
Ao usar a interface IAddressRepository, não se sabe de onde é a origem dos dados, se é de API externa, bancos de dados SQL/Oracle/MySQL/MongoDB ou de outras fontes.
Por fim, vale comentar a implementação BlazorAuthorizationService de IAuthorizationService responsável pela autorização do usuário na aplicação Blazor.
public sealed class BlazorAuthorizationService : IAuthorizationService { private readonly IApiClientService _apiClientService; public BlazorAuthorizationService( IApiClientService apiClientService) { _apiClientService = apiClientService; } public async Task<BaseResult<IUser>> AuthorizeAsync( LoginUser loginUser) { var result = new BaseResult<IUser>(); try { var instance = await _apiClientService.CreateInstanceAsync(); var login = await instance.PostAsync<AuthenticationResult>( "login", loginUser); if (login.Success && login.Data.Authenticated) { var user = await instance .UseAuthenticationBearer(login.Data.AccessToken) .GetAsync<MyLoggedUser>("user"); user.Data.AccessToken = login.Data.AccessToken; result.Data = user.Data; result.Success = result.Data.IsNotNull(); } } catch (Exception ex) { result.Message = ex.ToString(); result.Success = false; } return result; } }
A autorização é feita através de um POST no endpoint login da API FSL.MyApp.Api.
Ao ser autenticado com sucesso na API, é acionado outro endpoint para retornar mais dados do usuário logado.
Para saber mais sobre o LOGIN completo da aplicação Blazor leia meu artigo Cookies: Identity no Blazor e ASP.NET CORE 3.0.
#7 – FslFrameworkOptions
Uma coisa que ficou pendente mencionar foi a classe FslFrameworkOptions, responsável pela configuração no arquivo Startup.cs.
Então, recapitulando seu uso:
public void ConfigureServices( IServiceCollection services) { services .AddApiFslFramework(Configuration) .Config(opt => // here { opt.AddDefaultConfiguration(); opt.AddJwtAuthentication(); opt.AddAuthorizationService<ApiAuthorizationService>(); opt.AddAddressRepository<ApiAddressSqlRepository>(); }); }
O código fonte de FslFrameworkOptions ficou assim:
´ public sealed class FslFrameworkOptions { private readonly IServiceCollection _services; private readonly IConfiguration _configuration; public FslFrameworkOptions( IServiceCollection services, IConfiguration configuration) { _services = services; _configuration = configuration; } public FslFrameworkOptions Config( Action<FslFrameworkOptions> opt) { opt(this); return this; } public FslFrameworkOptions AddDefaultConfiguration() { _services.AddConfiguration<DefaultConfiguration>(_configuration); return this; } public FslFrameworkOptions AddConfiguration<T>() where T : class { _services.AddConfiguration<T>(_configuration); return this; } public FslFrameworkOptions AddJwtAuthentication() { _services.AddJwtAuthentication(_configuration); return this; } public FslFrameworkOptions AddCookiesAuthentication() { _services.AddCookiesAuthentication(_configuration); return this; } public FslFrameworkOptions AddApiClientService<T>() where T : class, IApiClientService { _services.AddSingleton<IApiClientService, T>(); return this; } public FslFrameworkOptions AddApiClientProvider<T>() where T : class, IApiClientProvider { _services.AddTransient<IApiClientProvider, T>(); return this; } public FslFrameworkOptions AddApiClient() { _services.AddApiClient(); return this; } public FslFrameworkOptions AddFactoryService<T>() where T : class,IFactoryService { _services.AddSingleton<IFactoryService, T>(); return this; } public FslFrameworkOptions AddAuthorizationService<T>() where T : class, IAuthorizationService { _services.AddSingleton<IAuthorizationService, T>(); return this; } public FslFrameworkOptions AddAddressRepository<T>() where T : class, IAddressRepository { _services.AddSingleton<IAddressRepository, T>(); return this; } }
Interessante ressaltar aqueles métodos genéricos onde é necessário passar um tipo T, onde (keyword where) T precisa ser uma classe e de uma interface específica, como por exemplo o método AddAddressRepository da interface IAddressRepository.
#8 – Considerações finais
Baixe o código fonte no meu github e deixe o projeto MyApp.Api (ou MyApp.Blazor) como default.
Restaure o NuGet e compile pra ver se tudo está OK.
Para testar a aplicação MyApp.Blazor, você precisará publicar (localmente) o projeto MyApp.Api.
Espero que tenha curtido esse artigo e o trabalho que venho desempenhando, e para você criar um framework faz sentido no seu dia a dia?
Obrigado e até a próxima 🙂
Artigos sobre Blazor e ASP.NET CORE:
Cookies: Identity no Blazor e ASP.NET CORE 3.0
Blazor: Muito mais que um WebForms de Luxo
Benchmark: ASP.NET 4.8 vs ASP.NET CORE 3.0
AppSettings: 6 Formas de Ler o Config no ASP.NET CORE 3.0
JWT: Customizando o Identity no ASP.NET CORE 3.0
Crie um Gerenciador de Arquivos do Zero em .NET Core e VueJS
IIS: Como Hospedar Aplicação .NET Core em 10 Passos
.NET Core para Desenvolvedores .NET
Blazor: O Começo do Fim do Javascript?
Faça download completo do código fonte no github. |