mirror of https://github.com/abpframework/abp.git
committed by
GitHub
243 changed files with 8571 additions and 2340 deletions
@ -0,0 +1,277 @@ |
|||
# Repository Pattern in the ASP.NET Core |
|||
|
|||
If you’ve built a .NET app with a database, you’ve likely used Entity Framework, Dapper, or ADO.NET. They’re useful tools; still, when they live inside your business logic or controllers, the code can become harder to keep tidy and to test. |
|||
|
|||
That’s where the **Repository Pattern** comes in. |
|||
|
|||
At its core, the Repository Pattern acts as a **middle layer between your domain and data access logic**. It abstracts the way you store and retrieve data, giving your application a clean separation of concerns: |
|||
|
|||
* **Separation of Concerns:** Business logic doesn’t depend on the database. |
|||
* **Easier Testing:** You can replace the repository with a fake or mock during unit tests. |
|||
* **Flexibility:** You can switch data sources (e.g., from SQL to MongoDB) without touching business logic. |
|||
|
|||
Let’s see how this works with a simple example. |
|||
|
|||
## A Simple Example with Product Repository |
|||
|
|||
Imagine we’re building a small e-commerce app. We’ll start by defining a repository interface for managing products. |
|||
|
|||
You can find the complete sample code in this GitHub repository: |
|||
|
|||
https://github.com/m-aliozkaya/RepositoryPattern |
|||
|
|||
### Domain model and context |
|||
|
|||
We start with a single entity and a matching `DbContext`. |
|||
|
|||
`Product.cs` |
|||
|
|||
```csharp |
|||
using System.ComponentModel.DataAnnotations; |
|||
|
|||
namespace RepositoryPattern.Web.Models; |
|||
|
|||
public class Product |
|||
{ |
|||
public int Id { get; set; } |
|||
|
|||
[Required, StringLength(64)] |
|||
public string Name { get; set; } = string.Empty; |
|||
|
|||
[Range(0, double.MaxValue)] |
|||
public decimal Price { get; set; } |
|||
|
|||
[StringLength(256)] |
|||
public string? Description { get; set; } |
|||
|
|||
public int Stock { get; set; } |
|||
} |
|||
``` |
|||
|
|||
`"AppDbContext.cs` |
|||
|
|||
```csharp |
|||
using Microsoft.EntityFrameworkCore; |
|||
using RepositoryPattern.Web.Models; |
|||
|
|||
namespace RepositoryPattern.Web.Data; |
|||
|
|||
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options) |
|||
{ |
|||
public DbSet<Product> Products => Set<Product>(); |
|||
} |
|||
``` |
|||
|
|||
### Generic repository contract and base class |
|||
|
|||
All entities share the same CRUD needs, so we define a generic interface and an EF Core implementation. |
|||
|
|||
`Repositories/IRepository.cs` |
|||
|
|||
```csharp |
|||
using System.Linq.Expressions; |
|||
|
|||
namespace RepositoryPattern.Web.Repositories; |
|||
|
|||
public interface IRepository<TEntity> where TEntity : class |
|||
{ |
|||
Task<TEntity?> GetByIdAsync(int id, CancellationToken cancellationToken = default); |
|||
Task<List<TEntity>> GetAllAsync(CancellationToken cancellationToken = default); |
|||
Task<List<TEntity>> GetListAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default); |
|||
Task AddAsync(TEntity entity, CancellationToken cancellationToken = default); |
|||
Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default); |
|||
Task DeleteAsync(int id, CancellationToken cancellationToken = default); |
|||
} |
|||
``` |
|||
|
|||
`Repositories/EfRepository.cs` |
|||
|
|||
```csharp |
|||
using Microsoft.EntityFrameworkCore; |
|||
using RepositoryPattern.Web.Data; |
|||
|
|||
namespace RepositoryPattern.Web.Repositories; |
|||
|
|||
public class EfRepository<TEntity>(AppDbContext context) : IRepository<TEntity> |
|||
where TEntity : class |
|||
{ |
|||
protected readonly AppDbContext Context = context; |
|||
|
|||
public virtual async Task<TEntity?> GetByIdAsync(int id, CancellationToken cancellationToken = default) |
|||
=> await Context.Set<TEntity>().FindAsync([id], cancellationToken); |
|||
|
|||
public virtual async Task<List<TEntity>> GetAllAsync(CancellationToken cancellationToken = default) |
|||
=> await Context.Set<TEntity>().AsNoTracking().ToListAsync(cancellationToken); |
|||
|
|||
public virtual async Task<List<TEntity>> GetListAsync( |
|||
System.Linq.Expressions.Expression<Func<TEntity, bool>> predicate, |
|||
CancellationToken cancellationToken = default) |
|||
=> await Context.Set<TEntity>() |
|||
.AsNoTracking() |
|||
.Where(predicate) |
|||
.ToListAsync(cancellationToken); |
|||
|
|||
public virtual async Task AddAsync(TEntity entity, CancellationToken cancellationToken = default) |
|||
{ |
|||
await Context.Set<TEntity>().AddAsync(entity, cancellationToken); |
|||
await Context.SaveChangesAsync(cancellationToken); |
|||
} |
|||
|
|||
public virtual async Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default) |
|||
{ |
|||
Context.Set<TEntity>().Update(entity); |
|||
await Context.SaveChangesAsync(cancellationToken); |
|||
} |
|||
|
|||
public virtual async Task DeleteAsync(int id, CancellationToken cancellationToken = default) |
|||
{ |
|||
var entity = await GetByIdAsync(id, cancellationToken); |
|||
if (entity is null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
Context.Set<TEntity>().Remove(entity); |
|||
await Context.SaveChangesAsync(cancellationToken); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Reads use `AsNoTracking()` to avoid tracking overhead, while write methods call `SaveChangesAsync` to keep the sample straightforward. |
|||
|
|||
### Product-specific repository |
|||
|
|||
Products need one extra query: list the items that are almost out of stock. We extend the generic repository with a dedicated interface and implementation. |
|||
|
|||
`Repositories/IProductRepository.cs` |
|||
|
|||
```csharp |
|||
using RepositoryPattern.Web.Models; |
|||
|
|||
namespace RepositoryPattern.Web.Repositories; |
|||
|
|||
public interface IProductRepository : IRepository<Product> |
|||
{ |
|||
Task<List<Product>> GetLowStockProductsAsync(int threshold, CancellationToken cancellationToken = default); |
|||
} |
|||
``` |
|||
|
|||
`Repositories/ProductRepository.cs` |
|||
|
|||
```csharp |
|||
using Microsoft.EntityFrameworkCore; |
|||
using RepositoryPattern.Web.Data; |
|||
using RepositoryPattern.Web.Models; |
|||
|
|||
namespace RepositoryPattern.Web.Repositories; |
|||
|
|||
public class ProductRepository(AppDbContext context) : EfRepository<Product>(context), IProductRepository |
|||
{ |
|||
public Task<List<Product>> GetLowStockProductsAsync(int threshold, CancellationToken cancellationToken = default) => |
|||
Context.Products |
|||
.AsNoTracking() |
|||
.Where(product => product.Stock <= threshold) |
|||
.OrderBy(product => product.Stock) |
|||
.ToListAsync(cancellationToken); |
|||
} |
|||
``` |
|||
|
|||
### 🧩 A Note on Unit of Work |
|||
|
|||
The Repository Pattern is often used together with the **Unit of Work** pattern to manage transactions efficiently. |
|||
|
|||
> 💡 *If you want to dive deeper into the Unit of Work pattern, check out our separate blog post dedicated to that topic. https://abp.io/community/articles/lv4v2tyf |
|||
|
|||
### Service layer and controller |
|||
|
|||
Controllers depend on a service, and the service depends on the repository. That keeps HTTP logic and data logic separate. |
|||
|
|||
`Services/ProductService.cs` |
|||
|
|||
```csharp |
|||
using RepositoryPattern.Web.Models; |
|||
using RepositoryPattern.Web.Repositories; |
|||
|
|||
namespace RepositoryPattern.Web.Services; |
|||
|
|||
public class ProductService(IProductRepository productRepository) |
|||
{ |
|||
private readonly IProductRepository _productRepository = productRepository; |
|||
|
|||
public Task<List<Product>> GetProductsAsync(CancellationToken cancellationToken = default) => |
|||
_productRepository.GetAllAsync(cancellationToken); |
|||
|
|||
public Task<List<Product>> GetLowStockAsync(int threshold, CancellationToken cancellationToken = default) => |
|||
_productRepository.GetLowStockProductsAsync(threshold, cancellationToken); |
|||
|
|||
public Task<Product?> GetByIdAsync(int id, CancellationToken cancellationToken = default) => |
|||
_productRepository.GetByIdAsync(id, cancellationToken); |
|||
|
|||
public Task CreateAsync(Product product, CancellationToken cancellationToken = default) => |
|||
_productRepository.AddAsync(product, cancellationToken); |
|||
|
|||
public Task UpdateAsync(Product product, CancellationToken cancellationToken = default) => |
|||
_productRepository.UpdateAsync(product, cancellationToken); |
|||
|
|||
public Task DeleteAsync(int id, CancellationToken cancellationToken = default) => |
|||
_productRepository.DeleteAsync(id, cancellationToken); |
|||
} |
|||
``` |
|||
|
|||
`Controllers/ProductsController.cs` |
|||
|
|||
```csharp |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using RepositoryPattern.Web.Models; |
|||
using RepositoryPattern.Web.Services; |
|||
|
|||
namespace RepositoryPattern.Web.Controllers; |
|||
|
|||
public class ProductsController(ProductService productService) : Controller |
|||
{ |
|||
private readonly ProductService _productService = productService; |
|||
|
|||
public async Task<IActionResult> Index(CancellationToken cancellationToken) |
|||
{ |
|||
const int lowStockThreshold = 5; |
|||
var products = await _productService.GetProductsAsync(cancellationToken); |
|||
var lowStock = await _productService.GetLowStockAsync(lowStockThreshold, cancellationToken); |
|||
|
|||
return View(new ProductListViewModel(products, lowStock, lowStockThreshold)); |
|||
} |
|||
|
|||
// remaining CRUD actions call through ProductService in the same way |
|||
} |
|||
``` |
|||
|
|||
The controller never reaches for `AppDbContext`. Every operation travels through the service, which keeps tests simple and makes future refactors easier. |
|||
|
|||
### Dependency registration and seeding |
|||
|
|||
The last step is wiring everything up in `Program.cs`. |
|||
|
|||
```csharp |
|||
builder.Services.AddDbContext<AppDbContext>(options => |
|||
options.UseInMemoryDatabase("ProductsDb")); |
|||
builder.Services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>)); |
|||
builder.Services.AddScoped<IProductRepository, ProductRepository>(); |
|||
builder.Services.AddScoped<ProductService>(); |
|||
``` |
|||
|
|||
The sample also seeds three products so the list page shows data on first run. |
|||
|
|||
Run the site with: |
|||
|
|||
```powershell |
|||
dotnet run --project RepositoryPattern.Web |
|||
``` |
|||
|
|||
## How ABP approaches the same idea |
|||
|
|||
ABP includes generic repositories by default (`IRepository<TEntity, TKey>`), so you often skip writing the implementation layer shown above. You inject the interface into an application service, call methods like `InsertAsync` or `CountAsync`, and ABP’s Unit of Work handles the transaction. When you need custom queries, you can still derive from `EfCoreRepository<TEntity, TKey>` and add them. |
|||
|
|||
For more details, check out the official ABP documentation on repositories: https://abp.io/docs/latest/framework/architecture/domain-driven-design/repositories |
|||
|
|||
### Closing note |
|||
|
|||
This setup keeps data access tidy without being heavy. Start with the generic repository, add small extensions per entity, pass everything through services, and register the dependencies once. Whether you hand-code it or let ABP supply the repository, the structure stays the same and your controllers remain clean. |
|||
|
After Width: | Height: | Size: 504 KiB |
@ -0,0 +1,98 @@ |
|||
# **Return Code vs Exceptions: Which One is Better?** |
|||
|
|||
Alright, so this debate pops up every few months on dev subreddits and forums |
|||
|
|||
> *Should you use return codes or exceptions for error handling?* |
|||
|
|||
And honestly, there’s no %100 right answer here! Both have pros/cons, and depending on the language or context, one might make more sense than the other. Let’s see... |
|||
|
|||
------ |
|||
|
|||
## 1. Return Codes --- Said to be "Old School Way" --- |
|||
|
|||
Return codes (like `0` for success, `-1` for failure, etc.) are the OG method. You mostly see them everywhere in C and C++. |
|||
They’re super explicit, the function literally *returns* the result of the operation. |
|||
|
|||
### ➕ Advantages of returning codes: |
|||
|
|||
- You *always* know when something went wrong |
|||
- No hidden control flow — what you see is what you get |
|||
- Usually faster (no stack unwinding, no exception overhead) |
|||
- Easy to use in systems programming, embedded stuff, or performance-critical code |
|||
|
|||
### ➖ Disadvantages of returning codes: |
|||
|
|||
- It’s easy to forget to check the return value (and boom, silent failure 😬) |
|||
- Makes code noisy... Everry function call followed by `if (result != SUCCESS)` gets annoying |
|||
- No stack trace or context unless you manually build one |
|||
|
|||
**For example:** |
|||
|
|||
```csharp |
|||
try |
|||
{ |
|||
await SendEmailAsync(); |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
Log.Exception(e.ToString()); |
|||
return -1; |
|||
} |
|||
``` |
|||
|
|||
Looks fine… until you forget one of those `if` conditions somewhere. |
|||
|
|||
------ |
|||
|
|||
## 2. Exceptions --- The Fancy & Modern Way --- |
|||
|
|||
Exceptions came in later, mostly with higher-level languages like Java, C#, and Python. |
|||
The idea is that you *throw* an error and handle it *somewhere else*. |
|||
|
|||
### ➕ Advantages of throwing exceptions: |
|||
|
|||
- Cleaner code... You can focus on the happy path and handle errors separately |
|||
- Can carry detailed info (stack traces, messages, inner exceptions...) |
|||
- Easier to handle complex error propagation |
|||
|
|||
### ➖ Disadvantages of throwing exceptions: |
|||
|
|||
- Hidden control flow — you don’t always see what might throw |
|||
- Performance hit (esp. in tight loops or low-level systems) |
|||
- Overused in some codebases (“everything throws everything”) |
|||
|
|||
**Example:** |
|||
|
|||
```csharp |
|||
try |
|||
{ |
|||
await SendEmailAsync(); |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
Log.Exception(e.ToString()); |
|||
throw e; |
|||
} |
|||
``` |
|||
|
|||
Way cleaner, but if `SendEmailAsync()` is deep in your call stack and it fails, it can be tricky to know exactly what went wrong unless you log properly. |
|||
|
|||
------ |
|||
|
|||
### And Which One’s Better? ⚖️ |
|||
|
|||
Depends on what you’re building. |
|||
|
|||
- **Low-level systems, drivers, real-time stuff 👉 Return codes.** Performance and control matter more. |
|||
- **Application-level, business logic, or high-level APIs 👉 Exceptions.** Cleaner and easier to maintain. |
|||
|
|||
And honestly, mixing both sometimes makes sense. |
|||
For example, you can use return codes internally and exceptions at the boundary of your API to surface meaningful errors to the user. |
|||
|
|||
------ |
|||
|
|||
### Conclusion |
|||
|
|||
Return codes = simple, explicit, but messy.t |
|||
Exceptions = clean, powerful, but can bite you. |
|||
Use what fits your project and your team’s sanity level 😅. |
|||
File diff suppressed because it is too large
@ -1,16 +1,4 @@ |
|||
$(function () { |
|||
|
|||
let l = abp.localization.getResource("CmsKit"); |
|||
|
|||
$('#deleteBlogPost').on('click', '', function (e) { |
|||
abp.message.confirm(l("DeleteBlogPostMessage"), function (ok) { |
|||
if (ok) { |
|||
volo.cmsKit.public.blogs.blogPostPublic.delete( |
|||
$('#BlogId').val() |
|||
).then(function () { |
|||
document.location.href = "/"; |
|||
}); |
|||
} |
|||
}) |
|||
}); |
|||
}); |
|||
|
|||
File diff suppressed because it is too large
@ -0,0 +1,40 @@ |
|||
using Microsoft.EntityFrameworkCore.Migrations; |
|||
|
|||
#nullable disable |
|||
|
|||
namespace Migrations |
|||
{ |
|||
/// <inheritdoc />
|
|||
public partial class ABP10 : Migration |
|||
{ |
|||
/// <inheritdoc />
|
|||
protected override void Up(MigrationBuilder migrationBuilder) |
|||
{ |
|||
migrationBuilder.AlterColumn<string>( |
|||
name: "DeviceInfo", |
|||
table: "AbpSessions", |
|||
type: "nvarchar(256)", |
|||
maxLength: 256, |
|||
nullable: true, |
|||
oldClrType: typeof(string), |
|||
oldType: "nvarchar(64)", |
|||
oldMaxLength: 64, |
|||
oldNullable: true); |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
protected override void Down(MigrationBuilder migrationBuilder) |
|||
{ |
|||
migrationBuilder.AlterColumn<string>( |
|||
name: "DeviceInfo", |
|||
table: "AbpSessions", |
|||
type: "nvarchar(64)", |
|||
maxLength: 64, |
|||
nullable: true, |
|||
oldClrType: typeof(string), |
|||
oldType: "nvarchar(256)", |
|||
oldMaxLength: 256, |
|||
oldNullable: true); |
|||
} |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
@ -0,0 +1,42 @@ |
|||
using System; |
|||
using Microsoft.EntityFrameworkCore.Migrations; |
|||
|
|||
#nullable disable |
|||
|
|||
namespace Migrations |
|||
{ |
|||
/// <inheritdoc />
|
|||
public partial class ABP10_1 : Migration |
|||
{ |
|||
/// <inheritdoc />
|
|||
protected override void Up(MigrationBuilder migrationBuilder) |
|||
{ |
|||
migrationBuilder.CreateTable( |
|||
name: "AbpUserPasswordHistories", |
|||
columns: table => new |
|||
{ |
|||
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: false), |
|||
Password = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false), |
|||
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true), |
|||
CreatedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false) |
|||
}, |
|||
constraints: table => |
|||
{ |
|||
table.PrimaryKey("PK_AbpUserPasswordHistories", x => new { x.UserId, x.Password }); |
|||
table.ForeignKey( |
|||
name: "FK_AbpUserPasswordHistories_AbpUsers_UserId", |
|||
column: x => x.UserId, |
|||
principalTable: "AbpUsers", |
|||
principalColumn: "Id", |
|||
onDelete: ReferentialAction.Cascade); |
|||
}); |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
protected override void Down(MigrationBuilder migrationBuilder) |
|||
{ |
|||
migrationBuilder.DropTable( |
|||
name: "AbpUserPasswordHistories"); |
|||
} |
|||
} |
|||
} |
|||
@ -1,15 +1,21 @@ |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Application.Dtos; |
|||
using Volo.Abp.Application.Services; |
|||
using Volo.Abp.Content; |
|||
using Volo.Docs.Admin.Projects; |
|||
using Volo.Docs.Common.Documents; |
|||
|
|||
namespace Volo.Docs.Admin.Documents; |
|||
|
|||
public interface IDocumentPdfAdminAppService : IDocumentPdfAppService |
|||
public interface IDocumentPdfAdminAppService : IApplicationService |
|||
{ |
|||
Task<IRemoteStreamContent> DownloadPdfAsync(DocumentPdfGeneratorInput input); |
|||
|
|||
Task<bool> ExistsAsync(DocumentPdfGeneratorInput input); |
|||
|
|||
Task GeneratePdfAsync(DocumentPdfGeneratorInput input); |
|||
|
|||
|
|||
Task<PagedResultDto<ProjectPdfFileDto>> GetPdfFilesAsync(GetPdfFilesInput input); |
|||
|
|||
Task DeletePdfFileAsync(DeletePdfFileInput input); |
|||
} |
|||
} |
|||
|
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue