Base solution for your next web application

Activities of "marble68"

Vanilla 13.1.1 project, on postgress - Adding a basic entity with GUID ID column has scaffolding issues.

No rendering get entity for view and tests with min max length are reversed.

Just an FYI.

Answer

You can make a console application that hits your API, then move it to a class.

Do that first. There's a plethora of examples out there.

Consider testing your calls with postman.

Then, create a windows service project, add your class to it.

Have your windows service perform the processes.

This may of value to you: https://learn.microsoft.com/en-us/dotnet/core/extensions/windows-service

There are a multitude of windows service projects out there you can check to create your own.

If you have swagger enabled -> this may be of distinct value to you: https://stackoverflow.com/questions/54094688/generate-net-client-from-swagger

You can put your credentials in the service's configuration file - but if you don't have direct control of where you install it, you may want to consider another strategy.

For anyone coming across this - since anyone who has permissions to set the visual settings for a tenant site, or as a host, site wide, does not have the ability to set their own personal theme - here's a dead simple way to do it. My project is jQuery based.

In the UiCustomizationViewModel.cs file, add a boolean for personal settings, as I've added PersonalSettings below.

    public class UiCustomizationViewModel
    {
        public string Theme { get; set; }

        public List<ThemeSettingsDto> Settings { get; set; }

        public bool HasUiCustomizationPagePermission { get; set; }
        public bool PersonalSettings { get; set; }

        public ThemeSettingsDto GetThemeSettings(string themeName)
        {
            return Settings.First(s => s.Theme == themeName);
        }
    }

Next, in your UiCustomizationController, you'll want a flag for if the user is setting their personal settings. This could likely be done in reverse, but this is how I did it. Note the new personal parameter with the default value and how it's set on the view model.

        public async Task<ActionResult> Index(bool personal = false)
        {
            var model = new UiCustomizationViewModel
            {
                Theme = await SettingManager.GetSettingValueAsync(AppSettings.UiManagement.Theme),
                Settings = await _uiCustomizationAppService.GetUiManagementSettings(),
                HasUiCustomizationPagePermission = await PermissionChecker.IsGrantedAsync(AppPermissions.Pages_Administration_UiCustomization),
                PersonalSettings = personal
            };

            return View(model);
        }

Then, in the index.cshtml under UiCustomization, you'll need to modify which of the buttons are shown based on the personal settings, like so:

Change the line that reads:

@if (Model.HasUiCustomizationPagePermission)

to read

@if (Model.PersonalSettings==false && Model.HasUiCustomizationPagePermission)

Lastly, you'll want to modify the dropdown menu html in the file Default.cshtml under AppUserMenu.

In that file, you remove or comment out the if statement that hides visual settings if the user has permission to visual settings. Comment out this if statement:

@if (!Model.HasUiCustomizationPagePermission)

Then, modify the action to pass the parameter personal=true so the controller / page knows you're wanting to edit your personal settings.

It should look like this:

@Url.Action("Index", "UiCustomization",new {personal=true})

That's it!

Now, an admin can set a system default, but use another theme for themselves. Users without a custom theme will get the default.

I've tested at host and tenant level and haven't seen any issues with it.

This allows for a better admin user experience.

Has anyone re-used pages and found a way to dynamically generate the breadcrumbs so they determine the previous page correctly?

For example, I have a page that is referenced in multiple locations throughout my solution.

The breadcrumbs are a list of breadcrumbitem and the helper renders it.

However, these are basically just hard coded.

Would it be better to do this client side and examine the history of the browser, or look at referrer (which could possibly be blocked)?

Not sure how to address this. I want to keep my code DRY.

For those who aren't familiar with Serilog and want to break their logs into logical domains, here's a configuration to get you started.

I've not broken out healthchecks and web requests, but this basically puts anything logged from an app service in one log, anything from entityframework in another, and anything from backgroundworkers in yet another.

Lastly - anything not logged into its own file lands in the general 'logs' file.

Obviously, there's a gigantic advantage to having the log files setup this way.

