C # Net Core 2.0 рефакторинг - PullRequest
       8

C # Net Core 2.0 рефакторинг

0 голосов
/ 26 апреля 2018

Написание кода для контроллеров может привести к повторению самого себя снова и снова.

Как можно использовать приведенный ниже код и применять принцип DRY в C # Net Core 2.0.Контроллеры MVC?

См. Пример ниже.

Код для получения полного списка отделов с помощью EF и веб-API выглядит следующим образом.

[HttpGet]
public async Task<IActionResult> Department()
{

    using (var client = await _apiHttpClient.GetHttpClientAsync())
    {
        var response = await client.GetAsync("api/Department");

        if (response.IsSuccessStatusCode)
        {
            var content = await response.Content.ReadAsStringAsync();
            var dptos = JsonConvert.DeserializeObject<Department[]>(content);

            return View(dptos);
        }

        if (response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.Forbidden)
            return RedirectToAction("AccessDenied", "Authorization");

        throw new Exception($"A problem happened while calling the API: {response.ReasonPhrase}");
    }
}

Действительнопочти идентично получению отдельного отделаздесь уместно применить рефакторинг и DRY, но в любом случае я скопирую его реализацию здесь.

BR и заранее благодарю за ваш ответ.

public class ApiHttpClient : IApiHttpClient
{
    private HttpClient _httpClient;
    private HttpClient HttpClient => _httpClient ?? (_httpClient = new HttpClient());

    private readonly IHttpContextAccessor _httpContextAccessor;

    public ApiHttpClient(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public async Task<HttpClient> GetHttpClientAsync()
    {
        string accessToken;

        var context = _httpContextAccessor.HttpContext;

        var expiresAt = await context.GetTokenAsync(Constants.Tokens.ExpiresAt);                 // Get expires_at value

        if (string.IsNullOrWhiteSpace(expiresAt)                                                 // Should we renew access & refresh tokens?
            || (DateTime.Parse(expiresAt).AddSeconds(-60)).ToUniversalTime() < DateTime.UtcNow)  // Make sure to use the exact UTC date formats for comparison 
        {
            accessToken = await RefreshTokensAsync(_httpContextAccessor.HttpContext);            // Get the current HttpContext to access the tokens
        }
        else
        {
            accessToken = await context.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);  // Get access token
        }

        HttpClient.BaseAddress = new Uri(Constants.Urls.ApiHost);

        if (!string.IsNullOrWhiteSpace(accessToken))
            HttpClient.SetBearerToken(accessToken);

        return HttpClient;
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected void Dispose(bool disposing)
    {
        if (disposing)
        {
            if (_httpClient != null)
            {
                _httpClient.Dispose();
                _httpClient = null;
            }
        }
    }

    public static async Task<string> RefreshTokensAsync(HttpContext context)
    {
        var discoveryResponse = await DiscoveryClient.GetAsync(Constants.Authority);                                            // Retrive metadata information about our IDP

        var tokenClient = new TokenClient(discoveryResponse.TokenEndpoint, Constants.ClientMvc.Id, Constants.ClientMvc.Secret); // Get token client using the token end point. We will use this client to request new tokens later on

        var refreshToken = await context.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken);                               // Get the current refresh token

        var tokenResponse = await tokenClient.RequestRefreshTokenAsync(refreshToken);                                           // We request a new pair of access and refresh tokens using the current refresh token

        if (tokenResponse.IsError)
            return null;            // Let's the unauthorized page bubbles up
          // throw new Exception("Problem encountered while refreshing tokens", tokenResponse.Exception);

        var expiresAt = (DateTime.UtcNow
                         + TimeSpan.FromSeconds(tokenResponse.ExpiresIn)).ToString("O", CultureInfo.InvariantCulture);         // New expires_at token ISO 860

        var authenticateResult = await context.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);           // HttpContext.Authentication.GetAuthenticateInfoAsync() deprecated

        authenticateResult.Properties.UpdateTokenValue(OpenIdConnectParameterNames.AccessToken, tokenResponse.AccessToken);    // New access_token
        authenticateResult.Properties.UpdateTokenValue(OpenIdConnectParameterNames.RefreshToken, tokenResponse.RefreshToken);  // New refresh_token 
        authenticateResult.Properties.UpdateTokenValue(Constants.Tokens.ExpiresAt, expiresAt);                                 // New expires_at token ISO 8601 WHY _at TODO

