Выдвижная остальное-клиент


Я работаю на остальное-клиент. Заказчик сам предоставляет только самые базовые и универсальные функциональные возможности. Все остальное делается с расширениями. Его использование сырья не должен быть удобным, но раздвижной. Он должен позволять изменять все необходимые параметры для каждого запроса (например, запрос заголовки) и защитить другие (например, метод HTTP тип). Удобство приходит через расширения.


Есть последующие


RestClient

В настоящее время я тестирую его с помощью всего двух способов: сделать и разместить. Каждый из них принимает Ури-строитель и его собственные HttpMethodConfiguration. Они в основном фантики для стандартный код запроса. Перед каждым запросом, сначала заголовки по умолчанию, а затем каждый абонент может настроить их.

[PublicAPI]
public interface IRestClient
{
    Task<T> GetAsync<T>([NotNull] UriDynamicPart uriDynamicPart, [CanBeNull] GetMethodConfiguration methodConfiguration);

    Task<T> PostAsync<T>([NotNull] UriDynamicPart uriDynamicPart, [NotNull] object body, [CanBeNull] PostMethodConfiguration methodConfiguration);
}

[PublicAPI]
public class RestClient : IRestClient
{
    private readonly Action<HttpRequestHeaders> _configureDefaultRequestHeaders;

    private readonly HttpClient _client;

    public RestClient(string baseUri, Action<HttpRequestHeaders> configureDefaultRequestHeaders)
    {
        _client = new HttpClient
        {
            BaseAddress = new Uri(baseUri)
        };
        _client.DefaultRequestHeaders.Accept.Clear();
        _configureDefaultRequestHeaders = configureDefaultRequestHeaders;
    }

    public async Task<T> GetAsync<T>(UriDynamicPart uriDynamicPart, GetMethodConfiguration methodConfiguration)
    {
        methodConfiguration = methodConfiguration ?? new GetMethodConfiguration();

        var request = new HttpRequestMessage(HttpMethod.Get, CreateAbsoluteUri(uriDynamicPart));
        _configureDefaultRequestHeaders(request.Headers);
        methodConfiguration.ConfigureRequestHeaders(request.Headers);

        var response = await _client.SendAsync(request, methodConfiguration.CancellationToken);
        if (methodConfiguration.EnsureSuccessStatusCode)
        {
            response.EnsureSuccessStatusCode();
        }

        return await response.Content.ReadAsAsync<T>(new[] { methodConfiguration.ResponseFormatter }, methodConfiguration.CancellationToken);
    }

    public async Task<T> PostAsync<T>(UriDynamicPart uriDynamicPart, object body, PostMethodConfiguration methodConfiguration)
    {
        methodConfiguration = methodConfiguration ?? new PostMethodConfiguration();

        var request = new HttpRequestMessage(HttpMethod.Post, CreateAbsoluteUri(uriDynamicPart))
        {
            Content = new ObjectContent(body.GetType(), body, methodConfiguration.RequestFormatter)
        };
        _configureDefaultRequestHeaders(request.Headers);
        methodConfiguration.ConfigureRequestHeaders(request.Headers);

        var response = await _client.SendAsync(request, methodConfiguration.CancellationToken);
        if (methodConfiguration.EnsureSuccessStatusCode)
        {
            response.EnsureSuccessStatusCode();
        }

        if (response.Content.Headers.ContentLength > 0)
        {
            return await response.Content.ReadAsAsync<T>(new[] { methodConfiguration.ResponseFormatter }, methodConfiguration.CancellationToken);
        }

        return await Task.FromResult(default(T));
    }        

    private Uri CreateAbsoluteUri(string uriDynamicPart)
    {
        return new Uri(_client.BaseAddress, uriDynamicPart);
    }
}

Конфигурации HTTP-метод

Протокола HTTP-метод-настройки-классы очень просты и содержат всего несколько свойств:

[PublicAPI]
public abstract class HttpMethodConfiguration
{
    [NotNull]
    public UriDynamicPart UriDynamicPart { get; set; } = new UriDynamicPart();

    [NotNull]
    public Action<HttpRequestHeaders> ConfigureRequestHeaders { get; set; } = headers => { };

    public bool EnsureSuccessStatusCode { get; set; } = true;

    public CancellationToken CancellationToken { get; set; } = CancellationToken.None;
}    

