Entity Framework Core Integration

This document explains how to integrate EF Core as an ORM provider to ABP based applications and how to configure it.

Installation

Volo.Abp.EntityFrameworkCore is the main nuget package for the EF Core integration. Install it to your project (for a layered application, to your data/infrastructure layer):

Install-Package Volo.Abp.EntityFrameworkCore

Then add AbpEntityFrameworkCoreModule module dependency (DependsOn attribute) to your module:

using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.Modularity;

namespace MyCompany.MyProject
{
    [DependsOn(typeof(AbpEntityFrameworkCoreModule))]
    public class MyModule : AbpModule
    {
        //...
    }
}

Note: Instead, you can directly download a startup template with EF Core pre-installed.

Database Management System Selection

Entity Framework Core supports various database management systems (see all). ABP framework and this document doesn't depend on any specific DBMS.

If you are creating a reusable application module, avoid to depend on a specific DBMS package. However, in a final application you eventually will select a DBMS.

See Switch to Another DBMS for Entity Framework Core document to learn how to switch the DBMS.

Creating DbContext

You can create your DbContext as you normally do. It should be derived from AbpDbContext<T> as shown below:

using Microsoft.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore;

namespace MyCompany.MyProject
{
    public class MyDbContext : AbpDbContext<MyDbContext>
    {
        //...your DbSet properties here

        public MyDbContext(DbContextOptions<MyDbContext> options)
            : base(options)
        {
        }
    }
}

About the EF Core Fluent Mapping

The application startup template has been configured to use the EF Core fluent configuration API to map your entities to your database tables.

You can still use the data annotation attributes (like [Required]) on the properties of your entity while the ABP documentation generally follows the fluent mapping API approach. It is up to you.

ABP Framework has some base entity classes and conventions (see the entities document) and it provides some useful extension methods to configure the properties inherited from the base entity classes.

ConfigureByConvention Method

ConfigureByConvention() is the main extension method that configures all the base properties and conventions for your entities. So, it is a best practice to call this method for all your entities, in your fluent mapping code.

Example: Assume that you've a Book entity derived from AggregateRoot<Guid> base class:

public class Book : AuditedAggregateRoot<Guid>
{
    public string Name { get; set; }
}

You can override the OnModelCreating method in your DbContext and configure the mapping as shown below:

protected override void OnModelCreating(ModelBuilder builder)
{
    //Always call the base method
    base.OnModelCreating(builder);

    builder.Entity<Book>(b =>
    {
        b.ToTable("Books");

        //Configure the base properties
        b.ConfigureByConvention();

        //Configure other properties (if you are using the fluent API)
        b.Property(x => x.Name).IsRequired().HasMaxLength(128);
    });
}
  • Calling b.ConfigureByConvention() is important here to properly configure the base properties.
  • You can configure the Name property here or you can use the data annotation attributes (see the EF Core document).

While there are many extension methods to configure your base properties, ConfigureByConvention() internally calls them if necessary. So, it is enough to call it.

Configure the Connection String Selection

If you have multiple databases in your application, you can configure the connection string name for your DbContext using the [ConnectionStringName] attribute. Example:

[ConnectionStringName("MySecondConnString")]
public class MyDbContext : AbpDbContext<MyDbContext>
{

}

If you don't configure, the Default connection string is used. If you configure a specific connection string name, but not define this connection string name in the application configuration then it fallbacks to the Default connection string (see the connection strings document for more information).

Registering DbContext To Dependency Injection

Use AddAbpDbContext method in your module to register your DbContext class for dependency injection system.

using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.Modularity;

namespace MyCompany.MyProject
{
    [DependsOn(typeof(AbpEntityFrameworkCoreModule))]
    public class MyModule : AbpModule
    {
        public override void ConfigureServices(ServiceConfigurationContext context)
        {
            context.Services.AddAbpDbContext<MyDbContext>();

            //...
        }
    }
}

Add Default Repositories

ABP can automatically create default generic repositories for the entities in your DbContext. Just use AddDefaultRepositories() option on the registration:

services.AddAbpDbContext<MyDbContext>(options =>
{
    options.AddDefaultRepositories();
});

This will create a repository for each aggregate root entity (classes derived from AggregateRoot) by default. If you want to create repositories for other entities too, then set includeAllEntities to true:

