Skip to content

Commit 52482ca

Browse files
committed
Merge branch 'release/v5.1.0'
2 parents 1d43d04 + bf0db20 commit 52482ca

12 files changed

Lines changed: 328 additions & 6 deletions

File tree

README.md

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,25 @@
55
![GitHub Release Date](https://img.shields.io/github/release-date/wissance/WebApiToolkit)
66
![GitHub release (latest by date)](https://img.shields.io/github/downloads/wissance/WebApiToolkit/v5.0.0/total?style=plastic)
77

8-
## 10 Lines of code = Full CRUD and even BULK with swagger docs
8+
![WebApiToolkit helps to build application easily](./img/logo_v4.0.0_256x256.jpg)
9+
10+
## Simplified mode : One Line of code for Full CRUD Controller with swagger
11+
12+
![1 line to add controller](./img/1lineadd.gif)
13+
14+
to add in one line, for example i break it to `Assembly` get and add Controller, i.e.:
15+
1. Generate assembly:
16+
```csharp
17+
Assembly stationControllerAssembly = services.AddSimplifiedAutoController<StationEntity, Guid, EmptyAdditionalFilters>(
18+
provider.GetRequiredService<ModelContext>(), "Station",
19+
ControllerType.FullCrud, null, provider.GetRequiredService<ILoggerFactory>());
20+
```
21+
2. Add Controller from assembly:
22+
```csharp
23+
services.AddControllers().AddApplicationPart(stationControllerAssembly).AddControllersAsServices();
24+
```
25+
26+
## 10 Lines of code to create fully manageable Full CRUD and even BULK with swagger docs
927

1028
This ultimate lib helps to build `REST API` with `C#` and `AspNet` easier than writing it from scratch over and over in different projects. It helps to build a consistent API (with the same `REST` routes approach for different controllers) with minimal amount of code: the minimal REST controller contains **10 lines of code** with full *auto* support for all `CRUD` and `BULK` operations.
1129

@@ -14,8 +32,6 @@ For the easiest way you only need:
1432
2. DbContext with `DbSets`
1533
3. Inject from DI Manager Class on startup level.
1634

17-
![WebApiToolkit helps to build application easily](./img/logo_v4.0.0_256x256.jpg)
18-
1935
* [1. Key Features](#1-key-features)
2036
* [2. API Contract](#2-api-contract)
2137
* [3. Requirements](#3-requirements)
@@ -58,8 +74,7 @@ Key concepts:
5874
- basic read controller (`BasicReadController`) contains 2 methods:
5975
- `GET /api/[controller]/?[page={page}&size={size}&sort={sort}&order={order}]` to get `PagedDataDto<T>`
6076
now we also have possibility to send **ANY number of query params**, you just have to pass filter func to `EfModelManager` or do it in your own way like in [WeatherControl example with edgedb](https://github.com/Wissance/WeatherControl/blob/master/WeatherControl/Wissance.WeatherControl.WebApi.V2/Helpers/EqlResolver.cs). We also pass sort (column name) && order (`asc` or `desc`) to manager classes,
61-
`EfModelManager` allows to sort **by any column**.
62-
<strike> Unfortunately here we have a ***ONE disadvantage*** - **we should override `Swagger` info to show query parameters usage!!!** </strike> Starting from `1.6.0` it is possible to see all parameters in `Swagger` and use them.
77+
`EfModelManager` allows to sort **by any column**. Starting from `1.6.0` it is possible to see all query parameters in `Swagger` and use them (`IReadFilterable`).
6378
- `GET /api/[controller]/{id}` to get one object by `id`
6479
- full `CRUD` controller (`BasicCrudController`) = basic read controller (`BasicReadController`) + `Create`, `Update` and `Delete` operations :
6580
- `POST /api/[controller]` - for new object creation
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
using System.Security.Cryptography;
2+
using Wissance.WebApiToolkit.Data.Entity;
3+
using Wissance.WebApiToolkit.Ef.Factories;
4+
5+
namespace Wissance.WebApiToolkit.Ef.Tests.Factories
6+
{
7+
public class User : IModelIdentifiable<Guid>
8+
{
9+
public string Name { get; set; }
10+
public Guid Id { get; set; }
11+
public virtual IList<Profile> Profiles { get; set; }
12+
public Guid OrganizationId { get; set; }
13+
public virtual Organization Organization {get; set; }
14+
}
15+
16+
public class Profile : IModelIdentifiable<Guid>
17+
{
18+
public Guid Id { get; set; }
19+
public string Name { get; set; }
20+
public virtual IList<User> Users { get; set; }
21+
}
22+
23+
public class Organization : IModelIdentifiable<Guid>
24+
{
25+
public Guid Id { get; set; }
26+
public string Name { get; set; }
27+
public virtual IList<User> Users { get; set; }
28+
}
29+
30+
public class TestPassFactory
31+
{
32+
[Fact]
33+
public void TestCreateWithCyclingDepsRemove()
34+
{
35+
Organization org = new Organization()
36+
{
37+
Id = Guid.NewGuid(),
38+
Name = "test_org"
39+
};
40+
User user1 = new User()
41+
{
42+
Id = Guid.NewGuid(),
43+
Name = "Ivan Ivanov",
44+
OrganizationId = org.Id,
45+
Organization = org
46+
};
47+
User user2 = new User()
48+
{
49+
Id = Guid.NewGuid(),
50+
Name = "Petr Petrov",
51+
OrganizationId = org.Id,
52+
Organization = org
53+
};
54+
Profile regularProfile = new Profile()
55+
{
56+
Id = Guid.NewGuid(),
57+
Name = "User",
58+
Users = new List<User>() {user1, user2}
59+
};
60+
Profile adminProfile = new Profile()
61+
{
62+
Id = Guid.NewGuid(),
63+
Name = "Admin",
64+
Users = new List<User>() {user2}
65+
};
66+
user1.Profiles = new List<Profile>() {regularProfile};
67+
user2.Profiles = new List<Profile>() {regularProfile, adminProfile};
68+
69+
org.Users = new List<User>() {user1, user2};
70+
71+
User user1Repr = PassFactory.Create(user1);
72+
Assert.Null(user1Repr.Profiles);
73+
Assert.Null(user1Repr.Organization);
74+
User user2Repr = PassFactory.Create(user2);
75+
Assert.Null(user2Repr.Profiles);
76+
Assert.Null(user2Repr.Organization);
77+
}
78+
}
79+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net6.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<IsPackable>false</IsPackable>
8+
<DebugType>portable</DebugType>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<PackageReference Include="coverlet.collector" Version="6.0.2" />
13+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
14+
<PackageReference Include="xunit" Version="2.9.2" />
15+
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
16+
</ItemGroup>
17+
18+
<ItemGroup>
19+
<Using Include="Xunit" />
20+
</ItemGroup>
21+
22+
<ItemGroup>
23+
<ProjectReference Include="..\Wissance.WebApiToolkit.Data\Wissance.WebApiToolkit.Data.csproj" />
24+
<ProjectReference Include="..\Wissance.WebApiToolkit.Ef\Wissance.WebApiToolkit.Ef.csproj" />
25+
</ItemGroup>
26+
27+
</Project>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using Wissance.WebApiToolkit.Core.Controllers;
2+
using Wissance.WebApiToolkit.Core.Data;
3+
using Wissance.WebApiToolkit.Core.Managers;
4+
5+
namespace Wissance.WebApiToolkit.Ef.Controllers
6+
{
7+
public class GenericBulkController<TRes, TData, TId, TFilter>: BasicBulkCrudController<TRes, TData, TId, TFilter>
8+
where TRes : class
9+
where TFilter: class, IReadFilterable
10+
{
11+
public GenericBulkController(IModelManager<TRes, TData, TId> manager)
12+
{
13+
Manager = manager;
14+
}
15+
}
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using Wissance.WebApiToolkit.Core.Controllers;
2+
using Wissance.WebApiToolkit.Core.Data;
3+
using Wissance.WebApiToolkit.Core.Managers;
4+
5+
namespace Wissance.WebApiToolkit.Ef.Controllers
6+
{
7+
public class GenericCrudController<TRes, TData, TId, TFilter> : BasicCrudController<TRes, TData, TId, TFilter>
8+
where TRes : class
9+
where TFilter: class, IReadFilterable
10+
{
11+
public GenericCrudController(IModelManager<TRes, TData, TId> manager)
12+
{
13+
Manager = manager;
14+
}
15+
}
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using Wissance.WebApiToolkit.Core.Controllers;
2+
using Wissance.WebApiToolkit.Core.Data;
3+
using Wissance.WebApiToolkit.Core.Managers;
4+
5+
namespace Wissance.WebApiToolkit.Ef.Controllers
6+
{
7+
public class GenericReadOnlyController<TRes, TData, TId, TFilter> : BasicReadController<TRes, TData, TId, TFilter>
8+
where TRes : class
9+
where TFilter: class, IReadFilterable
10+
{
11+
public GenericReadOnlyController(IModelManager<TRes, TData, TId> manager)
12+
{
13+
Manager = manager;
14+
}
15+
}
16+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Reflection;
4+
using Microsoft.EntityFrameworkCore;
5+
using Microsoft.Extensions.DependencyInjection;
6+
using Microsoft.Extensions.Logging;
7+
using Wissance.WebApiToolkit.Core.Data;
8+
using Wissance.WebApiToolkit.Data.Entity;
9+
using Wissance.WebApiToolkit.Ef.Factories;
10+
using Wissance.WebApiToolkit.Ef.Generators;
11+
12+
namespace Wissance.WebApiToolkit.Ef.Extensions
13+
{
14+
public static class ServiceCollectionExtensions
15+
{
16+
public static Assembly AddSimplifiedAutoController<TObj, TId, TFilter>(this IServiceCollection services,
17+
DbContext context, string resourceName, ControllerType controllerType,
18+
Func<TObj, IDictionary<string, string>, bool> filterFunc,
19+
ILoggerFactory loggerFactory)
20+
where TObj: class, IModelIdentifiable<TId>
21+
where TId: IComparable
22+
where TFilter: class, IReadFilterable
23+
{
24+
string assemblyName = $"AutoGenerated{resourceName}Controller";
25+
services.AddScoped(sp =>
26+
{
27+
return SimplifiedEfBasedManagerFactory.Create<TObj, TId>(context, filterFunc, loggerFactory);
28+
});
29+
30+
Tuple<Assembly, string> controllerAssembly = OnTheFlyServicesGenerator.GenerateController<TObj, TId, TFilter>(assemblyName, resourceName, false, controllerType);
31+
32+
return controllerAssembly.Item1;
33+
}
34+
}
35+
}

Wissance.WebApiToolkit/Wissance.WebApiToolkit.Ef/Factories/PassFactory.cs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
24
using System.Reflection;
35
using System.Security.Permissions;
46

@@ -8,11 +10,26 @@ public static class PassFactory
810
{
911
public static T Create<T>(T item)
1012
{
11-
return item;
13+
T res = item;
14+
PropertyInfo[] properties = item.GetType().GetProperties(BindingFlags.DeclaredOnly |
15+
BindingFlags.Public |
16+
BindingFlags.Instance);
17+
IList<PropertyInfo> virtProperties = properties.Where(p => p.GetAccessors()[0].IsVirtual &&
18+
!p.PropertyType.IsPrimitive &&
19+
p.PropertyType.IsClass || p.PropertyType.IsInterface).ToList();
20+
21+
foreach (PropertyInfo virtProperty in virtProperties)
22+
{
23+
virtProperty.SetValue(res, null);
24+
// object propertyValue = virtProperty.GetValue(res);
25+
// SetNullForVirtual(propertyValue);
26+
}
27+
return res;
1228
}
1329

1430
// todo(UMV): move Generic on a class level and init properties via static constructor
1531
public static void UpdateAll<T, TId>(T item, TId id, T updatingItem)
32+
where T : class
1633
{
1734
Type objType = typeof(T);
1835
PropertyInfo[] properties = objType.GetProperties();
@@ -21,5 +38,20 @@ public static void UpdateAll<T, TId>(T item, TId id, T updatingItem)
2138
prop.SetValue(updatingItem, prop.GetValue(item));
2239
}
2340
}
41+
42+
private static void SetNullForVirtual<T>(T obj)
43+
{
44+
PropertyInfo[] properties = obj.GetType().GetProperties(BindingFlags.DeclaredOnly |
45+
BindingFlags.Public |
46+
BindingFlags.Instance);
47+
IList<PropertyInfo> virtProperties = properties.Where(p => p.GetAccessors()[0].IsVirtual &&
48+
!p.PropertyType.IsPrimitive &&
49+
p.PropertyType.IsClass || p.PropertyType.IsInterface).ToList();
50+
51+
foreach (PropertyInfo virtProperty in virtProperties)
52+
{
53+
virtProperty.SetValue(obj, null);
54+
}
55+
}
2456
}
2557
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace Wissance.WebApiToolkit.Ef.Generators
2+
{
3+
public enum ControllerType
4+
{
5+
ReadOnly = 1,
6+
FullCrud = 2,
7+
Bulk = 3
8+
}
9+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using System;
2+
using System.Reflection;
3+
using System.Reflection.Emit;
4+
using Wissance.WebApiToolkit.Core.Data;
5+
using Wissance.WebApiToolkit.Core.Managers;
6+
using Wissance.WebApiToolkit.Data.Entity;
7+
using Wissance.WebApiToolkit.Ef.Controllers;
8+
9+
namespace Wissance.WebApiToolkit.Ef.Generators
10+
{
11+
internal static class OnTheFlyServicesGenerator
12+
{
13+
public static Tuple<Assembly, string> GenerateController<TObj, TId, TFilter>(string assemblyName, string controllerName,
14+
bool saveOnDisk, ControllerType controllerType)
15+
where TObj: class, IModelIdentifiable<TId>
16+
where TId: IComparable
17+
where TFilter: class, IReadFilterable
18+
{
19+
try
20+
{
21+
// todo(UMV): combine controllers in one assembly
22+
AssemblyName name = new AssemblyName(assemblyName);
23+
AssemblyBuilderAccess access = saveOnDisk ? AssemblyBuilderAccess.RunAndCollect : AssemblyBuilderAccess.Run;
24+
AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(name, access);
25+
26+
string fullControllerName = $"{controllerName}Controller";
27+
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule($"AutoGenerated{fullControllerName}");
28+
Type basicControllerType = GetAppropriateBaseControllerType<TObj, TId, TFilter>(controllerType);
29+
TypeBuilder typeBuilder = moduleBuilder.DefineType(fullControllerName,
30+
TypeAttributes.Public | TypeAttributes.Class, basicControllerType);
31+
ConstructorBuilder constructorBuilder = typeBuilder.DefineConstructor(
32+
MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName,
33+
CallingConventions.Standard,
34+
new Type[] {typeof(IModelManager<TObj, TObj, TId>)});
35+
ILGenerator ilGenerator = constructorBuilder.GetILGenerator();
36+
// load this
37+
ilGenerator.Emit(OpCodes.Ldarg_0);
38+
// load arg0 -> manager
39+
ilGenerator.Emit(OpCodes.Ldarg_1);
40+
ConstructorInfo baseCtor = basicControllerType.GetConstructor(new []{typeof(IModelManager<TObj, TObj, TId>)});
41+
ilGenerator.Emit(OpCodes.Call, baseCtor);
42+
ilGenerator.Emit(OpCodes.Ret); // Return from the constructor
43+
Type t = typeBuilder.CreateType();
44+
Assembly controllerAssembly = Assembly.GetAssembly(t);
45+
return new Tuple<Assembly, string>(controllerAssembly, String.Empty);
46+
}
47+
catch (Exception e)
48+
{
49+
return new Tuple<Assembly, string>(null, e.Message);
50+
}
51+
}
52+
53+
private static Type GetAppropriateBaseControllerType<TObj, TId, TFilter>(ControllerType controllerType)
54+
where TObj: class, IModelIdentifiable<TId>
55+
where TId: IComparable
56+
where TFilter: class, IReadFilterable
57+
{
58+
switch (controllerType)
59+
{
60+
case ControllerType.ReadOnly:
61+
return typeof(GenericReadOnlyController<TObj, TObj, TId, TFilter>);
62+
case ControllerType.FullCrud:
63+
return typeof(GenericCrudController<TObj, TObj, TId, TFilter>);
64+
case ControllerType.Bulk:
65+
return typeof(GenericBulkController<TObj, TObj, TId, TFilter>);
66+
default:
67+
return typeof(GenericCrudController<TObj, TObj, TId, TFilter>);
68+
}
69+
}
70+
}
71+
}

0 commit comments

Comments
 (0)