Anonymous-ID Middleware für .NET Core

aspnet-core
Tags: #<Tag:0x00007fb1c0dc6b70>

#1

Ich habe die Anonymous-ID bereits in ASP.NET MVC Anwendungen verwendet, doch leider gibt es dieses Feature in .NET Core nicht mehr. Also habe ich meine erste .NET Core Middleware entwickelt, die ich mit euch hier teilen möchte.

Die AnonymousIdCookieOptions können in der Startup.cs definiert werden.

public class AnonymousIdCookieOptions
{
    public string CookieName { get; set; } = ".ASPXANONYMOUS";
    public bool SlidingExpiration { get; set; } = true;
    public int TimeoutInSeconds { get; set; } = 60 * 60 * 24;
}

Das Feature, welches für die Abfrage der Anonymous-ID im Code verwendet wird.

public interface IAnonymousIdFeature
{
    string AnonymousId { get; set; }
}

public class AnonymousIdFeature : IAnonymousIdFeature
{
    public string AnonymousId { get; set; }
}

Das Cookie-Objekt, welches über einen Encoder kodiert/decodiert und um ein ExpireDate ergänzt wird.

internal class AnonymousIdData
{
    internal readonly string AnonymousId;
    internal DateTime ExpireDate;

    internal AnonymousIdData(string id, DateTime timeStamp)
    {
        AnonymousId = timeStamp > DateTime.Now ? id : null;
        ExpireDate = timeStamp;
    }
} 

internal static class AnonymousIdEncoder
{
    internal static string Encode(AnonymousIdData data)
    {
        if (data == null || string.IsNullOrWhiteSpace(data.AnonymousId))
        {
            return null;
        }

        var bufferId = Encoding.UTF8.GetBytes(data.AnonymousId);
        var bufferIdLenght = BitConverter.GetBytes(bufferId.Length);
        var bufferDate = BitConverter.GetBytes(data.ExpireDate.ToFileTimeUtc());
        var buffer = new byte[12 + bufferId.Length];

        Buffer.BlockCopy(bufferDate, 0, buffer, 0, 8);
        Buffer.BlockCopy(bufferIdLenght, 0, buffer, 8, 4);
        Buffer.BlockCopy(bufferId, 0, buffer, 12, bufferId.Length);

        return WebEncoders.Base64UrlEncode(buffer);
    }

    internal static AnonymousIdData Decode(string data)
    {
        if (string.IsNullOrEmpty(data))
        {
            return null;
        }

        try
        {
            var blob = WebEncoders.Base64UrlDecode(data);

            if (blob == null || blob.Length < 13)
            {
                return null;
            }

            var expireDate = DateTime.FromFileTimeUtc(BitConverter.ToInt64(blob, 0));

            if (expireDate < DateTime.UtcNow)
            {
                return null;
            }

            var len = BitConverter.ToInt32(blob, 8);

            if (len < 0 || len > blob.Length - 12)
            {
                return null;
            }

            var id = Encoding.UTF8.GetString(blob, 12, len);

            return new AnonymousIdData(id, expireDate);
        }
        catch
        {
            // ignored
        }

        return null;
    }
}

Die eigentliche Anonymous-ID Middleware.

public class AnonymousIdMiddleware
{
    private readonly RequestDelegate _nextDelegate;
    private readonly AnonymousIdCookieOptions _cookieOptions;

    public AnonymousIdMiddleware(RequestDelegate nextDelegate, AnonymousIdCookieOptions cookieOptions)
    {
        _nextDelegate = nextDelegate;
        _cookieOptions = cookieOptions;
    }

    public async Task Invoke(HttpContext httpContext)
    {
        HandleRequest(httpContext);
        await _nextDelegate.Invoke(httpContext);
    }

    private void HandleRequest(HttpContext httpContext)
    {
        var now = DateTime.Now;

        // Gets the value and anonymous Id data from the cookie, if available
        var encodedValue = httpContext.Request.Cookies[_cookieOptions.CookieName];
        var decodedValue = AnonymousIdEncoder.Decode(encodedValue);

        string anonymousId = null;

        if (!string.IsNullOrWhiteSpace(decodedValue?.AnonymousId))
        {
            // Copy the existing value in Request header
            anonymousId = decodedValue.AnonymousId;

            // Adds the feature to request collection
            httpContext.Features.Set<IAnonymousIdFeature>(new AnonymousIdFeature
            {
                AnonymousId = anonymousId
            });
        }

        if (string.IsNullOrWhiteSpace(anonymousId))
        {
            // Creates a new identity
            anonymousId = Guid.NewGuid().ToString();

            // Adds the feature to request collection
            httpContext.Features.Set<IAnonymousIdFeature>(new AnonymousIdFeature()
            {
                AnonymousId = anonymousId
            });
        }
        else
        {
            // Sliding expiration is not required for this request
            if (!_cookieOptions.SlidingExpiration || decodedValue.ExpireDate > now && (decodedValue.ExpireDate - now).TotalSeconds > _cookieOptions.TimeoutInSeconds / 2.0)
            {
                return;
            }
        }

        // Appends the new cookie
        var expires = now.AddSeconds(_cookieOptions.TimeoutInSeconds);
        var data = new AnonymousIdData(anonymousId, expires);
        encodedValue = AnonymousIdEncoder.Encode(data);
        httpContext.Response.Cookies.Append(_cookieOptions.CookieName, encodedValue, new CookieOptions
        {
            Expires = expires
        });
    }

    public static void ClearAnonymousId(HttpContext httpContext, AnonymousIdCookieOptions cookieOptions)
    {
        if (!string.IsNullOrWhiteSpace(httpContext.Request.Cookies[cookieOptions.CookieName]))
        {
            httpContext.Response.Cookies.Delete(cookieOptions.CookieName);
        }
    }
}

public static class AnonymousIdMiddlewareExtensions
{
    public static IApplicationBuilder UseAnonymousId(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<AnonymousIdMiddleware>(new AnonymousIdCookieOptions());
    }

    public static IApplicationBuilder UseAnonymousId(this IApplicationBuilder builder, Action<AnonymousIdCookieOptions> configureOptions)
    {
        var options = new AnonymousIdCookieOptions();
        configureOptions.Invoke(options);
        return builder.UseMiddleware<AnonymousIdMiddleware>(options);
    }
}

In der Startup.cs die Middleware einbetten:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    ...
    app.UseAnonymousId(opts=> { opts.CookieName = ".TestAnonymous"; });
    ...
}

Die Middleware kann nun per DepencyInjection in der Anwendung verwendet werden (z. B. im HomeController):

public class HomeController : Controller
{
    public HomeController(IAnonymousIdFeature anonymousIdFeature)
    {
        var anonymousId = anonymousIdFeature.AnonymousId;
    }
}

Ich hoffe, ihr könnt diese Middleware mal in euren Anwendungen gebrauchen.
Über Optimierungen können wir gerne diskutieren :slight_smile: