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

Exclude property from WebApi OData (EF) response in c#

我正在使用 C# 中的 WebApi 项目(首先是 EF 代码),并且我正在使用 OData。 我有一个 "User" 模型,其中包含 ID、姓名、姓氏、电子邮件和密码。

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

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

如果我调用 /odata/Users,我将获得所有数据:ID、姓名、姓氏、电子邮件和密码。

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

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

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

然后我在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();
}

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

在用户 Class 的密码 属性 上添加 [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;}
}

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

忽略它。来自 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);

您可以只用您需要的数据在数据库中创建新视图。然后为用户 table 设置 EntitySetRights.None 并为创建的视图创建必要的关系。 现在您可以执行常见的 odata 请求 (GET odata/UsersFromView) 并无需密码即可获取用户数据。 Post 请求您可以使用用户 table.

我来晚了一点,但我认为这可能会对你有所帮助。

我假设您希望加密密码以便存储。您是否看过使用 odata 操作来设置密码?使用操作可让您在设置实体时忽略密码 属性,同时仍向最终用户公开更新密码的干净方式。

第一:忽略密码属性

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

第二步:添加您的 odata 操作

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

然后将 SetPassword 方法添加到您的 UserInfoController。

您需要做的是制作一个 returns 原始实体的投影子集的 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());

...然后有两个 Odata 控制器,一个用于 FulLData,一个用于 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

可能有点晚了,但一个优雅的解决方案是添加自定义 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

你已经试过了?

只需更新 属性。

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

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

    return users;
}

我们可以利用 ConventionModelBuilder 并使用 DataContract/DataMember 明确启用 EdmModel 中的属性。

DataContract & DataMember

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

请注意,这不会影响 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;}
}

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

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

像这样在 TableController 中使用 HideSensitiveProperties() 扩展方法。

    // 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 的基础 class。

您不应该直接在控制器中查询域模型。相反,创建一个映射到域模型的 QueryModel DTO。

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

我也来晚了,但我找不到任何好的、干净的方法来实现它。

所以我有一个临时且丑陋的解决方案:

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
    } 

我暂时不想 'class-over-a-class' (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 - 它将业务逻辑与 'raw' 数据库映射

混合

所以我想唯一的 'real and clean' 解决方案是拥有一个 DTO Class,它可以更好地控制我的数据 read/write。

但是,如果我想在我的项目中保持一致,我将不得不用 'pure class' 和 'DTO class'.[=13 克隆我的 13 类 中的每一个=]

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