Extensible ASP.NET Core 2 application - asp.net

I need to create an ASP.NET Core 2 application that can be extensible.
An extension, is a project that reference the main assembly and can extends it adding new controllers, models (with EF migrations), views, razor pages, etc.
Because the extensions need use the main application base classes like base controller, base model or view/page layout, the main application cannot reference the module project (avoid circular references).
I'm not sure how can I achieve this, but the idea is an installation of the main project, can add new functionality simple putting the modules DLL (or by online market in the main application, that download the DLL).
In my research, I found Applications parts, but my problem here is I need specify the part assembly in Startup class, and I need in installed the capacity of install modules without doing any changes in the code.
Some modules, need be extensible too, for example, an accounting module, need to connect with bank, and it have an interface that defines the methods of working with the bank, for example:
public interface IBankingOperation
{
public async void PayInvoiceAsync(Invoice invoice);
// More methods...
}
Then, differents projects can reference the banking assembly and provide implementation for differents banks.
In the main application, this modules can be installed like other modules, but checking the base module is intalled, for example, I can install the Santander module like other module, but only if banking module is installed (module dependency).
In conclusion, I need to create a modular ASP.NET Core 2 application, but the main assembly cannot reference the modules, the modules must reference the main assembly. The modules can contain Controllers, Models, Views, Pages, Etc.

In the main web app you would call a class which loads the extensions in the memory
ModuleManager.LoadModules(Path.Combine(_env.ContentRootPath, "Modules"));
this is the load modules function
public static void LoadModules(string modulesFolder)
{
var appsFolders = new DirectoryInfo(modulesFolder).GetDirectories();
foreach (var appFolder in appsFolders)
{
var binFolder = new DirectoryInfo(Path.Combine(appFolder.FullName, "bin"));
if (!binFolder.Exists)
{
continue;
}
var assemblies = AssemblyProvider.GetAssemblies(binFolder.FullName);
foreach (var assembly in assemblies)
{
var iModuleClass = assembly.GetTypes().FirstOrDefault(type => type.GetInterfaces().Contains(typeof(IModule))
&& type.GetConstructor(Type.EmptyTypes) != null);
if (iModuleClass != null)
{
var module = Activator.CreateInstance(iModuleClass) as IModule;
module.ModuleFolder = appFolder;
Modules.Add(module);
Assemblies.Add(assembly);
break;
}
}
}
}
then you should have an interface which should be implemented by each module the class which implement this interface should do the work of registering services and database models and all staff needed and you will load them as follows
public static IEnumerable<IExtensionRegister> GetModuleRegistrars()
{
lock (Modules)
{
return Modules.Select(item => item.Registrar).Where(item=>item!=null).ToList();
}
}

Related

Add Model Project (EntityFramwork) to Asp.net Core 3 Project

I have 3 projects in my Solution like below:
ModelProject (EntityFramework)
ProjectA (ASP.NET MVC)
ProjectB (ASP.NET CORE 3 with Blazor)
So in model project i defined my tables ,relations, queries and their classes then added that on ProjectA and ProjectB.
on ProjectB (.net core) in startup.cs by services.AddDbContext tries to add dbcontext (ModelProject) but get this error:
Cannot convert lambda expression to type 'ServiceLifetime' because it is not a delegate type
Is this error for using EF on Core Project? how can i solve it?
ProjectB (Core):
services .AddEntityFrameworkSqlServer() .AddDbContext<Models.DataContext>(options => { options.UseSqlServer(Configuration.GetConnectionString("DataContext")); });
and dbcontext (EntityFramework Standard):
namespace ModelProject
{
public class DataContext : DbContext
{
public DataContext() : base("DataContext")
{
Database.SetInitializer(new MigrateDatabaseToLatestVersion<DataContext, Migrations.Configuration>());
(this as IObjectContextAdapter).ObjectContext.ContextOptions.UseCSharpNullComparisonBehavior = true;
((IObjectContextAdapter)this).ObjectContext.CommandTimeout = 300;
}
}
}
It needs to be:
.AddDbContext<Models.DataContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DataContext")));
In other words, without the curly brackets. You could alternatively just return the options instance, but that's all just unnecessary code-bloat. The exception is a red herring. Because your lambda currently is essentially an action (no return) rather than a func, it's not matching the right param for AddDbContext.

XUnit Net Core Web API Integration Test: "The ConnectionString property has not been initialized."