Of note, I have my serilog configuration in my appsettings (see above).

 "Serilog": {
    "Using": [
      "Serilog.Sinks.File",
      "Serilog.Expressions"

    ],
    "MinimumLevel": "Debug",
    "WriteTo": [
      {
        "Name": "Logger",
        "Args": {
          "configureLogger": {
            "Filter": [
              {
                "Name": "ByIncludingOnly",
                "Args": {
                  "expression": "EndsWith(SourceContext, 'BackgroundWorker')"
                }
              }
            ],
            "WriteTo": [
              {
                "Name": "File",
                "Args": {
                  "path": "./App_Data/Logs/BackgroundWorkersLog-.txt",
                  "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj} -- [{SourceContext}]{NewLine}{Exception}",
                  "rollingInterval": "Day",
                  "retainedFileCountLimit": 20,
                  "rollOnFileSizeLimit": true,
                  "fileSizeLimitBytes": 500000000
                }
              }
            ]
          }
        }
      },
      {
        "Name": "Logger",
        "Args": {
          "configureLogger": {
            "Filter": [
              {
                "Name": "ByIncludingOnly",
                "Args": {
                  "expression": "StartsWith(SourceContext, 'Microsoft.EntityFrameworkCore') or StartsWith(SourceContext, 'Microsoft.AspNetCore')"
                }
              }
            ],
            "WriteTo": [
              {
                "Name": "File",
                "Args": {
                  "path": "./App_Data/Logs/EntityFrameworkLog-.txt",
                  "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj} -- [{SourceContext}]{NewLine}{Exception}",
                  "rollingInterval": "Day",
                  "retainedFileCountLimit": 20,
                  "rollOnFileSizeLimit": true,
                  "fileSizeLimitBytes": 500000000
                }
              }
            ]
          }
        }
      },
      {
        "Name": "Logger",
        "Args": {
          "configureLogger": {
            "Filter": [
              {
                "Name": "ByIncludingOnly",
                "Args": {
                  "expression": "EndsWith(SourceContext, 'AppService')"
                }
              }
            ],
            "WriteTo": [
              {
                "Name": "File",
                "Args": {
                  "path": "./App_Data/Logs/AppServicesLog-.txt",
                  "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj} -- [{SourceContext}]{NewLine}{Exception}",
                  "rollingInterval": "Day",
                  "retainedFileCountLimit": 20,
                  "rollOnFileSizeLimit": true,
                  "fileSizeLimitBytes": 500000000
                }
              }
            ]
          }
        }
      },
      {
        "Name": "Logger",
        "Args": {
          "configureLogger": {
            "Filter": [
              {
                "Name": "ByExcluding",
                "Args": {
                  "expression": "EndsWith(SourceContext, 'AppService') or EndsWith(SourceContext, 'BackgroundWorker') or StartsWith(SourceContext, 'Microsoft.EntityFrameworkCore') or StartsWith(SourceContext, 'Microsoft.AspNetCore')"
                }
              }
            ],
            "WriteTo": [
              {
                "Name": "File",
                "Args": {
                  "path": "./App_Data/Logs/Log-.txt",
                  "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj} -- [{SourceContext}]{NewLine}{Exception}",
                  "rollingInterval": "Day",
                  "retainedFileCountLimit": 20,
                  "rollOnFileSizeLimit": true,
                  "fileSizeLimitBytes": 500000000
                }
              }
            ]
          }
        }
      }
    ],
    "Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ]
  }

Hi @Astech - Yeah, sort of.

Until ismcagdas get some of these libraries updates - you have to use the version 4 Serilog stuff.

So basically - I added Serilog: Settings.Configuration, sinks.file, expressions, and extensions hosting -

For Castle, it wants a factory. Serilog factory wants a serilog configuration - so, what you need in startup is:

                var _loggerConfig = new LoggerConfiguration()
                    .ReadFrom.Configuration(_appConfiguration)
                    .CreateLogger();

                options.IocManager.IocContainer.AddFacility<LoggingFacility>(
                     f => f.LogUsing<SerilogFactory>(new SerilogFactory(_loggerConfig))
                );

In your appsettings - put your Serilog configuration in there. Something like this very basic configuration:

  "Serilog": {
    "Using": [
      "Serilog.Sinks.File",
      "Serilog.Expressions"

    ],
    "MinimumLevel": "Debug",
    "WriteTo": [
      {
        "Name": "Logger",
        "Args": {
          "configureLogger": {
            "Filter": [
            ],
            "WriteTo": [
              {
                "Name": "File",
                "Args": {
                  "path": "./App_Data/Logs/SeriLog-.txt",
                  "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj} -- [{SourceContext}]{NewLine}{Exception}",
                  "rollingInterval": "Day",
                  "retainedFileCountLimit": 20,
                  "rollOnFileSizeLimit": true,
                  "fileSizeLimitBytes": 500000000
                }
              }
            ]
          }
        }
      },
    ],
    "Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ]
  }

My goal, which is probably like yours, is to get the sourcecontext flowing into serilog so we can break out the ANZ logs by domain etc. more easily.

While Log4Net is adequate for logging, Serilog is on a whole different level IMHO.

