Web API OData with EntityFramework exposing only DTOs - entity-framework

I prefer not to expose my Entity Framework models and my controllers expose DTO style objects.
I have tried to have my controller's Get method's to be
public IQueryable<PersonDTO> Get(ODataQueryOptions<PersonDTO> query )
buy I don't know how to apply ODataQueryOptions to db.Persons so that the filtering happens in the SQL.
I ended up with
public IQueryable<PersonDTO> Get(ODataQueryOptions<PersonEF> query )
{
using (var db = new AppDbContext())
{
var result = (query.ApplyTo(db.Persons) as IQueryable<PersonEF>).Select
(c => new PersonDTO()
{
FirstName = c.FirstName,
LastName = c.LastName,
DateOfBirth = c.DateOfBirth,
Gender = c.Gender
}
);
return result.ToList().AsQueryable();
}
}
In order for this to work I have both classes added to ODataConventionModelBuilder: in my Register method.
ODataModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<PersonDTO>("PersonDTO");
builder.EntitySet<PersonEF>("Person");
How can I expose only PersonDTO and apply the ODataQueryOptions filters to EF's PersonEF?

Related

How to query in LINQ & Entity Framework the unmapped property

So I partially followed from an SO answer on how to store a property with array datatype in Entity Framework. What I didn't follow on that answer is setting the string InternalData to be private instead of public as I find it a code smell if it is set to public (not enough reputation to comment there yet).
I also managed to map the private property in entity framework from this blog.
When I perform CR (create, read) from that entity, all goes well. However, when my LINQ query has a where clause using that property with array datatype, it says that "System.NotSupportedException: 'The specified type member is not supported in LINQ to Entities. Only initializers, entity members, and entity navigation properties are supported.'".
How to work around on this? Here are the relevant code blocks:
public class ReminderSettings
{
[Key]
public string UserID { get; set; }
[Column("RemindForPaymentStatus")]
private string _remindForPaymentStatusCSV { get; set; }
private Status[] _remindForPaymentStatus;
[NotMapped]
public Status[] RemindForPaymentStatus
{
get
{
return Array.ConvertAll(_remindForPaymentStatusCSV.Split(','), e => (Status)Enum.Parse(typeof(Status), e));
}
set
{
_remindForPaymentStatus = value;
_remindForPaymentStatusCSV = String.Join(",", _remindForPaymentStatus.Select(x => x.ToString()).ToArray());
}
}
public static readonly Expression<Func<ReminderSettings, string>> RemindForPaymentStatusExpression = p => p._remindForPaymentStatusCSV;
}
public enum Status
{
NotPaid = 0,
PartiallyPaid = 1,
FullyPaid = 2,
Overpaid = 3
}
protected override void OnModelCreating(DbModelBuuilder modelBuilder)
{
modelBuilder.Entity<ReminderSettings>().Property(ReminderSettings.RemindForPaymentStatusExpression);
}
//This query will cause the error
public IEnumerable<ReminderSettings> GetReminderSettingsByPaymentStatus(Status[] statusArray)
{
var query = ApplicationDbContext.ReminderSettings.Where(x => x.RemindForPaymentStatus.Intersect(statusArray).Any());
return query.ToList(); //System.NotSupportedException: 'The specified type member 'RemindForPaymentStatus' is not supported in LINQ to Entities. Only initializers, entity members, and entity navigation properties are supported.'
}
Entity Framework can not translate LINQ expressions to SQL if they access a property that is annotated as [NotMapped]. (It also can not translate if the property contains custom C# code in its getter/setter).
As a quick (but potentially low performance) workaround, you can execute the part of the query that does not cause problems, then apply additional filtering in-memory.
// execute query on DB server and fetch items into memory
var reminders = dbContext.ReminderSettings.ToList();
// now that we work in-memory, LINQ does not need to translate our custom code to SQL anymore
var filtered = reminders.Where(r => r.RemindForPaymentStatus.Contains(Status.NotPaid));
If this causes performance problems, you have to make the backing field of your NotMapped property public and work directly with it.
var filtered = dbContext.ReminderSettings
.Where(r => r._remindForPaymentStatusCSV.Contains(Status.NotPaid.ToString("D"));
Edit
To handle multiple Status as query parameters, you can attach Where clauses in a loop (which behaves like an AND). This works as long as your Status enum values are distinguishable (i.e. there is no Status "11" if there is also a Status "1").
var query = dbContext.ReminderSettings.Select(r => r);
foreach(var statusParam in queryParams.Status) {
var statusString = statusParam.ToString("D");
query = query.Where(r => r._remindForPaymentStatusCSV.Contains(statusString));
}
var result = query.ToArray();

How to create custom entity for Web API 2 OData Data controller

I have the need to migrate my traditional Web API 2 Data controllers over to OData v4 style data controllers. This works very easily with the standard one-to-one table-to-entity relationship but I now have the need to use several different tables (which have no real constraints) into my data controller response. I am having trouble figuring out how to register this new custom "entity" in my WebAPI.config file.
Here is an example of what my WebAPIconfig.cs looks like:
using System.Linq;
using System.Web.Http;
using System.Web.OData.Builder;
using System.Web.OData.Extensions;
using System.Web.OData.Routing;
using System.Web.OData.Routing.Conventions;
using MyProject.Models;
namespace MyProject
{
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API configuration and routes
config.MapHttpAttributeRoutes();
//OData configuration
ODataModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Order>("orders");
builder.EntitySet<Customer>("customers");
//what goes here for my "custom" entity?
var _model = builder.GetEdmModel();
var defaultConventions = ODataRoutingConventions.CreateDefaultWithAttributeRouting(config, _model);
//var defaultConventions = ODataRoutingConventions.CreateDefault();
var conventions = defaultConventions.Except(
defaultConventions.OfType<MetadataRoutingConvention>());
config.MapODataServiceRoute(
routeName: "ODataRoute",
routePrefix: "api",
routingConventions: conventions,
pathHandler: new DefaultODataPathHandler(),
model: _model);
//ensure JSON responses
var appXmlType = config.Formatters.XmlFormatter.SupportedMediaTypes.FirstOrDefault(t => t.MediaType == "application/xml");
config.Formatters.XmlFormatter.SupportedMediaTypes.Remove(appXmlType);
}
}
}
Here is an example of what my Web API 2 data controller looks like:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
using MyProject.Models;
namespace MyProject.DataControllers
{
public class OrderDetailsController : ApiController
{
public OrderDetails GetOrderDetails(int id)
{
var ctx = new MyDatabaseEntities();
var details = new OrderDetails();
var order = ctx.Orders.FirstOrDefault(o => o.orderID == id);
if (order == null)
{
return details; //return an empty details object to the UI and abandon this code
}
//Data objects necessary for the order details page
IEnumerable<orderCertification> coupons = ctx.Coupons;
var importances = ctx.OrderImportances.Where(x => x.ImportanceId == order.ImportanceId).Where(x => (x.Type == "IMPORTANCES")) ?? null;
var rankings = ctx.OrderImportances.Where(x => x.ImportanceId == order.ImportanceId).Where(x => (x.Type == "RANK")) ?? null;
var profits = ctx.OrderImportances.Where(x => x.ImportanceId == order.ImportanceId).Where(x => (x.Type == "PROFIT")) ?? null;
var address = ctx.CustomerAddress.Where(c => c.OrderId == order.Id) ?? null;
var email = ctx.CustomerEmail.Where(c => c.Id == order.Id).Where(x => x.Type == "EMAIL") ?? null;
var giftcards = ctx.GiftCardAssignments.Where(c => c.CardID == order.CardID).FirstOrDefault().ToList() ?? null;
var customerCoupons = coupons.Where(c => giftCards.Any(o => o.GiftCardID == c.Id)).OrderBy(c => c.CouponName) ?? null;
//lots of other fun and crazy properties get set here!! etc etc.
//Set the order details properties
details.OrderImportances = importances;
details.OrderRankings = rankings;
details.OrderProfits = profits;
details.OrderAddress = address;
details.OrderEmail = email;
details.OrderGiftCards = giftcards;
details.OrderCoupons = customerCoupons;
details.OrderDescription = "This is my order description string.";
return details;
}
}
}
And here is an example of what my OrderDetails() class currently looks like:
using System.Collections.Generic;
namespace MyProject.Models
{
public class OrderDetails
{
public IEnumerable<OrderImportance> OrderImportances { get; set; }
public IEnumerable<OrderImportance> OrderRankings { get; set; }
public IEnumerable<OrderImportance> OrderProfits { get; set; }
public string OrderAddress { get; set; }
public string OrderEmail { get; set; }
public IEnumerable<OrderGiftCard> OrderGiftCards { get; set; }
public IEnumerable<OrderCoupon> OrderCoupons { get; set; }
public string OrderDescription { get; set; }
}
}
How do I make the OData version of this Web API controller and how do I register my OrderDetails class for it, in my WebAPIConfig.cs?
OrderDetails does not appear to have a key property. Therefore, it is not an entity type (a named structured type with a key), but rather a complex type (a keyless named structured type consisting of a set of properties).
Since complex types do not have their own identity (i.e., key), they cannot be exposed as entity sets in an OData service. This means you will not configure OrderDetails in the model builder, nor will you create a separate controller for OrderDetails.
The most straightforward way to migrate your existing GetOrderDetails method to OData is to redefine it as an OData function bound to the orders entity set. Actions and Functions in OData v4 Using ASP.NET Web API 2.2 provides a good tutorial on defining and configuring OData functions, but here's the gist of what you need to do.
Declare the function in WebApiConfig.Register:
builder.EntityType<Order>().Function("GetOrderDetails").Returns<OrderDetails>();
Define the function in the controller for the orders entity set:
public class OrdersController : ODataController
{
// Other methods for GET, POST, etc., go here.
[HttpGet]
public OrderDetails GetOrderDetails([FromODataUri] int key)
{
// Your application logic goes here.
}
}
Finally, invoke the function as follows:
GET http://host/api/orders(123)/Default.GetOrderDetails
Note that Default is the default namespace of the service, which is normally required when invoking a bound function. To change this, you can either set builder.Namespace, or you can enable unqualified function calls with config.EnableUnqualifiedNameCall(true).

