.NET?6開(kāi)發(fā)TodoList應(yīng)用之實(shí)現(xiàn)Repository模式
需求
經(jīng)常寫CRUD程序的小伙伴們可能都經(jīng)歷過(guò)定義很多Repository接口,分別做對(duì)應(yīng)的實(shí)現(xiàn),依賴注入并使用的場(chǎng)景。有的時(shí)候會(huì)發(fā)現(xiàn),很多分散的XXXXRepository的邏輯都是基本一致的,于是開(kāi)始思考是否可以將這些操作抽象出去,當(dāng)然是可以的,而且被抽象出去的部分是可以不加改變地在今后的任何有此需求的項(xiàng)目中直接引入使用。
那么我們本文的需求就是:如何實(shí)現(xiàn)一個(gè)可重用的Repository模塊。
長(zhǎng)文預(yù)警,包含大量代碼。
目標(biāo)
實(shí)現(xiàn)通用Repository模式并進(jìn)行驗(yàn)證。
原理和思路
通用的基礎(chǔ)在于抽象,抽象的粒度決定了通用的程度,但是同時(shí)也決定了使用上的復(fù)雜度。對(duì)于自己的項(xiàng)目而言,抽象到什么程度最合適,需要自己去權(quán)衡,也許后面某個(gè)時(shí)候我會(huì)決定自己去實(shí)現(xiàn)一個(gè)完善的Repository庫(kù)提供出來(lái)(事實(shí)上已經(jīng)有很多人這樣做了,我們甚至可以直接下載Nuget包進(jìn)行使用,但是自己親手去實(shí)現(xiàn)的過(guò)程能讓你更好地去理解其中的原理,也理解如何開(kāi)發(fā)一個(gè)通用的類庫(kù)。)
總體思路是:在Application中定義相關(guān)的接口,在Infrastructure中實(shí)現(xiàn)基類的功能。
實(shí)現(xiàn)
通用Repository實(shí)現(xiàn)
對(duì)于要如何去設(shè)計(jì)一個(gè)通用的Repository庫(kù),實(shí)際上涉及的面非常多,尤其是在獲取數(shù)據(jù)的時(shí)候。而且根據(jù)每個(gè)人的習(xí)慣,實(shí)現(xiàn)起來(lái)的方式是有比較大的差別的,尤其是關(guān)于泛型接口到底需要提供哪些方法,每個(gè)人都有自己的理解,這里我只演示基本的思路,而且盡量保持簡(jiǎn)單,關(guān)于更復(fù)雜和更全面的實(shí)現(xiàn),GIthub上有很多已經(jīng)寫好的庫(kù)可以去學(xué)習(xí)和參考,我會(huì)列在下面:
很顯然,第一步要去做的是在Application/Common/Interfaces中增加一個(gè)IRepository<T>的定義用于適用不同類型的實(shí)體,然后在Infrastructure/Persistence/Repositories中創(chuàng)建一個(gè)基類RepositoryBase<T>實(shí)現(xiàn)這個(gè)接口,并有辦法能提供一致的對(duì)外方法簽名。
IRepository.cs
namespace TodoList.Application.Common.Interfaces;
public interface IRepository<T> where T : class
{
}
RepositoryBase.cs
using Microsoft.EntityFrameworkCore;
using TodoList.Application.Common.Interfaces;
namespace TodoList.Infrastructure.Persistence.Repositories;
public class RepositoryBase<T> : IRepository<T> where T : class
{
private readonly TodoListDbContext _dbContext;
public RepositoryBase(TodoListDbContext dbContext) => _dbContext = dbContext;
}
在動(dòng)手實(shí)際定義IRepository<T>之前,先思考一下:對(duì)數(shù)據(jù)庫(kù)的操作都會(huì)出現(xiàn)哪些情況:
新增實(shí)體(Create)
新增實(shí)體在Repository層面的邏輯很簡(jiǎn)單,傳入一個(gè)實(shí)體對(duì)象,然后保存到數(shù)據(jù)庫(kù)就可以了,沒(méi)有其他特殊的需求。
IRepository.cs
// 省略其他... // Create相關(guān)操作接口 Task<T> AddAsync(T entity, CancellationToken cancellationToken = default);
RepositoryBase.cs
// 省略其他...
public async Task<T> AddAsync(T entity, CancellationToken cancellationToken = default)
{
await _dbContext.Set<T>().AddAsync(entity, cancellationToken);
await _dbContext.SaveChangesAsync(cancellationToken);
return entity;
}
更新實(shí)體(Update)
和新增實(shí)體類似,但是更新時(shí)一般是單個(gè)實(shí)體對(duì)象去操作。
IRepository.cs
// 省略其他... // Update相關(guān)操作接口 Task UpdateAsync(T entity, CancellationToken cancellationToken = default);
RepositoryBase.cs
// 省略其他...
public async Task UpdateAsync(T entity, CancellationToken cancellationToken = default)
{
// 對(duì)于一般的更新而言,都是Attach到實(shí)體上的,只需要設(shè)置該實(shí)體的State為Modified就可以了
_dbContext.Entry(entity).State = EntityState.Modified;
await _dbContext.SaveChangesAsync(cancellationToken);
}
刪除實(shí)體(Delete)
對(duì)于刪除實(shí)體,可能會(huì)出現(xiàn)兩種情況:刪除一個(gè)實(shí)體;或者刪除一組實(shí)體。
IRepository.cs
// 省略其他... // Delete相關(guān)操作接口,這里根據(jù)key刪除對(duì)象的接口需要用到一個(gè)獲取對(duì)象的方法 ValueTask<T?> GetAsync(object key); Task DeleteAsync(object key); Task DeleteAsync(T entity, CancellationToken cancellationToken = default); Task DeleteRangeAsync(IEnumerable<T> entities, CancellationToken cancellationToken = default);
RepositoryBase.cs
// 省略其他...
public virtual ValueTask<T?> GetAsync(object key) => _dbContext.Set<T>().FindAsync(key);
public async Task DeleteAsync(object key)
{
var entity = await GetAsync(key);
if (entity is not null)
{
await DeleteAsync(entity);
}
}
public async Task DeleteAsync(T entity, CancellationToken cancellationToken = default)
{
_dbContext.Set<T>().Remove(entity);
await _dbContext.SaveChangesAsync(cancellationToken);
}
public async Task DeleteRangeAsync(IEnumerable<T> entities, CancellationToken cancellationToken = default)
{
_dbContext.Set<T>().RemoveRange(entities);
await _dbContext.SaveChangesAsync(cancellationToken);
}
獲取實(shí)體(Retrieve)
對(duì)于如何獲取實(shí)體,是最復(fù)雜的一部分。我們不僅要考慮通過(guò)什么方式獲取哪些數(shù)據(jù),還需要考慮獲取的數(shù)據(jù)有沒(méi)有特殊的要求比如排序、分頁(yè)、數(shù)據(jù)對(duì)象類型的轉(zhuǎn)換之類的問(wèn)題。
具體來(lái)說(shuō),比如下面這一個(gè)典型的LINQ查詢語(yǔ)句:
var results = await _context.A.Join(_context.B, a => a.Id, b => b.aId, (a, b) => new
{
// ...
})
.Where(ab => ab.Name == "name" && ab.Date == DateTime.Now)
.Select(ab => new
{
// ...
})
.OrderBy(o => o.Date)
.Skip(20 * 1)
.Take(20)
.ToListAsync();
可以將整個(gè)查詢結(jié)構(gòu)分割成以下幾個(gè)組成部分,而且每個(gè)部分基本都是以lambda表達(dá)式的方式表示的,這轉(zhuǎn)化成建模的話,可以使用Expression相關(guān)的對(duì)象來(lái)表示:
1.查詢數(shù)據(jù)集準(zhǔn)備過(guò)程,在這個(gè)過(guò)程中可能會(huì)出現(xiàn)Include/Join/GroupJoin/GroupBy等等類似的關(guān)鍵字,它們的作用是構(gòu)建一個(gè)用于接下來(lái)將要進(jìn)行查詢的數(shù)據(jù)集。
2.Where子句,用于過(guò)濾查詢集合。
3.Select子句,用于轉(zhuǎn)換原始數(shù)據(jù)類型到我們想要的結(jié)果類型。
4.Order子句,用于對(duì)結(jié)果集進(jìn)行排序,這里可能會(huì)包含類似:OrderBy/OrderByDescending/ThenBy/ThenByDescending等關(guān)鍵字。
5.Paging子句,用于對(duì)結(jié)果集進(jìn)行后端分頁(yè)返回,一般都是Skip/Take一起使用。
6.其他子句,多數(shù)是條件控制,比如AsNoTracking/SplitQuery等等。
為了保持我們的演示不會(huì)過(guò)于復(fù)雜,我會(huì)做一些取舍。在這里的實(shí)現(xiàn)我參考了Edi.Wang的Moonglade中的相關(guān)實(shí)現(xiàn)。有興趣的小伙伴也可以去找一下一個(gè)更完整的實(shí)現(xiàn):Ardalis.Specification。
首先來(lái)定義一個(gè)簡(jiǎn)單的ISpecification來(lái)表示查詢的各類條件:
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore.Query;
namespace TodoList.Application.Common.Interfaces;
public interface ISpecification<T>
{
// 查詢條件子句
Expression<Func<T, bool>> Criteria { get; }
// Include子句
Func<IQueryable<T>, IIncludableQueryable<T, object>> Include { get; }
// OrderBy子句
Expression<Func<T, object>> OrderBy { get; }
// OrderByDescending子句
Expression<Func<T, object>> OrderByDescending { get; }
// 分頁(yè)相關(guān)屬性
int Take { get; }
int Skip { get; }
bool IsPagingEnabled { get; }
}
并實(shí)現(xiàn)這個(gè)泛型接口,放在Application/Common中:
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore.Query;
using TodoList.Application.Common.Interfaces;
namespace TodoList.Application.Common;
public abstract class SpecificationBase<T> : ISpecification<T>
{
protected SpecificationBase() { }
protected SpecificationBase(Expression<Func<T, bool>> criteria) => Criteria = criteria;
public Expression<Func<T, bool>> Criteria { get; private set; }
public Func<IQueryable<T>, IIncludableQueryable<T, object>> Include { get; private set; }
public List<string> IncludeStrings { get; } = new();
public Expression<Func<T, object>> OrderBy { get; private set; }
public Expression<Func<T, object>> OrderByDescending { get; private set; }
public int Take { get; private set; }
public int Skip { get; private set; }
public bool IsPagingEnabled { get; private set; }
public void AddCriteria(Expression<Func<T, bool>> criteria) => Criteria = Criteria is not null ? Criteria.AndAlso(criteria) : criteria;
protected virtual void AddInclude(Func<IQueryable<T>, IIncludableQueryable<T, object>> includeExpression) => Include = includeExpression;
protected virtual void AddInclude(string includeString) => IncludeStrings.Add(includeString);
protected virtual void ApplyPaging(int skip, int take)
{
Skip = skip;
Take = take;
IsPagingEnabled = true;
}
protected virtual void ApplyOrderBy(Expression<Func<T, object>> orderByExpression) => OrderBy = orderByExpression;
protected virtual void ApplyOrderByDescending(Expression<Func<T, object>> orderByDescendingExpression) => OrderByDescending = orderByDescendingExpression;
}
// https://stackoverflow.com/questions/457316/combining-two-expressions-expressionfunct-bool
public static class ExpressionExtensions
{
public static Expression<Func<T, bool>> AndAlso<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2)
{
var parameter = Expression.Parameter(typeof(T));
var leftVisitor = new ReplaceExpressionVisitor(expr1.Parameters[0], parameter);
var left = leftVisitor.Visit(expr1.Body);
var rightVisitor = new ReplaceExpressionVisitor(expr2.Parameters[0], parameter);
var right = rightVisitor.Visit(expr2.Body);
return Expression.Lambda<Func<T, bool>>(
Expression.AndAlso(left ?? throw new InvalidOperationException(),
right ?? throw new InvalidOperationException()), parameter);
}
private class ReplaceExpressionVisitor : ExpressionVisitor
{
private readonly Expression _oldValue;
private readonly Expression _newValue;
public ReplaceExpressionVisitor(Expression oldValue, Expression newValue)
{
_oldValue = oldValue;
_newValue = newValue;
}
public override Expression Visit(Expression node) => node == _oldValue ? _newValue : base.Visit(node);
}
}
為了在RepositoryBase中能夠把所有的Spcification串起來(lái)形成查詢子句,我們還需要定義一個(gè)用于組織Specification的SpecificationEvaluator類:
using TodoList.Application.Common.Interfaces;
namespace TodoList.Application.Common;
public class SpecificationEvaluator<T> where T : class
{
public static IQueryable<T> GetQuery(IQueryable<T> inputQuery, ISpecification<T>? specification)
{
var query = inputQuery;
if (specification?.Criteria is not null)
{
query = query.Where(specification.Criteria);
}
if (specification?.Include is not null)
{
query = specification.Include(query);
}
if (specification?.OrderBy is not null)
{
query = query.OrderBy(specification.OrderBy);
}
else if (specification?.OrderByDescending is not null)
{
query = query.OrderByDescending(specification.OrderByDescending);
}
if (specification?.IsPagingEnabled != false)
{
query = query.Skip(specification!.Skip).Take(specification.Take);
}
return query;
}
}
在IRepository中添加查詢相關(guān)的接口,大致可以分為以下這幾類接口,每類中又可能存在同步接口和異步接口:
IRepository.cs
// 省略其他... // 1. 查詢基礎(chǔ)操作接口 IQueryable<T> GetAsQueryable(); IQueryable<T> GetAsQueryable(ISpecification<T> spec); // 2. 查詢數(shù)量相關(guān)接口 int Count(ISpecification<T>? spec = null); int Count(Expression<Func<T, bool>> condition); Task<int> CountAsync(ISpecification<T>? spec); // 3. 查詢存在性相關(guān)接口 bool Any(ISpecification<T>? spec); bool Any(Expression<Func<T, bool>>? condition = null); // 4. 根據(jù)條件獲取原始實(shí)體類型數(shù)據(jù)相關(guān)接口 Task<T?> GetAsync(Expression<Func<T, bool>> condition); Task<IReadOnlyList<T>> GetAsync(); Task<IReadOnlyList<T>> GetAsync(ISpecification<T>? spec); // 5. 根據(jù)條件獲取映射實(shí)體類型數(shù)據(jù)相關(guān)接口,涉及到Group相關(guān)操作也在其中,使用selector來(lái)傳入映射的表達(dá)式 TResult? SelectFirstOrDefault<TResult>(ISpecification<T>? spec, Expression<Func<T, TResult>> selector); Task<TResult?> SelectFirstOrDefaultAsync<TResult>(ISpecification<T>? spec, Expression<Func<T, TResult>> selector); Task<IReadOnlyList<TResult>> SelectAsync<TResult>(Expression<Func<T, TResult>> selector); Task<IReadOnlyList<TResult>> SelectAsync<TResult>(ISpecification<T>? spec, Expression<Func<T, TResult>> selector); Task<IReadOnlyList<TResult>> SelectAsync<TGroup, TResult>(Expression<Func<T, TGroup>> groupExpression, Expression<Func<IGrouping<TGroup, T>, TResult>> selector, ISpecification<T>? spec = null);
有了這些基礎(chǔ),我們就可以去Infrastructure/Persistence/Repositories中實(shí)現(xiàn)RepositoryBase類剩下的關(guān)于查詢部分的代碼了:
RepositoryBase.cs
// 省略其他...
// 1. 查詢基礎(chǔ)操作接口實(shí)現(xiàn)
public IQueryable<T> GetAsQueryable()
=> _dbContext.Set<T>();
public IQueryable<T> GetAsQueryable(ISpecification<T> spec)
=> ApplySpecification(spec);
// 2. 查詢數(shù)量相關(guān)接口實(shí)現(xiàn)
public int Count(Expression<Func<T, bool>> condition)
=> _dbContext.Set<T>().Count(condition);
public int Count(ISpecification<T>? spec = null)
=> null != spec ? ApplySpecification(spec).Count() : _dbContext.Set<T>().Count();
public Task<int> CountAsync(ISpecification<T>? spec)
=> ApplySpecification(spec).CountAsync();
// 3. 查詢存在性相關(guān)接口實(shí)現(xiàn)
public bool Any(ISpecification<T>? spec)
=> ApplySpecification(spec).Any();
public bool Any(Expression<Func<T, bool>>? condition = null)
=> null != condition ? _dbContext.Set<T>().Any(condition) : _dbContext.Set<T>().Any();
// 4. 根據(jù)條件獲取原始實(shí)體類型數(shù)據(jù)相關(guān)接口實(shí)現(xiàn)
public async Task<T?> GetAsync(Expression<Func<T, bool>> condition)
=> await _dbContext.Set<T>().FirstOrDefaultAsync(condition);
public async Task<IReadOnlyList<T>> GetAsync()
=> await _dbContext.Set<T>().AsNoTracking().ToListAsync();
public async Task<IReadOnlyList<T>> GetAsync(ISpecification<T>? spec)
=> await ApplySpecification(spec).AsNoTracking().ToListAsync();
// 5. 根據(jù)條件獲取映射實(shí)體類型數(shù)據(jù)相關(guān)接口實(shí)現(xiàn)
public TResult? SelectFirstOrDefault<TResult>(ISpecification<T>? spec, Expression<Func<T, TResult>> selector)
=> ApplySpecification(spec).AsNoTracking().Select(selector).FirstOrDefault();
public Task<TResult?> SelectFirstOrDefaultAsync<TResult>(ISpecification<T>? spec, Expression<Func<T, TResult>> selector)
=> ApplySpecification(spec).AsNoTracking().Select(selector).FirstOrDefaultAsync();
public async Task<IReadOnlyList<TResult>> SelectAsync<TResult>(Expression<Func<T, TResult>> selector)
=> await _dbContext.Set<T>().AsNoTracking().Select(selector).ToListAsync();
public async Task<IReadOnlyList<TResult>> SelectAsync<TResult>(ISpecification<T>? spec, Expression<Func<T, TResult>> selector)
=> await ApplySpecification(spec).AsNoTracking().Select(selector).ToListAsync();
public async Task<IReadOnlyList<TResult>> SelectAsync<TGroup, TResult>(
Expression<Func<T, TGroup>> groupExpression,
Expression<Func<IGrouping<TGroup, T>, TResult>> selector,
ISpecification<T>? spec = null)
=> null != spec ?
await ApplySpecification(spec).AsNoTracking().GroupBy(groupExpression).Select(selector).ToListAsync() :
await _dbContext.Set<T>().AsNoTracking().GroupBy(groupExpression).Select(selector).ToListAsync();
// 用于拼接所有Specification的輔助方法,接收一個(gè)`IQuerybale<T>對(duì)象(通常是數(shù)據(jù)集合)
// 和一個(gè)當(dāng)前實(shí)體定義的Specification對(duì)象,并返回一個(gè)`IQueryable<T>`對(duì)象為子句執(zhí)行后的結(jié)果。
private IQueryable<T> ApplySpecification(ISpecification<T>? spec)
=> SpecificationEvaluator<T>.GetQuery(_dbContext.Set<T>().AsQueryable(), spec);
引入使用
為了驗(yàn)證通用Repsitory的用法,我們可以先在Infrastructure/DependencyInjection.cs中進(jìn)行依賴注入:
// in AddInfrastructure, 省略其他 services.AddScoped(typeof(IRepository<>), typeof(RepositoryBase<>));
驗(yàn)證
用于初步驗(yàn)證(主要是查詢接口),我們?cè)?code>Application項(xiàng)目里新建文件夾TodoItems/Specs,創(chuàng)建一個(gè)TodoItemSpec類:
using TodoList.Application.Common;
using TodoList.Domain.Entities;
using TodoList.Domain.Enums;
namespace TodoList.Application.TodoItems.Specs;
public sealed class TodoItemSpec : SpecificationBase<TodoItem>
{
public TodoItemSpec(bool done, PriorityLevel priority) : base(t => t.Done == done && t.Priority == priority)
{
}
}
然后我們臨時(shí)使用示例接口WetherForecastController,通過(guò)日志來(lái)看一下查詢的正確性。
private readonly IRepository<TodoItem> _repository;
private readonly ILogger<WeatherForecastController> _logger;
// 為了驗(yàn)證,臨時(shí)在這注入IRepository<TodoItem>對(duì)象,驗(yàn)證完后撤銷修改
public WeatherForecastController(IRepository<TodoItem> repository, ILogger<WeatherForecastController> logger)
{
_repository = repository;
_logger = logger;
}
在Get方法里增加這段邏輯用于觀察日志輸出:
// 記錄日志
_logger.LogInformation($"maybe this log is provided by Serilog...");
var spec = new TodoItemSpec(true, PriorityLevel.High);
var items = _repository.GetAsync(spec).Result;
foreach (var item in items)
{
_logger.LogInformation($"item: {item.Id} - {item.Title} - {item.Priority}");
}
啟動(dòng)Api項(xiàng)目然后請(qǐng)求示例接口,觀察控制臺(tái)輸出:
# 以上省略,Controller日志開(kāi)始... [16:49:59 INF] maybe this log is provided by Serilog... [16:49:59 INF] Entity Framework Core 6.0.1 initialized 'TodoListDbContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer:6.0.1' with options: MigrationsAssembly=TodoList.Infrastructure, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null [16:49:59 INF] Executed DbCommand (51ms) [Parameters=[@__done_0='?' (DbType = Boolean), @__priority_1='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30'] SELECT [t].[Id], [t].[Created], [t].[CreatedBy], [t].[Done], [t].[LastModified], [t].[LastModifiedBy], [t].[ListId], [t].[Priority], [t].[Title] FROM [TodoItems] AS [t] WHERE ([t].[Done] = @__done_0) AND ([t].[Priority] = @__priority_1) # 下面這句是我們之前初始化數(shù)據(jù)庫(kù)的種子數(shù)據(jù),可以參考上一篇文章結(jié)尾的驗(yàn)證截圖。 [16:49:59 INF] item: 87f1ddf1-e6cd-4113-74ed-08d9c5112f6b - Apples - High [16:49:59 INF] Executing ObjectResult, writing value of type 'TodoList.Api.WeatherForecast[]'. [16:49:59 INF] Executed action TodoList.Api.Controllers.WeatherForecastController.Get (TodoList.Api) in 160.5517ms
總結(jié)
在本文中,我大致演示了實(shí)現(xiàn)一個(gè)通用Repository基礎(chǔ)框架的過(guò)程。實(shí)際上關(guān)于Repository的組織與實(shí)現(xiàn)有很多種實(shí)現(xiàn)方法,每個(gè)人的關(guān)注點(diǎn)和思路都會(huì)有不同,但是大的方向基本都是這樣,無(wú)非是抽象的粒度和提供的接口的方便程度不同。有興趣的像伙伴可以仔細(xì)研究一下參考資料里的第2個(gè)實(shí)現(xiàn),也可以從Nuget直接下載在項(xiàng)目中引用使用。?
參考資料
到此這篇關(guān)于.NET 6開(kāi)發(fā)TodoList應(yīng)用之實(shí)現(xiàn)Repository模式的文章就介紹到這了,更多相關(guān).NET 6 Repository模式內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- .NET 6開(kāi)發(fā)TodoList應(yīng)用之實(shí)現(xiàn)查詢分頁(yè)
- .NET 6開(kāi)發(fā)TodoList應(yīng)用之實(shí)現(xiàn)ActionFilter
- .NET 6開(kāi)發(fā)TodoList應(yīng)用之實(shí)現(xiàn)接口請(qǐng)求驗(yàn)證
- .NET?6開(kāi)發(fā)TodoList應(yīng)用之實(shí)現(xiàn)DELETE請(qǐng)求與HTTP請(qǐng)求冪等性
- .NET 6開(kāi)發(fā)TodoList應(yīng)用之實(shí)現(xiàn)PUT請(qǐng)求
- .NET 6開(kāi)發(fā)TodoList應(yīng)用之實(shí)現(xiàn)全局異常處理
- .NET 6開(kāi)發(fā)TodoList應(yīng)用之使用AutoMapper實(shí)現(xiàn)GET請(qǐng)求
- .NET?6開(kāi)發(fā)TodoList應(yīng)用之使用MediatR實(shí)現(xiàn)POST請(qǐng)求
- .NET 6開(kāi)發(fā)TodoList應(yīng)用引入數(shù)據(jù)存儲(chǔ)
- .NET?6開(kāi)發(fā)TodoList應(yīng)用引入第三方日志庫(kù)
- .NET 6開(kāi)發(fā)TodoList應(yīng)用實(shí)現(xiàn)結(jié)構(gòu)搭建
- .NET?6開(kāi)發(fā)TodoList應(yīng)用實(shí)現(xiàn)系列背景
- 使用.NET?6開(kāi)發(fā)TodoList應(yīng)用之引入數(shù)據(jù)存儲(chǔ)的思路詳解
- 使用.NET?6開(kāi)發(fā)TodoList應(yīng)用之領(lǐng)域?qū)嶓w創(chuàng)建原理和思路
- .NET?6開(kāi)發(fā)TodoList應(yīng)用之請(qǐng)求日志組件HttpLogging介紹
相關(guān)文章
asp.net實(shí)現(xiàn)Postgresql快速寫入/讀取大量數(shù)據(jù)實(shí)例
本篇文章主要介紹了asp.net實(shí)現(xiàn)Postgresql快速寫入/讀取大量數(shù)據(jù)實(shí)例,具有一定的參考價(jià)值,有興趣的可以了解一下2017-07-07
ASP.NET對(duì)SQLServer的通用數(shù)據(jù)庫(kù)訪問(wèn)類
這篇文章主要實(shí)現(xiàn)了ASP.NET對(duì)SQLServer的通用數(shù)據(jù)庫(kù)訪問(wèn)類2016-02-02
ASP.NET對(duì)無(wú)序列表批量操作的三種方法小結(jié)
在網(wǎng)頁(yè)開(kāi)發(fā)中,經(jīng)常要用到無(wú)序列表。事實(shí)上在符合W3C標(biāo)準(zhǔn)的div+css布局中,無(wú)序列表被大量使用,ASP.NET雖然內(nèi)置了BulletedList控件,用于創(chuàng)建和操作無(wú)序列表,但感覺(jué)不太好用2012-01-01
Entity Framework Core更新時(shí)間映射
這篇文章介紹了Entity Framework Core更新時(shí)間映射的方法,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-03-03
ASP.NET獲取各級(jí)目錄Server.MapPath詳解全
ASP.NET獲取各級(jí)目錄Server.MapPath詳解全,需要的朋友可以參考下。2011-12-12
asp.net使用ajaxFileUpload插件上傳文件(附源碼)
本文詳細(xì)講解了asp.net使用ajaxFileUpload插件上傳文件,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-12-12
.NET6接入Skywalking鏈路追蹤詳細(xì)過(guò)程
Skywalking是一款分布式鏈路追蹤組件,隨著微服務(wù)架構(gòu)的流行,服務(wù)按照不同的維度進(jìn)行拆分,一次請(qǐng)求往往需要涉及到多個(gè)服務(wù),這篇文章主要介紹了.NET6接入Skywalking鏈路追蹤完整流程,需要的朋友可以參考下2022-06-06

