.NET?6開發(fā)TodoList應(yīng)用之實(shí)現(xiàn)Repository模式
需求
經(jīng)常寫CRUD程序的小伙伴們可能都經(jīng)歷過定義很多Repository
接口,分別做對應(yīng)的實(shí)現(xiàn),依賴注入并使用的場景。有的時(shí)候會(huì)發(fā)現(xiàn),很多分散的XXXXRepository
的邏輯都是基本一致的,于是開始思考是否可以將這些操作抽象出去,當(dāng)然是可以的,而且被抽象出去的部分是可以不加改變地在今后的任何有此需求的項(xiàng)目中直接引入使用。
那么我們本文的需求就是:如何實(shí)現(xiàn)一個(gè)可重用的Repository
模塊。
長文預(yù)警,包含大量代碼。
目標(biāo)
實(shí)現(xiàn)通用Repository
模式并進(jìn)行驗(yàn)證。
原理和思路
通用的基礎(chǔ)在于抽象,抽象的粒度決定了通用的程度,但是同時(shí)也決定了使用上的復(fù)雜度。對于自己的項(xiàng)目而言,抽象到什么程度最合適,需要自己去權(quán)衡,也許后面某個(gè)時(shí)候我會(huì)決定自己去實(shí)現(xiàn)一個(gè)完善的Repository
庫提供出來(事實(shí)上已經(jīng)有很多人這樣做了,我們甚至可以直接下載Nuget包進(jìn)行使用,但是自己親手去實(shí)現(xiàn)的過程能讓你更好地去理解其中的原理,也理解如何開發(fā)一個(gè)通用的類庫。)
總體思路是:在Application
中定義相關(guān)的接口,在Infrastructure
中實(shí)現(xiàn)基類的功能。
實(shí)現(xiàn)
通用Repository實(shí)現(xiàn)
對于要如何去設(shè)計(jì)一個(gè)通用的Repository
庫,實(shí)際上涉及的面非常多,尤其是在獲取數(shù)據(jù)的時(shí)候。而且根據(jù)每個(gè)人的習(xí)慣,實(shí)現(xiàn)起來的方式是有比較大的差別的,尤其是關(guān)于泛型接口到底需要提供哪些方法,每個(gè)人都有自己的理解,這里我只演示基本的思路,而且盡量保持簡單,關(guān)于更復(fù)雜和更全面的實(shí)現(xiàn),GIthub上有很多已經(jīng)寫好的庫可以去學(xué)習(xí)和參考,我會(huì)列在下面:
很顯然,第一步要去做的是在Application/Common/Interfaces
中增加一個(gè)IRepository<T>
的定義用于適用不同類型的實(shí)體,然后在Infrastructure/Persistence/Repositories
中創(chuàng)建一個(gè)基類RepositoryBase<T>
實(shí)現(xiàn)這個(gè)接口,并有辦法能提供一致的對外方法簽名。
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>
之前,先思考一下:對數(shù)據(jù)庫的操作都會(huì)出現(xiàn)哪些情況:
新增實(shí)體(Create)
新增實(shí)體在Repository
層面的邏輯很簡單,傳入一個(gè)實(shí)體對象,然后保存到數(shù)據(jù)庫就可以了,沒有其他特殊的需求。
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í)體對象去操作。
IRepository.cs
// 省略其他... // Update相關(guān)操作接口 Task UpdateAsync(T entity, CancellationToken cancellationToken = default);
RepositoryBase.cs
// 省略其他... public async Task UpdateAsync(T entity, CancellationToken cancellationToken = default) { // 對于一般的更新而言,都是Attach到實(shí)體上的,只需要設(shè)置該實(shí)體的State為Modified就可以了 _dbContext.Entry(entity).State = EntityState.Modified; await _dbContext.SaveChangesAsync(cancellationToken); }
刪除實(shí)體(Delete)
對于刪除實(shí)體,可能會(huì)出現(xiàn)兩種情況:刪除一個(gè)實(shí)體;或者刪除一組實(shí)體。
IRepository.cs
// 省略其他... // Delete相關(guān)操作接口,這里根據(jù)key刪除對象的接口需要用到一個(gè)獲取對象的方法 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)
對于如何獲取實(shí)體,是最復(fù)雜的一部分。我們不僅要考慮通過什么方式獲取哪些數(shù)據(jù),還需要考慮獲取的數(shù)據(jù)有沒有特殊的要求比如排序、分頁、數(shù)據(jù)對象類型的轉(zhuǎn)換之類的問題。
具體來說,比如下面這一個(gè)典型的LINQ查詢語句:
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)的對象來表示:
1.查詢數(shù)據(jù)集準(zhǔn)備過程,在這個(gè)過程中可能會(huì)出現(xiàn)Include/Join/GroupJoin/GroupBy等等類似的關(guān)鍵字,它們的作用是構(gòu)建一個(gè)用于接下來將要進(jìn)行查詢的數(shù)據(jù)集。
2.Where
子句,用于過濾查詢集合。
3.Select
子句,用于轉(zhuǎn)換原始數(shù)據(jù)類型到我們想要的結(jié)果類型。
4.Order
子句,用于對結(jié)果集進(jìn)行排序,這里可能會(huì)包含類似:OrderBy/OrderByDescending/ThenBy/ThenByDescending等關(guān)鍵字。
5.Paging
子句,用于對結(jié)果集進(jìn)行后端分頁返回,一般都是Skip/Take一起使用。
6.其他子句,多數(shù)是條件控制,比如AsNoTracking/SplitQuery等等。
為了保持我們的演示不會(huì)過于復(fù)雜,我會(huì)做一些取舍。在這里的實(shí)現(xiàn)我參考了Edi.Wang的Moonglade中的相關(guān)實(shí)現(xiàn)。有興趣的小伙伴也可以去找一下一個(gè)更完整的實(shí)現(xiàn):Ardalis.Specification。
首先來定義一個(gè)簡單的ISpecification
來表示查詢的各類條件:
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; } // 分頁相關(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串起來形成查詢子句,我們還需要定義一個(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來傳入映射的表達(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>對象(通常是數(shù)據(jù)集合) // 和一個(gè)當(dāng)前實(shí)體定義的Specification對象,并返回一個(gè)`IQueryable<T>`對象為子句執(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)證(主要是查詢接口),我們在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
,通過日志來看一下查詢的正確性。
private readonly IRepository<TodoItem> _repository; private readonly ILogger<WeatherForecastController> _logger; // 為了驗(yàn)證,臨時(shí)在這注入IRepository<TodoItem>對象,驗(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)目然后請求示例接口,觀察控制臺(tái)輸出:
# 以上省略,Controller日志開始... [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ù)庫的種子數(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ǔ)框架的過程。實(shí)際上關(guān)于Repository的組織與實(shí)現(xiàn)有很多種實(shí)現(xiàn)方法,每個(gè)人的關(guān)注點(diǎn)和思路都會(huì)有不同,但是大的方向基本都是這樣,無非是抽象的粒度和提供的接口的方便程度不同。有興趣的像伙伴可以仔細(xì)研究一下參考資料里的第2個(gè)實(shí)現(xiàn),也可以從Nuget直接下載在項(xiàng)目中引用使用。?
參考資料
到此這篇關(guān)于.NET 6開發(fā)TodoList應(yīng)用之實(shí)現(xiàn)Repository模式的文章就介紹到這了,更多相關(guān).NET 6 Repository模式內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- .NET 6開發(fā)TodoList應(yīng)用之實(shí)現(xiàn)查詢分頁
- .NET 6開發(fā)TodoList應(yīng)用之實(shí)現(xiàn)ActionFilter
- .NET 6開發(fā)TodoList應(yīng)用之實(shí)現(xiàn)接口請求驗(yàn)證
- .NET?6開發(fā)TodoList應(yīng)用之實(shí)現(xiàn)DELETE請求與HTTP請求冪等性
- .NET 6開發(fā)TodoList應(yīng)用之實(shí)現(xiàn)PUT請求
- .NET 6開發(fā)TodoList應(yīng)用之實(shí)現(xiàn)全局異常處理
- .NET 6開發(fā)TodoList應(yīng)用之使用AutoMapper實(shí)現(xiàn)GET請求
- .NET?6開發(fā)TodoList應(yīng)用之使用MediatR實(shí)現(xiàn)POST請求
- .NET 6開發(fā)TodoList應(yīng)用引入數(shù)據(jù)存儲(chǔ)
- .NET?6開發(fā)TodoList應(yīng)用引入第三方日志庫
- .NET 6開發(fā)TodoList應(yīng)用實(shí)現(xiàn)結(jié)構(gòu)搭建
- .NET?6開發(fā)TodoList應(yīng)用實(shí)現(xiàn)系列背景
- 使用.NET?6開發(fā)TodoList應(yīng)用之引入數(shù)據(jù)存儲(chǔ)的思路詳解
- 使用.NET?6開發(fā)TodoList應(yīng)用之領(lǐng)域?qū)嶓w創(chuàng)建原理和思路
- .NET?6開發(fā)TodoList應(yī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-07ASP.NET對SQLServer的通用數(shù)據(jù)庫訪問類
這篇文章主要實(shí)現(xiàn)了ASP.NET對SQLServer的通用數(shù)據(jù)庫訪問類2016-02-02Entity Framework Core更新時(shí)間映射
這篇文章介紹了Entity Framework Core更新時(shí)間映射的方法,文中通過示例代碼介紹的非常詳細(xì)。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-03-03ASP.NET獲取各級目錄Server.MapPath詳解全
ASP.NET獲取各級目錄Server.MapPath詳解全,需要的朋友可以參考下。2011-12-12asp.net使用ajaxFileUpload插件上傳文件(附源碼)
本文詳細(xì)講解了asp.net使用ajaxFileUpload插件上傳文件,文中通過示例代碼介紹的非常詳細(xì)。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-12-12