Skip to main content
Version: 1.0.1

DataAccess.EfCore

What is ORM ?

Object-relational mapping (ORM) is a programming technique in which a metadata descriptor is used to connect object code to a relational database. ORM converts data between type systems that are unable to coexist within relational databases and OOP languages. Entity Framework (EF) Core is a lightweight, extensible, open source and cross-platform version of the popular Entity Framework data access technology. EF Core supports many database engines, see Database Providers for details.

Data Access with Milva

The library uses the Repository Design Pattern and EF Core to access the database. It is essentially an abstraction above EF Core. We recommend that you review the EF Core and Repository Design Pattern topics before continuing. Because we will explain assuming you know the basics.

Features;

  • With EntityBase mentioned in Milvasoft.Core layer, you can perform auditing automatically by inheriting only your entities.
  • Although the Repository Design Pattern is used, it allows writing join operations at the service layer instead of the data layer. This way you don't create a custom repository for each entity (except in special cases).
  • It allows different people working on the same project to develop projects with exactly the same code standards and performance.
  • Automatically converts DateTime values to UTC for you to develop Timezone standalone applications.
  • It ensures that Transaction and Rollback operations are performed in a standardized way.
  • It allows you to easily perform client-side encryption with AES.

Components;

  • Milva DbContext Bases : There are several versions of MilvaDbContext and MilvaDbContextBase for using DbContext Pooling or IdentityDbContext.

  • Base Repository : It is a class that contains dozens of methods such as pagination, sorting, grouping, insertion, deletion, update, and you can send most queries to the database with these methods parametrically. These methods are written in accordance with best practices prepared for ef core. All methods have async and sync versions.

  • Context Repository : It is a class created to perform operations that are not tied to a specific entity, but directly linked to DbContext. For example, the Transaction operation is an example of this.

  • Include Library : There are some patterns in Ef Core to get related data from database. Include Library was created to use eager loading pattern as DbContext independent. Note: If you do not want to use this structure and prefer a more efficient method instead, you can send projection expressions to all get methods in BaseRepository.

Let's do some step-by-step examples with all of these components.

Usage

Create SampleUser and SampleRole class;


public class SampleUser : FullAuditableEntityWithCustomUser<SampleUser, Guid, Guid>
{
public string Username { get; set; }

[ForeignKey("Role")]
public int RoleId { get; set; }

// Navigation property for join operations.
public virtual SampleRole Role { get; set; }
}

public class SampleRole : FullAuditableEntityWithCustomUser<SampleUser, Guid, int>
{
public string Name { get; set; }

// Navigation property for join operations.
public virtual ICollection<SampleUser> Users { get; set; }
}


Create your DbContext and inherit from MilvaDbContext;


public class SampleDbContext : MilvaDbContext<SampleUser, Guid>
{
private readonly IMilvaEncryptionProvider _provider;

// Provide some constructor injection for auditing and encyrption.
public SampleDbContext(DbContextOptions<SampleDbContext> options,
IHttpContextAccessor httpContextAccessor,
IAuditConfiguration auditConfiguration,
IMilvaEncryptionProvider provider) : base(options, httpContextAccessor, auditConfiguration, useUtcForDateTimes: true)
=> _provider = provider;

public DbSet<SampleUser> Users { get; set; }

public DbSet<SampleRole> Roles { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
//Enable some features

// Encrypts [MilvaEncryptedAttribute] tagged properties when inserting or updating values, decrypts when getting values.
modelBuilder.UseAnnotationEncryption(_provider);

// It assigns an index to the IsDeleted column for entities with IsDeleted property.
modelBuilder.UseIndexToIndelibleEntities();

// It assigns an index to the CreationDate column for entities with CreationDate property.
modelBuilder.UseIndexToCreationAuditableEntities();

// Applies precision to decimal properties
modelBuilder.UsePrecision(18, 10);

base.OnModelCreating(modelBuilder);
}

}

Register to required services to service collection;


// For auto auditing. Specify which properties you want to audit.
services.AddSingleton<IAuditConfiguration>(new AuditConfiguration(AuditCreationDate: true,
AuditCreator: true,
AuditModificationDate: true,
AuditModifier: true,
AuditDeletionDate: true,
AuditDeleter: true));

// We use PostgreSql for this sample. You can use whatever you want.
services.AddDbContext<SampleDbContext>(opts =>
{
opts.UseNpgsql("yourconnectionstring");
});

Create generic repository base for Generic Repository Design Pattern and inherit from IBaseRepository;


public interface ISampleRepositoryBase<TEntity, TKey> : IBaseRepository<TEntity, TKey, SampleDbContext>
where TEntity : class, IBaseEntity<TKey>
where TKey : struct, IEquatable<TKey>
{
}

public class SampleRepositoryBase<TEntity, TKey> : BaseRepository<TEntity, TKey, SampleDbContext>, ISampleRepositoryBase<TEntity, TKey>
where TEntity : class, IBaseEntity<TKey>
where TKey : struct, IEquatable<TKey>
{
public SampleRepositoryBase(SampleDbContext dbContext) : base(dbContext)
{
}
}

Register repository bases to service collection;


services.AddScoped(typeof(ISampleRepositoryBase<,>), typeof(SampleRepositoryBase<,>));
services.AddScoped<IContextRepository<SampleDbContext>, ContextRepository<SampleDbContext>>();
services.AddHttpContextAccessor();

It's time to use the structure we created. Don't forget create migrations!


