Issue recomposing on an ImportMany with MEF - c#

I've got a property defined on a class that has the importManyAttribute defined for it, the declaration is as follows:
public const string FontStyleProvidersPropertyName = "FontStyleProviders";
[ImportMany(typeof(IFontStyleProvider), RequiredCreationPolicy = CreationPolicy.Shared, AllowRecomposition=true)]
public List<IFontStyleProvider> FontStyleProviders { get; set; }
on first run I build my composition container as follows
private CompositionContainer BuildCompositionContainer()
{
//build our composable parts catalog
Assembly executingAssembly = Assembly.GetExecutingAssembly();
CompositionContainer applicationContainer;
string localPath = Path.GetDirectoryName(executingAssembly.Location);
try
{
aggregateCatalog = new AggregateCatalog();
aggregateCatalog.Catalogs.Add(new AssemblyCatalog(executingAssembly));
if (!Directory.Exists(Path.Combine(localPath, ApplicationExtensionsPath)))
{
Directory.CreateDirectory(Path.Combine(localPath, ApplicationExtensionsPath));
}
exportsCatalog = new DirectoryCatalog(Path.Combine(localPath, ApplicationExtensionsPath));
aggregateCatalog.Catalogs.Add(new DirectoryCatalog(Path.Combine(localPath, ApplicationExtensionsPath)));
//create a composition container
return applicationContainer = new CompositionContainer(aggregateCatalog);
}
catch (Exception e)
{
Debug.Fail("Catalog Construction Failed", e.StackTrace);
throw;
}
}
At this point, everything works as expected, but I can't seem to trigger re-composition on "this" class instance. I have an import method as follows:
private void Import()
{
exportsCatalog.Refresh();
CompositionBatch batch = new CompositionBatch();
batch.AddPart(this);
applicationContainer.Compose(batch);
var copy = PropertyChanged;
if (copy != null)
{
copy(this, new PropertyChangedEventArgs(FontStyleProvidersPropertyName));
copy(this, new PropertyChangedEventArgs(MessageContainerViewModelsPropertyName));
}
}
It finds new types in the ApplicationExtensionPath folder used by the exportsCatalog just fine, but it never actually re-builds FontStyleProviders (or MessageContainerViewModels)
I've been through the documents a few times and I can't seem to figure out why.

The problem is you aren't actually adding the catalog which you call Refresh() on to the AggregateCatalog. Change this:
exportsCatalog = new DirectoryCatalog(Path.Combine(localPath, ApplicationExtensionsPath));
aggregateCatalog.Catalogs.Add(new DirectoryCatalog(Path.Combine(localPath, ApplicationExtensionsPath)));
To this:
exportsCatalog = new DirectoryCatalog(Path.Combine(localPath, ApplicationExtensionsPath));
aggregateCatalog.Catalogs.Add(exportsCatalog);
Also, once your class has been composed once you shouldn't compose it again. Just calling exportsCatalog.Refresh() will be enough to cause recomposition.

Related

Load a XAML file in a ResourceDictionary from a ClassLibrary

