Exclude property from WebApi OData (EF) response i

2019-01-22 23:15发布

问题:

I'm working with a WebApi project in C# (EF code first) and I'm using OData. I have a "User" model with Id, Name, LastName, Email, and Password.

In controller for example I have this code:

// GET: odata/Users
[EnableQuery]
public IQueryable<User> GetUsers()
{
    return db.Users;
}

If I call /odata/Users I'll get all the data: Id, Name, LastName, Email, and Password.

How can I exclude Password from results but keep available in controller to make Linq queries?

回答1:

I'm a little late to the topic but I think this might help you out.

I assume that you will want to encrypt the password for storage purposes. Have you looked at using an odata action to set the password? Using an action lets you ignore the password property when setting up your entities while still exposing a clean way to the end user to update the password.

first: ignore the password property

builder.EntitySet<UserInfo>("UserInfo").EntityType.Ignore(ui => ui.Password);

second: add your odata action

builder.EntityType<UserInfo>().Action("SetPassword").Returns<IHttpActionResult>();

Then add the SetPassword method to your UserInfoController.



回答2:

Add [NotMapped] attribute on the Password property in User Class as following:

public class User
{
    public int Id { get; set; }

    public string Name { get; set; }

    public string Email { get; set; }

    public string LastName {get; set; }

    [NotMapped]
    public string Password {get; set;}
}


回答3:

It might be a little late, but an elegant solution would be to add a custom QueryableSelectAttribute and then simply list the fields that you want selected on the serve-side. In your case it will look something like this:

public class QueryableSelectAttribute : ActionFilterAttribute
{
    private const string ODataSelectOption = "$select=";
    private string selectValue;

    public QueryableSelectAttribute(string select)
    {
        this.selectValue = select;
    }

    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        base.OnActionExecuting(actionContext);

        var request = actionContext.Request;
        var query = request.RequestUri.Query.Substring(1);
        var parts = query.Split('&').ToList();

        for (int i = 0; i < parts.Count; i++)
        {
            string segment = parts[i];
            if (segment.StartsWith(ODataSelectOption, StringComparison.Ordinal))
            {
                parts.Remove(segment);
                break;
            }
        }

        parts.Add(ODataSelectOption + this.selectValue);

        var modifiedRequestUri = new UriBuilder(request.RequestUri);
        modifiedRequestUri.Query = string.Join("&", parts.Where(p => p.Length > 0));
        request.RequestUri = modifiedRequestUri.Uri;

        base.OnActionExecuting(actionContext);
    }
}

And in the controller you simply add the attribute with the desired properties:

[EnableQuery]
[QueryableSelect("Name,LastName,Email")]
public IQueryable<User> GetUsers()
{
    return db.Users;
}

And that's it!

Of course the same principle can be applied for a custom QueryableExpandAttribute.



回答4:

How can I exclude Password from results but keep available in controller to make Linq queries?

Ignore it. From Security Guidance for ASP.NET Web API 2 OData:

There are two ways to exlude a property from the EDM. You can set the [IgnoreDataMember] attribute on the property in the model class:

public class Employee
{
    public string Name { get; set; }
    public string Title { get; set; }
    [IgnoreDataMember]
    public decimal Salary { get; set; } // Not visible in the EDM
}

You can also remove the property from the EDM programmatically:

var employees = modelBuilder.EntitySet<Employee>("Employees");
employees.EntityType.Ignore(emp => emp.Salary);


回答5:

What you need to do is make an odata controller that returns a projected subset of the original entity.

//in WebApi Config Method
config.MapHttpAttributeRoutes();

ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<FullEntity>("FullData");
builder.EntitySet<SubsetEntity>("SubsetData");
config.Routes.MapODataServiceRoute("odata", "odata", builder.GetEdmModel());


config.Routes.MapHttpRoute(
  name: "DefaultApi",
  routeTemplate: "api/{controller}/{action}/{id}",
  defaults: new { id = RouteParameter.Optional, action = "GET" }
);
SetupJsonFormatters();
config.Filters.Add(new UncaughtErrorHandlingFilterAttribute());

... then have two Odata Controllers one for FulLData, one for SubsetData (with different security),

namespace myapp.Web.OData.Controllers
{
    public class SubsetDataController : ODataController
    {
        private readonly IWarehouseRepository<FullEntity> _fullRepository;
        private readonly IUserRepository _userRepository;

