There were epic major changes from version 1 to 2. I will try to provide guides to switch to 2.0 as soon as i can.
This library gives you a pre-implementation of an N-Tier crud application. So you
focus on writing other functionalities of your code. By Inheriting CrudControllerBase
you basically have a Controller using a service using UnitOfWork pattern to get a
repository to perform restful crud operations and all are plugged together and working.
- You can create a CrudController then add other endpoints to it if necessary.
- You can inject any service you need into your Controller
- You can Create your service from scratch, also you can extend
CrudService
which already has crud operations implemented, then add additional functionalities you need to it. - Since best practice for Uni-of-work pattern implies to have One UnitOfWork
per application, if you want to implement your own UnitOfWork class, you can implement
IUnitOfWork
interface or you can extends theUnitOfWorkBase
class to create your own unit of work. - For Repositories, it's the same; you can write them from scratch, implement
ICrudRepository
or extend (or wrap)CrudRepository
.
First you will add the package. It's available on NuGet.org. you can add the reference to your <project>.csproj:
PackageReference Include="EnTier" Version="2.1.0" />
or via package manager console:
Install-Package EnTier -Version 2.1.0
or dotnet Cli:
dotnet add package EnTier --version 2.1.0
The simplest use-case of the library would be
- create and new web-api dotnet core project
- create a model
- add a crud controller by extending
CrudControllerBase
, and adding a[Route(...)]
attribute to it.
Done!
The example project Example.SingleLayerEntity implements such use case. This way
EnTier will create and use an InMemoryUnitOfWork
, which itself produces in-memory
repositories and use them with CrudService
(s).
EnTier needs to find the id field in your models for certain operations. You can use Member attributes
(AutovalueMember
, UniqueMember
) on the properties or signal the Id field ba naming it Id. (case insensitive).
this is the priority that EnTier uses to detect your Id field:
- The property has an
AutovalueMember
attribute - A property with
UniqueMember
attribute - The property that its name is "Id" (case-insensitive)
In-Memory data access layer is suitable for testing. But you can change it to JsonFile
data access layer easily by calling services.AddJsonFileUnitOfWork();
at the
Startup class. This way you will use builtin JsonFileUnitOfWork
which produces
JsonFileCrudRepository
(s). This data access, persists data in json files kept
in a directory called JsonDatabase where your executable file is.
This data access layer might be more suitable for instant demos or such use cases.
The example project Example.JsonFile shows this implementation. In this example also different models are used for Transfer (Dtos), Domain and Storage. so EnTier uses its internal Mapper to map objects. Later you will see how to use another mapper instead.
Same as Json data access layer, you can switch your data access layer to EntityFramework
just by calling services.AddEntityFrameworkUnitOfWork(context);
at
startup class. This method takes a DbContext as argument, and uses this db context to
perform all crud data operations.
for this to work you should
- Have Entity Framework packages installed
- Add extension package: 'EnTier.DataAccess.EntityFramework' which is also available on NuGet.
- You should have your ef migrations created and applied.
-
Important note: do not forget to add DBSets properties for each entity
into your DBContext class. Otherwise Entity Framework would not recognize your entities and they will not be included in your migrations.
The example project Example.EntityFramework shows this implementation. This example also uses isolated models for each layer.
The builtin mapper, is very limited and is not capable of handling a production situation.
since mappers are being added using injection, you can use any Mapper of your choice
just by wrapping it in an implementation of IMapper
which is a very simple interface.
This is already done for AutoMapper package, so if you prefer to use AutoMapper, just add its AutoMapper package, then add AutoMapper extension package: 'EnTier.AutoMapper' from NuGet. And add AutoMapperAdapter to Ioc container for IMapper.
services.AddAutoMapper(config => ...);
The example project Example.AutoMapper shows this implementation. This project also shows how to have different types for Id property on Entity models using Mapper.
When you implement your own ICrudRepository
, you can get your Custom repository
in your services by calling IUnitOfWork.GetCrudRepository<TStorage, TId, TCustomCrudRepository>()
.
This way, everything will work fine but the IUnitOfWork.GetCrudRepository<TStorage, TId>()
method still will return the default Crud-Repository for your model, not the Custom Repository
you just created. To override this behavior, and get your that custom repository, for every usage
in your EnTier application, you need to do one step more and register your custom repository.
to do so, in the web applications you can call IApplicationBuilder.UseRepository<TStorage,TId,TCustomRepository>()
in your startup file like this:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
//...
app.UseRepository<PostStg, long, CustomRepository>();
}
and for other types of application like console applications you can just simply call
UnitOfWorkRepositoryConfigurations.GetInstance().RegisterCustomRepository<TStorage,TId,TCustomRepository>()
.
In Example.EntityFramework.CustomRepository, notice that the https://localhost:5001/Posts/ endpoint
will return data from CustomRepository, without explicitly using CustomRepository like the other endpoint
(https://localhost:5001/Posts/custom). That is because CustomRepository
is registered as the
implementation for ICrudRepository
at the startup. If you delete that line in startup code, then
https://localhost:5001/Posts/ method will return database data without any manipulation, because it will
use a default crud repository. But the https://localhost:5001/Posts/custom endpoint will still return
data from custom repository because it's explicitly using IUnitOfWork.GetRepository<TStorage,TId,TCustom>()
.
- Note: PLEASE NOTE THAT THE
ICrudRepository.Update(.)
METHOD IS NOT ACTUALLY WELL-CONFIRMING WITH the Repository Design Pattern. IT ONLY EXISTS FOR SO THAT THE CrudRepository OBJECTS HAVE A MORE COMPLETE AND HANDY SET OF METHODS FOR CASES THAT A LITTLE YAGNI WOULD BE MORE BENEFICIAL.
Custom Repositories and constructor injection
When you write a custom repository, it will be instantiated by your UnitOfWork. So That determines the limitations of injection.
-
If you are using the builtin Json DataAccessLayer or InMemory DataAccessLayer, then there will be no injection available for your custom repositories. Your custom repository must have a parameter-less constructor.
-
If you are using Entier.DataAccess.EntityFramework, you can have any number of
DbSet<TStorage>
parameters injected into the repository through the constructor. the EntityFrameworkUnitOfWork will find and instantiate and deliver these. But just make sure you have such db-sets as properties of your DbContext. Using this feature, you can have access to any db-set in your repository, create required queries inside the repository and return in-memory objects (vs queriables) to your services.
Stripping Non-Primitive-Properties
While inserting data into storage at data access layer, in most cases we store a pocco without non-primitive properties. So by default, CrudRepositoryBase class will strip away any non-primitive properties from entity by setting thos values to null. But In some cases (ie. test repositories and etc.) we might for some reason prefer to insert those values as well. so we would want to override this behavior. To do so, We can decorate the insert method with following attributes.
[KeepAllProperties]
[StripAllProperties]
[KeepProperty]
[StripProperty]
These attributes can be placed on ICrudRepository.Add(.)
method, or in higher lever, on ICrudService.Add(.)
method or in higher level on Controller.Add(.)
method. Basically on any method in call chain from Controller to
repository. In General, [StripProperty]
attributes supersede [KeepProperty]
attributes. And between
[KeepAllProperties]
and [StripAllProperties]
, the latter supersedes the former.
You can mark any number of entity types with [KeepProperty]
or [StripProperty]
, while using these attributes:
[StripProperty(typeof(StrippingProperty1),typeof(StrippingProperty2))]
[KeepProperty(typeof(KeepingProperty1))]
public Model Add(Model value){
//...
}
-
Note: You can change these behaviors while driving from
CrudRepositoryBase
class by overriding the add method. -
Note: While driving from
CrudRepositoryBase
, you can useStripNonPrimitives()
method which stripes all non-primitive properties, andStripMarkedSubEntities()
method which strips marked non-primitive properties regarding the delivered attributes to the call chain. -
Note: Since InMemoryCrudRepository and JsonFileCrudRepository, are mainly purposed for tests and demoes, these repositoreies will keep and actually insert all non-primitive properties by overriding the add method and adding
[KeepAllProperties]
attribute on it. This way you can override this behavior again inside your code.
[KeepAllProperties()]
public override TStorage Add(TStorage value)
{
return base.Add(value);
}
you can achieve same result by overriding the Add()
method and calling base.Insert()
instead, for less overhead.
You can add your custom service instead of the default Crud service that EnTier automatically will create for you.
Your custom service must implement ICrudService<..>
interface. The you can
- Pass your service instance to Controller's constructor
Or (Recommended)
- Creat a contract (interface/abstraction) for your custom service and register your custom service for your contract via your DI container. Then inject your contract into your controller's constructor. and pass the injected object into base constructor.
This way, your custom service will be used in EnTier calls and you will be able to manipulate the service's behavior in any way you need.
- NOTE: You can implement your custom service, by extending the class
CrudServiceBase<>
easier. - NOTE: If you are injecting your custom service into your controller, DO NOT FORGET TO REGISTER it on your container.
In some cases you might need to have some kind of regulation on your data before its being processed or stored. For example you might be receiving a chunk of data from the user to be inserted into Database, and it's crucial that a specific field of your data be filled, or an id must already be existing in the database or thing like this.
The DataAccessRegulation helps to implement this in Service level. An IDataAccessRegulator<TDomain,TStorage>
, has a
Regulate(TDomain model)
method. This method takes a domain value and returns a RegulationResult
. A RegulationResult
object, has a Status field, a TModel
field containing domain model, and a TStorage
field containing mapped
storage model. (So in practice you might need to inject your mapper into your regulators).
The Status field can be rejected, Accepted or Suspicious. The paradigm is to investigate received domain-model,
and provide a valid storage counterpart for it and putting both this values into a RegulationResult
object
with Ok Status.
- a RegulationResult with status = Ok, Means that both Domain and Storage fields are trustable and safe to use.
If the received domain model, is somehow un acceptable, The Regulate
method will return a RegulationResult object
with UnAcceptable status, meaning that the data is rejected and Domain and Storage fields are not trustable.
- a RegulationResult with status = UnAcceptable, Means that data is rejected and nighter Domain or Storage fields are not valid.
In some cases, the received data might be incorrect but you find it safe to fix the data and use it. In such cases the
Regulate
method will produce a fixed and valid storage model, and put it into a RegulationResult object with the
Suspicious status.
- a RegulationResult with status = Suspicious, Means that the Storage field is usable but there was a problem with the received data.
You can create Regulators by implementing IDataAccessRegulator<TDomain,TStorage>
or extending
DataAccessRegulatorBase<TDomain,TStorage>
. Regulators can be injected and used in your services.
Use Regulators in Default CrudServices
If you have a regulator for your storage/domain model pairs, you can make EnTier to use your regulator before performing Create and Update operations. To do that, you just need to pass an instance to the base controller's constructor. The better practice would be to define a contract for the regulator, Registering the regulators for its contract in DI Container. and injecting it into the controller to be passed to base controller. Example.Regulation project shows such a use case. For simplicity, this example only uses one Model type for all layers.
One thing this library solves for me, is to ease decoupling of data access layer from services. You can use for example EntityFramework for production, and in UnitTest os services, Contract tests and some of integration tests (basically any type of test that is not supposed to cover your data-access code itself), you can use InMemory data access.
EnTier also provides a very simple Fixture creation mechanism which will pre populate your data base easily. To do so you just
-
Add a Fixture class: Its a normal class which supports constructor injection. Plus you can declare any number of methods called
Setup(...)
. these methods will be executed before application starts. setup methods can have any number ofIRepository<TStorage,TId>
arguments. These repositories will be injected into method before being called, based on the unit of work you are using in your test project. -
Use Fixture classes at Startup class: By calling
app.UseFixture<ExampleFixture>();
inConfigure(IApplicationBuilder app, ...)
method of your startup class, you can use each Fixture class.
Example project Example.Test shows an integration-like test with EnTier and InMemory data access layer. it uses xUnit as test frame work and tests the project Example.JsonFile
- Note: If you change your DI, from a DI other than dotnet's builtin DI, the UseFixture method will change slightly regarding the DI/Container you are using.
By default, EnTier supports and uses dotnet core's builtin Di. But you can use it with
-
CastleWindsor: by adding the package EnTier.DependencyInjection.CastleWindsor from nuget.org.
- CastleWindsor may or may not be registered on
IServiceCollection
, so for adding fixtures, to make sure it works correctly, you should callAddFixtureWithWindsor<T>()
on your container object (IWindsorContainer
).
- CastleWindsor may or may not be registered on
-
Unity: by adding the package EnTier.DependencyInjection.Unity from nuget.org.
- For adding fixtures, you would call
app.AddFixtureWithUnity<T>()
atConfigure(IApplicationBuilder app,...)
method in your startup class.
- For adding fixtures, you would call
-
The example project Example.CastleWindsor shows a use case of EnTier alongside Castle.Windsor Ioc.
-
The example project Example.Unity shows a use case of EnTier alongside Unity Container Ioc.
-
Changing the DI, means methods like
services.AddAutoMapper()
are not there and you need to registerAutoMapperAdapter
forEnTier.Mapper.IMapper
on your DI system manually. Its also the same forservices.AddEntityFrameworkUnitOfWork()
, and you should register an instance ofEntityFrameworkUnitOfWork
(or any custom UnitOfWork implementation) for interfaceIUnitOfWork
manually.
-
Builtin Mapper is no good for production, its just for library to work out of box to ease the process of familiarizing and first usages. Using Automapper is Highly recommended.
-
Do not forget to add
[ApiController]
and[Route(...)]
, attributes on controllers, otherwise it might take a long time to figure the cause of silent bugs!
Type of Ids Can be different in each layer for an entity. But in order to use different Id Types you need to take in consideration
- Mapping of Entities,
- Mapping of Ids.
- The Builtin Mapper does not support configurations, therefore it cannot handle mapping different types for ids.
The Example: Example.AutoMapper shows a use-case with storage and domain using Guid for id type and transfer objects having string Ids.
I Hope this saves some of your time
Regards
Mani