从 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();
}
[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 类 中的每一个=]
我正在使用 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();
}
[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 类 中的每一个=]