diff --git a/.github/workflows/project-regular-checks.yml b/.github/workflows/project-regular-checks.yml index e1f49cea0..e729dc3c6 100644 --- a/.github/workflows/project-regular-checks.yml +++ b/.github/workflows/project-regular-checks.yml @@ -43,7 +43,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: 7 + dotnet-version: 8 - name: Build Solution run: dotnet build -c Release test: @@ -58,7 +58,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: 7 + dotnet-version: 8 - name: Test if: ${{ matrix.os != 'ubuntu-latest' }} run: dotnet test -c Release diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4791e9732..40fd376c8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -67,7 +67,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: 7 + dotnet-version: 8 - name: Pack run: | dotnet pack -o "packages" -c Release @@ -76,6 +76,7 @@ jobs: with: name: packages path: packages/ + push-packages: name: Push Packages needs: pack-projects @@ -118,6 +119,7 @@ jobs: name: docs path: docs/.theme/.output/public if-no-files-found: error + publish-docs: needs: generate-docs name: Publish Docs diff --git a/.github/workflows/samples-regular-checks.yml b/.github/workflows/samples-regular-checks.yml index df2b1df38..8a6ba78b9 100644 --- a/.github/workflows/samples-regular-checks.yml +++ b/.github/workflows/samples-regular-checks.yml @@ -32,7 +32,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: 7 + dotnet-version: 8 - name: samples/event-scheduler working-directory: samples/event-scheduler run: | diff --git a/Directory.Build.props b/Directory.Build.props index e5fd27359..acdc03046 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,8 +1,8 @@ - net7.0 - 11.0 + net8.0 + 12.0 true enable enable diff --git a/Directory.Packages.props b/Directory.Packages.props index 4e47fe041..04cbfff77 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,20 +8,22 @@ - + - - - - - + + + + + + + - - + + - + diff --git a/Makefile b/Makefile index e56f99290..587cfdd33 100644 --- a/Makefile +++ b/Makefile @@ -22,3 +22,6 @@ run: fi test: dotnet test + +# dotnet test -c Release --collect:"XPlat Code Coverage" --logger trx --results-directory .coverage --settings test/runsettings.xml +# reportgenerator -reports:.coverage\0d84daea-0041-4f8d-a93c-51d3d348fa69\coverage.cobertura.xml;.coverage\d606db4f-8ea5-4e9f-a304-f37b22a1f34b\coverage.cobertura.xml -targetdir:.coverage/report \ No newline at end of file diff --git a/docs/.theme/package-lock.json b/docs/.theme/package-lock.json index 0ae14ca55..e1b169096 100644 --- a/docs/.theme/package-lock.json +++ b/docs/.theme/package-lock.json @@ -12,7 +12,7 @@ "devDependencies": { "@mermaid-js/mermaid-cli": "^10.5.1", "@nuxt/content": "^2.8.5", - "@nuxt/devtools": "latest", + "@nuxt/devtools": "*", "@nuxtjs/eslint-config-typescript": "^12.1.0", "@pinia/nuxt": "^0.4.11", "eslint": "^8.51.0", @@ -6677,6 +6677,20 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -17338,9 +17352,9 @@ } }, "node_modules/vite": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz", - "integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.1.tgz", + "integrity": "sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==", "dev": true, "dependencies": { "esbuild": "^0.18.10", diff --git a/docs/architecture/feature.md b/docs/architecture/feature.md index 0c86b2272..3c5488e7b 100644 --- a/docs/architecture/feature.md +++ b/docs/architecture/feature.md @@ -154,12 +154,9 @@ below; `WelcomePageGreetingFeature.cs` ```csharp -public class WelcomePageGreetingFeature : IFeature +public class WelcomePageGreetingFeature(string _path) + : IFeature { - readonly string _path; - - public WelcomePageGreetingFeature(string path) => _path = path; - public void Configure(LayerConfigurator configurator) { configurator.ConfigureApplicationBuilder(app => diff --git a/docs/architecture/layer.md b/docs/architecture/layer.md index 986766978..303eca4ae 100644 --- a/docs/architecture/layer.md +++ b/docs/architecture/layer.md @@ -160,10 +160,9 @@ is `PhaseOrder.Normal`. If you need to change the order, pass the desired order to this parameter as shown below; ```csharp -public class DoThisEarlyOn : PhaseBase -{ - public DoThisEarlyOn() : base(PhaseOrder.Early) { } -} +public class DoThisEarlyOn() + : PhaseBase(PhaseOrder.Early) +{ } ``` ## Providing Configuration diff --git a/docs/blueprints/service.md b/docs/blueprints/service.md index eb3cc1278..04b8c5ce5 100644 --- a/docs/blueprints/service.md +++ b/docs/blueprints/service.md @@ -36,17 +36,18 @@ Layers in this blueprint are; Features with default options are; -| Features | Run | Test | Required | -| ------------------ | ----------- | --------------- | -------- | -| Business | Default | Default | Yes | -| Core | Dotnet | Mock | | -| Database | Sqlite | InMemory | Yes | -| Documentation | Default | | | -| Exception Handling | Default | | | -| Greeting | Hello World | | | -| Logging | Request | | | -| Mocking Overrider | | First Interface | | -| Orm | Default | Default | | +| Features | Run | Test | Required | +| ------------------ | ------------- | --------------- | -------- | +| Business | Default | Default | Yes | +| Caching | Scoped Memory | Scoped Memory | | +| Core | Dotnet | Mock | | +| Database | Sqlite | InMemory | Yes | +| Documentation | Default | | | +| Exception Handling | Default | | | +| Greeting | Hello World | | | +| Logging | Request | | | +| Mocking Overrider | | First Interface | | +| Orm | Default | Default | | Phase execution order; diff --git a/docs/features/caching.md b/docs/features/caching.md new file mode 100644 index 000000000..aa76002ea --- /dev/null +++ b/docs/features/caching.md @@ -0,0 +1,18 @@ +# Caching + +Implementations of this feature provides predefined caching behaviour. + +Add this feature using `AddCaching()` extension; + +```csharp +app.Features.AddCaching(...); +``` + +## Scoped Memory + +This feature implementation registers `Func` factory with scoped +`MemoryCache` implementation to provide request in-memory caching. + +```csharp +c => c.ScopedMemory() +``` diff --git a/docs/features/core.md b/docs/features/core.md index f93b4a74e..69b8d742a 100644 --- a/docs/features/core.md +++ b/docs/features/core.md @@ -1,6 +1,6 @@ # Core -This feature registers `ISystem` to services. +This feature registers `TimeProvider.System` to services. Add this feature using `AddCore()` extension; @@ -10,8 +10,8 @@ app.Features.AddCore(...); ## Dotnet -Adds a local implementation of `ISystem` interface to services to be used -throughout your application. +Adds a local implementation of `TimeProvider` to services to be used throughout +your application. ```csharp c => c.Dotnet() diff --git a/docs/features/exception-handling.md b/docs/features/exception-handling.md index 8101650e5..2a1ee7a54 100644 --- a/docs/features/exception-handling.md +++ b/docs/features/exception-handling.md @@ -11,5 +11,5 @@ app.Features.AddExceptionHandling(...); Adds default exception handler. ```csharp -c => c.Default() +c => c.Default(typeUrlFormat: "https://my-service.com/errors/{0}") ``` diff --git a/docs/release-notes/v0-4.md b/docs/release-notes/v0-4.md index 53c0bd50a..5fe2e152c 100644 --- a/docs/release-notes/v0-4.md +++ b/docs/release-notes/v0-4.md @@ -1,5 +1,12 @@ # v0.4 +## v0.4.3 + +### Improvements + +- `CommitAsync` now accepts nullable parameters +- Added generic overload for `MockMe.ASetting()` + ## v0.4.2 ### Improvements @@ -7,7 +14,7 @@ - Improved exception message when there is no valid json in the database. - Added `GetRequiredValue` extension to `IConfiguration` with default value option -- Mock `IConfiguration` return values for not defined settings can now be +- Mock `IConfiguration` return values for not defined settings can now be configured by overriding `ServiceSpec.GetDefaultSettingsValue()` ### Bugfixes @@ -93,4 +100,4 @@ Visit [Regular expressions][] for more details. | NUnit | 3.13.3 | 3.14.0 | [Central Package Management]: https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-7#central-package-management -[Regular expressions]: https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-7#regular-expressions \ No newline at end of file +[Regular expressions]: https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-7#regular-expressions diff --git a/docs/release-notes/v0-5.md b/docs/release-notes/v0-5.md new file mode 100644 index 000000000..33767228b --- /dev/null +++ b/docs/release-notes/v0-5.md @@ -0,0 +1,20 @@ +# v0.5 + +## v0.5.0 + +### Features + +- Beta features are available in do-blueprints-service package; + - `Caching` feature is now added with `ScopedMemory` implementation + +### Improvements + +- Added `CustomMySQL57Dialect` for enabling `varchar` column type with _1023_ + max capacity +- Added `TransientWithFactory` and + `ScopedWithFactory` extensions for registering + services with implementations +- Added new ServiceSpec extensions: + - `AnInteger` extension for `Stubber` + - `AMemoryCache` extension for `Stubber` + - `ShouldHaveCount` extension for `IMemoryCache` diff --git a/docs/release-notes/v0-6.md b/docs/release-notes/v0-6.md new file mode 100644 index 000000000..d27c83613 --- /dev/null +++ b/docs/release-notes/v0-6.md @@ -0,0 +1,87 @@ +# v0.6 + +## v0.6.0 + +### .NET Upgrade + +DO now supports .NET 8! Below you can find a task list to upgrade your projects. + +```markdown +- [ ] Upgrade .NET and C# versions + - [ ] in projects + - [ ] in docker files + - [ ] in GitHub workflows +- [ ] Upgrade DO version +- [ ] Syntax improvements + - [ ] Use primary constructors + - [ ] Use collection expressions +- [ ] Use `[FromKeyedServices]` and `[FromServices]` in controllers +``` + +#### Upgrade .NET and C# versions + +- Upgrade the project's `C#` language to `12`. +- Framework version upgrade to `net8.0` in the projects. +- Framework and sdk version upgrade to `8` in `Dockerfile`. +- Upgrade dotnet version `8` in Github actions. + +#### Syntax improvements + +##### Primary constructors + +Use primary constructor, when there is a dependency that needs to be injected at +the constructor without any logic or when the base class constructor is called. +Parameter names start with underscore. + +```csharp +public class Entity(IEntityContext _context) +{ + ... +} +``` + +##### Collection expressions + +Use new and simplified collection expressions, where possible. + +Visit [Collection expression][] for more details. + +#### `[FromServices]` in controllers + +Instead of using constructor injection, use `[FromServices]` attribute to inject +dependencies in controllers. + +```csharp +public Entity Get([FromServices] IQueryContext entityQuery) { } +``` + +### Improvements + +- `ExceptionHandling` now uses + `Microsoft.AspNetCore.Diagnostics.IExceptionHandling` + - `ExceptionHandling` feature now adds `StatusCodePages` middleware. +- Exceptions now return `ProblemDetails` as response +- `ISystem` is replaced with `TimeProvider.System` + - `Mocker`, `TheSystem` extension is renamed to `TheTime` + - Use `FakeTimeProvider` for tests by mocking `TheTime` +- Internal Server Error response included extra details in message, removed + - Extra details can be reached from the logs +- `HandledException` is now abstract + - Added optional `ExtraDetails` collection property to `HandledException` + +### Library Upgrades + +| Package | Old Version | New Version | +| ----------------------------------------------- | ----------- | ----------- | +| Microsoft.AspNetCore.Mvc.NewtonsoftJson | 7.0.13 | 8.0.0 | +| Microsoft.Extensions.Configuration.Abstractions | 7.0.0 | 8.0.0 | +| Microsoft.Extensions.Configuration.Binder | 7.0.0 | 8.0.0 | +| Microsoft.Extensions.Logging.Abstractions | 7.0.0 | 8.0.0 | +| Microsoft.NET.Test.Sdk | 7.0.0 | 8.0.0 | +| Microsoft.Extensions.TimeProvider.Testing | new | 8.0.0 | +| Moq | 4.20.69 | 4.20.70 | +| NHibernate | 5.4.6 | 5.5.0 | +| NUnit | 3.14.0 | 4.0.1 | +| StyleCop.Analyzers.Unstable | 1.2.0.507 | 1.2.0.556 | + +[Collection expression]: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/collection-expressions diff --git a/samples/Directory.Build.props b/samples/Directory.Build.props index 7a81741d9..70b828f55 100644 --- a/samples/Directory.Build.props +++ b/samples/Directory.Build.props @@ -1,8 +1,8 @@ - net7.0 - 11.0 + net8.0 + 12.0 true enable enable diff --git a/samples/event-scheduler/src/Mouseless.EventScheduler.Service/Mouseless.EventScheduler.Service.csproj b/samples/event-scheduler/src/Mouseless.EventScheduler.Service/Mouseless.EventScheduler.Service.csproj index d441c0c60..fe644c1cb 100644 --- a/samples/event-scheduler/src/Mouseless.EventScheduler.Service/Mouseless.EventScheduler.Service.csproj +++ b/samples/event-scheduler/src/Mouseless.EventScheduler.Service/Mouseless.EventScheduler.Service.csproj @@ -1,5 +1,15 @@  + + net8.0 + enable + enable + + + + ..\..\..\..\stylecop.ruleset + + diff --git a/samples/event-scheduler/src/Mouseless.EventScheduler.Service/RestApi/Analyzer/Contact.generated.cs b/samples/event-scheduler/src/Mouseless.EventScheduler.Service/RestApi/Analyzer/Contact.generated.cs index e60edff7c..ef620890c 100644 --- a/samples/event-scheduler/src/Mouseless.EventScheduler.Service/RestApi/Analyzer/Contact.generated.cs +++ b/samples/event-scheduler/src/Mouseless.EventScheduler.Service/RestApi/Analyzer/Contact.generated.cs @@ -8,18 +8,11 @@ namespace Mouseless.EventScheduler; [ApiController] public class ContactController { - readonly IServiceProvider _serviceProvider; - - public ContactController(IServiceProvider serviceProvider) => - _serviceProvider = serviceProvider; - [HttpGet] [Produces("application/json")] [Route("contacts")] - public List All() + public List All([FromServices] Contacts target) { - var target = _serviceProvider.GetRequiredService(); - return target.All(); } @@ -28,11 +21,11 @@ public record NewRequest(string Name); [HttpPost] [Produces("application/json")] [Route("contacts")] - public Contact New([FromBody] NewRequest request) + public Contact New([FromServices] Func newTarget, [FromBody] NewRequest request) { - var target = _serviceProvider.GetRequiredService>(); + var target = newTarget(); - return target().With(request.Name); + return target.With(request.Name); } public record EditRequest(string Name); @@ -40,9 +33,9 @@ public record EditRequest(string Name); [HttpPatch] [Produces("application/json")] [Route("contacts/{id}")] - public Contact Edit([FromRoute] Guid id, [FromBody] EditRequest request) + public Contact Edit([FromServices] IQueryContext contactQuery, [FromRoute] Guid id, [FromBody] EditRequest request) { - var target = _serviceProvider.GetRequiredService>().SingleById(id); + var target = contactQuery.SingleById(id); target.Edit(request.Name); diff --git a/samples/event-scheduler/src/Mouseless.EventScheduler.Service/RestApi/Analyzer/Meeting.generated.cs b/samples/event-scheduler/src/Mouseless.EventScheduler.Service/RestApi/Analyzer/Meeting.generated.cs index 03f6e818d..7c1308cd0 100644 --- a/samples/event-scheduler/src/Mouseless.EventScheduler.Service/RestApi/Analyzer/Meeting.generated.cs +++ b/samples/event-scheduler/src/Mouseless.EventScheduler.Service/RestApi/Analyzer/Meeting.generated.cs @@ -8,29 +8,20 @@ namespace Mouseless.EventScheduler; [ApiController] public class MeetingController { - readonly IServiceProvider _serviceProvider; - - public MeetingController(IServiceProvider serviceProvider) => - _serviceProvider = serviceProvider; - [HttpGet] [Produces("application/json")] [Route("meetings")] - public List By([FromQuery] DateTime? before, [FromQuery] DateTime? after) + public List By([FromServices] Meetings target, [FromQuery] DateTime? before, [FromQuery] DateTime? after) { - var target = _serviceProvider.GetRequiredService(); - return target.By(before, after); } [HttpGet] [Produces("application/json")] [Route("meetings/{id}")] - public Meeting Get([FromRoute] Guid id) + public Meeting Get([FromServices] IQueryContext meetingQuery, [FromRoute] Guid id) { - var target = _serviceProvider.GetRequiredService>(); - - return target.SingleById(id); + return meetingQuery.SingleById(id); } public record NewRequest(string Name, DateTime Date); @@ -38,19 +29,19 @@ public record NewRequest(string Name, DateTime Date); [HttpPost] [Produces("application/json")] [Route("meetings")] - public Meeting New([FromBody] NewRequest request) + public Meeting New([FromServices] Func newTarget, [FromBody] NewRequest request) { - var target = _serviceProvider.GetRequiredService>(); + var target = newTarget(); - return target().With(request.Name, request.Date); + return target.With(request.Name, request.Date); } [HttpGet] [Produces("application/json")] [Route("meetings/{id}/contacts")] - public List GetContacts([FromRoute] Guid id) + public List GetContacts([FromServices] IQueryContext meetingQuery, [FromRoute] Guid id) { - var target = _serviceProvider.GetRequiredService>().SingleById(id); + var target = meetingQuery.SingleById(id); return target.GetContacts(); } @@ -60,20 +51,20 @@ public record AddContactRequest(Guid ContactId); [HttpPost] [Produces("application/json")] [Route("meetings/{id}/contacts")] - public void AddContact([FromRoute] Guid id, [FromBody] AddContactRequest request) + public void AddContact([FromServices] IQueryContext meetingQuery, [FromServices] IQueryContext contactQuery, [FromRoute] Guid id, [FromBody] AddContactRequest request) { - var target = _serviceProvider.GetRequiredService>().SingleById(id); + var target = meetingQuery.SingleById(id); - target.AddContact(_serviceProvider.GetRequiredService>().SingleById(request.ContactId)); + target.AddContact(contactQuery.SingleById(request.ContactId)); } [HttpDelete] [Produces("application/json")] [Route("meetings/{id}")] - public void Delete([FromRoute] Guid id) + public void Delete([FromServices] IQueryContext meetingQuery, [FromRoute] Guid id) { - var target = _serviceProvider.GetRequiredService>(); + var target = meetingQuery.SingleById(id); - target.SingleById(id).Delete(); + target.Delete(); } } \ No newline at end of file diff --git a/samples/event-scheduler/src/Mouseless.EventScheduler.Service/appsettings.Development.json b/samples/event-scheduler/src/Mouseless.EventScheduler.Service/appsettings.Development.json index 0c208ae91..0f7ea7f7e 100644 --- a/samples/event-scheduler/src/Mouseless.EventScheduler.Service/appsettings.Development.json +++ b/samples/event-scheduler/src/Mouseless.EventScheduler.Service/appsettings.Development.json @@ -4,5 +4,10 @@ "Default": "Information", "Microsoft.AspNetCore": "Warning" } + }, + "Database": { + "Sqlite": { + "FileName": "Mouseless.EventScheduler.Service.db" + } } } diff --git a/samples/event-scheduler/src/Mouseless.EventScheduler/Contact.cs b/samples/event-scheduler/src/Mouseless.EventScheduler/Contact.cs index 442e7b89a..421ae64fd 100644 --- a/samples/event-scheduler/src/Mouseless.EventScheduler/Contact.cs +++ b/samples/event-scheduler/src/Mouseless.EventScheduler/Contact.cs @@ -2,15 +2,9 @@ namespace Mouseless.EventScheduler; -public class Contact +public class Contact(IEntityContext _context) { - readonly IEntityContext _context = default!; - - protected Contact() { } - public Contact(IEntityContext entityContext) - { - _context = entityContext; - } + protected Contact() : this(default!) { } public virtual Guid Id { get; protected set; } = default!; public virtual string Name { get; protected set; } = default!; @@ -28,13 +22,8 @@ public virtual void Edit(string name) } } -public class Contacts +public class Contacts(IQueryContext _context) { - readonly IQueryContext _context = default!; - - public Contacts(IQueryContext context) => - _context = context; - public List All() => _context.All(); } \ No newline at end of file diff --git a/samples/event-scheduler/src/Mouseless.EventScheduler/Meeting.cs b/samples/event-scheduler/src/Mouseless.EventScheduler/Meeting.cs index 35a944edf..01ada8407 100644 --- a/samples/event-scheduler/src/Mouseless.EventScheduler/Meeting.cs +++ b/samples/event-scheduler/src/Mouseless.EventScheduler/Meeting.cs @@ -2,19 +2,9 @@ namespace Mouseless.EventScheduler; -public class Meeting +public class Meeting(IEntityContext _context, Func _newMeetingContact, MeetingContacts _meetingContacts) { - readonly IEntityContext _context = default!; - readonly Func _newMeetingContact = default!; - readonly MeetingContacts _meetingContacts = default!; - - protected Meeting() { } - public Meeting(IEntityContext context, Func newMeetingContact, MeetingContacts meetingContacts) - { - _context = context; - _newMeetingContact = newMeetingContact; - _meetingContacts = meetingContacts; - } + protected Meeting() : this(default!, default!, default!) { } public virtual Guid Id { get; protected set; } = default!; public virtual string Name { get; protected set; } = default!; @@ -49,13 +39,8 @@ public virtual void Delete() } } -public class Meetings +public class Meetings(IQueryContext _context) { - readonly IQueryContext _context; - - public Meetings(IQueryContext context) => - _context = context; - public List By( DateTime? before = default, DateTime? after = default, diff --git a/samples/event-scheduler/src/Mouseless.EventScheduler/MeetingContact.cs b/samples/event-scheduler/src/Mouseless.EventScheduler/MeetingContact.cs index dc61dda87..fce521ce9 100644 --- a/samples/event-scheduler/src/Mouseless.EventScheduler/MeetingContact.cs +++ b/samples/event-scheduler/src/Mouseless.EventScheduler/MeetingContact.cs @@ -2,15 +2,9 @@ namespace Mouseless.EventScheduler; -public class MeetingContact +public class MeetingContact(IEntityContext _context) { - readonly IEntityContext _context = default!; - - protected MeetingContact() { } - public MeetingContact(IEntityContext entityContext) - { - _context = entityContext; - } + protected MeetingContact() : this(default!) { } public virtual Guid Id { get; protected set; } = default!; public virtual Meeting Meeting { get; protected set; } = default!; @@ -30,13 +24,8 @@ protected internal virtual void Delete() } } -public class MeetingContacts +public class MeetingContacts(IQueryContext _context) { - IQueryContext _context; - - public MeetingContacts(IQueryContext context) => - _context = context; - internal List ByMeeting(Meeting meeting) => _context.By(mc => mc.Meeting == meeting); } diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 138ec6d38..f92d2083a 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -10,7 +10,7 @@ DO - 0.4.2 + 0.6.0 Mouseless Mouseless Copyright (c) 2023 Mouseless diff --git a/src/blueprints/Do.Blueprints.Service.Application/Business/BusinessExtensions.cs b/src/blueprints/Do.Blueprints.Service.Application/Business/BusinessExtensions.cs index 1d6e5d9c9..3d22550d5 100644 --- a/src/blueprints/Do.Blueprints.Service.Application/Business/BusinessExtensions.cs +++ b/src/blueprints/Do.Blueprints.Service.Application/Business/BusinessExtensions.cs @@ -9,26 +9,40 @@ public static class BusinessExtensions { public static void AddBusiness(this List source, Func> configure) => source.Add(configure(new())); - static readonly MethodInfo _addTransientWithFactory = typeof(BusinessExtensions).GetMethod(nameof(AddTransientWithFactory), 1, new Type[] { typeof(IServiceCollection) }) ?? - throw new Exception("AddTransientWithFactory should have existed"); + static readonly MethodInfo _addTransientWithFactory = typeof(BusinessExtensions).GetMethod(nameof(AddTransientWithFactory), 2, [typeof(IServiceCollection)]) ?? + throw new Exception("AddTransientWithFactory should have existed"); - public static void AddTransientWithFactory(this IServiceCollection source, Type type) => - _addTransientWithFactory.MakeGenericMethod(type).Invoke(null, new object[] { source }); + public static void AddTransientWithFactory(this IServiceCollection source, Type service) => + _addTransientWithFactory.MakeGenericMethod(service, service).Invoke(null, new object[] { source }); + public static void AddTransientWithFactory(this IServiceCollection source, Type service, Type implementation) => + _addTransientWithFactory.MakeGenericMethod(service, implementation).Invoke(null, [source]); - public static void AddTransientWithFactory(this IServiceCollection source) where T : class + public static void AddTransientWithFactory(this IServiceCollection source) where TService : class => + source.AddTransientWithFactory(); + + public static void AddTransientWithFactory(this IServiceCollection source) + where TService : class + where TImplementation : class, TService { - source.AddSingleton>(sp => () => sp.GetRequiredServiceUsingRequestServices()); - source.AddTransient(); + source.AddSingleton>(sp => () => sp.GetRequiredServiceUsingRequestServices()); + source.AddTransient(); } - static readonly MethodInfo _addScopedWithFactory = typeof(BusinessExtensions).GetMethod(nameof(AddScopedWithFactory), 1, new Type[] { typeof(IServiceCollection) }) ?? - throw new Exception("AddScopedWithFactory should have existed"); + static readonly MethodInfo _addScopedWithFactory = typeof(BusinessExtensions).GetMethod(nameof(AddScopedWithFactory), 2, [typeof(IServiceCollection)]) ?? + throw new Exception("AddScopedWithFactory should have existed"); + + public static void AddScopedWithFactory(this IServiceCollection source, Type service) => + _addScopedWithFactory.MakeGenericMethod(service, service).Invoke(null, new object[] { source }); + public static void AddScopedWithFactory(this IServiceCollection source, Type service, Type implementation) => + _addScopedWithFactory.MakeGenericMethod(service, implementation).Invoke(null, [source]); + public static void AddScopedWithFactory(this IServiceCollection source) where TService : class => + source.AddScopedWithFactory(); - public static void AddScopedWithFactory(this IServiceCollection source, Type type) => - _addScopedWithFactory.MakeGenericMethod(type).Invoke(null, new object[] { source }); - public static void AddScopedWithFactory(this IServiceCollection source) where T : class + public static void AddScopedWithFactory(this IServiceCollection source) + where TService : class + where TImplementation : class, TService { - source.AddSingleton>(sp => () => sp.GetRequiredServiceUsingRequestServices()); - source.AddScoped(); + source.AddSingleton>(sp => () => sp.GetRequiredServiceUsingRequestServices()); + source.AddScoped(); } } diff --git a/src/blueprints/Do.Blueprints.Service.Application/Caching/CachingConfigurator.cs b/src/blueprints/Do.Blueprints.Service.Application/Caching/CachingConfigurator.cs new file mode 100644 index 000000000..882616c1c --- /dev/null +++ b/src/blueprints/Do.Blueprints.Service.Application/Caching/CachingConfigurator.cs @@ -0,0 +1,3 @@ +namespace Do.Caching; + +public class CachingConfigurator { } diff --git a/src/blueprints/Do.Blueprints.Service.Application/Caching/CachingExtensions.cs b/src/blueprints/Do.Blueprints.Service.Application/Caching/CachingExtensions.cs new file mode 100644 index 000000000..0093a44cd --- /dev/null +++ b/src/blueprints/Do.Blueprints.Service.Application/Caching/CachingExtensions.cs @@ -0,0 +1,9 @@ +using Do.Architecture; +using Do.Caching; + +namespace Do; + +public static class CachingExtensions +{ + public static void AddCaching(this List source, Func> configure) => source.Add(configure(new())); +} diff --git a/src/blueprints/Do.Blueprints.Service.Application/Caching/ScopedMemory/ScopedMemoryCachingExtensions.cs b/src/blueprints/Do.Blueprints.Service.Application/Caching/ScopedMemory/ScopedMemoryCachingExtensions.cs new file mode 100644 index 000000000..23f5c0183 --- /dev/null +++ b/src/blueprints/Do.Blueprints.Service.Application/Caching/ScopedMemory/ScopedMemoryCachingExtensions.cs @@ -0,0 +1,9 @@ +using Do.Caching; +using Do.Caching.ScopedMemory; + +namespace Do; + +public static class ScopedMemoryCachingExtensions +{ + public static ScopedMemoryCachingFeature ScopedMemory(this CachingConfigurator _) => new(); +} diff --git a/src/blueprints/Do.Blueprints.Service.Application/Caching/ScopedMemory/ScopedMemoryCachingFeature.cs b/src/blueprints/Do.Blueprints.Service.Application/Caching/ScopedMemory/ScopedMemoryCachingFeature.cs new file mode 100644 index 000000000..17aa7ee21 --- /dev/null +++ b/src/blueprints/Do.Blueprints.Service.Application/Caching/ScopedMemory/ScopedMemoryCachingFeature.cs @@ -0,0 +1,15 @@ +using Do.Architecture; +using Microsoft.Extensions.Caching.Memory; + +namespace Do.Caching.ScopedMemory; + +public class ScopedMemoryCachingFeature : IFeature +{ + public void Configure(LayerConfigurator configurator) + { + configurator.ConfigureServiceCollection(services => + { + services.AddScopedWithFactory(); + }); + } +} diff --git a/src/blueprints/Do.Blueprints.Service.Application/Configuration/ConfigurationLayer.cs b/src/blueprints/Do.Blueprints.Service.Application/Configuration/ConfigurationLayer.cs index f9fa0acb3..25175d2bd 100644 --- a/src/blueprints/Do.Blueprints.Service.Application/Configuration/ConfigurationLayer.cs +++ b/src/blueprints/Do.Blueprints.Service.Application/Configuration/ConfigurationLayer.cs @@ -15,10 +15,9 @@ protected override IEnumerable GetPhases() yield return new BuildConfiguration(); } - public class BuildConfiguration : PhaseBase + public class BuildConfiguration() + : PhaseBase(PhaseOrder.Earliest) { - public BuildConfiguration() : base(PhaseOrder.Earliest) { } - protected override void Initialize(ConfigurationManager configurationManager) { Settings.SetConfigurationRoot(configurationManager); diff --git a/src/blueprints/Do.Blueprints.Service.Application/Configuration/ConfigurationRequiredException.cs b/src/blueprints/Do.Blueprints.Service.Application/Configuration/ConfigurationRequiredException.cs index 02ebec2e8..88a773255 100644 --- a/src/blueprints/Do.Blueprints.Service.Application/Configuration/ConfigurationRequiredException.cs +++ b/src/blueprints/Do.Blueprints.Service.Application/Configuration/ConfigurationRequiredException.cs @@ -1,6 +1,4 @@ namespace Do.Configuration; -public class ConfigurationRequiredException : Exception -{ - public ConfigurationRequiredException(string key) : base($"Configuration required for {key}") { } -} +public class ConfigurationRequiredException(string _key) + : Exception($"Configuration required for {_key}") { } diff --git a/src/blueprints/Do.Blueprints.Service.Application/Core/Dotnet/DotnetCoreFeature.cs b/src/blueprints/Do.Blueprints.Service.Application/Core/Dotnet/DotnetCoreFeature.cs index be88147a4..13e18f70a 100644 --- a/src/blueprints/Do.Blueprints.Service.Application/Core/Dotnet/DotnetCoreFeature.cs +++ b/src/blueprints/Do.Blueprints.Service.Application/Core/Dotnet/DotnetCoreFeature.cs @@ -9,7 +9,7 @@ public void Configure(LayerConfigurator configurator) { configurator.ConfigureServiceCollection(services => { - services.AddSingleton(); + services.AddSingleton(TimeProvider.System); }); } } diff --git a/src/blueprints/Do.Blueprints.Service.Application/Core/Dotnet/System.cs b/src/blueprints/Do.Blueprints.Service.Application/Core/Dotnet/System.cs deleted file mode 100644 index b645747dd..000000000 --- a/src/blueprints/Do.Blueprints.Service.Application/Core/Dotnet/System.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Do.Core.Dotnet; - -public class System : ISystem -{ - public DateTime Now => DateTime.Now; -} diff --git a/src/blueprints/Do.Blueprints.Service.Application/Core/Mock/MockCoreFeature.cs b/src/blueprints/Do.Blueprints.Service.Application/Core/Mock/MockCoreFeature.cs index aaaf19c3c..3135d4629 100644 --- a/src/blueprints/Do.Blueprints.Service.Application/Core/Mock/MockCoreFeature.cs +++ b/src/blueprints/Do.Blueprints.Service.Application/Core/Mock/MockCoreFeature.cs @@ -1,5 +1,6 @@ using Do.Architecture; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; namespace Do.Core.Mock; @@ -7,10 +8,14 @@ public class MockCoreFeature : IFeature { public void Configure(LayerConfigurator configurator) { + configurator.ConfigureServiceCollection(services => + { + services.AddSingleton(); + }); + configurator.ConfigureTestConfiguration(test => { test.Mocks.Add(singleton: true); - test.Mocks.Add(singleton: true); }); } } diff --git a/src/blueprints/Do.Blueprints.Service.Application/Core/Mock/ResettableFakeTimeProvider.cs b/src/blueprints/Do.Blueprints.Service.Application/Core/Mock/ResettableFakeTimeProvider.cs new file mode 100644 index 000000000..f060cd6ef --- /dev/null +++ b/src/blueprints/Do.Blueprints.Service.Application/Core/Mock/ResettableFakeTimeProvider.cs @@ -0,0 +1,39 @@ +using Microsoft.Extensions.Time.Testing; + +namespace Do.Core.Mock; + +public class ResettableFakeTimeProvider : TimeProvider +{ + private FakeTimeProvider _inner = new(); + + public DateTimeOffset Start => _inner.Start; + public TimeSpan AutoAdvanceAmount + { + get => _inner.AutoAdvanceAmount; + set => _inner.AutoAdvanceAmount = value; + } + + public override TimeZoneInfo LocalTimeZone => _inner.LocalTimeZone; + public override long TimestampFrequency => _inner.TimestampFrequency; + + public void Reset() => + _inner = new(); + + public void SetUtcNow(DateTimeOffset time) => + _inner.SetUtcNow(time); + + public void Advance(TimeSpan delta) => + _inner.Advance(delta); + + public void SetLocalTimeZone(TimeZoneInfo localTimeZone) => + _inner.SetLocalTimeZone(localTimeZone); + + public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period) => + _inner.CreateTimer(callback, state, dueTime, period); + + public override long GetTimestamp() => + _inner.GetTimestamp(); + + public override DateTimeOffset GetUtcNow() => + _inner.GetUtcNow(); +} \ No newline at end of file diff --git a/src/blueprints/Do.Blueprints.Service.Application/DataAccess/AutomappingConfiguration.cs b/src/blueprints/Do.Blueprints.Service.Application/DataAccess/AutomappingConfiguration.cs index bdf9e93d5..d6682949e 100644 --- a/src/blueprints/Do.Blueprints.Service.Application/DataAccess/AutomappingConfiguration.cs +++ b/src/blueprints/Do.Blueprints.Service.Application/DataAccess/AutomappingConfiguration.cs @@ -4,7 +4,7 @@ namespace Do.DataAccess; public class AutomappingConfiguration { - public List> ShouldMapType { get; } = new(); - public List> ShouldMapMember { get; } = new(); - public List> MemberIsId { get; } = new(); + public List> ShouldMapType { get; } = []; + public List> ShouldMapMember { get; } = []; + public List> MemberIsId { get; } = []; } diff --git a/src/blueprints/Do.Blueprints.Service.Application/DataAccess/DelegatedAutomappingConfiguration.cs b/src/blueprints/Do.Blueprints.Service.Application/DataAccess/DelegatedAutomappingConfiguration.cs index e38414ceb..b4d8ba619 100644 --- a/src/blueprints/Do.Blueprints.Service.Application/DataAccess/DelegatedAutomappingConfiguration.cs +++ b/src/blueprints/Do.Blueprints.Service.Application/DataAccess/DelegatedAutomappingConfiguration.cs @@ -3,13 +3,9 @@ namespace Do.DataAccess; -public class DelegatedAutomappingConfiguration : DefaultAutomappingConfiguration +public class DelegatedAutomappingConfiguration(AutomappingConfiguration _configuration) + : DefaultAutomappingConfiguration { - readonly AutomappingConfiguration _configuration; - - public DelegatedAutomappingConfiguration(AutomappingConfiguration configuration) => - _configuration = configuration; - public override bool ShouldMap(Type type) => _configuration.ShouldMapType.Any(shouldMap => shouldMap(type)); public override bool IsId(Member member) => _configuration.MemberIsId.Any(isId => isId(member)); public override bool ShouldMap(Member member) => _configuration.ShouldMapMember.Any(shouldMap => shouldMap(member)); diff --git a/src/blueprints/Do.Blueprints.Service.Application/DataAccess/DelegatedInterceptor.cs b/src/blueprints/Do.Blueprints.Service.Application/DataAccess/DelegatedInterceptor.cs index edcee5ab2..1b3151e97 100644 --- a/src/blueprints/Do.Blueprints.Service.Application/DataAccess/DelegatedInterceptor.cs +++ b/src/blueprints/Do.Blueprints.Service.Application/DataAccess/DelegatedInterceptor.cs @@ -2,14 +2,9 @@ namespace Do.DataAccess; -public class DelegatedInterceptor : EmptyInterceptor +public class DelegatedInterceptor(IServiceProvider _serviceProvider, InterceptorConfiguration _interceptorConfiguration) + : EmptyInterceptor { - readonly IServiceProvider _serviceProvider; - readonly InterceptorConfiguration _interceptorConfiguration; - - public DelegatedInterceptor(IServiceProvider serviceProvider, InterceptorConfiguration interceptorConfiguration) => - (_serviceProvider, _interceptorConfiguration) = (serviceProvider, interceptorConfiguration); - ISessionFactory SessionFactory => _serviceProvider.GetRequiredServiceUsingRequestServices(); public override object Instantiate(string clazz, object id) diff --git a/src/blueprints/Do.Blueprints.Service.Application/Database/FlatTransaction.cs b/src/blueprints/Do.Blueprints.Service.Application/Database/FlatTransaction.cs index 800da741d..5e143c6fb 100644 --- a/src/blueprints/Do.Blueprints.Service.Application/Database/FlatTransaction.cs +++ b/src/blueprints/Do.Blueprints.Service.Application/Database/FlatTransaction.cs @@ -3,14 +3,9 @@ namespace Do.Database; -public class FlatTransaction : ITransaction +public class FlatTransaction(Func _getSession, ILogger _logger) + : ITransaction { - readonly Func _getSession; - readonly ILogger _logger; - - public FlatTransaction(Func getSession, ILogger log) => - (_getSession, _logger) = (getSession, log); - public async Task CommitAsync(Action action) { action(); @@ -43,15 +38,19 @@ public async Task CommitAsync(Func> action) return result; } - public async Task CommitAsync(TEntity entity, Action action) + public async Task CommitAsync(TEntity? entity, Action action) { + if (entity is null) { return; } + action(entity); await CommitAndBeginNewTransaction(); } - public async Task CommitAsync(TEntity entity, Func action) + public async Task CommitAsync(TEntity? entity, Func action) { + if (entity is null) { return; } + await action(entity); await CommitAndBeginNewTransaction(); diff --git a/src/blueprints/Do.Blueprints.Service.Application/Database/FlatTransactionMiddleware.cs b/src/blueprints/Do.Blueprints.Service.Application/Database/FlatTransactionMiddleware.cs index 94dade250..ebb94c53d 100644 --- a/src/blueprints/Do.Blueprints.Service.Application/Database/FlatTransactionMiddleware.cs +++ b/src/blueprints/Do.Blueprints.Service.Application/Database/FlatTransactionMiddleware.cs @@ -6,13 +6,8 @@ namespace Do.Database; -public class FlatTransactionMiddleware +public class FlatTransactionMiddleware(RequestDelegate _next) { - readonly RequestDelegate _next; - - public FlatTransactionMiddleware(RequestDelegate next) => - _next = next; - public async Task InvokeAsync(HttpContext context) { using (var session = context.RequestServices.GetRequiredService()) diff --git a/src/blueprints/Do.Blueprints.Service.Application/Database/InMemory/SkippedTransaction.cs b/src/blueprints/Do.Blueprints.Service.Application/Database/InMemory/SkippedTransaction.cs index 29e0bb6d0..c6547f90b 100644 --- a/src/blueprints/Do.Blueprints.Service.Application/Database/InMemory/SkippedTransaction.cs +++ b/src/blueprints/Do.Blueprints.Service.Application/Database/InMemory/SkippedTransaction.cs @@ -26,15 +26,19 @@ public async Task CommitAsync(Func> action) return await action(); } - public Task CommitAsync(TEntity entity, Action action) + public Task CommitAsync(TEntity? entity, Action action) { + if (entity is null) { return Task.CompletedTask; } + action(entity); return Task.CompletedTask; } - public async Task CommitAsync(TEntity entity, Func action) + public async Task CommitAsync(TEntity? entity, Func action) { + if (entity is null) { return; } + await action(entity); } } diff --git a/src/blueprints/Do.Blueprints.Service.Application/Database/MySql/CustomMySQL57Dialect.cs b/src/blueprints/Do.Blueprints.Service.Application/Database/MySql/CustomMySQL57Dialect.cs new file mode 100644 index 000000000..db19bbf69 --- /dev/null +++ b/src/blueprints/Do.Blueprints.Service.Application/Database/MySql/CustomMySQL57Dialect.cs @@ -0,0 +1,12 @@ +using NHibernate.Dialect; +using System.Data; + +namespace Do.Database.MySql; + +public class CustomMySQL57Dialect : MySQL57Dialect +{ + public CustomMySQL57Dialect() + { + RegisterColumnType(DbType.String, 1023, "VARCHAR($l)"); + } +} diff --git a/src/blueprints/Do.Blueprints.Service.Application/Database/MySql/MySqlDatabaseFeature.cs b/src/blueprints/Do.Blueprints.Service.Application/Database/MySql/MySqlDatabaseFeature.cs index a06ca4502..803275781 100644 --- a/src/blueprints/Do.Blueprints.Service.Application/Database/MySql/MySqlDatabaseFeature.cs +++ b/src/blueprints/Do.Blueprints.Service.Application/Database/MySql/MySqlDatabaseFeature.cs @@ -2,19 +2,12 @@ using Do.Configuration; using FluentNHibernate.Cfg.Db; using Microsoft.Extensions.DependencyInjection; -using NHibernate.Dialect; namespace Do.Database.MySql; -public class MySqlDatabaseFeature : IFeature +public class MySqlDatabaseFeature(Setting _connectionString, Setting _autoUpdateSchema, Setting _showSql) + : IFeature { - readonly Setting _connectionString; - readonly Setting _autoUpdateSchema; - readonly Setting _showSql; - - public MySqlDatabaseFeature(Setting connectionString, Setting autoUpdateSchema, Setting showSql) => - (_connectionString, _autoUpdateSchema, _showSql) = (connectionString, autoUpdateSchema, showSql); - public void Configure(LayerConfigurator configurator) { configurator.ConfigureServiceCollection(services => @@ -26,7 +19,7 @@ public void Configure(LayerConfigurator configurator) { var mysql = MySQLConfiguration.Standard .ConnectionString(_connectionString) - .Dialect(); + .Dialect(); // this should be in logging if (_showSql) { mysql.ShowSql(); } diff --git a/src/blueprints/Do.Blueprints.Service.Application/Database/Sqlite/SqliteDatabaseFeature.cs b/src/blueprints/Do.Blueprints.Service.Application/Database/Sqlite/SqliteDatabaseFeature.cs index 6e642e991..c18268e18 100644 --- a/src/blueprints/Do.Blueprints.Service.Application/Database/Sqlite/SqliteDatabaseFeature.cs +++ b/src/blueprints/Do.Blueprints.Service.Application/Database/Sqlite/SqliteDatabaseFeature.cs @@ -6,13 +6,9 @@ namespace Do.Database.Sqlite; -public class SqliteDatabaseFeature : IFeature +public class SqliteDatabaseFeature(Setting _fileName) + : IFeature { - readonly Setting _fileName; - - public SqliteDatabaseFeature(Setting fileName) => - _fileName = fileName; - string FullFilePath => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), _fileName); public void Configure(LayerConfigurator configurator) diff --git a/src/blueprints/Do.Blueprints.Service.Application/DependencyInjection/DependencyInjectionLayer.cs b/src/blueprints/Do.Blueprints.Service.Application/DependencyInjection/DependencyInjectionLayer.cs index 50a7168c8..f782df3d3 100644 --- a/src/blueprints/Do.Blueprints.Service.Application/DependencyInjection/DependencyInjectionLayer.cs +++ b/src/blueprints/Do.Blueprints.Service.Application/DependencyInjection/DependencyInjectionLayer.cs @@ -18,13 +18,8 @@ protected override IEnumerable GetPhases() yield return new AddServices(_services); } - public class AddServices : PhaseBase + public class AddServices(IServiceCollection _services) : PhaseBase(PhaseOrder.Early) { - readonly IServiceCollection _services; - - public AddServices(IServiceCollection services) : base(PhaseOrder.Early) => - _services = services; - protected override void Initialize(DomainModel _) { Context.Add(_services); diff --git a/src/blueprints/Do.Blueprints.Service.Application/Do.Blueprints.Service.Application.csproj b/src/blueprints/Do.Blueprints.Service.Application/Do.Blueprints.Service.Application.csproj index 95c56a130..d3e1f08ce 100644 --- a/src/blueprints/Do.Blueprints.Service.Application/Do.Blueprints.Service.Application.csproj +++ b/src/blueprints/Do.Blueprints.Service.Application/Do.Blueprints.Service.Application.csproj @@ -19,6 +19,7 @@ + diff --git a/src/blueprints/Do.Blueprints.Service.Application/Documentation/Default/DefaultDocumentationFeature.cs b/src/blueprints/Do.Blueprints.Service.Application/Documentation/Default/DefaultDocumentationFeature.cs index 123b0d7d3..c21d17aef 100644 --- a/src/blueprints/Do.Blueprints.Service.Application/Documentation/Default/DefaultDocumentationFeature.cs +++ b/src/blueprints/Do.Blueprints.Service.Application/Documentation/Default/DefaultDocumentationFeature.cs @@ -12,7 +12,7 @@ public void Configure(LayerConfigurator configurator) { swaggerGenOptions.CustomSchemaIds(t => { - string[] splitedNamespace = t.Namespace?.Split(".") ?? new string[0]; + string[] splitedNamespace = t.Namespace?.Split(".") ?? []; string name = t.IsNested && t.FullName is not null ? t.FullName.Replace($"{t.Namespace}.", string.Empty).Replace("Controller", string.Empty).Replace("+", ".") : t.Name; diff --git a/src/blueprints/Do.Blueprints.Service.Application/Domain/DomainLayer.cs b/src/blueprints/Do.Blueprints.Service.Application/Domain/DomainLayer.cs index 628277a84..604069033 100644 --- a/src/blueprints/Do.Blueprints.Service.Application/Domain/DomainLayer.cs +++ b/src/blueprints/Do.Blueprints.Service.Application/Domain/DomainLayer.cs @@ -22,18 +22,8 @@ protected override IEnumerable GetPhases() yield return new BuildDomain(_assemblyCollection, _typeCollection); } - public class BuildDomain : PhaseBase + public class BuildDomain(IAssemblyCollection _assemblyCollection, ITypeCollection _typeCollection) : PhaseBase(PhaseOrder.Early) { - readonly IAssemblyCollection _assemblyCollection; - readonly ITypeCollection _typeCollection; - - public BuildDomain(IAssemblyCollection assemblyCollection, ITypeCollection typeCollection) - : base(PhaseOrder.Early) - { - _assemblyCollection = assemblyCollection; - _typeCollection = typeCollection; - } - protected override void Initialize(ConfigurationManager _) { Context.Add(new(_assemblyCollection, _typeCollection)); diff --git a/src/blueprints/Do.Blueprints.Service.Application/Domain/Model/TypeModel.cs b/src/blueprints/Do.Blueprints.Service.Application/Domain/Model/TypeModel.cs index 3607a0a4a..d1849c592 100644 --- a/src/blueprints/Do.Blueprints.Service.Application/Domain/Model/TypeModel.cs +++ b/src/blueprints/Do.Blueprints.Service.Application/Domain/Model/TypeModel.cs @@ -9,10 +9,9 @@ List Methods ) { public TypeModel(Type type) - : this(type, type.Name, new()) + : this(type, type.Name, []) { - var methodInfos = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly) ?? - Array.Empty(); + var methodInfos = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly) ?? []; foreach (var method in methodInfos) { diff --git a/src/blueprints/Do.Blueprints.Service.Application/ExceptionHandling/Default/DefaultExceptionHandlingExtensions.cs b/src/blueprints/Do.Blueprints.Service.Application/ExceptionHandling/Default/DefaultExceptionHandlingExtensions.cs index d51a17cfa..e3ca2d46a 100644 --- a/src/blueprints/Do.Blueprints.Service.Application/ExceptionHandling/Default/DefaultExceptionHandlingExtensions.cs +++ b/src/blueprints/Do.Blueprints.Service.Application/ExceptionHandling/Default/DefaultExceptionHandlingExtensions.cs @@ -1,9 +1,12 @@ -using Do.ExceptionHandling; +using Do.Configuration; +using Do.ExceptionHandling; using Do.ExceptionHandling.Default; namespace Do; public static class DefaultExceptionHandlingExtensions { - public static DefaultExceptionHandlingFeature Default(this ExceptionHandlingConfigurator _) => new(); + public static DefaultExceptionHandlingFeature Default(this ExceptionHandlingConfigurator _, + Setting? typeUrlFormat = default + ) => new(typeUrlFormat); } diff --git a/src/blueprints/Do.Blueprints.Service.Application/ExceptionHandling/Default/DefaultExceptionHandlingFeature.cs b/src/blueprints/Do.Blueprints.Service.Application/ExceptionHandling/Default/DefaultExceptionHandlingFeature.cs index 260127701..b926d7a99 100644 --- a/src/blueprints/Do.Blueprints.Service.Application/ExceptionHandling/Default/DefaultExceptionHandlingFeature.cs +++ b/src/blueprints/Do.Blueprints.Service.Application/ExceptionHandling/Default/DefaultExceptionHandlingFeature.cs @@ -1,20 +1,32 @@ using Do.Architecture; +using Do.Configuration; +using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; namespace Do.ExceptionHandling.Default; -public class DefaultExceptionHandlingFeature : IFeature +public class DefaultExceptionHandlingFeature(Setting? _typeUrlFormat = default) + : IFeature { public void Configure(LayerConfigurator configurator) { configurator.ConfigureServiceCollection(services => { services.AddSingleton(); + services.AddSingleton(new ExceptionHandlerSettings(_typeUrlFormat)); + + services.AddExceptionHandler(); + services.AddProblemDetails(); }); configurator.ConfigureMiddlewareCollection(middlewares => { - middlewares.Add(order: -20); + middlewares.Add(app => + { + app.UseExceptionHandler(); + app.UseStatusCodePages(); + } + ); }); } } diff --git a/src/blueprints/Do.Blueprints.Service.Application/ExceptionHandling/Default/ExceptionHandler.cs b/src/blueprints/Do.Blueprints.Service.Application/ExceptionHandling/Default/ExceptionHandler.cs new file mode 100644 index 000000000..d29206700 --- /dev/null +++ b/src/blueprints/Do.Blueprints.Service.Application/ExceptionHandling/Default/ExceptionHandler.cs @@ -0,0 +1,49 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Do.ExceptionHandling.Default; + +public class ExceptionHandler(IEnumerable _handlers, ExceptionHandlerSettings _settings) + : Microsoft.AspNetCore.Diagnostics.IExceptionHandler +{ + readonly UnhandledExceptionHandler _unhandledExceptionHandler = new(); + + public async ValueTask TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken) + { + var exceptionInfo = HandlerFor(exception).Handle(exception); + + httpContext.Response.ContentType = "application/json"; + httpContext.Response.StatusCode = exceptionInfo.Code; + + await httpContext.Response.WriteAsJsonAsync(ToProblemDetails(exceptionInfo), cancellationToken); + + return true; + } + + IExceptionHandler HandlerFor(Exception exception) => + _handlers.FirstOrDefault(h => h.CanHandle(exception)) ?? _unhandledExceptionHandler; + + ProblemDetails ToProblemDetails(ExceptionInfo exceptionInfo) => + new() + { + Type = _settings.TypeUrlFormat is not null + ? string.Format(_settings.TypeUrlFormat.GetValue(), SnakeCase(NameOf(exceptionInfo.Exception))) + : null, + Title = TitleCase(NameOf(exceptionInfo.Exception)), + Status = exceptionInfo.Code, + Detail = exceptionInfo.Body, + Extensions = exceptionInfo.ExtraData ?? [] + }; + + string NameOf(Exception exception) => + exception.GetType().Name.Replace(nameof(Exception), string.Empty); + + string TitleCase(string pascalCaseString) => + SplitPascalCase(pascalCaseString, joinBy: " "); + + string SnakeCase(string pascalCaseString) => + SplitPascalCase(pascalCaseString, joinBy: "-").ToLowerInvariant(); + + string SplitPascalCase(string @string, string joinBy) => + Regexes.PascalCaseSplitter().Replace(@string, joinBy); +} diff --git a/src/blueprints/Do.Blueprints.Service.Application/ExceptionHandling/Default/ExceptionHandlerSettings.cs b/src/blueprints/Do.Blueprints.Service.Application/ExceptionHandling/Default/ExceptionHandlerSettings.cs new file mode 100644 index 000000000..fc886e361 --- /dev/null +++ b/src/blueprints/Do.Blueprints.Service.Application/ExceptionHandling/Default/ExceptionHandlerSettings.cs @@ -0,0 +1,5 @@ +using Do.Configuration; + +namespace Do.ExceptionHandling.Default; + +public record ExceptionHandlerSettings(Setting? TypeUrlFormat); diff --git a/src/blueprints/Do.Blueprints.Service.Application/ExceptionHandling/Default/ExceptionHandlingMiddleware.cs b/src/blueprints/Do.Blueprints.Service.Application/ExceptionHandling/Default/ExceptionHandlingMiddleware.cs deleted file mode 100644 index e00870f3b..000000000 --- a/src/blueprints/Do.Blueprints.Service.Application/ExceptionHandling/Default/ExceptionHandlingMiddleware.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Microsoft.AspNetCore.Http; - -namespace Do.ExceptionHandling.Default; - -public class ExceptionHandlingMiddleware -{ - readonly RequestDelegate _next; - readonly IEnumerable _handlers; - readonly UnhandledExceptionHandler _unhandledExceptionHandler = new(); - - public ExceptionHandlingMiddleware(IEnumerable handlers, RequestDelegate next) => - (_handlers, _next) = (handlers, next); - - public async Task InvokeAsync(HttpContext context) - { - try - { - await _next(context); - } - catch (Exception ex) - { - var handler = _handlers.FirstOrDefault(h => h.CanHandle(ex)) ?? _unhandledExceptionHandler; - var exceptionInfo = handler.Handle(ex); - - context.Response.ContentType = "application/json"; - context.Response.StatusCode = exceptionInfo.Code; - - object forwardedExceptionInfo = - new - { - Request = context.Request.Path.Value, - Message = exceptionInfo.Body - }; - - await context.Response.WriteAsJsonAsync(forwardedExceptionInfo); - } - } -} diff --git a/src/blueprints/Do.Blueprints.Service.Application/ExceptionHandling/Default/HandledExceptionHandler.cs b/src/blueprints/Do.Blueprints.Service.Application/ExceptionHandling/Default/HandledExceptionHandler.cs index 1f83d2945..cabdcff17 100644 --- a/src/blueprints/Do.Blueprints.Service.Application/ExceptionHandling/Default/HandledExceptionHandler.cs +++ b/src/blueprints/Do.Blueprints.Service.Application/ExceptionHandling/Default/HandledExceptionHandler.cs @@ -3,5 +3,5 @@ public class HandledExceptionHandler : IExceptionHandler { public bool CanHandle(Exception ex) => ex is HandledException; - public ExceptionInfo Handle(Exception ex) => new((int)((HandledException)ex).StatusCode, ex.Message); + public ExceptionInfo Handle(Exception ex) => new(ex, (int)((HandledException)ex).StatusCode, ex.Message, ((HandledException)ex).ExtraData); } diff --git a/src/blueprints/Do.Blueprints.Service.Application/ExceptionHandling/Default/Regexes.cs b/src/blueprints/Do.Blueprints.Service.Application/ExceptionHandling/Default/Regexes.cs new file mode 100644 index 000000000..e16f5a4fe --- /dev/null +++ b/src/blueprints/Do.Blueprints.Service.Application/ExceptionHandling/Default/Regexes.cs @@ -0,0 +1,9 @@ +using System.Text.RegularExpressions; + +namespace Do.ExceptionHandling.Default; + +internal static partial class Regexes +{ + [GeneratedRegex(@"(?<=[A-Z])(?=[A-Z][a-z])|(?<=[^A-Z])(?=[A-Z])|(?<=[A-Za-z])(?=[^A-Za-z])", RegexOptions.None, "en-US")] + public static partial Regex PascalCaseSplitter(); +} diff --git a/src/blueprints/Do.Blueprints.Service.Application/ExceptionHandling/Default/UnhandledExceptionHandler.cs b/src/blueprints/Do.Blueprints.Service.Application/ExceptionHandling/Default/UnhandledExceptionHandler.cs index 5ee0c4270..766bfab1a 100644 --- a/src/blueprints/Do.Blueprints.Service.Application/ExceptionHandling/Default/UnhandledExceptionHandler.cs +++ b/src/blueprints/Do.Blueprints.Service.Application/ExceptionHandling/Default/UnhandledExceptionHandler.cs @@ -5,5 +5,9 @@ namespace Do.ExceptionHandling.Default; public class UnhandledExceptionHandler : IExceptionHandler { public bool CanHandle(Exception ex) => true; - public ExceptionInfo Handle(Exception ex) => new((int)HttpStatusCode.InternalServerError, ex.Message); + public ExceptionInfo Handle(Exception ex) => new( + ex, + (int)HttpStatusCode.InternalServerError, + "An unexpected error has occured. Please contact the administrator." + ); } diff --git a/src/blueprints/Do.Blueprints.Service.Application/ForgeExtensions.cs b/src/blueprints/Do.Blueprints.Service.Application/ForgeExtensions.cs index ed57c1643..f5414c112 100644 --- a/src/blueprints/Do.Blueprints.Service.Application/ForgeExtensions.cs +++ b/src/blueprints/Do.Blueprints.Service.Application/ForgeExtensions.cs @@ -1,5 +1,6 @@ using Do.Architecture; using Do.Business; +using Do.Caching; using Do.Core; using Do.Database; using Do.Documentation; @@ -14,6 +15,7 @@ public static class ForgeExtensions { public static Application Service(this Forge source, Func>? business = default, + Func>? caching = default, Func>? core = default, Func>? database = default, Func>? documentation = default, @@ -25,6 +27,7 @@ public static Application Service(this Forge source, ) { business ??= c => c.Default(); + caching ??= c => c.ScopedMemory(); core ??= c => c.Dotnet(); database ??= c => c.Sqlite(); documentation ??= c => c.Default(); @@ -45,6 +48,7 @@ public static Application Service(this Forge source, app.Layers.AddRestApi(); app.Features.AddBusiness(business); + app.Features.AddCaching(caching); app.Features.AddCore(core); app.Features.AddDatabase(database); app.Features.AddDocumentation(documentation); diff --git a/src/blueprints/Do.Blueprints.Service.Application/Greeting/WelcomePage/WelcomePageGreetingFeature.cs b/src/blueprints/Do.Blueprints.Service.Application/Greeting/WelcomePage/WelcomePageGreetingFeature.cs index 95cda0917..830a0ea0b 100644 --- a/src/blueprints/Do.Blueprints.Service.Application/Greeting/WelcomePage/WelcomePageGreetingFeature.cs +++ b/src/blueprints/Do.Blueprints.Service.Application/Greeting/WelcomePage/WelcomePageGreetingFeature.cs @@ -3,13 +3,9 @@ namespace Do.Greeting.WelcomePage; -public class WelcomePageGreetingFeature : IFeature +public class WelcomePageGreetingFeature(string _path) + : IFeature { - readonly string _path; - - public WelcomePageGreetingFeature(string path) => - _path = path; - public void Configure(LayerConfigurator configurator) { configurator.ConfigureMiddlewareCollection(middlewares => diff --git a/src/blueprints/Do.Blueprints.Service.Application/HttpServer/HttpServerLayer.cs b/src/blueprints/Do.Blueprints.Service.Application/HttpServer/HttpServerLayer.cs index 7f3d74b3a..00608fcf6 100644 --- a/src/blueprints/Do.Blueprints.Service.Application/HttpServer/HttpServerLayer.cs +++ b/src/blueprints/Do.Blueprints.Service.Application/HttpServer/HttpServerLayer.cs @@ -24,10 +24,9 @@ protected override IEnumerable GetPhases() yield return new Run(_middlewares); } - public class CreateBuilder : PhaseBase + public class CreateBuilder() + : PhaseBase(PhaseOrder.Earliest) { - public CreateBuilder() : base(PhaseOrder.Earliest) { } - protected override void Initialize() { var build = WebApplication.CreateBuilder(); @@ -37,10 +36,9 @@ protected override void Initialize() } } - public class Build : PhaseBase + public class Build() + : PhaseBase(PhaseOrder.Latest) { - public Build() : base(PhaseOrder.Latest) { } - protected override void Initialize(WebApplicationBuilder build, IServiceCollection services) { foreach (var service in services) @@ -55,13 +53,9 @@ protected override void Initialize(WebApplicationBuilder build, IServiceCollecti } } - class Run : PhaseBase + class Run(IMiddlewareCollection _middlewares) + : PhaseBase(PhaseOrder.Latest) { - readonly IMiddlewareCollection _middlewares; - - public Run(IMiddlewareCollection middlewares) : base(PhaseOrder.Latest) => - _middlewares = middlewares; - protected override void Initialize(WebApplication app) { foreach (var middleware in _middlewares.OrderBy(m => m.Order)) diff --git a/src/blueprints/Do.Blueprints.Service.Application/Logging/Request/RequestLogMiddleware.cs b/src/blueprints/Do.Blueprints.Service.Application/Logging/Request/RequestLogMiddleware.cs index 883b0ee81..02a9894e6 100644 --- a/src/blueprints/Do.Blueprints.Service.Application/Logging/Request/RequestLogMiddleware.cs +++ b/src/blueprints/Do.Blueprints.Service.Application/Logging/Request/RequestLogMiddleware.cs @@ -1,19 +1,11 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; namespace Do.Logging.Request; -public class RequestLogMiddleware +public class RequestLogMiddleware(ILogger _logger, RequestDelegate _next) { - readonly ILogger _logger; - readonly RequestDelegate _next; - - public RequestLogMiddleware(ILogger logger, RequestDelegate next) - { - _logger = logger; - _next = next; - } - public async Task Invoke(HttpContext context) { _logger.LogInformation(message: $"Begin: {context.Request.Path}"); @@ -22,7 +14,11 @@ public async Task Invoke(HttpContext context) { await _next(context); - _logger.LogInformation(message: $"End: {context.Request.Path} StatusCode: {context.Response.StatusCode}"); + var exception = context.Features.Get(); + if (exception?.Error is not null) + { + _logger.LogError(exception: exception.Error, message: exception.Error.Message); + } } catch (Exception e) { @@ -30,5 +26,9 @@ public async Task Invoke(HttpContext context) throw; } + finally + { + _logger.LogInformation(message: $"End: {context.Request.Path} StatusCode: {context.Response.StatusCode}"); + } } } diff --git a/src/blueprints/Do.Blueprints.Service.Application/MockOverrider/FirstInterface/MockOverrider.cs b/src/blueprints/Do.Blueprints.Service.Application/MockOverrider/FirstInterface/MockOverrider.cs index cb6d10449..ccf8e8a87 100644 --- a/src/blueprints/Do.Blueprints.Service.Application/MockOverrider/FirstInterface/MockOverrider.cs +++ b/src/blueprints/Do.Blueprints.Service.Application/MockOverrider/FirstInterface/MockOverrider.cs @@ -31,7 +31,7 @@ public void Reset() var getMethod = typeof(Mock).GetMethod(nameof(Mock.Get), BindingFlags.Static | BindingFlags.Public) ?? throw new InvalidOperationException("method should not be null"); var genericGetMethod = getMethod.MakeGenericMethod(descriptor.Type); - Mock mock = (Mock)(genericGetMethod.Invoke(null, new[] { mockedObject }) ?? throw new InvalidOperationException("invoke result should not be null")); + Mock mock = (Mock)(genericGetMethod.Invoke(null, [mockedObject]) ?? throw new InvalidOperationException("invoke result should not be null")); mock.Reset(); descriptor.Setup?.Invoke(mock); diff --git a/src/blueprints/Do.Blueprints.Service.Application/Orm/Default/CorruptedJsonDataExceptionHandler.cs b/src/blueprints/Do.Blueprints.Service.Application/Orm/Default/CorruptedJsonDataExceptionHandler.cs deleted file mode 100644 index fad44d789..000000000 --- a/src/blueprints/Do.Blueprints.Service.Application/Orm/Default/CorruptedJsonDataExceptionHandler.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Net; -using Do.ExceptionHandling; -using NHibernate.Exceptions; - -namespace Do.Orm.Default; - -public class CorruptedJsonDataExceptionHandler : IExceptionHandler -{ - public bool CanHandle(Exception ex) => - ex is GenericADOException && - ex.InnerException is InvalidDataException; - - public ExceptionInfo Handle(Exception ex) => - new( - (int)HttpStatusCode.InternalServerError, - ex.InnerException?.Message ?? ex.Message - ); -} diff --git a/src/blueprints/Do.Blueprints.Service.Application/Orm/Default/DefaultOrmFeature.cs b/src/blueprints/Do.Blueprints.Service.Application/Orm/Default/DefaultOrmFeature.cs index f1c4fca9e..08ba94028 100644 --- a/src/blueprints/Do.Blueprints.Service.Application/Orm/Default/DefaultOrmFeature.cs +++ b/src/blueprints/Do.Blueprints.Service.Application/Orm/Default/DefaultOrmFeature.cs @@ -4,9 +4,12 @@ using Do.Orm.Default.UserTypes; using FluentNHibernate.Conventions.Helpers; using FluentNHibernate.Mapping; +using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using NHibernate; +using NHibernate.Exceptions; namespace Do.Orm.Default; @@ -18,7 +21,6 @@ public void Configure(LayerConfigurator configurator) { services.AddScoped(typeof(IEntityContext<>), typeof(EntityContext<>)); services.AddSingleton(typeof(IQueryContext<>), typeof(QueryContext<>)); - services.AddSingleton(); }); configurator.ConfigureAutoPersistenceModel(model => @@ -75,6 +77,24 @@ public void Configure(LayerConfigurator configurator) configurator.ConfigureMiddlewareCollection(middlewares => { + middlewares.Add(app => + app.Use(async (context, next) => + { + try + { + await next(context); + } + catch (GenericADOException e) + { + context.RequestServices + .GetRequiredService>() + .LogError(e.InnerException, e.InnerException?.Message); + + throw; + } + }) + ); + middlewares.Add(app => { var lifetime = app.ApplicationServices.GetRequiredService(); diff --git a/src/blueprints/Do.Blueprints.Service.Application/Orm/Default/EntityContext.cs b/src/blueprints/Do.Blueprints.Service.Application/Orm/Default/EntityContext.cs index ecaa90204..6f4bdb39a 100644 --- a/src/blueprints/Do.Blueprints.Service.Application/Orm/Default/EntityContext.cs +++ b/src/blueprints/Do.Blueprints.Service.Application/Orm/Default/EntityContext.cs @@ -2,13 +2,9 @@ namespace Do.Orm.Default; -public class EntityContext : IEntityContext +public class EntityContext(ISession _session) + : IEntityContext { - readonly ISession _session; - - public EntityContext(ISession session) => - _session = session; - public TEntity Insert(TEntity entity) { _session.Save(entity); diff --git a/src/blueprints/Do.Blueprints.Service.Application/Orm/Default/QueryContext.cs b/src/blueprints/Do.Blueprints.Service.Application/Orm/Default/QueryContext.cs index e99b96d7a..355ee7a0f 100644 --- a/src/blueprints/Do.Blueprints.Service.Application/Orm/Default/QueryContext.cs +++ b/src/blueprints/Do.Blueprints.Service.Application/Orm/Default/QueryContext.cs @@ -2,13 +2,9 @@ namespace Do.Orm.Default; -public class QueryContext : IQueryContext +public class QueryContext(Func _getSession) + : IQueryContext { - readonly Func _getSession; - - public QueryContext(Func getSession) => - _getSession = getSession; - public TEntity SingleById(Guid id) => _getSession().Get(id) ?? throw RecordNotFoundException.For(id); public IQueryable Query() => _getSession().Query(); } diff --git a/src/blueprints/Do.Blueprints.Service.Application/Orm/Default/UserTypes/JsonObjectStringType.cs b/src/blueprints/Do.Blueprints.Service.Application/Orm/Default/UserTypes/JsonObjectStringType.cs index a5b397179..db54fc28c 100644 --- a/src/blueprints/Do.Blueprints.Service.Application/Orm/Default/UserTypes/JsonObjectStringType.cs +++ b/src/blueprints/Do.Blueprints.Service.Application/Orm/Default/UserTypes/JsonObjectStringType.cs @@ -3,12 +3,10 @@ namespace Do.Orm.Default.UserTypes; -public class JsonObjectStringType : AbstractStringType +public class JsonObjectStringType() + : AbstractStringType(new SqlType(System.Data.DbType.String, ColumnLength)) { const int ColumnLength = 64 * 1024; - public JsonObjectStringType() - : base(new SqlType(System.Data.DbType.String, ColumnLength)) { } - public override string Name => nameof(JsonObjectStringType); } diff --git a/src/blueprints/Do.Blueprints.Service.Application/Orm/Default/UserTypes/ObjectUserType.cs b/src/blueprints/Do.Blueprints.Service.Application/Orm/Default/UserTypes/ObjectUserType.cs index 27ead691f..23229c905 100644 --- a/src/blueprints/Do.Blueprints.Service.Application/Orm/Default/UserTypes/ObjectUserType.cs +++ b/src/blueprints/Do.Blueprints.Service.Application/Orm/Default/UserTypes/ObjectUserType.cs @@ -7,8 +7,8 @@ namespace Do.Orm.Default.UserTypes; public class ObjectUserType : CompositeUserTypeBase { - public override string[] PropertyNames => new[] { "Value" }; - public override IType[] PropertyTypes => new[] { new JsonObjectStringType() }; + public override string[] PropertyNames => ["Value"]; + public override IType[] PropertyTypes => [new JsonObjectStringType()]; public override Type ReturnedClass => typeof(object); public override object GetPropertyValue(object component, int property) => JsonConvert.SerializeObject(component); diff --git a/src/blueprints/Do.Blueprints.Service.Application/ServiceSpec.cs b/src/blueprints/Do.Blueprints.Service.Application/ServiceSpec.cs index 9d1d8a158..70727c2b6 100644 --- a/src/blueprints/Do.Blueprints.Service.Application/ServiceSpec.cs +++ b/src/blueprints/Do.Blueprints.Service.Application/ServiceSpec.cs @@ -1,5 +1,6 @@ using Do.Architecture; using Do.Business; +using Do.Caching; using Do.Core; using Do.Database; using Do.ExceptionHandling; @@ -22,6 +23,7 @@ public abstract class ServiceSpec : Spec protected static ApplicationContext Init( Func>? business = default, + Func>? caching = default, Func>? core = default, Func>? database = default, Func>? exceptionHandling = default, @@ -31,6 +33,7 @@ protected static ApplicationContext Init( ) { business ??= c => c.Default(); + caching ??= c => c.ScopedMemory(); core ??= c => c.Mock(); database ??= c => c.InMemory(); exceptionHandling ??= c => c.Default(); @@ -48,6 +51,7 @@ protected static ApplicationContext Init( app.Layers.AddTesting(); app.Features.AddBusiness(business); + app.Features.AddCaching(caching); app.Features.AddCore(core); app.Features.AddDatabase(database); app.Features.AddExceptionHandling(exceptionHandling); @@ -71,10 +75,13 @@ public override void SetUp() _transaction = Session.BeginTransaction(); - Settings = new(); + Settings = []; MockMe.TheConfiguration(settings: Settings, defaultValueProvider: GetDefaultSettingsValue); - MockMe.TheSystem(now: new DateTime(2023, 09, 09, 10, 10, 00)); + + // This is the initial release date of DO. Do not change this the avoid + // potential "Cannot go back in time." errors. + MockMe.TheTime(now: new DateTime(2023, 06, 15, 16, 59, 00), reset: true); } public override void TearDown() @@ -86,6 +93,7 @@ public override void TearDown() Session.Clear(); GiveMe.The().Reset(); + GiveMe.AMemoryCache(clear: true); } protected virtual string? GetDefaultSettingsValue(string key) => diff --git a/src/blueprints/Do.Blueprints.Service.Application/ServiceSpecExtensions.cs b/src/blueprints/Do.Blueprints.Service.Application/ServiceSpecExtensions.cs index cf6042418..c4126cecf 100644 --- a/src/blueprints/Do.Blueprints.Service.Application/ServiceSpecExtensions.cs +++ b/src/blueprints/Do.Blueprints.Service.Application/ServiceSpecExtensions.cs @@ -1,6 +1,7 @@ -using Do.Core; +using Do.Core.Mock; using Do.MockOverrider; using Do.Testing; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Moq; @@ -52,6 +53,34 @@ public static Guid AGuid(this Stubber _, #endregion + #region Integer + + public static int AnInteger(this Stubber _) => 42; + + #endregion + + #region MemoryCache + + public static IMemoryCache AMemoryCache(this Stubber giveMe, + bool clear = false + ) + { + var getMemoryCache = giveMe.The>(); + var memoryCache = getMemoryCache(); + + if (clear) + { + (memoryCache as MemoryCache)?.Clear(); + } + + return memoryCache; + } + + public static void ShouldHaveCount(this IMemoryCache memoryCache, int count) => + ((MemoryCache)memoryCache).Count.ShouldBe(count); + + #endregion + #region MockOverrider public static T The(this Stubber _, params object?[] mockOverrides) where T : notnull => @@ -126,6 +155,11 @@ public static void ShouldHaveOneParameter(this MethodInfo source) #region Settings + public static void ASetting(this Mocker mockMe, + string? key = default, + T? value = default + ) => mockMe.ASetting(key: key, value: $"{value}"); + public static void ASetting(this Mocker mockMe, string? key = default, string? value = default @@ -145,7 +179,7 @@ internal static IConfiguration TheConfiguration(this Mocker mockMe, ) { defaultValueProvider ??= _ => default; - settings ??= new Dictionary(); + settings ??= []; var configuration = mockMe.Spec.GiveMe.The(); @@ -184,28 +218,32 @@ public static string AString(this Stubber _, #endregion - #region System + #region TimeProvider - public static ISystem TheSystem(this Mocker mockMe, + public static TimeProvider TheTime(this Mocker mockMe, DateTime? now = default, - bool passSomeTime = false + bool passSomeTime = false, + bool reset = false ) { - var system = mockMe.Spec.GiveMe.The(); - var mock = Mock.Get(system); + var fakeTimeProvider = (ResettableFakeTimeProvider)mockMe.Spec.GiveMe.The(); + + if (reset) + { + fakeTimeProvider.Reset(); + } if (now is not null) { - mock.Setup(c => c.Now).Returns(now.Value); + fakeTimeProvider.SetUtcNow(new(now.Value, fakeTimeProvider.LocalTimeZone.BaseUtcOffset)); } if (passSomeTime) { - var localNow = system.Now; - mock.Setup(c => c.Now).Returns(localNow.AddSeconds(1)); + fakeTimeProvider.Advance(TimeSpan.FromSeconds(1)); } - return system; + return fakeTimeProvider; } #endregion diff --git a/src/blueprints/Do.Blueprints.Service.Application/Testing/TestingLayer.cs b/src/blueprints/Do.Blueprints.Service.Application/Testing/TestingLayer.cs index 1339534c1..8955d37f0 100644 --- a/src/blueprints/Do.Blueprints.Service.Application/Testing/TestingLayer.cs +++ b/src/blueprints/Do.Blueprints.Service.Application/Testing/TestingLayer.cs @@ -38,20 +38,18 @@ protected override IEnumerable GetPhases() yield return new Run(); } - public class CreateConfigurationManager : PhaseBase + public class CreateConfigurationManager() + : PhaseBase(PhaseOrder.Earliest) { - public CreateConfigurationManager() : base(PhaseOrder.Earliest) { } - protected override void Initialize() { Context.Add(new ConfigurationManager()); } } - class Run : PhaseBase + class Run() + : PhaseBase(PhaseOrder.Latest) { - public Run() : base(PhaseOrder.Latest) { } - protected override void Initialize(IServiceCollection services) { var serviceProvider = services.BuildServiceProvider(); diff --git a/src/blueprints/Do.Blueprints.Service/Core/Dotnet/TimeProviderExtensions.cs b/src/blueprints/Do.Blueprints.Service/Core/Dotnet/TimeProviderExtensions.cs new file mode 100644 index 000000000..567d6bf28 --- /dev/null +++ b/src/blueprints/Do.Blueprints.Service/Core/Dotnet/TimeProviderExtensions.cs @@ -0,0 +1,7 @@ +namespace System; + +public static class TimeProviderExtensions +{ + public static DateTime GetNow(this TimeProvider timeProvider) => + timeProvider.GetLocalNow().DateTime; +} diff --git a/src/blueprints/Do.Blueprints.Service/Core/ISystem.cs b/src/blueprints/Do.Blueprints.Service/Core/ISystem.cs deleted file mode 100644 index 9bf07c0f2..000000000 --- a/src/blueprints/Do.Blueprints.Service/Core/ISystem.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Do.Core; - -public interface ISystem -{ - DateTime Now { get; } -} diff --git a/src/blueprints/Do.Blueprints.Service/Database/ITransaction.cs b/src/blueprints/Do.Blueprints.Service/Database/ITransaction.cs index ed7d3d752..79eecf4ba 100644 --- a/src/blueprints/Do.Blueprints.Service/Database/ITransaction.cs +++ b/src/blueprints/Do.Blueprints.Service/Database/ITransaction.cs @@ -8,6 +8,6 @@ public interface ITransaction Task CommitAsync(Func action); Task CommitAsync(Func> action); - Task CommitAsync(TEntity entity, Action action); - Task CommitAsync(TEntity entity, Func action); + Task CommitAsync(TEntity? entity, Action action); + Task CommitAsync(TEntity? entity, Func action); } diff --git a/src/blueprints/Do.Blueprints.Service/Do.Blueprints.Service.csproj b/src/blueprints/Do.Blueprints.Service/Do.Blueprints.Service.csproj index e87b8acee..40e713160 100644 --- a/src/blueprints/Do.Blueprints.Service/Do.Blueprints.Service.csproj +++ b/src/blueprints/Do.Blueprints.Service/Do.Blueprints.Service.csproj @@ -13,6 +13,7 @@ + diff --git a/src/blueprints/Do.Blueprints.Service/ExceptionHandling/ExceptionInfo.cs b/src/blueprints/Do.Blueprints.Service/ExceptionHandling/ExceptionInfo.cs index 7f83a1f4f..e707bb2e1 100644 --- a/src/blueprints/Do.Blueprints.Service/ExceptionHandling/ExceptionInfo.cs +++ b/src/blueprints/Do.Blueprints.Service/ExceptionHandling/ExceptionInfo.cs @@ -1,3 +1,8 @@ namespace Do.ExceptionHandling; -public record ExceptionInfo(int Code, object Body); +public record ExceptionInfo( + Exception Exception, + int Code, + string Body, + Dictionary? ExtraData = default +); diff --git a/src/blueprints/Do.Blueprints.Service/ExceptionHandling/HandledException.cs b/src/blueprints/Do.Blueprints.Service/ExceptionHandling/HandledException.cs index 4c9818b82..2c418cc64 100644 --- a/src/blueprints/Do.Blueprints.Service/ExceptionHandling/HandledException.cs +++ b/src/blueprints/Do.Blueprints.Service/ExceptionHandling/HandledException.cs @@ -2,10 +2,14 @@ namespace Do.ExceptionHandling; -public class HandledException : Exception +public abstract class HandledException(string message, Exception? innerException, + Dictionary? extraData = default +) : Exception(message, innerException) { + public virtual Dictionary ExtraData { get; private set; } = extraData ?? []; public virtual HttpStatusCode StatusCode => HttpStatusCode.BadRequest; - public HandledException(string message) : base(message) { } - public HandledException(string message, Exception innerException) : base(message, innerException) { } + public HandledException(string message, + Dictionary? extraData = default + ) : this(message, null, extraData) { } } diff --git a/src/blueprints/Do.Blueprints.Service/Orm/IQueryContext.cs b/src/blueprints/Do.Blueprints.Service/Orm/IQueryContext.cs index 36cf9ca37..a85681893 100644 --- a/src/blueprints/Do.Blueprints.Service/Orm/IQueryContext.cs +++ b/src/blueprints/Do.Blueprints.Service/Orm/IQueryContext.cs @@ -31,7 +31,7 @@ public List By(Expression> where, if (take is not null) { result = result.Take(take.Value); } if (skip is not null) { result = result.Skip(skip.Value); } - return result.ToList(); + return [.. result]; } public List By(Expression> where, @@ -49,7 +49,7 @@ public List By(Expression> where, if (take is not null) { result = result.Take(take.Value); } if (skip is not null) { result = result.Skip(skip.Value); } - return result.ToList(); + return [.. result]; } public List All( @@ -62,7 +62,7 @@ public List All( if (take is not null) { result = result.Take(take.Value); } if (skip is not null) { result = result.Skip(skip.Value); } - return result.ToList(); + return [.. result]; } public List All( @@ -80,7 +80,7 @@ public List All( if (take is not null) { result = result.Take(take.Value); } if (skip is not null) { result = result.Skip(skip.Value); } - return result.ToList(); + return [.. result]; } IQueryable Query(Expression> where) => diff --git a/src/blueprints/Do.Blueprints.Service/Orm/RecordNotFoundException.cs b/src/blueprints/Do.Blueprints.Service/Orm/RecordNotFoundException.cs index 713c2132a..b1503336f 100644 --- a/src/blueprints/Do.Blueprints.Service/Orm/RecordNotFoundException.cs +++ b/src/blueprints/Do.Blueprints.Service/Orm/RecordNotFoundException.cs @@ -3,7 +3,8 @@ namespace Do.Orm; -public class RecordNotFoundException : HandledException +public class RecordNotFoundException(Type entityType, string field, object value) + : HandledException($"{entityType.Name} with {field}: '{value}' does not exist") { public static RecordNotFoundException For(Guid id) => new(typeof(T), id); public static RecordNotFoundException For(string field, object value) => new(typeof(T), field, value); @@ -12,7 +13,4 @@ public class RecordNotFoundException : HandledException public RecordNotFoundException(Type entityType, Guid id) : this(entityType, "Id", $"{id}") { } - - public RecordNotFoundException(Type entityType, string field, object value) - : base($"{entityType.Name} with {field}: '{value}' does not exist") { } } \ No newline at end of file diff --git a/src/core/Do.Architecture/Architecture/Application.cs b/src/core/Do.Architecture/Architecture/Application.cs index dcbe20c1c..e3f656d9a 100644 --- a/src/core/Do.Architecture/Architecture/Application.cs +++ b/src/core/Do.Architecture/Architecture/Application.cs @@ -1,14 +1,10 @@ namespace Do.Architecture; -public class Application +public class Application(ApplicationContext _context) { - readonly ApplicationContext _context; - - public Application(ApplicationContext context) => _context = context; - - readonly List _layers = new(); - readonly List _features = new(); - readonly List _phases = new(); + readonly List _layers = []; + readonly List _features = []; + readonly List _phases = []; internal Application With(ApplicationDescriptor descriptor) { diff --git a/src/core/Do.Architecture/Architecture/ApplicationContext.cs b/src/core/Do.Architecture/Architecture/ApplicationContext.cs index e72852735..1db80398c 100644 --- a/src/core/Do.Architecture/Architecture/ApplicationContext.cs +++ b/src/core/Do.Architecture/Architecture/ApplicationContext.cs @@ -2,7 +2,7 @@ namespace Do.Architecture; public class ApplicationContext { - readonly Dictionary _context = new(); + readonly Dictionary _context = []; public void Add(T item) where T : notnull => _context[typeof(T)] = item; diff --git a/src/core/Do.Architecture/Architecture/ApplicationDescriptor.cs b/src/core/Do.Architecture/Architecture/ApplicationDescriptor.cs index 02d308fbc..3c6dd7413 100644 --- a/src/core/Do.Architecture/Architecture/ApplicationDescriptor.cs +++ b/src/core/Do.Architecture/Architecture/ApplicationDescriptor.cs @@ -2,6 +2,6 @@ namespace Do.Architecture; public class ApplicationDescriptor { - public List Layers { get; } = new(); - public List Features { get; } = new(); + public List Layers { get; } = []; + public List Features { get; } = []; } diff --git a/src/core/Do.Architecture/Architecture/CannotProceedException.cs b/src/core/Do.Architecture/Architecture/CannotProceedException.cs index b80112345..9a4052362 100644 --- a/src/core/Do.Architecture/Architecture/CannotProceedException.cs +++ b/src/core/Do.Architecture/Architecture/CannotProceedException.cs @@ -1,12 +1,8 @@ namespace Do.Architecture; -public class CannotProceedException : Exception -{ - public CannotProceedException(IEnumerable phases) - : base( - "Cannot proceed to run the application. " + - $"Phases ({string.Join(", ", phases.Select(p => p.GetType().Name))}) " + - "won't get ready for initialization." - ) - { } -} +public class CannotProceedException(IEnumerable _phases) + : Exception( + "Cannot proceed to run the application. " + + $"Phases ({string.Join(", ", _phases.Select(p => p.GetType().Name))}) " + + "won't get ready for initialization." + ) { } diff --git a/src/core/Do.Architecture/Architecture/LayerConfigurator.cs b/src/core/Do.Architecture/Architecture/LayerConfigurator.cs index d73d51a0a..7e33f6231 100644 --- a/src/core/Do.Architecture/Architecture/LayerConfigurator.cs +++ b/src/core/Do.Architecture/Architecture/LayerConfigurator.cs @@ -40,7 +40,7 @@ record Target(Type Type, object Value); LayerConfigurator(ApplicationContext context, params Target[] targets) { _context = context; - _targets = new(targets); + _targets = [..targets]; } public ApplicationContext Context => _context; diff --git a/src/core/Do.Architecture/Architecture/OverlappingPhaseException.cs b/src/core/Do.Architecture/Architecture/OverlappingPhaseException.cs index 2b509f62b..4b7ddbbdd 100644 --- a/src/core/Do.Architecture/Architecture/OverlappingPhaseException.cs +++ b/src/core/Do.Architecture/Architecture/OverlappingPhaseException.cs @@ -1,12 +1,8 @@ namespace Do.Architecture; -public class OverlappingPhaseException : Exception -{ - public OverlappingPhaseException(PhaseOrder order, IEnumerable phases) - : base( - $"More than one phase cannot have '{order}' at the same time. " + - "Change the order of phases. Overlapping phases are: " + - $"{string.Join(", ", phases.Where(p => p.Order == order).Select(p => p.GetType().Name))}" - ) - { } -} +public class OverlappingPhaseException(PhaseOrder _order, IEnumerable _phases) + : Exception( + $"More than one phase cannot have '{_order}' at the same time. " + + "Change the order of phases. Overlapping phases are: " + + $"{string.Join(", ", _phases.Where(p => p.Order == _order).Select(p => p.GetType().Name))}" + ) { } diff --git a/src/core/Do.Architecture/Architecture/PhaseBase.cs b/src/core/Do.Architecture/Architecture/PhaseBase.cs index b37d6d885..38d2b311d 100644 --- a/src/core/Do.Architecture/Architecture/PhaseBase.cs +++ b/src/core/Do.Architecture/Architecture/PhaseBase.cs @@ -1,50 +1,41 @@ namespace Do.Architecture; -public abstract class PhaseBase : IPhase +public abstract class PhaseBase(PhaseOrder order = PhaseOrder.Normal) + : IPhase { - readonly PhaseOrder _order; - - protected PhaseBase(PhaseOrder order = PhaseOrder.Normal) => _order = order; - protected virtual ApplicationContext Context { get; private set; } = default!; protected virtual bool IsReady => true; protected virtual void Initialize() { } + PhaseOrder IPhase.Order { get; } = order; ApplicationContext IPhase.Context { get => Context; set => Context = value; } bool IPhase.IsReady => IsReady; - PhaseOrder IPhase.Order => _order; void IPhase.Initialize() => Initialize(); } -public abstract class PhaseBase : PhaseBase +public abstract class PhaseBase(PhaseOrder order = PhaseOrder.Normal) + : PhaseBase(order) { - protected PhaseBase(PhaseOrder order = PhaseOrder.Normal) - : base(order: order) { } - protected override sealed bool IsReady => Context.Has(); protected override sealed void Initialize() => Initialize(Context.Get()); protected abstract void Initialize(T dependency); } -public abstract class PhaseBase : PhaseBase +public abstract class PhaseBase(PhaseOrder order = PhaseOrder.Normal) + : PhaseBase(order) { - protected PhaseBase(PhaseOrder order = PhaseOrder.Normal) - : base(order: order) { } - protected override sealed bool IsReady => Context.Has() && Context.Has(); protected override sealed void Initialize() => Initialize(Context.Get(), Context.Get()); protected abstract void Initialize(T1 dependency1, T2 dependency2); } -public abstract class PhaseBase : PhaseBase +public abstract class PhaseBase(PhaseOrder order = PhaseOrder.Normal) + : PhaseBase(order) { - protected PhaseBase(PhaseOrder order = PhaseOrder.Normal) - : base(order: order) { } - protected override sealed bool IsReady => Context.Has() && Context.Has() && Context.Has(); protected override sealed void Initialize() => Initialize(Context.Get(), Context.Get(), Context.Get()); diff --git a/src/core/Do.Architecture/Architecture/PhaseContext.cs b/src/core/Do.Architecture/Architecture/PhaseContext.cs index 4fabd4589..62cf03cc4 100644 --- a/src/core/Do.Architecture/Architecture/PhaseContext.cs +++ b/src/core/Do.Architecture/Architecture/PhaseContext.cs @@ -1,13 +1,12 @@ namespace Do.Architecture; -public class PhaseContext : IDisposable +public class PhaseContext(IEnumerable configurators) + : IDisposable { - public static readonly PhaseContext Empty = new(Enumerable.Empty()); + public static readonly PhaseContext Empty = new([]); - public IEnumerable Configurators { get; } + public IEnumerable Configurators { get; } = [..configurators]; public Action? OnDispose { get; init; } - public PhaseContext(IEnumerable configurators) => Configurators = new List(configurators); - void IDisposable.Dispose() => OnDispose?.Invoke(); } diff --git a/src/core/Do.Architecture/Architecture/PhaseContextBuilder.cs b/src/core/Do.Architecture/Architecture/PhaseContextBuilder.cs index fff4dd07c..6f5707085 100644 --- a/src/core/Do.Architecture/Architecture/PhaseContextBuilder.cs +++ b/src/core/Do.Architecture/Architecture/PhaseContextBuilder.cs @@ -3,7 +3,7 @@ namespace Do.Architecture; public class PhaseContextBuilder { readonly IPhase _phase = default!; - readonly List _configurators = new(); + readonly List _configurators = []; Action? _onDispose; internal PhaseContextBuilder(IPhase phase) => _phase = phase; diff --git a/src/core/Do.Architecture/Forge.cs b/src/core/Do.Architecture/Forge.cs index 1b2b71a45..167a2f673 100644 --- a/src/core/Do.Architecture/Forge.cs +++ b/src/core/Do.Architecture/Forge.cs @@ -3,19 +3,10 @@ namespace Do; -public class Forge +public class Forge(IBanner _banner, Func _newApplication) { public static Forge New => new(new DoBanner(), () => new(new())); - readonly IBanner _banner; - readonly Func _newApplication; - - public Forge(IBanner banner, Func newApplication) - { - _banner = banner; - _newApplication = newApplication; - } - public Application Application(Action describe) { _banner.Print(); diff --git a/test/blueprints/Do.Test.Blueprints.Service.Application/ConfigurationOverrider/ConfigurationOverriderFeature.cs b/test/blueprints/Do.Test.Blueprints.Service.Application/ConfigurationOverrider/ConfigurationOverriderFeature.cs index 1b6fdfecd..ab399433a 100644 --- a/test/blueprints/Do.Test.Blueprints.Service.Application/ConfigurationOverrider/ConfigurationOverriderFeature.cs +++ b/test/blueprints/Do.Test.Blueprints.Service.Application/ConfigurationOverrider/ConfigurationOverriderFeature.cs @@ -21,7 +21,7 @@ public void Configure(LayerConfigurator configurator) configurator.ConfigureAutoPersistenceModel(model => { - model.Override(x => x.Map(e => e.String).Length(200)); + model.Override(x => x.Map(e => e.String).Length(500)); }); configurator.ConfigureApplicationParts(applicationParts => diff --git a/test/blueprints/Do.Test.Blueprints.Service.Application/Dockerfile b/test/blueprints/Do.Test.Blueprints.Service.Application/Dockerfile index 1d978a0d6..8f7629956 100644 --- a/test/blueprints/Do.Test.Blueprints.Service.Application/Dockerfile +++ b/test/blueprints/Do.Test.Blueprints.Service.Application/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base ARG ENVIRONMENT=Production @@ -6,7 +6,7 @@ EXPOSE 80 ENV ASPNETCORE_ENVIRONMENT=$ENVIRONMENT ENV ASPNETCORE_URLS=http://+:80 -FROM mcr.microsoft.com/dotnet/sdk:7.0 AS publish +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS publish COPY . . RUN dotnet publish ./test/blueprints/Do.Test.Blueprints.Service.Application -c Release -o /app/publish diff --git a/test/blueprints/Do.Test.Blueprints.Service.Application/Program.cs b/test/blueprints/Do.Test.Blueprints.Service.Application/Program.cs index eeab47fcf..06ac8aa69 100644 --- a/test/blueprints/Do.Test.Blueprints.Service.Application/Program.cs +++ b/test/blueprints/Do.Test.Blueprints.Service.Application/Program.cs @@ -1,6 +1,7 @@ Forge.New .Service( database: c => c.MySql().ForDevelopment(c.Sqlite()), + exceptionHandling: ex => ex.Default(typeUrlFormat: "https://do.mouseless.codes/errors/{0}"), configure: app => app.Features.AddConfigurationOverrider() ) .Run(); diff --git a/test/blueprints/Do.Test.Blueprints.Service.Application/RestApi/Analyzer/Entities.generated.cs b/test/blueprints/Do.Test.Blueprints.Service.Application/RestApi/Analyzer/Entities.generated.cs new file mode 100644 index 000000000..0e1d430b7 --- /dev/null +++ b/test/blueprints/Do.Test.Blueprints.Service.Application/RestApi/Analyzer/Entities.generated.cs @@ -0,0 +1,100 @@ +// This file will be auto-generated + +using Do.Orm; +using Microsoft.AspNetCore.Mvc; + +namespace Do.Test; + +[ApiController] +public class EntitiesController +{ + public record ByRequest( + Guid? Guid = default, + string String = default, + string StringData = default, + int? Int32 = default, + Uri Uri = default, + Status? Status = default, + DateTime? DateTime = default + ); + + [HttpGet] + [Route("entities")] + public List By([FromServices] Entities target, [FromQuery] ByRequest request, [FromQuery] int? take = null, [FromQuery] int? skip = null) + { + var result = target.By( + guid: request.Guid, + @string: request.String, + stringData: request.StringData, + int32: request.Int32, + uri: request.Uri, + status: request.Status, + dateTime: request.DateTime, + take: take, + skip: skip + ); + + return result; + } + + [HttpGet] + [Route("entities/{id}")] + public Entity Get([FromServices] IQueryContext entityQuery, Guid id) + { + return entityQuery.SingleById(id); + } + + public record NewRequest( + Guid Guid = default, + string String = default, + string StringData = default, + int Int32 = default, + Uri Uri = default, + object Dynamic = default, + Status Status = default, + DateTime DateTime = default + ); + + [HttpPost] + [Route("entities")] + public Entity New([FromServices] Func newTarget, [FromBody] NewRequest request) + { + var target = newTarget(); + + return target.With(request.Guid, request.String, request.StringData, request.Int32, request.Uri, request.Dynamic, request.Status, request.DateTime); + } + + [HttpDelete] + [Route("entities/{id}")] + public void Delete([FromServices] IQueryContext entityQuery, [FromRoute] Guid id) + { + var target = entityQuery.SingleById(id); + + target.Delete(); + } + + public record UpdateRequest( + Guid Guid = default, + string String = default, + string StringData = default, + int Int32 = default, + Uri Uri = default, + object Dynamic = default, + Status Status = default, + DateTime DateTime = default, + bool useTransaction = false, + bool throwError = false + ); + + [HttpPut] + [Route("entities/{id}")] + public async Task Update([FromServices] IQueryContext entityQuery, [FromRoute] Guid id, [FromBody] UpdateRequest request) + { + var target = entityQuery.SingleById(id); + + await target.Update(request.Guid, request.String, request.StringData, request.Int32, request.Uri, request.Dynamic, request.Status, request.DateTime, + useTransaction: request.useTransaction, + throwError: request.throwError + ); + } +} diff --git a/test/blueprints/Do.Test.Blueprints.Service.Application/RestApi/Analyzer/Entity.generated.cs b/test/blueprints/Do.Test.Blueprints.Service.Application/RestApi/Analyzer/Entity.generated.cs deleted file mode 100644 index 56004dec4..000000000 --- a/test/blueprints/Do.Test.Blueprints.Service.Application/RestApi/Analyzer/Entity.generated.cs +++ /dev/null @@ -1,72 +0,0 @@ -// This file will be auto-generated - -using Do.Orm; -using Microsoft.AspNetCore.Mvc; - -namespace Do.Test; - -[ApiController] -public class EntityController -{ - readonly IServiceProvider _serviceProvider; - - public EntityController(IServiceProvider serviceProvider) => - _serviceProvider = serviceProvider; - - [HttpGet] - [Route("entities")] - public List By([FromQuery] string @string = default, [FromQuery] int? take = null, [FromQuery] int? skip = null) - { - var target = _serviceProvider.GetRequiredService(); - - var result = target.By(@string: @string, take: take, skip: skip); - - return result; - } - - [HttpGet] - [Route("entities/{id}")] - public Entity Get(Guid id) - { - var target = _serviceProvider.GetRequiredService>(); - - return target.SingleById(id); - } - - public record NewRequest(Guid Guid, string String, string StringData, int Int32, Uri Uri, object Dynamic, Status Status); - - [HttpPost] - [Route("entities")] - public Entity New([FromBody] NewRequest request) - { - var target = _serviceProvider.GetRequiredService(); - - return target.With(request.Guid, request.String, request.StringData, request.Int32, request.Uri, request.Dynamic, request.Status); - } - - [HttpDelete] - [Route("entities/{id}")] - public void Delete([FromRoute] Guid id) - { - var target = _serviceProvider.GetRequiredService>().SingleById(id); - - target.Delete(); - } - - public record UpdateRequest(Guid Guid, string String, string StringData, int Int32, Uri Uri, object Dynamic, Status Status, - bool useTransaction = false, - bool throwError = false - ); - - [HttpPut] - [Route("entities/{id}")] - public async Task Update([FromRoute] Guid id, [FromBody] UpdateRequest request) - { - var target = _serviceProvider.GetRequiredService>().SingleById(id); - - await target.Update(request.Guid, request.String, request.StringData, request.Int32, request.Uri, request.Dynamic, request.Status, - useTransaction: request.useTransaction, - throwError: request.throwError - ); - } -} diff --git a/test/blueprints/Do.Test.Blueprints.Service.Application/RestApi/Analyzer/Singleton.generated.cs b/test/blueprints/Do.Test.Blueprints.Service.Application/RestApi/Analyzer/Singleton.generated.cs index 1c0a4a879..a0ce25501 100644 --- a/test/blueprints/Do.Test.Blueprints.Service.Application/RestApi/Analyzer/Singleton.generated.cs +++ b/test/blueprints/Do.Test.Blueprints.Service.Application/RestApi/Analyzer/Singleton.generated.cs @@ -1,5 +1,6 @@ // This file will be auto-generated +using Do.Orm; using Microsoft.AspNetCore.Mvc; namespace Do.Test; @@ -7,18 +8,11 @@ namespace Do.Test; [ApiController] public class SingletonController { - readonly IServiceProvider _serviceProvider; - - public SingletonController(IServiceProvider serviceProvider) => - _serviceProvider = serviceProvider; - [HttpGet] [Produces("application/json")] [Route("singleton/time")] - public DateTime GetNow() + public DateTime GetNow([FromServices] Singleton target) { - var target = _serviceProvider.GetRequiredService(); - var result = target.GetNow(); return result; @@ -27,40 +21,46 @@ public DateTime GetNow() [HttpPost] [Produces("application/json")] [Route("singleton/test-transaction-action")] - public async Task TestTransactionAction() + public async Task TestTransactionAction([FromServices] Singleton target) { - var target = _serviceProvider.GetRequiredService(); - await target.TestTransactionAction(); } [HttpPost] [Produces("application/json")] [Route("singleton/test-transaction-func")] - public async Task TestTransactionFunc() + public async Task TestTransactionFunc([FromServices] Singleton target) { - var target = _serviceProvider.GetRequiredService(); - await target.TestTransactionFunc(); } [HttpPost] [Produces("application/json")] [Route("singleton/test-exception")] - public void TestException(bool handled) + public void TestException([FromServices] Singleton target, bool handled) { - var target = _serviceProvider.GetRequiredService(); - target.TestException(handled); } + public record TestTransactionNullableRequest(Guid? EntityId = default); + + [HttpPost] + [Produces("application/json")] + [Route("singleton/test-transaction-nullable")] + public async Task TestTransactionNullable([FromServices] Singleton target, [FromServices] IQueryContext entityQuery, [FromBody] TestTransactionNullableRequest request) + { + await target.TestTransactionNullable( + entity: request.EntityId.HasValue + ? entityQuery.SingleById(request.EntityId.Value) + : null + ); + } + [HttpPut] [Produces("application/json")] [Route("singleton/test-async-object")] - public async Task TestAsyncObject([FromBody] object request) + public async Task TestAsyncObject([FromServices] Singleton target, [FromBody] object request) { - var target = _serviceProvider.GetRequiredService(); - var result = await target.TestAsyncObject(request); return result; @@ -69,10 +69,8 @@ public async Task TestAsyncObject([FromBody] object request) [HttpPut] [Produces("application/json")] [Route("singleton/test-object")] - public object TestObject([FromBody] object request) + public object TestObject([FromServices] Singleton target, [FromBody] object request) { - var target = _serviceProvider.GetRequiredService(); - var result = target.TestObject(request); return result; diff --git a/test/blueprints/Do.Test.Blueprints.Service.Test/Caching/UsingScopedMemory.cs b/test/blueprints/Do.Test.Blueprints.Service.Test/Caching/UsingScopedMemory.cs new file mode 100644 index 000000000..b5a3d1006 --- /dev/null +++ b/test/blueprints/Do.Test.Blueprints.Service.Test/Caching/UsingScopedMemory.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.Caching.Memory; + +namespace Do.Test.Caching; + +public class UsingScopedMemory : TestServiceSpec +{ + public override void TearDown() + { + base.TearDown(); + + GiveMe.AMemoryCache().ShouldHaveCount(0); + } + + [Test] + public void Objects_can_be_cached_in__scoped_memory() + { + var cache = GiveMe.AMemoryCache(); + var expected = cache.GetOrCreate("key", _ => new()); + + var actual = cache.GetOrCreate("key", _ => new()); + + actual.ShouldBeSameAs(expected); + cache.ShouldHaveCount(1); + } +} diff --git a/test/blueprints/Do.Test.Blueprints.Service.Test/Configuration/MockingConfiguration.cs b/test/blueprints/Do.Test.Blueprints.Service.Test/Configuration/MockingConfiguration.cs index 87ec72ef9..1d6d102a3 100644 --- a/test/blueprints/Do.Test.Blueprints.Service.Test/Configuration/MockingConfiguration.cs +++ b/test/blueprints/Do.Test.Blueprints.Service.Test/Configuration/MockingConfiguration.cs @@ -15,6 +15,28 @@ public void Mock_configuration_returns_mocked_settings_value() actual.ShouldBe(10); } + [Test] + public void Mock_configuration_returns_default_value_when_not_set() + { + MockMe.ASetting("Config"); + var configuration = GiveMe.The(); + + var actual = configuration.GetRequiredValue("Config"); + + actual.ShouldBe(0); + } + + [Test] + public void Mock_ASetting_value_parameter_is_generic([Values(42, "value", false)] T value) + { + MockMe.ASetting("Config", value); + var configuration = GiveMe.The(); + + var actual = configuration.GetRequiredValue("Config"); + + actual.ShouldBeEquivalentTo(value); + } + [TestCase("Int", 42)] // defined in TestServiceSpec [TestCase("String", "test value")] // defined in TestServiceSpec public void Mock_configuration_uses_settings_value_provider_for_not_mocked_config_sections(string key, object value) diff --git a/test/blueprints/Do.Test.Blueprints.Service.Test/DataAccess/MappingProperties.cs b/test/blueprints/Do.Test.Blueprints.Service.Test/DataAccess/MappingProperties.cs new file mode 100644 index 000000000..3653b67da --- /dev/null +++ b/test/blueprints/Do.Test.Blueprints.Service.Test/DataAccess/MappingProperties.cs @@ -0,0 +1,92 @@ +namespace Do.Test.DataAccess; + +public class MappingProperties : TestServiceSpec +{ + [Test] + public async Task Guid() + { + var entity = GiveMe.An().With(guid: GiveMe.AGuid("EB8DD0A1-07B7-42EC-AAD3-14B2A623C01E")); + entity.Guid.ShouldBe(GiveMe.AGuid("EB8DD0A1-07B7-42EC-AAD3-14B2A623C01E")); + + await entity.Update(guid: GiveMe.AGuid("AB8DD0A1-07B7-42EC-AAD3-14B2A623C01E")); + entity.Guid.ShouldBe(GiveMe.AGuid("AB8DD0A1-07B7-42EC-AAD3-14B2A623C01E")); + + var actual = GiveMe.The().By(guid: GiveMe.AGuid("AB8DD0A1-07B7-42EC-AAD3-14B2A623C01E")).FirstOrDefault(); + actual.ShouldBe(entity); + } + + [Test] + public async Task String() + { + var entity = GiveMe.An().With(@string: "string"); + entity.String.ShouldBe("string"); + + await entity.Update(@string: "test"); + entity.String.ShouldBe("test"); + + var actual = GiveMe.The().By(@string: "test").FirstOrDefault(); + actual.ShouldBe(entity); + } + + [Test] + public async Task String_data() + { + var entity = GiveMe.An().With(stringData: "string"); + entity.StringData.ShouldBe("string"); + + await entity.Update(stringData: "test"); + entity.StringData.ShouldBe("test"); + + var actual = GiveMe.The().By(stringData: "test").FirstOrDefault(); + actual.ShouldBe(entity); + } + + [Test] + public async Task Object() + { + var entity = GiveMe.An().With(dynamic: new { dynamic = "dynamic" }); + entity.Dynamic.ShouldBe(new { dynamic = "dynamic" }); + + await entity.Update(dynamic: new { update = "update" }); + entity.Dynamic.ShouldBe(new { update = "update" }); + } + + [Test] + public async Task Int() + { + var entity = GiveMe.An().With(int32: 5); + entity.Int32.ShouldBe(5); + + await entity.Update(int32: 1); + entity.Int32.ShouldBe(1); + + var actual = GiveMe.The().By(int32: 1).FirstOrDefault(); + actual.ShouldBe(entity); + } + + [Test] + public async Task Enum() + { + var entity = GiveMe.An().With(@enum: Status.Enabled); + entity.Enum.ShouldBe(Status.Enabled); + + await entity.Update(@enum: Status.Disabled); + entity.Enum.ShouldBe(Status.Disabled); + + var actual = GiveMe.The().By(status: Status.Disabled).FirstOrDefault(); + actual.ShouldBe(entity); + } + + [Test] + public async Task DateTime() + { + var entity = GiveMe.An().With(dateTime: GiveMe.ADateTime(year: 2023, month: 11, day: 29)); + entity.DateTime.ShouldBe(GiveMe.ADateTime(year: 2023, month: 11, day: 29)); + + await entity.Update(dateTime: GiveMe.ADateTime(year: 2023, month: 11, day: 30)); + entity.DateTime.ShouldBe(GiveMe.ADateTime(year: 2023, month: 11, day: 30)); + + var actual = GiveMe.The().By(dateTime: GiveMe.ADateTime(year: 2023, month: 11, day: 30)).FirstOrDefault(); + actual.ShouldBe(entity); + } +} diff --git a/test/blueprints/Do.Test.Blueprints.Service.Test/DataAccess/PersistingEntity.cs b/test/blueprints/Do.Test.Blueprints.Service.Test/DataAccess/PersistingEntity.cs new file mode 100644 index 000000000..46bc07683 --- /dev/null +++ b/test/blueprints/Do.Test.Blueprints.Service.Test/DataAccess/PersistingEntity.cs @@ -0,0 +1,43 @@ +namespace Do.Test.DataAccess; + +public class PersistingEntity : TestServiceSpec +{ + [Test] + public void Created_entity_persists() + { + var newEntity = GiveMe.A>(); + + var actual = newEntity().With( + guid: Guid.NewGuid(), + @string: string.Empty, + stringData: string.Empty, + int32: 0, + uri: GiveMe.AUrl(), + @dynamic: new { }, + @enum: Status.Disabled + ); + + actual.ShouldBeInserted(); + } + + [Test] + public void Entity_is_deleted_successfully() + { + var entity = GiveMe.AnEntity(); + + entity.Delete(); + + entity.ShouldBeDeleted(); + } + + [Test] + public void Object_user_type_supports_special_characters_to_be_used_within_strings() + { + var entity = GiveMe.AnEntity(dynamic: new { test = "ğ€@test" }); + var entities = GiveMe.The(); + + Func> task = () => entities.By(@string: entity.String); + + task.ShouldNotThrow(); + } +} \ No newline at end of file diff --git a/test/blueprints/Do.Test.Blueprints.Service.Test/Database/TransactionalWork.cs b/test/blueprints/Do.Test.Blueprints.Service.Test/Database/TransactionalWork.cs new file mode 100644 index 000000000..cb45fc585 --- /dev/null +++ b/test/blueprints/Do.Test.Blueprints.Service.Test/Database/TransactionalWork.cs @@ -0,0 +1,63 @@ +namespace Do.Test.Database; + +public class TransactionalWork : TestServiceSpec +{ + [Test] + public void Commit_async_takes_nullable_parameters() + { + var singleton = GiveMe.The(); + + var task = singleton.TestTransactionNullable(null); + + task.ShouldNotThrow(); + } + + [Test] + public void Commit_async_update_occurs_when_entity_is_not_null() + { + var entity = GiveMe.AnEntity(@string: "string"); + var singleton = GiveMe.The(); + + var task = singleton.TestTransactionNullable(entity); + + task.ShouldNotThrow(); + entity.String.ShouldNotBe("string"); + } + + [Test] + public async Task Transaction_is_skipped_during_tests() + { + var entity = GiveMe.AnEntity(@string: "old"); + + await entity.Update( + @string: "new", + useTransaction: true + ); + + entity.String.ShouldBe("new"); + } + + [Test(Description = "Actual behaviour is not testable, this test is included only for documentation and to improve coverage")] + public void Entity_created_by_a_transaction_committed_asynchronously_persists_when_an_error_occurs() + { + var singleton = GiveMe.The(); + var entities = GiveMe.The(); + + var task = singleton.TestTransactionAction(); + + task.ShouldThrow(); + entities.By().ShouldNotBeEmpty(); + } + + [Test(Description = "Actual behaviour is not testable, this test is included only for documentation and to improve coverage")] + public void Only_the_updates_outside_of_transaction_are_rolled_back_when_an_error_occurs() + { + var singleton = GiveMe.The(); + var entities = GiveMe.The(); + + var task = singleton.TestTransactionFunc(); + + task.ShouldThrow(); + entities.By().ShouldNotBeEmpty(); + } +} diff --git a/test/blueprints/Do.Test.Blueprints.Service.Test/DependencyInjection/ServiceLifetime.cs b/test/blueprints/Do.Test.Blueprints.Service.Test/DependencyInjection/ServiceLifetime.cs new file mode 100644 index 000000000..5191fc1b4 --- /dev/null +++ b/test/blueprints/Do.Test.Blueprints.Service.Test/DependencyInjection/ServiceLifetime.cs @@ -0,0 +1,22 @@ +namespace Do.Test.DependencyInjection; + +public class ServiceLifetime : TestServiceSpec +{ + [Test] + public void A_single_instance_of_singleton_is_shared_across_application() + { + var singleton1 = GiveMe.The(); + var singleton2 = GiveMe.The(); + + singleton1.ShouldBeSameAs(singleton2); + } + + [Test] + public void New_instance_of_transient_is_created_at_each_request() + { + var entity1 = GiveMe.An(); + var entity2 = GiveMe.An(); + + entity1.ShouldNotBeSameAs(entity2); + } +} diff --git a/test/blueprints/Do.Test.Blueprints.Service.Test/DependencyInjection/Time.cs b/test/blueprints/Do.Test.Blueprints.Service.Test/DependencyInjection/Time.cs new file mode 100644 index 000000000..aa5dcf1f7 --- /dev/null +++ b/test/blueprints/Do.Test.Blueprints.Service.Test/DependencyInjection/Time.cs @@ -0,0 +1,15 @@ +namespace Do.Test.DependencyInjection; + +public class Time : TestServiceSpec +{ + [Test] + public void TimeProvider_is_injected_to_access_machine_time() + { + MockMe.TheTime(now: GiveMe.ADateTime(year: 2023, month: 11, day: 29)); + var singleton = GiveMe.The(); + + var actual = singleton.GetNow(); + + actual.ShouldBe(GiveMe.ADateTime(year: 2023, month: 11, day: 29)); + } +} diff --git a/test/blueprints/Do.Test.Blueprints.Service.Test/ExceptionHandling/HandlingExceptions.cs b/test/blueprints/Do.Test.Blueprints.Service.Test/ExceptionHandling/HandlingExceptions.cs new file mode 100644 index 000000000..e9d20ab14 --- /dev/null +++ b/test/blueprints/Do.Test.Blueprints.Service.Test/ExceptionHandling/HandlingExceptions.cs @@ -0,0 +1,14 @@ +namespace Do.Test.ExceptionHandling; + +public class HandlingExceptions : TestServiceSpec +{ + [Test(Description = "Actual behaviour is not testable, this test is included only for documentation and to improve coverage")] + public void HandledException_is_handled_by_default() + { + var singleton = GiveMe.The(); + + var task = () => singleton.TestException(handled: true); + + task.ShouldThrow(); + } +} diff --git a/test/blueprints/Do.Test.Blueprints.Service.Test/TestServiceSpecExtensions.cs b/test/blueprints/Do.Test.Blueprints.Service.Test/TestServiceSpecExtensions.cs new file mode 100644 index 000000000..4dbb86381 --- /dev/null +++ b/test/blueprints/Do.Test.Blueprints.Service.Test/TestServiceSpecExtensions.cs @@ -0,0 +1,30 @@ +using Do.Testing; + +namespace Do.Test; + +public static class TestServiceSpecExtensions +{ + public static Entity AnEntity(this Stubber giveMe, + Guid? guid = default, + string? @string = default, + string? stringData = default, + int? int32 = default, + Uri? uri = default, + object? @dynamic = default, + Status? @enum = default, + DateTime? dateTime = default, + bool? setNowForDateTime = false + ) => giveMe + .A() + .With( + guid ?? Guid.NewGuid(), + @string ?? string.Empty, + stringData ?? string.Empty, + int32 ?? 0, + uri ?? giveMe.AUrl(), + dynamic ?? new { }, + @enum ?? Status.Disabled, + dateTime ?? giveMe.ADateTime(), + setNowForDateTime ?? false + ); +} diff --git a/test/blueprints/Do.Test.Blueprints.Service.Test/TimeProvider/ProvidingTime.cs b/test/blueprints/Do.Test.Blueprints.Service.Test/TimeProvider/ProvidingTime.cs new file mode 100644 index 000000000..3bfd1fce7 --- /dev/null +++ b/test/blueprints/Do.Test.Blueprints.Service.Test/TimeProvider/ProvidingTime.cs @@ -0,0 +1,17 @@ +namespace Do.Test.ExceptionHandling; + +public class ProvidingTime : TestServiceSpec +{ + [Test] + public void When_time_is_given_in_FakeTimeProvider_TimeProvider_returns_the_given_time() + { + var time = GiveMe.ADateTime(2023, 12, 29, 18, 12, 10); + MockMe.TheTime(now: time); + GiveMe.AnEntity(setNowForDateTime: true); + var entities = GiveMe.The(); + + var actual = entities.By(dateTime: time); + + actual.Count.ShouldBeGreaterThan(0); + } +} diff --git a/test/blueprints/Do.Test.Blueprints.Service/Entity.cs b/test/blueprints/Do.Test.Blueprints.Service/Entity.cs index c3073315f..2d9a052e7 100644 --- a/test/blueprints/Do.Test.Blueprints.Service/Entity.cs +++ b/test/blueprints/Do.Test.Blueprints.Service/Entity.cs @@ -3,14 +3,9 @@ namespace Do.Test; -public class Entity +public class Entity(IEntityContext _context, ITransaction _transaction, TimeProvider _timeProvider) { - readonly IEntityContext _context = default!; - readonly ITransaction _transaction = default!; - - protected Entity() { } - public Entity(IEntityContext context, ITransaction transaction) => - (_context, _transaction) = (context, transaction); + protected Entity() : this(default!, default!, default!) { } public virtual Guid Id { get; protected set; } = default!; public virtual Guid Guid { get; protected set; } = default!; @@ -19,27 +14,75 @@ public Entity(IEntityContext context, ITransaction transaction) => public virtual int Int32 { get; protected set; } = default!; public virtual Uri Uri { get; protected set; } = default!; public virtual object Dynamic { get; protected set; } = default!; - public virtual Status Status { get; protected set; } = default!; + public virtual Status Enum { get; protected set; } = default!; + public virtual DateTime DateTime { get; protected set; } = default!; - public virtual Entity With(Guid guid, string @string, string stringData, int int32, Uri uri, object @dynamic, Status status) + public virtual Entity With( + Guid? guid = default, + string? @string = default, + string? stringData = default, + int? int32 = default, + Uri? uri = default, + object? @dynamic = default, + Status? @enum = default, + DateTime? dateTime = default, + bool? setNowForDateTime = default + ) { - Set(guid, @string, stringData, int32, uri, @dynamic, status); + Set( + guid: guid, + @string: @string, + stringData: stringData, + int32: int32, + uri: uri, + @dynamic: @dynamic, + @enum: @enum, + dateTime: setNowForDateTime == true ? _timeProvider.GetNow() : dateTime + ); return _context.Insert(this); } - public virtual async Task Update(Guid guid, string @string, string stringData, int int32, Uri uri, object @dynamic, Status status, + public virtual async Task Update( + Guid? guid = default, + string? @string = default, + string? stringData = default, + int? int32 = default, + Uri? uri = default, + object? @dynamic = default, + Status? @enum = default, + DateTime? dateTime = default, bool useTransaction = false, bool throwError = false ) { if (useTransaction) { - await _transaction.CommitAsync(this, @this => @this.Set(guid, @string, stringData, int32, uri, @dynamic, status)); + await _transaction.CommitAsync(this, @this => + @this.Set( + guid: guid, + @string: @string, + stringData: stringData, + int32: int32, + uri: uri, + @dynamic: @dynamic, + @enum: @enum, + dateTime: dateTime + ) + ); } else { - Set(guid, @string, stringData, int32, uri, @dynamic, status); + Set( + guid: guid, + @string: @string, + stringData: stringData, + int32: int32, + uri: uri, + @dynamic: @dynamic, + @enum: @enum, + dateTime: dateTime + ); } if (throwError) @@ -48,15 +91,25 @@ public virtual async Task Update(Guid guid, string @string, string stringData, i } } - protected virtual void Set(Guid guid, string @string, string stringData, int int32, Uri uri, object @dynamic, Status status) + protected virtual void Set( + Guid? guid = default, + string? @string = default, + string? stringData = default, + int? int32 = default, + Uri? uri = default, + object? @dynamic = default, + Status? @enum = default, + DateTime? dateTime = default + ) { - Guid = guid; - String = @string; - StringData = stringData; - Int32 = int32; - Uri = uri; - Dynamic = @dynamic; - Status = status; + Guid = guid ?? Guid; + String = @string ?? String; + StringData = stringData ?? StringData; + Int32 = int32 ?? Int32; + Uri = uri ?? Uri; + Dynamic = @dynamic ?? Dynamic; + Enum = @enum ?? Enum; + DateTime = dateTime ?? DateTime; } public virtual void Delete() @@ -65,21 +118,31 @@ public virtual void Delete() } } -public class Entities +public class Entities(IQueryContext _context) { - readonly IQueryContext _context; - - public Entities(IQueryContext context) => - _context = context; - public List By( + Guid? guid = default, string? @string = default, + string? stringData = default, + int? int32 = default, + Uri? uri = default, + Status? status = default, + DateTime? dateTime = default, int? take = default, int? skip = default ) { - if (@string == default) { return _context.All(take: take, skip: skip); } - - return _context.By(e => e.String == @string, take: take, skip: skip); + return _context.By( + where: e => + (guid == default || e.Guid == guid) && + (@string == default || e.String == @string) && + (stringData == default || e.StringData == @stringData) && + (int32 == default || e.Int32 == int32) && + (uri == default || e.Uri == uri) && + (status == default || e.Enum == status) && + (dateTime == default || e.DateTime == dateTime), + take: take, + skip: skip + ); } } diff --git a/test/blueprints/Do.Test.Blueprints.Service/Singleton.cs b/test/blueprints/Do.Test.Blueprints.Service/Singleton.cs index d876c63a0..e106a3a1c 100644 --- a/test/blueprints/Do.Test.Blueprints.Service/Singleton.cs +++ b/test/blueprints/Do.Test.Blueprints.Service/Singleton.cs @@ -1,29 +1,16 @@ -using Do.Core; -using Do.Database; -using Do.ExceptionHandling; +using Do.Database; namespace Do.Test; -public class Singleton +public class Singleton(TimeProvider _timeProvider, Func _newEntity, ITransaction _transaction) { - readonly ISystem _system; - readonly Func _newEntity; - readonly ITransaction _transaction; - - public Singleton(ISystem system, Func newEntity, ITransaction transaction) - { - _system = system; - _newEntity = newEntity; - _transaction = transaction; - } - - public DateTime GetNow() => _system.Now; + public DateTime GetNow() => _timeProvider.GetNow(); public void TestException(bool handled) { if (handled) { - throw new HandledException("A handled exception was thrown"); + throw new TestServiceHandledException("A handled exception was thrown"); } throw new InvalidOperationException(); @@ -34,7 +21,16 @@ public async Task TestTransactionAction() await _transaction.CommitAsync(() => { // do not remove this variable, this is to ensure call is made to `Action` overload - var _ = _newEntity().With(Guid.NewGuid(), "test", "transaction action", 1, new("https://action.com"), new { transaction = "action" }, Status.Enabled); + var _ = _newEntity().With( + guid: Guid.NewGuid(), + @string: "test", + stringData: "transaction action", + int32: 1, + uri: new("https://action.com"), + @dynamic: new { transaction = "action" }, + @enum: Status.Enabled, + dateTime: _timeProvider.GetNow() + ); }); throw new(); @@ -43,10 +39,28 @@ await _transaction.CommitAsync(() => public async Task TestTransactionFunc() { var entity = await _transaction.CommitAsync(() => - _newEntity().With(Guid.NewGuid(), "test", "transaction func", 1, new("https://func.com"), new { transaction = "func" }, Status.Enabled) + _newEntity().With( + guid: Guid.NewGuid(), + @string: "test", + stringData: "transaction func", + int32: 1, + uri: new("https://func.com"), + @dynamic: new { transaction = "func" }, + @enum: Status.Enabled, + dateTime: _timeProvider.GetNow() + ) ); - await entity.Update(Guid.NewGuid(), "rollback", "rollback", 2, new("https://rollback.com"), new { rollback = "rollback" }, Status.Disabled); + await entity.Update( + guid: Guid.NewGuid(), + @string: "rollback", + stringData: "rollback", + int32: 2, + uri: new("https://rollback.com"), + @dynamic: new { rollback = "rollback" }, + @enum: Status.Disabled, + dateTime: _timeProvider.GetNow() + ); throw new(); } @@ -59,4 +73,20 @@ public async Task TestAsyncObject(object request) return request; } + + public async Task TestTransactionNullable(Entity? entity) + { + await _transaction.CommitAsync(entity, entity => + entity.Update( + guid: Guid.NewGuid(), + @string: "test", + stringData: "transaction func", + int32: 1, + uri: new("https://func.com"), + @dynamic: new { transaction = "func" }, + @enum: Status.Enabled, + dateTime: _timeProvider.GetNow() + ) + ); + } } diff --git a/test/blueprints/Do.Test.Blueprints.Service/TestServiceHandledException.cs b/test/blueprints/Do.Test.Blueprints.Service/TestServiceHandledException.cs new file mode 100644 index 000000000..df5288c78 --- /dev/null +++ b/test/blueprints/Do.Test.Blueprints.Service/TestServiceHandledException.cs @@ -0,0 +1,6 @@ +using Do.ExceptionHandling; + +namespace Do.Test; + +public class TestServiceHandledException(string message) + : HandledException(message) { } diff --git a/test/core/Do.Test.Architecture/Architecture/Application/AddingExtensions.cs b/test/core/Do.Test.Architecture/Architecture/Application/AddingExtensions.cs index 78540fcb5..8798b888a 100644 --- a/test/core/Do.Test.Architecture/Architecture/Application/AddingExtensions.cs +++ b/test/core/Do.Test.Architecture/Architecture/Application/AddingExtensions.cs @@ -66,7 +66,7 @@ public void Feature_configures_target_configurations_of_the_layers() public void Layers_can_provide_multiple_configuration_targets() { var forge = GiveMe.AForge(); - var layer = MockMe.ALayer(targets: new object[] { "text", 10 }); + var layer = MockMe.ALayer(targets: ["text", 10]); var feature = MockMe.AFeature(); var app = forge.Application(app => @@ -86,7 +86,7 @@ public void Layers_can_provide_multiple_configuration_targets() public void Layers_are_skipped_when_they_provide_no_configuration_target() { var forge = GiveMe.AForge(); - var layer = MockMe.ALayer(targets: new object[0]); + var layer = MockMe.ALayer(targets: []); var feature = MockMe.AFeature(); var app = forge.Application(app => diff --git a/test/core/Do.Test.Architecture/Architecture/Application/RunningAnApplication.cs b/test/core/Do.Test.Architecture/Architecture/Application/RunningAnApplication.cs index 1f912b2c6..0c0e7eddb 100644 --- a/test/core/Do.Test.Architecture/Architecture/Application/RunningAnApplication.cs +++ b/test/core/Do.Test.Architecture/Architecture/Application/RunningAnApplication.cs @@ -10,9 +10,9 @@ public void Application_collects_phases_from_all_layers() var phase1 = MockMe.APhase(); var phase2 = MockMe.APhase(); var phase3 = MockMe.APhase(); - var layer1 = MockMe.ALayer(phases: new[] { phase1, phase2 }); + var layer1 = MockMe.ALayer(phases: [phase1, phase2]); var layer2 = MockMe.ALayer(phase: phase3); - var app = GiveMe.AnApplication(layers: new[] { layer1, layer2 }); + var app = GiveMe.AnApplication(layers: [layer1, layer2]); app.Run(); @@ -44,7 +44,7 @@ public void Each_phase_is_applied_separately_to_all_layers() var layer1 = MockMe.ALayer(phase: phase1); var layer2 = MockMe.ALayer(phase: phase2); - var app = GiveMe.AnApplication(layers: new[] { layer1, layer2 }); + var app = GiveMe.AnApplication(layers: [layer1, layer2]); app.Run(); @@ -152,7 +152,7 @@ public void Application_context_not_found_exception_message_includes_any_type_im [Test] public void Application_context_not_found_exception_message_includes_all_types_if_no_related_type_is_found() { - var context = GiveMe.AnApplicationContext(content1: 'c', content2: 5 ); + var context = GiveMe.AnApplicationContext(content1: 'c', content2: 5); var getAction = () => context.Get(); @@ -169,7 +169,7 @@ public void Application_resolves_which_phase_to_initialize_automatically_by_chec var phaseA = MockMe.APhase(onInitialize: () => phases.Add("phase a"), isReady: () => phases.Contains("phase b")); var phaseB = MockMe.APhase(onInitialize: () => phases.Add("phase b"), isReady: () => phases.Contains("phase c")); var phaseC = MockMe.APhase(onInitialize: () => phases.Add("phase c")); - var layer = MockMe.ALayer(phases: new[] { phaseA, phaseB, phaseC }); + var layer = MockMe.ALayer(phases: [phaseA, phaseB, phaseC]); var app = GiveMe.AnApplication(layer: layer); @@ -187,7 +187,7 @@ public void When_more_than_one_phase_is_ready_at_the_same_time__they_are_initial var phaseA = MockMe.APhase(onInitialize: () => phases.Add("phase a"), order: PhaseOrder.Late); var phaseB = MockMe.APhase(onInitialize: () => phases.Add("phase b"), order: PhaseOrder.Early); - var layer = MockMe.ALayer(phases: new[] { phaseA, phaseB }); + var layer = MockMe.ALayer(phases: [phaseA, phaseB]); var app = GiveMe.AnApplication(layer: layer); @@ -213,7 +213,7 @@ public void Only_one_phase_can_have_earliest_and_latest_priorities_at_the_same_t { var phaseA = MockMe.APhase(order: order); var phaseB = MockMe.APhase(order: order); - var layer = MockMe.ALayer(phases: new[] { phaseA, phaseB }); + var layer = MockMe.ALayer(phases: [phaseA, phaseB]); var app = GiveMe.AnApplication(layer: layer); var action = () => app.Run(); diff --git a/test/core/Do.Test.Architecture/Architecture/Feature/ConfiguringLayers.cs b/test/core/Do.Test.Architecture/Architecture/Feature/ConfiguringLayers.cs index eb7757871..aa1eb7c2e 100644 --- a/test/core/Do.Test.Architecture/Architecture/Feature/ConfiguringLayers.cs +++ b/test/core/Do.Test.Architecture/Architecture/Feature/ConfiguringLayers.cs @@ -9,12 +9,9 @@ public record ConfigurationA public string? Value { get; set; } } - public class FeatureA : IFeature + public class FeatureA(string _value) + : IFeature { - readonly string _value; - - public FeatureA(string value) => _value = value; - public void Configure(LayerConfigurator configurator) { configurator.Configure((ConfigurationA configuration) => configuration.Value = _value); diff --git a/test/core/Do.Test.Architecture/Architecture/Layer/AddingPhases.cs b/test/core/Do.Test.Architecture/Architecture/Layer/AddingPhases.cs index aa3c8aad3..c9bd44638 100644 --- a/test/core/Do.Test.Architecture/Architecture/Layer/AddingPhases.cs +++ b/test/core/Do.Test.Architecture/Architecture/Layer/AddingPhases.cs @@ -39,15 +39,9 @@ public void Layer_overrides_base_method_to_add_new_phases() phases.ShouldContain(phase => phase is TwoPhaseLayer.DoB); } - class IndependentAddsString : PhaseBase + class IndependentAddsString(string _artifact) + : PhaseBase { - readonly string _artifact; - - public IndependentAddsString(string artifact) - { - _artifact = artifact; - } - protected override void Initialize() { Context.Add(_artifact); @@ -58,7 +52,7 @@ protected override void Initialize() public void Phases_have_initialization_step_before_getting_applied_so_that_they_prepare_and_add_objects_to_application_context() { var context = GiveMe.AnApplicationContext(); - IPhase phase = new IndependentAddsString(artifact: "test"); + IPhase phase = new IndependentAddsString(_artifact: "test"); GiveMe.AnApplication(context: context, phase: phase); phase.Initialize(); @@ -66,17 +60,9 @@ public void Phases_have_initialization_step_before_getting_applied_so_that_they_ context.ShouldHave("test"); } - class StringDependentAddsInt : PhaseBase + class StringDependentAddsInt(string _expectedString, int _artifact) + : PhaseBase { - readonly string _expectedString; - readonly int _artifact; - - public StringDependentAddsInt(string expectedString, int artifact) - { - _expectedString = expectedString; - _artifact = artifact; - } - protected override void Initialize(string dependency) { dependency.ShouldBe(_expectedString); @@ -85,19 +71,9 @@ protected override void Initialize(string dependency) } } - class StringAndIntDependentAddsBool : PhaseBase + class StringAndIntDependentAddsBool(string _expectedString, int _expectedInt, bool _artifact) + : PhaseBase { - readonly string _expectedString; - readonly int _expectedInt; - readonly bool _artifact; - - public StringAndIntDependentAddsBool(string expectedString, int expectedInt, bool artifact) - { - _expectedString = expectedString; - _expectedInt = expectedInt; - _artifact = artifact; - } - protected override void Initialize(string dependency1, int dependency2) { dependency1.ShouldBe(_expectedString); @@ -107,21 +83,9 @@ protected override void Initialize(string dependency1, int dependency2) } } - class StringIntAndBoolDependentAddsChar : PhaseBase + class StringIntAndBoolDependentAddsChar(string _expectedString, int _expectedInt, bool _expectedBool, char _artifact) + : PhaseBase { - readonly string _expectedString; - readonly int _expectedInt; - readonly bool _expectedBool; - readonly char _artifact; - - public StringIntAndBoolDependentAddsChar(string expectedString, int expectedInt, bool expectedBool, char artifact) - { - _expectedString = expectedString; - _expectedInt = expectedInt; - _expectedBool = expectedBool; - _artifact = artifact; - } - protected override void Initialize(string dependency1, int dependency2, bool dependency3) { dependency1.ShouldBe(_expectedString); @@ -135,7 +99,7 @@ protected override void Initialize(string dependency1, int dependency2, bool dep [Test] public void Gives_error_when_dependency_is_not_the_exact_type() { - IPhase phase = new StringDependentAddsInt(expectedString: GiveMe.AString(), artifact: GiveMe.AnInt()); + IPhase phase = new StringDependentAddsInt(_expectedString: GiveMe.AString(), _artifact: GiveMe.AnInt()); var app = GiveMe.AnApplication( context: GiveMe.AnApplicationContext(content: GiveMe.AnInt()), phase: phase @@ -152,25 +116,25 @@ public void Phases_may_depend_on_one_or_more_objects_to_appear_in_context() var context = GiveMe.AnApplicationContext(); var app = GiveMe.AnApplication( context: context, - phases: new IPhase[] - { + phases: + [ new StringIntAndBoolDependentAddsChar( - expectedString: "test", - expectedInt: 42, - expectedBool: true, - artifact: 'a' + _expectedString: "test", + _expectedInt: 42, + _expectedBool: true, + _artifact: 'a' ), new StringAndIntDependentAddsBool( - expectedString: "test", - expectedInt: 42, - artifact: true + _expectedString: "test", + _expectedInt: 42, + _artifact: true ), new StringDependentAddsInt( - expectedString: "test", - artifact: 42 + _expectedString: "test", + _artifact: 42 ), - new IndependentAddsString(artifact: "test"), - } + new IndependentAddsString(_artifact: "test"), + ] ); app.Run(); @@ -178,10 +142,9 @@ public void Phases_may_depend_on_one_or_more_objects_to_appear_in_context() context.ShouldHave('a'); } - class OrderedPhase : PhaseBase - { - public OrderedPhase(PhaseOrder order) : base(order) { } - } + public class OrderedPhase(PhaseOrder _order) + : PhaseBase(_order) + { } [TestCase(PhaseOrder.Early)] [TestCase(PhaseOrder.Late)] diff --git a/test/core/Do.Test.Architecture/ArchitectureSpecExtensions.cs b/test/core/Do.Test.Architecture/ArchitectureSpecExtensions.cs index 0a98d44cc..6eeedf4a2 100644 --- a/test/core/Do.Test.Architecture/ArchitectureSpecExtensions.cs +++ b/test/core/Do.Test.Architecture/ArchitectureSpecExtensions.cs @@ -34,8 +34,8 @@ public static Application AnApplication(this Stubber giveMe, ApplicationContext? context = default ) { - layers ??= new[] { layer ?? giveMe.Spec.MockMe.ALayer(phase: phase, phases: phases) }; - features ??= new[] { feature ?? giveMe.Spec.MockMe.AFeature() }; + layers ??= [layer ?? giveMe.Spec.MockMe.ALayer(phase: phase, phases: phases)]; + features ??= [feature ?? giveMe.Spec.MockMe.AFeature()]; return giveMe.AForge(context: context).Application(app => { @@ -138,7 +138,7 @@ public static ILayer ALayer(this Mocker mockMe, ) { phaseContext ??= mockMe.Spec.GiveMe.APhaseContext(target: target, targets: targets); - phases ??= new[] { phase ?? mockMe.APhase() }; + phases ??= [phase ?? mockMe.APhase()]; var result = new Mock(); result.Setup(l => l.GetPhases()).Returns(phases); @@ -225,7 +225,7 @@ static LayerConfigurator ALayerConfigurator(this Stubber giveMe, .FirstOrDefault(c => c.Name == nameof(LayerConfigurator.Create) && c.GetGenericArguments().Length == 1); create.ShouldNotBeNull(); - var configurator = create.MakeGenericMethod(target.GetType()).Invoke(null, new[] { context, target }); + var configurator = create.MakeGenericMethod(target.GetType()).Invoke(null, [context, target]); configurator.ShouldNotBeNull(); return (LayerConfigurator)configurator; @@ -288,7 +288,7 @@ public static PhaseContext APhaseContext(this Stubber giveMe, Action? onDispose = default ) { - targets ??= new[] { target ?? new() }; + targets ??= [target ?? new()]; onDispose ??= () => { }; return new(targets.Select(t => giveMe.ALayerConfigurator(context: context, target: t)).ToList()) diff --git a/test/runsettings.xml b/test/runsettings.xml index 3c450b593..609f420f1 100644 --- a/test/runsettings.xml +++ b/test/runsettings.xml @@ -5,7 +5,7 @@ cobertura - [Do.Blueprints.*]*,[Do.Test.Blueprints.*]* + [Do.Blueprints.*]*,[Do.Test.Blueprints.Service.*]* false false