Base solution for your next web application
Open Closed

Issues with deserializing ExternalLoginProviderInfo when using Redis Cache #12164


User avatar
0
dirkvr created

We are using ASP.net Zero based on ABP 9.2.2. When opening a login page (where a tenant is already selected), an exception is thrown in the backend :

System.NotSupportedException: 'Reference metadata is not supported when deserializing constructor parameters. See type 'Abp.AspNetZeroCore.Web.Authentication.External.ExternalLoginProviderInfo'. The unsupported member type is located on type 'System.Collections.Generic.Dictionary`2[System.String,System.String]'. Path: $.AdditionalParams.$ref | LineNumber: 0 | BytePositionInLine: 284.'

How can this be solved?


9 Answer(s)
  • User Avatar
    0
    m.aliozkaya created
    Support Team

    Hi @dirkvr,

    Do you encounter this issue when creating a new project with ASP.NET Zero as well? Could you tell me the steps to reproduce it

  • User Avatar
    0
    dirkvr created

    We are using ASP.net Zero (Angular + ASP.Net) already for sometime and have build a custom application based on it. Until now our application is running on one server. Recently we have build a clustered environment where ASP.net is running on 2 servers behind a load-balancer. Both are running on the same database.

    The issue happens when we first login as a host-admin. Then we select a tenant and login as that tenant. And then we logout again. The issue happens when the login-page (with tenant logo) opens again. Meanwhile we have built a workaround where we have developed a JsonConverter for ExternalLoginProviderInfo

  • User Avatar
    0
    m.aliozkaya created
    Support Team

    Hi @dirkvr,

    This problem seems to be related to the migration from Newtonsoft.Json to System.Text.Json. If you upgrade your project to the latest abp version, this problem could be solved. If the problem is not resolved with this I will open an issue about it.

  • User Avatar
    0
    dirkvr created

    From which version on the switch was done from Newtonsoft.Json to System.Text.Json? If I look at the release notes this was in 9.1. We are on 9.2.2.

  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi,

    Could you follow this issue https://github.com/aspnetzero/aspnet-zero-core/issues/5399. You can implement the workaround offered here as a temporary fix https://github.com/aspnetzero/aspnet-zero-core/issues/5399#issuecomment-2358375816

  • User Avatar
    0
    dirkvr created

    Hi,

    If I click on the first link, I get a 404. Is this link correct?

  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi,

    You can add your GitHub user on https://aspnetzero.com/LicenseManagement or you can send your Github username to [email protected] so we can invite you. Then, you can see the content of the link.

  • User Avatar
    0
    dirkvr created

    Many Thanks for you help. Our workaround is very similar. We had multiple issues with the DefaultRedisCacheSerializer:

    • We wanted to serialize an array with circular references
    • We had issues with ApiProviderType : which is also solved with your workaround
    • We had issues with serializing the property "AdditionalParams" of ExternalLoginProviderInfo. This is solved with a separate converter for ExternalLogproviderInfo.

    This is our cache serializer :

    using Abp.Dependency; using Abp.Runtime.Caching.Redis; using Abp.Runtime.Caching; using StackExchange.Redis; using System; using System.Text.Encodings.Web; using System.Text.Json; using Abp.Json; using System.Text.Json.Serialization; using Abp.Json.SystemTextJson;

    namespace RmoniWeb.Helpers { public class RmoniWebRedisCacheSerializer : IRedisCacheSerializer, ITransientDependency { /// <summary> /// Creates an instance of the object from its serialized string representation. /// </summary> /// <param name="objbyte">String representation of the object from the Redis server.</param> /// <returns>Returns a newly constructed object.</returns> /// <seealso cref="IRedisCacheSerializer{TSource, TDestination}.Serialize" /> public virtual object Deserialize(RedisValue objbyte) private readonly JsonSerializerOptions _simpleJsonSerializerOptions = new() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, ReferenceHandler = ReferenceHandler.Preserve };

        private readonly JsonSerializerOptions _jsonSerializerOptionsWithConverters = new()
        {
            ReadCommentHandling = JsonCommentHandling.Skip,
            AllowTrailingCommas = true,
            Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
            TypeInfoResolver = new AbpDateTimeJsonTypeInfoResolver(),
            ReferenceHandler = ReferenceHandler.Preserve,
            Converters =
                {
                    new AbpStringToEnumFactory(),
                    new AbpStringToBooleanConverter(),
                    new AbpStringToGuidConverter(),
                    new AbpNullableStringToGuidConverter(),
                    new AbpNullableFromEmptyStringConverterFactory(),
                    new ObjectToInferredTypesConverter(),
                    new AbpJsonConverterForType(),
                    new ExternalLoginProviderInfoConverter(),
                }
        };
    
        /// &lt;summary&gt;
        ///     Creates an instance of the object from its serialized string representation.
        /// &lt;/summary&gt;
        /// &lt;param name=&quot;objbyte&quot;&gt;String representation of the object from the Redis server.&lt;/param&gt;
        /// &lt;returns&gt;Returns a newly constructed object.&lt;/returns&gt;
        /// &lt;seealso cref=&quot;IRedisCacheSerializer{TSource, TDestination}.Serialize&quot; /&gt;
        public virtual object Deserialize(RedisValue objbyte)
    	{
    		if ((string)objbyte is null)
    			return null;
    
    		var convertedValue = JsonSerializer.Deserialize&lt;AbpCacheData&gt;(objbyte, _jsonSerializerOptionsWithConverters);
    		var type = Type.GetType(convertedValue.Type, true, true);
    		return convertedValue.Payload.FromJsonString(type, _jsonSerializerOptionsWithConverters);
    	}
    
    	private readonly JsonSerializerOptions _simpleJsonSerializerOptions = new()
    	{
    		Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
    		ReferenceHandler = ReferenceHandler.Preserve
    	};
    
    	private readonly JsonSerializerOptions _jsonSerializerOptionsWithConverters = new()
    	{
    		ReadCommentHandling = JsonCommentHandling.Skip,
    		AllowTrailingCommas = true,
    		Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
    		TypeInfoResolver = new AbpDateTimeJsonTypeInfoResolver(),
    		ReferenceHandler = ReferenceHandler.Preserve,
    		Converters =
    			{
    				new AbpStringToEnumFactory(),
    				new AbpStringToBooleanConverter(),
    				new AbpStringToGuidConverter(),
    				new AbpNullableStringToGuidConverter(),
    				new AbpNullableFromEmptyStringConverterFactory(),
    				new ObjectToInferredTypesConverter(),
    				new AbpJsonConverterForType(),
    				new ExternalLoginProviderInfoConverter(),
    			}
    	};
    
    	/// &lt;summary&gt;
    	///     Produce a string representation of the supplied object.
    	/// &lt;/summary&gt;
    	/// &lt;param name=&quot;value&quot;&gt;Instance to serialize.&lt;/param&gt;
    	/// &lt;param name=&quot;type&quot;&gt;Type of the object.&lt;/param&gt;
    	/// &lt;returns&gt;Returns a string representing the object instance that can be placed into the Redis cache.&lt;/returns&gt;
    	/// &lt;seealso cref=&quot;IRedisCacheSerializer{TSource, TDestination}.Deserialize&quot; /&gt;
    	public virtual RedisValue Serialize(object value, Type type)
    	{
    		var typeString = TypeHelper.SerializeType(value.GetType()).ToString();
    		var convertedValue = JsonSerializer.Serialize(value, _jsonSerializerOptionsWithConverters);
    		var json = new AbpCacheData(typeString, convertedValue);
    
    		return JsonSerializer.Serialize(json, _jsonSerializerOptionsWithConverters);
    	}
    
    }
    

    }

    This is the converter for ExterLoginProviderInfo: using Abp.AspNetZeroCore.Web.Authentication.External; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks;

    namespace RmoniWeb.Helpers { public class ExternalLoginProviderInfoConverter : JsonConverter<ExternalLoginProviderInfo> { private Dictionary<string, string> ReadDictionary(ref Utf8JsonReader reader) { Dictionary<string, string> dictionary = new Dictionary<string, string>(); while (reader.Read()) { if (reader.TokenType == JsonTokenType.EndObject) { break; } if (reader.TokenType != JsonTokenType.StartObject) { throw new JsonException("Expected StartObject token."); }

    			while (reader.Read())
    			{
    
    				if (reader.TokenType == JsonTokenType.EndObject)
    				{
    					// End of the current JsonClaimMap object, break
    					break;
    				}
    				// Read the key (property name)
    				if (reader.TokenType == JsonTokenType.PropertyName)
    				{
    					string key = reader.GetString();
    
    					// Read the value
    					reader.Read();
    					if (reader.TokenType != JsonTokenType.Null && reader.TokenType != JsonTokenType.String)
    					{
    						throw new JsonException("Expected string value.");
    					}
    
    					string value = reader.GetString();
    
    					// Add the key-value pair to the dictionary
    					dictionary[key] = value;
    				}
    			}
    
    		}
    		return dictionary;
    	}
    	private List&lt;JsonClaimMap&gt; ReadJsonClaimMapList(ref Utf8JsonReader reader)
    	{
    		List&lt;JsonClaimMap&gt; claimMapList = new List&lt;JsonClaimMap&gt;();
    
    		// Ensure we are at the start of an array
    		//if (reader.TokenType != JsonTokenType.StartArray)
    		//{
    		//	throw new JsonException("Expected StartArray token.");
    		//}
    
    		// Read the array
    		while (reader.Read())
    		{
    			// Break if we reach the end of the array
    			if (reader.TokenType == JsonTokenType.EndArray)
    			{
    				break;
    			}
    
    			// Ensure we're starting an object
    			if (reader.TokenType != JsonTokenType.StartArray)
    			{
    				throw new JsonException("Expected StartArray token.");
    			}
    
    			// Create a new JsonClaimMap instance
    			JsonClaimMap claimMap = new JsonClaimMap();
    
    			// Read properties within the object
    			while (reader.Read())
    			{
    				if (reader.TokenType == JsonTokenType.EndObject)
    				{
    					// End of the current JsonClaimMap object, break
    					break;
    				}
    
    				// Get the property name
    				if (reader.TokenType == JsonTokenType.PropertyName)
    				{
    					string propertyName = reader.GetString();
    
    					// Read the next token (which should be the value)
    					if (!reader.Read())
    					{
    						throw new JsonException("Expected a value after property name.");
    					}
    
    					// Assign the property based on the property name
    					if (propertyName == "Claim")
    					{
    						claimMap.Claim = reader.GetString();
    					}
    					else if (propertyName == "Key")
    					{
    						claimMap.Key = reader.GetString();
    					}
    				}
    			}
    
    			// Add the parsed JsonClaimMap to the list
    			claimMapList.Add(claimMap);
    		}
    
    		return claimMapList;
    	}
    	public override ExternalLoginProviderInfo Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    	{
    		// Create a temporary object to hold the JSON data
    		Dictionary&lt;string, object&gt; tempObject = JsonSerializer.Deserialize&lt;Dictionary&lt;string, object&gt;>(ref reader, options);
    
    		// Extract the properties manually
    		string name = tempObject["Name"]?.ToString();
    		string clientId = tempObject["ClientId"]?.ToString();
    		string clientSecret = tempObject["ClientSecret"]?.ToString();
    
    		Type providerApiType = Type.GetType(tempObject["ProviderApiType"].ToString());
    		//Dictionary&lt;string, string&gt; additionalParams = JsonSerializer.Deserialize&lt;Dictionary&lt;string, string&gt;>(tempObject["AdditionalParams"].ToString());
    		byte[] paramBytes = Encoding.UTF8.GetBytes(tempObject["AdditionalParams"].ToString());
    		Type providerApiType = Type.GetType(tempObject["ProviderApiType"]?.ToString());
    		byte[] paramBytes = Encoding.UTF8.GetBytes(tempObject["AdditionalParams"]?.ToString());
    		ReadOnlySpan&lt;byte&gt; paramSpan = new ReadOnlySpan&lt;byte&gt;(paramBytes);
    		var paramReader = new Utf8JsonReader(paramSpan);
    		var additionalParams = ReadDictionary(ref paramReader);
    
    		byte[] claimBytes = Encoding.UTF8.GetBytes(tempObject["ClaimMappings"].ToString());
    		byte[] claimBytes = Encoding.UTF8.GetBytes(tempObject["ClaimMappings"]?.ToString());
    		ReadOnlySpan&lt;byte&gt; claimSpan = new ReadOnlySpan&lt;byte&gt;(claimBytes);
    		var claimReader = new Utf8JsonReader(claimSpan);
    		var claimMappings = ReadJsonClaimMapList(ref claimReader);
    
    		// Create the ExternalLoginProviderInfo instance using the constructor
    		return new ExternalLoginProviderInfo(name, clientId, clientSecret, providerApiType, additionalParams, claimMappings);
    
    	}
    
    	public override void Write(Utf8JsonWriter writer, ExternalLoginProviderInfo value, JsonSerializerOptions options)
    	{
    		// Handle serialization if needed
    		writer.WriteStartObject();
    		writer.WriteString("Name", value.Name);
    		writer.WriteString("ClientId", value.ClientId);
    		writer.WriteString("ClientSecret", value.ClientSecret);
    		var assemblyQualifiedName = TypeHelper.SerializeType(value.ProviderApiType).ToString();
    		writer.WriteString("ProviderApiType", assemblyQualifiedName);
    		writer.WritePropertyName("AdditionalParams");
    		writer.WriteStartObject();
    		foreach (var kvp in value.AdditionalParams)
    		{
    			// Write each key-value pair as a JSON property
    			writer.WriteString(kvp.Key, kvp.Value);
    		}
    		writer.WriteEndObject();
    		writer.WritePropertyName("ClaimMappings");
    		writer.WriteStartArray();
    		foreach (var claim in value.ClaimMappings)
    		{
    			writer.WriteStartObject();
    			writer.WriteString("Claim", claim.Claim);
    			writer.WriteString("Key", claim.Key);
    			writer.WriteEndObject();
    		}
    		writer.WriteEndArray();
    		writer.WriteEndObject();
    	}
    }
        private static Dictionary&lt;string, string&gt; ReadDictionary(ref Utf8JsonReader reader)
        {
            var dictionary = new Dictionary&lt;string, string&gt;();
            while (reader.Read())
            {
                if (reader.TokenType == JsonTokenType.EndObject)
                {
                    break;
                }
                if (reader.TokenType != JsonTokenType.StartObject)
                {
                    throw new JsonException("Expected StartObject token.");
                }
    
                while (reader.Read())
                {
    
                    if (reader.TokenType == JsonTokenType.EndObject)
                    {
                        // End of the current JsonClaimMap object, break
                        break;
                    }
                    // Read the key (property name)
                    if (reader.TokenType == JsonTokenType.PropertyName)
                    {
                        var key = reader.GetString();
    
                        // Read the value
                        reader.Read();
                        if (reader.TokenType != JsonTokenType.Null && reader.TokenType != JsonTokenType.String)
                        {
                            throw new JsonException("Expected string value.");
                        }
    
                        var value = reader.GetString();
    
                        // Add the key-value pair to the dictionary
                        if (key != null)
                            dictionary[key] = value;
                    }
                }
    
            }
            return dictionary;
        }
        private static List&lt;JsonClaimMap&gt; ReadJsonClaimMapList(ref Utf8JsonReader reader)
        {
            List&lt;JsonClaimMap&gt; claimMapList = new List&lt;JsonClaimMap&gt;();
    
            // Read the array
            while (reader.Read())
            {
                // Break if we reach the end of the array
                if (reader.TokenType == JsonTokenType.EndArray)
                {
                    break;
                }
    
                // Ensure we're starting an object
                if (reader.TokenType != JsonTokenType.StartArray)
                {
                    throw new JsonException("Expected StartArray token.");
                }
    
                // Create a new JsonClaimMap instance
                JsonClaimMap claimMap = new JsonClaimMap();
    
                // Read properties within the object
                while (reader.Read())
                {
                    if (reader.TokenType == JsonTokenType.EndObject)
                    {
                        // End of the current JsonClaimMap object, break
                        break;
                    }
    
                    // Get the property name
                    if (reader.TokenType == JsonTokenType.PropertyName)
                    {
                        string propertyName = reader.GetString();
    
                        // Read the next token (which should be the value)
                        if (!reader.Read())
                        {
                            throw new JsonException("Expected a value after property name.");
                        }
    
                        // Assign the property based on the property name
                        if (propertyName == "Claim")
                        {
                            claimMap.Claim = reader.GetString();
                        }
                        else if (propertyName == "Key")
                        {
                            claimMap.Key = reader.GetString();
                        }
                    }
                }
    
                // Add the parsed JsonClaimMap to the list
                claimMapList.Add(claimMap);
            }
    
            return claimMapList;
        }
    }
    

    }

  • User Avatar
    0
    m.aliozkaya created
    Support Team

    Hi @dirkvr,

    Thanks for sharing your solution with us. We will fix this problem in the next version.