        public SubsetDataController(
            IWarehouseRepository<fullEntity> fullRepository,
            IUserRepository userRepository
            )
        {
            _fullRepository = fullRepository;
            _userRepository = userRepository;
        }

public IQueryable<SubsetEntity> Get()
        {
            Object webHostHttpRequestContext = Request.Properties["MS_RequestContext"];
            System.Security.Claims.ClaimsPrincipal principal =
                (System.Security.Claims.ClaimsPrincipal)
                    webHostHttpRequestContext.GetType()
                        .GetProperty("Principal")
                        .GetValue(webHostHttpRequestContext, null);
            if (!principal.Identity.IsAuthenticated)
                throw new Exception("user is not authenticated cannot perform OData query");

            //do security in here

            //irrelevant but this just allows use of data by Word and Excel.
            if (Request.Headers.Accept.Count == 0)
                Request.Headers.Add("Accept", "application/atom+xml");

            return _fullRepository.Query().Select( b=>
                    new SubsetDataListEntity
                    {
                        Id = b.Id,
                        bitofData = b.bitofData
                    }
          } //end of query
   } //end of class


回答6:

You already tried this?

Just update the property.

[EnableQuery]
public async Task<IQueryable<User>> GetUsers()
{
    var users = db.User;

    await users.ForEachAsync(q => q.Password = null);

    return users;
}


回答7:

We can take advantage of the ConventionModelBuilder and use DataContract/DataMember to explicitly enable properties to be in the EdmModel.

DataContract & DataMember

Rule: If using DataContract or DataMember, only property with [DataMember] attribute will be added into Edm model.

Note, that this doesn't affect the EntityFramework model since we are not using the [NotMapped] attribute (unless you don't want it in either model)

[DataContract]
public class User
{
    [DataMember]
    public int Id { get; set; }

    [DataMember]
    public string Name { get; set; }

    [DataMember]
    public string Email { get; set; }

    [DataMember]
    public string LastName {get; set; }

    // NB Password won't be in EdmModel but still available to EF
    public string Password {get; set;}
}

This has the advantage of keeping all the mapping logic in one place in your project



回答8:

I made a craft and temporary solution to this problem (is not the best solution because UserInfo is not an entity type and not support $select or $expand). I created a new model called UserInfo just with the properties I need (apart of User):

public class UserInfo
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
}

Then I changed the method in the controller:

// GET: odata/Users
[EnableQuery]
public IQueryable<UserInfo> GetUsers()
{
    List<UserInfo> lstUserInfo = new List<UserInfo>();

    foreach(User usr in db.Users)
    {
        UserInfo userInfo = new UserInfo();
        userInfo.Id = usr.Id;
        userInfo.Name = usr.Name;
        userInfo.Email = usr.Email;

        lstUserInfo.Add(userInfo);
    }

    return lstUserInfo.AsQueryable();
}


回答9:

You can create new view in DB with the only data you need. Then set EntitySetRights.None for Users table and create necessary relationships for created view. Now you can do common odata requests (GET odata/UsersFromView) and getting users data without password. Post request you can do using Users table.



回答10:

Nothing else worked for me, so here's an elegant solution.

Use the HideSensitiveProperties() extension method in your TableController like this.

    // GET tables/User
    public IQueryable<User> GetAllUsers()
    {
        return Query().HideSensitiveProperties();
    }

    // GET tables/User/48D68C86-6EA6-4C25-AA33-223FC9A27959
    public SingleResult<User> GetUser(string id)
    {
        return Lookup(id).HideSensitiveProperties();
    }

    // PATCH tables/User/48D68C86-6EA6-4C25-AA33-223FC9A27959
    public Task<User> PatchUser(string id, Delta<User> patch)
    {
        return UpdateAsync(id, patch).HideSensitivePropertiesForItem();
    }

    // POST tables/User
    public async Task<IHttpActionResult> PostUser(User item)
    {
        User current = await InsertAsync(item);
        current.HideSensitivePropertiesForItem();
        return CreatedAtRoute("Tables", new { id = current.Id }, current);
    }

    // DELETE tables/User/48D68C86-6EA6-4C25-AA33-223FC9A27959
    public Task DeleteUser(string id)
    {
        return DeleteAsync(id);
    }

Though this will not remove the property name from the response, but it will set its value to null.

public static class HideSensitivePropertiesExtensions
{
    public static async Task<TData> HideSensitivePropertiesForItem<TData>(this Task<TData> task)
        where TData : ModelBase
    {
        return (await task).HideSensitivePropertiesForItem();
    }

    public static TData HideSensitivePropertiesForItem<TData>(this TData item)
        where TData : ModelBase
    {
        item.Password = null;
        return item;
    }

    public static SingleResult<TData> HideSensitiveProperties<TData>(this SingleResult<TData> singleResult)
        where TData : ModelBase
    {
        return new SingleResult<TData>(singleResult.Queryable.HideSensitiveProperties());
    }

    public static IQueryable<TData> HideSensitiveProperties<TData>(this IQueryable<TData> query)
        where TData : ModelBase
    {
        return query.ToList().HideSensitiveProperties().AsQueryable();
    }

    public static IEnumerable<TData> HideSensitiveProperties<TData>(this IEnumerable<TData> query)
        where TData : ModelBase
    {
        foreach (var item in query)
            yield return item.HideSensitivePropertiesForItem();
    }
}

Here ModelBase is the base class for all the DTOs.



回答11:

You should not query your Domain model directly in the controller. Instead, create a QueryModel DTO that maps to the Domain model.

You can read more about these concepts in DDD and CQRS



回答12:

use Automapper

[EnableQuery]
public IQueryable<User> GetUsers()
{
    //Leave password empty
    Mapper.CreateMap<User, User>().ForMember(x => x.Password, opt => opt.Ignore());

    return db.Users.ToList().Select(u=>Mapper.Map<User>(u)).AsQueryable();      
}