Modern is a set of modern .NET tools π¨ π§ for fast and efficient development of common backend tasks.
It allows to create a production ready applications with just set of models and configuration which can be further extended.
Modern tool are flexible, easily changeable and extendable.
It includes the following components:
- generic repositories for SQL and NoSQL databases
- generic services with and without caching support
- generic in memory services with in-memory filtering capabilities
- in-memory and redis generic caches
- generic set of CQRS queries and commands over repository (if you prefer CQRS over services)
- generic controllers for all types of services
- OData controllers for all types of services
For more information - see full documentation here.
Examples for all types of components - see here.
- How to get started?
- Roadmap
- Repositories
- Repositories for SQL databases
- Repositories for NoSQL databases
- Services
- Services with caching
- Services In Memory
- CQRS
- CQRS with caching
- Controllers
- Controllers CQRS
- Controllers In Memory
- OData Controllers
- OData Controllers In Memory
Lets create a Web Api with CRUD operations over Airplane entities. We need a repository, service and controller.
- Install the following Nuget packages:
-
Create classes for the following models: AirplaneDto and AirplaneDbo.
-
Create an EF Core DbContext for accessing the AirplaneDbo.
-
Register the Modern builder in DI and add the following components:
builder.Services
.AddModern()
.AddRepositoriesEfCore(options =>
{
options.AddRepository<FlyingDbContext, AirplaneDbo, long>();
})
.AddServices(options =>
{
options.AddService<AirplaneDto, AirplaneDbo, long, IModernRepository<AirplaneDbo, long>>();
})
.AddControllers(options =>
{
options.AddController<CreateRequest, UpdateRequest, AirplaneDto, AirplaneDbo, long>();
});
As a result a production ready API will be created:
GET π΅
/Airplanes/get/{id}
GET π΅ π΅
/Airplanes/get
POST β
/Airplanes/create
POST β
β
/Airplanes/create-many
PUT γ½οΈ
/Airplanes/update/{id}
PUT γ½οΈ γ½οΈ
/Airplanes/update-many
PATCH π²
/Airplanes/patch/{id}
DELETE β
/Airplanes/delete/{id}
DELETE β β
/Airplanes/delete-many
The following features will be implemented in the next releases:
- Assembly scan in DI packages
- Unit and integration tests
- MinimalsApis
- Reflection improvements
Modern generic repository is divided into 2 interfaces: IModernQueryRepository<TEntity, TId>
and IModernCrudRepository<TEntity, TId>
.
IModernQueryRepository
has the following methods:
Task<TEntity> GetByIdAsync(TId id, EntityIncludeQuery<TEntity>? includeQuery = null, CancellationToken cancellationToken = default);
Task<TEntity?> TryGetByIdAsync(TId id, EntityIncludeQuery<TEntity>? includeQuery = null, CancellationToken cancellationToken = default);
Task<IEnumerable<TEntity>> GetAllAsync(EntityIncludeQuery<TEntity>? includeQuery = null, CancellationToken cancellationToken = default);
Task<long> CountAsync(CancellationToken cancellationToken = default);
Task<long> CountAsync(Expression<Func<TEntity, bool>> predicate, EntityIncludeQuery<TEntity>? includeQuery = null,
CancellationToken cancellationToken = default);
Task<bool> ExistsAsync(Expression<Func<TEntity, bool>> predicate, EntityIncludeQuery<TEntity>? includeQuery = null, CancellationToken cancellationToken = default);
Task<TEntity?> FirstOrDefaultAsync(Expression<Func<TEntity, bool>> predicate, EntityIncludeQuery<TEntity>? includeQuery = null, CancellationToken cancellationToken = default);
Task<TEntity?> SingleOrDefaultAsync(Expression<Func<TEntity, bool>> predicate, EntityIncludeQuery<TEntity>? includeQuery = null, CancellationToken cancellationToken = default);
Task<IEnumerable<TEntity>> WhereAsync(Expression<Func<TEntity, bool>> predicate, EntityIncludeQuery<TEntity>? includeQuery = null, CancellationToken cancellationToken = default);
Task<PagedResult<TEntity>> WhereAsync(Expression<Func<TEntity, bool>> predicate, int pageNumber, int pageSize, EntityIncludeQuery<TEntity>? includeQuery = null, CancellationToken cancellationToken = default);
IQueryable<TEntity> AsQueryable();
IModernCrudRepository
has the following methods:
Task<TEntity> CreateAsync(TEntity entity, CancellationToken cancellationToken = default);
Task<List<TEntity>> CreateAsync(List<TEntity> entities, CancellationToken cancellationToken = default);
Task<TEntity> UpdateAsync(TId id, TEntity entity, CancellationToken cancellationToken = default);
Task<List<TEntity>> UpdateAsync(List<TEntity> entities, CancellationToken cancellationToken = default);
Task<TEntity> UpdateAsync(TId id, Action<TEntity> update, CancellationToken cancellationToken = default);
Task<bool> DeleteAsync(TId id, CancellationToken cancellationToken = default);
Task<bool> DeleteAsync(List<TId> ids, CancellationToken cancellationToken = default);
Task<TEntity> DeleteAndReturnAsync(TId id, CancellationToken cancellationToken = default);
Modern generic repositories for SQL databases are built on top of 2 the following ORM frameworks:
- EF Core
- Dapper
To use EF Core repository install the Modern.Repositories.EFCore
Nuget package and register it within Modern builder in DI:
builder.Services
.AddModern()
.AddRepositoriesEfCore(options =>
{
options.AddRepository<FlyingDbContext, AirplaneDbo, long>(useDbFactory: false);
});
Specify the type of EF Core DbContext, Dbo entity model and primary key.
useDbFactory
parameter indicates whether repository with DbContextFactory should be used. The default value is false
.
βΉοΈ Use this parameter if you plan to inherit from this generic repository and extend or change its functionality.
When usingDbContextFactory
every repository creates and closes a database connection in each method.
When NOT usingDbContextFactory
repository shares the same database connection during its lifetime.
β οΈ It is not recommended to useuseDbFactory = false
when repository is registered as SingleInstance, otherwise a single database connection will persist during the whole application lifetime
To use Dapper repository install the Modern.Repositories.Dapper
Nuget package and register it within Modern builder in DI:
builder.Services
.AddModern()
.AddRepositoriesDapper(options =>
{
options.ProvideDatabaseConnection(() => new NpgsqlConnection(connectionString));
options.AddRepository<AirplaneDapperMapping, AirplaneDbo, long>();
});
Specify the type of Dapper mapping class, Dbo entity model and primary key.
A dapper needs to know how to create a database connection.
Since there are multiple database connection classes - provide the needed one using ProvideDatabaseConnection
method.
Dapper repository requires to have a small mapping class that way generic repository can match the entity property name with database table column.
βΉοΈ Mapping class for Dapper is a part of Modern tools and not a part of Dapper library
For example consider the following mapping class:
public class AirplaneDapperMapping : DapperEntityMapping<AirplaneDbo>
{
protected override void CreateMapping()
{
Table("db_schema_name.airplanes")
.Id(nameof(AirplaneDbo.Id), "id")
.Column(nameof(AirplaneDbo.YearOfManufacture), "year_of_manufacture")
.Column(nameof(AirplaneDbo.ModelId), "model_id")
//...
;
}
}
Modern generic repositories for No SQL databases are built on of the following NoSQL databases:
- MongoDB
- LiteDb (embedded single-file NoSQL database)
To use MongoDB repository install the Modern.Repositories.MongoDB
Nuget package and register it within Modern builder in DI:
builder.Services
.AddModern()
.AddRepositoriesMongoDb(options =>
{
options.ConfigureMongoDbClient(mongoDbConnectionString);
options.AddRepository<AirplaneDbo, string>("database_name", "collection_name");
});
Specify the type of Dbo entity model and "_id" key.
Provide the connection string in ConfigureMongoDbClient
method.
You can also use the second parameter updateSettings
and configure the custom parameters in a MongoClientSettings
class of MongoDB Driver.
To use LiteDB repository install the Modern.Repositories.LiteDB
Nuget package and register it within Modern builder in DI:
builder.Services
.AddModern()
.AddRepositoriesLiteDb(options =>
{
options.AddRepository<AirplaneDbo, string>("connection_string", "collection_name");
});
Specify the type of Dbo entity model and "_id" key.
βΉοΈ Right out of the box LiteDB official library supports only synchronous methods. To use asynchronous methods a third party library can be used. Modern libraries support asynchronous methods in LiteDB using litedb-async library
β οΈ DISCLAIMER: LiteDb async repository uses litedb-async library which is not an official LiteDb project. Modern libraries are NOT responsible for any problems with litedb-async library, so use this package at your own risk.
To use LiteDB Async repository install the Modern.Repositories.LiteDB.Async
Nuget package and register it within Modern builder in DI:
builder.Services
.AddModern()
.AddRepositoriesLiteDbAsync(options =>
{
options.AddRepository<AirplaneDbo, long>("connection_string", "collection_name");
});
Specify the type of Dbo entity model and "_id" key.
Modern generic service is divided into 2 interfaces: IModernQueryService<TEntityDto, TEntityDbo, TId>
and IModernCrudService<TEntityDto, TEntityDbo, TId>
.
IModernQueryService
has the following methods:
Task<TEntityDto> GetByIdAsync(TId id, CancellationToken cancellationToken = default);
Task<TEntityDto?> TryGetByIdAsync(TId id, CancellationToken cancellationToken = default);
Task<List<TEntityDto>> GetAllAsync(CancellationToken cancellationToken = default);
Task<long> CountAsync(CancellationToken cancellationToken = default);
Task<long> CountAsync(Expression<Func<TEntityDbo, bool>> predicate, CancellationToken cancellationToken = default);
Task<bool> ExistsAsync(Expression<Func<TEntityDbo, bool>> predicate, CancellationToken cancellationToken = default);
Task<TEntityDto?> FirstOrDefaultAsync(Expression<Func<TEntityDbo, bool>> predicate, CancellationToken cancellationToken = default);
Task<TEntityDto?> SingleOrDefaultAsync(Expression<Func<TEntityDbo, bool>> predicate, CancellationToken cancellationToken = default);
Task<List<TEntityDto>> WhereAsync(Expression<Func<TEntityDbo, bool>> predicate, CancellationToken cancellationToken = default);
Task<PagedResult<TEntityDto>> WhereAsync(Expression<Func<TEntityDbo, bool>> predicate, int pageNumber, int pageSize, CancellationToken cancellationToken = default);
IQueryable<TEntityDbo> AsQueryable();
IModernCrudService
has the following methods:
Task<TEntityDto> CreateAsync(TEntityDto entity, CancellationToken cancellationToken = default);
Task<List<TEntityDto>> CreateAsync(List<TEntityDto> entities, CancellationToken cancellationToken = default);
Task<TEntityDto> UpdateAsync(TId id, TEntityDto entity, CancellationToken cancellationToken = default);
Task<List<TEntityDto>> UpdateAsync(List<TEntityDto> entities, CancellationToken cancellationToken = default);
Task<TEntityDto> UpdateAsync(TId id, Action<TEntityDto> update, CancellationToken cancellationToken = default);
Task<bool> DeleteAsync(TId id, CancellationToken cancellationToken = default);
Task<bool> DeleteAsync(List<TId> ids, CancellationToken cancellationToken = default);
Task<TEntityDto> DeleteAndReturnAsync(TId id, CancellationToken cancellationToken = default);
Modern generic services use Modern generic repositories to perform CRUD operations.
To use Service install the Modern.Services.DataStore
Nuget package and register it within Modern builder in DI:
builder.Services
.AddModern()
.AddServices(options =>
{
options.AddService<AirplaneDto, AirplaneDbo, long, IModernRepository<AirplaneDbo, long>>();
});
Specify the type of Dto and dbo entity models, primary key and modern repository.
Service requires one of modern repositories to be registered.
Modern generic services with caching support use Modern generic repositories and cache to perform CRUD operations.
To use Service with caching install the Modern.Services.DataStore.Cached
and Modern.Cache.InMemory
Nuget packages and register them within Modern builder in DI:
builder.Services
.AddModern()
.AddInMemoryCache(options =>
{
options.AddCache<AirplaneDto, long>();
options.CacheSettings.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30);
options.CacheSettings.SlidingExpiration = TimeSpan.FromMinutes(10);
})
.AddCachedServices(options =>
{
options.AddService<AirplaneDto, AirplaneDbo, long, IModernRepository<AirplaneDbo, long>>();
});
Or install the Modern.Services.DataStore.Cached
and Modern.Cache.Redis
Nuget packages and register them within Modern builder in DI:
builder.Services
.AddModern()
.AddRedisCache(options =>
{
options.AddCache<AirplaneDto, long>();
options.RedisConfiguration.ConnectionString = redisConnectionString;
options.RedisConfiguration.AbortOnConnectFail = false;
options.RedisCacheSettings.ExpiresIn = TimeSpan.FromMinutes(30);
})
.AddCachedServices(options =>
{
options.AddService<AirplaneDto, AirplaneDbo, long, IModernRepository<AirplaneDbo, long>>();
});
When registering service specify the type of Dto and dbo entity models, primary key and modern repository.
Service requires one of modern repositories to be registered.
When using InMemoryCache modify the CacheSettings of type MemoryCacheEntryOptions
to specify the cache expiration time.
When using RedisCache modify the RedisConfiguration
of StackExchange.Redis
package and expiration time in RedisCacheSettings
.
Modern generic in memory services use Modern generic repositories and in memory cache to perform CRUD operations.
In Memory Services holds all the data in cache and performs filtering in the memory. While CachedService only use cache for the items it retrieves frequently.
To use In Memory Service install the Modern.Services.DataStore.InMemory
Nuget package and register it within Modern builder in DI:
builder.Services
.AddModern()
.AddInMemoryServices(options =>
{
options.AddService<AirplaneDbo, AirplaneDbo, long, IModernRepository<AirplaneDbo, long>>();
});
Specify the type of Dto and dbo entity models, primary key and modern repository.
Service requires one of modern repositories to be registered.
The cache is registered under the hood and there is no Redis support as Redis doesn't support LINQ expressions on its items.
The cache can be changed to the custom one if needed.
Modern generic CQRS consist of Commands and Queries. CQRS has the following Queries:
GetAllQuery<TEntityDto, TId>() : IRequest<List<TEntityDto>>
GetByIdQuery<TEntityDto, TId>(TId Id) : IRequest<TEntityDto>
TryGetByIdQuery<TEntityDto, TId>(TId Id) : IRequest<TEntityDto?>
GetCountAllQuery<TEntityDto, TId> : IRequest<long>
GetCountQuery<TEntityDbo, TId>(Expression<Func<TEntityDbo, bool>> Predicate) : IRequest<long>
GetExistsQuery<TEntityDbo, TId>(Expression<Func<TEntityDbo, bool>> Predicate) : IRequest<bool>
GetFirstOrDefaultQuery<TEntityDto, TEntityDbo, TId>(Expression<Func<TEntityDbo, bool>> Predicate) : IRequest<TEntityDto?>
GetSingleOrDefaultQuery<TEntityDto, TEntityDbo, TId>(Expression<Func<TEntityDbo, bool>> Predicate) : IRequest<TEntityDto?>
GetWhereQuery<TEntityDto, TEntityDbo, TId>(Expression<Func<TEntityDbo, bool>> Predicate) : IRequest<List<TEntityDto>>
GetWherePagedQuery<TEntityDto, TEntityDbo, TId> : IRequest<PagedResult<TEntityDto>>
CQRS has the following Commands:
CreateEntityCommand<TEntityDto>(TEntityDto Entity) : IRequest<TEntityDto>
CreateEntitiesCommand<TEntityDto>(List<TEntityDto> Entities) : IRequest<List<TEntityDto>>
UpdateEntityCommand<TEntityDto, TId>(TId Id, TEntityDto Entity) : IRequest<TEntityDto>
UpdateEntityByActionCommand<TEntityDto, TId>(TId Id, Action<TEntityDto> UpdateAction) : IRequest<TEntityDto>
UpdateEntitiesCommand<TEntityDto>(List<TEntityDto> Entities) : IRequest<List<TEntityDto>>
DeleteEntityCommand<TId>(TId Id) : IRequest<bool>
DeleteEntitiesCommand<TId>(List<TId> Ids) : IRequest<bool>
DeleteAndReturnEntityCommand<TEntityDto, TId>(TId Id) : IRequest<TEntityDto>
Modern generic CQRS consist of Commands and Queries which use Modern generic repositories to perform CRUD operations.
To use CQRS install the Modern.CQRS.DataStore
Nuget package and register it within Modern builder in DI:
builder.Services
.AddModern()
.AddCqrs(options =>
{
options.AddQueriesCommandsAndHandlersFor<AirplaneDto, AirplaneDbo, long, IModernRepository<AirplaneDbo, long>>();
});
Specify the type of Dto and dbo entity models, primary key and modern repository.
CQRS requires one of modern repositories to be registered.
Modern generic CQRS Commands and Queries with caching support use Modern generic repositories and cache to perform CRUD operations.
To use Service with caching install the Modern.Services.DataStore.Cached
and Modern.Cache.InMemory
Nuget packages and register them within Modern builder in DI:
builder.Services
.AddModern()
.AddInMemoryCache(options =>
{
options.AddCache<AirplaneDto, long>();
options.CacheSettings.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30);
options.CacheSettings.SlidingExpiration = TimeSpan.FromMinutes(10);
})
.AddCachedCqrs(options =>
{
options.AddQueriesCommandsAndHandlersFor<AirplaneDto, AirplaneDbo, long, IModernRepository<AirplaneDbo, long>>();
});
Or install the Modern.Services.DataStore.Cached
and Modern.Cache.Redis
Nuget packages and register them within Modern builder in DI:
builder.Services
.AddModern()
.AddRedisCache(options =>
{
options.AddCache<AirplaneDto, long>();
options.RedisConfiguration.ConnectionString = redisConnectionString;
options.RedisConfiguration.AbortOnConnectFail = false;
options.RedisCacheSettings.ExpiresIn = TimeSpan.FromMinutes(30);
})
.AddCachedCqrs(options =>
{
options.AddQueriesCommandsAndHandlersFor<AirplaneDto, AirplaneDbo, long, IModernRepository<AirplaneDbo, long>>();
});
Specify the type of Dto and dbo entity models, primary key and modern repository.
CQRS requires one of modern repositories to be registered.
When using InMemoryCache modify the CacheSettings of type MemoryCacheEntryOptions
to specify the cache expiration time.
When using RedisCache modify the RedisConfiguration
of StackExchange.Redis
package and RedisCacheSettings
expiration time.
Modern generic controller has the following HTTP endpoints:
[Route("api/[controller]")]
public class ModernController<TCreateRequest, TUpdateRequest, TEntityDto, TEntityDbo, TId> : ControllerBase
{
[HttpGet("get/{id}")]
Task<IActionResult> GetById([Required] TId id)
[HttpGet("get")]
Task<IActionResult> GetAll(CancellationToken cancellationToken)
[HttpPost("create")]
Task<IActionResult> Create([FromBody, Required] TCreateRequest request)
[HttpPost("create-many")]
Task<IActionResult> CreateMany([FromBody, Required] List<TCreateRequest> requests)
[HttpPut("update/{id}")]
Task<IActionResult> Update([Required] TId id, [FromBody, Required] TUpdateRequest request)
[HttpPut("update-many")]
Task<IActionResult> UpdateMany([FromBody, Required] List<TUpdateRequest> requests)
[HttpPatch("patch/{id}")]
Task<IActionResult> Patch([Required] TId id, [FromBody] JsonPatchDocument<TEntityDto> patch)
[HttpDelete("delete/{id}")]
Task<IActionResult> Delete([Required] TId id)
[HttpDelete("delete-many")]
Task<IActionResult> DeleteMany([Required] List<TId> ids)
}
Modern generic controllers use Modern generic services to perform CRUD operations.
To use Controller install the Modern.Controllers.DataStore
Nuget package and register it within Modern builder in DI:
builder.Services
.AddModern()
.AddControllers(options =>
{
options.AddController<CreateRequest, UpdateRequest, AirplaneDto, AirplaneDbo, long>();
});
Specify the type of create and update requests, dto entity model and primary key.
Controller requires one of modern services to be registered: regular one or with caching.
Modern generic CQRS controllers use Modern CQRS Commands and Queries to perform CRUD operations.
To use CQRS Controller install the Modern.Controllers.CQRS.DataStore
Nuget package and register it within Modern builder in DI:
builder.Services
.AddModern()
.AddCqrsControllers(options =>
{
options.AddController<CreateRequest, UpdateRequest, AirplaneDto, AirplaneDbo, long>();
});
Specify the type of create and update requests, dto entity model and primary key.
Controller requires CQRS Commands and Queries to be registered: regular one or with caching.
Modern generic controllers use Modern generic services to perform CRUD operations.
To use In Memory Controller install the Modern.Controllers.DataStore.InMemory
Nuget package and register it within Modern builder in DI:
builder.Services
.AddModern()
.AddInMemoryControllers(options =>
{
options.AddController<CreateRequest, UpdateRequest, AirplaneDto, AirplaneDbo, long>();
});
Specify the type of create and update requests, dto entity model and primary key.
Controller requires a modern in memory service to be registered.
Modern generic OData controllers use Modern generic repositories to perform OData queries.
To use OData Controller install the Modern.Controllers.DataStore.OData
Nuget package and register it within Modern builder in DI:
builder.Services
.AddModern()
.AddODataControllers(options =>
{
options.AddController<AirplaneDbo, long>();
});
Specify the type of dto entity model and primary key.
OData Controller requires one of modern repositories to be registered.
Also register OData in the DI:
builder.Services.AddControllers(options =>
{
})
//..
.AddOData(opt =>
{
// Adjust settings as appropriate
opt.AddRouteComponents("api/odata", GetEdmModel());
opt.Select().Filter().Count().SkipToken().OrderBy().Expand().SetMaxTop(1000);
opt.TimeZone = TimeZoneInfo.Utc;
});
IEdmModel GetEdmModel()
{
// Adjust settings as appropriate
var builder = new ODataConventionModelBuilder();
builder.EnableLowerCamelCase();
// Register your OData models here. Name of the EntitySet should correspond to the name of OData controller
builder.EntitySet<AirplaneDbo>("airplanes");
builder.EntityType<AirplaneDbo>();
return builder.GetEdmModel();
}
Modern generic OData controllers use Modern generic repositories to perform OData queries.
To use In Memory OData Controller install the Modern.Controllers.DataStore.InMemory.OData
Nuget package and register it within Modern builder in DI:
builder.Services
.AddModern()
.AddInMemoryODataControllers(options =>
{
options.AddController<AirplaneDbo, long>();
});
Specify the type of dto entity model and primary key.
OData Controller requires one of modern repositories to be registered.
Remember to configure OData in DI as mentioned in see OData Controllers
If you find this package helpful, consider supporting my work by buying me a coffee β!
Your support is greatly appreciated and helps me continue developing and maintaining this project.
You can also give me a β on github to make my package more relevant to others.
Thank you for your support!