Как стать автором
Обновить
8
0
Алексей Захаров @leksiq

Пользователь

Отправить сообщение

Да, это в процессе исследования было, ближе к концу статьи после подзаголовка "Создание библиотеки" это уже не так.

Код
app.Use(async (context, next) =>
{
		IMemoryCache sessions = context.RequestServices.GetRequiredService<IMemoryCache>();
    string key = context.Request.Cookies[_fullStateOptions.Cookie.Name];
    //...
  	object? sessionObj = null;
    Session? session = null;
    bool isNewSession = false;
  	// Берём новую обёртку из скоупа запроса
    FullState fullState = (context.RequestServices.GetRequiredService<IFullState>() as FullState)!;
    if (
    		string.IsNullOrEmpty(key)
        || !sessions.TryGetValue(key, out sessionObj)
        || (session = sessionObj! as Session) is null
    )
    {
    		key = $"{Guid.NewGuid()}:{Interlocked.Increment(ref _cookieSequenceGen)}";
        session = new();
        session!.SessionServices = context.RequestServices.CreateScope().ServiceProvider;
        context.Response.Cookies.Append(_fullStateOptions.Cookie.Name, key, _fullStateOptions.Cookie.Build(context));
        isNewSession = true;
    }

  	// Добавляем в обёртку скоуп запроса
    fullState.RequestServices = context.RequestServices;
  	// Добавляем в обёртку скоуп сессии новой или ранее сохранённой
		fullState.Session = session;
		//...
});

Третье. По поводу вашей идеи хранить в сессии свойство RequestServiceProvider. Вы понимаете, что сессия — разделяемый объект, и к ней в один момент времени может обращаться несколько запросов?

У меня скоуп запроса в сессии не хранится. Сессия при каждом запросе обёртывается в объект времени жизни запроса, а скоуп текущего запроса подключается туда же. Я его ввёл только на тот крайний случай, если объект, порождённый скоупом сессии захочет во время запроса воспользоваться объектом времени жизни запроса.

Семафор уже успел добавить сам, остальные предложения изучу, спасибо!

IOptionsMonitor посмотрел, да доставать сессии удобно, складывать вообще не надо - сами создаются через стандартную фабрику:

Program.cs
using Microsoft.Extensions.Options;

string cookieName = "qq";
int cookieSequenceGen = 0;

var builder = WebApplication.CreateBuilder(new string[] { });

builder.Services.AddScoped<FullState1>();

WebApplication app = builder.Build();

app.Use(async (HttpContext context, Func<Task> next) =>
{
    string? key = context.Request.Cookies[cookieName];
    if(key is null)
    {
        key = $"{Guid.NewGuid()}:{Interlocked.Increment(ref cookieSequenceGen)}";
        context.Response.Cookies.Append(cookieName, key, new CookieBuilder().Build(context));
    }
    context.RequestServices.GetRequiredService<FullState1>().Session = context.RequestServices.GetRequiredService<IOptionsMonitor<Session1>>().Get(key);
    ++context.RequestServices.GetRequiredService<FullState1>().Session.RequestsCounter;

    next?.Invoke();
});

app.MapGet("/api", async (HttpContext context) =>
{
    Session1 session = context.RequestServices.GetRequiredService<FullState1>().Session;
    await context.Response.WriteAsync($"Hello, Client {session.GetHashCode()} (#{session.RequestsCounter})!");
});

app.Run();

public class FullState1
{
    internal Session1 Session { get; set; }
}

internal class Session1
{
    public int RequestsCounter { get; set; } = 0;
}

Запросы с разных клиентов поделал:

Скриншоты

Минус, на мой взгляд, в том, что видимо время простоя сессий нужно отслеживать, тогда как в MemoryCache это встроено. Его только трогать надо периодически, я в заметке упоминал об этом.

Насчёт CancellationTokenSource мне ваша идея понравилась, я ее реализовал. Так как это у меня условно фреймворк, то заранее неизвестно, какие задачи будут запускаться в сессиях, поэтому я храню в сессии родительский CancellationTokenSource, а выдаю связанные. Разработчик модели может сам их канцелировать, когда требуется, а вместе с родительским они канцелируются при истечении времени простоя.