services.AddAbpDbContext<MyDbContext>(options =>
{
    options.AddDefaultRepositories(includeAllEntities: true);
});

Then you can inject and use IRepository<TEntity, TPrimaryKey> in your services. Assume that you have a Book entity with Guid primary key:

public class Book : AggregateRoot<Guid>
{
    public string Name { get; set; }

    public BookType Type { get; set; }
}

(BookType is a simple enum here and not important) And you want to create a new Book entity in a domain service:

public class BookManager : DomainService
{
    private readonly IRepository<Book, Guid> _bookRepository;

    //inject default repository to the constructor
    public BookManager(IRepository<Book, Guid> bookRepository)
    {
        _bookRepository = bookRepository;
    }

    public async Task<Book> CreateBook(string name, BookType type)
    {
        Check.NotNullOrWhiteSpace(name, nameof(name));

        var book = new Book
        {
            Id = GuidGenerator.Create(),
            Name = name,
            Type = type
        };

        //Use a standard repository method
        await _bookRepository.InsertAsync(book);

        return book;
    }
}

This sample uses InsertAsync method to insert a new entity to the database.

Add Custom Repositories

Default generic repositories are powerful enough in most cases (since they implement IQueryable). However, you may need to create a custom repository to add your own repository methods. Assume that you want to delete all books by type.

It's suggested to define an interface for your custom repository:

public interface IBookRepository : IRepository<Book, Guid>
{
    Task DeleteBooksByType(BookType type);
}