public class GetMethodConfiguration : HttpMethodConfiguration
{
    [NotNull]
    public MediaTypeFormatter ResponseFormatter { get; set; } = new JsonMediaTypeFormatter();
}

public class PostMethodConfiguration : HttpMethodConfiguration
{
    [NotNull]
    public MediaTypeFormatter RequestFormatter { get; set; } = new JsonMediaTypeFormatter();

    [NotNull]
    public MediaTypeFormatter ResponseFormatter { get; set; } = new JsonMediaTypeFormatter();
}

public static class GetMethodConfigurationExtensions
{
    public static GetMethodConfiguration SetHeader(this GetMethodConfiguration methodConfiguration, string header, params string[] values)
    {
        methodConfiguration.ConfigureRequestHeaders = methodConfiguration.ConfigureRequestHeaders.Append(headers =>
        {
            headers.Remove(header);
            headers.Add(header, values);
        });
        return methodConfiguration;
    }
}

Расширения услуг

Есть три слоя вспомогательных расширений, которые вместе составляют интуитивно владеет АПИ - надеюсь. Работа у них такая:

IRestClient
    .ResourceFor -> IRestResource
    .Get/Post([customization]) -> IRestMethod
    ."Action" -> actual result

Они не предназначены, чтобы использоваться самостоятельно - слишком много обобщений - но кирпичи для окончательной API-интерфейс. настройки для специальных изменений в отдельные требования.

public static class RestResourceClientFactory
{
    public static IRestResource<TResource> ResourceFor<TResource>(this IRestClient client)
    {
        // We get the resource name either from the attribute or the name of the interface without the "I" prefix.
        var resourceName =
            typeof(TResource)
                .GetCustomAttribute<ResourceNameAttribute>()
                ?.ToString()
            ?? Regex.Replace(typeof(TResource).Name, "^I", string.Empty);

        return (IRestResource<TResource>)Activator.CreateInstance(typeof(RestResource<TResource>), new object[] { client, resourceName });
    }
}

// The generic argument is used for building strong extensions for particular resource.
public interface IRestResource<TResource>
{
    IRestClient Client { get; }

    UriDynamicPart UriDynamicPart { get; }
}

[PublicAPI]
public class RestResource<TResource> : IRestResource<TResource>
{
    public RestResource([NotNull] IRestClient client, params string[] path)
    {
        Client = client ?? throw new ArgumentNullException(nameof(client));
        UriDynamicPart = new UriDynamicPart(path);
    }

    [NotNull]
    public IRestClient Client { get; }

    public UriDynamicPart UriDynamicPart { get; }
}

public interface IRestMethod<out TRestMethod, TResource>
    where TRestMethod : HttpMethodConfiguration
{
    IRestResource<TResource> Resource { get; }

    TRestMethod Configuration { get; }

    Task<TResult> InvokeAsync<TResult>(object body);
}

public abstract class RestMethod<TRestMethod, TResource> : IRestMethod<TRestMethod, TResource>
    where TRestMethod : HttpMethodConfiguration
{
    protected RestMethod(IRestResource<TResource> resource, TRestMethod configuration)
    {
        Resource = resource;
        Configuration = configuration;
    }

    public IRestResource<TResource> Resource { get; }

    public TRestMethod Configuration { get; }

    public abstract Task<TResult> InvokeAsync<TResult>(object body);
}

public class GetMethod<TResource> : RestMethod<GetMethodConfiguration, TResource>
{
    public GetMethod(IRestResource<TResource> resource, GetMethodConfiguration configuration)
        : base(resource, configuration)
    { }

    public override Task<TResult> InvokeAsync<TResult>(object body)
    {
        return Resource.Client.GetAsync<TResult>(Resource.UriDynamicPart, Configuration);
    }
}

public class PostMethod<TResource> : RestMethod<PostMethodConfiguration, TResource>
{
    public PostMethod(IRestResource<TResource> resource, PostMethodConfiguration configuration)
        : base(resource, configuration)
    { }

    public override Task<TResult> InvokeAsync<TResult>(object body)
    {
        return Resource.Client.PostAsync<TResult>(Resource.UriDynamicPart, body, Configuration);
    }
}

public static class RestResourceExtensions
{
    public static GetMethod<TResource> Get<TResource>(this IRestResource<TResource> resource, Func<GetMethodConfiguration, GetMethodConfiguration> configure = null)
    {
        configure = configure ?? (_ => _);
        return new GetMethod<TResource>(resource, configure(new GetMethodConfiguration()));
    }
}