        await context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, authenticateResult.Principal, authenticateResult.Properties); // Signing in again with the new values, doing such a user relogin, ensuring that we change the cookies on client side. Doig so the user that has logged in has the refreshed tokens

        return tokenResponse.AccessToken;
    }

    public static async Task RevokeTokensAsync(HttpContext context)
    {
        var discoveryResponse = await DiscoveryClient.GetAsync(Constants.Authority);                                                                // Retrive metadata information about our IDP

        var revocationClient = new TokenRevocationClient(discoveryResponse.RevocationEndpoint, Constants.ClientMvc.Id, Constants.ClientMvc.Secret); // Get token revocation client using the token revocation endpoint. We will use this client to revoke tokens later on

        var accessToken = await context.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);                                                     // Get the access token token to revoke

        if (!string.IsNullOrWhiteSpace(accessToken))
        {
            var revokeAccessTokenTokenResponse = await revocationClient.RevokeAccessTokenAsync(accessToken);

            if (revokeAccessTokenTokenResponse.IsError)
                throw new Exception("Problem encountered while revoking the access token.", revokeAccessTokenTokenResponse.Exception);
        }

        var refreshToken = await context.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken);                                                   // Get the refresh token to revoke

        if (!string.IsNullOrWhiteSpace(refreshToken))
        {
            var revokeRefreshTokenResponse = await revocationClient.RevokeRefreshTokenAsync(refreshToken);

            if (revokeRefreshTokenResponse.IsError)
                throw new Exception("Problem encountered while revoking the refresh token.", revokeRefreshTokenResponse.Exception);
        }
    }

}

Ответы [ 2 ]

0 голосов
/ 27 апреля 2018

Я произвел рефакторинг кода следующим образом, имея в виду следующий рабочий процесс.

Нам понадобятся: а) класс обслуживания API, б) HttpContextAccessor и в) HttpClient.

1) DI принцип!Мы регистрируем их в нашем контейнере внедрения зависимостей на ConfigureServices

    services
        .AddTransient<IGameApiService, GameApiService>()
        .AddSingleton<IHttpContextAccessor, HttpContextAccessor>()
        .AddSingleton(c => new HttpClient { BaseAddress = new Uri(Constants.Urls.ApiHost) });

2) Большая работа !.Новый GameApiService выполнит «тяжелую работу» по вызову наших методов API.Мы будем вызывать API, используя «составленную» строку запроса.Служба API будет использовать наш HttpClient, передавая строку запроса и возвращая код ответа и A STRING!(вместо использования обобщений или других объектов) с контентом.(Мне нужна помощь при переходе на generic, так как я боюсь, что регистрация в контейнере зависимостей будет «трудной» для универсальных).(HttpContextAccessor используется для некоторых методов токенов)

public class GameApiService : IGameApiService
    {
        private readonly HttpClient _httpClient;
        private readonly HttpContext _httpContext;

        public GameApiService(HttpClient httpClient, IHttpContextAccessor httpContextAccessor)
        {
            _httpClient = httpClient;
            _httpContext = httpContextAccessor.HttpContext;

            _httpClient.AddBearerToken(_httpContext); // Add current access token to the authorization header
        }

        public async Task<(HttpResponseMessage response, string content)> GetDepartments()
        {
            return await GetAsync(Constants.EndPoints.GameApi.Department); // "api/Department"
        }

        public async Task<(HttpResponseMessage response, string content)> GetDepartmenById(string id)
        {
            return await GetAsync($"{Constants.EndPoints.GameApi.Department}/{id}"); // "api/Department/id"
        }

        private async Task<(HttpResponseMessage response, string content)> GetAsync(string request)
        {
            string content = null;

            var expiresAt = await _httpContext.GetTokenAsync(Constants.Tokens.ExpiresAt);             // Get expires_at value

            if (string.IsNullOrWhiteSpace(expiresAt)                                                  // Should we renew access & refresh tokens?
                || (DateTime.Parse(expiresAt).AddSeconds(-60)).ToUniversalTime() < DateTime.UtcNow)   // Make sure to use the exact UTC date formats for comparison 
            {
                var accessToken = await _httpClient.RefreshTokensAsync(_httpContext);                 // Try to ge a new access token

                if (!string.IsNullOrWhiteSpace(accessToken))                                          // If succeded set add the new access token to the authorization header
                    _httpClient.AddBearerToken(_httpContext);
            }

            var response = await _httpClient.GetAsync(request);

            if (response.IsSuccessStatusCode)
            {
                content = await response.Content.ReadAsStringAsync();
            }
            else if (response.StatusCode != HttpStatusCode.Unauthorized && response.StatusCode != HttpStatusCode.Forbidden)
            {
                throw new Exception($"A problem happened while calling the API: {response.ReasonPhrase}");
            }

            return (response, content);
        }

    }