You generally want to derive from the IRepository to inherit standard repository methods (while, you don't have to do). Repository interfaces are defined in the domain layer of a layered application. They are implemented in the data/infrastructure layer (EntityFrameworkCore project in a startup template).

Example implementation of the IBookRepository interface:

public class BookRepository
    : EfCoreRepository<BookStoreDbContext, Book, Guid>, IBookRepository
{
    public BookRepository(IDbContextProvider<BookStoreDbContext> dbContextProvider)
        : base(dbContextProvider)
    {
    }

    public async Task DeleteBooksByType(BookType type)
    {
        var dbContext = await GetDbContextAsync();
        await dbContext.Database.ExecuteSqlRawAsync(
            $"DELETE FROM Books WHERE Type = {(int)type}"
        );
    }
}

Now, it's possible to inject the IBookRepository and use the DeleteBooksByType method when needed.

Override the Default Generic Repository

Even if you create a custom repository, you can still inject the default generic repository (IRepository<Book, Guid> for this example). Default repository implementation will not use the class you have created.

If you want to replace default repository implementation with your custom repository, do it inside the AddAbpDbContext options:

context.Services.AddAbpDbContext<BookStoreDbContext>(options =>
{
    options.AddDefaultRepositories();

    //Replaces IRepository<Book, Guid>
    options.AddRepository<Book, BookRepository>();
});

This is especially important when you want to override a base repository method to customize it. For instance, you may want to override DeleteAsync method to delete a specific entity in a more efficient way:

public async override Task DeleteAsync(
    Guid id,
    bool autoSave = false,
    CancellationToken cancellationToken = default)
{
    //TODO: Custom implementation of the delete method
}

Loading Related Entities

Assume that you've an Order with a collection of OrderLines and the OrderLine has a navigation property to the Order:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Volo.Abp.Auditing;
using Volo.Abp.Domain.Entities;

namespace MyCrm
{
    public class Order : AggregateRoot<Guid>, IHasCreationTime
    {
        public Guid CustomerId { get; set; }
        public DateTime CreationTime { get; set; }

        public ICollection<OrderLine> Lines { get; set; } //Sub collection

        public Order()
        {
            Lines = new Collection<OrderLine>();
        }
    }

    public class OrderLine : Entity<Guid>
    {
        public Order Order { get; set; } //Navigation property
        public Guid OrderId { get; set; }

        public Guid ProductId { get; set; }
        public int Count { get; set; }
        public double UnitPrice { get; set; }
    }
}

And defined the database mapping as shown below:

builder.Entity<Order>(b =>
{
    b.ToTable("Orders");
    b.ConfigureByConvention();

    //Define the relation
    b.HasMany(x => x.Lines)
        .WithOne(x => x.Order)
        .HasForeignKey(x => x.OrderId)
        .IsRequired();
});

builder.Entity<OrderLine>(b =>
{
    b.ToTable("OrderLines");
    b.ConfigureByConvention();
});

When you query an Order, you may want to include all the OrderLines in a single query or you may want to load them later on demand.

Actually these are not directly related to the ABP Framework. You can follow the EF Core documentation to learn all the details. This section will cover some topics related to the ABP Framework.

Eager Loading / Load With Details

You have different options when you want to load the related entities while querying an entity.

Repository.WithDetails

IRepository.WithDetailsAsync(...) can be used to get an IQueryable<T> by including one relation collection/property.

Example: Get an order with lines

using System;
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Domain.Services;

namespace AbpDemo.Orders
{
    public class OrderManager : DomainService
    {
        private readonly IRepository<Order, Guid> _orderRepository;

        public OrderManager(IRepository<Order, Guid> orderRepository)
        {
            _orderRepository = orderRepository;
        }

        public async Task TestWithDetails(Guid id)
        {
            //Get a IQueryable<T> by including sub collections
            var queryable = await _orderRepository.WithDetailsAsync(x => x.Lines);
            
            //Apply additional LINQ extension methods
            var query = queryable.Where(x => x.Id == id);
            
            //Execute the query and get the result
            var order = await AsyncExecuter.FirstOrDefaultAsync(query);
        }
    }
}

AsyncExecuter is used to execute async LINQ extensions without depending on the EF Core. If you add EF Core NuGet package reference to your project, then you can directly use await query.FirstOrDefaultAsync(). But, this time you depend on the EF Core in your domain layer. See the repository document to learn more.

Example: Get a list of orders with their lines

public async Task TestWithDetails()
{
    //Get a IQueryable<T> by including sub collections
    var queryable = await _orderRepository.WithDetailsAsync(x => x.Lines);

    //Execute the query and get the result
    var orders = await AsyncExecuter.ToListAsync(queryable);
}

WithDetailsAsync method can get more than one expression parameter if you need to include more than one navigation property or collection.

DefaultWithDetailsFunc

If you don't pass any expression to the WithDetailsAsync method, then it includes all the details using the DefaultWithDetailsFunc option you provide.

You can configure DefaultWithDetailsFunc for an entity in the ConfigureServices method of your module in your EntityFrameworkCore project.

Example: Include Lines while querying an Order

Configure<AbpEntityOptions>(options =>
{
    options.Entity<Order>(orderOptions =>
    {
        orderOptions.DefaultWithDetailsFunc = query => query.Include(o => o.Lines);
    });
});

You can fully use the EF Core API here since this is located in the EF Core integration project.

Then you can use the WithDetails without any parameter:

public async Task TestWithDetails()
{
    //Get a IQueryable<T> by including all sub collections
    var queryable = await _orderRepository.WithDetailsAsync();

    //Execute the query and get the result
    var orders = await AsyncExecuter.ToListAsync(queryable);
}

WithDetailsAsync() executes the expression you've setup as the DefaultWithDetailsFunc.

Repository Get/Find Methods

Some of the standard Repository methods have optional includeDetails parameters;

  • GetAsync and FindAsync gets includeDetails with default value is true.
  • GetListAsync and GetPagedListAsync gets includeDetails with default value is false.

That means, the methods return a single entity includes details by default while list returning methods don't include details by default. You can explicitly pass includeDetails to change the behavior.

These methods use the DefaultWithDetailsFunc option that is explained above.

Example: Get an order with details

public async Task TestWithDetails(Guid id)
{
    var order = await _orderRepository.GetAsync(id);
}

Example: Get an order without details

public async Task TestWithoutDetails(Guid id)
{
    var order = await _orderRepository.GetAsync(id, includeDetails: false);
}

Example: Get list of entities with details

public async Task TestWithDetails()
{
    var orders = await _orderRepository.GetListAsync(includeDetails: true);
}

Alternatives

The repository pattern tries to encapsulate the EF Core, so your options are limited. If you need an advanced scenario, you can follow one of the options;

  • Create a custom repository method and use the complete EF Core API.
  • Reference to the Volo.Abp.EntityFrameworkCore package from your project. In this way, you can directly use Include and ThenInclude in your code.

See also eager loading document of the EF Core.

Explicit / Lazy Loading

If you don't include relations while querying an entity and later need to access to a navigation property or collection, you have different options.

EnsurePropertyLoadedAsync / EnsureCollectionLoadedAsync

Repositories provide EnsurePropertyLoadedAsync and EnsureCollectionLoadedAsync extension methods to explicitly load a navigation property or sub collection.

Example: Load Lines of an Order when needed

public async Task TestWithDetails(Guid id)
{
    var order = await _orderRepository.GetAsync(id, includeDetails: false);
    //order.Lines is empty on this stage

    await _orderRepository.EnsureCollectionLoadedAsync(order, x => x.Lines);
    //order.Lines is filled now
}

EnsurePropertyLoadedAsync and EnsureCollectionLoadedAsync methods do nothing if the property or collection was already loaded. So, calling multiple times has no problem.

See also explicit loading document of the EF Core.

Lazy Loading with Proxies

Explicit loading may not be possible in some cases, especially when you don't have a reference to the Repository or DbContext. Lazy Loading is a feature of the EF Core that loads the related properties / collections when you first access to it.

To enable lazy loading;

  1. Install the Microsoft.EntityFrameworkCore.Proxies package into your project (typically to the EF Core integration project)
  2. Configure UseLazyLoadingProxies for your DbContext (in the ConfigureServices method of your module in your EF Core project). Example:
Configure<AbpDbContextOptions>(options =>
{
    options.PreConfigure<MyCrmDbContext>(opts =>
    {
        opts.DbContextOptions.UseLazyLoadingProxies(); //Enable lazy loading
    });
    
    options.UseSqlServer();
});
  1. Make your navigation properties and collections virtual. Examples:
public virtual ICollection<OrderLine> Lines { get; set; } //virtual collection
public virtual Order Order { get; set; } //virtual navigation property

Once you enable lazy loading and arrange your entities, you can freely access to the navigation properties and collections:

public async Task TestWithDetails(Guid id)
{
    var order = await _orderRepository.GetAsync(id);
    //order.Lines is empty on this stage

    var lines = order.Lines;
    //order.Lines is filled (lazy loaded)
}

Whenever you access to a property/collection, EF Core automatically performs an additional query to load the property/collection from the database.

Lazy loading should be carefully used since it may cause performance problems in some specific cases.

See also lazy loading document of the EF Core.

Access to the EF Core API

In most cases, you want to hide EF Core APIs behind a repository (this is the main purpose of the repository pattern). However, if you want to access the DbContext instance over the repository, you can use GetDbContext() or GetDbSet() extension methods. Example:

public async Task TestAsync()
{
    var dbContext = await _orderRepository.GetDbContextAsync();
    var dbSet = await _orderRepository.GetDbSetAsync();
    //var dbSet = dbContext.Set<Order>(); //Alternative, when you have the DbContext
}
  • GetDbContextAsync returns a DbContext reference instead of BookStoreDbContext. You can cast it if you need. However, you don't need it in most cases.

Important: You must reference to the Volo.Abp.EntityFrameworkCore package from the project you want to access to the DbContext. This breaks encapsulation, but this is what you want in that case.

Extra Properties & Object Extension Manager

Extra Properties system allows you to set/get dynamic properties to entities those implement the IHasExtraProperties interface. It is especially useful when you want to add custom properties to the entities defined in an application module, when you use the module as package reference.

By default, all the extra properties of an entity are stored as a single JSON object in the database.

Entity extension system allows you to to store desired extra properties in separate fields in the related database table. For more information about the extra properties & the entity extension system, see the following documents:

This section only explains the EF Core related usage of the ObjectExtensionManager.

ObjectExtensionManager.Instance

ObjectExtensionManager implements the singleton pattern, so you need to use the static ObjectExtensionManager.Instance to perform all the operations.

MapEfCoreProperty

MapEfCoreProperty is a shortcut extension method to define an extension property for an entity and map to the database.

Example: Add Title property (database field) to the IdentityRole entity:

ObjectExtensionManager.Instance
    .MapEfCoreProperty<IdentityRole, string>(
        "Title",
        (entityBuilder, propertyBuilder) =>
        {
            propertyBuilder.HasMaxLength(64);
        }
    );

MapEfCoreEntity

MapEfCoreEntity is a shortcut extension method to configure the Entity.

Example: Set the max length of Name to the IdentityRole entity:

ObjectExtensionManager.Instance
    .MapEfCoreEntity<IdentityRole>(builder =>
    {
        builder.As<EntityTypeBuilder<IdentityRole>>().Property(x => x.Name).HasMaxLength(200);
    });

MapEfCoreDbContext

MapEfCoreDbContext is a shortcut extension method to configure the DbContext.

Example: Set the max length of Name to the IdentityRole entity of IdentityDbContext:

ObjectExtensionManager.Instance.MapEfCoreDbContext<IdentityDbContext>(b =>
{
    b.Entity<IdentityRole>().Property(x => x.Name).HasMaxLength(200);
});

If the related module has implemented this feature(explained below), then the new property is added to the model or the DbContext/Entity configure changed. Then you need to run the standard Add-Migration and Update-Database commands to update your database to add the new field.

The MapEfCoreProperty, MapEfCoreEntity and MapEfCoreDbContext methods must be called before using the related DbContext. It is a static method. The best way is to use it in your application as earlier as possible. The application startup template has a YourProjectNameEfCoreEntityExtensionMappings class that is safe to use this method inside.

ConfigureEfCoreEntity, ApplyObjectExtensionMappings and TryConfigureObjectExtensions

If you are building a reusable module and want to allow application developers to add properties to your entities, you can use the ConfigureEfCoreEntity, ApplyObjectExtensionMappings and TryConfigureObjectExtensions extension methods in your entity mapping.

Example:

public static class QADbContextModelCreatingExtensions
{
    public static void ConfigureQA(
        this ModelBuilder builder,
        Action<QAModelBuilderConfigurationOptions> optionsAction = null)
    {
        Check.NotNull(builder, nameof(builder));

        var options = new QAModelBuilderConfigurationOptions(
            QADatabaseDbProperties.DbTablePrefix,
            QADatabaseDbProperties.DbSchema
        );

        optionsAction?.Invoke(options);

        builder.Entity<QA_Question>(b =>
        {
            b.ToTable(options.TablePrefix + "Questions", options.Schema);
            b.ConfigureByConvention();
            //...

            //Call this in the end of buildAction.
            b.ApplyObjectExtensionMappings();
        });

        //...

        //Call this in the end of ConfigureQA.
        builder.TryConfigureObjectExtensions<QADbContext>();
    }
}

If you call ConfigureByConvention() extension method (like b.ConfigureByConvention() for this example), ABP Framework internally calls the ConfigureObjectExtensions and ConfigureEfCoreEntity methods. It is a best practice to use the ConfigureByConvention() method since it also configures database mapping for base properties by convention.

See the "ConfigureByConvention Method" section above for more information.

Advanced Topics

Controlling the Multi-Tenancy

If your solution is multi-tenant, tenants may have separate databases, you have multiple DbContext classes in your solution and some of your DbContext classes should be usable only from the host side, it is suggested to add [IgnoreMultiTenancy] attribute on your DbContext class. In this case, ABP guarantees that the related DbContext always uses the host connection string, even if you are in a tenant context.

Example:

[IgnoreMultiTenancy]
public class MyDbContext : AbpDbContext<MyDbContext>
{
    ...
}

Do not use the [IgnoreMultiTenancy] attribute if any one of your entities in your DbContext can be persisted in a tenant database.

When you use repositories, ABP already uses the host database for the entities don't implement the IMultiTenant interface. So, most of time you don't need to [IgnoreMultiTenancy] attribute if you are using the repositories to work with the database.

Set Default Repository Classes

Default generic repositories are implemented by EfCoreRepository class by default. You can create your own implementation and use it for all the default repository implementations.

First, define your default repository classes like that:

public class MyRepositoryBase<TEntity>
    : EfCoreRepository<BookStoreDbContext, TEntity>
      where TEntity : class, IEntity
{
    public MyRepositoryBase(IDbContextProvider<BookStoreDbContext> dbContextProvider)
        : base(dbContextProvider)
    {
    }
}

public class MyRepositoryBase<TEntity, TKey>
    : EfCoreRepository<BookStoreDbContext, TEntity, TKey>
    where TEntity : class, IEntity<TKey>
{
    public MyRepositoryBase(IDbContextProvider<BookStoreDbContext> dbContextProvider)
        : base(dbContextProvider)
    {
    }
}

First one is for entities with composite keys, second one is for entities with single primary key.

It's suggested to inherit from the EfCoreRepository class and override methods if needed. Otherwise, you will have to implement all the standard repository methods manually.

Now, you can use SetDefaultRepositoryClasses option:

context.Services.AddAbpDbContext<BookStoreDbContext>(options =>
{
    options.SetDefaultRepositoryClasses(
        typeof(MyRepositoryBase<,>),
        typeof(MyRepositoryBase<>)
    );

    //...
});

Set Base DbContext Class or Interface for Default Repositories

If your DbContext inherits from another DbContext or implements an interface, you can use that base class or interface as DbContext for default repositories. Example:

public interface IBookStoreDbContext : IEfCoreDbContext
{
    DbSet<Book> Books { get; }
}

IBookStoreDbContext is implemented by the BookStoreDbContext class. Then you can use generic overload of the AddDefaultRepositories:

context.Services.AddAbpDbContext<BookStoreDbContext>(options =>
{
    options.AddDefaultRepositories<IBookStoreDbContext>();
    //...
});

Now, your custom BookRepository can also use the IBookStoreDbContext interface:

public class BookRepository : EfCoreRepository<IBookStoreDbContext, Book, Guid>, IBookRepository
{
    //...
}

One advantage of using an interface for a DbContext is then it will be replaceable by another implementation.

Replace Other DbContextes

Once you properly define and use an interface for DbContext, then any other implementation can use the following ways to replace it:

ReplaceDbContextAttribute

[ReplaceDbContext(typeof(IBookStoreDbContext))]
public class OtherDbContext : AbpDbContext<OtherDbContext>, IBookStoreDbContext
{
    //...
}

ReplaceDbContext option

context.Services.AddAbpDbContext<OtherDbContext>(options =>
{
    //...
    options.ReplaceDbContext<IBookStoreDbContext>();
});

In this example, OtherDbContext implements IBookStoreDbContext. This feature allows you to have multiple DbContext (one per module) on development, but single DbContext (implements all interfaces of all DbContexts) on runtime.

Split Queries

ABP enables split queries globally by default for better performance. You can change it as needed.

Example

Configure<AbpDbContextOptions>(options =>
{
    options.UseSqlServer(optionsBuilder =>
    {
        optionsBuilder.UseQuerySplittingBehavior(QuerySplittingBehavior.SingleQuery);
    });
});

Customize Bulk Operations

If you have better logic or using an external library for bulk operations, you can override the logic via implementingIEfCoreBulkOperationProvider.

  • You may use example template below:
public class MyCustomEfCoreBulkOperationProvider
    : IEfCoreBulkOperationProvider, ITransientDependency
{
    public async Task DeleteManyAsync<TDbContext, TEntity>(
        IEfCoreRepository<TEntity> repository,
        IEnumerable<TEntity> entities,
        bool autoSave,
        CancellationToken cancellationToken)
        where TDbContext : IEfCoreDbContext
        where TEntity : class, IEntity
    {
        // Your logic here.
    }

    public async Task InsertManyAsync<TDbContext, TEntity>(
        IEfCoreRepository<TEntity> repository,
        IEnumerable<TEntity> entities,
        bool autoSave,
        CancellationToken cancellationToken)
        where TDbContext : IEfCoreDbContext
        where TEntity : class, IEntity
    {
        // Your logic here.
    }

    public async Task UpdateManyAsync<TDbContext, TEntity>(
        IEfCoreRepository<TEntity> repository,
        IEnumerable<TEntity> entities,
        bool autoSave,
        CancellationToken cancellationToken)
        where TDbContext : IEfCoreDbContext
        where TEntity : class, IEntity
    {
        // Your logic here.
    }
}

See Also

Was this page helpful?

Please make a selection.

To help us improve, please share your reason for the negative feedback in the field below.

Please enter a note.

Thank you for your valuable feedback!

Please note that although we cannot respond to feedback, our team will use your comments to improve the experience.

In this document
Community Talks

Building Modular Monolith Applications Using .NET and ABP Framework

17 Oct, 17:00
Online
Register Now
Mastering ABP Framework Book
Mastering ABP Framework

This book will help you gain a complete understanding of the framework and modern web application development techniques.

Learn More