Search code examples
c#sessionasp.net-core-mvc

How to store UserData across whole app in ASP.NET Core MVC


I am trying to create an app which is like a system for sellers to manage their stocks and orders. I don't want to have hundreds of calls to my API so I want to store the user after they log in and only call API again if I need to. I have tried all different "solutions" but nothing works. I don't want to use Identity because my API doesn't use it and it would be too complicated for what I am doing.

At first I stored the user in the singleton UserService as a static property but then I realised that that leads to using the same User property in all instances of the app so I tried to add sessions and authentication but I just don't understand how all that works and I don't even know where to begin learning it.

This is my UserService right now:

using Market.Data.Models;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Caching.Memory;
using System.Net;
using System.Security.Claims;
using System.Text;
using System.Text.Json;
using System.Diagnostics;

namespace Market.Services
{
    public class UserService : IUserService
    {
        private readonly IHttpClientFactory factory;
        private readonly IHttpContextAccessor httpContextAccessor;
        private readonly HttpClient client;
        private User? User;

        public UserService(IHttpClientFactory httpClientFactory, IHttpContextAccessor httpContextAccessor)
        {
            factory = httpClientFactory;
            client = factory.CreateClient();
            client.BaseAddress = new Uri("https://farmers-market.sommee.com/api/");
            this.httpContextAccessor = httpContextAccessor;
            User = GetUser();
        }

        private async Task SaveUserToContext(User user)
        {
            var claims = new List<Claim>
            {
                new Claim(ClaimTypes.UserData, JsonSerializer.Serialize(user)),
            };

            var claimsIdentity = new ClaimsIdentity(
                claims, CookieAuthenticationDefaults.AuthenticationScheme);

            httpContextAccessor.HttpContext.User = new ClaimsPrincipal(claimsIdentity);
        }

        public async Task<User> Login(string email, string password)
        {
            var url = $"https://farmers-market.somee.com/api/users/login?email={email}&password={password}";
            var response = await client.GetAsync(url);
            var result = new User();

            if (response.IsSuccessStatusCode)
            {
                var stringResponse = await response.Content.ReadAsStringAsync();
                Console.WriteLine(stringResponse);

                result = JsonSerializer.Deserialize<User>(stringResponse,
                         new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
                User = result;
                await SaveUserToContext(result!);
            }
            else
            {
                throw new HttpRequestException(response.ReasonPhrase);
            }

            if (result ==  null)
            { 
                throw new Exception("Error with login");
            }

            return result;
        }

        public async Task<HttpStatusCode> Register(User user)
        {
            var url = $"https://farmers-market.somee.com/api/users/add";
            var jsonParsed = JsonSerializer.Serialize<User>(user, new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
            HttpContent content = new StringContent(jsonParsed.ToString(), Encoding.UTF8, "application/json");
            var response = await client.PostAsync(url, content);
            return response.StatusCode;
        }

        public Task RemoveOrderAsync(int orderId)
        {
            if (User == null)
            {
                throw new Exception("User is not authenticated");
            }

            User.SoldOrders.Remove(User.SoldOrders.Single(x => x.Id == orderId));
            return Task.CompletedTask;
        }

        public void AddApprovedOrder(int id)
        {
            User!.SoldOrders.Single(x => x.Id == id).IsApproved = true;
        }

        public void AddDeliveredOrder(int id)
        {
            User!.SoldOrders.Single(x => x.Id == id).IsDelivered = true;
        }

        public User? GetUser()
        {
            var saved = httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.UserData);
            if (saved == null) 
                return null;

            User = JsonSerializer.Deserialize<User>(saved);
            return User;
        }
    }
}

In this approach I am saving the user to the HttpContext but after the service is used in another controller the saved UserData is gone.

This is my program.cs:

using Market.Services;
using Market.Services.Firebase;
using Market.Services.Inventory;
using Market.Services.Offers;
using Market.Services.Orders;
using Market.Services.Reviews;
using Microsoft.AspNetCore.Authentication.Cookies;
using Market.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Market.Data.Models;

var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddHttpClient();
builder.Services.AddDistributedMemoryCache();

builder.Services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromMinutes(30); // Adjust timeout as needed
    options.Cookie.HttpOnly = true;
    options.Cookie.Name = "SessionCookie_" + Guid.NewGuid().ToString();
    options.Cookie.IsEssential = true;
});

builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
        .AddCookie(options =>
        {
            // Set a unique cookie name for this instance of the app
            options.Cookie.Name = "AuthCookie_" + Guid.NewGuid().ToString(); // Or use another unique value
        });

builder.Services.AddHttpContextAccessor();

builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<IInventoryService, InventoryService>();
builder.Services.AddScoped<IOfferService, OfferService>();
builder.Services.AddScoped<IFirebaseServive, FirebaseService>();
builder.Services.AddScoped<IOrdersService, OrdersService>();
builder.Services.AddScoped<IReviewsService, ReviewsService>();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.UseSession();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

I am open to all solutions or different approaches.


Solution

  • In my opinion, session and cookie is not a good way to store the stocks,orders and have used the in-memory session or cookies.

    Firstly, In-memroy session will expire after the setting time and the application restarted which means if user is using your app, and suddenly the application restarted or something happened, all the data which user had will miss. It will cause a big usage problem. If the user want to query the stocks,orders they have used, but something happened which cause application couldn't store the data from the session, this makes your application not reliable.

    Besides, you also need consider your data is always the newest, in my opinion, the stocks and the orders is changed very frequently which means the data inside the session is the wrong data, how you keep all the data is the newest and the same?

    All in all, I still don't suggest you choose the session to store these two data.Using the API to get the data is the best approach and inside the cache you could store some not changed data per user.

    In this approach I am saving the user to the HttpContext but after the service is used in another controller the saved UserData is gone.

    This is related with your SaveUserToContext method, the SaveUserToContext method is add claims to the httpcontext, but the claims are read from the cookie, if you just add some claims inside the httpcontext, it will not keep the data into the next request, since it doesn't add them to the cookie.

    If you want to add the new claims to the cookie, you need re-call the httpContextAccessor.HttpContext.SignInAsync method to reset the cookie for the client user.

    Codes like below:

    private async Task SaveUserToContext(User user)
    {
        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.UserData, JsonSerializer.Serialize(user)),
        };
    
        var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
    
        var authProperties = new AuthenticationProperties
        {
            IsPersistent = true,
            ExpiresUtc = DateTime.UtcNow.AddMinutes(30), // Set a custom expiry if needed
        };
    
        await httpContextAccessor.HttpContext.SignInAsync(
            CookieAuthenticationDefaults.AuthenticationScheme,
            new ClaimsPrincipal(claimsIdentity),
            authProperties);
    }
    

    Result:

    My test example:

        private async Task SaveUserToContext( )
        {
            var claims = new List<Claim>
        {
            new Claim(ClaimTypes.UserData, "JsonSerializer.Serialize(user)"),
        };
    
            var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
    
            var authProperties = new AuthenticationProperties
            {
                IsPersistent = true,
                ExpiresUtc = DateTime.UtcNow.AddMinutes(30), // Set a custom expiry if needed
            };
    
            await _contextAccessor.HttpContext.SignInAsync(
                CookieAuthenticationDefaults.AuthenticationScheme,
                new ClaimsPrincipal(claimsIdentity),
                authProperties);
        }
    

    enter image description here