I think I'm not getting all the power of Serilog's context routing using this older version, but regardless, I have it almost there.

Now, we wait for Castle Windsor to make it to Castle Core 5.

V11 MVC Core JQuery

I have a host entity (HostEntity) with specific values for the tenants.

For the tenants, I have another entity (TenantEntity) when a FK to the host entity

When the app service, GetAll(), when the query includes HostEntityFK. The purpose is to get the name of the HostEntity for the view model.

However, if I disable the MayHaveTenant filter, I get all TenantEntities.

How can I disable the filter for the hostentityrepository in the IQueryable? Is this possible?

Update - I was finally able to resolve this. The solution was to ensure to use the core classes from ABP, and ensure to substitue in framework core.

I have this in the core project, which is a direct copy from abpzero project with no edits:

using Abp.Domain.Uow;
using Abp.EntityHistory;
using Abp.EntityHistory.Extensions;
using Abp.Events.Bus.Entities;
using Abp.Extensions;
using Abp.Json;
using JetBrains.Annotations;
using System;
using System.Collections.Generic;
using System.Data.Entity.Core;
using System.Data.Entity.Core.Metadata.Edm;
using System.Data.Entity.Core.Objects;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Reflection;
using DbContext = System.Data.Entity.DbContext;
using EntityState = System.Data.Entity.EntityState;
using AbpEntityHistoryHelper = Abp.EntityHistory.EntityHistoryHelper;
using System.Threading.Tasks;
using System.Transactions;

namespace mynamespace.EntityHistory
{
    public class MyEntityHistoryHelper : AbpEntityHistoryHelper, IMyEntityHistoryHelper
    {
        public MyEntityHistoryHelper(IEntityHistoryConfiguration entityHistoryConfiguration, IUnitOfWorkManager unitOfWorkManager) : base(entityHistoryConfiguration, unitOfWorkManager)
        {
        }

        public EntityChangeSet CreateEntityChangeSet(DbContext context)
        {
            var changeSet = new EntityChangeSet
            {
               .... code

            return changeSet;
        }


        public async Task SaveAsync(DbContext context, EntityChangeSet changeSet)
        {
           ... code
        }

        public void Save(DbContext context, EntityChangeSet changeSet)
        {
            ... code
        }

        [CanBeNull]
        protected virtual string GetEntityId(DbEntityEntry entityEntry, EntityType entityType)
        {
             ... code
        }

        [CanBeNull]
        private EntityChange CreateEntityChange(DbEntityEntry entityEntry, EntityType entityType)
        {
             ... code
        }

        private EntityType GetEntityType(ObjectContext context, Type entityType, bool useClrType = true)
        {
             ... code
        }

        private EntitySet GetEntitySet(ObjectContext context, EntityType entityType)
        {
             ... code
        }

        private static bool IsBaseTypeHasElementTypeName(string elementTypeName, EdmType entityEdmType)
        {
            ... code
        }

        /// <summary>
        /// Gets the property changes for this entry.
        /// </summary>
        private ICollection<EntityPropertyChange> GetPropertyChanges(DbEntityEntry entityEntry, EntityType entityType, EntitySet entitySet, bool auditedPropertiesOnly)
        {
             ... code
        }

        /// <summary>
        /// Gets the property changes for this entry.
        /// </summary>
        private ICollection<EntityPropertyChange> GetRelationshipChanges(DbEntityEntry entityEntry, EntityType entityType, EntitySet entitySet, ICollection<ObjectStateEntry> relationshipChanges, bool auditedPropertiesOnly)
        {
             ... code
        }

        /// <summary>
        /// Updates change time, entity id, Adds foreign keys, Removes/Updates property changes after SaveChanges is called.
        /// </summary>
        private void UpdateChangeSet(DbContext context, EntityChangeSet changeSet)
        {
            ... code
        }

        private EntityPropertyChange CreateEntityPropertyChange(object oldValue, object newValue, PropertyInfo propertyInfo)
        {
          ...
        }


    }
}

This is a copy of the EntityHistoryHelper in AspBoilderPlate.

I put a break point on all public methods on the first line.

Then, in every single PreInitialize method in the solution, I put the following as the very last command to try and ensure my custom entityhistoryhelper is used instead of the default.

Configuration.ReplaceService<IEntityHistoryHelper, MyEntityHistoryHelper>(DependencyLifeStyle.Transient);

I then edit an entity and Save, SaveAync, or CreateEntityChangeSet are not called.

I've tried basing on EntityHistoryHelperBase as well.

I'm obviously doing something wrong?

Showing 1 to 10 of 238 entries