I'm trying to make a simple plugin system in which a plugins are dynamically loaded from .dll files on application's startup and show up in the UI.
This Answer seems to be exactly what I'm looking for. It uses MEF to load the plugins. I tried to create a simple project and follow the instructions. My solution has the following structure:
MeftTest (contains main MefTest.exe, references only MefTest.SDK and not plugins)
MefTest.SDK (contains the IPlugin.cs and IPluginViewModel.cs and the Engine.cs which loads the plugins from the application's directory)
MefTest.Plugin1 (contains first plugin, references MefTest.SDK)
MefTest.Plugin2 (contains second plugin, references MefTest.SDK)
MefTest.SDK -> IPlugin.cs
public interface IPlugin
{
IPluginViewModel ViewModel { get; }
ResourceDictionary View { get; }
string Title { get; }
}
MefTest.SDK -> Engine.cs
public class Engine
{
[ImportMany]
private IEnumerable<IPlugin> plugins { get; set; }
public async Task<ObservableCollection<IPlugin>> GetPlugins()
{
try
{
var folder = AppDomain.CurrentDomain.BaseDirectory;
var catalog = new AggregateCatalog();
//catalog.Catalogs.Add(new AssemblyCatalog(Assembly.GetExecutingAssembly()));
catalog.Catalogs.Add(new DirectoryCatalog(folder));
var container = new CompositionContainer(catalog);
container.ComposeParts(this);
var result = new ObservableCollection<IPlugin>();
foreach (var p in plugins)
{
result.Add(p);
}
return result;
}
catch (Exception ex)
{
//I get the exception here...
var t = ex;
throw;
}
}
}
MefTest.Plugin1 -> Plugin1.cs
[Export(typeof(IPlugin))]
public class Plugin1 : IPlugin
{
private MainViewModel viewModel { get; set; }
public IPluginViewModel ViewModel
{
get { return viewModel; }
}
public ResourceDictionary View
{
get { return viewDictionary; }
}
private ResourceDictionary viewDictionary = new ResourceDictionary();
public string Title
{
get { return "Plugin 1"; }
}
[ImportingConstructor]
public Plugin1()
{
//I get the error here. tried both of these, none of them work
viewDictionary.Source =
// new Uri("pack://application:,,,/MefTest.Plugin1;component/views/main.xaml", UriKind.Absolute);
new Uri("/MefTest.Plugin1;component/views/main.xaml",
UriKind.Relative);
}
public override string ToString()
{
return Title;
}
}
however, I get the error Could not load file or assembly 'MefTest.Plugin1.dll, Culture=neutral' or one of its dependencies. The system cannot find the file specified.
The Main.xaml file is in MefTest.Plugin1\Views\Main.xaml folder. Output type of the project is ClassLibrary and Build Action of the xaml file is Page.
PS: I tried to reference the plugin directly and add it without the MEF (Plugins.Add(new Plugin3.Plugin3());) and it still threw the same exception. So I don't think the problem is with the MEF part of the solution.
How can I fix this? Also, is there a better option to this approach?
i do it this way in an xbap application...to retreive distant xaml. Try to do the same with your local resourtce
private ResourceDictionary LoadDictionary(string source)
{
Stream streamInfo = null;
ResourceDictionary dictionary = null;
try
{
streamInfo = DistantManager.Instance.GetResource(source);
if (streamInfo != null)
{
Uri baseUri = DistantManager.Instance.GetUri(source);
dictionary = XamlReader.Load(streamInfo) as ResourceDictionary;
dictionary.Source = baseUri;
}
}
catch (Exception e)
{
BusinessLogger.Manage(e);
return null;
}
return dictionary;
}
something like
Uri baseUri = new Uri(Mysource);
dictionary = XamlReader.Load(XXXX) as ResourceDictionary;
dictionary.Source = baseUri;
but on the other hand I do not understand why you want a ResourceDictionary as your plugin view...? just create the plugin user control ??

Creating an AppDomain and calling a method from an assembly in a subfolder

I have an example application that has a number of endpoints (c# classes) which use an interface which defines some methods. These endpoints are in their own class libraries.
In an assembly called "EndPoints"
namespace EndPoints
{
public interface IEndPoint
{
void Initialize(XmlDocument message);
bool Validate();
void Execute();
}
}
In an assembly called "EndPoints.EndPoint1"
namespace EndPoints
{
public class EndPoint1 : IEndPoint
{
private XmlDocument _message;
public void Initialize(XmlDocument message)
{
_message = message;
Console.WriteLine("Initialize EndPoint1");
}
public bool Validate()
{
Console.WriteLine("Validate EndPoint1");
return true;
}
public void Execute()
{
Console.WriteLine("Execute EndPoint1");
}
}
}
The application will "choose" an endpoint to use and then find the appropriate class, create an instance of it and then call the methods in turn.
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
// Generate "random" endpoint name
string endPointName = GetEndPointName();
// Get the class name from the namespaced class
string className = GetClassName(endPointName);
// Dummy xmldocument that used to pass into the end point
XmlDocument dummyXmlDocument = new XmlDocument();
// Load appropriate endpoint assembly because the application has no reference to it so the assembly would not have been loaded yet
LoadEndPointAssembly(endPointName);
// search currently loaded assemblies for that class
var classTypes = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(s => s.GetTypes())
.Where(p => p.FullName == endPointName)
.ToList();
// cycle through any found types (should be 1 only)
for (int i = 0; i < classTypes.Count; i++)
{
var classType = classTypes[i];
IEndPoint classInstance = Activator.CreateInstance(classType) as IEndPoint;
classInstance.Initialize(dummyXmlDocument);
if (classInstance.Validate())
{
classInstance.Execute();
}
}
}
private static void LoadEndPointAssembly(string endPointName)
{
using (StreamReader reader = new StreamReader(endPointName + ".dll", System.Text.Encoding.GetEncoding(1252), false))
{
byte[] b = new byte[reader.BaseStream.Length];
reader.BaseStream.Read(b, 0, System.Convert.ToInt32(reader.BaseStream.Length));
reader.Close();
AppDomain.CurrentDomain.Load(b);
}
}
private static string GetEndPointName()
{
// Code to create "random" endpoint class name
Random rand = new Random();
int randomEndPoint = rand.Next(1, 4);
return string.Format("EndPoints.EndPoint{0}", randomEndPoint);
}
private static string GetClassName(string namespacedClassName)
{
string className = null;
string[] components = namespacedClassName.Split('.');
if (components.Length > 0)
{
className = components[components.Length - 1];
}
return className;
}
}
}
I want to change the application to achieve the following;
- each endpoint assembly (and any config files and/or other assemblies that it uses) are contained in a subfolder below the application folder. the subfolder name would be the name of the endpoint class e.g. "EndPoint1"
- each endpoint runs in its own appdomain.
However, so far I've been unable to achieve this. I keep getting an exception stating its failed to load the appropriate assembly even though when I create the appdomain I specify the subfolder to be used by setting the ApplicationBase and PrivateBinPath properties of the AppDomainSetup; e.g.
AppDomain appDomain = null;
AppDomain root = AppDomain.CurrentDomain;
AppDomainSetup setup = new AppDomainSetup();
setup.ApplicationBase = root.SetupInformation.ApplicationBase + className + #"\";
setup.PrivateBinPath = root.SetupInformation.ApplicationBase + className + #"\";
appDomain = AppDomain.CreateDomain(className, null, setup);
I've then been trying to use the Load method on the newly created appDomain to load the assembly. That's when I get the error.
Please does anyone have any thoughts about how I can load the appropriate assembly and call the methods defined in the interface? Many thanks.
I would do it in the following way. Firstly you need a class derived from MarshalByRef. It will be responsible for loading EndPoints and executing them in separate application domains. Here, I assume that it is defined in ConsoleApplication1 but it can be moved somewhere else:
public class EndPointLoader : MarshalByRefObject
{
public void Load(string path, string endPointName)
{
Assembly.LoadFrom(path);
var classTypes = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(s => s.GetTypes())
.Where(p => p.FullName == endPointName)
.ToList();
for (int i = 0; i < classTypes.Count; i++)
{
....
}
}
}
Here is a code that uses this class. You can put in in your LoadEndPointAssembly method.
var appDomain = AppDomain.CreateDomain(endPointName);
var loader = (EndPointLoader)appDomain.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().FullName, typeof(EndPointLoader).FullName);
loader.Load(assemblyPath, endPointName);

