I try to follow this answer Is there a way to scaffold mysql json into custom type? to make custom json type convert, and it works perfect!
The only thing what bother me is that I should modify Context code manual, to insert builder => builder.UseNewtonsoftJson().
I am wonderring if it could be in the generation process, it would be a life saver.
I am inspired by the answer which metioned above, and try to make it work.
What I want is
public partial class spckContext : DbContext
{
...
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see http://go.microsoft.com/fwlink/?LinkId=723263.
optionsBuilder
.UseMySql("server=localhost;port=3306;database=spck;user=root;password=;treattinyasboolean=true", Microsoft.EntityFrameworkCore.ServerVersion.Parse("8.0.29-mysql"), builder => builder .UseNewtonsoftJson())
.EnableSensitiveDataLogging()
.LogTo(Log, LogFilter, DbContextLoggerOptions.DefaultWithLocalTime); // <= stucked here, how to pass method as parameter?
}
}
...
}
I add these to my project:
using System.Drawing;
using Microsoft.Extensions.Logging;
using Console = Colorful.Console;
public partial class spckContext
{
public static void Log(string content)
{
Console.WriteLineFormatted(content, Color.Aqua);
}
public static bool LogFilter(Microsoft.Extensions.Logging.EventId id, LogLevel level)
{
switch (level)
{
case LogLevel.Trace:
case LogLevel.Debug:
case LogLevel.Warning:
case LogLevel.None:
return false;
case LogLevel.Error:
case LogLevel.Critical:
case LogLevel.Information:
return true;
default:
return false;
}
}
}
public class MyDesignTimeServices : IDesignTimeServices
{
public void ConfigureDesignTimeServices(IServiceCollection services)
{
...
//Type Mapping
services.AddSingleton<IRelationalTypeMappingSource, CustomTypeMappingSource>(); // <= add this line
//Option Generator
services.AddSingleton<IProviderConfigurationCodeGenerator, ProviderConfigurationCodeGenerator>(); // <= and this line
...
}
}
using System.Reflection;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.EntityFrameworkCore.Design.Internal;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Scaffolding;
using Microsoft.Extensions.Logging;
using Pomelo.EntityFrameworkCore.MySql.Infrastructure.Internal;
using Pomelo.EntityFrameworkCore.MySql.Scaffolding.Internal;
using Pomelo.EntityFrameworkCore.MySql.Storage.Internal;
public class ProviderConfigurationCodeGenerator : MySqlCodeGenerator
{
private static readonly MethodInfo _enableSensitiveDataLoggingMethodInfo = typeof(DbContextOptionsBuilder).GetRequiredRuntimeMethod(
nameof(DbContextOptionsBuilder.EnableSensitiveDataLogging),
typeof(bool));
private static readonly MethodInfo _useNewtonJsonMethodInfo = typeof(MySqlJsonNewtonsoftDbContextOptionsBuilderExtensions).GetRequiredRuntimeMethod(
nameof(MySqlJsonNewtonsoftDbContextOptionsBuilderExtensions.UseNewtonsoftJson),
typeof(MySqlDbContextOptionsBuilder),
typeof(MySqlCommonJsonChangeTrackingOptions));
private static readonly MethodInfo _logToMethodInfo = typeof(DbContextOptionsBuilder).GetRequiredRuntimeMethod(
nameof(DbContextOptionsBuilder.LogTo),
typeof(Action<string>),
typeof(Func<EventId, LogLevel, bool>),
typeof(DbContextLoggerOptions?));
private static readonly MethodInfo _logMethodInfo = typeof(spckContext).GetRequiredRuntimeMethod(
nameof(spckContext.Log),
typeof(string));
private static readonly MethodInfo _logFilterMethodInfo = typeof(spckContext).GetRequiredRuntimeMethod(
nameof(spckContext.LogFilter),
typeof(EventId),
typeof(LogLevel));
private readonly ProviderCodeGeneratorDependencies _dependencies;
private readonly IMySqlOptions _options;
public ProviderConfigurationCodeGenerator(ProviderCodeGeneratorDependencies dependencies, IMySqlOptions options) : base(dependencies, options)
{
_dependencies = dependencies;
_options = options;
}
public override MethodCallCodeFragment GenerateUseProvider(string connectionString, MethodCallCodeFragment? providerOptions)
{
if (providerOptions == null)
{
providerOptions = new MethodCallCodeFragment(_useNewtonJsonMethodInfo);
}
else
{
providerOptions = providerOptions.Chain(new MethodCallCodeFragment(_useNewtonJsonMethodInfo));
}
var fragment = base.GenerateUseProvider(connectionString, providerOptions); //works
fragment = fragment.Chain(_enableSensitiveDataLoggingMethodInfo); //works
fragment = fragment.Chain(_logToMethodInfo,
new NestedClosureCodeFragment("str", new MethodCallCodeFragment(_logMethodInfo)), // <= try and failed! it convert into `str => str.Log()`
new MethodCall(_logFilterMethodInfo), // <= try and failed! error reported
DbContextLoggerOptions.DefaultWithLocalTime);
return fragment;
}
}
public static class TypeExtensions
{
public static MethodInfo GetRequiredRuntimeMethod(this Type type, string name, params Type[] parameters)
=> type.GetTypeInfo().GetRuntimeMethod(name, parameters)
?? throw new InvalidOperationException($"Could not find method '{name}' on type '{type}'");
}
using Microsoft.EntityFrameworkCore.Storage;
using Pomelo.EntityFrameworkCore.MySql.Infrastructure.Internal;
using Pomelo.EntityFrameworkCore.MySql.Storage.Internal;
public class CustomTypeMappingSource : MySqlTypeMappingSource
{
public CustomTypeMappingSource(TypeMappingSourceDependencies dependencies, RelationalTypeMappingSourceDependencies relationalDependencies, IMySqlOptions options) : base(dependencies, relationalDependencies, options)
{
}
protected override RelationalTypeMapping FindMapping(in RelationalTypeMappingInfo mappingInfo)
{
if (mappingInfo.ClrType == typeof(MethodCall))
{
return new MethodCallTypeMapping();
}
return base.FindMapping(mappingInfo);
}
}
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.EntityFrameworkCore.Storage;
public class MethodCall
{
public MethodInfo Method;
public MethodCall(MethodInfo info)
{
Method = info;
}
}
public class MethodCallTypeMapping : RelationalTypeMapping
{
private const string DummyStoreType = "clrOnly";
public MethodCallTypeMapping()
: base(new RelationalTypeMappingParameters(new CoreTypeMappingParameters(typeof(MethodCall)), DummyStoreType))
{
}
protected MethodCallTypeMapping(RelationalTypeMappingParameters parameters)
: base(parameters)
{
}
protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
=> new MethodCallTypeMapping(parameters);
public override string GenerateSqlLiteral(object value)
=> throw new InvalidOperationException("This type mapping exists for code generation only.");
public override Expression GenerateCodeLiteral(object value)
{
return value is MethodCall methodCall
? Expression.Call(methodCall.Method) // <= not working, how to fix this?
: null;
}
}
So my question is how to make a MethodCallCodeFragment with method parameter? I tried google, but can't find anything valuable. And MSDN has no sample code for this feature.
Injecting the .UseNewtonsoftJson() and .EnableSensitiveDataLogging() calls can simply be done by providing the design time services with your own IProviderCodeGeneratorPlugin implementation:
public class MyDesignTimeServices : IDesignTimeServices
{
public void ConfigureDesignTimeServices(IServiceCollection services)
{
services.AddSingleton<IProviderCodeGeneratorPlugin, CustomProviderCodeGeneratorPlugin>();
services.AddEntityFrameworkMySqlJsonNewtonsoft();
}
}
public class CustomProviderCodeGeneratorPlugin : IProviderCodeGeneratorPlugin
{
private static readonly MethodInfo EnableSensitiveDataLoggingMethodInfo = typeof(DbContextOptionsBuilder).GetRequiredRuntimeMethod(
nameof(DbContextOptionsBuilder.EnableSensitiveDataLogging),
typeof(bool));
private static readonly MethodInfo UseNewtonJsonMethodInfo = typeof(MySqlJsonNewtonsoftDbContextOptionsBuilderExtensions).GetRequiredRuntimeMethod(
nameof(MySqlJsonNewtonsoftDbContextOptionsBuilderExtensions.UseNewtonsoftJson),
typeof(MySqlDbContextOptionsBuilder),
typeof(MySqlCommonJsonChangeTrackingOptions));
public MethodCallCodeFragment GenerateProviderOptions()
=> new MethodCallCodeFragment(UseNewtonJsonMethodInfo);
public MethodCallCodeFragment GenerateContextOptions()
=> new MethodCallCodeFragment(EnableSensitiveDataLoggingMethodInfo);
}
Implementing the complex .LogTo(Log, LogFilter, DbContextLoggerOptions.DefaultWithLocalTime) call is not as straitforward, because the translation logic of EF Core for translating a code generation expression tree to C# code is very basic at best.
Implementing a dummy type mapping to return a complex expression will not work in the end, because EF Core will not be able to translate the lambda expressions of content => LogTo(content) and (id, level) => LogFilter(id, level). You could try to trick it, but the simplest solution is to just circumvent the whole expression translation mechanism.
To output any string as C# code, just override ICSharpHelper.UnknownLiteral(object value) in your own implementation.
Here is a fully working example:
using System;
using System.Diagnostics;
using System.Reflection;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.EntityFrameworkCore.Design.Internal;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Scaffolding;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace IssueConsoleTemplate;
public class MyDesignTimeServices : IDesignTimeServices
{
public void ConfigureDesignTimeServices(IServiceCollection services)
{
services.AddSingleton<IProviderCodeGeneratorPlugin, CustomProviderCodeGeneratorPlugin>();
services.AddSingleton<ICSharpHelper, CustomCSharpHelper>();
services.AddEntityFrameworkMySqlJsonNewtonsoft();
}
}
public static class TypeExtensions
{
public static MethodInfo GetRequiredRuntimeMethod(this Type type, string name, params Type[] parameters)
=> type.GetTypeInfo().GetRuntimeMethod(name, parameters)
?? throw new InvalidOperationException($"Could not find method '{name}' on type '{type}'");
}
public class CustomProviderCodeGeneratorPlugin : IProviderCodeGeneratorPlugin
{
private static readonly MethodInfo EnableSensitiveDataLoggingMethodInfo = typeof(DbContextOptionsBuilder).GetRequiredRuntimeMethod(
nameof(DbContextOptionsBuilder.EnableSensitiveDataLogging),
typeof(bool));
private static readonly MethodInfo UseNewtonJsonMethodInfo = typeof(MySqlJsonNewtonsoftDbContextOptionsBuilderExtensions).GetRequiredRuntimeMethod(
nameof(MySqlJsonNewtonsoftDbContextOptionsBuilderExtensions.UseNewtonsoftJson),
typeof(MySqlDbContextOptionsBuilder),
typeof(MySqlCommonJsonChangeTrackingOptions));
private static readonly MethodInfo LogToMethodInfo = typeof(DbContextOptionsBuilder).GetRequiredRuntimeMethod(
nameof(DbContextOptionsBuilder.LogTo),
typeof(Action<string>),
typeof(Func<EventId, LogLevel, bool>),
typeof(DbContextLoggerOptions?));
public MethodCallCodeFragment GenerateProviderOptions()
=> new MethodCallCodeFragment(UseNewtonJsonMethodInfo);
public MethodCallCodeFragment GenerateContextOptions()
=> new MethodCallCodeFragment(EnableSensitiveDataLoggingMethodInfo)
.Chain(GenerateLogToMethodCallCodeFragment());
private MethodCallCodeFragment GenerateLogToMethodCallCodeFragment()
=> new MethodCallCodeFragment(
LogToMethodInfo,
new CSharpCodeGenerationExpressionString("Log"),
new CSharpCodeGenerationExpressionString("LogFilter"),
new CSharpCodeGenerationExpressionString("Microsoft.EntityFrameworkCore.Diagnostics.DbContextLoggerOptions.DefaultWithLocalTime"));
}
public class CSharpCodeGenerationExpressionString
{
public string ExpressionString { get; }
public CSharpCodeGenerationExpressionString(string expressionString)
=> ExpressionString = expressionString;
}
public class CustomCSharpHelper : CSharpHelper
{
public CustomCSharpHelper(ITypeMappingSource typeMappingSource)
: base(typeMappingSource)
{
}
public override string UnknownLiteral(object value)
=> value is CSharpCodeGenerationExpressionString codeGenerationExpressionString
? codeGenerationExpressionString.ExpressionString
: base.UnknownLiteral(value);
}
public partial class Context
{
public static void Log(string content)
=> Console.Write(content);
public static bool LogFilter(EventId id, LogLevel level)
=> level >= LogLevel.Information;
}
internal static class Program
{
private static void Main()
{
}
}
We basically just create our own type called CSharpCodeGenerationExpressionString to hold the C# code string that we want to output and then tell the CustomCSharpHelper.UnknownLiteral() method to return it as is.
The generated OnConfiguring() method looks like this:
public partial class Context : DbContext
{
// ...
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see http://go.microsoft.com/fwlink/?LinkId=723263.
optionsBuilder
.UseMySql("server=127.0.0.1;port=3306;user=root;database=So73163124_01", Microsoft.EntityFrameworkCore.ServerVersion.Parse("8.0.29-mysql"), x => x.UseNewtonsoftJson())
.EnableSensitiveDataLogging()
.LogTo(Log, LogFilter, Microsoft.EntityFrameworkCore.Diagnostics.DbContextLoggerOptions.DefaultWithLocalTime);
}
}
// ...
}
Related
I try to make a custom class with the scaffold command.
This is my model of custom class Class.hbs
{{> imports}}
{{using-base-class}}
namespace {{namespace}}
{
{{#if comment}}
/// <summary>
{{comment}}
///
</summary>
{{/if}}
{{#each class-annotations}}
{{{class-annotation}}}
{{/each}}
public partial class {{class}} {{base-class}}
{
{{{> constructor}}}
{{{> properties}}}
}
}
My C# code:
public void ConfigureDesignTimeServices(IServiceCollection services)
{
services.AddHandlebarsScaffolding(options =>
{
options.TemplateData = new Dictionary<string, object>
{
{ "using-base-class", "using TEST_NET_7.Interface;" },
{ "base-class", ": IEntityId" }
};
});
}
The command working well with this configuration. It's adding my class to all generated classes, but now I want to ignore some class on scaffold. Like the entity class.
How I can do that ?
This work fine for me.
using HandlebarsDotNet;
using Microsoft.EntityFrameworkCore.Design;
using Newtonsoft.Json;
namespace ScaffoldingSample
{
public class CustomScaffoldingDesignTimeServices : IDesignTimeServices
{
private readonly List<string> IgnoreListClass = new()
{
"AspNetRole",
"AspNetRoleClaim",
"AspNetUser",
"AspNetUserClaim",
"AspNetUserLogin",
"AspNetUserToken"
};
public void ConfigureDesignTimeServices(IServiceCollection services)
{
/// This help me to debug app.
// System.Diagnostics.Debugger.Launch();
services.AddHandlebarsScaffolding(options =>
{
options.TemplateData = new Dictionary<string, object>
{
};
});
Handlebars.RegisterHelper("base-class", WriteBaseClass);
Handlebars.RegisterHelper("using-base-class", WriteUsingBaseClass);
}
void WriteBaseClass(EncodedTextWriter writer, Context context, Arguments parameters)
{
var obj = JsonConvert.DeserializeObject<ObjectContext>(JsonConvert.SerializeObject(context.Value));
/// Filter of class, write or not
if (!IgnoreListClass.Contains(obj?.#class ?? string.Empty))
writer.Write(": IEntityId");
}
void WriteUsingBaseClass(EncodedTextWriter writer, Context context, Arguments parameters)
{
var obj = JsonConvert.DeserializeObject<ObjectContext>(JsonConvert.SerializeObject(context.Value));
/// Filter of class, write or not
if (!IgnoreListClass.Contains(obj?.#class ?? string.Empty))
writer.Write("using TEST_NET_7.Interface;");
}
}
public class ObjectContext
{
public string #class { get; set; } = string.Empty;
}
}
So I have been at it for days, and for the life of me cannot find any documentation that fits my situation exactly here.
I have essentially set up a custom navigation service and would like to call the command from my ViewModel Class directly from my User Control.
I think I'm on the edge of having it here, but my lack of experience with C# is shooting me in the foot.
Here is the section of code from my Login.xaml.cs in question:
private LoginViewModel _loginViewModel;
public Login(LoginViewModel loginViewModel)
{
_loginViewModel = loginViewModel;
}
private void GrantAccess()
{
int userAccess = Int16.Parse(User.Access);
if (userAccess == 1)
{
MessageBox.Show("The bottom man");
}
if (userAccess == 2)
{
MessageBox.Show("The little boss");
}
if (userAccess == 3)
{
MessageBox.Show("The little big boss");
}
if (userAccess == 4)
{
{
_loginViewModel.NavigateMM1Command.Execute(null);
}
}
}
and here is the command I'm trying to reference from the ViewModel:
public class LoginViewModel : BaseViewModel
{
public ICommand NavigateMM1Command { get; }
public LoginViewModel(NavigationStore navigationStore)
{
NavigateMM1Command = new NavigateCommand<MM1ViewModel>(new NavigationService<MM1ViewModel>(navigationStore, () => new MM1ViewModel(navigationStore)));
}
}
Basically I've been going through tutorial after tutorial trying to apply what they teach to what I need and its worked for the most part but now _loginViewModel is throwing a null reference exception and I'm not sure why.
I have tried:
LoginViewModel loginViewModel = new loginViewModel();
but its asking me to pass a navigationStore argument through it and that feels wrong.
Any help here will cure my temporary insanity XD
You're receiving a Null Object Reference because navigationStore is null when LoginViewModel is being constructed.
That is, you have not configured a means to instantiate the type navigationStore when constructing LoginViewModel.
Dependency Injection (DI), or Invocation of Control (IoC) is bit more comprehensive a subject to cover in this answer.
Having said that,
I'll provide code to review here. It represents a means to configure a service provider using a collection of type mappings.
In this complete, ConsoleApp example, we'll explicitly instantiate a ServiceCollection, add Service Types (specifying mapping where application), and Build the ServiceProvider; With that provider, we'll resolve and instantiate Login type using GetService -- instantiating all the types;
The Types are essentially mockups of the types you've specified, but I've modified some aspects (an made up notioned like what your Execute method and usage of NavigationStore was).
DemoNavTypes
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleDemo.NavLoginDemo
{
public interface ICommand
{
void Execute(string? userName);
}
public interface INavigationStore {
public bool this[string index] { get;set; }
}
public interface INavigationService {
void GrantAccessToUser(string userName);
}
public interface INavigationViewModel { }
internal class NavigationStore : INavigationStore
{
private Dictionary<string, bool> userAccessDict;
public NavigationStore() {
userAccessDict = new Dictionary<string, bool>();
}
public bool this[string index] {
get => userAccessDict.TryGetValue(index, out var val) && val;
set => userAccessDict[index] = value;
}
}
internal class NavigationService : INavigationService
{
private readonly INavigationStore _navigationStore;
public NavigationService(INavigationStore navigationStore)
{
_navigationStore = navigationStore;
}
public void GrantAccessToUser(string? userName)
{
if (string.IsNullOrWhiteSpace(userName))
throw new ArgumentException(nameof(userName));
_navigationStore[userName!] = true;
}
}
internal class NavigationCommand : ICommand
{
private readonly INavigationService _navigationService;
public NavigationCommand(INavigationService navigationService)
{
_navigationService = navigationService;
}
public void Execute(string? userName)
{
if (userName != null)
{
_navigationService.GrantAccessToUser(userName);
}
}
}
internal class User
{
public string? Name { get; set; }
public string Access { get; set; } = "1";
}
public abstract class BaseViewModel
{
internal User User { get; set; } = new User();
protected BaseViewModel() { }
}
internal class LoginViewModel : BaseViewModel, INavigationViewModel
{
private readonly ICommand _command;
public LoginViewModel(ICommand command) : base()
{
_command = command;
}
internal ICommand NavigateMM1Command => _command;
}
internal class Login
{
private User User => _loginViewModel.User;
private readonly LoginViewModel _loginViewModel;
public Login(LoginViewModel loginViewModel)
{
_loginViewModel = loginViewModel;
}
internal void SetAccess(int access)
{
SetAccess($"{access}");
}
internal void SetAccess(string access)
{
User.Access = access;
}
internal void SetUserName(string userName) { User.Name = userName; }
internal async Task GrantAccessAsync()
{
await Task.Yield();
int userAccess = Int16.Parse(User.Access);
switch (userAccess)
{
case 1:
Console.WriteLine("The bottom man");
break;
case 2:
Console.WriteLine("The little boss");
break;
case 3:
Console.WriteLine("The little big boss");
break;
case 4:
_loginViewModel.NavigateMM1Command.Execute(User.Name);
break;
default:
throw new NotImplementedException();
}
}
}
}
Program.cs (using Microsoft.Extensions.DependencyInjection)
using Microsoft.Extensions.DependencyInjection;
using System.Collections.Immutable;
using System.ComponentModel.Design;
using System.Linq;
using ConsoleDemo.NavLoginDemo;
internal class Program
{
private static async Task Main(string[] args)
{
var services = new ServiceCollection();
var provder = ConfigureServices(services);
var login = provder.GetService<Login>();
if (login != null)
{
await login.GrantAccessAsync();
login.SetAccess(2);
await login.GrantAccessAsync();
login.SetAccess(3);
await login.GrantAccessAsync();
login.SetUserName("James Bond");
login.SetAccess(4);
await login.GrantAccessAsync();
}
}
private static IServiceProvider ConfigureServices(IServiceCollection services)
{
return services
.AddScoped<INavigationStore, NavigationStore>()
.AddScoped<INavigationService, NavigationService>()
.AddScoped<ICommand, NavigationCommand>()
.AddScoped<LoginViewModel>()
.AddScoped<Login>()
.BuildServiceProvider();
}
}
Note
-- However,
In your application, you'll probably already have a ServiceCollection instance in your Program.cs or Startup.cs file. And the ServiceProvider (or HostProvider) will be managed over by the application; So, you probably won't need to explicitly resolve (or GetService<T>) -- just add the Service Type (mappings) in ServiceCollection. Those parameter types will be instantiated and 'injected' into the constructor of Type that is itself being instantiated.
In this example, I want to patch PatchTarget.QSingleton\<T\>.get_Instance().
How to get it done with Harmony or MonoMod?
Harmony:
"Unhandled exception. System.NotSupportedException: Specified method
is not supported."
MonoMod:
"Unhandled exception. System.ArgumentException: The given generic
instantiation was invalid."
Code snippet: (runnable with dotnetfiddle.net)
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using HarmonyLib;
namespace PatchTarget {
public abstract class QSingleton<T> where T : QSingleton<T>, new() {
protected static T instance = null; protected QSingleton() { }
public static T Instance { get {
if (instance == null) {
instance = new T();
Console.Write($"{typeof(T).Name}.Instance: impl=QSingleton");
}
return instance;
} }
}
}
namespace Patch {
public class TypeHelper<T> where T : PatchTarget.QSingleton<T>, new() {
public static T InstanceHack() {
Console.Write($"{typeof(T).Name}.Instance: impl=InstanceHack");
return null;
}
}
public static class HarmonyPatch {
public static Harmony harmony = new Harmony("Try");
public static void init() {
var miOriginal = AccessTools.Property(typeof(PatchTarget.QSingleton<>), "Instance").GetMethod;
var miHack = AccessTools.Method(typeof(TypeHelper<>), "InstanceHack");
harmony.Patch(miOriginal, prefix: new HarmonyMethod(miHack));
}
}
public static class MonoModPatch {
public static MonoMod.RuntimeDetour.Detour sHook;
public static void init() {
var miOriginal = AccessTools.Property(typeof(PatchTarget.QSingleton<>), "Instance").GetMethod;
var miHack = AccessTools.Method(typeof(TypeHelper<>), "InstanceHack");
sHook = new MonoMod.RuntimeDetour.Detour(miOriginal, miHack);
}
}
}
class Program {
public static void Main() {
Patch.HarmonyPatch.init();
// Patch.MonoModPatch.init();
Console.WriteLine($"done");
}
}
After some trial and error, I got something working, but not the reason behind it.
Both Harmony and MonoMod.RuntimeDetour can hook with the typeof(QSingleton<SampleA>).GetMethod(), but not typeof(QSingleton<>).GetMethod().
Harmony output is unexpected.
Harmony attribute annotation doesn't seem to work.
Generating IL seems useless due to the potential lack of TypeSpec for generic.
Questions:
What is the difference between QSingleton<>.Instance and QSingleton<SampleA>.Instance in the sample?
I would guess that <>.Instance is MethodDef, while <SampleA>.Instance is TypeSpec.MemberRef.
Why does Harmony/MonoMod.RuntimeDetour need TypeSpec.MemberRef? For generating redirection stub?
Is it possible to fix the hook under Harmony?
Can Harmony/MonoMod generates "ldtoken <TypeSpec>" if TypeSpec already exists?
Can Harmony/MonoMod dynamically generates necessary TypeSpec for generics?
Code snippet: (runnable with dotnetfiddle.net)
using System;
using HarmonyLib;
namespace PatchTarget {
public abstract class QSingleton<T> where T : QSingleton<T>, new() {
protected static T instance = null; protected QSingleton() { }
public static T Instance { get {
if (instance == null) {
instance = new T();
Console.WriteLine($"{typeof(T).Name}.Instance: impl=QSingleton");
}
return instance;
} }
}
public class SampleA : QSingleton<SampleA> {
public SampleA() { Console.WriteLine("SampleA ctor"); }
}
public class SampleB : QSingleton<SampleB> {
public SampleB() { Console.WriteLine("SampleB ctor"); }
}
}
namespace Patch {
public class TypeHelper<T> where T : PatchTarget.QSingleton<T>, new() {
public static T InstanceHack() {
Console.WriteLine($"{typeof(T).Name}.Instance: impl=InstanceHack");
return null;
}
// For Harmony as Prefix, but attribute does not work.
public static bool InstanceHackPrefix(T __result) {
Console.WriteLine($"{typeof(T).Name}.Instance: impl=InstanceHack");
__result = null;
return false;
}
}
public static class HarmonyPatch {
public static Harmony harmony = new Harmony("Try");
public static void init() {
// Attribute does not work.
// Transpiler does not work because the lack of TypeSpec to setup generic parameters.
var miOriginal = AccessTools.Property(typeof(PatchTarget.QSingleton<PatchTarget.SampleB>), "Instance").GetMethod;
var miHack = AccessTools.Method(typeof(TypeHelper<PatchTarget.SampleB>), "InstanceHackPrefix");
harmony.Patch(miOriginal, prefix: new HarmonyMethod(miHack));
}
}
public static class MonoModPatch {
public static MonoMod.RuntimeDetour.Detour sHook;
public static void init() {
var miOriginal = AccessTools.Property(typeof(PatchTarget.QSingleton<PatchTarget.SampleB>), "Instance").GetMethod;
var miHack = AccessTools.Method(typeof(TypeHelper<PatchTarget.SampleB>), "InstanceHack");
sHook = new MonoMod.RuntimeDetour.Detour(miOriginal, miHack);
}
}
}
class Program {
public static void Main() {
_ = PatchTarget.SampleA.Instance;
// MonoMod works (replaces globally).
// Harmony hooks, but in an expected way (T becomes SampleB, not 1st generic type parameter).
// try { Patch.HarmonyPatch.init(); } catch (Exception e) { Console.WriteLine($"Harmony error: {e.ToString()}"); }
try { Patch.MonoModPatch.init(); } catch (Exception e) { Console.WriteLine($"MonoMod error: {e.ToString()}"); }
_ = PatchTarget.SampleB.Instance;
_ = PatchTarget.SampleA.Instance;
Console.WriteLine($"done");
}
}
MonoMod.RuntimeDetour Output:(Work as intended)
SampleA.Instance: impl=QSingleton
SampleB.Instance: impl=InstanceHack
SampleA.Instance: impl=InstanceHack
Harmony Output:(Broken <T>)
SampleA.Instance: impl=QSingleton
SampleB.Instance: impl=InstanceHack
SampleB.Instance: impl=InstanceHack
I want to be able to put an attribute on a method and have that method inspect the method parameters and the return value. Pseudo code.
[AttributeUsage(AttributeTargets.Method)]
class MyAttribute: Attribute {
public void MethodEnter(MethodInfo info) {
foreach (var param in info.MethodParameters {
Console.WriteLine(param.ToString());
}
}
public void MethodLeave(MethodInfo info) {
Console.WriteLine(info.ReturnValue);
}
}
public class MyClass {
[MyAttribute]
public SomeType foo(Order order, List<OrderLine> orderLines) {
...
}
}
Is this possible?
Basically, I want to be able to log everything that comes in and leaves the function. I've done this previously with PostSharp, but it seems like an overkill to use it for logging.
You could create an aspect by making your class derive from ContextBoundObject and your attribute from ContextAttribute. By reading this article I made the following sample that meet your requirements:
First, our Foo class derived from ContextBoundObject, decorated with our custom class attribute:
[Inpectable]
internal class Foo : ContextBoundObject
{
[InspectableProperty]
public int DoSomething(int a)
{
Console.WriteLine("I am doing something");
a += 1;
return a;
}
public void IamNotLogged()
{
Console.WriteLine("Lol");
}
[InspectableProperty]
public string IamLoggedToo()
{
var msg = "Lol too";
Console.WriteLine(msg);
return msg;
}
}
Now the Inspectable attribute
[AttributeUsage(AttributeTargets.Class)]
public class Inpectable : ContextAttribute
{
public Inpectable() : base("Inspectable")
{
}
public override void GetPropertiesForNewContext(IConstructionCallMessage ccm)
{
ccm.ContextProperties.Add(new InspectorProperty());
}
}
The InspectablePropertyAttribute which will only be used to identify those methods that should be logged:
[AttributeUsage(AttributeTargets.Method)]
public class InspectableProperty : Attribute
{
}
The InspectorProperty, which will capture the context of the instance and intercept the messages passing them to our aspect
public class InspectorProperty : IContextProperty,
IContributeObjectSink
{
public bool IsNewContextOK(Context newCtx) => true;
public void Freeze(Context newContext)
{
}
public string Name { get; } = "LOL";
public IMessageSink GetObjectSink(MarshalByRefObject o,IMessageSink next) => new InspectorAspect(next);
}
And where the magic works, the implementation of our InspectorAspect:
internal class InspectorAspect : IMessageSink
{
internal InspectorAspect(IMessageSink next)
{
NextSink = next;
}
public IMessageSink NextSink { get; }
public IMessage SyncProcessMessage(IMessage msg)
{
if (!(msg is IMethodMessage)) return NextSink.SyncProcessMessage(msg);
var call = (IMethodMessage) msg;
var type = Type.GetType(call.TypeName);
if (type == null) return NextSink.SyncProcessMessage(msg);
var methodInfo = type.GetMethod(call.MethodName);
if (!Attribute.IsDefined(methodInfo, typeof (InspectableProperty)))
return NextSink.SyncProcessMessage(msg);
Console.WriteLine($"Entering method: {call.MethodName}. Args being:");
foreach (var arg in call.Args)
Console.WriteLine(arg);
var returnMethod = NextSink.SyncProcessMessage(msg) as IMethodReturnMessage;
Console.WriteLine($"Method {call.MethodName} returned: {returnMethod?.ReturnValue}");
Console.WriteLine();
return returnMethod;
}
public IMessageCtrl AsyncProcessMessage(IMessage msg, IMessageSink replySink)
{
throw new InvalidOperationException();
}
}
Inside the SyncProcessMessage theInspectorAspect receives all the messages regarding the context (Foo), so we just filter those that are instances of IMethodMessage and then only those belonging to methods decorated with InspectableProperty attribute.
This may not be a ready-to-production solution, but I think that puts you in the right path to research for more info.
Finally testing:
private static void Main(string[] args)
{
var foo = new Foo();
foo.DoSomething(1);
foo.IamNotLogged();
foo.IamLoggedToo();
Console.ReadLine();
}
The output:
EDIT>>>
These are the namespaces needed:
using System.Runtime.Remoting.Activation;
using System.Runtime.Remoting.Contexts;
using System.Runtime.Remoting.Messaging;
You basicaly looking for AOP (Aspect Oriented Programming) solution. PostSharp uses IL injection in your binaries after compilation and in my opinion, this is worst framework I worked with.
Instead of looking into some very complex solution I recommend you to decorate your classes. For example:
public MyClass : IMyClass
{
public object[] MyMethod(object[] args){...}
}
public LoggerDecoratedMyClass : IMyClass
{
private readonly IMyClass _inner;
private readonly ILogger _logger;
public LoggerDecoratedMyClass(IMyClass inner, ILogger logger)
{
_inner = inner;
_logger = logger;
}
public object[] MyMethod(object[] args)
{
try
{
var result = _inner.MyMethod(args);
_logger.LogSuccess(...);
return result;
}
catch (Exception ex)
{
_logger.LogError(..., ex);
throw;
}
}
}
This looks a lot better than attribute bindings, and provide you with ability to control your Aspect dependecies. Also, it forces you to write interface oriented code.
UPDATE
Also, I often use scope logging:
internal struct ScopeLogger : IDisposable
{
private readonly string _name;
public ScopeLogger(ILogger logger, string scopeName, object[] args)
{
_name = scopeName;
_logger = logger;
_logger.LogInfo("Begin {name}: {args}", _name, args);
}
public void Dispose()
{
_logger.LogInfo("End {name}",_name);
}
}
public static IDisposable LogScope(this ILogger logger, string name, params object[] args)
{
return new ScopeLogger(logger, name, args);
}
And simply use it like this:
public LoggerDecoratedMyClass : IMyClass
{
private readonly IMyClass _inner;
private readonly ILogger _logger;
public LoggerDecoratedMyClass(IMyClass inner, ILogger logger)
{
_inner = inner;
_logger = logger;
}
public object[] MyMethod(object[] args)
{
using(_logger.LogScope(nameof(MyMethod), args))
{
return _inner.MyMethod(args);
}
}
}
Decorators this way looks a lot better and shorter.
I am trying to get AutoFac Delegate Factories & Type Interceptors to play nicely with each other, but I cannot seem to get the behaviour I want.
(http://docs.autofac.org/en/latest/advanced/delegate-factories.html)
(http://docs.autofac.org/en/latest/advanced/interceptors.html)
In the example below, I want calls to the IQuoteService.GetQuote(...) to be intercepted by the CallLogger interceptor.
I've tried both of the Enable___(); extension methods for enabling interception, but none of them seem to correctly intercept the call.
I suspect the problem is the way that Autofac is registering the proxy and the signature of the delegate, but to be honest I am a little stuck... I don't know Autofac as well as I know Castle Windsor, but this project is using Autofac.
ANSWERED BELOW
Code updated to working example:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Autofac;
using Autofac.Extras.DynamicProxy2;
using Castle.DynamicProxy;
namespace AutofacTest
{
public class Shareholding
{
public delegate Shareholding Factory(string symbol, uint holding);
private readonly IQuoteService _quoteService;
public Shareholding(string symbol, uint holding, IQuoteService quoteService)
{
_quoteService = quoteService;
Symbol = symbol;
Holding = holding;
}
public string Symbol { get; private set; }
public uint Holding { get; set; }
public decimal Value
{
get { return _quoteService.GetQuote(Symbol) * Holding; }
}
}
public class Portfolio
{
private readonly IList<Shareholding> _holdings = new List<Shareholding>();
private readonly Shareholding.Factory _shareholdingFactory;
public Portfolio(Shareholding.Factory shareholdingFactory)
{
_shareholdingFactory = shareholdingFactory;
}
public decimal Value
{
get { return _holdings.Sum(h => h.Value); }
}
public void Add(string symbol, uint holding)
{
_holdings.Add(_shareholdingFactory(symbol, holding));
}
}
public interface IQuoteService
{
decimal GetQuote(string symbol);
}
public class QuoteService : IQuoteService
{
public decimal GetQuote(string symbol)
{
return 10m;
}
}
public class CallLogger : IInterceptor
{
private readonly TextWriter _output;
public CallLogger(TextWriter output)
{
_output = output;
}
public void Intercept(IInvocation invocation)
{
_output.Write("Calling method {0} with parameters {1}... ",
invocation.Method.Name,
string.Join(", ", invocation.Arguments.Select(a => (a ?? "").ToString()).ToArray()));
invocation.Proceed();
_output.WriteLine("Done: result was {0}.", invocation.ReturnValue);
}
}
internal class Program
{
private static void Main(string[] args)
{
var builder = new ContainerBuilder();
builder.RegisterType<Shareholding>();
builder.RegisterType<Portfolio>();
builder.Register(c => new CallLogger(Console.Out));
builder.RegisterType<QuoteService>()
.As<IQuoteService>()
.EnableInterfaceInterceptors()
.InterceptedBy(typeof(CallLogger));
var container = builder.Build();
var portfolio = container.Resolve<Portfolio>();
portfolio.Add("ABC", 1234);
portfolio.Add("DEF", 4567);
Console.WriteLine(portfolio.Value);
Console.ReadKey();
}
}
}
// Magic?
builder.RegisterType<Portfolio>()
.As<IPortfolio>()
.EnableInterfaceInterceptors()
.InterceptedBy(typeof(CallLogger));;
builder.Register(c => new CallLogger(Console.Out));
var container = builder.Build();
var isResolved = container.Resolve<IPortfolio>();
It looks to me like you are missing an association between the DynamicProxy for IPortfolio and the IInterceptor type.
This can be remedied via registration:
builder.RegisterType<Portfolio>()
.As<IPortfolio>()
.EnableInterfaceInterceptors()
.InterceptedBy(typeof(CallLogger));
or via the InterceptAttribute
[Intercept(typeof(CallLogger))]
public interface IPortfolio
{
decimal Value { get; }
void Add(string symbol, uint holding);
}