使用.NET?6開發(fā)TodoList應(yīng)用之領(lǐng)域?qū)嶓w創(chuàng)建原理和思路
需求
上一篇文章中我們完成了數(shù)據(jù)存儲(chǔ)服務(wù)的接入,從這一篇開始將正式進(jìn)入業(yè)務(wù)邏輯部分的開發(fā)。
首先要定義和解決的問題是,根據(jù)TodoList項(xiàng)目的需求,我們應(yīng)該設(shè)計(jì)怎樣的數(shù)據(jù)實(shí)體,如何去進(jìn)行操作?
長(zhǎng)文預(yù)警!包含大量代碼
目標(biāo)
在本文中,我們希望達(dá)到以下幾個(gè)目標(biāo):
- 定義領(lǐng)域?qū)嶓w;
- 通過數(shù)據(jù)庫操作領(lǐng)域?qū)嶓w;
原理和思路
雖然TodoList是一個(gè)很簡(jiǎn)單的應(yīng)用,業(yè)務(wù)邏輯并不復(fù)雜,至少在這個(gè)系列文章中我并不想使其過度復(fù)雜。但是我還是打算借此簡(jiǎn)單地涉及領(lǐng)域驅(qū)動(dòng)開發(fā)(DDD)的基礎(chǔ)概念。
首先比較明確的是,我們的實(shí)體對(duì)象應(yīng)該有兩個(gè):TodoList和TodoItem,并且一個(gè)TodoList是由多個(gè)TodoItem的列表構(gòu)成,除此以外在實(shí)際的開發(fā)中,我們可能還需要追蹤實(shí)體的變更情況,比如需要知道創(chuàng)建時(shí)間/修改時(shí)間/創(chuàng)建者/修改者,這種需求一般作為審計(jì)要求出現(xiàn),而對(duì)實(shí)體的審計(jì)又是一個(gè)比較通用的需求。所以我們會(huì)將實(shí)體分成兩部分:和業(yè)務(wù)需求直接相關(guān)的屬性,以及和實(shí)體審計(jì)需求相關(guān)的屬性。
其次,對(duì)于實(shí)體的數(shù)據(jù)庫配置,有兩種方式:通過Attribute或者通過IEntityTypeConfiguration<T>以代碼的方式進(jìn)行。我推薦使用第二種方式,將所有的具體配置集中到Infrastructure層去管理,避免后續(xù)修改字段屬性而去頻繁修改位于Domain層的實(shí)體對(duì)象定義,我們希望實(shí)體定義本身是穩(wěn)定的。
最后,對(duì)于DDD來說有一些核心概念諸如領(lǐng)域事件,值對(duì)象,聚合根等等,我們都會(huì)在定義領(lǐng)域?qū)嶓w的時(shí)候有所涉及,但是目前還不會(huì)過多地使用。關(guān)于這些基本概念的含義,請(qǐng)參考這篇文章:淺談Java開發(fā)架構(gòu)之領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)DDD落地。在我們的開發(fā)過程中,會(huì)進(jìn)行一些精簡(jiǎn),有部分內(nèi)容也會(huì)隨著后續(xù)的文章逐步完善。
實(shí)現(xiàn)
基礎(chǔ)的領(lǐng)域概念框架搭建
所有和領(lǐng)域相關(guān)的概念都會(huì)進(jìn)入到Domain這個(gè)項(xiàng)目中,我們首先在Domain項(xiàng)目里新建文件夾Base用于存放所有的基礎(chǔ)定義,下面將一個(gè)一個(gè)去實(shí)現(xiàn)。(另一種方式是把這些最基礎(chǔ)的定義單獨(dú)提出去新建一個(gè)SharedDefinition類庫并讓Domain引用這個(gè)項(xiàng)目。)
基礎(chǔ)實(shí)體定義以及可審計(jì)實(shí)體定義
我這兩個(gè)類都應(yīng)該是抽象基類,他們的存在是為了讓我們的業(yè)務(wù)實(shí)體繼承使用的,并且為了允許不同的實(shí)體可以定義自己主鍵的類型,我們將基類定義成泛型的。
AuditableEntity.cs
namespace TodoList.Domain.Base;
public abstract class AuditableEntity
{
public DateTime Created { get; set; }
public string? CreatedBy { get; set; }
public DateTime? LastModified { get; set; }
public string? LastModifiedBy { get; set; }
}
在Base里增加Interface文件夾來保存接口定義。
IEntity.cs
namespace TodoList.Domain.Base.Interfaces;
public interface IEntity<T>
{
public T Id { get; set; }
}
除了這兩個(gè)對(duì)象之外,我們還需要增加關(guān)于領(lǐng)域事件框架的定義。
DomainEvent.cs
namespace TodoList.Domain.Base;
public abstract class DomainEvent
{
protected DomainEvent()
{
DateOccurred = DateTimeOffset.UtcNow;
}
public bool IsPublished { get; set; }
public DateTimeOffset DateOccurred { get; protected set; } = DateTime.UtcNow;
}
我們還剩下Aggregate Root, ValueObject和Domain Service以及Domain Exception,其他的相關(guān)概念暫時(shí)就不涉及了。
IHasDomainEvent.cs
namespace TodoList.Domain.Base.Interfaces;
public interface IHasDomainEvent
{
public List<DomainEvent> DomainEvents { get; set; }
}
ValueObject的實(shí)現(xiàn)有幾乎固定的寫法,請(qǐng)參考:https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/microservice-ddd-cqrs-patterns/implement-value-objects
IAggregateRoot.cs
namespace TodoList.Domain.Base.Interfaces;
// 聚合根對(duì)象僅僅作為標(biāo)記來使用
public interface IAggregateRoot { }
ValueObject.cs
namespace TodoList.Domain.Base;
public abstract class ValueObject
{
protected static bool EqualOperator(ValueObject left, ValueObject right)
{
if (left is null ^ right is null)
{
return false;
}
return left?.Equals(right!) != false;
}
protected static bool NotEqualOperator(ValueObject left, ValueObject right)
{
return !(EqualOperator(left, right));
}
protected abstract IEnumerable<object> GetEqualityComponents();
public override bool Equals(object? obj)
{
if (obj == null || obj.GetType() != GetType())
{
return false;
}
var other = (ValueObject)obj;
return GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
}
public override int GetHashCode()
{
return GetEqualityComponents()
.Select(x => x != null ? x.GetHashCode() : 0)
.Aggregate((x, y) => x ^ y);
}
}
關(guān)于Domain Exception的定義,是根據(jù)業(yè)務(wù)內(nèi)容來確定的,暫時(shí)不在Base里實(shí)現(xiàn),而是放到各個(gè)聚合根的層級(jí)里。
定義TodoLIst/TodoItem實(shí)體
TodoList對(duì)象從領(lǐng)域建模上來說屬于聚合根,并且下一節(jié)將要實(shí)現(xiàn)的TodoItem是構(gòu)成該聚合根的一部分,業(yè)務(wù)意思上不能單獨(dú)存在。有一種實(shí)現(xiàn)方式是按照聚合根的關(guān)聯(lián)性進(jìn)行代碼組織:即在Domain項(xiàng)目里新建文件夾AggregateRoots/TodoList來保存和這個(gè)聚合根相關(guān)的所有業(yè)務(wù)定義:即:Events/Exceptions和Enums三個(gè)文件夾用于存放對(duì)應(yīng)的內(nèi)容。但是這樣可能會(huì)導(dǎo)致項(xiàng)目的目錄層級(jí)太多,實(shí)際上在這里,我更傾向于在Domain項(xiàng)目的根目錄下創(chuàng)建Entities/Events/Enums/Exceptions/ValueObjects文件夾來扁平化領(lǐng)域模型,對(duì)于世紀(jì)的開發(fā)查找起來也并不麻煩。所以才去后一種方式,然后在Entities中創(chuàng)建TodoItem和TodoList實(shí)體:
TodoItem.cs
using TodoList.Domain.Base;
using TodoList.Domain.Base.Interfaces;
using TodoList.Domain.Enums;
using TodoList.Domain.Events;
namespace TodoList.Domain.Entities;
public class TodoItem : AuditableEntity, IEntity<Guid>, IHasDomainEvent
{
public Guid Id { get; set; }
public string? Title { get; set; }
public PriorityLevel Priority { get; set; }
private bool _done;
public bool Done
{
get => _done;
set
{
if (value && _done == false)
{
DomainEvents.Add(new TodoItemCompletedEvent(this));
}
_done = value;
}
}
public TodoList List { get; set; } = null!;
public List<DomainEvent> DomainEvents { get; set; } = new List<DomainEvent>();
}
PriorityLevel.cs
namespace TodoList.Domain.Enums;
public enum PriorityLevel
{
None = 0,
Low = 1,
Medium = 2,
High = 3
}
TodoItemCompletedEvent.cs
using TodoList.Domain.Base;
using TodoList.Domain.Entities;
namespace TodoList.Domain.Events;
public class TodoItemCompletedEvent : DomainEvent
{
public TodoItemCompletedEvent(TodoItem item) => Item = item;
public TodoItem Item { get; }
}
TodoList.cs
using TodoList.Domain.Base;
using TodoList.Domain.Base.Interfaces;
using TodoList.Domain.ValueObjects;
namespace TodoList.Domain.Entities;
public class TodoList : AuditableEntity, IEntity<Guid>, IHasDomainEvent, IAggregateRoot
{
public Guid Id { get; set; }
public string? Title { get; set; }
public Colour Colour { get; set; } = Colour.White;
public IList<TodoItem> Items { get; private set; } = new List<TodoItem>();
public List<DomainEvent> DomainEvents { get; set; } = new List<DomainEvent>();
}
為了演示ValueObject,添加了一個(gè)Colour對(duì)象,同時(shí)添加了一個(gè)領(lǐng)域異常對(duì)象UnsupportedColourException
Colour.cs
using TodoList.Domain.Base;
namespace TodoList.Domain.ValueObjects;
public class Colour : ValueObject
{
static Colour() { }
private Colour() { }
private Colour(string code) => Code = code;
public static Colour From(string code)
{
var colour = new Colour { Code = code };
if (!SupportedColours.Contains(colour))
{
throw new UnsupportedColourException(code);
}
return colour;
}
public static Colour White => new("#FFFFFF");
public static Colour Red => new("#FF5733");
public static Colour Orange => new("#FFC300");
public static Colour Yellow => new("#FFFF66");
public static Colour Green => new("#CCFF99 ");
public static Colour Blue => new("#6666FF");
public static Colour Purple => new("#9966CC");
public static Colour Grey => new("#999999");
public string Code { get; private set; } = "#000000";
public static implicit operator string(Colour colour) => colour.ToString();
public static explicit operator Colour(string code) => From(code);
public override string ToString() => Code;
protected static IEnumerable<Colour> SupportedColours
{
get
{
yield return White;
yield return Red;
yield return Orange;
yield return Yellow;
yield return Green;
yield return Blue;
yield return Purple;
yield return Grey;
}
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Code;
}
}
UnsupportedColourException.cs
namespace TodoList.Domain.Exceptions;
public class UnsupportedColourException : Exception
{
public UnsupportedColourException(string code)
: base($"Colour \"[code]\" is unsupported.")
{
}
}
關(guān)于領(lǐng)域服務(wù)的內(nèi)容我們暫時(shí)不去管,繼續(xù)看看如何向數(shù)據(jù)庫配置實(shí)體對(duì)象。
領(lǐng)域?qū)嶓w的數(shù)據(jù)庫配置
這部分內(nèi)容相對(duì)會(huì)熟悉一些,我們?cè)?code>Infrastructure/Persistence中新建文件夾Configurations用于存放實(shí)體配置:
TodoItemConfiguration.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using TodoList.Domain.Entities;
namespace TodoList.Infrastructure.Persistence.Configurations;
public class TodoItemConfiguration : IEntityTypeConfiguration<TodoItem>
{
public void Configure(EntityTypeBuilder<TodoItem> builder)
{
builder.Ignore(e => e.DomainEvents);
builder.Property(t => t.Title)
.HasMaxLength(200)
.IsRequired();
}
}
TodoListConfiguration.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace TodoList.Infrastructure.Persistence.Configurations;
public class TodoListConfiguration : IEntityTypeConfiguration<Domain.Entities.TodoList>
{
public void Configure(EntityTypeBuilder<Domain.Entities.TodoList> builder)
{
builder.Ignore(e => e.DomainEvents);
builder.Property(t => t.Title)
.HasMaxLength(200)
.IsRequired();
builder.OwnsOne(b => b.Colour);
}
}
修改DbContext
因?yàn)橄乱黄镂覀儗⒁褂?code>Repository模式,所以我們可以不需要讓TodoListDbContext繼續(xù)繼承IApplicationDbContext了。關(guān)于直接在Application里使用Context比較簡(jiǎn)單,就不繼續(xù)演示了。
在這一步里面,我們需要完成以下幾件事:
- 添加數(shù)據(jù)表;
- 重寫SaveChangesAsync方法,自動(dòng)補(bǔ)充審計(jì)相關(guān)字段值,并且在此發(fā)送領(lǐng)域事件;
對(duì)于第一件事,很簡(jiǎn)單。向TodoListDbContext.cs類定義中加入:
// TodoLIst實(shí)體與命名空間名稱有沖突,所以需要顯示引用其他命名空間里的對(duì)象 public DbSet<Domain.Entities.TodoList> TodoLists => Set<Domain.Entities.TodoList>(); public DbSet<TodoItem> TodoItems => Set<TodoItem>();
對(duì)于第二件事,我們需要先向Application/Common/Interfaces中添加一個(gè)接口用于管理領(lǐng)域事件的分發(fā),但是在講到CQRS之前,我們暫時(shí)以Dummy的方式實(shí)現(xiàn)這個(gè)接口,因?yàn)閷⒁褂玫谌娇蚣軐?shí)現(xiàn)具體邏輯,所以我們把實(shí)現(xiàn)類放到Infrastrcucture/Services目錄下,并在TodoListDbContext中注入使用。
IDomainEventService.cs
using TodoList.Domain.Base;
namespace TodoList.Application.Common.Interfaces;
public interface IDomainEventService
{
Task Publish(DomainEvent domainEvent);
}
DomainEventService.cs
using Microsoft.Extensions.Logging;
using TodoList.Application.Common.Interfaces;
using TodoList.Domain.Base;
namespace TodoList.Infrastructure.Services;
public class DomainEventService : IDomainEventService
{
private readonly ILogger<DomainEventService> _logger;
public DomainEventService(ILogger<DomainEventService> logger)
{
_logger = logger;
}
public async Task Publish(DomainEvent domainEvent)
{
// 在這里暫時(shí)什么都不做,到CQRS那一篇的時(shí)候再回來補(bǔ)充這里的邏輯
_logger.LogInformation("Publishing domain event. Event - {event}", domainEvent.GetType().Name);
}
}
在DependencyInjection中注入:
// 省略以上...并且這一句可以不需要了 // services.AddScoped<IApplicationDbContext>(provider => provider.GetRequiredService<TodoListDbContext>()); // 增加依賴注入 services.AddScoped<IDomainEventService, DomainEventService>(); return services;
最終的TodoListDbContext實(shí)現(xiàn)如下:
TodoListDbContext.cs
using System.Reflection;
using Microsoft.EntityFrameworkCore;
using TodoList.Application.Common.Interfaces;
using TodoList.Domain.Base;
using TodoList.Domain.Base.Interfaces;
using TodoList.Domain.Entities;
namespace TodoList.Infrastructure.Persistence;
public class TodoListDbContext : DbContext
{
private readonly IDomainEventService _domainEventService;
public TodoListDbContext(
DbContextOptions<TodoListDbContext> options,
IDomainEventService domainEventService) : base(options)
{
_domainEventService = domainEventService;
}
public DbSet<Domain.Entities.TodoList> TodoLists => Set<Domain.Entities.TodoList>();
public DbSet<TodoItem> TodoItems => Set<TodoItem>();
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new())
{
// 在我們重寫的SaveChangesAsync方法中,去設(shè)置審計(jì)相關(guān)的字段,目前對(duì)于修改人這個(gè)字段暫時(shí)先給個(gè)定值,等到后面講到認(rèn)證鑒權(quán)的時(shí)候再回過頭來看這里
foreach (var entry in ChangeTracker.Entries<AuditableEntity>())
{
switch (entry.State)
{
case EntityState.Added:
entry.Entity.CreatedBy = "Anonymous";
entry.Entity.Created = DateTime.UtcNow;
break;
case EntityState.Modified:
entry.Entity.LastModifiedBy = "Anonymous";
entry.Entity.LastModified = DateTime.UtcNow;
break;
}
}
// 在寫數(shù)據(jù)庫的時(shí)候同時(shí)發(fā)送領(lǐng)域事件,這里要注意一定要保證寫入數(shù)據(jù)庫成功后再發(fā)送領(lǐng)域事件,否則會(huì)導(dǎo)致領(lǐng)域?qū)ο鬆顟B(tài)的不一致問題。
var events = ChangeTracker.Entries<IHasDomainEvent>()
.Select(x => x.Entity.DomainEvents)
.SelectMany(x => x)
.Where(domainEvent => !domainEvent.IsPublished)
.ToArray();
var result = await base.SaveChangesAsync(cancellationToken);
await DispatchEvents(events);
return result;
}
protected override void OnModelCreating(ModelBuilder builder)
{
// 應(yīng)用當(dāng)前Assembly中定義的所有的Configurations,就不需要一個(gè)一個(gè)去寫了。
builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
base.OnModelCreating(builder);
}
private async Task DispatchEvents(DomainEvent[] events)
{
foreach (var @event in events)
{
@event.IsPublished = true;
await _domainEventService.Publish(@event);
}
}
}
驗(yàn)證
生成Migrations
老辦法,先生成Migrations。
$ dotnet ef migrations add AddEntities -p src/TodoList.Infrastructure/TodoList.Infrastructure.csproj -s src/TodoList.Api/TodoList.Api.csproj Build started... Build succeeded. [14:06:15 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 Done. To undo this action, use 'ef migrations remove'
使用種子數(shù)據(jù)更新數(shù)據(jù)庫
為了演示效果,在Infrastructure/Persistence/下創(chuàng)建TodoListDbContextSeed.cs文件并初始化種子數(shù)據(jù):
TodoListDbContextSeed.cs
using Microsoft.EntityFrameworkCore;
using TodoList.Domain.Entities;
using TodoList.Domain.Enums;
using TodoList.Domain.ValueObjects;
namespace TodoList.Infrastructure.Persistence;
public static class TodoListDbContextSeed
{
public static async Task SeedSampleDataAsync(TodoListDbContext context)
{
if (!context.TodoLists.Any())
{
var list = new Domain.Entities.TodoList
{
Title = "Shopping",
Colour = Colour.Blue
};
list.Items.Add(new TodoItem { Title = "Apples", Done = true, Priority = PriorityLevel.High});
list.Items.Add(new TodoItem { Title = "Milk", Done = true });
list.Items.Add(new TodoItem { Title = "Bread", Done = true });
list.Items.Add(new TodoItem { Title = "Toilet paper" });
list.Items.Add(new TodoItem { Title = "Pasta" });
list.Items.Add(new TodoItem { Title = "Tissues" });
list.Items.Add(new TodoItem { Title = "Tuna" });
list.Items.Add(new TodoItem { Title = "Water" });
context.TodoLists.Add(list);
await context.SaveChangesAsync();
}
}
public static async Task UpdateSampleDataAsync(TodoListDbContext context)
{
var sampleTodoList = await context.TodoLists.FirstOrDefaultAsync();
if (sampleTodoList == null)
{
return;
}
sampleTodoList.Title = "Shopping - modified";
// 演示更新時(shí)審計(jì)字段的變化
context.Update(sampleTodoList);
await context.SaveChangesAsync();
}
}
在應(yīng)用程序初始化的擴(kuò)展中進(jìn)行初始化和更新:
ApplicationStartupExtensions.cs
// 省略以上...
try
{
var context = services.GetRequiredService<TodoListDbContext>();
context.Database.Migrate();
// 生成種子數(shù)據(jù)
TodoListDbContextSeed.SeedSampleDataAsync(context).Wait();
// 更新部分種子數(shù)據(jù)以便查看審計(jì)字段
TodoListDbContextSeed.UpdateSampleDataAsync(context).Wait();
}
catch (Exception ex)
// 省略以下...
運(yùn)行Api項(xiàng)目,得到下面的輸出,中間我省略了一些SQL語句的輸出:
$ dotnet run --project src/TodoList.Api
Building...
# ...省略
[14:06:24 INF] Applying migration '20211222060615_AddEntities'.
# ...省略
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20211222060615_AddEntities', N'6.0.1');
# ...省略,注意下面的三個(gè)domain event,因?yàn)槲覀冊(cè)谠旆N子數(shù)據(jù)的時(shí)候有設(shè)置三個(gè)TodoItem標(biāo)記為已完成,將會(huì)觸發(fā)event。
[14:06:25 INF] Publishing domain event. Event - TodoItemCompletedEvent
[14:06:25 INF] Publishing domain event. Event - TodoItemCompletedEvent
[14:06:25 INF] Publishing domain event. Event - TodoItemCompletedEvent
# ...省略
[14:06:25 INF] Now listening on: https://localhost:7039
[14:06:25 INF] Now listening on: http://localhost:5050
[14:06:25 INF] Application started. Press Ctrl+C to shut down.
[14:06:25 INF] Hosting environment: Development
# ...省略
我們?cè)偃タ纯磾?shù)據(jù)庫中的數(shù)據(jù):
TodoLists數(shù)據(jù)表:

