从c#中的WebApi OData(EF)响应中排除属性

时间:2015-01-05 14:29:19

标签: c# linq entity-framework asp.net-web-api odata

我正在使用C#中的WebApi项目(EF代码优先),我正在使用OData。 我有一个“用户”模型,包含Id,Name,LastName,Email和Password。

在控制器例如,我有这个代码:

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

如果我打电话给/ odata / Users,我将获得所有数据:Id,Name,LastName,Email和Password。

如何从结果中排除密码,但在控制器中保持可用以进行Linq查询?

13 个答案:

答案 0 :(得分:14)

我对这个话题有点迟,但我认为这可能会帮助你。

我认为您需要加密密码以用于存储目的。你有没有看过使用odata动作来设置密码?使用操作可以在设置实体时忽略密码属性,同时仍然向最终用户公开更新密码的干净方式。

首先:忽略密码属性

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

第二名:添加你的odata动作

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

然后将SetPassword方法添加到UserInfoController。

答案 1 :(得分:7)

可能有点晚了,但优雅的解决方案是添加自定义的QueryableSelectAttribute,然后只列出要在服务端选择的字段。在你的情况下,它看起来像这样:

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);
    }
}

在控制器中,您只需添加具有所需属性的属性:

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

就是这样!

当然,相同的原则可以应用于自定义QueryableExpandAttribute

答案 2 :(得分:6)

在User Class的Password属性中添加[NotMapped]属性,如下所示:

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 :(得分:6)

  

如何从结果中排除密码,但在控制器中保持可用以进行Linq查询?

忽略它。来自Security Guidance for ASP.NET Web API 2 OData

  

有两种方法可以从EDM中排除财产。你可以设置   模型类中属性的[IgnoreDataMember]属性:

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

您也可以通过编程方式从EDM中删除该属性:

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

答案 4 :(得分:2)

您需要做的是创建一个odata控制器,它返回原始实体的预计子集。

//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());

...然后有一个用于FulLData的两个Odata控制器,一个用于SubsetData(具有不同的安全性),

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

答案 5 :(得分:1)

您已尝试过这个吗?

只需更新房产。

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

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

    return users;
}

答案 6 :(得分:1)

我们可以利用ConventionModelBuilder并使用DataContract / DataMember显式启用属性在EdmModel中。

  

DataContract&amp;数据成员

     

规则:如果使用DataContract或DataMember,则只有具有[DataMember]属性的属性才会添加到Edm模型中。

注意,这不会影响EntityFramework模型,因为我们没有使用[NotMapped]属性(除非你不想在任何一个模型中使用它)

[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;}
}

这样做的好处是可以将所有映射逻辑保存在项目的一个位置

答案 7 :(得分:0)

我为这个问题做了一个工艺和临时解决方案(不是最好的解决方案,因为UserInfo不是实体类型,不支持$ select或$ expand)。 我创建了一个名为UserInfo的新模型,只需要我需要的属性(除了User):

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

然后我改变了控制器中的方法:

// 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();
}

答案 8 :(得分:0)

您可以使用所需的唯一数据在DB中创建新视图。然后为Users表设置EntitySetRights.None并为创建的视图创建必要的关系。 现在,您可以执行常见的odata请求(GET odata / UsersFromView)并获取没有密码的用户数据。您可以使用用户表发布请求。

答案 9 :(得分:0)

没有其他任何对我有用,所以这是一个优雅的解决方案。

使用HideSensitiveProperties()中的TableController扩展名方法。

    // 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);
    }

虽然这不会从响应中删除属性名称,但会将其值设置为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();
    }
}

此处ModelBase是所有DTO的基类。

答案 10 :(得分:0)

您不应直接在控制器中查询您的域模型。而是创建映射到域模型的QueryModel DTO。

您可以在DDD和CQRS中阅读有关这些概念的更多信息

答案 11 :(得分:0)

我也迟到了,但我找不到任何好的和干净的方法来实现这一目标。

所以我有这个临时和丑陋的解决方案:

public class MyCustomerClassName
{
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    [Key]  
    public int Id { get; set; }

    //normal properties that are exposed to API and read/write to EF
    public string Name { get; set; }  
    public string Adress { get; set; }



    //and here, for ONE property named MildlySensitiveInformation :

    //the EF property, that is not exposed
    [IgnoreDataMember] //says 'dont show that when you serialize (convert to json -> exposed in API)
    [Column("MildlySensitiveInformation")] //says 'the REAL column name here is THAT
    public string MildlySensitiveInformation_MappedToDB { get; set; } //a 'false' column name, allowing me to expose the real column name in the next property

    //the WebApi/Odata property that is exposed
    [NotMapped] //Says 'Dont map to EF'
    public string MildlySensitiveInformation { 
        get { return ""; }  //says 'never fetch here'
        set { MildlySensitiveInformation_MappedToDB = value; }  //Says : assign to my real DB properties
    } 

我暂时不想参加“课堂上的课堂”(DTO)...

我有一个 ODATA 曝光,所以我不想在控制器部分玩:

   public class MyCustomerClassNameClassName : ODataController
{
    private MyDBContext db = new MyDBContext();

    // GET: odata/MyCustomerClassName
    [EnableQuery]
    public IQueryable<MyCustomerClassName> GetMembresPleinAir()
    {
        //don't mess here, there is enough 'black magic' going on with $select/$filter/$etc...
        var OrigQuery = db.AllMyCustomers;
        return OrigQuery;
    }

然而,我并不为那个解决方案感到自豪,因为: 1 - 这简直太丑了,我向下一个会触及它的程序员道歉。 2 - 将业务逻辑与“原始”数据库映射混合

所以我想唯一“真实而干净”的解决方案是拥有一个 DTO 类,它允许更好地控制我的数据的读/写方式。

但是,如果我想在我的项目中保持一致,我必须使用“纯类”和“DTO 类”克隆我的 13 个类中的每一个。

看看这里:https://docs.microsoft.com/en-us/aspnet/core/tutorials/first-web-api?view=aspnetcore-5.0&tabs=visual-studio#prevent-over-posting

答案 12 :(得分:-2)

使用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();      
}