Bummer! This is just a preview. You need to be signed in with a Basic account to view the entire video.
Start a free Basic trial
to watch this video
Let's define a generic repository base class which will allow us to define repository methods that will work with a generic entity type instead of a specific entity type.
Follow Along
To follow along committing your changes to this course, you'll need to fork the dotnet-comic-book-library-manager repo. Then you can clone, commit, and push your changes to your fork like this:
git clone <your-fork>
cd dotnet-comic-book-library-manager
git checkout tags/v3.5 -b creating-a-generic-base-repository-class
Keyboard Shortcuts
-
CTRL+M
CTRL+O
- Collapse to Definitions
Additional Learning
Using a Stub to Delete Entities in a Generic Base Repository Class
In order to keep our approach as simple as possible, we implemented the Delete
method using a retrieve/remove approach:
public void Delete(int id)
{
var set = Context.Set<TEntity>();
var entity = set.Find(id);
set.Remove(entity);
Context.SaveChanges();
}
The downside of this approach is that it requires us to retrieve the entity before we remove it. We can avoid the extra query to retrieve the entity by switching to an entity stub approach. But to do that within our generic BaseRepository class, there are a couple of changes that we need to make to our generic constraints.
public abstract class BaseRepository<TEntity>
where TEntity : class, IEntity, new()
{
...
}
Notice that we added IEntity
and new()
as generic constraints. The IEntity
constraint enforces that the generic type implement the IEntity interface and the new()
constraint enforces that the generic type define a default constructor.
By enforcing that the generic type implement the IEntity interface and have a default constructor, we can instantiate an instance of the generic type and set any of the properties defined on the IEntity interface like this:
public void Delete(int id)
{
var entity = new TEntity()
{
Id = id
};
Context.Entry(entity).State = EntityState.Deleted;
Context.SaveChanges();
}
For reference, here's the IEntity interface:
public interface IEntity
{
int Id { get; set; }
}
-
0:00
Remember how we created our own base controller class?
-
0:03
So we had a place for code that was being used across controllers.
-
0:06
We can do the same thing here, but with a twist.
-
0:09
Instead of just defining a repository base class, we'll define a generic base class.
-
0:15
Which will allow us to define repository methods
-
0:17
that will work with a generic entity type instead of a specific entity type.
-
0:22
Don't worry if the idea of creating a generic base class sounds confusing or
-
0:26
difficult to do.
-
0:27
We'll walk through the process step by step.
-
0:31
Let's start with adding the base repository class
-
0:34
to the shared class libraries data folder.
-
0:36
We named our base controller class BaseController.
-
0:39
So I'll follow the same naming convention and
-
0:42
name our base repository class BaseRepository.
-
0:46
Add the public access modifier so code in the web app project can access this class.
-
0:51
Let's also add the abstract keyword right before the class keyword so
-
0:55
this class can't be directly instantiated.
-
0:57
That helps to make our design intentions clear.
-
1:00
This class should only be used as a base class for our other repositories and
-
1:05
not instantiated and used directly.
-
1:08
Let's write out our plan for this class.
-
1:10
To start,
-
1:11
we'll need a constructor that'll accept an instance of our database context.
-
1:16
Ideally, this class would define methods for
-
1:18
the basic CRUD functions, reads, creates, updates, and deletes.
-
1:23
Let's define two read methods, one that returns a single entity and
-
1:28
another that returns a collection of entities.
-
1:30
The Get method will accept an integer representing the id
-
1:34
of the entity to retrieve.
-
1:36
While the GetList method will retrieve a list of entities.
-
1:39
Then we need methods to Add, Update,
-
1:49
And Delete.
-
1:53
The Add and Update methods will both accept an entity instance.
-
1:58
While the Delete method will accept an integer representing the id
-
2:01
of the entity to delete.
-
2:03
Okay, that'll give us a basic guide to follow as we write code.
-
2:07
Let's start with defining the constructor.
-
2:10
public BaseRepository and a parameter of type Context named context.
-
2:19
Now we need a field or a property to store the reference to the context.
-
2:23
Let's use a protected property so
-
2:25
that descendant classes can also access the context.
-
2:28
protected Context Context { get; set; }.
-
2:36
That'll give us the flexibility to write whatever queries we need to for
-
2:40
any of our repositories, like the queries for the validation methods.
-
2:45
We can make the property set as private
-
2:47
as we'll be initializing the property from within this classes constructor.
-
2:51
And then set the property to the Context instance that were being passed
-
2:56
via the constructor.
-
2:57
Context = context; now let's implement our Get method.
-
3:05
Public, hm, what do we use for the return type?
-
3:10
We can't use a specific entity type, like ComicBook,
-
3:15
because we want this method to work with any of our entity types.
-
3:19
We possibly could use object,
-
3:28
But then the caller would need to explicitly cast the return value to
-
3:32
whatever entity type they were working with.
-
3:35
That'd definitely be less than ideal.
-
3:40
This is where generics come to our rescue.
-
3:43
We can make the Get method a generic method by defining a generic type
-
3:47
parameter in brackets just after the method name.
-
3:50
. -
3:53
While it's not required, it's a convention to name the generic type parameter
-
3:57
starting with a T followed by a description of the type.
-
4:01
In our case, this is the Entity type, so TEntity seems like an appropriate name.
-
4:08
Once we've defined the generic type parameter,
-
4:10
we can then use it in our method.
-
4:12
For instance, we can change our return type to be of type TEntity.
-
4:18
Notice that we get an error trying to return null for the return value.
-
4:22
Currently, our genetic type parameter could be any of the available types.
-
4:27
Not only any custom types that our application defines,
-
4:30
like our Entity classes.
-
4:32
But also any of the built-in .NET types, like int or
-
4:36
Boole, given that null is not necessarily a valid value.
-
4:41
We'll see in a bit how we can constrain our generic type parameter
-
4:44
to a subset of the available types.
-
4:47
But for now, we could replace null with a call to the special default method.
-
4:52
Which will return the default value for the specified generic type parameter.
-
4:57
Now let's define the GetList method.
-
5:03
Public IList of type TEntity,
-
5:07
GetList
, which is our -
5:12
generic type parameter, return null.
-
5:19
Notice that we've defined the generic type parameter TEntity twice now.
-
5:24
Once for the Get method and once for the GetList method.
-
5:27
Since our Entity type focused repositories work with a single entity type,
-
5:31
TEntity is, in fact, the same type.
-
5:35
So instead of having to define TEntity on each of our methods,
-
5:40
we can define it once at the class level.
-
5:42
. -
5:45
Once the genetic type parameter is defined at the class level,
-
5:49
we'll need to remove the TEntity generic type parameters on our methods.
-
5:53
As you can't redefine a generic type parameter at the method level that's
-
5:57
already defined at the class level.
-
6:05
Now that we've stubbed out the Get and GetList methods,
-
6:08
it occurs to me that it might be easiest not to mention more flexible to allow
-
6:12
the descendant classes to provide the implementation details for these methods.
-
6:17
Since this is an abstract class, we can simply add the abstract keyword right
-
6:22
after the public access modifier and remove the method bodies.
-
6:33
This delegates the responsibility of implementing these methods to any class
-
6:38
that inherits from this base class.
-
6:40
Let's also add the includeRelatedEntities parameter to the Get method.
-
6:44
Like we did for the ComicBooks repository Get method.
-
6:48
Now on to the other CRUD methods, starting with the Add method.
-
6:51
public void Add, and a parameter of type TEntity named entity.
-
6:59
To add the entity we need to reference to a DbSet object.
-
7:03
Normally, we'd just reference the Context class DbSet property that we want to use.
-
7:08
Like the context ComicBooks property.
-
7:10
But remember, we don't know which Entity type we're working with.
-
7:14
So we can't reference a specific DbSet property on the Context class.
-
7:19
Luckily, the EFDB Context class, the class that our Context class inherits from,
-
7:24
defines a generic method named Set that we can call to get a reference
-
7:29
to a DbSet object for provided Entity type.
-
7:32
Context.Set of type TEntity.
-
7:38
When we call the Set method with our TEntity generic type parameter,
-
7:41
we get a build error.
-
7:43
The type 'TEntity' must be a reference type in order to use it as a parameter
-
7:49
'TEntity' in the generic type or method 'DbContext.set
. -
7:54
This means that the Set method is designed to only work with reference types.
-
7:59
Types defined using classes and not value types like int or Boole.
-
8:05
Remember that our TEntity generic type parameter
-
8:08
currently allows any of the available .NET types.
-
8:12
We can constrain our generic type parameter by using the where key word.
-
8:17
where, followed by the genetic type parameter to constrain, TEntity :.
-
8:24
Then the constrain we want to use, class,
-
8:28
which constrains the genetic type parameter to reference types.
-
8:31
Other constrains are available, including struct and new.
-
8:37
struct would constrain the generic type parameter to value types.
-
8:42
And new would constrain the generic type parameter to reference types
-
8:46
that define a default constructor.
-
8:48
For more information on generic type constraints,
-
8:50
including a complete list of the available constraints, see the teacher's notes.
-
8:55
With our generic type constrained in place,
-
8:57
we can now successfully call the Set method, which returns a DbSet object.
-
9:02
On which we can call the Add method, passing in the entity that is being
-
9:07
passed to our method, .Add(entity).
-
9:12
After calling the Add method, we just need to call the context's SaveChanges method.
-
9:18
Now let's implement the Update method.
-
9:21
public void Update, and a parameter of type TEntity named entity.
-
9:29
To update the entity,
-
9:30
we call the DbContext's entry method to get the entry for the passed-in entity.
-
9:36
Context.Entry(entity), and set it's State property to Modified.
-
9:44
To keep from having to specify the System.Data.Entity namespace here,
-
9:48
let's add a using directive for this namespace.
-
9:54
using System.Data.Entity.
-
9:57
Now we just need to call the SaveChanges method.
-
10:05
Notice that since this approach isn't dependent on a DbSet object,
-
10:09
it's the same code that we used in our entity-specific repository.
-
10:14
And now on to the Delete method.
-
10:17
public void Delete, and a parameter of type int named id.
-
10:23
Let's review the ComicBooksRepository's Delete method in order
-
10:26
to remind ourselves what the code looks like to delete an entity.
-
10:30
Notice how we're creating a stub entity using the passed-in id parameter value.
-
10:35
So we have an entity to pass to the DB context.Entry method.
-
10:38
Unfortunately, this approach won't work in our generic base repository class
-
10:44
because we don't know which entity type to instantiate.
-
10:47
Well, this isn't entirely true.
-
10:50
It is possible to use the same approach, but
-
10:53
there's more code we need to write in order to make it work.
-
10:56
I'm going to use the simpler approach.
-
10:58
If you'd like to see what you need to do in order to use the stub entity approach,
-
11:02
see the teacher's notes.
-
11:04
To start, we need a reference to the DbSet object for our current entity type.
-
11:10
var set = Context.Set of type TEntity.
-
11:19
Then we call the DbSet's Find method to retrieve the entity for
-
11:22
the passed-in id parameter value.
-
11:25
var entity = set.Find(id).
-
11:31
Now that we have a reference to the entity,
-
11:34
we can call the DbSet Remove method passing in the entity to remove.
-
11:38
set.Remove(entity).
-
11:41
And finally, we call the SaveChanges method, Context.SaveChanges.
-
11:47
The biggest drawback to using this approach is that we need to execute
-
11:51
a query to retrieve the entity from the database before we can delete it.
-
11:55
With the stub entity approach,
-
11:57
we can eliminate the query to retrieve the entity.
-
12:00
For our web app, I'm okay with the extra query.
-
12:03
I don't expect that users will be deleting entities very often.
-
12:07
Though it's always a good idea to validate assumptions like this
-
12:11
once your application has gone into production.
-
12:14
And that completes our generic base repository class.
-
12:18
Next we'll update our ComicBooks and
-
12:20
ComicBookArtist repositories to make use of it.
You need to sign up for Treehouse in order to download course files.
Sign up