TodoItems數(shù)據(jù)表

__EFMigrationsHistory遷移表:

總結(jié)
在本文中,我們著手搭建了基本的領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)對(duì)應(yīng)的Domain層實(shí)現(xiàn),包括兩個(gè)領(lǐng)域?qū)嶓w對(duì)象及其關(guān)聯(lián)的其他知識(shí)。最后通過種子數(shù)據(jù)的方式進(jìn)行數(shù)據(jù)庫數(shù)據(jù)操作的驗(yàn)證,下一篇我們將繼續(xù)實(shí)現(xiàn)一個(gè)通用的Repository模式。
參考資料
Domain Driven DesignDDD領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)基本理論知識(shí)總結(jié)
到此這篇關(guān)于使用.NET?6開發(fā)TodoList應(yīng)用之領(lǐng)域?qū)嶓w創(chuàng)建原理和思路的文章就介紹到這了,更多相關(guān).NET?6?開發(fā)TodoList應(yīng)用內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(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)接口請(qǐng)求驗(yàn)證
- .NET?6開發(fā)TodoList應(yīng)用之實(shí)現(xiàn)DELETE請(qǐng)求與HTTP請(qǐng)求冪等性
- .NET 6開發(fā)TodoList應(yīng)用之實(shí)現(xiàn)PUT請(qǐng)求
- .NET 6開發(fā)TodoList應(yīng)用之實(shí)現(xiàn)全局異常處理
- .NET 6開發(fā)TodoList應(yīng)用之使用AutoMapper實(shí)現(xiàn)GET請(qǐng)求
- .NET?6開發(fā)TodoList應(yīng)用之實(shí)現(xiàn)Repository模式
- .NET?6開發(fā)TodoList應(yīng)用之使用MediatR實(shí)現(xiàn)POST請(qǐng)求
- .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)用之請(qǐng)求日志組件HttpLogging介紹
相關(guān)文章
EFCore 通過實(shí)體Model生成創(chuàng)建SQL Server數(shù)據(jù)庫表腳本
這篇文章主要介紹了EFCore 通過實(shí)體Model生成創(chuàng)建SQL Server數(shù)據(jù)庫表腳本的示例,幫助大家更好的理解和學(xué)習(xí)使用.net框架,感興趣的朋友可以了解下2021-03-03
.net core項(xiàng)目中常用的幾款類庫詳解(值得收藏)
這篇文章主要給大家介紹了關(guān)于.net core項(xiàng)目中常用的幾款類庫的相關(guān)資料,文章通過示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用.net core具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2018-04-04
.Net Core+Angular Cli/Angular4開發(fā)環(huán)境搭建教程
這篇文章主要為大家詳細(xì)介紹了.Net Core+Angular Cli/Angular4開發(fā)環(huán)境搭建教程,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06
.Net Core配置Configuration具體實(shí)現(xiàn)
這篇文章主要介紹了.Net Core配置Configuration具體實(shí)現(xiàn),文中運(yùn)用大量代碼進(jìn)行講解,如果有對(duì)相關(guān)知識(shí)感興趣的小伙伴可以參考這篇文章,希望可以幫助到你2021-09-09
ASP.Net?Core中的內(nèi)存和GC機(jī)制
這篇文章介紹了ASP.Net?Core中的內(nèi)存和GC機(jī)制,對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-03-03
.NET的動(dòng)態(tài)編譯與WS服務(wù)調(diào)用詳解
這篇文章介紹了.NET的動(dòng)態(tài)編譯與WS服務(wù)調(diào)用詳解,有需要的朋友可以參考一下,希望對(duì)你有所幫助2013-07-07

