Reputation: 300
I'm making rest server using ASP.NET Core and EF Core with MySQL.
There are two tables in database - buildings and buildings arguments. Every building type has different vars, so I decided to solve this in that way.
So there is model Building, but also there are many building classes inherit from Building. A little example:
[BuildingType(EBuildingType.Stable)]
public class Building_Stable : Building
{
public int Slots
{
get
{
return GetArgumentValueOrDefault<int>("Slots");
}
set
{
UpdateOrCreateArgument("Slots", value);
}
}
}
I have access to all Building properties and I can get and set additional properties (arguments in database) in really pleasant way. When I want to get building from my repository I'm doing this:
public async Task<TBuilding> GetByType<TBuilding>(User user) where TBuilding : Building
{
try
{
if (user == null) return null;
EBuildingType? type;
type = _buildingTypesCollection.GetType(typeof(TBuilding));
if (type == null) return null;
Building uBuilding = await _dbContext.Buildings.Where(b => b.Type == type && b.OwnerId == user.Id).Include(p => p.Arguments).FirstOrDefaultAsync();
var parent = JsonConvert.SerializeObject(uBuilding);
TBuilding c = JsonConvert.DeserializeObject<TBuilding>(parent);
return c;
}
catch (Exception ex)
{
throw ex;
}
}
And it works, most of the time, but sometimes I have to really puzzle over how to get access to some properties and I'm not completely contented. So I have this feeling that I'm not doing this right.
Upvotes: 1
Views: 1020
Reputation: 205759
In case this is implemented through EF Core TPH inheritance strategy with Type
being the discriminator, there is no need to use the discriminator directly. The EF Core way of querying specific derived type is to use OfType
method:
if (user == null) return null;
TBuilding c = await _dbContext.Buildings
.Include(p => p.Arguments)
.OfType<TBuilding>() // <--
.Where(b => b.OwnerId == user.Id) // <-- no type filter
.FirstOrDefaultAsync();
return c;
Upvotes: 4
Reputation: 29252
I recently came across this exact implementation:
var parent = JsonConvert.SerializeObject(uBuilding);
TBuilding c = JsonConvert.DeserializeObject<TBuilding>(parent);
Suppose the generic argument you use for TBuilding
is Building
, but your repository returns a SkyScraper
, which inherits from Building
. The serialization/deserialization that follows could have an unintended effect.
Casting an object as its base type obviously has no effect on the object itself. But serializing the object and then deserializing it as its base type isn't casting. It returns a new object of the base type. So once it's been serialized and deserialized, the result you get back is no longer a Skyscraper
at all.
JsonConvert.DeserializeObject<TBuilding>
will deserialize everything needed for Building
and ignore the other properties of the Skyscraper
. So all you get back is a Building
.
To fix that behavior, make a slight change:
var parent = JsonConvert.SerializeObject(uBuilding);
TBuilding c = JsonConvert.DeserializeObject(parent, typeof(uBuilding))
Now the original type will be taken into account when deserializing. The result will be cast as Building
, but its actual type will be Skyscraper
.
What you're doing by serializing and deserializing the object to create a copy is called "cloning." Instead of getting a reference to the original object, you're creating a new object with the same properties as the original.
There are valid reasons for doing that, but you didn't mention why you're doing it here. Do you need to, or could you skip that step entirely and simplify the method to this:
public async Task<TBuilding> GetByType<TBuilding>(User user) where TBuilding : Building
{
if (user == null) return null;
EBuildingType? type;
type = _buildingTypesCollection.GetType(typeof(TBuilding));
if (type == null) return null;
Building uBuilding = await _dbContext.Buildings.Where(b => b.Type == type && b.OwnerId == user.Id).Include(p => p.Arguments).FirstOrDefaultAsync();
return uBuilding;
}
Whatever TBuilding
is, it's possible that the result might not actually be that type - it's something that inherits from it. But that's okay - that's how it's supposed to work. If you're looking for a Building
and you get a Skyscraper
, that's okay because a Skyscraper
is a Building
.
(The catch ex/throw ex
is unnecessary - it's actually worse than no exception handling because throw ex
wipes out the stack trace.)
Upvotes: 1
Reputation: 239430
First, you'll make your life much easier if you use Set<T>
rather than a named DbSet
property like Buildings
. That will only ever return instances of Building
, regardless of the actual type of the building. However, since you're already inside a generic method working with a particular TBuilding
type, then you can actually just do:
var building = await _dbContext.Set<TBuilding>().Where(b.OwnerId == user.Id).Include(p => p.Arguments).FirstOrDefaultAsync();
You should not persist a separate Type
property, as you're adding a point of failure. If this doesn't get set properly, parts of your code will fail even though the building may be of the correct type. If you use generics correctly (like above with Set<T>
), you can accomplish pretty much everything without actually needing to know the specific type.
Now, the one limitation is the type constraint on your generic method (and probably similar ones). It's not a bad thing, and it actually should be there. However, if essentially upcasts TBuilding
to Building
, meaning you can only access members of Building
and not more specific derived types. If you need to access a member of a specific derived type, you can use switch
with pattern matching (requires C# 7+):
switch (building)
{
case Building_Stable stable:
// `stable` is now a declared variable of type `Building_Stable`
// You can use this variable to work with specific members of this type
break;
}
Upvotes: 1