Entity Framework best practice for "include" in Repository

I have a Repository pattern for product entity and I have a method that retrieves a product by id. Each product has a category, among other complex properties. In some cases I want to retrieve the products without the category (lazy loading) but in some cases I want to return both entities (products and categories). Is there any better option that having two methods? this is what I coded:
Product GetById(int id)
{
// without includes
...
}
Product GetByIdFull(int id)
{
// with includes
...
}
You can do something like this:
public Product Get(int id, params Expression<Func<TEntity, object>>[] propertiesToInclude)
{
var query = context.Products;
foreach (var expression in propertiesToInclude)
{
query = query.Include(expression);
}
return query.SingleOrDefault(p => p.id == id);
}
The calling code could optionally specify the properties to be included using a lambda like so:
var justProduct = repo.Get(productId);
var productAndCategory = repo.Get(productId, p => p.Category);

IQueryable Entity Framework POCO Mappings

I am using ASP.NET MVC2 with EF4. I need to create POCOs for two of my classes PersonP and AddressP, which correspond to their EF4 'complex' classes (which include things like navigation properties and OnPropertyChanged()). Mapping just PersonP by itself works fine, but PersonP contains AddressP (foreign key) - how do I map this using an IQueryable expression?
Here is what I've tried:
class AddressP
{
int Id { get; set; }
string Street { get; set; }
}
class PersonP
{
int Id { get; set; }
string FirstName { get; set; }
AddressP Address { get; set; }
}
IQueryable<PersonP> persons = _repo.QueryAll()
.Include("Address")
.Select(p => new PersonP
{
Id = p.Id,
FirstName = p.FirstName,
//Address = p.Address <-- I'd like to do this, but p.Address is Address, not AddressP
//Address = (p.Address == null) ? null :
//new AddressP <-- does not work; can't use CLR object in LINQ runtime expression
//{
// Id = p.Address.Id,
// Street = p.Address.Street
//}
});
Without the .Include("Address") I would not retrieve anything from the Address table is this correct?
How do I map Address to AddressP inside PersonP, using the Select() statement above?
Thank you.
That is correct, if you've disable Lazy Loading or your object context has been disposed already and cannot be used to get Lazy Loading working.
Yes, it won't work since first you need to execute your query and then start mapping it, otherwise your mapping logic would be taken to be run in the database hence the exception. Something like this will work:
// First we execute the query:
IQueryable<PersonP> persons = _repo.QueryAll().Include("Address").ToList();
// Now we have a IEnumerable and we can safely do the mappings:
persons.Select(p => new PersonP
{
Id = p.Id,
FirstName = p.FirstName,
Address = (p.Address == null) ? null : new AddressP()
{
Id = p.Address.Id,
Street = p.Address.Street
}
}).ToList();
While this solution will do the trick but if the intention is to have POCO classes you should definitely consider to take advantage of EF4.0 POCO support and use POCO classes directly with EF instead of mapping them afterward. A good place to start would be this walkthrough:
Walkthrough: POCO Template for the Entity Framework

