網頁

Sunday, October 25, 2020

[.NET core] System.NotSupportedException: The collection type 'System.Object' on 'somewhere' is not supported.

Somehow if you are using .NET core and return a object on controller, you might meet this exception.
   

  System.NotSupportedException: The collection type 'System.Object' on 'somewhere.InnerResult.Data' is not supported.
   at System.Text.Json.JsonPropertyInfoNotNullable`4.GetDictionaryKeyAndValueFromGenericDictionary(WriteStackFrame& writeStackFrame, String& key, Object& value)
   at System.Text.Json.JsonPropertyInfo.GetDictionaryKeyAndValue(WriteStackFrame& writeStackFrame, String& key, Object& value)
   at System.Text.Json.JsonSerializer.HandleDictionary(JsonClassInfo elementClassInfo, JsonSerializerOptions options, Utf8JsonWriter writer, WriteStack& state)
   at System.Text.Json.JsonSerializer.Write(Utf8JsonWriter writer, Int32 originalWriterDepth, Int32 flushThreshold, JsonSerializerOptions options, WriteStack& state)
   at System.Text.Json.JsonSerializer.WriteCore(Utf8JsonWriter writer, Object value, Type type, JsonSerializerOptions options)
   at System.Text.Json.JsonSerializer.WriteCore(PooledByteBufferWriter output, Object value, Type type, JsonSerializerOptions options)
   at System.Text.Json.JsonSerializer.WriteCoreString(Object value, Type type, JsonSerializerOptions options)
   at System.Text.Json.JsonSerializer.Serialize[TValue](TValue value, JsonSerializerOptions options)
   at TestWeb.Controllers.AuthController.VerifyToken(String token, Int32 tp) in C:\repos\somewhere\TestWeb\Controllers\AuthController.cs:line 51

In this project, I always return my custom result called InnerResult in API controller.
    public class InnerResult
    {
        public bool Success { get; set; } = true;
        public int Code { get; set; }
        public string Message { get; set; }
        public dynamic Data { get; set; }
    }

If the result json was this format, .NET core default can handle it.
{
    "success": true,
    "code": 0,
    "message": null,
    "data": "vfu3MF5RFlQV9dWNQCFh6iRPNxeezFaV"
}

but if some object in property, it will throw System.NotSupportedException

{
    "success": true,
    "code": 0,
    "message": null,
    "data": {
        "name": "Died"
    }
}

That's because in .NET core , they default using System.Text.Json to deal with json convert, but somehow it can't handle object in property, so......

To solve it, we can use our old friend Json.NET to save the day. (actually, using Microsoft.AspNetCore.Mvc.NewtonsoftJson)

Install-Package Microsoft.AspNetCore.Mvc.NewtonsoftJson -Version 3.1.9

If you are using .NET core 2.x , add .AddNewtonsoftJson() after your AddMVC()

services.AddMvc().AddNewtonsoftJson();

If using .NET core 3.x , add it after what you have.

services.AddControllers().AddNewtonsoftJson();
services.AddControllersWithViews().AddNewtonsoftJson();
services.AddRazorPages().AddNewtonsoftJson();

Then no more this System.NotSupportedExceptio again.



Sunday, October 18, 2020

[.NET core] AddPolicy to Authorize on MVC controller

Sometimes when you are using C# identity, the [Authorize] attribute is not enough,for example, you want additional check not only claims or role, then you can try add policy to make it. 

In this example, we assume a part of method need check token in user claims before access, here is how to implement it. 

1.add a AuthorizationHandler for your policy 
 Handler/AuthorizeTokenHandler.cs
namespace Example.Handler
{
    // Custom AuthorizationHandler for check token in claim
    public class AuthorizeTokenHandler : AuthorizationHandler<TokenRequirement>
    {
        private readonly AuthService _auth;

        public AuthorizeTokenHandler(AuthService auth)
        {
            _auth = auth;
        }
        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, TokenRequirement requirement)
        {
            //no token claim
            if (!context.User.HasClaim(x => x.Type == "Token"))
            {
                context.Fail();
                return Task.CompletedTask;
            }

            var username = context.User.FindFirst(ClaimTypes.Name).Value;
            var token = context.User.FindFirst("Token").Value;

            #region check token
            if (_auth.CheckToken(username,token))
            {
                context.Succeed(requirement);
            }
            else
            {
                context.Fail();
            }
            #endregion

            return Task.CompletedTask;
        }
    }

    public class TokenRequirement : IAuthorizationRequirement
    {
    }

2. add to startup, in ConfigureServices

            //handler
            services.AddSingleton<IAuthorizationHandler, AuthorizeTokenHandler>();
            
            //add policy
            services.AddAuthorization(options =>
            {
                options.AddPolicy("TokenRequire", policy =>
                    policy.Requirements.Add(new TokenRequirement()));
            });

3. apply policy to Authorize when you need

[Authorize(Policy = "TokenRequire")]
[HttpPost]
public ApiResult SomeMethod()
{
}

That's all, enjoy it.

Sunday, October 11, 2020

[C#] Best Practices of Get Claim from Identity

If you are using ASP.NET Identity, whatever using ASP.NET Core Identy or ASP.NET Identity, usually you will add some claims into the identity and use it later, but I saw lot people using unsafe way to get claim value from identity, it may cause uncatch error if the claim type not exists, let's see the code.
//set some claims first
var claims = new List<Claim>
{
     new Claim(ClaimTypes.Name,"test name"),
     new Claim("Token","token123"),
     new Claim("Number","3")
     //new Claim("dummy","dummy string")
};
var ci = new ClaimsIdentity(claims);

//most seen way, throw error if not exists
var name = ci.Claims.First(x => x.Type == ClaimTypes.Name).Value;
var token = ci.Claims.First(x => x.Type == "Token").Value;  
//using FirstOrDefault() only, throw error if not exists
//var dunno = ci.Claims.FirstOrDefault(x => x.Type == "dunno").Value;

Using this way is easy, but if the claim not exists, if will cause error, whatever you are using First() or FirstOrDefault(). If want to using FirstOrDefault() to get claim value, you can using this fixed way.

            //fixed FirstOrDefault way: return null if not exists
            var dunno = ci.Claims.Where(x => x.Type == "dunno").Select(x => x.Value).FirstOrDefault();
            

but you can use HasClaim method to check claim exist first, and give a proper value if it not exists.

            //safe way: using HasClaim to check
            var dummy = ci.HasClaim(x => x.Type == "dummy")
                ? ci.Claims.First(x => x.Type == "dummy").Value
                : null;
            

or you can choose this short way by using FindFirst method.

            //short way: using FindFirst
            var dummyShort = ci.FindFirst("dummy")?.Value;
            //short way for int
            int.TryParse(ci.FindFirst("Number")?.Value, out var number);
            

I think use ClaimsIdentity.FindFirst Method is the best practices for get claim value.
You can test those here in here