Skip to content

Commit 3cda4dd

Browse files
authored
refactor(softdelete): Refactor soft delete (#52)
* feat(EntityFramework): Support EntityFrameWork * refactor(Data): Refactoring soft delete related functions * refactor(SoftDelete): Refactor soft delete * test(EntityFramework): Refactor EntityFramework UnitTest * chore: Change .editorconfig * chore: format code * docs(EntityFrameworkCore): Adjust filter documentation * chore: Adjust Global Using * Update .editorconfig * chore: Add method annotation
1 parent 4a2f393 commit 3cda4dd

File tree

136 files changed

+3366
-376
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

136 files changed

+3366
-376
lines changed

.editorconfig

+10
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@ indent_size = 4
2020
insert_final_newline = true
2121
charset = utf-8
2222

23+
# Use Upper Case for constant fields
24+
dotnet_naming_style.upper_case_style.capitalization = all_upper
25+
dotnet_naming_style.upper_case_style.word_separator = _
26+
dotnet_naming_rule.constant_fields_should_be_upper_case.severity = warning
27+
dotnet_naming_rule.constant_fields_should_be_upper_case.symbols = constant_fields
28+
dotnet_naming_rule.constant_fields_should_be_upper_case.style = upper_case_style
29+
dotnet_naming_symbols.constant_fields.applicable_kinds = field
30+
dotnet_naming_symbols.constant_fields.applicable_accessibilities = *
31+
dotnet_naming_symbols.constant_fields.required_modifiers = const
32+
2333
# Xml project files
2434
[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]
2535
indent_size = 2

Masa.Contrib.sln

+132-11
Large diffs are not rendered by default.
Submodule MASA.BuildingBlocks updated 26 files
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright (c) MASA Stack All rights reserved.
2+
// Licensed under the MIT License. See LICENSE.txt in the project root for license information.
3+
4+
namespace Masa.Contrib.Data.Contracts.EF.DataFiltering;
5+
6+
public class DataFilter : IDataFilter
7+
{
8+
private readonly IServiceProvider _serviceProvider;
9+
private readonly MemoryCache<Type, object> _cache;
10+
11+
public DataFilter(IServiceProvider serviceProvider)
12+
{
13+
_serviceProvider = serviceProvider;
14+
_cache = new();
15+
}
16+
17+
public IDisposable Enable<TFilter>() where TFilter : class
18+
=> GetFilter<TFilter>().Enable();
19+
20+
public IDisposable Disable<TFilter>() where TFilter : class
21+
=> GetFilter<TFilter>().Disable();
22+
23+
public bool IsEnabled<TFilter>() where TFilter : class
24+
=> GetFilter<TFilter>().Enabled;
25+
26+
private DataFilter<TFilter> GetFilter<TFilter>()
27+
where TFilter : class
28+
{
29+
return (_cache.GetOrAdd(
30+
typeof(TFilter),
31+
_ => _serviceProvider.GetRequiredService<DataFilter<TFilter>>()
32+
) as DataFilter<TFilter>)!;
33+
}
34+
}
35+
36+
public class DataFilter<TFilter> where TFilter : class
37+
{
38+
private readonly AsyncLocal<DataFilterState> _filter;
39+
40+
public DataFilter() => _filter = new AsyncLocal<DataFilterState>();
41+
42+
public bool Enabled
43+
{
44+
get
45+
{
46+
_filter.Value ??= new DataFilterState(true);
47+
48+
return _filter.Value!.Enabled;
49+
}
50+
}
51+
52+
public IDisposable Enable()
53+
{
54+
if (Enabled)
55+
return NullDisposable.Instance;
56+
57+
_filter.Value!.Enabled = true;
58+
59+
return new DisposeAction(() => Disable());
60+
}
61+
62+
public IDisposable Disable()
63+
{
64+
if (!Enabled)
65+
return NullDisposable.Instance;
66+
67+
_filter.Value!.Enabled = false;
68+
69+
return new DisposeAction(() => Enable());
70+
}
71+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright (c) MASA Stack All rights reserved.
2+
// Licensed under the MIT License. See LICENSE.txt in the project root for license information.
3+
4+
namespace Masa.Contrib.Data.Contracts.EF.DataFiltering;
5+
6+
public class SoftDeleteSaveChangesFilter<TDbContext> : ISaveChangesFilter where TDbContext : DbContext
7+
{
8+
private readonly TDbContext _context;
9+
private readonly MasaDbContextOptions<TDbContext> _masaDbContextOptions;
10+
11+
public SoftDeleteSaveChangesFilter(MasaDbContextOptions<TDbContext> masaDbContextOptions, TDbContext dbContext)
12+
{
13+
_masaDbContextOptions = masaDbContextOptions;
14+
_context = dbContext;
15+
}
16+
17+
public void OnExecuting(ChangeTracker changeTracker)
18+
{
19+
if (!_masaDbContextOptions.EnableSoftDelete)
20+
return;
21+
22+
changeTracker.DetectChanges();
23+
foreach (var entity in changeTracker.Entries().Where(entry => entry.State == EntityState.Deleted))
24+
{
25+
if (entity.Entity is ISoftDelete)
26+
{
27+
HandleNavigationEntry(entity.Navigations.Where(n => !((IReadOnlyNavigation)n.Metadata).IsOnDependent));
28+
29+
entity.State = EntityState.Modified;
30+
entity.CurrentValues[nameof(ISoftDelete.IsDeleted)] = true;
31+
}
32+
}
33+
}
34+
35+
protected virtual void HandleNavigationEntry(IEnumerable<NavigationEntry> navigationEntries)
36+
{
37+
foreach (var navigationEntry in navigationEntries)
38+
{
39+
if (navigationEntry is CollectionEntry collectionEntry)
40+
{
41+
foreach (var dependentEntry in collectionEntry.CurrentValue ?? new List<object>())
42+
{
43+
HandleDependent(dependentEntry);
44+
}
45+
}
46+
else
47+
{
48+
var dependentEntry = navigationEntry.CurrentValue;
49+
if (dependentEntry != null)
50+
{
51+
HandleDependent(dependentEntry);
52+
}
53+
}
54+
}
55+
}
56+
57+
protected virtual void HandleDependent(object dependentEntry)
58+
{
59+
var entityEntry = _context.Entry(dependentEntry);
60+
entityEntry.State = EntityState.Modified;
61+
62+
if (entityEntry.Entity is ISoftDelete)
63+
entityEntry.CurrentValues[nameof(ISoftDelete.IsDeleted)] = true;
64+
}
65+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) MASA Stack All rights reserved.
2+
// Licensed under the MIT License. See LICENSE.txt in the project root for license information.
3+
4+
namespace Masa.Contrib.Data.Contracts.EF.Internal;
5+
6+
internal class DataFilterState
7+
{
8+
public bool Enabled { get; set; }
9+
10+
public DataFilterState(bool enabled)
11+
{
12+
Enabled = enabled;
13+
}
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (c) MASA Stack All rights reserved.
2+
// Licensed under the MIT License. See LICENSE.txt in the project root for license information.
3+
4+
namespace Masa.Contrib.Data.Contracts.EF.Internal;
5+
6+
internal class DisposeAction : IDisposable
7+
{
8+
private readonly Action _action;
9+
10+
public DisposeAction(Action action) => _action = action;
11+
12+
public void Dispose() => _action.Invoke();
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright (c) MASA Stack All rights reserved.
2+
// Licensed under the MIT License. See LICENSE.txt in the project root for license information.
3+
4+
namespace Masa.Contrib.Data.Contracts.EF.Internal;
5+
6+
internal class InstanceBuilder
7+
{
8+
internal delegate object InvokeDelegate(params object[] parameters);
9+
10+
public static InvokeDelegate CreateInstanceDelegate(ConstructorInfo constructorInfo)
11+
{
12+
ParameterInfo[] parameters = constructorInfo.GetParameters();
13+
var parameterParameterExpression = Expression.Parameter(typeof(object[]), "parameters");
14+
15+
var parameterCast = new List<Expression>(parameters.Length);
16+
for (int i = 0; i < parameters.Length; i++)
17+
{
18+
var paramInfo = parameters[i];
19+
var valueObj = Expression.ArrayIndex(parameterParameterExpression, Expression.Constant(i));
20+
var valueCast = Expression.Convert(valueObj, paramInfo.ParameterType);
21+
parameterCast.Add(valueCast);
22+
}
23+
NewExpression newExp = Expression.New(constructorInfo, parameterCast);
24+
var lambdaExp = Expression.Lambda<InvokeDelegate>(newExp, parameterParameterExpression);
25+
return lambdaExp.Compile();
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (c) MASA Stack All rights reserved.
2+
// Licensed under the MIT License. See LICENSE.txt in the project root for license information.
3+
4+
namespace Masa.Contrib.Data.Contracts.EF.Internal;
5+
6+
internal class NullDisposable : IDisposable
7+
{
8+
public static NullDisposable Instance { get; } = new();
9+
10+
public void Dispose()
11+
{
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<TargetFramework>net6.0</TargetFramework>
@@ -7,13 +7,13 @@
77
</PropertyGroup>
88

99
<ItemGroup>
10-
<PackageReference Include="Masa.Utils.Data.EntityFrameworkCore" Version="0.4.0-preview.4" />
11-
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.0" />
12-
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.0" />
10+
<ProjectReference Include="..\..\BuildingBlocks\MASA.BuildingBlocks\src\Data\Masa.BuildingBlocks.Data.Contracts\Masa.BuildingBlocks.Data.Contracts.csproj" />
11+
<ProjectReference Include="..\Masa.Contrib.Data.EntityFrameworkCore\Masa.Contrib.Data.EntityFrameworkCore.csproj" />
1312
</ItemGroup>
1413

1514
<ItemGroup>
16-
<ProjectReference Include="..\..\BuildingBlocks\MASA.BuildingBlocks\src\Data\Masa.BuildingBlocks.Data.Contracts\Masa.BuildingBlocks.Data.Contracts.csproj" />
17-
<ProjectReference Include="..\..\BuildingBlocks\MASA.BuildingBlocks\src\Data\Masa.BuildingBlocks.Data.UoW\Masa.BuildingBlocks.Data.UoW.csproj" />
15+
<PackageReference Include="Masa.Utils.Caching.Memory" Version="0.4.0-preview.4" />
16+
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
1817
</ItemGroup>
18+
1919
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright (c) MASA Stack All rights reserved.
2+
// Licensed under the MIT License. See LICENSE.txt in the project root for license information.
3+
4+
namespace Masa.Contrib.Data.Contracts.EF;
5+
6+
public static class MasaDbContextOptionsBuilderExtensions
7+
{
8+
private static readonly List<Type> _types = new();
9+
10+
public static MasaDbContextOptionsBuilder UseFilter(
11+
this MasaDbContextOptionsBuilder masaDbContextOptionsBuilder,
12+
Action<FilterOptions>? options = null)
13+
=> masaDbContextOptionsBuilder.UseFilterCore(false, options);
14+
15+
public static MasaDbContextOptionsBuilder UseTestFilter(
16+
this MasaDbContextOptionsBuilder masaDbContextOptionsBuilder,
17+
Action<FilterOptions>? options = null)
18+
=> masaDbContextOptionsBuilder.UseFilterCore(true, options);
19+
20+
private static MasaDbContextOptionsBuilder UseFilterCore(
21+
this MasaDbContextOptionsBuilder masaDbContextOptionsBuilder,
22+
bool isTest,
23+
Action<FilterOptions>? options = null)
24+
{
25+
var filterOptions = new FilterOptions();
26+
options?.Invoke(filterOptions);
27+
28+
masaDbContextOptionsBuilder.Services.TryAddScoped(typeof(DataFilter<>));
29+
masaDbContextOptionsBuilder.Services.TryAddScoped<IDataFilter, DataFilter>();
30+
31+
if (filterOptions.EnableSoftDelete) masaDbContextOptionsBuilder.UseSoftDelete(isTest);
32+
33+
return masaDbContextOptionsBuilder;
34+
}
35+
36+
private static void UseSoftDelete(this MasaDbContextOptionsBuilder masaDbContextOptionsBuilder, bool isTest = false)
37+
{
38+
if (!isTest)
39+
{
40+
if (_types.Any(type => masaDbContextOptionsBuilder.DbContextType == type))
41+
return;
42+
43+
_types.Add(masaDbContextOptionsBuilder.DbContextType);
44+
}
45+
46+
var masaDbContextOptionsType = typeof(MasaDbContextOptions<>).MakeGenericType(masaDbContextOptionsBuilder.DbContextType);
47+
var softDeleteSaveChangesFilterType =
48+
typeof(SoftDeleteSaveChangesFilter<>).MakeGenericType(masaDbContextOptionsBuilder.DbContextType);
49+
var constructorInfo = softDeleteSaveChangesFilterType.GetConstructors().FirstOrDefault()!;
50+
var invokeDelegate = InstanceBuilder.CreateInstanceDelegate(constructorInfo);
51+
52+
masaDbContextOptionsBuilder.Services.TryAdd(
53+
new ServiceDescriptor(typeof(ISaveChangesFilter),
54+
serviceProvider =>
55+
{
56+
var instance = invokeDelegate.Invoke(
57+
serviceProvider.GetRequiredService(masaDbContextOptionsType),
58+
serviceProvider.GetRequiredService(masaDbContextOptionsBuilder.DbContextType));
59+
return instance;
60+
},
61+
ServiceLifetime.Scoped));
62+
63+
masaDbContextOptionsBuilder.EnableSoftDelete = true;
64+
}
65+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) MASA Stack All rights reserved.
2+
// Licensed under the MIT License. See LICENSE.txt in the project root for license information.
3+
4+
namespace Masa.Contrib.Data.Contracts.EF.Options;
5+
6+
public class FilterOptions
7+
{
8+
/// <summary>
9+
/// enable soft delete
10+
/// default: true
11+
/// If you are sure that you do not need to use soft delete in the project, you can change to false
12+
/// IDataFilter does not support ISoftDelete when soft delete is disabled
13+
/// </summary>
14+
public bool EnableSoftDelete { get; set; } = true;
15+
}

src/Data/Masa.Contrib.Data.Contracts.EF/README.md

-3
This file was deleted.

src/Data/Masa.Contrib.Data.Contracts.EF/README.zh-CN.md

-3
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright (c) MASA Stack All rights reserved.
2+
// Licensed under the MIT License. See LICENSE.txt in the project root for license information.
3+
4+
global using Masa.BuildingBlocks.Data.Contracts.DataFiltering;
5+
global using Masa.Contrib.Data.Contracts.EF.DataFiltering;
6+
global using Masa.Contrib.Data.Contracts.EF.Internal;
7+
global using Masa.Contrib.Data.Contracts.EF.Options;
8+
global using Masa.Contrib.Data.EntityFrameworkCore;
9+
global using Masa.Contrib.Data.EntityFrameworkCore.Filters;
10+
global using Masa.Utils.Caching.Memory;
11+
global using Microsoft.EntityFrameworkCore;
12+
global using Microsoft.EntityFrameworkCore.ChangeTracking;
13+
global using Microsoft.EntityFrameworkCore.Metadata;
14+
global using Microsoft.Extensions.DependencyInjection;
15+
global using Microsoft.Extensions.DependencyInjection.Extensions;
16+
global using System.Linq.Expressions;
17+
global using System.Reflection;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) MASA Stack All rights reserved.
2+
// Licensed under the MIT License. See LICENSE.txt in the project root for license information.
3+
4+
namespace Masa.Contrib.Data.EntityFrameworkCore.Cosmos.Internal;
5+
6+
internal static class Parser
7+
{
8+
public static Dictionary<string, string> ToDictionary(this string connectionString)
9+
{
10+
if (string.IsNullOrEmpty(connectionString))
11+
throw new ArgumentException("Cosmos: empty database connection string", nameof(connectionString));
12+
13+
Dictionary<string, string> dictionary = new();
14+
foreach (var item in connectionString.Split(';'))
15+
{
16+
if (string.IsNullOrEmpty(item))
17+
continue;
18+
19+
if (item.Split('=').Length != 2)
20+
throw new ArgumentException("Cosmos: Bad database connection string");
21+
22+
dictionary.Add(item.Split('=')[0], item.Split('=')[1]);
23+
}
24+
return dictionary;
25+
}
26+
}

0 commit comments

Comments
 (0)