Примеры

Код который должен быть написан для каждого запроса используется маркер интерфейс, здесь ITransactions. Без него не было бы можно писать расширения только для конкретного ресурса, так она прошла через всю цепочку.

public interface ITransactions { }

public static class TransactionsClient
{
    public static IRestResource<ITransactions> Transactions(this IRestClient client)
    {
        return client.ResourceFor<ITransactions>();
    }
}

public static class RequestBuilderExtensions
{
    public static Task<Transaction> NewTransactionAsync(this GetMethod<ITransactions> getMethod)
    {
        return getMethod.InvokeAsync<Transaction>();
    }
}

Фактическое использование этой системы выглядит так:

  • написание расширений для ресурсов
  • настройка клиента
  • вызова расширений
var configureDefaultRequestHeaders =
    (Action<HttpRequestHeaders>)(headers => headers
        .AcceptJson()
        .AddRange(new Dictionary<string, IEnumerable<string>>
        {
            ["X-CustomHeader"] = new[] { "development" },
        })
    );

var client = new RestClient("http://localhost:54245/api/", configureDefaultRequestHeaders);

var t = await client.Transactions().Get().NewTransactionAsync();

Она называет ресурсов в http://localhost:54245/api/transactions через GET.

Этот пример ничего не отменяют, но надо ли мне это или добавить некоторые параметры строки запроса, я могу сделать это в любой расширений для остальных-способ только изменение URI-строитель конфигурации:

public static Task<Transaction> NewTransactionAsync(this GetMethod<ITransactions> getMethod, bool encoded)
{
    getMethod
        .Configuration
        .RelativeUriBuilder
        .QueryString
        .Add("Encoded", encoded.ToString());
    return getMethod.InvokeAsync<Transaction>();
}

Что вы думаете об этом клиента? Это интуитивно понятный и простой в использовании? Легко ли продлить?

Я умышленно не пустые-чеков.



707
4
задан 16 февраля 2018 в 06:02 Источник Поделиться
Комментарии
1 ответ

Я работаю на простой клиент отдыхал, и мне интересно, если мой основной бит может быть в состоянии помочь вашей реализации, и наоборот. Я чуть-чуть здесь (общий сделать асинхронный метод и его внутренняя реализация) (также нулевые чеки удалены для пространства)

Одна из наиболее интересных вещей, которые я нашел в моем исследовании является то, что оба HttpRequestMessage и HttpResponseMessage как реализовать IDisposable. Да, так же HttpClientно это исключение из правил. HttpContent делает также, но утилизация HttpResponseMessage также имеется контент. В то время как это было легко вasync способ обернуть все дело в usingбудучи полностью async требуется ответное сообщение, чтобы остаться в живых, пока contentFunc была обработка. Во всяком случае, это может или не может быть применимо к вашей ситуации, но я упоминаю его в связи с вашим комментарием и отсутствие утилизации.

    private const string JsonContentType = "application/json";
private static readonly HttpClient _HttpClient = new HttpClient();

public static Task<T> GetAsync<T>(this Uri uri)
{
return uri.GetDeleteAsync(
HttpMethod.Get,
async content => JsonConvert.DeserializeObject<T>(
await content.ReadAsStringAsync().ConfigureAwait(false)));
}

private static async Task<T> GetDeleteAsync<T>(
this Uri uri,
HttpMethod httpMethod,
Func<HttpContent, Task<T>> contentFunc,
string contentType = JsonContentType,
bool disposeResponse = true)
{
HttpResponseMessage httpResponseMessage;

using (HttpRequestMessage httpRequestMessage = new HttpRequestMessage(httpMethod, uri))
{
httpRequestMessage.Headers.Accept.Clear();
httpRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(contentType));
httpResponseMessage = await _HttpClient.SendAsync(httpRequestMessage).ConfigureAwait(false);
}

httpResponseMessage.EnsureSuccessStatusCode();
if (contentFunc != null)
{
return await contentFunc(httpResponseMessage.Content).ContinueWith(previousTask =>
{
if (disposeResponse)
{
httpResponseMessage.Dispose();
}

return previousTask.Result;
}).ConfigureAwait(false);
}

httpResponseMessage.Dispose();
return default(T);
}

3
ответ дан 16 февраля 2018 в 09:02 Источник Поделиться