public interface IGameApiService
{
    Task<(HttpResponseMessage response, string content)> GetDepartments();
    Task<(HttpResponseMessage response, string content)> GetDepartmenById(string id);
}

3) Отличная СУХАЯ!Наш MVC-контроллер будет использовать этот новый API-сервис следующим образом ... (у нас на самом деле не очень много кода, и ЭТО ЦЕЛЬ .. ;-) ОТЛИЧНО !!.Мы по-прежнему несем ответственность за десериализацию строки содержимого в действии контроллера, для которого был вызван метод API службы.Код для API службы выглядит следующим образом ...

[Route ("[controller] / [action]")] открытый класс DepartmentController: Controller {private readonly IGameApiService _apiService;

public DepartmentController(IGameApiService apiService)
{
    _apiService = apiService;
}

[HttpGet]
public async Task<IActionResult> Department()
{
    ViewData["Name"] = User.Claims.FirstOrDefault(c => c.Type == JwtClaimTypes.Name)?.Value;

    var (response, content) = await _apiService.GetDepartments();

    if (!response.IsSuccessStatusCode) return Forbid();

    return View(JsonConvert.DeserializeObject<Department[]>(content));
}


[HttpGet]
public async Task<IActionResult> DepartmentEdit(string id)
{
    ViewData["id"] = id;

    var (response, content) = await _apiService.GetDepartmenById(id);

    if (!response.IsSuccessStatusCode) return Forbid();

    return View(JsonConvert.DeserializeObject<Department>(content));
}

}

4) Последний трюк !.Для перенаправления на пользовательскую страницу, когда мы не авторизованы или в праве было отказано, мы выдавали if (! Response.IsSuccessStatusCode) return Forbid ();да Запретить ().Но нам по-прежнему необходимо настроить запрещенную страницу по умолчанию в промежуточном программном обеспечении cookie.Таким образом, в ConfigureServices мы делаем это с помощью services.AddAuthentication (). AddCookie (AddCookie), настраивая соответствующие параметры, в основном параметр AccessDeniedPath, следующим образом.

private static void AddCookie(CookieAuthenticationOptions options)
{
    options.Cookie.Name = "mgame";
    options.AccessDeniedPath = "/Authorization/AccessDenied";                           // Redirect to custom access denied page when user get access is denied

    options.Cookie.HttpOnly = true;                                                     // Prevent cookies from being accessed by malicius javascript code
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;                            // Cookie only will be sent over https
    options.ExpireTimeSpan = TimeSpan.FromMinutes(Constants.CookieTokenExpireTimeSpan); // Cookie will expire automaticaly after being created and the client will redirect back to Identity Server
}

5) Слово о клиенте HTTP !.Он будет создан с использованием фабрики для внедрения зависимостей.Новый экземпляр создается для каждого экземпляра GameApiService.Вспомогательный код для установки токена носителя в заголовке и обновления токена доступа был перемещен в удобный вспомогательный класс метода расширения следующим образом.

