Следуйте за мной в Twitter @tim_deschryver | Подпишитесь на рассылку новостей | Первоначально опубликовано на timdeschryver.dev.
Кэширование в ASP.NET еще не было на высоте
но все изменилось с недавним добавлением нового промежуточного ПО для кэширования вывода, которое появилось в .NET 7 Preview 6.
в .NET 7 Preview 6.
В этой заметке мы рассмотрим возможности нового промежуточного ПО кэширования вывода и то, как его использовать.
В качестве примера давайте создадим новый API и добавим кэширование в конечную точку прогноза погоды.
- Создайте новый API
- Включите кэширование вывода
- Добавление слоя кэширования к конечной точке
- Кэш по параметру запроса
- Кэширование по заголовку запроса
- Кэширование по значению
- Истечение срока действия кэша
- Изменение времени истечения срока действия по умолчанию
- Очистка кэша
- RouteGroups
- Отключение кэширования с помощью NoCache
- Политики
- Именованные политики
- Политики по умолчанию (или «базовые»)
- Блокировка кэша
- Реализация DefaultPolicy
- Использование OutputCache с контроллерами
- Заключение
Создайте новый API
Используйте следующую команду для создания нового проекта веб-API.
Чтобы следовать вместе со мной, убедитесь, что у вас есть как минимум
версия 7.0.0-preview.6.
dotnet new webapi -minimal -o OutputCachingExample
Включите кэширование вывода
Чтобы использовать промежуточное ПО кэширования вывода поверх вашей конечной точки, вам сначала нужно зарегистрировать OutputCache
в вашем
приложении.
Для этого используйте расширение IServiceCollection.AddOutputCache
и IApplicationBuilder.UseOutputCache
.
при настройке API в файле Program.cs
.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOutputCache();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseOutputCache();
Добавление слоя кэширования к конечной точке
С включенным промежуточным ПО кэширования вывода мы можем начать добавлять слой кэширования поверх конечных точек API.
Самый простой способ сделать это — использовать расширение CacheOutput
в IEndpointRouteBuilder
(маршрут).
В следующем примере маршрут /weatherforecast
кэшируется, потому что мы «пометили» маршрут для кэширования с помощью CacheOutput
.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOutputCache();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseOutputCache();
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
app.MapGet("/weatherforecast", async () =>
{
await Task.Delay(1000);
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.CacheOutput();
app.Run();
internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
В результате обработчик конечной точки вызывается при первом получении запроса.
Затем, когда он получает новые запросы, слой кэширования возвращает кэшированный ответ, не выполняя логику конечной точки.
логики.
Это хорошо видно в следующем GIF, где в обработчик добавлена задержка, чтобы было понятнее, когда кэш пуст или когда кэш поражен.
Кэш по параметру запроса
Поскольку прогноз погоды зависит от региона, использование одного кэша не имеет смысла.
Давайте посмотрим, что произойдет, если мы добавим параметр запроса «city», чтобы запросить прогноз погоды для определенного города.
В приведенном ниже примере параметр запроса city (со значением Brussels) добавляется к URL, что означает выполнение обработчика конечной точки.
Когда значение города меняется (с Брюсселя на Париж), обработчик также вызывается снова.
Наконец, в URL добавляется второй параметр запроса «other», что также приводит к повторному запуску обработчика.
Проверив это, мы узнали, что поведение по умолчанию использует путь URL с параметрами запроса для идентификации уникальных запросов, а также для чтения и записи в кэш.
Когда значение параметра запроса изменяется или когда добавляется новый параметр запроса, кэш не посещается.
Если такое поведение не является желаемым, конфигурация выходного кэша может быть настроена в соответствии с вашими потребностями.
Чтобы кэшировать запрос по одному (или нескольким) уникальным параметрам запроса, используйте перегрузку CacheOutput
.
Перегрузка дает нам доступ к конструктору политики OutputCachePolicyBuilder
для настройки стратегии кэширования.
Одним из вариантов является использование метода OutputCachePolicyBuilder.VaryByQuery
для уникальной идентификации запросов по определенному параметру(ам) запроса. Метод предполагает отсутствие, одно или несколько имен параметров запроса для изменения кэша.
Давайте посмотрим, что произойдет, если мы настроим политику на идентификацию запросов по параметру запроса «город».
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOutputCache();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseOutputCache();
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
app.MapGet("/weatherforecast", async () =>
{
await Task.Delay(1000);
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.CacheOutput(p => p.VaryByQuery("city"));
app.Run();
internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
Чтобы полностью игнорировать параметры запроса в URL, используйте OutputCachePolicyBuilder.VaryByQuery
, но оставьте параметры запроса пустыми.
app.MapGet("/weatherforecast", async () =>
{
// handler logic here
})
.CacheOutput(p => p.VaryByQuery());
Это дает нам следующий результат.
Кэширование по заголовку запроса
Вместо использования параметров запроса вы также можете строить кэш по заголовкам запроса с помощью OutputCachePolicyBuilder.VaryByHeader
.
В следующем примере заголовок запроса «X-City» дифференцирует запросы по городам, каждый город имеет индивидуальный кэш.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOutputCache();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseOutputCache();
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
app.MapGet("/weatherforecast", async () =>
{
await Task.Delay(1000);
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.CacheOutput(p => p.VaryByHeader("X-City"));
app.Run();
internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
Кэширование по значению
Вы можете использовать OutputCachePolicyBuilder.VaryByValue
для более тонкого контроля над политикой кэширования.
или KeyValuePair<string, string>
в качестве результата.
У меня не было необходимости реализовывать кэш с помощью этого, если у вас есть хороший пример использования, не стесняйтесь связаться со мной.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOutputCache();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseOutputCache();
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
app.MapGet("/weatherforecast", async () =>
{
await Task.Delay(1000);
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.CacheOutput(x => x.VaryByValue(httpCtx => {
/*
* Implementation here
*/
}));
app.Run();
internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
Истечение срока действия кэша
В реализации по умолчанию кэш истекает через одну минуту (абсолютная продолжительность).
Чтобы контролировать, как долго запись в кэше будет действительна, установите срок действия для маршрутов, используя метод OutputCachePolicyBuilder.Expire
и передайте ему TimeSpan
.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOutputCache();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseOutputCache();
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
app.MapGet("/weatherforecast", async () =>
{
await Task.Delay(1000);
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.CacheOutput(p => p.Expire(TimeSpan.FromSeconds(2)));
app.Run();
internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
Изменение времени истечения срока действия по умолчанию
Чтобы изменить время истечения срока действия по умолчанию, обновите DefaultExpirationTimeSpan
при регистрации выходного кэша.
Например, чтобы использовать кэш в течение одного часа:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOutputCache(options =>
{
options.DefaultExpirationTimeSpan = TimeSpan.FromHours(1);
});
var app = builder.Build();
app.UseHttpsRedirection();
app.UseOutputCache();
Очистка кэша
В некоторых случаях вы можете захотеть очистить кэш при наступлении определенного события.
Например, когда вы знаете, что данные были обновлены, и хотите предоставить пользователям обновленные данные.
Когда кэш очищается, при получении следующего запроса происходит повторная проверка кэша.
Чтобы очистить кэш, сначала добавьте тег (который представляет собой строку
) в вывод кэша с помощью метода OutputCachePolicyBuilder.Tag
.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOutputCache();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseOutputCache();
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
app.MapGet("/weatherforecast", async () =>
{
await Task.Delay(1000);
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.CacheOutput(p => p.Tag("wf"));
app.Run();
internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
Затем инжектируйте хранилище IOutputCacheStore
и вызовите метод EvictByTagAsync
.
Метод принимает имя тега, который необходимо очистить.
public class SomeClass
{
private readonly IOutputCacheStore _cache;
public SomeClass(IOutputCacheStore cache)
{
_cache = cache;
}
public async Task SomeMethod(CancellationToken cancellationToken)
{
// Some logic here....
await _cache.EvictByTagAsync("wf", cancellationToken);
}
}
RouteGroups
Теперь, когда мы рассмотрели основы работы промежуточного ПО CacheOutput
, давайте посмотрим, как использовать его в более сложных сценариях.
Когда у вас есть несколько конечных точек, которым требуется кэш с аналогичными политиками, вам не нужно добавлять промежуточное ПО CacheOutput
к каждой конечной точке. Это было бы сложно поддерживать и легко допустить ошибки.
Чтобы сделать его более управляемым, создайте группу маршрутов и добавьте промежуточное ПО в эту группу.
В примере ниже мы сначала создаем группу маршрутов под названием wf
и применяем к ней промежуточное ПО выходного кэша.
Затем в группу добавляются две конечные точки.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOutputCache();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseOutputCache();
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
var wf = app.MapGroup("weatherforecast").CacheOutput();
wf.MapGet("", async () =>
{
await Task.Delay(1000);
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
});
wf.MapGet("other", async () =>
{
await Task.Delay(1000);
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
});
app.Run();
internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
Отключение кэширования с помощью NoCache
При работе с группами (или политиками позже) конфигурация, применяемая к отдельным конечным точкам, имеет приоритет над конфигурацией, применяемой к группе.
Например, если вы не хотите кэшировать конечную точку, вы можете использовать метод OutputCachePolicyBuilder.NoCache
.
В примере ниже маршрут /nocache
добавлен в группу wf
, которая была создана в предыдущем шаге.
К маршруту /nocache
применяется NoCache
, поэтому этот маршрут не будет использовать политики кэширования, которые были применены к группе wf
.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOutputCache();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseOutputCache();
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
var wf = app.MapGroup("weatherforecast").CacheOutput();
wf.MapGet("", async () =>
{
await Task.Delay(1000);
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
});
wf.MapGet("nocache", async () =>
{
await Task.Delay(1000);
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.CacheOutput(p => p.NoCache());
app.Run();
internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
Политики
Когда вы настраиваете множество конечных точек и/или групп конечных точек с одинаковой конфигурацией, создание собственной политики кэширования, вероятно, будет лучшим выходом. При создании пользовательских политик вы можете выбрать создание именованной политики или добавление политики по умолчанию для всего приложения.
Вы можете включить свои собственные политики в обратный вызов IServiceCollection.AddOutputCache
.
Именованные политики
В следующем примере именованная политика кэша InvariantQueries
создается для игнорирования всех параметров запроса.
Это делается с помощью команды AddPolicy
, которая ожидает имя политики (string
) и Action<OutputCachePolicyBuilder>
для настройки политики кэширования.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOutputCache(options =>
{
options.AddPolicy("InvariantQueries", builder =>
{
builder.VaryByQuery();
});
});
Чтобы использовать политику InvariantQueries
, используйте другую перегрузку промежуточного модуля CacheOutput
и передайте ему имя политики. В нашем примере InvariantQueries
.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOutputCache(options =>
{
options.AddPolicy("InvariantQueries", builder =>
{
builder.VaryByQuery();
});
});
var app = builder.Build();
app.UseHttpsRedirection();
app.UseOutputCache();
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
app.MapGet("/weatherforecast", async () =>
{
await Task.Delay(1000);
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.CacheOutput("InvariantQueries");
app.Run();
internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
Политики по умолчанию (или «базовые»)
Чтобы создать политику по умолчанию, которая будет использоваться для всех запросов, используйте AddBasePolicy
.
Разница с именованной политикой заключается в том, что AddBasePolicy
не ожидает имя политики в качестве аргумента.
Как и именованная политика, AddBasePolicy
использует Action<OutputCachePolicyBuilder>
для настройки политики кэширования.
В реализации примера ниже все запросы кэшируются, если они содержат заголовок X-Cached
.
Обратите внимание, что для включения этой базовой политики не требуется добавлять CacheOutput
в маршрут.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOutputCache(options =>
{
options.AddBasePolicy(builder =>
{
builder.With(p => p.HttpContext.Request.Headers.ContainsKey("X-Cached"));
});
});
var app = builder.Build();
app.UseHttpsRedirection();
app.UseOutputCache();
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
});
app.Run();
internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
Блокировка кэша
Когда запись в кэше не существует и ее нужно создать, но сервер получает одновременно несколько запросов на эту запись, то обработчик выполняется только один раз. Обработчик первого запроса заполняет кэш, а остальные запросы ждут завершения первого запроса. Это предотвращает перегрузку сервера запросами.
Реализация DefaultPolicy
Чтобы узнать, как ведет себя реализация по умолчанию, вы можете взглянуть на исходный код DefaultPolicy
.
Одна деталь, которая бросается в глаза, и которую я сначала не ожидал, но которая имеет смысл, это проверка того, следует ли использовать кэширование вывода.
Здесь мы видим, что авторизованные запросы игнорируются из кэширования.
private static bool AttemptOutputCaching(OutputCacheContext context)
{
// Check if the current request fulfills the requirements to be cached
var request = context.HttpContext.Request;
// Verify the method
if (!HttpMethods.IsGet(request.Method) && !HttpMethods.IsHead(request.Method))
{
context.Logger.RequestMethodNotCacheable(request.Method);
return false;
}
// Verify existence of authorization headers
if (!StringValues.IsNullOrEmpty(request.Headers.Authorization) || request.HttpContext.User?.Identity?.IsAuthenticated == true)
{
context.Logger.RequestWithAuthorizationNotCacheable();
return false;
}
return true;
}
Если это не подходит для вашего приложения, вы можете создать свою собственную политику, реализовав интерфейс IOutputCachePolicy
.
Затем созданную политику необходимо предоставить AddBasePolicy
.
Использование OutputCache с контроллерами
До сих пор мы видели, как использовать промежуточное ПО OutputCache
с минимальными API.
Но, поскольку это всего лишь промежуточное ПО, его можно использовать и с традиционными контроллерами.
Как видно из примера ниже, вам просто нужно добавить атрибут OutputCache
поверх метода контроллера.
Или, когда вы хотите включить кэширование вывода для всего контроллера, вы можете добавить атрибут OutputCache
в класс контроллера.
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OutputCaching;
namespace OutputCaching.Controllers;
[ApiController]
[Route("[controller]")]
[OutputCache] // enable caching for the whole controller
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
[HttpGet]
[OutputCache] // enable caching for a specifc endpoint
public async Task<IEnumerable<WeatherForecast>> Get()
{
await Task.Delay(1000);
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
Summaries[Random.Shared.Next(Summaries.Length)]
))
.ToArray();
return forecast;
}
}
Заключение
В этом посте мы рассмотрели большинство возможностей нового промежуточного ПО кэширования вывода.
Использование промежуточного ПО повышает производительность вашего приложения.
Рассматривая API, мы увидели, что для использования промежуточного ПО кэширования практически не требуется код, при этом оно гибко настраивается под ваши нужды.
Для получения дополнительной информации см. выпуск GitHub, в котором обсуждается новое промежуточное ПО кэширования вывода. Для получения дополнительных примеров вы также можете взглянуть на OutputCachingSample.
Следуйте за мной в Twitter по адресу @tim_deschryver | Подпишитесь на рассылку | Первоначально опубликовано на timdeschryver.dev.