Entity framework 4, Dynamic query

Is it possible to create a dynamic query with Entity Framework. I Have 18 tables, Each of them has the same structures. How can I create a dynamic query to reuse the same query for each tables. I would like to have a generic query for Create Read Update Delete.
The read contains the same "Where" Clause.
Thanks for your help.
Here you have simple example for pure CRUD scenario. Create interface which will contain shared properties for your queries. Implement this interface in all your entity classes. Than create repository. Repository is usually defined as generic but in your case I defined each method as generic so that you can use same repository instence for all entities.
public interface IWellKnownEntity
{
int Type { get; set; }
}
public class Repository
{
public T GetEntityByWellKnownQuery<T>() where T : IWellKnownEntity
{
using (var context = new MyContext())
{
return context.CreateObjectSet<T>().FirstOrDefault(e => e.Type == 1);
}
}
public IEnumerable<T> GetEntitiesByCustomQuery<T>(Expression<Func<T, bool>> where)
{
using (var context = new MyContext())
{
return context.CreateObjectSet<T>().Where(where).ToList();
}
}
public void Create<T>(T entity) where T : IWellKnownEntity
{
using (var context = new MyContext())
{
context.AddObject(entity);
context.SaveChanges();
}
}
public void Update<T>(T entity) where T : IWellKnownEntity
{
using (var context = new MyContext())
{
context.Attach(entity);
context.ObjectStateManager.ChageObjecState(entity, EntityState.Modified);
context.SaveChanges();
}
}
public void Delete<T>(T entity) where T : IWellKnownEntity
{
using (var context = new MyContext())
{
context.Attach(entity);
context.DeleteObject(entity);
context.SaveChanges();
}
}
}
Than you suppose that you have entity product and catebory which impelment well known interface. You can simply call:
var repository = new Repository();
var product = repository.GetEntityByWellKnownQuery<Product>();
product.Name = "Updated";
repository.Update<Product>(product);
var category = repository.GetEntitiesByCustomQuery<Category>(c => c.Id == 1).First();
repository.Delete<Category>(category);
You can futher improve the sample code. This code doesn't use shared context so it is more usable for disconnected scenario (web application). If you use connected scenario like WinForms application or batch application you can implement IDisposable on repository and share context among all methods. Dispose method on repository will hanlde disposing of context. Code for Update and Delete methods will be different because there is no need to attach entity back to context or set entity state.