public static class HttpClientExtensions
    {
        public static async void AddBearerToken(this HttpClient client, HttpContext context)
        {
            var accessToken = await context.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);

            if (!string.IsNullOrWhiteSpace(accessToken))
                client.SetBearerToken(accessToken);
        }

        public static async Task<string> RefreshTokensAsync(this HttpClient client, HttpContext context)
        {
            var discoveryResponse = await DiscoveryClient.GetAsync(Constants.Authority);                                                  // Retrive metadata information about our IDP

            var tokenClient = new TokenClient(discoveryResponse.TokenEndpoint, Constants.ClientMvc.Id, Constants.ClientMvc.Secret);       // Get token client using the token end point. We will use this client to request new tokens later on

            var refreshToken = await context.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken);                                     // Get the current refresh token

            var tokenResponse = await tokenClient.RequestRefreshTokenAsync(refreshToken);                                                 // We request a new pair of access and refresh tokens using the current refresh token

            if (tokenResponse.IsError)                                                                                                    // Let's the unauthorized page bubbles up instead doing throw new Exception("Problem encountered while refreshing tokens", tokenResponse.Exception)
                return null;

            var expiresAt = (DateTime.UtcNow + TimeSpan.FromSeconds(tokenResponse.ExpiresIn)).ToString("O", CultureInfo.InvariantCulture); // New expires_at token ISO 860

            var authenticateResult = await context.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);                  // HttpContext.Authentication.GetAuthenticateInfoAsync() deprecated

            authenticateResult.Properties.UpdateTokenValue(OpenIdConnectParameterNames.AccessToken, tokenResponse.AccessToken);           // New access_token
            authenticateResult.Properties.UpdateTokenValue(OpenIdConnectParameterNames.RefreshToken, tokenResponse.RefreshToken);         // New refresh_token 
            authenticateResult.Properties.UpdateTokenValue(Constants.Tokens.ExpiresAt, expiresAt);                                        // New expires_at token ISO 8601

            await context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, authenticateResult.Principal, authenticateResult.Properties); // Signing in again with the new values, doing such a user relogin, ensuring that we change the cookies on client side. Doig so the user that has logged in has the refreshed tokens

            return tokenResponse.AccessToken;
        }


        public static async Task RevokeTokensAsync(this HttpClient client, HttpContext context)
        {
            var discoveryResponse = await DiscoveryClient.GetAsync(Constants.Authority);                                                                // Retrive metadata information about our IDP

            var revocationClient = new TokenRevocationClient(discoveryResponse.RevocationEndpoint, Constants.ClientMvc.Id, Constants.ClientMvc.Secret); // Get token revocation client using the token revocation endpoint. We will use this client to revoke tokens later on

            var accessToken = await context.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);                                                     // Get the access token token to revoke

            if (!string.IsNullOrWhiteSpace(accessToken))
            {
                var revokeAccessTokenTokenResponse = await revocationClient.RevokeAccessTokenAsync(accessToken);

                if (revokeAccessTokenTokenResponse.IsError)
                    throw new Exception("Problem encountered while revoking the access token.", revokeAccessTokenTokenResponse.Exception);
            }

            var refreshToken = await context.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken);                                                   // Get the refresh token to revoke

            if (!string.IsNullOrWhiteSpace(refreshToken))
            {
                var revokeRefreshTokenResponse = await revocationClient.RevokeRefreshTokenAsync(refreshToken);

                if (revokeRefreshTokenResponse.IsError)
                    throw new Exception("Problem encountered while revoking the refresh token.", revokeRefreshTokenResponse.Exception);
            }
        }

    }

Теперь код после рефакторинга выглядит более симпатичным и чистым..; -)

0 голосов
/ 26 апреля 2018

Вы можете просто разделить его, используя дженерики.Я не отлаживал этот код (очевидно), но я думаю, что он приведет вас туда, куда вам нужно.

using System.Security.Authentication;

[HttpGet]
public async Task<IActionResult> Department() {
    try {
        var myObject = await GetSafeData<Department[]>("api/Department");
        return view(myObj);
    } catch(AuthenticationException ex) {
        return RedirectToAction("AccessDenied", "Authorization");
    }
}

internal T GetSafeData<T>(string url) {

    using (var client = await _apiHttpClient.GetHttpClientAsync()) {
        var response = await client.GetAsync(url);
        if (response.IsSuccessStatusCode) {
            var content = await response.Content.ReadAsStringAsync();
            return JsonConvert.DeserializeObject<T>(content);
        }
        if (response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.Forbidden)
            Throw New AuthenticationException("");

        throw new Exception($"A problem happened while calling the API: {response.ReasonPhrase}");
    }

}

Вы можете видеть, как вы можете передать response этому же методу, так что выможет также выполнить перенаправление AccessDenied в этом методе и уменьшить количество повторяющегося кода везде.

Это универсальный метод, так что вы можете использовать его для ЛЮБОГО вызова этого API.Этого должно быть достаточно, чтобы вы начали.Надеюсь, это поможет!

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...