Just trying to build an Integration Test project for a NET Core Web API.
So I've followed a few examples, including this one (https://dotnetcorecentral.com/blog/asp-net-core-web-api-integration-testing-with-xunit/) and naturally, I run into issues. When I run the simple GET test I get an exception:
"System.InvalidOperationException : The ConnectionString property has not been initialized."
Any help would be appreciated.
For server = new TestServer(new WebHostBuilder().UseStartup<Startup>());, you need to manually configure the appsettings.json path like
var server = new TestServer(WebHost.CreateDefaultBuilder()
.UseContentRoot(#"D:\Edward\SourceCode\AspNetCore\Tests\IntegrationTestMVC")
// This is the path for project which needs to be test
.UseStartup<Startup>()
);
For a convenience way, I would suggest you try Basic tests with the default WebApplicationFactory.
The WebApplicationFactory constructor infers the app content root path by searching for a WebApplicationFactoryContentRootAttribute on the assembly containing the integration tests with a key equal to the TEntryPoint assembly System.Reflection.Assembly.FullName. In case an attribute with the correct key isn't found, WebApplicationFactory falls back to searching for a solution file (*.sln) and appends the TEntryPoint assembly name to the solution directory. The app root directory (the content root path) is used to discover views and content files.
Reference:How the test infrastructure infers the app content root path
I had to override CreateHostBuilder in my derived WebApplicationFactory in order to add the configuration for the connection string (since it was read from user secrets).
public class CustomApplicationFactory : WebApplicationFactory<Sedab.MemberAuth.Startup>
{
protected override IHostBuilder CreateHostBuilder()
{
var initialData = new List<KeyValuePair<string, string>> {
new KeyValuePair<string, string>("ConnectionStrings:DefaultConnection", "test")
};
return base.CreateHostBuilder().ConfigureHostConfiguration(config => config.AddInMemoryCollection(initialData));
}
}

Register all the services at once using Default DI Container from ASP.NET 5 similar to Autofac

With ASP.NET 5, there's already a DI shipped as default and it looks interesting. I have been using Autofac with MVC 5 which has the option to register all the assembly at once. Here is a sample code that register all the classes that ends with "Service" in Autofac.
// Autofac Configuration for API
var builder = new ContainerBuilder();
builder.RegisterModule(new ServiceModule());
...
...
builder.RegisterAssemblyTypes(Assembly.Load("IMS.Service"))
.Where(t => t.Name.EndsWith("Service"))
.AsImplementedInterfaces()
.InstancePerLifetimeScope();
My question is, is there any thing similar to this in Default DI container from asp.net 5 where you can register all the services at once ?
My question is, is there any thing similar to this in Default DI container from asp.net 5 where you can register all the services at once ?
Not OOTB, but Kristian Hellang created a cool package named Scrutor that adds assembly scanning capabilities to the built-in container. You can find it on NuGet.org.
services.Scan(scan => scan
.FromAssemblyOf<ITransientService>()
.AddClasses(classes => classes.AssignableTo<ITransientService>())
.AsImplementedInterfaces()
.WithTransientLifetime()
.AddClasses(classes => classes.AssignableTo<IScopedService>())
.As<IScopedService>()
.WithScopedLifetime());
You can still use the Autofac with Asp.net(5/Core) or MVC 6. You need to change the signature of ConfigureServices method to return IServiceProvider and argument as IServiceCollection.
E.g.
public IServiceProvider ConfigureServices(IServiceCollection services)
{
// We add MVC here instead of in ConfigureServices.
services.AddMvc();
// Create the Autofac container builder.
var builder = new ContainerBuilder();
// Add any Autofac modules or registrations.
builder.RegisterModule(new AutofacModule());
// Populate the services.
builder.Populate(services);
// Build the container.
var container = builder.Build();
// Resolve and return the service provider.
return container.Resolve<IServiceProvider>();
}
See more details here about integrating Autofac with Asp.net(5/Core).
It might surprise you that most of what you need can be done in few lines without reliance on third-party packages!.. Once someone else has figured out what those few lines should be 😀
For example, here is a handful of concise DI extensions from Autofac and NetCore.AutoRegisterDi (all methods are annotated with source links). It lets you write registration logic like (example):
var types = typeof(BaseService<>).Assembly
.GetTypes()
.Where(t =>
!t.IsAbstract &&
t.IsClosedTypeOf(typeof(BaseService<>)))
).ToList();
services.RegisterAsImplementedInterfaces(types, ServiceLifetime.Scoped);
This blog post, .NET Core project without Autofac, has more examples and covers limitations if you want take one level further.
Add these lines to .net 6 program.cs file :
var appServices = typeof(Program).Assembly.GetTypes()
.Where(s => s.Name.EndsWith("Service") && s.IsInterface == false).ToList();
foreach (var appService in appServices)
builder.Services.Add(new ServiceDescriptor(appService, appService, ServiceLifetime.Scoped));
I don't write interface for my services (for writing less code).
These codes find all Types with these conditions : be class and not interface and EndsWith : "Service".
Then add all service classes to the .net DI container.
https://github.com/mammadkoma/webapi/blob/master/WebApi/Program.cs
Then inject the service classes everywhere you want, for example in UserController constructor :
https://github.com/mammadkoma/webapi/blob/master/WebApi/Controllers/UserController/UserController.cs

