MatterOfFact
MatterOfFact

Reputation: 1555

Conditionally exclude/include properties from dto using .NET 5 Web API (Field Level Access)

I'm developing a .NET 5 Web Api with the default System.Text.Json model binding. For several dtos I need something like this:

public class MyDto
    {
        public string Name { get; set; }
        public string PublicDetails { get; set; }
        [IncludeForRoles("admin", "staff")]
        public string InternalDetails { get; set; }
    }

If a user with a role other than "admin" or "staff" calls the endpoint which returns the above mentioned dto, the property "InternalDetails" should be ignored on model binding and not be added to the serialized dto. Is there a standard way to do this? If not, how could I implement this behavior manually?

Upvotes: 0

Views: 5373

Answers (3)

MatterOfFact
MatterOfFact

Reputation: 1555

I've found a kind of workaround which meets the requirements but has the following disadvantages:

  • Data which is not needed is loaded from the database (Automapper's .ProjectTo cannot be used)
  • Data is modified after mapping (Injection of services into Automapper's Profile is not possible, see here)
  • Two attributes have to be added to every property which should be ignored in some cases.

You see, this is not a beautiful and elegant solution. Feel free to suggest a better one!

The attribute:

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class IncludeForRolesAttribute : Attribute
{
    public IncludeForRolesAttribute(params string[] roleNames)
    {
        RoleNames = roleNames;
    }
    public string[] RoleNames { get; }
}

The Automapper Mapping Action:

public class RoleBasedSetNullAction<TSource, TDestination> : IMappingAction<TSource, TDestination>
{
    private readonly ITokenAccessor _tokenAccessor;

    public RoleBasedSetNullAction(ITokenAccessor tokenAccessor)
    {
        _tokenAccessor = tokenAccessor ?? throw new ArgumentNullException(nameof(tokenAccessor));
    }

    public void Process(TSource source, TDestination destination, ResolutionContext context)
    {
        IEnumerable<PropertyInfo> props = typeof(TDestination).GetProperties().Where(
        prop => Attribute.IsDefined(prop, typeof(IncludeForRolesAttribute)));

        foreach (PropertyInfo prop in props)
        {
            IncludeForRolesAttribute attr = prop.GetCustomAttribute<IncludeForRolesAttribute>();
            if (!_tokenAccessor.UserRoles.Intersect(attr.RoleNames).Any())
            {
                prop.SetValue(destination, null);
            }
        }
    }
}

The DTO:

public class MyDto : BaseEntityDto
{
    public string Name { get; set; }
    
    public string PublicDetails { get; set; }

    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
    [IncludeForRoles("admin", "staff")]
    public string InternalDetails { get; set; }
}

The Automapper Mapping Profile:

public class MyDtoMappingProfile : Profile
{
    public MyDtoMappingProfile()
    {
        CreateMap<MyEntity, MyDto>()
            .AfterMap<RoleBasedSetNullAction<MyEntity, MyDto>>();
    }
}

Upvotes: 0

blockingHD
blockingHD

Reputation: 463

There is a [JsonIgnore] attribute which takes a condition that will ignore properties if they are null or default.

Assuming that you will know the role in the controller, only get the InternalDetails information if they have the relevant role and keep it as null if not. This also reduces query times as you aren't getting information you don't need.

Upvotes: 2

Roman Kolesnikov
Roman Kolesnikov

Reputation: 12147

I would recommend to use Newtonsoft.JSON library for serialization and implement custom ContractResolver to dynamically decide which fields to serialize.

Upvotes: 0

Related Questions