Crie seu Framework em ASP.NET CORE 3 e Blazor

Crie agora seu próprio Framework modular para ser utilizado em suas aplicações ASP.NET CORE e Blazor.

Crie seu Framework em ASP.NET CORE 3.1 + 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.

Benchmark: ASP.NET 4.8 vs ASP.NET CORE 3.1

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.
Sobre o Autor:
Trabalha como arquiteto de soluções e desenvolvedor, tem mais de 18 anos de experiência em desenvolvimento de software em diversas plataformas sendo mais de 16 anos somente para o mercado de seguros.
Revisado por:
Apaixonado por tecnologia e sempre disposto a encarar novos desafios, atualmente trabalho focado em aplicações web e mobile com a plataforma .NET, e me aventurando nas diversas linguagens, desafios e experiências que a área nos proporciona.