public class UserService
{
private readonly IContextRepository<SampleDbContext> _contextRepository;
private readonly ISampleRepositoryBase<SampleUser, Guid> _userRepository;
private readonly ISampleRepositoryBase<SampleRole, int> _roleRepository;

public UserService(ISampleRepositoryBase<SampleUser, Guid> userRepository,
ISampleRepositoryBase<SampleRole, int> roleRepository,
IContextRepository<SampleDbContext> contextRepository)
{
_contextRepository = contextRepository;
_userRepository = userRepository;
_roleRepository = roleRepository;
}

public async Task DoSometingAsync()
{
var role = new SampleRole
{
Id = 1,
Name = "Sample Role"
};

// Optional. If you do not assign an id, EF will automatically assign an id to the inserted record.
var firstUserId = Guid.NewGuid();

// You do not need to manually set the audit properties mentioned in the Entity Bases section. MilvaDbContext does this automatically.
var users = new List<SampleUser>
{
new SampleUser
{
Id = firstUserId,
Username = "testuser",
PhoneNumber = "+905xxxxxxxxx",
RoleId = 1,
},
new SampleUser
{
Id = Guid.NewGuid(),
Username = "testuser2",
PhoneNumber = "+905xxxxxxxxx",
RoleId = 1,
}
};

// Ensures that all operations within the work unit are completed successfully. You can use it as the equivalent of the Unit of Work concept.
// If any operation fails, no change will be reflected in the database, the rollback method we have written will work and "An error occurred!" will be printed to the console.
await _contextRepository.ApplyTransactionAsync(async () =>
{
await _roleRepository.AddAsync(role);

await _userRepository.AddRangeAsync(users);
}, () =>
{
Console.WriteLine("An error occured!");
});

// Gets first user with role. In other words, the join operation is done to the Role database table.
var firstUser = await _userRepository.GetByIdAsync(firstUserId, i => i.Include(r => r.Role));

// Output : Sample Role
var roleName = firstUser.Role.Name;

// It paginates all the records in the database with 1 record on each page and returns the records on the 2nd page. This process takes place in the database.
var (usersInSecondPage, pageCount, totalDataCount) = await _userRepository.GetAsPaginatedAsync(requestedPageNumber: 2, countOfRequestedRecordsInPage: 1);

// Output : testuser2
var someUserUsername = usersInSecondPage.FirstOrDefault().Username;

// Removes all records in database.
// Since the entities we created inherit from the "FullAuditableEntityWithCustomUser" class, the deleted data will not be completely deleted from the database, instead the "IsDeleted" property will be assigned as "true" and other relevant audit properties will be updated.
await _userRepository.RemoveAllAsync();

// Output : false
var exists = await _userRepository.ExistsAsync(firstUserId);

// You can use this method to access soft deleted entities in database.
_userRepository.GetSoftDeletedEntitiesInNextProcess(true);

// Output : true
exists = await _userRepository.ExistsAsync(firstUserId);
}
}

BaseRepository has too many methods. For all methods, please see here.

Let's look at how to use these components with DbContext Pooling;

Previous steps are same as previous sample. Create your DbContext and inherit from MilvaPooledDbContext;


public class SamplePooledDbContext : MilvaPooledDbContext<SampleUser, Guid>
{
private readonly IMilvaEncryptionProvider _provider;

public SamplePooledDbContext(DbContextOptions<SamplePooledDbContext> options) : base(options)
=> _provider = new MilvaEncryptionProvider(key : "2r5u8x/A?D(G-KaP");

public DbSet<SampleUser> Users { get; set; }

public DbSet<SampleRole> Roles { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
//Enable some features

// Encrypts [MilvaEncryptedAttribute] tagged properties when inserting or updating values, decrypts when getting values.
modelBuilder.UseAnnotationEncryption(_provider);

// It assigns an index to the IsDeleted column for entities with IsDeleted property.
modelBuilder.UseIndexToIndelibleEntities();

// It assigns an index to the CreationDate column for entities with CreationDate property.
modelBuilder.UseIndexToCreationAuditableEntities();

// Applies precision to decimal properties
modelBuilder.UsePrecision(18, 10);

base.OnModelCreating(modelBuilder);
}

}

Create DbContextFactory for DbContext creation;


public class SamplePooledDbContextScopedFactory : IDbContextFactory<SamplePooledDbContext>
{
private readonly IDbContextFactory<SamplePooledDbContext> _pooledFactory;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IAuditConfiguration _auditConfiguration;

public SamplePooledDbContextScopedFactory(IDbContextFactory<SamplePooledDbContext> pooledFactory,
IHttpContextAccessor httpContextAccessor,
IAuditConfiguration auditConfiguration)
{
_pooledFactory = pooledFactory;
_httpContextAccessor = httpContextAccessor;
_auditConfiguration = auditConfiguration;
}

public SamplePooledDbContext CreateDbContext()
{
var context = _pooledFactory.CreateDbContext();

context.HttpMethod = _httpContextAccessor?.HttpContext?.Request?.Method;

// This is important for auditing
context.UserName = _httpContextAccessor?.HttpContext?.User?.Identity?.Name;
context.UseUtcForDateTimes = false;
context.AuditConfiguration = _auditConfiguration;

return context;
}
}

Register to required services to service collection;


// For auto auditing. Specify which properties you want to audit.
services.AddSingleton<IAuditConfiguration>(new AuditConfiguration(AuditCreationDate: true,
AuditCreator: true,
AuditModificationDate: true,
AuditModifier: true,
AuditDeletionDate: true,
AuditDeleter: true));

// We use PostgreSql for this sample. You can use whatever you want.
services.AddPooledDbContextFactory<SamplePooledDbContext>((provider, options) =>
{
options.UseNpgsql("yourconnectionstring");
});

services.AddScoped<SamplePooledDbContextScopedFactory>();
services.AddScoped(sp => sp.GetRequiredService<SamplePooledDbContextScopedFactory>().CreateDbContext());

Thats it. Later steps are same as previous sample.

For other features, please see here.