Satellite Assembly, ASP.NET language not changing

I'm converting a project to use a satellite assembly.
I created a new class library (named "Resources"). All of the default resources are at the root level *.resx.
Then there is a folder for each culture.
"en/"
with *.en.resx
en-GB/
with *.en-GB.resx
I changed the .resx files Access Modifier to "Public" and changed these settings.
BuildAction: EmbeddedResource
CopyToOutputDirectory: CopyAlways
I made sure the .resx designers *.Designer.cs use the "Resources" namespace.
I added the Class library to an ASP.NET MVC app and in global.asax.cs I have the culture being set as needed.
System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-GB");
System.Threading.Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB");
I tried adding the al.exe and Resgen.exe commands to the post-build event in the Resources class library.
"C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\Resgen.exe"
en-GB/MyResourceFile.en-GB.resx "C:\Program Files\Microsoft
SDKs\Windows\v7.1\Bin\al.exe" /t:lib
/embed:en-GB/MyResourceFile.en-GB.resources /culture:en-gb
/out:Resources.resources.dll
Looking inside the MVC apps bin/ folder there are folders for each language with 3 files:
MyResources.en-GB.Designer.cs, MyResources.en-GB.resx,
Resources.resources.dll
The root level only has Resources.dll and Resources.resources.dll but the default language works fine. The second *.resources.dll is due to the post-build event.
When I change the CurrentUICulture it doesn't change the UI language. Before moving the resources out of the App_GlobalResources/ to an external assembly it worked fine (Razor example).
#Resources.MyResources.StringIdentifier
Looking at the Resource .dlls using .NET Reflector these are the differences.
Satellite Resources: (Project: Resources)
Resources.en_GB.MyResources.en-GB.resources
App_GlobalResources: (Project MVCApp)
Namespace.MVCApp.App_GlobalResources.MyResources.en-GB.resources
Edit:
After setting the CurrentCulture and UICulture in the global.asax.cs, I tested manually retrieving a ResourceManager which actually worked and retrieved strings in the expected language.
System.Resources.ResourceManager gStrings =
new System.Resources.ResourceManager("Resources.MyResources", System.Reflection.Assembly.Load("Resources")); // Resources=Project Name
string test = gStrings.GetString("TestString"); // WORKS IN GLOBAL.ASAX.CS!!!
I tried the same test in an MVC Controller, but it doesn't work. So the only place the ResourceManager can be retrieved is the Global.asax.cs.
Oddly enough the same test at the UI level in razor would only return the default cultures text! The cultureInfo is correct, but the ResourceManager only returns the default language in the ASP.NET MVC Razor UI. (This is strictly for testing)
#{
System.Globalization.CultureInfo ci = System.Threading.Thread.CurrentThread.CurrentUICulture;
System.Resources.ResourceManager gStrings =
new System.Resources.ResourceManager("Resources.MyResource", System.Reflection.Assembly.Load("Resources"));
string s = gStrings.GetString("Test"); // DOESN'T WORK IN UI
}
TLDR version:
Converting from Resources (.resx) from App_GlobalResources to satellite assemblies.
The issue at hand.
I can manually call a ResourceManager for the satellite assembly, using the CurrentUICulture language in global.asax and it works as expected, however the same test fails from the UI side and fails in a MVC controller.
The default language works fine, so why doesn't the UI switch languages?
If have recently set up a project where the internaltionalization works correctly more or less like you describe.
https://github.com/Jacco/Perpetuality
I switch the language per HTTP request in OnAuthorize in the controller I use as base:
using Perpetuality.Data;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Principal;
using System.Threading;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace Perpetuality.Controllers
{
public partial class BaseController : Controller
{
private CultureInfo GetCultureInfo(AuthorizationContext filterContext)
{
switch ((string)filterContext.RouteData.Values["language"])
{
case "nl":
return CultureInfo.CreateSpecificCulture("nl-NL");
case "en":
return CultureInfo.CreateSpecificCulture("en-US");
case "pt":
return CultureInfo.CreateSpecificCulture("pt-PT");
case "de":
return CultureInfo.CreateSpecificCulture("de-DE");
case "es":
return CultureInfo.CreateSpecificCulture("es-ES");
case "fr":
return CultureInfo.CreateSpecificCulture("fr-FR");
case "it":
return CultureInfo.CreateSpecificCulture("it-IT");
case "jp":
return CultureInfo.CreateSpecificCulture("ja-JP");
default:
return CultureInfo.CreateSpecificCulture("en-US");
}
}
protected override void OnAuthorization(AuthorizationContext filterContext)
{
Thread.CurrentThread.CurrentCulture = GetCultureInfo(filterContext);
Thread.CurrentThread.CurrentUICulture = GetCultureInfo(filterContext);
base.OnAuthorization(filterContext);
}
}
}
In my project the RESX files are specified differently than in yours:
BuildAction: EmbeddedResource
CopyToOutputDirectory: DoNotCopy
In my vies I access the strings like this:
#Resources.Home.Index.FacebookButtonCaption
I did setup nothing post or pre build.
In my project the language is always on the url. That was a user can switch language if no appropriate language was initially chosen.