Объект, который хранится в кэше:

Session.cs
internal class Session: IDisposable
{
    internal IServiceProvider SessionServices { get; set; } = null!;

    internal CancellationTokenSource CancellationTokenSource { get; init; } = new();

    public void Dispose()
    {
        if (!CancellationTokenSource.IsCancellationRequested)
        {
            CancellationTokenSource.Cancel();
        }
        CancellationTokenSource.Dispose();
        if (SessionServices is IDisposable disposable)
        {
            disposable.Dispose();
        }
    }

}

Интерфейс для доступа к скопу сессии из скопа запроса и наоборот, а также для получения связанного с сессией CancellationTokenSource:

Hidden text

IFullState.cs

public interface IFullState
{
    IServiceProvider RequestServices { get; }
    IServiceProvider SessionServices { get; }
    CancellationTokenSource CreateCancellationTokenSource();
}

Реализация:

FullState.cs
internal class FullState : IFullState
{
    internal Session Session { get; set; } = null!;

    public IServiceProvider RequestServices { get; internal set; } = null!;

    public IServiceProvider SessionServices => Session.SessionServices;

    public CancellationTokenSource CreateCancellationTokenSource()
    {
        return CancellationTokenSource.CreateLinkedTokenSource(Session.CancellationTokenSource.Token);
    }
}

При истечении сессии CancellationTokenSource канцелируется и вызывается его Dispose().

Добрый день, мне трудно проявиться, потому что я изложил свой взгляд, а то, что вы пишете, мне ещё нужно обдумать, возможно, проверить. Но ваши комментарии - повод двигаться дальше.

Который запросы по годам распределяет

Хорошо, спасибо за информацию

Наверное, менеджер можно так настроить, но утверждать не буду

Пока мне кажется проблемой, что мы должны получить именно нашу задачу или ее промежуточные результаты, а не сам сервис.

Наверное, сессия может обслуживаться всё время своей жизни на кластере, на котором началась.

Насколько я себе представляю, ссылку на hosted service всё равно надо как-то хранить, чтобы потом в сессии получить результат этой работы, или даже частичный результат, наработанный между запросами. Стандартный механизм не позволяет это делать (сохранить ссылку на объект).

Задача кратко описана здесь:

https://habr.com/ru/post/653395/

Это решение не для распределённой системы, а для системы масштаба предприятия.

Стандартный механизм не хранит объекты, только состояние в виде сериализации, мне же нужно, чтобы был объект и выполнял какую-то работу между запросами. Об этом я в заметке даже упомянул.

Попробовал. В принципе, всё работает, но есть неприятность, что VS не подсвечивает и не предлагает код, какие-то плагины подсвечивают, но не нашел, чтобы предлагали. То, что есть, не все работает в VS2022. То есть razorpages элементарно удобнее писать.

Спасибо, для рассмотренного примера самое то, как мне показалось, от вас впервые об этом услышал, буду изучать )

Спасибо, рассмотрел и решил так и сделать!

На мой взгляд, ORM всё же завязан на БД, предполагает достаточно прямое соответствие модели и таблиц. Не всегда это возможно. В моём случае используется унаследованная БД, довольно запутанная, но стоит задача совместимости старой и новой системы в течение долгого периода. Также, на мой взгляд, в вашем случае будут происходить отдельные запросы БД вместо одного. Также, данные могут приходить не только из БД. Например, файловая система или учетная запись IMAP...

На мой взгляд, ORM всё же завязан на БД, предполагает достаточно прямое соответствие модели и таблиц. Не всегда это возможно. В моём случае используется унаследованная БД, довольно запутанная, но стоит задача совместимости старой и новой системы в течение долгого периода. Также, на мой взгляд, в вашем случае будут происходить отдельные запросы БД вместо одного. Также, данные могут приходить не только из БД. Например, файловая система или учетная запись IMAP...

Информация

В рейтинге
Не участвует
Откуда
Пушкин, Санкт-Петербург и область, Россия
Зарегистрирован
Активность

Специализация

Fullstack Developer
Lead
C#
WPF
SOLID
Multiple thread
SQL
XSLT
ASP.NET Web API