Plugin Application and WPF issues?

I am working on a project where allowing 3rd-party plugins is required. I have worked with plugins before and I never had a problem.
I'm sure my problem is because WPF doesn't like me using Assembly.LoadFile(file) & Activator.CreateInstance(t)!
The error I encounter is:
The component 'Servus.Forms.MainWindow' does not have a resource identified by the URI '/Servus;component/forms/mainwindow.xaml'.
which shows in my MainForm constructor at:
InitializeComponent();
If I load the plugins after loading the MainForm it loads without issues, however when opening any other forms(there are many in my application) I experience the same issue as about but with the relevant error for that particular form.
I have also tried to load the plugins in there own AppDomain like this:
PluginDomain temp = new PluginDomain();
PluginBase tempPlug = temp.GetPlugin(file);
With the following classes:
public class PluginDomain
{
public AppDomain CurrentDomain { get; set; }
public ServusAssemblyLoader CurrentAssemblyLoader { get; set; }
private readonly Random _rand = new Random();
public PluginDomain()
{
}
public PluginBase GetPlugin(string assemblyName)
{
try
{
string appBase = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
var ads = new AppDomainSetup { ApplicationBase = appBase, PrivateBinPath = appBase, ShadowCopyFiles = "true" };
CurrentDomain = AppDomain.CreateDomain("ServusDomain_Plugin_" + _rand.Next(0, 100000), null, ads);
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
CurrentAssemblyLoader = (ServusAssemblyLoader)
CurrentDomain.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().FullName, typeof(ServusAssemblyLoader).FullName);
AppDomain.CurrentDomain.AssemblyResolve -= CurrentDomain_AssemblyResolve;
return CurrentAssemblyLoader.Load(assemblyName);
}
catch (Exception e)
{
CConsole.WriteLine("Error: " + e.Message);
}
finally
{
CurrentAssemblyLoader = null;
AppDomain.Unload(CurrentDomain);
}
return null;
}
static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
string[] parts = args.Name.Split(',');
string file = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + "\\" + parts[0].Trim() + ".dll";
return Assembly.LoadFrom(file);
}
}
public class ServusAssemblyLoader : MarshalByRefObject, IAssemblyLoader
{
public PluginBase Load(string file)
{
Assembly asm = Assembly.LoadFrom(file);
foreach (Type t in asm.GetTypes())
{
if (t.IsSubclassOf(typeof(PluginBase)))
{
return (PluginBase)Activator.CreateInstance(t);
}
}
return null;
}
}
public interface IAssemblyLoader
{
PluginBase Load(string file);
}
This returns an TransparentProxy object like this:
{System.Runtime.Remoting.Proxies.__TransparentProxy}
However I am unsure how to use this as I was expecting it to return a PluginBase Object.
I have read that many people have also have this issue, they have answers that say to use a new AppDomain, but as you can see this doesn't help me right now.
I hope I have provided you enough information, can anyone help?
It turns out I had a few things wrong in my PluginDomain Class.
Fix #1:
Replace:
return (PluginBase)Activator.CreateInstance(t);
With:
(PluginBase)asm.CreateInstance(t.ToString());
Fix #2:
Remove:
AppDomain.Unload(CurrentDomain);
Fix #3: (Purely for debugging)
Replace:
return CurrentAssemblyLoader.Load(assemblyName);
With:
PluginBase obj = CurrentAssemblyLoader.Load(assemblyName);
return obj;
EDIT:
It should be noted that the new AppDomain wont be able to access objects in the old one; so my problem is only half fixed.