WCF Client Proxies, Client/Channel Caching in ASP.Net - Code Review

long time ASP.Net interface developer being asked to learn WCF, looking for some education on more architecture related fronts - as its not my strong suit but I'm having to deal.
In our current ASMX world we adopted a model of creating ServiceManager static classes for our interaction with web services. We're starting to migrate to WCF, attempting to follow the same model. At first I was dealing with performance problems, but I've tweaked a bit and we're running smoothly now, but I'm questioning my tactics. Here's a simplified version (removed error handling, caching, object manipulation, etc.) of what we're doing:
public static class ContentManager
{
private static StoryManagerClient _clientProxy = null;
const string _contentServiceResourceCode = "StorySvc";
// FOR CACHING
const int _getStoriesTTL = 300;
private static Dictionary<string, GetStoriesCacheItem> _getStoriesCache = new Dictionary<string, GetStoriesCacheItem>();
private static ReaderWriterLockSlim _cacheLockStories = new ReaderWriterLockSlim();
public static Story[] GetStories(string categoryGuid)
{
// OMITTED - if category is cached and not expired, return from cache
// get endpoint address from FinderClient (ResourceManagement SVC)
UrlResource ur = FinderClient.GetUrlResource(_contentServiceResourceCode);
// Get proxy
StoryManagerClient svc = GetStoryServiceClient(ur.Url);
// create request params
GetStoriesRequest request = new GetStoriesRequest{}; // SIMPLIFIED
Manifest manifest = new Manifest{}; // SIMPLIFIED
// execute GetStories at WCF service
try
{
GetStoriesResponse response = svc.GetStories(manifest, request);
}
catch (Exception)
{
if (svc.State == CommunicationState.Faulted)
{
svc.Abort();
}
throw;
}
// OMITTED - do stuff with response, cache if needed
// return....
}
internal static StoryManagerClient GetStoryServiceClient(string endpointAddress)
{
if (_clientProxy == null)
_clientProxy = new StoryManagerClient(GetServiceBinding(_contentServiceResourceCode), new EndpointAddress(endpointAddress));
return _clientProxy;
}
public static Binding GetServiceBinding(string bindingSettingName)
{
// uses Finder service to load a binding object - our alternative to definition in web.config
}
public static void PreloadContentServiceClient()
{
// get finder location
UrlResource ur = FinderClient.GetUrlResource(_contentServiceResourceCode);
// preload proxy
GetStoryServiceClient(ur.Url);
}
}
We're running smoothly now with round-trip calls completing in the 100ms range. Creating the PreloadContentServiceClient() method and adding to our global.asax got that "first call" performance down to that same level. And you might want to know we're using the DataContractSerializer, and the "Add Service Reference" method.
I've done a lot of reading on static classes, singletons, shared data contract assemblies, how to use the ChannelFactory pattern and a whole bunch of other things that I could do to our usage model...admittedly, some of its gone over my head. And, like I said, we seem to be running smoothly. I know I'm not seeing the big picture, though. Can someone tell me what I've ended up here with regards to channel pooling, proxy failures, etc. and why I should head down the ChannelFactory path? My gut says to just do it, but my head can't comprehend why...
Thanks!
ChannelFactory is typically used when you aren't using Add Service Reference - you have the contract via a shared assembly not generated via a WSDL. Add Service Reference uses ClientBase which is essentially creating the WCF channel for you behind the scenes.
When you are dealing with REST-ful services, WebChannelFactory provides a service-client like interface based off the shared assembly contract. You can't use Add Service Reference if your service only supports a REST-ful endpoint binding.
The only difference to you is preference - do you need full access the channel for custom behaviors, bindings, etc. or does Add Service Reference + SOAP supply you with enough of an interface for your needs.

Resources