* Disclaimer: This answer does not give a strict answer to my question. It is rather a different approach to solving my own problem. I know this is a concrete example for a specific situation that will not work for everyone. I post this approach in the hope that it will help someone, but will not mark it as an answer, since I still hope for a real solution.
To begin with, let me accept the fact that the only useful information we can get from the current code is whether a record exists or not. Any attempt at dynamic queries after this will throw a RuntimeBinderException.
Then we continue with another fact; DbContext.Add (object) and DbContext.Update (object) are not template-based, so we can use them to save our models (instead of db.A.Add () or db.A.Update ())
In my own situation, it is no longer necessary to develop a procedure
- Define models a little differently
First I need a field that can be restored in all my models, which obviously should be a way to identify a unique record.
// IModel give me a reliable common field to all my models ( Fits my DB design maybe not yours though ) interface IModel { Guid Id { get; set; } } // ModelA inherit IModel so that I always have access to an 'Id' class ModelA : IModel { public Guid Id { get; set; } public int OtherField { get; set; } } // ModelB inherit IModel so that I always have access to an 'Id' class ModelB : IModel { public Guid Id { get; set; } public string WhateverOtherField { get; set; } }
- Reuse dynamic queries to do something we know.
I did not find a way to smartly query dynamically, so instead I know that I can reliably identify the record and know if it exists or not.
class MyDbContext : DbContext { public DbSet<ModelA> A { get; set; } public DbSet<ModelB> B { get; set; } // In my case, this method help me to know the next action I need to do // The switch/case option is not pretty but might have better performance // than Reflection. Anyhow, this is one choice. public bool HasRecord_SwitchTest(string name) { switch (name) { case "A": return A.AsNoTracking().Any(o => o.Id == id); case "B": return B.AsNoTracking().Any(o => o.Id == id); } return false; } // In my case, this method help me to know the next action I need to do public bool HasRecord_ReflectionTest(string fullname) { Type targetType = Type.GetType(fullname); var model = GetType() .GetRuntimeProperties() .Where(o => o.PropertyType.IsGenericType && o.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>) && o.PropertyType.GenericTypeArguments.Contains(targetType)) .FirstOrDefault(); if (null != model) return (bool)model.GetValue(this).AsNoTracking().Any(o => o.Id == id); return false; } // Update and save immediately - simplified for example public async Task<bool> UpdateDynamic(object content) { EntityEntry entry = Update(content, GraphBehavior.SingleObject); return 1 == await SaveChangesAsync(true); } // Insert and save immediately - simplified for example public async Task<bool> InsertDynamic(object content) { EntityEntry entry = Add(content, GraphBehavior.SingleObject); return 1 == await SaveChangesAsync(true); } }
- A little plumbing to give meaning to my situation.
Further, what I needed to do with these dynamic queries was a way to replicate data from the server to my client. (To simplify this example, I skipped most of the architecture)
class ReplicationItem { public ReplicationAction Action { get; set; } // = Create, Update, Delete public string ModelName { get; set; } // Model name public Guid Id { get; set; } // Unique identified across whole platform }
- Connection bits.
Now, here is the procedure that connects the bits
public async void ProcessReplicationItem(ReplicationItem replicationItem) { using (var db = new MyDbContext()) { // Custom method that attempts to get remote value by Model Name and Id // This is where I get the strongly typed object var remoteRecord = await TryGetAsync(replicationItem.ModelName, replicationItem.Id); bool hasRemoteRecord = remoteRecord.Content != null; // Get to know if a local copy of this record exists. bool hasLocalRecord = db.HasRecord_ReflectionTest(replicationItem.ModelName, replicationItem.Id); // Ensure response is valid whether it is a successful get or error is meaningful ( ie. NotFound ) if (remoteRecord.Success || remoteRecord.ResponseCode == System.Net.HttpStatusCode.NotFound) { switch (replicationItem.Action) { case ReplicationAction.Create: { if (hasRemoteRecord) { if (hasLocalRecord) await db.UpdateDynamic(remoteRecord.Content); else await db.InsertDynamic(remoteRecord.Content); } // else - Do nothing break; } case ReplicationAction.Update: [etc...] } } } } // Get record from server and with 'response.Content.ReadAsAsync' type it // already to the appropriately public static async Task<Response> TryGetAsync(ReplicationItem item) { if (string.IsNullOrWhiteSpace(item.ModelName)) { throw new ArgumentException("Missing a model name", nameof(item)); } if (item.Id == Guid.Empty) { throw new ArgumentException("Missing a primary key", nameof(item)); } // This black box, just extrapolate a uri based on model name and id // typically "api/ModelA/{the-guid}" string uri = GetPathFromMessage(item); using (var client = new HttpClient()) { client.BaseAddress = new Uri("http://localhost:12345"); HttpResponseMessage response = await client.GetAsync(uri); if (response.IsSuccessStatusCode) { return new Response() { Content = await response.Content.ReadAsAsync(Type.GetType(item.ModelName)), Success = true, ResponseCode = response.StatusCode }; } else { return new Response() { Success = false, ResponseCode = response.StatusCode }; } } } public class Response { public object Content { get; set; } public bool Success { get; set; } public HttpStatusCode ResponseCode { get; set; } }
ps: I'm still interested in the real answer, so please continue to post for another answer, if you have a real one to share.