Unity IOC and Factory pattern, and repository pattern

I am trying to use the Unity Block of the enterprise library in a program i am writing.
But i think i am using dependency injection wrong. I was wondering if some one could point me in the right direction.
static void Main(string[] args)
{
using (IUnityContainer container = new UnityContainer())
{
InitialiseContainer(container);
DataCopierFactory dcFactory = new DataCopierFactory();
ERunOptions dataCopierType = ExtractParams(args);
IDataCopier dataCopier = dcFactory.CreateDataCopier((int)dataCopierType, container);
dataCopier.DetectChanges();
dataCopier.ParseData();
dataCopier.CopyData();
}
}
}
//use the ioc container to register the EF context type to the repository interfaces..
private static void InitialiseContainer(IUnityContainer container)
{
//add Extensions:
container.AddNewExtension<Interception>();
//Licence Schedule
container.RegisterType<IEFContext, LTE_DownFromWeb_EFContext>("DataCopier.ScheduleDataCopier.Source");
container.RegisterType<IEFContext, LTE_Licensing_EFContext>("DataCopier.ScheduleDataCopier.Destination");
container.RegisterType<IRepositorySession>("Schedule_Source",new InjectionConstructor(container.Resolve<IEFContext>("DataCopier.ScheduleDataCopier.Source")));
container.RegisterType<IRepositorySession>("Schedule_Destination",new InjectionConstructor(container.Resolve<IEFContext>("DataCopier.ScheduleDataCopier.Destination")));
}
So basically the DataCopier Factory creates an instance of a DataCopier like so:
DataCopierFactory:
//return a data copier that will transfer data from any DB to any other DB
public IDataCopier CreateDataCopier(int i, IUnityContainer container)
{
switch(i)
{
case 1:
return new ScheduleDataCopier(container);
default:
throw new InvalidOperationException("Parameter " + i + " does not exist");
}
}
a data copier looks like this:
class ScheduleDataCopier : IDataCopier
{
private List<Site> _sites;
private List<SitesAndApparatuses> _scheduleList;
private IUnityContainer _container;
public ScheduleDataCopier(IUnityContainer container)
{
_container = container;
_scheduleList = new List<SitesAndApparatuses>();
}
//check if new sites registration has arrived in tblSites on down from web db.
public bool DetectChanges()
{
using (var db = _container.Resolve<IRepositorySession>("Schedule_Source"))
{
SiteAudit lastSite = new SitesAuditRepository().GetLatest();
var sitesRepo = new SitesRepository();
var sites = sitesRepo.Where(x => x.SID > lastSite.SALatestSID);
if (sites.Count() < 1)
{
return false;
}
_sites = sites.ToList();
db.Dispose();
}
return true;
}
//parse the data into a list of object SitesAndApparatuses
public bool ParseData()
{
try
{
foreach (Site s in _sites)
{
var schedule = (SitesAndApparatuses)XmlObjectBuilder.Deserialize(typeof(SitesAndApparatuses), s.XMLFile);
schedule.acCode = s.Registration.RAcCode;
_scheduleList.Add(schedule);
}
}
catch (Exception ex)
{
throw new NotImplementedException("HANDLE THIS SHIT!", ex);
}
return true;
}
public bool CopyData()
{
try
{
using (var db = _container.Resolve<IRepositorySession>("Schedule_Destination"))
{
var licensingScheduleRepo = new LicensingScheduleRepository();
//some logic
db.Commit();
}
}
catch (Exception ex)
{
}
return true;
}
}
Second question, i resolve my unit of work object called RepositorySession in the Datacopier classes using the unity container i passed... is this the wrong approach and why, im struggling to find any info on it online?
This is probably too much code for someone to read.. but im hoping for an answer!
Thanks in advance
Neil
I'd do something like:
container.RegisterType<IEFContext, LTE_DownFromWeb_EFContext>("Source");
container.RegisterType<IEFContext, LTE_Licensing_EFContext>("Destination");
container.RegisterType<IRepositorySession>("Source",new InjectionConstructor(new ResolvedParameter<IEFContext>("Source"));
container.RegisterType<IRepositorySession>("Destination",new InjectionConstructor(new ResolvedParameter<IEFContext>("Destination")));
container.RegisterType<IDataCopier,ScheduleDataCopier>("0",new InjectionConstructor(new[] {new ResolvedParameter<IRepositorySession("Source"),new ResolvedParameter<IRepositorySesison>("Destination")}));
//Now resolve
ERunOptions dataCopierType = ExtractParams(args);
IDataCopier dataCopier = container.Resolve<IDataCopier(dataCopierType.ToString());
dataCopier.DetectChanges();
dataCopier.ParseData();
dataCopier.CopyData();
DataCopier Class
class ScheduleDataCopier : IDataCopier
{
private List<Site> _sites;
private List<SitesAndApparatuses> _scheduleList;
private IRepositorySession _source;
private (IRepositorySession _destination;
public ScheduleDataCopier(IRepositorySession source, (IRepositorySession destination)
{
_source=source;
_destination=destination;
_scheduleList = new List<SitesAndApparatuses>();
}
//check if new sites registration has arrived in tblSites on down from web db.
public bool DetectChanges()
{
SiteAudit lastSite = new SitesAuditRepository().GetLatest();
var sitesRepo = new SitesRepository();
var sites = sitesRepo.Where(x => x.SID > lastSite.SALatestSID);
if (sites.Count() < 1)
{
return false;
}
_sites = sites.ToList();
_source.DoSomething();
_source.CommitAndReleaseResources();//clean up but leave object reusable
return true;
}
//parse the data into a list of object SitesAndApparatuses
public bool ParseData()
{
try
{
foreach (Site s in _sites)
{
var schedule = (SitesAndApparatuses)XmlObjectBuilder.Deserialize(typeof(SitesAndApparatuses), s.XMLFile);
schedule.acCode = s.Registration.RAcCode;
_scheduleList.Add(schedule);
}
}
catch (Exception ex)
{
throw new NotImplementedException("HANDLE THIS SHIT!", ex);
}
return true;
}
public bool CopyData()
{
try
{
var licensingScheduleRepo = new LicensingScheduleRepository();
//some logic
_desitnation.Commit();
}
catch (Exception ex)
{
//handle exception
}
return true;
}
}
The two main differences between what you're doing and the above is that I'm using Injection Parameters (the ResolvedParameter class) to dynamically resolve instances of objects when they're needed.
This allows me to get Unity to do my entire DI process for me, including resolve my DataCopier. If I add another Datacopier I'd just need to add the new DataCopier type to Unity with a name that matches the appropriate ERunOptions type and I'd be able to resolve the new DataCopier with no change to my code:
container.RegisterType<IDataCopier,RandomDataCopier>("0",new InjectionConstructor(new[] {new ResolvedParameter<IRepositorySession("RandomSource"),new ResolvedParameter<IRepositorySesison>("RandomDestination")}));
and:
ERunOptions dataCopierType = ExtractParams(args);
IDataCopier dataCopier = container.Resolve<IDataCopier(dataCopierType.ToString());
dataCopier.DetectChanges();
dataCopier.ParseData();
dataCopier.CopyData();
Stays the same but can process a ScheduledDataCopier or a RandomDataCopier

load Prism modules from dll files (at startup)

Atm in my application I do like this:
class Bootstrapper : UnityBootstrapper
{
protected override DependencyObject CreateShell()
{
return Container.Resolve<Shell>();
}
protected override void InitializeShell()
{
base.InitializeShell();
App.Current.MainWindow = (Window)Shell;
App.Current.MainWindow.Show();
}
protected override void ConfigureModuleCatalog()
{
base.ConfigureModuleCatalog();
var moduleCatalog = (ModuleCatalog)ModuleCatalog;
moduleCatalog.AddModule(typeof(FooModule));
moduleCatalog.AddModule(typeof(BarModule));
}
}
I would like to load FooModule and BarModule by indicating the path to the dll file, something like this:
protected override void ConfigureModuleCatalog()
{
...
var assembly = Assembly.LoadFrom(#"libs\FooLib.dll");
var type = assembly.GetType("FooLib.FooModule");
moduleCatalog.AddModule(type);
...
}
but it doesn't work, I get this error message on Bootstrapper.Run() :
There is currently no moduleTypeLoader in the ModuleManager that can retrieve the specified module.
EDIT:
this is my FooModule:
public class FooModule : IModule
{
private readonly IRegionViewRegistry _regionViewRegistry;
public FooModule(IRegionViewRegistry registry)
{
_regionViewRegistry = registry;
}
public void Initialize()
{
_regionViewRegistry.RegisterViewWithRegion("MainRegion", typeof(Main));
}
}
Ok, try to make your ConfigureModuleCatalog looking like this:
protected override void ConfigureModuleCatalog()
{
string path = #"libs\FooLib.dll";
var assembly = Assembly.LoadFrom(path);
var type = assembly.GetType("FooLib.FooModule");
ModuleCatalog.AddModule(new ModuleInfo
{
ModuleName = type.Name,
ModuleType = type.AssemblyQualifiedName,
Ref = new Uri(path, UriKind.RelativeOrAbsolute).AbsoluteUri
});
}
The key thing is:
Ref = new Uri(path, UriKind.RelativeOrAbsolute).AbsoluteUri
prism checks whether Ref property refers to physical file(file://) and loads assembly from this file.
I think Prism v4 Loading modules on demand with DirectoryModuleCatalog could help.
UPDATE:
Sorry, just realized that reference mentioned above won't help.
Try this one from msdn - "Loading Modules on Demand" section, I think that's what you need.
One easier method not to enter path manually is, obtain it from type->assembly->location
Type module1Type = typeof(Module1.Module1);
string path = module1Type.Assembly.Location;
moduleCatalog.AddModule(
new ModuleInfo()
{
ModuleName = module1Type.Name,
ModuleType = module1Type.AssemblyQualifiedName,
Ref = new Uri(path, UriKind.RelativeOrAbsolute).AbsoluteUri
});
return moduleCatalog;

Categories